diff --git a/docs/en/guide/entity.md b/docs/en/guide/entity.md new file mode 100644 index 00000000..884f2769 --- /dev/null +++ b/docs/en/guide/entity.md @@ -0,0 +1,444 @@ +# Entity + +In ECS architecture, an Entity is the basic object in the game world. An entity itself does not contain game logic or data - it's just a container that combines different components to achieve various functionalities. + +## Basic Concepts + +An entity is a lightweight object mainly used for: +- Serving as a container for components +- Providing a unique identifier (ID) +- Managing component lifecycle + +::: tip About Parent-Child Hierarchy +Parent-child hierarchy relationships between entities are managed through `HierarchyComponent` and `HierarchySystem`, not built-in Entity properties. This design follows ECS composition principles - only entities that need hierarchy relationships add this component. + +See [Hierarchy System](./hierarchy.md) documentation. +::: + +## Creating Entities + +**Important: Entities must be created through Scene, manual creation is not supported!** + +Entities must be created through the scene's `createEntity()` method to ensure: +- Entity is properly added to the scene's entity management system +- Entity is added to the query system for system use +- Entity gets the correct scene reference +- Related lifecycle events are triggered + +```typescript +// Correct way: create entity through scene +const player = scene.createEntity("Player"); + +// Wrong way: manually create entity +// const entity = new Entity("MyEntity", 1); // System cannot manage such entities +``` + +## Adding Components + +Entities gain functionality by adding components: + +```typescript +import { Component, ECSComponent } from '@esengine/ecs-framework'; + +// Define position component +@ECSComponent('Position') +class Position extends Component { + x: number = 0; + y: number = 0; + + constructor(x: number = 0, y: number = 0) { + super(); + this.x = x; + this.y = y; + } +} + +// Define health component +@ECSComponent('Health') +class Health extends Component { + current: number = 100; + max: number = 100; + + constructor(max: number = 100) { + super(); + this.max = max; + this.current = max; + } +} + +// Add components to entity +const player = scene.createEntity("Player"); +player.addComponent(new Position(100, 200)); +player.addComponent(new Health(150)); +``` + +## Getting Components + +```typescript +// Get component (pass component class, not instance) +const position = player.getComponent(Position); // Returns Position | null +const health = player.getComponent(Health); // Returns Health | null + +// Check if component exists +if (position) { + console.log(`Player position: x=${position.x}, y=${position.y}`); +} + +// Check if entity has a component +if (player.hasComponent(Position)) { + console.log("Player has position component"); +} + +// Get all component instances (read-only property) +const allComponents = player.components; // readonly Component[] + +// Get all components of specified type (supports multiple components of same type) +const allHealthComponents = player.getComponents(Health); // Health[] + +// Get or create component (creates automatically if not exists) +const position = player.getOrCreateComponent(Position, 0, 0); // Pass constructor arguments +const health = player.getOrCreateComponent(Health, 100); // Returns existing if present, creates new if not +``` + +## Removing Components + +```typescript +// Method 1: Remove by component type +const removedHealth = player.removeComponentByType(Health); +if (removedHealth) { + console.log("Health component removed"); +} + +// Method 2: Remove by component instance +const healthComponent = player.getComponent(Health); +if (healthComponent) { + player.removeComponent(healthComponent); +} + +// Batch remove multiple component types +const removedComponents = player.removeComponentsByTypes([Position, Health]); + +// Check if component was removed +if (!player.hasComponent(Health)) { + console.log("Health component has been removed"); +} +``` + +## Finding Entities + +Scene provides multiple ways to find entities: + +### Find by Name + +```typescript +// Find single entity +const player = scene.findEntity("Player"); +// Or use alias method +const player2 = scene.getEntityByName("Player"); + +if (player) { + console.log("Found player entity"); +} +``` + +### Find by ID + +```typescript +// Find by entity ID +const entity = scene.findEntityById(123); +``` + +### Find by Tag + +Entities support a tag system for quick categorization and lookup: + +```typescript +// Set tags +player.tag = 1; // Player tag +enemy.tag = 2; // Enemy tag + +// Find all entities by tag +const players = scene.findEntitiesByTag(1); +const enemies = scene.findEntitiesByTag(2); +// Or use alias method +const allPlayers = scene.getEntitiesByTag(1); +``` + +## Entity Lifecycle + +```typescript +// Destroy entity +player.destroy(); + +// Check if entity is destroyed +if (player.isDestroyed) { + console.log("Entity has been destroyed"); +} +``` + +## Entity Events + +Component changes on entities trigger events: + +```typescript +// Listen for component added event +scene.eventSystem.on('component:added', (data) => { + console.log('Component added:', data); +}); + +// Listen for entity created event +scene.eventSystem.on('entity:created', (data) => { + console.log('Entity created:', data.entityName); +}); +``` + +## Performance Optimization + +### Batch Entity Creation + +The framework provides high-performance batch creation methods: + +```typescript +// Batch create 100 bullet entities (high-performance version) +const bullets = scene.createEntities(100, "Bullet"); + +// Add components to each bullet +bullets.forEach((bullet, index) => { + bullet.addComponent(new Position(Math.random() * 800, Math.random() * 600)); + bullet.addComponent(new Velocity(Math.random() * 100 - 50, Math.random() * 100 - 50)); +}); +``` + +`createEntities()` method will: +- Batch allocate entity IDs +- Batch add to entity list +- Optimize query system updates +- Reduce system cache clearing times + +## Best Practices + +### 1. Appropriate Component Granularity + +```typescript +// Good practice: single-purpose components +@ECSComponent('Position') +class Position extends Component { + x: number = 0; + y: number = 0; +} + +@ECSComponent('Velocity') +class Velocity extends Component { + dx: number = 0; + dy: number = 0; +} + +// Avoid: overly complex components +@ECSComponent('Player') +class Player extends Component { + // Avoid including too many unrelated properties in one component + x: number; + y: number; + health: number; + inventory: Item[]; + skills: Skill[]; +} +``` + +### 2. Use Decorators + +Always use `@ECSComponent` decorator: + +```typescript +@ECSComponent('Transform') +class Transform extends Component { + // Component implementation +} +``` + +### 3. Proper Naming + +```typescript +// Clear entity naming +const mainCharacter = scene.createEntity("MainCharacter"); +const enemy1 = scene.createEntity("Goblin_001"); +const collectible = scene.createEntity("HealthPotion"); +``` + +### 4. Timely Cleanup + +```typescript +// Destroy entities that are no longer needed +if (enemy.getComponent(Health).current <= 0) { + enemy.destroy(); +} +``` + +## Debugging Entities + +The framework provides debugging features to help development: + +```typescript +// Get entity debug info +const debugInfo = entity.getDebugInfo(); +console.log('Entity info:', debugInfo); + +// List all components of entity +entity.components.forEach(component => { + console.log('Component:', component.constructor.name); +}); +``` + +Entities are one of the core concepts in ECS architecture. Understanding how to use entities correctly will help you build efficient, maintainable game code. + +## Entity Handle (EntityHandle) + +Entity handles provide a safe way to reference entities, solving the "referencing destroyed entity" problem. + +### Problem Scenario + +Suppose your AI system needs to track a target enemy: + +```typescript +// Wrong approach: directly store entity reference +class AISystem extends EntitySystem { + private targetEnemy: Entity | null = null; + + setTarget(enemy: Entity) { + this.targetEnemy = enemy; + } + + process() { + if (this.targetEnemy) { + // Dangerous! Enemy might be destroyed, but reference still exists + // Worse: this memory location might be reused by a new entity + const health = this.targetEnemy.getComponent(Health); + // Might operate on the wrong entity! + } + } +} +``` + +### Correct Approach Using Handles + +Each entity is automatically assigned a handle when created, accessible via `entity.handle`: + +```typescript +import { EntityHandle, NULL_HANDLE, isValidHandle } from '@esengine/ecs-framework'; + +class AISystem extends EntitySystem { + // Store handle instead of entity reference + private targetHandle: EntityHandle = NULL_HANDLE; + + setTarget(enemy: Entity) { + // Save enemy's handle + this.targetHandle = enemy.handle; + } + + process() { + if (!isValidHandle(this.targetHandle)) { + return; // No target + } + + // Get entity through handle (automatically checks validity) + const enemy = this.scene.findEntityByHandle(this.targetHandle); + + if (!enemy) { + // Enemy was destroyed, clear reference + this.targetHandle = NULL_HANDLE; + return; + } + + // Safe operation + const health = enemy.getComponent(Health); + } +} +``` + +### Complete Example: Skill Target Locking + +```typescript +import { + EntitySystem, Entity, EntityHandle, NULL_HANDLE, isValidHandle +} from '@esengine/ecs-framework'; + +@ECSSystem('SkillTargeting') +class SkillTargetingSystem extends EntitySystem { + // Store handles for multiple targets + private lockedTargets: Map = new Map(); + + // Lock target + lockTarget(caster: Entity, target: Entity) { + this.lockedTargets.set(caster, target.handle); + } + + // Get locked target + getLockedTarget(caster: Entity): Entity | null { + const handle = this.lockedTargets.get(caster); + + if (!handle || !isValidHandle(handle)) { + return null; + } + + // findEntityByHandle checks if handle is valid + const target = this.scene.findEntityByHandle(handle); + + if (!target) { + // Target died, clear lock + this.lockedTargets.delete(caster); + } + + return target; + } + + // Cast skill + castSkill(caster: Entity) { + const target = this.getLockedTarget(caster); + + if (!target) { + console.log('Target lost, skill cancelled'); + return; + } + + // Safely deal damage to target + const health = target.getComponent(Health); + if (health) { + health.current -= 10; + } + } +} +``` + +### Handle vs Entity Reference + +| Scenario | Recommended Approach | +|----------|---------------------| +| Temporary use within same frame | Use `Entity` reference directly | +| Cross-frame storage (e.g., AI target, skill target) | Use `EntityHandle` | +| Needs serialization | Use `EntityHandle` (numeric type) | +| Network synchronization | Use `EntityHandle` (can be transmitted directly) | + +### API Quick Reference + +```typescript +// Get entity's handle +const handle = entity.handle; + +// Check if handle is non-null +if (isValidHandle(handle)) { ... } + +// Get entity through handle (automatically checks validity) +const entity = scene.findEntityByHandle(handle); + +// Check if entity corresponding to handle is alive +const alive = scene.handleManager.isAlive(handle); + +// Null handle constant +const emptyHandle = NULL_HANDLE; +``` + +## Next Steps + +- Learn about [Hierarchy System](./hierarchy.md) to establish parent-child relationships +- Learn about [Component System](./component.md) to add functionality to entities +- Learn about [Scene Management](./scene.md) to organize and manage entities