Skip to content
Closed
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 <[email protected]>",
"Christian Legnitto <[email protected]>",
]
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