Skip to content

Component System (ECS)

Hytale uses an Entity-Component-System architecture for game objects. This provides efficient data access and flexible composition.

Store<ECS_TYPE>
├── ComponentRegistry - Type registration
├── Archetype[] - Component combinations
│ └── ArchetypeChunk[] - Entity storage
│ └── Component[][] - Component data
├── Resource[] - Global resources
└── System[] - Logic processors

Lightweight references to entity data. A Ref is a pointer to an entity within a Store:

public class Ref<ECS_TYPE> {
private final Store<ECS_TYPE> store;
private volatile int index;
public Store<ECS_TYPE> getStore() {
return this.store;
}
public boolean isValid() {
return index != Integer.MIN_VALUE;
}
public void validate() {
if (this.index == Integer.MIN_VALUE) {
throw new IllegalStateException("Invalid entity reference!");
}
}
}

Data containers attached to entities. Components must implement Cloneable for entity copying:

public interface Component<ECS_TYPE> extends Cloneable {
public static final Component[] EMPTY_ARRAY = new Component[0];
@Nullable
Component<ECS_TYPE> clone();
@Nullable
default Component<ECS_TYPE> cloneSerializable() {
return this.clone();
}
}

The cloneSerializable() method is used for persistence and can be overridden to exclude transient data.

Logic processors that operate on components. Systems define processing logic and execution order:

public interface ISystem<ECS_TYPE> {
public static final ISystem[] EMPTY_ARRAY = new ISystem[0];
default void onSystemRegistered() {}
default void onSystemUnregistered() {}
@Nullable
default SystemGroup<ECS_TYPE> getGroup() {
return null;
}
@Nonnull
default Set<Dependency<ECS_TYPE>> getDependencies() {
return Collections.emptySet();
}
}

Global shared state per store. Resources are singleton objects accessible from any system:

public interface Resource<ECS_TYPE> extends Cloneable {
public static final Resource[] EMPTY_ARRAY = new Resource[0];
@Nullable
Resource<ECS_TYPE> clone();
}
public class HealthComponent implements Component<EntityStore> {
public static final BuilderCodec<HealthComponent> CODEC =
BuilderCodec.builder(HealthComponent.class, HealthComponent::new)
.append(new KeyedCodec<>("MaxHealth", Codec.FLOAT),
(c, v) -> c.maxHealth = v, c -> c.maxHealth)
.add()
.append(new KeyedCodec<>("CurrentHealth", Codec.FLOAT),
(c, v) -> c.currentHealth = v, c -> c.currentHealth)
.add()
.build();
private float maxHealth = 100f;
private float currentHealth = 100f;
public HealthComponent() {}
public HealthComponent(float maxHealth) {
this.maxHealth = maxHealth;
this.currentHealth = maxHealth;
}
public float getMaxHealth() { return maxHealth; }
public float getCurrentHealth() { return currentHealth; }
public void setCurrentHealth(float health) {
this.currentHealth = Math.min(health, maxHealth);
}
public void damage(float amount) {
this.currentHealth = Math.max(0, currentHealth - amount);
}
@Override
public Component<EntityStore> clone() {
HealthComponent copy = new HealthComponent(maxHealth);
copy.currentHealth = this.currentHealth;
return copy;
}
}

For boolean flags (no data needed):

public class FlyingMarker implements Component<EntityStore> {
public static final FlyingMarker INSTANCE = new FlyingMarker();
public static final BuilderCodec<FlyingMarker> CODEC =
BuilderCodec.builder(FlyingMarker.class, () -> INSTANCE).build();
private FlyingMarker() {}
@Override
public Component<EntityStore> clone() {
return INSTANCE;
}
}
public class MyPlugin extends JavaPlugin {
private ComponentType<EntityStore, HealthComponent> healthComponentType;
@Override
protected void setup() {
// With serialization (saved to disk)
healthComponentType = getEntityStoreRegistry().registerComponent(
HealthComponent.class,
"Health",
HealthComponent.CODEC
);
// Without serialization (runtime only)
ComponentType<EntityStore, TempData> tempType =
getEntityStoreRegistry().registerComponent(
TempData.class,
TempData::new
);
}
public ComponentType<EntityStore, HealthComponent> getHealthComponentType() {
return healthComponentType;
}
}
Ref<EntityStore> entityRef = /* ... */;
Store<EntityStore> store = entityRef.getStore();
// May return null if entity doesn't have component
HealthComponent health = store.getComponent(entityRef, healthComponentType);
if (health != null) {
float current = health.getCurrentHealth();
}
// Throws if component missing
HealthComponent health = store.ensureAndGetComponent(entityRef, healthComponentType);
CommandBuffer<EntityStore> commandBuffer = /* ... */;
commandBuffer.addComponent(
entityRef,
healthComponentType,
new HealthComponent(200f)
);
commandBuffer.removeComponent(entityRef, healthComponentType);
HealthComponent health = store.getComponent(entityRef, healthComponentType);
if (health != null) {
health.damage(25f);
// Changes are automatically tracked
}

Hytale provides several base system classes:

System TypeDescription
TickingSystemBase ticking system, receives Store reference
EntityTickingSystemIterates over entities matching a query
ArchetypeTickingSystemIterates over archetype chunks matching a query

The TickingSystem is the simplest form, receiving the full store each tick:

