Update script to write to vendor/hytale-server
This commit is contained in:
414
docs/05-entity-system.md
Normal file
414
docs/05-entity-system.md
Normal file
@@ -0,0 +1,414 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user