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
RngComponent
s are created using GlobalRng
. RngComponent
can also
be used to seed other RngComponent
s.
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();
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 RngComponent
s while still
having a deterministic app. RngComponent
s derived from a GlobalRng
can also then seed other RngComponent
s 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 RngComponent
s
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 implementsRngCore
so to allow for compatibility withrand
ecosystem of crates.serialize
- EnablesSerialize
andDeserialize
derives.
Modules§
- 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. - Module for dealing directly with
turborand
and its features.
Structs§
- ChaCha
RngComponent chacha
- Global
ChaCha Rng chacha
A GlobalChaChaRng
instance, meant for use as a Resource. Gets created automatically with [RngPlugin
], or can be created and added manually. - Global
Rng wyrand
A GlobalRng
instance, meant for use as a Resource. Gets created automatically with [RngPlugin
], or can be created and added manually. - RngComponent
wyrand
Traits§
- Trait for enabling creating new
TurboCore
instances 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. - 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. - A marker trait to be applied to anything that implements
TurboCore
in order to indicate that a PRNG source is cryptographically secure, so being a CSPRNG. - Trait for implementing Seedable PRNGs, requiring that the PRNG implements
TurboCore
as a baseline. Seeds must beSized
in order to be used as the internal state of a PRNG. - 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. - Extension trait for automatically implementing all
TurboRand
methods, as long as the struct implementsTurboCore
&GenCore
. All methods are provided as default implementations that build on top ofTurboCore
andGenCore
, and thus are not recommended to be overridden, lest you potentially change the expected outcome of the methods.