6 releases (breaking)
Uses new Rust 2024
| 0.6.0 | Feb 16, 2026 |
|---|---|
| 0.5.0 | Feb 12, 2026 |
| 0.4.0 | Feb 12, 2026 |
| 0.3.0 | Feb 12, 2026 |
| 0.1.0 | Jan 27, 2026 |
#178 in Game dev
Used in 2 crates
(via bevy_autodiff)
85KB
1.5K
SLoC
bevy_entity_ptr
Smart-pointer-like access to entities in bevy_ecs, a high-performance Entity Component System library. Immutable only, by design.
Why This Crate?
When working with entity relationships in ECS (parent/child hierarchies, linked structures, graphs), accessing related entities requires repeatedly passing &World through every function call. This crate provides two approaches that make entity traversal ergonomic:
| Type | Safety | Ergonomics | Use When |
|---|---|---|---|
EntityPtr |
Safe API* | No lifetime params | Graph traversal, recursion, deep chains |
BoundEntity<'w> |
Fully safe | Scoped lifetime | Simple access, compiler-checked lifetimes |
EntityHandle |
Fully safe | Explicit world param | Store in components |
*One internal unsafe hidden by the WorldExt extension trait — see Safety.
Installation
[dependencies]
bevy_entity_ptr = "0.6"
Quick Start
Import the WorldExt trait and use world.entity_ptr() — no unsafe needed:
use bevy_ecs::prelude::*;
use bevy_entity_ptr::{WorldExt, EntityPtr, EntityHandle};
#[derive(Component)]
struct Manager(EntityHandle);
#[derive(Component)]
struct Label(&'static str);
// Follow a reference to a related entity — no &World parameter needed
fn get_manager_label(employee: EntityPtr) -> Option<&'static str> {
employee
.follow::<Manager, _>(|m| m.0)?
.get::<Label>()
.map(|l| l.0)
}
// Usage: world.entity_ptr() creates an EntityPtr from any &World context
fn example(world: &World, entity: Entity) {
let ptr = world.entity_ptr(entity);
if let Some(label) = get_manager_label(ptr) {
println!("Manager: {}", label);
}
}
Recursive Traversal
EntityPtr carries its world reference internally, so recursive functions don't need a &World parameter:
use bevy_ecs::prelude::*;
use bevy_entity_ptr::{WorldExt, EntityPtr, EntityHandle};
#[derive(Component)]
struct ParentRef(EntityHandle);
#[derive(Component)]
struct Children(Vec<EntityHandle>);
#[derive(Component)]
struct Size(f64);
// Find the root of a hierarchy — no &World parameter needed
fn find_root(node: EntityPtr) -> EntityPtr {
match node.follow::<ParentRef, _>(|p| p.0) {
Some(parent) => find_root(parent),
None => node,
}
}
// Sum a value across an entire subtree
fn subtree_size(node: EntityPtr) -> f64 {
let my_size = node.get::<Size>().map(|s| s.0).unwrap_or(0.0);
let children_size: f64 = node
.get::<Children>()
.map(|c| {
c.0.iter()
.map(|h| subtree_size(node.follow_handle(*h)))
.sum()
})
.unwrap_or(0.0);
my_size + children_size
}
Optional References
Use follow_opt when a reference component might be None:
use bevy_ecs::prelude::*;
use bevy_entity_ptr::{EntityPtr, EntityHandle};
#[derive(Component)]
struct Supervisor(Option<EntityHandle>);
#[derive(Component)]
struct Label(&'static str);
fn get_supervisor_label(employee: EntityPtr) -> Option<&'static str> {
employee
.follow_opt::<Supervisor, _>(|s| s.0)?
.get::<Label>()
.map(|l| l.0)
}
Safety
The WorldExt::entity_ptr() method internally transmutes &World to &'static World so that EntityPtr can carry the world reference without a lifetime parameter. This is what makes the ergonomic API possible — but because the 'static lifetime is fabricated, the compiler cannot catch use-after-free on the world reference.
Sound within ECS systems: When called from a system with &World access, the world is guaranteed to outlive the system scope. EntityPtr is !Send, preventing escape to other threads. All operations are read-only.
Not sound in arbitrary code: Because EntityPtr holds a 'static reference internally, the compiler won't prevent you from using it after the World is dropped. This would be undefined behavior.
// GOOD: EntityPtr used within a function that borrows &World
fn process_entities(world: &World, entities: &[Entity]) {
for &entity in entities {
let ptr = world.entity_ptr(entity);
// ... use ptr ...
} // ptr dropped before &World borrow ends
}
// BAD: Do NOT do this — the 'static lifetime means the compiler won't stop you
fn bad_example() {
let mut world = World::new();
let entity = world.spawn(()).id();
let ptr = world.entity_ptr(entity);
drop(world); // World dropped — but ptr still holds a 'static reference!
// ptr.get::<T>(); // undefined behavior — dangling 'static reference
}
For fully safe code with no soundness caveats, use EntityHandle and BoundEntity<'w> — they carry proper lifetime parameters and are checked by the compiler.
Fully Safe Alternative: EntityHandle + BoundEntity
If you prefer zero unsafe with compiler-verified lifetimes:
use bevy_ecs::prelude::*;
use bevy_entity_ptr::{EntityHandle, BoundEntity};
#[derive(Component)]
struct ParentRef(EntityHandle);
#[derive(Component)]
struct Label(&'static str);
fn find_parent_label<'w>(entity: Entity, world: &'w World) -> Option<&'w str> {
let bound = EntityHandle::new(entity).bind(world);
let parent = bound.follow::<ParentRef, _>(|p| p.0)?;
parent.get::<Label>().map(|l| l.0)
}
EntityHandle is Send + Sync, making it safe to store in components.
Mixed Usage
Store handles in components, convert to smart pointers for traversal:
use bevy_ecs::prelude::*;
use bevy_entity_ptr::{EntityHandle, WorldExt, EntityPtr};
// EntityHandle is Send + Sync — safe to store in components
#[derive(Component)]
struct Related {
items: Vec<EntityHandle>,
}
#[derive(Component)]
struct Weight(f32);
fn total_weight(node: EntityPtr) -> f32 {
node.get::<Related>()
.map(|rel| {
rel.items
.iter()
.filter_map(|h| node.follow_handle(*h).get::<Weight>())
.map(|w| w.0)
.sum()
})
.unwrap_or(0.0)
}
Navigation Traits (Optional)
Enable the nav-traits feature for parent/child navigation helpers:
[dependencies]
bevy_entity_ptr = { version = "0.6", features = ["nav-traits"] }
Implement the traits on your components:
use bevy_ecs::prelude::*;
use bevy_entity_ptr::{WorldExt, EntityHandle, HasParent, HasChildren};
#[derive(Component)]
struct ParentRef(Option<EntityHandle>);
impl HasParent for ParentRef {
fn parent_handle(&self) -> Option<EntityHandle> {
self.0
}
}
#[derive(Component)]
struct ChildRefs(Vec<EntityHandle>);
impl HasChildren for ChildRefs {
fn children_handles(&self) -> &[EntityHandle] {
&self.0
}
}
fn navigate(world: &World, entity: Entity) {
let ptr = world.entity_ptr(entity);
// Navigate to parent
if let Some(parent) = ptr.nav().parent::<ParentRef>() {
println!("Has parent: {:?}", parent.entity());
}
// Iterate children (returns an iterator, zero allocation)
let child_count = ptr.nav_many().children::<ChildRefs>().count();
println!("Has {} children", child_count);
}
Thread Safety
| Type | Send | Sync | Notes |
|---|---|---|---|
EntityHandle |
Yes | Yes | Safe to store in components |
BoundEntity<'w> |
No | No | Borrows &World |
WorldRef |
No | No | System-scoped only |
EntityPtr |
No | No | System-scoped only |
Multiple read-only systems can use bevy_entity_ptr concurrently — the scheduler runs them in parallel when all systems only read.
Using EntityPtr in Collections
EntityPtr implements Eq and Hash (comparing entity ID only), enabling use in HashSet and HashMap:
use std::collections::HashSet;
use bevy_ecs::prelude::*;
use bevy_entity_ptr::{WorldExt, EntityPtr};
fn collect_unique(world: &World, entities: &[Entity]) -> HashSet<Entity> {
let mut seen = HashSet::new();
for &entity in entities {
seen.insert(world.entity_ptr(entity));
}
seen.into_iter().map(|ptr| ptr.entity()).collect()
}
Stale Reference Handling
Both approaches gracefully handle despawned entities — returning None instead of undefined behavior:
use bevy_ecs::prelude::*;
use bevy_entity_ptr::EntityHandle;
#[derive(Component)]
struct Label(&'static str);
fn stale_handling(world: &mut World) {
let entity = world.spawn(Label("temporary")).id();
let handle = EntityHandle::new(entity);
assert!(handle.is_alive(world));
assert_eq!(handle.get::<Label>(world).unwrap().0, "temporary");
world.despawn(entity);
// Gracefully returns None — no undefined behavior
assert!(!handle.is_alive(world));
assert!(handle.get::<Label>(world).is_none());
}
What This Crate Does NOT Support (By Design)
- Mutable access — Use the ECS's native APIs for mutations
- Despawning — Use
world.despawn()directly - Component insertion/removal — Use the ECS's native APIs
- Cross-scope storage of
EntityPtr— UseEntityHandleor rawEntityfor storage
Bevy Compatibility
bevy_entity_ptr |
Bevy |
|---|---|
| 0.6 | 0.18 |
| 0.5 | 0.18 |
| 0.4 | 0.17 |
| 0.3 | 0.16 |
| 0.2 | 0.15 |
| 0.1 | 0.15 |
Development
This crate is co-developed with Claude Code.
License
MIT
Dependencies
~13MB
~240K SLoC