From 3365640cd815473e951c1c2c6390f9f15776cf2c Mon Sep 17 00:00:00 2001 From: Christian Legnitto Date: Mon, 19 Jul 2021 19:36:43 -0700 Subject: [PATCH 01/12] Add bevy_entropy default plugin --- Cargo.toml | 6 +- crates/bevy_ecs/Cargo.toml | 3 +- crates/bevy_ecs/examples/change_detection.rs | 47 +++++-- crates/bevy_ecs/examples/resources.rs | 14 +- crates/bevy_entropy/Cargo.toml | 25 ++++ crates/bevy_entropy/src/lib.rs | 139 +++++++++++++++++++ crates/bevy_internal/Cargo.toml | 1 + crates/bevy_internal/src/default_plugins.rs | 1 + crates/bevy_internal/src/lib.rs | 5 + crates/bevy_internal/src/prelude.rs | 2 +- examples/app/random.rs | 121 ++++++++++++++++ examples/ecs/component_change_detection.rs | 20 ++- examples/ecs/iter_combinations.rs | 8 +- examples/games/alien_cake_addict.rs | 23 ++- 14 files changed, 389 insertions(+), 26 deletions(-) create mode 100644 crates/bevy_entropy/Cargo.toml create mode 100644 crates/bevy_entropy/src/lib.rs create mode 100644 examples/app/random.rs diff --git a/Cargo.toml b/Cargo.toml index 3a094ff9f3722..a846cc26cbff8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" @@ -563,6 +563,10 @@ description = "Demonstrates the creation and registration of a custom plugin gro category = "Application" wasm = true +[[example]] +name = "random" +path = "examples/app/random.rs" + [[example]] name = "return_after_run" path = "examples/app/return_after_run.rs" diff --git a/crates/bevy_ecs/Cargo.toml b/crates/bevy_ecs/Cargo.toml index 277ecc480ffc9..dfd7f66cabfcc 100644 --- a/crates/bevy_ecs/Cargo.toml +++ b/crates/bevy_ecs/Cargo.toml @@ -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" diff --git a/crates/bevy_ecs/examples/change_detection.rs b/crates/bevy_ecs/examples/change_detection.rs index 1a3e135b654e2..34830389c4a9e 100644 --- a/crates/bevy_ecs/examples/change_detection.rs +++ b/crates/bevy_ecs/examples/change_detection.rs @@ -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 // @@ -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 execution 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)); @@ -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) { + 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) { - if rand::thread_rng().gen_bool(0.6) { +fn spawn_entities( + mut commands: Commands, + mut entity_counter: ResMut, + mut rng: ResMut, +) { + if rng.gen_bool(0.6) { let entity_id = commands.spawn().insert(Age::default()).id(); println!(" spawning {:?}", entity_id); entity_counter.value += 1; diff --git a/crates/bevy_ecs/examples/resources.rs b/crates/bevy_ecs/examples/resources.rs index c6700a6b9ed9d..035b1fa1b3556 100644 --- a/crates/bevy_ecs/examples/resources.rs +++ b/crates/bevy_ecs/examples/resources.rs @@ -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, @@ -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 }); @@ -32,8 +37,11 @@ struct Counter { pub value: i32, } -fn increase_counter(mut counter: ResMut) { - if rand::thread_rng().gen_bool(0.5) { +fn increase_counter(mut counter: ResMut, mut entropy: ResMut) { + // 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"); } diff --git a/crates/bevy_entropy/Cargo.toml b/crates/bevy_entropy/Cargo.toml new file mode 100644 index 0000000000000..538f6c0e8ce9b --- /dev/null +++ b/crates/bevy_entropy/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "bevy_entropy" +version = "0.8.0-dev" +edition = "2018" +authors = [ + "Bevy Contributors ", + "Christian Legnitto ", +] +description = "Provides entropy functionality for Bevy Engine" +homepage = "https://bevyengine.org" +repository = "https://github.com/bevyengine/bevy" +license = "MIT" +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 = { version = "0.8", features = ["std_rng"] } + +[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"] } diff --git a/crates/bevy_entropy/src/lib.rs b/crates/bevy_entropy/src/lib.rs new file mode 100644 index 0000000000000..4b4aab8906e3a --- /dev/null +++ b/crates/bevy_entropy/src/lib.rs @@ -0,0 +1,139 @@ +use bevy_app::{App, Plugin}; +use bevy_utils::tracing::{debug, trace}; +use rand::{rngs::StdRng, RngCore, SeedableRng}; + +pub mod prelude { + #[doc(hidden)] + pub use crate::Entropy; +} + +/// Provides a source of entropy. +/// This enables deterministic random number generation. +/// +// See 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::() { + trace!("Creating entropy"); + app.init_resource::(); + } + } +} + +/// A resource that provides entropy. +pub struct Entropy(StdRng); + +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 = StdRng::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 = StdRng::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); + + // The result of the app we will check to make sure it is always the same. + let mut expected_result: Option> = 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, Receiver) = 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, + result_channel: Res, + mut app_exit_events: EventWriter, + ) { + // 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 = 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)); + } + } +} diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 7fccda37863d0..19d0a1a8222dd 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -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" } diff --git a/crates/bevy_internal/src/default_plugins.rs b/crates/bevy_internal/src/default_plugins.rs index 7bdb4c8af4ae4..fa0b7ad9e457a 100644 --- a/crates/bevy_internal/src/default_plugins.rs +++ b/crates/bevy_internal/src/default_plugins.rs @@ -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()); diff --git a/crates/bevy_internal/src/lib.rs b/crates/bevy_internal/src/lib.rs index cc63909ab3cb8..1e9b661fd982f 100644 --- a/crates/bevy_internal/src/lib.rs +++ b/crates/bevy_internal/src/lib.rs @@ -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::*; diff --git a/crates/bevy_internal/src/prelude.rs b/crates/bevy_internal/src/prelude.rs index fd2cb9d848327..0693ae9cec680 100644 --- a/crates/bevy_internal/src/prelude.rs +++ b/crates/bevy_internal/src/prelude.rs @@ -1,6 +1,6 @@ #[doc(hidden)] pub use crate::{ - app::prelude::*, asset::prelude::*, core::prelude::*, ecs::prelude::*, hierarchy::prelude::*, + 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, diff --git a/examples/app/random.rs b/examples/app/random.rs new file mode 100644 index 0000000000000..9b69e93885e4d --- /dev/null +++ b/examples/app/random.rs @@ -0,0 +1,121 @@ +use bevy::prelude::*; +use rand::{prelude::IteratorRandom, rngs::SmallRng, Rng, SeedableRng}; + +// This example illustrates how to use entropy to control randomness in bevy. +// We randomly choose a coin to toss (which is itself random) and record the result. +// +// Because all the random number generators are seeded from the same "world seed", +// the chosen coin and results of the coin tosses are deterministic across runs. +// +// There are many different random number generators with different tradeoffs. +// We use a `SmallRng`, which is an insecure random number generator designed to +// be fast, simple, require little memory, and have good output quality. +// This would be an inappropriate choice for cryptographic randomness. +// +// See for more details + +#[derive(Component)] +struct Coin; + +impl Coin { + // Toss the coin and return the `Face` that lands up. + // Coin tosses are independent so each toss needs its own + // random number generator. + fn toss(&self, seed: [u8; 32]) -> Face { + let mut rng = SmallRng::from_seed(seed); + if rng.gen_bool(0.5) { + Face::Heads + } else { + Face::Tails + } + } +} + +struct CoinChooser(SmallRng); + +#[derive(Component, Debug, PartialEq)] +enum Face { + Heads, + Tails, +} + +fn main() { + // We create a random seed for the world. Random number generators created with + // or derived from this seed will appear random during execution but will be + // deterministic across multiple executions. + // See and + // + // for more details. + // + // The seed you choose may have security implications or influence the + // distribution of the random numbers generated. + // See for more details + // about how to pick a "good" random seed for your needs. + // + // Normally you would do one of the following: + // 1. Get a good random seed out-of-band and hardcode it in the source. + // 2. Dynamically call to the OS and print the seed so the user can rerun + // deterministically. + // 3. Dynamically call to the OS and share the seed with a server so the + // client and server deterministically execute together. + // 4. Load the seed from a server so the client and server deterministically + // execute together. + let world_seed: [u8; 32] = [1; 32]; + + // Create a source of entropy for the world using the random seed. + let mut world_entropy = Entropy::from(world_seed); + + // Create a coin chooser, seeded from the world's entropy. + // We do this at the start of the world so the random number generator backing + // the coin chooser is not influenced by coin tosses. + let seed = world_entropy.get(); + let coin_chooser = CoinChooser(SmallRng::from_seed(seed)); + + App::new() + // Delete the following line to use the default OS-provided entropy source. + // Note that doing so introduces non-determinism. + .insert_resource(world_entropy) + .insert_resource(coin_chooser) + .add_plugins_with(MinimalPlugins, |group| { + group.add(bevy::entropy::EntropyPlugin); + group.add(bevy::log::LogPlugin) + }) + .add_startup_system(spawn_coins) + .add_system(toss_coin) + .run(); +} + +// System to spawn coins into the world. +fn spawn_coins(mut commands: Commands) { + for _ in 1..100 { + commands.spawn().insert(Coin).insert(Face::Heads); + } + info!("Spawned coins") +} + +// System to toss a random coin. +fn toss_coin( + mut query: Query<(Entity, &Coin, &mut Face), With>, + coin_chooser: ResMut, + mut entropy: ResMut, +) { + // Pick a random coin. + if let Some((ent, coin, mut face)) = query.iter_mut().choose(&mut coin_chooser.into_inner().0) { + // Toss it to determine the resulting [Face]. + // Tosses are seeded from the world's entropy and are deterministic. + let seed = entropy.get(); + let new_face = coin.toss(seed); + + info!( + "Tossed entity {:?} - old: {:?} new: {:?}", + ent, *face, new_face + ); + + // If the face has changed, update it. + if *face != new_face { + info!(" - Updating face for entity {:?} to {:?}", ent, new_face); + let f = face.as_mut(); + *f = new_face; + } + } +} diff --git a/examples/ecs/component_change_detection.rs b/examples/ecs/component_change_detection.rs index 68e18a58469d8..e735d08e91dc9 100644 --- a/examples/ecs/component_change_detection.rs +++ b/examples/ecs/component_change_detection.rs @@ -1,10 +1,13 @@ //! This example illustrates how to react to component change. use bevy::prelude::*; -use rand::Rng; +use rand::{rngs::SmallRng, Rng, SeedableRng}; fn main() { + let world_seed = [1; 32]; + App::new() + .insert_resource(Entropy::from(world_seed)) .add_plugins(DefaultPlugins) .add_startup_system(setup) .add_system(change_component) @@ -16,14 +19,23 @@ fn main() { #[derive(Component, Debug)] struct MyComponent(f64); -fn setup(mut commands: Commands) { +fn setup(mut commands: Commands, mut entropy: ResMut) { + let seed = entropy.get(); + let rng = SmallRng::from_seed(seed); + commands.insert_resource(rng); + info!("inserted rng resource with seed: {:?}", seed); + commands.spawn().insert(MyComponent(0.)); commands.spawn().insert(Transform::identity()); } -fn change_component(time: Res