Expand description
A plugin to enable ECS optimised random number generation for the Bevy game engine.
The plugin makes use of turborand,
on which the implementation uses Wyrand,
a simple and fast generator but not cryptographically secure, as well as
ChaCha8, a cryptographically secure generator tuned
to 8 rounds of the ChaCha algorithm, for the purpose of increasing throughput at the
expense of slightly less security (though plenty secure enough for cryptographic purposes).
This plugin exposes GlobalRng & GlobalChaChaRng for use as a Resource,
as well as RngComponent & ChaChaRngComponent for providing rng instances
at a per-entity level. By exposing random number generation as a component allows
for better parallelisation of systems making use of PRNG, as well as making it
easier to enable determinism in an otherwise multi-threaded engine.
Relying on a single Rng instance for the entire application is not
conducive to multi-threading, and imposes far too strict ordering
requirements in order to ensure that each time the Rng is called and its
internal state is modified, that it is done so in a deterministic manner.
By splitting one instance into components, each RngComponent only is
responsible for the entity it is applied to. It also prevents other
actions in the game from affecting the outcome of unrelated entities.
Also, Bevy’s queries are not stable in their ordering, so each time
a system runs, the order by which a query iterates through selected
entities will be different. By providing an instance of an Rng per
entity, it then makes the question of stable ordering in queries moot.
Thus, determinism can be achieved regardless of unstable query ordering
and multi-threaded execution.
§Notice
For all intents and purposes, bevy_turborand will no longer receive new features
or work, and is mostly on maintenance only mode. I will keep this crate up-to-date
with bevy releases, but otherwise all new work and efforts is currently directed towards
bevy_rand. Folks who wish to add more
capability to this crate are free to submit PRs.
§Usage
For both global and component RNGs, both must be accessed with a
mut reference in order to ensure each system using the TurboCore instances
are not run in parallel over the entities being accessed. This should
also allow for better diagnostics with Bevy’s ambiguous ordering tool
for finding systems that should be more explicitly ordered.
On its own, the TurboCore is not threadsafe unless it is accessed via a mut
reference. By doing so, the RNG can be even more performant and not
have to rely on atomics (which impose considerable overhead).
After requesting GlobalRng resource or the RngComponent, one can use
delegated methods directly to get random numbers that way, or call .get_mut()
in order to get the TurboCore instance itself. From there, all TurboRand methods in
turborand are available to be
used, though most are available as delegated methods in GlobalRng and
RngComponent. The same applies to GlobalChaChaRng and
ChaChaRngComponent.
GlobalRng is provided as a means to seed RngComponent with randomised
states, and should not be used as a direct source of entropy for
systems in general. All systems that access the GlobalRng cannot be
parallelised easily, so RngComponent should be used instead as much as
possible. On the plus side with GlobalRng, only one seed needs to be
provided in order to have all instances be deterministic, as long as all
RngComponents are created using GlobalRng. RngComponent can also
be used to seed other RngComponents.
Note: GlobalChaChaRng & ChaChaRngComponent can seed both ChaChaRngComponent
and RngComponent. However, GlobalRng & RngComponent cannot be used
to seed ChaChaRngComponent as they don’t implement SecureCore.
You can only seed from high quality to same quality entropy sources, but never from
worse quality entropy sources.
§Example
Basic example of setting up and using the Rng.
use bevy::prelude::*;
use bevy_turborand::prelude::*;
#[derive(Debug, Component)]
struct Player;
fn setup_player(mut commands: Commands, mut global_rng: ResMut<GlobalRng>) {
commands.spawn((
Player,
RngComponent::from(&mut global_rng),
));
}
fn do_damage(mut q_player: Query<&mut RngComponent, With<Player>>) {
let mut rng = q_player.single_mut().unwrap();
println!("Player attacked for {} damage!", rng.u32(10..=20));
}
fn main() {
App::new()
.add_plugins(RngPlugin::default())
.add_systems(Startup, setup_player)
.add_systems(Update, do_damage)
.run();
}§How to enable Determinism
In order to obtain determinism for your game/app, the TurboRand sources must be
seeded. GlobalRng and [RngPlugin] can given a seed which then sets the
internal PRNG to behave deterministically. Instead of having to seed every
RngComponent manually, as long as the GlobalRng is seeded, then
RngComponent can be created directly from the global instance, cloning
the internal Rng to itself, which gives it a random but deterministic seed.
This allows for better randomised states among RngComponents while still
having a deterministic app. RngComponents derived from a GlobalRng
can also then seed other RngComponents in a deterministic manner.
Systems also must be ordered correctly for determinism to occur. Systems
however do not need to be strictly ordered against every one as if some
linear path. Only related systems that access a given set of RngComponents
need to be ordered. Ones that are unrelated can run in parallel and still
yield a deterministic result. So systems selecting a Player entity with
a RngComponent should all be ordered against each other, but systems
selecting an Item entity with an RngComponent that never interacts with
Player don’t need to be ordered with Player systems, only between
themselves.
To see an example of this, view the project’s tests to see how to make use of determinism for testing random systems.
§Caveats about Determinism
Usage of the TurboRand method TurboRand::usize will not exhibit the same result
on 64-bit systems and 32-bit systems. The method output will be different
on those platforms, though it will be deterministically different. This is
because the output of the RNG source for usize on 32-bit platforms
is u32 and thus is truncating the full output from the generator.
As such, it will not be the same value between 32-bit and 64-bit platforms.
For ensuring stable results between 32-bit and 64-bit platforms, use the TurboRand::index
method instead. All sampling/shuffing methods use this method internally to ensure
stable results. Do note, TurboRand optimises cases for 64-bit platforms,
as these are much more common for general and game applications.
§Features
wyrand- EnablesGlobalRng&RngComponent. Is enabled by default. Having this feature flag enabled also enables [RngPlugin].chacha- EnablesGlobalChaChaRng&ChaChaRngComponent. Having this feature flag enabled also enables [RngPlugin].rand- Provides [RandBorrowed], which implementsRngCoreso to allow for compatibility withrandecosystem of crates.serialize- EnablesSerializeandDeserializederives.
Modules§
- prelude
- Prelude for
bevy_turborand, exposing all necessary traits for default usage of the crate, as well as whatever component/resources are configured to be exposed by whichever features are enabled. - rng
- Module for dealing directly with
turborandand its features.
Structs§
- ChaCha
RngComponent chacha - A
ChaChaRngcomponent that wraps a random number generator, specifically theChaChaRngstruct, which provides a cryptographically secure source based on ChaCha8. - Global
ChaCha Rng chacha - A Global
ChaChaRnginstance, meant for use as a Resource. Gets created automatically with [RngPlugin], or can be created and added manually. - Global
Rng wyrand - A Global
Rnginstance, meant for use as a Resource. Gets created automatically with [RngPlugin], or can be created and added manually. - RngComponent
wyrand - A
Rngcomponent that wraps a random number generator, specifically theRngstruct, which provides a fast, but not cryptographically secure source based on WyRand.
Traits§
- Delegated
Rng - A trait for applying to
Components and Resources that wrap aTurboCoreRNG source. - Forkable
Core - Trait for enabling creating new
TurboCoreinstances from an original instance. Similar to cloning, except forking modifies the state of the original instance in order to provide a new, random state for the forked instance. This allows for creating many randomised instances from a single seed in a deterministic manner. - GenCore
- This trait provides the means to easily generate all integer types, provided
the main method underpinning this is implemented:
GenCore::gen. Once implemented, the rest of the trait provides default implementations for generating all integer types, though it is not recommended to override these. - Secure
Core - A marker trait to be applied to anything that implements
TurboCorein order to indicate that a PRNG source is cryptographically secure, so being a CSPRNG. - Seeded
Core - Trait for implementing Seedable PRNGs, requiring that the PRNG
implements
TurboCoreas a baseline. Seeds must beSizedin order to be used as the internal state of a PRNG. - Turbo
Core - Base trait for implementing a PRNG. Only one method must be
implemented:
TurboCore::fill_bytes, which provides the basis for any PRNG, to fill a buffer of bytes with random data. - Turbo
Rand - Extension trait for automatically implementing all
TurboRandmethods, as long as the struct implementsTurboCore&GenCore. All methods are provided as default implementations that build on top ofTurboCoreandGenCore, and thus are not recommended to be overridden, lest you potentially change the expected outcome of the methods.