Skip to content

Entity Component System (ECS) Overview

Hytale uses an Entity Component System architecture for managing game entities, chunks, and their associated data. ECS separates data (components) from behavior (systems), enabling efficient processing of large numbers of entities.

  • Core ECS: com.hypixel.hytale.component
  • Systems: com.hypixel.hytale.component.system
  • Queries: com.hypixel.hytale.component.query
  • EntityStore: com.hypixel.hytale.server.core.universe.world.storage.EntityStore
  • ChunkStore: com.hypixel.hytale.server.core.universe.world.storage.ChunkStore
┌─────────────────────────────────────────────────────────────────┐
│ Store │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Entities (Refs) │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ Entity │ │ Entity │ │ Entity │ │ Entity │ ... │ │
│ │ │ Ref 0 │ │ Ref 1 │ │ Ref 2 │ │ Ref 3 │ │ │
│ │ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │ │
│ └───────│───────────│───────────│───────────│──────────────┘ │
│ ▼ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Components (Data) │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │Position │ │Velocity │ │ Health │ │Inventory │ ... │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Resources (World Data) │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │TimeOfDay │ │Weather │ │GameRules │ ... │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Systems (Logic) │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Physics │ │ AI │ │ Combat │ │ Weather │ ... │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
TermDescription
StoreContainer for entities, components, resources, and systems
Entity (Ref)A unique identifier for a game object
ComponentData attached to an entity (no behavior)
ResourceWorld-level data shared across all entities
SystemLogic that operates on entities with specific components
ArchetypeThe set of component types an entity has
QueryFilter for selecting entities by component composition
HolderTemporary container for entity components

Hytale uses two primary store types:

For entity-level data (players, mobs, items, etc.):

// Access via plugin registry
getEntityStoreRegistry().registerComponent(MyComponent.class, MyComponent::new);
getEntityStoreRegistry().registerSystem(new MyEntitySystem());

For chunk-level data (terrain, block data, etc.):

// Access via plugin registry
getChunkStoreRegistry().registerComponent(ChunkData.class, ChunkData::new);
getChunkStoreRegistry().registerSystem(new ChunkProcessingSystem());

Components are pure data containers attached to entities. They should contain no game logic.

import com.hypixel.hytale.component.Component;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
public class HealthComponent implements Component<EntityStore> {
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 = health; }
public float getMaxHealth() { return maxHealth; }
public void setMaxHealth(float maxHealth) { this.maxHealth = maxHealth; }
@Override
public Component<EntityStore> clone() {
HealthComponent copy = new HealthComponent();
copy.health = this.health;
copy.maxHealth = this.maxHealth;
return copy;
}
}
@Override
protected void setup() {
// Register without serialization
ComponentType<EntityStore, HealthComponent> healthType =
getEntityStoreRegistry().registerComponent(
HealthComponent.class,
HealthComponent::new
);
// Register with serialization (saved to disk)
ComponentType<EntityStore, PersistentData> dataType =
getEntityStoreRegistry().registerComponent(
PersistentData.class,
"myplugin:persistent_data",
PersistentData.CODEC
);
}

ComponentType<ECS_TYPE, T> is a handle for accessing a specific component type:

private ComponentType<EntityStore, HealthComponent> healthType;
// In setup()
this.healthType = getEntityStoreRegistry().registerComponent(
HealthComponent.class,
HealthComponent::new
);
// In system
HealthComponent health = store.getComponent(ref, healthType);
if (health != null) {
health.setHealth(health.getHealth() - 5);
}

Resources are world-level data shared across all entities. Unlike components, resources aren’t attached to specific entities.

import com.hypixel.hytale.component.Resource;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
public class WeatherResource implements Resource<EntityStore> {
private String currentWeather = "sunny";
private float intensity = 1.0f;
public String getCurrentWeather() { return currentWeather; }
public void setCurrentWeather(String weather) { this.currentWeather = weather; }
public float getIntensity() { return intensity; }
public void setIntensity(float intensity) { this.intensity = intensity; }
@Override
public Resource<EntityStore> clone() {
WeatherResource copy = new WeatherResource();
copy.currentWeather = this.currentWeather;
copy.intensity = this.intensity;
return copy;
}
}
@Override
protected void setup() {
ResourceType<EntityStore, WeatherResource> weatherType =
getEntityStoreRegistry().registerResource(
WeatherResource.class,
WeatherResource::new
);
}
// Get resource from store
WeatherResource weather = store.getResource(weatherType);
weather.setCurrentWeather("rain");

Systems contain the logic that operates on entities. They query for entities with specific components and process them.

import com.hypixel.hytale.component.system.System;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
public class HealthRegenSystem extends System<EntityStore> {
private ComponentType<EntityStore, HealthComponent> healthType;
public HealthRegenSystem() {
// Register components this system needs
this.healthType = registerComponent(HealthComponent.class, HealthComponent::new);
}
}
System TypeDescription
TickingSystemRuns every tick
TickableSystemTicking with enable/disable control
EntityTickingSystemProcesses each matching entity per tick
ArchetypeTickingSystemBatch processes entities by archetype
ArchetypeChunkSystemProcesses archetype chunks
EventSystemResponds to ECS events
EntityEventSystemHandles entity-specific events
WorldEventSystemHandles world-level events
QuerySystemQuery-based entity iteration
RefSystemTracks entity references
RefChangeSystemDetects reference changes
StoreSystemStore-level operations
HolderSystemWorks with entity holders
import com.hypixel.hytale.component.ArchetypeChunk;
import com.hypixel.hytale.component.CommandBuffer;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.component.system.tick.EntityTickingSystem;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
public class HealthRegenSystem extends EntityTickingSystem<EntityStore> {
private ComponentType<EntityStore, HealthComponent> healthType;
public HealthRegenSystem() {
this.healthType = registerComponent(HealthComponent.class, HealthComponent::new);
}
@Override
public Query<EntityStore> getQuery() {
return healthType; // Only process entities with HealthComponent
}
@Override
public void tick(float dt, int index, ArchetypeChunk<EntityStore> chunk,
Store<EntityStore> store, CommandBuffer<EntityStore> commandBuffer) {
// Get component from the archetype chunk at the given index
HealthComponent health = chunk.getComponent(index, healthType);
if (health.getHealth() < health.getMaxHealth()) {
health.setHealth(Math.min(health.getHealth() + 0.1f * dt, health.getMaxHealth()));
}
}
}
@Override
protected void setup() {
getEntityStoreRegistry().registerSystem(new HealthRegenSystem());
}

