Bevy 0.16 (#46)

# Objective

- Working branch to target all fixes for bevy 0.16

Co-authored-by: Zachary Harrold <zac@harrold.com.au>
This commit is contained in:
Aevyrie 2025-04-09 23:09:19 -07:00 committed by GitHub
parent 9bae63e4b4
commit 44ff1f32de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 861 additions and 455 deletions

View File

@ -29,7 +29,7 @@ jobs:
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2.7.0
- run: cargo check --features=all --all-targets
- run: cargo check --all-features --all-targets
check-no-defaults:
runs-on: ubuntu-latest
@ -38,7 +38,7 @@ jobs:
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2.7.0
- run: cargo check --no-default-features --all-targets
- run: cargo check --no-default-features --all-targets --features=libm
clippy:
runs-on: ubuntu-latest
@ -48,7 +48,7 @@ jobs:
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2.7.0
- run: rustup component add clippy
- run: cargo clippy --features=all --all-targets -- -D warnings
- run: cargo clippy --all-features --all-targets -- -D warnings
doc:
runs-on: ubuntu-latest
@ -57,7 +57,7 @@ jobs:
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2.7.0
- run: cargo doc --features=all --no-deps
- run: cargo doc --all-features --no-deps
env:
RUSTDOCFLAGS: -D warnings
@ -68,7 +68,7 @@ jobs:
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2.7.0
- run: cargo test --features=all
- run: cargo test --all-features
doctest:
runs-on: ubuntu-latest
@ -77,4 +77,4 @@ jobs:
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2.7.0
- run: cargo test --features=all --doc
- run: cargo test --all-features --doc

View File

@ -2,6 +2,12 @@
## UNRELEASED
### 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.
## v0.9.0 - 2024-12-23
### New: `GridCell` Spatial Hashing
Spatial hashing makes fast spatial queries and neighbor lookups possible. This release adds the `GridHashMap`, an automatically updated map of the entities in each grid cell. This makes it possible to query things like:
@ -37,5 +43,4 @@ The newly added types follow this pattern:
- `GridPartition`: Group of adjacent grid cells.
- `GridPartitionMap`: A map for finding independent partitions of entities.
It should now be more clear how all of the `Grid` types are related to each other.

View File

@ -9,42 +9,57 @@ repository = "https://github.com/aevyrie/big_space"
documentation = "https://docs.rs/crate/big_space/latest"
[features]
default = []
all = ["debug", "camera"] # Can't use all-features, integer type features are incompatible.
debug = ["bevy_gizmos", "bevy_color"]
camera = ["bevy_render", "bevy_time", "bevy_input"]
default = ["std"]
debug = ["std", "bevy_gizmos", "bevy_color"]
camera = ["std", "bevy_render", "bevy_time", "bevy_input"]
i8 = []
i16 = []
i32 = []
i64 = []
i128 = []
[dependencies]
tracing = "0.1" # Less deps than pulling in bevy_log
smallvec = "1.13.2" # Already used by bevy in commands
bevy_app = { version = "0.15.0", default-features = false }
bevy_ecs = { version = "0.15.0", default-features = true }
bevy_hierarchy = { version = "0.15.0", default-features = false }
bevy_math = { version = "0.15.0", default-features = false }
bevy_reflect = { version = "0.15.0", default-features = false }
bevy_tasks = { version = "0.15.0", default-features = false }
bevy_transform = { version = "0.15.0", default-features = false, features = [
"bevy-support",
] }
bevy_utils = { version = "0.15.0", default-features = false }
# Optional
bevy_color = { version = "0.15.0", default-features = false, optional = true }
bevy_gizmos = { version = "0.15.0", default-features = false, optional = true }
bevy_render = { version = "0.15.0", default-features = false, optional = true }
bevy_input = { version = "0.15.0", default-features = false, optional = true }
bevy_time = { version = "0.15.0", default-features = false, optional = true }
std = [
"bevy_app/std",
"bevy_ecs/std",
"bevy_math/std",
"bevy_reflect/std",
"bevy_tasks/std",
"bevy_transform/std",
"bevy_utils/std",
"bevy_platform_support/std",
"bevy_color?/std",
"bevy_input?/std",
"bevy_time?/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-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"] }
# 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 }
libm = { version = "0.2", default-features = false, optional = true }
[dev-dependencies]
big_space = { path = "", features = ["debug", "camera"] }
bevy = { version = "0.15.0", default-features = false, features = [
bevy = { version = "0.16.0-rc.3", default-features = false, features = [
"bevy_scene",
"bevy_asset",
"bevy_color",
"bevy_gltf",
"bevy_winit",
"default_font",
@ -61,7 +76,36 @@ noise = "0.9"
turborand = "0.10"
criterion = "0.5"
bytemuck = "1.20"
bevy_hanabi = "0.14"
# bevy_hanabi = "0.14" # TODO: Update
[lints.clippy]
doc_markdown = "warn"
manual_let_else = "warn"
match_same_arms = "warn"
redundant_closure_for_method_calls = "warn"
redundant_else = "warn"
semicolon_if_nothing_returned = "warn"
type_complexity = "allow"
undocumented_unsafe_blocks = "warn"
unwrap_or_default = "warn"
ptr_as_ptr = "warn"
ptr_cast_constness = "warn"
ref_as_ptr = "warn"
# see: https://github.com/bevyengine/bevy/pull/15375#issuecomment-2366966219
too_long_first_doc_paragraph = "allow"
std_instead_of_core = "warn"
std_instead_of_alloc = "warn"
alloc_instead_of_core = "warn"
[lints.rust]
missing_docs = "warn"
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(docsrs_dep)'] }
unsafe_code = "deny"
unsafe_op_in_unsafe_fn = "warn"
unused_qualifications = "warn"
[[bench]]
name = "benchmarks"
@ -70,18 +114,19 @@ harness = false
[[example]]
name = "debug"
path = "examples/debug.rs"
required-features = ["debug"]
doc-scrape-examples = false
[[example]]
name = "demo"
path = "examples/demo.rs"
required-features = ["i128"]
required-features = ["i128", "camera", "debug"]
doc-scrape-examples = false
[[example]]
name = "error_child"
path = "examples/error_child.rs"
required-features = ["camera", "debug"]
doc-scrape-examples = false
[[example]]
@ -92,25 +137,41 @@ doc-scrape-examples = false
[[example]]
name = "infinite"
path = "examples/infinite.rs"
required-features = ["i8"]
required-features = ["i8", "camera", "debug"]
doc-scrape-examples = false
[[example]]
name = "minimal"
path = "examples/minimal.rs"
required-features = ["camera"]
doc-scrape-examples = false
[[example]]
name = "particles"
path = "examples/particles.rs"
doc-scrape-examples = false
# TODO: Uncomment once bevy_hanabi is updated
# [[example]]
# name = "particles"
# path = "examples/particles.rs"
# doc-scrape-examples = false
[[example]]
name = "planets"
path = "examples/planets.rs"
required-features = ["camera"]
doc-scrape-examples = false
[[example]]
name = "small_scale"
path = "examples/small_scale.rs"
required-features = ["camera", "debug"]
doc-scrape-examples = false
[[example]]
name = "spatial_hash"
path = "examples/spatial_hash.rs"
required-features = ["camera"]
doc-scrape-examples = false
[[example]]
name = "split_screen"
path = "examples/split_screen.rs"
required-features = ["camera", "debug"]
doc-scrape-examples = false

View File

@ -5,7 +5,7 @@
<img src="https://raw.githubusercontent.com/aevyrie/big_space/refs/heads/main/assets/bigspacebanner.svg" width="80%">
Huge worlds, high performance, no dependencies, ecosystem compatibility. [Read the docs](https://docs.rs/big_space)
[![crates.io](https://img.shields.io/crates/v/big_space)](https://crates.io/crates/big_space)
[![docs.rs](https://docs.rs/big_space/badge.svg)](https://docs.rs/big_space)
[![test suite](https://github.com/aevyrie/big_space/actions/workflows/rust.yml/badge.svg)](https://github.com/aevyrie/big_space/actions/workflows/rust.yml)
@ -18,11 +18,10 @@ Huge worlds, high performance, no dependencies, ecosystem compatibility. [Read t
- Uses `Transform`, making it compatible with most of the Bevy ecosystem.
- No added dependencies.
- Absolute coordinates without drift, unlike camera-relative or periodic recentering solutions.
- Chunks the world into integer grids, from `i8` up to `i128`.
- Grids can be nested.
- Chunks the world into nestable integer grids, from `i8` up to `i128`.
- Spatial hashing for fast grid cell lookups and neighbor search.
- Spatial partitioning to group sets of disconnected entities.
- 3-5x faster than Bevy's transform propagation for wide hierarchies.
- Spatial partitioning to group sets of connected cells.
- Great performance scaling and parallelism with massive entity counts.
- 👉 [Extensive documentation you should read.](https://docs.rs/big_space)
![screenshot](https://github.com/user-attachments/assets/736a1dec-91a1-4ac1-9382-82084ebe6c1c)
@ -40,7 +39,8 @@ https://github.com/user-attachments/assets/9ce5283f-7d48-47dc-beef-9a7626858ed4
## Bevy Version Support
| bevy | big_space |
| ---- | --------- |
|------|-----------|
| 0.16 | 0.10 |
| 0.15 | 0.8, 0.9 |
| 0.14 | 0.7 |
| 0.13 | 0.5, 0.6 |

View File

@ -1,9 +1,11 @@
//! `big_space` benchmarks.
#![allow(clippy::type_complexity)]
#![allow(missing_docs)]
use bevy::prelude::*;
use big_space::prelude::*;
use core::{iter::repeat_with, ops::Neg};
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use std::{iter::repeat_with, ops::Neg};
use turborand::prelude::*;
criterion_group!(
@ -52,7 +54,7 @@ fn deep_hierarchy(c: &mut Criterion) {
fn translate(mut transforms: Query<&mut Transform>) {
transforms.iter_mut().for_each(|mut transform| {
transform.translation += Vec3::ONE;
})
});
}
let mut app = App::new();
@ -91,7 +93,7 @@ fn wide_hierarchy(c: &mut Criterion) {
fn translate(mut transforms: Query<&mut Transform>) {
transforms.iter_mut().for_each(|mut transform| {
transform.translation += Vec3::ONE;
})
});
}
let mut app = App::new();
@ -126,9 +128,9 @@ fn spatial_hashing(c: &mut Criterion) {
let rng = Rng::with_seed(342525);
let values: Vec<_> = repeat_with(|| {
[
rng.i64(-HALF_WIDTH..=HALF_WIDTH),
rng.i64(-HALF_WIDTH..=HALF_WIDTH),
rng.i64(-HALF_WIDTH..=HALF_WIDTH),
rng.i64(-HALF_WIDTH..=HALF_WIDTH) as GridPrecision,
rng.i64(-HALF_WIDTH..=HALF_WIDTH) as GridPrecision,
rng.i64(-HALF_WIDTH..=HALF_WIDTH) as GridPrecision,
]
})
.take(N_SPAWN)
@ -143,7 +145,7 @@ fn spatial_hashing(c: &mut Criterion) {
fn translate(mut cells: Query<&mut GridCell>) {
cells.iter_mut().take(N_MOVE).for_each(|mut cell| {
*cell += GridCell::ONE;
})
});
}
let mut app = App::new();
@ -204,7 +206,7 @@ fn spatial_hashing(c: &mut Criterion) {
// });
// });
fn setup_uniform<const HALF_EXTENT: i64>(mut commands: Commands) {
fn setup_uniform<const HALF_EXTENT: GridPrecision>(mut commands: Commands) {
commands.spawn_big_space(Grid::new(1.0, 0.0), |root| {
for x in HALF_EXTENT.neg()..HALF_EXTENT {
for y in HALF_EXTENT.neg()..HALF_EXTENT {
@ -226,7 +228,8 @@ fn spatial_hashing(c: &mut Criterion) {
let parent = app
.world_mut()
.query_filtered::<Entity, With<BigSpace>>()
.single(app.world());
.single(app.world())
.unwrap();
let spatial_map = app.world().resource::<GridHashMap>();
let hash = GridHash::__new_manual(parent, &GridCell { x: 0, y: 0, z: 0 });
let entry = spatial_map.get(&hash).unwrap();
@ -254,7 +257,8 @@ fn spatial_hashing(c: &mut Criterion) {
let parent = app
.world_mut()
.query_filtered::<Entity, With<BigSpace>>()
.single(app.world());
.single(app.world())
.unwrap();
let spatial_map = app.world().resource::<GridHashMap>();
let hash = GridHash::__new_manual(parent, &GridCell { x: 0, y: 0, z: 0 });
let entry = spatial_map.get(&hash).unwrap();
@ -288,9 +292,9 @@ fn hash_filtering(c: &mut Criterion) {
let rng = Rng::with_seed(342525);
let values: Vec<_> = repeat_with(|| {
[
rng.i64(-HALF_WIDTH..=HALF_WIDTH),
rng.i64(-HALF_WIDTH..=HALF_WIDTH),
rng.i64(-HALF_WIDTH..=HALF_WIDTH),
rng.i64(-HALF_WIDTH..=HALF_WIDTH) as GridPrecision,
rng.i64(-HALF_WIDTH..=HALF_WIDTH) as GridPrecision,
rng.i64(-HALF_WIDTH..=HALF_WIDTH) as GridPrecision,
]
})
.take(N_ENTITIES)

View File

@ -1,3 +1,4 @@
//! Demonstrates debugging visualization for `big_space` components.
#![allow(clippy::type_complexity)]
use bevy::{color::palettes, prelude::*};
@ -6,9 +7,9 @@ use big_space::prelude::*;
fn main() {
App::new()
.add_plugins((
DefaultPlugins,
DefaultPlugins.build().disable::<TransformPlugin>(),
BigSpacePlugin::default(),
big_space::debug::FloatingOriginDebugPlugin::default(),
FloatingOriginDebugPlugin::default(),
))
.add_systems(Startup, setup)
.add_systems(Update, (movement, rotation))
@ -26,7 +27,7 @@ fn movement(
Query<&mut Transform, With<Mover<3>>>,
Query<&mut Transform, With<Mover<4>>>,
)>,
) {
) -> Result {
let delta_translation = |offset: f32, scale: f32| -> Vec3 {
let t_1 = time.elapsed_secs() * 0.1 + offset;
let dt = time.delta_secs() * 0.1;
@ -38,10 +39,12 @@ fn movement(
p1 - p0
};
q.p0().single_mut().translation += delta_translation(20.0, 1.0);
q.p1().single_mut().translation += delta_translation(251.0, 1.0);
q.p2().single_mut().translation += delta_translation(812.0, 1.0);
q.p3().single_mut().translation += delta_translation(863.0, 0.4);
q.p0().single_mut()?.translation += delta_translation(20.0, 1.0);
q.p1().single_mut()?.translation += delta_translation(251.0, 1.0);
q.p2().single_mut()?.translation += delta_translation(812.0, 1.0);
q.p3().single_mut()?.translation += delta_translation(863.0, 0.4);
Ok(())
}
#[derive(Component)]

View File

@ -1,3 +1,5 @@
//! Demonstrates using the plugin over a wide range of scales, from protons to the universe.
use bevy::{
color::palettes,
prelude::*,
@ -13,10 +15,10 @@ use big_space::{
fn main() {
App::new()
.add_plugins((
DefaultPlugins,
DefaultPlugins.build().disable::<TransformPlugin>(),
BigSpacePlugin::default(),
FloatingOriginDebugPlugin::default(),
big_space::camera::CameraControllerPlugin::default(),
CameraControllerPlugin::default(),
))
.insert_resource(ClearColor(Color::BLACK))
.add_systems(Startup, (setup, ui_setup))
@ -79,10 +81,10 @@ fn setup(
}
#[derive(Component, Reflect)]
pub struct BigSpaceDebugText;
struct BigSpaceDebugText;
#[derive(Component, Reflect)]
pub struct FunFactText;
struct FunFactText;
fn ui_setup(mut commands: Commands) {
commands.spawn((
@ -125,13 +127,11 @@ fn highlight_nearest_sphere(
cameras: Query<&CameraController>,
objects: Query<&GlobalTransform>,
mut gizmos: Gizmos,
) {
let Some((entity, _)) = cameras.single().nearest_object() else {
return;
};
let Ok(transform) = objects.get(entity) else {
return;
) -> Result {
let Some((entity, _)) = cameras.single()?.nearest_object() else {
return Ok(());
};
let transform = objects.get(entity)?;
// Ignore rotation due to panicking in gizmos, as of bevy 0.13
let (scale, _, translation) = transform.to_scale_rotation_translation();
gizmos
@ -141,6 +141,7 @@ fn highlight_nearest_sphere(
Color::Srgba(palettes::basic::RED),
)
.resolution(128);
Ok(())
}
#[allow(clippy::type_complexity)]
@ -155,8 +156,8 @@ fn ui_text_system(
origin: Query<(Entity, GridTransformReadOnly), With<FloatingOrigin>>,
camera: Query<&CameraController>,
objects: Query<&Transform, With<Mesh3d>>,
) {
let (origin_entity, origin_pos) = origin.single();
) -> Result {
let (origin_entity, origin_pos) = origin.single()?;
let translation = origin_pos.transform.translation;
let grid_text = format!(
@ -170,7 +171,7 @@ fn ui_text_system(
);
let Some(grid) = grids.parent_grid(origin_entity) else {
return;
return Ok(());
};
let real_position = grid.grid_position_double(origin_pos.cell, origin_pos.transform);
@ -183,7 +184,7 @@ fn ui_text_system(
real_position.x as f32, real_position.y as f32, real_position.z as f32
);
let velocity = camera.single().velocity();
let velocity = camera.single()?.velocity();
let speed = velocity.0.length() / time.delta_secs_f64();
let camera_text = if speed > 3.0e8 {
format!("Speed: {:.0e} * speed of light", speed / 3.0e8)
@ -191,8 +192,8 @@ fn ui_text_system(
format!("Speed: {:.2e} m/s", speed)
};
let (nearest_text, fact_text) = if let Some(nearest) = camera.single().nearest_object() {
let dia = objects.get(nearest.0).unwrap().scale.max_element();
let (nearest_text, fact_text) = if let Some(nearest) = camera.single()?.nearest_object() {
let dia = objects.get(nearest.0)?.scale.max_element();
let (fact_dia, fact) = closest(dia);
let dist = nearest.1;
let multiple = dia / fact_dia;
@ -206,13 +207,15 @@ fn ui_text_system(
("".into(), "".into())
};
let mut debug_text = debug_text.single_mut();
let mut debug_text = debug_text.single_mut()?;
debug_text.0.0 = format!(
"{grid_text}\n{translation_text}\n\n{real_position_f64_text}\n{real_position_f32_text}\n\n{camera_text}\n{nearest_text}"
);
fun_text.single_mut().0 = fact_text
fun_text.single_mut()?.0 = fact_text;
Ok(())
}
fn closest<'a>(diameter: f32) -> (f32, &'a str) {
@ -262,10 +265,8 @@ fn cursor_grab_system(
mut cam: ResMut<CameraInput>,
btn: Res<ButtonInput<MouseButton>>,
key: Res<ButtonInput<KeyCode>>,
) {
let Some(mut window) = windows.get_single_mut().ok() else {
return;
};
) -> Result {
let mut window = windows.single_mut()?;
if btn.just_pressed(MouseButton::Left) {
window.cursor_options.grab_mode = CursorGrabMode::Locked;
@ -280,4 +281,6 @@ fn cursor_grab_system(
// window.mode = WindowMode::Windowed;
cam.defaults_disabled = true;
}
Ok(())
}

View File

@ -10,7 +10,10 @@ use big_space::prelude::*;
fn main() {
App::new()
.add_plugins((DefaultPlugins, BigSpacePlugin::default()))
.add_plugins((
DefaultPlugins.build().disable::<TransformPlugin>(),
BigSpacePlugin::default(),
))
.add_systems(Startup, (setup_scene, setup_ui))
.add_systems(Update, (rotator_system, toggle_plugin))
.run();
@ -22,8 +25,8 @@ fn main() {
/// floating point error when we disable this plugin.
///
/// This plugin can function much further from the origin without any issues. Try setting this to:
/// 10_000_000_000_000_000 with the default i64 feature, or
/// 10_000_000_000_000_000_000_000_000_000_000_000_000 with the i128 feature.
/// `10_000_000_000_000_000` with the default i64 feature, or
/// `10_000_000_000_000_000_000_000_000_000_000_000_000` with the i128 feature.
const DISTANCE: GridPrecision = 2_000_000;
/// Move the floating origin back to the "true" origin when the user presses the spacebar to emulate
@ -35,14 +38,16 @@ fn toggle_plugin(
mut text: Query<&mut Text>,
mut disabled: Local<bool>,
mut floating_origin: Query<(Entity, &mut GridCell), With<FloatingOrigin>>,
) {
) -> Result {
if input.just_pressed(KeyCode::Space) {
*disabled = !*disabled;
}
let this_grid = grids.parent_grid(floating_origin.single().0).unwrap();
let this_grid = grids
.parent_grid(floating_origin.single().unwrap().0)
.unwrap();
let mut origin_cell = floating_origin.single_mut().1;
let mut origin_cell = floating_origin.single_mut()?.1;
let index_max = DISTANCE / this_grid.cell_edge_length() as GridPrecision;
let increment = index_max / 100;
@ -73,14 +78,16 @@ fn toggle_plugin(
.as_bytes()
.rchunks(3)
.rev()
.map(std::str::from_utf8)
.map(core::str::from_utf8)
.collect::<Result<Vec<&str>, _>>()
.unwrap()
.join(",") // separator
};
text.single_mut().0 =
format!("Press Spacebar to toggle: {msg}\nCamera distance to floating origin: {}\nMesh distance from origin: {}", thousands(dist), thousands(DISTANCE))
text.single_mut()?.0 =
format!("Press Spacebar to toggle: {msg}\nCamera distance to floating origin: {}\nMesh distance from origin: {}", thousands(dist), thousands(DISTANCE));
Ok(())
}
#[derive(Component)]

View File

@ -1,15 +1,14 @@
//! This example demonstrates error accumulating from parent to children in nested grids.
use bevy::{math::DVec3, prelude::*};
use bevy_color::palettes;
use bevy::{color::palettes, math::DVec3, prelude::*};
use big_space::prelude::*;
fn main() {
App::new()
.add_plugins((
DefaultPlugins,
DefaultPlugins.build().disable::<TransformPlugin>(),
BigSpacePlugin::default(),
big_space::camera::CameraControllerPlugin::default(),
big_space::debug::FloatingOriginDebugPlugin::default(),
CameraControllerPlugin::default(),
FloatingOriginDebugPlugin::default(),
))
.add_systems(Startup, setup_scene)
.run();
@ -86,7 +85,7 @@ fn setup_scene(
..default()
}),
FloatingOrigin,
big_space::camera::CameraController::default() // Built-in camera controller
CameraController::default() // Built-in camera controller
.with_speed_bounds([10e-18, 10e35])
.with_smoothness(0.9, 0.8)
.with_speed(1.0),

View File

@ -1,4 +1,5 @@
//! Big spaces are infinite, looping back on themselves smoothly.
//! Big spaces are infinite, looping back on themselves smoothly. This example requires the use of
//! the `i8` feature, because a small world is needed to be able to see the "edge".
use bevy::prelude::*;
use big_space::prelude::*;
@ -6,10 +7,10 @@ use big_space::prelude::*;
fn main() {
App::new()
.add_plugins((
DefaultPlugins,
DefaultPlugins.build().disable::<TransformPlugin>(),
BigSpacePlugin::default(),
FloatingOriginDebugPlugin::default(), // Draws cell AABBs and grids
big_space::camera::CameraControllerPlugin::default(), // Compatible controller
FloatingOriginDebugPlugin::default(),
CameraControllerPlugin::default(),
))
.add_systems(Startup, setup_scene)
.run();
@ -44,7 +45,7 @@ fn setup_scene(
Camera3d::default(),
Transform::from_xyz(0.0, 0.0, 10.0),
FloatingOrigin,
big_space::camera::CameraController::default()
CameraController::default()
.with_speed(10.)
.with_smoothness(0.99, 0.95),
));

View File

@ -10,10 +10,10 @@ const BIG_DISTANCE: f64 = 1_000_000_000_000_000_000.0;
fn main() {
App::new()
.add_plugins((
DefaultPlugins,
DefaultPlugins.build().disable::<TransformPlugin>(),
BigSpacePlugin::default(),
FloatingOriginDebugPlugin::default(), // Draws cell AABBs and grids
big_space::camera::CameraControllerPlugin::default(), // Compatible controller
CameraControllerPlugin::default(), // Compatible controller
))
.add_systems(Startup, setup_scene)
.run();
@ -67,7 +67,7 @@ fn setup_scene(
Transform::from_translation(cell_offset + Vec3::new(0.0, 0.0, 10.0)),
grid_cell,
FloatingOrigin,
big_space::camera::CameraController::default(),
CameraController::default(),
));
});
}

View File

@ -1,4 +1,7 @@
use std::collections::VecDeque;
//! A practical example of a spare ship on a planet, in a solar system, surrounded by stars.
extern crate alloc;
use alloc::collections::VecDeque;
use bevy::{
color::palettes,
@ -15,14 +18,15 @@ use turborand::{rng::Rng, TurboRand};
fn main() {
App::new()
.add_plugins((
DefaultPlugins,
DefaultPlugins.build().disable::<TransformPlugin>(),
BigSpacePlugin::new(true),
big_space::camera::CameraControllerPlugin::default(),
CameraControllerPlugin::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(
@ -69,11 +73,12 @@ fn rotate(mut rotate_query: Query<(&mut Transform, &Rotates)>) {
fn lighting(
mut light: Query<(&mut Transform, &mut GlobalTransform), With<PrimaryLight>>,
sun: Query<&GlobalTransform, (With<Sun>, Without<PrimaryLight>)>,
) {
let sun_pos = sun.single().translation();
let (mut light_tr, mut light_gt) = light.single_mut();
) -> Result {
let sun_pos = sun.single()?.translation();
let (mut light_tr, mut light_gt) = light.single_mut()?;
light_tr.look_at(-sun_pos, Vec3::Y);
*light_gt = (*light_tr).into();
Ok(())
}
fn springy_ship(
@ -81,7 +86,7 @@ fn springy_ship(
mut ship: Query<&mut Transform, With<Spaceship>>,
mut desired_dir: Local<(Vec3, Quat)>,
mut smoothed_rot: Local<VecDeque<Vec3>>,
) {
) -> Result {
desired_dir.0 = DVec3::new(cam_input.right, cam_input.up, -cam_input.forward).as_vec3()
* (1.0 + cam_input.boost as u8 as f32);
@ -89,7 +94,7 @@ fn springy_ship(
smoothed_rot.push_front(DVec3::new(cam_input.pitch, cam_input.yaw, cam_input.roll).as_vec3());
let avg_rot = smoothed_rot.iter().sum::<Vec3>() / smoothed_rot.len() as f32;
use std::f32::consts::*;
use core::f32::consts::*;
desired_dir.1 = Quat::IDENTITY.slerp(
Quat::from_euler(
EulerRot::XYZ,
@ -100,12 +105,14 @@ fn springy_ship(
0.2,
) * Quat::from_rotation_y(PI);
ship.single_mut().translation = ship
.single_mut()
ship.single_mut()?.translation = ship
.single_mut()?
.translation
.lerp(desired_dir.0 * Vec3::new(0.5, 0.5, -2.0), 0.02);
ship.single_mut().rotation = ship.single_mut().rotation.slerp(desired_dir.1, 0.02);
ship.single_mut()?.rotation = ship.single_mut()?.rotation.slerp(desired_dir.1, 0.02);
Ok(())
}
fn spawn_solar_system(
@ -218,7 +225,7 @@ fn spawn_solar_system(
camera.insert((
FloatingOrigin,
Transform::from_translation(cam_pos).looking_to(Vec3::NEG_Z, Vec3::X),
big_space::camera::CameraController::default() // Built-in camera controller
CameraController::default() // Built-in camera controller
.with_speed_bounds([0.1, 10e35])
.with_smoothness(0.98, 0.98)
.with_speed(1.0),
@ -244,7 +251,7 @@ fn spawn_solar_system(
camera.with_child((
Spaceship,
SceneRoot(asset_server.load("models/low_poly_spaceship/scene.gltf#Scene0")),
Transform::from_rotation(Quat::from_rotation_y(std::f32::consts::PI)),
Transform::from_rotation(Quat::from_rotation_y(core::f32::consts::PI)),
));
});
});
@ -276,10 +283,8 @@ fn cursor_grab_system(
mut cam: ResMut<big_space::camera::CameraInput>,
btn: Res<ButtonInput<MouseButton>>,
key: Res<ButtonInput<KeyCode>>,
) {
let Some(mut window) = windows.get_single_mut().ok() else {
return;
};
) -> Result<()> {
let mut window = windows.single_mut()?;
if btn.just_pressed(MouseButton::Right) {
window.cursor_options.grab_mode = bevy::window::CursorGrabMode::Locked;
@ -294,4 +299,6 @@ fn cursor_grab_system(
// window.mode = WindowMode::Windowed;
cam.defaults_disabled = true;
}
Ok(())
}

View File

@ -9,6 +9,7 @@
use bevy::prelude::*;
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
@ -16,10 +17,10 @@ const PROTON_DIA: f32 = 1.68e-15; // Diameter of a proton
fn main() {
App::new()
.add_plugins((
DefaultPlugins,
DefaultPlugins.build().disable::<TransformPlugin>(),
BigSpacePlugin::default(),
FloatingOriginDebugPlugin::default(), // Draws cell AABBs and grids
big_space::camera::CameraControllerPlugin::default(), // Compatible controller
CameraControllerPlugin::default(), // Compatible controller
))
.add_systems(Startup, setup_scene)
.add_systems(Update, (bounce_atoms, toggle_cam_pos))
@ -75,14 +76,14 @@ fn setup_scene(
Transform::from_xyz(0.0, 0.0, PROTON_DIA * 2.0),
grid_cell,
FloatingOrigin,
big_space::camera::CameraController::default(),
CameraController::default(),
));
// A space ship
root_grid.spawn_spatial((
SceneRoot(asset_server.load("models/low_poly_spaceship/scene.gltf#Scene0")),
Transform::from_xyz(0.0, 0.0, 2.5)
.with_rotation(Quat::from_rotation_y(std::f32::consts::PI)),
.with_rotation(Quat::from_rotation_y(core::f32::consts::PI)),
grid_cell,
));
});
@ -104,12 +105,15 @@ fn toggle_cam_pos(
grid: Query<&Grid>,
keyboard: Res<ButtonInput<KeyCode>>,
protons: Query<&GlobalTransform, With<Proton>>,
) {
) -> Result {
if !keyboard.just_pressed(KeyCode::KeyT) {
return;
return Ok(());
}
*cam.single_mut() = if *toggle {
grid.single().translation_to_grid(DVec3::X * UNIVERSE_DIA).0
*cam.single_mut()? = if *toggle {
grid.single()
.unwrap()
.translation_to_grid(DVec3::X * UNIVERSE_DIA)
.0
} else {
GridCell::ZERO
};
@ -120,4 +124,5 @@ fn toggle_cam_pos(
for proton in &protons {
info!("Proton x coord: {}", proton.translation().x);
}
Ok(())
}

View File

@ -1,14 +1,14 @@
use std::hash::Hasher;
//! Demonstrates the included optional spatial hashing and partitioning of grid cells.
use bevy::{
core_pipeline::{bloom::Bloom, fxaa::Fxaa, tonemapping::Tonemapping},
core_pipeline::{bloom::Bloom, tonemapping::Tonemapping},
prelude::*,
};
use bevy_ecs::entity::EntityHasher;
use bevy_ecs::{entity::EntityHasher, relationship::Relationship};
use bevy_math::DVec3;
use big_space::prelude::*;
use core::hash::Hasher;
use noise::{NoiseFn, Simplex};
use smallvec::SmallVec;
use turborand::prelude::*;
// Try bumping this up to really stress test. I'm able to push a million entities with an M3 Max.
@ -21,11 +21,11 @@ const PERCENT_STATIC: f32 = 0.99;
fn main() {
App::new()
.add_plugins((
DefaultPlugins,
DefaultPlugins.build().disable::<TransformPlugin>(),
BigSpacePlugin::default(),
GridHashPlugin::<()>::default(),
GridPartitionPlugin::<()>::default(),
big_space::camera::CameraControllerPlugin::default(),
CameraControllerPlugin::default(),
))
.add_systems(Startup, (spawn, setup_ui))
.add_systems(
@ -69,7 +69,7 @@ impl FromWorld for MaterialPresets {
let mut meshes = world.resource_mut::<Assets<Mesh>>();
let sphere = meshes.add(
Sphere::new(HALF_WIDTH / (1_000_000_f32).powf(0.33) * 0.5)
Sphere::new(HALF_WIDTH / 1_000_000_f32.powf(0.33) * 0.5)
.mesh()
.ico(0)
.unwrap(),
@ -89,10 +89,12 @@ fn draw_partitions(
partitions: Res<GridPartitionMap>,
grids: Query<(&GlobalTransform, &Grid)>,
camera: Query<&GridHash, With<Camera>>,
) {
) -> Result {
let camera = camera.single()?;
for (id, p) in partitions.iter().take(10_000) {
let Ok((transform, grid)) = grids.get(p.grid()) else {
return;
return Ok(());
};
let l = grid.cell_edge_length();
@ -102,7 +104,7 @@ fn draw_partitions(
let hue = (f % 360) as f32;
p.iter()
.filter(|hash| *hash != camera.single())
.filter(|hash| *hash != camera)
.take(1_000)
.for_each(|h| {
let center = [h.cell().x as i32, h.cell().y as i32, h.cell().z as i32];
@ -110,7 +112,7 @@ fn draw_partitions(
.with_scale(Vec3::splat(l));
gizmos.cuboid(
transform.mul_transform(local_trans),
Hsla::new(hue, 1.0, 0.5, 0.05),
Hsla::new(hue, 1.0, 0.5, 0.2),
);
});
@ -123,19 +125,20 @@ fn draw_partitions(
gizmos.cuboid(
transform.mul_transform(local_trans),
Hsla::new(hue, 1.0, 0.5, 0.2),
Hsla::new(hue, 1.0, 0.5, 0.9),
);
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)]
fn move_player(
time: Res<Time>,
mut _gizmos: Gizmos,
mut player: Query<(&mut Transform, &mut GridCell, &Parent, &GridHash), With<Player>>,
mut player: Query<(&mut Transform, &mut GridCell, &ChildOf, &GridHash), With<Player>>,
mut non_player: Query<
(&mut Transform, &mut GridCell, &Parent),
(&mut Transform, &mut GridCell, &ChildOf),
(Without<Player>, With<NonPlayer>),
>,
mut materials: Query<&mut MeshMaterial3d<StandardMaterial>, Without<Player>>,
@ -146,11 +149,11 @@ fn move_player(
mut text: Query<&mut Text>,
hash_stats: Res<big_space::timing::SmoothedStat<big_space::timing::GridHashStats>>,
prop_stats: Res<big_space::timing::SmoothedStat<big_space::timing::PropagationStats>>,
) {
) -> Result {
let n_entities = non_player.iter().len();
for neighbor in neighbors.iter() {
if let Ok(mut material) = materials.get_mut(*neighbor) {
**material = material_presets.default.clone_weak();
material.set_if_neq(material_presets.default.clone_weak().into());
}
}
@ -170,14 +173,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, parent, 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())
.unwrap()
.get(parent.get())?
.imprecise_translation_to_grid(absolute_pos);
neighbors.clear();
@ -185,15 +187,8 @@ fn move_player(
hash_grid.flood(hash, None).entities().for_each(|entity| {
neighbors.push(entity);
if let Ok(mut material) = materials.get_mut(entity) {
**material = material_presets.flood.clone_weak();
material.set_if_neq(material_presets.flood.clone_weak().into());
}
// let grid = grid.get(entry.grid).unwrap();
// let transform = grid.global_transform(
// &entry.cell,
// &Transform::from_scale(Vec3::splat(grid.cell_edge_length() * 0.99)),
// );
// gizmos.cuboid(transform, Color::linear_rgba(1.0, 1.0, 1.0, 0.2));
});
hash_grid
@ -204,11 +199,11 @@ fn move_player(
.for_each(|entity| {
neighbors.push(entity);
if let Ok(mut material) = materials.get_mut(entity) {
**material = material_presets.highlight.clone_weak();
material.set_if_neq(material_presets.highlight.clone_weak().into());
}
});
let mut text = text.single_mut();
let mut text = text.single_mut()?;
text.0 = format!(
"\
Controls:
@ -236,9 +231,8 @@ Total: {: >22.1?}",
.as_bytes()
.rchunks(3)
.rev()
.map(std::str::from_utf8)
.collect::<Result<Vec<&str>, _>>()
.unwrap()
.map(core::str::from_utf8)
.collect::<Result<Vec<&str>, _>>()?
.join(","),
//
prop_stats.avg().grid_recentering(),
@ -254,6 +248,8 @@ Total: {: >22.1?}",
//
prop_stats.avg().total() + hash_stats.avg().total(),
);
Ok(())
}
fn spawn(mut commands: Commands) {
@ -267,11 +263,10 @@ fn spawn(mut commands: Commands) {
},
Tonemapping::AcesFitted,
Transform::from_xyz(0.0, 0.0, HALF_WIDTH * CELL_WIDTH * 2.0),
big_space::camera::CameraController::default()
CameraController::default()
.with_smoothness(0.98, 0.93)
.with_slowing(false)
.with_speed(15.0),
Fxaa::default(),
Bloom::default(),
GridCell::new(0, 0, HALF_WIDTH as GridPrecision / 2),
))
@ -287,54 +282,41 @@ fn spawn_spheres(
mut commands: Commands,
input: Res<ButtonInput<KeyCode>>,
material_presets: Res<MaterialPresets>,
mut grid: Query<(Entity, &Grid, &mut Children)>,
grid: Query<Entity, With<Grid>>,
non_players: Query<(), With<NonPlayer>>,
) {
) -> Result {
let n_entities = non_players.iter().len().max(1);
let n_spawn = if input.pressed(KeyCode::KeyG) {
n_entities
} else if input.pressed(KeyCode::KeyF) {
1_000
} else {
return;
return Ok(());
};
let (entity, _grid, mut children) = grid.single_mut();
let mut dyn_parent = bevy_reflect::DynamicTupleStruct::default();
dyn_parent.insert(entity);
let dyn_parent = dyn_parent.as_partial_reflect();
let new_children = sample_noise(n_spawn, &Simplex::new(345612), &Rng::new())
.map(|value| {
let entity = grid.single()?;
commands.entity(entity).with_children(|builder| {
for value in sample_noise(n_spawn, &Simplex::new(345612), &Rng::new()) {
let hash = GridHash::__new_manual(entity, &GridCell::default());
commands
.spawn((
Transform::from_xyz(value.x, value.y, value.z),
GlobalTransform::default(),
GridCell::default(),
FastGridHash::from(hash),
hash,
NonPlayer,
Parent::from_reflect(dyn_parent).unwrap(),
Mesh3d(material_presets.sphere.clone_weak()),
MeshMaterial3d(material_presets.default.clone_weak()),
bevy_render::view::VisibilityRange {
start_margin: 1.0..5.0,
end_margin: HALF_WIDTH * CELL_WIDTH * 0.5..HALF_WIDTH * CELL_WIDTH * 0.8,
use_aabb: false,
},
bevy_render::view::NoFrustumCulling,
))
.id()
})
.chain(children.iter().copied())
.collect::<SmallVec<[Entity; 8]>>();
let mut dyn_children = bevy_reflect::DynamicTupleStruct::default();
dyn_children.insert(new_children);
let dyn_children = dyn_children.as_partial_reflect();
*children = Children::from_reflect(dyn_children).unwrap();
builder.spawn((
Transform::from_xyz(value.x, value.y, value.z),
GlobalTransform::default(),
GridCell::default(),
FastGridHash::from(hash),
hash,
NonPlayer,
Mesh3d(material_presets.sphere.clone_weak()),
MeshMaterial3d(material_presets.default.clone_weak()),
bevy_render::view::VisibilityRange {
start_margin: 1.0..5.0,
end_margin: HALF_WIDTH * CELL_WIDTH * 0.5..HALF_WIDTH * CELL_WIDTH * 0.8,
use_aabb: false,
},
bevy_render::view::NoFrustumCulling,
));
}
});
Ok(())
}
#[inline]
@ -343,7 +325,7 @@ fn sample_noise<'a, T: NoiseFn<f64, 3>>(
noise: &'a T,
rng: &'a Rng,
) -> impl Iterator<Item = Vec3> + use<'a, T> {
std::iter::repeat_with(
core::iter::repeat_with(
|| loop {
let noise_scale = 0.05 * HALF_WIDTH as f64;
let threshold = 0.50;
@ -390,8 +372,8 @@ fn cursor_grab(
keyboard: Res<ButtonInput<KeyCode>>,
mouse: Res<ButtonInput<MouseButton>>,
mut windows: Query<&mut Window, With<bevy::window::PrimaryWindow>>,
) {
let mut primary_window = windows.single_mut();
) -> Result {
let mut primary_window = windows.single_mut()?;
if mouse.just_pressed(MouseButton::Left) {
primary_window.cursor_options.grab_mode = bevy::window::CursorGrabMode::Locked;
primary_window.cursor_options.visible = false;
@ -400,4 +382,5 @@ fn cursor_grab(
primary_window.cursor_options.grab_mode = bevy::window::CursorGrabMode::None;
primary_window.cursor_options.visible = true;
}
Ok(())
}

View File

@ -1,24 +1,24 @@
//! Demonstrates how a single bevy world can contain multiple big_space hierarchies, each rendered
//! Demonstrates how a single bevy world can contain multiple `big_space` hierarchies, each rendered
//! relative to a floating origin inside that big space.
//!
//! This takes the simplest approach, of simply duplicating the worlds and players for each split
//! screen, and synchronizing the player locations between both.
use bevy::{
color::palettes,
prelude::*,
render::{camera::Viewport, view::RenderLayers},
transform::TransformSystem,
};
use bevy_color::palettes;
use big_space::prelude::*;
fn main() {
App::new()
.add_plugins((
DefaultPlugins,
DefaultPlugins.build().disable::<TransformPlugin>(),
BigSpacePlugin::default(),
FloatingOriginDebugPlugin::default(),
big_space::camera::CameraControllerPlugin::default(),
CameraControllerPlugin::default(),
))
.add_systems(Startup, setup)
.add_systems(Update, set_camera_viewports)
@ -60,7 +60,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),
big_space::camera::CameraController::default().with_smoothness(0.8, 0.8),
CameraController::default().with_smoothness(0.8, 0.8),
RenderLayers::layer(2),
LeftCamera,
FloatingOrigin,
@ -182,12 +182,14 @@ fn update_cameras(
Without<RightCamera>,
),
>,
) {
*left_rep.single_mut().cell = *left.single().cell;
*left_rep.single_mut().transform = *left.single().transform;
) -> Result {
*left_rep.single_mut()?.cell = *left.single()?.cell;
*left_rep.single_mut()?.transform = *left.single()?.transform;
*right_rep.single_mut().cell = *right.single().cell;
*right_rep.single_mut().transform = *right.single().transform;
*right_rep.single_mut()?.cell = *right.single()?.cell;
*right_rep.single_mut()?.transform = *right.single()?.transform;
Ok(())
}
fn set_camera_viewports(
@ -195,13 +197,13 @@ fn set_camera_viewports(
mut resize_events: EventReader<bevy::window::WindowResized>,
mut left_camera: Query<&mut Camera, (With<LeftCamera>, Without<RightCamera>)>,
mut right_camera: Query<&mut Camera, With<RightCamera>>,
) {
) -> Result {
// We need to dynamically resize the camera's viewports whenever the window size changes
// so then each camera always takes up half the screen.
// A resize_event is sent when the window is first created, allowing us to reuse this system for initial setup.
for resize_event in resize_events.read() {
let window = windows.get(resize_event.window).unwrap();
let mut left_camera = left_camera.single_mut();
let window = windows.get(resize_event.window)?;
let mut left_camera = left_camera.single_mut()?;
left_camera.viewport = Some(Viewport {
physical_position: UVec2::new(0, 0),
physical_size: UVec2::new(
@ -211,7 +213,7 @@ fn set_camera_viewports(
..default()
});
let mut right_camera = right_camera.single_mut();
let mut right_camera = right_camera.single_mut()?;
right_camera.viewport = Some(Viewport {
physical_position: UVec2::new(window.resolution.physical_width() / 2, 0),
physical_size: UVec2::new(
@ -221,4 +223,6 @@ fn set_camera_viewports(
..default()
});
}
Ok(())
}

View File

@ -1,4 +1,4 @@
//! Component bundles for big_space.
//! Component bundles for `big_space`.
use crate::prelude::*;
use bevy_ecs::prelude::*;

View File

@ -3,9 +3,10 @@
use crate::prelude::*;
use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use bevy_hierarchy::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_reflect::prelude::*;
use bevy_render::{
primitives::Aabb,
@ -13,7 +14,6 @@ use bevy_render::{
};
use bevy_time::prelude::*;
use bevy_transform::{prelude::*, TransformSystem};
use bevy_utils::HashSet;
/// Adds the `big_space` camera controller
#[derive(Default)]
@ -131,7 +131,7 @@ impl Default for CameraController {
}
}
/// ButtonInput state used to command camera motion. Reset every time the values are read to update
/// `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.
#[derive(Clone, Debug, Default, Reflect, Resource)]
@ -226,7 +226,7 @@ pub fn nearest_objects_in_grid(
)>,
children: Query<&Children>,
) {
let Ok((cam_entity, mut camera, cam_pos, cam_layer)) = camera.get_single_mut() else {
let Ok((cam_entity, mut camera, cam_pos, cam_layer)) = camera.single_mut() else {
return;
};
if !camera.slow_near_objects {
@ -260,7 +260,7 @@ pub fn nearest_objects_in_grid(
/// Uses [`CameraInput`] state to update the camera position.
pub fn camera_controller(
time: Res<Time>,
grids: crate::grid::local_origin::Grids,
grids: Grids,
mut input: ResMut<CameraInput>,
mut camera: Query<(Entity, &mut GridCell, &mut Transform, &mut CameraController)>,
) {

View File

@ -1,8 +1,7 @@
//! Adds `big_space`-specific commands to bevy's `Commands`.
use crate::prelude::*;
use bevy_ecs::prelude::*;
use bevy_hierarchy::prelude::*;
use bevy_ecs::{prelude::*, relationship::RelatedSpawnerCommands};
use bevy_transform::prelude::*;
use smallvec::SmallVec;
@ -177,7 +176,7 @@ impl Drop for GridCommands<'_> {
let entity = self.entity;
self.commands
.entity(entity)
.insert(std::mem::take(&mut self.grid))
.insert(core::mem::take(&mut self.grid))
.add_children(&self.children);
}
}
@ -189,13 +188,13 @@ pub struct SpatialEntityCommands<'a> {
}
impl<'a> SpatialEntityCommands<'a> {
/// Insert a component on this grid
/// Insert a component into this grid.
pub fn insert(&mut self, bundle: impl Bundle) -> &mut Self {
self.commands.entity(self.entity).insert(bundle);
self
}
/// Removes a `Bundle`` of components from the entity.
/// Removes a `Bundle` of components from the entity.
pub fn remove<T>(&mut self) -> &mut Self
where
T: Bundle,
@ -204,8 +203,11 @@ impl<'a> SpatialEntityCommands<'a> {
self
}
/// Takes a closure which provides a [`ChildBuilder`].
pub fn with_children(&mut self, spawn_children: impl FnOnce(&mut ChildBuilder)) -> &mut Self {
/// Spawns children of this entity (with a [`ChildOf`] relationship) by taking a function that operates on a [`ChildSpawner`].
pub fn with_children(
&mut self,
spawn_children: impl FnOnce(&mut RelatedSpawnerCommands<'_, ChildOf>),
) -> &mut Self {
self.commands
.entity(self.entity)
.with_children(|child_builder| spawn_children(child_builder));
@ -213,12 +215,16 @@ impl<'a> SpatialEntityCommands<'a> {
}
/// Spawns the passed bundle and adds it to this entity as a child.
///
/// For efficient spawning of multiple children, use [`with_children`].
///
/// [`with_children`]: SpatialEntityCommands::with_children
pub fn with_child<B: Bundle>(&mut self, bundle: B) -> &mut Self {
self.commands.entity(self.entity).with_child(bundle);
self
}
/// Returns the [`Entity``] id of the entity.
/// Returns the [`Entity`] id of the entity.
pub fn id(&self) -> Entity {
self.entity
}

View File

@ -20,16 +20,16 @@ impl Plugin for FloatingOriginDebugPlugin {
PostUpdate,
(update_debug_bounds, update_grid_axes)
.chain()
.after(bevy_transform::TransformSystem::TransformPropagate),
.after(TransformSystem::TransformPropagate),
);
}
}
fn setup_gizmos(mut store: ResMut<GizmoConfigStore>) {
let (config, _) = store.config_mut::<BigSpaceGizmoConfig>();
config.line_perspective = false;
config.line_joints = GizmoLineJoint::Round(4);
config.line_width = 1.0;
config.line.perspective = false;
config.line.joints = GizmoLineJoint::Round(4);
config.line.width = 1.0;
}
/// Update the rendered debug bounds to only highlight occupied [`GridCell`]s.
@ -47,7 +47,7 @@ fn update_debug_bounds(
&Transform::from_scale(Vec3::splat(grid.cell_edge_length() * 0.999)),
);
if origin.is_none() {
gizmos.cuboid(transform, Color::linear_rgb(0.0, 1.0, 0.0))
gizmos.cuboid(transform, Color::linear_rgb(0.0, 1.0, 0.0));
} else {
// gizmos.cuboid(transform, Color::rgba(0.0, 0.0, 1.0, 0.5))
}

View File

@ -1,9 +1,8 @@
//! A floating origin for camera-relative rendering, to maximize precision when converting to f32.
use bevy_ecs::prelude::*;
use bevy_hierarchy::prelude::*;
use bevy_platform_support::collections::HashMap;
use bevy_reflect::prelude::*;
use bevy_utils::HashMap;
/// Marks the entity to use as the floating origin.
///
@ -45,13 +44,13 @@ pub struct BigSpace {
}
impl BigSpace {
/// Return the this grid's floating origin if it exists and is a descendent of this root.
/// Return this grid's floating origin if it exists and is a descendant of this root.
///
/// `this_entity`: the entity this component belongs to.
pub(crate) fn validate_floating_origin(
&self,
this_entity: Entity,
parents: &Query<&Parent>,
parents: &Query<&ChildOf>,
) -> Option<Entity> {
let floating_origin = self.floating_origin?;
let origin_root_entity = parents.iter_ancestors(floating_origin).last()?;
@ -63,10 +62,10 @@ impl BigSpace {
/// `BigSpace` hierarchy.
pub fn find_floating_origin(
floating_origins: Query<Entity, With<FloatingOrigin>>,
parent_query: Query<&Parent>,
parent_query: Query<&ChildOf>,
mut big_spaces: Query<(Entity, &mut BigSpace)>,
) {
let mut spaces_set = HashMap::new();
let mut spaces_set = HashMap::<_, _>::default();
// Reset all floating origin fields, so we know if any are missing.
for (entity, mut space) in &mut big_spaces {
space.floating_origin = None;
@ -86,7 +85,7 @@ impl BigSpace {
tracing::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
space.floating_origin = None;
} else {
space.floating_origin = Some(origin);
}
@ -99,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.",)
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.",);
}
}
}

View File

@ -1,12 +1,11 @@
//! Contains the grid cell implementation
use crate::prelude::*;
use bevy_ecs::prelude::*;
use bevy_hierarchy::prelude::*;
use bevy_ecs::{prelude::*, relationship::Relationship};
use bevy_math::{DVec3, IVec3};
use bevy_platform_support::time::Instant;
use bevy_reflect::prelude::*;
use bevy_transform::prelude::*;
use bevy_utils::Instant;
/// Locates an entity in a cell within its parent's [`Grid`]. The [`Transform`] of an entity with
/// this component is a transformation from the center of this cell.
@ -84,7 +83,7 @@ impl GridCell {
pub fn recenter_large_transforms(
mut stats: ResMut<crate::timing::PropagationStats>,
grids: Query<&Grid>,
mut changed_transform: Query<(&mut Self, &mut Transform, &Parent), Changed<Transform>>,
mut changed_transform: Query<(&mut Self, &mut Transform, &ChildOf), Changed<Transform>>,
) {
let start = Instant::now();
changed_transform
@ -111,7 +110,7 @@ impl GridCell {
}
}
impl std::ops::Add for GridCell {
impl core::ops::Add for GridCell {
type Output = GridCell;
fn add(self, rhs: Self) -> Self::Output {
@ -123,7 +122,7 @@ impl std::ops::Add for GridCell {
}
}
impl std::ops::Add<IVec3> for GridCell {
impl core::ops::Add<IVec3> for GridCell {
type Output = GridCell;
fn add(self, rhs: IVec3) -> Self::Output {
@ -135,7 +134,7 @@ impl std::ops::Add<IVec3> for GridCell {
}
}
impl std::ops::Sub for GridCell {
impl core::ops::Sub for GridCell {
type Output = GridCell;
fn sub(self, rhs: Self) -> Self::Output {
@ -147,7 +146,7 @@ impl std::ops::Sub for GridCell {
}
}
impl std::ops::Sub<IVec3> for GridCell {
impl core::ops::Sub<IVec3> for GridCell {
type Output = GridCell;
fn sub(self, rhs: IVec3) -> Self::Output {
@ -159,7 +158,7 @@ impl std::ops::Sub<IVec3> for GridCell {
}
}
impl std::ops::Add for &GridCell {
impl core::ops::Add for &GridCell {
type Output = GridCell;
fn add(self, rhs: Self) -> Self::Output {
@ -167,7 +166,7 @@ impl std::ops::Add for &GridCell {
}
}
impl std::ops::Add<IVec3> for &GridCell {
impl core::ops::Add<IVec3> for &GridCell {
type Output = GridCell;
fn add(self, rhs: IVec3) -> Self::Output {
@ -175,7 +174,7 @@ impl std::ops::Add<IVec3> for &GridCell {
}
}
impl std::ops::Sub for &GridCell {
impl core::ops::Sub for &GridCell {
type Output = GridCell;
fn sub(self, rhs: Self) -> Self::Output {
@ -183,7 +182,7 @@ impl std::ops::Sub for &GridCell {
}
}
impl std::ops::Sub<IVec3> for &GridCell {
impl core::ops::Sub<IVec3> for &GridCell {
type Output = GridCell;
fn sub(self, rhs: IVec3) -> Self::Output {
@ -191,35 +190,35 @@ impl std::ops::Sub<IVec3> for &GridCell {
}
}
impl std::ops::AddAssign for GridCell {
impl core::ops::AddAssign for GridCell {
fn add_assign(&mut self, rhs: Self) {
use std::ops::Add;
use core::ops::Add;
*self = self.add(rhs);
}
}
impl std::ops::AddAssign<IVec3> for GridCell {
impl core::ops::AddAssign<IVec3> for GridCell {
fn add_assign(&mut self, rhs: IVec3) {
use std::ops::Add;
use core::ops::Add;
*self = self.add(rhs);
}
}
impl std::ops::SubAssign for GridCell {
impl core::ops::SubAssign for GridCell {
fn sub_assign(&mut self, rhs: Self) {
use std::ops::Sub;
use core::ops::Sub;
*self = self.sub(rhs);
}
}
impl std::ops::SubAssign<IVec3> for GridCell {
impl core::ops::SubAssign<IVec3> for GridCell {
fn sub_assign(&mut self, rhs: IVec3) {
use std::ops::Sub;
use core::ops::Sub;
*self = self.sub(rhs);
}
}
impl std::ops::Mul<GridPrecision> for GridCell {
impl core::ops::Mul<GridPrecision> for GridCell {
type Output = GridCell;
fn mul(self, rhs: GridPrecision) -> Self::Output {
@ -231,7 +230,7 @@ impl std::ops::Mul<GridPrecision> for GridCell {
}
}
impl std::ops::Mul<GridPrecision> for &GridCell {
impl core::ops::Mul<GridPrecision> for &GridCell {
type Output = GridCell;
fn mul(self, rhs: GridPrecision) -> Self::Output {

View File

@ -5,13 +5,14 @@
use crate::prelude::*;
use bevy_ecs::{
prelude::*,
relationship::Relationship,
system::{
lifetimeless::{Read, Write},
SystemParam,
},
};
use bevy_hierarchy::prelude::*;
use bevy_math::{prelude::*, DAffine3, DQuat};
use bevy_platform_support::prelude::*;
use bevy_transform::prelude::*;
pub use inner::LocalFloatingOrigin;
@ -75,7 +76,7 @@ mod inner {
impl LocalFloatingOrigin {
/// The grid transform from the local grid, to the floating origin's grid. See
/// [Self::grid_transform].
/// [`Self::grid_transform`].
#[inline]
pub fn grid_transform(&self) -> DAffine3 {
self.grid_transform
@ -236,14 +237,14 @@ fn propagate_origin_to_child(
child_origin_translation_float,
origin_child_rotation,
);
})
});
}
/// A system param for more easily navigating a hierarchy of [`Grid`]s.
#[derive(SystemParam)]
pub struct Grids<'w, 's> {
parent: Query<'w, 's, Read<Parent>>,
grid_query: Query<'w, 's, (Entity, Read<Grid>, Option<Read<Parent>>)>,
parent: Query<'w, 's, Read<ChildOf>>,
grid_query: Query<'w, 's, (Entity, Read<Grid>, Option<Read<ChildOf>>)>,
}
impl Grids<'_, '_> {
@ -266,7 +267,7 @@ impl Grids<'_, '_> {
/// Get the ID of the grid that `this` `Entity` is a child of, if it exists.
#[inline]
pub fn parent_grid_entity(&self, this: Entity) -> Option<Entity> {
match self.parent.get(this).map(|parent| **parent) {
match self.parent.get(this).map(Relationship::get) {
Err(_) => None,
Ok(parent) => match self.grid_query.contains(parent) {
true => Some(parent),
@ -291,7 +292,7 @@ impl Grids<'_, '_> {
.iter()
.filter_map(move |(entity, _, parent)| {
parent
.map(|p| p.get())
.map(Relationship::get)
.filter(|parent| *parent == this)
.map(|_| entity)
})
@ -317,9 +318,9 @@ impl Grids<'_, '_> {
/// A system param for more easily navigating a hierarchy of grids mutably.
#[derive(SystemParam)]
pub struct GridsMut<'w, 's> {
parent: Query<'w, 's, Read<Parent>>,
parent: Query<'w, 's, Read<ChildOf>>,
position: Query<'w, 's, (Read<GridCell>, Read<Transform>), With<Grid>>,
grid_query: Query<'w, 's, (Entity, Write<Grid>, Option<Read<Parent>>)>,
grid_query: Query<'w, 's, (Entity, Write<Grid>, Option<Read<ChildOf>>)>,
}
impl GridsMut<'_, '_> {
@ -374,7 +375,7 @@ impl GridsMut<'_, '_> {
/// Get the ID of the grid that `this` `Entity` is a child of, if it exists.
#[inline]
pub fn parent_grid_entity(&self, this: Entity) -> Option<Entity> {
match self.parent.get(this).map(|parent| **parent) {
match self.parent.get(this).map(Relationship::get) {
Err(_) => None,
Ok(parent) => match self.grid_query.contains(parent) {
true => Some(parent),
@ -399,7 +400,7 @@ impl GridsMut<'_, '_> {
.iter()
.filter_map(move |(entity, _, parent)| {
parent
.map(|p| p.get())
.map(Relationship::get)
.filter(|parent| *parent == this)
.map(|_| entity)
})
@ -435,9 +436,9 @@ impl LocalFloatingOrigin {
mut scratch_buffer: Local<Vec<Entity>>,
cells: Query<(Entity, Ref<GridCell>)>,
roots: Query<(Entity, &BigSpace)>,
parents: Query<&Parent>,
parents: Query<&ChildOf>,
) {
let start = bevy_utils::Instant::now();
let start = bevy_platform_support::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?
@ -493,7 +494,7 @@ impl LocalFloatingOrigin {
// child, these do no alias.
for child_grid in scratch_buffer.drain(..) {
propagate_origin_to_child(this_grid, &mut grids, child_grid);
grid_stack.push(child_grid) // Push processed child onto the stack
grid_stack.push(child_grid); // Push processed child onto the stack
}
}
@ -507,7 +508,7 @@ 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.")
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.");
}
stats.local_origin_propagation += start.elapsed();
@ -550,7 +551,7 @@ mod tests {
let result = grids.child_grids(child_1).collect::<Vec<_>>();
assert_eq!(result, Vec::new());
// Parent
// ChildOf
let result = grids.parent_grid_entity(root);
assert_eq!(result, None);
let result = grids.parent_grid_entity(parent);
@ -575,7 +576,7 @@ mod tests {
local_floating_origin: LocalFloatingOrigin::new(
GridCell::new(1_000_000, -1, -1),
Vec3::ZERO,
DQuat::from_rotation_z(-std::f64::consts::FRAC_PI_2),
DQuat::from_rotation_z(-core::f64::consts::FRAC_PI_2),
),
..default()
};
@ -587,7 +588,7 @@ mod tests {
let child = app
.world_mut()
.spawn((
Transform::from_rotation(Quat::from_rotation_z(std::f32::consts::FRAC_PI_2))
Transform::from_rotation(Quat::from_rotation_z(core::f32::consts::FRAC_PI_2))
.with_translation(Vec3::new(1.0, 1.0, 0.0)),
GridCell::new(1_000_000, 0, 0),
Grid::default(),
@ -609,7 +610,7 @@ mod tests {
assert_eq!(computed_grid, correct_grid);
let computed_rot = child_grid.local_floating_origin.rotation();
let correct_rot = DQuat::from_rotation_z(std::f64::consts::PI);
let correct_rot = DQuat::from_rotation_z(core::f64::consts::PI);
let rot_error = computed_rot.angle_between(correct_rot);
assert!(rot_error < 1e-10);
@ -637,14 +638,14 @@ mod tests {
let child = app
.world_mut()
.spawn((
Transform::from_rotation(Quat::from_rotation_z(std::f32::consts::FRAC_PI_2))
Transform::from_rotation(Quat::from_rotation_z(core::f32::consts::FRAC_PI_2))
.with_translation(Vec3::new(1.0, 1.0, 0.0)),
GridCell::new(150_000_003_000, 0, 0), // roughly radius of earth orbit
Grid {
local_floating_origin: LocalFloatingOrigin::new(
GridCell::new(0, 3_000, 0),
Vec3::new(5.0, 5.0, 0.0),
DQuat::from_rotation_z(-std::f64::consts::FRAC_PI_2),
DQuat::from_rotation_z(-core::f64::consts::FRAC_PI_2),
),
..Default::default()
},
@ -708,7 +709,7 @@ mod tests {
.world_mut()
.spawn((
Transform::default()
.with_rotation(Quat::from_rotation_z(-std::f32::consts::FRAC_PI_2))
.with_rotation(Quat::from_rotation_z(-core::f32::consts::FRAC_PI_2))
.with_translation(Vec3::new(3.0, 3.0, 0.0)),
GridCell::new(0, 0, 0),
Grid::default(),
@ -729,7 +730,7 @@ mod tests {
let computed_pos = computed_transform.transform_point3(child_local_point);
let correct_transform = DAffine3::from_rotation_translation(
DQuat::from_rotation_z(-std::f64::consts::FRAC_PI_2),
DQuat::from_rotation_z(-core::f64::consts::FRAC_PI_2),
DVec3::new(2.0, 2.0, 0.0),
);
let correct_pos = correct_transform.transform_point3(child_local_point);

View File

@ -115,9 +115,9 @@ impl Grid {
return (GridCell::default(), input.as_vec3());
}
let x_r = (x / l).round();
let y_r = (y / l).round();
let z_r = (z / l).round();
let x_r = round(x / l);
let y_r = round(y / l);
let z_r = round(z / l);
let t_x = x - x_r * l;
let t_y = y - y_r * l;
let t_z = z - z_r * l;
@ -165,3 +165,21 @@ impl Grid {
.into()
}
}
fn round(x: f64) -> f64 {
#[cfg(feature = "libm")]
{
libm::round(x)
}
#[cfg(all(not(feature = "libm"), feature = "std"))]
{
x.round()
}
#[cfg(all(not(feature = "libm"), not(feature = "std")))]
{
compile_error!("Must enable the `libm` and/or `std` feature.");
f64::NAN
}
}

View File

@ -1,8 +1,7 @@
//! Logic for propagating transforms through the hierarchy of grids.
use crate::prelude::*;
use bevy_ecs::prelude::*;
use bevy_hierarchy::prelude::*;
use bevy_ecs::{prelude::*, relationship::Relationship};
use bevy_reflect::Reflect;
use bevy_transform::prelude::*;
@ -26,13 +25,13 @@ impl Grid {
Query<(
Ref<GridCell>,
Ref<Transform>,
Ref<Parent>,
Ref<ChildOf>,
&mut GlobalTransform,
)>,
Query<(&Grid, &mut GlobalTransform), With<BigSpace>>,
)>,
) {
let start = bevy_utils::Instant::now();
let start = bevy_platform_support::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
@ -95,13 +94,13 @@ impl Grid {
mut commands: Commands,
valid_parent: Query<(), (With<GridCell>, With<GlobalTransform>, With<Children>)>,
unmarked: Query<
(Entity, &Parent),
(Entity, &ChildOf),
(
With<Transform>,
With<GlobalTransform>,
Without<GridCell>,
Without<LowPrecisionRoot>,
Or<(Changed<Parent>, Added<Transform>)>,
Or<(Changed<ChildOf>, Added<Transform>)>,
),
>,
invalidated: Query<
@ -112,13 +111,13 @@ impl Grid {
Without<Transform>,
Without<GlobalTransform>,
With<GridCell>,
Without<Parent>,
Without<ChildOf>,
)>,
),
>,
has_possibly_invalid_parent: Query<(Entity, &Parent), With<LowPrecisionRoot>>,
has_possibly_invalid_parent: Query<(Entity, &ChildOf), With<LowPrecisionRoot>>,
) {
let start = bevy_utils::Instant::now();
let start = bevy_platform_support::time::Instant::now();
for (entity, parent) in unmarked.iter() {
if valid_parent.contains(parent.get()) {
commands.entity(entity).insert(LowPrecisionRoot);
@ -150,17 +149,17 @@ impl Grid {
Or<(With<Grid>, With<GridCell>)>,
),
>,
roots: Query<(Entity, &Parent), With<LowPrecisionRoot>>,
roots: Query<(Entity, &ChildOf), With<LowPrecisionRoot>>,
transform_query: Query<
(Ref<Transform>, &mut GlobalTransform, Option<&Children>),
(
With<Parent>,
With<ChildOf>,
Without<GridCell>, // Used to prove access to GlobalTransform is disjoint
Without<Grid>,
),
>,
parent_query: Query<
(Entity, Ref<Parent>),
(Entity, Ref<ChildOf>),
(
With<Transform>,
With<GlobalTransform>,
@ -169,7 +168,7 @@ impl Grid {
),
>,
) {
let start = bevy_utils::Instant::now();
let start = bevy_platform_support::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.
@ -186,6 +185,10 @@ impl Grid {
// 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 {
Self::propagate_recursive(
&parent_transform,
@ -206,8 +209,6 @@ impl Grid {
stats.low_precision_propagation += start.elapsed();
}
/// COPIED FROM BEVY
///
/// Recursively propagates the transforms for `entity` and all of its descendants.
///
/// # Panics
@ -221,18 +222,22 @@ impl Grid {
/// 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<Parent>,
With<ChildOf>,
Without<GridCell>, // ***ADDED*** Only recurse low-precision entities
Without<Grid>, // ***ADDED*** Only recurse low-precision entities
),
>,
parent_query: &Query<
(Entity, Ref<Parent>),
(Entity, Ref<ChildOf>),
(
With<Transform>,
With<GlobalTransform>,
@ -245,28 +250,34 @@ impl Grid {
) {
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 Parent
// 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 {
// 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;
};
@ -278,11 +289,11 @@ impl Grid {
};
let Some(children) = children else { return };
for (child, actual_parent) in parent_query.iter_many(children) {
for (child, child_of) in parent_query.iter_many(children) {
assert_eq!(
actual_parent.get(), entity,
"Malformed hierarchy. This probably means that your hierarchy has been improperly maintained, or contains a cycle"
);
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.
//
@ -294,7 +305,7 @@ impl Grid {
transform_query,
parent_query,
child,
changed || actual_parent.is_changed(),
changed || child_of.is_changed(),
);
}
}
@ -337,10 +348,10 @@ mod tests {
let mut q = app
.world_mut()
.query_filtered::<&GlobalTransform, With<Test>>();
let actual_transform = *q.single(app.world());
let actual_transform = *q.single(app.world()).unwrap();
assert_eq!(
actual_transform,
GlobalTransform::from_xyz(2004.0, 2005.0, 2006.0)
)
);
}
}

View File

@ -1,16 +1,18 @@
//! Components for spatial hashing.
use std::hash::{Hash, Hasher};
use alloc::vec::Vec;
use core::hash::{BuildHasher, Hash, Hasher};
use crate::prelude::*;
use bevy_ecs::prelude::*;
use bevy_hierarchy::Parent;
use bevy_ecs::{prelude::*, relationship::Relationship};
use bevy_math::IVec3;
use bevy_platform_support::{hash::FixedHasher, time::Instant};
use bevy_reflect::Reflect;
use bevy_utils::{AHasher, Instant, Parallel};
use super::{ChangedGridHashes, GridHashMapFilter};
use crate::portable_par::PortableParallel;
/// A fast but lossy version of [`GridHash`]. Use this component when you don't care about false
/// positives (hash collisions). See the docs on [`GridHash::fast_eq`] for more details on fast but
/// lossy equality checks.
@ -47,7 +49,7 @@ impl From<GridHash> for FastGridHash {
/// [`GridHashMap`] resource.
///
/// Due to grids and multiple big spaces in a single world, this must use both the [`GridCell`] and
/// the [`Parent`] of the entity to uniquely identify its position. These two values are then hashed
/// the [`ChildOf`] of the entity to uniquely identify its position. These two values are then hashed
/// and stored in this spatial hash component.
#[derive(Component, Clone, Copy, Debug, Reflect)]
pub struct GridHash {
@ -89,15 +91,15 @@ impl GridHash {
/// Intentionally left private, so we can ensure the only place these are constructed/mutated is
/// this module. This allows us to optimize change detection using [`ChangedGridHashes`].
#[inline]
pub(super) fn new(parent: &Parent, cell: &GridCell) -> Self {
pub(super) fn new(parent: &ChildOf, cell: &GridCell) -> Self {
Self::from_parent(parent.get(), cell)
}
#[inline]
pub(super) fn from_parent(parent: Entity, cell: &GridCell) -> Self {
let hasher = &mut AHasher::default();
let mut hasher = FixedHasher.build_hasher();
hasher.write_u64(parent.to_bits());
cell.hash(hasher);
cell.hash(&mut hasher);
GridHash {
cell: *cell,
@ -131,7 +133,7 @@ impl GridHash {
/// errors.
///
/// In other words, this should only be used for acceleration, when you want to quickly cull
/// non-overlapping cells, and you will be double checking for false positives later.
/// non-overlapping cells, and you will be double-checking for false positives later.
#[inline]
pub fn fast_eq(&self, other: &Self) -> bool {
self.pre_hash == other.pre_hash
@ -160,13 +162,19 @@ impl GridHash {
mut commands: Commands,
mut changed_hashes: ResMut<ChangedGridHashes<F>>,
mut spatial_entities: Query<
(Entity, &Parent, &GridCell, &mut GridHash, &mut FastGridHash),
(F, Or<(Changed<Parent>, Changed<GridCell>)>),
(
Entity,
&ChildOf,
&GridCell,
&mut GridHash,
&mut FastGridHash,
),
(F, Or<(Changed<ChildOf>, Changed<GridCell>)>),
>,
added_entities: Query<(Entity, &Parent, &GridCell), (F, Without<GridHash>)>,
added_entities: Query<(Entity, &ChildOf, &GridCell), (F, Without<GridHash>)>,
mut stats: Option<ResMut<crate::timing::GridHashStats>>,
mut thread_updated_hashes: Local<Parallel<Vec<Entity>>>,
mut thread_commands: Local<Parallel<Vec<(Entity, GridHash, FastGridHash)>>>,
mut thread_updated_hashes: Local<PortableParallel<Vec<Entity>>>,
mut thread_commands: Local<PortableParallel<Vec<(Entity, GridHash, FastGridHash)>>>,
) {
let start = Instant::now();
@ -206,7 +214,7 @@ impl GridHash {
self.cell
}
/// The [`Parent`] [`Grid`] of this spatial hash.
/// The [`ChildOf`] [`Grid`] of this spatial hash.
pub fn grid(&self) -> Entity {
self.grid
}

View File

@ -1,13 +1,16 @@
//! The [`GridHashMap`] that contains mappings between entities and their spatial hash.
use std::{collections::VecDeque, marker::PhantomData, time::Instant};
use alloc::collections::VecDeque;
use core::marker::PhantomData;
use super::GridHashMapFilter;
use crate::prelude::*;
use bevy_ecs::{entity::EntityHash, prelude::*};
use bevy_utils::{
hashbrown::{HashMap, HashSet},
PassHash,
use bevy_platform_support::{
collections::{HashMap, HashSet},
hash::PassHash,
prelude::*,
time::Instant,
};
/// An entry in a [`GridHashMap`], accessed with a [`GridHash`].
@ -51,10 +54,10 @@ pub trait SpatialEntryToEntities<'a> {
impl<'a, T, I> SpatialEntryToEntities<'a> for T
where
T: Iterator<Item = I> + 'a,
I: SpatialEntryToEntities<'a>,
I: SpatialEntryToEntities<'a> + 'a,
{
fn entities(self) -> impl Iterator<Item = Entity> + 'a {
self.flat_map(|entry| entry.entities())
self.flat_map(SpatialEntryToEntities::entities)
}
}
@ -87,11 +90,11 @@ where
spooky: PhantomData<F>,
}
impl<F> std::fmt::Debug for GridHashMap<F>
impl<F> core::fmt::Debug for GridHashMap<F>
where
F: GridHashMapFilter,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("GridHashMap")
.field("map", &self.map)
.field("reverse_map", &self.reverse_map)
@ -150,9 +153,9 @@ where
&'a self,
entry: &'a GridHashEntry,
) -> impl Iterator<Item = &'a GridHashEntry> + 'a {
// Use `std::iter::once` to avoid returning a function-local variable.
// Use `core::iter::once` to avoid returning a function-local variable.
Iterator::chain(
std::iter::once(entry),
core::iter::once(entry),
entry.occupied_neighbors.iter().map(|neighbor_hash| {
self.get(neighbor_hash)
.expect("occupied_neighbors should be occupied")
@ -177,8 +180,8 @@ where
center: &'a GridHash,
radius: u8,
) -> impl Iterator<Item = &'a GridHashEntry> + 'a {
// Use `std::iter::once` to avoid returning a function-local variable.
Iterator::chain(std::iter::once(*center), center.adjacent(radius))
// Use `core::iter::once` to avoid returning a function-local variable.
Iterator::chain(core::iter::once(*center), center.adjacent(radius))
.filter_map(|hash| self.get(&hash))
}
@ -194,7 +197,7 @@ where
///
/// Consider the case of a long thin U-shaped set of connected cells. While iterating from one
/// end of the "U" to the other with this flood fill, if any of the cells near the base of the
/// "U" exceed the max_depth (radius), iteration will stop. Even if the "U" loops back within
/// "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.
@ -261,7 +264,7 @@ where
spatial_map.map.just_removed.clear();
for entity in removed.read() {
spatial_map.remove(entity)
spatial_map.remove(entity);
}
if let Some(ref mut stats) = stats {
@ -309,7 +312,7 @@ where
#[inline]
fn remove(&mut self, entity: Entity) {
if let Some(old_hash) = self.reverse_map.remove(&entity) {
self.map.remove(entity, old_hash)
self.map.remove(entity, old_hash);
}
}
}

View File

@ -1,10 +1,11 @@
//! Spatial hashing acceleration structure. See [`GridHashPlugin`].
use std::marker::PhantomData;
use core::marker::PhantomData;
use crate::prelude::*;
use bevy_app::prelude::*;
use bevy_ecs::{prelude::*, query::QueryFilter};
use bevy_platform_support::prelude::*;
pub mod component;
pub mod map;
@ -110,10 +111,8 @@ impl<F: GridHashMapFilter> Default for ChangedGridHashes<F> {
// hash ever recomputed? Is it removed? Is the spatial map updated?
#[cfg(test)]
mod tests {
use std::sync::OnceLock;
use crate::{hash::map::SpatialEntryToEntities, prelude::*};
use bevy_utils::hashbrown::HashSet;
use bevy_platform_support::{collections::HashSet, sync::OnceLock};
#[test]
fn entity_despawn() {
@ -269,7 +268,7 @@ mod tests {
let entities = app.world().resource::<Entities>().clone();
let parent = app
.world_mut()
.query::<&Parent>()
.query::<&ChildOf>()
.get(app.world(), entities.a)
.unwrap();

View File

@ -1,14 +1,16 @@
//! Detect and update groups of nearby occupied cells.
use std::{hash::Hash, marker::PhantomData, ops::Deref};
use core::{hash::Hash, marker::PhantomData, ops::Deref};
use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use bevy_tasks::{ComputeTaskPool, ParallelSliceMut};
use bevy_utils::{
hashbrown::{HashMap, HashSet},
Instant, PassHash,
use bevy_platform_support::prelude::*;
use bevy_platform_support::{
collections::{HashMap, HashSet},
hash::PassHash,
time::Instant,
};
use bevy_tasks::{ComputeTaskPool, ParallelSliceMut};
use super::{GridCell, GridHash, GridHashMap, GridHashMapFilter, GridHashMapSystem};
@ -55,7 +57,7 @@ impl GridPartitionId {
impl Hash for GridPartitionId {
#[inline]
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
state.write_u64(self.0);
}
}
@ -146,7 +148,7 @@ where
#[inline]
fn push(&mut self, partition: &GridPartitionId, hash: &GridHash) {
if let Some(partition) = self.partitions.get_mut(partition) {
partition.insert(*hash)
partition.insert(*hash);
} else {
return;
}
@ -180,11 +182,7 @@ where
fn merge(&mut self, partitions: &[GridPartitionId]) {
let Some(largest_partition) = partitions
.iter()
.filter_map(|id| {
self.resolve(id)
.map(|partition| partition.num_cells())
.zip(Some(id))
})
.filter_map(|id| self.resolve(id).map(GridPartition::num_cells).zip(Some(id)))
.reduce(|acc, elem| if elem.0 > acc.0 { elem } else { acc })
.map(|(_cells, id)| id)
else {
@ -307,10 +305,10 @@ where
// it means the partition has not been split, and we can continue to
// the next partition.
return None;
} else {
new_partitions
.push(hash_grid.flood(&this_cell, None).map(|n| n.0).collect());
}
new_partitions
.push(hash_grid.flood(&this_cell, None).map(|n| n.0).collect());
counter += 1;
}
@ -330,7 +328,7 @@ where
{
// We want the original partition to retain the most cells to ensure that the smaller
// sets are the ones that are assigned a new partition ID.
new_partitions.sort_unstable_by_key(|set| set.len());
new_partitions.sort_unstable_by_key(HashSet::len);
if let Some(largest_partition) = new_partitions.pop() {
partition_map.insert(*original_partition_id, largest_partition);
}
@ -357,7 +355,7 @@ mod private {
use super::{GridCell, GridHash};
use crate::precision::GridPrecision;
use bevy_ecs::prelude::*;
use bevy_utils::{hashbrown::HashSet, PassHash};
use bevy_platform_support::{collections::HashSet, hash::PassHash, prelude::*};
/// A group of nearby [`GridCell`](crate::GridCell)s in an island disconnected from all other
/// [`GridCell`](crate::GridCell)s.
@ -385,7 +383,7 @@ mod private {
/// Returns the total number of cells in this partition.
#[inline]
pub fn num_cells(&self) -> usize {
self.tables.iter().map(|t| t.len()).sum()
self.tables.iter().map(HashSet::len).sum()
}
/// The grid this partition resides in.
@ -462,22 +460,23 @@ mod private {
}
#[inline]
pub(crate) fn extend(&mut self, mut partition: GridPartition) {
for mut table in partition.tables.drain(..) {
if table.len() < Self::MIN_TABLE_SIZE {
pub(crate) fn extend(&mut self, mut other: GridPartition) {
assert_eq!(self.grid, other.grid);
for other_table in other.tables.drain(..) {
if other_table.len() < Self::MIN_TABLE_SIZE {
if let Some(i) = self.smallest_table() {
for hash in table.drain() {
self.tables[i].insert_unique_unchecked(hash);
}
self.tables[i].reserve(other_table.len());
self.tables[i].extend(other_table);
} else {
self.tables.push(table);
self.tables.push(other_table);
}
} else {
self.tables.push(table);
self.tables.push(other_table);
}
}
self.min = self.min.min(partition.min);
self.max = self.max.max(partition.max);
self.min = self.min.min(other.min);
self.max = self.max.max(other.max);
}
/// Removes a grid hash from the partition. Returns whether the value was present.
@ -512,12 +511,8 @@ mod private {
/// partition.
#[inline]
fn compute_min(&mut self) {
if let Some(min) = self
.iter()
.map(|hash| hash.cell())
.reduce(|acc, e| acc.min(e))
{
self.min = min
if let Some(min) = self.iter().map(GridHash::cell).reduce(|acc, e| acc.min(e)) {
self.min = min;
} else {
self.min = GridCell::ONE * 1e10f64 as GridPrecision;
}
@ -527,12 +522,8 @@ mod private {
/// partition.
#[inline]
fn compute_max(&mut self) {
if let Some(max) = self
.iter()
.map(|hash| hash.cell())
.reduce(|acc, e| acc.max(e))
{
self.max = max
if let Some(max) = self.iter().map(GridHash::cell).reduce(|acc, e| acc.max(e)) {
self.max = max;
} else {
self.min = GridCell::ONE * -1e10 as GridPrecision;
}

View File

@ -106,7 +106,7 @@
//! grid, in high precision.
//!
//! Entities at the root of bevy's entity hierarchy are not in a grid. This allows plugins from the
//! rest of the ecosystem to operate normally, such as bevy_ui, which relies on the built-in
//! rest of the ecosystem to operate normally, such as `bevy_ui`, which relies on the built-in
//! transform propagation system. This also means that if you don't need to place entities in a
//! high-precision grid, you don't have to, as the process is opt-in. The high-precision
//! hierarchical grids are explicit. Each high-precision tree must have a [`BigSpace`] at the root,
@ -197,12 +197,17 @@
#![allow(clippy::type_complexity)]
#![warn(missing_docs)]
#![no_std]
extern crate alloc;
#[allow(unused_imports)] // For docs
use bevy_transform::prelude::*;
#[allow(unused_imports)] // For docs
use prelude::*;
pub(crate) mod portable_par;
pub mod bundles;
pub mod commands;
pub mod floating_origins;
@ -220,13 +225,11 @@ pub mod debug;
#[cfg(test)]
mod tests;
/// Common big_space imports.
/// Common `big_space` imports.
pub mod prelude {
use crate::*;
pub use bundles::{BigGridBundle, BigSpaceRootBundle, BigSpatialBundle};
pub use commands::{BigSpaceCommands, GridCommands, SpatialEntityCommands};
#[cfg(feature = "debug")]
pub use debug::FloatingOriginDebugPlugin;
pub use floating_origins::{BigSpace, FloatingOrigin};
pub use grid::{
cell::GridCell,
@ -242,6 +245,11 @@ pub mod prelude {
pub use plugin::{BigSpacePlugin, FloatingOriginSystem};
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;
}
/// Contains the [`GridPrecision`] integer index type, which defines how much precision is available

View File

@ -1,6 +1,7 @@
//! The bevy plugin for big_space.
//! The bevy plugin for `big_space`.
use crate::prelude::*;
use alloc::vec::Vec;
use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use bevy_transform::prelude::*;
@ -39,9 +40,6 @@ pub enum FloatingOriginSystem {
impl Plugin for BigSpacePlugin {
fn build(&self, app: &mut App) {
// Silence bevy's built-in error spam about GlobalTransforms in the hierarchy
app.insert_resource(bevy_hierarchy::ReportHierarchyIssue::<GlobalTransform>::new(false));
// Performance timings
app.add_plugins(crate::timing::TimingStatsPlugin);
@ -72,6 +70,7 @@ impl Plugin for BigSpacePlugin {
// Reflect
.register_type::<Transform>()
.register_type::<GlobalTransform>()
.register_type::<TransformTreeChanged>()
.register_type::<GridCell>()
.register_type::<Grid>()
.register_type::<BigSpace>()
@ -101,18 +100,166 @@ impl Plugin for BigSpacePlugin {
.add_systems(
PostStartup,
(
propagate_parent_transforms,
bevy_transform::systems::sync_simple_transforms,
bevy_transform::systems::propagate_transforms,
)
.in_set(TransformSystem::TransformPropagate),
)
.add_systems(
PostUpdate,
(
propagate_parent_transforms,
bevy_transform::systems::sync_simple_transforms,
bevy_transform::systems::propagate_transforms,
)
.in_set(TransformSystem::TransformPropagate),
);
}
}
/// 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(),
);
}
}
}

126
src/portable_par.rs Normal file
View File

@ -0,0 +1,126 @@
//! Module for defining a `no_std` compatible `Parallel` thread local.
#![allow(dead_code)]
use alloc::vec::Vec;
use core::ops::DerefMut;
/// A no_std-compatible version of bevy's `Parallel`.
#[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>>,
);
/// A scope guard of a `Parallel`, when this struct is dropped ,the value will writeback to its `Parallel`
impl<T: Send> PortableParallel<T> {
/// Gets a mutable iterator over all of the per-thread queues.
pub fn iter_mut(&mut self) -> impl Iterator<Item = impl DerefMut<Target = T> + '_> {
#[cfg(feature = "std")]
{
self.0.iter_mut()
}
#[cfg(not(feature = "std"))]
{
self.0.get_mut().unwrap().iter_mut()
}
}
/// Clears all of the stored thread local values.
pub fn clear(&mut self) {
#[cfg(feature = "std")]
self.0.clear();
#[cfg(not(feature = "std"))]
{
*self.0.write().unwrap() = None;
}
}
}
impl<T: Default + Send> PortableParallel<T> {
/// Retrieves the thread-local value for the current thread and runs `f` on it.
///
/// If there is no thread-local value, it will be initialized to its default.
pub fn scope<R>(&self, f: impl FnOnce(&mut T) -> R) -> R {
#[cfg(feature = "std")]
let ret = self.0.scope(f);
#[cfg(not(feature = "std"))]
let ret = {
let mut cell = self.0.write().unwrap();
let value = cell.get_or_insert_default();
f(value)
};
ret
}
/// Mutably borrows the thread-local value.
///
/// If there is no thread-local value, it will be initialized to it's default.
pub fn borrow_local_mut(&self) -> impl DerefMut<Target = T> + '_ {
#[cfg(feature = "std")]
let ret = self.0.borrow_local_mut();
#[cfg(not(feature = "std"))]
let ret = {
if self.0.read().unwrap().is_none() {
*self.0.write().unwrap() = Some(Default::default());
}
let opt = self.0.write().unwrap();
no_std_deref::UncheckedDerefMutWrapper(opt)
};
ret
}
}
/// 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 core::ops::{Deref, DerefMut};
pub struct UncheckedDerefMutWrapper<'a, T>(pub(super) RwLockWriteGuard<'a, Option<T>>);
impl<T> Deref for UncheckedDerefMutWrapper<'_, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
self.0.as_ref().unwrap()
}
}
impl<T> DerefMut for UncheckedDerefMutWrapper<'_, T> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.0.as_mut().unwrap()
}
}
}
impl<T, I> PortableParallel<I>
where
I: IntoIterator<Item = T> + Default + Send + 'static,
{
/// Drains all enqueued items from all threads and returns an iterator over them.
///
/// Unlike [`Vec::drain`], this will piecemeal remove chunks of the data stored.
/// If iteration is terminated part way, the rest of the enqueued items in the same
/// chunk will be dropped, and the rest of the undrained elements will remain.
///
/// The ordering is not guaranteed.
pub fn drain(&mut self) -> impl Iterator<Item = T> + '_ {
#[cfg(feature = "std")]
let ret = self.0.drain();
#[cfg(not(feature = "std"))]
let ret = self.0.get_mut().unwrap().take().into_iter().flatten();
ret
}
}
impl<T: Send + 'static> PortableParallel<Vec<T>> {
/// Collect all enqueued items from all threads and appends them to the end of a
/// single Vec.
///
/// The ordering is not guaranteed.
pub fn drain_into(&mut self, out: &mut Vec<T>) {
#[cfg(feature = "std")]
self.0.drain_into(out);
#[cfg(not(feature = "std"))]
out.extend(self.drain());
}
}

View File

@ -1,6 +1,7 @@
//! Timing statistics for transform propagation
use std::{collections::VecDeque, iter::Sum, ops::Div, time::Duration};
use alloc::collections::VecDeque;
use core::{iter::Sum, ops::Div, time::Duration};
use crate::prelude::*;
use bevy_app::prelude::*;
@ -12,7 +13,7 @@ use bevy_transform::TransformSystem;
pub struct TimingStatsPlugin;
impl Plugin for TimingStatsPlugin {
fn build(&self, app: &mut bevy_app::App) {
fn build(&self, app: &mut App) {
app.init_resource::<PropagationStats>()
.register_type::<PropagationStats>()
.init_resource::<GridHashStats>()
@ -108,7 +109,7 @@ impl PropagationStats {
}
}
impl<'a> std::iter::Sum<&'a PropagationStats> for PropagationStats {
impl<'a> Sum<&'a PropagationStats> for PropagationStats {
fn sum<I: Iterator<Item = &'a PropagationStats>>(iter: I) -> Self {
iter.fold(PropagationStats::default(), |mut acc, e| {
acc.grid_recentering += e.grid_recentering;
@ -122,7 +123,7 @@ impl<'a> std::iter::Sum<&'a PropagationStats> for PropagationStats {
}
}
impl std::ops::Div<u32> for PropagationStats {
impl Div<u32> for PropagationStats {
type Output = Self;
fn div(self, rhs: u32) -> Self::Output {
@ -178,7 +179,7 @@ impl GridHashStats {
}
}
impl<'a> std::iter::Sum<&'a GridHashStats> for GridHashStats {
impl<'a> Sum<&'a GridHashStats> for GridHashStats {
fn sum<I: Iterator<Item = &'a GridHashStats>>(iter: I) -> Self {
iter.fold(GridHashStats::default(), |mut acc, e| {
acc.hash_update_duration += e.hash_update_duration;

View File

@ -1,9 +1,11 @@
//! Tools for validating high-precision transform hierarchies
use bevy_ecs::prelude::*;
use bevy_hierarchy::prelude::*;
use bevy_platform_support::{
collections::{HashMap, HashSet},
prelude::*,
};
use bevy_transform::prelude::*;
use bevy_utils::{HashMap, HashSet};
use crate::{grid::Grid, BigSpace, FloatingOrigin, GridCell};
@ -16,7 +18,7 @@ struct ValidationStackEntry {
struct ValidatorCaches {
query_state_cache: HashMap<&'static str, QueryState<(Entity, Option<&'static Children>)>>,
validator_cache: HashMap<&'static str, Vec<Box<dyn ValidHierarchyNode>>>,
root_query: Option<QueryState<Entity, Without<Parent>>>,
root_query: Option<QueryState<Entity, Without<ChildOf>>>,
stack: Vec<ValidationStackEntry>,
/// Only report errors for an entity one time.
error_entities: HashSet<Entity>,
@ -29,7 +31,7 @@ pub fn validate_hierarchy<V: 'static + ValidHierarchyNode + Default>(world: &mut
let root_entities = caches
.root_query
.get_or_insert(world.query_filtered::<Entity, Without<Parent>>())
.get_or_insert(world.query_filtered::<Entity, Without<ChildOf>>())
.iter(world)
.collect();
@ -89,11 +91,15 @@ pub fn validate_hierarchy<V: 'static + ValidHierarchyNode + Default>(world: &mut
});
let mut inspect = String::new();
world.inspect_entity(*entity).for_each(|info| {
inspect.push_str(" - ");
inspect.push_str(info.name());
inspect.push('\n');
});
world
.inspect_entity(*entity)
.into_iter()
.flatten()
.for_each(|info| {
inspect.push_str(" - ");
inspect.push_str(info.name());
inspect.push('\n');
});
tracing::error!("
-------------------------------------------
@ -134,12 +140,13 @@ pub trait ValidHierarchyNode: sealed::CloneHierarchy + Send + Sync {
fn allowed_child_nodes(&self) -> Vec<Box<dyn ValidHierarchyNode>>;
/// A unique identifier of this type
fn name(&self) -> &'static str {
std::any::type_name::<Self>()
core::any::type_name::<Self>()
}
}
mod sealed {
use super::ValidHierarchyNode;
use bevy_platform_support::prelude::*;
pub trait CloneHierarchy {
fn clone_box(&self) -> Box<dyn ValidHierarchyNode>;
@ -219,7 +226,7 @@ impl ValidHierarchyNode for RootFrame {
.with::<GlobalTransform>()
.without::<GridCell>()
.without::<Transform>()
.without::<Parent>()
.without::<ChildOf>()
.without::<FloatingOrigin>();
}
@ -248,7 +255,7 @@ impl ValidHierarchyNode for RootSpatialLowPrecision {
.without::<GridCell>()
.without::<BigSpace>()
.without::<Grid>()
.without::<Parent>()
.without::<ChildOf>()
.without::<FloatingOrigin>();
}
@ -274,7 +281,7 @@ impl ValidHierarchyNode for ChildFrame {
.with::<GridCell>()
.with::<Transform>()
.with::<GlobalTransform>()
.with::<Parent>()
.with::<ChildOf>()
.without::<BigSpace>();
}
@ -300,7 +307,7 @@ impl ValidHierarchyNode for ChildRootSpatialLowPrecision {
query
.with::<Transform>()
.with::<GlobalTransform>()
.with::<Parent>()
.with::<ChildOf>()
.with::<crate::grid::propagation::LowPrecisionRoot>()
.without::<GridCell>()
.without::<BigSpace>()
@ -328,7 +335,7 @@ impl ValidHierarchyNode for ChildSpatialLowPrecision {
query
.with::<Transform>()
.with::<GlobalTransform>()
.with::<Parent>()
.with::<ChildOf>()
.without::<GridCell>()
.without::<BigSpace>()
.without::<Grid>()
@ -356,7 +363,7 @@ impl ValidHierarchyNode for ChildSpatialHighPrecision {
.with::<GridCell>()
.with::<Transform>()
.with::<GlobalTransform>()
.with::<Parent>()
.with::<ChildOf>()
.without::<BigSpace>()
.without::<Grid>();
}

View File

@ -68,7 +68,7 @@ pub struct GridTransformOwned {
pub cell: GridCell,
}
impl std::ops::Sub for GridTransformOwned {
impl core::ops::Sub for GridTransformOwned {
type Output = Self;
/// Compute a new transform that maps from `source` to `self`.
@ -81,7 +81,7 @@ impl std::ops::Sub for GridTransformOwned {
}
}
impl std::ops::Add for GridTransformOwned {
impl core::ops::Add for GridTransformOwned {
type Output = Self;
/// Compute a new transform that shifts, scales and rotates `self` by `diff`.