Understanding the Entity Component System (ECS) in Hytale

tutorial java plugin ecs architecture

If you’ve worked with Hytale’s plugin API, you’ve encountered types like Ref, Store, and ComponentType. These are part of Hytale’s Entity Component System (ECS) - and understanding ECS is key to writing effective plugins. Let’s start with the problem ECS solves.

Prefer video? Watch this tutorial on YouTube.

The Problem with Traditional Object-Oriented Programming

Before diving into ECS, let’s quickly recap Object-Oriented Programming (OOP). In OOP, a Class is a blueprint that defines what data and behavior something has. An Object is a specific instance of that class - one particular player, one specific zombie. Inheritance allows a class to extend another, so class Player extends Character means the Player automatically gets everything the Character has, plus it can add its own stuff.

This inheritance model seems elegant at first. Consider a simple example:

class Character {
    int health;
    void takeDamage(int amount) { health -= amount; }
}

class Player extends Character {
    // Player automatically has health and takeDamage()
    // Plus its own stuff:
    String username;
    void respawn() { /* ... */ }
}

Clean and organized. Now imagine you’re building a game with players that move and have health, Non-Player Characters (NPCs) that move and have Artificial Intelligence (AI), projectiles that move and deal damage, and buildings that players can enter. The classic OOP approach organizes this with inheritance:

GameObject
├── Movable
│   ├── Character
│   │   ├── Player      // Has health, input control
│   │   └── NPC         // Has health, AI
│   └── Projectile      // Moves, deals damage
└── Static
    └── Building        // Can be entered

This looks clean at first. But then you need to add a vehicle - like a ship. A vehicle moves like things under Movable, but players can enter it like a Building. In Java, you can only extend one class. So where does the vehicle go? If Building extends Static, it can never be movable. You’d have to create a completely separate MovableBuilding class and duplicate all the building code.

The Diamond Problem

Some languages support multiple inheritance, but that introduces its own headache: the Diamond Problem. Imagine a Player that needs to inherit from both Character and Vehicle:

graph TD
    A[Movable] --> B[Character]
    A --> C[Vehicle]
    B --> D[Player]
    C --> D

What happens if both Character and Vehicle define a takeDamage() method? Which implementation does Player use? This ambiguity is exactly why Java only allows single inheritance - but that forces us into rigid hierarchies where everything must fit neatly into one branch of the family tree.

Class Explosion

It gets worse. Say you want a flying pig that breathes fire and can be ridden as a mount. It needs animal behavior (pig), flying capability, fire breath ability, and mountable functionality. With inheritance, you’d need a class like FlyingMountableFireBreathingPig. Every new feature combination needs its own class. With just 5 features, you could theoretically need 32 different classes. This is called class explosion.

These problems - rigid hierarchies, the diamond problem, and class explosion - make traditional OOP a poor fit for games where features need to be freely combined. There must be a better way.

Enter ECS: Composition Over Inheritance

ECS flips the model completely. Instead of asking “what TYPE of object is this?”, we ask “what CAPABILITIES does it have?” This mental shift is fundamental: you stop thinking in categories and start thinking in capabilities. An entity CAN move if it has Position and Velocity. It CAN take damage if it has Health. It CAN think if it has AI.

The architecture rests on three pillars: Entities, Components, and Systems. Let’s explore each one in depth.

Entities: Just an ID

An Entity is nothing more than a unique identifier - think of it as an empty folder or a row number in a database. It has no data and no behavior of its own. Entity_42 might be a zombie, Entity_1337 might be a flying pig, but the Entity itself doesn’t know or care. It’s simply a container that links components together.

Components: Pure Data

Components are where the actual data lives. A component is a simple data structure - just fields, no methods, no behavior. Think of components as files in that empty folder.

public class Position {
    public double x, y, z;
}

public class Health {
    public int current, max;
}

The Velocity component stores movement direction and speed as three values:

public class Velocity {
    public double vx;  // velocity on x-axis
    public double vy;  // velocity on y-axis
    public double vz;  // velocity on z-axis
}

