diff --git a/CHANGELOG.md b/CHANGELOG.md index b9c0ee2c75..df91b41c44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -145,6 +145,7 @@ These PRs are not directly user-facing, but improve the development experience. - **Elements**: All hooks are now free functions (i.e. `use_state(hooks, ..)` instead of `hooks.use_state(..)`) - **UI**: Focus is now global across different packages, and we've removed the FocusRoot component - **API**: CursorLockGuard removed and `hide_cursor` package introduced. +- **Hierarchies**: The `children` component is now automatically derived from `parent` components (unless the user opts out of this). The `children` component is also not networked any longer, since it's calculated on the client side. #### Non-breaking diff --git a/app/src/server/mod.rs b/app/src/server/mod.rs index a0837eb9f3..d0a585b4c2 100644 --- a/app/src/server/mod.rs +++ b/app/src/server/mod.rs @@ -221,6 +221,7 @@ fn systems(_world: &mut World) -> SystemGroup { // Can happen *during* the physics step Box::new(ambient_core::async_ecs::async_ecs_systems()), Box::new(ambient_prefab::systems()), + Box::new(ambient_core::hierarchy::systems()), // Happens after the physics step ambient_physics::fetch_simulation_system(), Box::new(ambient_animation::animation_systems()), diff --git a/campfire/src/package.rs b/campfire/src/package.rs index 52646b9482..837e88ea2c 100644 --- a/campfire/src/package.rs +++ b/campfire/src/package.rs @@ -144,6 +144,7 @@ fn check_all() -> anyhow::Result<()> { if !features.is_empty() { command.args(["--features", features]); } + command.args(["--", "-A", "clippy::collapsible-if"]); if !command.spawn()?.wait()?.success() { anyhow::bail!( diff --git a/crates/app/src/lib.rs b/crates/app/src/lib.rs index 802a3a8d67..c9a2c30486 100644 --- a/crates/app/src/lib.rs +++ b/crates/app/src/lib.rs @@ -99,6 +99,7 @@ pub fn world_instance_systems(full: bool) -> SystemGroup { Box::new(async_ecs_systems()), remove_at_time_system(), refcount_system(), + Box::new(ambient_core::hierarchy::systems()), Box::new(WorldEventsSystem), Box::new(ambient_focus::systems()), if full { diff --git a/crates/core/src/hierarchy.rs b/crates/core/src/hierarchy.rs index fd84b4c6e4..913ef2eb2a 100644 --- a/crates/core/src/hierarchy.rs +++ b/crates/core/src/hierarchy.rs @@ -1,6 +1,9 @@ use std::collections::HashSet; -use ambient_ecs::{query, Component, ComponentValue, ECSError, Entity, EntityId, World}; +use ambient_ecs::{ + generated::hierarchy::components::unmanaged_children, query, Component, ComponentValue, + ECSError, Entity, EntityId, SystemGroup, World, +}; use itertools::Itertools; use yaml_rust::YamlEmitter; @@ -8,6 +11,41 @@ pub use ambient_ecs::generated::hierarchy::components::{children, parent}; use crate::name; +pub fn systems() -> SystemGroup { + SystemGroup::new( + "hierarchy", + vec![ + query(parent().changed()).to_system_with_name("update_children", |q, world, qs, _| { + for (id, parent) in q.collect_cloned(world, qs) { + if world.has_component(parent, unmanaged_children()) { + continue; + } + if let Ok(children) = world.get_mut(parent, children()) { + if !children.contains(&id) { + children.push(id); + } + } else { + world.add_component(parent, children(), vec![id]).unwrap(); + } + } + }), + query(parent()).despawned().to_system_with_name( + "remove_children", + |q, world, qs, _| { + for (id, parent) in q.collect_cloned(world, qs) { + if world.has_component(parent, unmanaged_children()) { + continue; + } + if let Ok(children) = world.get_mut(parent, children()) { + children.retain(|c| *c != id); + } + } + }, + ), + ], + ) +} + pub fn despawn_recursive(world: &mut World, entity: EntityId) -> Option { despawn_children_recursive(world, entity); world.despawn(entity) diff --git a/crates/ecs/src/generated.rs b/crates/ecs/src/generated.rs index f6dbaeccc1..64d84e9ff3 100644 --- a/crates/ecs/src/generated.rs +++ b/crates/ecs/src/generated.rs @@ -240,7 +240,7 @@ mod raw { }; use glam::{Mat4, Quat, UVec2, UVec3, UVec4, Vec2, Vec3, Vec4}; use std::time::Duration; - components ! ("hierarchy" , { # [doc = "**Parent**: The parent of this entity.\n\n*Attributes*: Debuggable, Networked, Store"] @ [Debuggable , Networked , Store , Name ["Parent"] , Description ["The parent of this entity."]] parent : EntityId , # [doc = "**Children**: The children of this entity.\n\n*Attributes*: Debuggable, Networked, Store, MaybeResource"] @ [Debuggable , Networked , Store , MaybeResource , Name ["Children"] , Description ["The children of this entity."]] children : Vec :: < EntityId > , }); + components ! ("hierarchy" , { # [doc = "**Parent**: The parent of this entity.\n\n*Attributes*: Debuggable, Networked, Store"] @ [Debuggable , Networked , Store , Name ["Parent"] , Description ["The parent of this entity."]] parent : EntityId , # [doc = "**Children**: The children of this entity.\n\n*Attributes*: Debuggable, Store, MaybeResource"] @ [Debuggable , Store , MaybeResource , Name ["Children"] , Description ["The children of this entity."]] children : Vec :: < EntityId > , # [doc = "**Unmanaged children**: This children component is not updated automatically for this entity when this component is attached.\n\n*Attributes*: Debuggable, Networked, Store, MaybeResource"] @ [Debuggable , Networked , Store , MaybeResource , Name ["Unmanaged children"] , Description ["This children component is not updated automatically for this entity when this component is attached."]] unmanaged_children : () , }); } } #[allow(unused)] diff --git a/crates/model/src/model.rs b/crates/model/src/model.rs index bbadadb943..37ab321e65 100644 --- a/crates/model/src/model.rs +++ b/crates/model/src/model.rs @@ -13,7 +13,8 @@ use ambient_core::{ }, }; use ambient_ecs::{ - generated::animation::components::bind_id, query, ComponentDesc, Entity, EntityId, World, + generated::{animation::components::bind_id, hierarchy::components::unmanaged_children}, + query, ComponentDesc, Entity, EntityId, World, }; use ambient_gpu::gpu::Gpu; use ambient_native_std::{ @@ -207,7 +208,8 @@ impl Model { .with(children(), vec![]) .with(local_to_parent(), transform) .with(local_to_world(), Default::default()) - .with(is_model_node(), ()), + .with(is_model_node(), ()) + .with(unmanaged_children(), ()), count, ); for (transform, root) in transform_roots.iter().zip(roots.iter()) { @@ -375,6 +377,9 @@ impl Model { } } if self.0.has_component(id, children()) { + for id in &entities { + world.add_component(*id, unmanaged_children(), ()).ok(); + } for c in self.0.get_ref(id, children()).unwrap().iter() { self.spawn_subtree( gpu, diff --git a/docs/src/reference/hierarchies.md b/docs/src/reference/hierarchies.md index 30e2806337..fb46a3f746 100644 --- a/docs/src/reference/hierarchies.md +++ b/docs/src/reference/hierarchies.md @@ -1,10 +1,10 @@ # Hierarchies and transforms -Ambient supports hierarchies of entities using the `parent` and `children` components. Both need to be present for a hierarchy to be valid - as an example, the following entities in the ECS +Ambient supports hierarchies of entities using the `parent` and `children` components. The user only specifies the `parent` component, the `children` are automatically derived from the existing parents. +As an example, the following entities in the ECS ```yml entity a: - - children: [b, c] entity b: - parent: a entity c: @@ -19,8 +19,6 @@ entity a entity c ``` -If you are creating hierachies yourself, you need to make sure that both `parent` and `children` exists and are correct for the hierarchy to work. - The `entity::add_child` and `entity::remove_child` functions can be used to add and remove children from a parent. When using the `model_from_url` or `prefab_from_url` components, the entire model sub-tree will be spawned in, with the root of the sub-tree being added as a child to the entity with the component. Each entity in the sub-tree will be part of the hierarchy using their own `parent` and `children` components. @@ -32,7 +30,6 @@ To apply transforms to a hierarchy, `local_to_parent` must be used: ```yml entity a: - - children: [b] - local_to_world: Mat4(..) entity b: - parent: a @@ -46,7 +43,6 @@ In this case, `b.local_to_world` will be calculated as `a.local_to_world * b.loc ```yml entity a: - - children: [b] - local_to_world: Mat4(..) - translation: vec3(5., 2., 9.) - rotation: quat(..) @@ -88,3 +84,9 @@ mesh_to_world = local_to_world * mesh_to_local This also means that you can attach a mesh in the middle of a hierarchy, with an offset. For instance, if you have a bone hierarchy on a character, you can attach an mesh to the upper arm bone, but without `mesh_to_local/world` it would be rendered at the center of the arm (inside the arm), so by using `mesh_to_local/world` you can offset it. + +## Opting out of automatically derived children + +If you wish to manage the `children` component yourself, you can attach an `unmanaged_children` component to your +entity. This stops `children` from being automatically created, and it's now up to you to populate the `children` +component to create a valid hierarchy. diff --git a/guest/rust/Cargo.lock b/guest/rust/Cargo.lock index 3655043585..258348a5cf 100644 --- a/guest/rust/Cargo.lock +++ b/guest/rust/Cargo.lock @@ -23,13 +23,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" -[[package]] -name = "afps_fpsanim" -version = "0.0.1" -dependencies = [ - "ambient_api", -] - [[package]] name = "afps_fpsaudio" version = "0.0.1" @@ -815,6 +808,13 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "character_animation" +version = "0.0.1" +dependencies = [ + "ambient_api", +] + [[package]] name = "character_movement" version = "0.0.1" diff --git a/guest/rust/api_core/src/client/audio.rs b/guest/rust/api_core/src/client/audio.rs index a7e34c2580..b75ec002d6 100644 --- a/guest/rust/api_core/src/client/audio.rs +++ b/guest/rust/api_core/src/client/audio.rs @@ -2,7 +2,7 @@ use crate::{ core::{ app::components::name, audio::components::*, - hierarchy::components::{children, parent}, + hierarchy::components::{children, parent, unmanaged_children}, }, entity, prelude::{Entity, EntityId}, @@ -82,6 +82,7 @@ impl AudioPlayer { .with(is_audio_player(), ()) .with(name(), "Audio player".to_string()) .with(children(), vec![]) + .with(unmanaged_children(), ()) .spawn(); Self { entity: player } } diff --git a/guest/rust/api_core/src/entity.rs b/guest/rust/api_core/src/entity.rs index 2f201802c1..00d5a66adc 100644 --- a/guest/rust/api_core/src/entity.rs +++ b/guest/rust/api_core/src/entity.rs @@ -4,6 +4,7 @@ use crate::{ internal::{ component::{Component, Entity, SupportedValue, UntypedComponent}, conversion::{FromBindgen, IntoBindgen}, + generated::ambient_core::hierarchy::components::unmanaged_children, wit, }, prelude::block_until, @@ -224,17 +225,19 @@ pub fn mutate_component_with_default( /// Adds `child` as a child to `entity`. pub fn add_child(entity: EntityId, child: EntityId) { - if has_component(entity, children()) { - mutate_component(entity, children(), |children| children.push(child)); - } else { - add_component(entity, children(), vec![child]); + if has_component(entity, unmanaged_children()) { + if has_component(entity, children()) { + mutate_component(entity, children(), |children| children.push(child)); + } else { + add_component(entity, children(), vec![child]); + } } add_component(child, parent(), entity); } /// Removes `child` as a child to `entity`. pub fn remove_child(entity: EntityId, child: EntityId) { - if has_component(entity, children()) { + if has_component(entity, unmanaged_children()) { mutate_component(entity, children(), |children| { children.retain(|x| *x != child) }); diff --git a/guest/rust/api_core/src/internal/generated.rs b/guest/rust/api_core/src/internal/generated.rs index 71c2c5ce16..9781b2a97f 100644 --- a/guest/rust/api_core/src/internal/generated.rs +++ b/guest/rust/api_core/src/internal/generated.rs @@ -761,10 +761,17 @@ mod raw { } static CHILDREN: Lazy>> = Lazy::new(|| __internal_get_component("ambient_core::hierarchy::children")); - #[doc = "**Children**: The children of this entity.\n\n*Attributes*: Debuggable, Networked, Store, MaybeResource"] + #[doc = "**Children**: The children of this entity.\n\n*Attributes*: Debuggable, Store, MaybeResource"] pub fn children() -> Component> { *CHILDREN } + static UNMANAGED_CHILDREN: Lazy> = Lazy::new(|| { + __internal_get_component("ambient_core::hierarchy::unmanaged_children") + }); + #[doc = "**Unmanaged children**: This children component is not updated automatically for this entity when this component is attached.\n\n*Attributes*: Debuggable, Networked, Store, MaybeResource"] + pub fn unmanaged_children() -> Component<()> { + *UNMANAGED_CHILDREN + } } } #[allow(unused)] diff --git a/guest/rust/packages/games/minigolf/src/server.rs b/guest/rust/packages/games/minigolf/src/server.rs index 2b3e5dd86e..a5d77dc37f 100644 --- a/guest/rust/packages/games/minigolf/src/server.rs +++ b/guest/rust/packages/games/minigolf/src/server.rs @@ -5,7 +5,7 @@ use ambient_api::{ components::{active_camera, aspect_ratio_from_window}, concepts::make_perspective_infinite_reverse_camera, }, - hierarchy::components::children, + hierarchy::components::parent, messages::Collision, model::components::model_from_url, physics::components::{ @@ -142,6 +142,7 @@ pub fn main() { .with(color(), next_color) .with(user_id(), player_user_id.clone()) .with(text(), player_user_id.clone()) + .with(parent(), player) .spawn(); entity::add_component(player, player_text(), text); @@ -153,7 +154,6 @@ pub fn main() { .with(local_to_world(), Default::default()) .with(spherical_billboard(), ()) .with(translation(), vec3(-5., 0., 5.)) - .with(children(), vec![text]) .spawn(), ); diff --git a/guest/rust/packages/games/tangent/core/src/client.rs b/guest/rust/packages/games/tangent/core/src/client.rs index 81de7961e4..3886a02c6a 100644 --- a/guest/rust/packages/games/tangent/core/src/client.rs +++ b/guest/rust/packages/games/tangent/core/src/client.rs @@ -5,7 +5,7 @@ use ambient_api::{ components::{aspect_ratio_from_window, fog, fovy}, concepts::make_perspective_infinite_reverse_camera, }, - hierarchy::components::{children, parent}, + hierarchy::components::parent, messages::Frame, physics::components::linear_velocity, rect::components::{line_from, line_to, line_width, rect}, @@ -72,11 +72,8 @@ pub fn main() { despawn_query(vehicle_hud()) .requires(vehicle()) .bind(move |vehicles| { - for (vehicle_id, hud_id) in vehicles { + for (_vehicle_id, hud_id) in vehicles { entity::despawn(hud_id); - entity::mutate_component(vehicle_id, children(), |children| { - children.retain(|&c| c != hud_id); - }); } }); diff --git a/guest/rust/packages/std/character_animation/Cargo.toml b/guest/rust/packages/std/character_animation/Cargo.toml index 478d6e8848..f73f6d29e6 100644 --- a/guest/rust/packages/std/character_animation/Cargo.toml +++ b/guest/rust/packages/std/character_animation/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "afps_fpsanim" +name = "character_animation" edition = "2021" publish = false @@ -9,9 +9,9 @@ version = "0.0.1" ambient_api = { workspace = true } [[bin]] -name = "fpsanim_server" -path = "src/server.rs" -required-features = ["server"] +name = "character_animation_client" +path = "src/client.rs" +required-features = ["client"] [features] client = ["ambient_api/client"] diff --git a/guest/rust/packages/std/character_animation/ambient.toml b/guest/rust/packages/std/character_animation/ambient.toml index d8875331c1..e51e2f0401 100644 --- a/guest/rust/packages/std/character_animation/ambient.toml +++ b/guest/rust/packages/std/character_animation/ambient.toml @@ -1,6 +1,6 @@ [package] id = "character_animation" -name = "Basic character animation" +name = "Character animation" version = "0.0.1" content = { type = "Asset", animations = true, code = true } @@ -8,7 +8,10 @@ content = { type = "Asset", animations = true, code = true } unit_schema = { path = "../../schemas/unit", deployment = "7JvheyuVjOG0IP3MDpgLE0" } [components] -basic_character_animations = { type = "EntityId", description = "Apply animations to the model this points to. Parameters such as health etc. is read from the entity this component is attached to." } +basic_character_animations = { type = "EntityId", description = "Apply animations to the model this points to. Parameters such as health etc. is read from the entity this component is attached to.", attributes = [ + "Debuggable", + "Networked", +] } # Overrides for the default animations walk_forward = { type = "String", description = "Url to animation" } diff --git a/guest/rust/packages/std/character_animation/src/server.rs b/guest/rust/packages/std/character_animation/src/client.rs similarity index 100% rename from guest/rust/packages/std/character_animation/src/server.rs rename to guest/rust/packages/std/character_animation/src/client.rs diff --git a/schema/schema/hierarchy.toml b/schema/schema/hierarchy.toml index 2234ceb1f1..3c7ce2c301 100644 --- a/schema/schema/hierarchy.toml +++ b/schema/schema/hierarchy.toml @@ -15,4 +15,10 @@ attributes = ["Debuggable", "Networked", "Store"] type = { type = "Vec", element_type = "EntityId" } name = "Children" description = "The children of this entity." +attributes = ["Debuggable", "Store", "MaybeResource"] + +[components.unmanaged_children] +type = "Empty" +name = "Unmanaged children" +description = "This children component is not updated automatically for this entity when this component is attached." attributes = ["Debuggable", "Networked", "Store", "MaybeResource"] diff --git a/shared_crates/element/src/tree.rs b/shared_crates/element/src/tree.rs index d663f8744c..e2926de581 100644 --- a/shared_crates/element/src/tree.rs +++ b/shared_crates/element/src/tree.rs @@ -20,7 +20,7 @@ use ambient_guest_bridge::core::hierarchy::components::{children, parent}; #[cfg(feature = "native")] use ambient_guest_bridge::ecs::{query, Component, SystemGroup}; use ambient_guest_bridge::{ - core::app::components::name, + core::{app::components::name, hierarchy::components::unmanaged_children}, ecs::{Entity, EntityId, World}, }; use itertools::Itertools; @@ -559,13 +559,17 @@ impl ElementTree { let mut all_children = Vec::new(); self.get_full_instance_children(id, &mut all_children); world - .add_component( + .add_components( instance.entity, - children(), - all_children - .iter() - .map(|c| self.instances.get(c).unwrap().entity) - .collect_vec(), + Entity::new() + .with( + children(), + all_children + .iter() + .map(|c| self.instances.get(c).unwrap().entity) + .collect_vec(), + ) + .with(unmanaged_children(), ()), ) .unwrap(); }