Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add bevy_entropy plugin and include it in the default set #2504

Closed
wants to merge 12 commits into from
12 changes: 11 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ bevy_internal = { path = "crates/bevy_internal", version = "0.8.0-dev", default-

[dev-dependencies]
anyhow = "1.0.4"
rand = "0.8.0"
rand = { version = "0.8.0", features = ["small_rng"] }
ron = "0.7.0"
serde = { version = "1", features = ["derive"] }
bytemuck = "1.7"
Expand Down Expand Up @@ -563,6 +563,16 @@ description = "Demonstrates the creation and registration of a custom plugin gro
category = "Application"
wasm = true

[[example]]
name = "random"
path = "examples/app/random.rs"

[package.metadata.example.random]
name = "Random"
description = "Demonstrates how to use entropy to control randomness in Bevy."
category = "Application"
wasm = true

[[example]]
name = "return_after_run"
path = "examples/app/return_after_run.rs"
Expand Down
3 changes: 2 additions & 1 deletion crates/bevy_ecs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ downcast-rs = "1.2"
serde = "1"

[dev-dependencies]
rand = "0.8"
bevy_entropy = { path = "../bevy_entropy", version = "0.8.0-dev" }
rand = { version = "0.8", features = ["small_rng"] }

[[example]]
name = "events"
Expand Down
47 changes: 38 additions & 9 deletions crates/bevy_ecs/examples/change_detection.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use bevy_ecs::prelude::*;
use rand::Rng;
use bevy_ecs::{prelude::*, schedule::ShouldRun};
use bevy_entropy::Entropy;
use rand::{rngs::SmallRng, Rng, SeedableRng};
use std::ops::Deref;

// In this example we will simulate a population of entities. In every tick we will:
// 1. spawn a new entity with a certain possibility
// 1. spawn a new entity with a certain deterministic probability
// 2. age all entities
// 3. despawn entities with age > 2
//
Expand All @@ -13,17 +14,33 @@ fn main() {
// Create a new empty World to hold our Entities, Components and Resources
let mut world = World::new();

// Add the entropy resource for future random number generators to use.
// This makes random number generation deterministic.
let world_seed = [1; 32];
world.insert_resource(Entropy::from(world_seed));

// Add the counter resource to remember how many entities where spawned
world.insert_resource(EntityCounter { value: 0 });

// Create a new Schedule, which defines an execution strategy for Systems
// Create a new Schedule, which defines an execution strategy for Systems.
let mut schedule = Schedule::default();

// Create a Stage to add to our Schedule. Each Stage in a schedule runs all of its systems
// before moving on to the next Stage
let mut update = SystemStage::parallel();
// before moving on to the next Stage.
// Here, we are creating a "startup" Stage with a schedule that runs once.
let mut startup = SystemStage::parallel();
startup.add_system(create_rng);
schedule.add_stage(
"startup",
Schedule::default()
.with_run_criteria(ShouldRun::once)
.with_stage("only_once", startup),
);

// Add systems to the Stage to execute our app logic
// Add systems to another Stage to execute our app logic.
// We can label our systems to force a specific run-order between some of them
// within the Stage.
let mut update = SystemStage::parallel();
update.add_system(spawn_entities.label(SimulationSystem::Spawn));
update.add_system(print_counter_when_changed.after(SimulationSystem::Spawn));
update.add_system(age_all_entities.label(SimulationSystem::Age));
Expand Down Expand Up @@ -58,11 +75,23 @@ enum SimulationSystem {
Age,
}

// This system creates a random number generator resource from [`Entropy`].
fn create_rng(mut commands: Commands, mut entropy: ResMut<Entropy>) {
let seed = entropy.get();
println!(" seeding rng from entropy: {:?}", seed);
let rng = SmallRng::from_seed(seed);
commands.insert_resource(rng);
}

// This system randomly spawns a new entity in 60% of all frames
// The entity will start with an age of 0 frames
// If an entity gets spawned, we increase the counter in the EntityCounter resource
fn spawn_entities(mut commands: Commands, mut entity_counter: ResMut<EntityCounter>) {
if rand::thread_rng().gen_bool(0.6) {
fn spawn_entities(
mut commands: Commands,
mut entity_counter: ResMut<EntityCounter>,
mut rng: ResMut<SmallRng>,
) {
if rng.gen_bool(0.6) {
let entity_id = commands.spawn().insert(Age::default()).id();
println!(" spawning {:?}", entity_id);
entity_counter.value += 1;
Expand Down
14 changes: 11 additions & 3 deletions crates/bevy_ecs/examples/resources.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use bevy_ecs::prelude::*;
use rand::Rng;
use bevy_entropy::Entropy;
use rand::{prelude::SmallRng, Rng, SeedableRng};
use std::ops::Deref;

// In this example we add a counter resource and increase it's value in one system,
Expand All @@ -8,6 +9,10 @@ fn main() {
// Create a world
let mut world = World::new();

// Add the entropy resource
let world_seed = [1; 32];
world.insert_resource(Entropy::from(world_seed));

// Add the counter resource
world.insert_resource(Counter { value: 0 });

Expand All @@ -32,8 +37,11 @@ struct Counter {
pub value: i32,
}

fn increase_counter(mut counter: ResMut<Counter>) {
if rand::thread_rng().gen_bool(0.5) {
fn increase_counter(mut counter: ResMut<Counter>, mut entropy: ResMut<Entropy>) {
// Note that in a real system it would be better to create this once
// as a resource.
let mut rng = SmallRng::from_seed(entropy.get());
if rng.gen_bool(0.5) {
counter.value += 1;
println!(" Increased counter value");
}
Expand Down
26 changes: 26 additions & 0 deletions crates/bevy_entropy/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[package]
name = "bevy_entropy"
version = "0.8.0-dev"
edition = "2018"
authors = [
"Bevy Contributors <bevyengine@gmail.com>",
"Christian Legnitto <christian@legnitto.com>",
]
description = "Provides entropy functionality for Bevy Engine"
homepage = "https://bevyengine.org"
repository = "https://github.com/bevyengine/bevy"
license = "MIT OR Apache-2.0"
keywords = ["bevy", "random", "entropy"]

[dependencies]
# bevy
bevy_app = { path = "../bevy_app", version = "0.8.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.8.0-dev" }
# other
rand_core = { version = "0.6.3", features = ["getrandom"] }
rand_chacha = { version = "0.3.1" }

[dev-dependencies]
bevy_internal = { path = "../bevy_internal", version = "0.8.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.8.0-dev" }
rand = { version = "0.8", features = ["std_rng", "small_rng"] }
140 changes: 140 additions & 0 deletions crates/bevy_entropy/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
use bevy_app::{App, Plugin};
use bevy_utils::tracing::{debug, trace};
use rand_chacha::ChaCha12Rng;
use rand_core::{RngCore, SeedableRng};

pub mod prelude {
#[doc(hidden)]
pub use crate::Entropy;
}

/// Provides a source of entropy.
/// This enables deterministic random number generation.
///
/// See <https://github.com/bevyengine/bevy/discussions/2480> for issues
/// to be mindful of if you desire complete determinism.
#[derive(Default)]
pub struct EntropyPlugin;

impl Plugin for EntropyPlugin {
fn build(&self, app: &mut App) {
if !app.world.contains_resource::<Entropy>() {
trace!("Creating entropy");
app.init_resource::<Entropy>();
}
}
}

/// A resource that provides entropy.
pub struct Entropy(ChaCha12Rng);

impl Default for Entropy {
/// The default entropy source is non-deterministic and seeded from the operating system.
/// For a deterministic source, use [`Entropy::from`].
fn default() -> Self {
debug!("Entropy created via the operating system");
let rng = ChaCha12Rng::from_entropy();
Entropy(rng)
}
}

impl Entropy {
/// Create a deterministic source of entropy. All random number generators
/// later seeded from an [`Entropy`] created this way will be deterministic.
/// If determinism is not required, use [`Entropy::default`].
pub fn from(seed: [u8; 32]) -> Self {
debug!("Entropy created via seed: {:?} ", seed);
let rng = ChaCha12Rng::from_seed(seed);
Entropy(rng)
}

/// Fill `dest` with entropy data. For an allocating alternative, see [`Entropy::get`].
pub fn fill_bytes(&mut self, dest: &mut [u8]) {
self.0.fill_bytes(dest);
}

/// Allocate and return entropy data. For a non-allocating alternative, see [`Entropy::fill_bytes`].
pub fn get(&mut self) -> [u8; 32] {
let mut dest = [0; 32];
self.0.fill_bytes(&mut dest);
dest
}
}

#[cfg(test)]
mod test {
use bevy_app::AppExit;
use bevy_ecs::prelude::*;
use bevy_internal::prelude::*;
use rand::{rngs::SmallRng, seq::IteratorRandom, SeedableRng};
use std::sync::mpsc;
use std::sync::mpsc::{Receiver, SyncSender};

#[test]
fn is_deterministic() {
const APP_RUN_COUNT: u8 = 10;
const CHOOSE_COUNT: u8 = 5;
const THING_COUNT: u8 = 100;

#[derive(Component)]
struct Thing(u8);
struct ResultChannel(SyncSender<u8>);

// The result of the app we will check to make sure it is always the same.
let mut expected_result: Option<Vec<u8>> = None;

// The seed we will use for the random number generator in all app runs.
let world_seed: [u8; 32] = [1; 32];

// Run the app multiple times.
for runs in 0..APP_RUN_COUNT {
let (tx, rx): (SyncSender<u8>, Receiver<u8>) = mpsc::sync_channel(CHOOSE_COUNT.into());

App::new()
.insert_resource(Entropy::from(world_seed))
.insert_resource(ResultChannel(tx))
.add_plugins_with(MinimalPlugins, |group| group.add(super::EntropyPlugin))
.add_startup_system(spawn_things)
.add_system(choose_things)
.run();

fn spawn_things(mut commands: Commands) {
for x in 1..THING_COUNT {
commands.spawn().insert(Thing(x));
}
}

fn choose_things(
query: Query<&Thing>,
mut entropy: ResMut<Entropy>,
result_channel: Res<ResultChannel>,
mut app_exit_events: EventWriter<AppExit>,
) {
// Create RNG from global entropy.
let seed = entropy.get();
let mut rng = SmallRng::from_seed(seed);

// Choose some random things.
for _ in 0..CHOOSE_COUNT {
if let Some(thing) = query.iter().choose(&mut rng) {
// Send the chosen thing out of the app so it can be inspected
// after the app exits.
result_channel.0.send(thing.0).expect("result to send");
}
}
app_exit_events.send(AppExit);
}

// The result of running the app.
let run_result: Vec<u8> = rx.iter().collect();

// If it is the first run, treat the current result as the expected
// result we will check future runs against.
if runs == 0 {
expected_result = Some(run_result.clone());
}

assert_eq!(expected_result, Some(run_result));
}
}
}
1 change: 1 addition & 0 deletions crates/bevy_internal/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ bevy_core = { path = "../bevy_core", version = "0.8.0-dev" }
bevy_derive = { path = "../bevy_derive", version = "0.8.0-dev" }
bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.8.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.8.0-dev" }
bevy_entropy = { path = "../bevy_entropy", version = "0.8.0-dev" }
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.8.0-dev" }
bevy_input = { path = "../bevy_input", version = "0.8.0-dev" }
bevy_log = { path = "../bevy_log", version = "0.8.0-dev" }
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_internal/src/default_plugins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ impl PluginGroup for DefaultPlugins {
#[cfg(feature = "debug_asset_server")]
group.add(bevy_asset::debug_asset_server::DebugAssetServerPlugin::default());
group.add(bevy_scene::ScenePlugin::default());
group.add(bevy_entropy::EntropyPlugin::default());

#[cfg(feature = "bevy_winit")]
group.add(bevy_winit::WinitPlugin::default());
Expand Down
5 changes: 5 additions & 0 deletions crates/bevy_internal/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ pub mod ecs {
pub use bevy_ecs::*;
}

pub mod entropy {
//! Resources for entropy.
pub use bevy_entropy::*;
}

pub mod input {
//! Resources and events for inputs, e.g. mouse/keyboard, touch, gamepads, etc.
pub use bevy_input::*;
Expand Down
8 changes: 4 additions & 4 deletions crates/bevy_internal/src/prelude.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
#[doc(hidden)]
pub use crate::{
app::prelude::*, asset::prelude::*, core::prelude::*, ecs::prelude::*, hierarchy::prelude::*,
input::prelude::*, log::prelude::*, math::prelude::*, reflect::prelude::*, scene::prelude::*,
time::prelude::*, transform::prelude::*, utils::prelude::*, window::prelude::*, DefaultPlugins,
MinimalPlugins,
app::prelude::*, asset::prelude::*, core::prelude::*, ecs::prelude::*, entropy::prelude::*,
hierarchy::prelude::*, input::prelude::*, log::prelude::*, math::prelude::*,
reflect::prelude::*, scene::prelude::*, time::prelude::*, transform::prelude::*,
utils::prelude::*, window::prelude::*, DefaultPlugins, MinimalPlugins,
};

pub use bevy_derive::{bevy_main, Deref, DerefMut};
Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ Example | Description
[Logs](../examples/app/logs.rs) | Illustrate how to use generate log output
[Plugin](../examples/app/plugin.rs) | Demonstrates the creation and registration of a custom plugin
[Plugin Group](../examples/app/plugin_group.rs) | Demonstrates the creation and registration of a custom plugin group
[Random](../examples/app/random.rs) | Demonstrates how to use entropy to control randomness in Bevy.
[Return after Run](../examples/app/return_after_run.rs) | Show how to return to main after the Bevy app has exited
[Thread Pool Resources](../examples/app/thread_pool_resources.rs) | Creates and customizes the internal thread pool
[Without Winit](../examples/app/without_winit.rs) | Create an application without winit (runs single time, no event loop)
Expand Down
Loading