An AI component might store the current behavioral state and a reference to what the entity is focused on:

public class AI {
    public String state;    // "IDLE", "CHASE", "ATTACK"
    public int targetId;    // which entity to pursue
}

Notice how these components are completely generic. A Velocity component works the same whether it’s attached to a player, a projectile, or a falling anvil. The component doesn’t know what kind of entity it belongs to - it just holds data.

Systems: Pure Logic

Systems are where behavior lives. A System is pure logic that processes all entities matching a specific set of components. The MovementSystem finds all entities that have both Position and Velocity, then updates their positions:

public class MovementSystem {
    void process(Entity entity) {
        Position pos = entity.get(Position.class);
        Velocity vel = entity.get(Velocity.class);

        pos.x += vel.vx;
        pos.y += vel.vy;
        pos.z += vel.vz;
    }
}

The System doesn’t care if it’s processing a player, an NPC, or a flying pig. It only asks one question: “Does this entity have the components I need?” If yes, process it. If no, skip it.

Putting It Together: Entity Examples

Let’s see how this works with concrete examples. Here’s a zombie NPC:

Entity_42 (Zombie)
├── Position    { x: 100, y: 64, z: 200 }
├── Health      { current: 80, max: 100 }
├── Velocity    { vx: 0, vy: 0, vz: 1.5 }
└── AI          { state: "CHASE", targetId: 7 }

This zombie is currently chasing Entity_7 (probably a player), moving in the z-direction at speed 1.5, and has taken 20 damage.

And here’s our exotic flying pig:

Entity_1337 (Flying Pig)
├── Position    { x: 50, y: 100, z: 50 }
├── Velocity    { vx: 0, vy: 0, vz: 0 }
├── Health      { current: 100, max: 100 }
├── Flying      { altitude: 20 }
├── FireBreath  { damage: 10, cooldown: 5.0 }
└── Mountable   { riderId: 42 }

Which Systems Process Which Entities?

The beauty of ECS is that Systems automatically find the entities they care about based on component requirements:

SystemRequired ComponentsWhat it does
MovementSystemPosition + VelocityUpdates position based on velocity
AISystemAI + PositionDetermines behavior, picks targets
DamageSystemHealthApplies damage, handles death
FlyingSystemFlying + PositionHandles altitude, wing physics
MountSystemMountableManages rider attachment

Both the zombie and the flying pig have Position + Velocity, so both get processed by MovementSystem. Only the zombie has AI, so only the zombie gets processed by AISystem. Only the flying pig has Flying, so only it gets processed by FlyingSystem. Each System is decoupled from the others - they don’t know about each other, they just process the entities that match their requirements.

The ECS Core Pattern

The following diagram shows how these three pillars relate to each other:

flowchart TB
    subgraph Entity["Entity (just an ID)"]
        E[Entity_42]
    end

    subgraph Components["Components (pure data)"]
        P[Position<br/>x, y, z]
        H[Health<br/>current, max]
        V[Velocity<br/>vx, vy, vz]
        A[AI<br/>state, targetId]
    end

    subgraph Systems["Systems (pure logic)"]
        MS[MovementSystem]
        DS[DamageSystem]
        AS[AISystem]
    end

    E --> P
    E --> H
    E --> V
    E --> A

    P --> MS
    V --> MS
    H --> DS
    A --> AS
    P --> AS

The Entity is just an ID that links components together. Components hold data. Systems read and modify that data. This separation is what makes ECS so flexible and powerful.

Composition Over Inheritance

No inheritance hierarchy. No class explosion. Want a swimming pig instead of a flying pig? Remove the Flying component and add a Swimming component. Want a fire-breathing player? Just add FireBreath to any entity that should have it.

This is composition over inheritance - we build entities from parts like LEGO blocks, rather than trying to fit them into a family tree. The flying pig doesn’t need to inherit from FlyingCreature, MountableAnimal, and FireBreathingEntity. It just has the components it needs.