public class GlobalUpdateSystem extends TickingSystem<EntityStore> {
@Override
public void tick(float dt, int index, Store<EntityStore> store) {
// Access resources, perform global updates
}
}

The EntityTickingSystem iterates over individual entities matching a query:

public class HealthRegenSystem extends EntityTickingSystem<EntityStore> {
private final ComponentType<EntityStore, HealthComponent> healthType;
public HealthRegenSystem(ComponentType<EntityStore, HealthComponent> healthType) {
this.healthType = healthType;
}
@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) {
HealthComponent health = chunk.getComponent(index, healthType);
if (health.getCurrentHealth() < health.getMaxHealth()) {
health.setCurrentHealth(health.getCurrentHealth() + dt * 5f);
}
}
}
@Override
protected void setup() {
getEntityStoreRegistry().registerSystem(new HealthRegenSystem(healthComponentType));
}

Control execution order with dependencies using SystemDependency:

import com.hypixel.hytale.component.dependency.SystemDependency;
import com.hypixel.hytale.component.dependency.Order;
import com.hypixel.hytale.component.dependency.OrderPriority;
public class MySystem extends TickingSystem<EntityStore> {
@Override
public Set<Dependency<EntityStore>> getDependencies() {
return Set.of(
// Run after OtherSystem
new SystemDependency<>(Order.AFTER, OtherSystem.class),
// Run before AnotherSystem with high priority
new SystemDependency<>(Order.BEFORE, AnotherSystem.class, OrderPriority.HIGH)
);
}
@Override
public void tick(float dt, int index, Store<EntityStore> store) {
// Process
}
}
public class GameStateResource implements Resource<EntityStore> {
private int score = 0;
private boolean gameOver = false;
public int getScore() { return score; }
public void addScore(int points) { score += points; }
public boolean isGameOver() { return gameOver; }
public void setGameOver(boolean over) { gameOver = over; }
@Override
public Resource<EntityStore> clone() {
GameStateResource copy = new GameStateResource();
copy.score = this.score;
copy.gameOver = this.gameOver;
return copy;
}
}
private ResourceType<EntityStore, GameStateResource> gameStateType;
@Override
protected void setup() {
gameStateType = getEntityStoreRegistry().registerResource(
GameStateResource.class,
GameStateResource::new
);
}
// Access in code
Store<EntityStore> store = /* ... */;
GameStateResource state = store.getResource(gameStateType);
state.addScore(100);

Filter entities by component composition. The Query interface provides static factory methods:

import com.hypixel.hytale.component.query.Query;
// Entities with HealthComponent (ComponentType implements Query)
Query<EntityStore> query = healthComponentType;
// Entities with both Health AND Position
Query<EntityStore> both = Query.and(healthType, positionType);
// Entities with Health OR Armor
Query<EntityStore> either = Query.or(healthType, armorType);
// Entities with Health but NOT Dead marker
Query<EntityStore> alive = Query.and(healthType, Query.not(deadMarkerType));
// All entities
Query<EntityStore> all = Query.any();

Queries are used by systems to determine which entities they should process:

public class CombatSystem extends EntityTickingSystem<EntityStore> {
private final ComponentType<EntityStore, HealthComponent> healthType;
private final ComponentType<EntityStore, ArmorComponent> armorType;
@Override
public Query<EntityStore> getQuery() {
// Only process entities with both Health and Armor
return Query.and(healthType, armorType);
}
@Override
public void tick(float dt, int index,
ArchetypeChunk<EntityStore> chunk,
Store<EntityStore> store,
CommandBuffer<EntityStore> commandBuffer) {
// Process entity at index
}
}

All entity modifications go through CommandBuffer for thread safety. Commands are queued and executed at the end of the tick:

CommandBuffer<EntityStore> buffer = store.getCommandBuffer();
// Queue operations - these don't execute immediately
Ref<EntityStore> newEntity = buffer.addEntity(holder, AddReason.SPAWNED);
buffer.addComponent(newEntity, healthType, new HealthComponent(100f));
buffer.removeEntity(oldEntity, RemoveReason.KILLED);
// Run arbitrary code at end of tick
buffer.run(store -> {
// This runs after all queued commands
});
// Operations execute at end of tick

For game entities (players, NPCs, items):

getEntityStoreRegistry().registerComponent(...)
getEntityStoreRegistry().registerSystem(...)
getEntityStoreRegistry().registerResource(...)

For chunk-level data:

getChunkStoreRegistry().registerComponent(...)
getChunkStoreRegistry().registerSystem(...)

Hytale provides many built-in components for common functionality:

ComponentDescription
TransformComponentEntity position, rotation, scale
ModelComponentVisual model reference
HealthComponent (in game modules)Health and damage
ItemComponentItem data for dropped items
PlayerSkinComponentPlayer skin data
DisplayNameComponentEntity display name
AudioComponentSound emission
CollisionResultComponentCollision detection results
  1. Use components for data - Keep logic in systems
  2. Implement clone() - Required for entity copying
  3. Use CommandBuffer - Never modify directly during iteration
  4. Define codecs - For persistence support
  5. Use marker components - For boolean flags (no data needed)
  6. Query efficiently - Combine queries to minimize iteration
  7. Respect system order - Use dependencies correctly
  8. Cache ComponentTypes - Store references for fast access
  9. Validate Refs before use - Always check isValid() before accessing entity data
  10. Use Resources for global state - Avoid storing shared state in components