Skip to content

Provide bevy_entropy crate for RNG sources within ecs #7871

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 28 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
b026fcd
initial bevy_entropy implementation
Bluefinger Mar 2, 2023
af2acc3
Add cargo.toml info and license
Bluefinger Mar 2, 2023
56a1153
Add plugin doctest as initial smoke test
Bluefinger Mar 2, 2023
d9a5d3a
Apply cargo fmt
Bluefinger Mar 2, 2023
4c02a47
Add missing features doc
Bluefinger Mar 2, 2023
38177e2
Add missing semi-colon
Bluefinger Mar 2, 2023
25d0da5
fix feature enablement and organise code better
Bluefinger Mar 3, 2023
608dec1
provide examples on deterministic RNG output
Bluefinger Mar 4, 2023
b10205c
reorganise trait soup into wrapper traits
Bluefinger Mar 6, 2023
bc97d7a
first-pass on splitting features,
Bluefinger Mar 7, 2023
058b540
fix dependency versions post rebase
Bluefinger Mar 7, 2023
3dded38
cargo fmt fix
Bluefinger Mar 7, 2023
e221dea
make use of new system config features in examples
Bluefinger Mar 7, 2023
0ce4535
enforce lifetime tracking on thread local mut rng ref
Bluefinger Mar 7, 2023
1efa1a8
fix from implementation with Mut reference
Bluefinger Mar 7, 2023
68c0fb7
fix from implementation, add forking test
Bluefinger Mar 7, 2023
683db77
first-pass on documentation
Bluefinger Mar 8, 2023
d797a32
Document registering a PRNG
Bluefinger Mar 8, 2023
541c496
add reflection/serialization tests
Bluefinger Mar 9, 2023
ebeec67
cargo fmt the reflection tests
Bluefinger Mar 9, 2023
da038e9
revise safety of internal get_rng method for ThreadLocalEntropy
Bluefinger Mar 9, 2023
06165fe
Ensure unique initialisation per generic plugin
Bluefinger Mar 10, 2023
60ac6dd
use rfc text for overview on bevy_entropy
Bluefinger Mar 13, 2023
c1a4bb4
Note about PRNG algorithms and how to select them
Bluefinger Mar 16, 2023
526f486
update examples to use new add_systems API
Bluefinger Mar 27, 2023
2cd98eb
Fix markdown lint error
Bluefinger Mar 27, 2023
700ef36
update doc examples with add_systems
Bluefinger Mar 27, 2023
80bb082
More reflection derives, converge to single trait
Bluefinger Apr 13, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ default = [
"animation",
"bevy_asset",
"bevy_audio",
"bevy_entropy",
"bevy_gilrs",
"bevy_scene",
"bevy_winit",
Expand Down Expand Up @@ -69,6 +70,9 @@ bevy_audio = ["bevy_internal/bevy_audio"]
# Provides cameras and other basic render pipeline features
bevy_core_pipeline = ["bevy_internal/bevy_core_pipeline", "bevy_asset", "bevy_render"]

# Plugin for providing PRNG integration into bevy
bevy_entropy = ["bevy_internal/bevy_entropy"]

# Plugin for dynamic loading (using [libloading](https://crates.io/crates/libloading))
bevy_dynamic_plugin = ["bevy_internal/bevy_dynamic_plugin"]

Expand Down
41 changes: 41 additions & 0 deletions crates/bevy_entropy/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
[package]
name = "bevy_entropy"
version = "0.11.0-dev"
edition = "2021"
description = "Bevy Engine's RNG integration"
homepage = "https://bevyengine.org"
repository = "https://github.com/bevyengine/bevy"
license = "MIT OR Apache-2.0"
keywords = ["game", "bevy", "rand", "rng"]
categories = ["game-engines", "algorithms"]

[features]
bevy_reflect = ["dep:bevy_reflect", "bevy_app/bevy_reflect", "serialize"]
default = ["bevy_reflect"]
serialize = ["dep:serde", "rand_core/serde1", "rand_chacha/serde1"]

[dependencies]
# bevy
bevy_app = { path = "../bevy_app", version = "0.11.0-dev", default-features = false }
bevy_ecs = { path = "../bevy_ecs", version = "0.11.0-dev", default-features = false }
bevy_reflect = { path = "../bevy_reflect", version = "0.11.0-dev", optional = true }

# others
serde = { version = "1.0", features = ["derive"], optional = true }
rand_core = { version = "0.6", features = ["std"] }
rand_chacha = "0.3"

[dev-dependencies]
rand = "0.8"
ron = { version = "0.8.0", features = ["integer128"] }

[target.'cfg(all(any(target_arch = "wasm32", target_arch = "wasm64"), target_os = "unknown"))'.dependencies]
getrandom = { version = "0.2", features = ["js"] }

[[example]]
name = "determinism"
path = "examples/determinism.rs"

[[example]]
name = "parallelism"
path = "examples/parallelism.rs"
161 changes: 161 additions & 0 deletions crates/bevy_entropy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# Bevy Entropy

[![Crates.io](https://img.shields.io/crates/v/bevy_entropy.svg)](https://crates.io/crates/bevy_entropy)
[![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/bevyengine/bevy/blob/HEAD/LICENSE)
[![Discord](https://img.shields.io/discord/691052431525675048.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/bevy)

## What is Bevy Entropy?

Bevy Entropy is a plugin to provide integration of `rand` ecosystem PRNGs in an ECS friendly way. It provides a set of wrapper component and resource types that allow for safe access to a PRNG for generating random numbers, giving features like reflection, serialization for free. And with these types, it becomes possible to have determinism with the usage of these integrated PRNGs in ways that work with multi-threading and also avoid pitfalls such as unstable query iteration order.

## Prerequisites

For a PRNG crate to be usable with Bevy Entropy, at its minimum, it must implement `RngCore` and `SeedableRng` traits from `rand_core`, as well as `PartialEq`, `Clone`, and `Debug` traits. For reflection/serialization support, it should also implement `Serialize`/`Deserialize` traits from `serde`, though this can be disabled if one is not making use of reflection/serialization. As long as these traits are implemented, the PRNG can just be plugged in without an issue.

## Overview

Games often use randomness as a core mechanic. For example, card games generate a random deck for each game and killing monsters in an RPG often rewards players with a random item. While randomness makes games more interesting and increases replayability, it also makes games harder to test and prevents advanced techniques such as [deterministic lockstep](https://gafferongames.com/post/deterministic_lockstep/).

Let's pretend you are creating a poker game where a human player can play against the computer. The computer's poker logic is very simple: when the computer has a good hand, it bets all of its money. To make sure the behavior works, you write a test to first check the computer's hand and if it is good confirm that all its money is bet. If the test passes does it ensure the computer behaves as intended? Sadly, no.

Because the deck is randomly shuffled for each game (without doing so the player would already know the card order from the previous game), it is not guaranteed that the computer player gets a good hand and thus the betting logic goes unchecked.
While there are ways around this (a fake deck that is not shuffled, running the test many times to increase confidence, breaking the logic into units and testing those) it would be very helpful to have randomness as well as a way to make it _less_ random.

Luckily, when a computer needs a random number it doesn't use real randomness and instead uses a [pseudorandom number generator](https://en.wikipedia.org/wiki/Pseudorandom_number_generator). Popular Rust libraries containing pseudorandom number generators are [`rand`](https://crates.io/crates/rand) and [`fastrand`](https://crates.io/crates/fastrand).

Pseudorandom number generators require a source of [entropy](https://en.wikipedia.org/wiki/Entropy) called a [random seed](https://en.wikipedia.org/wiki/Random_seed). The random seed is used as input to generate numbers that _appear_ random but are instead in a specific and deterministic order. For the same random seed, a pseudorandom number generator always returns the same numbers in the same order.

For example, let's say you seed a pseudorandom number generator with `1234`.
You then ask for a random number between `10` and `99` and the pseudorandom number generator returns `12`.
If you run the program again with the same seed (`1234`) and ask for another random number between `1` and `99`, you will again get `12`.
If you then change the seed to `4567` and run the program, more than likely the result will not be `12` and will instead be a different number.
If you run the program again with the `4567` seed, you should see the same number from the previous `4567`-seeded run.

There are many types of pseudorandom number generators each with their own strengths and weaknesses. Because of this, Bevy does not include a pseudorandom number generator. Instead, the `bevy_entropy` plugin includes a source of entropy to use as a random seed for your chosen pseudorandom number generator.

Note that Bevy currently has [other sources of non-determinism](https://github.com/bevyengine/bevy/discussions/2480) unrelated to pseudorandom number generators.

## Concepts

Bevy Entropy operates around a global entropy source provided as a resource, and then entropy components that can then be attached to entities. The use of resources/components allow the ECS to schedule systems appropriately so to make it easier to achieve determinism.

### GlobalEntropy

`GlobalEntropy` is the main resource for providing a global entropy source. It can only be accessed via a `ResMut` if looking to generate random numbers from it, as `RngCore` only exposes `&mut self` methods. As a result, working with `ResMut<GlobalEntropy<T>>` means any systems that access it will not be able to run in parallel to each other, as the `mut` access requires the scheduler to ensure that only one system at a time is accessing it. Therefore, if one intends on parallelising RNG workloads, limiting use/access of `GlobalEntropy` is vital. However, if one intends on having a single seed to deterministic control/derive many RNGs, `GlobalEntropy` is the best source for this purpose.

### EntropyComponent

`EntropyComponent` is a wrapper component that allows for entities to have their own RNG source. In order to generate random numbers from it, the `EntropyComponent` must be accessed with a `&mut` reference. Doing so will limit systems accessing the same source, but to increase parallelism, one can create many different sources instead. For ensuring determinism, query iteration must be accounted for as well as it isn't stable. Therefore, entities that need to perform some randomised task should 'own' their own `EntropyComponent`.

`EntropyComponent` can be seeded directly, or be created from a `GlobalEntropy` source or other `EntropyComponent`s.

### Forking

If cloning creates a second instance that shares the same state as the original, forking derives a new state from the original, leaving the original 'changed' and the new instance with a randomised seed. Forking RNG instances from a global source is a way to ensure that one seed produces many deterministic states, while making it difficult to predict outputs from many sources and also ensuring no one source shares the same state either with the original or with each other.

Bevy Entropy approaches forking via `From` implementations of the various component/resource types, making it straightforward to use.

## Using Bevy Entropy

Usage of Bevy Entropy can range from very simple to quite complex use-cases, all depending on whether one cares about deterministic output or not.

### Registering a PRNG for use with Bevy Entropy

Before a PRNG can be used via `GlobalEntropy` or `EntropyComponent`, it must be registered via the plugin.

```rust
use bevy_app::App;
use bevy_entropy::prelude::*;
use rand_core::RngCore;
use rand_chacha::ChaCha8Rng;

fn main() {
App::new()
.add_plugin(EntropyPlugin::<ChaCha8Rng>::default())
.run();
}
```

### Basic Usage

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_ecs::prelude::ResMut;
use bevy_entropy::prelude::*;
use rand_core::RngCore;
use rand_chacha::ChaCha8Rng;

fn print_random_value(mut rng: ResMut<GlobalEntropy<ChaCha8Rng>>) {
println!("Random value: {}", rng.next_u32());
}
```

### Forking RNGs

For seeding `EntropyComponent`s from a global source, it is best to make use of forking instead of generating the seed value directly.

```rust
use bevy_ecs::{
prelude::{Component, ResMut},
system::Commands,
};
use bevy_entropy::prelude::*;
use rand_chacha::ChaCha8Rng;

#[derive(Component)]
struct Source;

fn setup_source(mut commands: Commands, mut global: ResMut<GlobalEntropy<ChaCha8Rng>>) {
commands
.spawn((
Source,
EntropyComponent::from(&mut global),
));
}
```

`EntropyComponent`s can be seeded/forked from other `EntropyComponent`s as well.

```rust
use bevy_ecs::{
prelude::{Component, Query, With, Without},
system::Commands,
};
use bevy_entropy::prelude::*;
use rand_chacha::ChaCha8Rng;

#[derive(Component)]
struct Npc;

#[derive(Component)]
struct Source;

fn setup_npc_from_source(
mut commands: Commands,
mut q_source: Query<&mut EntropyComponent<ChaCha8Rng>, (With<Source>, Without<Npc>)>,
) {
let mut source = q_source.single_mut();
for _ in 0..2 {
commands
.spawn((
Npc,
EntropyComponent::from(&mut source)
));
}
}
```

### Enabling Determinism

Determinism relies on not just how RNGs are seeded, but also how systems are grouped and ordered relative to each other. Systems accessing the same source/entities will run serially to each other, but if you can separate entities into different groups that do not overlap with each other, systems can then run in parallel as well. Overall, care must be taken with regards to system ordering and scheduling, as well as unstable query iteration meaning the order of entities a query iterates through is not the same per run. This can affect the outcome/state of the PRNGs, producing different results.

The examples provided in this repo demonstrate the two different concepts of parallelisation and deterministic outputs, so check them out to see how one might achieve determinism.

### Selecting PRNG algorithms

`rand` provides a number of PRNGs, but under types such as `StdRng` and `SmallRng`. These are **not** intended to be stable/deterministic across different versions of `rand`. `rand` might change the underlying implementations of `StdRng` and `SmallRng` at any point, yielding different output. If the lack of stability is fine, then plugging these into `bevy_entropy` is fine. Else, the recommendation (made by `rand` crate as well) is that if determinism of output and stability of the algorithm used is important, then to use the algorithm crates directly. So instead of using `StdRng`, use `ChaCha12Rng` from the `rand_chacha` crate.

As a whole, which algorithm should be used/selected is dependent on a range of factors. Cryptographically Secure PRNGs (CSPRNGs) produce very hard to predict output (very high quality entropy), but in general are slow. The ChaCha algorithm can be sped up by using versions with less rounds (iterations of the algorithm), but this in turn reduces the quality of the output (making it easier to predict). However, `ChaCha8Rng` is still far stronger than what is feasible to be attacked, and is considerably faster as a source of entropy than the full `ChaCha20Rng`. `rand` uses `ChaCha12Rng` as a balance between security/quality of output and speed for its `StdRng`. CSPRNGs are important for cases when you _really_ don't want your output to be predictable and you need that extra level of assurance, such as doing any cryptography/authentication/security tasks.

If that extra level of security is not necessary, but there is still need for extra speed while maintaining good enough randomness, other PRNG algorithms exist for this purpose. These algorithms still try to output as high quality entropy as possible, but the level of entropy is not enough for cryptographic purposes. These algorithms should **never be used in situations that demand security**. Algorithms like `WyRand` and `Xoshiro256++` are tuned for maximum throughput, while still possessing _good enough_ entropy for use as a source of randomness for non-security purposes. It still matters that the output is not predictable, but not to the same extent as CSPRNGs are required to be.
Loading