From ee28a64035922e86cbf5f7ed497f8bdad4993a77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Rica=20Pais=20da=20Silva?= Date: Tue, 12 Nov 2024 07:55:13 +0100 Subject: [PATCH] feat: Observer driven seeding + forking (#26) --- Cargo.toml | 10 +- README.md | 13 +- bevy_prng/Cargo.toml | 2 +- bevy_prng/src/chacha.rs | 4 +- bevy_prng/src/lib.rs | 23 +- bevy_prng/src/newtype.rs | 3 + bevy_prng/src/pcg.rs | 4 +- bevy_prng/src/wyrand.rs | 4 +- bevy_prng/src/xoshiro.rs | 7 +- examples/turn_based_game.rs | 3 +- src/component.rs | 17 +- src/lib.rs | 4 + src/observers.rs | 229 ++++++++++++++++++ src/plugin.rs | 52 ++++- src/resource.rs | 13 +- src/seed.rs | 9 +- src/thread_local_entropy.rs | 26 +-- src/traits.rs | 12 +- src/tutorial.rs | 3 + tests/integration/determinism.rs | 5 +- tests/integration/reseeding.rs | 282 ++++++++++++----------- tutorial/01-choosing-prng.md | 6 +- tutorial/02-basic-usage.md | 6 +- tutorial/03-components-forking.md | 10 +- tutorial/04-observer-driven-reseeding.md | 94 ++++++++ tutorial/04-tips-patterns.md | 3 - 26 files changed, 607 insertions(+), 237 deletions(-) create mode 100644 src/observers.rs create mode 100644 tutorial/04-observer-driven-reseeding.md delete mode 100644 tutorial/04-tips-patterns.md diff --git a/Cargo.toml b/Cargo.toml index 09204e1..f13deb2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,9 @@ version = "0.8.0" rust-version = "1.76.0" [workspace.dependencies] -bevy = { version = "0.15.0-rc.3", default-features = false } +bevy_app = { version = "0.15.0-rc.3" } +bevy_ecs = { version = "0.15.0-rc.3" } +bevy_reflect = { version = "0.15.0-rc.3", default-features = false } serde = "1" serde_derive = "1" rand_core = { version = "0.6", features = ["std"] } @@ -35,6 +37,7 @@ rust-version = { workspace = true } [features] default = ["serialize", "thread_local_entropy"] +experimental = [] thread_local_entropy = ["dep:rand_chacha"] serialize = ["dep:serde", "dep:serde_derive", "rand_core/serde1"] rand_chacha = ["bevy_prng/rand_chacha"] @@ -43,8 +46,9 @@ rand_xoshiro = ["bevy_prng/rand_xoshiro"] wyrand = ["bevy_prng/wyrand"] [dependencies] -# bevy -bevy.workspace = true +bevy_app.workspace = true +bevy_ecs.workspace = true +bevy_reflect.workspace = true bevy_prng = { path = "bevy_prng", version = "0.8" } # others diff --git a/README.md b/README.md index e7feee0..d43d596 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,8 @@ The summary of what RNG algorithm to choose is: pick `wyrand` for almost all cas Before a PRNG can be used via `GlobalEntropy` or `EntropyComponent`, it must be registered via the plugin. ```rust -use bevy::prelude::*; +use bevy_ecs::prelude::*; +use bevy_app::App; use bevy_prng::WyRand; use bevy_rand::prelude::EntropyPlugin; use rand_core::RngCore; @@ -54,7 +55,7 @@ fn main() { At the simplest case, using `GlobalEntropy` directly for all random number generation, though this does limit how well systems using `GlobalEntropy` can be parallelised. All systems that access `GlobalEntropy` will run serially to each other. ```rust -use bevy::prelude::ResMut; +use bevy_ecs::prelude::ResMut; use bevy_prng::WyRand; use bevy_rand::prelude::GlobalEntropy; use rand_core::RngCore; @@ -69,7 +70,7 @@ fn print_random_value(mut rng: ResMut>) { For seeding `EntropyComponent`s from a global source, it is best to make use of forking instead of generating the seed value directly. `GlobalEntropy` can only exist as a singular instance, so when forking normally, it will always fork as `EntropyComponent` instances. ```rust -use bevy::prelude::*; +use bevy_ecs::prelude::*; use bevy_prng::WyRand; use bevy_rand::prelude::{GlobalEntropy, ForkableRng}; @@ -88,7 +89,7 @@ fn setup_source(mut commands: Commands, mut global: ResMut `EntropyComponent`s can be seeded/forked from other `EntropyComponent`s as well. ```rust -use bevy::prelude::*; +use bevy_ecs::prelude::*; use bevy_prng::WyRand; use bevy_rand::prelude::{EntropyComponent, ForkableRng}; @@ -121,6 +122,7 @@ fn setup_npc_from_source( - **`rand_pcg`** - This enables the exporting of newtyped `Pcg*` structs from `rand_pcg`. - **`rand_xoshiro`** - This enables the exporting of newtyped `Xoshiro*` structs from `rand_xoshiro`. It also exports a remote-reflected version of `Seed512` so to allow setting up `Xoshiro512StarStar` and so forth. - **`wyrand`** - This enables the exporting of newtyped `WyRand` from `wyrand`, the same algorithm in use within `fastrand`/`turborand`. +- **`experimental`** - This enables any unstable/experimental features for `bevy_rand`. Currently, this will expose utilities for making use of observers for reseeding sources. ## Supported Versions & MSRV @@ -128,7 +130,8 @@ fn setup_npc_from_source( | `bevy` | `bevy_rand` | | ------ | ------------ | -| v0.14 | v0.7 - v0.8 | +| v0.15 | v0.8 | +| v0.14 | v0.7 | | v0.13 | v0.5 - v0.6 | | v0.12 | v0.4 | | v0.11 | v0.2 - v0.3 | diff --git a/bevy_prng/Cargo.toml b/bevy_prng/Cargo.toml index ec8c165..5178625 100644 --- a/bevy_prng/Cargo.toml +++ b/bevy_prng/Cargo.toml @@ -24,7 +24,7 @@ serialize = [ ] [dependencies] -bevy.workspace = true +bevy_reflect.workspace = true rand_core.workspace = true serde = { workspace = true, optional = true } serde_derive = { workspace = true, optional = true } diff --git a/bevy_prng/src/chacha.rs b/bevy_prng/src/chacha.rs index dac9c78..bc008f1 100644 --- a/bevy_prng/src/chacha.rs +++ b/bevy_prng/src/chacha.rs @@ -1,10 +1,10 @@ use crate::{newtype::newtype_prng, SeedableEntropySource}; -use bevy::prelude::{Reflect, ReflectFromReflect}; +use bevy_reflect::{Reflect, ReflectFromReflect}; use rand_core::{RngCore, SeedableRng}; #[cfg(feature = "serialize")] -use bevy::prelude::{ReflectDeserialize, ReflectSerialize}; +use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; newtype_prng!( ChaCha8Rng, diff --git a/bevy_prng/src/lib.rs b/bevy_prng/src/lib.rs index 42ba0ff..f220800 100644 --- a/bevy_prng/src/lib.rs +++ b/bevy_prng/src/lib.rs @@ -21,10 +21,7 @@ mod xoshiro; use std::fmt::Debug; -use bevy::{ - prelude::{FromReflect, Reflect}, - reflect::{GetTypeRegistration, TypePath, Typed}, -}; +use bevy_reflect::{FromReflect, Reflectable, Typed}; use rand_core::{RngCore, SeedableRng}; #[cfg(feature = "serialize")] use serde::{Deserialize, Serialize}; @@ -49,11 +46,8 @@ pub trait SeedableEntropySource: + PartialEq + Sync + Send - + Reflect - + TypePath + FromReflect - + GetTypeRegistration - + Typed + + Reflectable + Serialize + for<'a> Deserialize<'a> + private::SealedSeedable @@ -71,10 +65,8 @@ pub trait EntropySeed: + Clone + Sync + Send - + Reflect - + TypePath + + Reflectable + FromReflect - + GetTypeRegistration + Serialize + for<'a> Deserialize<'a> { @@ -89,10 +81,8 @@ impl< + Clone + Sync + Send - + Reflect - + TypePath + + Reflectable + FromReflect - + GetTypeRegistration + Serialize + for<'a> Deserialize<'a>, > EntropySeed for T @@ -109,11 +99,8 @@ pub trait SeedableEntropySource: + Debug + PartialEq + AsMut<[u8]> - + Reflect - + TypePath + + Reflectable + FromReflect - + GetTypeRegistration - + Typed + Sync + Send + private::SealedSeedable diff --git a/bevy_prng/src/newtype.rs b/bevy_prng/src/newtype.rs index abee5b3..9665c1d 100644 --- a/bevy_prng/src/newtype.rs +++ b/bevy_prng/src/newtype.rs @@ -2,6 +2,7 @@ macro_rules! newtype_prng { ($newtype:tt, $rng:ty, $doc:tt, $feature:tt) => { #[doc = $doc] #[derive(Debug, Clone, PartialEq, Reflect)] + #[reflect(opaque)] #[cfg_attr( feature = "serialize", derive(::serde_derive::Serialize, ::serde_derive::Deserialize) @@ -75,6 +76,7 @@ macro_rules! newtype_prng { }; } +#[cfg(feature = "rand_xoshiro")] macro_rules! newtype_prng_remote { ($newtype:tt, $rng:ty, $seed:ty, $doc:tt, $feature:tt) => { #[doc = $doc] @@ -153,4 +155,5 @@ macro_rules! newtype_prng_remote { } pub(crate) use newtype_prng; +#[cfg(feature = "rand_xoshiro")] pub(crate) use newtype_prng_remote; diff --git a/bevy_prng/src/pcg.rs b/bevy_prng/src/pcg.rs index 126bc70..9745c76 100644 --- a/bevy_prng/src/pcg.rs +++ b/bevy_prng/src/pcg.rs @@ -1,10 +1,10 @@ use crate::{newtype::newtype_prng, SeedableEntropySource}; -use bevy::prelude::{Reflect, ReflectFromReflect}; +use bevy_reflect::{Reflect, ReflectFromReflect}; use rand_core::{RngCore, SeedableRng}; #[cfg(feature = "serialize")] -use bevy::prelude::{ReflectDeserialize, ReflectSerialize}; +use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; newtype_prng!( Pcg32, diff --git a/bevy_prng/src/wyrand.rs b/bevy_prng/src/wyrand.rs index 0048234..37dab03 100644 --- a/bevy_prng/src/wyrand.rs +++ b/bevy_prng/src/wyrand.rs @@ -1,10 +1,10 @@ use crate::{newtype::newtype_prng, SeedableEntropySource}; -use bevy::prelude::{Reflect, ReflectFromReflect}; +use bevy_reflect::{Reflect, ReflectFromReflect}; use rand_core::{RngCore, SeedableRng}; #[cfg(feature = "serialize")] -use bevy::prelude::{ReflectDeserialize, ReflectSerialize}; +use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; newtype_prng!( WyRand, diff --git a/bevy_prng/src/xoshiro.rs b/bevy_prng/src/xoshiro.rs index dc38342..fc591e2 100644 --- a/bevy_prng/src/xoshiro.rs +++ b/bevy_prng/src/xoshiro.rs @@ -3,14 +3,11 @@ use crate::{ SeedableEntropySource, }; -use bevy::{ - prelude::{Reflect, ReflectDefault, ReflectFromReflect}, - reflect::reflect_remote, -}; +use bevy_reflect::{reflect_remote, std_traits::ReflectDefault, Reflect, ReflectFromReflect}; use rand_core::{RngCore, SeedableRng}; #[cfg(feature = "serialize")] -use bevy::prelude::{ReflectDeserialize, ReflectSerialize}; +use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; /// Remote reflected version of [`rand_xoshiro::Seed512`], needed to support /// proper reflection for the 512 bit variants of the Xoshiro PRNG. diff --git a/examples/turn_based_game.rs b/examples/turn_based_game.rs index 1ab7116..b632ff3 100644 --- a/examples/turn_based_game.rs +++ b/examples/turn_based_game.rs @@ -1,6 +1,7 @@ #![allow(clippy::type_complexity)] -use bevy::prelude::*; +use bevy_app::prelude::*; +use bevy_ecs::prelude::*; use bevy_prng::ChaCha8Rng; use bevy_rand::prelude::{EntropyComponent, EntropyPlugin, ForkableRng, GlobalEntropy}; use rand::prelude::{IteratorRandom, Rng}; diff --git a/src/component.rs b/src/component.rs index 095bcf1..5a8015d 100644 --- a/src/component.rs +++ b/src/component.rs @@ -7,15 +7,16 @@ use crate::{ ForkableRng, ForkableSeed, }, }; -use bevy::prelude::{Component, Reflect, ReflectComponent, ReflectFromReflect}; +use bevy_ecs::prelude::{Component, ReflectComponent}; use bevy_prng::SeedableEntropySource; +use bevy_reflect::{Reflect, ReflectFromReflect}; use rand_core::{RngCore, SeedableRng}; #[cfg(feature = "thread_local_entropy")] use crate::thread_local_entropy::ThreadLocalEntropy; #[cfg(feature = "serialize")] -use bevy::prelude::{ReflectDeserialize, ReflectSerialize}; +use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; #[cfg(feature = "serialize")] use serde::Deserialize; @@ -33,7 +34,7 @@ use serde::Deserialize; /// /// Randomised Component: /// ``` -/// use bevy::prelude::*; +/// use bevy_ecs::prelude::*; /// use bevy_prng::WyRand; /// use bevy_rand::prelude::EntropyComponent; /// @@ -51,7 +52,7 @@ use serde::Deserialize; /// /// Seeded from a resource: /// ``` -/// use bevy::prelude::*; +/// use bevy_ecs::prelude::*; /// use bevy_prng::ChaCha8Rng; /// use bevy_rand::prelude::{GlobalEntropy, ForkableRng}; /// @@ -69,7 +70,7 @@ use serde::Deserialize; /// /// Seeded from a component: /// ``` -/// use bevy::prelude::*; +/// use bevy_ecs::prelude::*; /// use bevy_prng::WyRand; /// use bevy_rand::prelude::{EntropyComponent, ForkableRng}; /// @@ -233,8 +234,8 @@ where #[cfg(test)] mod tests { - use bevy::reflect::TypePath; use bevy_prng::{ChaCha12Rng, ChaCha8Rng}; + use bevy_reflect::TypePath; use super::*; @@ -293,7 +294,7 @@ mod tests { #[cfg(feature = "serialize")] #[test] fn rng_untyped_serialization() { - use bevy::reflect::{ + use bevy_reflect::{ serde::{ReflectDeserializer, ReflectSerializer}, FromReflect, TypeRegistry, }; @@ -341,7 +342,7 @@ mod tests { #[cfg(feature = "serialize")] #[test] fn rng_typed_serialization() { - use bevy::reflect::{ + use bevy_reflect::{ serde::{TypedReflectDeserializer, TypedReflectSerializer}, FromReflect, GetTypeRegistration, TypeRegistry, }; diff --git a/src/lib.rs b/src/lib.rs index c09334d..154fa97 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +#![allow(clippy::type_complexity)] #![warn(clippy::undocumented_unsafe_blocks)] #![cfg_attr(docsrs, feature(doc_cfg))] #![cfg_attr(docsrs, allow(unused_attributes))] @@ -6,6 +7,9 @@ /// Components for integrating [`RngCore`] PRNGs into bevy. Must be newtyped to support [`Reflect`]. pub mod component; +#[cfg(feature = "experimental")] +/// Utility observers for handling seeding between parent/child entropy sources +pub mod observers; /// Plugin for integrating [`RngCore`] PRNGs into bevy. Must be newtyped to support [`Reflect`]. pub mod plugin; /// Prelude for providing all necessary types for easy use. diff --git a/src/observers.rs b/src/observers.rs new file mode 100644 index 0000000..b331a11 --- /dev/null +++ b/src/observers.rs @@ -0,0 +1,229 @@ +use std::marker::PhantomData; + +use bevy_ecs::{ + prelude::{ + Commands, Component, Entity, EntityWorldMut, Event, OnInsert, Query, ResMut, Trigger, With, + }, + query::QuerySingleError, +}; + +use bevy_prng::SeedableEntropySource; + +use crate::{ + prelude::{EntropyComponent, ForkableSeed, GlobalEntropy}, + seed::RngSeed, + traits::SeedSource, +}; + +/// Component to denote a source has linked children entities +#[derive(Debug, Component)] +pub struct RngChildren(PhantomData); + +impl Default for RngChildren { + fn default() -> Self { + Self(PhantomData) + } +} + +/// Component to denote has a relation to a parent Rng source entity. +#[derive(Debug, Component)] +pub struct RngParent(Entity, PhantomData); + +impl RngParent { + /// Initialises the relation component with the parent entity + pub fn new(parent: Entity) -> Self { + Self(parent, PhantomData) + } + + /// Get the parent source entity + pub fn entity(&self) -> Entity { + self.0 + } +} + +/// Observer event for triggering an entity to pull a new seed value from a +/// GlobalEntropy source. +#[derive(Debug, Event)] +pub struct SeedFromGlobal(PhantomData); + +impl Default for SeedFromGlobal { + fn default() -> Self { + Self(PhantomData) + } +} + +/// Observer event for triggering an entity to pull a new seed value from a +/// linked parent entity. +#[derive(Debug, Event)] +pub struct SeedFromParent(PhantomData); + +impl Default for SeedFromParent { + fn default() -> Self { + Self(PhantomData) + } +} + +/// Observer event for triggering an entity to use a new seed value from the +/// the event. +#[derive(Debug, Event)] +pub struct ReseedRng(Rng::Seed); + +impl ReseedRng +where + Rng::Seed: Send + Sync + Clone, +{ + /// Create a new reseed event with a specified seed value. + pub fn new(seed: Rng::Seed) -> Self { + Self(seed) + } +} + +/// Observer event for linking a source Rng to one or many target Rngs. This then creates the +/// association needed so that when the source Rng's seed is changed, it propagates new seeds to +/// all linked Rngs. +#[derive(Debug, Event)] +pub struct LinkRngSourceToTarget { + rng: PhantomData, + source: PhantomData, + target: PhantomData, +} + +impl Default + for LinkRngSourceToTarget +where + Rng::Seed: Sync + Send + Clone, +{ + fn default() -> Self { + Self { + rng: PhantomData, + source: PhantomData, + target: PhantomData, + } + } +} + +/// Observer system for reseeding a target RNG on an entity with a provided seed value. +pub fn reseed(trigger: Trigger>, mut commands: Commands) +where + Rng::Seed: Sync + Send + Clone, +{ + let target = trigger.entity(); + + if target != Entity::PLACEHOLDER { + commands + .entity(target) + .insert(RngSeed::::from_seed(trigger.0.clone())); + } +} + +/// Observer System for pulling in a new seed from a GlobalEntropy source +pub fn seed_from_global( + trigger: Trigger>, + mut source: ResMut>, + mut commands: Commands, +) where + Rng::Seed: Send + Sync + Clone, +{ + if let Some(mut entity) = commands.get_entity(trigger.entity()) { + entity.insert(source.fork_seed()); + } +} + +/// Observer System for pulling in a new seed for the current entity from its parent Rng source. +pub fn seed_from_parent( + trigger: Trigger>, + mut commands: Commands, +) where + Rng::Seed: Send + Sync + Clone, +{ + let target = trigger.entity(); + + if target != Entity::PLACEHOLDER { + commands.entity(target).queue(|mut entity: EntityWorldMut| { + let Some(parent) = entity.get::>().map(|parent| parent.entity()) else { + return; + }; + entity + .world_scope(|world| { + world.get_entity_mut(parent).ok().and_then(|mut parent| { + parent + .get_mut::>() + .map(|mut rng| rng.fork_seed()) + }) + }) + .map(|seed| entity.insert(seed)); + }); + } +} + +/// Observer System for handling seed propagation from source Rng to all child entities. +pub fn seed_children( + trigger: Trigger>, + mut q_source: Query<&mut EntropyComponent, (With, With>)>, + q_target: Query>, + mut commands: Commands, +) where + Rng::Seed: Send + Sync + Clone, +{ + let source = trigger.entity(); + + if source != Entity::PLACEHOLDER { + if let Ok(mut rng) = q_source.get_mut(source) { + let batch: Vec<(Entity, RngSeed)> = q_target + .iter() + .map(|target| (target, rng.fork_seed())) + .collect(); + + commands.insert_batch(batch); + } + } +} + +/// Observer System for handling linking a source Rng with all target entities. Highly recommended +/// that the Source Rng is unique, or has a marker component that designates it as unique, otherwise +/// this observer will pick whichever get queried first during linking. +pub fn link_targets( + _trigger: Trigger>, + q_source: Query>, + q_target: Query>, + mut commands: Commands, +) { + let source = match q_source.get_single() { + Ok(parent) => Some(parent), + // If we somehow have more than one source, just use the first one and stick with that. + Err(QuerySingleError::MultipleEntities(_)) => q_source.iter().next(), + Err(QuerySingleError::NoEntities(_)) => None, + }; + + if let Some(parent) = source { + let mut targets = q_target.iter(); + + let assigned = match targets.size_hint().0 { + 0 => false, + 1 => { + let target = targets.next().unwrap(); + + commands + .entity(target) + .insert(RngParent::::new(parent)); + + true + } + _ => { + let targets: Vec<_> = targets + .map(|target| (target, RngParent::::new(parent))) + .collect(); + + commands.insert_batch(targets); + + true + } + }; + + if assigned { + commands + .entity(parent) + .insert(RngChildren::::default()); + } + } +} diff --git a/src/plugin.rs b/src/plugin.rs index 9e11af1..7953bbf 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -1,5 +1,10 @@ +#[cfg(feature = "experimental")] +use std::marker::PhantomData; + use crate::{component::EntropyComponent, resource::GlobalEntropy, seed::RngSeed}; -use bevy::prelude::{App, Plugin}; +use bevy_app::{App, Plugin}; +#[cfg(feature = "experimental")] +use bevy_ecs::prelude::Component; use bevy_prng::{EntropySeed, SeedableEntropySource}; use rand_core::SeedableRng; @@ -8,7 +13,8 @@ use rand_core::SeedableRng; /// entropy components. /// /// ``` -/// use bevy::prelude::*; +/// use bevy_app::prelude::*; +/// use bevy_ecs::prelude::*; /// use bevy_prng::{ChaCha8Rng, WyRand}; /// use bevy_rand::prelude::{EntropyPlugin, GlobalEntropy}; /// use rand_core::RngCore; @@ -74,7 +80,47 @@ where } else { app.init_resource::>(); } - + #[cfg(feature = "experimental")] + app.add_observer(crate::observers::seed_from_global::) + .add_observer(crate::observers::reseed::); app.world_mut().register_component_hooks::>(); } } + +/// Plugin for setting up linked RNG sources +#[cfg(feature = "experimental")] +pub struct LinkedEntropySources< + Source: Component, + Target: Component, + Rng: SeedableEntropySource + 'static, +> { + rng: PhantomData, + source: PhantomData, + target: PhantomData, +} + +#[cfg(feature = "experimental")] +impl Default + for LinkedEntropySources +{ + fn default() -> Self { + Self { + rng: PhantomData, + source: PhantomData, + target: PhantomData, + } + } +} + +#[cfg(feature = "experimental")] +impl Plugin + for LinkedEntropySources +where + Rng::Seed: Send + Sync + Clone, +{ + fn build(&self, app: &mut App) { + app.add_observer(crate::observers::seed_from_parent::) + .add_observer(crate::observers::seed_children::) + .add_observer(crate::observers::link_targets::); + } +} diff --git a/src/resource.rs b/src/resource.rs index c6a0b43..b2fa392 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -8,15 +8,16 @@ use crate::{ ForkableRng, ForkableSeed, }, }; -use bevy::prelude::{Reflect, ReflectFromReflect, ReflectFromWorld, ReflectResource, Resource}; +use bevy_ecs::prelude::{ReflectFromWorld, ReflectResource, Resource}; use bevy_prng::SeedableEntropySource; +use bevy_reflect::{Reflect, ReflectFromReflect}; use rand_core::{RngCore, SeedableRng}; #[cfg(feature = "thread_local_entropy")] use crate::thread_local_entropy::ThreadLocalEntropy; #[cfg(feature = "serialize")] -use bevy::prelude::{ReflectDeserialize, ReflectSerialize}; +use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; #[cfg(feature = "serialize")] use serde::{Deserialize, Serialize}; @@ -28,7 +29,7 @@ use serde::{Deserialize, Serialize}; /// # Example /// /// ``` -/// use bevy::prelude::*; +/// use bevy_ecs::prelude::*; /// use bevy_prng::ChaCha8Rng; /// use bevy_rand::prelude::GlobalEntropy; /// use rand_core::RngCore; @@ -210,8 +211,8 @@ where #[cfg(test)] mod tests { - use bevy::reflect::TypePath; use bevy_prng::{ChaCha12Rng, ChaCha8Rng, WyRand}; + use bevy_reflect::TypePath; use super::*; @@ -271,7 +272,7 @@ mod tests { #[cfg(feature = "serialize")] #[test] fn rng_untyped_serialization() { - use bevy::reflect::{ + use bevy_reflect::{ serde::{ReflectDeserializer, ReflectSerializer}, FromReflect, TypeRegistry, }; @@ -319,7 +320,7 @@ mod tests { #[cfg(feature = "serialize")] #[test] fn rng_typed_serialization() { - use bevy::reflect::{ + use bevy_reflect::{ serde::{TypedReflectDeserializer, TypedReflectSerializer}, FromReflect, GetTypeRegistration, TypeRegistry, }; diff --git a/src/seed.rs b/src/seed.rs index 3db301d..6df9a5e 100644 --- a/src/seed.rs +++ b/src/seed.rs @@ -1,7 +1,8 @@ use std::marker::PhantomData; -use bevy::{ecs::component::StorageType, prelude::Component, reflect::Reflect}; +use bevy_ecs::{component::StorageType, prelude::Component}; use bevy_prng::SeedableEntropySource; +use bevy_reflect::Reflect; use rand_core::SeedableRng; use crate::{component::EntropyComponent, traits::SeedSource}; @@ -48,7 +49,7 @@ where { const STORAGE_TYPE: StorageType = StorageType::Table; - fn register_component_hooks(hooks: &mut bevy::ecs::component::ComponentHooks) { + fn register_component_hooks(hooks: &mut bevy_ecs::component::ComponentHooks) { hooks .on_insert(|mut world, entity, _| { let seed = world.get::>(entity).unwrap().seed.clone(); @@ -73,11 +74,11 @@ mod tests { #[cfg(feature = "serialize")] #[test] fn reflection_serialization_round_trip_works() { - use bevy::reflect::{ + use bevy_prng::WyRand; + use bevy_reflect::{ serde::{TypedReflectDeserializer, TypedReflectSerializer}, FromReflect, GetTypeRegistration, TypeRegistry, }; - use bevy_prng::WyRand; use ron::to_string; use serde::de::DeserializeSeed; diff --git a/src/thread_local_entropy.rs b/src/thread_local_entropy.rs index 6c6f283..1abf302 100644 --- a/src/thread_local_entropy.rs +++ b/src/thread_local_entropy.rs @@ -104,19 +104,14 @@ mod tests { #[test] fn unique_source_per_thread() { - use std::sync::mpsc::channel; - let mut bytes1: Vec = vec![0u8; 128]; let mut bytes2: Vec = vec![0u8; 128]; let b1 = bytes1.as_mut(); let b2 = bytes2.as_mut(); - let (sender, receiver) = channel(); - let sender2 = sender.clone(); - - std::thread::scope(|s| { - s.spawn(move || { + let (a, b) = std::thread::scope(|s| { + let a = s.spawn(move || { // Obtain a thread local entropy source from this thread context. // It should be initialised with a random state. let mut rng = ThreadLocalEntropy::new(); @@ -124,11 +119,9 @@ mod tests { // Use the source to produce some stored entropy. rng.fill_bytes(b1); - let source = rng.access_local_source(|rng| rng.clone()); - - sender.send(source).unwrap(); + rng.access_local_source(|rng| rng.clone()) }); - s.spawn(move || { + let b = s.spawn(move || { // Obtain a thread local entropy source from this thread context. // It should be initialised with a random state. let mut rng = ThreadLocalEntropy::new(); @@ -136,15 +129,14 @@ mod tests { // Use the source to produce some stored entropy. rng.fill_bytes(b2); - let source = rng.access_local_source(|rng| rng.clone()); - - sender2.send(source).unwrap(); + rng.access_local_source(|rng| rng.clone()) }); + + (a.join(), b.join()) }); - // Wait for the threads to execute and resolve. - let a = receiver.recv().unwrap(); - let b = receiver.recv().unwrap(); + let a = a.unwrap(); + let b = b.unwrap(); // The references to the thread local RNG sources will not be // the same, as they each were initialised with different random diff --git a/src/traits.rs b/src/traits.rs index 2e7632a..0c4b359 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -11,7 +11,7 @@ pub trait ForkableRng: EcsEntropySource { /// Fork the original instance to yield a new instance with a generated seed. /// This method preserves the RNG algorithm between original and forked instances. /// ``` - /// use bevy::prelude::*; + /// use bevy_ecs::prelude::*; /// use bevy_prng::ChaCha8Rng; /// use bevy_rand::prelude::{GlobalEntropy, ForkableRng}; /// @@ -43,7 +43,7 @@ pub trait ForkableAsRng: EcsEntropySource { /// Fork the original instance to yield a new instance with a generated seed. /// This method allows one to specify the RNG algorithm to be used for the forked instance. /// ``` - /// use bevy::prelude::*; + /// use bevy_ecs::prelude::*; /// use bevy_rand::prelude::{GlobalEntropy, ForkableAsRng}; /// use bevy_prng::{ChaCha8Rng, ChaCha12Rng}; /// @@ -73,7 +73,7 @@ pub trait ForkableInnerRng: EcsEntropySource { /// Fork the original instance to yield a new instance with a generated seed. /// This method yields the inner PRNG instance directly as a forked instance. /// ``` - /// use bevy::prelude::*; + /// use bevy_ecs::prelude::*; /// use bevy_rand::prelude::{GlobalEntropy, ForkableInnerRng}; /// use bevy_prng::ChaCha8Rng; /// use rand_core::RngCore; @@ -109,7 +109,7 @@ where /// Fork a new seed from the original entropy source. /// This method preserves the RNG algorithm between original instance and forked seed. /// ``` - /// use bevy::prelude::*; + /// use bevy_ecs::prelude::*; /// use bevy_prng::ChaCha8Rng; /// use bevy_rand::prelude::{GlobalEntropy, ForkableSeed}; /// @@ -147,7 +147,7 @@ pub trait ForkableAsSeed: EcsEntropySource { /// Fork a new seed from the original entropy source. /// This method allows one to specify the RNG algorithm to be used for the forked seed. /// ``` - /// use bevy::prelude::*; + /// use bevy_ecs::prelude::*; /// use bevy_rand::prelude::{GlobalEntropy, ForkableAsSeed}; /// use bevy_prng::{ChaCha8Rng, ChaCha12Rng}; /// @@ -187,7 +187,7 @@ where /// Fork a new seed from the original entropy source. /// This method preserves the RNG algorithm between original instance and forked seed. /// ``` - /// use bevy::prelude::*; + /// use bevy_ecs::prelude::*; /// use bevy_prng::ChaCha8Rng; /// use bevy_rand::prelude::{GlobalEntropy, ForkableInnerSeed, SeedSource, RngSeed}; /// diff --git a/src/tutorial.rs b/src/tutorial.rs index 6442baf..0c76442 100644 --- a/src/tutorial.rs +++ b/src/tutorial.rs @@ -20,3 +20,6 @@ pub mod ch02_basic_usage {} #[doc = include_str!("../tutorial/03-components-forking.md")] pub mod ch03_components_forking {} + +#[doc = include_str!("../tutorial/04-observer-driven-reseeding.md")] +pub mod ch04_observer_driven_reseeding {} diff --git a/tests/integration/determinism.rs b/tests/integration/determinism.rs index a2b4fc6..a939f1c 100644 --- a/tests/integration/determinism.rs +++ b/tests/integration/determinism.rs @@ -1,4 +1,5 @@ -use bevy::prelude::*; +use bevy_app::prelude::*; +use bevy_ecs::prelude::*; use bevy_prng::{ChaCha12Rng, ChaCha8Rng, WyRand}; use bevy_rand::prelude::{ EntropyComponent, EntropyPlugin, ForkableAsRng, ForkableRng, GlobalEntropy, @@ -107,7 +108,7 @@ fn test_parallel_determinism() { #[cfg(not(target_arch = "wasm32"))] app.edit_schedule(Update, |schedule| { - use bevy::ecs::schedule::ExecutorKind; + use bevy_ecs::schedule::ExecutorKind; // Ensure the Update schedule is Multithreaded on non-WASM platforms schedule.set_executor_kind(ExecutorKind::MultiThreaded); diff --git a/tests/integration/reseeding.rs b/tests/integration/reseeding.rs index 95832e9..83c5d9e 100644 --- a/tests/integration/reseeding.rs +++ b/tests/integration/reseeding.rs @@ -1,13 +1,11 @@ -use bevy::{ - app::{App, PreStartup, Update}, - prelude::{Commands, Query, ResMut}, -}; +use bevy_app::prelude::*; +use bevy_ecs::prelude::*; use bevy_prng::{ChaCha8Rng, WyRand}; use bevy_rand::{ plugin::EntropyPlugin, prelude::EntropyComponent, resource::GlobalEntropy, - traits::{ForkableAsSeed, ForkableSeed, SeedSource}, + traits::{ForkableAsSeed, ForkableSeed}, }; use rand_core::{RngCore, SeedableRng}; @@ -131,23 +129,15 @@ fn component_fork_as_seed() { } #[test] +#[cfg(feature = "experimental")] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] fn observer_global_reseeding() { - use bevy::prelude::{Entity, Event, PostUpdate, PreUpdate, Startup, Trigger, With}; - use bevy_rand::{seed::RngSeed, traits::ForkableInnerSeed}; + use bevy_app::prelude::{PostUpdate, PreUpdate, Startup}; + use bevy_ecs::prelude::{Entity, With}; + use bevy_rand::{observers::ReseedRng, seed::RngSeed, traits::{ForkableInnerSeed, SeedSource}}; let seed = [2; 8]; - #[derive(Event)] - struct Reseed([u8; 8]); - - fn reseed(trigger: Trigger, mut commands: Commands) { - if let Some(mut entity) = commands.get_entity(trigger.entity()) { - let seed = trigger.event(); - entity.insert(RngSeed::::from_seed(seed.0)); - } - } - let mut app = App::new(); app.add_plugins(EntropyPlugin::::with_seed(seed)) @@ -180,7 +170,7 @@ fn observer_global_reseeding() { query: Query>>, mut source: ResMut>| { for e in &query { - commands.trigger_targets(Reseed(source.fork_inner_seed()), e); + commands.trigger_targets(ReseedRng::::new(source.fork_inner_seed()), e); } }, ) @@ -198,142 +188,157 @@ fn observer_global_reseeding() { .into_iter() .zip(seeds.map(u64::from_ne_bytes)) .for_each(|(expected, actual)| assert_ne!(expected, actual)); - }) - .add_observer(reseed); + }); app.run(); } #[test] +#[cfg(feature = "experimental")] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] -fn observer_children_reseeding() { - use bevy::prelude::{ - Component, Entity, EntityWorldMut, Event, Last, PostUpdate, PreUpdate, Startup, Trigger, - With, Without, World, - }; +fn generic_observer_reseeding_from_parent() { + use bevy_app::prelude::{PostUpdate, PreUpdate, Startup}; + use bevy_ecs::prelude::{Entity, With}; use bevy_rand::{ + observers::{LinkRngSourceToTarget, SeedFromGlobal, SeedFromParent}, + plugin::LinkedEntropySources, seed::RngSeed, - traits::{ForkableInnerSeed, ForkableRng}, + traits::SeedSource, }; - let seed = [2; 8]; + let seed = [2u8; 8]; #[derive(Component)] - struct SeedChildren(Vec); - - #[derive(Event)] - struct ReseedChildren(EntropyComponent); - - #[derive(Event)] - struct Reseed([u8; 8]); - - fn reseed( - trigger: Trigger, - q_children: Query<(&EntropyComponent, &SeedChildren)>, - mut commands: Commands, - ) { - let entity = trigger.entity(); - - if let Some(mut entity_commands) = commands.get_entity(trigger.entity()) { - let seed = trigger.event(); - entity_commands.insert(RngSeed::::from_seed(seed.0)); - } - - // MUST trigger AFTER the insert - if let Ok((rng, _)) = q_children.get(entity) { - // Clone our RNG component to track changes to the entity later - commands.trigger_targets(ReseedChildren(rng.clone()), entity); - } - } - - fn reseed_children( - trigger: Trigger, - mut q_source: Query<(&mut EntropyComponent, &SeedChildren)>, - mut commands: Commands, - ) { - let entity = trigger.entity(); - - if let Ok((mut rng, children)) = q_source.get_mut(entity) { - let previous = &trigger.event().0; - // We check to ensure that the RNG component has been updated via hooks before - // the observer has been run. - assert_ne!(previous, rng.as_ref()); - - for child in children.0.iter() { - commands.entity(*child).insert(rng.fork_seed()); - } - } - } + struct Source; + #[derive(Component)] + struct Target; let mut app = App::new(); - app.add_plugins(EntropyPlugin::::with_seed(seed)) - .add_systems( - Startup, - |mut commands: Commands, mut source: ResMut>| { - let seed = source.fork_seed(); + app.add_plugins(( + EntropyPlugin::::with_seed(seed), + LinkedEntropySources::::default(), + )) + .add_systems(Startup, |mut commands: Commands| { + let source = commands.spawn(Source).id(); + commands.spawn(Target); + + commands.trigger(LinkRngSourceToTarget::::default()); + commands.trigger_targets(SeedFromGlobal::::default(), source); + }) + .add_systems(PreUpdate, |query: Query<&RngSeed, With>| { + let expected = 6445550333322662121; + let seed = u64::from_ne_bytes(query.single().clone_seed()); + + assert_eq!(seed, expected); + }) + .add_systems(PreUpdate, |query: Query<&RngSeed, With>| { + let expected = 2484862625678185386; + let seed = u64::from_ne_bytes(query.single().clone_seed()); + + assert_eq!(seed, expected); + }) + .add_systems( + Update, + |mut commands: Commands, query: Query>| { + commands.trigger_targets(SeedFromParent::::default(), query.single()); + }, + ) + .add_systems( + PostUpdate, + |query: Query<&RngSeed, With>| { + let prev_expected = 6445550333322662121; + let expected = 14968821102299026759; + let seed = u64::from_ne_bytes(query.single().clone_seed()); + + assert_ne!(seed, prev_expected); + assert_eq!(seed, expected); + }, + ); - // Ensure the forked seed and original source don't match - assert_ne!(source.get_seed(), seed.get_seed()); + app.run(); +} - let mut source = commands.spawn(seed); +#[test] +#[cfg(feature = "experimental")] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] +fn generic_observer_reseeding_children() { + use bevy_app::prelude::{Last, PostUpdate, PreUpdate, Startup}; + use bevy_ecs::prelude::{Component, Entity, With, Without}; + use bevy_rand::{ + observers::{LinkRngSourceToTarget, SeedFromGlobal}, + plugin::LinkedEntropySources, + seed::RngSeed, + traits::SeedSource, + }; - source.queue(|mut entity: EntityWorldMut| { - // FORK! Quicker than allocating a new Vec of components to spawn. - let mut rng = entity - .get_mut::>() - .unwrap() - .fork_rng(); + let seed = [2u8; 8]; - let children: Vec = entity.world_scope(move |world: &mut World| { - world.spawn_batch((0..5).map(|_| rng.fork_seed())).collect() - }); + #[derive(Component)] + struct Source; + #[derive(Component, Clone, Copy)] + struct Target; - entity.insert(SeedChildren(children)); - }); - }, - ) - .add_systems(PreUpdate, |query: Query<&RngSeed>| { - // Before we send a Reseed event, let's check if we have properly initialised - // seeds. - let expected = [ - 9035371013317154993u64, - 8695044747327652655, - 4791951605491714159, - 7661732659691953580, - 4722119124111390177, - ]; - let seeds = query.iter().map(RngSeed::::clone_seed); + let mut app = App::new(); - expected - .into_iter() - .zip(seeds.map(u64::from_ne_bytes)) - .for_each(|(expected, actual)| assert_eq!(expected, actual)); - }) - .add_systems( - Update, - |mut commands: Commands, - query: Query>, - mut source: ResMut>| { - for e in &query { - commands.trigger_targets(Reseed(source.fork_inner_seed()), e); - } - }, - ) - .add_systems(PostUpdate, |query: Query<&RngSeed>| { + app.add_plugins(( + EntropyPlugin::::with_seed(seed), + LinkedEntropySources::::default(), + )) + .add_systems(Startup, |mut commands: Commands| { + commands.spawn_batch(vec![Target; 5]); + let source = commands.spawn(Source).id(); + + commands.trigger(LinkRngSourceToTarget::::default()); + commands.trigger_targets(SeedFromGlobal::::default(), source); + }) + .add_systems(PreUpdate, |query: Query<&RngSeed, With>| { + let expected = [ + 6445550333322662121u64, + 14968821102299026759, + 12617564484450995185, + 908888629357954483, + 6128439264405451235, + ]; + let seeds = query.iter().map(RngSeed::::clone_seed); + + assert_eq!(seeds.size_hint().0, 5); + + expected + .into_iter() + .zip(seeds.map(u64::from_ne_bytes)) + .for_each(|(expected, actual)| assert_eq!(expected, actual)); + }) + .add_systems(PreUpdate, |query: Query<&RngSeed, With>| { + let expected = 2484862625678185386u64; + let seeds = u64::from_ne_bytes(query.single().clone_seed()); + + assert_eq!(expected, seeds); + }) + .add_systems( + Update, + |mut commands: Commands, query: Query>| { + for entity in &query { + commands.trigger_targets(SeedFromGlobal::::default(), entity); + } + }, + ) + .add_systems( + PostUpdate, + |query: Query<&RngSeed, With>| { let prev_expected = [ - 9035371013317154993u64, - 8695044747327652655, - 4791951605491714159, - 7661732659691953580, - 4722119124111390177, + 6445550333322662121u64, + 14968821102299026759, + 12617564484450995185, + 908888629357954483, + 6128439264405451235, ]; let expected = [ - 6101264679293753504u64, - 2656876351602726802, + 2656876351602726802u64, 4226413670151402273, 2344778986622729714, 9109365740673988404, + 6101264679293753504, ]; prev_expected @@ -351,18 +356,17 @@ fn observer_children_reseeding() { // Should equal the expected updated seeds. assert_eq!(expected, actual) }); - }) - .add_systems( - Last, - |source: Query<&EntropyComponent, With>, - children: Query<&EntropyComponent, Without>| { - // Check we have the correct amount of allocated RNG entities - assert_eq!(source.iter().size_hint().0, 1); - assert_eq!(children.iter().size_hint().0, 5); - }, - ) - .add_observer(reseed) - .add_observer(reseed_children); + }, + ) + .add_systems( + Last, + |source: Query<&RngSeed, With>, + children: Query<&RngSeed, Without>| { + // Check we have the correct amount of allocated RNG entities + assert_eq!(source.iter().size_hint().0, 1); + assert_eq!(children.iter().size_hint().0, 5); + }, + ); app.run(); } diff --git a/tutorial/01-choosing-prng.md b/tutorial/01-choosing-prng.md index c79411e..5179ff1 100644 --- a/tutorial/01-choosing-prng.md +++ b/tutorial/01-choosing-prng.md @@ -27,7 +27,8 @@ use bevy_prng::{ChaCha8Rng, WyRand}; When you've selected and imported the PRNG algorithm you want to use, you then need to enable it by adding the `EntropyPlugin` to your app. This then makes it available for use with `GlobalEntropy` and `EntropyComponent`, as well as registering the types for reflection. ```rust -use bevy::prelude::*; +use bevy_ecs::prelude::*; +use bevy_app::prelude::*; use bevy_prng::WyRand; use bevy_rand::prelude::EntropyPlugin; use rand_core::RngCore; @@ -42,7 +43,8 @@ fn main() { By default, the plugin will instantiate the `GlobalEntropy` resource with a random seed from OS sources. If you want to initialise the plugin and `GlobalEntropy` with a set seed or from a different source, use [`crate::prelude::EntropyPlugin::with_seed`] instead. ```rust -use bevy::prelude::*; +use bevy_ecs::prelude::*; +use bevy_app::prelude::*; use bevy_prng::WyRand; use bevy_rand::prelude::EntropyPlugin; use rand_core::RngCore; diff --git a/tutorial/02-basic-usage.md b/tutorial/02-basic-usage.md index f949786..35ad0b1 100644 --- a/tutorial/02-basic-usage.md +++ b/tutorial/02-basic-usage.md @@ -3,7 +3,7 @@ At the simplest case, using `GlobalEntropy` directly for all random number generation, though this does limit how well systems using `GlobalEntropy` can be parallelised. All systems that access `GlobalEntropy` will run serially to each other. ```rust -use bevy::prelude::*; +use bevy_ecs::prelude::*; use bevy_prng::ChaCha8Rng; use bevy_rand::prelude::GlobalEntropy; use rand_core::RngCore; @@ -26,7 +26,7 @@ When using `GlobalEntropy`, the way to ensure deterministic output/usage with th An example of a guaranteed deterministic system is perhaps spawning new entities with a randomised component value: ```rust -use bevy::prelude::*; +use bevy_ecs::prelude::*; use bevy_prng::WyRand; use bevy_rand::prelude::GlobalEntropy; use rand_core::RngCore; @@ -52,7 +52,7 @@ The above system will iterate a set number of times, and will yield 10 randomis However, iterating over queries will **not** yield deterministic output, as queries are not guaranteed to iterate over collected entities in the same order every time the system is ran. Therefore, the below example will not have deterministic output. ```rust -use bevy::prelude::*; +use bevy_ecs::prelude::*; use bevy_prng::WyRand; use bevy_rand::prelude::GlobalEntropy; use rand_core::RngCore; diff --git a/tutorial/03-components-forking.md b/tutorial/03-components-forking.md index e6aa8e1..006499b 100644 --- a/tutorial/03-components-forking.md +++ b/tutorial/03-components-forking.md @@ -3,7 +3,7 @@ In order to move beyond the restrictions placed by `GlobalEntropy` and achieve determinism *with parallelism*, where the RNG source lives has to go from a global source to one owned by the entities themselves. `EntropyComponent` enables us to attach a PRNG to any given entity, and thus sidesteps not only forcing systems to run serially to each other, but also avoids the problem of queries not being stable in ordering. In fact, as ordering is no longer an issue, parallel iteration of queries is made possible as we avoid borrowing issues if each entity we queried owns its own RNG source. ```rust -use bevy::prelude::*; +use bevy_ecs::prelude::*; use bevy_prng::WyRand; use bevy_rand::prelude::EntropyComponent; @@ -22,7 +22,7 @@ fn setup_source(mut commands: Commands) { In the above example, we are creating an entity with a `Source` marker component and attaching an `EntropyComponent` to it with the `WyRand` algorithm and a randomised seed. To then access this source, we simply query `Query<&mut EntropyComponent, With>`. In this case, we are creating a single entity with an RNG source, but there's no reason why many more can't have an RNG source attached to them. ```rust -use bevy::prelude::*; +use bevy_ecs::prelude::*; use bevy_prng::WyRand; use bevy_rand::prelude::EntropyComponent; @@ -49,7 +49,7 @@ Forking is the process of generating a new seed from an RNG source and creating Because PRNG algorithms are deterministic, forking is a deterministic process, and it allows us to have one seed state create many "random" states while being hard to predict. `bevy_rand` makes it super easy to fork new `EntropyComponent`s, allowing you to source new RNGs from `GlobalEntropy` or even other `EntropyComponent`s! ```rust -use bevy::prelude::*; +use bevy_ecs::prelude::*; use bevy_prng::ChaCha8Rng; use bevy_rand::prelude::{EntropyComponent, GlobalEntropy, ForkableRng}; @@ -68,7 +68,7 @@ fn setup_source(mut commands: Commands, mut global: ResMut>) { + for target in &q_targets { + commands.trigger_targets(SeedFromGlobal::::default(), target); + } +} +``` + +Alternatively, one can provide a set seed to reseed all target entities with: + +```rust ignore +use bevy_ecs::prelude::*; +use bevy_prng::WyRand; +use bevy_rand::observers::ReseedRng; + +#[derive(Component)] +struct Target; + +fn reseed_target_entities_from_set_seed(mut commands: Commands, mut q_targets: Query>) { + let seed = u64::to_ne_bytes(42); + + for target in &q_targets { + commands.trigger_targets(ReseedRng::::new(seed), target); + } +} +``` + +With these observers, you can initialise entropy components on all targetted entities simply by triggering a reseed event on them. As long as your entities have been spawned with a component you can target them with, they will automatically be given an `RngSeed` component (which stores the initial seed value) and an `EntropyComponent`. + +Additionally, you can link entities to draw their seeds from other source entities instead of the global resources. So one `Source` entity can then seed many `Target` entities, and whenever the `Source` entity is updated with a new seed value, it then automatically pushes new seeds to its linked targets. Note: this is NOT a `bevy_hierarchy` relationship, and while the `Source` will have "child" entities, removing/despawning the source entity will *not* despawn the children entities. They will simply no longer have a valid "link". A new link can be established by triggering another "link" event. + +```rust ignore +use bevy_ecs::prelude::*; +use bevy_prng::WyRand; +use bevy_rand::observers::{LinkRngSourceToTarget, SeedFromGlobal}; + +#[derive(Component)] +struct Source; + +#[derive(Component, Clone, Copy)] +struct Target; + +fn initial_setup(mut commands: Commands) { + // Create the source entity and get the Entity id. + let source = commands.spawn(Source).id(); + + // Spawn many target entities + commands.spawn_batch(vec![Target; 5]); + + // Link the target entities to the Source entity + commands.trigger(LinkRngSourceToTarget::::default()); + + // Initialise the Source entity to be an RNG source and then seed all its + // linked entities. + commands.trigger_targets(SeedFromGlobal::::default(), source); +} +``` + +Once the link has been created, child entities can also pull a new seed from its parent source. So if you want to reseed *one* entity from its parent source, but not all of the entities that have the same source, you can use the `SeedFromParent` observer event to achieve this. + +```rust ignore +use bevy_ecs::prelude::*; +use bevy_prng::WyRand; +use bevy_rand::observers::SeedFromParent; + +#[derive(Component)] +struct Source; + +#[derive(Component, Clone, Copy)] +struct Target; + +fn pull_seed_from_parent(mut commands: Commands, mut q_targets: Query>) { + for target in &q_targets { + commands.trigger_targets(SeedFromParent::::default(), target); + } +} +``` diff --git a/tutorial/04-tips-patterns.md b/tutorial/04-tips-patterns.md deleted file mode 100644 index 2c317e2..0000000 --- a/tutorial/04-tips-patterns.md +++ /dev/null @@ -1,3 +0,0 @@ -# Tips and Patterns - -To be added.