Entities
An entity is a handle. It owns nothing. It is a generational id the storage uses to locate the components that belong to it.
#![allow(unused)] fn main() { pub use freecs::Entity; }
Entity is a Copy struct holding an index and a generation counter. The index points at a slot in the entity location map. The generation is bumped every time that slot is recycled, which is how stale handles get rejected. If you despawn entity 42 and the allocator hands 42 back out for a new entity, the old handle still has the old generation and will fail to resolve. Stale lookups return None instead of silently pointing at the wrong entity.
Spawning
spawn_entities takes a component flag mask and a count, and returns a Vec<Entity> of newly-created entities sitting in the archetype defined by that mask. Components start at Default::default().
#![allow(unused)] fn main() { let entity = world.spawn_entities( LOCAL_TRANSFORM | GLOBAL_TRANSFORM | RENDER_MESH | MATERIAL_REF, 1, )[0]; }
Set the component values after spawn.
#![allow(unused)] fn main() { world.core.set_local_transform(entity, LocalTransform { translation: Vec3::new(0.0, 1.0, 0.0), rotation: Quat::identity(), scale: Vec3::new(1.0, 1.0, 1.0), }); world.core.set_render_mesh(entity, RenderMesh { name: "cube".to_string(), id: None, }); world.core.set_material_ref(entity, MaterialRef::new("Default")); }
Batch spawning
The count parameter is the speed-versus-individual-handling trade-off. Spawning a hundred entities in one call writes the archetype table once and grows each component vec once, instead of a hundred separate spawns each doing the same work.
#![allow(unused)] fn main() { let entities = world.spawn_entities(LOCAL_TRANSFORM | GLOBAL_TRANSFORM, 100); for (index, entity) in entities.iter().enumerate() { world.core.set_local_transform(*entity, LocalTransform { translation: Vec3::new(index as f32 * 2.0, 0.0, 0.0), ..Default::default() }); } }
Helper functions
Common entity shapes have spawn helpers in the prelude. The helpers pick the right component mask, spawn the entity, and write sensible defaults.
#![allow(unused)] fn main() { spawn_cube_at(world, Vec3::new(0.0, 1.0, 0.0)); spawn_sphere_at(world, Vec3::new(2.0, 1.0, 0.0)); spawn_plane_at(world, Vec3::zeros()); spawn_cylinder_at(world, Vec3::new(4.0, 1.0, 0.0)); spawn_cone_at(world, Vec3::new(6.0, 1.0, 0.0)); spawn_torus_at(world, Vec3::new(8.0, 1.0, 0.0)); let camera = spawn_camera(world, Vec3::new(0.0, 5.0, 10.0), "Main Camera".to_string()); world.resources.active_camera = Some(camera); let sun = spawn_sun(world); let (player_entity, camera_entity) = spawn_first_person_player(world, Vec3::new(0.0, 2.0, 0.0)); }
Loading models
Models from glTF and GLB files come through the prefab pipeline. The importer extracts textures, meshes, and prefab definitions, and the game inserts each piece into the right cache before spawning instances.
#![allow(unused)] fn main() { use nightshade::ecs::prefab::*; let model_bytes = include_bytes!("../assets/character.glb"); let result = import_gltf_from_bytes(model_bytes)?; for (name, (rgba_data, width, height)) in result.textures { world.queue_command(WorldCommand::LoadTexture { name, rgba_data, width, height, }); } for (name, mesh) in result.meshes { mesh_cache_insert(&mut world.resources.mesh_cache, name, mesh); } for prefab in result.prefabs { spawn_prefab_with_animations( world, &prefab, &result.animations, Vec3::new(0.0, 0.0, 0.0), ); } }
Textures go through the command queue rather than the cache directly because texture uploads require GPU access and have to wait for the render phase. See Tags, Events, and Commands for the rest of the deferred-operation API.
Despawning
#![allow(unused)] fn main() { world.despawn_entities(&[entity]); despawn_recursive_immediate(world, entity); world.queue_command(WorldCommand::DespawnRecursive { entity }); }
The three calls do different things. despawn_entities removes a single entity and recycles its handle. despawn_recursive_immediate walks the entity's transform hierarchy and despawns every descendant alongside it, immediately. The queued DespawnRecursive command does the same recursive walk but waits until the next command-processing point, which is the safe choice when the iteration borrowing the world cannot yet release it.
Adding components after spawn
add_components migrates an entity to a new archetype that includes the requested bits. The existing components are moved across, the newly-added components start at Default::default(), and the entity's location is updated.
#![allow(unused)] fn main() { world.core.add_components(entity, AUDIO_SOURCE); world.core.set_audio_source(entity, AudioSource::new("music").playing()); world.core.add_components(camera, AUDIO_LISTENER); world.core.set_audio_listener(camera, AudioListener); }
The cost of add_components is one archetype migration. Every existing component on the entity gets moved between tables. For markers that flip every frame, that cost is wasted and a tag is the better fit. For one-time additions like "give this camera an audio listener," it is fine.
Checking components
#![allow(unused)] fn main() { if world.core.entity_has_components(entity, RENDER_MESH) { // entity has a mesh } if world.core.entity_has_components(entity, ANIMATION_PLAYER | SKIN) { // entity is an animated skinned model } }
The check is a single bitwise AND against the archetype mask. It does not touch the entity's component data.
Parent-child relationships
Children are entities with a Parent component pointing at the parent's handle. The parent does not need to know about its children up front. The transform system walks the dirty-flag chain to propagate matrices, and the children cache (world.resources.children_cache) is rebuilt incrementally from the parent links.
#![allow(unused)] fn main() { let parent = world.spawn_entities(LOCAL_TRANSFORM | GLOBAL_TRANSFORM, 1)[0]; let child = world.spawn_entities(LOCAL_TRANSFORM | GLOBAL_TRANSFORM | PARENT, 1)[0]; world.core.set_parent(child, Parent(Some(parent))); world.core.set_local_transform(child, LocalTransform { translation: Vec3::new(0.0, 1.0, 0.0), ..Default::default() }); }
Full coverage of hierarchical transforms is in Transform Hierarchy.