From 511c8bedca702c32dd7a130fcf4904dc94e17a63 Mon Sep 17 00:00:00 2001 From: Felipe Jorge Date: Mon, 5 Apr 2021 20:31:33 -0300 Subject: [PATCH 1/3] named hierachy --- crates/bevy_transform/Cargo.toml | 1 + .../hierarchy/{hierarchy.rs => commands.rs} | 0 crates/bevy_transform/src/hierarchy/mod.rs | 7 +- .../src/hierarchy/named_hierarchy.rs | 505 ++++++++++++++++++ 4 files changed, 510 insertions(+), 3 deletions(-) rename crates/bevy_transform/src/hierarchy/{hierarchy.rs => commands.rs} (100%) create mode 100644 crates/bevy_transform/src/hierarchy/named_hierarchy.rs diff --git a/crates/bevy_transform/Cargo.toml b/crates/bevy_transform/Cargo.toml index 3f1584991cc69..2de5f26956bb6 100644 --- a/crates/bevy_transform/Cargo.toml +++ b/crates/bevy_transform/Cargo.toml @@ -14,6 +14,7 @@ keywords = ["bevy"] [dependencies] # bevy +bevy_core = { path = "../bevy_core", version = "0.4.0" } bevy_app = { path = "../bevy_app", version = "0.4.0" } bevy_ecs = { path = "../bevy_ecs", version = "0.4.0" } bevy_math = { path = "../bevy_math", version = "0.4.0" } diff --git a/crates/bevy_transform/src/hierarchy/hierarchy.rs b/crates/bevy_transform/src/hierarchy/commands.rs similarity index 100% rename from crates/bevy_transform/src/hierarchy/hierarchy.rs rename to crates/bevy_transform/src/hierarchy/commands.rs diff --git a/crates/bevy_transform/src/hierarchy/mod.rs b/crates/bevy_transform/src/hierarchy/mod.rs index 23e9108e2dec2..987df329db7dc 100644 --- a/crates/bevy_transform/src/hierarchy/mod.rs +++ b/crates/bevy_transform/src/hierarchy/mod.rs @@ -1,8 +1,9 @@ mod child_builder; -#[allow(clippy::module_inception)] -mod hierarchy; +mod commands; mod hierarchy_maintenance_system; +mod named_hierarchy; pub use child_builder::*; -pub use hierarchy::*; +pub use commands::*; pub use hierarchy_maintenance_system::*; +pub use named_hierarchy::*; diff --git a/crates/bevy_transform/src/hierarchy/named_hierarchy.rs b/crates/bevy_transform/src/hierarchy/named_hierarchy.rs new file mode 100644 index 0000000000000..f2962a8c46738 --- /dev/null +++ b/crates/bevy_transform/src/hierarchy/named_hierarchy.rs @@ -0,0 +1,505 @@ +use bevy_core::Name; +use bevy_ecs::prelude::*; +//use serde::{Deserialize, Serialize}; +use private::*; +use smallvec::{smallvec, SmallVec}; + +use crate::{Children, Parent}; + +// TODO: Panic if the root entity is not defined prior to a query +// TODO: Create a simplified version without the children ?! +// TODO: Should I just use usize? and get done with it? + +/// Provides a way of describing a hierarchy or named entities +/// and means for finding then in the world +/// +/// By default and to save memory the nodes are indexed using `u16` +/// but you can change it when needed. +#[derive(Debug, Clone)] +pub struct NamedHierarchy { + /// Entity identification made by parent index and name + entities: Vec<(I, Name)>, + // ? NOTE: SmallVec<[u16; 10]> occupy the same 32 bytes as the SmallVec<[u16; 8]>, but the latter + // ? should be only take 24 bytes using the "union" feature + children: Vec>, +} + +impl Default for NamedHierarchy { + fn default() -> Self { + Self { + // ? NOTE: Since the root has no parent in this context it points to a place outside the vec bounds + entities: vec![(I::MAX_VALUE, Name::default())], + children: vec![smallvec![]], + } + } +} + +impl NamedHierarchy { + pub fn new() -> Self { + Default::default() + } + + /// Used when the hierarchy must be in a specific order, + /// this function takes an vec of entities defined by their parent index + /// (on the same vec) and name. + /// + /// Any root entity should be indexed using `I::MAX_VALUE` or `I::MAX`. + /// + /// Many different root nodes are supported although having other roots + /// make hard to search entities, please refer to the documentation of + /// `find_entity` or `find_entity_in_world` to see how. + /// + /// **WARNING** Be caution when using this function because it may create a + /// an invalid hierarchy + pub fn from_ordered_entities(entities: Vec<(I, Name)>) -> Self { + assert_eq!( + entities[0].0, + I::MAX_VALUE, + "first entry must be an root entity" + ); + + let mut children = vec![]; + children.resize_with(entities.len(), || smallvec![]); + + for (entity_index, (parent_index, _)) in entities.iter().enumerate() { + if let Some(c) = children.get_mut(parent_index.as_usize()) { + c.push(I::from_usize_checked(entity_index)); + } + } + + Self { entities, children } + } + + /// Merge other hierarchy into this one, it will collect the + /// new entities indexes of merged hierarchy. + pub fn merge(&mut self, other_hierarchy: &NamedHierarchy, mapped_entities: &mut Vec) { + mapped_entities.clear(); + mapped_entities.resize(other_hierarchy.len(), I::MAX_VALUE); + + assert!( + other_hierarchy.entities[0].0 == I::MAX_VALUE, + "first element isn't the root" + ); + + let root_index = I::from_usize(0); + mapped_entities[0] = root_index; + + // At this point they coincide + self.internal_merge(other_hierarchy, root_index, root_index, mapped_entities); + } + + // TODO: Expose to allow for merging hierarchies with multiple roots + fn internal_merge( + &mut self, + other_hierarchy: &NamedHierarchy, + other_parent_index: I, + parent_index: I, + mapped_entities: &mut Vec, + ) { + for other_index in &other_hierarchy.children[other_parent_index.as_usize()] { + let (_, other_name) = &other_hierarchy.entities[other_index.as_usize()]; + let child = (&parent_index, other_name); + + let entity_index = + if let Some(i) = self.entities.iter().position(|(i, n)| (i, n) == child) { + let entity_index = I::from_usize(i); + // Found corresponding entity + mapped_entities[other_index.as_usize()] = entity_index; + entity_index + } else { + // Add entity + // Soft limit added to save memory, identical to the curve limit + let entity_index = I::from_usize_checked(self.entities.len()); + self.entities.push((parent_index, other_name.clone())); + self.children.push(smallvec![]); + self.children[parent_index.as_usize()].push(entity_index); + mapped_entities[other_index.as_usize()] = entity_index; + entity_index + }; + + self.internal_merge(other_hierarchy, *other_index, entity_index, mapped_entities); + } + } + + /// Number of entities registered. + #[inline] + pub fn len(&self) -> usize { + self.entities.len() + } + + #[inline] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Iterates over each entity parent index, name and children indexes + #[inline] + pub fn iter(&self) -> impl Iterator { + self.entities + .iter() + .zip(self.children.iter().map(|c| &c[..])) + } + + /// Gets the entity parent index and `Name` components + #[inline] + pub fn get_entity(&self, entity_index: I) -> &(I, Name) { + &self.entities[entity_index.as_usize()] + } + + pub fn depth_first(&self, entity_index: I, visitor: &mut F) { + let i = entity_index.as_usize(); + let (_, name) = &self.entities[i]; + + visitor(entity_index, name); + + for child_index in &self.children[i] { + self.depth_first(*child_index, visitor); + } + } + + /// Adds a new entity hierarchy path separated by backslashes (`'/'`) + /// return the entity index and if was or not inserted + pub fn get_or_insert_entity(&mut self, entity_path: &str) -> (I, bool) { + let mut entity_created = false; + let mut entity = I::from_usize(0); // Start search from root + for name in entity_path.split('/') { + // Ignore the first '/' or '///' + if name.is_empty() { + continue; + } + + if let Some(e) = self + .entities + .iter() + .position(|(p, n)| (*p, n.as_str()) == (entity, name)) + { + // Found entity + // ? NOTE: Conversion will never panic because the collection + // ? size will only increase in the else branch where a + // ? safe cast is performed + entity = I::from_usize(e); + } else { + // Add entity + let e = self.entities.len(); + self.entities.push((entity, Name::new(name.to_string()))); + self.children.push(smallvec![]); + entity_created = true; + // Soft limit added to save memory, identical to the curve limit + let _parent = entity; + entity = I::from_usize_checked(e); + self.children[_parent.as_usize()].push(entity) + } + } + + (entity, entity_created) + } + + /// Returns the entity path if found. + /// + /// The `NamedHierarchy` stores a the entity path in a specific way to improve search performance + /// thus it needs to rebuilt in the human readable format + pub fn get_entity_path_at(&self, mut entity_index: I) -> Option { + let mut path = None; + + while let Some((parent_index, name)) = self.entities.get(entity_index.as_usize()) { + if let Some(path) = path.as_mut() { + *path = format!("{}/{}", name.as_str(), path); + } else { + path = Some(name.as_str().to_string()); + } + + entity_index = *parent_index; + } + + path + } + + /// Finds an entity given a set of queries, see the example bellow + /// how to proper call this function, + /// + /// ```rust,ignore + /// let mut entities_table_cache = vec![]; + /// entities_table_cache.resize(named_hierarchy.len(), None); + /// // Assign the root entity as the first element + /// entities_table_cache[0] = Some(root); + /// + /// let found_entity = named_hierarchy.find_entity(2, &mut entities_table_cache, &children_query, &name_query); + /// ``` + /// + /// *NOTE* Keep in mind that you can have as many root as you want + /// but each root must be manually find and inserted in the `entities_table_cache` + /// before calling this function. + pub fn find_entity( + &self, + entity_index: I, + entities_table_cache: &mut Vec>, + children_query: &Query<&Children>, + name_query: &Query<(&Parent, &Name)>, + ) -> Option { + if let Some(entity) = &entities_table_cache[entity_index.as_usize()] { + Some(*entity) + } else { + let (parent_index, entity_name) = &self.entities[entity_index.as_usize()]; + + // Use recursion to find the entity parent + self.find_entity( + *parent_index, + entities_table_cache, + children_query, + name_query, + ) + .and_then(|parent_entity| { + if let Ok(children) = children_query.get(parent_entity) { + children + .iter() + .find(|entity| { + if let Ok((current_parent, name)) = name_query.get(**entity) { + // ! FIXME: Parent changes before the children update it self, + // ! to account for that we also must double check entity parent component it self + if current_parent.0 != parent_entity || name != entity_name { + return false; + } + + // Update cache + entities_table_cache[entity_index.as_usize()] = Some(**entity); + true + } else { + false + } + }) + .copied() + } else { + None + } + }) + } + } + + /// Finds an entity given a reference to the (`World`)[bevy_ecs::World], see the example bellow + /// how to proper call this function, + /// + /// ```rust,ignore + /// let mut entities_table_cache = vec![]; + /// entities_table_cache.resize(named_hierarchy.len(), None); + /// // Assign the root entity as the first element + /// entities_table_cache[0] = Some(root); + /// + /// let found_entity = named_hierarchy.find_entity_in_world(2, &mut entities_table_cache, &world); + /// ``` + /// + /// *NOTE* Keep in mind that you can have as many root as you want + /// but each root must be manually find and inserted in the `entities_table_cache` + /// before calling this function. + pub fn find_entity_in_world( + &self, + entity_index: I, + entities_table_cache: &mut Vec>, + world: &World, + ) -> Option { + if let Some(entity) = &entities_table_cache[entity_index.as_usize()] { + Some(*entity) + } else { + let (parent_index, entity_name) = &self.entities[entity_index.as_usize()]; + + // Use recursion to find the entity parent + self.find_entity_in_world(*parent_index, entities_table_cache, world) + .and_then(|parent_entity| { + if let Some(children) = world.get::(parent_entity) { + children + .iter() + .find(|entity| { + if let Some(entity_ref) = world.get_entity(**entity) { + let current_parent = entity_ref.get::(); + let name = entity_ref.get::(); + + // ! FIXME: Parent changes before the children update it self, + // ! to account for that we also must double check entity parent component it self + if current_parent != Some(&Parent(parent_entity)) + || name != Some(entity_name) + { + return false; + } + + // Update cache + entities_table_cache[entity_index.as_usize()] = Some(**entity); + true + } else { + false + } + }) + .copied() + } else { + None + } + }) + } + } +} + +mod private { + use std::{ + convert::TryFrom, + fmt::{Debug, Display}, + }; + + /// Implemented by unsigned types + pub trait Index: Sized + PartialEq + Copy + Clone + Debug + Display { + const MAX_VALUE: Self; + + fn as_usize(&self) -> usize; + fn from_usize(index: usize) -> Self; + fn from_usize_checked(index: usize) -> Self; + } + + macro_rules! impl_index { + ($t:ty) => { + impl Index for $t { + const MAX_VALUE: $t = <$t>::MAX; + + fn as_usize(&self) -> usize { + *self as usize + } + + fn from_usize(index: usize) -> Self { + index as Self + } + + fn from_usize_checked(index: usize) -> Self { + Self::try_from(index).expect(concat!( + "entities limit reached, indexed with ", + stringify!($t) + )) + } + } + }; + } + + impl_index!(u8); + impl_index!(u16); + impl_index!(u32); + impl_index!(u64); + impl_index!(usize); +} + +#[cfg(test)] +mod tests { + use bevy_ecs::{ + schedule::{Schedule, Stage, SystemStage}, + system::{CommandQueue, IntoSystem}, + world::World, + }; + + use super::*; + use crate::{ + hierarchy::{parent_update_system, BuildChildren}, + transform_propagate_system::transform_propagate_system, + }; + + #[test] + fn find_entities_in_world() { + let mut world = World::default(); + + let mut update_stage = SystemStage::parallel(); + update_stage.add_system(parent_update_system.system()); + update_stage.add_system(transform_propagate_system.system()); + + let mut schedule = Schedule::default(); + schedule.add_stage("update", update_stage); + + // Add parent entities + let mut command_queue = CommandQueue::default(); + let mut commands = Commands::new(&mut command_queue, &world); + + let mut entities = Vec::new(); + + let root = commands.spawn().insert(Name::new("root")).id(); + entities.push(root); + + commands.entity(root).with_children(|parent| { + entities.push(parent.spawn().insert(Name::new("entity_1")).id()); + entities.push(parent.spawn().insert(Name::new("entity_2")).id()); + }); + + commands.entity(entities[1]).with_children(|parent| { + entities.push(parent.spawn().insert(Name::new("entity_3")).id()); + }); + + command_queue.apply(&mut world); + schedule.run(&mut world); + + let hierarchy = NamedHierarchy::from_ordered_entities(vec![ + (u16::MAX_VALUE, Name::new("root")), + (0, Name::new("entity_1")), + (0, Name::new("entity_2")), + (1, Name::new("entity_3")), + ]); + + let mut entities_table_cache = vec![]; + entities_table_cache.resize(hierarchy.len(), None); + + // ? NOTE: The root entity must be know prior the search + entities_table_cache[0] = Some(root); + + for (i, entity) in entities.iter().enumerate().skip(1) { + let found = hierarchy.find_entity_in_world(i as u16, &mut entities_table_cache, &world); + assert_eq!(Some(*entity), found); + } + } + + #[test] + fn merge_different_hierarchies() { + let mut hierarchy_a = NamedHierarchy::::new(); + hierarchy_a.get_or_insert_entity("/NodeA0/NodeB0"); + hierarchy_a.get_or_insert_entity("/NodeA1/NodeB1/NodeC0"); + hierarchy_a.get_or_insert_entity("/NodeA2/NodeB1/NodeC1"); + + let mut hierarchy_b = NamedHierarchy::::new(); + hierarchy_b.get_or_insert_entity("/NodeA1/NodeB2/NodeC2"); + hierarchy_b.get_or_insert_entity("/NodeA1/NodeB0/NodeC3"); + + let mut mapped_entities = vec![]; + hierarchy_a.merge(&hierarchy_b, &mut mapped_entities); + + assert!( + mapped_entities.iter().all(|index| *index < u16::MAX), + "some entities weren't mapped or merged" + ); + + for i in 0..hierarchy_b.len() { + assert_eq!( + hierarchy_a.get_entity_path_at(mapped_entities[i]), + hierarchy_b.get_entity_path_at(i as u16) + ); + } + } + + #[test] + fn merge_equal_but_scrambled_hierarchies() { + let mut hierarchy_a = NamedHierarchy::::new(); + hierarchy_a.get_or_insert_entity("/NodeA0/NodeB0"); + hierarchy_a.get_or_insert_entity("/NodeA1/NodeB1/NodeC0"); + hierarchy_a.get_or_insert_entity("/NodeA2/NodeB1/NodeC1"); + hierarchy_a.get_or_insert_entity("/NodeA1/NodeB0/NodeC3"); + + let mut hierarchy_b = NamedHierarchy::::new(); + hierarchy_b.get_or_insert_entity("/NodeA2/NodeB1/NodeC1"); + hierarchy_b.get_or_insert_entity("/NodeA1/NodeB0/NodeC3"); + hierarchy_b.get_or_insert_entity("/NodeA1/NodeB1/NodeC0"); + hierarchy_b.get_or_insert_entity("/NodeA0/NodeB0"); + + let mut mapped_entities = vec![]; + hierarchy_a.merge(&hierarchy_b, &mut mapped_entities); + + assert!( + mapped_entities.iter().all(|index| *index < u16::MAX), + "some entities weren't mapped or merged" + ); + + for i in 0..hierarchy_b.len() { + assert_eq!( + hierarchy_a.get_entity_path_at(mapped_entities[i]), + hierarchy_b.get_entity_path_at(i as u16) + ); + } + } +} From b9566b7cac80783cc07c8aa81882207583b4c4d8 Mon Sep 17 00:00:00 2001 From: Felipe Jorge Date: Mon, 5 Apr 2021 23:04:24 -0300 Subject: [PATCH 2/3] clippy fix --- .../bevy_transform/src/hierarchy/named_hierarchy.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/bevy_transform/src/hierarchy/named_hierarchy.rs b/crates/bevy_transform/src/hierarchy/named_hierarchy.rs index f2962a8c46738..ab6cdae977fde 100644 --- a/crates/bevy_transform/src/hierarchy/named_hierarchy.rs +++ b/crates/bevy_transform/src/hierarchy/named_hierarchy.rs @@ -465,10 +465,10 @@ mod tests { "some entities weren't mapped or merged" ); - for i in 0..hierarchy_b.len() { + for (index, mapped_index) in mapped_entities.iter().copied().enumerate() { assert_eq!( - hierarchy_a.get_entity_path_at(mapped_entities[i]), - hierarchy_b.get_entity_path_at(i as u16) + hierarchy_a.get_entity_path_at(mapped_index), + hierarchy_b.get_entity_path_at(index as u16) ); } } @@ -495,10 +495,10 @@ mod tests { "some entities weren't mapped or merged" ); - for i in 0..hierarchy_b.len() { + for (index, mapped_index) in mapped_entities.iter().copied().enumerate() { assert_eq!( - hierarchy_a.get_entity_path_at(mapped_entities[i]), - hierarchy_b.get_entity_path_at(i as u16) + hierarchy_a.get_entity_path_at(mapped_index), + hierarchy_b.get_entity_path_at(index as u16) ); } } From 0202fc757550f0a3d2e751d90df83b41db1d5a13 Mon Sep 17 00:00:00 2001 From: Felipe Jorge Date: Tue, 6 Apr 2021 19:37:58 -0300 Subject: [PATCH 3/3] cleanup and NO_PARENT const --- .../src/hierarchy/named_hierarchy.rs | 41 ++++++++++++------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/crates/bevy_transform/src/hierarchy/named_hierarchy.rs b/crates/bevy_transform/src/hierarchy/named_hierarchy.rs index ab6cdae977fde..0e154dad85ef3 100644 --- a/crates/bevy_transform/src/hierarchy/named_hierarchy.rs +++ b/crates/bevy_transform/src/hierarchy/named_hierarchy.rs @@ -7,20 +7,18 @@ use smallvec::{smallvec, SmallVec}; use crate::{Children, Parent}; // TODO: Panic if the root entity is not defined prior to a query -// TODO: Create a simplified version without the children ?! -// TODO: Should I just use usize? and get done with it? /// Provides a way of describing a hierarchy or named entities /// and means for finding then in the world /// -/// By default and to save memory the nodes are indexed using `u16` -/// but you can change it when needed. +/// By default and to save memory the nodes are indexed using [`u16`], +/// but you can change to any unsigned type. #[derive(Debug, Clone)] pub struct NamedHierarchy { /// Entity identification made by parent index and name entities: Vec<(I, Name)>, // ? NOTE: SmallVec<[u16; 10]> occupy the same 32 bytes as the SmallVec<[u16; 8]>, but the latter - // ? should be only take 24 bytes using the "union" feature + // ? should be only take 24 bytes (same as Vec) using the "union" feature children: Vec>, } @@ -28,13 +26,16 @@ impl Default for NamedHierarchy { fn default() -> Self { Self { // ? NOTE: Since the root has no parent in this context it points to a place outside the vec bounds - entities: vec![(I::MAX_VALUE, Name::default())], + entities: vec![(Self::NO_PARENT, Name::default())], children: vec![smallvec![]], } } } impl NamedHierarchy { + /// Defines the parent of a root entity + pub const NO_PARENT: I = I::MAX_VALUE; + pub fn new() -> Self { Default::default() } @@ -43,18 +44,25 @@ impl NamedHierarchy { /// this function takes an vec of entities defined by their parent index /// (on the same vec) and name. /// - /// Any root entity should be indexed using `I::MAX_VALUE` or `I::MAX`. + /// Root entities should have a parent as [`NamedHierarchy::NO_PARENT`] or [`I::MAX_VALUE`]. + /// + /// ```rust,ignore + /// NamedHierarchy::from_ordered_entities(vec![ + /// (NamedHierarchy::NO_PARENT, Name::new("root")), + /// ... + /// ]) + /// ``` /// /// Many different root nodes are supported although having other roots /// make hard to search entities, please refer to the documentation of - /// `find_entity` or `find_entity_in_world` to see how. + /// [`find_entity`] or [`find_entity_in_world`] to see how. /// /// **WARNING** Be caution when using this function because it may create a /// an invalid hierarchy pub fn from_ordered_entities(entities: Vec<(I, Name)>) -> Self { assert_eq!( entities[0].0, - I::MAX_VALUE, + Self::NO_PARENT, "first entry must be an root entity" ); @@ -74,10 +82,10 @@ impl NamedHierarchy { /// new entities indexes of merged hierarchy. pub fn merge(&mut self, other_hierarchy: &NamedHierarchy, mapped_entities: &mut Vec) { mapped_entities.clear(); - mapped_entities.resize(other_hierarchy.len(), I::MAX_VALUE); + mapped_entities.resize(other_hierarchy.len(), Self::NO_PARENT); assert!( - other_hierarchy.entities[0].0 == I::MAX_VALUE, + other_hierarchy.entities[0].0 == Self::NO_PARENT, "first element isn't the root" ); @@ -88,7 +96,8 @@ impl NamedHierarchy { self.internal_merge(other_hierarchy, root_index, root_index, mapped_entities); } - // TODO: Expose to allow for merging hierarchies with multiple roots + // TODO: Merging hierarchies with multiple roots + fn internal_merge( &mut self, other_hierarchy: &NamedHierarchy, @@ -220,7 +229,7 @@ impl NamedHierarchy { /// ```rust,ignore /// let mut entities_table_cache = vec![]; /// entities_table_cache.resize(named_hierarchy.len(), None); - /// // Assign the root entity as the first element + /// // Assign the root entity as the first element (all roots if more than one must be assigned) /// entities_table_cache[0] = Some(root); /// /// let found_entity = named_hierarchy.find_entity(2, &mut entities_table_cache, &children_query, &name_query); @@ -236,6 +245,7 @@ impl NamedHierarchy { children_query: &Query<&Children>, name_query: &Query<(&Parent, &Name)>, ) -> Option { + assert!(entity_index != Self::NO_PARENT, "root not assigned"); if let Some(entity) = &entities_table_cache[entity_index.as_usize()] { Some(*entity) } else { @@ -275,13 +285,13 @@ impl NamedHierarchy { } } - /// Finds an entity given a reference to the (`World`)[bevy_ecs::World], see the example bellow + /// Finds an entity given a reference to the [`World`], see the example bellow /// how to proper call this function, /// /// ```rust,ignore /// let mut entities_table_cache = vec![]; /// entities_table_cache.resize(named_hierarchy.len(), None); - /// // Assign the root entity as the first element + /// // Assign the root entity as the first element (all roots if more than one must be assigned) /// entities_table_cache[0] = Some(root); /// /// let found_entity = named_hierarchy.find_entity_in_world(2, &mut entities_table_cache, &world); @@ -296,6 +306,7 @@ impl NamedHierarchy { entities_table_cache: &mut Vec>, world: &World, ) -> Option { + assert!(entity_index != Self::NO_PARENT, "root not assigned"); if let Some(entity) = &entities_table_cache[entity_index.as_usize()] { Some(*entity) } else {