
# Objective There are two problems this aims to solve. First, `Entity::index` is currently a `u32`. That means there are `u32::MAX + 1` possible entities. Not only is that awkward, but it also make `Entity` allocation more difficult. I discovered this while working on remote entity reservation, but even on main, `Entities` doesn't handle the `u32::MAX + 1` entity very well. It can not be batch reserved because that iterator uses exclusive ranges, which has a maximum upper bound of `u32::MAX - 1`. In other words, having `u32::MAX` as a valid index can be thought of as a bug right now. We either need to make that invalid (this PR), which makes Entity allocation cleaner and makes remote reservation easier (because the length only needs to be u32 instead of u64, which, in atomics is a big deal), or we need to take another pass at `Entities` to make it handle the `u32::MAX` index properly. Second, `TableRow`, `ArchetypeRow` and `EntityIndex` (a type alias for u32) all have `u32` as the underlying type. That means using these as the index type in a `SparseSet` uses 64 bits for the sparse list because it stores `Option<IndexType>`. By using `NonMaxU32` here, we cut the memory of that list in half. To my knowledge, `EntityIndex` is the only thing that would really benefit from this niche. `TableRow` and `ArchetypeRow` I think are not stored in an `Option` in bulk. But if they ever are, this would help. Additionally this ensures `TableRow::INVALID` and `ArchetypeRow::INVALID` never conflict with an actual row, which in a nice bonus. As a related note, if we do components as entities where `ComponentId` becomes `Entity`, the the `SparseSet<ComponentId>` will see a similar memory improvement too. ## Solution Create a new type `EntityRow` that wraps `NonMaxU32`, similar to `TableRow` and `ArchetypeRow`. Change `Entity::index` to this type. ## Downsides `NonMax` is implemented as a `NonZero` with a binary inversion. That means accessing and storing the value takes one more instruction. I don't think that's a big deal, but it's worth mentioning. As a consequence, `to_bits` uses `transmute` to skip the inversion which keeps it a nop. But that also means that ordering has now flipped. In other words, higher indices are considered less than lower indices. I don't think that's a problem, but it's also worth mentioning. ## Alternatives We could keep the index as a u32 type and just document that `u32::MAX` is invalid, modifying `Entities` to ensure it never gets handed out. (But that's not enforced by the type system.) We could still take advantage of the niche here in `ComponentSparseSet`. We'd just need some unsafe manual conversions, which is probably fine, but opens up the possibility for correctness problems later. We could change `Entities` to fully support the `u32::MAX` index. (But that makes `Entities` more complex and potentially slightly slower.) ## Testing - CI - A few tests were changed because they depend on different ordering and `to_bits` values. ## Future Work - It might be worth removing the niche on `Entity::generation` since there is now a different niche. - We could move `Entity::generation` into it's own type too for clarity. - We should change `ComponentSparseSet` to take advantage of the new niche. (This PR doesn't change that yet.) - Consider removing or updating `Identifier`. This is only used for `Entity`, so it might be worth combining since `Entity` is now more unique. --------- Co-authored-by: atlv <email@atlasdostal.com> Co-authored-by: Zachary Harrold <zac@harrold.com.au>
193 lines
6.2 KiB
Rust
193 lines
6.2 KiB
Rust
//! Showcases how fallible systems and observers can make use of Rust's powerful result handling
|
|
//! syntax.
|
|
//!
|
|
//! Important note: to set the global error handler, the `configurable_error_handler` feature must be
|
|
//! enabled. This feature is disabled by default, as it may introduce runtime overhead, especially for commands.
|
|
|
|
use bevy::ecs::{
|
|
error::{warn, GLOBAL_ERROR_HANDLER},
|
|
world::DeferredWorld,
|
|
};
|
|
use bevy::math::sampling::UniformMeshSampler;
|
|
use bevy::prelude::*;
|
|
|
|
use rand::distributions::Distribution;
|
|
use rand::SeedableRng;
|
|
use rand_chacha::ChaCha8Rng;
|
|
|
|
fn main() {
|
|
// By default, fallible systems that return an error will panic.
|
|
//
|
|
// We can change this by setting a custom error handler, which applies globally.
|
|
// Here we set the global error handler using one of the built-in
|
|
// error handlers. Bevy provides built-in handlers for `panic`, `error`, `warn`, `info`,
|
|
// `debug`, `trace` and `ignore`.
|
|
GLOBAL_ERROR_HANDLER
|
|
.set(warn)
|
|
.expect("The error handler can only be set once, globally.");
|
|
|
|
let mut app = App::new();
|
|
|
|
app.add_plugins(DefaultPlugins);
|
|
|
|
#[cfg(feature = "bevy_mesh_picking_backend")]
|
|
app.add_plugins(MeshPickingPlugin);
|
|
|
|
// Fallible systems can be used the same way as regular systems. The only difference is they
|
|
// return a `Result<(), BevyError>` instead of a `()` (unit) type. Bevy will handle both
|
|
// types of systems the same way, except for the error handling.
|
|
app.add_systems(Startup, setup);
|
|
|
|
// Commands can also return `Result`s, which are automatically handled by the global error handler
|
|
// if not explicitly handled by the user.
|
|
app.add_systems(Startup, failing_commands);
|
|
|
|
// Individual systems can also be handled by piping the output result:
|
|
app.add_systems(
|
|
PostStartup,
|
|
failing_system.pipe(|result: In<Result>| {
|
|
let _ = result.0.inspect_err(|err| info!("captured error: {err}"));
|
|
}),
|
|
);
|
|
|
|
// Fallible observers are also supported.
|
|
app.add_observer(fallible_observer);
|
|
|
|
// If we run the app, we'll see the following output at startup:
|
|
//
|
|
// WARN Encountered an error in system `fallible_systems::failing_system`: Resource not initialized
|
|
// ERROR fallible_systems::failing_system failed: Resource not initialized
|
|
// INFO captured error: Resource not initialized
|
|
app.run();
|
|
}
|
|
|
|
/// An example of a system that calls several fallible functions with the question mark operator.
|
|
///
|
|
/// See: <https://doc.rust-lang.org/reference/expressions/operator-expr.html#the-question-mark-operator>
|
|
fn setup(
|
|
mut commands: Commands,
|
|
mut meshes: ResMut<Assets<Mesh>>,
|
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
|
) -> Result {
|
|
let mut seeded_rng = ChaCha8Rng::seed_from_u64(19878367467712);
|
|
|
|
// Make a plane for establishing space.
|
|
commands.spawn((
|
|
Mesh3d(meshes.add(Plane3d::default().mesh().size(12.0, 12.0))),
|
|
MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))),
|
|
Transform::from_xyz(0.0, -2.5, 0.0),
|
|
));
|
|
|
|
// Spawn a light:
|
|
commands.spawn((
|
|
PointLight {
|
|
shadows_enabled: true,
|
|
..default()
|
|
},
|
|
Transform::from_xyz(4.0, 8.0, 4.0),
|
|
));
|
|
|
|
// Spawn a camera:
|
|
commands.spawn((
|
|
Camera3d::default(),
|
|
Transform::from_xyz(-2.0, 3.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
|
|
));
|
|
|
|
// Create a new sphere mesh:
|
|
let mut sphere_mesh = Sphere::new(1.0).mesh().ico(7)?;
|
|
sphere_mesh.generate_tangents()?;
|
|
|
|
// Spawn the mesh into the scene:
|
|
let mut sphere = commands.spawn((
|
|
Mesh3d(meshes.add(sphere_mesh.clone())),
|
|
MeshMaterial3d(materials.add(StandardMaterial::default())),
|
|
Transform::from_xyz(-1.0, 1.0, 0.0),
|
|
));
|
|
|
|
// Generate random sample points:
|
|
let triangles = sphere_mesh.triangles()?;
|
|
let distribution = UniformMeshSampler::try_new(triangles)?;
|
|
|
|
// Setup sample points:
|
|
let point_mesh = meshes.add(Sphere::new(0.01).mesh().ico(3)?);
|
|
let point_material = materials.add(StandardMaterial {
|
|
base_color: Srgba::RED.into(),
|
|
emissive: LinearRgba::rgb(1.0, 0.0, 0.0),
|
|
..default()
|
|
});
|
|
|
|
// Add sample points as children of the sphere:
|
|
for point in distribution.sample_iter(&mut seeded_rng).take(10000) {
|
|
sphere.with_child((
|
|
Mesh3d(point_mesh.clone()),
|
|
MeshMaterial3d(point_material.clone()),
|
|
Transform::from_translation(point),
|
|
));
|
|
}
|
|
|
|
// Indicate the system completed successfully:
|
|
Ok(())
|
|
}
|
|
|
|
// Observer systems can also return a `Result`.
|
|
fn fallible_observer(
|
|
trigger: Trigger<Pointer<Move>>,
|
|
mut world: DeferredWorld,
|
|
mut step: Local<f32>,
|
|
) -> Result {
|
|
let mut transform = world
|
|
.get_mut::<Transform>(trigger.target)
|
|
.ok_or("No transform found.")?;
|
|
|
|
*step = if transform.translation.x > 3. {
|
|
-0.1
|
|
} else if transform.translation.x < -3. || *step == 0. {
|
|
0.1
|
|
} else {
|
|
*step
|
|
};
|
|
|
|
transform.translation.x += *step;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[derive(Resource)]
|
|
struct UninitializedResource;
|
|
|
|
fn failing_system(world: &mut World) -> Result {
|
|
world
|
|
// `get_resource` returns an `Option<T>`, so we use `ok_or` to convert it to a `Result` on
|
|
// which we can call `?` to propagate the error.
|
|
.get_resource::<UninitializedResource>()
|
|
// We can provide a `str` here because `BevyError` implements `From<&str>`.
|
|
.ok_or("Resource not initialized")?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn failing_commands(mut commands: Commands) {
|
|
commands
|
|
// This entity doesn't exist!
|
|
.entity(Entity::from_raw_u32(12345678).unwrap())
|
|
// Normally, this failed command would panic,
|
|
// but since we've set the global error handler to `warn`
|
|
// it will log a warning instead.
|
|
.insert(Transform::default());
|
|
|
|
// The error handlers for commands can be set individually as well,
|
|
// by using the queue_handled method.
|
|
commands.queue_handled(
|
|
|world: &mut World| -> Result {
|
|
world
|
|
.get_resource::<UninitializedResource>()
|
|
.ok_or("Resource not initialized when accessed in a command")?;
|
|
|
|
Ok(())
|
|
},
|
|
|error, context| {
|
|
error!("{error}, {context}");
|
|
},
|
|
);
|
|
}
|