Skip to content

Commit b9a404e

Browse files
committed
Add bevy_entropy default plugin
1 parent 90586a4 commit b9a404e

18 files changed

+422
-36
lines changed

Cargo.toml

+5-1
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ bevy_internal = { path = "crates/bevy_internal", version = "0.5.0", default-feat
9292

9393
[dev-dependencies]
9494
anyhow = "1.0.4"
95-
rand = "0.8.0"
95+
rand = { version = "0.8.0", features = ["small_rng"] }
9696
ron = "0.6.2"
9797
serde = { version = "1", features = ["derive"] }
9898
# Needed to poll Task examples
@@ -217,6 +217,10 @@ path = "examples/app/plugin.rs"
217217
name = "plugin_group"
218218
path = "examples/app/plugin_group.rs"
219219

220+
[[example]]
221+
name = "random"
222+
path = "examples/app/random.rs"
223+
220224
[[example]]
221225
name = "return_after_run"
222226
path = "examples/app/return_after_run.rs"

crates/bevy_ecs/Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ rand = "0.8"
3232
serde = "1"
3333

3434
[dev-dependencies]
35+
bevy_entropy = { path = "../bevy_entropy", version = "0.5.0" }
3536
parking_lot = "0.11"
37+
rand = { version = "0.8", features = ["small_rng"] }
3638

3739
[[example]]
3840
name = "events"

crates/bevy_ecs/examples/change_detection.rs

+38-8
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
use bevy_ecs::prelude::*;
2-
use rand::Rng;
2+
use bevy_ecs::schedule::RunOnce;
3+
use bevy_entropy::Entropy;
4+
use rand::{rngs::SmallRng, Rng, SeedableRng};
35
use std::ops::Deref;
46

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

18+
// Add the entropy resource for future random number generators to use.
19+
// This makes execution deterministic.
20+
let world_seed = [1; 32];
21+
world.insert_resource(Entropy::from(world_seed));
22+
1623
// Add the counter resource to remember how many entities where spawned
1724
world.insert_resource(EntityCounter { value: 0 });
1825

19-
// Create a new Schedule, which defines an execution strategy for Systems
26+
// Create a new Schedule, which defines an execution strategy for Systems.
2027
let mut schedule = Schedule::default();
28+
2129
// Create a Stage to add to our Schedule. Each Stage in a schedule runs all of its systems
22-
// before moving on to the next Stage
23-
let mut update = SystemStage::parallel();
30+
// before moving on to the next Stage.
31+
// Here, we are creating a "startup" Stage with a schedule that runs once.
32+
let mut startup = SystemStage::parallel();
33+
startup.add_system(create_rng.system());
34+
schedule.add_stage(
35+
"startup",
36+
Schedule::default()
37+
.with_run_criteria(RunOnce::default())
38+
.with_stage("only_once", startup),
39+
);
2440

25-
// Add systems to the Stage to execute our app logic
41+
// Add systems to another Stage to execute our app logic.
2642
// We can label our systems to force a specific run-order between some of them
43+
// within the Stage.
44+
let mut update = SystemStage::parallel();
2745
update.add_system(spawn_entities.label(SimulationSystem::Spawn));
2846
update.add_system(print_counter_when_changed.after(SimulationSystem::Spawn));
2947
update.add_system(age_all_entities.label(SimulationSystem::Age));
@@ -58,11 +76,23 @@ enum SimulationSystem {
5876
Age,
5977
}
6078

79+
// This system creates a random number generator resource from [`Entropy`].
80+
fn create_rng(mut commands: Commands, mut entropy: ResMut<Entropy>) {
81+
let seed = entropy.get();
82+
println!(" seeding rng from entropy: {:?}", seed);
83+
let rng = SmallRng::from_seed(seed);
84+
commands.insert_resource(rng);
85+
}
86+
6187
// This system randomly spawns a new entity in 60% of all frames
6288
// The entity will start with an age of 0 frames
6389
// If an entity gets spawned, we increase the counter in the EntityCounter resource
64-
fn spawn_entities(mut commands: Commands, mut entity_counter: ResMut<EntityCounter>) {
65-
if rand::thread_rng().gen_bool(0.6) {
90+
fn spawn_entities(
91+
mut commands: Commands,
92+
mut entity_counter: ResMut<EntityCounter>,
93+
mut rng: ResMut<SmallRng>,
94+
) {
95+
if rng.gen_bool(0.6) {
6696
let entity_id = commands.spawn().insert(Age::default()).id();
6797
println!(" spawning {:?}", entity_id);
6898
entity_counter.value += 1;

crates/bevy_ecs/examples/resources.rs

+11-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use bevy_ecs::prelude::*;
2-
use rand::Rng;
2+
use bevy_entropy::Entropy;
3+
use rand::{prelude::SmallRng, Rng, SeedableRng};
34
use std::ops::Deref;
45

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

12+
// Add the entropy resource
13+
let world_seed = [1; 32];
14+
world.insert_resource(Entropy::from(world_seed));
15+
1116
// Add the counter resource
1217
world.insert_resource(Counter { value: 0 });
1318

@@ -38,8 +43,11 @@ enum CounterSystem {
3843
Increase,
3944
}
4045

41-
fn increase_counter(mut counter: ResMut<Counter>) {
42-
if rand::thread_rng().gen_bool(0.5) {
46+
fn increase_counter(mut counter: ResMut<Counter>, mut entropy: ResMut<Entropy>) {
47+
// Note that in a real system it would be better to create this once
48+
// as a resource.
49+
let mut rng = SmallRng::from_seed(entropy.get());
50+
if rng.gen_bool(0.5) {
4351
counter.value += 1;
4452
println!(" Increased counter value");
4553
}

crates/bevy_entropy/Cargo.toml

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
[package]
2+
name = "bevy_entropy"
3+
version = "0.5.0"
4+
edition = "2018"
5+
authors = [
6+
"Bevy Contributors <[email protected]>",
7+
"Christian Legnitto <[email protected]>",
8+
]
9+
description = "Provides entropy functionality for Bevy Engine"
10+
homepage = "https://bevyengine.org"
11+
repository = "https://github.com/bevyengine/bevy"
12+
license = "MIT"
13+
keywords = ["bevy", "random", "entropy"]
14+
15+
[dependencies]
16+
# bevy
17+
bevy_app = { path = "../bevy_app", version = "0.5.0" }
18+
bevy_utils = { path = "../bevy_utils", version = "0.5.0" }
19+
# other
20+
rand = { version = "0.8", features = ["std_rng"] }
21+
22+
[dev-dependencies]
23+
bevy_internal = { path = "../bevy_internal", version = "0.5.0" }
24+
bevy_ecs = { path = "../bevy_ecs", version = "0.5.0" }
25+
rand = { version = "0.8", features = ["std_rng", "small_rng"] }

crates/bevy_entropy/src/lib.rs

+138
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
use bevy_app::{App, Plugin};
2+
use bevy_utils::tracing::{debug, trace};
3+
use rand::{rngs::StdRng, RngCore, SeedableRng};
4+
5+
pub mod prelude {
6+
#[doc(hidden)]
7+
pub use crate::Entropy;
8+
}
9+
10+
/// Provides a source of entropy.
11+
/// This enables deterministic random number generation.
12+
///
13+
// See <https://github.com/bevyengine/bevy/discussions/2480> for issues
14+
// to be mindful of if you desire complete determinism.
15+
#[derive(Default)]
16+
pub struct EntropyPlugin;
17+
18+
impl Plugin for EntropyPlugin {
19+
fn build(&self, app: &mut App) {
20+
if !app.world.contains_resource::<Entropy>() {
21+
trace!("Creating entropy");
22+
app.init_resource::<Entropy>();
23+
}
24+
}
25+
}
26+
27+
/// A resource that provides entropy.
28+
pub struct Entropy(StdRng);
29+
30+
impl Default for Entropy {
31+
/// The default entropy source is non-deterministic and seeded from the operating system.
32+
/// For a deterministic source, use [`Entropy::from`].
33+
fn default() -> Self {
34+
debug!("Entropy created via the operating system");
35+
let rng = StdRng::from_entropy();
36+
Entropy(rng)
37+
}
38+
}
39+
40+
impl Entropy {
41+
/// Create a deterministic source of entropy. All random number generators
42+
/// later seeded from an [`Entropy`] created this way will be deterministic.
43+
/// If determinism is not required, use [`Entropy::default`].
44+
pub fn from(seed: [u8; 32]) -> Self {
45+
debug!("Entropy created via seed: {:?} ", seed);
46+
let rng = StdRng::from_seed(seed);
47+
Entropy(rng)
48+
}
49+
50+
/// Fill `dest` with entropy data. For an allocating alternative, see [`Entropy::get`].
51+
pub fn fill_bytes(&mut self, dest: &mut [u8]) {
52+
self.0.fill_bytes(dest)
53+
}
54+
55+
/// Allocate and return entropy data. For a non-allocating alternative, see [`Entropy::fill_bytes`].
56+
pub fn get(&mut self) -> [u8; 32] {
57+
let mut dest = [0; 32];
58+
self.0.fill_bytes(&mut dest);
59+
dest
60+
}
61+
}
62+
63+
#[cfg(test)]
64+
mod test {
65+
use bevy_app::AppExit;
66+
use bevy_ecs::prelude::*;
67+
use bevy_internal::prelude::*;
68+
use rand::{rngs::SmallRng, seq::IteratorRandom, SeedableRng};
69+
use std::sync::mpsc;
70+
use std::sync::mpsc::{Receiver, SyncSender};
71+
72+
#[test]
73+
fn is_deterministic() {
74+
const APP_RUN_COUNT: u8 = 10;
75+
const CHOOSE_COUNT: u8 = 5;
76+
const THING_COUNT: u8 = 100;
77+
78+
struct Thing(u8);
79+
struct ResultChannel(SyncSender<u8>);
80+
81+
// The result of the app we will check to make sure it is always the same.
82+
let mut expected_result: Option<Vec<u8>> = None;
83+
84+
// The seed we will use for the random number generator in all app runs.
85+
let world_seed: [u8; 32] = [1; 32];
86+
87+
// Run the app multiple times.
88+
for runs in 0..APP_RUN_COUNT {
89+
let (tx, rx): (SyncSender<u8>, Receiver<u8>) = mpsc::sync_channel(CHOOSE_COUNT.into());
90+
91+
App::new()
92+
.insert_resource(Entropy::from(world_seed))
93+
.insert_resource(ResultChannel(tx))
94+
.add_plugins_with(MinimalPlugins, |group| group.add(super::EntropyPlugin))
95+
.add_startup_system(spawn_things)
96+
.add_system(choose_things)
97+
.run();
98+
99+
fn spawn_things(mut commands: Commands) {
100+
for x in 1..THING_COUNT {
101+
commands.spawn().insert(Thing(x));
102+
}
103+
}
104+
105+
fn choose_things(
106+
query: Query<&Thing>,
107+
mut entropy: ResMut<Entropy>,
108+
result_channel: Res<ResultChannel>,
109+
mut app_exit_events: EventWriter<AppExit>,
110+
) {
111+
// Create RNG from global entropy.
112+
let seed = entropy.get();
113+
let mut rng = SmallRng::from_seed(seed);
114+
115+
// Choose some random things.
116+
for _ in 0..CHOOSE_COUNT {
117+
if let Some(thing) = query.iter().choose(&mut rng) {
118+
// Send the chosen thing out of the app so it can be inspected
119+
// after the app exits.
120+
result_channel.0.send(thing.0).expect("result to send");
121+
}
122+
}
123+
app_exit_events.send(AppExit)
124+
}
125+
126+
// The result of running the app.
127+
let run_result: Vec<u8> = rx.iter().collect();
128+
129+
// If it is the first run, treat the current result as the expected
130+
// result we will check future runs against.
131+
if runs == 0 {
132+
expected_result = Some(run_result.clone());
133+
}
134+
135+
assert_eq!(expected_result, Some(run_result));
136+
}
137+
}
138+
}

crates/bevy_internal/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ bevy_core = { path = "../bevy_core", version = "0.5.0" }
5555
bevy_derive = { path = "../bevy_derive", version = "0.5.0" }
5656
bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.5.0" }
5757
bevy_ecs = { path = "../bevy_ecs", version = "0.5.0" }
58+
bevy_entropy = { path = "../bevy_entropy", version = "0.5.0" }
5859
bevy_input = { path = "../bevy_input", version = "0.5.0" }
5960
bevy_log = { path = "../bevy_log", version = "0.5.0" }
6061
bevy_math = { path = "../bevy_math", version = "0.5.0" }

crates/bevy_internal/src/default_plugins.rs

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use bevy_asset::AssetPlugin;
66
use bevy_audio::AudioPlugin;
77
use bevy_core::CorePlugin;
88
use bevy_diagnostic::DiagnosticsPlugin;
9+
use bevy_entropy::EntropyPlugin;
910
#[cfg(feature = "bevy_gilrs")]
1011
use bevy_gilrs::GilrsPlugin;
1112
#[cfg(feature = "bevy_gltf")]
@@ -39,6 +40,7 @@ use bevy_winit::WinitPlugin;
3940
/// * [`WindowPlugin`]
4041
/// * [`AssetPlugin`]
4142
/// * [`ScenePlugin`]
43+
/// * [`EntropyPlugin`]
4244
/// * [`RenderPlugin`] - with feature `bevy_render`
4345
/// * [`SpritePlugin`] - with feature `bevy_sprite`
4446
/// * [`PbrPlugin`] - with feature `bevy_pbr`
@@ -61,6 +63,7 @@ impl PluginGroup for DefaultPlugins {
6163
group.add(WindowPlugin::default());
6264
group.add(AssetPlugin::default());
6365
group.add(ScenePlugin::default());
66+
group.add(EntropyPlugin::default());
6467

6568
#[cfg(feature = "bevy_render")]
6669
group.add(RenderPlugin::default());

crates/bevy_internal/src/lib.rs

+5
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ pub mod ecs {
2929
pub use bevy_ecs::*;
3030
}
3131

32+
pub mod entropy {
33+
//! Resources for entropy.
34+
pub use bevy_entropy::*;
35+
}
36+
3237
pub mod input {
3338
//! Resources and events for inputs, e.g. mouse/keyboard, touch, gamepads, etc.
3439
pub use bevy_input::*;

crates/bevy_internal/src/prelude.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#[doc(hidden)]
22
pub use crate::{
3-
app::prelude::*, asset::prelude::*, core::prelude::*, ecs::prelude::*, input::prelude::*,
4-
log::prelude::*, math::prelude::*, reflect::prelude::*, scene::prelude::*,
3+
app::prelude::*, asset::prelude::*, core::prelude::*, ecs::prelude::*, entropy::prelude::*,
4+
input::prelude::*, log::prelude::*, math::prelude::*, reflect::prelude::*, scene::prelude::*,
55
transform::prelude::*, window::prelude::*, DefaultPlugins, MinimalPlugins,
66
};
77

0 commit comments

Comments
 (0)