Entity Component System

Learn how Aether's archetype-based ECS drives every object in the engine, from avatars to terrain, with cache-friendly storage and parallel system scheduling.

Overview

The aether-ecs crate is the foundational layer of the Aether engine. Every object in a world -- avatars, props, terrain chunks, particles, UI panels -- is an entity composed of components and processed by systems. This architecture separates data from logic, enabling massive parallelism and cache-friendly memory access that are critical for VR's sub-20ms frame budgets.

Aether's ECS follows the archetype model: entities sharing the same set of component types are stored together in contiguous memory, maximizing CPU cache utilization.

Entities

An entity is a lightweight identifier -- two 32-bit integers. The index points to an allocator slot, while the generation counter prevents the ABA problem when slots are recycled.

let mut world = aether_ecs::World::new();
let entity = world.spawn();
assert!(world.is_alive(entity));

// Despawn recycles the slot and bumps the generation
world.despawn(entity);
assert!(!world.is_alive(entity));

When despawned, the index enters a free list. The next spawn() reuses it with an incremented generation, so stale handles are safely detected as invalid.

Components

Components are plain data types implementing the Component trait. They must be 'static + Send + Sync for parallel system execution.

use aether_ecs::component::{Component, ReplicationMode};

struct Transform {
    position: [f32; 3],
    rotation: [f32; 4],
    scale: [f32; 3],
}
impl Component for Transform {
    fn replication_mode() -> ReplicationMode {
        ReplicationMode::Replicated
    }
}

struct AiState { behavior_tree_id: u32 }
impl Component for AiState {} // Defaults to ServerOnly

Each type gets a ComponentId derived from TypeId. The ComponentRegistry stores metadata including size, alignment, drop function, and replication mode.

Archetype Storage

An archetype is defined by a unique set of component types. All entities with the same combination share an archetype, stored in Struct-of-Arrays (SoA) layout:

Archetype [Transform, Mesh, RigidBody]
  Column: Transform[]   -> [T0, T1, T2, ...]
  Column: Mesh[]        -> [M0, M1, M2, ...]
  Column: RigidBody[]   -> [R0, R1, R2, ...]

Each column is a contiguous Vec<u8> with typed access. Iterating all Transform values in an archetype is a sequential memory scan -- ideal for cache prefetching.

Adding or removing a component triggers archetype migration: the entity's data moves to a new archetype with the updated component set. This is O(N) in component count but only happens when the set changes, not every frame.

Queries and Systems

Queries declare read/write access to component types. The query engine returns all archetypes containing the requested types, and access tracking enables parallel scheduling.

use aether_ecs::query::AccessDescriptor;
use aether_ecs::component::ComponentId;

let access = AccessDescriptor::new()
    .read(ComponentId::of::<Transform>())
    .write(ComponentId::of::<Velocity>());

let result = aether_ecs::query::query(world.storage(), &access);
for archetype in result.iter_archetypes() {
    println!("Matched {} entities", archetype.len());
}

Systems are functions that operate on queries. The scheduler builds a dependency graph from access declarations and runs non-conflicting systems in parallel via rayon:

let system = SystemBuilder::new("movement")
    .with_access(
        AccessDescriptor::new()
            .read(ComponentId::of::<Transform>())
            .write(ComponentId::of::<Velocity>())
    )
    .build(movement_system);

Stage Pipeline

Systems are organized into stages executing in a fixed order:

Input -> PrePhysics -> Physics -> PostPhysics -> Animation -> PreRender -> Render -> NetworkSync

Each stage completes before the next begins. Within a stage, non-conflicting systems run in parallel.

world.add_system(Stage::PrePhysics, gravity_system);
world.add_system(Stage::Physics, collision_system);
world.add_system(Stage::Render, draw_system);
world.add_system(Stage::NetworkSync, replication_system);

Event Bus

The event system provides double-buffered channels for inter-system communication. Events written in the current tick become readable in the next tick.

use aether_ecs::Events;

struct CollisionEvent { entity_a: Entity, entity_b: Entity, impulse: f32 }

let mut events = Events::<CollisionEvent>::new();
events.send(CollisionEvent { entity_a: player, entity_b: wall, impulse: 12.5 });

// After tick swap (automatic), other systems read:
for event in events.read() {
    println!("Collision: {:?} and {:?}", event.entity_a, event.entity_b);
}

The double-buffer design ensures writers and readers never access the same buffer simultaneously.

Network-Aware Components

Every component carries a ReplicationMode controlling network synchronization:

  • Replicated -- Serialized and sent to clients during NetworkSync. Use for transforms, health, and visual state.
  • ServerOnly -- Stays on the server. Use for AI state, physics handles, and anti-cheat data.

The NetworkIdentity component links an ECS entity to a network object ID and tracks authority (server or specific client). The NetworkSync stage serializes only changed replicated data for transmission.

Schedule Diagnostics

The scheduler emits per-stage and per-system timing metrics:

let diagnostics = world.diagnostics();
for stage in diagnostics.stages() {
    println!("Stage {:?}: {:.2}ms", stage.stage, stage.duration_ms);
}
for system in diagnostics.systems() {
    println!("System '{}': {:.2}ms", system.name, system.duration_ms);
}

Key Types Reference

TypeModulePurpose
EntityentityLightweight generational entity ID
ComponentcomponentTrait for all ECS data types
ComponentIdcomponentRuntime type identifier for components
ReplicationModecomponentControls network sync behavior
ArchetypeStoragearchetypeManages all archetype column data
AccessDescriptorqueryDeclares read/write access for queries
SystemsystemTrait for executable systems
StagestageEnum of pipeline stages
SchedulescheduleDependency graph and parallel executor
Events<T>eventDouble-buffered event channel
WorldworldTop-level container tying everything together
NetworkIdentitynetworkLinks entities to network object IDs