Hytale’s ECS Implementation

Now let’s see how Hytale implements these concepts concretely.

The server has a Universe at the top - one per server. The Universe contains multiple Worlds, and each World has an EntityStore. The EntityStore is where all ECS magic happens - it holds all entities, their components, and runs the systems that process them.

Universe (one per server)

├── PlayerRef[]              ← All connected players
├── PlayerStorage            ← Saves player data to disk

└── World[]                  ← Multiple game worlds

    ├── EntityStore          ← THE ECS! All entities live here
    │   ├── Entities         ← Players, NPCs, items, projectiles...
    │   ├── Components       ← Their data
    │   └── Systems          ← Logic that processes them

    └── ChunkStore           ← Block data (also uses ECS internally!)

Accessing the Universe

The Universe is a singleton - there’s exactly one instance per server. You access it through a static method, making it available from anywhere in your plugin code:

Universe universe = Universe.get();  // Static singleton access

From the Universe, you can retrieve specific worlds by name. Each world is independent with its own entities, systems, and game state:

World adventure = universe.getWorld("adventure");
World hub = universe.getWorld("hub");

World Details

A World is one game instance. Beyond the EntityStore, it provides several important features. The ChunkStore holds all block and terrain data. You can access the list of players currently in this world. The World also maintains its own settings like time of day and weather conditions.

// Get the EntityStore from a world
Store<EntityStore> store = world.getEntityStore().getStore();

// Get all players in this world
Collection<PlayerRef> players = world.getPlayers();

Understanding PlayerRef

You might wonder: if players are entities in the ECS, why is there also a PlayerRef type? The answer reveals an important architectural distinction.

PlayerRef is the bridge between the network connection and the ECS entity. When a player connects to the server, they receive a PlayerRef immediately. This object holds their permanent identity:

PlayerRef
├── UUID uuid                    ← Permanent player identity
├── String username              ← Display name ("Steve")
├── PacketHandler packetHandler  ← Network connection

├── Ref<EntityStore> entity      ← ECS entity (when IN a world)
└── Holder<EntityStore> holder   ← Saved data (when NOT in a world)

The crucial insight is that a player becomes an ECS entity only when they join a world. Before that, the PlayerRef exists but has no corresponding entity. When the player enters a world, the entity field gets populated with a Ref<EntityStore> pointing to their actual ECS entity. If they leave the world (perhaps to switch to another), the link breaks - the PlayerRef remains, but the entity reference becomes invalid. Their data is preserved in the holder field until they join a new World.

You can find players through the Universe using either their UUID (Universally Unique Identifier - a permanent player identity that never changes) or their username:

// By UUID (always works)
PlayerRef player = universe.getPlayer(uuid);

// By name
PlayerRef steve = universe.getPlayerByUsername("Steve", NameMatching.EXACT);
PlayerRef partial = universe.getPlayerByUsername("Ste", NameMatching.STARTS_WITH);

Working with Entities

In Hytale, an entity reference is wrapped in a Ref - think of it as a house address that points to one specific entity. The Store is the village where all those houses live.

Ref<EntityStore> zombieRef = ...;  // Points to one zombie
Store<EntityStore> store = world.getEntityStore().getStore();

To access components, you also need a ComponentType - a fast identifier that uses integer indices internally for performance. Hytale processes thousands of entities per frame, so this efficiency matters.

Validating References

Entity references can become invalid. If an entity is removed from the world (killed, despawned, or the player disconnects), any Ref pointing to it becomes stale. Before using a Ref, you should check its validity:

if (ref.isValid()) {
    // Safe to use the ref
    TransformComponent transform = store.getComponent(ref, TransformComponent.getComponentType());
    // ...
} else {
    // Entity was removed - handle gracefully
}

Alternatively, you can attempt the operation and catch the exception if the entity no longer exists. The choice depends on your use case - validation is cleaner for predictable flows, while exception handling works better when invalid Refs are rare edge cases.

