Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

  1. The field name (snake_case). Used to generate the accessor methods.
  2. The component type. The Rust struct stored in the archetype tables.
  3. 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

FlagFieldTypeCategory
ANIMATION_PLAYERanimation_playerAnimationPlayerAnimation
NAMEnameNameIdentity
LOCAL_TRANSFORMlocal_transformLocalTransformTransform
GLOBAL_TRANSFORMglobal_transformGlobalTransformTransform
LOCAL_TRANSFORM_DIRTYlocal_transform_dirtyLocalTransformDirtyTransform
PARENTparentParentTransform
IGNORE_PARENT_SCALEignore_parent_scaleIgnoreParentScaleTransform
AUDIO_SOURCEaudio_sourceAudioSourceAudio
AUDIO_LISTENERaudio_listenerAudioListenerAudio
CAMERAcameraCameraCamera
PAN_ORBIT_CAMERApan_orbit_cameraPanOrbitCameraCamera
LIGHTlightLightLighting
LINESlinesLinesDebug
VISIBILITYvisibilityVisibilityRendering
DECALdecalDecalRendering
RENDER_MESHrender_meshRenderMeshRendering
MATERIAL_REFmaterial_refMaterialRefRendering
RENDER_LAYERrender_layerRenderLayerRendering
TEXTtextTextText
TEXT_CHARACTER_COLORStext_character_colorsTextCharacterColorsText
TEXT_CHARACTER_BACKGROUND_COLORStext_character_background_colorsTextCharacterBackgroundColorsText
BOUNDING_VOLUMEbounding_volumeBoundingVolumeSpatial
HOVEREDhoveredHoveredInput
ROTATIONrotationRotationTransform
CASTS_SHADOWcasts_shadowCastsShadowRendering
RIGID_BODYrigid_bodyRigidBodyComponentPhysics
COLLIDERcolliderColliderComponentPhysics
CHARACTER_CONTROLLERcharacter_controllerCharacterControllerComponentPhysics
COLLISION_LISTENERcollision_listenerCollisionListenerPhysics
PHYSICS_INTERPOLATIONphysics_interpolationPhysicsInterpolationPhysics
INSTANCED_MESHinstanced_meshInstancedMeshRendering
PARTICLE_EMITTERparticle_emitterParticleEmitterParticles
PREFAB_SOURCEprefab_sourcePrefabSourcePrefabs
PREFAB_INSTANCEprefab_instancePrefabInstancePrefabs
SKINskinSkinAnimation
JOINTjointJointAnimation
MORPH_WEIGHTSmorph_weightsMorphWeightsAnimation
NAVMESH_AGENTnavmesh_agentNavMeshAgentNavigation
GRASS_REGIONgrass_regionGrassRegionRendering
GRASS_INTERACTORgrass_interactorGrassInteractorRendering

Resources

FieldTypeFeature Gate
windowWindowalways
secondary_windowsSecondaryWindowsalways
user_interfaceUserInterfacealways
graphicsGraphicsalways
inputInputalways
audioAudioEngineaudio
physicsPhysicsWorldphysics
navmeshNavMeshWorldalways
text_cacheTextCachealways
mesh_cacheMeshCachealways
animation_cacheAnimationCachealways
prefab_cachePrefabCachealways
material_registryMaterialRegistryalways
texture_cacheTextureCachealways
active_cameraOption<Entity>always
event_busEventBusalways
command_queueVec<WorldCommand>always
entity_namesHashMap<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.