415 lines
9.9 KiB
Markdown
415 lines
9.9 KiB
Markdown
# 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>`:
|
|
|
|
```java
|
|
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()`:
|
|
|
|
```java
|
|
@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
|
|
|
|
```java
|
|
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
|
|
|
|
```java
|
|
@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
|
|
|
|
```java
|
|
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:
|
|
|
|
```java
|
|
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:
|
|
|
|
```java
|
|
// 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
|
|
|
|
```java
|
|
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:
|
|
|
|
```java
|
|
// 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:
|
|
|
|
```java
|
|
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
|
|
|
|
```java
|
|
@Override
|
|
protected void setup() {
|
|
// Register system with the world module
|
|
getEntityStoreRegistry().registerSystem(new HealthRegenSystem());
|
|
}
|
|
```
|
|
|
|
## Working with Players
|
|
|
|
### Getting Players
|
|
|
|
```java
|
|
// 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
|
|
|
|
```java
|
|
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:
|
|
|
|
```java
|
|
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:
|
|
|
|
```java
|
|
// 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
|
|
|
|
```java
|
|
// 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
|
|
|
|
1. **Keep components data-only** - Components should not contain logic, only data
|
|
2. **Use systems for behavior** - Put game logic in systems, not components
|
|
3. **Prefer composition** - Use multiple small components instead of large monolithic ones
|
|
4. **Query efficiently** - Cache queries and reuse them
|
|
5. **Use archetypes** - Define archetypes for common entity types
|
|
6. **Handle null components** - Always check if a component exists before using it
|
|
7. **Clean up entities** - Remove entities when they're no longer needed
|
|
8. **Use appropriate entity types** - Extend `LivingEntity` for damageable entities
|