Queries filter entities based on their component composition.

QueryDescription
ComponentTypeEntities that have this component
AndQueryEntities with ALL specified components
OrQueryEntities with ANY of the specified components
NotQueryEntities that DON’T have the component
AnyQueryMatches any entity
ExactArchetypeQueryMatches exact archetype
import com.hypixel.hytale.component.query.AndQuery;
import com.hypixel.hytale.component.query.NotQuery;
// Entities with both Position and Velocity
Query<EntityStore> movingEntities = new AndQuery<>(positionType, velocityType);
// Entities with Health but NOT Dead
Query<EntityStore> aliveEntities = new AndQuery<>(
healthType,
new NotQuery<>(deadType)
);
// Use in system
@Override
public Query<EntityStore> getQuery() {
return new AndQuery<>(healthType, hungerType);
}

Ref<ECS_TYPE> is a reference to an entity in a store.

// Check if ref is valid
if (ref.isValid()) {
// Get component
HealthComponent health = store.getComponent(ref, healthType);
// Check if entity has component
if (store.hasComponent(ref, healthType)) {
// ...
}
}
// Create a holder with components
Holder<EntityStore> holder = store.getRegistry().newHolder();
holder.addComponent(positionType, new PositionComponent(x, y, z));
holder.addComponent(healthType, new HealthComponent());
// Spawn entity from holder
Ref<EntityStore> ref = store.spawn(holder);
store.destroy(ref);

Holder<ECS_TYPE> is a temporary container for entity components, used when creating entities or transferring component data.

// Create holder
Holder<EntityStore> holder = store.getRegistry().newHolder();
// Add components
holder.addComponent(componentType, component);
// Get component
Component comp = holder.getComponent(componentType);
// Check for component
if (holder.getArchetype().contains(componentType)) {
// ...
}
// Clone holder
Holder<EntityStore> cloned = holder.clone();

For components that need to be saved/loaded, define a codec:

public class PersistentComponent implements Component<EntityStore> {
private int level;
private String name;
public static final BuilderCodec<PersistentComponent> CODEC = BuilderCodec
.builder(PersistentComponent.class, PersistentComponent::new)
.append(new KeyedCodec<>("Level", Codec.INTEGER),
(c, v) -> c.level = v, c -> c.level)
.add()
.append(new KeyedCodec<>("Name", Codec.STRING),
(c, v) -> c.name = v, c -> c.name)
.add()
.build();
// ... getters, setters, clone
}

Register with serialization:

getEntityStoreRegistry().registerComponent(
PersistentComponent.class,
"myplugin:persistent",
PersistentComponent.CODEC
);
  1. Keep components pure data: No game logic in components
  2. Systems do the work: All behavior goes in systems
  3. Use appropriate queries: Only query for what you need
  4. Cache ComponentTypes: Store them as fields, not in methods
  5. Check ref validity: Always check ref.isValid() before use
  6. Clone when copying: Use clone() for component copies
  7. Register in setup(): All registrations happen during plugin setup
public class DamagePlugin extends JavaPlugin {
private ComponentType<EntityStore, HealthComponent> healthType;
private ComponentType<EntityStore, DamageOverTimeComponent> dotType;
public DamagePlugin(JavaPluginInit init) {
super(init);
}
@Override
protected void setup() {
// Register components
this.healthType = getEntityStoreRegistry().registerComponent(
HealthComponent.class,
HealthComponent::new
);
this.dotType = getEntityStoreRegistry().registerComponent(
DamageOverTimeComponent.class,
DamageOverTimeComponent::new
);
// Register system
getEntityStoreRegistry().registerSystem(new DamageOverTimeSystem(healthType, dotType));
}
}
public class DamageOverTimeSystem extends EntityTickingSystem<EntityStore> {
private final ComponentType<EntityStore, HealthComponent> healthType;
private final ComponentType<EntityStore, DamageOverTimeComponent> dotType;
public DamageOverTimeSystem(
ComponentType<EntityStore, HealthComponent> healthType,
ComponentType<EntityStore, DamageOverTimeComponent> dotType
) {
this.healthType = healthType;
this.dotType = dotType;
}
@Override
public Query<EntityStore> getQuery() {
return new AndQuery<>(healthType, dotType);
}
@Override
public void tick(float dt, int index, ArchetypeChunk<EntityStore> chunk,
Store<EntityStore> store, CommandBuffer<EntityStore> commandBuffer) {
HealthComponent health = chunk.getComponent(index, healthType);
DamageOverTimeComponent dot = chunk.getComponent(index, dotType);
health.setHealth(health.getHealth() - dot.getDamagePerTick());
dot.decrementDuration();
if (dot.getDuration() <= 0) {
// Use command buffer for structural changes
commandBuffer.removeComponent(chunk.getRef(index), dotType);
}
}
}