The freecs Macro
freecs::ecs! is one declarative macro that takes a list of components, tags, events, and resources and emits the entire World. The struct, the per-component fields on each archetype table, the typed accessors, the spawn and despawn code, the query iterator, the archetype graph, the query cache, the change-detection tick stamps, and the Resources block all come out of a single invocation.
Macro syntax
#![allow(unused)] fn main() { freecs::ecs! { World { Core { local_transform: LocalTransform => LOCAL_TRANSFORM, global_transform: GlobalTransform => GLOBAL_TRANSFORM, render_mesh: RenderMesh => RENDER_MESH, material_ref: MaterialRef => MATERIAL_REF, camera: Camera => CAMERA, light: Light => LIGHT, } Ui { ui_layout_root: UiLayoutRoot => UI_LAYOUT_ROOT, ui_layout_node: UiLayoutNode => UI_LAYOUT_NODE, } } Resources { window: Window, input: Input, graphics: Graphics, active_camera: Option<Entity>, } } }
Nightshade splits its components across two sub-worlds. Core holds the 3D engine components. Ui holds the retained UI components. Accessors are scoped to their sub-world. The light getter is world.core.get_light(entity). The UI node getter is world.ui.get_ui_layout_node(entity).
Each line in a component block has three parts.
- The field name (snake_case). Used to generate the accessor methods.
- The component type. The Rust struct stored in the archetype tables.
- The flag constant (UPPER_SNAKE_CASE). The bit position used in queries.
What the macro generates
The World struct
#![allow(unused)] fn main() { pub struct World { entities: EntityStorage, pub resources: Resources, } }
EntityStorage holds the archetype tables, the entity location map, the allocator, and the caches. The resources field holds whatever was declared in the Resources block.
Per-component accessors
For every foo: Foo => FOO, the macro stamps out the three accessors a system needs.
#![allow(unused)] fn main() { world.core.get_foo(entity) -> Option<&Foo> world.core.get_foo_mut(entity) -> Option<&mut Foo> world.core.set_foo(entity, value: Foo) }
get_foo and get_foo_mut return None for a stale or invalid handle, and for a live entity whose archetype does not contain Foo. set_foo writes the value if the slot exists. The setter does not add the component to entities that did not have it at spawn time. To add a component to a live entity, see add_components below.
Entity management
#![allow(unused)] fn main() { world.spawn_entities(flags: ComponentFlags, count: usize) -> Vec<Entity> world.despawn_entities(entities: &[Entity]) world.core.entity_has_components(entity: Entity, flags: ComponentFlags) -> bool world.core.add_components(entity: Entity, flags: ComponentFlags) }
spawn_entities allocates count entities into the archetype identified by flags and returns their handles. despawn_entities removes a batch of entities and recycles their ids with a bumped generation. add_components migrates an entity to the archetype current_mask | flags, leaving its existing components in place.
Query methods
#![allow(unused)] fn main() { world.core.query_entities(flags: ComponentFlags) -> impl Iterator<Item = Entity> }
Yields every entity whose archetype is a superset of flags. See Queries and Iteration for the closure-form and structural-change patterns.
Component flag constants
#![allow(unused)] fn main() { pub const LOCAL_TRANSFORM: ComponentFlags = 1 << 0; pub const GLOBAL_TRANSFORM: ComponentFlags = 1 << 1; pub const RENDER_MESH: ComponentFlags = 1 << 2; // one constant per component, ascending bit positions }
ComponentFlags is a u64, so a world holds at most 64 component types per sub-world. Nightshade declares 45 in Core and 10 in Ui, well under the ceiling.
The Resources struct
#![allow(unused)] fn main() { pub struct Resources { pub window: Window, pub input: Input, pub graphics: Graphics, pub active_camera: Option<Entity>, } }
Every resource is a public field with Default initialization. Systems read and write directly. world.resources.graphics.bloom_enabled = true is the API.
Nightshade's declaration
The engine declares 45 core components and 10 UI components across the two sub-worlds, plus 30-odd resources. The core component table follows.
Components
| Flag | Field | Type | Category |
|---|---|---|---|
ANIMATION_PLAYER | animation_player | AnimationPlayer | Animation |
NAME | name | Name | Identity |
LOCAL_TRANSFORM | local_transform | LocalTransform | Transform |
GLOBAL_TRANSFORM | global_transform | GlobalTransform | Transform |
LOCAL_TRANSFORM_DIRTY | local_transform_dirty | LocalTransformDirty | Transform |
PARENT | parent | Parent | Transform |
IGNORE_PARENT_SCALE | ignore_parent_scale | IgnoreParentScale | Transform |
AUDIO_SOURCE | audio_source | AudioSource | Audio |
AUDIO_LISTENER | audio_listener | AudioListener | Audio |
CAMERA | camera | Camera | Camera |
PAN_ORBIT_CAMERA | pan_orbit_camera | PanOrbitCamera | Camera |
LIGHT | light | Light | Lighting |
LINES | lines | Lines | Debug |
VISIBILITY | visibility | Visibility | Rendering |
DECAL | decal | Decal | Rendering |
RENDER_MESH | render_mesh | RenderMesh | Rendering |
MATERIAL_REF | material_ref | MaterialRef | Rendering |
RENDER_LAYER | render_layer | RenderLayer | Rendering |
TEXT | text | Text | Text |
TEXT_CHARACTER_COLORS | text_character_colors | TextCharacterColors | Text |
TEXT_CHARACTER_BACKGROUND_COLORS | text_character_background_colors | TextCharacterBackgroundColors | Text |
BOUNDING_VOLUME | bounding_volume | BoundingVolume | Spatial |
HOVERED | hovered | Hovered | Input |
ROTATION | rotation | Rotation | Transform |
CASTS_SHADOW | casts_shadow | CastsShadow | Rendering |
RIGID_BODY | rigid_body | RigidBodyComponent | Physics |
COLLIDER | collider | ColliderComponent | Physics |
CHARACTER_CONTROLLER | character_controller | CharacterControllerComponent | Physics |
COLLISION_LISTENER | collision_listener | CollisionListener | Physics |
PHYSICS_INTERPOLATION | physics_interpolation | PhysicsInterpolation | Physics |
INSTANCED_MESH | instanced_mesh | InstancedMesh | Rendering |
PARTICLE_EMITTER | particle_emitter | ParticleEmitter | Particles |
PREFAB_SOURCE | prefab_source | PrefabSource | Prefabs |
PREFAB_INSTANCE | prefab_instance | PrefabInstance | Prefabs |
SKIN | skin | Skin | Animation |
JOINT | joint | Joint | Animation |
MORPH_WEIGHTS | morph_weights | MorphWeights | Animation |
NAVMESH_AGENT | navmesh_agent | NavMeshAgent | Navigation |
GRASS_REGION | grass_region | GrassRegion | Rendering |
GRASS_INTERACTOR | grass_interactor | GrassInteractor | Rendering |
Resources
| Field | Type | Feature Gate |
|---|---|---|
window | Window | always |
secondary_windows | SecondaryWindows | always |
user_interface | UserInterface | always |
graphics | Graphics | always |
input | Input | always |
audio | AudioEngine | audio |
physics | PhysicsWorld | physics |
navmesh | NavMeshWorld | always |
text_cache | TextCache | always |
mesh_cache | MeshCache | always |
animation_cache | AnimationCache | always |
prefab_cache | PrefabCache | always |
material_registry | MaterialRegistry | always |
texture_cache | TextureCache | always |
active_camera | Option<Entity> | always |
event_bus | EventBus | always |
command_queue | Vec<WorldCommand> | always |
entity_names | HashMap<String, Entity> | always |
Feature-gated resources are wrapped in #[cfg(feature = "...")] inside the macro invocation. They simply do not exist on the Resources struct when the feature is off.