Store Methods

The Store provides four fundamental operations for working with components:

MethodWhat it does
getComponent(ref, type)Get a component from an entity
addComponent(ref, type, instance)Add a component to an entity
removeComponent(ref, type)Remove a component
hasComponent(ref, type)Check if entity has a component

The fundamental pattern you’ll use constantly is: get the Ref, get the Store, get the component, read the data.

Ref<EntityStore> ref = ...;
Store<EntityStore> store = ...;

TransformComponent transform = store.getComponent(
    ref,
    TransformComponent.getComponentType()
);

Vector3d position = transform.getPosition();

Writing components follows the same pattern. You can add new components, modify existing ones, or remove them:

// Add a speed boost
store.addComponent(ref, SpeedBoostComponent.getComponentType(),
    new SpeedBoostComponent(1.5f, 10.0f));

// Modify position (teleport)
TransformComponent transform = store.getComponent(ref, TransformComponent.getComponentType());
transform.getPosition().set(100, 64, 200);

// Remove the speed boost
store.removeComponent(ref, SpeedBoostComponent.getComponentType());

A common ECS pattern is using components as triggers. To teleport a player, you don’t modify their position directly - you add a Teleport component. A TeleportSystem runs every tick, finds entities with Teleport components, moves them, and then removes the component. The component acts as a “request” that a System fulfills.

System Types

Hytale has different System types depending on when your code needs to run. Choosing the right System type is crucial for plugin developers - it determines not only when your code executes, but also how efficiently it integrates with Hytale’s game loop.

EntityTickingSystem

The EntityTickingSystem runs every frame. Use it for continuous logic like timers, physics, AI behavior, or health regeneration. You define a Query that filters which entities to process:

public class PoisonDamageSystem extends EntityTickingSystem<EntityStore> {

    @Override
    public Query<EntityStore> getQuery() {
        return Query.and(
            HealthComponent.getComponentType(),
            PoisonedComponent.getComponentType()
        );
    }

    @Override
    public void tick(float dt, int index, ArchetypeChunk<EntityStore> chunk,
                     Store<EntityStore> store, CommandBuffer<EntityStore> cmd) {
        // dt = delta time (seconds since last frame)
        // Use it for frame-rate independent logic
    }
}

The dt parameter is the delta time - the seconds since the last frame. At 60 frames per second (FPS), it’s about 0.016 seconds. Always multiply time-based values by dt so your logic works the same regardless of frame rate:

health -= 10 * dt;  // Correct: same damage per second regardless of FPS
health -= 10;        // Wrong: 600 damage/sec at 60fps, 300 at 30fps!

Queries: Filtering Entities

Queries tell a System which entities to process. Without a Query, you’d manually check every entity in the world. With a Query, Hytale does the filtering efficiently for you - skipping entire Archetypes that don’t match.

// Entities with BOTH Health AND Poison
Query.and(HealthComponent.getComponentType(),
          PoisonedComponent.getComponentType())

// Entities that are either Players OR NPCs
Query.or(PlayerRef.getComponentType(),
         NPCEntity.getComponentType())

// Entities that do NOT have Invulnerable
Query.not(Invulnerable.getComponentType())

You can combine these for more complex filters:

// Damageable entities: Has Health, NOT Invulnerable
Query.and(
    HealthComponent.getComponentType(),
    Query.not(Invulnerable.getComponentType())
)

RefSystem

The RefSystem runs when entities matching your Query are added to or removed from the world. This is perfect for welcome messages, initialization logic, or cleanup when entities disappear.

public class WelcomeSystem extends RefSystem<EntityStore> {

    @Override
    public Query<EntityStore> getQuery() {
        return PlayerRef.getComponentType();  // Only players
    }

    @Override
    public void onEntityAdded(Ref<EntityStore> ref, AddReason reason,
                              Store<EntityStore> store, CommandBuffer<EntityStore> cmd) {
        // Player joined! Send welcome message
        PlayerRef playerRef = store.getComponent(ref, PlayerRef.getComponentType());
        playerRef.sendMessage(Message.raw("Welcome to the server!"));
    }

