Skip to content

ECS FAQ - Entity Component System Questions

Get quick answers to common questions about working with Hytale’s Entity Component System.

Entity Component System (ECS) is an architectural pattern that separates data (components) from behavior (systems). Instead of using inheritance hierarchies, ECS composes entities from small, reusable components and processes them with specialized systems.

ECS provides:

  • Performance - Cache-friendly data layout, efficient iteration
  • Flexibility - Compose entities from any combination of components
  • Maintainability - Small, focused systems instead of monolithic classes
  • Scalability - Easy to parallelize and optimize

Not always! For simple plugins (commands, basic events), you don’t need ECS. Use ECS when:

  • Creating custom entity behaviors
  • Implementing per-tick logic
  • Building complex game mechanics
  • Storing serializable game state

What’s the difference between ECS and traditional OOP?

Section titled “What’s the difference between ECS and traditional OOP?”

Traditional OOP:

class Monster extends Entity {
// All data and behavior mixed together
}

ECS:

// Data only
class HealthComponent { int health; }
// Behavior only
class DamageSystem {
void update(Query<HealthComponent> entities) {
// Process all entities with HealthComponent
}
}

A component is a small, data-only structure that describes a single aspect of an entity. Examples: PositionComponent, HealthComponent, InventoryComponent.

Implement the Component interface:

public class MyComponent implements Component {
public int value;
public String data;
}

Then register it with the ComponentRegistry.

No! Components should be pure data. Put all logic in Systems.

Yes, but use entity IDs (references), not direct object references:

public class OwnerComponent implements Component {
public long ownerId; // Entity ID, not Entity object
}

How many components should an entity have?

Section titled “How many components should an entity have?”

Keep it focused! An entity might have 3-10 components typically. Too many components can indicate over-engineering.

Yes! Use the CommandBuffer within systems:

commandBuffer.addComponent(entityId, new MyComponent());
commandBuffer.removeComponent(entityId, MyComponent.class);

A system is behavior that operates on entities matching specific component requirements. Systems run every tick, on events, or on intervals.

  • Ticking Systems - Run every server tick
  • Event Systems - Run when specific events fire
  • Delayed Systems - Run on intervals (e.g., every 5 seconds)
  • Data Systems - Provide derived data without mutating state

Extend the appropriate system base class:

public class MyTickingSystem extends TickingSystem {
@Override
public void tick(Store store, float deltaTime) {
Query query = Query.with(MyComponent.class);
store.forEach(query, entity -> {
// Process entity
});
}
}

Use dependencies or system groups:

@DependsOn(OtherSystem.class)
public class MySystem extends TickingSystem {
// This runs after OtherSystem
}

Some systems can be parallelized, but be careful with shared state. Use the CommandBuffer to safely queue changes during iteration.

  • Ticking systems - Every server tick (~20 times per second)
  • Event systems - When their event fires
  • Delayed systems - Based on configured interval

A query is a filter that selects entities based on component requirements. It’s how systems find entities to process.

Use the builder pattern:

Query query = Query.builder()
.with(PositionComponent.class)
.with(HealthComponent.class)
.without(DeadComponent.class)
.build();
  • with()/all() - Entity must have these components
  • without()/none() - Entity must NOT have these
  • or() - Entity must have at least one of these
  • exact() - Match exact archetype

No! Create queries once (e.g., in constructor) and reuse them:

public class MySystem extends TickingSystem {
private final Query query;
public MySystem() {
this.query = Query.with(MyComponent.class);
}
@Override
public void tick(Store store, float deltaTime) {
store.forEach(query, entity -> {
// Use cached query
});
}
}

Very! Hytale’s ECS uses archetypes, so queries are basically array iterations. Don’t worry about query performance unless you have millions of entities.

No. Each world has its own Store. Queries operate on a single store.

An entity is just an ID (long integer). All meaning comes from the components attached to it.

Use the CommandBuffer:

long entityId = commandBuffer.createEntity();
commandBuffer.addComponent(entityId, new PositionComponent());
commandBuffer.addComponent(entityId, new HealthComponent());
commandBuffer.destroyEntity(entityId);

Yes, but prefer querying for specific components:

PositionComponent pos = store.getComponent(entityId, PositionComponent.class);
if (store.hasEntity(entityId)) {
// Entity exists
PositionComponent pos = store.getComponent(entityId, PositionComponent.class);
}

ECS events are typed payloads that flow through the ECS system. They’re different from regular Hytale events.

  • EntityEventType - Events targeting specific entities
  • WorldEventType - Events affecting the entire world

Create an event system:

public class MyEventSystem extends EventSystem<MyEvent> {
@Override
public void handle(Store store, MyEvent event) {
// Process event
}
}

Some events are cancellable. Check the event type:

if (event instanceof CancellableEvent) {
((CancellableEvent) event).cancel();
}

Register your event type and invoke it:

store.invoke(new MyCustomEvent(data));

An archetype is the unique set of components an entity has. Entities with the same components share an archetype.

Example: All entities with [Position, Health, Inventory] components share one archetype.

Performance! Entities in the same archetype are stored contiguously in memory, making iteration very fast.

Yes:

Query query = Query.exactArchetype(PositionComponent.class, HealthComponent.class);

No! Archetypes are created and managed automatically when you add/remove components.

The central catalog that holds:

  • Component types
  • System definitions
  • Event types
  • Resources

Usually registered during plugin initialization.

A per-world ECS instance that owns entities, archetypes, and runs the tick loop. Each world has its own Store.

Usually passed to your system methods:

@Override
public void tick(Store store, float deltaTime) {
// Use store
}

Yes! Each world has its own Store. Don’t share data between stores without explicit coordination.

A buffer for queuing ECS changes (create/destroy entities, add/remove components). Changes are applied after the current system finishes to avoid iteration corruption.

Why use CommandBuffer instead of direct changes?

Section titled “Why use CommandBuffer instead of direct changes?”

Thread safety! Direct changes during iteration can corrupt data structures. CommandBuffer ensures safe mutation.

After the current system completes its tick/event handling.

Can I read CommandBuffer changes immediately?

Section titled “Can I read CommandBuffer changes immediately?”

No. Changes are pending until applied. Design systems to handle this delay.

Thousands to tens of thousands efficiently. Performance depends on system complexity and component count.

What’s the most common ECS performance mistake?

Section titled “What’s the most common ECS performance mistake?”

Creating queries inside loops:

// BAD!
for (Entity e : entities) {
Query q = Query.with(MyComponent.class); // Don't do this!
}
// GOOD!
Query q = Query.with(MyComponent.class); // Create once
for (Entity e : entities) {
// Use cached query
}

For tight loops, yes:

PositionComponent pos = store.getComponent(entityId, PositionComponent.class);
// Use pos multiple times

Use Java profilers (JProfiler, YourKit) to identify slow systems. Focus on systems that run every tick.

Should I use many small components or fewer large ones?

Section titled “Should I use many small components or fewer large ones?”

Many small components! This enables better reusability and flexibility:

// Good
class PositionComponent { Vector3 pos; }
class VelocityComponent { Vector3 vel; }
// Less flexible
class MovementComponent { Vector3 pos; Vector3 vel; }

Use Resources - non-entity data registered with the registry:

registry.registerResource(new MySharedData());

In ticking systems, yes (but use CommandBuffer for structural changes). In event systems, check if mutation is allowed.

  • Log entity IDs and component values
  • Check system execution order
  • Verify queries match expected entities
  • Use CommandBuffer correctly

Last updated: February 2026