Compare commits
95 Commits
custom
...
release-0.
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b231ebbc19 | ||
![]() |
201dd62a74 | ||
![]() |
ff070da7e2 | ||
![]() |
1db0214f24 | ||
![]() |
309c224ca8 | ||
![]() |
24fdad3a36 | ||
![]() |
e7333510c3 | ||
![]() |
31b861401c | ||
![]() |
99c465dcb5 | ||
![]() |
d7a0cc6bce | ||
![]() |
fda2e4b59c | ||
![]() |
dc56614b86 | ||
![]() |
4a05c737a2 | ||
![]() |
036d0026be | ||
![]() |
73b43aa6cf | ||
![]() |
20638f3a10 | ||
![]() |
26f7313212 | ||
![]() |
bea8823aa9 | ||
![]() |
7b98db6d7c | ||
![]() |
7c603874bf | ||
![]() |
5d7da827b7 | ||
![]() |
4f0b0e0989 | ||
![]() |
cc1764772e | ||
![]() |
99db59c176 | ||
![]() |
9a4de9c54d | ||
![]() |
a6feb5ba74 | ||
![]() |
e65e4be98c | ||
![]() |
27aa9d2b7e | ||
![]() |
f8e165be0a | ||
![]() |
4736fe0dea | ||
![]() |
f89f7f306c | ||
![]() |
82f01569e8 | ||
![]() |
65daab8517 | ||
![]() |
9eb547ec1f | ||
![]() |
8c1b9a6c18 | ||
![]() |
a0e7429363 | ||
![]() |
a41ed7822f | ||
![]() |
b56a693c34 | ||
![]() |
073db8cf36 | ||
![]() |
8af12c8775 | ||
![]() |
e73063e1d5 | ||
![]() |
cf66df1d0d | ||
![]() |
783fc29cd3 | ||
![]() |
47ad37ec84 | ||
![]() |
472a6e8283 | ||
![]() |
5d5e67fa35 | ||
![]() |
b74b3b2123 | ||
![]() |
4608708d8d | ||
![]() |
f2e66726b6 | ||
![]() |
165f399489 | ||
![]() |
6cf04c213b | ||
![]() |
1c838ff6b3 | ||
![]() |
03f63e72cf | ||
![]() |
55ee49b7ad | ||
![]() |
eca8220761 | ||
![]() |
5ed296ff03 | ||
![]() |
cc4681fc44 | ||
![]() |
ccfae7ebe7 | ||
![]() |
1ea1b76c75 | ||
![]() |
ac50539aab | ||
![]() |
727b0f6e27 | ||
![]() |
7c0b1b9029 | ||
![]() |
84be2b3f1e | ||
![]() |
eca7e87d47 | ||
![]() |
cdd1f71596 | ||
![]() |
92176ce576 | ||
![]() |
e14f3ba1aa | ||
![]() |
9a82ecfcba | ||
![]() |
93f48edbc3 | ||
![]() |
7cd90990f9 | ||
![]() |
65dbfe249b | ||
![]() |
effbcdfc92 | ||
![]() |
3ced49f672 | ||
![]() |
a3916b4af4 | ||
![]() |
c060e3e1fe | ||
![]() |
854983dc7e | ||
![]() |
a944598812 | ||
![]() |
cbebcb0d3f | ||
![]() |
8c5769ab92 | ||
![]() |
1ae616fef1 | ||
![]() |
a2c5b0d415 | ||
![]() |
be65fbb691 | ||
![]() |
91db570d6e | ||
![]() |
621cd23ffc | ||
![]() |
7ae3c94b0f | ||
![]() |
b14684ee12 | ||
![]() |
971723e4b4 | ||
![]() |
4df95384ba | ||
![]() |
c71fa76f12 | ||
![]() |
25306e7532 | ||
![]() |
ce19b0f63f | ||
![]() |
79a93de606 | ||
![]() |
5f2e927ae5 | ||
![]() |
43204447e8 | ||
![]() |
2d11d9a48d |
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
@ -6,6 +6,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- release-*
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
1
.github/workflows/validation-jobs.yml
vendored
1
.github/workflows/validation-jobs.yml
vendored
@ -6,6 +6,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- release-*
|
||||
|
||||
concurrency:
|
||||
group: ${{github.workflow}}-${{github.ref}}
|
||||
|
72
Cargo.toml
72
Cargo.toml
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bevy"
|
||||
version = "0.14.0-dev"
|
||||
version = "0.14.0"
|
||||
edition = "2021"
|
||||
categories = ["game-engines", "graphics", "gui", "rendering"]
|
||||
description = "A refreshingly simple data-driven game engine and app framework"
|
||||
@ -10,7 +10,7 @@ keywords = ["game", "engine", "gamedev", "graphics", "bevy"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/bevyengine/bevy"
|
||||
documentation = "https://docs.rs/bevy"
|
||||
rust-version = "1.78.0"
|
||||
rust-version = "1.79.0"
|
||||
|
||||
[workspace]
|
||||
exclude = [
|
||||
@ -76,6 +76,7 @@ default = [
|
||||
"bevy_gizmos",
|
||||
"android_shared_stdcxx",
|
||||
"tonemapping_luts",
|
||||
"smaa_luts",
|
||||
"default_font",
|
||||
"webgl2",
|
||||
"sysinfo_plugin",
|
||||
@ -282,6 +283,9 @@ detailed_trace = ["bevy_internal/detailed_trace"]
|
||||
# Include tonemapping Look Up Tables KTX2 files. If everything is pink, you need to enable this feature or change the `Tonemapping` method on your `Camera2dBundle` or `Camera3dBundle`.
|
||||
tonemapping_luts = ["bevy_internal/tonemapping_luts", "ktx2", "zstd"]
|
||||
|
||||
# Include SMAA Look Up Tables KTX2 Files
|
||||
smaa_luts = ["bevy_internal/smaa_luts"]
|
||||
|
||||
# Enable AccessKit on Unix backends (currently only works with experimental screen readers and forks.)
|
||||
accesskit_unix = ["bevy_internal/accesskit_unix"]
|
||||
|
||||
@ -308,6 +312,9 @@ pbr_multi_layer_material_textures = [
|
||||
"bevy_internal/pbr_multi_layer_material_textures",
|
||||
]
|
||||
|
||||
# Enable support for anisotropy texture in the `StandardMaterial`, at the risk of blowing past the global, per-shader texture limit on older/lower-end GPUs
|
||||
pbr_anisotropy_texture = ["bevy_internal/pbr_anisotropy_texture"]
|
||||
|
||||
# Enable some limitations to be able to use WebGL2. Please refer to the [WebGL2 and WebGPU](https://github.com/bevyengine/bevy/tree/latest/examples#webgl2-and-webgpu) section of the examples README for more information on how to run Wasm builds with WebGPU.
|
||||
webgl2 = ["bevy_internal/webgl"]
|
||||
|
||||
@ -339,11 +346,11 @@ ios_simulator = ["bevy_internal/ios_simulator"]
|
||||
bevy_state = ["bevy_internal/bevy_state"]
|
||||
|
||||
[dependencies]
|
||||
bevy_internal = { path = "crates/bevy_internal", version = "0.14.0-dev", default-features = false }
|
||||
bevy_internal = { path = "crates/bevy_internal", version = "0.14.0", default-features = false }
|
||||
|
||||
# WASM does not support dynamic linking.
|
||||
[target.'cfg(not(target_family = "wasm"))'.dependencies]
|
||||
bevy_dylib = { path = "crates/bevy_dylib", version = "0.14.0-dev", default-features = false, optional = true }
|
||||
bevy_dylib = { path = "crates/bevy_dylib", version = "0.14.0", default-features = false, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
rand = "0.8.0"
|
||||
@ -2534,6 +2541,17 @@ description = "Systems run in parallel, but their order isn't always determinist
|
||||
category = "ECS (Entity Component System)"
|
||||
wasm = false
|
||||
|
||||
[[example]]
|
||||
name = "observers"
|
||||
path = "examples/ecs/observers.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[package.metadata.example.observers]
|
||||
name = "Observers"
|
||||
description = "Demonstrates observers that react to events (both built-in life-cycle events and custom events)"
|
||||
category = "ECS (Entity Component System)"
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "3d_rotation"
|
||||
path = "examples/transforms/3d_rotation.rs"
|
||||
@ -3024,6 +3042,17 @@ description = "Demonstrates all the primitives which can be sampled."
|
||||
category = "Math"
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "custom_primitives"
|
||||
path = "examples/math/custom_primitives.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[package.metadata.example.custom_primitives]
|
||||
name = "Custom Primitives"
|
||||
description = "Demonstrates how to add custom primitives and useful traits for them."
|
||||
category = "Math"
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "random_sampling"
|
||||
path = "examples/math/random_sampling.rs"
|
||||
@ -3086,6 +3115,28 @@ path = "examples/dev_tools/fps_overlay.rs"
|
||||
doc-scrape-examples = true
|
||||
required-features = ["bevy_dev_tools"]
|
||||
|
||||
[[example]]
|
||||
name = "2d_top_down_camera"
|
||||
path = "examples/camera/2d_top_down_camera.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[package.metadata.example.2d_top_down_camera]
|
||||
name = "2D top-down camera"
|
||||
description = "A 2D top-down camera smoothly following player movements"
|
||||
category = "Camera"
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "first_person_view_model"
|
||||
path = "examples/camera/first_person_view_model.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[package.metadata.example.first_person_view_model]
|
||||
name = "First person view model"
|
||||
description = "A first-person camera that uses a world model and a view model with different field of views (FOV)"
|
||||
category = "Camera"
|
||||
wasm = true
|
||||
|
||||
[package.metadata.example.fps_overlay]
|
||||
name = "FPS overlay"
|
||||
description = "Demonstrates FPS overlay"
|
||||
@ -3163,7 +3214,7 @@ wasm = true
|
||||
name = "anisotropy"
|
||||
path = "examples/3d/anisotropy.rs"
|
||||
doc-scrape-examples = true
|
||||
required-features = ["jpeg"]
|
||||
required-features = ["jpeg", "pbr_anisotropy_texture"]
|
||||
|
||||
[package.metadata.example.anisotropy]
|
||||
name = "Anisotropy"
|
||||
@ -3171,6 +3222,17 @@ description = "Displays an example model with anisotropy"
|
||||
category = "3D Rendering"
|
||||
wasm = false
|
||||
|
||||
[[example]]
|
||||
name = "custom_phase_item"
|
||||
path = "examples/shader/custom_phase_item.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[package.metadata.example.custom_phase_item]
|
||||
name = "Custom phase item"
|
||||
description = "Demonstrates how to enqueue custom draw commands in a render phase"
|
||||
category = "Shaders"
|
||||
wasm = true
|
||||
|
||||
[profile.wasm-release]
|
||||
inherits = "release"
|
||||
opt-level = "z"
|
||||
|
288
assets/models/ship/craft_speederD.gltf
Normal file
288
assets/models/ship/craft_speederD.gltf
Normal file
@ -0,0 +1,288 @@
|
||||
{
|
||||
"extensionsUsed": [
|
||||
"KHR_materials_unlit"
|
||||
],
|
||||
"asset": {
|
||||
"generator": "UniGLTF-1.27",
|
||||
"version": "2.0"
|
||||
},
|
||||
"buffers": [
|
||||
{
|
||||
"uri": "craft_speederD_data.bin",
|
||||
"byteLength": 20120
|
||||
}
|
||||
],
|
||||
"bufferViews": [
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteOffset": 0,
|
||||
"byteLength": 6096,
|
||||
"target": 34962
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteOffset": 6096,
|
||||
"byteLength": 6096,
|
||||
"target": 34962
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteOffset": 12192,
|
||||
"byteLength": 4064,
|
||||
"target": 34962
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteOffset": 16256,
|
||||
"byteLength": 732,
|
||||
"target": 34963
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteOffset": 16988,
|
||||
"byteLength": 1368,
|
||||
"target": 34963
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteOffset": 18356,
|
||||
"byteLength": 456,
|
||||
"target": 34963
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteOffset": 18812,
|
||||
"byteLength": 1308,
|
||||
"target": 34963
|
||||
}
|
||||
],
|
||||
"accessors": [
|
||||
{
|
||||
"bufferView": 0,
|
||||
"byteOffset": 0,
|
||||
"type": "VEC3",
|
||||
"componentType": 5126,
|
||||
"count": 508,
|
||||
"max": [
|
||||
1.4,
|
||||
0.9,
|
||||
1.11283529
|
||||
],
|
||||
"min": [
|
||||
-1.4,
|
||||
0,
|
||||
-1.11283529
|
||||
],
|
||||
"normalized": false
|
||||
},
|
||||
{
|
||||
"bufferView": 1,
|
||||
"byteOffset": 0,
|
||||
"type": "VEC3",
|
||||
"componentType": 5126,
|
||||
"count": 508,
|
||||
"normalized": false
|
||||
},
|
||||
{
|
||||
"bufferView": 2,
|
||||
"byteOffset": 0,
|
||||
"type": "VEC2",
|
||||
"componentType": 5126,
|
||||
"count": 508,
|
||||
"normalized": false
|
||||
},
|
||||
{
|
||||
"bufferView": 3,
|
||||
"byteOffset": 0,
|
||||
"type": "SCALAR",
|
||||
"componentType": 5125,
|
||||
"count": 183,
|
||||
"normalized": false
|
||||
},
|
||||
{
|
||||
"bufferView": 4,
|
||||
"byteOffset": 0,
|
||||
"type": "SCALAR",
|
||||
"componentType": 5125,
|
||||
"count": 342,
|
||||
"normalized": false
|
||||
},
|
||||
{
|
||||
"bufferView": 5,
|
||||
"byteOffset": 0,
|
||||
"type": "SCALAR",
|
||||
"componentType": 5125,
|
||||
"count": 114,
|
||||
"normalized": false
|
||||
},
|
||||
{
|
||||
"bufferView": 6,
|
||||
"byteOffset": 0,
|
||||
"type": "SCALAR",
|
||||
"componentType": 5125,
|
||||
"count": 327,
|
||||
"normalized": false
|
||||
}
|
||||
],
|
||||
"materials": [
|
||||
{
|
||||
"name": "metal",
|
||||
"pbrMetallicRoughness": {
|
||||
"baseColorFactor": [
|
||||
0.843137264,
|
||||
0.870588243,
|
||||
0.9098039,
|
||||
1
|
||||
],
|
||||
"metallicFactor": 1,
|
||||
"roughnessFactor": 1
|
||||
},
|
||||
"doubleSided": false,
|
||||
"alphaMode": "OPAQUE"
|
||||
},
|
||||
{
|
||||
"name": "metalDark",
|
||||
"pbrMetallicRoughness": {
|
||||
"baseColorFactor": [
|
||||
0.6750623,
|
||||
0.7100219,
|
||||
0.7735849,
|
||||
1
|
||||
],
|
||||
"metallicFactor": 1,
|
||||
"roughnessFactor": 1
|
||||
},
|
||||
"doubleSided": false,
|
||||
"alphaMode": "OPAQUE"
|
||||
},
|
||||
{
|
||||
"name": "dark",
|
||||
"pbrMetallicRoughness": {
|
||||
"baseColorFactor": [
|
||||
0.274509817,
|
||||
0.298039228,
|
||||
0.34117648,
|
||||
1
|
||||
],
|
||||
"metallicFactor": 1,
|
||||
"roughnessFactor": 1
|
||||
},
|
||||
"doubleSided": false,
|
||||
"alphaMode": "OPAQUE"
|
||||
},
|
||||
{
|
||||
"name": "metalRed",
|
||||
"pbrMetallicRoughness": {
|
||||
"baseColorFactor": [
|
||||
1,
|
||||
0.628524244,
|
||||
0.2028302,
|
||||
1
|
||||
],
|
||||
"metallicFactor": 1,
|
||||
"roughnessFactor": 1
|
||||
},
|
||||
"doubleSided": false,
|
||||
"alphaMode": "OPAQUE"
|
||||
}
|
||||
],
|
||||
"meshes": [
|
||||
{
|
||||
"name": "Mesh craft_speederD",
|
||||
"primitives": [
|
||||
{
|
||||
"mode": 4,
|
||||
"indices": 3,
|
||||
"attributes": {
|
||||
"POSITION": 0,
|
||||
"NORMAL": 1,
|
||||
"TEXCOORD_0": 2
|
||||
},
|
||||
"material": 0
|
||||
},
|
||||
{
|
||||
"mode": 4,
|
||||
"indices": 4,
|
||||
"attributes": {
|
||||
"POSITION": 0,
|
||||
"NORMAL": 1,
|
||||
"TEXCOORD_0": 2
|
||||
},
|
||||
"material": 1
|
||||
},
|
||||
{
|
||||
"mode": 4,
|
||||
"indices": 5,
|
||||
"attributes": {
|
||||
"POSITION": 0,
|
||||
"NORMAL": 1,
|
||||
"TEXCOORD_0": 2
|
||||
},
|
||||
"material": 2
|
||||
},
|
||||
{
|
||||
"mode": 4,
|
||||
"indices": 6,
|
||||
"attributes": {
|
||||
"POSITION": 0,
|
||||
"NORMAL": 1,
|
||||
"TEXCOORD_0": 2
|
||||
},
|
||||
"material": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"nodes": [
|
||||
{
|
||||
"children": [
|
||||
1
|
||||
],
|
||||
"name": "tmpParent",
|
||||
"translation": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1
|
||||
],
|
||||
"scale": [
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "craft_speederD",
|
||||
"translation": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1
|
||||
],
|
||||
"scale": [
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"mesh": 0
|
||||
}
|
||||
],
|
||||
"scenes": [
|
||||
{
|
||||
"nodes": [
|
||||
1
|
||||
]
|
||||
}
|
||||
],
|
||||
"scene": 0
|
||||
}
|
BIN
assets/models/ship/craft_speederD_data.bin
Normal file
BIN
assets/models/ship/craft_speederD_data.bin
Normal file
Binary file not shown.
@ -49,7 +49,6 @@ fn fragment(
|
||||
double_sided,
|
||||
is_front,
|
||||
Nt,
|
||||
view.mip_bias,
|
||||
);
|
||||
#endif
|
||||
|
||||
|
36
assets/shaders/custom_phase_item.wgsl
Normal file
36
assets/shaders/custom_phase_item.wgsl
Normal file
@ -0,0 +1,36 @@
|
||||
// `custom_phase_item.wgsl`
|
||||
//
|
||||
// This shader goes with the `custom_phase_item` example. It demonstrates how to
|
||||
// enqueue custom rendering logic in a `RenderPhase`.
|
||||
|
||||
// The GPU-side vertex structure.
|
||||
struct Vertex {
|
||||
// The world-space position of the vertex.
|
||||
@location(0) position: vec3<f32>,
|
||||
// The color of the vertex.
|
||||
@location(1) color: vec3<f32>,
|
||||
};
|
||||
|
||||
// Information passed from the vertex shader to the fragment shader.
|
||||
struct VertexOutput {
|
||||
// The clip-space position of the vertex.
|
||||
@builtin(position) clip_position: vec4<f32>,
|
||||
// The color of the vertex.
|
||||
@location(0) color: vec3<f32>,
|
||||
};
|
||||
|
||||
// The vertex shader entry point.
|
||||
@vertex
|
||||
fn vertex(vertex: Vertex) -> VertexOutput {
|
||||
// Use an orthographic projection.
|
||||
var vertex_output: VertexOutput;
|
||||
vertex_output.clip_position = vec4(vertex.position.xyz, 1.0);
|
||||
vertex_output.color = vertex.color;
|
||||
return vertex_output;
|
||||
}
|
||||
|
||||
// The fragment shader entry point.
|
||||
@fragment
|
||||
fn fragment(vertex_output: VertexOutput) -> @location(0) vec4<f32> {
|
||||
return vec4(vertex_output.color, 1.0);
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bevy_a11y"
|
||||
version = "0.14.0-dev"
|
||||
version = "0.14.0"
|
||||
edition = "2021"
|
||||
description = "Provides accessibility support for Bevy Engine"
|
||||
homepage = "https://bevyengine.org"
|
||||
@ -10,9 +10,9 @@ keywords = ["bevy", "accessibility", "a11y"]
|
||||
|
||||
[dependencies]
|
||||
# bevy
|
||||
bevy_app = { path = "../bevy_app", version = "0.14.0-dev" }
|
||||
bevy_derive = { path = "../bevy_derive", version = "0.14.0-dev" }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" }
|
||||
bevy_app = { path = "../bevy_app", version = "0.14.0" }
|
||||
bevy_derive = { path = "../bevy_derive", version = "0.14.0" }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0" }
|
||||
|
||||
accesskit = "0.14"
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bevy_animation"
|
||||
version = "0.14.0-dev"
|
||||
version = "0.14.0"
|
||||
edition = "2021"
|
||||
description = "Provides animation functionality for Bevy Engine"
|
||||
homepage = "https://bevyengine.org"
|
||||
@ -10,23 +10,23 @@ keywords = ["bevy"]
|
||||
|
||||
[dependencies]
|
||||
# bevy
|
||||
bevy_app = { path = "../bevy_app", version = "0.14.0-dev" }
|
||||
bevy_asset = { path = "../bevy_asset", version = "0.14.0-dev" }
|
||||
bevy_color = { path = "../bevy_color", version = "0.14.0-dev" }
|
||||
bevy_core = { path = "../bevy_core", version = "0.14.0-dev" }
|
||||
bevy_derive = { path = "../bevy_derive", version = "0.14.0-dev" }
|
||||
bevy_log = { path = "../bevy_log", version = "0.14.0-dev" }
|
||||
bevy_math = { path = "../bevy_math", version = "0.14.0-dev" }
|
||||
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev", features = [
|
||||
bevy_app = { path = "../bevy_app", version = "0.14.0" }
|
||||
bevy_asset = { path = "../bevy_asset", version = "0.14.0" }
|
||||
bevy_color = { path = "../bevy_color", version = "0.14.1" }
|
||||
bevy_core = { path = "../bevy_core", version = "0.14.0" }
|
||||
bevy_derive = { path = "../bevy_derive", version = "0.14.0" }
|
||||
bevy_log = { path = "../bevy_log", version = "0.14.0" }
|
||||
bevy_math = { path = "../bevy_math", version = "0.14.0" }
|
||||
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0", features = [
|
||||
"bevy",
|
||||
"petgraph",
|
||||
] }
|
||||
bevy_render = { path = "../bevy_render", version = "0.14.0-dev" }
|
||||
bevy_time = { path = "../bevy_time", version = "0.14.0-dev" }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" }
|
||||
bevy_transform = { path = "../bevy_transform", version = "0.14.0-dev" }
|
||||
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.14.0-dev" }
|
||||
bevy_render = { path = "../bevy_render", version = "0.14.0" }
|
||||
bevy_time = { path = "../bevy_time", version = "0.14.0" }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.14.0" }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0" }
|
||||
bevy_transform = { path = "../bevy_transform", version = "0.14.0" }
|
||||
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.14.0" }
|
||||
|
||||
# other
|
||||
fixedbitset = "0.5"
|
||||
|
@ -236,7 +236,7 @@ impl Hash for AnimationTargetId {
|
||||
/// Note that each entity can only be animated by one animation player at a
|
||||
/// time. However, you can change [`AnimationTarget`]'s `player` property at
|
||||
/// runtime to change which player is responsible for animating the entity.
|
||||
#[derive(Clone, Component, Reflect)]
|
||||
#[derive(Clone, Copy, Component, Reflect)]
|
||||
#[reflect(Component, MapEntities)]
|
||||
pub struct AnimationTarget {
|
||||
/// The ID of this animation target.
|
||||
@ -326,7 +326,7 @@ pub enum RepeatAnimation {
|
||||
/// playing, but is presently paused.
|
||||
///
|
||||
/// An stopped animation is considered no longer active.
|
||||
#[derive(Debug, Reflect)]
|
||||
#[derive(Debug, Clone, Copy, Reflect)]
|
||||
pub struct ActiveAnimation {
|
||||
/// The factor by which the weight from the [`AnimationGraph`] is multiplied.
|
||||
weight: f32,
|
||||
@ -515,6 +515,21 @@ pub struct AnimationPlayer {
|
||||
blend_weights: HashMap<AnimationNodeIndex, f32>,
|
||||
}
|
||||
|
||||
// This is needed since `#[derive(Clone)]` does not generate optimized `clone_from`.
|
||||
impl Clone for AnimationPlayer {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
active_animations: self.active_animations.clone(),
|
||||
blend_weights: self.blend_weights.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn clone_from(&mut self, source: &Self) {
|
||||
self.active_animations.clone_from(&source.active_animations);
|
||||
self.blend_weights.clone_from(&source.blend_weights);
|
||||
}
|
||||
}
|
||||
|
||||
/// The components that we might need to read or write during animation of each
|
||||
/// animation target.
|
||||
struct AnimationTargetContext<'a> {
|
||||
|
@ -33,8 +33,23 @@ pub struct AnimationTransitions {
|
||||
transitions: Vec<AnimationTransition>,
|
||||
}
|
||||
|
||||
// This is needed since `#[derive(Clone)]` does not generate optimized `clone_from`.
|
||||
impl Clone for AnimationTransitions {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
main_animation: self.main_animation,
|
||||
transitions: self.transitions.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn clone_from(&mut self, source: &Self) {
|
||||
self.main_animation = source.main_animation;
|
||||
self.transitions.clone_from(&source.transitions);
|
||||
}
|
||||
}
|
||||
|
||||
/// An animation that is being faded out as part of a transition
|
||||
#[derive(Debug, Reflect)]
|
||||
#[derive(Debug, Clone, Copy, Reflect)]
|
||||
pub struct AnimationTransition {
|
||||
/// The current weight. Starts at 1.0 and goes to 0.0 during the fade-out.
|
||||
current_weight: f32,
|
||||
@ -78,6 +93,11 @@ impl AnimationTransitions {
|
||||
self.main_animation = Some(new_animation);
|
||||
player.start(new_animation)
|
||||
}
|
||||
|
||||
/// Obtain the currently playing main animation.
|
||||
pub fn get_main_animation(&self) -> Option<AnimationNodeIndex> {
|
||||
self.main_animation
|
||||
}
|
||||
}
|
||||
|
||||
/// A system that alters the weight of currently-playing transitions based on
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bevy_app"
|
||||
version = "0.14.0-dev"
|
||||
version = "0.14.0"
|
||||
edition = "2021"
|
||||
description = "Provides core App functionality for Bevy Engine"
|
||||
homepage = "https://bevyengine.org"
|
||||
@ -13,18 +13,16 @@ trace = []
|
||||
bevy_debug_stepping = []
|
||||
default = ["bevy_reflect"]
|
||||
bevy_reflect = ["dep:bevy_reflect", "bevy_ecs/bevy_reflect"]
|
||||
serialize = ["bevy_ecs/serde"]
|
||||
|
||||
[dependencies]
|
||||
# bevy
|
||||
bevy_derive = { path = "../bevy_derive", version = "0.14.0-dev" }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev", default-features = false }
|
||||
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev", optional = true }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" }
|
||||
bevy_tasks = { path = "../bevy_tasks", version = "0.14.0-dev" }
|
||||
bevy_derive = { path = "../bevy_derive", version = "0.14.0" }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0", default-features = false }
|
||||
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0", optional = true }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.14.0" }
|
||||
bevy_tasks = { path = "../bevy_tasks", version = "0.14.0" }
|
||||
|
||||
# other
|
||||
serde = { version = "1.0", features = ["derive"], optional = true }
|
||||
downcast-rs = "1.2.0"
|
||||
thiserror = "1.0"
|
||||
|
||||
|
@ -8,7 +8,7 @@ use bevy_ecs::{
|
||||
intern::Interned,
|
||||
prelude::*,
|
||||
schedule::{ScheduleBuildSettings, ScheduleLabel},
|
||||
system::SystemId,
|
||||
system::{IntoObserverSystem, SystemId},
|
||||
};
|
||||
#[cfg(feature = "trace")]
|
||||
use bevy_utils::tracing::info_span;
|
||||
@ -424,9 +424,12 @@ impl App {
|
||||
self
|
||||
}
|
||||
|
||||
/// Inserts the [`!Send`](Send) resource into the app, initialized with its default value,
|
||||
/// if there is no existing instance of `R`.
|
||||
pub fn init_non_send_resource<R: 'static + Default>(&mut self) -> &mut Self {
|
||||
/// Inserts the [`!Send`](Send) resource into the app if there is no existing instance of `R`.
|
||||
///
|
||||
/// `R` must implement [`FromWorld`].
|
||||
/// If `R` implements [`Default`], [`FromWorld`] will be automatically implemented and
|
||||
/// initialize the [`Resource`] with [`Default::default`].
|
||||
pub fn init_non_send_resource<R: 'static + FromWorld>(&mut self) -> &mut Self {
|
||||
self.world_mut().init_non_send_resource::<R>();
|
||||
self
|
||||
}
|
||||
@ -436,12 +439,7 @@ impl App {
|
||||
plugin: Box<dyn Plugin>,
|
||||
) -> Result<&mut Self, AppError> {
|
||||
debug!("added plugin: {}", plugin.name());
|
||||
if plugin.is_unique()
|
||||
&& !self
|
||||
.main_mut()
|
||||
.plugin_names
|
||||
.insert(plugin.name().to_string())
|
||||
{
|
||||
if plugin.is_unique() && self.main_mut().plugin_names.contains(plugin.name()) {
|
||||
Err(AppError::DuplicatePlugin {
|
||||
plugin_name: plugin.name().to_string(),
|
||||
})?;
|
||||
@ -456,6 +454,9 @@ impl App {
|
||||
|
||||
self.main_mut().plugin_build_depth += 1;
|
||||
let result = catch_unwind(AssertUnwindSafe(|| plugin.build(self)));
|
||||
self.main_mut()
|
||||
.plugin_names
|
||||
.insert(plugin.name().to_string());
|
||||
self.main_mut().plugin_build_depth -= 1;
|
||||
|
||||
if let Err(payload) = result {
|
||||
@ -662,6 +663,11 @@ impl App {
|
||||
self.sub_apps.sub_apps.remove(&label.intern())
|
||||
}
|
||||
|
||||
/// Extract data from the main world into the [`SubApp`] with the given label and perform an update if it exists.
|
||||
pub fn update_sub_app_by_label(&mut self, label: impl AppLabel) {
|
||||
self.sub_apps.update_subapp_by_label(label);
|
||||
}
|
||||
|
||||
/// Inserts a new `schedule` under the provided `label`, overwriting any existing
|
||||
/// schedule with the same label.
|
||||
pub fn add_schedule(&mut self, schedule: Schedule) -> &mut Self {
|
||||
@ -828,6 +834,15 @@ impl App {
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Spawns an [`Observer`] entity, which will watch for and respond to the given event.
|
||||
pub fn observe<E: Event, B: Bundle, M>(
|
||||
&mut self,
|
||||
observer: impl IntoObserverSystem<E, B, M>,
|
||||
) -> &mut Self {
|
||||
self.world_mut().observe(observer);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
type RunnerFn = Box<dyn FnOnce(App) -> AppExit>;
|
||||
@ -916,11 +931,21 @@ impl Termination for AppExit {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{marker::PhantomData, mem};
|
||||
use std::{iter, marker::PhantomData, mem, sync::Mutex};
|
||||
|
||||
use bevy_ecs::{event::EventWriter, schedule::ScheduleLabel, system::Commands};
|
||||
use bevy_ecs::{
|
||||
change_detection::{DetectChanges, ResMut},
|
||||
component::Component,
|
||||
entity::Entity,
|
||||
event::{Event, EventWriter, Events},
|
||||
query::With,
|
||||
removal_detection::RemovedComponents,
|
||||
schedule::{IntoSystemConfigs, ScheduleLabel},
|
||||
system::{Commands, Query, Resource},
|
||||
world::{FromWorld, World},
|
||||
};
|
||||
|
||||
use crate::{App, AppExit, Plugin, Update};
|
||||
use crate::{App, AppExit, Plugin, SubApp, Update};
|
||||
|
||||
struct PluginA;
|
||||
impl Plugin for PluginA {
|
||||
@ -1123,6 +1148,62 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_clears_trackers_once() {
|
||||
#[derive(Component, Copy, Clone)]
|
||||
struct Foo;
|
||||
|
||||
let mut app = App::new();
|
||||
app.world_mut().spawn_batch(iter::repeat(Foo).take(5));
|
||||
|
||||
fn despawn_one_foo(mut commands: Commands, foos: Query<Entity, With<Foo>>) {
|
||||
if let Some(e) = foos.iter().next() {
|
||||
commands.entity(e).despawn();
|
||||
};
|
||||
}
|
||||
fn check_despawns(mut removed_foos: RemovedComponents<Foo>) {
|
||||
let mut despawn_count = 0;
|
||||
for _ in removed_foos.read() {
|
||||
despawn_count += 1;
|
||||
}
|
||||
|
||||
assert_eq!(despawn_count, 2);
|
||||
}
|
||||
|
||||
app.add_systems(Update, despawn_one_foo);
|
||||
app.update(); // Frame 0
|
||||
app.update(); // Frame 1
|
||||
app.add_systems(Update, check_despawns.after(despawn_one_foo));
|
||||
app.update(); // Should see despawns from frames 1 & 2, but not frame 0
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_sees_changes() {
|
||||
use super::AppLabel;
|
||||
use crate::{self as bevy_app};
|
||||
|
||||
#[derive(AppLabel, Clone, Copy, Hash, PartialEq, Eq, Debug)]
|
||||
struct MySubApp;
|
||||
|
||||
#[derive(Resource)]
|
||||
struct Foo(usize);
|
||||
|
||||
let mut app = App::new();
|
||||
app.world_mut().insert_resource(Foo(0));
|
||||
app.add_systems(Update, |mut foo: ResMut<Foo>| {
|
||||
foo.0 += 1;
|
||||
});
|
||||
|
||||
let mut sub_app = SubApp::new();
|
||||
sub_app.set_extract(|main_world, _sub_world| {
|
||||
assert!(main_world.get_resource_ref::<Foo>().unwrap().is_changed());
|
||||
});
|
||||
|
||||
app.insert_sub_app(MySubApp, sub_app);
|
||||
|
||||
app.update();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runner_returns_correct_exit_code() {
|
||||
fn raise_exits(mut exits: EventWriter<AppExit>) {
|
||||
@ -1178,4 +1259,83 @@ mod tests {
|
||||
// it's nice they're so small let's keep it that way.
|
||||
assert_eq!(mem::size_of::<AppExit>(), mem::size_of::<u8>());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initializing_resources_from_world() {
|
||||
#[derive(Resource)]
|
||||
struct TestResource;
|
||||
impl FromWorld for TestResource {
|
||||
fn from_world(_world: &mut World) -> Self {
|
||||
TestResource
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
struct NonSendTestResource {
|
||||
_marker: PhantomData<Mutex<()>>,
|
||||
}
|
||||
impl FromWorld for NonSendTestResource {
|
||||
fn from_world(_world: &mut World) -> Self {
|
||||
NonSendTestResource {
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
App::new()
|
||||
.init_non_send_resource::<NonSendTestResource>()
|
||||
.init_resource::<TestResource>();
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Plugin should not be considered inserted while it's being built
|
||||
///
|
||||
/// bug: <https://github.com/bevyengine/bevy/issues/13815>
|
||||
fn plugin_should_not_be_added_during_build_time() {
|
||||
pub struct Foo;
|
||||
|
||||
impl Plugin for Foo {
|
||||
fn build(&self, app: &mut App) {
|
||||
assert!(!app.is_plugin_added::<Self>());
|
||||
}
|
||||
}
|
||||
|
||||
App::new().add_plugins(Foo);
|
||||
}
|
||||
#[test]
|
||||
fn events_should_be_updated_once_per_update() {
|
||||
#[derive(Event, Clone)]
|
||||
struct TestEvent;
|
||||
|
||||
let mut app = App::new();
|
||||
app.add_event::<TestEvent>();
|
||||
|
||||
// Starts empty
|
||||
let test_events = app.world().resource::<Events<TestEvent>>();
|
||||
assert_eq!(test_events.len(), 0);
|
||||
assert_eq!(test_events.iter_current_update_events().count(), 0);
|
||||
app.update();
|
||||
|
||||
// Sending one event
|
||||
app.world_mut().send_event(TestEvent);
|
||||
|
||||
let test_events = app.world().resource::<Events<TestEvent>>();
|
||||
assert_eq!(test_events.len(), 1);
|
||||
assert_eq!(test_events.iter_current_update_events().count(), 1);
|
||||
app.update();
|
||||
|
||||
// Sending two events on the next frame
|
||||
app.world_mut().send_event(TestEvent);
|
||||
app.world_mut().send_event(TestEvent);
|
||||
|
||||
let test_events = app.world().resource::<Events<TestEvent>>();
|
||||
assert_eq!(test_events.len(), 3); // Events are double-buffered, so we see 1 + 2 = 3
|
||||
assert_eq!(test_events.iter_current_update_events().count(), 2);
|
||||
app.update();
|
||||
|
||||
// Sending zero events
|
||||
let test_events = app.world().resource::<Events<TestEvent>>();
|
||||
assert_eq!(test_events.len(), 2); // Events are double-buffered, so we see 2 + 0 = 2
|
||||
assert_eq!(test_events.iter_current_update_events().count(), 0);
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ use bevy_ecs::{
|
||||
/// Then it will run:
|
||||
/// * [`First`]
|
||||
/// * [`PreUpdate`]
|
||||
/// * [`StateTransition`]
|
||||
/// * [`StateTransition`](bevy_state::transition::StateTransition)
|
||||
/// * [`RunFixedMainLoop`]
|
||||
/// * This will run [`FixedMain`] zero to many times, based on how much time has elapsed.
|
||||
/// * [`Update`]
|
||||
|
@ -1,4 +1,4 @@
|
||||
use crate::{App, InternedAppLabel, Plugin, Plugins, PluginsState};
|
||||
use crate::{App, AppLabel, InternedAppLabel, Plugin, Plugins, PluginsState};
|
||||
use bevy_ecs::{
|
||||
event::EventRegistry,
|
||||
prelude::*,
|
||||
@ -125,7 +125,9 @@ impl SubApp {
|
||||
}
|
||||
|
||||
/// Runs the default schedule.
|
||||
pub fn update(&mut self) {
|
||||
///
|
||||
/// Does not clear internal trackers used for change detection.
|
||||
pub fn run_default_schedule(&mut self) {
|
||||
if self.is_building_plugins() {
|
||||
panic!("SubApp::update() was called while a plugin was building.");
|
||||
}
|
||||
@ -133,6 +135,11 @@ impl SubApp {
|
||||
if let Some(label) = self.update_schedule {
|
||||
self.world.run_schedule(label);
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs the default schedule and updates internal component trackers.
|
||||
pub fn update(&mut self) {
|
||||
self.run_default_schedule();
|
||||
self.world.clear_trackers();
|
||||
}
|
||||
|
||||
@ -421,7 +428,7 @@ impl SubApps {
|
||||
{
|
||||
#[cfg(feature = "trace")]
|
||||
let _bevy_frame_update_span = info_span!("main app").entered();
|
||||
self.main.update();
|
||||
self.main.run_default_schedule();
|
||||
}
|
||||
for (_label, sub_app) in self.sub_apps.iter_mut() {
|
||||
#[cfg(feature = "trace")]
|
||||
@ -442,4 +449,12 @@ impl SubApps {
|
||||
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut SubApp> + '_ {
|
||||
std::iter::once(&mut self.main).chain(self.sub_apps.values_mut())
|
||||
}
|
||||
|
||||
/// Extract data from the main world into the [`SubApp`] with the given label and perform an update if it exists.
|
||||
pub fn update_subapp_by_label(&mut self, label: impl AppLabel) {
|
||||
if let Some(sub_app) = self.sub_apps.get_mut(&label.intern()) {
|
||||
sub_app.extract(&mut self.main.world);
|
||||
sub_app.update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bevy_asset"
|
||||
version = "0.14.0-dev"
|
||||
version = "0.14.0"
|
||||
edition = "2021"
|
||||
description = "Provides asset functionality for Bevy Engine"
|
||||
homepage = "https://bevyengine.org"
|
||||
@ -19,14 +19,14 @@ watch = []
|
||||
trace = []
|
||||
|
||||
[dependencies]
|
||||
bevy_app = { path = "../bevy_app", version = "0.14.0-dev" }
|
||||
bevy_asset_macros = { path = "macros", version = "0.14.0-dev" }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" }
|
||||
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev", features = [
|
||||
bevy_app = { path = "../bevy_app", version = "0.14.0" }
|
||||
bevy_asset_macros = { path = "macros", version = "0.14.0" }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0" }
|
||||
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0", features = [
|
||||
"uuid",
|
||||
] }
|
||||
bevy_tasks = { path = "../bevy_tasks", version = "0.14.0-dev" }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" }
|
||||
bevy_tasks = { path = "../bevy_tasks", version = "0.14.0" }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.14.0" }
|
||||
|
||||
async-broadcast = "0.5"
|
||||
async-fs = "2.0"
|
||||
@ -43,7 +43,7 @@ thiserror = "1.0"
|
||||
uuid = { version = "1.0", features = ["v4"] }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
bevy_winit = { path = "../bevy_winit", version = "0.14.0-dev" }
|
||||
bevy_winit = { path = "../bevy_winit", version = "0.14.0" }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
wasm-bindgen = { version = "0.2" }
|
||||
@ -59,8 +59,8 @@ js-sys = "0.3"
|
||||
notify-debouncer-full = { version = "0.3.1", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
bevy_core = { path = "../bevy_core", version = "0.14.0-dev" }
|
||||
bevy_log = { path = "../bevy_log", version = "0.14.0-dev" }
|
||||
bevy_core = { path = "../bevy_core", version = "0.14.0" }
|
||||
bevy_log = { path = "../bevy_log", version = "0.14.0" }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bevy_asset_macros"
|
||||
version = "0.14.0-dev"
|
||||
version = "0.14.0"
|
||||
edition = "2021"
|
||||
description = "Derive implementations for bevy_asset"
|
||||
homepage = "https://bevyengine.org"
|
||||
@ -12,7 +12,7 @@ keywords = ["bevy"]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.14.0-dev" }
|
||||
bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.14.0" }
|
||||
|
||||
syn = "2.0"
|
||||
proc-macro2 = "1.0"
|
||||
|
@ -288,13 +288,17 @@ impl Hash for UntypedAssetId {
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for UntypedAssetId {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.type_id()
|
||||
.cmp(&other.type_id())
|
||||
.then_with(|| self.internal().cmp(&other.internal()))
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for UntypedAssetId {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
if self.type_id() != other.type_id() {
|
||||
None
|
||||
} else {
|
||||
Some(self.internal().cmp(&other.internal()))
|
||||
}
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -718,9 +718,9 @@ impl AssetServer {
|
||||
///
|
||||
/// After the asset has been fully loaded, it will show up in the relevant [`Assets`] storage.
|
||||
#[must_use = "not using the returned strong handle may result in the unexpected release of the asset"]
|
||||
pub fn add_async<A: Asset>(
|
||||
pub fn add_async<A: Asset, E: std::error::Error + Send + Sync + 'static>(
|
||||
&self,
|
||||
future: impl Future<Output = Result<A, AssetLoadError>> + Send + 'static,
|
||||
future: impl Future<Output = Result<A, E>> + Send + 'static,
|
||||
) -> Handle<A> {
|
||||
let handle = self
|
||||
.data
|
||||
@ -741,12 +741,15 @@ impl AssetServer {
|
||||
.unwrap();
|
||||
}
|
||||
Err(error) => {
|
||||
let error = AddAsyncError {
|
||||
error: Arc::new(error),
|
||||
};
|
||||
error!("{error}");
|
||||
event_sender
|
||||
.send(InternalAssetEvent::Failed {
|
||||
id,
|
||||
path: Default::default(),
|
||||
error,
|
||||
error: AssetLoadError::AddAsyncError(error),
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
@ -1403,6 +1406,8 @@ pub enum AssetLoadError {
|
||||
CannotLoadIgnoredAsset { path: AssetPath<'static> },
|
||||
#[error(transparent)]
|
||||
AssetLoaderError(#[from] AssetLoaderError),
|
||||
#[error(transparent)]
|
||||
AddAsyncError(#[from] AddAsyncError),
|
||||
#[error("The file at '{}' does not contain the labeled asset '{}'; it contains the following {} assets: {}",
|
||||
base_path,
|
||||
label,
|
||||
@ -1441,6 +1446,22 @@ impl AssetLoaderError {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug, Clone)]
|
||||
#[error("An error occurred while resolving an asset added by `add_async`: {error}")]
|
||||
pub struct AddAsyncError {
|
||||
error: Arc<dyn std::error::Error + Send + Sync + 'static>,
|
||||
}
|
||||
|
||||
impl PartialEq for AddAsyncError {
|
||||
/// Equality comparison is not full (only through `TypeId`)
|
||||
#[inline]
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.error.type_id() == other.error.type_id()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for AddAsyncError {}
|
||||
|
||||
/// An error that occurs when an [`AssetLoader`] is not registered for a given extension.
|
||||
#[derive(Error, Debug, Clone, PartialEq, Eq)]
|
||||
#[error("no `AssetLoader` found{}", format_missing_asset_ext(.extensions))]
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bevy_audio"
|
||||
version = "0.14.0-dev"
|
||||
version = "0.14.0"
|
||||
edition = "2021"
|
||||
description = "Provides audio functionality for Bevy Engine"
|
||||
homepage = "https://bevyengine.org"
|
||||
@ -10,17 +10,17 @@ keywords = ["bevy"]
|
||||
|
||||
[dependencies]
|
||||
# bevy
|
||||
bevy_app = { path = "../bevy_app", version = "0.14.0-dev" }
|
||||
bevy_asset = { path = "../bevy_asset", version = "0.14.0-dev" }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" }
|
||||
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.14.0-dev" }
|
||||
bevy_math = { path = "../bevy_math", version = "0.14.0-dev" }
|
||||
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev", features = [
|
||||
bevy_app = { path = "../bevy_app", version = "0.14.0" }
|
||||
bevy_asset = { path = "../bevy_asset", version = "0.14.0" }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0" }
|
||||
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.14.0" }
|
||||
bevy_math = { path = "../bevy_math", version = "0.14.0" }
|
||||
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0", features = [
|
||||
"bevy",
|
||||
] }
|
||||
bevy_transform = { path = "../bevy_transform", version = "0.14.0-dev" }
|
||||
bevy_derive = { path = "../bevy_derive", version = "0.14.0-dev" }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" }
|
||||
bevy_transform = { path = "../bevy_transform", version = "0.14.0" }
|
||||
bevy_derive = { path = "../bevy_derive", version = "0.14.0" }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.14.0" }
|
||||
|
||||
# other
|
||||
rodio = { version = "0.18", default-features = false }
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bevy_color"
|
||||
version = "0.14.0-dev"
|
||||
version = "0.14.1"
|
||||
edition = "2021"
|
||||
description = "Types for representing and manipulating color values"
|
||||
homepage = "https://bevyengine.org"
|
||||
@ -10,14 +10,14 @@ keywords = ["bevy", "color"]
|
||||
rust-version = "1.76.0"
|
||||
|
||||
[dependencies]
|
||||
bevy_math = { path = "../bevy_math", version = "0.14.0-dev" }
|
||||
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev", features = [
|
||||
bevy_math = { path = "../bevy_math", version = "0.14.0" }
|
||||
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0", features = [
|
||||
"bevy",
|
||||
] }
|
||||
bytemuck = { version = "1", features = ["derive"] }
|
||||
serde = { version = "1.0", features = ["derive"], optional = true }
|
||||
thiserror = "1.0"
|
||||
wgpu-types = { version = "0.19", default-features = false, optional = true }
|
||||
wgpu-types = { version = "0.20", default-features = false, optional = true }
|
||||
encase = { version = "0.8", default-features = false }
|
||||
|
||||
[features]
|
||||
|
@ -73,7 +73,12 @@ impl StandardColor for Color {}
|
||||
|
||||
impl Color {
|
||||
/// Return the color as a linear RGBA color.
|
||||
pub fn linear(&self) -> LinearRgba {
|
||||
pub fn to_linear(&self) -> LinearRgba {
|
||||
(*self).into()
|
||||
}
|
||||
|
||||
/// Return the color as an SRGBA color.
|
||||
pub fn to_srgba(&self) -> Srgba {
|
||||
(*self).into()
|
||||
}
|
||||
|
||||
|
@ -115,6 +115,18 @@ pub trait ColorToComponents {
|
||||
fn from_vec3(color: Vec3) -> Self;
|
||||
}
|
||||
|
||||
/// Trait with methods for converting colors to packed non-color types
|
||||
pub trait ColorToPacked {
|
||||
/// Convert to [u8; 4] where that makes sense (Srgba is most relevant)
|
||||
fn to_u8_array(self) -> [u8; 4];
|
||||
/// Convert to [u8; 3] where that makes sense (Srgba is most relevant)
|
||||
fn to_u8_array_no_alpha(self) -> [u8; 3];
|
||||
/// Convert from [u8; 4] where that makes sense (Srgba is most relevant)
|
||||
fn from_u8_array(color: [u8; 4]) -> Self;
|
||||
/// Convert to [u8; 3] where that makes sense (Srgba is most relevant)
|
||||
fn from_u8_array_no_alpha(color: [u8; 3]) -> Self;
|
||||
}
|
||||
|
||||
/// Utility function for interpolating hue values. This ensures that the interpolation
|
||||
/// takes the shortest path around the color wheel, and that the result is always between
|
||||
/// 0 and 360.
|
||||
|
@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
color_difference::EuclideanDistance, impl_componentwise_vector_space, Alpha, ColorToComponents,
|
||||
Gray, Luminance, Mix, StandardColor,
|
||||
ColorToPacked, Gray, Luminance, Mix, StandardColor,
|
||||
};
|
||||
use bevy_math::{Vec3, Vec4};
|
||||
use bevy_reflect::prelude::*;
|
||||
@ -149,24 +149,12 @@ impl LinearRgba {
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts the color into a [f32; 4] array in RGBA order.
|
||||
///
|
||||
/// This is useful for passing the color to a shader.
|
||||
pub fn to_f32_array(&self) -> [f32; 4] {
|
||||
[self.red, self.green, self.blue, self.alpha]
|
||||
}
|
||||
|
||||
/// Converts this color to a u32.
|
||||
///
|
||||
/// Maps the RGBA channels in RGBA order to a little-endian byte array (GPUs are little-endian).
|
||||
/// `A` will be the most significant byte and `R` the least significant.
|
||||
pub fn as_u32(&self) -> u32 {
|
||||
u32::from_le_bytes([
|
||||
(self.red * 255.0) as u8,
|
||||
(self.green * 255.0) as u8,
|
||||
(self.blue * 255.0) as u8,
|
||||
(self.alpha * 255.0) as u8,
|
||||
])
|
||||
u32::from_le_bytes(self.to_u8_array())
|
||||
}
|
||||
}
|
||||
|
||||
@ -310,6 +298,25 @@ impl ColorToComponents for LinearRgba {
|
||||
}
|
||||
}
|
||||
|
||||
impl ColorToPacked for LinearRgba {
|
||||
fn to_u8_array(self) -> [u8; 4] {
|
||||
[self.red, self.green, self.blue, self.alpha]
|
||||
.map(|v| (v.clamp(0.0, 1.0) * 255.0).round() as u8)
|
||||
}
|
||||
|
||||
fn to_u8_array_no_alpha(self) -> [u8; 3] {
|
||||
[self.red, self.green, self.blue].map(|v| (v.clamp(0.0, 1.0) * 255.0).round() as u8)
|
||||
}
|
||||
|
||||
fn from_u8_array(color: [u8; 4]) -> Self {
|
||||
Self::from_f32_array(color.map(|u| u as f32 / 255.0))
|
||||
}
|
||||
|
||||
fn from_u8_array_no_alpha(color: [u8; 3]) -> Self {
|
||||
Self::from_f32_array_no_alpha(color.map(|u| u as f32 / 255.0))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "wgpu-types")]
|
||||
impl From<LinearRgba> for wgpu_types::Color {
|
||||
fn from(color: LinearRgba) -> Self {
|
||||
@ -416,6 +423,34 @@ mod tests {
|
||||
assert_eq!(a.distance_squared(&b), 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_and_from_u8() {
|
||||
// from_u8_array
|
||||
let a = LinearRgba::from_u8_array([255, 0, 0, 255]);
|
||||
let b = LinearRgba::new(1.0, 0.0, 0.0, 1.0);
|
||||
assert_eq!(a, b);
|
||||
|
||||
// from_u8_array_no_alpha
|
||||
let a = LinearRgba::from_u8_array_no_alpha([255, 255, 0]);
|
||||
let b = LinearRgba::rgb(1.0, 1.0, 0.0);
|
||||
assert_eq!(a, b);
|
||||
|
||||
// to_u8_array
|
||||
let a = LinearRgba::new(0.0, 0.0, 1.0, 1.0).to_u8_array();
|
||||
let b = [0, 0, 255, 255];
|
||||
assert_eq!(a, b);
|
||||
|
||||
// to_u8_array_no_alpha
|
||||
let a = LinearRgba::rgb(0.0, 1.0, 1.0).to_u8_array_no_alpha();
|
||||
let b = [0, 255, 255];
|
||||
assert_eq!(a, b);
|
||||
|
||||
// clamping
|
||||
let a = LinearRgba::rgb(0.0, 100.0, -100.0).to_u8_array_no_alpha();
|
||||
let b = [0, 255, 0];
|
||||
assert_eq!(a, b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn darker_lighter() {
|
||||
// Darker and lighter should be commutative.
|
||||
|
@ -1,7 +1,7 @@
|
||||
use crate::color_difference::EuclideanDistance;
|
||||
use crate::{
|
||||
impl_componentwise_vector_space, Alpha, ColorToComponents, Gray, LinearRgba, Luminance, Mix,
|
||||
StandardColor, Xyza,
|
||||
impl_componentwise_vector_space, Alpha, ColorToComponents, ColorToPacked, Gray, LinearRgba,
|
||||
Luminance, Mix, StandardColor, Xyza,
|
||||
};
|
||||
use bevy_math::{Vec3, Vec4};
|
||||
use bevy_reflect::prelude::*;
|
||||
@ -168,10 +168,7 @@ impl Srgba {
|
||||
|
||||
/// Convert this color to CSS-style hexadecimal notation.
|
||||
pub fn to_hex(&self) -> String {
|
||||
let r = (self.red * 255.0).round() as u8;
|
||||
let g = (self.green * 255.0).round() as u8;
|
||||
let b = (self.blue * 255.0).round() as u8;
|
||||
let a = (self.alpha * 255.0).round() as u8;
|
||||
let [r, g, b, a] = self.to_u8_array();
|
||||
match a {
|
||||
255 => format!("#{:02X}{:02X}{:02X}", r, g, b),
|
||||
_ => format!("#{:02X}{:02X}{:02X}{:02X}", r, g, b, a),
|
||||
@ -189,7 +186,7 @@ impl Srgba {
|
||||
/// See also [`Srgba::new`], [`Srgba::rgba_u8`], [`Srgba::hex`].
|
||||
///
|
||||
pub fn rgb_u8(r: u8, g: u8, b: u8) -> Self {
|
||||
Self::rgba_u8(r, g, b, u8::MAX)
|
||||
Self::from_u8_array_no_alpha([r, g, b])
|
||||
}
|
||||
|
||||
// Float operations in const fn are not stable yet
|
||||
@ -206,12 +203,7 @@ impl Srgba {
|
||||
/// See also [`Srgba::new`], [`Srgba::rgb_u8`], [`Srgba::hex`].
|
||||
///
|
||||
pub fn rgba_u8(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||
Self::new(
|
||||
r as f32 / u8::MAX as f32,
|
||||
g as f32 / u8::MAX as f32,
|
||||
b as f32 / u8::MAX as f32,
|
||||
a as f32 / u8::MAX as f32,
|
||||
)
|
||||
Self::from_u8_array([r, g, b, a])
|
||||
}
|
||||
|
||||
/// Converts a non-linear sRGB value to a linear one via [gamma correction](https://en.wikipedia.org/wiki/Gamma_correction).
|
||||
@ -373,6 +365,25 @@ impl ColorToComponents for Srgba {
|
||||
}
|
||||
}
|
||||
|
||||
impl ColorToPacked for Srgba {
|
||||
fn to_u8_array(self) -> [u8; 4] {
|
||||
[self.red, self.green, self.blue, self.alpha]
|
||||
.map(|v| (v.clamp(0.0, 1.0) * 255.0).round() as u8)
|
||||
}
|
||||
|
||||
fn to_u8_array_no_alpha(self) -> [u8; 3] {
|
||||
[self.red, self.green, self.blue].map(|v| (v.clamp(0.0, 1.0) * 255.0).round() as u8)
|
||||
}
|
||||
|
||||
fn from_u8_array(color: [u8; 4]) -> Self {
|
||||
Self::from_f32_array(color.map(|u| u as f32 / 255.0))
|
||||
}
|
||||
|
||||
fn from_u8_array_no_alpha(color: [u8; 3]) -> Self {
|
||||
Self::from_f32_array_no_alpha(color.map(|u| u as f32 / 255.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LinearRgba> for Srgba {
|
||||
#[inline]
|
||||
fn from(value: LinearRgba) -> Self {
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bevy_core"
|
||||
version = "0.14.0-dev"
|
||||
version = "0.14.0"
|
||||
edition = "2021"
|
||||
description = "Provides core functionality for Bevy Engine"
|
||||
homepage = "https://bevyengine.org"
|
||||
@ -10,17 +10,17 @@ keywords = ["bevy"]
|
||||
|
||||
[dependencies]
|
||||
# bevy
|
||||
bevy_app = { path = "../bevy_app", version = "0.14.0-dev", features = [
|
||||
bevy_app = { path = "../bevy_app", version = "0.14.0", features = [
|
||||
"bevy_reflect",
|
||||
] }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev", features = [
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0", features = [
|
||||
"bevy_reflect",
|
||||
] }
|
||||
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev", features = [
|
||||
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0", features = [
|
||||
"bevy",
|
||||
] }
|
||||
bevy_tasks = { path = "../bevy_tasks", version = "0.14.0-dev" }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" }
|
||||
bevy_tasks = { path = "../bevy_tasks", version = "0.14.0" }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.14.0" }
|
||||
|
||||
# other
|
||||
serde = { version = "1.0", optional = true }
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bevy_core_pipeline"
|
||||
version = "0.14.0-dev"
|
||||
version = "0.14.0"
|
||||
edition = "2021"
|
||||
authors = [
|
||||
"Bevy Contributors <bevyengine@gmail.com>",
|
||||
@ -18,20 +18,21 @@ trace = []
|
||||
webgl = []
|
||||
webgpu = []
|
||||
tonemapping_luts = ["bevy_render/ktx2", "bevy_render/zstd"]
|
||||
smaa_luts = ["bevy_render/ktx2", "bevy_render/zstd"]
|
||||
|
||||
[dependencies]
|
||||
# bevy
|
||||
bevy_app = { path = "../bevy_app", version = "0.14.0-dev" }
|
||||
bevy_asset = { path = "../bevy_asset", version = "0.14.0-dev" }
|
||||
bevy_core = { path = "../bevy_core", version = "0.14.0-dev" }
|
||||
bevy_color = { path = "../bevy_color", version = "0.14.0-dev" }
|
||||
bevy_derive = { path = "../bevy_derive", version = "0.14.0-dev" }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" }
|
||||
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev" }
|
||||
bevy_render = { path = "../bevy_render", version = "0.14.0-dev" }
|
||||
bevy_transform = { path = "../bevy_transform", version = "0.14.0-dev" }
|
||||
bevy_math = { path = "../bevy_math", version = "0.14.0-dev" }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" }
|
||||
bevy_app = { path = "../bevy_app", version = "0.14.0" }
|
||||
bevy_asset = { path = "../bevy_asset", version = "0.14.0" }
|
||||
bevy_core = { path = "../bevy_core", version = "0.14.0" }
|
||||
bevy_color = { path = "../bevy_color", version = "0.14.1" }
|
||||
bevy_derive = { path = "../bevy_derive", version = "0.14.0" }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0" }
|
||||
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0" }
|
||||
bevy_render = { path = "../bevy_render", version = "0.14.0" }
|
||||
bevy_transform = { path = "../bevy_transform", version = "0.14.0" }
|
||||
bevy_math = { path = "../bevy_math", version = "0.14.0" }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.14.0" }
|
||||
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
bitflags = "2.3"
|
||||
|
@ -49,7 +49,7 @@ pub const CORE_3D_DEPTH_FORMAT: TextureFormat = TextureFormat::Depth32Float;
|
||||
/// `sampler2DShadow` and will cheerfully generate invalid GLSL that tries to
|
||||
/// perform non-percentage-closer-filtering with such a sampler. Therefore we
|
||||
/// disable depth of field and screen space reflections entirely on WebGL 2.
|
||||
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
|
||||
#[cfg(not(any(feature = "webgpu", not(target_arch = "wasm32"))))]
|
||||
pub const DEPTH_TEXTURE_SAMPLING_SUPPORTED: bool = false;
|
||||
|
||||
/// True if multisampled depth textures are supported on this platform.
|
||||
@ -64,7 +64,7 @@ pub const DEPTH_TEXTURE_SAMPLING_SUPPORTED: bool = true;
|
||||
|
||||
use std::ops::Range;
|
||||
|
||||
use bevy_asset::AssetId;
|
||||
use bevy_asset::{AssetId, UntypedAssetId};
|
||||
use bevy_color::LinearRgba;
|
||||
pub use camera_3d::*;
|
||||
pub use main_opaque_pass_3d_node::*;
|
||||
@ -76,7 +76,6 @@ use bevy_math::FloatOrd;
|
||||
use bevy_render::{
|
||||
camera::{Camera, ExtractedCamera},
|
||||
extract_component::ExtractComponentPlugin,
|
||||
mesh::Mesh,
|
||||
prelude::Msaa,
|
||||
render_graph::{EmptyNode, RenderGraphApp, ViewNodeRunner},
|
||||
render_phase::{
|
||||
@ -136,6 +135,14 @@ impl Plugin for Core3dPlugin {
|
||||
.init_resource::<DrawFunctions<AlphaMask3dPrepass>>()
|
||||
.init_resource::<DrawFunctions<Opaque3dDeferred>>()
|
||||
.init_resource::<DrawFunctions<AlphaMask3dDeferred>>()
|
||||
.init_resource::<ViewBinnedRenderPhases<Opaque3d>>()
|
||||
.init_resource::<ViewBinnedRenderPhases<AlphaMask3d>>()
|
||||
.init_resource::<ViewBinnedRenderPhases<Opaque3dPrepass>>()
|
||||
.init_resource::<ViewBinnedRenderPhases<AlphaMask3dPrepass>>()
|
||||
.init_resource::<ViewBinnedRenderPhases<Opaque3dDeferred>>()
|
||||
.init_resource::<ViewBinnedRenderPhases<AlphaMask3dDeferred>>()
|
||||
.init_resource::<ViewSortedRenderPhases<Transmissive3d>>()
|
||||
.init_resource::<ViewSortedRenderPhases<Transparent3d>>()
|
||||
.add_systems(ExtractSchedule, extract_core_3d_camera_phases)
|
||||
.add_systems(ExtractSchedule, extract_camera_prepass_phase)
|
||||
.add_systems(
|
||||
@ -213,7 +220,7 @@ pub struct Opaque3d {
|
||||
pub extra_index: PhaseItemExtraIndex,
|
||||
}
|
||||
|
||||
/// Data that must be identical in order to batch meshes together.
|
||||
/// Data that must be identical in order to batch phase items together.
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Opaque3dBinKey {
|
||||
/// The identifier of the render pipeline.
|
||||
@ -222,8 +229,11 @@ pub struct Opaque3dBinKey {
|
||||
/// The function used to draw.
|
||||
pub draw_function: DrawFunctionId,
|
||||
|
||||
/// The mesh.
|
||||
pub asset_id: AssetId<Mesh>,
|
||||
/// The asset that this phase item is associated with.
|
||||
///
|
||||
/// Normally, this is the ID of the mesh, but for non-mesh items it might be
|
||||
/// the ID of another type of asset.
|
||||
pub asset_id: UntypedAssetId,
|
||||
|
||||
/// The ID of a bind group specific to the material.
|
||||
///
|
||||
|
@ -122,17 +122,17 @@ impl ViewNode for DeferredGBufferPrepassNode {
|
||||
let view_entity = graph.view_entity();
|
||||
render_context.add_command_buffer_generation_task(move |render_device| {
|
||||
#[cfg(feature = "trace")]
|
||||
let _deferred_span = info_span!("deferred").entered();
|
||||
let _deferred_span = info_span!("deferred_prepass").entered();
|
||||
|
||||
// Command encoder setup
|
||||
let mut command_encoder =
|
||||
render_device.create_command_encoder(&CommandEncoderDescriptor {
|
||||
label: Some("deferred_command_encoder"),
|
||||
label: Some("deferred_prepass_command_encoder"),
|
||||
});
|
||||
|
||||
// Render pass setup
|
||||
let render_pass = command_encoder.begin_render_pass(&RenderPassDescriptor {
|
||||
label: Some("deferred"),
|
||||
label: Some("deferred_prepass"),
|
||||
color_attachments: &color_attachments,
|
||||
depth_stencil_attachment,
|
||||
timestamp_writes: None,
|
||||
@ -144,24 +144,24 @@ impl ViewNode for DeferredGBufferPrepassNode {
|
||||
}
|
||||
|
||||
// Opaque draws
|
||||
if !opaque_deferred_phase.batchable_keys.is_empty()
|
||||
|| !opaque_deferred_phase.unbatchable_keys.is_empty()
|
||||
if !opaque_deferred_phase.batchable_mesh_keys.is_empty()
|
||||
|| !opaque_deferred_phase.unbatchable_mesh_keys.is_empty()
|
||||
{
|
||||
#[cfg(feature = "trace")]
|
||||
let _opaque_prepass_span = info_span!("opaque_deferred").entered();
|
||||
let _opaque_prepass_span = info_span!("opaque_deferred_prepass").entered();
|
||||
opaque_deferred_phase.render(&mut render_pass, world, view_entity);
|
||||
}
|
||||
|
||||
// Alpha masked draws
|
||||
if !alpha_mask_deferred_phase.is_empty() {
|
||||
#[cfg(feature = "trace")]
|
||||
let _alpha_mask_deferred_span = info_span!("alpha_mask_deferred").entered();
|
||||
let _alpha_mask_deferred_span = info_span!("alpha_mask_deferred_prepass").entered();
|
||||
alpha_mask_deferred_phase.render(&mut render_pass, world, view_entity);
|
||||
}
|
||||
|
||||
drop(render_pass);
|
||||
|
||||
// Copy prepass depth to the main depth texture
|
||||
// After rendering to the view depth texture, copy it to the prepass depth texture
|
||||
if let Some(prepass_depth_texture) = &view_prepass_textures.depth {
|
||||
command_encoder.copy_texture_to_texture(
|
||||
view_depth_texture.texture.as_image_copy(),
|
||||
|
@ -29,12 +29,11 @@ pub mod node;
|
||||
|
||||
use std::ops::Range;
|
||||
|
||||
use bevy_asset::AssetId;
|
||||
use bevy_asset::UntypedAssetId;
|
||||
use bevy_ecs::prelude::*;
|
||||
use bevy_math::Mat4;
|
||||
use bevy_reflect::Reflect;
|
||||
use bevy_render::{
|
||||
mesh::Mesh,
|
||||
render_phase::{
|
||||
BinnedPhaseItem, CachedRenderPipelinePhaseItem, DrawFunctionId, PhaseItem,
|
||||
PhaseItemExtraIndex,
|
||||
@ -147,7 +146,7 @@ pub struct Opaque3dPrepass {
|
||||
}
|
||||
|
||||
// TODO: Try interning these.
|
||||
/// The data used to bin each opaque 3D mesh in the prepass and deferred pass.
|
||||
/// The data used to bin each opaque 3D object in the prepass and deferred pass.
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct OpaqueNoLightmap3dBinKey {
|
||||
/// The ID of the GPU pipeline.
|
||||
@ -156,8 +155,8 @@ pub struct OpaqueNoLightmap3dBinKey {
|
||||
/// The function used to draw the mesh.
|
||||
pub draw_function: DrawFunctionId,
|
||||
|
||||
/// The ID of the mesh.
|
||||
pub asset_id: AssetId<Mesh>,
|
||||
/// The ID of the asset.
|
||||
pub asset_id: UntypedAssetId,
|
||||
|
||||
/// The ID of a bind group specific to the material.
|
||||
///
|
||||
|
@ -120,8 +120,8 @@ impl ViewNode for PrepassNode {
|
||||
}
|
||||
|
||||
// Opaque draws
|
||||
if !opaque_prepass_phase.batchable_keys.is_empty()
|
||||
|| !opaque_prepass_phase.unbatchable_keys.is_empty()
|
||||
if !opaque_prepass_phase.batchable_mesh_keys.is_empty()
|
||||
|| !opaque_prepass_phase.unbatchable_mesh_keys.is_empty()
|
||||
{
|
||||
#[cfg(feature = "trace")]
|
||||
let _opaque_prepass_span = info_span!("opaque_prepass").entered();
|
||||
@ -162,7 +162,7 @@ impl ViewNode for PrepassNode {
|
||||
pass_span.end(&mut render_pass);
|
||||
drop(render_pass);
|
||||
|
||||
// Copy prepass depth to the main depth texture if deferred isn't going to
|
||||
// After rendering to the view depth texture, copy it to the prepass depth texture if deferred isn't going to
|
||||
if deferred_prepass.is_none() {
|
||||
if let Some(prepass_depth_texture) = &view_prepass_textures.depth {
|
||||
command_encoder.copy_texture_to_texture(
|
||||
|
@ -24,7 +24,7 @@ use bevy_render::{
|
||||
};
|
||||
use prepass::{SkyboxPrepassPipeline, SKYBOX_PREPASS_SHADER_HANDLE};
|
||||
|
||||
use crate::core_3d::CORE_3D_DEPTH_FORMAT;
|
||||
use crate::{core_3d::CORE_3D_DEPTH_FORMAT, prepass::PreviousViewUniforms};
|
||||
|
||||
const SKYBOX_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(55594763423201);
|
||||
|
||||
@ -53,6 +53,7 @@ impl Plugin for SkyboxPlugin {
|
||||
render_app
|
||||
.init_resource::<SpecializedRenderPipelines<SkyboxPipeline>>()
|
||||
.init_resource::<SpecializedRenderPipelines<SkyboxPrepassPipeline>>()
|
||||
.init_resource::<PreviousViewUniforms>()
|
||||
.add_systems(
|
||||
Render,
|
||||
(
|
||||
|
@ -31,7 +31,9 @@
|
||||
//! [SMAA]: https://www.iryoku.com/smaa/
|
||||
|
||||
use bevy_app::{App, Plugin};
|
||||
use bevy_asset::{load_internal_asset, load_internal_binary_asset, Handle};
|
||||
#[cfg(feature = "smaa_luts")]
|
||||
use bevy_asset::load_internal_binary_asset;
|
||||
use bevy_asset::{load_internal_asset, Handle};
|
||||
use bevy_derive::{Deref, DerefMut};
|
||||
use bevy_ecs::{
|
||||
component::Component,
|
||||
@ -47,7 +49,7 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect};
|
||||
use bevy_render::{
|
||||
camera::ExtractedCamera,
|
||||
extract_component::{ExtractComponent, ExtractComponentPlugin},
|
||||
render_asset::{RenderAssetUsages, RenderAssets},
|
||||
render_asset::RenderAssets,
|
||||
render_graph::{
|
||||
NodeRunError, RenderGraphApp as _, RenderGraphContext, ViewNode, ViewNodeRunner,
|
||||
},
|
||||
@ -65,15 +67,19 @@ use bevy_render::{
|
||||
VertexState,
|
||||
},
|
||||
renderer::{RenderContext, RenderDevice, RenderQueue},
|
||||
texture::{
|
||||
BevyDefault, CachedTexture, CompressedImageFormats, GpuImage, Image, ImageFormat,
|
||||
ImageSampler, ImageType, TextureCache,
|
||||
},
|
||||
texture::{BevyDefault, CachedTexture, GpuImage, Image, TextureCache},
|
||||
view::{ExtractedView, ViewTarget},
|
||||
Render, RenderApp, RenderSet,
|
||||
};
|
||||
#[cfg(feature = "smaa_luts")]
|
||||
use bevy_render::{
|
||||
render_asset::RenderAssetUsages,
|
||||
texture::{CompressedImageFormats, ImageFormat, ImageSampler, ImageType},
|
||||
};
|
||||
use bevy_utils::prelude::default;
|
||||
|
||||
#[cfg(not(feature = "smaa_luts"))]
|
||||
use crate::tonemapping::lut_placeholder;
|
||||
use crate::{
|
||||
core_2d::graph::{Core2d, Node2d},
|
||||
core_3d::graph::{Core3d, Node3d},
|
||||
@ -287,6 +293,7 @@ impl Plugin for SmaaPlugin {
|
||||
|
||||
// Load the two lookup textures. These are compressed textures in KTX2
|
||||
// format.
|
||||
#[cfg(feature = "smaa_luts")]
|
||||
load_internal_binary_asset!(
|
||||
app,
|
||||
SMAA_AREA_LUT_TEXTURE_HANDLE,
|
||||
@ -304,6 +311,7 @@ impl Plugin for SmaaPlugin {
|
||||
.expect("Failed to load SMAA area LUT")
|
||||
);
|
||||
|
||||
#[cfg(feature = "smaa_luts")]
|
||||
load_internal_binary_asset!(
|
||||
app,
|
||||
SMAA_SEARCH_LUT_TEXTURE_HANDLE,
|
||||
@ -321,6 +329,16 @@ impl Plugin for SmaaPlugin {
|
||||
.expect("Failed to load SMAA search LUT")
|
||||
);
|
||||
|
||||
#[cfg(not(feature = "smaa_luts"))]
|
||||
app.world_mut()
|
||||
.resource_mut::<bevy_asset::Assets<Image>>()
|
||||
.insert(SMAA_AREA_LUT_TEXTURE_HANDLE.id(), lut_placeholder());
|
||||
|
||||
#[cfg(not(feature = "smaa_luts"))]
|
||||
app.world_mut()
|
||||
.resource_mut::<bevy_asset::Assets<Image>>()
|
||||
.insert(SMAA_SEARCH_LUT_TEXTURE_HANDLE.id(), lut_placeholder());
|
||||
|
||||
app.add_plugins(ExtractComponentPlugin::<SmaaSettings>::default())
|
||||
.register_type::<SmaaSettings>();
|
||||
|
||||
|
@ -4,6 +4,7 @@ use bevy_ecs::prelude::*;
|
||||
use bevy_render::camera::{CameraOutputMode, ExtractedCamera};
|
||||
use bevy_render::view::ViewTarget;
|
||||
use bevy_render::{render_resource::*, Render, RenderApp, RenderSet};
|
||||
use bevy_utils::HashSet;
|
||||
|
||||
mod node;
|
||||
|
||||
@ -32,16 +33,32 @@ fn prepare_view_upscaling_pipelines(
|
||||
blit_pipeline: Res<BlitPipeline>,
|
||||
view_targets: Query<(Entity, &ViewTarget, Option<&ExtractedCamera>)>,
|
||||
) {
|
||||
let mut output_textures = HashSet::new();
|
||||
for (entity, view_target, camera) in view_targets.iter() {
|
||||
let out_texture_id = view_target.out_texture().id();
|
||||
let blend_state = if let Some(ExtractedCamera {
|
||||
output_mode: CameraOutputMode::Write { blend_state, .. },
|
||||
..
|
||||
}) = camera
|
||||
{
|
||||
*blend_state
|
||||
match *blend_state {
|
||||
None => {
|
||||
// If we've already seen this output for a camera and it doesn't have a output blend
|
||||
// mode configured, default to alpha blend so that we don't accidentally overwrite
|
||||
// the output texture
|
||||
if output_textures.contains(&out_texture_id) {
|
||||
Some(BlendState::ALPHA_BLENDING)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => *blend_state,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
output_textures.insert(out_texture_id);
|
||||
|
||||
let key = BlitPipelineKey {
|
||||
texture_format: view_target.out_texture_format(),
|
||||
blend_state,
|
||||
|
@ -1,11 +1,11 @@
|
||||
use crate::{blit::BlitPipeline, upscaling::ViewUpscalingPipeline};
|
||||
use bevy_ecs::{prelude::*, query::QueryItem};
|
||||
use bevy_render::camera::{ClearColor, ClearColorConfig};
|
||||
use bevy_render::{
|
||||
camera::{CameraOutputMode, ExtractedCamera},
|
||||
render_graph::{NodeRunError, RenderGraphContext, ViewNode},
|
||||
render_resource::{
|
||||
BindGroup, BindGroupEntries, LoadOp, Operations, PipelineCache, RenderPassColorAttachment,
|
||||
RenderPassDescriptor, StoreOp, TextureViewId,
|
||||
BindGroup, BindGroupEntries, PipelineCache, RenderPassDescriptor, TextureViewId,
|
||||
},
|
||||
renderer::RenderContext,
|
||||
view::ViewTarget,
|
||||
@ -33,19 +33,22 @@ impl ViewNode for UpscalingNode {
|
||||
) -> Result<(), NodeRunError> {
|
||||
let pipeline_cache = world.get_resource::<PipelineCache>().unwrap();
|
||||
let blit_pipeline = world.get_resource::<BlitPipeline>().unwrap();
|
||||
let clear_color_global = world.get_resource::<ClearColor>().unwrap();
|
||||
|
||||
let color_attachment_load_op = if let Some(camera) = camera {
|
||||
let clear_color = if let Some(camera) = camera {
|
||||
match camera.output_mode {
|
||||
CameraOutputMode::Write {
|
||||
color_attachment_load_op,
|
||||
..
|
||||
} => color_attachment_load_op,
|
||||
CameraOutputMode::Write { clear_color, .. } => clear_color,
|
||||
CameraOutputMode::Skip => return Ok(()),
|
||||
}
|
||||
} else {
|
||||
LoadOp::Clear(Default::default())
|
||||
ClearColorConfig::Default
|
||||
};
|
||||
|
||||
let clear_color = match clear_color {
|
||||
ClearColorConfig::Default => Some(clear_color_global.0),
|
||||
ClearColorConfig::Custom(color) => Some(color),
|
||||
ClearColorConfig::None => None,
|
||||
};
|
||||
let converted_clear_color = clear_color.map(|color| color.into());
|
||||
let upscaled_texture = target.main_texture_view();
|
||||
|
||||
let mut cached_bind_group = self.cached_texture_bind_group.lock().unwrap();
|
||||
@ -69,14 +72,9 @@ impl ViewNode for UpscalingNode {
|
||||
|
||||
let pass_descriptor = RenderPassDescriptor {
|
||||
label: Some("upscaling_pass"),
|
||||
color_attachments: &[Some(RenderPassColorAttachment {
|
||||
view: target.out_texture(),
|
||||
resolve_target: None,
|
||||
ops: Operations {
|
||||
load: color_attachment_load_op,
|
||||
store: StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
color_attachments: &[Some(
|
||||
target.out_texture_color_attachment(converted_clear_color),
|
||||
)],
|
||||
depth_stencil_attachment: None,
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bevy_derive"
|
||||
version = "0.14.0-dev"
|
||||
version = "0.14.0"
|
||||
edition = "2021"
|
||||
description = "Provides derive implementations for Bevy Engine"
|
||||
homepage = "https://bevyengine.org"
|
||||
@ -12,7 +12,7 @@ keywords = ["bevy"]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
bevy_macro_utils = { path = "../bevy_macro_utils", version = "0.14.0-dev" }
|
||||
bevy_macro_utils = { path = "../bevy_macro_utils", version = "0.14.0" }
|
||||
|
||||
quote = "1.0"
|
||||
syn = { version = "2.0", features = ["full"] }
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bevy_dev_tools"
|
||||
version = "0.14.0-dev"
|
||||
version = "0.14.0"
|
||||
edition = "2021"
|
||||
description = "Collection of developer tools for the Bevy Engine"
|
||||
homepage = "https://bevyengine.org"
|
||||
@ -15,28 +15,26 @@ bevy_ui_debug = []
|
||||
|
||||
[dependencies]
|
||||
# bevy
|
||||
bevy_app = { path = "../bevy_app", version = "0.14.0-dev" }
|
||||
bevy_asset = { path = "../bevy_asset", version = "0.14.0-dev" }
|
||||
bevy_color = { path = "../bevy_color", version = "0.14.0-dev" }
|
||||
bevy_core = { path = "../bevy_core", version = "0.14.0-dev" }
|
||||
bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.14.0-dev" }
|
||||
bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.14.0-dev" }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" }
|
||||
bevy_gizmos = { path = "../bevy_gizmos", version = "0.14.0-dev" }
|
||||
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.14.0-dev" }
|
||||
bevy_input = { path = "../bevy_input", version = "0.14.0-dev" }
|
||||
bevy_math = { path = "../bevy_math", version = "0.14.0-dev" }
|
||||
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev" }
|
||||
bevy_render = { path = "../bevy_render", version = "0.14.0-dev" }
|
||||
bevy_time = { path = "../bevy_time", version = "0.14.0-dev" }
|
||||
bevy_transform = { path = "../bevy_transform", version = "0.14.0-dev" }
|
||||
bevy_ui = { path = "../bevy_ui", version = "0.14.0-dev", features = [
|
||||
"bevy_text",
|
||||
] }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" }
|
||||
bevy_window = { path = "../bevy_window", version = "0.14.0-dev" }
|
||||
bevy_text = { path = "../bevy_text", version = "0.14.0-dev" }
|
||||
bevy_state = { path = "../bevy_state", version = "0.14.0-dev" }
|
||||
bevy_app = { path = "../bevy_app", version = "0.14.0" }
|
||||
bevy_asset = { path = "../bevy_asset", version = "0.14.0" }
|
||||
bevy_color = { path = "../bevy_color", version = "0.14.1" }
|
||||
bevy_core = { path = "../bevy_core", version = "0.14.0" }
|
||||
bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.14.0" }
|
||||
bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.14.0" }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0" }
|
||||
bevy_gizmos = { path = "../bevy_gizmos", version = "0.14.0" }
|
||||
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.14.0" }
|
||||
bevy_input = { path = "../bevy_input", version = "0.14.0" }
|
||||
bevy_math = { path = "../bevy_math", version = "0.14.0" }
|
||||
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0" }
|
||||
bevy_render = { path = "../bevy_render", version = "0.14.0" }
|
||||
bevy_time = { path = "../bevy_time", version = "0.14.0" }
|
||||
bevy_transform = { path = "../bevy_transform", version = "0.14.0" }
|
||||
bevy_ui = { path = "../bevy_ui", version = "0.14.0", features = ["bevy_text"] }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.14.0" }
|
||||
bevy_window = { path = "../bevy_window", version = "0.14.0" }
|
||||
bevy_text = { path = "../bevy_text", version = "0.14.0" }
|
||||
bevy_state = { path = "../bevy_state", version = "0.14.0" }
|
||||
|
||||
# other
|
||||
serde = { version = "1.0", features = ["derive"], optional = true }
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bevy_diagnostic"
|
||||
version = "0.14.0-dev"
|
||||
version = "0.14.0"
|
||||
edition = "2021"
|
||||
description = "Provides diagnostic functionality for Bevy Engine"
|
||||
homepage = "https://bevyengine.org"
|
||||
@ -15,11 +15,12 @@ sysinfo_plugin = ["sysinfo"]
|
||||
|
||||
[dependencies]
|
||||
# bevy
|
||||
bevy_app = { path = "../bevy_app", version = "0.14.0-dev" }
|
||||
bevy_core = { path = "../bevy_core", version = "0.14.0-dev" }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" }
|
||||
bevy_time = { path = "../bevy_time", version = "0.14.0-dev" }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" }
|
||||
bevy_app = { path = "../bevy_app", version = "0.14.0" }
|
||||
bevy_core = { path = "../bevy_core", version = "0.14.0" }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0" }
|
||||
bevy_time = { path = "../bevy_time", version = "0.14.0" }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.14.0" }
|
||||
bevy_tasks = { path = "../bevy_tasks", version = "0.14.0" }
|
||||
|
||||
const-fnv1a-hash = "1.1.0"
|
||||
|
||||
|
@ -4,6 +4,9 @@ use bevy_ecs::system::Resource;
|
||||
|
||||
/// Adds a System Information Diagnostic, specifically `cpu_usage` (in %) and `mem_usage` (in %)
|
||||
///
|
||||
/// Note that gathering system information is a time intensive task and therefore can't be done on every frame.
|
||||
/// Any system diagnostics gathered by this plugin may not be current when you access them.
|
||||
///
|
||||
/// Supported targets:
|
||||
/// * linux,
|
||||
/// * windows,
|
||||
@ -19,8 +22,7 @@ use bevy_ecs::system::Resource;
|
||||
pub struct SystemInformationDiagnosticsPlugin;
|
||||
impl Plugin for SystemInformationDiagnosticsPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(Startup, internal::setup_system)
|
||||
.add_systems(Update, internal::diagnostic_system);
|
||||
internal::setup_plugin(app);
|
||||
}
|
||||
}
|
||||
|
||||
@ -58,6 +60,14 @@ pub struct SystemInfo {
|
||||
))]
|
||||
pub mod internal {
|
||||
use bevy_ecs::{prelude::ResMut, system::Local};
|
||||
use std::{
|
||||
sync::{Arc, Mutex},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use bevy_app::{App, First, Startup, Update};
|
||||
use bevy_ecs::system::Resource;
|
||||
use bevy_tasks::{available_parallelism, block_on, poll_once, AsyncComputeTaskPool, Task};
|
||||
use bevy_utils::tracing::info;
|
||||
use sysinfo::{CpuRefreshKind, MemoryRefreshKind, RefreshKind, System};
|
||||
|
||||
@ -67,41 +77,91 @@ pub mod internal {
|
||||
|
||||
const BYTES_TO_GIB: f64 = 1.0 / 1024.0 / 1024.0 / 1024.0;
|
||||
|
||||
pub(crate) fn setup_system(mut diagnostics: ResMut<DiagnosticsStore>) {
|
||||
pub(super) fn setup_plugin(app: &mut App) {
|
||||
app.add_systems(Startup, setup_system)
|
||||
.add_systems(First, launch_diagnostic_tasks)
|
||||
.add_systems(Update, read_diagnostic_tasks)
|
||||
.init_resource::<SysinfoTasks>();
|
||||
}
|
||||
|
||||
fn setup_system(mut diagnostics: ResMut<DiagnosticsStore>) {
|
||||
diagnostics
|
||||
.add(Diagnostic::new(SystemInformationDiagnosticsPlugin::CPU_USAGE).with_suffix("%"));
|
||||
diagnostics
|
||||
.add(Diagnostic::new(SystemInformationDiagnosticsPlugin::MEM_USAGE).with_suffix("%"));
|
||||
}
|
||||
|
||||
pub(crate) fn diagnostic_system(
|
||||
mut diagnostics: Diagnostics,
|
||||
mut sysinfo: Local<Option<System>>,
|
||||
struct SysinfoRefreshData {
|
||||
current_cpu_usage: f64,
|
||||
current_used_mem: f64,
|
||||
}
|
||||
|
||||
#[derive(Resource, Default)]
|
||||
struct SysinfoTasks {
|
||||
tasks: Vec<Task<SysinfoRefreshData>>,
|
||||
}
|
||||
|
||||
fn launch_diagnostic_tasks(
|
||||
mut tasks: ResMut<SysinfoTasks>,
|
||||
// TODO: Consider a fair mutex
|
||||
mut sysinfo: Local<Option<Arc<Mutex<System>>>>,
|
||||
// TODO: FromWorld for Instant?
|
||||
mut last_refresh: Local<Option<Instant>>,
|
||||
) {
|
||||
if sysinfo.is_none() {
|
||||
*sysinfo = Some(System::new_with_specifics(
|
||||
let sysinfo = sysinfo.get_or_insert_with(|| {
|
||||
Arc::new(Mutex::new(System::new_with_specifics(
|
||||
RefreshKind::new()
|
||||
.with_cpu(CpuRefreshKind::new().with_cpu_usage())
|
||||
.with_memory(MemoryRefreshKind::everything()),
|
||||
));
|
||||
}
|
||||
let Some(sys) = sysinfo.as_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
sys.refresh_cpu_specifics(CpuRefreshKind::new().with_cpu_usage());
|
||||
sys.refresh_memory();
|
||||
let current_cpu_usage = sys.global_cpu_info().cpu_usage();
|
||||
// `memory()` fns return a value in bytes
|
||||
let total_mem = sys.total_memory() as f64 / BYTES_TO_GIB;
|
||||
let used_mem = sys.used_memory() as f64 / BYTES_TO_GIB;
|
||||
let current_used_mem = used_mem / total_mem * 100.0;
|
||||
|
||||
diagnostics.add_measurement(&SystemInformationDiagnosticsPlugin::CPU_USAGE, || {
|
||||
current_cpu_usage as f64
|
||||
)))
|
||||
});
|
||||
diagnostics.add_measurement(&SystemInformationDiagnosticsPlugin::MEM_USAGE, || {
|
||||
current_used_mem
|
||||
|
||||
let last_refresh = last_refresh.get_or_insert_with(Instant::now);
|
||||
|
||||
let thread_pool = AsyncComputeTaskPool::get();
|
||||
|
||||
// Only queue a new system refresh task when necessary
|
||||
// Queueing earlier than that will not give new data
|
||||
if last_refresh.elapsed() > sysinfo::MINIMUM_CPU_UPDATE_INTERVAL
|
||||
// These tasks don't yield and will take up all of the task pool's
|
||||
// threads if we don't limit their amount.
|
||||
&& tasks.tasks.len() * 2 < available_parallelism()
|
||||
{
|
||||
let sys = Arc::clone(sysinfo);
|
||||
let task = thread_pool.spawn(async move {
|
||||
let mut sys = sys.lock().unwrap();
|
||||
|
||||
sys.refresh_cpu_specifics(CpuRefreshKind::new().with_cpu_usage());
|
||||
sys.refresh_memory();
|
||||
let current_cpu_usage = sys.global_cpu_info().cpu_usage().into();
|
||||
// `memory()` fns return a value in bytes
|
||||
let total_mem = sys.total_memory() as f64 / BYTES_TO_GIB;
|
||||
let used_mem = sys.used_memory() as f64 / BYTES_TO_GIB;
|
||||
let current_used_mem = used_mem / total_mem * 100.0;
|
||||
|
||||
SysinfoRefreshData {
|
||||
current_cpu_usage,
|
||||
current_used_mem,
|
||||
}
|
||||
});
|
||||
tasks.tasks.push(task);
|
||||
*last_refresh = Instant::now();
|
||||
}
|
||||
}
|
||||
|
||||
fn read_diagnostic_tasks(mut diagnostics: Diagnostics, mut tasks: ResMut<SysinfoTasks>) {
|
||||
tasks.tasks.retain_mut(|task| {
|
||||
let Some(data) = block_on(poll_once(task)) else {
|
||||
return true;
|
||||
};
|
||||
|
||||
diagnostics.add_measurement(&SystemInformationDiagnosticsPlugin::CPU_USAGE, || {
|
||||
data.current_cpu_usage
|
||||
});
|
||||
diagnostics.add_measurement(&SystemInformationDiagnosticsPlugin::MEM_USAGE, || {
|
||||
data.current_used_mem
|
||||
});
|
||||
false
|
||||
});
|
||||
}
|
||||
|
||||
@ -145,12 +205,14 @@ pub mod internal {
|
||||
not(feature = "dynamic_linking")
|
||||
)))]
|
||||
pub mod internal {
|
||||
pub(crate) fn setup_system() {
|
||||
bevy_utils::tracing::warn!("This platform and/or configuration is not supported!");
|
||||
use bevy_app::{App, Startup};
|
||||
|
||||
pub(super) fn setup_plugin(app: &mut App) {
|
||||
app.add_systems(Startup, setup_system);
|
||||
}
|
||||
|
||||
pub(crate) fn diagnostic_system() {
|
||||
// no-op
|
||||
fn setup_system() {
|
||||
bevy_utils::tracing::warn!("This platform and/or configuration is not supported!");
|
||||
}
|
||||
|
||||
impl Default for super::SystemInfo {
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bevy_dylib"
|
||||
version = "0.14.0-dev"
|
||||
version = "0.14.0"
|
||||
edition = "2021"
|
||||
description = "Force the Bevy Engine to be dynamically linked for faster linking"
|
||||
homepage = "https://bevyengine.org"
|
||||
@ -12,7 +12,7 @@ keywords = ["bevy"]
|
||||
crate-type = ["dylib"]
|
||||
|
||||
[dependencies]
|
||||
bevy_internal = { path = "../bevy_internal", version = "0.14.0-dev", default-features = false }
|
||||
bevy_internal = { path = "../bevy_internal", version = "0.14.0", default-features = false }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bevy_dynamic_plugin"
|
||||
version = "0.14.0-dev"
|
||||
version = "0.14.0"
|
||||
edition = "2021"
|
||||
description = "Provides dynamic plugin loading capabilities for non-wasm platforms"
|
||||
homepage = "https://bevyengine.org"
|
||||
@ -10,7 +10,7 @@ keywords = ["bevy"]
|
||||
|
||||
[dependencies]
|
||||
# bevy
|
||||
bevy_app = { path = "../bevy_app", version = "0.14.0-dev" }
|
||||
bevy_app = { path = "../bevy_app", version = "0.14.0" }
|
||||
|
||||
# other
|
||||
libloading = { version = "0.8" }
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bevy_ecs"
|
||||
version = "0.14.0-dev"
|
||||
version = "0.14.0"
|
||||
edition = "2021"
|
||||
description = "Bevy Engine's entity component system"
|
||||
homepage = "https://bevyengine.org"
|
||||
@ -15,13 +15,14 @@ trace = []
|
||||
multi_threaded = ["bevy_tasks/multi_threaded", "arrayvec"]
|
||||
bevy_debug_stepping = []
|
||||
default = ["bevy_reflect"]
|
||||
serialize = ["dep:serde"]
|
||||
|
||||
[dependencies]
|
||||
bevy_ptr = { path = "../bevy_ptr", version = "0.14.0-dev" }
|
||||
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev", optional = true }
|
||||
bevy_tasks = { path = "../bevy_tasks", version = "0.14.0-dev" }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" }
|
||||
bevy_ecs_macros = { path = "macros", version = "0.14.0-dev" }
|
||||
bevy_ptr = { path = "../bevy_ptr", version = "0.14.0" }
|
||||
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0", optional = true }
|
||||
bevy_tasks = { path = "../bevy_tasks", version = "0.14.0" }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.14.0" }
|
||||
bevy_ecs_macros = { path = "macros", version = "0.14.0" }
|
||||
|
||||
petgraph = "0.6"
|
||||
bitflags = "2.3"
|
||||
|
@ -307,4 +307,52 @@ fn reader(mut reader: EventReader<MyEvent>) {
|
||||
|
||||
A minimal set up using events can be seen in [`events.rs`](examples/events.rs).
|
||||
|
||||
### Observers
|
||||
|
||||
Observers are systems that listen for a "trigger" of a specific `Event`:
|
||||
|
||||
```rust
|
||||
use bevy_ecs::prelude::*;
|
||||
|
||||
#[derive(Event)]
|
||||
struct MyEvent {
|
||||
message: String
|
||||
}
|
||||
|
||||
let mut world = World::new();
|
||||
|
||||
world.observe(|trigger: Trigger<MyEvent>| {
|
||||
println!("{}", trigger.event().message);
|
||||
});
|
||||
|
||||
world.flush();
|
||||
|
||||
world.trigger(MyEvent {
|
||||
message: "hello!".to_string(),
|
||||
});
|
||||
```
|
||||
|
||||
These differ from `EventReader` and `EventWriter` in that they are "reactive". Rather than happening at a specific point in a schedule, they happen _immediately_ whenever a trigger happens. Triggers can trigger other triggers, and they all will be evaluated at the same time!
|
||||
|
||||
Events can also be triggered to target specific entities:
|
||||
|
||||
```rust
|
||||
use bevy_ecs::prelude::*;
|
||||
|
||||
#[derive(Event)]
|
||||
struct Explode;
|
||||
|
||||
let mut world = World::new();
|
||||
let entity = world.spawn_empty().id();
|
||||
|
||||
world.observe(|trigger: Trigger<Explode>, mut commands: Commands| {
|
||||
println!("Entity {:?} goes BOOM!", trigger.entity());
|
||||
commands.entity(trigger.entity()).despawn();
|
||||
});
|
||||
|
||||
world.flush();
|
||||
|
||||
world.trigger_targets(Explode, entity);
|
||||
```
|
||||
|
||||
[bevy]: https://bevyengine.org/
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bevy_ecs_macros"
|
||||
version = "0.14.0-dev"
|
||||
version = "0.14.0"
|
||||
description = "Bevy ECS Macros"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
@ -9,7 +9,7 @@ license = "MIT OR Apache-2.0"
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.14.0-dev" }
|
||||
bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.14.0" }
|
||||
|
||||
syn = { version = "2.0", features = ["full"] }
|
||||
quote = "1.0"
|
||||
|
@ -18,6 +18,10 @@ pub fn derive_event(input: TokenStream) -> TokenStream {
|
||||
TokenStream::from(quote! {
|
||||
impl #impl_generics #bevy_ecs_path::event::Event for #struct_name #type_generics #where_clause {
|
||||
}
|
||||
|
||||
impl #impl_generics #bevy_ecs_path::component::Component for #struct_name #type_generics #where_clause {
|
||||
const STORAGE_TYPE: #bevy_ecs_path::component::StorageType = #bevy_ecs_path::component::StorageType::SparseSet;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -74,6 +74,7 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream {
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut field_component_ids = Vec::new();
|
||||
let mut field_get_component_ids = Vec::new();
|
||||
let mut field_get_components = Vec::new();
|
||||
let mut field_from_components = Vec::new();
|
||||
for (((i, field_type), field_kind), field) in field_type
|
||||
@ -87,6 +88,9 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream {
|
||||
field_component_ids.push(quote! {
|
||||
<#field_type as #ecs_path::bundle::Bundle>::component_ids(components, storages, &mut *ids);
|
||||
});
|
||||
field_get_component_ids.push(quote! {
|
||||
<#field_type as #ecs_path::bundle::Bundle>::get_component_ids(components, &mut *ids);
|
||||
});
|
||||
match field {
|
||||
Some(field) => {
|
||||
field_get_components.push(quote! {
|
||||
@ -133,6 +137,13 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream {
|
||||
#(#field_component_ids)*
|
||||
}
|
||||
|
||||
fn get_component_ids(
|
||||
components: &#ecs_path::component::Components,
|
||||
ids: &mut impl FnMut(Option<#ecs_path::component::ComponentId>)
|
||||
){
|
||||
#(#field_get_component_ids)*
|
||||
}
|
||||
|
||||
#[allow(unused_variables, non_snake_case)]
|
||||
unsafe fn from_components<__T, __F>(ctx: &mut __T, func: &mut __F) -> Self
|
||||
where
|
||||
@ -435,6 +446,10 @@ pub fn derive_system_param(input: TokenStream) -> TokenStream {
|
||||
<#fields_alias::<'_, '_, #punctuated_generic_idents> as #path::system::SystemParam>::apply(&mut state.state, system_meta, world);
|
||||
}
|
||||
|
||||
fn queue(state: &mut Self::State, system_meta: &#path::system::SystemMeta, world: #path::world::DeferredWorld) {
|
||||
<#fields_alias::<'_, '_, #punctuated_generic_idents> as #path::system::SystemParam>::queue(&mut state.state, system_meta, world);
|
||||
}
|
||||
|
||||
unsafe fn get_param<'w, 's>(
|
||||
state: &'s mut Self::State,
|
||||
system_meta: &#path::system::SystemMeta,
|
||||
|
@ -164,9 +164,9 @@ pub(crate) fn world_query_impl(
|
||||
#( <#field_types>::update_component_access(&state.#named_field_idents, _access); )*
|
||||
}
|
||||
|
||||
fn init_state(initializer: &mut #path::component::ComponentInitializer) -> #state_struct_name #user_ty_generics {
|
||||
fn init_state(world: &mut #path::world::World) -> #state_struct_name #user_ty_generics {
|
||||
#state_struct_name {
|
||||
#(#named_field_idents: <#field_types>::init_state(initializer),)*
|
||||
#(#named_field_idents: <#field_types>::init_state(world),)*
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -23,6 +23,7 @@ use crate::{
|
||||
bundle::BundleId,
|
||||
component::{ComponentId, Components, StorageType},
|
||||
entity::{Entity, EntityLocation},
|
||||
observer::Observers,
|
||||
storage::{ImmutableSparseSet, SparseArray, SparseSet, SparseSetIndex, TableId, TableRow},
|
||||
};
|
||||
use std::{
|
||||
@ -119,6 +120,7 @@ pub(crate) struct AddBundle {
|
||||
/// For each component iterated in the same order as the source [`Bundle`](crate::bundle::Bundle),
|
||||
/// indicate if the component is newly added to the target archetype or if it already existed
|
||||
pub bundle_status: Vec<ComponentStatus>,
|
||||
pub added: Vec<ComponentId>,
|
||||
}
|
||||
|
||||
/// This trait is used to report the status of [`Bundle`](crate::bundle::Bundle) components
|
||||
@ -202,12 +204,14 @@ impl Edges {
|
||||
bundle_id: BundleId,
|
||||
archetype_id: ArchetypeId,
|
||||
bundle_status: Vec<ComponentStatus>,
|
||||
added: Vec<ComponentId>,
|
||||
) {
|
||||
self.add_bundle.insert(
|
||||
bundle_id,
|
||||
AddBundle {
|
||||
archetype_id,
|
||||
bundle_status,
|
||||
added,
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -314,6 +318,9 @@ bitflags::bitflags! {
|
||||
const ON_ADD_HOOK = (1 << 0);
|
||||
const ON_INSERT_HOOK = (1 << 1);
|
||||
const ON_REMOVE_HOOK = (1 << 2);
|
||||
const ON_ADD_OBSERVER = (1 << 3);
|
||||
const ON_INSERT_OBSERVER = (1 << 4);
|
||||
const ON_REMOVE_OBSERVER = (1 << 5);
|
||||
}
|
||||
}
|
||||
|
||||
@ -335,6 +342,7 @@ pub struct Archetype {
|
||||
impl Archetype {
|
||||
pub(crate) fn new(
|
||||
components: &Components,
|
||||
observers: &Observers,
|
||||
id: ArchetypeId,
|
||||
table_id: TableId,
|
||||
table_components: impl Iterator<Item = (ComponentId, ArchetypeComponentId)>,
|
||||
@ -348,6 +356,7 @@ impl Archetype {
|
||||
// SAFETY: We are creating an archetype that includes this component so it must exist
|
||||
let info = unsafe { components.get_info_unchecked(component_id) };
|
||||
info.update_archetype_flags(&mut flags);
|
||||
observers.update_archetype_flags(component_id, &mut flags);
|
||||
archetype_components.insert(
|
||||
component_id,
|
||||
ArchetypeComponentInfo {
|
||||
@ -361,6 +370,7 @@ impl Archetype {
|
||||
// SAFETY: We are creating an archetype that includes this component so it must exist
|
||||
let info = unsafe { components.get_info_unchecked(component_id) };
|
||||
info.update_archetype_flags(&mut flags);
|
||||
observers.update_archetype_flags(component_id, &mut flags);
|
||||
archetype_components.insert(
|
||||
component_id,
|
||||
ArchetypeComponentInfo {
|
||||
@ -580,21 +590,45 @@ impl Archetype {
|
||||
|
||||
/// Returns true if any of the components in this archetype have `on_add` hooks
|
||||
#[inline]
|
||||
pub(crate) fn has_on_add(&self) -> bool {
|
||||
pub fn has_add_hook(&self) -> bool {
|
||||
self.flags().contains(ArchetypeFlags::ON_ADD_HOOK)
|
||||
}
|
||||
|
||||
/// Returns true if any of the components in this archetype have `on_insert` hooks
|
||||
#[inline]
|
||||
pub(crate) fn has_on_insert(&self) -> bool {
|
||||
pub fn has_insert_hook(&self) -> bool {
|
||||
self.flags().contains(ArchetypeFlags::ON_INSERT_HOOK)
|
||||
}
|
||||
|
||||
/// Returns true if any of the components in this archetype have `on_remove` hooks
|
||||
#[inline]
|
||||
pub(crate) fn has_on_remove(&self) -> bool {
|
||||
pub fn has_remove_hook(&self) -> bool {
|
||||
self.flags().contains(ArchetypeFlags::ON_REMOVE_HOOK)
|
||||
}
|
||||
|
||||
/// Returns true if any of the components in this archetype have at least one [`OnAdd`] observer
|
||||
///
|
||||
/// [`OnAdd`]: crate::world::OnAdd
|
||||
#[inline]
|
||||
pub fn has_add_observer(&self) -> bool {
|
||||
self.flags().contains(ArchetypeFlags::ON_ADD_OBSERVER)
|
||||
}
|
||||
|
||||
/// Returns true if any of the components in this archetype have at least one [`OnInsert`] observer
|
||||
///
|
||||
/// [`OnInsert`]: crate::world::OnInsert
|
||||
#[inline]
|
||||
pub fn has_insert_observer(&self) -> bool {
|
||||
self.flags().contains(ArchetypeFlags::ON_INSERT_OBSERVER)
|
||||
}
|
||||
|
||||
/// Returns true if any of the components in this archetype have at least one [`OnRemove`] observer
|
||||
///
|
||||
/// [`OnRemove`]: crate::world::OnRemove
|
||||
#[inline]
|
||||
pub fn has_remove_observer(&self) -> bool {
|
||||
self.flags().contains(ArchetypeFlags::ON_REMOVE_OBSERVER)
|
||||
}
|
||||
}
|
||||
|
||||
/// The next [`ArchetypeId`] in an [`Archetypes`] collection.
|
||||
@ -681,6 +715,7 @@ impl Archetypes {
|
||||
unsafe {
|
||||
archetypes.get_id_or_insert(
|
||||
&Components::default(),
|
||||
&Observers::default(),
|
||||
TableId::empty(),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
@ -782,6 +817,7 @@ impl Archetypes {
|
||||
pub(crate) unsafe fn get_id_or_insert(
|
||||
&mut self,
|
||||
components: &Components,
|
||||
observers: &Observers,
|
||||
table_id: TableId,
|
||||
table_components: Vec<ComponentId>,
|
||||
sparse_set_components: Vec<ComponentId>,
|
||||
@ -808,6 +844,7 @@ impl Archetypes {
|
||||
(sparse_start..*archetype_component_count).map(ArchetypeComponentId);
|
||||
archetypes.push(Archetype::new(
|
||||
components,
|
||||
observers,
|
||||
id,
|
||||
table_id,
|
||||
table_components.into_iter().zip(table_archetype_components),
|
||||
@ -832,6 +869,20 @@ impl Archetypes {
|
||||
archetype.clear_entities();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn update_flags(
|
||||
&mut self,
|
||||
component_id: ComponentId,
|
||||
flags: ArchetypeFlags,
|
||||
set: bool,
|
||||
) {
|
||||
// TODO: Refactor component index to speed this up.
|
||||
for archetype in &mut self.archetypes {
|
||||
if archetype.contains(component_id) {
|
||||
archetype.flags.set(flags, set);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<RangeFrom<ArchetypeGeneration>> for Archetypes {
|
||||
|
@ -2,8 +2,9 @@
|
||||
//!
|
||||
//! This module contains the [`Bundle`] trait and some other helper types.
|
||||
|
||||
use std::any::TypeId;
|
||||
|
||||
pub use bevy_ecs_macros::Bundle;
|
||||
use bevy_utils::{HashMap, HashSet, TypeIdMap};
|
||||
|
||||
use crate::{
|
||||
archetype::{
|
||||
@ -12,14 +13,15 @@ use crate::{
|
||||
},
|
||||
component::{Component, ComponentId, Components, StorageType, Tick},
|
||||
entity::{Entities, Entity, EntityLocation},
|
||||
observer::Observers,
|
||||
prelude::World,
|
||||
query::DebugCheckedUnwrap,
|
||||
storage::{SparseSetIndex, SparseSets, Storages, Table, TableRow},
|
||||
world::unsafe_world_cell::UnsafeWorldCell,
|
||||
world::{unsafe_world_cell::UnsafeWorldCell, ON_ADD, ON_INSERT},
|
||||
};
|
||||
|
||||
use bevy_ptr::{ConstNonNull, OwningPtr};
|
||||
use bevy_utils::all_tuples;
|
||||
use std::any::TypeId;
|
||||
use bevy_utils::{all_tuples, HashMap, HashSet, TypeIdMap};
|
||||
use std::ptr::NonNull;
|
||||
|
||||
/// The `Bundle` trait enables insertion and removal of [`Component`]s from an entity.
|
||||
@ -155,6 +157,9 @@ pub unsafe trait Bundle: DynamicBundle + Send + Sync + 'static {
|
||||
ids: &mut impl FnMut(ComponentId),
|
||||
);
|
||||
|
||||
/// Gets this [`Bundle`]'s component ids. This will be [`None`] if the component has not been registered.
|
||||
fn get_component_ids(components: &Components, ids: &mut impl FnMut(Option<ComponentId>));
|
||||
|
||||
/// Calls `func`, which should return data for each component in the bundle, in the order of
|
||||
/// this bundle's [`Component`]s
|
||||
///
|
||||
@ -204,6 +209,10 @@ unsafe impl<C: Component> Bundle for C {
|
||||
// Safety: The id given in `component_ids` is for `Self`
|
||||
unsafe { ptr.read() }
|
||||
}
|
||||
|
||||
fn get_component_ids(components: &Components, ids: &mut impl FnMut(Option<ComponentId>)) {
|
||||
ids(components.get_id(TypeId::of::<C>()));
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Component> DynamicBundle for C {
|
||||
@ -227,6 +236,11 @@ macro_rules! tuple_impl {
|
||||
$(<$name as Bundle>::component_ids(components, storages, ids);)*
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
fn get_component_ids(components: &Components, ids: &mut impl FnMut(Option<ComponentId>)){
|
||||
$(<$name as Bundle>::get_component_ids(components, ids);)*
|
||||
}
|
||||
|
||||
#[allow(unused_variables, unused_mut)]
|
||||
#[allow(clippy::unused_unit)]
|
||||
unsafe fn from_components<T, F>(ctx: &mut T, func: &mut F) -> Self
|
||||
@ -432,6 +446,7 @@ impl BundleInfo {
|
||||
archetypes: &mut Archetypes,
|
||||
storages: &mut Storages,
|
||||
components: &Components,
|
||||
observers: &Observers,
|
||||
archetype_id: ArchetypeId,
|
||||
) -> ArchetypeId {
|
||||
if let Some(add_bundle_id) = archetypes[archetype_id].edges().get_add_bundle(self.id) {
|
||||
@ -440,6 +455,7 @@ impl BundleInfo {
|
||||
let mut new_table_components = Vec::new();
|
||||
let mut new_sparse_set_components = Vec::new();
|
||||
let mut bundle_status = Vec::with_capacity(self.component_ids.len());
|
||||
let mut added = Vec::new();
|
||||
|
||||
let current_archetype = &mut archetypes[archetype_id];
|
||||
for component_id in self.component_ids.iter().cloned() {
|
||||
@ -447,6 +463,7 @@ impl BundleInfo {
|
||||
bundle_status.push(ComponentStatus::Mutated);
|
||||
} else {
|
||||
bundle_status.push(ComponentStatus::Added);
|
||||
added.push(component_id);
|
||||
// SAFETY: component_id exists
|
||||
let component_info = unsafe { components.get_info_unchecked(component_id) };
|
||||
match component_info.storage_type() {
|
||||
@ -459,7 +476,7 @@ impl BundleInfo {
|
||||
if new_table_components.is_empty() && new_sparse_set_components.is_empty() {
|
||||
let edges = current_archetype.edges_mut();
|
||||
// the archetype does not change when we add this bundle
|
||||
edges.insert_add_bundle(self.id, archetype_id, bundle_status);
|
||||
edges.insert_add_bundle(self.id, archetype_id, bundle_status, added);
|
||||
archetype_id
|
||||
} else {
|
||||
let table_id;
|
||||
@ -498,6 +515,7 @@ impl BundleInfo {
|
||||
// SAFETY: ids in self must be valid
|
||||
let new_archetype_id = archetypes.get_id_or_insert(
|
||||
components,
|
||||
observers,
|
||||
table_id,
|
||||
table_components,
|
||||
sparse_set_components,
|
||||
@ -507,6 +525,7 @@ impl BundleInfo {
|
||||
self.id,
|
||||
new_archetype_id,
|
||||
bundle_status,
|
||||
added,
|
||||
);
|
||||
new_archetype_id
|
||||
}
|
||||
@ -567,6 +586,7 @@ impl<'w> BundleInserter<'w> {
|
||||
&mut world.archetypes,
|
||||
&mut world.storages,
|
||||
&world.components,
|
||||
&world.observers,
|
||||
archetype_id,
|
||||
);
|
||||
if new_archetype_id == archetype_id {
|
||||
@ -786,27 +806,21 @@ impl<'w> BundleInserter<'w> {
|
||||
}
|
||||
};
|
||||
|
||||
let new_archetype = &*new_archetype;
|
||||
// SAFETY: We have no outstanding mutable references to world as they were dropped
|
||||
let mut deferred_world = unsafe { self.world.into_deferred() };
|
||||
|
||||
if new_archetype.has_on_add() {
|
||||
// SAFETY: All components in the bundle are guaranteed to exist in the World
|
||||
// as they must be initialized before creating the BundleInfo.
|
||||
unsafe {
|
||||
deferred_world.trigger_on_add(
|
||||
entity,
|
||||
bundle_info
|
||||
.iter_components()
|
||||
.zip(add_bundle.bundle_status.iter())
|
||||
.filter(|(_, &status)| status == ComponentStatus::Added)
|
||||
.map(|(id, _)| id),
|
||||
);
|
||||
// SAFETY: All components in the bundle are guaranteed to exist in the World
|
||||
// as they must be initialized before creating the BundleInfo.
|
||||
unsafe {
|
||||
deferred_world.trigger_on_add(new_archetype, entity, add_bundle.added.iter().cloned());
|
||||
if new_archetype.has_add_observer() {
|
||||
deferred_world.trigger_observers(ON_ADD, entity, add_bundle.added.iter().cloned());
|
||||
}
|
||||
deferred_world.trigger_on_insert(new_archetype, entity, bundle_info.iter_components());
|
||||
if new_archetype.has_insert_observer() {
|
||||
deferred_world.trigger_observers(ON_INSERT, entity, bundle_info.iter_components());
|
||||
}
|
||||
}
|
||||
if new_archetype.has_on_insert() {
|
||||
// SAFETY: All components in the bundle are guaranteed to exist in the World
|
||||
// as they must be initialized before creating the BundleInfo.
|
||||
unsafe { deferred_world.trigger_on_insert(entity, bundle_info.iter_components()) }
|
||||
}
|
||||
|
||||
new_location
|
||||
@ -853,6 +867,7 @@ impl<'w> BundleSpawner<'w> {
|
||||
&mut world.archetypes,
|
||||
&mut world.storages,
|
||||
&world.components,
|
||||
&world.observers,
|
||||
ArchetypeId::EMPTY,
|
||||
);
|
||||
let archetype = &mut world.archetypes[new_archetype_id];
|
||||
@ -882,12 +897,12 @@ impl<'w> BundleSpawner<'w> {
|
||||
entity: Entity,
|
||||
bundle: T,
|
||||
) -> EntityLocation {
|
||||
let table = self.table.as_mut();
|
||||
let archetype = self.archetype.as_mut();
|
||||
// SAFETY: We do not make any structural changes to the archetype graph through self.world so these pointers always remain valid
|
||||
let bundle_info = self.bundle_info.as_ref();
|
||||
|
||||
// SAFETY: We do not make any structural changes to the archetype graph through self.world so this pointer always remain valid
|
||||
let location = {
|
||||
let table = self.table.as_mut();
|
||||
let archetype = self.archetype.as_mut();
|
||||
|
||||
// SAFETY: Mutable references do not alias and will be dropped after this block
|
||||
let (sparse_sets, entities) = {
|
||||
let world = self.world.world_mut();
|
||||
@ -910,16 +925,20 @@ impl<'w> BundleSpawner<'w> {
|
||||
|
||||
// SAFETY: We have no outstanding mutable references to world as they were dropped
|
||||
let mut deferred_world = unsafe { self.world.into_deferred() };
|
||||
if archetype.has_on_add() {
|
||||
// SAFETY: All components in the bundle are guaranteed to exist in the World
|
||||
// as they must be initialized before creating the BundleInfo.
|
||||
unsafe { deferred_world.trigger_on_add(entity, bundle_info.iter_components()) };
|
||||
}
|
||||
if archetype.has_on_insert() {
|
||||
// SAFETY: All components in the bundle are guaranteed to exist in the World
|
||||
// as they must be initialized before creating the BundleInfo.
|
||||
unsafe { deferred_world.trigger_on_insert(entity, bundle_info.iter_components()) };
|
||||
}
|
||||
// SAFETY: `DeferredWorld` cannot provide mutable access to `Archetypes`.
|
||||
let archetype = self.archetype.as_ref();
|
||||
// SAFETY: All components in the bundle are guaranteed to exist in the World
|
||||
// as they must be initialized before creating the BundleInfo.
|
||||
unsafe {
|
||||
deferred_world.trigger_on_add(archetype, entity, bundle_info.iter_components());
|
||||
if archetype.has_add_observer() {
|
||||
deferred_world.trigger_observers(ON_ADD, entity, bundle_info.iter_components());
|
||||
}
|
||||
deferred_world.trigger_on_insert(archetype, entity, bundle_info.iter_components());
|
||||
if archetype.has_insert_observer() {
|
||||
deferred_world.trigger_observers(ON_INSERT, entity, bundle_info.iter_components());
|
||||
}
|
||||
};
|
||||
|
||||
location
|
||||
}
|
||||
@ -947,7 +966,7 @@ impl<'w> BundleSpawner<'w> {
|
||||
#[inline]
|
||||
pub(crate) unsafe fn flush_commands(&mut self) {
|
||||
// SAFETY: pointers on self can be invalidated,
|
||||
self.world.world_mut().flush_commands();
|
||||
self.world.world_mut().flush();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1223,13 +1242,13 @@ mod tests {
|
||||
world
|
||||
.register_component_hooks::<C>()
|
||||
.on_add(|mut world, _, _| {
|
||||
world.resource_mut::<R>().assert_order(2);
|
||||
world.resource_mut::<R>().assert_order(3);
|
||||
});
|
||||
|
||||
world
|
||||
.register_component_hooks::<D>()
|
||||
.on_add(|mut world, _, _| {
|
||||
world.resource_mut::<R>().assert_order(3);
|
||||
world.resource_mut::<R>().assert_order(2);
|
||||
});
|
||||
|
||||
world.spawn(A).flush();
|
||||
|
@ -21,7 +21,6 @@ use std::{
|
||||
borrow::Cow,
|
||||
marker::PhantomData,
|
||||
mem::needs_drop,
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
/// A data type that can be used to store data for an [entity].
|
||||
@ -838,75 +837,6 @@ impl Components {
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper over a mutable [`Components`] reference that allows for state initialization.
|
||||
/// This can be obtained with [`World::component_initializer`].
|
||||
pub struct ComponentInitializer<'w> {
|
||||
pub(crate) components: &'w mut Components,
|
||||
pub(crate) storages: &'w mut Storages,
|
||||
}
|
||||
|
||||
impl<'w> Deref for ComponentInitializer<'w> {
|
||||
type Target = Components;
|
||||
|
||||
fn deref(&self) -> &Components {
|
||||
self.components
|
||||
}
|
||||
}
|
||||
|
||||
impl<'w> ComponentInitializer<'w> {
|
||||
/// Initializes a component of type `T` with this instance.
|
||||
/// If a component of this type has already been initialized, this will return
|
||||
/// the ID of the pre-existing component.
|
||||
///
|
||||
/// # See also
|
||||
///
|
||||
/// * [`Components::component_id()`]
|
||||
/// * [`Components::init_component_with_descriptor()`]
|
||||
#[inline]
|
||||
pub fn init_component<T: Component>(&mut self) -> ComponentId {
|
||||
self.components.init_component::<T>(self.storages)
|
||||
}
|
||||
|
||||
/// Initializes a component described by `descriptor`.
|
||||
///
|
||||
/// ## Note
|
||||
///
|
||||
/// If this method is called multiple times with identical descriptors, a distinct `ComponentId`
|
||||
/// will be created for each one.
|
||||
///
|
||||
/// # See also
|
||||
///
|
||||
/// * [`Components::component_id()`]
|
||||
/// * [`Components::init_component()`]
|
||||
pub fn init_component_with_descriptor(
|
||||
&mut self,
|
||||
descriptor: ComponentDescriptor,
|
||||
) -> ComponentId {
|
||||
self.components
|
||||
.init_component_with_descriptor(self.storages, descriptor)
|
||||
}
|
||||
|
||||
/// Initializes a [`Resource`] of type `T` with this instance.
|
||||
/// If a resource of this type has already been initialized, this will return
|
||||
/// the ID of the pre-existing resource.
|
||||
///
|
||||
/// # See also
|
||||
///
|
||||
/// * [`Components::resource_id()`]
|
||||
#[inline]
|
||||
pub fn init_resource<T: Resource>(&mut self) -> ComponentId {
|
||||
self.components.init_resource::<T>()
|
||||
}
|
||||
|
||||
/// Initializes a [non-send resource](crate::system::NonSend) of type `T` with this instance.
|
||||
/// If a resource of this type has already been initialized, this will return
|
||||
/// the ID of the pre-existing resource.
|
||||
#[inline]
|
||||
pub fn init_non_send<T: Any>(&mut self) -> ComponentId {
|
||||
self.components.init_non_send::<T>()
|
||||
}
|
||||
}
|
||||
|
||||
/// A value that tracks when a system ran relative to other systems.
|
||||
/// This is used to power change detection.
|
||||
///
|
||||
|
@ -38,7 +38,7 @@
|
||||
mod map_entities;
|
||||
#[cfg(feature = "bevy_reflect")]
|
||||
use bevy_reflect::Reflect;
|
||||
#[cfg(all(feature = "bevy_reflect", feature = "serde"))]
|
||||
#[cfg(all(feature = "bevy_reflect", feature = "serialize"))]
|
||||
use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
|
||||
pub use map_entities::*;
|
||||
|
||||
@ -57,7 +57,7 @@ use crate::{
|
||||
},
|
||||
storage::{SparseSetIndex, TableId, TableRow},
|
||||
};
|
||||
#[cfg(feature = "serde")]
|
||||
#[cfg(feature = "serialize")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{fmt, hash::Hash, mem, num::NonZeroU32, sync::atomic::Ordering};
|
||||
|
||||
@ -146,7 +146,7 @@ type IdCursor = isize;
|
||||
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
|
||||
#[cfg_attr(feature = "bevy_reflect", reflect_value(Hash, PartialEq))]
|
||||
#[cfg_attr(
|
||||
all(feature = "bevy_reflect", feature = "serde"),
|
||||
all(feature = "bevy_reflect", feature = "serialize"),
|
||||
reflect_value(Serialize, Deserialize)
|
||||
)]
|
||||
// Alignment repr necessary to allow LLVM to better output
|
||||
@ -368,7 +368,7 @@ impl From<Entity> for Identifier {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
#[cfg(feature = "serialize")]
|
||||
impl Serialize for Entity {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
@ -378,7 +378,7 @@ impl Serialize for Entity {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
#[cfg(feature = "serialize")]
|
||||
impl<'de> Deserialize<'de> for Entity {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
@ -392,13 +392,7 @@ impl<'de> Deserialize<'de> for Entity {
|
||||
|
||||
impl fmt::Display for Entity {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}v{}|{}",
|
||||
self.index(),
|
||||
self.generation(),
|
||||
self.to_bits()
|
||||
)
|
||||
write!(f, "{}v{}", self.index(), self.generation())
|
||||
}
|
||||
}
|
||||
|
||||
@ -1162,9 +1156,7 @@ mod tests {
|
||||
fn entity_display() {
|
||||
let entity = Entity::from_raw(42);
|
||||
let string = format!("{}", entity);
|
||||
let bits = entity.to_bits().to_string();
|
||||
assert!(string.contains("42"));
|
||||
assert!(string.contains("v1"));
|
||||
assert!(string.contains(&bits));
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,12 @@
|
||||
//! Event handling types.
|
||||
|
||||
use crate as bevy_ecs;
|
||||
#[cfg(feature = "multi_threaded")]
|
||||
use crate::batching::BatchingStrategy;
|
||||
use crate::change_detection::MutUntyped;
|
||||
use crate::{
|
||||
change_detection::{DetectChangesMut, Mut},
|
||||
component::{ComponentId, Tick},
|
||||
component::{Component, ComponentId, Tick},
|
||||
system::{Local, Res, ResMut, Resource, SystemParam},
|
||||
world::World,
|
||||
};
|
||||
@ -24,16 +25,30 @@ use std::{
|
||||
slice::Iter,
|
||||
};
|
||||
|
||||
/// A type that can be stored in an [`Events<E>`] resource
|
||||
/// Something that "happens" and might be read / observed by app logic.
|
||||
///
|
||||
/// Events can be stored in an [`Events<E>`] resource
|
||||
/// You can conveniently access events using the [`EventReader`] and [`EventWriter`] system parameter.
|
||||
///
|
||||
/// Events can also be "triggered" on a [`World`], which will then cause any [`Observer`] of that trigger to run.
|
||||
///
|
||||
/// This trait can be derived.
|
||||
///
|
||||
/// Events implement the [`Component`] type (and they automatically do when they are derived). Events are (generally)
|
||||
/// not directly inserted as components. More often, the [`ComponentId`] is used to identify the event type within the
|
||||
/// context of the ECS.
|
||||
///
|
||||
/// Events must be thread-safe.
|
||||
///
|
||||
/// [`World`]: crate::world::World
|
||||
/// [`ComponentId`]: crate::component::ComponentId
|
||||
/// [`Observer`]: crate::observer::Observer
|
||||
#[diagnostic::on_unimplemented(
|
||||
message = "`{Self}` is not an `Event`",
|
||||
label = "invalid `Event`",
|
||||
note = "consider annotating `{Self}` with `#[derive(Event)]`"
|
||||
)]
|
||||
pub trait Event: Send + Sync + 'static {}
|
||||
pub trait Event: Component {}
|
||||
|
||||
/// An `EventId` uniquely identifies an event stored in a specific [`World`].
|
||||
///
|
||||
@ -495,6 +510,7 @@ impl<'w, 's, E: Event> EventReader<'w, 's, E> {
|
||||
/// assert_eq!(counter.into_inner(), 4950);
|
||||
/// ```
|
||||
///
|
||||
#[cfg(feature = "multi_threaded")]
|
||||
pub fn par_read(&mut self) -> EventParIter<'_, E> {
|
||||
self.reader.par_read(&self.events)
|
||||
}
|
||||
@ -556,6 +572,11 @@ impl<'w, 's, E: Event> EventReader<'w, 's, E> {
|
||||
///
|
||||
/// # bevy_ecs::system::assert_is_system(my_system);
|
||||
/// ```
|
||||
/// # Observers
|
||||
///
|
||||
/// "Buffered" Events, such as those sent directly in [`Events`] or sent using [`EventWriter`], do _not_ automatically
|
||||
/// trigger any [`Observer`]s watching for that event, as each [`Event`] has different requirements regarding _if_ it will
|
||||
/// be triggered, and if so, _when_ it will be triggered in the schedule.
|
||||
///
|
||||
/// # Concurrency
|
||||
///
|
||||
@ -588,6 +609,8 @@ impl<'w, 's, E: Event> EventReader<'w, 's, E> {
|
||||
/// }
|
||||
/// ```
|
||||
/// Note that this is considered *non-idiomatic*, and should only be used when `EventWriter` will not work.
|
||||
///
|
||||
/// [`Observer`]: crate::observer::Observer
|
||||
#[derive(SystemParam)]
|
||||
pub struct EventWriter<'w, E: Event> {
|
||||
events: ResMut<'w, Events<E>>,
|
||||
@ -701,6 +724,7 @@ impl<E: Event> ManualEventReader<E> {
|
||||
}
|
||||
|
||||
/// See [`EventReader::par_read`]
|
||||
#[cfg(feature = "multi_threaded")]
|
||||
pub fn par_read<'a>(&'a mut self, events: &'a Events<E>) -> EventParIter<'a, E> {
|
||||
EventParIter::new(self, events)
|
||||
}
|
||||
@ -869,13 +893,16 @@ impl<'a, E: Event> ExactSizeIterator for EventIteratorWithId<'a, E> {
|
||||
}
|
||||
|
||||
/// A parallel iterator over `Event`s.
|
||||
#[cfg(feature = "multi_threaded")]
|
||||
#[derive(Debug)]
|
||||
pub struct EventParIter<'a, E: Event> {
|
||||
reader: &'a mut ManualEventReader<E>,
|
||||
slices: [&'a [EventInstance<E>]; 2],
|
||||
batching_strategy: BatchingStrategy,
|
||||
unread: usize,
|
||||
}
|
||||
|
||||
#[cfg(feature = "multi_threaded")]
|
||||
impl<'a, E: Event> EventParIter<'a, E> {
|
||||
/// Creates a new parallel iterator over `events` that have not yet been seen by `reader`.
|
||||
pub fn new(reader: &'a mut ManualEventReader<E>, events: &'a Events<E>) -> Self {
|
||||
@ -897,6 +924,7 @@ impl<'a, E: Event> EventParIter<'a, E> {
|
||||
reader,
|
||||
slices: [a, b],
|
||||
batching_strategy: BatchingStrategy::default(),
|
||||
unread: unread_count,
|
||||
}
|
||||
}
|
||||
|
||||
@ -932,7 +960,7 @@ impl<'a, E: Event> EventParIter<'a, E> {
|
||||
/// initialized and run from the ECS scheduler, this should never panic.
|
||||
///
|
||||
/// [`ComputeTaskPool`]: bevy_tasks::ComputeTaskPool
|
||||
pub fn for_each_with_id<FN: Fn(&'a E, EventId<E>) + Send + Sync + Clone>(self, func: FN) {
|
||||
pub fn for_each_with_id<FN: Fn(&'a E, EventId<E>) + Send + Sync + Clone>(mut self, func: FN) {
|
||||
#[cfg(any(target_arch = "wasm32", not(feature = "multi_threaded")))]
|
||||
{
|
||||
self.into_iter().for_each(|(e, i)| func(e, i));
|
||||
@ -962,6 +990,10 @@ impl<'a, E: Event> EventParIter<'a, E> {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Events are guaranteed to be read at this point.
|
||||
self.reader.last_event_count += self.unread;
|
||||
self.unread = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -976,6 +1008,7 @@ impl<'a, E: Event> EventParIter<'a, E> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "multi_threaded")]
|
||||
impl<'a, E: Event> IntoIterator for EventParIter<'a, E> {
|
||||
type IntoIter = EventIteratorWithId<'a, E>;
|
||||
type Item = <Self::IntoIter as Iterator>::Item;
|
||||
@ -1010,12 +1043,30 @@ struct RegisteredEvent {
|
||||
/// to update all of the events.
|
||||
#[derive(Resource, Default)]
|
||||
pub struct EventRegistry {
|
||||
needs_update: bool,
|
||||
/// Should the events be updated?
|
||||
///
|
||||
/// This field is generally automatically updated by the [`signal_event_update_system`](crate::event::update::signal_event_update_system).
|
||||
pub should_update: ShouldUpdateEvents,
|
||||
event_updates: Vec<RegisteredEvent>,
|
||||
}
|
||||
|
||||
/// Controls whether or not the events in an [`EventRegistry`] should be updated.
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ShouldUpdateEvents {
|
||||
/// Without any fixed timestep, events should always be updated each frame.
|
||||
#[default]
|
||||
Always,
|
||||
/// We need to wait until at least one pass of the fixed update schedules to update the events.
|
||||
Waiting,
|
||||
/// At least one pass of the fixed update schedules has occurred, and the events are ready to be updated.
|
||||
Ready,
|
||||
}
|
||||
|
||||
impl EventRegistry {
|
||||
/// Registers an event type to be updated.
|
||||
/// Registers an event type to be updated in a given [`World`]
|
||||
///
|
||||
/// If no instance of the [`EventRegistry`] exists in the world, this will add one - otherwise it will use
|
||||
/// the existing instance.
|
||||
pub fn register_event<T: Event>(world: &mut World) {
|
||||
// By initializing the resource here, we can be sure that it is present,
|
||||
// and receive the correct, up-to-date `ComponentId` even if it was previously removed.
|
||||
@ -1033,6 +1084,16 @@ impl EventRegistry {
|
||||
});
|
||||
}
|
||||
|
||||
/// Removes an event from the world and it's associated [`EventRegistry`].
|
||||
pub fn deregister_events<T: Event>(world: &mut World) {
|
||||
let component_id = world.init_resource::<Events<T>>();
|
||||
let mut registry = world.get_resource_or_insert_with(Self::default);
|
||||
registry
|
||||
.event_updates
|
||||
.retain(|e| e.component_id != component_id);
|
||||
world.remove_resource::<Events<T>>();
|
||||
}
|
||||
|
||||
/// Updates all of the registered events in the World.
|
||||
pub fn run_updates(&mut self, world: &mut World, last_change_tick: Tick) {
|
||||
for registered_event in &mut self.event_updates {
|
||||
@ -1058,9 +1119,12 @@ impl EventRegistry {
|
||||
pub struct EventUpdates;
|
||||
|
||||
/// Signals the [`event_update_system`] to run after `FixedUpdate` systems.
|
||||
///
|
||||
/// This will change the behavior of the [`EventRegistry`] to only run after a fixed update cycle has passed.
|
||||
/// Normally, this will simply run every frame.
|
||||
pub fn signal_event_update_system(signal: Option<ResMut<EventRegistry>>) {
|
||||
if let Some(mut registry) = signal {
|
||||
registry.needs_update = true;
|
||||
registry.should_update = ShouldUpdateEvents::Ready;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1069,18 +1133,34 @@ pub fn event_update_system(world: &mut World, mut last_change_tick: Local<Tick>)
|
||||
if world.contains_resource::<EventRegistry>() {
|
||||
world.resource_scope(|world, mut registry: Mut<EventRegistry>| {
|
||||
registry.run_updates(world, *last_change_tick);
|
||||
// Disable the system until signal_event_update_system runs again.
|
||||
registry.needs_update = false;
|
||||
|
||||
registry.should_update = match registry.should_update {
|
||||
// If we're always updating, keep doing so.
|
||||
ShouldUpdateEvents::Always => ShouldUpdateEvents::Always,
|
||||
// Disable the system until signal_event_update_system runs again.
|
||||
ShouldUpdateEvents::Waiting | ShouldUpdateEvents::Ready => {
|
||||
ShouldUpdateEvents::Waiting
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
*last_change_tick = world.change_tick();
|
||||
}
|
||||
|
||||
/// A run condition for [`event_update_system`].
|
||||
pub fn event_update_condition(signal: Option<Res<EventRegistry>>) -> bool {
|
||||
// If we haven't got a signal to update the events, but we *could* get such a signal
|
||||
// return early and update the events later.
|
||||
signal.map_or(false, |signal| signal.needs_update)
|
||||
///
|
||||
/// If [`signal_event_update_system`] has been run at least once,
|
||||
/// we will wait for it to be run again before updating the events.
|
||||
///
|
||||
/// Otherwise, we will always update the events.
|
||||
pub fn event_update_condition(maybe_signal: Option<Res<EventRegistry>>) -> bool {
|
||||
match maybe_signal {
|
||||
Some(signal) => match signal.should_update {
|
||||
ShouldUpdateEvents::Always | ShouldUpdateEvents::Ready => true,
|
||||
ShouldUpdateEvents::Waiting => false,
|
||||
},
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Iterator`] over sent [`EventIds`](`EventId`) from a batch.
|
||||
@ -1517,28 +1597,83 @@ mod tests {
|
||||
#[cfg(feature = "multi_threaded")]
|
||||
#[test]
|
||||
fn test_events_par_iter() {
|
||||
use std::{collections::HashSet, sync::mpsc};
|
||||
|
||||
use crate::prelude::*;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
#[derive(Resource)]
|
||||
struct Counter(AtomicUsize);
|
||||
|
||||
let mut world = World::new();
|
||||
world.init_resource::<Events<TestEvent>>();
|
||||
for i in 0..100 {
|
||||
world.send_event(TestEvent { i });
|
||||
for _ in 0..100 {
|
||||
world.send_event(TestEvent { i: 1 });
|
||||
}
|
||||
|
||||
let mut schedule = Schedule::default();
|
||||
|
||||
schedule.add_systems(|mut events: EventReader<TestEvent>| {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
events.par_read().for_each(|event| {
|
||||
tx.send(event.i).unwrap();
|
||||
});
|
||||
drop(tx);
|
||||
|
||||
let observed: HashSet<_> = rx.into_iter().collect();
|
||||
assert_eq!(observed, HashSet::from_iter(0..100));
|
||||
});
|
||||
schedule.add_systems(
|
||||
|mut events: EventReader<TestEvent>, counter: ResMut<Counter>| {
|
||||
events.par_read().for_each(|event| {
|
||||
counter.0.fetch_add(event.i, Ordering::Relaxed);
|
||||
});
|
||||
},
|
||||
);
|
||||
world.insert_resource(Counter(AtomicUsize::new(0)));
|
||||
schedule.run(&mut world);
|
||||
let counter = world.remove_resource::<Counter>().unwrap();
|
||||
assert_eq!(counter.0.into_inner(), 100);
|
||||
|
||||
world.insert_resource(Counter(AtomicUsize::new(0)));
|
||||
schedule.run(&mut world);
|
||||
let counter = world.remove_resource::<Counter>().unwrap();
|
||||
assert_eq!(counter.0.into_inner(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iter_current_update_events_iterates_over_current_events() {
|
||||
#[derive(Event, Clone)]
|
||||
struct TestEvent;
|
||||
|
||||
let mut test_events = Events::<TestEvent>::default();
|
||||
|
||||
// Starting empty
|
||||
assert_eq!(test_events.len(), 0);
|
||||
assert_eq!(test_events.iter_current_update_events().count(), 0);
|
||||
test_events.update();
|
||||
|
||||
// Sending one event
|
||||
test_events.send(TestEvent);
|
||||
|
||||
assert_eq!(test_events.len(), 1);
|
||||
assert_eq!(test_events.iter_current_update_events().count(), 1);
|
||||
test_events.update();
|
||||
|
||||
// Sending two events on the next frame
|
||||
test_events.send(TestEvent);
|
||||
test_events.send(TestEvent);
|
||||
|
||||
assert_eq!(test_events.len(), 3); // Events are double-buffered, so we see 1 + 2 = 3
|
||||
assert_eq!(test_events.iter_current_update_events().count(), 2);
|
||||
test_events.update();
|
||||
|
||||
// Sending zero events
|
||||
assert_eq!(test_events.len(), 2); // Events are double-buffered, so we see 2 + 0 = 2
|
||||
assert_eq!(test_events.iter_current_update_events().count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_event_registry_can_add_and_remove_events_to_world() {
|
||||
use bevy_ecs::prelude::*;
|
||||
|
||||
let mut world = World::new();
|
||||
EventRegistry::register_event::<TestEvent>(&mut world);
|
||||
|
||||
let has_events = world.get_resource::<Events<TestEvent>>().is_some();
|
||||
|
||||
assert!(has_events, "Should have the events resource");
|
||||
|
||||
EventRegistry::deregister_events::<TestEvent>(&mut world);
|
||||
|
||||
let has_events = world.get_resource::<Events<TestEvent>>().is_some();
|
||||
|
||||
assert!(!has_events, "Should not have the events resource");
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ pub mod event;
|
||||
pub mod identifier;
|
||||
pub mod intern;
|
||||
pub mod label;
|
||||
pub mod observer;
|
||||
pub mod query;
|
||||
#[cfg(feature = "bevy_reflect")]
|
||||
pub mod reflect;
|
||||
@ -45,7 +46,8 @@ pub mod prelude {
|
||||
change_detection::{DetectChanges, DetectChangesMut, Mut, Ref},
|
||||
component::Component,
|
||||
entity::{Entity, EntityMapper},
|
||||
event::{Event, EventReader, EventWriter, Events},
|
||||
event::{Event, EventReader, EventWriter, Events, ShouldUpdateEvents},
|
||||
observer::{Observer, Trigger},
|
||||
query::{Added, AnyOf, Changed, Has, Or, QueryBuilder, QueryState, With, Without},
|
||||
removal_detection::RemovedComponents,
|
||||
schedule::{
|
||||
@ -57,7 +59,9 @@ pub mod prelude {
|
||||
ParamSet, Query, ReadOnlySystem, Res, ResMut, Resource, System, SystemBuilder,
|
||||
SystemParamFunction,
|
||||
},
|
||||
world::{EntityMut, EntityRef, EntityWorldMut, FromWorld, World},
|
||||
world::{
|
||||
EntityMut, EntityRef, EntityWorldMut, FromWorld, OnAdd, OnInsert, OnRemove, World,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
42
crates/bevy_ecs/src/observer/entity_observer.rs
Normal file
42
crates/bevy_ecs/src/observer/entity_observer.rs
Normal file
@ -0,0 +1,42 @@
|
||||
use crate::{
|
||||
component::{Component, ComponentHooks, StorageType},
|
||||
entity::Entity,
|
||||
observer::ObserverState,
|
||||
};
|
||||
|
||||
/// Tracks a list of entity observers for the [`Entity`] [`ObservedBy`] is added to.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct ObservedBy(pub(crate) Vec<Entity>);
|
||||
|
||||
impl Component for ObservedBy {
|
||||
const STORAGE_TYPE: StorageType = StorageType::SparseSet;
|
||||
|
||||
fn register_component_hooks(hooks: &mut ComponentHooks) {
|
||||
hooks.on_remove(|mut world, entity, _| {
|
||||
let observed_by = {
|
||||
let mut component = world.get_mut::<ObservedBy>(entity).unwrap();
|
||||
std::mem::take(&mut component.0)
|
||||
};
|
||||
for e in observed_by {
|
||||
let (total_entities, despawned_watched_entities) = {
|
||||
let Some(mut entity_mut) = world.get_entity_mut(e) else {
|
||||
continue;
|
||||
};
|
||||
let Some(mut state) = entity_mut.get_mut::<ObserverState>() else {
|
||||
continue;
|
||||
};
|
||||
state.despawned_watched_entities += 1;
|
||||
(
|
||||
state.descriptor.entities.len(),
|
||||
state.despawned_watched_entities as usize,
|
||||
)
|
||||
};
|
||||
|
||||
// Despawn Observer if it has no more active sources.
|
||||
if total_entities == despawned_watched_entities {
|
||||
world.commands().entity(e).despawn();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
665
crates/bevy_ecs/src/observer/mod.rs
Normal file
665
crates/bevy_ecs/src/observer/mod.rs
Normal file
@ -0,0 +1,665 @@
|
||||
//! Types for creating and storing [`Observer`]s
|
||||
|
||||
mod entity_observer;
|
||||
mod runner;
|
||||
mod trigger_event;
|
||||
|
||||
pub use runner::*;
|
||||
pub use trigger_event::*;
|
||||
|
||||
use crate::observer::entity_observer::ObservedBy;
|
||||
use crate::{archetype::ArchetypeFlags, system::IntoObserverSystem, world::*};
|
||||
use crate::{component::ComponentId, prelude::*, world::DeferredWorld};
|
||||
use bevy_ptr::Ptr;
|
||||
use bevy_utils::{EntityHashMap, HashMap};
|
||||
use std::marker::PhantomData;
|
||||
|
||||
/// Type containing triggered [`Event`] information for a given run of an [`Observer`]. This contains the
|
||||
/// [`Event`] data itself. If it was triggered for a specific [`Entity`], it includes that as well.
|
||||
pub struct Trigger<'w, E, B: Bundle = ()> {
|
||||
event: &'w mut E,
|
||||
trigger: ObserverTrigger,
|
||||
_marker: PhantomData<B>,
|
||||
}
|
||||
|
||||
impl<'w, E, B: Bundle> Trigger<'w, E, B> {
|
||||
/// Creates a new trigger for the given event and observer information.
|
||||
pub fn new(event: &'w mut E, trigger: ObserverTrigger) -> Self {
|
||||
Self {
|
||||
event,
|
||||
trigger,
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the event type of this trigger.
|
||||
pub fn event_type(&self) -> ComponentId {
|
||||
self.trigger.event_type
|
||||
}
|
||||
|
||||
/// Returns a reference to the triggered event.
|
||||
pub fn event(&self) -> &E {
|
||||
self.event
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the triggered event.
|
||||
pub fn event_mut(&mut self) -> &mut E {
|
||||
self.event
|
||||
}
|
||||
|
||||
/// Returns a pointer to the triggered event.
|
||||
pub fn event_ptr(&self) -> Ptr {
|
||||
Ptr::from(&self.event)
|
||||
}
|
||||
|
||||
/// Returns the entity that triggered the observer, could be [`Entity::PLACEHOLDER`].
|
||||
pub fn entity(&self) -> Entity {
|
||||
self.trigger.entity
|
||||
}
|
||||
}
|
||||
|
||||
/// A description of what an [`Observer`] observes.
|
||||
#[derive(Default, Clone)]
|
||||
pub struct ObserverDescriptor {
|
||||
/// The events the observer is watching.
|
||||
events: Vec<ComponentId>,
|
||||
|
||||
/// The components the observer is watching.
|
||||
components: Vec<ComponentId>,
|
||||
|
||||
/// The entities the observer is watching.
|
||||
entities: Vec<Entity>,
|
||||
}
|
||||
|
||||
impl ObserverDescriptor {
|
||||
/// Add the given `events` to the descriptor.
|
||||
/// # Safety
|
||||
/// The type of each [`ComponentId`] in `events` _must_ match the actual value
|
||||
/// of the event passed into the observer.
|
||||
pub unsafe fn with_events(mut self, events: Vec<ComponentId>) -> Self {
|
||||
self.events = events;
|
||||
self
|
||||
}
|
||||
|
||||
/// Add the given `components` to the descriptor.
|
||||
pub fn with_components(mut self, components: Vec<ComponentId>) -> Self {
|
||||
self.components = components;
|
||||
self
|
||||
}
|
||||
|
||||
/// Add the given `entities` to the descriptor.
|
||||
pub fn with_entities(mut self, entities: Vec<Entity>) -> Self {
|
||||
self.entities = entities;
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn merge(&mut self, descriptor: &ObserverDescriptor) {
|
||||
self.events.extend(descriptor.events.iter().copied());
|
||||
self.components
|
||||
.extend(descriptor.components.iter().copied());
|
||||
self.entities.extend(descriptor.entities.iter().copied());
|
||||
}
|
||||
}
|
||||
|
||||
/// Event trigger metadata for a given [`Observer`],
|
||||
#[derive(Debug)]
|
||||
pub struct ObserverTrigger {
|
||||
/// The [`Entity`] of the observer handling the trigger.
|
||||
pub observer: Entity,
|
||||
|
||||
/// The [`ComponentId`] the trigger targeted.
|
||||
pub event_type: ComponentId,
|
||||
|
||||
/// The entity the trigger targeted.
|
||||
pub entity: Entity,
|
||||
}
|
||||
|
||||
// Map between an observer entity and its runner
|
||||
type ObserverMap = EntityHashMap<Entity, ObserverRunner>;
|
||||
|
||||
/// Collection of [`ObserverRunner`] for [`Observer`] registered to a particular trigger targeted at a specific component.
|
||||
#[derive(Default, Debug)]
|
||||
pub struct CachedComponentObservers {
|
||||
// Observers listening to triggers targeting this component
|
||||
map: ObserverMap,
|
||||
// Observers listening to triggers targeting this component on a specific entity
|
||||
entity_map: EntityHashMap<Entity, ObserverMap>,
|
||||
}
|
||||
|
||||
/// Collection of [`ObserverRunner`] for [`Observer`] registered to a particular trigger.
|
||||
#[derive(Default, Debug)]
|
||||
pub struct CachedObservers {
|
||||
// Observers listening for any time this trigger is fired
|
||||
map: ObserverMap,
|
||||
// Observers listening for this trigger fired at a specific component
|
||||
component_observers: HashMap<ComponentId, CachedComponentObservers>,
|
||||
// Observers listening for this trigger fired at a specific entity
|
||||
entity_observers: EntityHashMap<Entity, ObserverMap>,
|
||||
}
|
||||
|
||||
/// Metadata for observers. Stores a cache mapping trigger ids to the registered observers.
|
||||
#[derive(Default, Debug)]
|
||||
pub struct Observers {
|
||||
// Cached ECS observers to save a lookup most common triggers.
|
||||
on_add: CachedObservers,
|
||||
on_insert: CachedObservers,
|
||||
on_remove: CachedObservers,
|
||||
// Map from trigger type to set of observers
|
||||
cache: HashMap<ComponentId, CachedObservers>,
|
||||
}
|
||||
|
||||
impl Observers {
|
||||
pub(crate) fn get_observers(&mut self, event_type: ComponentId) -> &mut CachedObservers {
|
||||
match event_type {
|
||||
ON_ADD => &mut self.on_add,
|
||||
ON_INSERT => &mut self.on_insert,
|
||||
ON_REMOVE => &mut self.on_remove,
|
||||
_ => self.cache.entry(event_type).or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn try_get_observers(&self, event_type: ComponentId) -> Option<&CachedObservers> {
|
||||
match event_type {
|
||||
ON_ADD => Some(&self.on_add),
|
||||
ON_INSERT => Some(&self.on_insert),
|
||||
ON_REMOVE => Some(&self.on_remove),
|
||||
_ => self.cache.get(&event_type),
|
||||
}
|
||||
}
|
||||
|
||||
/// This will run the observers of the given `event_type`, targeting the given `entity` and `components`.
|
||||
pub(crate) fn invoke<T>(
|
||||
mut world: DeferredWorld,
|
||||
event_type: ComponentId,
|
||||
entity: Entity,
|
||||
components: impl Iterator<Item = ComponentId>,
|
||||
data: &mut T,
|
||||
) {
|
||||
// SAFETY: You cannot get a mutable reference to `observers` from `DeferredWorld`
|
||||
let (mut world, observers) = unsafe {
|
||||
let world = world.as_unsafe_world_cell();
|
||||
// SAFETY: There are no outstanding world references
|
||||
world.increment_trigger_id();
|
||||
let observers = world.observers();
|
||||
let Some(observers) = observers.try_get_observers(event_type) else {
|
||||
return;
|
||||
};
|
||||
// SAFETY: The only outstanding reference to world is `observers`
|
||||
(world.into_deferred(), observers)
|
||||
};
|
||||
|
||||
let mut trigger_observer = |(&observer, runner): (&Entity, &ObserverRunner)| {
|
||||
(runner)(
|
||||
world.reborrow(),
|
||||
ObserverTrigger {
|
||||
observer,
|
||||
event_type,
|
||||
entity,
|
||||
},
|
||||
data.into(),
|
||||
);
|
||||
};
|
||||
|
||||
// Trigger observers listening for any kind of this trigger
|
||||
observers.map.iter().for_each(&mut trigger_observer);
|
||||
|
||||
// Trigger entity observers listening for this kind of trigger
|
||||
if entity != Entity::PLACEHOLDER {
|
||||
if let Some(map) = observers.entity_observers.get(&entity) {
|
||||
map.iter().for_each(&mut trigger_observer);
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger observers listening to this trigger targeting a specific component
|
||||
components.for_each(|id| {
|
||||
if let Some(component_observers) = observers.component_observers.get(&id) {
|
||||
component_observers
|
||||
.map
|
||||
.iter()
|
||||
.for_each(&mut trigger_observer);
|
||||
|
||||
if entity != Entity::PLACEHOLDER {
|
||||
if let Some(map) = component_observers.entity_map.get(&entity) {
|
||||
map.iter().for_each(&mut trigger_observer);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn is_archetype_cached(event_type: ComponentId) -> Option<ArchetypeFlags> {
|
||||
match event_type {
|
||||
ON_ADD => Some(ArchetypeFlags::ON_ADD_OBSERVER),
|
||||
ON_INSERT => Some(ArchetypeFlags::ON_INSERT_OBSERVER),
|
||||
ON_REMOVE => Some(ArchetypeFlags::ON_REMOVE_OBSERVER),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn update_archetype_flags(
|
||||
&self,
|
||||
component_id: ComponentId,
|
||||
flags: &mut ArchetypeFlags,
|
||||
) {
|
||||
if self.on_add.component_observers.contains_key(&component_id) {
|
||||
flags.insert(ArchetypeFlags::ON_ADD_OBSERVER);
|
||||
}
|
||||
if self
|
||||
.on_insert
|
||||
.component_observers
|
||||
.contains_key(&component_id)
|
||||
{
|
||||
flags.insert(ArchetypeFlags::ON_INSERT_OBSERVER);
|
||||
}
|
||||
if self
|
||||
.on_remove
|
||||
.component_observers
|
||||
.contains_key(&component_id)
|
||||
{
|
||||
flags.insert(ArchetypeFlags::ON_REMOVE_OBSERVER);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl World {
|
||||
/// Spawn a "global" [`Observer`] and returns it's [`Entity`].
|
||||
pub fn observe<E: Event, B: Bundle, M>(
|
||||
&mut self,
|
||||
system: impl IntoObserverSystem<E, B, M>,
|
||||
) -> EntityWorldMut {
|
||||
self.spawn(Observer::new(system))
|
||||
}
|
||||
|
||||
/// Triggers the given `event`, which will run any observers watching for it.
|
||||
pub fn trigger(&mut self, event: impl Event) {
|
||||
TriggerEvent { event, targets: () }.apply(self);
|
||||
}
|
||||
|
||||
/// Triggers the given `event` for the given `targets`, which will run any observers watching for it.
|
||||
pub fn trigger_targets(&mut self, event: impl Event, targets: impl TriggerTargets) {
|
||||
TriggerEvent { event, targets }.apply(self);
|
||||
}
|
||||
|
||||
/// Register an observer to the cache, called when an observer is created
|
||||
pub(crate) fn register_observer(&mut self, observer_entity: Entity) {
|
||||
// SAFETY: References do not alias.
|
||||
let (observer_state, archetypes, observers) = unsafe {
|
||||
let observer_state: *const ObserverState =
|
||||
self.get::<ObserverState>(observer_entity).unwrap();
|
||||
// Populate ObservedBy for each observed entity.
|
||||
for watched_entity in &(*observer_state).descriptor.entities {
|
||||
let mut entity_mut = self.entity_mut(*watched_entity);
|
||||
let mut observed_by = entity_mut.entry::<ObservedBy>().or_default();
|
||||
observed_by.0.push(observer_entity);
|
||||
}
|
||||
(&*observer_state, &mut self.archetypes, &mut self.observers)
|
||||
};
|
||||
let descriptor = &observer_state.descriptor;
|
||||
|
||||
for &event_type in &descriptor.events {
|
||||
let cache = observers.get_observers(event_type);
|
||||
|
||||
if descriptor.components.is_empty() && descriptor.entities.is_empty() {
|
||||
cache.map.insert(observer_entity, observer_state.runner);
|
||||
} else if descriptor.components.is_empty() {
|
||||
// Observer is not targeting any components so register it as an entity observer
|
||||
for &watched_entity in &observer_state.descriptor.entities {
|
||||
let map = cache.entity_observers.entry(watched_entity).or_default();
|
||||
map.insert(observer_entity, observer_state.runner);
|
||||
}
|
||||
} else {
|
||||
// Register observer for each watched component
|
||||
for &component in &descriptor.components {
|
||||
let observers =
|
||||
cache
|
||||
.component_observers
|
||||
.entry(component)
|
||||
.or_insert_with(|| {
|
||||
if let Some(flag) = Observers::is_archetype_cached(event_type) {
|
||||
archetypes.update_flags(component, flag, true);
|
||||
}
|
||||
CachedComponentObservers::default()
|
||||
});
|
||||
if descriptor.entities.is_empty() {
|
||||
// Register for all triggers targeting the component
|
||||
observers.map.insert(observer_entity, observer_state.runner);
|
||||
} else {
|
||||
// Register for each watched entity
|
||||
for &watched_entity in &descriptor.entities {
|
||||
let map = observers.entity_map.entry(watched_entity).or_default();
|
||||
map.insert(observer_entity, observer_state.runner);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove the observer from the cache, called when an observer gets despawned
|
||||
pub(crate) fn unregister_observer(&mut self, entity: Entity, descriptor: ObserverDescriptor) {
|
||||
let archetypes = &mut self.archetypes;
|
||||
let observers = &mut self.observers;
|
||||
|
||||
for &event_type in &descriptor.events {
|
||||
let cache = observers.get_observers(event_type);
|
||||
if descriptor.components.is_empty() && descriptor.entities.is_empty() {
|
||||
cache.map.remove(&entity);
|
||||
} else if descriptor.components.is_empty() {
|
||||
for watched_entity in &descriptor.entities {
|
||||
// This check should be unnecessary since this observer hasn't been unregistered yet
|
||||
let Some(observers) = cache.entity_observers.get_mut(watched_entity) else {
|
||||
continue;
|
||||
};
|
||||
observers.remove(&entity);
|
||||
if observers.is_empty() {
|
||||
cache.entity_observers.remove(watched_entity);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for component in &descriptor.components {
|
||||
let Some(observers) = cache.component_observers.get_mut(component) else {
|
||||
continue;
|
||||
};
|
||||
if descriptor.entities.is_empty() {
|
||||
observers.map.remove(&entity);
|
||||
} else {
|
||||
for watched_entity in &descriptor.entities {
|
||||
let Some(map) = observers.entity_map.get_mut(watched_entity) else {
|
||||
continue;
|
||||
};
|
||||
map.remove(&entity);
|
||||
if map.is_empty() {
|
||||
observers.entity_map.remove(watched_entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if observers.map.is_empty() && observers.entity_map.is_empty() {
|
||||
cache.component_observers.remove(component);
|
||||
if let Some(flag) = Observers::is_archetype_cached(event_type) {
|
||||
archetypes.update_flags(*component, flag, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use bevy_ptr::OwningPtr;
|
||||
|
||||
use crate as bevy_ecs;
|
||||
use crate::observer::{EmitDynamicTrigger, Observer, ObserverDescriptor, ObserverState};
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Component)]
|
||||
struct A;
|
||||
|
||||
#[derive(Component)]
|
||||
struct B;
|
||||
|
||||
#[derive(Component)]
|
||||
struct C;
|
||||
|
||||
#[derive(Component)]
|
||||
#[component(storage = "SparseSet")]
|
||||
struct S;
|
||||
|
||||
#[derive(Event)]
|
||||
struct EventA;
|
||||
|
||||
#[derive(Resource, Default)]
|
||||
struct R(usize);
|
||||
|
||||
impl R {
|
||||
#[track_caller]
|
||||
fn assert_order(&mut self, count: usize) {
|
||||
assert_eq!(count, self.0);
|
||||
self.0 += 1;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn observer_order_spawn_despawn() {
|
||||
let mut world = World::new();
|
||||
world.init_resource::<R>();
|
||||
|
||||
world.observe(|_: Trigger<OnAdd, A>, mut res: ResMut<R>| res.assert_order(0));
|
||||
world.observe(|_: Trigger<OnInsert, A>, mut res: ResMut<R>| res.assert_order(1));
|
||||
world.observe(|_: Trigger<OnRemove, A>, mut res: ResMut<R>| res.assert_order(2));
|
||||
|
||||
let entity = world.spawn(A).id();
|
||||
world.despawn(entity);
|
||||
assert_eq!(3, world.resource::<R>().0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn observer_order_insert_remove() {
|
||||
let mut world = World::new();
|
||||
world.init_resource::<R>();
|
||||
|
||||
world.observe(|_: Trigger<OnAdd, A>, mut res: ResMut<R>| res.assert_order(0));
|
||||
world.observe(|_: Trigger<OnInsert, A>, mut res: ResMut<R>| res.assert_order(1));
|
||||
world.observe(|_: Trigger<OnRemove, A>, mut res: ResMut<R>| res.assert_order(2));
|
||||
|
||||
let mut entity = world.spawn_empty();
|
||||
entity.insert(A);
|
||||
entity.remove::<A>();
|
||||
entity.flush();
|
||||
assert_eq!(3, world.resource::<R>().0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn observer_order_insert_remove_sparse() {
|
||||
let mut world = World::new();
|
||||
world.init_resource::<R>();
|
||||
|
||||
world.observe(|_: Trigger<OnAdd, S>, mut res: ResMut<R>| res.assert_order(0));
|
||||
world.observe(|_: Trigger<OnInsert, S>, mut res: ResMut<R>| res.assert_order(1));
|
||||
world.observe(|_: Trigger<OnRemove, S>, mut res: ResMut<R>| res.assert_order(2));
|
||||
|
||||
let mut entity = world.spawn_empty();
|
||||
entity.insert(S);
|
||||
entity.remove::<S>();
|
||||
entity.flush();
|
||||
assert_eq!(3, world.resource::<R>().0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn observer_order_recursive() {
|
||||
let mut world = World::new();
|
||||
world.init_resource::<R>();
|
||||
world.observe(
|
||||
|obs: Trigger<OnAdd, A>, mut res: ResMut<R>, mut commands: Commands| {
|
||||
res.assert_order(0);
|
||||
commands.entity(obs.entity()).insert(B);
|
||||
},
|
||||
);
|
||||
world.observe(
|
||||
|obs: Trigger<OnRemove, A>, mut res: ResMut<R>, mut commands: Commands| {
|
||||
res.assert_order(2);
|
||||
commands.entity(obs.entity()).remove::<B>();
|
||||
},
|
||||
);
|
||||
|
||||
world.observe(
|
||||
|obs: Trigger<OnAdd, B>, mut res: ResMut<R>, mut commands: Commands| {
|
||||
res.assert_order(1);
|
||||
commands.entity(obs.entity()).remove::<A>();
|
||||
},
|
||||
);
|
||||
world.observe(|_: Trigger<OnRemove, B>, mut res: ResMut<R>| {
|
||||
res.assert_order(3);
|
||||
});
|
||||
|
||||
let entity = world.spawn(A).flush();
|
||||
let entity = world.get_entity(entity).unwrap();
|
||||
assert!(!entity.contains::<A>());
|
||||
assert!(!entity.contains::<B>());
|
||||
assert_eq!(4, world.resource::<R>().0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn observer_multiple_listeners() {
|
||||
let mut world = World::new();
|
||||
world.init_resource::<R>();
|
||||
|
||||
world.observe(|_: Trigger<OnAdd, A>, mut res: ResMut<R>| res.0 += 1);
|
||||
world.observe(|_: Trigger<OnAdd, A>, mut res: ResMut<R>| res.0 += 1);
|
||||
|
||||
world.spawn(A).flush();
|
||||
assert_eq!(2, world.resource::<R>().0);
|
||||
// Our A entity plus our two observers
|
||||
assert_eq!(world.entities().len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn observer_multiple_events() {
|
||||
let mut world = World::new();
|
||||
world.init_resource::<R>();
|
||||
let on_remove = world.init_component::<OnRemove>();
|
||||
world.spawn(
|
||||
// SAFETY: OnAdd and OnRemove are both unit types, so this is safe
|
||||
unsafe {
|
||||
Observer::new(|_: Trigger<OnAdd, A>, mut res: ResMut<R>| res.0 += 1)
|
||||
.with_event(on_remove)
|
||||
},
|
||||
);
|
||||
|
||||
let entity = world.spawn(A).id();
|
||||
world.despawn(entity);
|
||||
assert_eq!(2, world.resource::<R>().0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn observer_multiple_components() {
|
||||
let mut world = World::new();
|
||||
world.init_resource::<R>();
|
||||
world.init_component::<A>();
|
||||
world.init_component::<B>();
|
||||
|
||||
world.observe(|_: Trigger<OnAdd, (A, B)>, mut res: ResMut<R>| res.0 += 1);
|
||||
|
||||
let entity = world.spawn(A).id();
|
||||
world.entity_mut(entity).insert(B);
|
||||
world.flush();
|
||||
assert_eq!(2, world.resource::<R>().0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn observer_despawn() {
|
||||
let mut world = World::new();
|
||||
world.init_resource::<R>();
|
||||
|
||||
let observer = world
|
||||
.observe(|_: Trigger<OnAdd, A>| panic!("Observer triggered after being despawned."))
|
||||
.id();
|
||||
world.despawn(observer);
|
||||
world.spawn(A).flush();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn observer_multiple_matches() {
|
||||
let mut world = World::new();
|
||||
world.init_resource::<R>();
|
||||
|
||||
world.observe(|_: Trigger<OnAdd, (A, B)>, mut res: ResMut<R>| res.0 += 1);
|
||||
|
||||
world.spawn((A, B)).flush();
|
||||
assert_eq!(1, world.resource::<R>().0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn observer_no_target() {
|
||||
let mut world = World::new();
|
||||
world.init_resource::<R>();
|
||||
|
||||
world
|
||||
.spawn_empty()
|
||||
.observe(|_: Trigger<EventA>| panic!("Trigger routed to non-targeted entity."));
|
||||
world.observe(move |obs: Trigger<EventA>, mut res: ResMut<R>| {
|
||||
assert_eq!(obs.entity(), Entity::PLACEHOLDER);
|
||||
res.0 += 1;
|
||||
});
|
||||
|
||||
// TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut
|
||||
// and therefore does not automatically flush.
|
||||
world.flush();
|
||||
world.trigger(EventA);
|
||||
world.flush();
|
||||
assert_eq!(1, world.resource::<R>().0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn observer_entity_routing() {
|
||||
let mut world = World::new();
|
||||
world.init_resource::<R>();
|
||||
|
||||
world
|
||||
.spawn_empty()
|
||||
.observe(|_: Trigger<EventA>| panic!("Trigger routed to non-targeted entity."));
|
||||
let entity = world
|
||||
.spawn_empty()
|
||||
.observe(|_: Trigger<EventA>, mut res: ResMut<R>| res.0 += 1)
|
||||
.id();
|
||||
world.observe(move |obs: Trigger<EventA>, mut res: ResMut<R>| {
|
||||
assert_eq!(obs.entity(), entity);
|
||||
res.0 += 1;
|
||||
});
|
||||
|
||||
// TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut
|
||||
// and therefore does not automatically flush.
|
||||
world.flush();
|
||||
world.trigger_targets(EventA, entity);
|
||||
world.flush();
|
||||
assert_eq!(2, world.resource::<R>().0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn observer_dynamic_component() {
|
||||
let mut world = World::new();
|
||||
world.init_resource::<R>();
|
||||
|
||||
let component_id = world.init_component::<A>();
|
||||
world.spawn(
|
||||
Observer::new(|_: Trigger<OnAdd>, mut res: ResMut<R>| res.0 += 1)
|
||||
.with_component(component_id),
|
||||
);
|
||||
|
||||
let mut entity = world.spawn_empty();
|
||||
OwningPtr::make(A, |ptr| {
|
||||
// SAFETY: we registered `component_id` above.
|
||||
unsafe { entity.insert_by_id(component_id, ptr) };
|
||||
});
|
||||
let entity = entity.flush();
|
||||
|
||||
world.trigger_targets(EventA, entity);
|
||||
world.flush();
|
||||
assert_eq!(1, world.resource::<R>().0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn observer_dynamic_trigger() {
|
||||
let mut world = World::new();
|
||||
world.init_resource::<R>();
|
||||
let event_a = world.init_component::<EventA>();
|
||||
|
||||
world.spawn(ObserverState {
|
||||
// SAFETY: we registered `event_a` above and it matches the type of TriggerA
|
||||
descriptor: unsafe { ObserverDescriptor::default().with_events(vec![event_a]) },
|
||||
runner: |mut world, _trigger, _ptr| {
|
||||
world.resource_mut::<R>().0 += 1;
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
world.commands().add(
|
||||
// SAFETY: we registered `event_a` above and it matches the type of TriggerA
|
||||
unsafe { EmitDynamicTrigger::new_with_id(event_a, EventA, ()) },
|
||||
);
|
||||
world.flush();
|
||||
assert_eq!(1, world.resource::<R>().0);
|
||||
}
|
||||
}
|
412
crates/bevy_ecs/src/observer/runner.rs
Normal file
412
crates/bevy_ecs/src/observer/runner.rs
Normal file
@ -0,0 +1,412 @@
|
||||
use crate::{
|
||||
component::{ComponentHooks, ComponentId, StorageType},
|
||||
observer::{ObserverDescriptor, ObserverTrigger},
|
||||
prelude::*,
|
||||
query::DebugCheckedUnwrap,
|
||||
system::{IntoObserverSystem, ObserverSystem},
|
||||
world::DeferredWorld,
|
||||
};
|
||||
use bevy_ptr::PtrMut;
|
||||
|
||||
/// Contains [`Observer`] information. This defines how a given observer behaves. It is the
|
||||
/// "source of truth" for a given observer entity's behavior.
|
||||
pub struct ObserverState {
|
||||
pub(crate) descriptor: ObserverDescriptor,
|
||||
pub(crate) runner: ObserverRunner,
|
||||
pub(crate) last_trigger_id: u32,
|
||||
pub(crate) despawned_watched_entities: u32,
|
||||
}
|
||||
|
||||
impl Default for ObserverState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
runner: |_, _, _| {},
|
||||
last_trigger_id: 0,
|
||||
despawned_watched_entities: 0,
|
||||
descriptor: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ObserverState {
|
||||
/// Observe the given `event`. This will cause the [`Observer`] to run whenever an event with the given [`ComponentId`]
|
||||
/// is triggered.
|
||||
pub fn with_event(mut self, event: ComponentId) -> Self {
|
||||
self.descriptor.events.push(event);
|
||||
self
|
||||
}
|
||||
|
||||
/// Observe the given event list. This will cause the [`Observer`] to run whenever an event with any of the given [`ComponentId`]s
|
||||
/// is triggered.
|
||||
pub fn with_events(mut self, events: impl IntoIterator<Item = ComponentId>) -> Self {
|
||||
self.descriptor.events.extend(events);
|
||||
self
|
||||
}
|
||||
|
||||
/// Observe the given [`Entity`] list. This will cause the [`Observer`] to run whenever the [`Event`] is triggered
|
||||
/// for any [`Entity`] target in the list.
|
||||
pub fn with_entities(mut self, entities: impl IntoIterator<Item = Entity>) -> Self {
|
||||
self.descriptor.entities.extend(entities);
|
||||
self
|
||||
}
|
||||
|
||||
/// Observe the given [`ComponentId`] list. This will cause the [`Observer`] to run whenever the [`Event`] is triggered
|
||||
/// for any [`ComponentId`] target in the list.
|
||||
pub fn with_components(mut self, components: impl IntoIterator<Item = ComponentId>) -> Self {
|
||||
self.descriptor.components.extend(components);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for ObserverState {
|
||||
const STORAGE_TYPE: StorageType = StorageType::SparseSet;
|
||||
|
||||
fn register_component_hooks(hooks: &mut ComponentHooks) {
|
||||
hooks.on_add(|mut world, entity, _| {
|
||||
world.commands().add(move |world: &mut World| {
|
||||
world.register_observer(entity);
|
||||
});
|
||||
});
|
||||
hooks.on_remove(|mut world, entity, _| {
|
||||
let descriptor = std::mem::take(
|
||||
&mut world
|
||||
.entity_mut(entity)
|
||||
.get_mut::<ObserverState>()
|
||||
.unwrap()
|
||||
.as_mut()
|
||||
.descriptor,
|
||||
);
|
||||
world.commands().add(move |world: &mut World| {
|
||||
world.unregister_observer(entity, descriptor);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Type for function that is run when an observer is triggered.
|
||||
/// Typically refers to the default runner that runs the system stored in the associated [`ObserverSystemComponent`],
|
||||
/// but can be overridden for custom behaviour.
|
||||
pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut);
|
||||
|
||||
/// An [`Observer`] system. Add this [`Component`] to an [`Entity`] to turn it into an "observer".
|
||||
///
|
||||
/// Observers listen for a "trigger" of a specific [`Event`]. Events are triggered by calling [`World::trigger`] or [`World::trigger_targets`].
|
||||
///
|
||||
/// Note that "buffered" events sent using [`EventReader`] and [`EventWriter`] are _not_ automatically triggered. They must be triggered at a specific
|
||||
/// point in the schedule.
|
||||
///
|
||||
/// # Usage
|
||||
///
|
||||
/// The simplest usage
|
||||
/// of the observer pattern looks like this:
|
||||
///
|
||||
/// ```
|
||||
/// # use bevy_ecs::prelude::*;
|
||||
/// # let mut world = World::default();
|
||||
/// #[derive(Event)]
|
||||
/// struct Speak {
|
||||
/// message: String,
|
||||
/// }
|
||||
///
|
||||
/// world.observe(|trigger: Trigger<Speak>| {
|
||||
/// println!("{}", trigger.event().message);
|
||||
/// });
|
||||
///
|
||||
/// // Observers currently require a flush() to be registered. In the context of schedules,
|
||||
/// // this will generally be done for you.
|
||||
/// world.flush();
|
||||
///
|
||||
/// world.trigger(Speak {
|
||||
/// message: "Hello!".into(),
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// Notice that we used [`World::observe`]. This is just a shorthand for spawning an [`Observer`] manually:
|
||||
///
|
||||
/// ```
|
||||
/// # use bevy_ecs::prelude::*;
|
||||
/// # let mut world = World::default();
|
||||
/// # #[derive(Event)]
|
||||
/// # struct Speak;
|
||||
/// // These are functionally the same:
|
||||
/// world.observe(|trigger: Trigger<Speak>| {});
|
||||
/// world.spawn(Observer::new(|trigger: Trigger<Speak>| {}));
|
||||
/// ```
|
||||
///
|
||||
/// Observers are systems. They can access arbitrary [`World`] data by adding [`SystemParam`]s:
|
||||
///
|
||||
/// ```
|
||||
/// # use bevy_ecs::prelude::*;
|
||||
/// # let mut world = World::default();
|
||||
/// # #[derive(Event)]
|
||||
/// # struct PrintNames;
|
||||
/// # #[derive(Component, Debug)]
|
||||
/// # struct Name;
|
||||
/// world.observe(|trigger: Trigger<PrintNames>, names: Query<&Name>| {
|
||||
/// for name in &names {
|
||||
/// println!("{name:?}");
|
||||
/// }
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// Note that [`Trigger`] must always be the first parameter.
|
||||
///
|
||||
/// You can also add [`Commands`], which means you can spawn new entities, insert new components, etc:
|
||||
///
|
||||
/// ```
|
||||
/// # use bevy_ecs::prelude::*;
|
||||
/// # let mut world = World::default();
|
||||
/// # #[derive(Event)]
|
||||
/// # struct SpawnThing;
|
||||
/// # #[derive(Component, Debug)]
|
||||
/// # struct Thing;
|
||||
/// world.observe(|trigger: Trigger<SpawnThing>, mut commands: Commands| {
|
||||
/// commands.spawn(Thing);
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// Observers can also trigger new events:
|
||||
///
|
||||
/// ```
|
||||
/// # use bevy_ecs::prelude::*;
|
||||
/// # let mut world = World::default();
|
||||
/// # #[derive(Event)]
|
||||
/// # struct A;
|
||||
/// # #[derive(Event)]
|
||||
/// # struct B;
|
||||
/// world.observe(|trigger: Trigger<A>, mut commands: Commands| {
|
||||
/// commands.trigger(B);
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// When the commands are flushed (including these "nested triggers") they will be
|
||||
/// recursively evaluated until there are no commands left, meaning nested triggers all
|
||||
/// evaluate at the same time!
|
||||
///
|
||||
/// Events can be triggered for entities, which will be passed to the [`Observer`]:
|
||||
///
|
||||
/// ```
|
||||
/// # use bevy_ecs::prelude::*;
|
||||
/// # let mut world = World::default();
|
||||
/// # let entity = world.spawn_empty().id();
|
||||
/// #[derive(Event)]
|
||||
/// struct Explode;
|
||||
///
|
||||
/// world.observe(|trigger: Trigger<Explode>, mut commands: Commands| {
|
||||
/// println!("Entity {:?} goes BOOM!", trigger.entity());
|
||||
/// commands.entity(trigger.entity()).despawn();
|
||||
/// });
|
||||
///
|
||||
/// world.flush();
|
||||
///
|
||||
/// world.trigger_targets(Explode, entity);
|
||||
/// ```
|
||||
///
|
||||
/// You can trigger multiple entities at once:
|
||||
///
|
||||
/// ```
|
||||
/// # use bevy_ecs::prelude::*;
|
||||
/// # let mut world = World::default();
|
||||
/// # let e1 = world.spawn_empty().id();
|
||||
/// # let e2 = world.spawn_empty().id();
|
||||
/// # #[derive(Event)]
|
||||
/// # struct Explode;
|
||||
/// world.trigger_targets(Explode, [e1, e2]);
|
||||
/// ```
|
||||
///
|
||||
/// Observers can also watch _specific_ entities, which enables you to assign entity-specific logic:
|
||||
///
|
||||
/// ```
|
||||
/// # use bevy_ecs::prelude::*;
|
||||
/// # #[derive(Component, Debug)]
|
||||
/// # struct Name(String);
|
||||
/// # let mut world = World::default();
|
||||
/// # let e1 = world.spawn_empty().id();
|
||||
/// # let e2 = world.spawn_empty().id();
|
||||
/// # #[derive(Event)]
|
||||
/// # struct Explode;
|
||||
/// world.entity_mut(e1).observe(|trigger: Trigger<Explode>, mut commands: Commands| {
|
||||
/// println!("Boom!");
|
||||
/// commands.entity(trigger.entity()).despawn();
|
||||
/// });
|
||||
///
|
||||
/// world.entity_mut(e2).observe(|trigger: Trigger<Explode>, mut commands: Commands| {
|
||||
/// println!("The explosion fizzles! This entity is immune!");
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// If all entities watched by a given [`Observer`] are despawned, the [`Observer`] entity will also be despawned.
|
||||
/// This protects against observer "garbage" building up over time.
|
||||
///
|
||||
/// The examples above calling [`EntityWorldMut::observe`] to add entity-specific observer logic are (once again)
|
||||
/// just shorthand for spawning an [`Observer`] directly:
|
||||
///
|
||||
/// ```
|
||||
/// # use bevy_ecs::prelude::*;
|
||||
/// # let mut world = World::default();
|
||||
/// # let entity = world.spawn_empty().id();
|
||||
/// # #[derive(Event)]
|
||||
/// # struct Explode;
|
||||
/// let mut observer = Observer::new(|trigger: Trigger<Explode>| {});
|
||||
/// observer.watch_entity(entity);
|
||||
/// world.spawn(observer);
|
||||
/// ```
|
||||
///
|
||||
/// Note that the [`Observer`] component is not added to the entity it is observing. Observers should always be their own entities!
|
||||
///
|
||||
/// You can call [`Observer::watch_entity`] more than once, which allows you to watch multiple entities with the same [`Observer`].
|
||||
///
|
||||
/// When first added, [`Observer`] will also create an [`ObserverState`] component, which registers the observer with the [`World`] and
|
||||
/// serves as the "source of truth" of the observer.
|
||||
///
|
||||
/// [`SystemParam`]: crate::system::SystemParam
|
||||
pub struct Observer<T: 'static, B: Bundle> {
|
||||
system: BoxedObserverSystem<T, B>,
|
||||
descriptor: ObserverDescriptor,
|
||||
}
|
||||
|
||||
impl<E: Event, B: Bundle> Observer<E, B> {
|
||||
/// Creates a new [`Observer`], which defaults to a "global" observer. This means it will run whenever the event `E` is triggered
|
||||
/// for _any_ entity (or no entity).
|
||||
pub fn new<M>(system: impl IntoObserverSystem<E, B, M>) -> Self {
|
||||
Self {
|
||||
system: Box::new(IntoObserverSystem::into_system(system)),
|
||||
descriptor: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Observe the given `entity`. This will cause the [`Observer`] to run whenever the [`Event`] is triggered
|
||||
/// for the `entity`.
|
||||
pub fn with_entity(mut self, entity: Entity) -> Self {
|
||||
self.descriptor.entities.push(entity);
|
||||
self
|
||||
}
|
||||
|
||||
/// Observe the given `entity`. This will cause the [`Observer`] to run whenever the [`Event`] is triggered
|
||||
/// for the `entity`.
|
||||
/// Note that if this is called _after_ an [`Observer`] is spawned, it will produce no effects.
|
||||
pub fn watch_entity(&mut self, entity: Entity) {
|
||||
self.descriptor.entities.push(entity);
|
||||
}
|
||||
|
||||
/// Observe the given `component`. This will cause the [`Observer`] to run whenever the [`Event`] is triggered
|
||||
/// with the given component target.
|
||||
pub fn with_component(mut self, component: ComponentId) -> Self {
|
||||
self.descriptor.components.push(component);
|
||||
self
|
||||
}
|
||||
|
||||
/// Observe the given `event`. This will cause the [`Observer`] to run whenever an event with the given [`ComponentId`]
|
||||
/// is triggered.
|
||||
/// # Safety
|
||||
/// The type of the `event` [`ComponentId`] _must_ match the actual value
|
||||
/// of the event passed into the observer system.
|
||||
pub unsafe fn with_event(mut self, event: ComponentId) -> Self {
|
||||
self.descriptor.events.push(event);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Event, B: Bundle> Component for Observer<E, B> {
|
||||
const STORAGE_TYPE: StorageType = StorageType::SparseSet;
|
||||
fn register_component_hooks(hooks: &mut ComponentHooks) {
|
||||
hooks.on_add(|mut world, entity, _| {
|
||||
world.commands().add(move |world: &mut World| {
|
||||
let event_type = world.init_component::<E>();
|
||||
let mut components = Vec::new();
|
||||
B::component_ids(&mut world.components, &mut world.storages, &mut |id| {
|
||||
components.push(id);
|
||||
});
|
||||
let mut descriptor = ObserverDescriptor {
|
||||
events: vec![event_type],
|
||||
components,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Initialize System
|
||||
let system: *mut dyn ObserverSystem<E, B> =
|
||||
if let Some(mut observe) = world.get_mut::<Self>(entity) {
|
||||
descriptor.merge(&observe.descriptor);
|
||||
&mut *observe.system
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
// SAFETY: World reference is exclusive and initialize does not touch system, so references do not alias
|
||||
unsafe {
|
||||
(*system).initialize(world);
|
||||
}
|
||||
|
||||
{
|
||||
let mut entity = world.entity_mut(entity);
|
||||
if let crate::world::Entry::Vacant(entry) = entity.entry::<ObserverState>() {
|
||||
entry.insert(ObserverState {
|
||||
descriptor,
|
||||
runner: observer_system_runner::<E, B>,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Equivalent to [`BoxedSystem`](crate::system::BoxedSystem) for [`ObserverSystem`].
|
||||
pub type BoxedObserverSystem<E = (), B = ()> = Box<dyn ObserverSystem<E, B>>;
|
||||
|
||||
fn observer_system_runner<E: Event, B: Bundle>(
|
||||
mut world: DeferredWorld,
|
||||
observer_trigger: ObserverTrigger,
|
||||
ptr: PtrMut,
|
||||
) {
|
||||
let world = world.as_unsafe_world_cell();
|
||||
// SAFETY: Observer was triggered so must still exist in world
|
||||
let observer_cell = unsafe {
|
||||
world
|
||||
.get_entity(observer_trigger.observer)
|
||||
.debug_checked_unwrap()
|
||||
};
|
||||
// SAFETY: Observer was triggered so must have an `ObserverState`
|
||||
let mut state = unsafe {
|
||||
observer_cell
|
||||
.get_mut::<ObserverState>()
|
||||
.debug_checked_unwrap()
|
||||
};
|
||||
|
||||
// TODO: Move this check into the observer cache to avoid dynamic dispatch
|
||||
// SAFETY: We only access world metadata
|
||||
let last_trigger = unsafe { world.world_metadata() }.last_trigger_id();
|
||||
if state.last_trigger_id == last_trigger {
|
||||
return;
|
||||
}
|
||||
state.last_trigger_id = last_trigger;
|
||||
|
||||
// SAFETY: Caller ensures `ptr` is castable to `&mut T`
|
||||
let trigger: Trigger<E, B> = Trigger::new(unsafe { ptr.deref_mut() }, observer_trigger);
|
||||
// SAFETY: the static lifetime is encapsulated in Trigger / cannot leak out.
|
||||
// Additionally, IntoObserverSystem is only implemented for functions starting
|
||||
// with for<'a> Trigger<'a>, meaning users cannot specify Trigger<'static> manually,
|
||||
// allowing the Trigger<'static> to be moved outside of the context of the system.
|
||||
// This transmute is obviously not ideal, but it is safe. Ideally we can remove the
|
||||
// static constraint from ObserverSystem, but so far we have not found a way.
|
||||
let trigger: Trigger<'static, E, B> = unsafe { std::mem::transmute(trigger) };
|
||||
// SAFETY: Observer was triggered so must have an `ObserverSystemComponent`
|
||||
let system = unsafe {
|
||||
&mut observer_cell
|
||||
.get_mut::<Observer<E, B>>()
|
||||
.debug_checked_unwrap()
|
||||
.system
|
||||
};
|
||||
|
||||
system.update_archetype_component_access(world);
|
||||
|
||||
// SAFETY:
|
||||
// - `update_archetype_component_access` was just called
|
||||
// - there are no outstanding references to world except a private component
|
||||
// - system is an `ObserverSystem` so won't mutate world beyond the access of a `DeferredWorld`
|
||||
// - system is the same type erased system from above
|
||||
unsafe {
|
||||
system.run_unsafe(trigger, world);
|
||||
system.queue_deferred(world.into_deferred());
|
||||
}
|
||||
}
|
165
crates/bevy_ecs/src/observer/trigger_event.rs
Normal file
165
crates/bevy_ecs/src/observer/trigger_event.rs
Normal file
@ -0,0 +1,165 @@
|
||||
use crate::{
|
||||
component::ComponentId,
|
||||
entity::Entity,
|
||||
event::Event,
|
||||
world::{Command, DeferredWorld, World},
|
||||
};
|
||||
|
||||
/// A [`Command`] that emits a given trigger for a given set of targets.
|
||||
pub struct TriggerEvent<E, Targets: TriggerTargets = ()> {
|
||||
/// The event to trigger.
|
||||
pub event: E,
|
||||
|
||||
/// The targets to trigger the event for.
|
||||
pub targets: Targets,
|
||||
}
|
||||
|
||||
impl<E: Event, Targets: TriggerTargets> Command for TriggerEvent<E, Targets> {
|
||||
fn apply(mut self, world: &mut World) {
|
||||
let event_type = world.init_component::<E>();
|
||||
trigger_event(world, event_type, &mut self.event, self.targets);
|
||||
}
|
||||
}
|
||||
|
||||
/// Emit a trigger for a dynamic component id. This is unsafe and must be verified manually.
|
||||
pub struct EmitDynamicTrigger<T, Targets: TriggerTargets = ()> {
|
||||
event_type: ComponentId,
|
||||
event_data: T,
|
||||
targets: Targets,
|
||||
}
|
||||
|
||||
impl<E, Targets: TriggerTargets> EmitDynamicTrigger<E, Targets> {
|
||||
/// Sets the event type of the resulting trigger, used for dynamic triggers
|
||||
/// # Safety
|
||||
/// Caller must ensure that the component associated with `event_type` is accessible as E
|
||||
pub unsafe fn new_with_id(event_type: ComponentId, event_data: E, targets: Targets) -> Self {
|
||||
Self {
|
||||
event_type,
|
||||
event_data,
|
||||
targets,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Event, Targets: TriggerTargets> Command for EmitDynamicTrigger<E, Targets> {
|
||||
fn apply(mut self, world: &mut World) {
|
||||
trigger_event(world, self.event_type, &mut self.event_data, self.targets);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn trigger_event<E, Targets: TriggerTargets>(
|
||||
world: &mut World,
|
||||
event_type: ComponentId,
|
||||
event_data: &mut E,
|
||||
targets: Targets,
|
||||
) {
|
||||
let mut world = DeferredWorld::from(world);
|
||||
if targets.entities().len() == 0 {
|
||||
// SAFETY: T is accessible as the type represented by self.trigger, ensured in `Self::new`
|
||||
unsafe {
|
||||
world.trigger_observers_with_data(
|
||||
event_type,
|
||||
Entity::PLACEHOLDER,
|
||||
targets.components(),
|
||||
event_data,
|
||||
);
|
||||
};
|
||||
} else {
|
||||
for target in targets.entities() {
|
||||
// SAFETY: T is accessible as the type represented by self.trigger, ensured in `Self::new`
|
||||
unsafe {
|
||||
world.trigger_observers_with_data(
|
||||
event_type,
|
||||
target,
|
||||
targets.components(),
|
||||
event_data,
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a collection of targets for a specific [`Trigger`] of an [`Event`]. Targets can be of type [`Entity`] or [`ComponentId`].
|
||||
/// When a trigger occurs for a given event and [`TriggerTargets`], any [`Observer`] that watches for that specific event-target combination
|
||||
/// will run.
|
||||
///
|
||||
/// [`Trigger`]: crate::observer::Trigger
|
||||
/// [`Observer`]: crate::observer::Observer
|
||||
pub trait TriggerTargets: Send + Sync + 'static {
|
||||
/// The components the trigger should target.
|
||||
fn components(&self) -> impl ExactSizeIterator<Item = ComponentId>;
|
||||
|
||||
/// The entities the trigger should target.
|
||||
fn entities(&self) -> impl ExactSizeIterator<Item = Entity>;
|
||||
}
|
||||
|
||||
impl TriggerTargets for () {
|
||||
fn components(&self) -> impl ExactSizeIterator<Item = ComponentId> {
|
||||
[].into_iter()
|
||||
}
|
||||
|
||||
fn entities(&self) -> impl ExactSizeIterator<Item = Entity> {
|
||||
[].into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl TriggerTargets for Entity {
|
||||
fn components(&self) -> impl ExactSizeIterator<Item = ComponentId> {
|
||||
[].into_iter()
|
||||
}
|
||||
|
||||
fn entities(&self) -> impl ExactSizeIterator<Item = Entity> {
|
||||
std::iter::once(*self)
|
||||
}
|
||||
}
|
||||
|
||||
impl TriggerTargets for Vec<Entity> {
|
||||
fn components(&self) -> impl ExactSizeIterator<Item = ComponentId> {
|
||||
[].into_iter()
|
||||
}
|
||||
|
||||
fn entities(&self) -> impl ExactSizeIterator<Item = Entity> {
|
||||
self.iter().copied()
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> TriggerTargets for [Entity; N] {
|
||||
fn components(&self) -> impl ExactSizeIterator<Item = ComponentId> {
|
||||
[].into_iter()
|
||||
}
|
||||
|
||||
fn entities(&self) -> impl ExactSizeIterator<Item = Entity> {
|
||||
self.iter().copied()
|
||||
}
|
||||
}
|
||||
|
||||
impl TriggerTargets for ComponentId {
|
||||
fn components(&self) -> impl ExactSizeIterator<Item = ComponentId> {
|
||||
std::iter::once(*self)
|
||||
}
|
||||
|
||||
fn entities(&self) -> impl ExactSizeIterator<Item = Entity> {
|
||||
[].into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl TriggerTargets for Vec<ComponentId> {
|
||||
fn components(&self) -> impl ExactSizeIterator<Item = ComponentId> {
|
||||
self.iter().copied()
|
||||
}
|
||||
|
||||
fn entities(&self) -> impl ExactSizeIterator<Item = Entity> {
|
||||
[].into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> TriggerTargets for [ComponentId; N] {
|
||||
fn components(&self) -> impl ExactSizeIterator<Item = ComponentId> {
|
||||
self.iter().copied()
|
||||
}
|
||||
|
||||
fn entities(&self) -> impl ExactSizeIterator<Item = Entity> {
|
||||
[].into_iter()
|
||||
}
|
||||
}
|
@ -647,6 +647,16 @@ impl<T: SparseSetIndex> FilteredAccessSet<T> {
|
||||
.extend(filtered_access_set.filtered_accesses);
|
||||
}
|
||||
|
||||
/// Marks the set as reading all possible indices of type T.
|
||||
pub fn read_all(&mut self) {
|
||||
self.combined_access.read_all();
|
||||
}
|
||||
|
||||
/// Marks the set as writing all T.
|
||||
pub fn write_all(&mut self) {
|
||||
self.combined_access.write_all();
|
||||
}
|
||||
|
||||
/// Removes all accesses stored in this set.
|
||||
pub fn clear(&mut self) {
|
||||
self.combined_access.clear();
|
||||
|
@ -43,9 +43,8 @@ pub struct QueryBuilder<'w, D: QueryData = (), F: QueryFilter = ()> {
|
||||
impl<'w, D: QueryData, F: QueryFilter> QueryBuilder<'w, D, F> {
|
||||
/// Creates a new builder with the accesses required for `Q` and `F`
|
||||
pub fn new(world: &'w mut World) -> Self {
|
||||
let initializer = &mut world.component_initializer();
|
||||
let fetch_state = D::init_state(initializer);
|
||||
let filter_state = F::init_state(initializer);
|
||||
let fetch_state = D::init_state(world);
|
||||
let filter_state = F::init_state(world);
|
||||
|
||||
let mut access = FilteredAccess::default();
|
||||
D::update_component_access(&fetch_state, &mut access);
|
||||
@ -96,7 +95,7 @@ impl<'w, D: QueryData, F: QueryFilter> QueryBuilder<'w, D, F> {
|
||||
|
||||
/// Adds accesses required for `T` to self.
|
||||
pub fn data<T: QueryData>(&mut self) -> &mut Self {
|
||||
let state = T::init_state(&mut self.world.component_initializer());
|
||||
let state = T::init_state(self.world);
|
||||
let mut access = FilteredAccess::default();
|
||||
T::update_component_access(&state, &mut access);
|
||||
self.extend_access(access);
|
||||
@ -105,7 +104,7 @@ impl<'w, D: QueryData, F: QueryFilter> QueryBuilder<'w, D, F> {
|
||||
|
||||
/// Adds filter from `T` to self.
|
||||
pub fn filter<T: QueryFilter>(&mut self) -> &mut Self {
|
||||
let state = T::init_state(&mut self.world.component_initializer());
|
||||
let state = T::init_state(self.world);
|
||||
let mut access = FilteredAccess::default();
|
||||
T::update_component_access(&state, &mut access);
|
||||
self.extend_access(access);
|
||||
@ -223,9 +222,8 @@ impl<'w, D: QueryData, F: QueryFilter> QueryBuilder<'w, D, F> {
|
||||
pub fn transmute_filtered<NewD: QueryData, NewF: QueryFilter>(
|
||||
&mut self,
|
||||
) -> &mut QueryBuilder<'w, NewD, NewF> {
|
||||
let initializer = &mut self.world.component_initializer();
|
||||
let mut fetch_state = NewD::init_state(initializer);
|
||||
let filter_state = NewF::init_state(initializer);
|
||||
let mut fetch_state = NewD::init_state(self.world);
|
||||
let filter_state = NewF::init_state(self.world);
|
||||
|
||||
NewD::set_access(&mut fetch_state, &self.access);
|
||||
|
||||
|
@ -1,20 +1,20 @@
|
||||
use crate::{
|
||||
archetype::{Archetype, Archetypes},
|
||||
change_detection::{Ticks, TicksMut},
|
||||
component::{Component, ComponentId, ComponentInitializer, Components, StorageType, Tick},
|
||||
component::{Component, ComponentId, Components, StorageType, Tick},
|
||||
entity::{Entities, Entity, EntityLocation},
|
||||
query::{Access, DebugCheckedUnwrap, FilteredAccess, WorldQuery},
|
||||
storage::{ComponentSparseSet, Table, TableRow},
|
||||
world::{
|
||||
unsafe_world_cell::UnsafeWorldCell, EntityMut, EntityRef, FilteredEntityMut,
|
||||
FilteredEntityRef, Mut, Ref,
|
||||
FilteredEntityRef, Mut, Ref, World,
|
||||
},
|
||||
};
|
||||
use bevy_ptr::{ThinSlicePtr, UnsafeCellDeref};
|
||||
use bevy_utils::all_tuples;
|
||||
use std::{cell::UnsafeCell, marker::PhantomData};
|
||||
|
||||
/// Types that can be fetched from a [`World`](crate::world::World) using a [`Query`].
|
||||
/// Types that can be fetched from a [`World`] using a [`Query`].
|
||||
///
|
||||
/// There are many types that natively implement this trait:
|
||||
///
|
||||
@ -335,7 +335,7 @@ unsafe impl WorldQuery for Entity {
|
||||
|
||||
fn update_component_access(_state: &Self::State, _access: &mut FilteredAccess<ComponentId>) {}
|
||||
|
||||
fn init_state(_initializer: &mut ComponentInitializer) {}
|
||||
fn init_state(_world: &mut World) {}
|
||||
|
||||
fn get_state(_components: &Components) -> Option<()> {
|
||||
Some(())
|
||||
@ -407,7 +407,7 @@ unsafe impl WorldQuery for EntityLocation {
|
||||
|
||||
fn update_component_access(_state: &Self::State, _access: &mut FilteredAccess<ComponentId>) {}
|
||||
|
||||
fn init_state(_initializer: &mut ComponentInitializer) {}
|
||||
fn init_state(_world: &mut World) {}
|
||||
|
||||
fn get_state(_components: &Components) -> Option<()> {
|
||||
Some(())
|
||||
@ -486,7 +486,7 @@ unsafe impl<'a> WorldQuery for EntityRef<'a> {
|
||||
access.read_all();
|
||||
}
|
||||
|
||||
fn init_state(_initializer: &mut ComponentInitializer) {}
|
||||
fn init_state(_world: &mut World) {}
|
||||
|
||||
fn get_state(_components: &Components) -> Option<()> {
|
||||
Some(())
|
||||
@ -562,7 +562,7 @@ unsafe impl<'a> WorldQuery for EntityMut<'a> {
|
||||
access.write_all();
|
||||
}
|
||||
|
||||
fn init_state(_initializer: &mut ComponentInitializer) {}
|
||||
fn init_state(_world: &mut World) {}
|
||||
|
||||
fn get_state(_components: &Components) -> Option<()> {
|
||||
Some(())
|
||||
@ -660,7 +660,7 @@ unsafe impl<'a> WorldQuery for FilteredEntityRef<'a> {
|
||||
filtered_access.access.extend(&state.access);
|
||||
}
|
||||
|
||||
fn init_state(_initializer: &mut ComponentInitializer) -> Self::State {
|
||||
fn init_state(_world: &mut World) -> Self::State {
|
||||
FilteredAccess::default()
|
||||
}
|
||||
|
||||
@ -772,7 +772,7 @@ unsafe impl<'a> WorldQuery for FilteredEntityMut<'a> {
|
||||
filtered_access.access.extend(&state.access);
|
||||
}
|
||||
|
||||
fn init_state(_initializer: &mut ComponentInitializer) -> Self::State {
|
||||
fn init_state(_world: &mut World) -> Self::State {
|
||||
FilteredAccess::default()
|
||||
}
|
||||
|
||||
@ -846,7 +846,7 @@ unsafe impl WorldQuery for &Archetype {
|
||||
|
||||
fn update_component_access(_state: &Self::State, _access: &mut FilteredAccess<ComponentId>) {}
|
||||
|
||||
fn init_state(_initializer: &mut ComponentInitializer) {}
|
||||
fn init_state(_world: &mut World) {}
|
||||
|
||||
fn get_state(_components: &Components) -> Option<()> {
|
||||
Some(())
|
||||
@ -995,8 +995,8 @@ unsafe impl<T: Component> WorldQuery for &T {
|
||||
access.add_read(component_id);
|
||||
}
|
||||
|
||||
fn init_state(initializer: &mut ComponentInitializer) -> ComponentId {
|
||||
initializer.init_component::<T>()
|
||||
fn init_state(world: &mut World) -> ComponentId {
|
||||
world.init_component::<T>()
|
||||
}
|
||||
|
||||
fn get_state(components: &Components) -> Option<Self::State> {
|
||||
@ -1178,8 +1178,8 @@ unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> {
|
||||
access.add_read(component_id);
|
||||
}
|
||||
|
||||
fn init_state(initializer: &mut ComponentInitializer<'_>) -> ComponentId {
|
||||
initializer.init_component::<T>()
|
||||
fn init_state(world: &mut World) -> ComponentId {
|
||||
world.init_component::<T>()
|
||||
}
|
||||
|
||||
fn get_state(components: &Components) -> Option<Self::State> {
|
||||
@ -1361,8 +1361,8 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T {
|
||||
access.add_write(component_id);
|
||||
}
|
||||
|
||||
fn init_state(initializer: &mut ComponentInitializer<'_>) -> ComponentId {
|
||||
initializer.init_component::<T>()
|
||||
fn init_state(world: &mut World) -> ComponentId {
|
||||
world.init_component::<T>()
|
||||
}
|
||||
|
||||
fn get_state(components: &Components) -> Option<Self::State> {
|
||||
@ -1460,8 +1460,8 @@ unsafe impl<'__w, T: Component> WorldQuery for Mut<'__w, T> {
|
||||
}
|
||||
|
||||
// Forwarded to `&mut T`
|
||||
fn init_state(initializer: &mut ComponentInitializer) -> ComponentId {
|
||||
<&mut T as WorldQuery>::init_state(initializer)
|
||||
fn init_state(world: &mut World) -> ComponentId {
|
||||
<&mut T as WorldQuery>::init_state(world)
|
||||
}
|
||||
|
||||
// Forwarded to `&mut T`
|
||||
@ -1581,8 +1581,8 @@ unsafe impl<T: WorldQuery> WorldQuery for Option<T> {
|
||||
access.extend_access(&intermediate);
|
||||
}
|
||||
|
||||
fn init_state(initializer: &mut ComponentInitializer) -> T::State {
|
||||
T::init_state(initializer)
|
||||
fn init_state(world: &mut World) -> T::State {
|
||||
T::init_state(world)
|
||||
}
|
||||
|
||||
fn get_state(components: &Components) -> Option<Self::State> {
|
||||
@ -1736,8 +1736,8 @@ unsafe impl<T: Component> WorldQuery for Has<T> {
|
||||
access.access_mut().add_archetypal(component_id);
|
||||
}
|
||||
|
||||
fn init_state(initializer: &mut ComponentInitializer) -> ComponentId {
|
||||
initializer.init_component::<T>()
|
||||
fn init_state(world: &mut World) -> ComponentId {
|
||||
world.init_component::<T>()
|
||||
}
|
||||
|
||||
fn get_state(components: &Components) -> Option<Self::State> {
|
||||
@ -1862,16 +1862,17 @@ macro_rules! impl_anytuple_fetch {
|
||||
}
|
||||
|
||||
fn update_component_access(state: &Self::State, _access: &mut FilteredAccess<ComponentId>) {
|
||||
let ($($name,)*) = state;
|
||||
|
||||
let mut _new_access = _access.clone();
|
||||
|
||||
// update the filters (Or<(With<$name>,)>)
|
||||
let ($($name,)*) = state;
|
||||
let mut _not_first = false;
|
||||
$(
|
||||
if _not_first {
|
||||
// we use an intermediate access because we only want to update the filter_sets, not the access
|
||||
let mut intermediate = _access.clone();
|
||||
$name::update_component_access($name, &mut intermediate);
|
||||
_new_access.append_or(&intermediate);
|
||||
_new_access.extend_access(&intermediate);
|
||||
} else {
|
||||
$name::update_component_access($name, &mut _new_access);
|
||||
_new_access.required = _access.required.clone();
|
||||
@ -1879,11 +1880,16 @@ macro_rules! impl_anytuple_fetch {
|
||||
}
|
||||
)*
|
||||
|
||||
*_access = _new_access;
|
||||
_access.filter_sets = _new_access.filter_sets;
|
||||
|
||||
// update the access (add the read/writes)
|
||||
// Option<T> updates the access but not the filter_sets
|
||||
<($(Option<$name>,)*)>::update_component_access(state, _access);
|
||||
|
||||
}
|
||||
#[allow(unused_variables)]
|
||||
fn init_state(initializer: &mut ComponentInitializer) -> Self::State {
|
||||
($($name::init_state(initializer),)*)
|
||||
fn init_state(world: &mut World) -> Self::State {
|
||||
($($name::init_state(world),)*)
|
||||
}
|
||||
#[allow(unused_variables)]
|
||||
fn get_state(components: &Components) -> Option<Self::State> {
|
||||
@ -1959,8 +1965,8 @@ unsafe impl<D: QueryData> WorldQuery for NopWorldQuery<D> {
|
||||
|
||||
fn update_component_access(_state: &D::State, _access: &mut FilteredAccess<ComponentId>) {}
|
||||
|
||||
fn init_state(initializer: &mut ComponentInitializer) -> Self::State {
|
||||
D::init_state(initializer)
|
||||
fn init_state(world: &mut World) -> Self::State {
|
||||
D::init_state(world)
|
||||
}
|
||||
|
||||
fn get_state(components: &Components) -> Option<Self::State> {
|
||||
@ -2026,7 +2032,7 @@ unsafe impl<T: ?Sized> WorldQuery for PhantomData<T> {
|
||||
|
||||
fn update_component_access(_state: &Self::State, _access: &mut FilteredAccess<ComponentId>) {}
|
||||
|
||||
fn init_state(_initializer: &mut ComponentInitializer) -> Self::State {}
|
||||
fn init_state(_world: &mut World) -> Self::State {}
|
||||
|
||||
fn get_state(_components: &Components) -> Option<Self::State> {
|
||||
Some(())
|
||||
|
@ -1,10 +1,10 @@
|
||||
use crate::{
|
||||
archetype::Archetype,
|
||||
component::{Component, ComponentId, ComponentInitializer, Components, StorageType, Tick},
|
||||
component::{Component, ComponentId, Components, StorageType, Tick},
|
||||
entity::Entity,
|
||||
query::{DebugCheckedUnwrap, FilteredAccess, WorldQuery},
|
||||
storage::{Column, ComponentSparseSet, Table, TableRow},
|
||||
world::unsafe_world_cell::UnsafeWorldCell,
|
||||
world::{unsafe_world_cell::UnsafeWorldCell, World},
|
||||
};
|
||||
use bevy_ptr::{ThinSlicePtr, UnsafeCellDeref};
|
||||
use bevy_utils::all_tuples;
|
||||
@ -183,8 +183,8 @@ unsafe impl<T: Component> WorldQuery for With<T> {
|
||||
access.and_with(id);
|
||||
}
|
||||
|
||||
fn init_state(initializer: &mut ComponentInitializer) -> ComponentId {
|
||||
initializer.init_component::<T>()
|
||||
fn init_state(world: &mut World) -> ComponentId {
|
||||
world.init_component::<T>()
|
||||
}
|
||||
|
||||
fn get_state(components: &Components) -> Option<Self::State> {
|
||||
@ -291,8 +291,8 @@ unsafe impl<T: Component> WorldQuery for Without<T> {
|
||||
access.and_without(id);
|
||||
}
|
||||
|
||||
fn init_state(initializer: &mut ComponentInitializer) -> ComponentId {
|
||||
initializer.init_component::<T>()
|
||||
fn init_state(world: &mut World) -> ComponentId {
|
||||
world.init_component::<T>()
|
||||
}
|
||||
|
||||
fn get_state(components: &Components) -> Option<Self::State> {
|
||||
@ -461,8 +461,8 @@ macro_rules! impl_or_query_filter {
|
||||
*access = _new_access;
|
||||
}
|
||||
|
||||
fn init_state(initializer: &mut ComponentInitializer) -> Self::State {
|
||||
($($filter::init_state(initializer),)*)
|
||||
fn init_state(world: &mut World) -> Self::State {
|
||||
($($filter::init_state(world),)*)
|
||||
}
|
||||
|
||||
fn get_state(components: &Components) -> Option<Self::State> {
|
||||
@ -693,8 +693,8 @@ unsafe impl<T: Component> WorldQuery for Added<T> {
|
||||
access.add_read(id);
|
||||
}
|
||||
|
||||
fn init_state(initializer: &mut ComponentInitializer) -> ComponentId {
|
||||
initializer.init_component::<T>()
|
||||
fn init_state(world: &mut World) -> ComponentId {
|
||||
world.init_component::<T>()
|
||||
}
|
||||
|
||||
fn get_state(components: &Components) -> Option<ComponentId> {
|
||||
@ -904,8 +904,8 @@ unsafe impl<T: Component> WorldQuery for Changed<T> {
|
||||
access.add_read(id);
|
||||
}
|
||||
|
||||
fn init_state(initializer: &mut ComponentInitializer) -> ComponentId {
|
||||
initializer.init_component::<T>()
|
||||
fn init_state(world: &mut World) -> ComponentId {
|
||||
world.init_component::<T>()
|
||||
}
|
||||
|
||||
fn get_state(components: &Components) -> Option<ComponentId> {
|
||||
|
@ -730,6 +730,77 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sorts all query items into a new iterator with a key extraction function over the query lens.
|
||||
///
|
||||
/// This sort is unstable (i.e., may reorder equal elements).
|
||||
///
|
||||
/// This uses [`slice::sort_unstable_by_key`] internally.
|
||||
///
|
||||
/// Defining the lens works like [`transmute_lens`](crate::system::Query::transmute_lens).
|
||||
/// This includes the allowed parameter type changes listed under [allowed transmutes].
|
||||
/// However, the lens uses the filter of the original query when present.
|
||||
///
|
||||
/// The sort is not cached across system runs.
|
||||
///
|
||||
/// [allowed transmutes]: crate::system::Query#allowed-transmutes
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This will panic if `next` has been called on `QueryIter` before, unless the underlying `Query` is empty.
|
||||
pub fn sort_unstable_by_key<L: ReadOnlyQueryData + 'w, K>(
|
||||
self,
|
||||
mut f: impl FnMut(&L::Item<'w>) -> K,
|
||||
) -> QuerySortedIter<
|
||||
'w,
|
||||
's,
|
||||
D,
|
||||
F,
|
||||
impl ExactSizeIterator<Item = Entity> + DoubleEndedIterator + FusedIterator + 'w,
|
||||
>
|
||||
where
|
||||
K: Ord,
|
||||
{
|
||||
// On the first successful iteration of `QueryIterationCursor`, `archetype_entities` or `table_entities`
|
||||
// will be set to a non-zero value. The correctness of this method relies on this.
|
||||
// I.e. this sort method will execute if and only if `next` on `QueryIterationCursor` of a
|
||||
// non-empty `QueryIter` has not yet been called. When empty, this sort method will not panic.
|
||||
if !self.cursor.archetype_entities.is_empty() || !self.cursor.table_entities.is_empty() {
|
||||
panic!("it is not valid to call sort() after next()")
|
||||
}
|
||||
|
||||
let world = self.world;
|
||||
|
||||
let query_lens_state = self
|
||||
.query_state
|
||||
.transmute_filtered::<(L, Entity), F>(world.components());
|
||||
|
||||
// SAFETY:
|
||||
// `self.world` has permission to access the required components.
|
||||
// The original query iter has not been iterated on, so no items are aliased from it.
|
||||
let query_lens = unsafe {
|
||||
query_lens_state.iter_unchecked_manual(
|
||||
world,
|
||||
world.last_change_tick(),
|
||||
world.change_tick(),
|
||||
)
|
||||
};
|
||||
let mut keyed_query: Vec<_> = query_lens.collect();
|
||||
keyed_query.sort_unstable_by_key(|(lens, _)| f(lens));
|
||||
let entity_iter = keyed_query.into_iter().map(|(.., entity)| entity);
|
||||
// SAFETY:
|
||||
// `self.world` has permission to access the required components.
|
||||
// Each lens query item is dropped before the respective actual query item is accessed.
|
||||
unsafe {
|
||||
QuerySortedIter::new(
|
||||
world,
|
||||
self.query_state,
|
||||
entity_iter,
|
||||
world.last_change_tick(),
|
||||
world.change_tick(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Sort all query items into a new iterator with a key extraction function over the query lens.
|
||||
///
|
||||
/// This sort is stable (i.e., does not reorder equal elements).
|
||||
@ -874,7 +945,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Debug for QueryIter<'w, 's, D, F> {
|
||||
///
|
||||
/// This struct is created by the [`QueryIter::sort`], [`QueryIter::sort_unstable`],
|
||||
/// [`QueryIter::sort_by`], [`QueryIter::sort_unstable_by`], [`QueryIter::sort_by_key`],
|
||||
/// and [`QueryIter::sort_by_cached_key`] methods.
|
||||
/// [`QueryIter::sort_unstable_by_key`], and [`QueryIter::sort_by_cached_key`] methods.
|
||||
pub struct QuerySortedIter<'w, 's, D: QueryData, F: QueryFilter, I>
|
||||
where
|
||||
I: Iterator<Item = Entity>,
|
||||
@ -1681,6 +1752,11 @@ mod tests {
|
||||
.sort_by_key::<Entity, _>(|&e| e)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let sort_unstable_by_key = query
|
||||
.iter(&world)
|
||||
.sort_unstable_by_key::<Entity, _>(|&e| e)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let sort_by_cached_key = query
|
||||
.iter(&world)
|
||||
.sort_by_cached_key::<Entity, _>(|&e| e)
|
||||
@ -1701,6 +1777,9 @@ mod tests {
|
||||
let mut sort_by_key_v2 = query.iter(&world).collect::<Vec<_>>();
|
||||
sort_by_key_v2.sort_by_key(|&e| e);
|
||||
|
||||
let mut sort_unstable_by_key_v2 = query.iter(&world).collect::<Vec<_>>();
|
||||
sort_unstable_by_key_v2.sort_unstable_by_key(|&e| e);
|
||||
|
||||
let mut sort_by_cached_key_v2 = query.iter(&world).collect::<Vec<_>>();
|
||||
sort_by_cached_key_v2.sort_by_cached_key(|&e| e);
|
||||
|
||||
@ -1709,6 +1788,7 @@ mod tests {
|
||||
assert_eq!(sort_by, sort_by_v2);
|
||||
assert_eq!(sort_unstable_by, sort_unstable_by_v2);
|
||||
assert_eq!(sort_by_key, sort_by_key_v2);
|
||||
assert_eq!(sort_unstable_by_key, sort_unstable_by_key_v2);
|
||||
assert_eq!(sort_by_cached_key, sort_by_cached_key_v2);
|
||||
}
|
||||
|
||||
|
@ -178,9 +178,8 @@ impl<D: QueryData, F: QueryFilter> QueryState<D, F> {
|
||||
/// `new_archetype` and its variants must be called on all of the World's archetypes before the
|
||||
/// state can return valid query results.
|
||||
fn new_uninitialized(world: &mut World) -> Self {
|
||||
let initializer = &mut world.component_initializer();
|
||||
let fetch_state = D::init_state(initializer);
|
||||
let filter_state = F::init_state(initializer);
|
||||
let fetch_state = D::init_state(world);
|
||||
let filter_state = F::init_state(world);
|
||||
|
||||
let mut component_access = FilteredAccess::default();
|
||||
D::update_component_access(&fetch_state, &mut component_access);
|
||||
@ -215,9 +214,8 @@ impl<D: QueryData, F: QueryFilter> QueryState<D, F> {
|
||||
|
||||
/// Creates a new [`QueryState`] from a given [`QueryBuilder`] and inherits its [`FilteredAccess`].
|
||||
pub fn from_builder(builder: &mut QueryBuilder<D, F>) -> Self {
|
||||
let initializer = &mut builder.world_mut().component_initializer();
|
||||
let mut fetch_state = D::init_state(initializer);
|
||||
let filter_state = F::init_state(initializer);
|
||||
let mut fetch_state = D::init_state(builder.world_mut());
|
||||
let filter_state = F::init_state(builder.world_mut());
|
||||
D::set_access(&mut fetch_state, builder.access());
|
||||
|
||||
let mut state = Self {
|
||||
|
@ -1,10 +1,10 @@
|
||||
use crate::{
|
||||
archetype::Archetype,
|
||||
component::{ComponentId, ComponentInitializer, Components, Tick},
|
||||
component::{ComponentId, Components, Tick},
|
||||
entity::Entity,
|
||||
query::FilteredAccess,
|
||||
storage::{Table, TableRow},
|
||||
world::unsafe_world_cell::UnsafeWorldCell,
|
||||
world::{unsafe_world_cell::UnsafeWorldCell, World},
|
||||
};
|
||||
use bevy_utils::all_tuples;
|
||||
|
||||
@ -79,7 +79,7 @@ pub unsafe trait WorldQuery {
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// - `archetype` and `tables` must be from the same [`World`](crate::world::World) that [`WorldQuery::init_state`] was called on.
|
||||
/// - `archetype` and `tables` must be from the same [`World`] that [`WorldQuery::init_state`] was called on.
|
||||
/// - `table` must correspond to `archetype`.
|
||||
/// - `state` must be the [`State`](Self::State) that `fetch` was initialized with.
|
||||
unsafe fn set_archetype<'w>(
|
||||
@ -94,7 +94,7 @@ pub unsafe trait WorldQuery {
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// - `table` must be from the same [`World`](crate::world::World) that [`WorldQuery::init_state`] was called on.
|
||||
/// - `table` must be from the same [`World`] that [`WorldQuery::init_state`] was called on.
|
||||
/// - `state` must be the [`State`](Self::State) that `fetch` was initialized with.
|
||||
unsafe fn set_table<'w>(fetch: &mut Self::Fetch<'w>, state: &Self::State, table: &'w Table);
|
||||
|
||||
@ -127,7 +127,7 @@ pub unsafe trait WorldQuery {
|
||||
fn update_component_access(state: &Self::State, access: &mut FilteredAccess<ComponentId>);
|
||||
|
||||
/// Creates and initializes a [`State`](WorldQuery::State) for this [`WorldQuery`] type.
|
||||
fn init_state(initializer: &mut ComponentInitializer) -> Self::State;
|
||||
fn init_state(world: &mut World) -> Self::State;
|
||||
|
||||
/// Attempts to initialize a [`State`](WorldQuery::State) for this [`WorldQuery`] type using read-only
|
||||
/// access to [`Components`].
|
||||
@ -213,8 +213,8 @@ macro_rules! impl_tuple_world_query {
|
||||
$($name::update_component_access($name, _access);)*
|
||||
}
|
||||
#[allow(unused_variables)]
|
||||
fn init_state(initializer: &mut ComponentInitializer) -> Self::State {
|
||||
($($name::init_state(initializer),)*)
|
||||
fn init_state(world: &mut World) -> Self::State {
|
||||
($($name::init_state(world),)*)
|
||||
}
|
||||
#[allow(unused_variables)]
|
||||
fn get_state(components: &Components) -> Option<Self::State> {
|
||||
|
@ -116,11 +116,11 @@ impl RemovedComponentEvents {
|
||||
///
|
||||
/// If you are using `bevy_ecs` as a standalone crate,
|
||||
/// note that the `RemovedComponents` list will not be automatically cleared for you,
|
||||
/// and will need to be manually flushed using [`World::clear_trackers`](World::clear_trackers)
|
||||
/// and will need to be manually flushed using [`World::clear_trackers`](World::clear_trackers).
|
||||
///
|
||||
/// For users of `bevy` and `bevy_app`, this is automatically done in `bevy_app::App::update`.
|
||||
/// For the main world, [`World::clear_trackers`](World::clear_trackers) is run after the main schedule is run and after
|
||||
/// `SubApp`'s have run.
|
||||
/// For users of `bevy` and `bevy_app`, [`World::clear_trackers`](World::clear_trackers) is
|
||||
/// automatically called by `bevy_app::App::update` and `bevy_app::SubApp::update`.
|
||||
/// For the main world, this is delayed until after all `SubApp`s have run.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
|
@ -127,6 +127,11 @@ where
|
||||
self.system.apply_deferred(world);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn queue_deferred(&mut self, world: crate::world::DeferredWorld) {
|
||||
self.system.queue_deferred(world);
|
||||
}
|
||||
|
||||
fn initialize(&mut self, world: &mut crate::prelude::World) {
|
||||
self.system.initialize(world);
|
||||
}
|
||||
|
@ -202,6 +202,12 @@ where
|
||||
self.b.apply_deferred(world);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn queue_deferred(&mut self, mut world: crate::world::DeferredWorld) {
|
||||
self.a.queue_deferred(world.reborrow());
|
||||
self.b.queue_deferred(world);
|
||||
}
|
||||
|
||||
fn initialize(&mut self, world: &mut World) {
|
||||
self.a.initialize(world);
|
||||
self.b.initialize(world);
|
||||
|
@ -1,11 +1,13 @@
|
||||
mod parallel_scope;
|
||||
|
||||
use super::{Deferred, IntoSystem, RegisterSystem, Resource};
|
||||
use super::{Deferred, IntoObserverSystem, IntoSystem, RegisterSystem, Resource};
|
||||
use crate::{
|
||||
self as bevy_ecs,
|
||||
bundle::Bundle,
|
||||
component::ComponentId,
|
||||
entity::{Entities, Entity},
|
||||
event::Event,
|
||||
observer::{Observer, TriggerEvent, TriggerTargets},
|
||||
system::{RunSystemWithInput, SystemId},
|
||||
world::command_queue::RawCommandQueue,
|
||||
world::{Command, CommandQueue, EntityWorldMut, FromWorld, World},
|
||||
@ -116,6 +118,17 @@ const _: () = {
|
||||
world,
|
||||
);
|
||||
}
|
||||
fn queue(
|
||||
state: &mut Self::State,
|
||||
system_meta: &bevy_ecs::system::SystemMeta,
|
||||
world: bevy_ecs::world::DeferredWorld,
|
||||
) {
|
||||
<__StructFieldsAlias<'_, '_> as bevy_ecs::system::SystemParam>::queue(
|
||||
&mut state.state,
|
||||
system_meta,
|
||||
world,
|
||||
);
|
||||
}
|
||||
unsafe fn get_param<'w, 's>(
|
||||
state: &'s mut Self::State,
|
||||
system_meta: &bevy_ecs::system::SystemMeta,
|
||||
@ -150,7 +163,7 @@ impl<'w, 's> Commands<'w, 's> {
|
||||
///
|
||||
/// [system parameter]: crate::system::SystemParam
|
||||
pub fn new(queue: &'s mut CommandQueue, world: &'w World) -> Self {
|
||||
Self::new_from_entities(queue, world.entities())
|
||||
Self::new_from_entities(queue, &world.entities)
|
||||
}
|
||||
|
||||
/// Returns a new `Commands` instance from a [`CommandQueue`] and an [`Entities`] reference.
|
||||
@ -735,6 +748,26 @@ impl<'w, 's> Commands<'w, 's> {
|
||||
pub fn add<C: Command>(&mut self, command: C) {
|
||||
self.push(command);
|
||||
}
|
||||
|
||||
/// Sends a "global" [`Trigger`] without any targets. This will run any [`Observer`] of the `event` that
|
||||
/// isn't scoped to specific targets.
|
||||
pub fn trigger(&mut self, event: impl Event) {
|
||||
self.add(TriggerEvent { event, targets: () });
|
||||
}
|
||||
|
||||
/// Sends a [`Trigger`] for the given targets. This will run any [`Observer`] of the `event` that
|
||||
/// watches those targets.
|
||||
pub fn trigger_targets(&mut self, event: impl Event, targets: impl TriggerTargets) {
|
||||
self.add(TriggerEvent { event, targets });
|
||||
}
|
||||
|
||||
/// Spawn an [`Observer`] and returns the [`EntityCommands`] associated with the entity that stores the observer.
|
||||
pub fn observe<E: Event, B: Bundle, M>(
|
||||
&mut self,
|
||||
observer: impl IntoObserverSystem<E, B, M>,
|
||||
) -> EntityCommands {
|
||||
self.spawn(Observer::new(observer))
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`Command`] which gets executed for a given [`Entity`].
|
||||
@ -794,6 +827,7 @@ pub trait EntityCommand<Marker = ()>: Send + 'static {
|
||||
/// Executes this command for the given [`Entity`].
|
||||
fn apply(self, id: Entity, world: &mut World);
|
||||
/// Returns a [`Command`] which executes this [`EntityCommand`] for the given [`Entity`].
|
||||
#[must_use = "commands do nothing unless applied to a `World`"]
|
||||
fn with_entity(self, id: Entity) -> WithEntity<Marker, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
@ -1017,6 +1051,7 @@ impl EntityCommands<'_> {
|
||||
}
|
||||
|
||||
/// Despawns the entity.
|
||||
/// This will emit a warning if the entity does not exist.
|
||||
///
|
||||
/// See [`World::despawn`] for more details.
|
||||
///
|
||||
@ -1025,10 +1060,6 @@ impl EntityCommands<'_> {
|
||||
/// This won't clean up external references to the entity (such as parent-child relationships
|
||||
/// if you're using `bevy_hierarchy`), which may leave the world in an invalid state.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// The command will panic when applied if the associated entity does not exist.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
@ -1128,6 +1159,15 @@ impl EntityCommands<'_> {
|
||||
pub fn commands(&mut self) -> Commands {
|
||||
self.commands.reborrow()
|
||||
}
|
||||
|
||||
/// Creates an [`Observer`](crate::observer::Observer) listening for a trigger of type `T` that targets this entity.
|
||||
pub fn observe<E: Event, B: Bundle, M>(
|
||||
&mut self,
|
||||
system: impl IntoObserverSystem<E, B, M>,
|
||||
) -> &mut Self {
|
||||
self.add(observe(system));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> Command for F
|
||||
@ -1288,6 +1328,16 @@ fn log_components(entity: Entity, world: &mut World) {
|
||||
info!("Entity {:?}: {:?}", entity, debug_infos);
|
||||
}
|
||||
|
||||
fn observe<E: Event, B: Bundle, M>(
|
||||
observer: impl IntoObserverSystem<E, B, M>,
|
||||
) -> impl EntityCommand {
|
||||
move |entity, world: &mut World| {
|
||||
if let Some(mut entity) = world.get_entity_mut(entity) {
|
||||
entity.observe(observer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::float_cmp, clippy::approx_constant)]
|
||||
mod tests {
|
||||
|
@ -110,7 +110,7 @@ where
|
||||
);
|
||||
let out = self.func.run(world, input, params);
|
||||
|
||||
world.flush_commands();
|
||||
world.flush();
|
||||
let change_tick = world.change_tick.get_mut();
|
||||
self.system_meta.last_run.set(*change_tick);
|
||||
*change_tick = change_tick.wrapping_add(1);
|
||||
@ -126,6 +126,13 @@ where
|
||||
// might have buffers to apply, but this is handled by `PipeSystem`.
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn queue_deferred(&mut self, _world: crate::world::DeferredWorld) {
|
||||
// "pure" exclusive systems do not have any buffers to apply.
|
||||
// Systems made by piping a normal system with an exclusive system
|
||||
// might have buffers to apply, but this is handled by `PipeSystem`.
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn initialize(&mut self, world: &mut World) {
|
||||
self.system_meta.last_run = world.change_tick().relative_to(Tick::MAX);
|
||||
|
@ -5,7 +5,7 @@ use crate::{
|
||||
query::{Access, FilteredAccessSet},
|
||||
schedule::{InternedSystemSet, SystemSet},
|
||||
system::{check_system_change_tick, ReadOnlySystemParam, System, SystemParam, SystemParamItem},
|
||||
world::{unsafe_world_cell::UnsafeWorldCell, World, WorldId},
|
||||
world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World, WorldId},
|
||||
};
|
||||
|
||||
use bevy_utils::all_tuples;
|
||||
@ -399,8 +399,8 @@ where
|
||||
F: SystemParamFunction<Marker>,
|
||||
{
|
||||
func: F,
|
||||
param_state: Option<<F::Param as SystemParam>::State>,
|
||||
system_meta: SystemMeta,
|
||||
pub(crate) param_state: Option<<F::Param as SystemParam>::State>,
|
||||
pub(crate) system_meta: SystemMeta,
|
||||
world_id: Option<WorldId>,
|
||||
archetype_generation: ArchetypeGeneration,
|
||||
// NOTE: PhantomData<fn()-> T> gives this safe Send/Sync impls
|
||||
@ -542,6 +542,12 @@ where
|
||||
F::Param::apply(param_state, &self.system_meta, world);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn queue_deferred(&mut self, world: DeferredWorld) {
|
||||
let param_state = self.param_state.as_mut().expect(Self::PARAM_MESSAGE);
|
||||
F::Param::queue(param_state, &self.system_meta, world);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn initialize(&mut self, world: &mut World) {
|
||||
if let Some(id) = self.world_id {
|
||||
|
@ -108,6 +108,7 @@ mod commands;
|
||||
mod exclusive_function_system;
|
||||
mod exclusive_system_param;
|
||||
mod function_system;
|
||||
mod observer_system;
|
||||
mod query;
|
||||
#[allow(clippy::module_inception)]
|
||||
mod system;
|
||||
@ -124,6 +125,7 @@ pub use commands::*;
|
||||
pub use exclusive_function_system::*;
|
||||
pub use exclusive_system_param::*;
|
||||
pub use function_system::*;
|
||||
pub use observer_system::*;
|
||||
pub use query::*;
|
||||
pub use system::*;
|
||||
pub use system_name::*;
|
||||
@ -554,6 +556,58 @@ mod tests {
|
||||
run_system(&mut world, sys);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn any_of_working() {
|
||||
fn sys(_: Query<AnyOf<(&mut A, &B)>>) {}
|
||||
let mut world = World::default();
|
||||
run_system(&mut world, sys);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn any_of_with_and_without_common() {
|
||||
fn sys(_: Query<(&mut D, &C, AnyOf<(&A, &B)>)>, _: Query<&mut D, Without<C>>) {}
|
||||
let mut world = World::default();
|
||||
run_system(&mut world, sys);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic = "&bevy_ecs::system::tests::A conflicts with a previous access in this query."]
|
||||
fn any_of_with_mut_and_ref() {
|
||||
fn sys(_: Query<AnyOf<(&mut A, &A)>>) {}
|
||||
let mut world = World::default();
|
||||
run_system(&mut world, sys);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic = "&mut bevy_ecs::system::tests::A conflicts with a previous access in this query."]
|
||||
fn any_of_with_ref_and_mut() {
|
||||
fn sys(_: Query<AnyOf<(&A, &mut A)>>) {}
|
||||
let mut world = World::default();
|
||||
run_system(&mut world, sys);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic = "&bevy_ecs::system::tests::A conflicts with a previous access in this query."]
|
||||
fn any_of_with_mut_and_option() {
|
||||
fn sys(_: Query<AnyOf<(&mut A, Option<&A>)>>) {}
|
||||
let mut world = World::default();
|
||||
run_system(&mut world, sys);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn any_of_with_entity_and_mut() {
|
||||
fn sys(_: Query<AnyOf<(Entity, &mut A)>>) {}
|
||||
let mut world = World::default();
|
||||
run_system(&mut world, sys);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn any_of_with_empty_and_mut() {
|
||||
fn sys(_: Query<AnyOf<((), &mut A)>>) {}
|
||||
let mut world = World::default();
|
||||
run_system(&mut world, sys);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic = "error[B0001]"]
|
||||
fn any_of_has_no_filter_with() {
|
||||
@ -562,6 +616,14 @@ mod tests {
|
||||
run_system(&mut world, sys);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic = "&mut bevy_ecs::system::tests::A conflicts with a previous access in this query."]
|
||||
fn any_of_with_conflicting() {
|
||||
fn sys(_: Query<AnyOf<(&mut A, &mut A)>>) {}
|
||||
let mut world = World::default();
|
||||
run_system(&mut world, sys);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn any_of_has_filter_with_when_both_have_it() {
|
||||
fn sys(_: Query<(AnyOf<(&A, &A)>, &mut B)>, _: Query<&mut B, Without<A>>) {}
|
||||
|
71
crates/bevy_ecs/src/system/observer_system.rs
Normal file
71
crates/bevy_ecs/src/system/observer_system.rs
Normal file
@ -0,0 +1,71 @@
|
||||
use bevy_utils::all_tuples;
|
||||
|
||||
use crate::{
|
||||
prelude::{Bundle, Trigger},
|
||||
system::{System, SystemParam, SystemParamFunction, SystemParamItem},
|
||||
};
|
||||
|
||||
use super::IntoSystem;
|
||||
|
||||
/// Implemented for systems that have an [`Observer`] as the first argument.
|
||||
pub trait ObserverSystem<E: 'static, B: Bundle>:
|
||||
System<In = Trigger<'static, E, B>, Out = ()> + Send + 'static
|
||||
{
|
||||
}
|
||||
|
||||
impl<E: 'static, B: Bundle, T: System<In = Trigger<'static, E, B>, Out = ()> + Send + 'static>
|
||||
ObserverSystem<E, B> for T
|
||||
{
|
||||
}
|
||||
|
||||
/// Implemented for systems that convert into [`ObserverSystem`].
|
||||
pub trait IntoObserverSystem<E: 'static, B: Bundle, M>: Send + 'static {
|
||||
/// The type of [`System`] that this instance converts into.
|
||||
type System: ObserverSystem<E, B>;
|
||||
|
||||
/// Turns this value into its corresponding [`System`].
|
||||
fn into_system(this: Self) -> Self::System;
|
||||
}
|
||||
|
||||
impl<S: IntoSystem<Trigger<'static, E, B>, (), M> + Send + 'static, M, E: 'static, B: Bundle>
|
||||
IntoObserverSystem<E, B, M> for S
|
||||
where
|
||||
S::System: ObserverSystem<E, B>,
|
||||
{
|
||||
type System = <S as IntoSystem<Trigger<'static, E, B>, (), M>>::System;
|
||||
|
||||
fn into_system(this: Self) -> Self::System {
|
||||
IntoSystem::into_system(this)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_system_function {
|
||||
($($param: ident),*) => {
|
||||
#[allow(non_snake_case)]
|
||||
impl<E: 'static, B: Bundle, Func: Send + Sync + 'static, $($param: SystemParam),*> SystemParamFunction<fn(Trigger<E, B>, $($param,)*)> for Func
|
||||
where
|
||||
for <'a> &'a mut Func:
|
||||
FnMut(Trigger<E, B>, $($param),*) +
|
||||
FnMut(Trigger<E, B>, $(SystemParamItem<$param>),*)
|
||||
{
|
||||
type In = Trigger<'static, E, B>;
|
||||
type Out = ();
|
||||
type Param = ($($param,)*);
|
||||
#[inline]
|
||||
fn run(&mut self, input: Trigger<'static, E, B>, param_value: SystemParamItem< ($($param,)*)>) {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn call_inner<E: 'static, B: Bundle, $($param,)*>(
|
||||
mut f: impl FnMut(Trigger<'static, E, B>, $($param,)*),
|
||||
input: Trigger<'static, E, B>,
|
||||
$($param: $param,)*
|
||||
){
|
||||
f(input, $($param,)*)
|
||||
}
|
||||
let ($($param,)*) = param_value;
|
||||
call_inner(self, input, $($param),*)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
all_tuples!(impl_system_function, 0, 16, F);
|
@ -1385,9 +1385,9 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> {
|
||||
|
||||
/// Returns a [`QueryLens`] that can be used to get a query with the combined fetch.
|
||||
///
|
||||
/// For example, this can take a `Query<&A>` and a `Queryy<&B>` and return a `Query<&A, &B>`.
|
||||
/// The returned query will only return items with both `A` and `B`. Note that since filter
|
||||
/// are dropped, non-archetypal filters like `Added` and `Changed` will no be respected.
|
||||
/// For example, this can take a `Query<&A>` and a `Query<&B>` and return a `Query<(&A, &B)>`.
|
||||
/// The returned query will only return items with both `A` and `B`. Note that since filters
|
||||
/// are dropped, non-archetypal filters like `Added` and `Changed` will not be respected.
|
||||
/// To maintain or change filter terms see `Self::join_filtered`.
|
||||
///
|
||||
/// ## Example
|
||||
@ -1446,7 +1446,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> {
|
||||
|
||||
/// Equivalent to [`Self::join`] but also includes a [`QueryFilter`] type.
|
||||
///
|
||||
/// Note that the lens with iterate a subset of the original queries tables
|
||||
/// Note that the lens with iterate a subset of the original queries' tables
|
||||
/// and archetypes. This means that additional archetypal query terms like
|
||||
/// `With` and `Without` will not necessarily be respected and non-archetypal
|
||||
/// terms like `Added` and `Changed` will only be respected if they are in
|
||||
|
@ -4,6 +4,7 @@ use core::fmt::Debug;
|
||||
use crate::component::Tick;
|
||||
use crate::schedule::InternedSystemSet;
|
||||
use crate::world::unsafe_world_cell::UnsafeWorldCell;
|
||||
use crate::world::DeferredWorld;
|
||||
use crate::{archetype::ArchetypeComponentId, component::ComponentId, query::Access, world::World};
|
||||
|
||||
use std::any::TypeId;
|
||||
@ -89,6 +90,10 @@ pub trait System: Send + Sync + 'static {
|
||||
/// This is where [`Commands`](crate::system::Commands) get applied.
|
||||
fn apply_deferred(&mut self, world: &mut World);
|
||||
|
||||
/// Enqueues any [`Deferred`](crate::system::Deferred) system parameters (or other system buffers)
|
||||
/// of this system into the world's command buffer.
|
||||
fn queue_deferred(&mut self, world: DeferredWorld);
|
||||
|
||||
/// Initialize the system.
|
||||
fn initialize(&mut self, _world: &mut World);
|
||||
|
||||
|
@ -11,7 +11,7 @@ use crate::{
|
||||
ReadOnlyQueryData,
|
||||
},
|
||||
system::{Query, SystemMeta},
|
||||
world::{unsafe_world_cell::UnsafeWorldCell, FromWorld, World},
|
||||
world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, FromWorld, World},
|
||||
};
|
||||
use bevy_ecs_macros::impl_param_set;
|
||||
pub use bevy_ecs_macros::Resource;
|
||||
@ -159,6 +159,11 @@ pub unsafe trait SystemParam: Sized {
|
||||
#[allow(unused_variables)]
|
||||
fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) {}
|
||||
|
||||
/// Queues any deferred mutations to be applied at the next [`apply_deferred`](crate::prelude::apply_deferred).
|
||||
#[inline]
|
||||
#[allow(unused_variables)]
|
||||
fn queue(state: &mut Self::State, system_meta: &SystemMeta, world: DeferredWorld) {}
|
||||
|
||||
/// Creates a parameter to be passed into a [`SystemParamFunction`].
|
||||
///
|
||||
/// [`SystemParamFunction`]: super::SystemParamFunction
|
||||
@ -712,6 +717,27 @@ unsafe impl SystemParam for &'_ World {
|
||||
}
|
||||
}
|
||||
|
||||
/// SAFETY: `DeferredWorld` can read all components and resources but cannot be used to gain any other mutable references.
|
||||
unsafe impl<'w> SystemParam for DeferredWorld<'w> {
|
||||
type State = ();
|
||||
type Item<'world, 'state> = DeferredWorld<'world>;
|
||||
|
||||
fn init_state(_world: &mut World, system_meta: &mut SystemMeta) -> Self::State {
|
||||
system_meta.component_access_set.read_all();
|
||||
system_meta.component_access_set.write_all();
|
||||
system_meta.set_has_deferred();
|
||||
}
|
||||
|
||||
unsafe fn get_param<'world, 'state>(
|
||||
_state: &'state mut Self::State,
|
||||
_system_meta: &SystemMeta,
|
||||
world: UnsafeWorldCell<'world>,
|
||||
_change_tick: Tick,
|
||||
) -> Self::Item<'world, 'state> {
|
||||
world.into_deferred()
|
||||
}
|
||||
}
|
||||
|
||||
/// A system local [`SystemParam`].
|
||||
///
|
||||
/// A local may only be accessed by the system itself and is therefore not visible to other systems.
|
||||
@ -848,6 +874,8 @@ impl<'w, T: FromWorld + Send + 'static> BuildableSystemParam for Local<'w, T> {
|
||||
pub trait SystemBuffer: FromWorld + Send + 'static {
|
||||
/// Applies any deferred mutations to the [`World`].
|
||||
fn apply(&mut self, system_meta: &SystemMeta, world: &mut World);
|
||||
/// Queues any deferred mutations to be applied at the next [`apply_deferred`](crate::prelude::apply_deferred).
|
||||
fn queue(&mut self, _system_meta: &SystemMeta, _world: DeferredWorld) {}
|
||||
}
|
||||
|
||||
/// A [`SystemParam`] that stores a buffer which gets applied to the [`World`] during
|
||||
@ -1012,6 +1040,10 @@ unsafe impl<T: SystemBuffer> SystemParam for Deferred<'_, T> {
|
||||
state.get().apply(system_meta, world);
|
||||
}
|
||||
|
||||
fn queue(state: &mut Self::State, system_meta: &SystemMeta, world: DeferredWorld) {
|
||||
state.get().queue(system_meta, world);
|
||||
}
|
||||
|
||||
unsafe fn get_param<'w, 's>(
|
||||
state: &'s mut Self::State,
|
||||
_system_meta: &SystemMeta,
|
||||
@ -1424,6 +1456,11 @@ macro_rules! impl_system_param_tuple {
|
||||
$($param::apply($param, _system_meta, _world);)*
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn queue(($($param,)*): &mut Self::State, _system_meta: &SystemMeta, mut _world: DeferredWorld) {
|
||||
$($param::queue($param, _system_meta, _world.reborrow());)*
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[allow(clippy::unused_unit)]
|
||||
unsafe fn get_param<'w, 's>(
|
||||
@ -1572,6 +1609,10 @@ unsafe impl<P: SystemParam + 'static> SystemParam for StaticSystemParam<'_, '_,
|
||||
P::apply(state, system_meta, world);
|
||||
}
|
||||
|
||||
fn queue(state: &mut Self::State, system_meta: &SystemMeta, world: DeferredWorld) {
|
||||
P::queue(state, system_meta, world);
|
||||
}
|
||||
|
||||
unsafe fn get_param<'world, 'state>(
|
||||
state: &'state mut Self::State,
|
||||
system_meta: &SystemMeta,
|
||||
|
@ -3,6 +3,7 @@ use crate::system::{SystemBuffer, SystemMeta};
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
mem::MaybeUninit,
|
||||
panic::{self, AssertUnwindSafe},
|
||||
ptr::{addr_of_mut, NonNull},
|
||||
};
|
||||
|
||||
@ -11,6 +12,8 @@ use bevy_utils::tracing::warn;
|
||||
|
||||
use crate::world::{Command, World};
|
||||
|
||||
use super::DeferredWorld;
|
||||
|
||||
struct CommandMeta {
|
||||
/// SAFETY: The `value` must point to a value of type `T: Command`,
|
||||
/// where `T` is some specific type that was used to produce this metadata.
|
||||
@ -18,11 +21,8 @@ struct CommandMeta {
|
||||
/// `world` is optional to allow this one function pointer to perform double-duty as a drop.
|
||||
///
|
||||
/// Advances `cursor` by the size of `T` in bytes.
|
||||
consume_command_and_get_size: unsafe fn(
|
||||
value: OwningPtr<Unaligned>,
|
||||
world: Option<NonNull<World>>,
|
||||
cursor: NonNull<usize>,
|
||||
),
|
||||
consume_command_and_get_size:
|
||||
unsafe fn(value: OwningPtr<Unaligned>, world: Option<NonNull<World>>, cursor: &mut usize),
|
||||
}
|
||||
|
||||
/// Densely and efficiently stores a queue of heterogenous types implementing [`Command`].
|
||||
@ -41,6 +41,7 @@ pub struct CommandQueue {
|
||||
// be passed to the corresponding `CommandMeta.apply_command_and_get_size` fn pointer.
|
||||
pub(crate) bytes: Vec<MaybeUninit<u8>>,
|
||||
pub(crate) cursor: usize,
|
||||
pub(crate) panic_recovery: Vec<MaybeUninit<u8>>,
|
||||
}
|
||||
|
||||
/// Wraps pointers to a [`CommandQueue`], used internally to avoid stacked borrow rules when
|
||||
@ -49,6 +50,7 @@ pub struct CommandQueue {
|
||||
pub(crate) struct RawCommandQueue {
|
||||
pub(crate) bytes: NonNull<Vec<MaybeUninit<u8>>>,
|
||||
pub(crate) cursor: NonNull<usize>,
|
||||
pub(crate) panic_recovery: NonNull<Vec<MaybeUninit<u8>>>,
|
||||
}
|
||||
|
||||
// CommandQueue needs to implement Debug manually, rather than deriving it, because the derived impl just prints
|
||||
@ -117,6 +119,7 @@ impl CommandQueue {
|
||||
RawCommandQueue {
|
||||
bytes: NonNull::new_unchecked(addr_of_mut!(self.bytes)),
|
||||
cursor: NonNull::new_unchecked(addr_of_mut!(self.cursor)),
|
||||
panic_recovery: NonNull::new_unchecked(addr_of_mut!(self.panic_recovery)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -130,6 +133,7 @@ impl RawCommandQueue {
|
||||
Self {
|
||||
bytes: NonNull::new_unchecked(Box::into_raw(Box::default())),
|
||||
cursor: NonNull::new_unchecked(Box::into_raw(Box::new(0usize))),
|
||||
panic_recovery: NonNull::new_unchecked(Box::into_raw(Box::default())),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -164,17 +168,23 @@ impl RawCommandQueue {
|
||||
}
|
||||
|
||||
let meta = CommandMeta {
|
||||
consume_command_and_get_size: |command, world, mut cursor| {
|
||||
// SAFETY: Pointer is assured to be valid in `CommandQueue.apply_or_drop_queued`
|
||||
unsafe { *cursor.as_mut() += std::mem::size_of::<C>() }
|
||||
consume_command_and_get_size: |command, world, cursor| {
|
||||
*cursor += std::mem::size_of::<C>();
|
||||
|
||||
// SAFETY: According to the invariants of `CommandMeta.consume_command_and_get_size`,
|
||||
// `command` must point to a value of type `C`.
|
||||
let command: C = unsafe { command.read_unaligned() };
|
||||
match world {
|
||||
// Apply command to the provided world...
|
||||
// SAFETY: Calller ensures pointer is not null
|
||||
Some(mut world) => command.apply(unsafe { world.as_mut() }),
|
||||
Some(mut world) => {
|
||||
// SAFETY: Caller ensures pointer is not null
|
||||
let world = unsafe { world.as_mut() };
|
||||
command.apply(world);
|
||||
// The command may have queued up world commands, which we flush here to ensure they are also picked up.
|
||||
// If the current command queue already the World Command queue, this will still behave appropriately because the global cursor
|
||||
// is still at the current `stop`, ensuring only the newly queued Commands will be applied.
|
||||
world.flush();
|
||||
}
|
||||
// ...or discard it.
|
||||
None => drop(command),
|
||||
}
|
||||
@ -222,50 +232,79 @@ impl RawCommandQueue {
|
||||
pub(crate) unsafe fn apply_or_drop_queued(&mut self, world: Option<NonNull<World>>) {
|
||||
// SAFETY: If this is the command queue on world, world will not be dropped as we have a mutable reference
|
||||
// If this is not the command queue on world we have exclusive ownership and self will not be mutated
|
||||
while *self.cursor.as_ref() < self.bytes.as_ref().len() {
|
||||
let start = *self.cursor.as_ref();
|
||||
let stop = self.bytes.as_ref().len();
|
||||
let mut local_cursor = start;
|
||||
// SAFETY: we are setting the global cursor to the current length to prevent the executing commands from applying
|
||||
// the remaining commands currently in this list. This is safe.
|
||||
*self.cursor.as_mut() = stop;
|
||||
|
||||
while local_cursor < stop {
|
||||
// SAFETY: The cursor is either at the start of the buffer, or just after the previous command.
|
||||
// Since we know that the cursor is in bounds, it must point to the start of a new command.
|
||||
let meta = unsafe {
|
||||
self.bytes
|
||||
.as_mut()
|
||||
.as_mut_ptr()
|
||||
.add(*self.cursor.as_ref())
|
||||
.add(local_cursor)
|
||||
.cast::<CommandMeta>()
|
||||
.read_unaligned()
|
||||
};
|
||||
|
||||
// Advance to the bytes just after `meta`, which represent a type-erased command.
|
||||
// SAFETY: For most types of `Command`, the pointer immediately following the metadata
|
||||
// is guaranteed to be in bounds. If the command is a zero-sized type (ZST), then the cursor
|
||||
// might be 1 byte past the end of the buffer, which is safe.
|
||||
unsafe { *self.cursor.as_mut() += std::mem::size_of::<CommandMeta>() };
|
||||
local_cursor += std::mem::size_of::<CommandMeta>();
|
||||
// Construct an owned pointer to the command.
|
||||
// SAFETY: It is safe to transfer ownership out of `self.bytes`, since the increment of `cursor` above
|
||||
// guarantees that nothing stored in the buffer will get observed after this function ends.
|
||||
// `cmd` points to a valid address of a stored command, so it must be non-null.
|
||||
let cmd = unsafe {
|
||||
OwningPtr::<Unaligned>::new(std::ptr::NonNull::new_unchecked(
|
||||
self.bytes
|
||||
.as_mut()
|
||||
.as_mut_ptr()
|
||||
.add(*self.cursor.as_ref())
|
||||
.cast(),
|
||||
self.bytes.as_mut().as_mut_ptr().add(local_cursor).cast(),
|
||||
))
|
||||
};
|
||||
// SAFETY: The data underneath the cursor must correspond to the type erased in metadata,
|
||||
// since they were stored next to each other by `.push()`.
|
||||
// For ZSTs, the type doesn't matter as long as the pointer is non-null.
|
||||
// This also advances the cursor past the command. For ZSTs, the cursor will not move.
|
||||
// At this point, it will either point to the next `CommandMeta`,
|
||||
// or the cursor will be out of bounds and the loop will end.
|
||||
unsafe { (meta.consume_command_and_get_size)(cmd, world, self.cursor) };
|
||||
let result = panic::catch_unwind(AssertUnwindSafe(|| {
|
||||
// SAFETY: The data underneath the cursor must correspond to the type erased in metadata,
|
||||
// since they were stored next to each other by `.push()`.
|
||||
// For ZSTs, the type doesn't matter as long as the pointer is non-null.
|
||||
// This also advances the cursor past the command. For ZSTs, the cursor will not move.
|
||||
// At this point, it will either point to the next `CommandMeta`,
|
||||
// or the cursor will be out of bounds and the loop will end.
|
||||
unsafe { (meta.consume_command_and_get_size)(cmd, world, &mut local_cursor) };
|
||||
}));
|
||||
|
||||
if let Err(payload) = result {
|
||||
// local_cursor now points to the location _after_ the panicked command.
|
||||
// Add the remaining commands that _would have_ been applied to the
|
||||
// panic_recovery queue.
|
||||
//
|
||||
// This uses `current_stop` instead of `stop` to account for any commands
|
||||
// that were queued _during_ this panic.
|
||||
//
|
||||
// This is implemented in such a way that if apply_or_drop_queued() are nested recursively in,
|
||||
// an applied Command, the correct command order will be retained.
|
||||
let panic_recovery = self.panic_recovery.as_mut();
|
||||
let bytes = self.bytes.as_mut();
|
||||
let current_stop = bytes.len();
|
||||
panic_recovery.extend_from_slice(&bytes[local_cursor..current_stop]);
|
||||
bytes.set_len(start);
|
||||
*self.cursor.as_mut() = start;
|
||||
|
||||
// This was the "top of the apply stack". If we are _not_ at the top of the apply stack,
|
||||
// when we call`resume_unwind" the caller "closer to the top" will catch the unwind and do this check,
|
||||
// until we reach the top.
|
||||
if start == 0 {
|
||||
bytes.append(panic_recovery);
|
||||
}
|
||||
panic::resume_unwind(payload);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset the buffer, so it can be reused after this function ends.
|
||||
// SAFETY: `set_len(0)` is always valid.
|
||||
// Reset the buffer: all commands past the original `start` cursor have been applied.
|
||||
// SAFETY: we are setting the length of bytes to the original length, minus the length of the original
|
||||
// list of commands being considered. All bytes remaining in the Vec are still valid, unapplied commands.
|
||||
unsafe {
|
||||
self.bytes.as_mut().set_len(0);
|
||||
*self.cursor.as_mut() = 0;
|
||||
self.bytes.as_mut().set_len(start);
|
||||
*self.cursor.as_mut() = start;
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -287,11 +326,18 @@ impl SystemBuffer for CommandQueue {
|
||||
let _span_guard = _system_meta.commands_span.enter();
|
||||
self.apply(world);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn queue(&mut self, _system_meta: &SystemMeta, mut world: DeferredWorld) {
|
||||
world.commands().append(self);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate as bevy_ecs;
|
||||
use crate::system::Resource;
|
||||
use std::{
|
||||
panic::AssertUnwindSafe,
|
||||
sync::{
|
||||
@ -412,10 +458,6 @@ mod test {
|
||||
queue.apply(&mut world);
|
||||
}));
|
||||
|
||||
// even though the first command panicking.
|
||||
// the cursor was incremented.
|
||||
assert!(queue.cursor > 0);
|
||||
|
||||
// Even though the first command panicked, it's still ok to push
|
||||
// more commands.
|
||||
queue.push(SpawnCommand);
|
||||
@ -424,6 +466,37 @@ mod test {
|
||||
assert_eq!(world.entities().len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_command_queue_inner_nested_panic_safe() {
|
||||
std::panic::set_hook(Box::new(|_| {}));
|
||||
|
||||
#[derive(Resource, Default)]
|
||||
struct Order(Vec<usize>);
|
||||
|
||||
let mut world = World::new();
|
||||
world.init_resource::<Order>();
|
||||
|
||||
fn add_index(index: usize) -> impl Command {
|
||||
move |world: &mut World| world.resource_mut::<Order>().0.push(index)
|
||||
}
|
||||
world.commands().add(add_index(1));
|
||||
world.commands().add(|world: &mut World| {
|
||||
world.commands().add(add_index(2));
|
||||
world.commands().add(PanicCommand("I panic!".to_owned()));
|
||||
world.commands().add(add_index(3));
|
||||
world.flush_commands();
|
||||
});
|
||||
world.commands().add(add_index(4));
|
||||
|
||||
let _ = std::panic::catch_unwind(AssertUnwindSafe(|| {
|
||||
world.flush_commands();
|
||||
}));
|
||||
|
||||
world.commands().add(add_index(5));
|
||||
world.flush_commands();
|
||||
assert_eq!(&world.resource::<Order>().0, &[1, 2, 3, 4, 5]);
|
||||
}
|
||||
|
||||
// NOTE: `CommandQueue` is `Send` because `Command` is send.
|
||||
// If the `Command` trait gets reworked to be non-send, `CommandQueue`
|
||||
// should be reworked.
|
||||
|
23
crates/bevy_ecs/src/world/component_constants.rs
Normal file
23
crates/bevy_ecs/src/world/component_constants.rs
Normal file
@ -0,0 +1,23 @@
|
||||
use super::*;
|
||||
use crate::{self as bevy_ecs};
|
||||
/// Internal components used by bevy with a fixed component id.
|
||||
/// Constants are used to skip [`TypeId`] lookups in hot paths.
|
||||
|
||||
/// [`ComponentId`] for [`OnAdd`]
|
||||
pub const ON_ADD: ComponentId = ComponentId::new(0);
|
||||
/// [`ComponentId`] for [`OnInsert`]
|
||||
pub const ON_INSERT: ComponentId = ComponentId::new(1);
|
||||
/// [`ComponentId`] for [`OnRemove`]
|
||||
pub const ON_REMOVE: ComponentId = ComponentId::new(2);
|
||||
|
||||
/// Trigger emitted when a component is added to an entity.
|
||||
#[derive(Event)]
|
||||
pub struct OnAdd;
|
||||
|
||||
/// Trigger emitted when a component is inserted on to to an entity.
|
||||
#[derive(Event)]
|
||||
pub struct OnInsert;
|
||||
|
||||
/// Trigger emitted when a component is removed from an entity.
|
||||
#[derive(Event)]
|
||||
pub struct OnRemove;
|
@ -1,10 +1,12 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use crate::{
|
||||
archetype::Archetype,
|
||||
change_detection::MutUntyped,
|
||||
component::ComponentId,
|
||||
entity::Entity,
|
||||
event::{Event, EventId, Events, SendBatchIds},
|
||||
observer::{Observers, TriggerTargets},
|
||||
prelude::{Component, QueryState},
|
||||
query::{QueryData, QueryFilter},
|
||||
system::{Commands, Query, Resource},
|
||||
@ -52,6 +54,12 @@ impl<'w> From<&'w mut World> for DeferredWorld<'w> {
|
||||
}
|
||||
|
||||
impl<'w> DeferredWorld<'w> {
|
||||
/// Reborrow self as a new instance of [`DeferredWorld`]
|
||||
#[inline]
|
||||
pub fn reborrow(&mut self) -> DeferredWorld {
|
||||
DeferredWorld { world: self.world }
|
||||
}
|
||||
|
||||
/// Creates a [`Commands`] instance that pushes to the world's command queue
|
||||
#[inline]
|
||||
pub fn commands(&mut self) -> Commands {
|
||||
@ -65,37 +73,42 @@ impl<'w> DeferredWorld<'w> {
|
||||
/// Returns `None` if the `entity` does not have a [`Component`] of the given type.
|
||||
#[inline]
|
||||
pub fn get_mut<T: Component>(&mut self, entity: Entity) -> Option<Mut<T>> {
|
||||
// SAFETY: &mut self ensure that there are no outstanding accesses to the component
|
||||
// SAFETY:
|
||||
// - `as_unsafe_world_cell` is the only thing that is borrowing world
|
||||
// - `as_unsafe_world_cell` provides mutable permission to everything
|
||||
// - `&mut self` ensures no other borrows on world data
|
||||
unsafe { self.world.get_entity(entity)?.get_mut() }
|
||||
}
|
||||
|
||||
/// Retrieves an [`EntityMut`] that exposes read and write operations for the given `entity`.
|
||||
/// This will panic if the `entity` does not exist. Use [`Self::get_entity_mut`] if you want
|
||||
/// to check for entity existence instead of implicitly panic-ing.
|
||||
#[inline]
|
||||
#[track_caller]
|
||||
pub fn entity_mut(&mut self, entity: Entity) -> EntityMut {
|
||||
#[inline(never)]
|
||||
#[cold]
|
||||
#[track_caller]
|
||||
fn panic_no_entity(entity: Entity) -> ! {
|
||||
panic!("Entity {entity:?} does not exist");
|
||||
}
|
||||
|
||||
match self.get_entity_mut(entity) {
|
||||
Some(entity) => entity,
|
||||
None => panic_no_entity(entity),
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves an [`EntityMut`] that exposes read and write operations for the given `entity`.
|
||||
/// Returns [`None`] if the `entity` does not exist.
|
||||
/// Instead of unwrapping the value returned from this function, prefer [`Self::entity_mut`].
|
||||
#[inline]
|
||||
pub fn get_entity_mut(&mut self, entity: Entity) -> Option<EntityMut> {
|
||||
let location = self.entities.get(entity)?;
|
||||
// SAFETY: `entity` exists and `location` is that entity's location
|
||||
Some(unsafe { EntityMut::new(UnsafeEntityCell::new(self.world, entity, location)) })
|
||||
// SAFETY: if the Entity is invalid, the function returns early.
|
||||
// Additionally, Entities::get(entity) returns the correct EntityLocation if the entity exists.
|
||||
let entity_cell = UnsafeEntityCell::new(self.as_unsafe_world_cell(), entity, location);
|
||||
// SAFETY: The UnsafeEntityCell has read access to the entire world.
|
||||
let entity_ref = unsafe { EntityMut::new(entity_cell) };
|
||||
Some(entity_ref)
|
||||
}
|
||||
|
||||
/// Retrieves an [`EntityMut`] that exposes read and write operations for the given `entity`.
|
||||
/// This will panic if the `entity` does not exist. Use [`Self::get_entity_mut`] if you want
|
||||
/// to check for entity existence instead of implicitly panic-ing.
|
||||
#[inline]
|
||||
pub fn entity_mut(&mut self, entity: Entity) -> EntityMut {
|
||||
#[inline(never)]
|
||||
#[cold]
|
||||
fn panic_no_entity(entity: Entity) -> ! {
|
||||
panic!("Entity {entity:?} does not exist");
|
||||
}
|
||||
|
||||
match self.get_entity_mut(entity) {
|
||||
Some(entity) => entity,
|
||||
None => panic_no_entity(entity),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns [`Query`] for the given [`QueryState`], which is used to efficiently
|
||||
@ -266,14 +279,17 @@ impl<'w> DeferredWorld<'w> {
|
||||
#[inline]
|
||||
pub(crate) unsafe fn trigger_on_add(
|
||||
&mut self,
|
||||
archetype: &Archetype,
|
||||
entity: Entity,
|
||||
targets: impl Iterator<Item = ComponentId>,
|
||||
) {
|
||||
for component_id in targets {
|
||||
// SAFETY: Caller ensures that these components exist
|
||||
let hooks = unsafe { self.components().get_info_unchecked(component_id) }.hooks();
|
||||
if let Some(hook) = hooks.on_add {
|
||||
hook(DeferredWorld { world: self.world }, entity, component_id);
|
||||
if archetype.has_add_hook() {
|
||||
for component_id in targets {
|
||||
// SAFETY: Caller ensures that these components exist
|
||||
let hooks = unsafe { self.components().get_info_unchecked(component_id) }.hooks();
|
||||
if let Some(hook) = hooks.on_add {
|
||||
hook(DeferredWorld { world: self.world }, entity, component_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -285,14 +301,17 @@ impl<'w> DeferredWorld<'w> {
|
||||
#[inline]
|
||||
pub(crate) unsafe fn trigger_on_insert(
|
||||
&mut self,
|
||||
archetype: &Archetype,
|
||||
entity: Entity,
|
||||
targets: impl Iterator<Item = ComponentId>,
|
||||
) {
|
||||
for component_id in targets {
|
||||
// SAFETY: Caller ensures that these components exist
|
||||
let hooks = unsafe { self.world.components().get_info_unchecked(component_id) }.hooks();
|
||||
if let Some(hook) = hooks.on_insert {
|
||||
hook(DeferredWorld { world: self.world }, entity, component_id);
|
||||
if archetype.has_insert_hook() {
|
||||
for component_id in targets {
|
||||
// SAFETY: Caller ensures that these components exist
|
||||
let hooks = unsafe { self.components().get_info_unchecked(component_id) }.hooks();
|
||||
if let Some(hook) = hooks.on_insert {
|
||||
hook(DeferredWorld { world: self.world }, entity, component_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -304,15 +323,67 @@ impl<'w> DeferredWorld<'w> {
|
||||
#[inline]
|
||||
pub(crate) unsafe fn trigger_on_remove(
|
||||
&mut self,
|
||||
archetype: &Archetype,
|
||||
entity: Entity,
|
||||
targets: impl Iterator<Item = ComponentId>,
|
||||
) {
|
||||
for component_id in targets {
|
||||
// SAFETY: Caller ensures that these components exist
|
||||
let hooks = unsafe { self.world.components().get_info_unchecked(component_id) }.hooks();
|
||||
if let Some(hook) = hooks.on_remove {
|
||||
hook(DeferredWorld { world: self.world }, entity, component_id);
|
||||
if archetype.has_remove_hook() {
|
||||
for component_id in targets {
|
||||
let hooks =
|
||||
// SAFETY: Caller ensures that these components exist
|
||||
unsafe { self.world.components().get_info_unchecked(component_id) }.hooks();
|
||||
if let Some(hook) = hooks.on_remove {
|
||||
hook(DeferredWorld { world: self.world }, entity, component_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Triggers all event observers for [`ComponentId`] in target.
|
||||
///
|
||||
/// # Safety
|
||||
/// Caller must ensure observers listening for `event` can accept ZST pointers
|
||||
#[inline]
|
||||
pub(crate) unsafe fn trigger_observers(
|
||||
&mut self,
|
||||
event: ComponentId,
|
||||
entity: Entity,
|
||||
components: impl Iterator<Item = ComponentId>,
|
||||
) {
|
||||
Observers::invoke(self.reborrow(), event, entity, components, &mut ());
|
||||
}
|
||||
|
||||
/// Triggers all event observers for [`ComponentId`] in target.
|
||||
///
|
||||
/// # Safety
|
||||
/// Caller must ensure `E` is accessible as the type represented by `event`
|
||||
#[inline]
|
||||
pub(crate) unsafe fn trigger_observers_with_data<E>(
|
||||
&mut self,
|
||||
event: ComponentId,
|
||||
entity: Entity,
|
||||
components: impl Iterator<Item = ComponentId>,
|
||||
data: &mut E,
|
||||
) {
|
||||
Observers::invoke(self.reborrow(), event, entity, components, data);
|
||||
}
|
||||
|
||||
/// Sends a "global" [`Trigger`] without any targets.
|
||||
pub fn trigger<T: Event>(&mut self, trigger: impl Event) {
|
||||
self.commands().trigger(trigger);
|
||||
}
|
||||
|
||||
/// Sends a [`Trigger`] with the given `targets`.
|
||||
pub fn trigger_targets(&mut self, trigger: impl Event, targets: impl TriggerTargets) {
|
||||
self.commands().trigger_targets(trigger, targets);
|
||||
}
|
||||
|
||||
/// Gets an [`UnsafeWorldCell`] containing the underlying world.
|
||||
///
|
||||
/// # Safety
|
||||
/// - must only be used to to make non-structural ECS changes
|
||||
#[inline]
|
||||
pub(crate) fn as_unsafe_world_cell(&mut self) -> UnsafeWorldCell {
|
||||
self.world
|
||||
}
|
||||
}
|
||||
|
@ -4,16 +4,19 @@ use crate::{
|
||||
change_detection::MutUntyped,
|
||||
component::{Component, ComponentId, ComponentTicks, Components, StorageType},
|
||||
entity::{Entities, Entity, EntityLocation},
|
||||
event::Event,
|
||||
observer::{Observer, Observers},
|
||||
query::Access,
|
||||
removal_detection::RemovedComponentEvents,
|
||||
storage::Storages,
|
||||
world::{Mut, World},
|
||||
system::IntoObserverSystem,
|
||||
world::{DeferredWorld, Mut, World},
|
||||
};
|
||||
use bevy_ptr::{OwningPtr, Ptr};
|
||||
use std::{any::TypeId, marker::PhantomData};
|
||||
use thiserror::Error;
|
||||
|
||||
use super::{unsafe_world_cell::UnsafeEntityCell, Ref};
|
||||
use super::{unsafe_world_cell::UnsafeEntityCell, Ref, ON_REMOVE};
|
||||
|
||||
/// A read-only reference to a particular [`Entity`] and all of its components.
|
||||
///
|
||||
@ -876,6 +879,7 @@ impl<'w> EntityWorldMut<'w> {
|
||||
&mut world.archetypes,
|
||||
storages,
|
||||
components,
|
||||
&world.observers,
|
||||
old_location.archetype_id,
|
||||
bundle_info,
|
||||
false,
|
||||
@ -898,11 +902,14 @@ impl<'w> EntityWorldMut<'w> {
|
||||
)
|
||||
};
|
||||
|
||||
if old_archetype.has_on_remove() {
|
||||
// SAFETY: All components in the archetype exist in world
|
||||
unsafe {
|
||||
deferred_world.trigger_on_remove(entity, bundle_info.iter_components());
|
||||
}
|
||||
// SAFETY: all bundle components exist in World
|
||||
unsafe {
|
||||
trigger_on_remove_hooks_and_observers(
|
||||
&mut deferred_world,
|
||||
old_archetype,
|
||||
entity,
|
||||
bundle_info,
|
||||
);
|
||||
}
|
||||
|
||||
let archetypes = &mut world.archetypes;
|
||||
@ -1053,6 +1060,7 @@ impl<'w> EntityWorldMut<'w> {
|
||||
&mut world.archetypes,
|
||||
&mut world.storages,
|
||||
&world.components,
|
||||
&world.observers,
|
||||
location.archetype_id,
|
||||
bundle_info,
|
||||
// components from the bundle that are not present on the entity are ignored
|
||||
@ -1075,11 +1083,14 @@ impl<'w> EntityWorldMut<'w> {
|
||||
)
|
||||
};
|
||||
|
||||
if old_archetype.has_on_remove() {
|
||||
// SAFETY: All components in the archetype exist in world
|
||||
unsafe {
|
||||
deferred_world.trigger_on_remove(entity, bundle_info.iter_components());
|
||||
}
|
||||
// SAFETY: all bundle components exist in World
|
||||
unsafe {
|
||||
trigger_on_remove_hooks_and_observers(
|
||||
&mut deferred_world,
|
||||
old_archetype,
|
||||
entity,
|
||||
bundle_info,
|
||||
);
|
||||
}
|
||||
|
||||
let old_archetype = &world.archetypes[location.archetype_id];
|
||||
@ -1209,10 +1220,11 @@ impl<'w> EntityWorldMut<'w> {
|
||||
(&*archetype, world.into_deferred())
|
||||
};
|
||||
|
||||
if archetype.has_on_remove() {
|
||||
// SAFETY: All components in the archetype exist in world
|
||||
unsafe {
|
||||
deferred_world.trigger_on_remove(self.entity, archetype.components());
|
||||
// SAFETY: All components in the archetype exist in world
|
||||
unsafe {
|
||||
deferred_world.trigger_on_remove(archetype, self.entity, archetype.components());
|
||||
if archetype.has_remove_observer() {
|
||||
deferred_world.trigger_observers(ON_REMOVE, self.entity, archetype.components());
|
||||
}
|
||||
}
|
||||
|
||||
@ -1276,12 +1288,12 @@ impl<'w> EntityWorldMut<'w> {
|
||||
world.archetypes[moved_location.archetype_id]
|
||||
.set_entity_table_row(moved_location.archetype_row, table_row);
|
||||
}
|
||||
world.flush_commands();
|
||||
world.flush();
|
||||
}
|
||||
|
||||
/// Ensures any commands triggered by the actions of Self are applied, equivalent to [`World::flush_commands`]
|
||||
/// Ensures any commands triggered by the actions of Self are applied, equivalent to [`World::flush`]
|
||||
pub fn flush(self) -> Entity {
|
||||
self.world.flush_commands();
|
||||
self.world.flush();
|
||||
self.entity
|
||||
}
|
||||
|
||||
@ -1397,6 +1409,30 @@ impl<'w> EntityWorldMut<'w> {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an [`Observer`](crate::observer::Observer) listening for events of type `E` targeting this entity.
|
||||
/// In order to trigger the callback the entity must also match the query when the event is fired.
|
||||
pub fn observe<E: Event, B: Bundle, M>(
|
||||
&mut self,
|
||||
observer: impl IntoObserverSystem<E, B, M>,
|
||||
) -> &mut Self {
|
||||
self.world
|
||||
.spawn(Observer::new(observer).with_entity(self.entity));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// SAFETY: all components in the archetype must exist in world
|
||||
unsafe fn trigger_on_remove_hooks_and_observers(
|
||||
deferred_world: &mut DeferredWorld,
|
||||
archetype: &Archetype,
|
||||
entity: Entity,
|
||||
bundle_info: &BundleInfo,
|
||||
) {
|
||||
deferred_world.trigger_on_remove(archetype, entity, bundle_info.iter_components());
|
||||
if archetype.has_remove_observer() {
|
||||
deferred_world.trigger_observers(ON_REMOVE, entity, bundle_info.iter_components());
|
||||
}
|
||||
}
|
||||
|
||||
/// A view into a single entity and component in a world, which may either be vacant or occupied.
|
||||
@ -2292,6 +2328,7 @@ unsafe fn remove_bundle_from_archetype(
|
||||
archetypes: &mut Archetypes,
|
||||
storages: &mut Storages,
|
||||
components: &Components,
|
||||
observers: &Observers,
|
||||
archetype_id: ArchetypeId,
|
||||
bundle_info: &BundleInfo,
|
||||
intersection: bool,
|
||||
@ -2362,6 +2399,7 @@ unsafe fn remove_bundle_from_archetype(
|
||||
|
||||
let new_archetype_id = archetypes.get_id_or_insert(
|
||||
components,
|
||||
observers,
|
||||
next_table_id,
|
||||
next_table_components,
|
||||
next_sparse_set_components,
|
||||
|
@ -1,20 +1,25 @@
|
||||
//! Defines the [`World`] and APIs for accessing it directly.
|
||||
|
||||
pub(crate) mod command_queue;
|
||||
mod component_constants;
|
||||
mod deferred_world;
|
||||
mod entity_ref;
|
||||
pub mod error;
|
||||
mod identifier;
|
||||
mod spawn_batch;
|
||||
pub mod unsafe_world_cell;
|
||||
|
||||
pub use crate::change_detection::{Mut, Ref, CHECK_TICK_THRESHOLD};
|
||||
pub use crate::world::command_queue::CommandQueue;
|
||||
use crate::{component::ComponentInitializer, entity::EntityHashSet};
|
||||
pub use crate::{
|
||||
change_detection::{Mut, Ref, CHECK_TICK_THRESHOLD},
|
||||
world::command_queue::CommandQueue,
|
||||
};
|
||||
pub use component_constants::*;
|
||||
pub use deferred_world::DeferredWorld;
|
||||
pub use entity_ref::{
|
||||
EntityMut, EntityRef, EntityWorldMut, Entry, FilteredEntityMut, FilteredEntityRef,
|
||||
OccupiedEntry, VacantEntry,
|
||||
};
|
||||
pub use identifier::WorldId;
|
||||
pub use spawn_batch::*;
|
||||
|
||||
use crate::{
|
||||
@ -25,8 +30,9 @@ use crate::{
|
||||
Component, ComponentDescriptor, ComponentHooks, ComponentId, ComponentInfo, ComponentTicks,
|
||||
Components, Tick,
|
||||
},
|
||||
entity::{AllocAtWithoutReplacement, Entities, Entity, EntityLocation},
|
||||
entity::{AllocAtWithoutReplacement, Entities, Entity, EntityHashSet, EntityLocation},
|
||||
event::{Event, EventId, Events, SendBatchIds},
|
||||
observer::Observers,
|
||||
query::{DebugCheckedUnwrap, QueryData, QueryEntityError, QueryFilter, QueryState},
|
||||
removal_detection::RemovedComponentEvents,
|
||||
schedule::{Schedule, ScheduleLabel, Schedules},
|
||||
@ -43,10 +49,7 @@ use std::{
|
||||
mem::MaybeUninit,
|
||||
sync::atomic::{AtomicU32, Ordering},
|
||||
};
|
||||
mod identifier;
|
||||
|
||||
use self::unsafe_world_cell::{UnsafeEntityCell, UnsafeWorldCell};
|
||||
pub use identifier::WorldId;
|
||||
use unsafe_world_cell::{UnsafeEntityCell, UnsafeWorldCell};
|
||||
|
||||
/// A [`World`] mutation.
|
||||
///
|
||||
@ -110,30 +113,36 @@ pub struct World {
|
||||
pub(crate) archetypes: Archetypes,
|
||||
pub(crate) storages: Storages,
|
||||
pub(crate) bundles: Bundles,
|
||||
pub(crate) observers: Observers,
|
||||
pub(crate) removed_components: RemovedComponentEvents,
|
||||
pub(crate) change_tick: AtomicU32,
|
||||
pub(crate) last_change_tick: Tick,
|
||||
pub(crate) last_check_tick: Tick,
|
||||
pub(crate) last_trigger_id: u32,
|
||||
pub(crate) command_queue: RawCommandQueue,
|
||||
}
|
||||
|
||||
impl Default for World {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
let mut world = Self {
|
||||
id: WorldId::new().expect("More `bevy` `World`s have been created than is supported"),
|
||||
entities: Entities::new(),
|
||||
components: Default::default(),
|
||||
archetypes: Archetypes::new(),
|
||||
storages: Default::default(),
|
||||
bundles: Default::default(),
|
||||
observers: Observers::default(),
|
||||
removed_components: Default::default(),
|
||||
// Default value is `1`, and `last_change_tick`s default to `0`, such that changes
|
||||
// are detected on first system runs and for direct world queries.
|
||||
change_tick: AtomicU32::new(1),
|
||||
last_change_tick: Tick::new(0),
|
||||
last_check_tick: Tick::new(0),
|
||||
last_trigger_id: 0,
|
||||
command_queue: RawCommandQueue::new(),
|
||||
}
|
||||
};
|
||||
world.bootstrap();
|
||||
world
|
||||
}
|
||||
}
|
||||
|
||||
@ -149,6 +158,14 @@ impl Drop for World {
|
||||
}
|
||||
|
||||
impl World {
|
||||
/// This performs initialization that _must_ happen for every [`World`] immediately upon creation (such as claiming specific component ids).
|
||||
/// This _must_ be run as part of constructing a [`World`], before it is returned to the caller.
|
||||
#[inline]
|
||||
fn bootstrap(&mut self) {
|
||||
assert_eq!(ON_ADD, self.init_component::<OnAdd>());
|
||||
assert_eq!(ON_INSERT, self.init_component::<OnInsert>());
|
||||
assert_eq!(ON_REMOVE, self.init_component::<OnRemove>());
|
||||
}
|
||||
/// Creates a new empty [`World`].
|
||||
///
|
||||
/// # Panics
|
||||
@ -219,15 +236,6 @@ impl World {
|
||||
&self.bundles
|
||||
}
|
||||
|
||||
/// Creates a [`ComponentInitializer`] for this world.
|
||||
#[inline]
|
||||
pub fn component_initializer(&mut self) -> ComponentInitializer {
|
||||
ComponentInitializer {
|
||||
components: &mut self.components,
|
||||
storages: &mut self.storages,
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves this world's [`RemovedComponentEvents`] collection
|
||||
#[inline]
|
||||
pub fn removed_components(&self) -> &RemovedComponentEvents {
|
||||
@ -235,7 +243,7 @@ impl World {
|
||||
}
|
||||
|
||||
/// Creates a new [`Commands`] instance that writes to the world's command queue
|
||||
/// Use [`World::flush_commands`] to apply all queued commands
|
||||
/// Use [`World::flush`] to apply all queued commands
|
||||
#[inline]
|
||||
pub fn commands(&mut self) -> Commands {
|
||||
// SAFETY: command_queue is stored on world and always valid while the world exists
|
||||
@ -502,7 +510,7 @@ impl World {
|
||||
/// scheme worked out to share an ID space (which doesn't happen by default).
|
||||
#[inline]
|
||||
pub fn get_or_spawn(&mut self, entity: Entity) -> Option<EntityWorldMut> {
|
||||
self.flush_entities();
|
||||
self.flush();
|
||||
match self.entities.alloc_at_without_replacement(entity) {
|
||||
AllocAtWithoutReplacement::Exists(location) => {
|
||||
// SAFETY: `entity` exists and `location` is that entity's location
|
||||
@ -895,7 +903,7 @@ impl World {
|
||||
/// assert_eq!(position.x, 0.0);
|
||||
/// ```
|
||||
pub fn spawn_empty(&mut self) -> EntityWorldMut {
|
||||
self.flush_entities();
|
||||
self.flush();
|
||||
let entity = self.entities.alloc();
|
||||
// SAFETY: entity was just allocated
|
||||
unsafe { self.spawn_at_empty_internal(entity) }
|
||||
@ -961,7 +969,7 @@ impl World {
|
||||
/// assert_eq!(position.x, 2.0);
|
||||
/// ```
|
||||
pub fn spawn<B: Bundle>(&mut self, bundle: B) -> EntityWorldMut {
|
||||
self.flush_entities();
|
||||
self.flush();
|
||||
let change_tick = self.change_tick();
|
||||
let entity = self.entities.alloc();
|
||||
let entity_location = {
|
||||
@ -1092,6 +1100,7 @@ impl World {
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn despawn(&mut self, entity: Entity) -> bool {
|
||||
self.flush();
|
||||
if let Some(entity) = self.get_entity_mut(entity) {
|
||||
entity.despawn();
|
||||
true
|
||||
@ -1113,9 +1122,9 @@ impl World {
|
||||
/// By clearing this internal state, the world "forgets" about those changes, allowing a new round
|
||||
/// of detection to be recorded.
|
||||
///
|
||||
/// When using `bevy_ecs` as part of the full Bevy engine, this method is added as a system to the
|
||||
/// main app, to run during `Last`, so you don't need to call it manually. When using `bevy_ecs`
|
||||
/// as a separate standalone crate however, you need to call this manually.
|
||||
/// When using `bevy_ecs` as part of the full Bevy engine, this method is called automatically
|
||||
/// by `bevy_app::App::update` and `bevy_app::SubApp::update`, so you don't need to call it manually.
|
||||
/// When using `bevy_ecs` as a separate standalone crate however, you do need to call this manually.
|
||||
///
|
||||
/// ```
|
||||
/// # use bevy_ecs::prelude::*;
|
||||
@ -1743,7 +1752,7 @@ impl World {
|
||||
I::IntoIter: Iterator<Item = (Entity, B)>,
|
||||
B: Bundle,
|
||||
{
|
||||
self.flush_entities();
|
||||
self.flush();
|
||||
|
||||
let change_tick = self.change_tick();
|
||||
|
||||
@ -2029,9 +2038,15 @@ impl World {
|
||||
}
|
||||
}
|
||||
|
||||
/// Calls both [`World::flush_entities`] and [`World::flush_commands`].
|
||||
#[inline]
|
||||
pub fn flush(&mut self) {
|
||||
self.flush_entities();
|
||||
self.flush_commands();
|
||||
}
|
||||
|
||||
/// Applies any commands in the world's internal [`CommandQueue`].
|
||||
/// This does not apply commands from any systems, only those stored in the world.
|
||||
#[inline]
|
||||
pub fn flush_commands(&mut self) {
|
||||
// SAFETY: `self.command_queue` is only de-allocated in `World`'s `Drop`
|
||||
if !unsafe { self.command_queue.is_empty() } {
|
||||
@ -2082,6 +2097,13 @@ impl World {
|
||||
self.last_change_tick
|
||||
}
|
||||
|
||||
/// Returns the id of the last ECS event that was fired.
|
||||
/// Used internally to ensure observers don't trigger multiple times for the same event.
|
||||
#[inline]
|
||||
pub(crate) fn last_trigger_id(&self) -> u32 {
|
||||
self.last_trigger_id
|
||||
}
|
||||
|
||||
/// Sets [`World::last_change_tick()`] to the specified value during a scope.
|
||||
/// When the scope terminates, it will return to its old value.
|
||||
///
|
||||
|
@ -27,7 +27,7 @@ where
|
||||
pub(crate) fn new(world: &'w mut World, iter: I) -> Self {
|
||||
// Ensure all entity allocations are accounted for so `self.entities` can realloc if
|
||||
// necessary
|
||||
world.flush_entities();
|
||||
world.flush();
|
||||
|
||||
let change_tick = world.change_tick();
|
||||
|
||||
|
@ -9,6 +9,7 @@ use crate::{
|
||||
change_detection::{MutUntyped, Ticks, TicksMut},
|
||||
component::{ComponentId, ComponentTicks, Components, StorageType, Tick, TickCells},
|
||||
entity::{Entities, Entity, EntityLocation},
|
||||
observer::Observers,
|
||||
prelude::Component,
|
||||
removal_detection::RemovedComponentEvents,
|
||||
storage::{Column, ComponentSparseSet, Storages},
|
||||
@ -231,6 +232,13 @@ impl<'w> UnsafeWorldCell<'w> {
|
||||
&unsafe { self.world_metadata() }.removed_components
|
||||
}
|
||||
|
||||
/// Retrieves this world's [`Observers`] collection.
|
||||
pub(crate) unsafe fn observers(self) -> &'w Observers {
|
||||
// SAFETY:
|
||||
// - we only access world metadata
|
||||
&unsafe { self.world_metadata() }.observers
|
||||
}
|
||||
|
||||
/// Retrieves this world's [`Bundles`] collection.
|
||||
#[inline]
|
||||
pub fn bundles(self) -> &'w Bundles {
|
||||
@ -571,6 +579,14 @@ impl<'w> UnsafeWorldCell<'w> {
|
||||
// - caller ensures that we have permission to access the queue
|
||||
unsafe { (*self.0).command_queue.clone() }
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// It is the callers responsibility to ensure that there are no outstanding
|
||||
/// references to `last_trigger_id`.
|
||||
pub(crate) unsafe fn increment_trigger_id(self) {
|
||||
// SAFETY: Caller ensure there are no outstanding references
|
||||
unsafe { (*self.0).last_trigger_id += 1 }
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for UnsafeWorldCell<'_> {
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bevy_encase_derive"
|
||||
version = "0.14.0-dev"
|
||||
version = "0.14.0"
|
||||
edition = "2021"
|
||||
description = "Bevy derive macro for encase"
|
||||
homepage = "https://bevyengine.org"
|
||||
@ -12,7 +12,7 @@ keywords = ["bevy"]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
bevy_macro_utils = { path = "../bevy_macro_utils", version = "0.14.0-dev" }
|
||||
bevy_macro_utils = { path = "../bevy_macro_utils", version = "0.14.0" }
|
||||
encase_derive_impl = "0.8"
|
||||
|
||||
[lints]
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bevy_gilrs"
|
||||
version = "0.14.0-dev"
|
||||
version = "0.14.0"
|
||||
edition = "2021"
|
||||
description = "Gamepad system made using Gilrs for Bevy Engine"
|
||||
homepage = "https://bevyengine.org"
|
||||
@ -10,11 +10,11 @@ keywords = ["bevy"]
|
||||
|
||||
[dependencies]
|
||||
# bevy
|
||||
bevy_app = { path = "../bevy_app", version = "0.14.0-dev" }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" }
|
||||
bevy_input = { path = "../bevy_input", version = "0.14.0-dev" }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" }
|
||||
bevy_time = { path = "../bevy_time", version = "0.14.0-dev" }
|
||||
bevy_app = { path = "../bevy_app", version = "0.14.0" }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0" }
|
||||
bevy_input = { path = "../bevy_input", version = "0.14.0" }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.14.0" }
|
||||
bevy_time = { path = "../bevy_time", version = "0.14.0" }
|
||||
|
||||
# other
|
||||
gilrs = "0.10.1"
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bevy_gizmos"
|
||||
version = "0.14.0-dev"
|
||||
version = "0.14.0"
|
||||
edition = "2021"
|
||||
description = "Provides gizmos for Bevy Engine"
|
||||
homepage = "https://bevyengine.org"
|
||||
@ -14,20 +14,20 @@ webgpu = []
|
||||
|
||||
[dependencies]
|
||||
# Bevy
|
||||
bevy_pbr = { path = "../bevy_pbr", version = "0.14.0-dev", optional = true }
|
||||
bevy_sprite = { path = "../bevy_sprite", version = "0.14.0-dev", optional = true }
|
||||
bevy_app = { path = "../bevy_app", version = "0.14.0-dev" }
|
||||
bevy_color = { path = "../bevy_color", version = "0.14.0-dev" }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" }
|
||||
bevy_math = { path = "../bevy_math", version = "0.14.0-dev" }
|
||||
bevy_asset = { path = "../bevy_asset", version = "0.14.0-dev" }
|
||||
bevy_render = { path = "../bevy_render", version = "0.14.0-dev" }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" }
|
||||
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev" }
|
||||
bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.14.0-dev" }
|
||||
bevy_transform = { path = "../bevy_transform", version = "0.14.0-dev" }
|
||||
bevy_gizmos_macros = { path = "macros", version = "0.14.0-dev" }
|
||||
bevy_time = { path = "../bevy_time", version = "0.14.0-dev" }
|
||||
bevy_pbr = { path = "../bevy_pbr", version = "0.14.0", optional = true }
|
||||
bevy_sprite = { path = "../bevy_sprite", version = "0.14.0", optional = true }
|
||||
bevy_app = { path = "../bevy_app", version = "0.14.0" }
|
||||
bevy_color = { path = "../bevy_color", version = "0.14.1" }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0" }
|
||||
bevy_math = { path = "../bevy_math", version = "0.14.0" }
|
||||
bevy_asset = { path = "../bevy_asset", version = "0.14.0" }
|
||||
bevy_render = { path = "../bevy_render", version = "0.14.0" }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.14.0" }
|
||||
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0" }
|
||||
bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.14.0" }
|
||||
bevy_transform = { path = "../bevy_transform", version = "0.14.0" }
|
||||
bevy_gizmos_macros = { path = "macros", version = "0.14.0" }
|
||||
bevy_time = { path = "../bevy_time", version = "0.14.0" }
|
||||
|
||||
bytemuck = "1.0"
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bevy_gizmos_macros"
|
||||
version = "0.14.0-dev"
|
||||
version = "0.14.0"
|
||||
edition = "2021"
|
||||
description = "Derive implementations for bevy_gizmos"
|
||||
homepage = "https://bevyengine.org"
|
||||
@ -13,7 +13,7 @@ proc-macro = true
|
||||
|
||||
|
||||
[dependencies]
|
||||
bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.14.0-dev" }
|
||||
bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.14.0" }
|
||||
|
||||
syn = "2.0"
|
||||
proc-macro2 = "1.0"
|
||||
|
@ -74,8 +74,8 @@ fn vertex(vertex: VertexInput) -> VertexOutput {
|
||||
let near_clipping_plane_height = length(pos0.xyz - pos1.xyz);
|
||||
|
||||
// We can't use vertex.position_X because we may have changed the clip positions with clip_near_plane
|
||||
let position_a = view.inverse_clip_from_world * clip_a;
|
||||
let position_b = view.inverse_clip_from_world * clip_b;
|
||||
let position_a = view.world_from_clip * clip_a;
|
||||
let position_b = view.world_from_clip * clip_b;
|
||||
let world_distance = length(position_a.xyz - position_b.xyz);
|
||||
|
||||
// Offset to compensate for moved clip positions. If removed dots on lines will slide when position a is ofscreen.
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bevy_gltf"
|
||||
version = "0.14.0-dev"
|
||||
version = "0.14.0"
|
||||
edition = "2021"
|
||||
description = "Bevy Engine GLTF loading"
|
||||
homepage = "https://bevyengine.org"
|
||||
@ -12,29 +12,30 @@ keywords = ["bevy"]
|
||||
dds = ["bevy_render/dds"]
|
||||
pbr_transmission_textures = ["bevy_pbr/pbr_transmission_textures"]
|
||||
pbr_multi_layer_material_textures = []
|
||||
pbr_anisotropy_texture = []
|
||||
|
||||
[dependencies]
|
||||
# bevy
|
||||
bevy_animation = { path = "../bevy_animation", version = "0.14.0-dev", optional = true }
|
||||
bevy_app = { path = "../bevy_app", version = "0.14.0-dev" }
|
||||
bevy_asset = { path = "../bevy_asset", version = "0.14.0-dev" }
|
||||
bevy_color = { path = "../bevy_color", version = "0.14.0-dev" }
|
||||
bevy_core = { path = "../bevy_core", version = "0.14.0-dev" }
|
||||
bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.14.0-dev" }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" }
|
||||
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.14.0-dev" }
|
||||
bevy_math = { path = "../bevy_math", version = "0.14.0-dev" }
|
||||
bevy_pbr = { path = "../bevy_pbr", version = "0.14.0-dev" }
|
||||
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev", features = [
|
||||
bevy_animation = { path = "../bevy_animation", version = "0.14.0", optional = true }
|
||||
bevy_app = { path = "../bevy_app", version = "0.14.0" }
|
||||
bevy_asset = { path = "../bevy_asset", version = "0.14.0" }
|
||||
bevy_color = { path = "../bevy_color", version = "0.14.1" }
|
||||
bevy_core = { path = "../bevy_core", version = "0.14.0" }
|
||||
bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.14.0" }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0" }
|
||||
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.14.0" }
|
||||
bevy_math = { path = "../bevy_math", version = "0.14.0" }
|
||||
bevy_pbr = { path = "../bevy_pbr", version = "0.14.0" }
|
||||
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0", features = [
|
||||
"bevy",
|
||||
] }
|
||||
bevy_render = { path = "../bevy_render", version = "0.14.0-dev" }
|
||||
bevy_scene = { path = "../bevy_scene", version = "0.14.0-dev", features = [
|
||||
bevy_render = { path = "../bevy_render", version = "0.14.0" }
|
||||
bevy_scene = { path = "../bevy_scene", version = "0.14.0", features = [
|
||||
"bevy_render",
|
||||
] }
|
||||
bevy_transform = { path = "../bevy_transform", version = "0.14.0-dev" }
|
||||
bevy_tasks = { path = "../bevy_tasks", version = "0.14.0-dev" }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" }
|
||||
bevy_transform = { path = "../bevy_transform", version = "0.14.0" }
|
||||
bevy_tasks = { path = "../bevy_tasks", version = "0.14.0" }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.14.0" }
|
||||
|
||||
# other
|
||||
gltf = { version = "1.4.0", default-features = false, features = [
|
||||
|
@ -1086,7 +1086,9 @@ fn load_material(
|
||||
clearcoat_normal_texture: clearcoat.clearcoat_normal_texture,
|
||||
anisotropy_strength: anisotropy.anisotropy_strength.unwrap_or_default() as f32,
|
||||
anisotropy_rotation: anisotropy.anisotropy_rotation.unwrap_or_default() as f32,
|
||||
#[cfg(feature = "pbr_anisotropy_texture")]
|
||||
anisotropy_channel: anisotropy.anisotropy_channel,
|
||||
#[cfg(feature = "pbr_anisotropy_texture")]
|
||||
anisotropy_texture: anisotropy.anisotropy_texture,
|
||||
..Default::default()
|
||||
}
|
||||
@ -1898,11 +1900,14 @@ impl ClearcoatExtension {
|
||||
struct AnisotropyExtension {
|
||||
anisotropy_strength: Option<f64>,
|
||||
anisotropy_rotation: Option<f64>,
|
||||
#[cfg(feature = "pbr_anisotropy_texture")]
|
||||
anisotropy_channel: UvChannel,
|
||||
#[cfg(feature = "pbr_anisotropy_texture")]
|
||||
anisotropy_texture: Option<Handle<Image>>,
|
||||
}
|
||||
|
||||
impl AnisotropyExtension {
|
||||
#[allow(unused_variables)]
|
||||
fn parse(
|
||||
load_context: &mut LoadContext,
|
||||
document: &Document,
|
||||
@ -1913,6 +1918,7 @@ impl AnisotropyExtension {
|
||||
.get("KHR_materials_anisotropy")?
|
||||
.as_object()?;
|
||||
|
||||
#[cfg(feature = "pbr_anisotropy_texture")]
|
||||
let (anisotropy_channel, anisotropy_texture) = extension
|
||||
.get("anisotropyTexture")
|
||||
.and_then(|value| value::from_value::<json::texture::Info>(value.clone()).ok())
|
||||
@ -1927,7 +1933,9 @@ impl AnisotropyExtension {
|
||||
Some(AnisotropyExtension {
|
||||
anisotropy_strength: extension.get("anisotropyStrength").and_then(Value::as_f64),
|
||||
anisotropy_rotation: extension.get("anisotropyRotation").and_then(Value::as_f64),
|
||||
#[cfg(feature = "pbr_anisotropy_texture")]
|
||||
anisotropy_channel: anisotropy_channel.unwrap_or_default(),
|
||||
#[cfg(feature = "pbr_anisotropy_texture")]
|
||||
anisotropy_texture,
|
||||
})
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bevy_hierarchy"
|
||||
version = "0.14.0-dev"
|
||||
version = "0.14.0"
|
||||
edition = "2021"
|
||||
description = "Provides hierarchy functionality for Bevy Engine"
|
||||
homepage = "https://bevyengine.org"
|
||||
@ -16,14 +16,14 @@ reflect = ["bevy_ecs/bevy_reflect", "bevy_reflect"]
|
||||
|
||||
[dependencies]
|
||||
# bevy
|
||||
bevy_app = { path = "../bevy_app", version = "0.14.0-dev", optional = true }
|
||||
bevy_core = { path = "../bevy_core", version = "0.14.0-dev", optional = true }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev", default-features = false }
|
||||
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev", features = [
|
||||
bevy_app = { path = "../bevy_app", version = "0.14.0", optional = true }
|
||||
bevy_core = { path = "../bevy_core", version = "0.14.0", optional = true }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0", default-features = false }
|
||||
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0", features = [
|
||||
"bevy",
|
||||
"smallvec",
|
||||
], optional = true }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.14.0" }
|
||||
|
||||
smallvec = { version = "1.11", features = ["union", "const_generics"] }
|
||||
|
||||
|
@ -612,6 +612,10 @@ impl<'w> BuildWorldChildren for EntityWorldMut<'w> {
|
||||
}
|
||||
|
||||
fn push_children(&mut self, children: &[Entity]) -> &mut Self {
|
||||
if children.is_empty() {
|
||||
return self;
|
||||
}
|
||||
|
||||
let parent = self.id();
|
||||
if children.contains(&parent) {
|
||||
panic!("Cannot push entity as a child of itself.");
|
||||
@ -1242,4 +1246,14 @@ mod tests {
|
||||
let children = query.get(&world, parent).unwrap();
|
||||
assert_eq!(**children, [child]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_children_does_not_insert_empty_children() {
|
||||
let mut world = World::new();
|
||||
let parent = world.spawn_empty().push_children(&[]).id();
|
||||
|
||||
let mut query = world.query::<&Children>();
|
||||
let children = query.get(&world, parent);
|
||||
assert!(children.is_err());
|
||||
}
|
||||
}
|
||||
|
@ -91,6 +91,7 @@ pub trait DespawnRecursiveExt {
|
||||
|
||||
impl DespawnRecursiveExt for EntityCommands<'_> {
|
||||
/// Despawns the provided entity and its children.
|
||||
/// This will emit warnings for any entity that does not exist.
|
||||
fn despawn_recursive(mut self) {
|
||||
let entity = self.id();
|
||||
self.commands().add(DespawnRecursive { entity });
|
||||
@ -105,6 +106,7 @@ impl DespawnRecursiveExt for EntityCommands<'_> {
|
||||
|
||||
impl<'w> DespawnRecursiveExt for EntityWorldMut<'w> {
|
||||
/// Despawns the provided entity and its children.
|
||||
/// This will emit warnings for any entity that does not exist.
|
||||
fn despawn_recursive(self) {
|
||||
let entity = self.id();
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bevy_input"
|
||||
version = "0.14.0-dev"
|
||||
version = "0.14.0"
|
||||
edition = "2021"
|
||||
description = "Provides input functionality for Bevy Engine"
|
||||
homepage = "https://bevyengine.org"
|
||||
@ -14,11 +14,11 @@ serialize = ["serde"]
|
||||
|
||||
[dependencies]
|
||||
# bevy
|
||||
bevy_app = { path = "../bevy_app", version = "0.14.0-dev" }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" }
|
||||
bevy_math = { path = "../bevy_math", version = "0.14.0-dev" }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" }
|
||||
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev", features = [
|
||||
bevy_app = { path = "../bevy_app", version = "0.14.0" }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0" }
|
||||
bevy_math = { path = "../bevy_math", version = "0.14.0" }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.14.0" }
|
||||
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0", features = [
|
||||
"glam",
|
||||
"smol_str",
|
||||
] }
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bevy_internal"
|
||||
version = "0.14.0-dev"
|
||||
version = "0.14.0"
|
||||
edition = "2021"
|
||||
description = "An internal Bevy crate used to facilitate optional dynamic linking via the 'dynamic_linking' feature"
|
||||
homepage = "https://bevyengine.org"
|
||||
@ -48,6 +48,9 @@ zstd = ["bevy_render/zstd"]
|
||||
# Include tonemapping LUT KTX2 files.
|
||||
tonemapping_luts = ["bevy_core_pipeline/tonemapping_luts"]
|
||||
|
||||
# Include SMAA LUT KTX2 Files
|
||||
smaa_luts = ["bevy_core_pipeline/smaa_luts"]
|
||||
|
||||
# Audio format support (vorbis is enabled by default)
|
||||
flac = ["bevy_audio/flac"]
|
||||
mp3 = ["bevy_audio/mp3"]
|
||||
@ -71,6 +74,7 @@ shader_format_spirv = ["bevy_render/shader_format_spirv"]
|
||||
serialize = [
|
||||
"bevy_core/serialize",
|
||||
"bevy_input/serialize",
|
||||
"bevy_ecs/serialize",
|
||||
"bevy_time/serialize",
|
||||
"bevy_window/serialize",
|
||||
"bevy_winit?/serialize",
|
||||
@ -107,6 +111,12 @@ pbr_multi_layer_material_textures = [
|
||||
"bevy_gltf?/pbr_multi_layer_material_textures",
|
||||
]
|
||||
|
||||
# Anisotropy texture in `StandardMaterial`:
|
||||
pbr_anisotropy_texture = [
|
||||
"bevy_pbr?/pbr_anisotropy_texture",
|
||||
"bevy_gltf?/pbr_anisotropy_texture",
|
||||
]
|
||||
|
||||
# Optimise for WebGL2
|
||||
webgl = [
|
||||
"bevy_core_pipeline?/webgl",
|
||||
@ -187,44 +197,44 @@ bevy_state = ["dep:bevy_state"]
|
||||
|
||||
[dependencies]
|
||||
# bevy
|
||||
bevy_a11y = { path = "../bevy_a11y", version = "0.14.0-dev" }
|
||||
bevy_app = { path = "../bevy_app", version = "0.14.0-dev" }
|
||||
bevy_core = { path = "../bevy_core", version = "0.14.0-dev" }
|
||||
bevy_derive = { path = "../bevy_derive", version = "0.14.0-dev" }
|
||||
bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.14.0-dev" }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" }
|
||||
bevy_state = { path = "../bevy_state", optional = true, version = "0.14.0-dev" }
|
||||
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.14.0-dev" }
|
||||
bevy_input = { path = "../bevy_input", version = "0.14.0-dev" }
|
||||
bevy_log = { path = "../bevy_log", version = "0.14.0-dev" }
|
||||
bevy_math = { path = "../bevy_math", version = "0.14.0-dev" }
|
||||
bevy_ptr = { path = "../bevy_ptr", version = "0.14.0-dev" }
|
||||
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev", features = [
|
||||
bevy_a11y = { path = "../bevy_a11y", version = "0.14.0" }
|
||||
bevy_app = { path = "../bevy_app", version = "0.14.0" }
|
||||
bevy_core = { path = "../bevy_core", version = "0.14.0" }
|
||||
bevy_derive = { path = "../bevy_derive", version = "0.14.0" }
|
||||
bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.14.0" }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0" }
|
||||
bevy_state = { path = "../bevy_state", optional = true, version = "0.14.0" }
|
||||
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.14.0" }
|
||||
bevy_input = { path = "../bevy_input", version = "0.14.0" }
|
||||
bevy_log = { path = "../bevy_log", version = "0.14.0" }
|
||||
bevy_math = { path = "../bevy_math", version = "0.14.0" }
|
||||
bevy_ptr = { path = "../bevy_ptr", version = "0.14.0" }
|
||||
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0", features = [
|
||||
"bevy",
|
||||
] }
|
||||
bevy_time = { path = "../bevy_time", version = "0.14.0-dev" }
|
||||
bevy_transform = { path = "../bevy_transform", version = "0.14.0-dev" }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" }
|
||||
bevy_window = { path = "../bevy_window", version = "0.14.0-dev" }
|
||||
bevy_tasks = { path = "../bevy_tasks", version = "0.14.0-dev" }
|
||||
bevy_time = { path = "../bevy_time", version = "0.14.0" }
|
||||
bevy_transform = { path = "../bevy_transform", version = "0.14.0" }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.14.0" }
|
||||
bevy_window = { path = "../bevy_window", version = "0.14.0" }
|
||||
bevy_tasks = { path = "../bevy_tasks", version = "0.14.0" }
|
||||
# bevy (optional)
|
||||
bevy_animation = { path = "../bevy_animation", optional = true, version = "0.14.0-dev" }
|
||||
bevy_asset = { path = "../bevy_asset", optional = true, version = "0.14.0-dev" }
|
||||
bevy_audio = { path = "../bevy_audio", optional = true, version = "0.14.0-dev" }
|
||||
bevy_color = { path = "../bevy_color", optional = true, version = "0.14.0-dev" }
|
||||
bevy_core_pipeline = { path = "../bevy_core_pipeline", optional = true, version = "0.14.0-dev" }
|
||||
bevy_gltf = { path = "../bevy_gltf", optional = true, version = "0.14.0-dev" }
|
||||
bevy_pbr = { path = "../bevy_pbr", optional = true, version = "0.14.0-dev" }
|
||||
bevy_render = { path = "../bevy_render", optional = true, version = "0.14.0-dev" }
|
||||
bevy_dynamic_plugin = { path = "../bevy_dynamic_plugin", optional = true, version = "0.14.0-dev" }
|
||||
bevy_scene = { path = "../bevy_scene", optional = true, version = "0.14.0-dev" }
|
||||
bevy_sprite = { path = "../bevy_sprite", optional = true, version = "0.14.0-dev" }
|
||||
bevy_text = { path = "../bevy_text", optional = true, version = "0.14.0-dev" }
|
||||
bevy_ui = { path = "../bevy_ui", optional = true, version = "0.14.0-dev" }
|
||||
bevy_winit = { path = "../bevy_winit", optional = true, version = "0.14.0-dev" }
|
||||
bevy_gilrs = { path = "../bevy_gilrs", optional = true, version = "0.14.0-dev" }
|
||||
bevy_gizmos = { path = "../bevy_gizmos", optional = true, version = "0.14.0-dev", default-features = false }
|
||||
bevy_dev_tools = { path = "../bevy_dev_tools", optional = true, version = "0.14.0-dev" }
|
||||
bevy_animation = { path = "../bevy_animation", optional = true, version = "0.14.0" }
|
||||
bevy_asset = { path = "../bevy_asset", optional = true, version = "0.14.0" }
|
||||
bevy_audio = { path = "../bevy_audio", optional = true, version = "0.14.0" }
|
||||
bevy_color = { path = "../bevy_color", optional = true, version = "0.14.1" }
|
||||
bevy_core_pipeline = { path = "../bevy_core_pipeline", optional = true, version = "0.14.0" }
|
||||
bevy_gltf = { path = "../bevy_gltf", optional = true, version = "0.14.0" }
|
||||
bevy_pbr = { path = "../bevy_pbr", optional = true, version = "0.14.0" }
|
||||
bevy_render = { path = "../bevy_render", optional = true, version = "0.14.0" }
|
||||
bevy_dynamic_plugin = { path = "../bevy_dynamic_plugin", optional = true, version = "0.14.0" }
|
||||
bevy_scene = { path = "../bevy_scene", optional = true, version = "0.14.0" }
|
||||
bevy_sprite = { path = "../bevy_sprite", optional = true, version = "0.14.0" }
|
||||
bevy_text = { path = "../bevy_text", optional = true, version = "0.14.0" }
|
||||
bevy_ui = { path = "../bevy_ui", optional = true, version = "0.14.0" }
|
||||
bevy_winit = { path = "../bevy_winit", optional = true, version = "0.14.0" }
|
||||
bevy_gilrs = { path = "../bevy_gilrs", optional = true, version = "0.14.0" }
|
||||
bevy_gizmos = { path = "../bevy_gizmos", optional = true, version = "0.14.0", default-features = false }
|
||||
bevy_dev_tools = { path = "../bevy_dev_tools", optional = true, version = "0.14.0" }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
@ -28,6 +28,8 @@ use bevy_app::{Plugin, PluginGroup, PluginGroupBuilder};
|
||||
/// * [`AudioPlugin`](crate::audio::AudioPlugin) - with feature `bevy_audio`
|
||||
/// * [`GilrsPlugin`](crate::gilrs::GilrsPlugin) - with feature `bevy_gilrs`
|
||||
/// * [`AnimationPlugin`](crate::animation::AnimationPlugin) - with feature `bevy_animation`
|
||||
/// * [`GizmoPlugin`](crate::gizmos::GizmoPlugin) - with feature `bevy_gizmos`
|
||||
/// * [`StatesPlugin`](crate::app::StatesPlugin) - with feature `bevy_state`
|
||||
/// * [`DevToolsPlugin`](crate::dev_tools::DevToolsPlugin) - with feature `bevy_dev_tools`
|
||||
/// * [`CiTestingPlugin`](crate::dev_tools::ci_testing::CiTestingPlugin) - with feature `bevy_ci_testing`
|
||||
///
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bevy_log"
|
||||
version = "0.14.0-dev"
|
||||
version = "0.14.0"
|
||||
edition = "2021"
|
||||
description = "Provides logging for Bevy Engine"
|
||||
homepage = "https://bevyengine.org"
|
||||
@ -13,9 +13,9 @@ trace = ["tracing-error"]
|
||||
trace_tracy_memory = ["dep:tracy-client"]
|
||||
|
||||
[dependencies]
|
||||
bevy_app = { path = "../bevy_app", version = "0.14.0-dev" }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" }
|
||||
bevy_app = { path = "../bevy_app", version = "0.14.0" }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.14.0" }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0" }
|
||||
|
||||
tracing-subscriber = { version = "0.3.1", features = [
|
||||
"registry",
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bevy_macro_utils"
|
||||
version = "0.14.0-dev"
|
||||
version = "0.14.0"
|
||||
edition = "2021"
|
||||
description = "A collection of utils for Bevy Engine"
|
||||
homepage = "https://bevyengine.org"
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bevy_math"
|
||||
version = "0.14.0-dev"
|
||||
version = "0.14.0"
|
||||
edition = "2021"
|
||||
description = "Provides math functionality for Bevy Engine"
|
||||
homepage = "https://bevyengine.org"
|
||||
@ -20,7 +20,7 @@ rand = { version = "0.8", features = [
|
||||
], default-features = false, optional = true }
|
||||
smallvec = { version = "1.11" }
|
||||
|
||||
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev", features = [
|
||||
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0", features = [
|
||||
"glam",
|
||||
], optional = true }
|
||||
|
||||
@ -30,7 +30,7 @@ approx = "0.5"
|
||||
rand = "0.8"
|
||||
rand_chacha = "0.3"
|
||||
# Enable the approx feature when testing.
|
||||
bevy_math = { path = ".", version = "0.14.0-dev", features = ["approx"] }
|
||||
bevy_math = { path = ".", version = "0.14.0", features = ["approx"] }
|
||||
glam = { version = "0.27", features = ["approx"] }
|
||||
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bevy_mikktspace"
|
||||
version = "0.14.0-dev"
|
||||
version = "0.14.0"
|
||||
edition = "2021"
|
||||
authors = [
|
||||
"Benjamin Wasty <benny.wasty@gmail.com>",
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bevy_pbr"
|
||||
version = "0.14.0-dev"
|
||||
version = "0.14.0"
|
||||
edition = "2021"
|
||||
description = "Adds PBR rendering to Bevy Engine"
|
||||
homepage = "https://bevyengine.org"
|
||||
@ -13,6 +13,7 @@ webgl = []
|
||||
webgpu = []
|
||||
pbr_transmission_textures = []
|
||||
pbr_multi_layer_material_textures = []
|
||||
pbr_anisotropy_texture = []
|
||||
shader_format_glsl = ["bevy_render/shader_format_glsl"]
|
||||
trace = ["bevy_render/trace"]
|
||||
ios_simulator = ["bevy_render/ios_simulator"]
|
||||
@ -29,20 +30,20 @@ meshlet_processor = ["meshlet", "dep:meshopt", "dep:metis", "dep:itertools"]
|
||||
|
||||
[dependencies]
|
||||
# bevy
|
||||
bevy_app = { path = "../bevy_app", version = "0.14.0-dev" }
|
||||
bevy_asset = { path = "../bevy_asset", version = "0.14.0-dev" }
|
||||
bevy_color = { path = "../bevy_color", version = "0.14.0-dev" }
|
||||
bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.14.0-dev" }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" }
|
||||
bevy_math = { path = "../bevy_math", version = "0.14.0-dev" }
|
||||
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev", features = [
|
||||
bevy_app = { path = "../bevy_app", version = "0.14.0" }
|
||||
bevy_asset = { path = "../bevy_asset", version = "0.14.0" }
|
||||
bevy_color = { path = "../bevy_color", version = "0.14.1" }
|
||||
bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.14.0" }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0" }
|
||||
bevy_math = { path = "../bevy_math", version = "0.14.0" }
|
||||
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0", features = [
|
||||
"bevy",
|
||||
] }
|
||||
bevy_render = { path = "../bevy_render", version = "0.14.0-dev" }
|
||||
bevy_transform = { path = "../bevy_transform", version = "0.14.0-dev" }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" }
|
||||
bevy_window = { path = "../bevy_window", version = "0.14.0-dev" }
|
||||
bevy_derive = { path = "../bevy_derive", version = "0.14.0-dev" }
|
||||
bevy_render = { path = "../bevy_render", version = "0.14.0" }
|
||||
bevy_transform = { path = "../bevy_transform", version = "0.14.0" }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.14.0" }
|
||||
bevy_window = { path = "../bevy_window", version = "0.14.0" }
|
||||
bevy_derive = { path = "../bevy_derive", version = "0.14.0" }
|
||||
|
||||
|
||||
# other
|
||||
@ -56,7 +57,7 @@ serde = { version = "1", features = ["derive", "rc"], optional = true }
|
||||
bincode = { version = "1", optional = true }
|
||||
thiserror = { version = "1", optional = true }
|
||||
range-alloc = { version = "0.1", optional = true }
|
||||
meshopt = { version = "0.2.1", optional = true }
|
||||
meshopt = { version = "0.3.0", optional = true }
|
||||
metis = { version = "0.2", optional = true }
|
||||
itertools = { version = "0.13", optional = true }
|
||||
# direct dependency required for derive macro
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user