Component System
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> { public static final Ref<?>[] EMPTY_ARRAY = new Ref[0];
@Nonnull private final Store<ECS_TYPE> store; private volatile int index; private volatile transient int hashCode; private volatile Throwable invalidatedBy;
public Ref(@Nonnull Store<ECS_TYPE> store) { this(store, Integer.MIN_VALUE); }
public Ref(@Nonnull Store<ECS_TYPE> store, int index) { this.store = store; this.index = index; this.hashCode = this.hashCode0(); }
@Nonnull public Store<ECS_TYPE> getStore() { return this.store; }
public int getIndex() { return this.index; }
public boolean isValid() { return this.index != Integer.MIN_VALUE; }
public void validate() { if (this.index == Integer.MIN_VALUE) { throw new IllegalStateException("Invalid entity reference!", this.invalidatedBy); } }}Components
Section titled “Components”Data containers attached to entities. Components must implement Cloneable for entity copying:
public interface Component<ECS_TYPE> extends Cloneable { @Nonnull 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”// Creates the component if 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); // You're mutating the stored instance; no reinsert needed}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 closer priority (executes closer to target) new SystemDependency<>(Order.BEFORE, AnotherSystem.class, OrderPriority.CLOSE) ); }
@Override public void tick(float dt, int index, Store<EntityStore> store) { // Process }}Available OrderPriority values:
CLOSEST- Highest priority, executes closest to the target systemCLOSE- High priorityNORMAL- Default priorityFURTHER- Lower priorityFURTHEST- Lowest priority, executes furthest from the target system
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:
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();Command Buffer
Section titled “Command Buffer”When iterating or ticking systems, use the CommandBuffer provided by the store/system to queue entity mutations safely:
public class CleanupSystem extends EntityTickingSystem<EntityStore> {
@Override public Query<EntityStore> getQuery() { return healthType; }
@Override public void tick(float dt, int index, ArchetypeChunk<EntityStore> chunk, Store<EntityStore> store, CommandBuffer<EntityStore> buffer) { Ref<EntityStore> ref = chunk.getReferenceTo(index); HealthComponent health = chunk.getComponent(index, healthType);
if (health != null && health.getCurrentHealth() <= 0f) { buffer.removeEntity(ref, RemoveReason.REMOVE); }
buffer.run(storeRef -> { // Runs after queued commands are consumed }); }}Available AddReason values:
SPAWN- Entity is being spawned (e.g., player joins, mob spawns)LOAD- Entity is being loaded from storage
Available RemoveReason values:
REMOVE- Entity is being permanently removed (e.g., death, despawn)UNLOAD- Entity is being unloaded to storage (e.g., chunk unload)
Built-in Components
Section titled “Built-in Components”Hytale provides many built-in components for common functionality:
| Component | Package | Description |
|---|---|---|
TransformComponent | modules.entity.component | Entity position and rotation (uses Vector3d for position, Vector3f for rotation) |
ModelComponent | modules.entity.component | Visual model reference |
EntityScaleComponent | modules.entity.component | Entity scale modifier |
PositionDataComponent | modules.entity.component | Additional position-related data |
BoundingBox | modules.entity.component | Entity collision bounding box |
ItemComponent | modules.entity.item | Item data for dropped items |
PlayerSkinComponent | modules.entity.player | Player skin data |
DisplayNameComponent | modules.entity.component | Entity display name |
AudioComponent | modules.entity.component | Sound emission |
MovementAudioComponent | modules.entity.component | Movement-related sounds |
CollisionResultComponent | modules.entity.component | Collision detection results |
UUIDComponent | entity | Unique entity identifier |
EffectControllerComponent | entity.effect | Active effects on entity |
All built-in components are in the com.hypixel.hytale.server.core package hierarchy.
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