Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions app/src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
Expand Down
1 change: 1 addition & 0 deletions campfire/src/package.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand Down
1 change: 1 addition & 0 deletions crates/app/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
40 changes: 39 additions & 1 deletion crates/core/src/hierarchy.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,51 @@
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;

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<Entity> {
despawn_children_recursive(world, entity);
world.despawn(entity)
Expand Down
2 changes: 1 addition & 1 deletion crates/ecs/src/generated.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
9 changes: 7 additions & 2 deletions crates/model/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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()) {
Expand Down Expand Up @@ -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,
Expand Down
14 changes: 8 additions & 6 deletions docs/src/reference/hierarchies.md
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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.
Expand All @@ -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
Expand All @@ -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(..)
Expand Down Expand Up @@ -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.
14 changes: 7 additions & 7 deletions guest/rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion guest/rust/api_core/src/client/audio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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 }
}
Expand Down
13 changes: 8 additions & 5 deletions guest/rust/api_core/src/entity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -224,17 +225,19 @@ pub fn mutate_component_with_default<T: SupportedValue + Clone + PartialEq>(

/// 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)
});
Expand Down
9 changes: 8 additions & 1 deletion guest/rust/api_core/src/internal/generated.rs
Original file line number Diff line number Diff line change
Expand Up @@ -761,10 +761,17 @@ mod raw {
}
static CHILDREN: Lazy<Component<Vec<EntityId>>> =
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<Vec<EntityId>> {
*CHILDREN
}
static UNMANAGED_CHILDREN: Lazy<Component<()>> = 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)]
Expand Down
4 changes: 2 additions & 2 deletions guest/rust/packages/games/minigolf/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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);

Expand All @@ -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(),
);

Expand Down
7 changes: 2 additions & 5 deletions guest/rust/packages/games/tangent/core/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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);
});
}
});

Expand Down
8 changes: 4 additions & 4 deletions guest/rust/packages/std/character_animation/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[package]
name = "afps_fpsanim"
name = "character_animation"

edition = "2021"
publish = false
Expand All @@ -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"]
Expand Down
7 changes: 5 additions & 2 deletions guest/rust/packages/std/character_animation/ambient.toml
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
[package]
id = "character_animation"
name = "Basic character animation"
name = "Character animation"
version = "0.0.1"
content = { type = "Asset", animations = true, code = true }

[dependencies]
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" }
Expand Down
6 changes: 6 additions & 0 deletions schema/schema/hierarchy.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
18 changes: 11 additions & 7 deletions shared_crates/element/src/tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
Expand Down