Crate bevy_turborand

source ·
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.

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();

    println!("Player attacked for {} damage!", rng.u32(10..=20));
}

fn main() {
    App::new()
        .add_plugin(RngPlugin::default())
        .add_startup_system(setup_player)
        .add_system(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

Any TurboRand method that relies on usize will not exhibit the same result on 64-bit systems and 32-bit systems. The TurboRand 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.

Methods that are susceptible to this are TurboRand::usize, TurboRand::sample, TurboRand::sample_multiple, TurboRand::weighted_sample and TurboRand::shuffle.

Features

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

A ChaChaRng component that wraps a random number generator, specifically the ChaChaRng struct, which provides a cryptographically secure source based on ChaCha8.
A Global ChaChaRng instance, meant for use as a Resource. Gets created automatically with RngPlugin, or can be created and added manually.
GlobalRngwyrand
A Global Rng instance, meant for use as a Resource. Gets created automatically with RngPlugin, or can be created and added manually.
A Rng component that wraps a random number generator, specifically the Rng struct, which provides a fast, but not cryptographically secure source based on WyRand.
RngPluginwyrand or chacha
A [Plugin] for initialising a GlobalRng & GlobalChaChaRng (if the feature flags are enabled for either of them) into a Bevy App.

Traits

A trait for applying to [Component]s and Resources that wrap a TurboCore RNG source.
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 be Sized 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 implements TurboCore & GenCore. All methods are provided as default implementations that build on top of TurboCore and GenCore, and thus are not recommended to be overridden, lest you potentially change the expected outcome of the methods.