10 KiB
Entity Component System (ECS)
The Hytale server uses an Entity Component System architecture for managing game entities. This provides high performance and flexibility for handling large numbers of entities with varied behaviors.
ECS Concepts
Overview
The ECS pattern separates data (Components) from behavior (Systems):
- Entities: Unique identifiers (IDs) that represent game objects
- Components: Pure data containers attached to entities
- Systems: Logic that operates on entities with specific component combinations
- Stores: Data storage containers that hold components
- Archetypes: Predefined combinations of components
Key Classes
| Class | Description |
|---|---|
Component<S> |
Base component interface |
ComponentRegistry |
Central registry for component types |
Store |
ECS data storage |
Ref |
Entity reference (ID wrapper) |
Holder<C> |
Component holder/accessor |
Archetype |
Entity archetype definition |
SystemType |
System type definition |
Query |
ECS query for finding entities |
Components
Defining a Component
Components are pure data classes implementing Component<Store>:
public class HealthComponent implements Component<EntityStore> {
public static final ComponentRegistry.Entry<HealthComponent> ENTRY =
EntityStore.REGISTRY.register("health", HealthComponent.class, HealthComponent::new);
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 = Math.max(0, Math.min(health, maxHealth));
}
public float getMaxHealth() {
return maxHealth;
}
public void setMaxHealth(float maxHealth) {
this.maxHealth = maxHealth;
}
public boolean isDead() {
return health <= 0;
}
public void damage(float amount) {
setHealth(health - amount);
}
public void heal(float amount) {
setHealth(health + amount);
}
}
Built-in Components
The server provides several built-in components in EntityStore.REGISTRY:
| Component | Description |
|---|---|
TransformComponent |
Position, rotation, and scale |
UUIDComponent |
Unique entity identifier |
ModelComponent |
Visual model reference |
MovementAudioComponent |
Movement sound effects |
PositionDataComponent |
Position tracking data |
HeadRotation |
Head rotation for living entities |
Registering Components
Register custom components in your plugin's setup():
@Override
protected void setup() {
// Components are registered through the EntityStoreRegistry
getEntityStoreRegistry().registerComponent(MyComponent.ENTRY);
}
Entities
Entity Hierarchy
Component<EntityStore>
└── Entity // Base entity
└── LivingEntity // Has health and can take damage
├── Player // Player entity
└── NPCEntity // NPC entity
Creating a Custom Entity
public class CustomEntity extends Entity {
public static final Codec<CustomEntity> CODEC = BuilderCodec.of(CustomEntity::new)
// Add codec fields
.build();
private int customData;
public CustomEntity(World world) {
super(world);
this.customData = 0;
}
public int getCustomData() {
return customData;
}
public void setCustomData(int data) {
this.customData = data;
}
@Override
public void tick() {
super.tick();
// Custom tick logic
}
}
Registering Entities
@Override
protected void setup() {
getEntityRegistry().register(
"customEntity",
CustomEntity.class,
CustomEntity::new
);
// With serialization codec
getEntityRegistry().register(
"customEntity",
CustomEntity.class,
CustomEntity::new,
CustomEntity.CODEC
);
}
Accessing Entity Components
Entity entity = world.getEntity(entityId);
// Get a component
TransformComponent transform = entity.get(TransformComponent.ENTRY);
if (transform != null) {
Vector3d position = transform.getPosition();
}
// Check if entity has component
if (entity.has(HealthComponent.ENTRY)) {
// Entity has health
}
// Add a component
entity.add(MyComponent.ENTRY, new MyComponent());
// Remove a component
entity.remove(MyComponent.ENTRY);
Entity Store
The EntityStore manages entity data within a world:
World world = Universe.get().getWorld("default");
EntityStore store = world.getEntityStore();
// Create entity
Ref entityRef = store.create();
// Add components to entity
store.add(entityRef, TransformComponent.ENTRY, new TransformComponent(position));
store.add(entityRef, HealthComponent.ENTRY, new HealthComponent());
// Get component from entity
HealthComponent health = store.get(entityRef, HealthComponent.ENTRY);
// Remove entity
store.remove(entityRef);
Queries
Queries find entities with specific component combinations:
// Query for entities with both Transform and Health components
Query query = Query.builder()
.with(TransformComponent.ENTRY)
.with(HealthComponent.ENTRY)
.build();
// Execute query
EntityStore store = world.getEntityStore();
store.query(query, (ref, transform, health) -> {
// Process each matching entity
if (health.isDead()) {
// Handle dead entity
}
});
Query Builder
Query query = Query.builder()
.with(ComponentA.ENTRY) // Must have ComponentA
.with(ComponentB.ENTRY) // Must have ComponentB
.without(ComponentC.ENTRY) // Must NOT have ComponentC
.build();
Archetypes
Archetypes define common component combinations for entity types:
// Define an archetype for enemies
Archetype enemyArchetype = Archetype.builder()
.add(TransformComponent.ENTRY)
.add(HealthComponent.ENTRY)
.add(AIComponent.ENTRY)
.add(CombatComponent.ENTRY)
.build();
// Create entity with archetype
Ref enemy = store.create(enemyArchetype);
Systems
Systems contain the logic that operates on entities:
public class HealthRegenSystem implements System {
private static final Query QUERY = Query.builder()
.with(HealthComponent.ENTRY)
.build();
@Override
public void tick(World world) {
EntityStore store = world.getEntityStore();
store.query(QUERY, (ref, health) -> {
if (!health.isDead() && health.getHealth() < health.getMaxHealth()) {
// Regenerate 0.1 health per tick
health.heal(0.1f);
}
});
}
}
Registering Systems
@Override
protected void setup() {
// Register system with the world module
getEntityStoreRegistry().registerSystem(new HealthRegenSystem());
}
Working with Players
Getting Players
// Get all players
Collection<Player> players = Universe.get().getPlayers();
// Get player by name
Player player = Universe.get().getPlayer("PlayerName");
// Get player by UUID
Player player = Universe.get().getPlayer(uuid);
// Get players in world
World world = Universe.get().getWorld("default");
Collection<Player> worldPlayers = world.getPlayers();
Player Properties
Player player = getPlayer();
// Position
Vector3d position = player.getPosition();
player.setPosition(new Vector3d(x, y, z));
// Rotation
float yaw = player.getYaw();
float pitch = player.getPitch();
// Health (if LivingEntity)
player.setHealth(20.0f);
float health = player.getHealth();
// Game mode
GameMode mode = player.getGameMode();
player.setGameMode(GameMode.CREATIVE);
// Inventory
Inventory inventory = player.getInventory();
Living Entities
LivingEntity extends Entity with health and damage capabilities:
public class CustomMob extends LivingEntity {
public CustomMob(World world) {
super(world);
setMaxHealth(50.0f);
setHealth(50.0f);
}
@Override
public void onDamage(DamageSource source, float amount) {
super.onDamage(source, amount);
// Custom damage handling
}
@Override
public void onDeath() {
super.onDeath();
// Drop loot, play effects, etc.
}
}
Entity Events
Subscribe to entity-related events:
// Entity removed from world
getEventRegistry().register(EntityRemoveEvent.class, event -> {
Entity entity = event.getEntity();
// Handle entity removal
});
// Living entity uses block
getEventRegistry().register(LivingEntityUseBlockEvent.class, event -> {
LivingEntity entity = event.getEntity();
// Handle block usage
});
Spawning Entities
// Get the world
World world = Universe.get().getWorld("default");
// Spawn entity at position
Vector3d position = new Vector3d(100, 64, 100);
CustomEntity entity = new CustomEntity(world);
entity.setPosition(position);
world.addEntity(entity);
// Spawn with specific properties
CustomEntity entity = new CustomEntity(world);
entity.setPosition(position);
entity.setCustomData(42);
world.addEntity(entity);
Best Practices
- Keep components data-only - Components should not contain logic, only data
- Use systems for behavior - Put game logic in systems, not components
- Prefer composition - Use multiple small components instead of large monolithic ones
- Query efficiently - Cache queries and reuse them
- Use archetypes - Define archetypes for common entity types
- Handle null components - Always check if a component exists before using it
- Clean up entities - Remove entities when they're no longer needed
- Use appropriate entity types - Extend
LivingEntityfor damageable entities