Plugin Refactors (#45)

Refactors plugins to make usage more flexible. Originally intended to
allow for running in the fixed update schedule, but decided against this
in favor of making plugins more granular, and realizing running in fixed
update wouldn't actually be desirable.

---------

Co-authored-by: Zachary Harrold <zac@harrold.com.au>
This commit is contained in:
Aevyrie 2025-05-14 21:10:58 -07:00 committed by GitHub
parent 44ff1f32de
commit 5345af11d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 668 additions and 493 deletions

View File

@ -2,6 +2,58 @@
## UNRELEASED
### Updated: Bevy 0.16
Due to changes in bevy, this plugin once again requires you to disable bevy's built-in transform system:
```rs
DefaultPlugins.build().disable::<TransformPlugin>(),
```
### Changed: `BigSpaceDefaultPlugins` plugin group
Instead of adding common plugins individually, they have been grouped into the `BigSpaceDefaultPlugins` plugin group, similar to the `DefaultPlugins` plugin group in Bevy.
For example, the hierarchy validation plugin is enabled whenever debug assertions are enabled but can be manually enabled or disabled to override this behavior:
```rs
BigSpaceDefaultPlugins
.build()
.enable::<BigSpaceValidationPlugin>()
```
Plugins that are behind feature flags are automatically enabled when their corresponding feature is enabled. For example, you no longer need to manually add the camera controller plugin, you only need to enable the feature and add `BigSpaceDefaultPlugins` to your app.
This replaces `BigSpacePlugin`.
The existing plugin structure has been organized into more fine grained plugins, with the addition of the `BigSpaceMinimalPlugins` composed of `BigSpaceCorePlugin` and `BigSpacePropagationPlugin`. These are particularly useful for tests, benchmarks, and serverside applications. Future serverside physics will likely only use the `BigSpaceCorePlugin` to handle grid cell recentering, and not do any propagation which is only needed for rendering.
### Changed: Naming consistency
To avoid common name collisions and improve searchability, names have been standardized:
- `FloatingOriginSystem` -> `BigSpaceSystems`
- `CameraControllerPlugin` -> `BigSpaceCameraControllerPlugin`
- `CameraController` -> `BigSpaceCameraController`
- `CameraInput` -> `BigSpaceCameraInput`
- `TimingStatsPlugin` -> `BigSpaceTimingStatsPlugin`
- `FloatingOriginDebugPlugin` -> `BigSpaceDebugPlugin`
- `BigSpaceValidationPlugin` (new)
- `BigSpaceDefaultPlugins` (new)
- `BigSpaceMinimalPlugins` (new)
- `BigSpaceCorePlugin` (new)
- `BigSpacePropagationPlugin` (new)
### Changed: Default plugin filters
Plugins that accept an optional query filter no longer require specifying the default empty filter tuple turbofish `::<()>`:
- `GridHashPlugin::<()>::default()` -> `GridHashPlugin::default()`
- `GridPartitionPlugin::<()>::default()` -> `GridPartitionPlugin::default()`
To construct a plugin with a custom filter, use the `new()` method:
`GridHashPlugin::<With<Player>>::new()`
### New: `no_std` Support
Thanks to `bushrat011899`'s efforts upstream and in this crate, it is now possible to use the plugin without the rust standard library. This is particularly useful when targeting embedded or console targets.
@ -24,7 +76,7 @@ The map has received a few rounds of optimization passes to make incremental upd
Built on top of the new spatial hashing feature is the `GridPartitionMap`. This map tracks groups of adjacent grid cells that have at least one entity. Each of these partitions contains many entities, and each partition is independent. That is, entities in partition A are guaranteed to be unable to collide with entities in partition B.
This lays the groundwork for adding physics integrations. Because each partition is a clump of entities independent from all other entities, it should be possible to have independent physics simulations for each partition. Not only will this allow for extreme parallelism, it becomes possible to use 32-bit physics simulations in a 160-bit big_space.
This lays the groundwork for adding physics integrations. Because each partition is a clump of entities independent of all other entities, it should be possible to have independent physics simulations for each partition. Not only will this allow for extreme parallelism, it becomes possible to use 32-bit physics simulations in a 160-bit big_space.
### `ReferenceFrame` Renamed `Grid`

View File

@ -7,6 +7,10 @@ license = "MIT OR Apache-2.0"
keywords = ["bevy", "floating-origin", "large-scale", "space"]
repository = "https://github.com/aevyrie/big_space"
documentation = "https://docs.rs/crate/big_space/latest"
exclude = ["assets"]
[package.metadata.docs.rs]
all-features = true
[features]
default = ["std"]
@ -26,7 +30,7 @@ std = [
"bevy_tasks/std",
"bevy_transform/std",
"bevy_utils/std",
"bevy_platform_support/std",
"bevy_platform/std",
"bevy_color?/std",
"bevy_input?/std",
"bevy_time?/std",
@ -34,33 +38,40 @@ std = [
libm = ["bevy_math/libm", "dep:libm"]
[dependencies]
tracing = { version = "0.1", default-features = false } # Less deps than pulling in bevy_log
smallvec = { version = "1.13.2", default-features = false } # Already used by bevy in commands
bevy_app = { version = "0.16.0-rc.3", default-features = false, features = ["bevy_reflect"] }
bevy_ecs = { version = "0.16.0-rc.3", default-features = false }
bevy_math = { version = "0.16.0-rc.3", default-features = false }
bevy_reflect = { version = "0.16.0-rc.3", default-features = false, features = ["glam"] }
bevy_tasks = { version = "0.16.0-rc.3", default-features = false }
bevy_transform = { version = "0.16.0-rc.3", default-features = false, features = [
bevy_app = { version = "0.16.0", default-features = false, features = [
"bevy_reflect",
] }
bevy_ecs = { version = "0.16.0", default-features = false }
bevy_log = { version = "0.16.0", default-features = false }
bevy_math = { version = "0.16.0", default-features = false }
bevy_reflect = { version = "0.16.0", default-features = false, features = [
"glam",
] }
bevy_tasks = { version = "0.16.0", default-features = false }
bevy_transform = { version = "0.16.0", default-features = false, features = [
"bevy-support",
"bevy_reflect",
] }
bevy_utils = { version = "0.16.0-rc.3", default-features = false }
bevy_platform_support = { version = "0.16.0-rc.3", default-features = false, features = ["alloc"] }
bevy_utils = { version = "0.16.0", default-features = false }
bevy_platform = { version = "0.16.0", default-features = false, features = [
"alloc",
] }
# Optional
bevy_color = { version = "0.16.0-rc.3", default-features = false, optional = true }
bevy_gizmos = { version = "0.16.0-rc.3", default-features = false, optional = true }
bevy_render = { version = "0.16.0-rc.3", default-features = false, optional = true }
bevy_input = { version = "0.16.0-rc.3", default-features = false, optional = true }
bevy_time = { version = "0.16.0-rc.3", default-features = false, optional = true }
bevy_color = { version = "0.16.0", default-features = false, optional = true }
bevy_gizmos = { version = "0.16.0", default-features = false, optional = true }
bevy_render = { version = "0.16.0", default-features = false, optional = true }
bevy_input = { version = "0.16.0", default-features = false, optional = true }
bevy_time = { version = "0.16.0", default-features = false, optional = true }
libm = { version = "0.2", default-features = false, optional = true }
[dev-dependencies]
bevy = { version = "0.16.0-rc.3", default-features = false, features = [
bevy = { version = "0.16.0", default-features = false, features = [
"bevy_scene",
"bevy_asset",
"bevy_color",
"bevy_gltf",
"bevy_remote",
"bevy_winit",
"default_font",
"bevy_ui",
@ -71,11 +82,13 @@ bevy = { version = "0.16.0-rc.3", default-features = false, features = [
"x11",
"tonemapping_luts",
"multi_threaded",
"png",
] }
noise = "0.9"
turborand = "0.10"
criterion = "0.5"
bytemuck = "1.20"
bevy-inspector-egui = "0.31.0"
bevy_egui = "0.34.1"
# bevy_hanabi = "0.14" # TODO: Update
[lints.clippy]
@ -124,14 +137,14 @@ required-features = ["i128", "camera", "debug"]
doc-scrape-examples = false
[[example]]
name = "error_child"
path = "examples/error_child.rs"
required-features = ["camera", "debug"]
name = "error"
path = "examples/error.rs"
doc-scrape-examples = false
[[example]]
name = "error"
path = "examples/error.rs"
name = "error_child"
path = "examples/error_child.rs"
required-features = ["camera", "debug"]
doc-scrape-examples = false
[[example]]
@ -143,7 +156,7 @@ doc-scrape-examples = false
[[example]]
name = "minimal"
path = "examples/minimal.rs"
required-features = ["camera"]
required-features = ["camera", "debug"]
doc-scrape-examples = false
# TODO: Uncomment once bevy_hanabi is updated

View File

@ -2,7 +2,7 @@
# Big Space
<img src="https://raw.githubusercontent.com/aevyrie/big_space/refs/heads/main/assets/bigspacebanner.svg" width="80%">
<img src="https://raw.githubusercontent.com/aevyrie/big_space/refs/heads/main/assets/bigspacebanner.svg" width="80%" alt="partitioning screenshot">
Huge worlds, high performance, no dependencies, ecosystem compatibility. [Read the docs](https://docs.rs/big_space)

BIN
assets/images/cubemap.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 B

View File

@ -310,7 +310,7 @@
"emissiveFactor": [
0.0,
0.161466,
1.0
50.0
],
"name": "Material.004"
}

View File

@ -3,6 +3,7 @@
#![allow(missing_docs)]
use bevy::prelude::*;
use big_space::plugin::BigSpaceMinimalPlugins;
use big_space::prelude::*;
use core::{iter::repeat_with, ops::Neg};
use criterion::{black_box, criterion_group, criterion_main, Criterion};
@ -60,8 +61,8 @@ fn deep_hierarchy(c: &mut Criterion) {
let mut app = App::new();
app.add_plugins((
MinimalPlugins,
GridHashPlugin::<()>::default(),
BigSpacePlugin::default(),
BigSpaceMinimalPlugins,
GridHashPlugin::default(),
))
.add_systems(Startup, setup)
.add_systems(Update, translate)
@ -99,8 +100,8 @@ fn wide_hierarchy(c: &mut Criterion) {
let mut app = App::new();
app.add_plugins((
MinimalPlugins,
GridHashPlugin::<()>::default(),
BigSpacePlugin::default(),
BigSpaceMinimalPlugins,
GridHashPlugin::default(),
))
.add_systems(Startup, setup)
.add_systems(Update, translate)
@ -149,7 +150,7 @@ fn spatial_hashing(c: &mut Criterion) {
}
let mut app = App::new();
app.add_plugins(GridHashPlugin::<()>::default())
app.add_plugins(GridHashPlugin::default())
.add_systems(Startup, setup)
.update();
@ -221,7 +222,7 @@ fn spatial_hashing(c: &mut Criterion) {
// Uniform Grid Population 1_000
let mut app = App::new();
app.add_plugins(GridHashPlugin::<()>::default())
app.add_plugins(GridHashPlugin::default())
.add_systems(Startup, setup_uniform::<5>)
.update();
@ -250,7 +251,7 @@ fn spatial_hashing(c: &mut Criterion) {
// Uniform Grid Population 1_000_000
let mut app = App::new();
app.add_plugins(GridHashPlugin::<()>::default())
app.add_plugins(GridHashPlugin::default())
.add_systems(Startup, setup_uniform::<50>)
.update();
@ -321,7 +322,7 @@ fn hash_filtering(c: &mut Criterion) {
.add_systems(Update, translate)
.update();
app.update();
app.add_plugins((GridHashPlugin::<()>::default(),));
app.add_plugins((GridHashPlugin::default(),));
group.bench_function("No Filter Plugin", |b| {
b.iter(|| {
black_box(app.update());
@ -333,7 +334,7 @@ fn hash_filtering(c: &mut Criterion) {
.add_systems(Update, translate)
.update();
app.update();
app.add_plugins((GridHashPlugin::<With<Player>>::default(),));
app.add_plugins((GridHashPlugin::<With<Player>>::new(),));
group.bench_function("With Player Plugin", |b| {
b.iter(|| {
black_box(app.update());
@ -345,7 +346,7 @@ fn hash_filtering(c: &mut Criterion) {
.add_systems(Update, translate)
.update();
app.update();
app.add_plugins((GridHashPlugin::<Without<Player>>::default(),));
app.add_plugins((GridHashPlugin::<Without<Player>>::new(),));
group.bench_function("Without Player Plugin", |b| {
b.iter(|| {
black_box(app.update());
@ -357,9 +358,9 @@ fn hash_filtering(c: &mut Criterion) {
.add_systems(Update, translate)
.update();
app.update();
app.add_plugins((GridHashPlugin::<()>::default(),))
.add_plugins((GridHashPlugin::<With<Player>>::default(),))
.add_plugins((GridHashPlugin::<Without<Player>>::default(),));
app.add_plugins((GridHashPlugin::default(),))
.add_plugins((GridHashPlugin::<With<Player>>::new(),))
.add_plugins((GridHashPlugin::<Without<Player>>::new(),));
group.bench_function("All Plugins", |b| {
b.iter(|| {
black_box(app.update());
@ -372,7 +373,6 @@ fn vs_bevy(c: &mut Criterion) {
let mut group = c.benchmark_group("transform_prop");
use bevy::prelude::*;
use BigSpacePlugin;
const N_ENTITIES: usize = 1_000_000;
@ -407,7 +407,7 @@ fn vs_bevy(c: &mut Criterion) {
});
let mut app = App::new();
app.add_plugins((MinimalPlugins, BigSpacePlugin::default()))
app.add_plugins((MinimalPlugins, BigSpaceMinimalPlugins))
.add_systems(Startup, setup_big)
.update();
@ -436,7 +436,7 @@ fn vs_bevy(c: &mut Criterion) {
});
let mut app = App::new();
app.add_plugins((MinimalPlugins, BigSpacePlugin::default()))
app.add_plugins((MinimalPlugins, BigSpaceMinimalPlugins))
.add_systems(Startup, setup_big)
.add_systems(Update, translate)
.update();

View File

@ -8,8 +8,7 @@ fn main() {
App::new()
.add_plugins((
DefaultPlugins.build().disable::<TransformPlugin>(),
BigSpacePlugin::default(),
FloatingOriginDebugPlugin::default(),
BigSpaceDefaultPlugins,
))
.add_systems(Startup, setup)
.add_systems(Update, (movement, rotation))

View File

@ -6,19 +6,13 @@ use bevy::{
transform::TransformSystem,
window::{CursorGrabMode, PrimaryWindow},
};
use big_space::{
camera::{CameraController, CameraInput},
prelude::*,
world_query::GridTransformReadOnly,
};
use big_space::prelude::*;
fn main() {
App::new()
.add_plugins((
DefaultPlugins.build().disable::<TransformPlugin>(),
BigSpacePlugin::default(),
FloatingOriginDebugPlugin::default(),
CameraControllerPlugin::default(),
BigSpaceDefaultPlugins,
))
.insert_resource(ClearColor(Color::BLACK))
.add_systems(Startup, (setup, ui_setup))
@ -44,7 +38,7 @@ fn setup(
}),
Transform::from_xyz(0.0, 0.0, 8.0).looking_at(Vec3::new(0.0, 0.0, 0.0), Vec3::Y),
FloatingOrigin, // Important: marks the floating origin entity for rendering.
CameraController::default() // Built-in camera controller
BigSpaceCameraController::default() // Built-in camera controller
.with_speed_bounds([10e-18, 10e35])
.with_smoothness(0.9, 0.8)
.with_speed(1.0),
@ -124,7 +118,7 @@ fn ui_setup(mut commands: Commands) {
}
fn highlight_nearest_sphere(
cameras: Query<&CameraController>,
cameras: Query<&BigSpaceCameraController>,
objects: Query<&GlobalTransform>,
mut gizmos: Gizmos,
) -> Result {
@ -154,7 +148,7 @@ fn ui_text_system(
grids: Grids,
time: Res<Time>,
origin: Query<(Entity, GridTransformReadOnly), With<FloatingOrigin>>,
camera: Query<&CameraController>,
camera: Query<&BigSpaceCameraController>,
objects: Query<&Transform, With<Mesh3d>>,
) -> Result {
let (origin_entity, origin_pos) = origin.single()?;
@ -262,7 +256,7 @@ fn closest<'a>(diameter: f32) -> (f32, &'a str) {
fn cursor_grab_system(
mut windows: Query<&mut Window, With<PrimaryWindow>>,
mut cam: ResMut<CameraInput>,
mut cam: ResMut<big_space::camera::BigSpaceCameraInput>,
btn: Res<ButtonInput<MouseButton>>,
key: Res<ButtonInput<KeyCode>>,
) -> Result {

View File

@ -12,7 +12,7 @@ fn main() {
App::new()
.add_plugins((
DefaultPlugins.build().disable::<TransformPlugin>(),
BigSpacePlugin::default(),
BigSpaceDefaultPlugins,
))
.add_systems(Startup, (setup_scene, setup_ui))
.add_systems(Update, (rotator_system, toggle_plugin))

View File

@ -6,9 +6,7 @@ fn main() {
App::new()
.add_plugins((
DefaultPlugins.build().disable::<TransformPlugin>(),
BigSpacePlugin::default(),
CameraControllerPlugin::default(),
FloatingOriginDebugPlugin::default(),
BigSpaceDefaultPlugins,
))
.add_systems(Startup, setup_scene)
.run();
@ -85,7 +83,7 @@ fn setup_scene(
..default()
}),
FloatingOrigin,
CameraController::default() // Built-in camera controller
BigSpaceCameraController::default() // Built-in camera controller
.with_speed_bounds([10e-18, 10e35])
.with_smoothness(0.9, 0.8)
.with_speed(1.0),

View File

@ -8,9 +8,7 @@ fn main() {
App::new()
.add_plugins((
DefaultPlugins.build().disable::<TransformPlugin>(),
BigSpacePlugin::default(),
FloatingOriginDebugPlugin::default(),
CameraControllerPlugin::default(),
BigSpaceDefaultPlugins,
))
.add_systems(Startup, setup_scene)
.run();
@ -45,7 +43,7 @@ fn setup_scene(
Camera3d::default(),
Transform::from_xyz(0.0, 0.0, 10.0),
FloatingOrigin,
CameraController::default()
BigSpaceCameraController::default()
.with_speed(10.)
.with_smoothness(0.99, 0.95),
));

View File

@ -11,9 +11,7 @@ fn main() {
App::new()
.add_plugins((
DefaultPlugins.build().disable::<TransformPlugin>(),
BigSpacePlugin::default(),
FloatingOriginDebugPlugin::default(), // Draws cell AABBs and grids
CameraControllerPlugin::default(), // Compatible controller
BigSpaceDefaultPlugins,
))
.add_systems(Startup, setup_scene)
.run();
@ -25,16 +23,14 @@ fn setup_scene(
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
// Using `spawn_big_space` helps you avoid mistakes when building hierarchies. Most notably,
// it will allow you to only write out the `GridPrecision` generic value (i64 in this case)
// once, without needing to repeat this generic when spawning `GridCell<i64>`s
// Using `spawn_big_space` helps you avoid mistakes when building hierarchies.
//
// A world can have multiple independent BigSpaces, with their own floating origins. This can
// come in handy if you want to have two cameras very far from each other, rendering at the same
// time like split screen, or portals.
// time as split screen, or portals.
commands.spawn_big_space_default(|root_grid| {
// Because BIG_DISTANCE is so large, we want to avoid using bevy's f32 transforms alone and
// experience rounding errors. Instead, we use this helper to convert an f64 position into a
// experience rounding errors. Instead, we use this helper to convert f64 position into a
// grid cell and f32 offset.
let (grid_cell, cell_offset) = root_grid
.grid()
@ -67,7 +63,7 @@ fn setup_scene(
Transform::from_translation(cell_offset + Vec3::new(0.0, 0.0, 10.0)),
grid_cell,
FloatingOrigin,
CameraController::default(),
BigSpaceCameraController::default(),
));
});
}

View File

@ -3,6 +3,7 @@ extern crate alloc;
use alloc::collections::VecDeque;
use bevy::core_pipeline::Skybox;
use bevy::{
color::palettes,
core_pipeline::bloom::Bloom,
@ -13,29 +14,35 @@ use bevy::{
transform::TransformSystem,
};
use big_space::prelude::*;
use big_space::validation::BigSpaceValidationPlugin;
use turborand::{rng::Rng, TurboRand};
fn main() {
App::new()
.add_plugins((
DefaultPlugins.build().disable::<TransformPlugin>(),
BigSpacePlugin::new(true),
CameraControllerPlugin::default(),
BigSpaceDefaultPlugins
.build()
.enable::<BigSpaceValidationPlugin>(),
bevy_egui::EguiPlugin {
enable_multipass_for_primary_context: true,
},
bevy_inspector_egui::quick::WorldInspectorPlugin::default(),
))
.insert_resource(ClearColor(Color::BLACK))
.insert_resource(AmbientLight {
color: Color::WHITE,
brightness: 200.0,
..Default::default()
})
.add_systems(Startup, spawn_solar_system)
.add_systems(Update, configure_skybox_image)
.add_systems(
PostUpdate,
(
rotate,
lighting
.in_set(TransformSystem::TransformPropagate)
.after(FloatingOriginSystem::PropagateLowPrecision),
.after(BigSpaceSystems::PropagateLowPrecision),
cursor_grab_system,
springy_ship
.after(big_space::camera::default_camera_inputs)
@ -82,7 +89,7 @@ fn lighting(
}
fn springy_ship(
cam_input: Res<big_space::camera::CameraInput>,
cam_input: Res<big_space::camera::BigSpaceCameraInput>,
mut ship: Query<&mut Transform, With<Spaceship>>,
mut desired_dir: Local<(Vec3, Quat)>,
mut smoothed_rot: Local<VecDeque<Vec3>>,
@ -152,7 +159,7 @@ fn spawn_solar_system(
Mesh3d(sun_mesh_handle),
MeshMaterial3d(materials.add(StandardMaterial {
base_color: Color::WHITE,
emissive: LinearRgba::rgb(1000., 1000., 1000.),
emissive: LinearRgba::rgb(10000., 10000., 10000.),
..default()
})),
NotShadowCaster,
@ -221,38 +228,47 @@ fn spawn_solar_system(
let cam_pos = DVec3::X * (EARTH_RADIUS_M + 1.0);
let (cam_cell, cam_pos) = earth.grid().translation_to_grid(cam_pos);
earth.with_grid_default(|camera| {
camera.insert((
earth.with_grid_default(|floating_origin| {
floating_origin.insert((
FloatingOrigin,
Transform::from_translation(cam_pos).looking_to(Vec3::NEG_Z, Vec3::X),
CameraController::default() // Built-in camera controller
BigSpaceCameraController::default() // Built-in camera controller
.with_speed_bounds([0.1, 10e35])
.with_smoothness(0.98, 0.98)
.with_speed(1.0),
cam_cell,
));
camera.spawn_spatial((
floating_origin.spawn_spatial((
Camera3d::default(),
Transform::from_xyz(0.0, 4.0, 22.0),
Camera {
hdr: true,
clear_color: ClearColorConfig::None,
..default()
},
Exposure::SUNLIGHT,
Bloom::NATURAL,
Bloom::ANAMORPHIC,
bevy::core_pipeline::post_process::ChromaticAberration {
intensity: 0.01,
..Default::default()
},
bevy::core_pipeline::motion_blur::MotionBlur::default(),
bevy::core_pipeline::motion_blur::MotionBlur {
shutter_angle: 1.0,
samples: 8,
},
Msaa::Off,
));
camera.with_child((
Spaceship,
SceneRoot(asset_server.load("models/low_poly_spaceship/scene.gltf#Scene0")),
Transform::from_rotation(Quat::from_rotation_y(core::f32::consts::PI)),
));
floating_origin.with_spatial(|ship_scene| {
ship_scene.insert((
Spaceship,
SceneRoot(
asset_server.load("models/low_poly_spaceship/scene.gltf#Scene0"),
),
Transform::from_rotation(Quat::from_rotation_y(core::f32::consts::PI)),
));
});
});
});
});
@ -280,17 +296,19 @@ fn spawn_solar_system(
fn cursor_grab_system(
mut windows: Query<&mut Window, With<bevy::window::PrimaryWindow>>,
mut cam: ResMut<big_space::camera::CameraInput>,
mut cam: ResMut<big_space::camera::BigSpaceCameraInput>,
btn: Res<ButtonInput<MouseButton>>,
key: Res<ButtonInput<KeyCode>>,
mut triggered: Local<bool>,
) -> Result<()> {
let mut window = windows.single_mut()?;
if btn.just_pressed(MouseButton::Right) {
if btn.just_pressed(MouseButton::Right) || !*triggered {
window.cursor_options.grab_mode = bevy::window::CursorGrabMode::Locked;
window.cursor_options.visible = false;
// window.mode = WindowMode::BorderlessFullscreen;
cam.defaults_disabled = false;
*triggered = true;
}
if key.just_pressed(KeyCode::Escape) {
@ -302,3 +320,46 @@ fn cursor_grab_system(
Ok(())
}
#[derive(Resource)]
struct Cubemap(Handle<Image>, bool);
fn configure_skybox_image(
mut commands: Commands,
asset_server: Res<AssetServer>,
cubemap: Option<ResMut<Cubemap>>,
cameras: Query<Entity, With<Camera>>,
mut images: ResMut<Assets<Image>>,
) {
let mut cubemap = match cubemap {
None => {
commands.insert_resource(Cubemap(asset_server.load("images/cubemap.png"), false));
return;
}
Some(cubemap) => cubemap,
};
if cubemap.1 {
return;
}
if asset_server.load_state(&cubemap.0).is_loaded() {
let image = images.get_mut(&cubemap.0).unwrap();
// NOTE: PNGs do not have any metadata that could indicate they contain a cubemap texture,
// so they appear as one texture. The following code reconfigures the texture as necessary.
if image.texture_descriptor.array_layer_count() == 1 {
image.reinterpret_stacked_2d_as_array(image.height() / image.width());
image.texture_view_descriptor =
Some(bevy_render::render_resource::TextureViewDescriptor {
dimension: Some(bevy_render::render_resource::TextureViewDimension::Cube),
..default()
});
}
let camera = cameras.single().unwrap();
commands.entity(camera).insert(Skybox {
image: cubemap.0.clone(),
..Default::default()
});
cubemap.1 = true;
}
}

View File

@ -7,9 +7,9 @@
//! milky way galaxy.
use bevy::prelude::*;
use bevy_log::info;
use bevy_math::DVec3;
use big_space::prelude::*;
use tracing::info;
const UNIVERSE_DIA: f64 = 8.8e26; // Diameter of the observable universe
const PROTON_DIA: f32 = 1.68e-15; // Diameter of a proton
@ -18,9 +18,7 @@ fn main() {
App::new()
.add_plugins((
DefaultPlugins.build().disable::<TransformPlugin>(),
BigSpacePlugin::default(),
FloatingOriginDebugPlugin::default(), // Draws cell AABBs and grids
CameraControllerPlugin::default(), // Compatible controller
BigSpaceDefaultPlugins,
))
.add_systems(Startup, setup_scene)
.add_systems(Update, (bounce_atoms, toggle_cam_pos))
@ -76,10 +74,10 @@ fn setup_scene(
Transform::from_xyz(0.0, 0.0, PROTON_DIA * 2.0),
grid_cell,
FloatingOrigin,
CameraController::default(),
BigSpaceCameraController::default(),
));
// A space ship
// A spaceship
root_grid.spawn_spatial((
SceneRoot(asset_server.load("models/low_poly_spaceship/scene.gltf#Scene0")),
Transform::from_xyz(0.0, 0.0, 2.5)

View File

@ -4,7 +4,7 @@ use bevy::{
core_pipeline::{bloom::Bloom, tonemapping::Tonemapping},
prelude::*,
};
use bevy_ecs::{entity::EntityHasher, relationship::Relationship};
use bevy_ecs::entity::EntityHasher;
use bevy_math::DVec3;
use big_space::prelude::*;
use core::hash::Hasher;
@ -16,17 +16,18 @@ const HALF_WIDTH: f32 = 50.0;
const CELL_WIDTH: f32 = 10.0;
// How fast the entities should move, causing them to move into neighboring cells.
const MOVEMENT_SPEED: f32 = 5.0;
const PERCENT_STATIC: f32 = 0.99;
const PERCENT_STATIC: f32 = 1.0;
fn main() {
App::new()
.add_plugins((
DefaultPlugins.build().disable::<TransformPlugin>(),
BigSpacePlugin::default(),
GridHashPlugin::<()>::default(),
GridPartitionPlugin::<()>::default(),
CameraControllerPlugin::default(),
BigSpaceDefaultPlugins,
GridHashPlugin::default(),
GridPartitionPlugin::default(),
))
.add_plugins(bevy::remote::RemotePlugin::default()) // Core remote protocol
.add_plugins(bevy::remote::http::RemoteHttpPlugin::default()) // Enable HTTP transport
.add_systems(Startup, (spawn, setup_ui))
.add_systems(
PostUpdate,
@ -173,13 +174,13 @@ fn move_player(
}
let t = time.elapsed_secs() * 0.01;
let (mut transform, mut cell, parent, hash) = player.single_mut()?;
let (mut transform, mut cell, child_of, hash) = player.single_mut()?;
let absolute_pos = HALF_WIDTH
* CELL_WIDTH
* 0.8
* Vec3::new((5.0 * t).sin(), (7.0 * t).cos(), (20.0 * t).sin());
(*cell, transform.translation) = grids
.get(parent.get())?
.get(child_of.parent())?
.imprecise_translation_to_grid(absolute_pos);
neighbors.clear();
@ -263,7 +264,7 @@ fn spawn(mut commands: Commands) {
},
Tonemapping::AcesFitted,
Transform::from_xyz(0.0, 0.0, HALF_WIDTH * CELL_WIDTH * 2.0),
CameraController::default()
BigSpaceCameraController::default()
.with_smoothness(0.98, 0.93)
.with_slowing(false)
.with_speed(15.0),

View File

@ -16,9 +16,7 @@ fn main() {
App::new()
.add_plugins((
DefaultPlugins.build().disable::<TransformPlugin>(),
BigSpacePlugin::default(),
FloatingOriginDebugPlugin::default(),
CameraControllerPlugin::default(),
BigSpaceDefaultPlugins,
))
.add_systems(Startup, setup)
.add_systems(Update, set_camera_viewports)
@ -60,7 +58,7 @@ fn setup(
Camera3d::default(),
Transform::from_xyz(1_000_000.0 - 10.0, 100_005.0, 0.0)
.looking_to(Vec3::NEG_X, Vec3::Y),
CameraController::default().with_smoothness(0.8, 0.8),
BigSpaceCameraController::default().with_smoothness(0.8, 0.8),
RenderLayers::layer(2),
LeftCamera,
FloatingOrigin,

154
src/bevy_compat.rs Normal file
View File

@ -0,0 +1,154 @@
//! Systems for [`Transform`] propagation compatibility with entities outside a
//! [`BigSpace`](crate::BigSpace), needed when bevy's built in transform propagation is disabled.
use alloc::vec::Vec;
use bevy_ecs::{change_detection::Ref, prelude::*};
use bevy_transform::prelude::*;
/// Copied from bevy. This is the simpler propagation implementation that doesn't use dirty tree
/// marking. This is needed because dirty tree marking doesn't start from the root, and will end up
/// doing the work for big space hierarchies, which it cannot affect anyway.
pub fn propagate_parent_transforms(
mut root_query: Query<
(Entity, &Children, Ref<Transform>, &mut GlobalTransform),
Without<ChildOf>,
>,
mut orphaned: RemovedComponents<ChildOf>,
transform_query: Query<
(Ref<Transform>, &mut GlobalTransform, Option<&Children>),
With<ChildOf>,
>,
child_query: Query<(Entity, Ref<ChildOf>), With<GlobalTransform>>,
mut orphaned_entities: Local<Vec<Entity>>,
) {
orphaned_entities.clear();
orphaned_entities.extend(orphaned.read());
orphaned_entities.sort_unstable();
root_query.par_iter_mut().for_each(
|(entity, children, transform, mut global_transform)| {
let changed = transform.is_changed() || global_transform.is_added() || orphaned_entities.binary_search(&entity).is_ok();
if changed {
*global_transform = GlobalTransform::from(*transform);
}
for (child, child_of) in child_query.iter_many(children) {
assert_eq!(
child_of.parent(), entity,
"Malformed hierarchy. This probably means that your hierarchy has been improperly maintained, or contains a cycle"
);
// SAFETY:
// - `child` must have consistent parentage, or the above assertion would panic.
// Since `child` is parented to a root entity, the entire hierarchy leading to it
// is consistent.
// - We may operate as if all descendants are consistent, since
// `propagate_recursive` will panic before continuing to propagate if it
// encounters an entity with inconsistent parentage.
// - Since each root entity is unique and the hierarchy is consistent and
// forest-like, other root entities' `propagate_recursive` calls will not conflict
// with this one.
// - Since this is the only place where `transform_query` gets used, there will be
// no conflicting fetches elsewhere.
#[expect(unsafe_code, reason = "`propagate_recursive()` is unsafe due to its use of `Query::get_unchecked()`.")]
unsafe {
propagate_recursive(
&global_transform,
&transform_query,
&child_query,
child,
changed || child_of.is_changed(),
);
}
}
},
);
}
/// Recursively propagates the transforms for `entity` and all of its descendants.
///
/// # Panics
///
/// If `entity`'s descendants have a malformed hierarchy, this function will panic occur before
/// propagating the transforms of any malformed entities and their descendants.
///
/// # Safety
///
/// - While this function is running, `transform_query` must not have any fetches for `entity`,
/// nor any of its descendants.
/// - The caller must ensure that the hierarchy leading to `entity` is well-formed and must
/// remain as a tree or a forest. Each entity must have at most one parent.
#[expect(
unsafe_code,
reason = "This function uses `Query::get_unchecked()`, which can result in multiple mutable references if the preconditions are not met."
)]
unsafe fn propagate_recursive(
parent: &GlobalTransform,
transform_query: &Query<
(Ref<Transform>, &mut GlobalTransform, Option<&Children>),
With<ChildOf>,
>,
child_query: &Query<(Entity, Ref<ChildOf>), With<GlobalTransform>>,
entity: Entity,
mut changed: bool,
) {
let (global_matrix, children) = {
let Ok((transform, mut global_transform, children)) =
// SAFETY: This call cannot create aliased mutable references.
// - The top level iteration parallelizes on the roots of the hierarchy.
// - The caller ensures that each child has one and only one unique parent throughout
// the entire hierarchy.
//
// For example, consider the following malformed hierarchy:
//
// A
// / \
// B C
// \ /
// D
//
// D has two parents, B and C. If the propagation passes through C, but the ChildOf
// component on D points to B, the above check will panic as the origin parent does
// match the recorded parent.
//
// Also consider the following case, where A and B are roots:
//
// A B
// \ /
// C D
// \ /
// E
//
// Even if these A and B start two separate tasks running in parallel, one of them will
// panic before attempting to mutably access E.
(unsafe { transform_query.get_unchecked(entity) }) else {
return;
};
changed |= transform.is_changed() || global_transform.is_added();
if changed {
*global_transform = parent.mul_transform(*transform);
}
(global_transform, children)
};
let Some(children) = children else { return };
for (child, child_of) in child_query.iter_many(children) {
assert_eq!(
child_of.parent(), entity,
"Malformed hierarchy. This probably means that your hierarchy has been improperly maintained, or contains a cycle"
);
// SAFETY: The caller guarantees that `transform_query` will not be fetched for any
// descendants of `entity`, so it is safe to call `propagate_recursive` for each child.
//
// The above assertion ensures that each child has one and only one unique parent
// throughout the entire hierarchy.
unsafe {
propagate_recursive(
global_matrix.as_ref(),
transform_query,
child_query,
child,
changed || child_of.is_changed(),
);
}
}
}

View File

@ -5,8 +5,7 @@ use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use bevy_input::{mouse::MouseMotion, prelude::*};
use bevy_math::{prelude::*, DQuat, DVec3};
use bevy_platform_support::collections::HashSet;
use bevy_platform_support::prelude::*;
use bevy_platform::{collections::HashSet, prelude::*};
use bevy_reflect::prelude::*;
use bevy_render::{
primitives::Aabb,
@ -15,28 +14,32 @@ use bevy_render::{
use bevy_time::prelude::*;
use bevy_transform::{prelude::*, TransformSystem};
/// Adds the `big_space` camera controller
#[derive(Default)]
pub struct CameraControllerPlugin(());
impl Plugin for CameraControllerPlugin {
/// Runs the [`big_space`](crate) [`BigSpaceCameraController`].
pub struct BigSpaceCameraControllerPlugin;
impl Plugin for BigSpaceCameraControllerPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<CameraInput>().add_systems(
PostUpdate,
(
default_camera_inputs
.before(camera_controller)
.run_if(|input: Res<CameraInput>| !input.defaults_disabled),
nearest_objects_in_grid.before(camera_controller),
camera_controller.before(TransformSystem::TransformPropagate),
),
);
app.register_type::<BigSpaceCameraController>()
.register_type::<BigSpaceCameraInput>()
.init_resource::<BigSpaceCameraInput>()
.add_systems(
PostUpdate,
(
default_camera_inputs
.before(camera_controller)
.run_if(|input: Res<BigSpaceCameraInput>| !input.defaults_disabled),
nearest_objects_in_grid.before(camera_controller),
camera_controller.before(TransformSystem::TransformPropagate),
),
);
}
}
/// Per-camera settings for the `big_space` floating origin camera controller.
/// A simple fly-cam camera controller.
///
/// Add to a camera to enable the built-in [`big_space`](crate) camera controller.
#[derive(Clone, Debug, Reflect, Component)]
#[reflect(Component)]
pub struct CameraController {
pub struct BigSpaceCameraController {
/// Smoothness of translation, from `0.0` to `1.0`.
pub smoothness: f64,
/// Rotational smoothness, from `0.0` to `1.0`.
@ -58,7 +61,7 @@ pub struct CameraController {
vel_rotation: DQuat,
}
impl CameraController {
impl BigSpaceCameraController {
/// Sets the `smoothness` parameter of the controller, and returns the modified result.
pub fn with_smoothness(mut self, translation: f64, rotation: f64) -> Self {
self.smoothness = translation;
@ -113,7 +116,7 @@ impl CameraController {
}
}
impl Default for CameraController {
impl Default for BigSpaceCameraController {
fn default() -> Self {
Self {
smoothness: 0.85,
@ -131,11 +134,12 @@ impl Default for CameraController {
}
}
/// `ButtonInput` state used to command camera motion. Reset every time the values are read to update
/// the camera. Allows you to map any input to camera motions. Uses aircraft principle axes
/// conventions.
/// `ButtonInput` state used to command [`BigSpaceCameraController`] motion. Reset every time the values
/// are read to update the camera. Allows you to map any input to camera motions. Uses aircraft
/// principle axes conventions.
#[derive(Clone, Debug, Default, Reflect, Resource)]
pub struct CameraInput {
#[reflect(Resource)]
pub struct BigSpaceCameraInput {
/// When disabled, the camera input system is not run.
pub defaults_disabled: bool,
/// Z-negative
@ -154,10 +158,10 @@ pub struct CameraInput {
pub boost: bool,
}
impl CameraInput {
/// Reset the controller back to zero to ready fro the next grid.
impl BigSpaceCameraInput {
/// Reset the controller back to zero to ready for the next grid.
pub fn reset(&mut self) {
*self = CameraInput {
*self = BigSpaceCameraInput {
defaults_disabled: self.defaults_disabled,
..Default::default()
};
@ -166,7 +170,7 @@ impl CameraInput {
/// Returns the desired velocity transform.
pub fn target_velocity(
&self,
controller: &CameraController,
controller: &BigSpaceCameraController,
speed: f64,
dt: f64,
) -> (DVec3, DQuat) {
@ -187,7 +191,7 @@ impl CameraInput {
pub fn default_camera_inputs(
keyboard: Res<ButtonInput<KeyCode>>,
mut mouse_move: EventReader<MouseMotion>,
mut cam: ResMut<CameraInput>,
mut cam: ResMut<BigSpaceCameraInput>,
) {
keyboard.pressed(KeyCode::KeyW).then(|| cam.forward -= 1.0);
keyboard.pressed(KeyCode::KeyS).then(|| cam.forward += 1.0);
@ -220,7 +224,7 @@ pub fn nearest_objects_in_grid(
)>,
mut camera: Query<(
Entity,
&mut CameraController,
&mut BigSpaceCameraController,
&GlobalTransform,
Option<&RenderLayers>,
)>,
@ -257,12 +261,17 @@ pub fn nearest_objects_in_grid(
camera.nearest_object = nearest_object;
}
/// Uses [`CameraInput`] state to update the camera position.
/// Uses [`BigSpaceCameraInput`] state to update the camera position.
pub fn camera_controller(
time: Res<Time>,
grids: Grids,
mut input: ResMut<CameraInput>,
mut camera: Query<(Entity, &mut GridCell, &mut Transform, &mut CameraController)>,
mut input: ResMut<BigSpaceCameraInput>,
mut camera: Query<(
Entity,
&mut GridCell,
&mut Transform,
&mut BigSpaceCameraController,
)>,
) {
for (camera, mut cell, mut transform, mut controller) in camera.iter_mut() {
let Some(grid) = grids.parent_grid(camera) else {

View File

@ -10,9 +10,8 @@ use bevy_reflect::Reflect;
use bevy_transform::prelude::*;
/// This plugin will render the bounds of occupied grid cells.
#[derive(Default)]
pub struct FloatingOriginDebugPlugin(());
impl Plugin for FloatingOriginDebugPlugin {
pub struct BigSpaceDebugPlugin;
impl Plugin for BigSpaceDebugPlugin {
fn build(&self, app: &mut App) {
app.init_gizmo_group::<BigSpaceGizmoConfig>()
.add_systems(Startup, setup_gizmos)

View File

@ -1,7 +1,7 @@
//! A floating origin for camera-relative rendering, to maximize precision when converting to f32.
use bevy_ecs::prelude::*;
use bevy_platform_support::collections::HashMap;
use bevy_platform::collections::HashMap;
use bevy_reflect::prelude::*;
/// Marks the entity to use as the floating origin.
@ -82,7 +82,7 @@ impl BigSpace {
let space_origins = spaces_set.entry(root).or_default();
*space_origins += 1;
if *space_origins > 1 {
tracing::error!(
bevy_log::error!(
"BigSpace {root:#?} has multiple floating origins. There must be exactly one. Resetting this big space and disabling the floating origin to avoid unexpected propagation behavior.",
);
space.floating_origin = None;
@ -98,7 +98,7 @@ impl BigSpace {
.filter(|(_k, v)| **v == 0)
.map(|(k, _v)| k)
{
tracing::error!("BigSpace {space:#} has no floating origins. There must be exactly one. Transform propagation will not work until there is a FloatingOrigin in the hierarchy.",);
bevy_log::error!("BigSpace {space:#} has no floating origins. There must be exactly one. Transform propagation will not work until there is a FloatingOrigin in the hierarchy.",);
}
}
}

View File

@ -1,9 +1,9 @@
//! Contains the grid cell implementation
use crate::prelude::*;
use bevy_ecs::{prelude::*, relationship::Relationship};
use bevy_ecs::prelude::*;
use bevy_math::{DVec3, IVec3};
use bevy_platform_support::time::Instant;
use bevy_platform::time::Instant;
use bevy_reflect::prelude::*;
use bevy_transform::prelude::*;
@ -58,7 +58,7 @@ impl GridCell {
/// Returns a cell containing the minimum values for each element of self and rhs.
///
/// In other words this computes [self.x.min(rhs.x), self.y.min(rhs.y), ..].
/// In other words this computes [self.x.min(rhs.x), self.y.min(rhs.y), ...].
pub fn min(&self, rhs: Self) -> Self {
Self {
x: self.x.min(rhs.x),
@ -69,7 +69,7 @@ impl GridCell {
/// Returns a cell containing the maximum values for each element of self and rhs.
///
/// In other words this computes [self.x.max(rhs.x), self.y.max(rhs.y), ..].
/// In other words this computes [self.x.max(rhs.x), self.y.max(rhs.y), ...].
pub fn max(&self, rhs: Self) -> Self {
Self {
x: self.x.max(rhs.x),
@ -81,7 +81,7 @@ impl GridCell {
/// If an entity's transform translation becomes larger than the limit specified in its
/// [`Grid`], it will be relocated to the nearest grid cell to reduce the size of the transform.
pub fn recenter_large_transforms(
mut stats: ResMut<crate::timing::PropagationStats>,
mut stats: Option<ResMut<crate::timing::PropagationStats>>,
grids: Query<&Grid>,
mut changed_transform: Query<(&mut Self, &mut Transform, &ChildOf), Changed<Transform>>,
) {
@ -89,7 +89,7 @@ impl GridCell {
changed_transform
.par_iter_mut()
.for_each(|(mut grid_pos, mut transform, parent)| {
let Ok(grid) = grids.get(parent.get()) else {
let Ok(grid) = grids.get(parent.parent()) else {
return;
};
if transform
@ -106,7 +106,9 @@ impl GridCell {
transform.translation = translation;
}
});
stats.grid_recentering += start.elapsed();
if let Some(stats) = stats.as_mut() {
stats.grid_recentering += start.elapsed();
}
}
}

View File

@ -12,7 +12,7 @@ use bevy_ecs::{
},
};
use bevy_math::{prelude::*, DAffine3, DQuat};
use bevy_platform_support::prelude::*;
use bevy_platform::prelude::*;
use bevy_transform::prelude::*;
pub use inner::LocalFloatingOrigin;
@ -430,7 +430,7 @@ impl LocalFloatingOrigin {
/// rendering precision. The high precision coordinates ([`GridCell`] and [`Transform`]) are the
/// source of truth and never mutated.
pub fn compute_all(
mut stats: ResMut<crate::timing::PropagationStats>,
mut stats: Option<ResMut<crate::timing::PropagationStats>>,
mut grids: GridsMut,
mut grid_stack: Local<Vec<Entity>>,
mut scratch_buffer: Local<Vec<Entity>>,
@ -438,7 +438,7 @@ impl LocalFloatingOrigin {
roots: Query<(Entity, &BigSpace)>,
parents: Query<&ChildOf>,
) {
let start = bevy_platform_support::time::Instant::now();
let start = bevy_platform::time::Instant::now();
/// The maximum grid tree depth, defensively prevents infinite looping in case there is a
/// degenerate hierarchy. It might take a while, but at least it's not forever?
@ -452,7 +452,7 @@ impl LocalFloatingOrigin {
.filter_map(|origin| cells.get(origin).ok())
{
let Some(mut this_grid) = grids.parent_grid_entity(origin_entity) else {
tracing::error!("The floating origin is not in a valid grid. The floating origin entity must be a child of an entity with the `Grid` component.");
bevy_log::error!("The floating origin is not in a valid grid. The floating origin entity must be a child of an entity with the `Grid` component.");
continue;
};
@ -485,9 +485,9 @@ impl LocalFloatingOrigin {
}
}
// All of the grids pushed on the stack have been processed. We can now pop those
// off the stack and recursively process their children all the way out to the
// leaves of the tree.
// All the grids pushed on the stack have been processed. We can now pop those off
// the stack and recursively process their children all the way out to the leaves of
// the tree.
while let Some(this_grid) = grid_stack.pop() {
scratch_buffer.extend(grids.child_grids(this_grid));
// TODO: This loop could be run in parallel, because we are mutating each unique
@ -508,24 +508,26 @@ impl LocalFloatingOrigin {
}
}
tracing::error!("Reached the maximum grid depth ({MAX_REFERENCE_FRAME_DEPTH}), and exited early to prevent an infinite loop. This might be caused by a degenerate hierarchy.");
bevy_log::error!("Reached the maximum grid depth ({MAX_REFERENCE_FRAME_DEPTH}), and exited early to prevent an infinite loop. This might be caused by a degenerate hierarchy.");
}
stats.local_origin_propagation += start.elapsed();
if let Some(stats) = stats.as_mut() {
stats.local_origin_propagation += start.elapsed();
}
}
}
#[cfg(test)]
mod tests {
use bevy::{ecs::system::SystemState, math::DVec3, prelude::*};
use super::*;
use crate::plugin::BigSpaceMinimalPlugins;
use bevy::{ecs::system::SystemState, math::DVec3, prelude::*};
/// Test that the grid getters do what they say they do.
#[test]
fn grid_hierarchy_getters() {
let mut app = App::new();
app.add_plugins(BigSpacePlugin::default());
app.add_plugins(BigSpaceMinimalPlugins);
let grid_bundle = (Transform::default(), GridCell::default(), Grid::default());
@ -570,7 +572,7 @@ mod tests {
#[test]
fn child_propagation() {
let mut app = App::new();
app.add_plugins(BigSpacePlugin::default());
app.add_plugins(BigSpaceMinimalPlugins);
let root_grid = Grid {
local_floating_origin: LocalFloatingOrigin::new(
@ -630,7 +632,7 @@ mod tests {
#[test]
fn parent_propagation() {
let mut app = App::new();
app.add_plugins(BigSpacePlugin::default());
app.add_plugins(BigSpaceMinimalPlugins);
let grid_bundle = (Transform::default(), GridCell::default(), Grid::default());
let root = app.world_mut().spawn(grid_bundle.clone()).id();
@ -687,7 +689,7 @@ mod tests {
#[test]
fn origin_transform() {
let mut app = App::new();
app.add_plugins(BigSpacePlugin::default());
app.add_plugins(BigSpaceMinimalPlugins);
let root = app
.world_mut()

View File

@ -25,7 +25,7 @@ pub mod propagation;
/// Entities in the same grid as the [`FloatingOrigin`] will be rendered with the most precision.
/// Transforms are propagated starting from the floating origin, ensuring that grids in a similar
/// point in the hierarchy have accumulated the least error. Grids are transformed relative to each
/// other using 64 bit float transforms.
/// other using 64-bit float transforms.
#[derive(Debug, Clone, Reflect, Component)]
#[reflect(Component)]
// We do not require the Transform, GlobalTransform, or GridCell, because these are not required in

View File

@ -1,7 +1,7 @@
//! Logic for propagating transforms through the hierarchy of grids.
use crate::prelude::*;
use bevy_ecs::{prelude::*, relationship::Relationship};
use bevy_ecs::prelude::*;
use bevy_reflect::Reflect;
use bevy_transform::prelude::*;
@ -19,7 +19,7 @@ impl Grid {
/// Update the `GlobalTransform` of entities with a [`GridCell`], using the [`Grid`] the entity
/// belongs to.
pub fn propagate_high_precision(
mut stats: ResMut<crate::timing::PropagationStats>,
mut stats: Option<ResMut<crate::timing::PropagationStats>>,
grids: Query<&Grid>,
mut entities: ParamSet<(
Query<(
@ -31,7 +31,7 @@ impl Grid {
Query<(&Grid, &mut GlobalTransform), With<BigSpace>>,
)>,
) {
let start = bevy_platform_support::time::Instant::now();
let start = bevy_platform::time::Instant::now();
// Performance note: I've also tried to iterate over each grid's children at once, to avoid
// the grid and parent lookup, but that made things worse because it prevented dumb
@ -42,7 +42,7 @@ impl Grid {
.p0()
.par_iter_mut()
.for_each(|(cell, transform, parent, mut global_transform)| {
if let Ok(grid) = grids.get(parent.get()) {
if let Ok(grid) = grids.get(parent.parent()) {
// Optimization: we don't need to recompute the transforms if the entity hasn't
// moved and the floating origin's local origin in that grid hasn't changed.
//
@ -85,12 +85,14 @@ impl Grid {
grid.global_transform(&GridCell::default(), &Transform::IDENTITY);
});
stats.high_precision_propagation += start.elapsed();
if let Some(stats) = stats.as_mut() {
stats.high_precision_propagation += start.elapsed();
}
}
/// Marks entities with [`LowPrecisionRoot`]. Handles adding and removing the component.
pub fn tag_low_precision_roots(
mut stats: ResMut<crate::timing::PropagationStats>,
mut stats: Option<ResMut<crate::timing::PropagationStats>>,
mut commands: Commands,
valid_parent: Query<(), (With<GridCell>, With<GlobalTransform>, With<Children>)>,
unmarked: Query<
@ -117,9 +119,9 @@ impl Grid {
>,
has_possibly_invalid_parent: Query<(Entity, &ChildOf), With<LowPrecisionRoot>>,
) {
let start = bevy_platform_support::time::Instant::now();
let start = bevy_platform::time::Instant::now();
for (entity, parent) in unmarked.iter() {
if valid_parent.contains(parent.get()) {
if valid_parent.contains(parent.parent()) {
commands.entity(entity).insert(LowPrecisionRoot);
}
}
@ -129,18 +131,20 @@ impl Grid {
}
for (entity, parent) in has_possibly_invalid_parent.iter() {
if !valid_parent.contains(parent.get()) {
if !valid_parent.contains(parent.parent()) {
commands.entity(entity).remove::<LowPrecisionRoot>();
}
}
stats.low_precision_root_tagging += start.elapsed();
if let Some(stats) = stats.as_mut() {
stats.low_precision_root_tagging += start.elapsed();
}
}
/// Update the [`GlobalTransform`] of entities with a [`Transform`], without a [`GridCell`], and
/// that are children of an entity with a [`GlobalTransform`]. This will recursively propagate
/// entities that only have low-precision [`Transform`]s, just like bevy's built in systems.
pub fn propagate_low_precision(
mut stats: ResMut<crate::timing::PropagationStats>,
mut stats: Option<ResMut<crate::timing::PropagationStats>>,
root_parents: Query<
Ref<GlobalTransform>,
(
@ -168,7 +172,7 @@ impl Grid {
),
>,
) {
let start = bevy_platform_support::time::Instant::now();
let start = bevy_platform::time::Instant::now();
let update_transforms = |low_precision_root, parent_transform: Ref<GlobalTransform>| {
// High precision global transforms are change-detected, and are only updated if that
// entity has moved relative to the floating origin's grid cell.
@ -201,12 +205,14 @@ impl Grid {
};
roots.par_iter().for_each(|(low_precision_root, parent)| {
if let Ok(parent_transform) = root_parents.get(parent.get()) {
if let Ok(parent_transform) = root_parents.get(parent.parent()) {
update_transforms(low_precision_root, parent_transform);
}
});
stats.low_precision_propagation += start.elapsed();
if let Some(stats) = stats.as_mut() {
stats.low_precision_propagation += start.elapsed();
}
}
/// Recursively propagates the transforms for `entity` and all of its descendants.
@ -254,29 +260,6 @@ impl Grid {
// - The top level iteration parallelizes on the roots of the hierarchy.
// - The caller ensures that each child has one and only one unique parent
// throughout the entire hierarchy.
//
// For example, consider the following malformed hierarchy:
//
// A
// / \
// B C
// \ /
// D
//
// D has two parents, B and C. If the propagation passes through C, but the ChildOf
// component on D points to B, the above check will panic as the origin parent does
// match the recorded parent.
//
// Also consider the following case, where A and B are roots:
//
// A B
// \ /
// C D
// \ /
// E
//
// Even if these A and B start two separate tasks running in parallel, one of them
// will panic before attempting to mutably access E.
(unsafe { transform_query.get_unchecked(entity) }) else {
return;
};
@ -291,7 +274,7 @@ impl Grid {
let Some(children) = children else { return };
for (child, child_of) in parent_query.iter_many(children) {
assert_eq!(
child_of.parent, entity,
child_of.parent(), entity,
"Malformed hierarchy. This probably means that your hierarchy has been improperly maintained, or contains a cycle"
);
// SAFETY: The caller guarantees that `transform_query` will not be fetched for any
@ -314,6 +297,7 @@ impl Grid {
#[cfg(test)]
mod tests {
use crate::plugin::BigSpaceMinimalPlugins;
use crate::prelude::*;
use bevy::prelude::*;
@ -323,9 +307,8 @@ mod tests {
struct Test;
let mut app = App::new();
app.add_plugins(BigSpacePlugin::default()).add_systems(
Startup,
|mut commands: Commands| {
app.add_plugins(BigSpaceMinimalPlugins)
.add_systems(Startup, |mut commands: Commands| {
commands.spawn_big_space_default(|root| {
root.spawn_spatial(FloatingOrigin);
root.spawn_spatial((
@ -340,8 +323,7 @@ mod tests {
));
});
});
},
);
});
app.update();

View File

@ -4,9 +4,9 @@ use alloc::vec::Vec;
use core::hash::{BuildHasher, Hash, Hasher};
use crate::prelude::*;
use bevy_ecs::{prelude::*, relationship::Relationship};
use bevy_ecs::prelude::*;
use bevy_math::IVec3;
use bevy_platform_support::{hash::FixedHasher, time::Instant};
use bevy_platform::{hash::FixedHasher, time::Instant};
use bevy_reflect::Reflect;
use super::{ChangedGridHashes, GridHashMapFilter};
@ -92,7 +92,7 @@ impl GridHash {
/// this module. This allows us to optimize change detection using [`ChangedGridHashes`].
#[inline]
pub(super) fn new(parent: &ChildOf, cell: &GridCell) -> Self {
Self::from_parent(parent.get(), cell)
Self::from_parent(parent.parent(), cell)
}
#[inline]

View File

@ -6,7 +6,7 @@ use core::marker::PhantomData;
use super::GridHashMapFilter;
use crate::prelude::*;
use bevy_ecs::{entity::EntityHash, prelude::*};
use bevy_platform_support::{
use bevy_platform::{
collections::{HashMap, HashSet},
hash::PassHash,
prelude::*,
@ -200,13 +200,13 @@ where
/// "U" exceed the `max_depth` (radius), iteration will stop. Even if the "U" loops back within
/// the radius, those cells will never be visited.
///
/// Also note that the `max_depth` (radius) is a chebyshev distance, not a euclidean distance.
/// Also note that the `max_depth` (radius) is a Chebyshev distance, not a Euclidean distance.
#[doc(alias = "bfs")]
pub fn flood<'a>(
&'a self,
pub fn flood(
&self,
seed: &GridHash,
max_depth: Option<GridPrecision>,
) -> impl Iterator<Item = Neighbor<'a>> {
) -> impl Iterator<Item = Neighbor> {
let starting_cell_cell = seed.cell();
ContiguousNeighborsIter {
initial_hash: Some(*seed),

View File

@ -5,7 +5,7 @@ use core::marker::PhantomData;
use crate::prelude::*;
use bevy_app::prelude::*;
use bevy_ecs::{prelude::*, query::QueryFilter};
use bevy_platform_support::prelude::*;
use bevy_platform::prelude::*;
pub mod component;
pub mod map;
@ -25,6 +25,16 @@ pub struct GridHashPlugin<F = ()>(PhantomData<F>)
where
F: GridHashMapFilter;
impl<F> GridHashPlugin<F>
where
F: GridHashMapFilter,
{
/// Create a new instance of [`GridHashPlugin`].
pub fn new() -> Self {
Self(PhantomData)
}
}
impl<F> Plugin for GridHashPlugin<F>
where
F: GridHashMapFilter,
@ -38,7 +48,7 @@ where
(
GridHash::update::<F>
.in_set(GridHashMapSystem::UpdateHash)
.after(FloatingOriginSystem::RecenterLargeTransforms),
.after(BigSpaceSystems::RecenterLargeTransforms),
GridHashMap::<F>::update
.in_set(GridHashMapSystem::UpdateMap)
.after(GridHashMapSystem::UpdateHash),
@ -47,7 +57,7 @@ where
}
}
impl<F: GridHashMapFilter> Default for GridHashPlugin<F> {
impl Default for GridHashPlugin<()> {
fn default() -> Self {
Self(PhantomData)
}
@ -68,7 +78,7 @@ pub enum GridHashMapSystem {
/// hashing.The trait is automatically implemented for all compatible types, like [`With`] or
/// [`Without`].
///
/// By default, this is `()`, but it can be overidden when adding the [`GridHashPlugin`] and
/// By default, this is `()`, but it can be overridden when adding the [`GridHashPlugin`] and
/// [`GridHashMap`]. For example, if you use `With<Players>` as your filter, only `Player`s would be
/// considered when building spatial hash maps. This is useful when you only care about querying
/// certain entities, and want to avoid the plugin doing bookkeeping work for entities you don't
@ -111,8 +121,9 @@ impl<F: GridHashMapFilter> Default for ChangedGridHashes<F> {
// hash ever recomputed? Is it removed? Is the spatial map updated?
#[cfg(test)]
mod tests {
use crate::plugin::BigSpaceMinimalPlugins;
use crate::{hash::map::SpatialEntryToEntities, prelude::*};
use bevy_platform_support::{collections::HashSet, sync::OnceLock};
use bevy_platform::{collections::HashSet, sync::OnceLock};
#[test]
fn entity_despawn() {
@ -128,7 +139,7 @@ mod tests {
};
let mut app = App::new();
app.add_plugins(GridHashPlugin::<()>::default())
app.add_plugins(GridHashPlugin::default())
.add_systems(Update, setup)
.update();
@ -183,7 +194,7 @@ mod tests {
};
let mut app = App::new();
app.add_plugins(GridHashPlugin::<()>::default())
app.add_plugins(GridHashPlugin::default())
.add_systems(Update, setup);
app.update();
@ -260,7 +271,7 @@ mod tests {
};
let mut app = App::new();
app.add_plugins(GridHashPlugin::<()>::default())
app.add_plugins(GridHashPlugin::default())
.add_systems(Startup, setup);
app.update();
@ -310,9 +321,9 @@ mod tests {
let mut app = App::new();
app.add_plugins((
GridHashPlugin::<()>::default(),
GridHashPlugin::<With<Player>>::default(),
GridHashPlugin::<Without<Player>>::default(),
GridHashPlugin::default(),
GridHashPlugin::<With<Player>>::new(),
GridHashPlugin::<Without<Player>>::new(),
))
.add_systems(Startup, setup)
.update();
@ -365,7 +376,7 @@ mod tests {
};
let mut app = App::new();
app.add_plugins((BigSpacePlugin::default(), GridHashPlugin::<()>::default()))
app.add_plugins((BigSpaceMinimalPlugins, GridHashPlugin::default()))
.add_systems(Startup, setup);
app.update();

View File

@ -4,8 +4,8 @@ use core::{hash::Hash, marker::PhantomData, ops::Deref};
use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use bevy_platform_support::prelude::*;
use bevy_platform_support::{
use bevy_platform::prelude::*;
use bevy_platform::{
collections::{HashMap, HashSet},
hash::PassHash,
time::Instant,
@ -21,10 +21,17 @@ pub struct GridPartitionPlugin<F = ()>(PhantomData<F>)
where
F: GridHashMapFilter;
impl<F> Default for GridPartitionPlugin<F>
impl<F> GridPartitionPlugin<F>
where
F: GridHashMapFilter,
{
/// Create a new instance of [`GridPartitionPlugin`].
pub fn new() -> Self {
Self(PhantomData)
}
}
impl Default for GridPartitionPlugin<()> {
fn default() -> Self {
Self(PhantomData)
}
@ -284,7 +291,7 @@ where
ComputeTaskPool::get(),
None,
|_, affected_cells| {
let _task_span = tracing::info_span!("parallel partition split").entered();
let _task_span = bevy_log::info_span!("parallel partition split").entered();
affected_cells
.iter_mut()
.filter_map(|(id, adjacent_hashes)| {
@ -355,10 +362,9 @@ mod private {
use super::{GridCell, GridHash};
use crate::precision::GridPrecision;
use bevy_ecs::prelude::*;
use bevy_platform_support::{collections::HashSet, hash::PassHash, prelude::*};
use bevy_platform::{collections::HashSet, hash::PassHash, prelude::*};
/// A group of nearby [`GridCell`](crate::GridCell)s in an island disconnected from all other
/// [`GridCell`](crate::GridCell)s.
/// A group of nearby [`GridCell`]s on an island disconnected from all other [`GridCell`]s.
#[derive(Debug)]
pub struct GridPartition {
grid: Entity,

View File

@ -85,7 +85,7 @@
//! can exist in the high precision hierarchy. This allows you to load in GLTFs or other
//! low-precision entity hierarchies with no added effort or cost.
//!
//! While using the [`BigSpacePlugin`], the position of entities is now defined with the [`Grid`],
//! While using the [`BigSpaceDefaultPlugins`], the position of entities is now defined with the [`Grid`],
//! [`GridCell`], and [`Transform`] components. The `Grid` is a large integer grid of cells;
//! entities are located within this grid as children using the `GridCell` component. Finally, the
//! `Transform` is used to position the entity relative to the center of its `GridCell`. If an
@ -137,9 +137,9 @@
//! # Usage
//!
//! To start using this plugin, you will first need to choose how big your world should be! Do you
//! need an i8, or an i128? See [`GridPrecision`] for more details and documentation.
//! need an i8, or an i128? See [`precision`] for more details and documentation.
//!
//! 1. Add the [`BigSpacePlugin`] to your `App`
//! 1. Add the [`BigSpaceDefaultPlugins`] to your `App`
//! 2. Spawn a [`BigSpace`] with [`spawn_big_space`](BigSpaceCommands::spawn_big_space), and add
//! entities to it.
//! 3. Add the [`FloatingOrigin`] to your active camera in the [`BigSpace`].
@ -208,6 +208,7 @@ use prelude::*;
pub(crate) mod portable_par;
pub mod bevy_compat;
pub mod bundles;
pub mod commands;
pub mod floating_origins;
@ -242,14 +243,12 @@ pub mod prelude {
partition::{GridPartition, GridPartitionId, GridPartitionMap, GridPartitionPlugin},
GridHashMapSystem, GridHashPlugin,
};
pub use plugin::{BigSpacePlugin, FloatingOriginSystem};
pub use plugin::{BigSpaceDefaultPlugins, BigSpaceSystems};
pub use precision::GridPrecision;
pub use world_query::{GridTransform, GridTransformOwned, GridTransformReadOnly};
#[cfg(feature = "camera")]
pub use camera::{CameraController, CameraControllerPlugin};
#[cfg(feature = "debug")]
pub use debug::FloatingOriginDebugPlugin;
pub use camera::BigSpaceCameraController;
}
/// Contains the [`GridPrecision`] integer index type, which defines how much precision is available
@ -269,7 +268,7 @@ pub mod prelude {
/// - `i64`: 19.5 million light years = ~100 times the width of the milky way galaxy
/// - `i128`: 3.6e+26 light years = ~3.9e+15 times the width of the observable universe
///
/// where `usable_edge_length = 2^(integer_bits) * cell_edge_length`, resulting in a worst case
/// where `usable_edge_length = 2^(integer_bits) * cell_edge_length`, resulting in the worst case
/// precision of 0.5mm in any of these cases.
///
/// This can also be used for small scales. With a cell edge length of `1e-11`, and using `i128`,

View File

@ -1,36 +1,64 @@
//! The bevy plugin for `big_space`.
use crate::prelude::*;
use alloc::vec::Vec;
use bevy_app::prelude::*;
use crate::*;
use bevy_app::{prelude::*, PluginGroupBuilder};
use bevy_ecs::prelude::*;
use bevy_transform::prelude::*;
/// Add this plugin to your [`App`] for floating origin functionality.
pub struct BigSpacePlugin {
validate_hierarchies: bool,
}
pub use crate::{timing::BigSpaceTimingStatsPlugin, validation::BigSpaceValidationPlugin};
#[cfg(feature = "camera")]
pub use camera::BigSpaceCameraControllerPlugin;
#[cfg(feature = "debug")]
pub use debug::BigSpaceDebugPlugin;
impl BigSpacePlugin {
/// Create a big space plugin, and specify whether hierarchy validation should be enabled.
pub fn new(validate_hierarchies: bool) -> Self {
Self {
validate_hierarchies,
}
/// Set of plugins needed for bare-bones floating origin functionality.
pub struct BigSpaceMinimalPlugins;
impl PluginGroup for BigSpaceMinimalPlugins {
fn build(self) -> PluginGroupBuilder {
PluginGroupBuilder::start::<Self>()
.add(BigSpaceCorePlugin)
.add(BigSpacePropagationPlugin)
}
}
impl Default for BigSpacePlugin {
fn default() -> Self {
Self {
validate_hierarchies: cfg!(debug_assertions),
/// All plugins needed for the core functionality of [`big_space`](crate).
///
/// By default,
/// - [`BigSpaceValidationPlugin`] is enabled in debug, and disabled in release.
/// - [`BigSpaceDebugPlugin`] is enabled if the `debug` feature is enabled.
/// - [`BigSpaceCameraControllerPlugin`] is enabled if the `camera` feature is enabled.
///
/// Hierarchy validation is not behind a feature flag because it does not add dependencies.
pub struct BigSpaceDefaultPlugins;
impl PluginGroup for BigSpaceDefaultPlugins {
fn build(self) -> PluginGroupBuilder {
let mut group = PluginGroupBuilder::start::<Self>();
group = group
.add_group(BigSpaceMinimalPlugins)
.add(BigSpaceTimingStatsPlugin)
.add(BigSpaceValidationPlugin);
#[cfg(not(debug_assertions))]
{
group = group.disable::<BigSpaceValidationPlugin>();
}
#[cfg(feature = "debug")]
{
group = group.add(BigSpaceDebugPlugin);
}
#[cfg(feature = "camera")]
{
group = group.add(BigSpaceCameraControllerPlugin);
}
group
}
}
#[allow(missing_docs)]
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
pub enum FloatingOriginSystem {
pub enum BigSpaceSystems {
Init,
RecenterLargeTransforms,
LocalFloatingOrigins,
@ -38,228 +66,89 @@ pub enum FloatingOriginSystem {
PropagateLowPrecision,
}
impl Plugin for BigSpacePlugin {
/// Core setup needed for all uses of big space - reflection and grid cell recentering.
///
/// This plugin does not handle any transform propagation it only maintains the local transforms and
/// grid cells.
pub struct BigSpaceCorePlugin;
impl Plugin for BigSpaceCorePlugin {
fn build(&self, app: &mut App) {
// Performance timings
app.add_plugins(crate::timing::TimingStatsPlugin);
let system_set_config = || {
(
Grid::tag_low_precision_roots // loose ordering on this set
.after(FloatingOriginSystem::Init)
.before(FloatingOriginSystem::PropagateLowPrecision),
(
GridCell::recenter_large_transforms,
BigSpace::find_floating_origin,
)
.in_set(FloatingOriginSystem::RecenterLargeTransforms),
LocalFloatingOrigin::compute_all
.in_set(FloatingOriginSystem::LocalFloatingOrigins)
.after(FloatingOriginSystem::RecenterLargeTransforms),
Grid::propagate_high_precision
.in_set(FloatingOriginSystem::PropagateHighPrecision)
.after(FloatingOriginSystem::LocalFloatingOrigins),
Grid::propagate_low_precision
.in_set(FloatingOriginSystem::PropagateLowPrecision)
.after(FloatingOriginSystem::PropagateHighPrecision),
)
.in_set(TransformSystem::TransformPropagate)
};
app
// Reflect
.register_type::<Transform>()
app.register_type::<Transform>()
.register_type::<GlobalTransform>()
.register_type::<TransformTreeChanged>()
.register_type::<GridCell>()
.register_type::<Grid>()
.register_type::<BigSpace>()
.register_type::<FloatingOrigin>()
// Meat of the plugin, once on startup, as well as every update
.add_systems(PostStartup, system_set_config())
.add_systems(PostUpdate, system_set_config())
// Validation
.add_systems(
PostUpdate,
crate::validation::validate_hierarchy::<crate::validation::SpatialHierarchyRoot>
.after(TransformSystem::TransformPropagate)
.run_if({
let run = self.validate_hierarchies;
move || run
}),
)
// These are the bevy transform propagation systems. Because these start from the root
// of the hierarchy, and BigSpace bundles (at the root) do not contain a Transform,
// these systems will not interact with any high precision entities in big space. These
// systems are added for ecosystem compatibility with bevy, although the rendered
// behavior might look strange if they share a camera with one using the floating
// origin.
//
// This is most useful for bevy_ui, which relies on the transform systems to work, or if
// you want to render a camera that only needs to render a low-precision scene.
.add_systems(
PostStartup,
(
propagate_parent_transforms,
bevy_transform::systems::sync_simple_transforms,
)
.in_set(TransformSystem::TransformPropagate),
)
.add_systems(
PostUpdate,
(
propagate_parent_transforms,
bevy_transform::systems::sync_simple_transforms,
)
.in_set(TransformSystem::TransformPropagate),
GridCell::recenter_large_transforms
.in_set(BigSpaceSystems::RecenterLargeTransforms),
);
}
fn cleanup(&self, app: &mut App) {
if app.is_plugin_added::<TransformPlugin>() {
panic!("\nERROR: Bevy's default transformation plugin must be disabled while using `big_space`: \n\tDefaultPlugins.build().disable::<TransformPlugin>();\n");
}
}
}
/// Copied from bevy. This is the simpler propagation implementation that doesn't use dirty tree
/// marking. This is needed because dirty tree marking doesn't start from the root, and will end up
/// doing the work for big space hierarchies, which it cannot affect anyway.
pub fn propagate_parent_transforms(
mut root_query: Query<
(Entity, &Children, Ref<Transform>, &mut GlobalTransform),
Without<ChildOf>,
>,
mut orphaned: RemovedComponents<ChildOf>,
transform_query: Query<
(Ref<Transform>, &mut GlobalTransform, Option<&Children>),
With<ChildOf>,
>,
child_query: Query<(Entity, Ref<ChildOf>), With<GlobalTransform>>,
mut orphaned_entities: Local<Vec<Entity>>,
) {
orphaned_entities.clear();
orphaned_entities.extend(orphaned.read());
orphaned_entities.sort_unstable();
root_query.par_iter_mut().for_each(
|(entity, children, transform, mut global_transform)| {
let changed = transform.is_changed() || global_transform.is_added() || orphaned_entities.binary_search(&entity).is_ok();
if changed {
*global_transform = GlobalTransform::from(*transform);
}
for (child, child_of) in child_query.iter_many(children) {
assert_eq!(
child_of.parent, entity,
"Malformed hierarchy. This probably means that your hierarchy has been improperly maintained, or contains a cycle"
);
// SAFETY:
// - `child` must have consistent parentage, or the above assertion would panic.
// Since `child` is parented to a root entity, the entire hierarchy leading to it
// is consistent.
// - We may operate as if all descendants are consistent, since
// `propagate_recursive` will panic before continuing to propagate if it
// encounters an entity with inconsistent parentage.
// - Since each root entity is unique and the hierarchy is consistent and
// forest-like, other root entities' `propagate_recursive` calls will not conflict
// with this one.
// - Since this is the only place where `transform_query` gets used, there will be
// no conflicting fetches elsewhere.
#[expect(unsafe_code, reason = "`propagate_recursive()` is unsafe due to its use of `Query::get_unchecked()`.")]
unsafe {
propagate_recursive(
&global_transform,
&transform_query,
&child_query,
child,
changed || child_of.is_changed(),
);
}
}
},
);
}
/// Recursively propagates the transforms for `entity` and all of its descendants.
/// Adds transform propagation, computing `GlobalTransforms` from hierarchies of [`Transform`],
/// [`GridCell`], [`Grid`], and [`BigSpace`]s.
///
/// # Panics
/// Disable Bevy's [`TransformPlugin`] while using this plugin.
///
/// If `entity`'s descendants have a malformed hierarchy, this function will panic occur before
/// propagating the transforms of any malformed entities and their descendants.
///
/// # Safety
///
/// - While this function is running, `transform_query` must not have any fetches for `entity`,
/// nor any of its descendants.
/// - The caller must ensure that the hierarchy leading to `entity` is well-formed and must
/// remain as a tree or a forest. Each entity must have at most one parent.
#[expect(
unsafe_code,
reason = "This function uses `Query::get_unchecked()`, which can result in multiple mutable references if the preconditions are not met."
)]
unsafe fn propagate_recursive(
parent: &GlobalTransform,
transform_query: &Query<
(Ref<Transform>, &mut GlobalTransform, Option<&Children>),
With<ChildOf>,
>,
child_query: &Query<(Entity, Ref<ChildOf>), With<GlobalTransform>>,
entity: Entity,
mut changed: bool,
) {
let (global_matrix, children) = {
let Ok((transform, mut global_transform, children)) =
// SAFETY: This call cannot create aliased mutable references.
// - The top level iteration parallelizes on the roots of the hierarchy.
// - The caller ensures that each child has one and only one unique parent throughout
// the entire hierarchy.
//
// For example, consider the following malformed hierarchy:
//
// A
// / \
// B C
// \ /
// D
//
// D has two parents, B and C. If the propagation passes through C, but the ChildOf
// component on D points to B, the above check will panic as the origin parent does
// match the recorded parent.
//
// Also consider the following case, where A and B are roots:
//
// A B
// \ /
// C D
// \ /
// E
//
// Even if these A and B start two separate tasks running in parallel, one of them will
// panic before attempting to mutably access E.
(unsafe { transform_query.get_unchecked(entity) }) else {
return;
/// This also adds support for Bevy's low-precision [`Transform`] hierarchies.
pub struct BigSpacePropagationPlugin;
impl Plugin for BigSpacePropagationPlugin {
fn build(&self, app: &mut App) {
let configs = || {
(
Grid::tag_low_precision_roots // loose ordering on this set
.after(BigSpaceSystems::Init)
.before(BigSpaceSystems::PropagateLowPrecision),
BigSpace::find_floating_origin.in_set(BigSpaceSystems::RecenterLargeTransforms),
LocalFloatingOrigin::compute_all
.in_set(BigSpaceSystems::LocalFloatingOrigins)
.after(BigSpaceSystems::RecenterLargeTransforms),
Grid::propagate_high_precision
.in_set(BigSpaceSystems::PropagateHighPrecision)
.after(BigSpaceSystems::LocalFloatingOrigins),
Grid::propagate_low_precision
.in_set(BigSpaceSystems::PropagateLowPrecision)
.after(BigSpaceSystems::PropagateHighPrecision),
)
.in_set(TransformSystem::TransformPropagate)
};
changed |= transform.is_changed() || global_transform.is_added();
if changed {
*global_transform = parent.mul_transform(*transform);
}
(global_transform, children)
};
app.add_systems(PostStartup, configs())
.add_systems(PostUpdate, configs());
let Some(children) = children else { return };
for (child, child_of) in child_query.iter_many(children) {
assert_eq!(
child_of.parent, entity,
"Malformed hierarchy. This probably means that your hierarchy has been improperly maintained, or contains a cycle"
);
// SAFETY: The caller guarantees that `transform_query` will not be fetched for any
// descendants of `entity`, so it is safe to call `propagate_recursive` for each child.
// These are the bevy transform propagation systems. Because these start from the root
// of the hierarchy, and BigSpace bundles (at the root) do not contain a Transform,
// these systems will not interact with any high-precision entities in `big_space`. These
// systems are added for ecosystem compatibility with bevy, although the rendered
// behavior might look strange if they share a camera with one using the floating
// origin.
//
// The above assertion ensures that each child has one and only one unique parent
// throughout the entire hierarchy.
unsafe {
propagate_recursive(
global_matrix.as_ref(),
transform_query,
child_query,
child,
changed || child_of.is_changed(),
);
}
// This is most useful for bevy_ui, which relies on the transform systems to work, or if
// you want to render a camera that only needs to render a low-precision scene.
app.add_systems(
PostStartup,
(
bevy_compat::propagate_parent_transforms,
bevy_transform::systems::sync_simple_transforms,
)
.in_set(TransformSystem::TransformPropagate),
)
.add_systems(
PostUpdate,
(
bevy_compat::propagate_parent_transforms,
bevy_transform::systems::sync_simple_transforms,
)
.in_set(TransformSystem::TransformPropagate),
);
}
}

View File

@ -8,12 +8,13 @@ use core::ops::DerefMut;
#[derive(Default)]
pub struct PortableParallel<T: Send>(
#[cfg(feature = "std")] bevy_utils::Parallel<T>,
#[cfg(not(feature = "std"))] bevy_platform_support::sync::RwLock<Option<T>>,
#[cfg(not(feature = "std"))] bevy_platform::sync::RwLock<Option<T>>,
);
/// A scope guard of a `Parallel`, when this struct is dropped ,the value will writeback to its `Parallel`
/// A scope guard of a `Parallel`, when this struct is dropped ,the value will write back to its
/// `Parallel`
impl<T: Send> PortableParallel<T> {
/// Gets a mutable iterator over all of the per-thread queues.
/// Gets a mutable iterator over all the per-thread queues.
pub fn iter_mut(&mut self) -> impl Iterator<Item = impl DerefMut<Target = T> + '_> {
#[cfg(feature = "std")]
{
@ -25,7 +26,7 @@ impl<T: Send> PortableParallel<T> {
}
}
/// Clears all of the stored thread local values.
/// Clears all the stored thread local values.
pub fn clear(&mut self) {
#[cfg(feature = "std")]
self.0.clear();
@ -54,7 +55,7 @@ impl<T: Default + Send> PortableParallel<T> {
/// Mutably borrows the thread-local value.
///
/// If there is no thread-local value, it will be initialized to it's default.
/// If there is no thread-local value, it will be initialized to its default.
pub fn borrow_local_mut(&self) -> impl DerefMut<Target = T> + '_ {
#[cfg(feature = "std")]
let ret = self.0.borrow_local_mut();
@ -74,7 +75,7 @@ impl<T: Default + Send> PortableParallel<T> {
/// Needed until Parallel is portable. This assumes the value is `Some`.
#[cfg(not(feature = "std"))]
mod no_std_deref {
use bevy_platform_support::sync::RwLockWriteGuard;
use bevy_platform::sync::RwLockWriteGuard;
use core::ops::{Deref, DerefMut};
pub struct UncheckedDerefMutWrapper<'a, T>(pub(super) RwLockWriteGuard<'a, Option<T>>);

View File

@ -1,10 +1,11 @@
use crate::plugin::BigSpaceMinimalPlugins;
use crate::prelude::*;
use bevy::prelude::*;
#[test]
fn changing_floating_origin_updates_global_transform() {
let mut app = App::new();
app.add_plugins(BigSpacePlugin::default());
app.add_plugins(BigSpaceMinimalPlugins);
let first = app
.world_mut()
@ -45,7 +46,7 @@ fn changing_floating_origin_updates_global_transform() {
#[test]
fn child_global_transforms_are_updated_when_floating_origin_changes() {
let mut app = App::new();
app.add_plugins(BigSpacePlugin::default());
app.add_plugins(BigSpaceMinimalPlugins);
let first = app
.world_mut()

View File

@ -10,9 +10,9 @@ use bevy_reflect::prelude::*;
use bevy_transform::TransformSystem;
/// Summarizes plugin performance timings
pub struct TimingStatsPlugin;
pub struct BigSpaceTimingStatsPlugin;
impl Plugin for TimingStatsPlugin {
impl Plugin for BigSpaceTimingStatsPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<PropagationStats>()
.register_type::<PropagationStats>()
@ -24,7 +24,7 @@ impl Plugin for TimingStatsPlugin {
.register_type::<SmoothedStat<GridHashStats>>()
.add_systems(
PostUpdate,
(GridHashStats::reset, PropagationStats::reset).in_set(FloatingOriginSystem::Init),
(GridHashStats::reset, PropagationStats::reset).in_set(BigSpaceSystems::Init),
)
.add_systems(
PostUpdate,

View File

@ -1,7 +1,8 @@
//! Tools for validating high-precision transform hierarchies
use bevy_app::{App, Plugin, PostUpdate};
use bevy_ecs::prelude::*;
use bevy_platform_support::{
use bevy_platform::{
collections::{HashMap, HashSet},
prelude::*,
};
@ -14,6 +15,17 @@ struct ValidationStackEntry {
children: Vec<Entity>,
}
/// Adds hierarchy validation features.
pub struct BigSpaceValidationPlugin;
impl Plugin for BigSpaceValidationPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
PostUpdate,
validate_hierarchy::<SpatialHierarchyRoot>.after(TransformSystem::TransformPropagate),
);
}
}
#[derive(Default, Resource)]
struct ValidatorCaches {
query_state_cache: HashMap<&'static str, QueryState<(Entity, Option<&'static Children>)>>,
@ -101,7 +113,7 @@ pub fn validate_hierarchy<V: 'static + ValidHierarchyNode + Default>(world: &mut
inspect.push('\n');
});
tracing::error!("
bevy_log::error!("
-------------------------------------------
big_space hierarchy validation error report
-------------------------------------------
@ -134,19 +146,19 @@ If possible, use commands.spawn_big_space(), which prevents these errors, instea
/// kinds of nodes its children can be. This can be used recursively to validate an entire entity
/// hierarchy by starting from the root.
pub trait ValidHierarchyNode: sealed::CloneHierarchy + Send + Sync {
/// Add filters to a query to check if entities match this type of node
fn match_self(&self, query: &mut QueryBuilder<(Entity, Option<&Children>)>);
/// The types of nodes that can be children of this node.
fn allowed_child_nodes(&self) -> Vec<Box<dyn ValidHierarchyNode>>;
/// A unique identifier of this type
fn name(&self) -> &'static str {
core::any::type_name::<Self>()
}
/// Add filters to a query to check if entities match this type of node
fn match_self(&self, query: &mut QueryBuilder<(Entity, Option<&Children>)>);
/// The types of nodes that can be children of this node.
fn allowed_child_nodes(&self) -> Vec<Box<dyn ValidHierarchyNode>>;
}
mod sealed {
use super::ValidHierarchyNode;
use bevy_platform_support::prelude::*;
use bevy_platform::prelude::*;
pub trait CloneHierarchy {
fn clone_box(&self) -> Box<dyn ValidHierarchyNode>;