Component System (ECS)
Hytale uses an Entity-Component-System architecture for game objects. This provides efficient data access and flexible composition.
Architecture Overview
Section titled “Architecture Overview”Store<ECS_TYPE>├── ComponentRegistry - Type registration├── Archetype[] - Component combinations│ └── ArchetypeChunk[] - Entity storage│ └── Component[][] - Component data├── Resource[] - Global resources└── System[] - Logic processorsCore Concepts
Section titled “Core Concepts”Entities (Ref)
Section titled “Entities (Ref)”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!"); } }}Components
Section titled “Components”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.
Systems
Section titled “Systems”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(); }}Resources
Section titled “Resources”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();}Creating Components
Section titled “Creating Components”Simple Data Component
Section titled “Simple Data Component”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; }}Marker Component
Section titled “Marker Component”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; }}Registering Components
Section titled “Registering Components”In Plugin Setup
Section titled “In Plugin Setup”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; }}Accessing Components
Section titled “Accessing Components”Get Component
Section titled “Get Component”Ref<EntityStore> entityRef = /* ... */;Store<EntityStore> store = entityRef.getStore();
// May return null if entity doesn't have componentHealthComponent health = store.getComponent(entityRef, healthComponentType);
if (health != null) { float current = health.getCurrentHealth();}Ensure Component Exists
Section titled “Ensure Component Exists”// Throws if component missingHealthComponent health = store.ensureAndGetComponent(entityRef, healthComponentType);Add Component
Section titled “Add Component”CommandBuffer<EntityStore> commandBuffer = /* ... */;
commandBuffer.addComponent( entityRef, healthComponentType, new HealthComponent(200f));Remove Component
Section titled “Remove Component”commandBuffer.removeComponent(entityRef, healthComponentType);Modify Component
Section titled “Modify Component”HealthComponent health = store.getComponent(entityRef, healthComponentType);if (health != null) { health.damage(25f); // Changes are automatically tracked}Creating Systems
Section titled “Creating Systems”System Types
Section titled “System Types”Hytale provides several base system classes:
| System Type | Description |
|---|---|
TickingSystem | Base ticking system, receives Store reference |
EntityTickingSystem | Iterates over entities matching a query |
ArchetypeTickingSystem | Iterates over archetype chunks matching a query |
Basic Ticking System
Section titled “Basic Ticking System”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 }}Entity Ticking System
Section titled “Entity Ticking System”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); } }}Register System
Section titled “Register System”@Overrideprotected void setup() { getEntityStoreRegistry().registerSystem(new HealthRegenSystem(healthComponentType));}System Dependencies
Section titled “System Dependencies”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 }}Resources (Global State)
Section titled “Resources (Global State)”Define Resource
Section titled “Define Resource”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; }}Register and Access Resource
Section titled “Register and Access Resource”private ResourceType<EntityStore, GameStateResource> gameStateType;
@Overrideprotected void setup() { gameStateType = getEntityStoreRegistry().registerResource( GameStateResource.class, GameStateResource::new );}
// Access in codeStore<EntityStore> store = /* ... */;GameStateResource state = store.getResource(gameStateType);state.addScore(100);Queries
Section titled “Queries”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 PositionQuery<EntityStore> both = Query.and(healthType, positionType);
// Entities with Health OR ArmorQuery<EntityStore> either = Query.or(healthType, armorType);
// Entities with Health but NOT Dead markerQuery<EntityStore> alive = Query.and(healthType, Query.not(deadMarkerType));
// All entitiesQuery<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 }}Command Buffer
Section titled “Command Buffer”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 immediatelyRef<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 tickbuffer.run(store -> { // This runs after all queued commands});
// Operations execute at end of tickStore Types
Section titled “Store Types”EntityStore
Section titled “EntityStore”For game entities (players, NPCs, items):
getEntityStoreRegistry().registerComponent(...)getEntityStoreRegistry().registerSystem(...)getEntityStoreRegistry().registerResource(...)ChunkStore
Section titled “ChunkStore”For chunk-level data:
getChunkStoreRegistry().registerComponent(...)getChunkStoreRegistry().registerSystem(...)Built-in Components
Section titled “Built-in Components”Hytale provides many built-in components for common functionality:
| Component | Description |
|---|---|
TransformComponent | Entity position, rotation, scale |
ModelComponent | Visual model reference |
HealthComponent (in game modules) | Health and damage |
ItemComponent | Item data for dropped items |
PlayerSkinComponent | Player skin data |
DisplayNameComponent | Entity display name |
AudioComponent | Sound emission |
CollisionResultComponent | Collision detection results |
Best Practices
Section titled “Best Practices”- Use components for data - Keep logic in systems
- Implement clone() - Required for entity copying
- Use CommandBuffer - Never modify directly during iteration
- Define codecs - For persistence support
- Use marker components - For boolean flags (no data needed)
- Query efficiently - Combine queries to minimize iteration
- Respect system order - Use dependencies correctly
- Cache ComponentTypes - Store references for fast access
- Validate Refs before use - Always check
isValid()before accessing entity data - Use Resources for global state - Avoid storing shared state in components