Files
hytale-server/docs/05-entity-system.md

10 KiB

Entity Component System (ECS)

The Hytale server uses an Entity Component System architecture for managing game entities. This provides high performance and flexibility for handling large numbers of entities with varied behaviors.

ECS Concepts

Overview

The ECS pattern separates data (Components) from behavior (Systems):

  • Entities: Unique identifiers (IDs) that represent game objects
  • Components: Pure data containers attached to entities
  • Systems: Logic that operates on entities with specific component combinations
  • Stores: Data storage containers that hold components
  • Archetypes: Predefined combinations of components

Key Classes

Class Description
Component<S> Base component interface
ComponentRegistry Central registry for component types
Store ECS data storage
Ref Entity reference (ID wrapper)
Holder<C> Component holder/accessor
Archetype Entity archetype definition
SystemType System type definition
Query ECS query for finding entities

Components

Defining a Component

Components are pure data classes implementing Component<Store>:

public class HealthComponent implements Component<EntityStore> {
    public static final ComponentRegistry.Entry<HealthComponent> ENTRY = 
        EntityStore.REGISTRY.register("health", HealthComponent.class, HealthComponent::new);
    
    private float health;
    private float maxHealth;
    
    public HealthComponent() {
        this.health = 20.0f;
        this.maxHealth = 20.0f;
    }
    
    public float getHealth() {
        return health;
    }
    
    public void setHealth(float health) {
        this.health = Math.max(0, Math.min(health, maxHealth));
    }
    
    public float getMaxHealth() {
        return maxHealth;
    }
    
    public void setMaxHealth(float maxHealth) {
        this.maxHealth = maxHealth;
    }
    
    public boolean isDead() {
        return health <= 0;
    }
    
    public void damage(float amount) {
        setHealth(health - amount);
    }
    
    public void heal(float amount) {
        setHealth(health + amount);
    }
}

Built-in Components

The server provides several built-in components in EntityStore.REGISTRY:

Component Description
TransformComponent Position, rotation, and scale
UUIDComponent Unique entity identifier
ModelComponent Visual model reference
MovementAudioComponent Movement sound effects
PositionDataComponent Position tracking data
HeadRotation Head rotation for living entities

Registering Components

Register custom components in your plugin's setup():

@Override
protected void setup() {
    // Components are registered through the EntityStoreRegistry
    getEntityStoreRegistry().registerComponent(MyComponent.ENTRY);
}

Entities

Entity Hierarchy

Component<EntityStore>
└── Entity                    // Base entity
    └── LivingEntity         // Has health and can take damage
        ├── Player           // Player entity
        └── NPCEntity        // NPC entity

Creating a Custom Entity

public class CustomEntity extends Entity {
    
    public static final Codec<CustomEntity> CODEC = BuilderCodec.of(CustomEntity::new)
        // Add codec fields
        .build();
    
    private int customData;
    
    public CustomEntity(World world) {
        super(world);
        this.customData = 0;
    }
    
    public int getCustomData() {
        return customData;
    }
    
    public void setCustomData(int data) {
        this.customData = data;
    }
    
    @Override
    public void tick() {
        super.tick();
        // Custom tick logic
    }
}

Registering Entities

@Override
protected void setup() {
    getEntityRegistry().register(
        "customEntity",
        CustomEntity.class,
        CustomEntity::new
    );
    
    // With serialization codec
    getEntityRegistry().register(
        "customEntity",
        CustomEntity.class,
        CustomEntity::new,
        CustomEntity.CODEC
    );
}

Accessing Entity Components

Entity entity = world.getEntity(entityId);

// Get a component
TransformComponent transform = entity.get(TransformComponent.ENTRY);
if (transform != null) {
    Vector3d position = transform.getPosition();
}

// Check if entity has component
if (entity.has(HealthComponent.ENTRY)) {
    // Entity has health
}

// Add a component
entity.add(MyComponent.ENTRY, new MyComponent());

// Remove a component
entity.remove(MyComponent.ENTRY);

Entity Store

The EntityStore manages entity data within a world:

World world = Universe.get().getWorld("default");
EntityStore store = world.getEntityStore();

// Create entity
Ref entityRef = store.create();

// Add components to entity
store.add(entityRef, TransformComponent.ENTRY, new TransformComponent(position));
store.add(entityRef, HealthComponent.ENTRY, new HealthComponent());

// Get component from entity
HealthComponent health = store.get(entityRef, HealthComponent.ENTRY);

