Skip to content
Draft
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
95 changes: 93 additions & 2 deletions crates/bevy_ecs/src/archetype.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ use alloc::{boxed::Box, vec::Vec};
use bevy_platform::collections::{hash_map::Entry, HashMap};
use core::{
hash::Hash,
ops::{Index, IndexMut, RangeFrom},
ops::{Index, IndexMut, Range, RangeFrom},
};
use nonmax::NonMaxU32;

Expand Down Expand Up @@ -327,6 +327,7 @@ impl Edges {
}

/// Metadata about an [`Entity`] in a [`Archetype`].
#[derive(Clone, Copy)]
pub struct ArchetypeEntity {
entity: Entity,
table_row: TableRow,
Expand Down Expand Up @@ -394,6 +395,7 @@ pub struct Archetype {
table_id: TableId,
edges: Edges,
entities: Vec<ArchetypeEntity>,
pub(crate) disabled_entities: u32,
components: ImmutableSparseSet<ComponentId, ArchetypeComponentInfo>,
pub(crate) flags: ArchetypeFlags,
}
Expand Down Expand Up @@ -453,6 +455,7 @@ impl Archetype {
id,
table_id,
entities: Vec::new(),
disabled_entities: 0,
components: archetype_components.into_immutable(),
edges: Default::default(),
flags,
Expand Down Expand Up @@ -485,9 +488,26 @@ impl Archetype {
&self.entities
}

/// Fetches the enabled entities contained in this archetype.
#[inline]
pub fn enabled_entities(&self) -> &[ArchetypeEntity] {
&self.entities[self.disabled_entities as usize..]
}

/// Get the valid table rows (i.e. non-disabled entities).
#[inline]
pub fn archetype_rows(&self) -> Range<u32> {
// - No entity row may be in more than one table row at once, so there are no duplicates,
// and there can not be an entity row of u32::MAX. Therefore, this can not be max either.
// - self.disabled_entities <= self.len()
self.disabled_entities..self.len()
}

/// Fetches the entities contained in this archetype.
#[inline]
pub fn entities_with_location(&self) -> impl Iterator<Item = (Entity, EntityLocation)> {
pub fn entities_with_location(
&self,
) -> impl DoubleEndedIterator<Item = (Entity, EntityLocation)> + ExactSizeIterator {
self.entities.iter().enumerate().map(
|(archetype_row, &ArchetypeEntity { entity, table_row })| {
(
Expand Down Expand Up @@ -623,6 +643,71 @@ impl Archetype {
self.entities.reserve(additional);
}

/// Disables or enables the entity at `row` by swapping it with the first
/// enabled or disabled entity. Returns the swapped entities with their
/// respective table and archetype rows, or `None` if no swap occurred. If a
/// swap occurred, the caller is responsible for updating the entity's
/// location.
///
/// # Panics
/// This function will panic if `row >= self.entities.len()`
#[inline]
pub(crate) fn swap_disable(
&mut self,
row: ArchetypeRow,
) -> (
(ArchetypeEntity, ArchetypeRow),
Option<(ArchetypeEntity, ArchetypeRow)>,
) {
debug_assert!(row.index_u32() < self.len());

let disabled_row = if row.index_u32() >= self.disabled_entities {
// the entity is currently enabled, swap it with the first enabled entity:

// SAFETY: `self.disabled_entities` is always less than `u32::MAX`,
// as guaranteed by `allocate`.
let disabled_row =
ArchetypeRow::new(unsafe { NonMaxU32::new_unchecked(self.disabled_entities) });
self.disabled_entities += 1;

disabled_row
} else {
self.disabled_entities -= 1;
// the entity is currently disabled, swap it with the last disabled entity:

// SAFETY: `self.disabled_entities` is always less than `u32::MAX`,
// as guaranteed by `allocate`.
ArchetypeRow::new(unsafe { NonMaxU32::new_unchecked(self.disabled_entities) })
};

if row == disabled_row {
// the entity is already in the correct position, no swap needed

// SAFETY: `row` is guaranteed to be in-bounds.
(
(unsafe { *self.entities.get_unchecked(row.index()) }, row),
None,
)
} else {
// SAFETY: `self.disabled_entities` is always less than `u32::MAX`, as guaranteed by `allocate`.
let disabled_row =
ArchetypeRow::new(unsafe { NonMaxU32::new_unchecked(self.disabled_entities) });

self.entities.swap(row.index(), disabled_row.index());

// SAFETY: Both `row` and `other` are guaranteed to be in-bounds.
unsafe {
(
(
*self.entities.get_unchecked(disabled_row.index()),
disabled_row,
),
Some((*self.entities.get_unchecked(row.index()), row)),
)
}
}
}

/// Removes the entity at `row` by swapping it out. Returns the table row the entity is stored
/// in.
///
Expand Down Expand Up @@ -650,6 +735,12 @@ impl Archetype {
self.entities.len() as u32
}

/// Gets the number of entities that belong to the archetype, without disabled entities.
#[inline]
pub fn entity_count(&self) -> u32 {
self.len() - self.disabled_entities
}

/// Checks if the archetype has any entities.
#[inline]
pub fn is_empty(&self) -> bool {
Expand Down
10 changes: 10 additions & 0 deletions crates/bevy_ecs/src/entity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1318,6 +1318,16 @@ pub struct EntityLocation {
/// It is also useful for reserving an id; commands will often allocate an `Entity` but not provide it a location until the command is applied.
pub type EntityIdLocation = Option<EntityLocation>;

/// An [`Entity`] that has been disabled. This entity will not be found by queries or accessible via other ECS operations, but it's components are still stored in the ECS.
/// This is useful for temporarily disabling an entity without fully despawning it or invoking archetype moves. This entity keeps track of its location, so it can be re-enabled later.
pub struct DisabledEntity {
/// The disabled entity.
pub entity: Entity,
/// The location of the entity after it was disabled.
/// This may not necessarily be it's location when it is re-enabled.
pub location: EntityLocation,
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
24 changes: 8 additions & 16 deletions crates/bevy_ecs/src/query/fetch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1725,15 +1725,11 @@ unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> {
) {
let column = table.get_column(component_id).debug_checked_unwrap();
let table_data = Some((
column.get_data_slice(table.entity_count() as usize).into(),
column.get_data_slice(table.len() as usize).into(),
column.get_added_ticks_slice(table.len() as usize).into(),
column.get_changed_ticks_slice(table.len() as usize).into(),
column
.get_added_ticks_slice(table.entity_count() as usize)
.into(),
column
.get_changed_ticks_slice(table.entity_count() as usize)
.into(),
column
.get_changed_by_slice(table.entity_count() as usize)
.get_changed_by_slice(table.len() as usize)
.map(Into::into),
));
// SAFETY: set_table is only called when T::STORAGE_TYPE = StorageType::Table
Expand Down Expand Up @@ -1931,15 +1927,11 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T {
) {
let column = table.get_column(component_id).debug_checked_unwrap();
let table_data = Some((
column.get_data_slice(table.entity_count() as usize).into(),
column
.get_added_ticks_slice(table.entity_count() as usize)
.into(),
column
.get_changed_ticks_slice(table.entity_count() as usize)
.into(),
column.get_data_slice(table.len() as usize).into(),
column.get_added_ticks_slice(table.len() as usize).into(),
column.get_changed_ticks_slice(table.len() as usize).into(),
column
.get_changed_by_slice(table.entity_count() as usize)
.get_changed_by_slice(table.len() as usize)
.map(Into::into),
));
// SAFETY: set_table is only called when T::STORAGE_TYPE = StorageType::Table
Expand Down
Loading