Entity Component System
How MAEngine uses Bevy ECS for game state management and networking
Entity Component System (ECS)
MAEngine uses Bevy ECS as its core data architecture. This provides a performant, cache-friendly way to manage game entities while supporting the unique requirements of a multiplayer server.
Overview
The ECS pattern separates data (Components) from behavior (Systems), organizing game state as:
- Entities: Unique identifiers (just IDs, no data)
- Components: Pure data attached to entities
- Resources: Singleton data shared across the world
- Systems: Functions that query and modify components
┌─────────────────────────────────────────────────────────────────┐
│ Bevy World │
├─────────────────────────────────────────────────────────────────┤
│ Resources (Singletons) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ ServerClock │ │ EntityMap │ │ PlayerMap │ ... │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ Entities with Components │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Entity 0: [Player, PlayerMarker, PlayerConnection] │ │
│ ├──────────────────────────────────────────────────────────┤ │
│ │ Entity 1: [NetworkId, Transform, CharacterMarker, ...] │ │
│ ├──────────────────────────────────────────────────────────┤ │
│ │ Entity 2: [NetworkId, Transform, EntityType, ...] │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘Core Entity Types
MAEngine defines four core entity archetypes that map to the Lua API:
Player
Represents a connected client session (not an in-world object).
#[derive(Component)]
pub struct Player {
pub id: PlayerId, // Unique session ID
pub auth_id: String, // External auth (Steam ID, etc.)
pub state: PlayerState, // Connected, Loading, Ready, etc.
pub character: Option<Entity>, // Currently possessed character
pub latency: u32, // RTT in milliseconds
}Associated Components:
PlayerMarker- Tag for filtering queriesPlayerConnection- Network connection statePlayerValues- Lua-defined synced values
Character
Player-controlled entity with movement capabilities.
#[derive(Component)]
pub struct CharacterMarker;
#[derive(Component)]
pub struct ControlledBy {
pub player_id: PlayerId,
}
#[derive(Component)]
pub struct Movement {
pub velocity: Vec3,
pub control_rotation: Quat, // Camera/aim direction
pub character_rotation: Quat, // Body facing
pub grounded: bool,
pub movement_mode: MovementMode,
}Characters inherit all Entity components plus these character-specific ones.
Entity
Base for all world objects that are replicated to clients.
#[derive(Component)]
pub struct NetworkId { pub id: EntityId } // Unique network identifier
#[derive(Component)]
pub struct Transform {
pub position: Vec3,
pub rotation: Quat,
}
#[derive(Component)]
pub struct Owner { pub player_id: Option<PlayerId> }
#[derive(Component)]
pub struct Authority { pub holder: Option<PlayerId> } // None = serverValue Components (for Lua-defined properties):
PublicValues- Synced to all clientsPrivateValues- Synced only to ownerLocalValues- Server-only (no sync)
Zone
World container for spatial segmentation (maps, instances, levels).
#[derive(Component)]
pub struct Zone {
pub id: ZoneId,
pub name: String,
pub bounds: AABB,
pub active: bool,
}
#[derive(Component)]
pub struct ZoneEntities {
pub entities: Vec<Entity>, // Fast lookup of zone contents
}Resources (Global State)
Resources are singletons accessible by any system:
ServerClock
Fixed-timestep game time:
pub struct ServerClock {
pub tick: TickNumber, // Current server tick
pub time_ms: ServerTime, // Time since start
pub tick_delta: f64, // Fixed timestep (1/tick_rate)
pub tick_rate: u32, // Ticks per second (default: 30)
}EntityMap & PlayerMap
Bidirectional lookups between network IDs and ECS entities:
// Network ID <-> ECS Entity
pub struct EntityMap {
pub network_to_ecs: HashMap<EntityId, Entity>,
pub ecs_to_network: HashMap<Entity, EntityId>,
}
// Player ID <-> Player Entity
pub struct PlayerMap {
pub id_to_entity: HashMap<PlayerId, Entity>,
pub entity_to_id: HashMap<Entity, PlayerId>,
}These maps are critical for translating between:
- Lua scripts (use network IDs)
- ECS systems (use Entity handles)
- Network packets (use network IDs)
ID Generators
Sequential ID generation for entities and players:
pub struct EntityIdGenerator { next_id: EntityId }
pub struct PlayerIdGenerator { next_id: PlayerId }Component Bundles
Bundles group related components for convenient spawning:
#[derive(Bundle)]
pub struct NetworkedEntityBundle {
pub network_id: NetworkId,
pub entity_type: EntityType,
pub transform: Transform,
pub previous_transform: PreviousTransform,
pub in_zone: InZone,
pub active: Active,
pub owner: Owner,
pub authority: Authority,
pub public_values: PublicValues,
pub private_values: PrivateValues,
pub local_values: LocalValues,
pub replicated: Replicated,
}
#[derive(Bundle)]
pub struct CharacterBundle {
pub marker: CharacterMarker,
pub controlled_by: ControlledBy,
pub movement: Movement,
pub previous_movement: PreviousMovement,
}The EntityManager Bridge
Lua scripting cannot directly access Bevy ECS (different memory model). The EntityManager acts as a bridge:
┌──────────────┐ ┌───────────────────┐ ┌──────────────┐
│ Lua Scripts │ ←────→ │ EntityManager │ ←────→ │ Bevy ECS │
│ │ │ (Shared State) │ │ │
│ Entity.Spawn │ │ Arc<RwLock<...>> │ │ Components │
│ entity:Set │ │ pending_spawns │ │ Resources │
│ entity:Get │ │ pending_updates │ │ Systems │
└──────────────┘ └───────────────────┘ └──────────────┘The EntityManager stores:
EntityData- All entity state (position, rotation, values, etc.)pending_spawns- Entities created this tickpending_updates- State changes this tickpending_despawns- Entities to remove
Each tick:
- Lua scripts modify EntityManager state
- Main loop reads pending changes
- Changes are broadcast to relevant clients
- ECS components are updated to match
Replication Components
Special components control network behavior:
Replicated
Marks an entity for network replication:
#[derive(Component)]
pub struct Replicated; // Tag - entity will be sent to clientsDormant
Optimization for rarely-changing entities:
#[derive(Component)]
pub struct Dormant {
pub is_dormant: bool,
pub last_update_tick: u64,
}Dormant entities only replicate when state actually changes.
RelevanceOverride
Custom relevance rules per-entity:
#[derive(Component)]
pub struct RelevanceOverride {
pub always_relevant_to: Vec<PlayerId>, // Always visible
pub only_relevant_to: Option<Vec<PlayerId>>, // Exclusive visibility
pub cull_distance: Option<f32>, // Custom range
pub priority: Option<u8>, // Update priority
}Delta Detection
"Previous" components enable efficient change detection:
// Current state
pub struct Transform { pub position: Vec3, pub rotation: Quat }
// Previous tick's state
pub struct PreviousTransform { pub position: Vec3, pub rotation: Quat }Systems compare current vs previous to:
- Only replicate changed components
- Build delta packets (changed fields only)
- Track entity dirtiness
Source Code
The ECS module is located at:
crates/core-server/src/ecs/mod.rs- Module exportscrates/core-server/src/ecs/components.rs- Component definitionscrates/core-server/src/ecs/resources.rs- Resource definitionscrates/core-server/src/ecs/entity_manager.rs- Lua bridge