// Remove entity
store.remove(entityRef);

Queries

Queries find entities with specific component combinations:

// Query for entities with both Transform and Health components
Query query = Query.builder()
    .with(TransformComponent.ENTRY)
    .with(HealthComponent.ENTRY)
    .build();

// Execute query
EntityStore store = world.getEntityStore();
store.query(query, (ref, transform, health) -> {
    // Process each matching entity
    if (health.isDead()) {
        // Handle dead entity
    }
});

Query Builder

Query query = Query.builder()
    .with(ComponentA.ENTRY)        // Must have ComponentA
    .with(ComponentB.ENTRY)        // Must have ComponentB
    .without(ComponentC.ENTRY)     // Must NOT have ComponentC
    .build();

Archetypes

Archetypes define common component combinations for entity types:

// Define an archetype for enemies
Archetype enemyArchetype = Archetype.builder()
    .add(TransformComponent.ENTRY)
    .add(HealthComponent.ENTRY)
    .add(AIComponent.ENTRY)
    .add(CombatComponent.ENTRY)
    .build();

// Create entity with archetype
Ref enemy = store.create(enemyArchetype);

Systems

Systems contain the logic that operates on entities:

public class HealthRegenSystem implements System {
    
    private static final Query QUERY = Query.builder()
        .with(HealthComponent.ENTRY)
        .build();
    
    @Override
    public void tick(World world) {
        EntityStore store = world.getEntityStore();
        
        store.query(QUERY, (ref, health) -> {
            if (!health.isDead() && health.getHealth() < health.getMaxHealth()) {
                // Regenerate 0.1 health per tick
                health.heal(0.1f);
            }
        });
    }
}

Registering Systems

@Override
protected void setup() {
    // Register system with the world module
    getEntityStoreRegistry().registerSystem(new HealthRegenSystem());
}

Working with Players

Getting Players

// Get all players
Collection<Player> players = Universe.get().getPlayers();

// Get player by name
Player player = Universe.get().getPlayer("PlayerName");

// Get player by UUID
Player player = Universe.get().getPlayer(uuid);

// Get players in world
World world = Universe.get().getWorld("default");
Collection<Player> worldPlayers = world.getPlayers();

Player Properties

Player player = getPlayer();

// Position
Vector3d position = player.getPosition();
player.setPosition(new Vector3d(x, y, z));

// Rotation
float yaw = player.getYaw();
float pitch = player.getPitch();

// Health (if LivingEntity)
player.setHealth(20.0f);
float health = player.getHealth();

// Game mode
GameMode mode = player.getGameMode();
player.setGameMode(GameMode.CREATIVE);

// Inventory
Inventory inventory = player.getInventory();

Living Entities

LivingEntity extends Entity with health and damage capabilities:

public class CustomMob extends LivingEntity {
    
    public CustomMob(World world) {
        super(world);
        setMaxHealth(50.0f);
        setHealth(50.0f);
    }
    
    @Override
    public void onDamage(DamageSource source, float amount) {
        super.onDamage(source, amount);
        // Custom damage handling
    }
    
    @Override
    public void onDeath() {
        super.onDeath();
        // Drop loot, play effects, etc.
    }
}

Entity Events

Subscribe to entity-related events:

// Entity removed from world
getEventRegistry().register(EntityRemoveEvent.class, event -> {
    Entity entity = event.getEntity();
    // Handle entity removal
});

// Living entity uses block
getEventRegistry().register(LivingEntityUseBlockEvent.class, event -> {
    LivingEntity entity = event.getEntity();
    // Handle block usage
});

Spawning Entities

// Get the world
World world = Universe.get().getWorld("default");

// Spawn entity at position
Vector3d position = new Vector3d(100, 64, 100);
CustomEntity entity = new CustomEntity(world);
entity.setPosition(position);
world.addEntity(entity);

// Spawn with specific properties
CustomEntity entity = new CustomEntity(world);
entity.setPosition(position);
entity.setCustomData(42);
world.addEntity(entity);

Best Practices

  1. Keep components data-only - Components should not contain logic, only data
  2. Use systems for behavior - Put game logic in systems, not components
  3. Prefer composition - Use multiple small components instead of large monolithic ones
  4. Query efficiently - Cache queries and reuse them
  5. Use archetypes - Define archetypes for common entity types
  6. Handle null components - Always check if a component exists before using it
  7. Clean up entities - Remove entities when they're no longer needed
  8. Use appropriate entity types - Extend LivingEntity for damageable entities