Entity Component System (ECS) Deep Dive - Master Scalable Game Architecture
Complete guide to Entity Component System architecture for blockchain games. Learn ECS patterns, performance optimization, and best practices with Move smart contracts.
Entity Component System (ECS) is a software architectural pattern used extensively in game development and increasingly in blockchain applications. It provides a flexible, performant way to compose complex behaviors from simple, reusable parts.
Try this: In traditional OOP, youโd need separate Player, Monster, and Treasure classes.
With ECS, any entity can have any combination of components, processed by relevant systems.
Entities
Unique Identifiers - Think of them as empty containers or labels
// Just a number!let player_entity: u64 = 42;let monster_entity: u64 = 1337;
Components
Pure Data - Properties and attributes without behavior
struct HealthComponent has store, drop { current: u64, maximum: u64,}
Systems
Pure Logic - Functions that operate on entities with specific components
public entry fun healing_system(world: &mut World) { // Process all entities with HealthComponent}
// Rigid inheritance hierarchyclass GameObject { constructor() { } update() { } render() { }}class Character extends GameObject { constructor() { super(); this.health = 100; this.inventory = []; } move() { } attack() { }}class Player extends Character { levelUp() { }}class NPC extends Character { followPath() { }}// What about a flying enemy? Swimming player?// Inheritance becomes messy and inflexibleclass FlyingEnemy extends Character { fly() { } // But it inherits move() which doesn't make sense for flying}
Problems:
๐ซ Rigid inheritance chains
๐ซ Diamond problem (multiple inheritance)
๐ซ Hard to add new behaviors
๐ซ Tight coupling between data and behavior
๐ซ Difficult to test individual behaviors
// Flexible component composition// Pure data componentsstruct HealthComponent has store, drop { current: u64, maximum: u64,}struct PositionComponent has store, drop { x: u64, y: u64,}struct FlyingComponent has store, drop { altitude: u64, max_altitude: u64,}struct InventoryComponent has store, drop { items: vector<u64>, capacity: u32,}// Mix and match any combination:// Player = Health + Position + Inventory// Flying Enemy = Health + Position + Flying// Swimming Player = Health + Position + Inventory + Swimming
Components should contain only data, no behavior or logic.
// โ Good: Pure data componentstruct WeaponComponent has store, drop { damage: u64, durability: u64, weapon_type: u8, // 0=sword, 1=bow, 2=staff}// โ Bad: Component with behaviorstruct WeaponComponent has store, drop { damage: u64, durability: u64, weapon_type: u8,}// Don't do this in components!impl WeaponComponent { public fun attack(&self, target: &mut Entity) { // Logic belongs in systems, not components! }}
2. Components Should Be Small and Focused
Each component should represent a single concept or aspect.
// โ Good: Focused componentsstruct HealthComponent has store, drop { current: u64, maximum: u64,}struct PositionComponent has store, drop { x: u64, y: u64,}struct VelocityComponent has store, drop { dx: u64, dy: u64,}// โ Bad: Kitchen sink componentstruct PlayerComponent has store, drop { health: u64, max_health: u64, x: u64, y: u64, dx: u64, dy: u64, level: u8, experience: u64, inventory: vector<u64>, // Too many different concepts in one component!}
3. Components Should Be Composable
Design components so they work well together in various combinations.
// These components can be mixed and matched:// Basic movement// Entity: Position + Velocity// Player character // Entity: Position + Velocity + Health + Inventory + Experience// Flying creature// Entity: Position + Velocity + Health + Flying + AI// Static item// Entity: Position + Item + Renderable// Projectile// Entity: Position + Velocity + Damage + Lifetime
Components that represent the current state of an entity.
struct HealthComponent has store, drop { current: u64, maximum: u64,}struct ManaComponent has store, drop { current: u64, maximum: u64, regeneration_rate: u64,}struct PositionComponent has store, drop { x: u64, y: u64, facing_direction: u8,}
Components that define how entities behave.
struct MovementConfig has store, drop { speed: u64, acceleration: u64, max_speed: u64,}struct AIConfig has store, drop { behavior_type: u8, // 0=aggressive, 1=passive, 2=neutral detection_range: u64, patrol_path: vector<u64>, // Path waypoints}struct RenderConfig has store, drop { sprite_id: u64, scale: u64, // Fixed-point scale factor layer: u8, // Rendering layer}
Components that define relationships between entities.
struct OwnerComponent has store, drop { owner_entity: u64,}struct ParentComponent has store, drop { parent_entity: u64,}struct TargetComponent has store, drop { target_entity: u64, target_priority: u8,}struct FollowingComponent has store, drop { leader_entity: u64, follow_distance: u64,}
Components that mark entities as having certain properties.
// Empty structs that act as flags/tagsstruct PlayerTag has store, drop { }struct EnemyTag has store, drop { }struct CollectibleTag has store, drop { }struct DeadTag has store, drop { }struct FrozenTag has store, drop { }struct InvisibleTag has store, drop { }
public entry fun movement_system() acquires World { // Query all entities with Position and Velocity components let entities = world::query_entities_with<PositionComponent, VelocityComponent>(); let i = 0; while (i < vector::length(&entities)) { let entity = *vector::borrow(&entities, i); // Get mutable references to components let position = world::get_mut_component<PositionComponent>(entity); let velocity = world::get_component<VelocityComponent>(entity); // Apply velocity to position position.x = position.x + velocity.dx; position.y = position.y + velocity.dy; i = i + 1; };}
public entry fun combat_system() acquires World { // Find all entities with attack intentions let attackers = world::query_entities_with<AttackIntentComponent>(); let i = 0; while (i < vector::length(&attackers)) { let attacker = *vector::borrow(&attackers, i); let intent = world::get_component<AttackIntentComponent>(attacker); let weapon = world::get_component<WeaponComponent>(attacker); // Check if target exists and has health if (world::has_component<HealthComponent>(intent.target)) { let target_health = world::get_mut_component<HealthComponent>(intent.target); // Calculate damage let damage = calculate_damage(&weapon, &intent); // Apply damage if (target_health.current > damage) { target_health.current = target_health.current - damage; } else { target_health.current = 0; // Add death tag world::add_component(intent.target, DeadTag {}); }; }; // Remove the attack intent (it's been processed) world::remove_component<AttackIntentComponent>(attacker); i = i + 1; };}fun calculate_damage(weapon: &WeaponComponent, intent: &AttackIntentComponent): u64 { // Damage calculation logic weapon.damage + intent.bonus_damage}
public entry fun pickup_system() acquires World { // Find entities trying to pick up items let pickers = world::query_entities_with<PickupIntentComponent>(); let i = 0; while (i < vector::length(&pickers)) { let picker = *vector::borrow(&pickers, i); let intent = world::get_component<PickupIntentComponent>(picker); let inventory = world::get_mut_component<InventoryComponent>(picker); // Check if item exists and is collectible if (world::has_component<CollectibleTag>(intent.item) && world::has_component<ItemComponent>(intent.item)) { // Check inventory space if (vector::length(&inventory.items) < (inventory.capacity as u64)) { // Add item to inventory vector::push_back(&mut inventory.items, intent.item); // Remove item from world world::despawn_entity(intent.item); // Emit pickup event event::emit(ItemPickedUpEvent { picker: picker, item: intent.item, }); }; }; // Remove pickup intent world::remove_component<PickupIntentComponent>(picker); i = i + 1; };}
public entry fun ai_system() acquires World { // Process all entities with AI let ai_entities = world::query_entities_with<AIComponent>(); let i = 0; while (i < vector::length(&ai_entities)) { let entity = *vector::borrow(&ai_entities, i); let ai = world::get_mut_component<AIComponent>(entity); let position = world::get_component<PositionComponent>(entity); // Different behavior based on AI type if (ai.behavior_type == AI_AGGRESSIVE) { handle_aggressive_ai(entity, &mut ai, &position); } else if (ai.behavior_type == AI_PATROL) { handle_patrol_ai(entity, &mut ai, &position); } else if (ai.behavior_type == AI_FLEE) { handle_flee_ai(entity, &mut ai, &position); }; i = i + 1; };}fun handle_aggressive_ai(entity: u64, ai: &mut AIComponent, pos: &PositionComponent) { // Find nearest player let players = world::query_entities_with<PlayerTag>(); let nearest_player = find_nearest_entity(&players, pos); if (option::is_some(&nearest_player)) { let target = option::extract(&mut nearest_player); // Add attack intent if close enough if (distance_to(pos, &world::get_component<PositionComponent>(target)) < ai.attack_range) { world::add_component(entity, AttackIntentComponent { target: target, bonus_damage: 0, }); } else { // Move towards target let target_pos = world::get_component<PositionComponent>(target); world::add_component(entity, MoveIntentComponent { target_x: target_pos.x, target_y: target_pos.y, }); }; };}
// Get all entities with a specific componentlet healthy_entities = world::query_entities_with<HealthComponent>();// Get all entities with multiple components (AND query)let moveable_entities = world::query_entities_with<PositionComponent, VelocityComponent>();// Check if specific entity has componentif (world::has_component<PlayerTag>(entity_id)) { // Handle player-specific logic};
Complex Queries
// Find all living playerspublic fun find_living_players(): vector<u64> acquires World { let all_players = world::query_entities_with<PlayerTag>(); let living_players = vector::empty<u64>(); let i = 0; while (i < vector::length(&all_players)) { let player = *vector::borrow(&all_players, i); // Only include if they don't have DeadTag if (!world::has_component<DeadTag>(player)) { vector::push_back(&mut living_players, player); }; i = i + 1; }; living_players}// Find entities within rangepublic fun find_entities_in_range( center_pos: &PositionComponent, range: u64): vector<u64> acquires World { let all_positioned = world::query_entities_with<PositionComponent>(); let in_range = vector::empty<u64>(); let i = 0; while (i < vector::length(&all_positioned)) { let entity = *vector::borrow(&all_positioned, i); let pos = world::get_component<PositionComponent>(entity); if (distance(center_pos, pos) <= range) { vector::push_back(&mut in_range, entity); }; i = i + 1; }; in_range}
// Events for component changesstruct ComponentAddedEvent<T: store + drop> has drop { entity: u64, component: T,}struct ComponentRemovedEvent has drop { entity: u64, component_type: String,}struct EntitySpawnedEvent has drop { entity: u64, components: vector<String>,}struct EntityDespawnedEvent has drop { entity: u64,}
Game Events
// Game-specific eventsstruct PlayerLevelUpEvent has drop { player: u64, old_level: u8, new_level: u8,}struct ItemCraftedEvent has drop { crafter: u64, item: u64, recipe: u64,}struct CombatEvent has drop { attacker: u64, defender: u64, damage_dealt: u64, weapon_used: u64,}struct QuestCompletedEvent has drop { player: u64, quest_id: u64, reward_items: vector<u64>, experience_gained: u64,}
Keep components small to minimize gas costs and improve cache performance
Batch Operations
Process multiple entities in single system calls when possible
Memory Layout
Batch Processing
// โ Good: Small, focused componentsstruct PositionComponent has store, drop { x: u64, // 8 bytes y: u64, // 8 bytes} // Total: 16 bytesstruct VelocityComponent has store, drop { dx: u64, // 8 bytes dy: u64, // 8 bytes } // Total: 16 bytes// โ Bad: Large, unfocused componentstruct MassiveComponent has store, drop { position_x: u64, position_y: u64, velocity_x: u64, velocity_y: u64, health: u64, max_health: u64, mana: u64, max_mana: u64, level: u8, experience: u64, inventory: vector<u64>, // Variable size! // ... many more fields} // Total: 100+ bytes plus vector data
// Process multiple entities efficientlypublic entry fun batch_damage_system( entities: vector<u64>, damages: vector<u64>) acquires World { assert!(vector::length(&entities) == vector::length(&damages), ESIZE_MISMATCH); let i = 0; while (i < vector::length(&entities)) { let entity = *vector::borrow(&entities, i); let damage = *vector::borrow(&damages, i); if (world::has_component<HealthComponent>(entity)) { let health = world::get_mut_component<HealthComponent>(entity); health.current = if (health.current > damage) { health.current - damage } else { 0 }; }; i = i + 1; };}
// Execute systems in logical orderpublic entry fun game_tick() { // 1. Process input and AI decisions input_system(); ai_system(); // 2. Apply physics and movement movement_system(); collision_system(); // 3. Handle interactions combat_system(); pickup_system(); // 4. Update derived state animation_system(); sound_system(); // 5. Clean up death_system(); cleanup_system();}
2
Conditional System Execution
// Only run expensive systems when neededpublic entry fun conditional_systems() acquires World { // Only run AI system if there are AI entities if (!vector::is_empty(&world::query_entities_with<AIComponent>())) { ai_system(); }; // Only run combat system if there are attack intents if (!vector::is_empty(&world::query_entities_with<AttackIntentComponent>())) { combat_system(); }; // Always run movement (most entities move) movement_system();}