    @Override
    public void onEntityRemove(Ref<EntityStore> ref, RemoveReason reason,
                               Store<EntityStore> store, CommandBuffer<EntityStore> cmd) {
        // Player left - cleanup if needed
    }
}

The AddReason and RemoveReason parameters tell you why the entity appeared or disappeared - whether it spawned naturally, was created by a plugin, teleported from another world, or was explicitly destroyed.

RefChangeSystem

The RefChangeSystem runs when a specific component’s value changes on an entity. Unlike EntityTickingSystem (which runs every frame) or RefSystem (which runs on entity add/remove), RefChangeSystem only triggers when the watched component is actually modified.

This is ideal for reactive logic - responding to state changes without constantly polling. For example, you could watch for changes to a player’s inventory, their permission level, or their team assignment.

public class TeamChangeSystem extends RefChangeSystem<EntityStore, TeamComponent> {

    @Override
    public void onComponentChanged(Ref<EntityStore> ref, TeamComponent oldValue,
                                   TeamComponent newValue, Store<EntityStore> store,
                                   CommandBuffer<EntityStore> cmd) {
        // React to team assignment changes
        PlayerRef player = store.getComponent(ref, PlayerRef.getComponentType());
        player.sendMessage(Message.raw("You joined team: " + newValue.getTeamName()));
    }
}

RefChangeSystem is more efficient than polling in an EntityTickingSystem because your code only runs when there’s actually something to respond to.

DamageEventSystem

The DamageEventSystem runs whenever damage is dealt to an entity. This is your hook for armor calculations, damage modifiers, invulnerability checks, or damage logging.

public class ArmorSystem extends DamageEventSystem {

    @Override
    public void handle(int index, ArchetypeChunk<EntityStore> chunk,
                       Store<EntityStore> store, CommandBuffer<EntityStore> cmd,
                       DamageEventComponent damage) {

        // Check if entity has armor
        ArmorComponent armor = chunk.getComponent(index, ArmorComponent.getComponentType());
        if (armor != null) {
            // Reduce damage by armor value
            float reduced = damage.getAmount() * (1 - armor.getReduction());
            damage.setAmount(reduced);
        }
    }
}

The DamageEventComponent contains all information about the damage event - the amount, the damage type, the source entity, and more. You can modify damage.setAmount() to reduce (or increase) the final damage dealt.

System Types Overview

Here’s a quick reference for when to use each System type:

System TypeWhen it runsUse case
EntityTickingSystemEvery frameTimers, physics, AI, regeneration
RefSystemEntity added/removedWelcome messages, initialization, cleanup
RefChangeSystemComponent value changesReact to state changes without polling
DamageEventSystemDamage is dealtArmor, damage modifiers, logging

The pattern is: think about when your code needs to run, then pick the right System type. Don’t use an EntityTickingSystem for logic that only needs to run once when a player joins - use a RefSystem instead. Don’t poll for changes every frame when you can use a RefChangeSystem to react only when changes occur.

Archetypes and Performance

Hytale groups entities by their component set into Archetypes. All players might share one archetype (Transform, Health, PlayerRef), all NPCs another (Transform, Health, AI), all projectiles another (Transform, Velocity, Damage).

When your Query needs PlayerRef, Hytale skips the NPC and projectile archetypes entirely - it doesn’t check every single entity. This alone is a significant optimization, but the real performance gain comes from how data is stored.

Structure of Arrays (SoA)

Traditional object-oriented programming uses an “Array of Structures” layout - each entity object contains all its data together. If you iterate over 1000 entities to update their positions, your code jumps around in memory to access each entity’s position field.

Hytale uses the opposite approach called Structure of Arrays (SoA). Instead of storing each entity as one object, it stores all components of the same type together in contiguous arrays. Entities with the same archetype live in ArchetypeChunks that look like this:

ArchetypeChunk for [Transform, Health, Player]:

index:      0         1         2         3
refs:     [Ref_42]  [Ref_99]  [Ref_17]  [Ref_256]
Transform:[T_42]    [T_99]    [T_17]    [T_256]    <- All Transforms together
Health:   [H_42]    [H_99]    [H_17]    [H_256]    <- All Health together
Player:   [P_42]    [P_99]    [P_17]    [P_256]    <- All Player data together

When a System iterates over Transform components, they’re all sitting next to each other in memory. The CPU doesn’t have to jump around - it reads one Transform, then the next one is right there. Modern CPUs are extremely good at this pattern because they can prefetch upcoming data. While processing Transform_42, the CPU is already loading Transform_99 into cache because it knows sequential memory access is coming.

This is called cache locality, and it’s one of the most important performance optimizations in modern game engines. A cache miss (having to fetch data from main RAM instead of the CPU cache) can be 100 times slower than a cache hit. By laying out component data contiguously, Hytale ensures that iterating over thousands of entities stays fast even on modest hardware.

A Note on Interactions

Player actions like attacking use the Interaction System, which is separate from ECS. This distinction is important: ECS handles the game state (what exists, what properties things have), while Interactions handle the timing of when that state changes.

Consider what happens when a player swings a sword. The click doesn’t immediately deal damage - first, an animation plays, and only at a specific point in that animation does the hit actually register. This timing logic doesn’t fit the ECS model of “pure data + systems that process every tick.” Instead, Interactions manage the sequence of events.

flowchart TD
    A[Player clicks attack] --> B[Interaction starts]
    B --> C[Swing animation plays]
    C --> D[At hit point: Modify ECS]
    D --> E[Add DamageEvent component]
    E --> F[DamageEventSystem processes]
    F --> G[Health component updated]

The flow shows how Interactions bridge player input to ECS changes. The Interaction handles steps A through D - the timing, the animation, knowing when the “hit moment” arrives. Only then does it hand off to ECS by adding a DamageEvent component, which systems process to update health.

Systems vs Events vs Interactions

These three concepts work together but serve different purposes:

ConceptWhat it isExample
SystemECS processor that runs every tickAutoHealSystem regenerates health over time
EventNotification broadcast to listenersPlayerChatEvent fires when someone sends a message
InteractionTimed action sequence with animationsSword swing, bow draw, block placement

Systems are continuous - they check conditions and update state every frame. Events are instantaneous notifications. Interactions are temporal sequences that unfold over multiple frames with precise timing.

Interaction Types

Hytale includes over 45 different Interaction types, each handling a specific category of player action. Some examples:

  • PlaceBlockInteraction - Handles block placement with placement sounds and particles
  • DamageEntityInteraction - Manages melee attacks with per-angle damage values and hit effects
  • ProjectileInteraction - Spawns and fires projectiles like arrows or thrown items
  • ChargingInteraction - Powers up attacks like drawing a bow or charging a spell
  • UseEntityInteraction - Handles right-clicking on NPCs or other entities

These Interactions are configured in JSON (JavaScript Object Notation) assets rather than hardcoded. A sword’s attack, a bow’s draw, a shovel’s dig - all defined as Interaction chains that eventually modify ECS components. This data-driven approach lets modders create new weapons and tools without writing code.

Key Takeaways

The mental shift with ECS is thinking in capabilities rather than categories. Instead of “a Player IS a Character IS a Movable IS a GameObject”, you think “this entity HAS position, HAS health, HAS input control.”

  • Entities are just IDs - containers for components
  • Components are pure data - no behavior
  • Systems are pure logic - they process entities with matching components
  • Read pattern: store.getComponent(ref, Type.getComponentType())
  • Adding components can trigger behavior via systems that watch for them
  • Archetypes make queries fast by grouping similar entities

Once you internalize this pattern, Hytale’s API makes a lot more sense. You stop fighting inheritance hierarchies and start composing exactly the entities your game needs.


Happy modding!