docs: split Entity docs into sub-modules and fix Starlight CI (#362)
* docs: split Entity docs into sub-modules and fix Starlight CI - Split monolithic entity.md into 4 focused sub-documents: - guide/entity/index.md - Overview and basic concepts - guide/entity/component-operations.md - Component API operations - guide/entity/entity-handle.md - EntityHandle system for safe references - guide/entity/lifecycle.md - Lifecycle and persistence management - Created bilingual versions (Chinese and English) - Updated sidebar configuration in astro.config.mjs - Fixed CI workflow for Starlight migration: - Updated docs.yml to upload from docs/dist instead of .vitepress/dist - Updated package.json scripts to use pnpm filter for docs - Added docs directory to pnpm-workspace.yaml - Renamed docs package to @esengine/docs - Documented missing Entity APIs: - createComponent() method - addComponents() batch method - getComponentByType() with inheritance support - markDirty() for change detection * docs: split Network docs and fix API errors - Split network module into focused sub-documents: - modules/network/index.md - Overview and quick start - modules/network/client.md - Client-side usage - modules/network/server.md - Server-side GameServer/Room - modules/network/sync.md - Interpolation and prediction - modules/network/api.md - Complete API reference - Fixed incorrect API documentation: - localClientId → clientId - ENetworkState enum values (strings → numbers) - connect() method signature - Removed non-existent localPlayerId property - Fixed onConnected callback signature - Created bilingual versions (Chinese and English) - Updated sidebar configuration - Updated pnpm-lock.yaml for docs workspace
This commit is contained in:
2
.github/workflows/docs.yml
vendored
2
.github/workflows/docs.yml
vendored
@@ -56,7 +56,7 @@ jobs:
|
|||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-pages-artifact@v3
|
uses: actions/upload-pages-artifact@v3
|
||||||
with:
|
with:
|
||||||
path: docs/.vitepress/dist
|
path: docs/dist
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -39,7 +39,16 @@ export default defineConfig({
|
|||||||
label: '核心概念',
|
label: '核心概念',
|
||||||
translations: { en: 'Core Concepts' },
|
translations: { en: 'Core Concepts' },
|
||||||
items: [
|
items: [
|
||||||
{ label: '实体', slug: 'guide/entity', translations: { en: 'Entity' } },
|
{
|
||||||
|
label: '实体',
|
||||||
|
translations: { en: 'Entity' },
|
||||||
|
items: [
|
||||||
|
{ label: '概述', slug: 'guide/entity', translations: { en: 'Overview' } },
|
||||||
|
{ label: '组件操作', slug: 'guide/entity/component-operations', translations: { en: 'Component Operations' } },
|
||||||
|
{ label: '实体句柄', slug: 'guide/entity/entity-handle', translations: { en: 'Entity Handle' } },
|
||||||
|
{ label: '生命周期', slug: 'guide/entity/lifecycle', translations: { en: 'Lifecycle' } },
|
||||||
|
],
|
||||||
|
},
|
||||||
{ label: '层级结构', slug: 'guide/hierarchy', translations: { en: 'Hierarchy' } },
|
{ label: '层级结构', slug: 'guide/hierarchy', translations: { en: 'Hierarchy' } },
|
||||||
{
|
{
|
||||||
label: '组件',
|
label: '组件',
|
||||||
@@ -132,7 +141,17 @@ export default defineConfig({
|
|||||||
{ label: '寻路', slug: 'modules/pathfinding', translations: { en: 'Pathfinding' } },
|
{ label: '寻路', slug: 'modules/pathfinding', translations: { en: 'Pathfinding' } },
|
||||||
{ label: '蓝图', slug: 'modules/blueprint', translations: { en: 'Blueprint' } },
|
{ label: '蓝图', slug: 'modules/blueprint', translations: { en: 'Blueprint' } },
|
||||||
{ label: '程序生成', slug: 'modules/procgen', translations: { en: 'Procgen' } },
|
{ label: '程序生成', slug: 'modules/procgen', translations: { en: 'Procgen' } },
|
||||||
{ label: '网络', slug: 'modules/network', translations: { en: 'Network' } },
|
{
|
||||||
|
label: '网络',
|
||||||
|
translations: { en: 'Network' },
|
||||||
|
items: [
|
||||||
|
{ label: '概述', slug: 'modules/network', translations: { en: 'Overview' } },
|
||||||
|
{ label: '客户端', slug: 'modules/network/client', translations: { en: 'Client' } },
|
||||||
|
{ label: '服务器', slug: 'modules/network/server', translations: { en: 'Server' } },
|
||||||
|
{ label: '状态同步', slug: 'modules/network/sync', translations: { en: 'State Sync' } },
|
||||||
|
{ label: 'API 参考', slug: 'modules/network/api', translations: { en: 'API Reference' } },
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "docs-new",
|
"name": "@esengine/docs",
|
||||||
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,446 +0,0 @@
|
|||||||
---
|
|
||||||
title: "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/) 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<Entity, EntityHandle> = 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/) to establish parent-child relationships
|
|
||||||
- Learn about [Component System](./component/) to add functionality to entities
|
|
||||||
- Learn about [Scene Management](./scene/) to organize and manage entities
|
|
||||||
273
docs/src/content/docs/en/guide/entity/component-operations.md
Normal file
273
docs/src/content/docs/en/guide/entity/component-operations.md
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
---
|
||||||
|
title: "Component Operations"
|
||||||
|
description: "Detailed guide to adding, getting, and removing entity components"
|
||||||
|
---
|
||||||
|
|
||||||
|
Entities gain functionality by adding components. This section details all component operation APIs.
|
||||||
|
|
||||||
|
## Adding Components
|
||||||
|
|
||||||
|
### addComponent
|
||||||
|
|
||||||
|
Add an already-created component instance:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Component, ECSComponent } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const player = scene.createEntity("Player");
|
||||||
|
const position = new Position(100, 200);
|
||||||
|
player.addComponent(position);
|
||||||
|
```
|
||||||
|
|
||||||
|
### createComponent
|
||||||
|
|
||||||
|
Pass the component type and constructor arguments directly—the entity creates the instance (recommended):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Create and add component
|
||||||
|
const position = player.createComponent(Position, 100, 200);
|
||||||
|
const health = player.createComponent(Health, 150);
|
||||||
|
|
||||||
|
// Equivalent to:
|
||||||
|
// const position = new Position(100, 200);
|
||||||
|
// player.addComponent(position);
|
||||||
|
```
|
||||||
|
|
||||||
|
### addComponents
|
||||||
|
|
||||||
|
Add multiple components at once:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const components = player.addComponents([
|
||||||
|
new Position(100, 200),
|
||||||
|
new Health(150),
|
||||||
|
new Velocity(0, 0)
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
:::note[Important Notes]
|
||||||
|
- An entity cannot have two components of the same type—an exception will be thrown
|
||||||
|
- The entity must be added to a scene before adding components
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Getting Components
|
||||||
|
|
||||||
|
### getComponent
|
||||||
|
|
||||||
|
Get a component of a specific type:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Returns Position | null
|
||||||
|
const position = player.getComponent(Position);
|
||||||
|
|
||||||
|
if (position) {
|
||||||
|
position.x += 10;
|
||||||
|
position.y += 20;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### hasComponent
|
||||||
|
|
||||||
|
Check if an entity has a specific component type:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
if (player.hasComponent(Position)) {
|
||||||
|
const position = player.getComponent(Position)!;
|
||||||
|
// Use ! because we confirmed it exists
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### getComponents
|
||||||
|
|
||||||
|
Get all components of a specific type (for multi-component scenarios):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const allHealthComponents = player.getComponents(Health);
|
||||||
|
```
|
||||||
|
|
||||||
|
### getComponentByType
|
||||||
|
|
||||||
|
Get components with inheritance support using `instanceof` checking:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Find CompositeNodeComponent or any subclass
|
||||||
|
const composite = entity.getComponentByType(CompositeNodeComponent);
|
||||||
|
if (composite) {
|
||||||
|
// composite could be SequenceNode, SelectorNode, etc.
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Difference from `getComponent()`:
|
||||||
|
|
||||||
|
| Method | Lookup Method | Performance | Use Case |
|
||||||
|
|--------|---------------|-------------|----------|
|
||||||
|
| `getComponent` | Exact type match (bitmask) | High | Known exact type |
|
||||||
|
| `getComponentByType` | `instanceof` check | Lower | Need inheritance support |
|
||||||
|
|
||||||
|
### getOrCreateComponent
|
||||||
|
|
||||||
|
Get or create a component—automatically creates if it doesn't exist:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Ensure entity has Position component
|
||||||
|
const position = player.getOrCreateComponent(Position, 0, 0);
|
||||||
|
position.x = 100;
|
||||||
|
|
||||||
|
// If exists, returns existing component
|
||||||
|
// If not, creates new component with (0, 0) args
|
||||||
|
```
|
||||||
|
|
||||||
|
### components Property
|
||||||
|
|
||||||
|
Get all entity components (read-only):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const allComponents = player.components; // readonly Component[]
|
||||||
|
|
||||||
|
allComponents.forEach(component => {
|
||||||
|
console.log(component.constructor.name);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Removing Components
|
||||||
|
|
||||||
|
### removeComponent
|
||||||
|
|
||||||
|
Remove by component instance:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const healthComponent = player.getComponent(Health);
|
||||||
|
if (healthComponent) {
|
||||||
|
player.removeComponent(healthComponent);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### removeComponentByType
|
||||||
|
|
||||||
|
Remove by component type:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const removedHealth = player.removeComponentByType(Health);
|
||||||
|
if (removedHealth) {
|
||||||
|
console.log("Health component removed");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### removeComponentsByTypes
|
||||||
|
|
||||||
|
Remove multiple component types at once:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const removedComponents = player.removeComponentsByTypes([
|
||||||
|
Position,
|
||||||
|
Health,
|
||||||
|
Velocity
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### removeAllComponents
|
||||||
|
|
||||||
|
Remove all components:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
player.removeAllComponents();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Change Detection
|
||||||
|
|
||||||
|
### markDirty
|
||||||
|
|
||||||
|
Mark components as modified for frame-level change detection:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const pos = entity.getComponent(Position)!;
|
||||||
|
pos.x = 100;
|
||||||
|
entity.markDirty(pos);
|
||||||
|
|
||||||
|
// Or mark multiple components
|
||||||
|
const vel = entity.getComponent(Velocity)!;
|
||||||
|
entity.markDirty(pos, vel);
|
||||||
|
```
|
||||||
|
|
||||||
|
Use with reactive queries:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Query for components modified this frame
|
||||||
|
const changedQuery = scene.createReactiveQuery({
|
||||||
|
all: [Position],
|
||||||
|
changed: [Position] // Only match modified this frame
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const entity of changedQuery.getEntities()) {
|
||||||
|
// Handle entities with position changes
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Component Mask
|
||||||
|
|
||||||
|
Each entity maintains a component bitmask for efficient `hasComponent` checks:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Get component mask (internal use)
|
||||||
|
const mask = entity.componentMask;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Complete Example
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Component, ECSComponent, Scene } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
@ECSComponent('Position')
|
||||||
|
class Position extends Component {
|
||||||
|
constructor(public x = 0, public y = 0) { super(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@ECSComponent('Health')
|
||||||
|
class Health extends Component {
|
||||||
|
constructor(public current = 100, public max = 100) { super(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create entity and add components
|
||||||
|
const player = scene.createEntity("Player");
|
||||||
|
player.createComponent(Position, 100, 200);
|
||||||
|
player.createComponent(Health, 150, 150);
|
||||||
|
|
||||||
|
// Get and modify component
|
||||||
|
const position = player.getComponent(Position);
|
||||||
|
if (position) {
|
||||||
|
position.x += 10;
|
||||||
|
player.markDirty(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get or create component
|
||||||
|
const velocity = player.getOrCreateComponent(Velocity, 0, 0);
|
||||||
|
|
||||||
|
// Check component existence
|
||||||
|
if (player.hasComponent(Health)) {
|
||||||
|
const health = player.getComponent(Health)!;
|
||||||
|
health.current -= 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove component
|
||||||
|
player.removeComponentByType(Velocity);
|
||||||
|
|
||||||
|
// List all components
|
||||||
|
console.log(player.components.map(c => c.constructor.name));
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- [Entity Handle](/en/guide/entity/entity-handle/) - Safe cross-frame entity references
|
||||||
|
- [Component System](/en/guide/component/) - Component definition and lifecycle
|
||||||
265
docs/src/content/docs/en/guide/entity/entity-handle.md
Normal file
265
docs/src/content/docs/en/guide/entity/entity-handle.md
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
---
|
||||||
|
title: "Entity Handle"
|
||||||
|
description: "Using EntityHandle to safely reference entities and avoid referencing destroyed entities"
|
||||||
|
---
|
||||||
|
|
||||||
|
Entity handles (EntityHandle) provide a safe way to reference entities, solving the "referencing destroyed entities" problem.
|
||||||
|
|
||||||
|
## The Problem
|
||||||
|
|
||||||
|
Imagine your AI system needs to track a target enemy:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ Wrong: Storing entity reference directly
|
||||||
|
class AISystem extends EntitySystem {
|
||||||
|
private targetEnemy: Entity | null = null;
|
||||||
|
|
||||||
|
setTarget(enemy: Entity) {
|
||||||
|
this.targetEnemy = enemy;
|
||||||
|
}
|
||||||
|
|
||||||
|
process() {
|
||||||
|
if (this.targetEnemy) {
|
||||||
|
// Danger! 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 wrong entity!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## What is EntityHandle
|
||||||
|
|
||||||
|
EntityHandle is a numeric entity identifier containing:
|
||||||
|
- **Index**: Entity's position in the array
|
||||||
|
- **Generation**: Number of times the entity slot has been reused
|
||||||
|
|
||||||
|
When an entity is destroyed, even if its index is reused by a new entity, the generation increases, invalidating old handles.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { EntityHandle, NULL_HANDLE, isValidHandle } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
// Each entity gets a handle when created
|
||||||
|
const handle: EntityHandle = entity.handle;
|
||||||
|
|
||||||
|
// Null handle constant
|
||||||
|
const emptyHandle = NULL_HANDLE;
|
||||||
|
|
||||||
|
// Check if handle is non-null
|
||||||
|
if (isValidHandle(handle)) {
|
||||||
|
// Handle is valid
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## The Correct Approach
|
||||||
|
|
||||||
|
```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) {
|
||||||
|
this.targetHandle = enemy.handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
process() {
|
||||||
|
if (!isValidHandle(this.targetHandle)) {
|
||||||
|
return; // No target
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get entity via handle (auto-validates)
|
||||||
|
const enemy = this.scene.findEntityByHandle(this.targetHandle);
|
||||||
|
|
||||||
|
if (!enemy) {
|
||||||
|
// Enemy destroyed, clear reference
|
||||||
|
this.targetHandle = NULL_HANDLE;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safe operation
|
||||||
|
const health = enemy.getComponent(Health);
|
||||||
|
if (health) {
|
||||||
|
// Deal damage to enemy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### Getting Handle
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Get handle from entity
|
||||||
|
const handle = entity.handle;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Validating Handle
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { isValidHandle, NULL_HANDLE } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
// Check if handle is non-null
|
||||||
|
if (isValidHandle(handle)) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if entity is alive
|
||||||
|
const alive = scene.handleManager.isAlive(handle);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Getting Entity by Handle
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Returns Entity | null
|
||||||
|
const entity = scene.findEntityByHandle(handle);
|
||||||
|
|
||||||
|
if (entity) {
|
||||||
|
// Entity exists and is valid
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Complete Example: Skill Target Locking
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
EntitySystem,
|
||||||
|
Entity,
|
||||||
|
EntityHandle,
|
||||||
|
NULL_HANDLE,
|
||||||
|
isValidHandle
|
||||||
|
} from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
@ECSSystem('SkillTargeting')
|
||||||
|
class SkillTargetingSystem extends EntitySystem {
|
||||||
|
// Store multiple target handles
|
||||||
|
private lockedTargets: Map<number, EntityHandle> = new Map();
|
||||||
|
|
||||||
|
// Lock target
|
||||||
|
lockTarget(casterId: number, target: Entity) {
|
||||||
|
this.lockedTargets.set(casterId, target.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get locked target
|
||||||
|
getLockedTarget(casterId: number): Entity | null {
|
||||||
|
const handle = this.lockedTargets.get(casterId);
|
||||||
|
|
||||||
|
if (!handle || !isValidHandle(handle)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = this.scene.findEntityByHandle(handle);
|
||||||
|
|
||||||
|
if (!target) {
|
||||||
|
// Target dead, clear lock
|
||||||
|
this.lockedTargets.delete(casterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cast skill
|
||||||
|
castSkill(caster: Entity) {
|
||||||
|
const target = this.getLockedTarget(caster.id);
|
||||||
|
|
||||||
|
if (!target) {
|
||||||
|
console.log('Target lost, skill cancelled');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const health = target.getComponent(Health);
|
||||||
|
if (health) {
|
||||||
|
health.current -= 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear target for specific caster
|
||||||
|
clearTarget(casterId: number) {
|
||||||
|
this.lockedTargets.delete(casterId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage Guidelines
|
||||||
|
|
||||||
|
| Scenario | Recommended Approach |
|
||||||
|
|----------|---------------------|
|
||||||
|
| Same-frame temporary use | Direct `Entity` reference |
|
||||||
|
| Cross-frame storage (AI target, skill target) | Use `EntityHandle` |
|
||||||
|
| Serialization/save | Use `EntityHandle` (numeric type) |
|
||||||
|
| Network sync | Use `EntityHandle` (directly transferable) |
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
- EntityHandle is a numeric type with small memory footprint
|
||||||
|
- `findEntityByHandle` is O(1) operation
|
||||||
|
- Safer and more reliable than checking `entity.isDestroyed` every frame
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Optional Target Reference
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class FollowComponent extends Component {
|
||||||
|
private _targetHandle: EntityHandle = NULL_HANDLE;
|
||||||
|
|
||||||
|
setTarget(target: Entity | null) {
|
||||||
|
this._targetHandle = target?.handle ?? NULL_HANDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTarget(scene: IScene): Entity | null {
|
||||||
|
if (!isValidHandle(this._targetHandle)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return scene.findEntityByHandle(this._targetHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
hasTarget(): boolean {
|
||||||
|
return isValidHandle(this._targetHandle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multi-Target Tracking
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class MultiTargetComponent extends Component {
|
||||||
|
private targets: EntityHandle[] = [];
|
||||||
|
|
||||||
|
addTarget(target: Entity) {
|
||||||
|
this.targets.push(target.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeTarget(target: Entity) {
|
||||||
|
const index = this.targets.indexOf(target.handle);
|
||||||
|
if (index >= 0) {
|
||||||
|
this.targets.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getValidTargets(scene: IScene): Entity[] {
|
||||||
|
const valid: Entity[] = [];
|
||||||
|
const stillValid: EntityHandle[] = [];
|
||||||
|
|
||||||
|
for (const handle of this.targets) {
|
||||||
|
const entity = scene.findEntityByHandle(handle);
|
||||||
|
if (entity) {
|
||||||
|
valid.push(entity);
|
||||||
|
stillValid.push(handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up invalid handles
|
||||||
|
this.targets = stillValid;
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- [Lifecycle](/en/guide/entity/lifecycle/) - Entity destruction and persistence
|
||||||
|
- [Entity Reference](/en/guide/component/entity-ref/) - Entity reference decorators in components
|
||||||
174
docs/src/content/docs/en/guide/entity/index.md
Normal file
174
docs/src/content/docs/en/guide/entity/index.md
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
---
|
||||||
|
title: "Entity Overview"
|
||||||
|
description: "Basic concepts and usage of entities in ECS architecture"
|
||||||
|
---
|
||||||
|
|
||||||
|
In ECS architecture, an Entity is a fundamental object in the game world. Entities contain no game logic or data themselves—they are simply containers that combine different components to achieve various functionalities.
|
||||||
|
|
||||||
|
## Basic Concepts
|
||||||
|
|
||||||
|
An entity is a lightweight object primarily used for:
|
||||||
|
- Acting as a container for components
|
||||||
|
- Providing unique identifiers (ID and persistentId)
|
||||||
|
- Managing component lifecycles
|
||||||
|
|
||||||
|
:::tip[About Parent-Child Hierarchy]
|
||||||
|
Parent-child 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 the [Hierarchy System](/en/guide/hierarchy/) documentation for details.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Creating Entities
|
||||||
|
|
||||||
|
**Entities must be created through the scene, not manually.**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Correct: Create entity through scene
|
||||||
|
const player = scene.createEntity("Player");
|
||||||
|
|
||||||
|
// ❌ Wrong: Manual creation
|
||||||
|
// const entity = new Entity("MyEntity", 1);
|
||||||
|
```
|
||||||
|
|
||||||
|
Creating through the scene ensures:
|
||||||
|
- Entity is properly added to the scene's entity management system
|
||||||
|
- Entity is added to the query system for use by systems
|
||||||
|
- Entity gets the correct scene reference
|
||||||
|
- Related lifecycle events are triggered
|
||||||
|
|
||||||
|
### Batch Creation
|
||||||
|
|
||||||
|
The framework provides high-performance batch creation:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Batch create 100 bullet entities
|
||||||
|
const bullets = scene.createEntities(100, "Bullet");
|
||||||
|
|
||||||
|
bullets.forEach((bullet, index) => {
|
||||||
|
bullet.createComponent(Position, Math.random() * 800, Math.random() * 600);
|
||||||
|
bullet.createComponent(Velocity, Math.random() * 100, Math.random() * 100);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
`createEntities()` batches ID allocation, optimizes query system updates, and reduces system cache clearing.
|
||||||
|
|
||||||
|
## Entity Identifiers
|
||||||
|
|
||||||
|
Each entity has three types of identifiers:
|
||||||
|
|
||||||
|
| Property | Type | Description |
|
||||||
|
|----------|------|-------------|
|
||||||
|
| `id` | `number` | Runtime unique identifier for fast lookups |
|
||||||
|
| `persistentId` | `string` | GUID for maintaining reference consistency during serialization |
|
||||||
|
| `handle` | `EntityHandle` | Lightweight handle, see [Entity Handle](/en/guide/entity/entity-handle/) |
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const entity = scene.createEntity("Player");
|
||||||
|
|
||||||
|
console.log(entity.id); // 1
|
||||||
|
console.log(entity.persistentId); // "a1b2c3d4-..."
|
||||||
|
console.log(entity.handle); // Numeric handle
|
||||||
|
```
|
||||||
|
|
||||||
|
## Entity Properties
|
||||||
|
|
||||||
|
### Name and Tag
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Name - for debugging and lookup
|
||||||
|
entity.name = "Player";
|
||||||
|
|
||||||
|
// Tag - for fast categorization and querying
|
||||||
|
entity.tag = 1; // Player tag
|
||||||
|
enemy.tag = 2; // Enemy tag
|
||||||
|
```
|
||||||
|
|
||||||
|
### State Control
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Enable/disable state
|
||||||
|
entity.enabled = false;
|
||||||
|
|
||||||
|
// Active state
|
||||||
|
entity.active = false;
|
||||||
|
|
||||||
|
// Update order (lower values have higher priority)
|
||||||
|
entity.updateOrder = 10;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Finding Entities
|
||||||
|
|
||||||
|
The scene provides multiple ways to find entities:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Find by name
|
||||||
|
const player = scene.findEntity("Player");
|
||||||
|
// Or use alias
|
||||||
|
const player2 = scene.getEntityByName("Player");
|
||||||
|
|
||||||
|
// Find by ID
|
||||||
|
const entity = scene.findEntityById(123);
|
||||||
|
|
||||||
|
// Find all entities by tag
|
||||||
|
const enemies = scene.findEntitiesByTag(2);
|
||||||
|
// Or use alias
|
||||||
|
const allEnemies = scene.getEntitiesByTag(2);
|
||||||
|
|
||||||
|
// Find by handle
|
||||||
|
const entity = scene.findEntityByHandle(handle);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Entity Events
|
||||||
|
|
||||||
|
Entity changes trigger events:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Listen for component additions
|
||||||
|
scene.eventSystem.on('component:added', (data) => {
|
||||||
|
console.log(`${data.entityName} added ${data.componentType}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for component removals
|
||||||
|
scene.eventSystem.on('component:removed', (data) => {
|
||||||
|
console.log(`${data.entityName} removed ${data.componentType}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for entity creation
|
||||||
|
scene.eventSystem.on('entity:created', (data) => {
|
||||||
|
console.log(`Entity created: ${data.entityName}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for active state changes
|
||||||
|
scene.eventSystem.on('entity:activeChanged', (data) => {
|
||||||
|
console.log(`${data.entity.name} active: ${data.active}`);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Debugging
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Get entity debug info
|
||||||
|
const debugInfo = entity.getDebugInfo();
|
||||||
|
console.log(debugInfo);
|
||||||
|
// {
|
||||||
|
// name: "Player",
|
||||||
|
// id: 1,
|
||||||
|
// persistentId: "a1b2c3d4-...",
|
||||||
|
// enabled: true,
|
||||||
|
// active: true,
|
||||||
|
// destroyed: false,
|
||||||
|
// componentCount: 3,
|
||||||
|
// componentTypes: ["Position", "Health", "Velocity"],
|
||||||
|
// ...
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Entity string representation
|
||||||
|
console.log(entity.toString());
|
||||||
|
// "Entity[Player:1:a1b2c3d4]"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- [Component Operations](/en/guide/entity/component-operations/) - Add, get, and remove components
|
||||||
|
- [Entity Handle](/en/guide/entity/entity-handle/) - Safe entity reference method
|
||||||
|
- [Lifecycle](/en/guide/entity/lifecycle/) - Destruction and persistence
|
||||||
238
docs/src/content/docs/en/guide/entity/lifecycle.md
Normal file
238
docs/src/content/docs/en/guide/entity/lifecycle.md
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
---
|
||||||
|
title: "Lifecycle"
|
||||||
|
description: "Entity lifecycle management, destruction, and persistence"
|
||||||
|
---
|
||||||
|
|
||||||
|
Entity lifecycle includes three phases: creation, runtime, and destruction. This section covers how to properly manage entity lifecycles.
|
||||||
|
|
||||||
|
## Destroying Entities
|
||||||
|
|
||||||
|
### Basic Destruction
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Destroy entity
|
||||||
|
player.destroy();
|
||||||
|
|
||||||
|
// Check if entity is destroyed
|
||||||
|
if (player.isDestroyed) {
|
||||||
|
console.log("Entity has been destroyed");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When destroying an entity:
|
||||||
|
1. All components are removed (triggering `onRemovedFromEntity` callbacks)
|
||||||
|
2. Entity is removed from query systems
|
||||||
|
3. Entity is removed from scene entity list
|
||||||
|
4. All reference tracking is cleaned up
|
||||||
|
|
||||||
|
### Conditional Destruction
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Common pattern: Destroy when health depleted
|
||||||
|
const health = enemy.getComponent(Health);
|
||||||
|
if (health && health.current <= 0) {
|
||||||
|
enemy.destroy();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Destruction Safety
|
||||||
|
|
||||||
|
Destruction is idempotent—multiple calls won't cause errors:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
player.destroy();
|
||||||
|
player.destroy(); // Safe, no error
|
||||||
|
```
|
||||||
|
|
||||||
|
## Persistent Entities
|
||||||
|
|
||||||
|
By default, entities are destroyed during scene transitions. Persistence allows entities to survive across scenes.
|
||||||
|
|
||||||
|
### Setting Persistence
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Method 1: Chain call
|
||||||
|
const player = scene.createEntity('Player')
|
||||||
|
.setPersistent()
|
||||||
|
.createComponent(PlayerComponent);
|
||||||
|
|
||||||
|
// Method 2: Separate call
|
||||||
|
player.setPersistent();
|
||||||
|
|
||||||
|
// Check persistence
|
||||||
|
if (player.isPersistent) {
|
||||||
|
console.log("This is a persistent entity");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Removing Persistence
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Restore to scene-local entity
|
||||||
|
player.setSceneLocal();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lifecycle Policies
|
||||||
|
|
||||||
|
Entities have two lifecycle policies:
|
||||||
|
|
||||||
|
| Policy | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `SceneLocal` | Default, destroyed with scene |
|
||||||
|
| `Persistent` | Survives scene transitions |
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { EEntityLifecyclePolicy } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
// Get current policy
|
||||||
|
const policy = entity.lifecyclePolicy;
|
||||||
|
|
||||||
|
if (policy === EEntityLifecyclePolicy.Persistent) {
|
||||||
|
// Persistent entity
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Use Cases
|
||||||
|
|
||||||
|
Persistent entities are suitable for:
|
||||||
|
- Player characters
|
||||||
|
- Global managers
|
||||||
|
- UI entities
|
||||||
|
- Game state that needs to survive scene transitions
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Player character
|
||||||
|
const player = scene.createEntity('Player')
|
||||||
|
.setPersistent();
|
||||||
|
|
||||||
|
// Game manager
|
||||||
|
const gameManager = scene.createEntity('GameManager')
|
||||||
|
.setPersistent()
|
||||||
|
.createComponent(GameStateComponent);
|
||||||
|
|
||||||
|
// Score manager
|
||||||
|
const scoreManager = scene.createEntity('ScoreManager')
|
||||||
|
.setPersistent()
|
||||||
|
.createComponent(ScoreComponent);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Scene Transition Behavior
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Scene manager switches scenes
|
||||||
|
sceneManager.loadScene('Level2');
|
||||||
|
|
||||||
|
// During transition:
|
||||||
|
// 1. SceneLocal entities are destroyed
|
||||||
|
// 2. Persistent entities migrate to new scene
|
||||||
|
// 3. New scene entities are created
|
||||||
|
```
|
||||||
|
|
||||||
|
:::caution[Note]
|
||||||
|
Persistent entities automatically migrate to the new scene during transitions, but other non-persistent entities they reference may be destroyed. Use [EntityHandle](/en/guide/entity/entity-handle/) to safely handle this situation.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Reference Cleanup
|
||||||
|
|
||||||
|
The framework provides reference tracking that automatically cleans up references when entities are destroyed:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Reference tracker cleans up all references to this entity on destruction
|
||||||
|
scene.referenceTracker?.clearReferencesTo(entity.id);
|
||||||
|
```
|
||||||
|
|
||||||
|
Using the `@entityRef` decorator handles this automatically:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class FollowComponent extends Component {
|
||||||
|
@entityRef()
|
||||||
|
targetId: number | null = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// When target is destroyed, targetId is automatically set to null
|
||||||
|
```
|
||||||
|
|
||||||
|
See [Component References](/en/guide/component/entity-ref/) for details.
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### 1. Destroy Unneeded Entities Promptly
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Destroy bullets that fly off screen
|
||||||
|
if (position.x < 0 || position.x > screenWidth) {
|
||||||
|
bullet.destroy();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Use Object Pools Instead of Frequent Create/Destroy
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class BulletPool {
|
||||||
|
private pool: Entity[] = [];
|
||||||
|
|
||||||
|
acquire(scene: Scene): Entity {
|
||||||
|
if (this.pool.length > 0) {
|
||||||
|
const bullet = this.pool.pop()!;
|
||||||
|
bullet.enabled = true;
|
||||||
|
return bullet;
|
||||||
|
}
|
||||||
|
return scene.createEntity('Bullet');
|
||||||
|
}
|
||||||
|
|
||||||
|
release(bullet: Entity) {
|
||||||
|
bullet.enabled = false;
|
||||||
|
this.pool.push(bullet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Use Persistence Sparingly
|
||||||
|
|
||||||
|
Only use persistence for entities that truly need to survive scene transitions—too many persistent entities increase memory usage.
|
||||||
|
|
||||||
|
### 4. Clean Up References Before Destruction
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Notify related systems before destruction
|
||||||
|
const aiSystem = scene.getSystem(AISystem);
|
||||||
|
aiSystem?.clearTarget(enemy.id);
|
||||||
|
|
||||||
|
enemy.destroy();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lifecycle Events
|
||||||
|
|
||||||
|
You can listen to entity destruction events:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Method 1: Through event system
|
||||||
|
scene.eventSystem.on('entity:destroyed', (data) => {
|
||||||
|
console.log(`Entity ${data.entityName} destroyed`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Method 2: In component
|
||||||
|
class MyComponent extends Component {
|
||||||
|
onRemovedFromEntity() {
|
||||||
|
console.log('Component removed, entity may be destroying');
|
||||||
|
// Clean up resources
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Debugging
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Get entity status
|
||||||
|
const debugInfo = entity.getDebugInfo();
|
||||||
|
console.log({
|
||||||
|
destroyed: debugInfo.destroyed,
|
||||||
|
enabled: debugInfo.enabled,
|
||||||
|
active: debugInfo.active
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- [Component Operations](/en/guide/entity/component-operations/) - Adding and removing components
|
||||||
|
- [Scene Management](/en/guide/scene/) - Scene switching and management
|
||||||
111
docs/src/content/docs/en/modules/network/api.md
Normal file
111
docs/src/content/docs/en/modules/network/api.md
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
---
|
||||||
|
title: "API Reference"
|
||||||
|
description: "Complete Network module API documentation"
|
||||||
|
---
|
||||||
|
|
||||||
|
## NetworkPlugin
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class NetworkPlugin implements IPlugin {
|
||||||
|
readonly name: string;
|
||||||
|
readonly version: string;
|
||||||
|
|
||||||
|
get networkService(): NetworkService;
|
||||||
|
get syncSystem(): NetworkSyncSystem;
|
||||||
|
get spawnSystem(): NetworkSpawnSystem;
|
||||||
|
get inputSystem(): NetworkInputSystem;
|
||||||
|
get isConnected(): boolean;
|
||||||
|
|
||||||
|
connect(serverUrl: string, playerName: string, roomId?: string): Promise<boolean>;
|
||||||
|
disconnect(): Promise<void>;
|
||||||
|
registerPrefab(prefabType: string, factory: PrefabFactory): void;
|
||||||
|
sendMoveInput(x: number, y: number): void;
|
||||||
|
sendActionInput(action: string): void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## NetworkService
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class NetworkService {
|
||||||
|
get state(): ENetworkState;
|
||||||
|
get isConnected(): boolean;
|
||||||
|
get clientId(): number;
|
||||||
|
get roomId(): string;
|
||||||
|
|
||||||
|
connect(serverUrl: string, playerName: string, roomId?: string): Promise<boolean>;
|
||||||
|
disconnect(): Promise<void>;
|
||||||
|
sendInput(input: IPlayerInput): void;
|
||||||
|
setCallbacks(callbacks: INetworkCallbacks): void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Enums
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const enum ENetworkState {
|
||||||
|
Disconnected = 0,
|
||||||
|
Connecting = 1,
|
||||||
|
Connected = 2
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Interfaces
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface INetworkCallbacks {
|
||||||
|
onConnected?: (clientId: number, roomId: string) => void;
|
||||||
|
onDisconnected?: () => void;
|
||||||
|
onSync?: (msg: MsgSync) => void;
|
||||||
|
onSpawn?: (msg: MsgSpawn) => void;
|
||||||
|
onDespawn?: (msg: MsgDespawn) => void;
|
||||||
|
onError?: (error: Error) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type PrefabFactory = (scene: Scene, spawn: MsgSpawn) => Entity;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class NetworkIdentity extends Component {
|
||||||
|
netId: number;
|
||||||
|
ownerId: number;
|
||||||
|
bIsLocalPlayer: boolean;
|
||||||
|
bHasAuthority: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class NetworkTransform extends Component {
|
||||||
|
position: { x: number; y: number };
|
||||||
|
rotation: number;
|
||||||
|
velocity: { x: number; y: number };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Service Tokens
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
NetworkServiceToken,
|
||||||
|
NetworkSyncSystemToken,
|
||||||
|
NetworkSpawnSystemToken,
|
||||||
|
NetworkInputSystemToken
|
||||||
|
} from '@esengine/network';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Server API
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class GameServer {
|
||||||
|
start(): Promise<void>;
|
||||||
|
stop(): Promise<void>;
|
||||||
|
getOrCreateRoom(roomId: string): Room;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Room {
|
||||||
|
readonly id: string;
|
||||||
|
readonly playerCount: number;
|
||||||
|
addPlayer(name: string, connection: Connection): IPlayer | null;
|
||||||
|
removePlayer(clientId: number): void;
|
||||||
|
}
|
||||||
|
```
|
||||||
141
docs/src/content/docs/en/modules/network/client.md
Normal file
141
docs/src/content/docs/en/modules/network/client.md
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
---
|
||||||
|
title: "Client Usage"
|
||||||
|
description: "NetworkPlugin, components and systems client-side guide"
|
||||||
|
---
|
||||||
|
|
||||||
|
## NetworkPlugin
|
||||||
|
|
||||||
|
NetworkPlugin is the core entry point for client-side networking.
|
||||||
|
|
||||||
|
### Basic Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Core } from '@esengine/ecs-framework';
|
||||||
|
import { NetworkPlugin } from '@esengine/network';
|
||||||
|
|
||||||
|
// Create and install plugin
|
||||||
|
const networkPlugin = new NetworkPlugin();
|
||||||
|
await Core.installPlugin(networkPlugin);
|
||||||
|
|
||||||
|
// Connect to server
|
||||||
|
const success = await networkPlugin.connect('ws://localhost:3000', 'PlayerName');
|
||||||
|
|
||||||
|
// Disconnect
|
||||||
|
await networkPlugin.disconnect();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Properties and Methods
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class NetworkPlugin {
|
||||||
|
readonly name: string;
|
||||||
|
readonly version: string;
|
||||||
|
|
||||||
|
// Accessors
|
||||||
|
get networkService(): NetworkService;
|
||||||
|
get syncSystem(): NetworkSyncSystem;
|
||||||
|
get spawnSystem(): NetworkSpawnSystem;
|
||||||
|
get inputSystem(): NetworkInputSystem;
|
||||||
|
get isConnected(): boolean;
|
||||||
|
|
||||||
|
// Connect to server
|
||||||
|
connect(serverUrl: string, playerName: string, roomId?: string): Promise<boolean>;
|
||||||
|
|
||||||
|
// Disconnect
|
||||||
|
disconnect(): Promise<void>;
|
||||||
|
|
||||||
|
// Register prefab factory
|
||||||
|
registerPrefab(prefabType: string, factory: PrefabFactory): void;
|
||||||
|
|
||||||
|
// Send input
|
||||||
|
sendMoveInput(x: number, y: number): void;
|
||||||
|
sendActionInput(action: string): void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
### NetworkIdentity
|
||||||
|
|
||||||
|
Network identity component, required for every networked entity:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class NetworkIdentity extends Component {
|
||||||
|
netId: number; // Network unique ID
|
||||||
|
ownerId: number; // Owner client ID
|
||||||
|
bIsLocalPlayer: boolean; // Whether local player
|
||||||
|
bHasAuthority: boolean; // Whether has control authority
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### NetworkTransform
|
||||||
|
|
||||||
|
Network transform component for position and rotation sync:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class NetworkTransform extends Component {
|
||||||
|
position: { x: number; y: number };
|
||||||
|
rotation: number;
|
||||||
|
velocity: { x: number; y: number };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Systems
|
||||||
|
|
||||||
|
### NetworkSyncSystem
|
||||||
|
|
||||||
|
Handles server state synchronization and interpolation.
|
||||||
|
|
||||||
|
### NetworkSpawnSystem
|
||||||
|
|
||||||
|
Handles network entity spawning and despawning.
|
||||||
|
|
||||||
|
### NetworkInputSystem
|
||||||
|
|
||||||
|
Handles local player input sending:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Via NetworkPlugin (recommended)
|
||||||
|
networkPlugin.sendMoveInput(0, 1);
|
||||||
|
networkPlugin.sendActionInput('jump');
|
||||||
|
|
||||||
|
// Or use inputSystem directly
|
||||||
|
networkPlugin.inputSystem.addMoveInput(0, 1);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prefab Factory
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
networkPlugin.registerPrefab('player', (scene, spawn) => {
|
||||||
|
const entity = scene.createEntity(`player_${spawn.netId}`);
|
||||||
|
|
||||||
|
const identity = entity.addComponent(new NetworkIdentity());
|
||||||
|
identity.netId = spawn.netId;
|
||||||
|
identity.ownerId = spawn.ownerId;
|
||||||
|
identity.bIsLocalPlayer = spawn.ownerId === networkPlugin.networkService.clientId;
|
||||||
|
|
||||||
|
entity.addComponent(new NetworkTransform());
|
||||||
|
|
||||||
|
if (identity.bIsLocalPlayer) {
|
||||||
|
entity.addComponent(new LocalInputComponent());
|
||||||
|
}
|
||||||
|
|
||||||
|
return entity;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Connection State Monitoring
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
networkPlugin.networkService.setCallbacks({
|
||||||
|
onConnected: (clientId, roomId) => {
|
||||||
|
console.log(`Connected: client ${clientId}, room ${roomId}`);
|
||||||
|
},
|
||||||
|
onDisconnected: () => {
|
||||||
|
console.log('Disconnected');
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('Network error:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "Network System"
|
title: "Network System"
|
||||||
|
description: "TSRPC-based multiplayer game network synchronization solution"
|
||||||
---
|
---
|
||||||
|
|
||||||
`@esengine/network` provides a TSRPC-based client-server network synchronization solution for multiplayer games, including entity synchronization, input handling, and state interpolation.
|
`@esengine/network` provides a TSRPC-based client-server network synchronization solution for multiplayer games, including entity synchronization, input handling, and state interpolation.
|
||||||
@@ -24,49 +25,17 @@ npm install @esengine/network
|
|||||||
npm install @esengine/network-server
|
npm install @esengine/network-server
|
||||||
```
|
```
|
||||||
|
|
||||||
## Quick Setup with CLI
|
## Architecture
|
||||||
|
|
||||||
We recommend using ESEngine CLI to quickly create a complete game server project:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Create project directory
|
|
||||||
mkdir my-game-server && cd my-game-server
|
|
||||||
npm init -y
|
|
||||||
|
|
||||||
# Initialize Node.js server with CLI
|
|
||||||
npx @esengine/cli init -p nodejs
|
|
||||||
```
|
|
||||||
|
|
||||||
The CLI will generate the following project structure:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
my-game-server/
|
Client Server
|
||||||
├── src/
|
┌────────────────┐ ┌────────────────┐
|
||||||
│ ├── index.ts # Entry point
|
│ NetworkPlugin │◄──── WS ────► │ GameServer │
|
||||||
│ ├── server/
|
│ ├─ Service │ │ ├─ Room │
|
||||||
│ │ └── GameServer.ts # Network server configuration
|
│ ├─ SyncSystem │ │ └─ Players │
|
||||||
│ └── game/
|
│ ├─ SpawnSystem │ └────────────────┘
|
||||||
│ ├── Game.ts # ECS game class
|
│ └─ InputSystem │
|
||||||
│ ├── scenes/
|
└────────────────┘
|
||||||
│ │ └── MainScene.ts # Main scene
|
|
||||||
│ ├── components/ # ECS components
|
|
||||||
│ │ ├── PositionComponent.ts
|
|
||||||
│ │ └── VelocityComponent.ts
|
|
||||||
│ └── systems/ # ECS systems
|
|
||||||
│ └── MovementSystem.ts
|
|
||||||
├── tsconfig.json
|
|
||||||
├── package.json
|
|
||||||
└── README.md
|
|
||||||
```
|
|
||||||
|
|
||||||
Start the server:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Development mode (hot reload)
|
|
||||||
npm run dev
|
|
||||||
|
|
||||||
# Production mode
|
|
||||||
npm run start
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
@@ -75,21 +44,16 @@ npm run start
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Core, Scene } from '@esengine/ecs-framework';
|
import { Core, Scene } from '@esengine/ecs-framework';
|
||||||
import {
|
import { NetworkPlugin, NetworkIdentity, NetworkTransform } from '@esengine/network';
|
||||||
NetworkPlugin,
|
|
||||||
NetworkIdentity,
|
|
||||||
NetworkTransform
|
|
||||||
} from '@esengine/network';
|
|
||||||
|
|
||||||
// Define game scene
|
// Define game scene
|
||||||
class GameScene extends Scene {
|
class GameScene extends Scene {
|
||||||
initialize(): void {
|
initialize(): void {
|
||||||
this.name = 'Game';
|
this.name = 'Game';
|
||||||
// Network systems are automatically added by NetworkPlugin
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize Core
|
// Initialize
|
||||||
Core.create({ debug: false });
|
Core.create({ debug: false });
|
||||||
const scene = new GameScene();
|
const scene = new GameScene();
|
||||||
Core.setScene(scene);
|
Core.setScene(scene);
|
||||||
@@ -105,7 +69,7 @@ networkPlugin.registerPrefab('player', (scene, spawn) => {
|
|||||||
const identity = entity.addComponent(new NetworkIdentity());
|
const identity = entity.addComponent(new NetworkIdentity());
|
||||||
identity.netId = spawn.netId;
|
identity.netId = spawn.netId;
|
||||||
identity.ownerId = spawn.ownerId;
|
identity.ownerId = spawn.ownerId;
|
||||||
identity.isLocalPlayer = spawn.ownerId === networkPlugin.networkService.localClientId;
|
identity.bIsLocalPlayer = spawn.ownerId === networkPlugin.networkService.clientId;
|
||||||
|
|
||||||
entity.addComponent(new NetworkTransform());
|
entity.addComponent(new NetworkTransform());
|
||||||
return entity;
|
return entity;
|
||||||
@@ -116,20 +80,10 @@ const success = await networkPlugin.connect('ws://localhost:3000', 'PlayerName')
|
|||||||
if (success) {
|
if (success) {
|
||||||
console.log('Connected!');
|
console.log('Connected!');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Game loop
|
|
||||||
function gameLoop(dt: number) {
|
|
||||||
Core.update(dt);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disconnect
|
|
||||||
await networkPlugin.disconnect();
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
|
|
||||||
After creating a server project with CLI, the generated code already configures GameServer:
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { GameServer } from '@esengine/network-server';
|
import { GameServer } from '@esengine/network-server';
|
||||||
|
|
||||||
@@ -145,436 +99,39 @@ await server.start();
|
|||||||
console.log('Server started on ws://localhost:3000');
|
console.log('Server started on ws://localhost:3000');
|
||||||
```
|
```
|
||||||
|
|
||||||
## Core Concepts
|
## Quick Setup with CLI
|
||||||
|
|
||||||
### Architecture
|
We recommend using ESEngine CLI to quickly create a complete game server project:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
Client Server
|
mkdir my-game-server && cd my-game-server
|
||||||
┌────────────────┐ ┌────────────────┐
|
npm init -y
|
||||||
│ NetworkPlugin │◄──── WS ────► │ GameServer │
|
npx @esengine/cli init -p nodejs
|
||||||
│ ├─ Service │ │ ├─ Room │
|
|
||||||
│ ├─ SyncSystem │ │ └─ Players │
|
|
||||||
│ ├─ SpawnSystem │ └────────────────┘
|
|
||||||
│ └─ InputSystem │
|
|
||||||
└────────────────┘
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Components
|
Generated project structure:
|
||||||
|
|
||||||
#### NetworkIdentity
|
```
|
||||||
|
my-game-server/
|
||||||
Network identity component, required for every networked entity:
|
├── src/
|
||||||
|
│ ├── index.ts
|
||||||
```typescript
|
│ ├── server/
|
||||||
class NetworkIdentity extends Component {
|
│ │ └── GameServer.ts
|
||||||
netId: number; // Network unique ID
|
│ └── game/
|
||||||
ownerId: number; // Owner client ID
|
│ ├── Game.ts
|
||||||
bIsLocalPlayer: boolean; // Whether local player
|
│ ├── scenes/
|
||||||
bHasAuthority: boolean; // Whether has control authority
|
│ ├── components/
|
||||||
}
|
│ └── systems/
|
||||||
|
├── tsconfig.json
|
||||||
|
└── package.json
|
||||||
```
|
```
|
||||||
|
|
||||||
#### NetworkTransform
|
## Documentation
|
||||||
|
|
||||||
Network transform component for position and rotation sync:
|
- [Client Usage](/en/modules/network/client/) - NetworkPlugin, components and systems
|
||||||
|
- [Server Side](/en/modules/network/server/) - GameServer and Room management
|
||||||
```typescript
|
- [State Sync](/en/modules/network/sync/) - Interpolation, prediction and snapshots
|
||||||
class NetworkTransform extends Component {
|
- [API Reference](/en/modules/network/api/) - Complete API documentation
|
||||||
position: { x: number; y: number };
|
|
||||||
rotation: number;
|
|
||||||
velocity: { x: number; y: number };
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Systems
|
|
||||||
|
|
||||||
#### NetworkSyncSystem
|
|
||||||
|
|
||||||
Handles server state synchronization and interpolation:
|
|
||||||
|
|
||||||
- Receives server state snapshots
|
|
||||||
- Stores states in snapshot buffer
|
|
||||||
- Performs interpolation for remote entities
|
|
||||||
|
|
||||||
#### NetworkSpawnSystem
|
|
||||||
|
|
||||||
Handles network entity spawning and despawning:
|
|
||||||
|
|
||||||
- Listens for Spawn/Despawn messages
|
|
||||||
- Creates entities using registered prefab factories
|
|
||||||
- Manages networked entity lifecycle
|
|
||||||
|
|
||||||
#### NetworkInputSystem
|
|
||||||
|
|
||||||
Handles local player input sending:
|
|
||||||
|
|
||||||
- Collects local player input
|
|
||||||
- Sends input to server
|
|
||||||
- Supports movement and action inputs
|
|
||||||
|
|
||||||
## API Reference
|
|
||||||
|
|
||||||
### NetworkPlugin
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
class NetworkPlugin {
|
|
||||||
constructor(config: INetworkPluginConfig);
|
|
||||||
|
|
||||||
// Install plugin
|
|
||||||
install(services: ServiceContainer): void;
|
|
||||||
|
|
||||||
// Connect to server
|
|
||||||
connect(playerName: string, roomId?: string): Promise<void>;
|
|
||||||
|
|
||||||
// Disconnect
|
|
||||||
disconnect(): void;
|
|
||||||
|
|
||||||
// Register prefab factory
|
|
||||||
registerPrefab(prefab: string, factory: PrefabFactory): void;
|
|
||||||
|
|
||||||
// Properties
|
|
||||||
readonly localPlayerId: number | null;
|
|
||||||
readonly isConnected: boolean;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Configuration:**
|
|
||||||
|
|
||||||
| Property | Type | Required | Description |
|
|
||||||
|----------|------|----------|-------------|
|
|
||||||
| `serverUrl` | `string` | Yes | WebSocket server URL |
|
|
||||||
|
|
||||||
### NetworkService
|
|
||||||
|
|
||||||
Network service managing WebSocket connections:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
class NetworkService {
|
|
||||||
// Connection state
|
|
||||||
readonly state: ENetworkState;
|
|
||||||
readonly isConnected: boolean;
|
|
||||||
readonly clientId: number | null;
|
|
||||||
readonly roomId: string | null;
|
|
||||||
|
|
||||||
// Connection control
|
|
||||||
connect(serverUrl: string): Promise<void>;
|
|
||||||
disconnect(): void;
|
|
||||||
|
|
||||||
// Join room
|
|
||||||
join(playerName: string, roomId?: string): Promise<ResJoin>;
|
|
||||||
|
|
||||||
// Send input
|
|
||||||
sendInput(input: IPlayerInput): void;
|
|
||||||
|
|
||||||
// Event callbacks
|
|
||||||
setCallbacks(callbacks: Partial<INetworkCallbacks>): void;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Network state enum:**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
enum ENetworkState {
|
|
||||||
Disconnected = 'disconnected',
|
|
||||||
Connecting = 'connecting',
|
|
||||||
Connected = 'connected',
|
|
||||||
Joining = 'joining',
|
|
||||||
Joined = 'joined'
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Callbacks interface:**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface INetworkCallbacks {
|
|
||||||
onConnected?: () => void;
|
|
||||||
onDisconnected?: () => void;
|
|
||||||
onJoined?: (clientId: number, roomId: string) => void;
|
|
||||||
onSync?: (msg: MsgSync) => void;
|
|
||||||
onSpawn?: (msg: MsgSpawn) => void;
|
|
||||||
onDespawn?: (msg: MsgDespawn) => void;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Prefab Factory
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
type PrefabFactory = (scene: Scene, spawn: MsgSpawn) => Entity;
|
|
||||||
```
|
|
||||||
|
|
||||||
Register prefab factories for network entity creation:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
networkPlugin.registerPrefab('enemy', (scene, spawn) => {
|
|
||||||
const entity = scene.createEntity(`enemy_${spawn.netId}`);
|
|
||||||
|
|
||||||
const identity = entity.addComponent(new NetworkIdentity());
|
|
||||||
identity.netId = spawn.netId;
|
|
||||||
identity.ownerId = spawn.ownerId;
|
|
||||||
|
|
||||||
entity.addComponent(new NetworkTransform());
|
|
||||||
entity.addComponent(new EnemyComponent());
|
|
||||||
return entity;
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Input System
|
|
||||||
|
|
||||||
#### NetworkInputSystem
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
class NetworkInputSystem extends EntitySystem {
|
|
||||||
// Add movement input
|
|
||||||
addMoveInput(x: number, y: number): void;
|
|
||||||
|
|
||||||
// Add action input
|
|
||||||
addActionInput(action: string): void;
|
|
||||||
|
|
||||||
// Clear input
|
|
||||||
clearInput(): void;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Usage example:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Send input via NetworkPlugin (recommended)
|
|
||||||
networkPlugin.sendMoveInput(0, 1); // Movement
|
|
||||||
networkPlugin.sendActionInput('jump'); // Action
|
|
||||||
|
|
||||||
// Or use inputSystem directly
|
|
||||||
const inputSystem = networkPlugin.inputSystem;
|
|
||||||
if (keyboard.isPressed('W')) {
|
|
||||||
inputSystem.addMoveInput(0, 1);
|
|
||||||
}
|
|
||||||
if (keyboard.isPressed('Space')) {
|
|
||||||
inputSystem.addActionInput('jump');
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## State Synchronization
|
|
||||||
|
|
||||||
### Snapshot Buffer
|
|
||||||
|
|
||||||
Stores server state snapshots for interpolation:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { createSnapshotBuffer, type IStateSnapshot } from '@esengine/network';
|
|
||||||
|
|
||||||
const buffer = createSnapshotBuffer<IStateSnapshot>({
|
|
||||||
maxSnapshots: 30, // Max snapshots
|
|
||||||
interpolationDelay: 100 // Interpolation delay (ms)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add snapshot
|
|
||||||
buffer.addSnapshot({
|
|
||||||
time: serverTime,
|
|
||||||
entities: states
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get interpolated state
|
|
||||||
const interpolated = buffer.getInterpolatedState(clientTime);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Transform Interpolators
|
|
||||||
|
|
||||||
#### Linear Interpolator
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { createTransformInterpolator } from '@esengine/network';
|
|
||||||
|
|
||||||
const interpolator = createTransformInterpolator();
|
|
||||||
|
|
||||||
// Add state
|
|
||||||
interpolator.addState(time, { x: 0, y: 0, rotation: 0 });
|
|
||||||
|
|
||||||
// Get interpolated result
|
|
||||||
const state = interpolator.getInterpolatedState(currentTime);
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Hermite Interpolator
|
|
||||||
|
|
||||||
Uses Hermite splines for smoother interpolation:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { createHermiteTransformInterpolator } from '@esengine/network';
|
|
||||||
|
|
||||||
const interpolator = createHermiteTransformInterpolator({
|
|
||||||
bufferSize: 10
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add state with velocity
|
|
||||||
interpolator.addState(time, {
|
|
||||||
x: 100,
|
|
||||||
y: 200,
|
|
||||||
rotation: 0,
|
|
||||||
vx: 5,
|
|
||||||
vy: 0
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get smooth interpolated result
|
|
||||||
const state = interpolator.getInterpolatedState(currentTime);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Client Prediction
|
|
||||||
|
|
||||||
Implement client-side prediction with server reconciliation:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { createClientPrediction } from '@esengine/network';
|
|
||||||
|
|
||||||
const prediction = createClientPrediction({
|
|
||||||
maxPredictedInputs: 60,
|
|
||||||
reconciliationThreshold: 0.1
|
|
||||||
});
|
|
||||||
|
|
||||||
// Predict input
|
|
||||||
const seq = prediction.predict(inputState, currentState, (state, input) => {
|
|
||||||
// Apply input to state
|
|
||||||
return applyInput(state, input);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Server reconciliation
|
|
||||||
const corrected = prediction.reconcile(
|
|
||||||
serverState,
|
|
||||||
serverSeq,
|
|
||||||
(state, input) => applyInput(state, input)
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
## Server Side
|
|
||||||
|
|
||||||
### GameServer
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { GameServer } from '@esengine/network-server';
|
|
||||||
|
|
||||||
const server = new GameServer({
|
|
||||||
port: 3000,
|
|
||||||
roomConfig: {
|
|
||||||
maxPlayers: 16, // Max players per room
|
|
||||||
tickRate: 20 // Sync rate (Hz)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Start server
|
|
||||||
await server.start();
|
|
||||||
|
|
||||||
// Get room
|
|
||||||
const room = server.getOrCreateRoom('room-id');
|
|
||||||
|
|
||||||
// Stop server
|
|
||||||
await server.stop();
|
|
||||||
```
|
|
||||||
|
|
||||||
### Room
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
class Room {
|
|
||||||
readonly id: string;
|
|
||||||
readonly playerCount: number;
|
|
||||||
readonly isFull: boolean;
|
|
||||||
|
|
||||||
// Add player
|
|
||||||
addPlayer(name: string, connection: Connection): IPlayer | null;
|
|
||||||
|
|
||||||
// Remove player
|
|
||||||
removePlayer(clientId: number): void;
|
|
||||||
|
|
||||||
// Get player
|
|
||||||
getPlayer(clientId: number): IPlayer | undefined;
|
|
||||||
|
|
||||||
// Handle input
|
|
||||||
handleInput(clientId: number, input: IPlayerInput): void;
|
|
||||||
|
|
||||||
// Destroy room
|
|
||||||
destroy(): void;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Player interface:**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface IPlayer {
|
|
||||||
clientId: number; // Client ID
|
|
||||||
name: string; // Player name
|
|
||||||
connection: Connection; // Connection object
|
|
||||||
netId: number; // Network entity ID
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Protocol Types
|
|
||||||
|
|
||||||
### Message Types
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// State sync message
|
|
||||||
interface MsgSync {
|
|
||||||
time: number;
|
|
||||||
entities: IEntityState[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Entity state
|
|
||||||
interface IEntityState {
|
|
||||||
netId: number;
|
|
||||||
pos?: Vec2;
|
|
||||||
rot?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spawn message
|
|
||||||
interface MsgSpawn {
|
|
||||||
netId: number;
|
|
||||||
ownerId: number;
|
|
||||||
prefab: string;
|
|
||||||
pos: Vec2;
|
|
||||||
rot: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Despawn message
|
|
||||||
interface MsgDespawn {
|
|
||||||
netId: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Input message
|
|
||||||
interface MsgInput {
|
|
||||||
input: IPlayerInput;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Player input
|
|
||||||
interface IPlayerInput {
|
|
||||||
seq?: number;
|
|
||||||
moveDir?: Vec2;
|
|
||||||
actions?: string[];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### API Types
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Join request
|
|
||||||
interface ReqJoin {
|
|
||||||
playerName: string;
|
|
||||||
roomId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Join response
|
|
||||||
interface ResJoin {
|
|
||||||
clientId: number;
|
|
||||||
roomId: string;
|
|
||||||
playerCount: number;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Blueprint Nodes
|
|
||||||
|
|
||||||
The network module provides blueprint nodes for visual scripting:
|
|
||||||
|
|
||||||
- `IsLocalPlayer` - Check if entity is local player
|
|
||||||
- `IsServer` - Check if running on server
|
|
||||||
- `HasAuthority` - Check if has authority over entity
|
|
||||||
- `GetNetworkId` - Get entity's network ID
|
|
||||||
- `GetLocalPlayerId` - Get local player ID
|
|
||||||
|
|
||||||
## Service Tokens
|
## Service Tokens
|
||||||
|
|
||||||
@@ -588,142 +145,15 @@ import {
|
|||||||
NetworkInputSystemToken
|
NetworkInputSystemToken
|
||||||
} from '@esengine/network';
|
} from '@esengine/network';
|
||||||
|
|
||||||
// Get service
|
|
||||||
const networkService = services.get(NetworkServiceToken);
|
const networkService = services.get(NetworkServiceToken);
|
||||||
```
|
```
|
||||||
|
|
||||||
## Practical Example
|
## Blueprint Nodes
|
||||||
|
|
||||||
### Complete Multiplayer Client
|
The network module provides visual scripting support:
|
||||||
|
|
||||||
```typescript
|
- `IsLocalPlayer` - Check if entity is local player
|
||||||
import { Core, Scene, EntitySystem, Matcher, Entity } from '@esengine/ecs-framework';
|
- `IsServer` - Check if running on server
|
||||||
import {
|
- `HasAuthority` - Check if has authority over entity
|
||||||
NetworkPlugin,
|
- `GetNetworkId` - Get entity's network ID
|
||||||
NetworkIdentity,
|
- `GetLocalPlayerId` - Get local player ID
|
||||||
NetworkTransform
|
|
||||||
} from '@esengine/network';
|
|
||||||
|
|
||||||
// Define game scene
|
|
||||||
class GameScene extends Scene {
|
|
||||||
initialize(): void {
|
|
||||||
this.name = 'MultiplayerGame';
|
|
||||||
// Network systems are automatically added by NetworkPlugin
|
|
||||||
// Add custom systems
|
|
||||||
this.addSystem(new LocalInputHandler());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize
|
|
||||||
async function initGame() {
|
|
||||||
Core.create({ debug: false });
|
|
||||||
|
|
||||||
const scene = new GameScene();
|
|
||||||
Core.setScene(scene);
|
|
||||||
|
|
||||||
// Install network plugin
|
|
||||||
const networkPlugin = new NetworkPlugin();
|
|
||||||
await Core.installPlugin(networkPlugin);
|
|
||||||
|
|
||||||
// Register player prefab
|
|
||||||
networkPlugin.registerPrefab('player', (scene, spawn) => {
|
|
||||||
const entity = scene.createEntity(`player_${spawn.netId}`);
|
|
||||||
|
|
||||||
const identity = entity.addComponent(new NetworkIdentity());
|
|
||||||
identity.netId = spawn.netId;
|
|
||||||
identity.ownerId = spawn.ownerId;
|
|
||||||
identity.isLocalPlayer = spawn.ownerId === networkPlugin.networkService.localClientId;
|
|
||||||
|
|
||||||
entity.addComponent(new NetworkTransform());
|
|
||||||
|
|
||||||
// If local player, add input marker
|
|
||||||
if (identity.isLocalPlayer) {
|
|
||||||
entity.addComponent(new LocalInputComponent());
|
|
||||||
}
|
|
||||||
|
|
||||||
return entity;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Connect to server
|
|
||||||
const success = await networkPlugin.connect('ws://localhost:3000', 'Player1');
|
|
||||||
if (success) {
|
|
||||||
console.log('Connected!');
|
|
||||||
} else {
|
|
||||||
console.error('Connection failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
return networkPlugin;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Game loop
|
|
||||||
function gameLoop(deltaTime: number) {
|
|
||||||
Core.update(deltaTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
initGame();
|
|
||||||
```
|
|
||||||
|
|
||||||
### Handling Input
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
class LocalInputHandler extends EntitySystem {
|
|
||||||
private _networkPlugin: NetworkPlugin | null = null;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super(Matcher.empty().all(NetworkIdentity, LocalInputComponent));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected onAddedToScene(): void {
|
|
||||||
// Get NetworkPlugin reference
|
|
||||||
this._networkPlugin = Core.getPlugin(NetworkPlugin);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected processEntity(entity: Entity, dt: number): void {
|
|
||||||
if (!this._networkPlugin) return;
|
|
||||||
|
|
||||||
const identity = entity.getComponent(NetworkIdentity)!;
|
|
||||||
if (!identity.isLocalPlayer) return;
|
|
||||||
|
|
||||||
// Read keyboard input
|
|
||||||
let moveX = 0;
|
|
||||||
let moveY = 0;
|
|
||||||
|
|
||||||
if (keyboard.isPressed('A')) moveX -= 1;
|
|
||||||
if (keyboard.isPressed('D')) moveX += 1;
|
|
||||||
if (keyboard.isPressed('W')) moveY += 1;
|
|
||||||
if (keyboard.isPressed('S')) moveY -= 1;
|
|
||||||
|
|
||||||
if (moveX !== 0 || moveY !== 0) {
|
|
||||||
this._networkPlugin.sendMoveInput(moveX, moveY);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keyboard.isJustPressed('Space')) {
|
|
||||||
this._networkPlugin.sendActionInput('jump');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
1. **Set appropriate sync rate**: Choose `tickRate` based on game type, action games typically need 20-60 Hz
|
|
||||||
|
|
||||||
2. **Use interpolation delay**: Set appropriate `interpolationDelay` to balance latency and smoothness
|
|
||||||
|
|
||||||
3. **Client prediction**: Use client-side prediction for local players to reduce input lag
|
|
||||||
|
|
||||||
4. **Prefab management**: Register prefab factories for each networked entity type
|
|
||||||
|
|
||||||
5. **Authority checks**: Use `bHasAuthority` to check entity control permissions
|
|
||||||
|
|
||||||
6. **Connection state**: Monitor connection state changes, handle reconnection
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
networkService.setCallbacks({
|
|
||||||
onConnected: () => console.log('Connected'),
|
|
||||||
onDisconnected: () => {
|
|
||||||
console.log('Disconnected');
|
|
||||||
// Handle reconnection logic
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|||||||
76
docs/src/content/docs/en/modules/network/server.md
Normal file
76
docs/src/content/docs/en/modules/network/server.md
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
---
|
||||||
|
title: "Server Side"
|
||||||
|
description: "GameServer and Room management"
|
||||||
|
---
|
||||||
|
|
||||||
|
## GameServer
|
||||||
|
|
||||||
|
GameServer is the core server-side class managing WebSocket connections and rooms.
|
||||||
|
|
||||||
|
### Basic Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { GameServer } from '@esengine/network-server';
|
||||||
|
|
||||||
|
const server = new GameServer({
|
||||||
|
port: 3000,
|
||||||
|
roomConfig: {
|
||||||
|
maxPlayers: 16,
|
||||||
|
tickRate: 20
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.start();
|
||||||
|
await server.stop();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
| Property | Type | Description |
|
||||||
|
|----------|------|-------------|
|
||||||
|
| `port` | `number` | WebSocket port |
|
||||||
|
| `roomConfig.maxPlayers` | `number` | Max players per room |
|
||||||
|
| `roomConfig.tickRate` | `number` | Sync rate (Hz) |
|
||||||
|
|
||||||
|
## Room
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class Room {
|
||||||
|
readonly id: string;
|
||||||
|
readonly playerCount: number;
|
||||||
|
readonly isFull: boolean;
|
||||||
|
|
||||||
|
addPlayer(name: string, connection: Connection): IPlayer | null;
|
||||||
|
removePlayer(clientId: number): void;
|
||||||
|
getPlayer(clientId: number): IPlayer | undefined;
|
||||||
|
handleInput(clientId: number, input: IPlayerInput): void;
|
||||||
|
destroy(): void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Protocol Types
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface MsgSync {
|
||||||
|
time: number;
|
||||||
|
entities: IEntityState[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MsgSpawn {
|
||||||
|
netId: number;
|
||||||
|
ownerId: number;
|
||||||
|
prefab: string;
|
||||||
|
pos: Vec2;
|
||||||
|
rot: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MsgDespawn {
|
||||||
|
netId: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Set appropriate tick rate**: Choose based on game type (20-60 Hz for action games)
|
||||||
|
2. **Room size control**: Set reasonable `maxPlayers` based on server capacity
|
||||||
|
3. **State validation**: Server should validate client inputs to prevent cheating
|
||||||
69
docs/src/content/docs/en/modules/network/sync.md
Normal file
69
docs/src/content/docs/en/modules/network/sync.md
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
---
|
||||||
|
title: "State Sync"
|
||||||
|
description: "Interpolation, prediction and snapshot buffers"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Snapshot Buffer
|
||||||
|
|
||||||
|
Stores server state snapshots for interpolation:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createSnapshotBuffer } from '@esengine/network';
|
||||||
|
|
||||||
|
const buffer = createSnapshotBuffer({
|
||||||
|
maxSnapshots: 30,
|
||||||
|
interpolationDelay: 100
|
||||||
|
});
|
||||||
|
|
||||||
|
buffer.addSnapshot({ time: serverTime, entities: states });
|
||||||
|
const interpolated = buffer.getInterpolatedState(clientTime);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Transform Interpolators
|
||||||
|
|
||||||
|
### Linear Interpolator
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createTransformInterpolator } from '@esengine/network';
|
||||||
|
|
||||||
|
const interpolator = createTransformInterpolator();
|
||||||
|
interpolator.addState(time, { x: 0, y: 0, rotation: 0 });
|
||||||
|
const state = interpolator.getInterpolatedState(currentTime);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hermite Interpolator
|
||||||
|
|
||||||
|
Smoother interpolation using Hermite splines:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createHermiteTransformInterpolator } from '@esengine/network';
|
||||||
|
|
||||||
|
const interpolator = createHermiteTransformInterpolator({ bufferSize: 10 });
|
||||||
|
interpolator.addState(time, { x: 100, y: 200, rotation: 0, vx: 5, vy: 0 });
|
||||||
|
const state = interpolator.getInterpolatedState(currentTime);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Client Prediction
|
||||||
|
|
||||||
|
Reduces input lag with client-side prediction and server reconciliation:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createClientPrediction } from '@esengine/network';
|
||||||
|
|
||||||
|
const prediction = createClientPrediction({
|
||||||
|
maxPredictedInputs: 60,
|
||||||
|
reconciliationThreshold: 0.1
|
||||||
|
});
|
||||||
|
|
||||||
|
// Predict
|
||||||
|
const seq = prediction.predict(input, state, applyInput);
|
||||||
|
|
||||||
|
// Reconcile with server
|
||||||
|
const corrected = prediction.reconcile(serverState, serverSeq, applyInput);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Interpolation delay**: 100-150ms for typical networks
|
||||||
|
2. **Prediction**: Use only for local player, interpolate remote players
|
||||||
|
3. **Snapshot count**: Keep enough snapshots to handle network jitter
|
||||||
@@ -1,448 +0,0 @@
|
|||||||
---
|
|
||||||
title: "实体类"
|
|
||||||
---
|
|
||||||
|
|
||||||
在 ECS 架构中,实体(Entity)是游戏世界中的基本对象。实体本身不包含游戏逻辑或数据,它只是一个容器,用来组合不同的组件来实现各种功能。
|
|
||||||
|
|
||||||
## 基本概念
|
|
||||||
|
|
||||||
实体是一个轻量级的对象,主要用于:
|
|
||||||
- 作为组件的容器
|
|
||||||
- 提供唯一标识(ID)
|
|
||||||
- 管理组件的生命周期
|
|
||||||
|
|
||||||
::: tip 关于父子层级关系
|
|
||||||
实体间的父子层级关系通过 `HierarchyComponent` 和 `HierarchySystem` 管理,而非 Entity 内置属性。这种设计遵循 ECS 组合原则 —— 只有需要层级关系的实体才添加此组件。
|
|
||||||
|
|
||||||
详见 [层级系统](./hierarchy/) 文档。
|
|
||||||
:::
|
|
||||||
|
|
||||||
## 创建实体
|
|
||||||
|
|
||||||
**重要提示:实体必须通过场景创建,不支持手动创建!**
|
|
||||||
|
|
||||||
实体必须通过场景的 `createEntity()` 方法来创建,这样才能确保:
|
|
||||||
- 实体被正确添加到场景的实体管理系统中
|
|
||||||
- 实体被添加到查询系统中,供系统使用
|
|
||||||
- 实体获得正确的场景引用
|
|
||||||
- 触发相关的生命周期事件
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 正确的方式:通过场景创建实体
|
|
||||||
const player = scene.createEntity("Player");
|
|
||||||
|
|
||||||
// ❌ 错误的方式:手动创建实体
|
|
||||||
// const entity = new Entity("MyEntity", 1); // 这样创建的实体系统无法管理
|
|
||||||
```
|
|
||||||
|
|
||||||
## 添加组件
|
|
||||||
|
|
||||||
实体通过添加组件来获得功能:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { Component, ECSComponent } from '@esengine/ecs-framework';
|
|
||||||
|
|
||||||
// 定义位置组件
|
|
||||||
@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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定义健康组件
|
|
||||||
@ECSComponent('Health')
|
|
||||||
class Health extends Component {
|
|
||||||
current: number = 100;
|
|
||||||
max: number = 100;
|
|
||||||
|
|
||||||
constructor(max: number = 100) {
|
|
||||||
super();
|
|
||||||
this.max = max;
|
|
||||||
this.current = max;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 给实体添加组件
|
|
||||||
const player = scene.createEntity("Player");
|
|
||||||
player.addComponent(new Position(100, 200));
|
|
||||||
player.addComponent(new Health(150));
|
|
||||||
```
|
|
||||||
|
|
||||||
## 获取组件
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 获取组件(传入组件类,不是实例)
|
|
||||||
const position = player.getComponent(Position); // 返回 Position | null
|
|
||||||
const health = player.getComponent(Health); // 返回 Health | null
|
|
||||||
|
|
||||||
// 检查组件是否存在
|
|
||||||
if (position) {
|
|
||||||
console.log(`玩家位置: x=${position.x}, y=${position.y}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否有某个组件
|
|
||||||
if (player.hasComponent(Position)) {
|
|
||||||
console.log("玩家有位置组件");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取所有组件实例(只读属性)
|
|
||||||
const allComponents = player.components; // readonly Component[]
|
|
||||||
|
|
||||||
// 获取指定类型的所有组件(支持同类型多组件)
|
|
||||||
const allHealthComponents = player.getComponents(Health); // Health[]
|
|
||||||
|
|
||||||
// 获取或创建组件(如果不存在则自动创建)
|
|
||||||
const position = player.getOrCreateComponent(Position, 0, 0); // 传入构造参数
|
|
||||||
const health = player.getOrCreateComponent(Health, 100); // 如果存在则返回现有的,不存在则创建新的
|
|
||||||
```
|
|
||||||
|
|
||||||
## 移除组件
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 方式1:通过组件类型移除
|
|
||||||
const removedHealth = player.removeComponentByType(Health);
|
|
||||||
if (removedHealth) {
|
|
||||||
console.log("健康组件已被移除");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 方式2:通过组件实例移除
|
|
||||||
const healthComponent = player.getComponent(Health);
|
|
||||||
if (healthComponent) {
|
|
||||||
player.removeComponent(healthComponent);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量移除多种组件类型
|
|
||||||
const removedComponents = player.removeComponentsByTypes([Position, Health]);
|
|
||||||
|
|
||||||
// 检查组件是否被移除
|
|
||||||
if (!player.hasComponent(Health)) {
|
|
||||||
console.log("健康组件已被移除");
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 实体查找
|
|
||||||
|
|
||||||
场景提供了多种方式来查找实体:
|
|
||||||
|
|
||||||
### 通过名称查找
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 查找单个实体
|
|
||||||
const player = scene.findEntity("Player");
|
|
||||||
// 或使用别名方法
|
|
||||||
const player2 = scene.getEntityByName("Player");
|
|
||||||
|
|
||||||
if (player) {
|
|
||||||
console.log("找到玩家实体");
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 通过 ID 查找
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 通过实体 ID 查找
|
|
||||||
const entity = scene.findEntityById(123);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 通过标签查找
|
|
||||||
|
|
||||||
实体支持标签系统,用于快速分类和查找:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 设置标签
|
|
||||||
player.tag = 1; // 玩家标签
|
|
||||||
enemy.tag = 2; // 敌人标签
|
|
||||||
|
|
||||||
// 通过标签查找所有相关实体
|
|
||||||
const players = scene.findEntitiesByTag(1);
|
|
||||||
const enemies = scene.findEntitiesByTag(2);
|
|
||||||
// 或使用别名方法
|
|
||||||
const allPlayers = scene.getEntitiesByTag(1);
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## 实体生命周期
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 销毁实体
|
|
||||||
player.destroy();
|
|
||||||
|
|
||||||
// 检查实体是否已销毁
|
|
||||||
if (player.isDestroyed) {
|
|
||||||
console.log("实体已被销毁");
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 实体事件
|
|
||||||
|
|
||||||
实体的组件变化会触发事件:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 监听组件添加事件
|
|
||||||
scene.eventSystem.on('component:added', (data) => {
|
|
||||||
console.log('组件已添加:', data);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听实体创建事件
|
|
||||||
scene.eventSystem.on('entity:created', (data) => {
|
|
||||||
console.log('实体已创建:', data.entityName);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## 性能优化
|
|
||||||
|
|
||||||
|
|
||||||
### 批量创建实体
|
|
||||||
|
|
||||||
框架提供了高性能的批量创建方法:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 批量创建 100 个子弹实体(高性能版本)
|
|
||||||
const bullets = scene.createEntities(100, "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()` 方法会:
|
|
||||||
- 批量分配实体 ID
|
|
||||||
- 批量添加到实体列表
|
|
||||||
- 优化查询系统更新
|
|
||||||
- 减少系统缓存清理次数
|
|
||||||
|
|
||||||
## 最佳实践
|
|
||||||
|
|
||||||
### 1. 合理的组件粒度
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 好的做法:功能单一的组件
|
|
||||||
@ECSComponent('Position')
|
|
||||||
class Position extends Component {
|
|
||||||
x: number = 0;
|
|
||||||
y: number = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@ECSComponent('Velocity')
|
|
||||||
class Velocity extends Component {
|
|
||||||
dx: number = 0;
|
|
||||||
dy: number = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 避免:功能过于复杂的组件
|
|
||||||
@ECSComponent('Player')
|
|
||||||
class Player extends Component {
|
|
||||||
// 避免在一个组件中包含太多不相关的属性
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
health: number;
|
|
||||||
inventory: Item[];
|
|
||||||
skills: Skill[];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 使用装饰器
|
|
||||||
|
|
||||||
始终使用 `@ECSComponent` 装饰器:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
@ECSComponent('Transform')
|
|
||||||
class Transform extends Component {
|
|
||||||
// 组件实现
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 合理命名
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 清晰的实体命名
|
|
||||||
const mainCharacter = scene.createEntity("MainCharacter");
|
|
||||||
const enemy1 = scene.createEntity("Goblin_001");
|
|
||||||
const collectible = scene.createEntity("HealthPotion");
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 及时清理
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 不再需要的实体应该及时销毁
|
|
||||||
if (enemy.getComponent(Health).current <= 0) {
|
|
||||||
enemy.destroy();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 调试实体
|
|
||||||
|
|
||||||
框架提供了调试功能来帮助开发:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 获取实体调试信息
|
|
||||||
const debugInfo = entity.getDebugInfo();
|
|
||||||
console.log('实体信息:', debugInfo);
|
|
||||||
|
|
||||||
// 列出实体的所有组件
|
|
||||||
entity.components.forEach(component => {
|
|
||||||
console.log('组件:', component.constructor.name);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
实体是 ECS 架构的核心概念之一,理解如何正确使用实体将帮助你构建高效、可维护的游戏代码。
|
|
||||||
|
|
||||||
## 实体句柄 (EntityHandle)
|
|
||||||
|
|
||||||
实体句柄是一种安全的实体引用方式,用于解决"引用已销毁实体"的问题。
|
|
||||||
|
|
||||||
### 问题场景
|
|
||||||
|
|
||||||
假设你的 AI 系统需要追踪一个目标敌人:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 错误做法:直接存储实体引用
|
|
||||||
class AISystem extends EntitySystem {
|
|
||||||
private targetEnemy: Entity | null = null;
|
|
||||||
|
|
||||||
setTarget(enemy: Entity) {
|
|
||||||
this.targetEnemy = enemy;
|
|
||||||
}
|
|
||||||
|
|
||||||
process() {
|
|
||||||
if (this.targetEnemy) {
|
|
||||||
// 危险!敌人可能已被销毁,但引用还在
|
|
||||||
// 更糟糕:这个内存位置可能被新实体复用了
|
|
||||||
const health = this.targetEnemy.getComponent(Health);
|
|
||||||
// 可能操作了错误的实体!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 使用句柄的正确做法
|
|
||||||
|
|
||||||
每个实体创建时会自动分配一个句柄,通过 `entity.handle` 获取:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { EntityHandle, NULL_HANDLE, isValidHandle } from '@esengine/ecs-framework';
|
|
||||||
|
|
||||||
class AISystem extends EntitySystem {
|
|
||||||
// 存储句柄而非实体引用
|
|
||||||
private targetHandle: EntityHandle = NULL_HANDLE;
|
|
||||||
|
|
||||||
setTarget(enemy: Entity) {
|
|
||||||
// 保存敌人的句柄
|
|
||||||
this.targetHandle = enemy.handle;
|
|
||||||
}
|
|
||||||
|
|
||||||
process() {
|
|
||||||
if (!isValidHandle(this.targetHandle)) {
|
|
||||||
return; // 没有目标
|
|
||||||
}
|
|
||||||
|
|
||||||
// 通过句柄获取实体(自动检测是否有效)
|
|
||||||
const enemy = this.scene.findEntityByHandle(this.targetHandle);
|
|
||||||
|
|
||||||
if (!enemy) {
|
|
||||||
// 敌人已被销毁,清空引用
|
|
||||||
this.targetHandle = NULL_HANDLE;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 安全操作
|
|
||||||
const health = enemy.getComponent(Health);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 完整示例:技能目标锁定
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import {
|
|
||||||
EntitySystem, Entity, EntityHandle, NULL_HANDLE, isValidHandle
|
|
||||||
} from '@esengine/ecs-framework';
|
|
||||||
|
|
||||||
@ECSSystem('SkillTargeting')
|
|
||||||
class SkillTargetingSystem extends EntitySystem {
|
|
||||||
// 存储多个目标的句柄
|
|
||||||
private lockedTargets: Map<Entity, EntityHandle> = new Map();
|
|
||||||
|
|
||||||
// 锁定目标
|
|
||||||
lockTarget(caster: Entity, target: Entity) {
|
|
||||||
this.lockedTargets.set(caster, target.handle);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取锁定的目标
|
|
||||||
getLockedTarget(caster: Entity): Entity | null {
|
|
||||||
const handle = this.lockedTargets.get(caster);
|
|
||||||
|
|
||||||
if (!handle || !isValidHandle(handle)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// findEntityByHandle 会检查句柄是否有效
|
|
||||||
const target = this.scene.findEntityByHandle(handle);
|
|
||||||
|
|
||||||
if (!target) {
|
|
||||||
// 目标已死亡,清除锁定
|
|
||||||
this.lockedTargets.delete(caster);
|
|
||||||
}
|
|
||||||
|
|
||||||
return target;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 释放技能
|
|
||||||
castSkill(caster: Entity) {
|
|
||||||
const target = this.getLockedTarget(caster);
|
|
||||||
|
|
||||||
if (!target) {
|
|
||||||
console.log('目标丢失,技能取消');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 安全地对目标造成伤害
|
|
||||||
const health = target.getComponent(Health);
|
|
||||||
if (health) {
|
|
||||||
health.current -= 10;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 句柄 vs 实体引用
|
|
||||||
|
|
||||||
| 场景 | 推荐方式 |
|
|
||||||
|-----|---------|
|
|
||||||
| 同一帧内临时使用 | 直接用 `Entity` 引用 |
|
|
||||||
| 跨帧存储(如 AI 目标、技能目标) | 使用 `EntityHandle` |
|
|
||||||
| 需要序列化保存 | 使用 `EntityHandle`(数字类型) |
|
|
||||||
| 网络同步 | 使用 `EntityHandle`(可直接传输) |
|
|
||||||
|
|
||||||
### API 速查
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 获取实体的句柄
|
|
||||||
const handle = entity.handle;
|
|
||||||
|
|
||||||
// 检查句柄是否非空
|
|
||||||
if (isValidHandle(handle)) { ... }
|
|
||||||
|
|
||||||
// 通过句柄获取实体(自动检测有效性)
|
|
||||||
const entity = scene.findEntityByHandle(handle);
|
|
||||||
|
|
||||||
// 检查句柄对应的实体是否存活
|
|
||||||
const alive = scene.handleManager.isAlive(handle);
|
|
||||||
|
|
||||||
// 空句柄常量
|
|
||||||
const emptyHandle = NULL_HANDLE;
|
|
||||||
```
|
|
||||||
|
|
||||||
## 下一步
|
|
||||||
|
|
||||||
- 了解 [层级系统](./hierarchy/) 建立实体间的父子关系
|
|
||||||
- 了解 [组件系统](./component/) 为实体添加功能
|
|
||||||
- 了解 [场景管理](./scene/) 组织和管理实体
|
|
||||||
273
docs/src/content/docs/guide/entity/component-operations.md
Normal file
273
docs/src/content/docs/guide/entity/component-operations.md
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
---
|
||||||
|
title: "组件操作"
|
||||||
|
description: "实体的组件添加、获取、移除等操作详解"
|
||||||
|
---
|
||||||
|
|
||||||
|
实体通过添加组件来获得功能。本节详细介绍所有组件操作 API。
|
||||||
|
|
||||||
|
## 添加组件
|
||||||
|
|
||||||
|
### addComponent
|
||||||
|
|
||||||
|
添加已创建的组件实例:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Component, ECSComponent } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const player = scene.createEntity("Player");
|
||||||
|
const position = new Position(100, 200);
|
||||||
|
player.addComponent(position);
|
||||||
|
```
|
||||||
|
|
||||||
|
### createComponent
|
||||||
|
|
||||||
|
直接传入组件类型和构造参数,由实体创建组件实例(推荐方式):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 创建并添加组件
|
||||||
|
const position = player.createComponent(Position, 100, 200);
|
||||||
|
const health = player.createComponent(Health, 150);
|
||||||
|
|
||||||
|
// 等价于
|
||||||
|
// const position = new Position(100, 200);
|
||||||
|
// player.addComponent(position);
|
||||||
|
```
|
||||||
|
|
||||||
|
### addComponents
|
||||||
|
|
||||||
|
批量添加多个组件:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const components = player.addComponents([
|
||||||
|
new Position(100, 200),
|
||||||
|
new Health(150),
|
||||||
|
new Velocity(0, 0)
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
:::note[注意事项]
|
||||||
|
- 同一实体不能添加相同类型的组件两次,会抛出异常
|
||||||
|
- 实体必须已添加到场景后才能添加组件
|
||||||
|
:::
|
||||||
|
|
||||||
|
## 获取组件
|
||||||
|
|
||||||
|
### getComponent
|
||||||
|
|
||||||
|
获取指定类型的组件:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 返回 Position | null
|
||||||
|
const position = player.getComponent(Position);
|
||||||
|
|
||||||
|
if (position) {
|
||||||
|
position.x += 10;
|
||||||
|
position.y += 20;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### hasComponent
|
||||||
|
|
||||||
|
检查实体是否拥有指定类型的组件:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
if (player.hasComponent(Position)) {
|
||||||
|
const position = player.getComponent(Position)!;
|
||||||
|
// 使用 ! 因为我们已经确认存在
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### getComponents
|
||||||
|
|
||||||
|
获取指定类型的所有组件(支持同类型多组件场景):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const allHealthComponents = player.getComponents(Health);
|
||||||
|
```
|
||||||
|
|
||||||
|
### getComponentByType
|
||||||
|
|
||||||
|
支持继承查找的组件获取,使用 `instanceof` 检查:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 查找 CompositeNodeComponent 或其任意子类
|
||||||
|
const composite = entity.getComponentByType(CompositeNodeComponent);
|
||||||
|
if (composite) {
|
||||||
|
// composite 可能是 SequenceNode, SelectorNode 等
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
与 `getComponent()` 的区别:
|
||||||
|
|
||||||
|
| 方法 | 查找方式 | 性能 | 使用场景 |
|
||||||
|
|-----|---------|-----|---------|
|
||||||
|
| `getComponent` | 精确类型匹配(位掩码) | 高 | 知道确切类型 |
|
||||||
|
| `getComponentByType` | `instanceof` 检查 | 较低 | 需要支持继承 |
|
||||||
|
|
||||||
|
### getOrCreateComponent
|
||||||
|
|
||||||
|
获取或创建组件,如果不存在则自动创建:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 确保实体拥有 Position 组件
|
||||||
|
const position = player.getOrCreateComponent(Position, 0, 0);
|
||||||
|
position.x = 100;
|
||||||
|
|
||||||
|
// 如果已存在,返回现有组件
|
||||||
|
// 如果不存在,使用 (0, 0) 参数创建新组件
|
||||||
|
```
|
||||||
|
|
||||||
|
### components 属性
|
||||||
|
|
||||||
|
获取实体的所有组件(只读):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const allComponents = player.components; // readonly Component[]
|
||||||
|
|
||||||
|
allComponents.forEach(component => {
|
||||||
|
console.log(component.constructor.name);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 移除组件
|
||||||
|
|
||||||
|
### removeComponent
|
||||||
|
|
||||||
|
通过组件实例移除:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const healthComponent = player.getComponent(Health);
|
||||||
|
if (healthComponent) {
|
||||||
|
player.removeComponent(healthComponent);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### removeComponentByType
|
||||||
|
|
||||||
|
通过组件类型移除:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const removedHealth = player.removeComponentByType(Health);
|
||||||
|
if (removedHealth) {
|
||||||
|
console.log("健康组件已被移除");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### removeComponentsByTypes
|
||||||
|
|
||||||
|
批量移除多种组件类型:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const removedComponents = player.removeComponentsByTypes([
|
||||||
|
Position,
|
||||||
|
Health,
|
||||||
|
Velocity
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### removeAllComponents
|
||||||
|
|
||||||
|
移除所有组件:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
player.removeAllComponents();
|
||||||
|
```
|
||||||
|
|
||||||
|
## 变更检测
|
||||||
|
|
||||||
|
### markDirty
|
||||||
|
|
||||||
|
标记组件为已修改,用于帧级变更检测系统:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const pos = entity.getComponent(Position)!;
|
||||||
|
pos.x = 100;
|
||||||
|
entity.markDirty(pos);
|
||||||
|
|
||||||
|
// 或标记多个组件
|
||||||
|
const vel = entity.getComponent(Velocity)!;
|
||||||
|
entity.markDirty(pos, vel);
|
||||||
|
```
|
||||||
|
|
||||||
|
配合响应式查询使用:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 在系统中查询本帧修改过的组件
|
||||||
|
const changedQuery = scene.createReactiveQuery({
|
||||||
|
all: [Position],
|
||||||
|
changed: [Position] // 只匹配本帧修改过的
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const entity of changedQuery.getEntities()) {
|
||||||
|
// 处理位置变化的实体
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 组件掩码
|
||||||
|
|
||||||
|
每个实体维护一个组件位掩码,用于高效的 `hasComponent` 检查:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 获取组件掩码(内部使用)
|
||||||
|
const mask = entity.componentMask;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 完整示例
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Component, ECSComponent, Scene } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
@ECSComponent('Position')
|
||||||
|
class Position extends Component {
|
||||||
|
constructor(public x = 0, public y = 0) { super(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@ECSComponent('Health')
|
||||||
|
class Health extends Component {
|
||||||
|
constructor(public current = 100, public max = 100) { super(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建实体并添加组件
|
||||||
|
const player = scene.createEntity("Player");
|
||||||
|
player.createComponent(Position, 100, 200);
|
||||||
|
player.createComponent(Health, 150, 150);
|
||||||
|
|
||||||
|
// 获取并修改组件
|
||||||
|
const position = player.getComponent(Position);
|
||||||
|
if (position) {
|
||||||
|
position.x += 10;
|
||||||
|
player.markDirty(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取或创建组件
|
||||||
|
const velocity = player.getOrCreateComponent(Velocity, 0, 0);
|
||||||
|
|
||||||
|
// 检查组件存在
|
||||||
|
if (player.hasComponent(Health)) {
|
||||||
|
const health = player.getComponent(Health)!;
|
||||||
|
health.current -= 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除组件
|
||||||
|
player.removeComponentByType(Velocity);
|
||||||
|
|
||||||
|
// 列出所有组件
|
||||||
|
console.log(player.components.map(c => c.constructor.name));
|
||||||
|
```
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
- [实体句柄](/guide/entity/entity-handle/) - 安全的跨帧实体引用
|
||||||
|
- [组件系统](/guide/component/) - 组件的定义和生命周期
|
||||||
265
docs/src/content/docs/guide/entity/entity-handle.md
Normal file
265
docs/src/content/docs/guide/entity/entity-handle.md
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
---
|
||||||
|
title: "实体句柄"
|
||||||
|
description: "使用 EntityHandle 安全地引用实体,避免引用已销毁实体的问题"
|
||||||
|
---
|
||||||
|
|
||||||
|
实体句柄(EntityHandle)是一种安全的实体引用方式,用于解决"引用已销毁实体"的问题。
|
||||||
|
|
||||||
|
## 问题场景
|
||||||
|
|
||||||
|
假设你的 AI 系统需要追踪一个目标敌人:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ 错误做法:直接存储实体引用
|
||||||
|
class AISystem extends EntitySystem {
|
||||||
|
private targetEnemy: Entity | null = null;
|
||||||
|
|
||||||
|
setTarget(enemy: Entity) {
|
||||||
|
this.targetEnemy = enemy;
|
||||||
|
}
|
||||||
|
|
||||||
|
process() {
|
||||||
|
if (this.targetEnemy) {
|
||||||
|
// 危险!敌人可能已被销毁,但引用还在
|
||||||
|
// 更糟糕:这个内存位置可能被新实体复用了
|
||||||
|
const health = this.targetEnemy.getComponent(Health);
|
||||||
|
// 可能操作了错误的实体!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 什么是 EntityHandle
|
||||||
|
|
||||||
|
EntityHandle 是一个数值类型的实体标识符,包含:
|
||||||
|
- **索引(Index)**:实体在数组中的位置
|
||||||
|
- **代数(Generation)**:实体被复用的次数
|
||||||
|
|
||||||
|
当实体被销毁后,即使其索引被新实体复用,代数也会增加,使得旧句柄失效。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { EntityHandle, NULL_HANDLE, isValidHandle } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
// 每个实体创建时会自动分配句柄
|
||||||
|
const handle: EntityHandle = entity.handle;
|
||||||
|
|
||||||
|
// 空句柄常量
|
||||||
|
const emptyHandle = NULL_HANDLE;
|
||||||
|
|
||||||
|
// 检查句柄是否非空
|
||||||
|
if (isValidHandle(handle)) {
|
||||||
|
// 句柄有效
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用句柄的正确做法
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { EntityHandle, NULL_HANDLE, isValidHandle } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
class AISystem extends EntitySystem {
|
||||||
|
// ✅ 存储句柄而非实体引用
|
||||||
|
private targetHandle: EntityHandle = NULL_HANDLE;
|
||||||
|
|
||||||
|
setTarget(enemy: Entity) {
|
||||||
|
this.targetHandle = enemy.handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
process() {
|
||||||
|
if (!isValidHandle(this.targetHandle)) {
|
||||||
|
return; // 没有目标
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通过句柄获取实体(自动检测是否有效)
|
||||||
|
const enemy = this.scene.findEntityByHandle(this.targetHandle);
|
||||||
|
|
||||||
|
if (!enemy) {
|
||||||
|
// 敌人已被销毁,清空引用
|
||||||
|
this.targetHandle = NULL_HANDLE;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 安全操作
|
||||||
|
const health = enemy.getComponent(Health);
|
||||||
|
if (health) {
|
||||||
|
// 对敌人造成伤害
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API 参考
|
||||||
|
|
||||||
|
### 获取句柄
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 从实体获取句柄
|
||||||
|
const handle = entity.handle;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 验证句柄
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { isValidHandle, NULL_HANDLE } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
// 检查句柄是否非空
|
||||||
|
if (isValidHandle(handle)) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查实体是否存活
|
||||||
|
const alive = scene.handleManager.isAlive(handle);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 通过句柄获取实体
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 返回 Entity | null
|
||||||
|
const entity = scene.findEntityByHandle(handle);
|
||||||
|
|
||||||
|
if (entity) {
|
||||||
|
// 实体存在且有效
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 完整示例:技能目标锁定
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
EntitySystem,
|
||||||
|
Entity,
|
||||||
|
EntityHandle,
|
||||||
|
NULL_HANDLE,
|
||||||
|
isValidHandle
|
||||||
|
} from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
@ECSSystem('SkillTargeting')
|
||||||
|
class SkillTargetingSystem extends EntitySystem {
|
||||||
|
// 存储多个目标的句柄
|
||||||
|
private lockedTargets: Map<number, EntityHandle> = new Map();
|
||||||
|
|
||||||
|
// 锁定目标
|
||||||
|
lockTarget(casterId: number, target: Entity) {
|
||||||
|
this.lockedTargets.set(casterId, target.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取锁定的目标
|
||||||
|
getLockedTarget(casterId: number): Entity | null {
|
||||||
|
const handle = this.lockedTargets.get(casterId);
|
||||||
|
|
||||||
|
if (!handle || !isValidHandle(handle)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = this.scene.findEntityByHandle(handle);
|
||||||
|
|
||||||
|
if (!target) {
|
||||||
|
// 目标已死亡,清除锁定
|
||||||
|
this.lockedTargets.delete(casterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 释放技能
|
||||||
|
castSkill(caster: Entity) {
|
||||||
|
const target = this.getLockedTarget(caster.id);
|
||||||
|
|
||||||
|
if (!target) {
|
||||||
|
console.log('目标丢失,技能取消');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const health = target.getComponent(Health);
|
||||||
|
if (health) {
|
||||||
|
health.current -= 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除指定施法者的目标
|
||||||
|
clearTarget(casterId: number) {
|
||||||
|
this.lockedTargets.delete(casterId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用场景指南
|
||||||
|
|
||||||
|
| 场景 | 推荐方式 |
|
||||||
|
|-----|---------|
|
||||||
|
| 同一帧内临时使用 | 直接用 `Entity` 引用 |
|
||||||
|
| 跨帧存储(AI 目标、技能目标) | 使用 `EntityHandle` |
|
||||||
|
| 需要序列化保存 | 使用 `EntityHandle`(数字类型) |
|
||||||
|
| 网络同步 | 使用 `EntityHandle`(可直接传输) |
|
||||||
|
|
||||||
|
## 性能考虑
|
||||||
|
|
||||||
|
- EntityHandle 是数字类型,内存占用小
|
||||||
|
- `findEntityByHandle` 是 O(1) 操作
|
||||||
|
- 比每帧检查 `entity.isDestroyed` 更安全可靠
|
||||||
|
|
||||||
|
## 常见模式
|
||||||
|
|
||||||
|
### 可选目标引用
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class FollowComponent extends Component {
|
||||||
|
private _targetHandle: EntityHandle = NULL_HANDLE;
|
||||||
|
|
||||||
|
setTarget(target: Entity | null) {
|
||||||
|
this._targetHandle = target?.handle ?? NULL_HANDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTarget(scene: IScene): Entity | null {
|
||||||
|
if (!isValidHandle(this._targetHandle)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return scene.findEntityByHandle(this._targetHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
hasTarget(): boolean {
|
||||||
|
return isValidHandle(this._targetHandle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 多目标追踪
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class MultiTargetComponent extends Component {
|
||||||
|
private targets: EntityHandle[] = [];
|
||||||
|
|
||||||
|
addTarget(target: Entity) {
|
||||||
|
this.targets.push(target.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeTarget(target: Entity) {
|
||||||
|
const index = this.targets.indexOf(target.handle);
|
||||||
|
if (index >= 0) {
|
||||||
|
this.targets.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getValidTargets(scene: IScene): Entity[] {
|
||||||
|
const valid: Entity[] = [];
|
||||||
|
const stillValid: EntityHandle[] = [];
|
||||||
|
|
||||||
|
for (const handle of this.targets) {
|
||||||
|
const entity = scene.findEntityByHandle(handle);
|
||||||
|
if (entity) {
|
||||||
|
valid.push(entity);
|
||||||
|
stillValid.push(handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理无效句柄
|
||||||
|
this.targets = stillValid;
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
- [生命周期](/guide/entity/lifecycle/) - 实体的销毁和持久化
|
||||||
|
- [组件引用](/guide/component/entity-ref/) - 组件中的实体引用装饰器
|
||||||
174
docs/src/content/docs/guide/entity/index.md
Normal file
174
docs/src/content/docs/guide/entity/index.md
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
---
|
||||||
|
title: "实体概述"
|
||||||
|
description: "ECS 架构中实体的基本概念和使用方式"
|
||||||
|
---
|
||||||
|
|
||||||
|
在 ECS 架构中,实体(Entity)是游戏世界中的基本对象。实体本身不包含游戏逻辑或数据,它只是一个容器,用来组合不同的组件来实现各种功能。
|
||||||
|
|
||||||
|
## 基本概念
|
||||||
|
|
||||||
|
实体是一个轻量级的对象,主要用于:
|
||||||
|
- 作为组件的容器
|
||||||
|
- 提供唯一标识(ID 和 persistentId)
|
||||||
|
- 管理组件的生命周期
|
||||||
|
|
||||||
|
:::tip[关于父子层级关系]
|
||||||
|
实体间的父子层级关系通过 `HierarchyComponent` 和 `HierarchySystem` 管理,而非 Entity 内置属性。这种设计遵循 ECS 组合原则 —— 只有需要层级关系的实体才添加此组件。
|
||||||
|
|
||||||
|
详见 [层级系统](/guide/hierarchy/) 文档。
|
||||||
|
:::
|
||||||
|
|
||||||
|
## 创建实体
|
||||||
|
|
||||||
|
**实体必须通过场景创建,不支持手动创建。**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 正确的方式:通过场景创建实体
|
||||||
|
const player = scene.createEntity("Player");
|
||||||
|
|
||||||
|
// ❌ 错误的方式:手动创建实体
|
||||||
|
// const entity = new Entity("MyEntity", 1);
|
||||||
|
```
|
||||||
|
|
||||||
|
通过场景创建可以确保:
|
||||||
|
- 实体被正确添加到场景的实体管理系统中
|
||||||
|
- 实体被添加到查询系统中,供系统使用
|
||||||
|
- 实体获得正确的场景引用
|
||||||
|
- 触发相关的生命周期事件
|
||||||
|
|
||||||
|
### 批量创建
|
||||||
|
|
||||||
|
框架提供了高性能的批量创建方法:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 批量创建 100 个子弹实体
|
||||||
|
const bullets = scene.createEntities(100, "Bullet");
|
||||||
|
|
||||||
|
bullets.forEach((bullet, index) => {
|
||||||
|
bullet.createComponent(Position, Math.random() * 800, Math.random() * 600);
|
||||||
|
bullet.createComponent(Velocity, Math.random() * 100, Math.random() * 100);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
`createEntities()` 会批量分配 ID、优化查询系统更新,减少系统缓存清理次数。
|
||||||
|
|
||||||
|
## 实体标识
|
||||||
|
|
||||||
|
每个实体有三种标识符:
|
||||||
|
|
||||||
|
| 属性 | 类型 | 说明 |
|
||||||
|
|-----|------|-----|
|
||||||
|
| `id` | `number` | 运行时唯一标识符,用于快速查找 |
|
||||||
|
| `persistentId` | `string` | GUID,序列化时保持引用一致性 |
|
||||||
|
| `handle` | `EntityHandle` | 轻量级句柄,详见[实体句柄](/guide/entity/entity-handle/) |
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const entity = scene.createEntity("Player");
|
||||||
|
|
||||||
|
console.log(entity.id); // 1
|
||||||
|
console.log(entity.persistentId); // "a1b2c3d4-..."
|
||||||
|
console.log(entity.handle); // 数字类型句柄
|
||||||
|
```
|
||||||
|
|
||||||
|
## 实体属性
|
||||||
|
|
||||||
|
### 名称和标签
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 名称 - 用于调试和查找
|
||||||
|
entity.name = "Player";
|
||||||
|
|
||||||
|
// 标签 - 用于快速分类和查询
|
||||||
|
entity.tag = 1; // 玩家标签
|
||||||
|
enemy.tag = 2; // 敌人标签
|
||||||
|
```
|
||||||
|
|
||||||
|
### 状态控制
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 启用/禁用状态
|
||||||
|
entity.enabled = false;
|
||||||
|
|
||||||
|
// 激活状态
|
||||||
|
entity.active = false;
|
||||||
|
|
||||||
|
// 更新顺序(数值越小越优先)
|
||||||
|
entity.updateOrder = 10;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 实体查找
|
||||||
|
|
||||||
|
场景提供了多种查找方式:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 通过名称查找
|
||||||
|
const player = scene.findEntity("Player");
|
||||||
|
// 或别名
|
||||||
|
const player2 = scene.getEntityByName("Player");
|
||||||
|
|
||||||
|
// 通过 ID 查找
|
||||||
|
const entity = scene.findEntityById(123);
|
||||||
|
|
||||||
|
// 通过标签查找所有相关实体
|
||||||
|
const enemies = scene.findEntitiesByTag(2);
|
||||||
|
// 或别名
|
||||||
|
const allEnemies = scene.getEntitiesByTag(2);
|
||||||
|
|
||||||
|
// 通过句柄查找
|
||||||
|
const entity = scene.findEntityByHandle(handle);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 实体事件
|
||||||
|
|
||||||
|
实体的变化会触发事件:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 监听组件添加
|
||||||
|
scene.eventSystem.on('component:added', (data) => {
|
||||||
|
console.log(`${data.entityName} 添加了 ${data.componentType}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听组件移除
|
||||||
|
scene.eventSystem.on('component:removed', (data) => {
|
||||||
|
console.log(`${data.entityName} 移除了 ${data.componentType}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听实体创建
|
||||||
|
scene.eventSystem.on('entity:created', (data) => {
|
||||||
|
console.log(`实体已创建: ${data.entityName}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听激活状态变化
|
||||||
|
scene.eventSystem.on('entity:activeChanged', (data) => {
|
||||||
|
console.log(`${data.entity.name} 激活状态: ${data.active}`);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 调试
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 获取实体调试信息
|
||||||
|
const debugInfo = entity.getDebugInfo();
|
||||||
|
console.log(debugInfo);
|
||||||
|
// {
|
||||||
|
// name: "Player",
|
||||||
|
// id: 1,
|
||||||
|
// persistentId: "a1b2c3d4-...",
|
||||||
|
// enabled: true,
|
||||||
|
// active: true,
|
||||||
|
// destroyed: false,
|
||||||
|
// componentCount: 3,
|
||||||
|
// componentTypes: ["Position", "Health", "Velocity"],
|
||||||
|
// ...
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 实体字符串表示
|
||||||
|
console.log(entity.toString());
|
||||||
|
// "Entity[Player:1:a1b2c3d4]"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
- [组件操作](/guide/entity/component-operations/) - 添加、获取、移除组件
|
||||||
|
- [实体句柄](/guide/entity/entity-handle/) - 安全的实体引用方式
|
||||||
|
- [生命周期](/guide/entity/lifecycle/) - 销毁和持久化
|
||||||
238
docs/src/content/docs/guide/entity/lifecycle.md
Normal file
238
docs/src/content/docs/guide/entity/lifecycle.md
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
---
|
||||||
|
title: "生命周期"
|
||||||
|
description: "实体的生命周期管理、销毁和持久化"
|
||||||
|
---
|
||||||
|
|
||||||
|
实体的生命周期包括创建、运行和销毁三个阶段。本节介绍如何正确管理实体的生命周期。
|
||||||
|
|
||||||
|
## 销毁实体
|
||||||
|
|
||||||
|
### 基本销毁
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 销毁实体
|
||||||
|
player.destroy();
|
||||||
|
|
||||||
|
// 检查实体是否已销毁
|
||||||
|
if (player.isDestroyed) {
|
||||||
|
console.log("实体已被销毁");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
销毁实体时会:
|
||||||
|
1. 移除所有组件(触发 `onRemovedFromEntity` 回调)
|
||||||
|
2. 从查询系统中移除
|
||||||
|
3. 从场景实体列表中移除
|
||||||
|
4. 清理所有引用追踪
|
||||||
|
|
||||||
|
### 条件销毁
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 常见模式:生命值耗尽时销毁
|
||||||
|
const health = enemy.getComponent(Health);
|
||||||
|
if (health && health.current <= 0) {
|
||||||
|
enemy.destroy();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 销毁保护
|
||||||
|
|
||||||
|
销毁操作是幂等的,多次调用不会出错:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
player.destroy();
|
||||||
|
player.destroy(); // 安全,不会报错
|
||||||
|
```
|
||||||
|
|
||||||
|
## 持久化实体
|
||||||
|
|
||||||
|
默认情况下,实体在场景切换时会被销毁。使用持久化可以让实体跨场景存活。
|
||||||
|
|
||||||
|
### 设置持久化
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 方式一:链式调用
|
||||||
|
const player = scene.createEntity('Player')
|
||||||
|
.setPersistent()
|
||||||
|
.createComponent(PlayerComponent);
|
||||||
|
|
||||||
|
// 方式二:单独设置
|
||||||
|
player.setPersistent();
|
||||||
|
|
||||||
|
// 检查是否持久化
|
||||||
|
if (player.isPersistent) {
|
||||||
|
console.log("这是持久化实体");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 取消持久化
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 恢复为场景本地实体
|
||||||
|
player.setSceneLocal();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 生命周期策略
|
||||||
|
|
||||||
|
实体有两种生命周期策略:
|
||||||
|
|
||||||
|
| 策略 | 说明 |
|
||||||
|
|-----|------|
|
||||||
|
| `SceneLocal` | 默认,随场景销毁 |
|
||||||
|
| `Persistent` | 跨场景保留 |
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { EEntityLifecyclePolicy } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
// 获取当前策略
|
||||||
|
const policy = entity.lifecyclePolicy;
|
||||||
|
|
||||||
|
if (policy === EEntityLifecyclePolicy.Persistent) {
|
||||||
|
// 持久化实体
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 使用场景
|
||||||
|
|
||||||
|
持久化实体适用于:
|
||||||
|
- 玩家角色
|
||||||
|
- 全局管理器
|
||||||
|
- UI 实体
|
||||||
|
- 需要跨场景保留的游戏状态
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 玩家角色
|
||||||
|
const player = scene.createEntity('Player')
|
||||||
|
.setPersistent();
|
||||||
|
|
||||||
|
// 游戏管理器
|
||||||
|
const gameManager = scene.createEntity('GameManager')
|
||||||
|
.setPersistent()
|
||||||
|
.createComponent(GameStateComponent);
|
||||||
|
|
||||||
|
// 分数管理
|
||||||
|
const scoreManager = scene.createEntity('ScoreManager')
|
||||||
|
.setPersistent()
|
||||||
|
.createComponent(ScoreComponent);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 场景切换时的行为
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 场景管理器切换场景
|
||||||
|
sceneManager.loadScene('Level2');
|
||||||
|
|
||||||
|
// 切换时:
|
||||||
|
// 1. SceneLocal 实体被销毁
|
||||||
|
// 2. Persistent 实体被迁移到新场景
|
||||||
|
// 3. 新场景的实体被创建
|
||||||
|
```
|
||||||
|
|
||||||
|
:::caution[注意]
|
||||||
|
持久化实体在场景切换时会自动迁移到新场景,但其引用的其他非持久化实体可能已被销毁。使用 [EntityHandle](/guide/entity/entity-handle/) 来安全地处理这种情况。
|
||||||
|
:::
|
||||||
|
|
||||||
|
## 实体引用清理
|
||||||
|
|
||||||
|
框架提供了引用追踪系统,在实体销毁时自动清理引用:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 引用追踪会在实体销毁时清理指向该实体的所有引用
|
||||||
|
scene.referenceTracker?.clearReferencesTo(entity.id);
|
||||||
|
```
|
||||||
|
|
||||||
|
配合 `@entityRef` 装饰器使用可以自动处理:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class FollowComponent extends Component {
|
||||||
|
@entityRef()
|
||||||
|
targetId: number | null = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当 target 被销毁时,targetId 会自动设为 null
|
||||||
|
```
|
||||||
|
|
||||||
|
详见 [组件引用](/guide/component/entity-ref/)。
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
### 1. 及时销毁不需要的实体
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 子弹飞出屏幕后销毁
|
||||||
|
if (position.x < 0 || position.x > screenWidth) {
|
||||||
|
bullet.destroy();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 使用对象池代替频繁创建销毁
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class BulletPool {
|
||||||
|
private pool: Entity[] = [];
|
||||||
|
|
||||||
|
acquire(scene: Scene): Entity {
|
||||||
|
if (this.pool.length > 0) {
|
||||||
|
const bullet = this.pool.pop()!;
|
||||||
|
bullet.enabled = true;
|
||||||
|
return bullet;
|
||||||
|
}
|
||||||
|
return scene.createEntity('Bullet');
|
||||||
|
}
|
||||||
|
|
||||||
|
release(bullet: Entity) {
|
||||||
|
bullet.enabled = false;
|
||||||
|
this.pool.push(bullet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 谨慎使用持久化
|
||||||
|
|
||||||
|
只对真正需要跨场景的实体使用持久化,过多的持久化实体会增加内存占用。
|
||||||
|
|
||||||
|
### 4. 销毁前清理引用
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 销毁前通知相关系统
|
||||||
|
const aiSystem = scene.getSystem(AISystem);
|
||||||
|
aiSystem?.clearTarget(enemy.id);
|
||||||
|
|
||||||
|
enemy.destroy();
|
||||||
|
```
|
||||||
|
|
||||||
|
## 生命周期事件
|
||||||
|
|
||||||
|
可以监听实体销毁事件:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 方式一:通过事件系统
|
||||||
|
scene.eventSystem.on('entity:destroyed', (data) => {
|
||||||
|
console.log(`实体 ${data.entityName} 已销毁`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 方式二:在组件中监听
|
||||||
|
class MyComponent extends Component {
|
||||||
|
onRemovedFromEntity() {
|
||||||
|
console.log('组件被移除,实体可能正在销毁');
|
||||||
|
// 清理资源
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 调试
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 获取实体状态
|
||||||
|
const debugInfo = entity.getDebugInfo();
|
||||||
|
console.log({
|
||||||
|
destroyed: debugInfo.destroyed,
|
||||||
|
enabled: debugInfo.enabled,
|
||||||
|
active: debugInfo.active
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
- [组件操作](/guide/entity/component-operations/) - 组件的添加和移除
|
||||||
|
- [场景管理](/guide/scene/) - 场景切换和管理
|
||||||
288
docs/src/content/docs/modules/network/api.md
Normal file
288
docs/src/content/docs/modules/network/api.md
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
---
|
||||||
|
title: "API 参考"
|
||||||
|
description: "Network 模块完整 API 文档"
|
||||||
|
---
|
||||||
|
|
||||||
|
## NetworkPlugin
|
||||||
|
|
||||||
|
客户端网络插件核心类。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class NetworkPlugin implements IPlugin {
|
||||||
|
readonly name: string;
|
||||||
|
readonly version: string;
|
||||||
|
|
||||||
|
// 访问器
|
||||||
|
get networkService(): NetworkService;
|
||||||
|
get syncSystem(): NetworkSyncSystem;
|
||||||
|
get spawnSystem(): NetworkSpawnSystem;
|
||||||
|
get inputSystem(): NetworkInputSystem;
|
||||||
|
get isConnected(): boolean;
|
||||||
|
|
||||||
|
// 生命周期
|
||||||
|
install(core: Core, services: ServiceContainer): void;
|
||||||
|
uninstall(): void;
|
||||||
|
|
||||||
|
// 连接管理
|
||||||
|
connect(serverUrl: string, playerName: string, roomId?: string): Promise<boolean>;
|
||||||
|
disconnect(): Promise<void>;
|
||||||
|
|
||||||
|
// 预制体注册
|
||||||
|
registerPrefab(prefabType: string, factory: PrefabFactory): void;
|
||||||
|
|
||||||
|
// 输入发送
|
||||||
|
sendMoveInput(x: number, y: number): void;
|
||||||
|
sendActionInput(action: string): void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## NetworkService
|
||||||
|
|
||||||
|
网络服务,管理 WebSocket 连接。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class NetworkService {
|
||||||
|
// 访问器
|
||||||
|
get state(): ENetworkState;
|
||||||
|
get isConnected(): boolean;
|
||||||
|
get clientId(): number;
|
||||||
|
get roomId(): string;
|
||||||
|
|
||||||
|
// 连接管理
|
||||||
|
connect(serverUrl: string, playerName: string, roomId?: string): Promise<boolean>;
|
||||||
|
disconnect(): Promise<void>;
|
||||||
|
|
||||||
|
// 输入发送
|
||||||
|
sendInput(input: IPlayerInput): void;
|
||||||
|
|
||||||
|
// 回调设置
|
||||||
|
setCallbacks(callbacks: INetworkCallbacks): void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 枚举类型
|
||||||
|
|
||||||
|
### ENetworkState
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const enum ENetworkState {
|
||||||
|
Disconnected = 0,
|
||||||
|
Connecting = 1,
|
||||||
|
Connected = 2
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 接口类型
|
||||||
|
|
||||||
|
### INetworkCallbacks
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface INetworkCallbacks {
|
||||||
|
onConnected?: (clientId: number, roomId: string) => void;
|
||||||
|
onDisconnected?: () => void;
|
||||||
|
onSync?: (msg: MsgSync) => void;
|
||||||
|
onSpawn?: (msg: MsgSpawn) => void;
|
||||||
|
onDespawn?: (msg: MsgDespawn) => void;
|
||||||
|
onError?: (error: Error) => void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### PrefabFactory
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type PrefabFactory = (scene: Scene, spawn: MsgSpawn) => Entity;
|
||||||
|
```
|
||||||
|
|
||||||
|
### IPlayerInput
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface IPlayerInput {
|
||||||
|
seq?: number;
|
||||||
|
moveDir?: Vec2;
|
||||||
|
actions?: string[];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 组件
|
||||||
|
|
||||||
|
### NetworkIdentity
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class NetworkIdentity extends Component {
|
||||||
|
netId: number;
|
||||||
|
ownerId: number;
|
||||||
|
bIsLocalPlayer: boolean;
|
||||||
|
bHasAuthority: boolean;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### NetworkTransform
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class NetworkTransform extends Component {
|
||||||
|
position: { x: number; y: number };
|
||||||
|
rotation: number;
|
||||||
|
velocity: { x: number; y: number };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 系统
|
||||||
|
|
||||||
|
### NetworkSyncSystem
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class NetworkSyncSystem extends EntitySystem {
|
||||||
|
// 内部使用,由 NetworkPlugin 自动管理
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### NetworkSpawnSystem
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class NetworkSpawnSystem extends EntitySystem {
|
||||||
|
registerPrefab(prefabType: string, factory: PrefabFactory): void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### NetworkInputSystem
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class NetworkInputSystem extends EntitySystem {
|
||||||
|
addMoveInput(x: number, y: number): void;
|
||||||
|
addActionInput(action: string): void;
|
||||||
|
clearInput(): void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 服务令牌
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
NetworkServiceToken,
|
||||||
|
NetworkSyncSystemToken,
|
||||||
|
NetworkSpawnSystemToken,
|
||||||
|
NetworkInputSystemToken
|
||||||
|
} from '@esengine/network';
|
||||||
|
|
||||||
|
// 使用
|
||||||
|
const networkService = services.get(NetworkServiceToken);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 服务器端 API
|
||||||
|
|
||||||
|
### GameServer
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class GameServer {
|
||||||
|
constructor(config: IGameServerConfig);
|
||||||
|
|
||||||
|
start(): Promise<void>;
|
||||||
|
stop(): Promise<void>;
|
||||||
|
|
||||||
|
getOrCreateRoom(roomId: string): Room;
|
||||||
|
getRoom(roomId: string): Room | undefined;
|
||||||
|
destroyRoom(roomId: string): void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Room
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class Room {
|
||||||
|
readonly id: string;
|
||||||
|
readonly playerCount: number;
|
||||||
|
readonly isFull: boolean;
|
||||||
|
|
||||||
|
addPlayer(name: string, connection: Connection): IPlayer | null;
|
||||||
|
removePlayer(clientId: number): void;
|
||||||
|
getPlayer(clientId: number): IPlayer | undefined;
|
||||||
|
handleInput(clientId: number, input: IPlayerInput): void;
|
||||||
|
destroy(): void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### IPlayer
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface IPlayer {
|
||||||
|
clientId: number;
|
||||||
|
name: string;
|
||||||
|
connection: Connection;
|
||||||
|
netId: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 协议消息
|
||||||
|
|
||||||
|
### MsgSync
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface MsgSync {
|
||||||
|
time: number;
|
||||||
|
entities: IEntityState[];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### MsgSpawn
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface MsgSpawn {
|
||||||
|
netId: number;
|
||||||
|
ownerId: number;
|
||||||
|
prefab: string;
|
||||||
|
pos: Vec2;
|
||||||
|
rot: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### MsgDespawn
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface MsgDespawn {
|
||||||
|
netId: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### IEntityState
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface IEntityState {
|
||||||
|
netId: number;
|
||||||
|
pos?: Vec2;
|
||||||
|
rot?: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 工具函数
|
||||||
|
|
||||||
|
### createSnapshotBuffer
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function createSnapshotBuffer<T>(config: {
|
||||||
|
maxSnapshots: number;
|
||||||
|
interpolationDelay: number;
|
||||||
|
}): ISnapshotBuffer<T>;
|
||||||
|
```
|
||||||
|
|
||||||
|
### createTransformInterpolator
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function createTransformInterpolator(): ITransformInterpolator;
|
||||||
|
```
|
||||||
|
|
||||||
|
### createHermiteTransformInterpolator
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function createHermiteTransformInterpolator(config: {
|
||||||
|
bufferSize: number;
|
||||||
|
}): IHermiteTransformInterpolator;
|
||||||
|
```
|
||||||
|
|
||||||
|
### createClientPrediction
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function createClientPrediction(config: {
|
||||||
|
maxPredictedInputs: number;
|
||||||
|
reconciliationThreshold: number;
|
||||||
|
}): IClientPrediction;
|
||||||
|
```
|
||||||
256
docs/src/content/docs/modules/network/client.md
Normal file
256
docs/src/content/docs/modules/network/client.md
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
---
|
||||||
|
title: "客户端使用"
|
||||||
|
description: "NetworkPlugin、组件和系统的客户端使用指南"
|
||||||
|
---
|
||||||
|
|
||||||
|
## NetworkPlugin
|
||||||
|
|
||||||
|
NetworkPlugin 是客户端网络功能的核心入口。
|
||||||
|
|
||||||
|
### 基本用法
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Core } from '@esengine/ecs-framework';
|
||||||
|
import { NetworkPlugin } from '@esengine/network';
|
||||||
|
|
||||||
|
// 创建并安装插件
|
||||||
|
const networkPlugin = new NetworkPlugin();
|
||||||
|
await Core.installPlugin(networkPlugin);
|
||||||
|
|
||||||
|
// 连接服务器
|
||||||
|
const success = await networkPlugin.connect('ws://localhost:3000', 'PlayerName');
|
||||||
|
|
||||||
|
// 断开连接
|
||||||
|
await networkPlugin.disconnect();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 属性和方法
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class NetworkPlugin {
|
||||||
|
readonly name: string;
|
||||||
|
readonly version: string;
|
||||||
|
|
||||||
|
// 访问器
|
||||||
|
get networkService(): NetworkService;
|
||||||
|
get syncSystem(): NetworkSyncSystem;
|
||||||
|
get spawnSystem(): NetworkSpawnSystem;
|
||||||
|
get inputSystem(): NetworkInputSystem;
|
||||||
|
get isConnected(): boolean;
|
||||||
|
|
||||||
|
// 连接服务器
|
||||||
|
connect(serverUrl: string, playerName: string, roomId?: string): Promise<boolean>;
|
||||||
|
|
||||||
|
// 断开连接
|
||||||
|
disconnect(): Promise<void>;
|
||||||
|
|
||||||
|
// 注册预制体工厂
|
||||||
|
registerPrefab(prefabType: string, factory: PrefabFactory): void;
|
||||||
|
|
||||||
|
// 发送输入
|
||||||
|
sendMoveInput(x: number, y: number): void;
|
||||||
|
sendActionInput(action: string): void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 组件
|
||||||
|
|
||||||
|
### NetworkIdentity
|
||||||
|
|
||||||
|
网络标识组件,每个网络同步的实体必须拥有:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class NetworkIdentity extends Component {
|
||||||
|
netId: number; // 网络唯一 ID
|
||||||
|
ownerId: number; // 所有者客户端 ID
|
||||||
|
bIsLocalPlayer: boolean; // 是否为本地玩家
|
||||||
|
bHasAuthority: boolean; // 是否有权限控制
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**使用示例:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
networkPlugin.registerPrefab('player', (scene, spawn) => {
|
||||||
|
const entity = scene.createEntity(`player_${spawn.netId}`);
|
||||||
|
|
||||||
|
const identity = entity.addComponent(new NetworkIdentity());
|
||||||
|
identity.netId = spawn.netId;
|
||||||
|
identity.ownerId = spawn.ownerId;
|
||||||
|
identity.bIsLocalPlayer = spawn.ownerId === networkPlugin.networkService.clientId;
|
||||||
|
|
||||||
|
return entity;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### NetworkTransform
|
||||||
|
|
||||||
|
网络变换组件,用于位置和旋转同步:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class NetworkTransform extends Component {
|
||||||
|
position: { x: number; y: number };
|
||||||
|
rotation: number;
|
||||||
|
velocity: { x: number; y: number };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 系统
|
||||||
|
|
||||||
|
### NetworkSyncSystem
|
||||||
|
|
||||||
|
处理服务器状态同步和插值:
|
||||||
|
|
||||||
|
- 接收服务器状态快照
|
||||||
|
- 将状态存入快照缓冲区
|
||||||
|
- 对远程实体进行插值平滑
|
||||||
|
|
||||||
|
### NetworkSpawnSystem
|
||||||
|
|
||||||
|
处理实体的网络生成和销毁:
|
||||||
|
|
||||||
|
- 监听 Spawn/Despawn 消息
|
||||||
|
- 使用注册的预制体工厂创建实体
|
||||||
|
- 管理网络实体的生命周期
|
||||||
|
|
||||||
|
### NetworkInputSystem
|
||||||
|
|
||||||
|
处理本地玩家输入的网络发送:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class NetworkInputSystem extends EntitySystem {
|
||||||
|
addMoveInput(x: number, y: number): void;
|
||||||
|
addActionInput(action: string): void;
|
||||||
|
clearInput(): void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**使用示例:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 方式 1:通过 NetworkPlugin(推荐)
|
||||||
|
networkPlugin.sendMoveInput(0, 1);
|
||||||
|
networkPlugin.sendActionInput('jump');
|
||||||
|
|
||||||
|
// 方式 2:直接使用 inputSystem
|
||||||
|
const inputSystem = networkPlugin.inputSystem;
|
||||||
|
inputSystem.addMoveInput(0, 1);
|
||||||
|
inputSystem.addActionInput('jump');
|
||||||
|
```
|
||||||
|
|
||||||
|
## 预制体工厂
|
||||||
|
|
||||||
|
预制体工厂用于创建网络实体:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type PrefabFactory = (scene: Scene, spawn: MsgSpawn) => Entity;
|
||||||
|
```
|
||||||
|
|
||||||
|
**完整示例:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 注册玩家预制体
|
||||||
|
networkPlugin.registerPrefab('player', (scene, spawn) => {
|
||||||
|
const entity = scene.createEntity(`player_${spawn.netId}`);
|
||||||
|
|
||||||
|
const identity = entity.addComponent(new NetworkIdentity());
|
||||||
|
identity.netId = spawn.netId;
|
||||||
|
identity.ownerId = spawn.ownerId;
|
||||||
|
identity.bIsLocalPlayer = spawn.ownerId === networkPlugin.networkService.clientId;
|
||||||
|
|
||||||
|
entity.addComponent(new NetworkTransform());
|
||||||
|
|
||||||
|
// 本地玩家添加输入组件
|
||||||
|
if (identity.bIsLocalPlayer) {
|
||||||
|
entity.addComponent(new LocalInputComponent());
|
||||||
|
}
|
||||||
|
|
||||||
|
return entity;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 注册敌人预制体
|
||||||
|
networkPlugin.registerPrefab('enemy', (scene, spawn) => {
|
||||||
|
const entity = scene.createEntity(`enemy_${spawn.netId}`);
|
||||||
|
|
||||||
|
const identity = entity.addComponent(new NetworkIdentity());
|
||||||
|
identity.netId = spawn.netId;
|
||||||
|
identity.ownerId = spawn.ownerId;
|
||||||
|
|
||||||
|
entity.addComponent(new NetworkTransform());
|
||||||
|
entity.addComponent(new EnemyComponent());
|
||||||
|
|
||||||
|
return entity;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 处理输入
|
||||||
|
|
||||||
|
创建自定义输入处理系统:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { EntitySystem, Matcher, Entity } from '@esengine/ecs-framework';
|
||||||
|
import { NetworkPlugin, NetworkIdentity } from '@esengine/network';
|
||||||
|
|
||||||
|
class LocalInputHandler extends EntitySystem {
|
||||||
|
private _networkPlugin: NetworkPlugin | null = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.empty().all(NetworkIdentity, LocalInputComponent));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onAddedToScene(): void {
|
||||||
|
this._networkPlugin = Core.getPlugin(NetworkPlugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected processEntity(entity: Entity, dt: number): void {
|
||||||
|
if (!this._networkPlugin) return;
|
||||||
|
|
||||||
|
const identity = entity.getComponent(NetworkIdentity)!;
|
||||||
|
if (!identity.bIsLocalPlayer) return;
|
||||||
|
|
||||||
|
// 读取键盘输入
|
||||||
|
let moveX = 0;
|
||||||
|
let moveY = 0;
|
||||||
|
|
||||||
|
if (keyboard.isPressed('A')) moveX -= 1;
|
||||||
|
if (keyboard.isPressed('D')) moveX += 1;
|
||||||
|
if (keyboard.isPressed('W')) moveY += 1;
|
||||||
|
if (keyboard.isPressed('S')) moveY -= 1;
|
||||||
|
|
||||||
|
if (moveX !== 0 || moveY !== 0) {
|
||||||
|
this._networkPlugin.sendMoveInput(moveX, moveY);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyboard.isJustPressed('Space')) {
|
||||||
|
this._networkPlugin.sendActionInput('jump');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 连接状态监听
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
networkPlugin.networkService.setCallbacks({
|
||||||
|
onConnected: (clientId, roomId) => {
|
||||||
|
console.log(`已连接: 客户端 ${clientId}, 房间 ${roomId}`);
|
||||||
|
},
|
||||||
|
onDisconnected: () => {
|
||||||
|
console.log('已断开');
|
||||||
|
// 处理重连逻辑
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('网络错误:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
1. **权限检查**:使用 `bHasAuthority` 检查是否有权限修改实体
|
||||||
|
|
||||||
|
2. **本地玩家标识**:通过 `bIsLocalPlayer` 区分本地和远程玩家
|
||||||
|
|
||||||
|
3. **预制体管理**:为每种网络实体类型注册对应的预制体工厂
|
||||||
|
|
||||||
|
4. **输入发送**:推荐使用 `NetworkPlugin.sendMoveInput()` 和 `sendActionInput()` 方法
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "网络同步系统 (Network)"
|
title: "网络同步系统 (Network)"
|
||||||
|
description: "基于 TSRPC 的多人游戏网络同步解决方案"
|
||||||
---
|
---
|
||||||
|
|
||||||
`@esengine/network` 提供基于 TSRPC 的客户端-服务器网络同步解决方案,用于多人游戏的实体同步、输入处理和状态插值。
|
`@esengine/network` 提供基于 TSRPC 的客户端-服务器网络同步解决方案,用于多人游戏的实体同步、输入处理和状态插值。
|
||||||
@@ -24,49 +25,17 @@ npm install @esengine/network
|
|||||||
npm install @esengine/network-server
|
npm install @esengine/network-server
|
||||||
```
|
```
|
||||||
|
|
||||||
## 使用 CLI 快速创建服务端
|
## 架构
|
||||||
|
|
||||||
推荐使用 ESEngine CLI 快速创建完整的游戏服务端项目:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 创建项目目录
|
|
||||||
mkdir my-game-server && cd my-game-server
|
|
||||||
npm init -y
|
|
||||||
|
|
||||||
# 使用 CLI 初始化 Node.js 服务端
|
|
||||||
npx @esengine/cli init -p nodejs
|
|
||||||
```
|
|
||||||
|
|
||||||
CLI 会自动生成以下项目结构:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
my-game-server/
|
客户端 服务器
|
||||||
├── src/
|
┌────────────────┐ ┌────────────────┐
|
||||||
│ ├── index.ts # 入口文件
|
│ NetworkPlugin │◄──── WS ────► │ GameServer │
|
||||||
│ ├── server/
|
│ ├─ Service │ │ ├─ Room │
|
||||||
│ │ └── GameServer.ts # 网络服务器配置
|
│ ├─ SyncSystem │ │ └─ Players │
|
||||||
│ └── game/
|
│ ├─ SpawnSystem │ └────────────────┘
|
||||||
│ ├── Game.ts # ECS 游戏主类
|
│ └─ InputSystem │
|
||||||
│ ├── scenes/
|
└────────────────┘
|
||||||
│ │ └── MainScene.ts # 主场景
|
|
||||||
│ ├── components/ # ECS 组件
|
|
||||||
│ │ ├── PositionComponent.ts
|
|
||||||
│ │ └── VelocityComponent.ts
|
|
||||||
│ └── systems/ # ECS 系统
|
|
||||||
│ └── MovementSystem.ts
|
|
||||||
├── tsconfig.json
|
|
||||||
├── package.json
|
|
||||||
└── README.md
|
|
||||||
```
|
|
||||||
|
|
||||||
启动服务端:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 开发模式(热重载)
|
|
||||||
npm run dev
|
|
||||||
|
|
||||||
# 生产模式
|
|
||||||
npm run start
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
@@ -75,21 +44,16 @@ npm run start
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Core, Scene } from '@esengine/ecs-framework';
|
import { Core, Scene } from '@esengine/ecs-framework';
|
||||||
import {
|
import { NetworkPlugin, NetworkIdentity, NetworkTransform } from '@esengine/network';
|
||||||
NetworkPlugin,
|
|
||||||
NetworkIdentity,
|
|
||||||
NetworkTransform
|
|
||||||
} from '@esengine/network';
|
|
||||||
|
|
||||||
// 定义游戏场景
|
// 定义游戏场景
|
||||||
class GameScene extends Scene {
|
class GameScene extends Scene {
|
||||||
initialize(): void {
|
initialize(): void {
|
||||||
this.name = 'Game';
|
this.name = 'Game';
|
||||||
// 网络系统由 NetworkPlugin 自动添加
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化 Core
|
// 初始化
|
||||||
Core.create({ debug: false });
|
Core.create({ debug: false });
|
||||||
const scene = new GameScene();
|
const scene = new GameScene();
|
||||||
Core.setScene(scene);
|
Core.setScene(scene);
|
||||||
@@ -105,7 +69,7 @@ networkPlugin.registerPrefab('player', (scene, spawn) => {
|
|||||||
const identity = entity.addComponent(new NetworkIdentity());
|
const identity = entity.addComponent(new NetworkIdentity());
|
||||||
identity.netId = spawn.netId;
|
identity.netId = spawn.netId;
|
||||||
identity.ownerId = spawn.ownerId;
|
identity.ownerId = spawn.ownerId;
|
||||||
identity.bIsLocalPlayer = spawn.ownerId === networkPlugin.networkService.localClientId;
|
identity.bIsLocalPlayer = spawn.ownerId === networkPlugin.networkService.clientId;
|
||||||
|
|
||||||
entity.addComponent(new NetworkTransform());
|
entity.addComponent(new NetworkTransform());
|
||||||
return entity;
|
return entity;
|
||||||
@@ -116,20 +80,10 @@ const success = await networkPlugin.connect('ws://localhost:3000', 'PlayerName')
|
|||||||
if (success) {
|
if (success) {
|
||||||
console.log('Connected!');
|
console.log('Connected!');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 游戏循环
|
|
||||||
function gameLoop(dt: number) {
|
|
||||||
Core.update(dt);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 断开连接
|
|
||||||
await networkPlugin.disconnect();
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 服务器端
|
### 服务器端
|
||||||
|
|
||||||
使用 CLI 创建服务端项目后,默认生成的代码已经配置好了 GameServer:
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { GameServer } from '@esengine/network-server';
|
import { GameServer } from '@esengine/network-server';
|
||||||
|
|
||||||
@@ -145,436 +99,39 @@ await server.start();
|
|||||||
console.log('Server started on ws://localhost:3000');
|
console.log('Server started on ws://localhost:3000');
|
||||||
```
|
```
|
||||||
|
|
||||||
## 核心概念
|
## 使用 CLI 快速创建
|
||||||
|
|
||||||
### 架构
|
推荐使用 ESEngine CLI 快速创建完整的游戏服务端项目:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
客户端 服务器
|
mkdir my-game-server && cd my-game-server
|
||||||
┌────────────────┐ ┌────────────────┐
|
npm init -y
|
||||||
│ NetworkPlugin │◄──── WS ────► │ GameServer │
|
npx @esengine/cli init -p nodejs
|
||||||
│ ├─ Service │ │ ├─ Room │
|
|
||||||
│ ├─ SyncSystem │ │ └─ Players │
|
|
||||||
│ ├─ SpawnSystem │ └────────────────┘
|
|
||||||
│ └─ InputSystem │
|
|
||||||
└────────────────┘
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 组件
|
生成的项目结构:
|
||||||
|
|
||||||
#### NetworkIdentity
|
```
|
||||||
|
my-game-server/
|
||||||
网络标识组件,每个网络同步的实体必须拥有:
|
├── src/
|
||||||
|
│ ├── index.ts
|
||||||
```typescript
|
│ ├── server/
|
||||||
class NetworkIdentity extends Component {
|
│ │ └── GameServer.ts
|
||||||
netId: number; // 网络唯一 ID
|
│ └── game/
|
||||||
ownerId: number; // 所有者客户端 ID
|
│ ├── Game.ts
|
||||||
bIsLocalPlayer: boolean; // 是否为本地玩家
|
│ ├── scenes/
|
||||||
bHasAuthority: boolean; // 是否有权限控制
|
│ ├── components/
|
||||||
}
|
│ └── systems/
|
||||||
|
├── tsconfig.json
|
||||||
|
└── package.json
|
||||||
```
|
```
|
||||||
|
|
||||||
#### NetworkTransform
|
## 文档导航
|
||||||
|
|
||||||
网络变换组件,用于位置和旋转同步:
|
- [客户端使用](/modules/network/client/) - NetworkPlugin、组件和系统
|
||||||
|
- [服务器端](/modules/network/server/) - GameServer 和 Room 管理
|
||||||
```typescript
|
- [状态同步](/modules/network/sync/) - 插值、预测和快照
|
||||||
class NetworkTransform extends Component {
|
- [API 参考](/modules/network/api/) - 完整 API 文档
|
||||||
position: { x: number; y: number };
|
|
||||||
rotation: number;
|
|
||||||
velocity: { x: number; y: number };
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 系统
|
|
||||||
|
|
||||||
#### NetworkSyncSystem
|
|
||||||
|
|
||||||
处理服务器状态同步和插值:
|
|
||||||
|
|
||||||
- 接收服务器状态快照
|
|
||||||
- 将状态存入快照缓冲区
|
|
||||||
- 对远程实体进行插值平滑
|
|
||||||
|
|
||||||
#### NetworkSpawnSystem
|
|
||||||
|
|
||||||
处理实体的网络生成和销毁:
|
|
||||||
|
|
||||||
- 监听 Spawn/Despawn 消息
|
|
||||||
- 使用注册的预制体工厂创建实体
|
|
||||||
- 管理网络实体的生命周期
|
|
||||||
|
|
||||||
#### NetworkInputSystem
|
|
||||||
|
|
||||||
处理本地玩家输入的网络发送:
|
|
||||||
|
|
||||||
- 收集本地玩家输入
|
|
||||||
- 发送输入到服务器
|
|
||||||
- 支持移动和动作输入
|
|
||||||
|
|
||||||
## API 参考
|
|
||||||
|
|
||||||
### NetworkPlugin
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
class NetworkPlugin {
|
|
||||||
constructor(config: INetworkPluginConfig);
|
|
||||||
|
|
||||||
// 安装插件
|
|
||||||
install(services: ServiceContainer): void;
|
|
||||||
|
|
||||||
// 连接服务器
|
|
||||||
connect(playerName: string, roomId?: string): Promise<void>;
|
|
||||||
|
|
||||||
// 断开连接
|
|
||||||
disconnect(): void;
|
|
||||||
|
|
||||||
// 注册预制体工厂
|
|
||||||
registerPrefab(prefab: string, factory: PrefabFactory): void;
|
|
||||||
|
|
||||||
// 属性
|
|
||||||
readonly localPlayerId: number | null;
|
|
||||||
readonly isConnected: boolean;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**配置选项:**
|
|
||||||
|
|
||||||
| 属性 | 类型 | 必需 | 描述 |
|
|
||||||
|------|------|------|------|
|
|
||||||
| `serverUrl` | `string` | 是 | WebSocket 服务器地址 |
|
|
||||||
|
|
||||||
### NetworkService
|
|
||||||
|
|
||||||
网络服务,管理 WebSocket 连接:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
class NetworkService {
|
|
||||||
// 连接状态
|
|
||||||
readonly state: ENetworkState;
|
|
||||||
readonly isConnected: boolean;
|
|
||||||
readonly clientId: number | null;
|
|
||||||
readonly roomId: string | null;
|
|
||||||
|
|
||||||
// 连接控制
|
|
||||||
connect(serverUrl: string): Promise<void>;
|
|
||||||
disconnect(): void;
|
|
||||||
|
|
||||||
// 加入房间
|
|
||||||
join(playerName: string, roomId?: string): Promise<ResJoin>;
|
|
||||||
|
|
||||||
// 发送输入
|
|
||||||
sendInput(input: IPlayerInput): void;
|
|
||||||
|
|
||||||
// 事件回调
|
|
||||||
setCallbacks(callbacks: Partial<INetworkCallbacks>): void;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**网络状态枚举:**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
enum ENetworkState {
|
|
||||||
Disconnected = 'disconnected',
|
|
||||||
Connecting = 'connecting',
|
|
||||||
Connected = 'connected',
|
|
||||||
Joining = 'joining',
|
|
||||||
Joined = 'joined'
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**回调接口:**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface INetworkCallbacks {
|
|
||||||
onConnected?: () => void;
|
|
||||||
onDisconnected?: () => void;
|
|
||||||
onJoined?: (clientId: number, roomId: string) => void;
|
|
||||||
onSync?: (msg: MsgSync) => void;
|
|
||||||
onSpawn?: (msg: MsgSpawn) => void;
|
|
||||||
onDespawn?: (msg: MsgDespawn) => void;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 预制体工厂
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
type PrefabFactory = (scene: Scene, spawn: MsgSpawn) => Entity;
|
|
||||||
```
|
|
||||||
|
|
||||||
注册预制体工厂用于网络实体的创建:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
networkPlugin.registerPrefab('enemy', (scene, spawn) => {
|
|
||||||
const entity = scene.createEntity(`enemy_${spawn.netId}`);
|
|
||||||
|
|
||||||
const identity = entity.addComponent(new NetworkIdentity());
|
|
||||||
identity.netId = spawn.netId;
|
|
||||||
identity.ownerId = spawn.ownerId;
|
|
||||||
|
|
||||||
entity.addComponent(new NetworkTransform());
|
|
||||||
entity.addComponent(new EnemyComponent());
|
|
||||||
return entity;
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### 输入系统
|
|
||||||
|
|
||||||
#### NetworkInputSystem
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
class NetworkInputSystem extends EntitySystem {
|
|
||||||
// 添加移动输入
|
|
||||||
addMoveInput(x: number, y: number): void;
|
|
||||||
|
|
||||||
// 添加动作输入
|
|
||||||
addActionInput(action: string): void;
|
|
||||||
|
|
||||||
// 清除输入
|
|
||||||
clearInput(): void;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
使用示例:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 通过 NetworkPlugin 发送输入(推荐)
|
|
||||||
networkPlugin.sendMoveInput(0, 1); // 移动
|
|
||||||
networkPlugin.sendActionInput('jump'); // 动作
|
|
||||||
|
|
||||||
// 或直接使用 inputSystem
|
|
||||||
const inputSystem = networkPlugin.inputSystem;
|
|
||||||
if (keyboard.isPressed('W')) {
|
|
||||||
inputSystem.addMoveInput(0, 1);
|
|
||||||
}
|
|
||||||
if (keyboard.isPressed('Space')) {
|
|
||||||
inputSystem.addActionInput('jump');
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 状态同步
|
|
||||||
|
|
||||||
### 快照缓冲区
|
|
||||||
|
|
||||||
用于存储服务器状态快照并进行插值:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { createSnapshotBuffer, type IStateSnapshot } from '@esengine/network';
|
|
||||||
|
|
||||||
const buffer = createSnapshotBuffer<IStateSnapshot>({
|
|
||||||
maxSnapshots: 30, // 最大快照数
|
|
||||||
interpolationDelay: 100 // 插值延迟 (ms)
|
|
||||||
});
|
|
||||||
|
|
||||||
// 添加快照
|
|
||||||
buffer.addSnapshot({
|
|
||||||
time: serverTime,
|
|
||||||
entities: states
|
|
||||||
});
|
|
||||||
|
|
||||||
// 获取插值状态
|
|
||||||
const interpolated = buffer.getInterpolatedState(clientTime);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 变换插值器
|
|
||||||
|
|
||||||
#### 线性插值器
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { createTransformInterpolator } from '@esengine/network';
|
|
||||||
|
|
||||||
const interpolator = createTransformInterpolator();
|
|
||||||
|
|
||||||
// 添加状态
|
|
||||||
interpolator.addState(time, { x: 0, y: 0, rotation: 0 });
|
|
||||||
|
|
||||||
// 获取插值结果
|
|
||||||
const state = interpolator.getInterpolatedState(currentTime);
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Hermite 插值器
|
|
||||||
|
|
||||||
使用 Hermite 样条实现更平滑的插值:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { createHermiteTransformInterpolator } from '@esengine/network';
|
|
||||||
|
|
||||||
const interpolator = createHermiteTransformInterpolator({
|
|
||||||
bufferSize: 10
|
|
||||||
});
|
|
||||||
|
|
||||||
// 添加带速度的状态
|
|
||||||
interpolator.addState(time, {
|
|
||||||
x: 100,
|
|
||||||
y: 200,
|
|
||||||
rotation: 0,
|
|
||||||
vx: 5,
|
|
||||||
vy: 0
|
|
||||||
});
|
|
||||||
|
|
||||||
// 获取平滑的插值结果
|
|
||||||
const state = interpolator.getInterpolatedState(currentTime);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 客户端预测
|
|
||||||
|
|
||||||
实现客户端预测和服务器校正:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { createClientPrediction } from '@esengine/network';
|
|
||||||
|
|
||||||
const prediction = createClientPrediction({
|
|
||||||
maxPredictedInputs: 60,
|
|
||||||
reconciliationThreshold: 0.1
|
|
||||||
});
|
|
||||||
|
|
||||||
// 预测输入
|
|
||||||
const seq = prediction.predict(inputState, currentState, (state, input) => {
|
|
||||||
// 应用输入到状态
|
|
||||||
return applyInput(state, input);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 服务器校正
|
|
||||||
const corrected = prediction.reconcile(
|
|
||||||
serverState,
|
|
||||||
serverSeq,
|
|
||||||
(state, input) => applyInput(state, input)
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
## 服务器端
|
|
||||||
|
|
||||||
### GameServer
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { GameServer } from '@esengine/network-server';
|
|
||||||
|
|
||||||
const server = new GameServer({
|
|
||||||
port: 3000,
|
|
||||||
roomConfig: {
|
|
||||||
maxPlayers: 16, // 房间最大玩家数
|
|
||||||
tickRate: 20 // 同步频率 (Hz)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 启动服务器
|
|
||||||
await server.start();
|
|
||||||
|
|
||||||
// 获取房间
|
|
||||||
const room = server.getOrCreateRoom('room-id');
|
|
||||||
|
|
||||||
// 停止服务器
|
|
||||||
await server.stop();
|
|
||||||
```
|
|
||||||
|
|
||||||
### Room
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
class Room {
|
|
||||||
readonly id: string;
|
|
||||||
readonly playerCount: number;
|
|
||||||
readonly isFull: boolean;
|
|
||||||
|
|
||||||
// 添加玩家
|
|
||||||
addPlayer(name: string, connection: Connection): IPlayer | null;
|
|
||||||
|
|
||||||
// 移除玩家
|
|
||||||
removePlayer(clientId: number): void;
|
|
||||||
|
|
||||||
// 获取玩家
|
|
||||||
getPlayer(clientId: number): IPlayer | undefined;
|
|
||||||
|
|
||||||
// 处理输入
|
|
||||||
handleInput(clientId: number, input: IPlayerInput): void;
|
|
||||||
|
|
||||||
// 销毁房间
|
|
||||||
destroy(): void;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**玩家接口:**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface IPlayer {
|
|
||||||
clientId: number; // 客户端 ID
|
|
||||||
name: string; // 玩家名称
|
|
||||||
connection: Connection; // 连接对象
|
|
||||||
netId: number; // 网络实体 ID
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 协议类型
|
|
||||||
|
|
||||||
### 消息类型
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 状态同步消息
|
|
||||||
interface MsgSync {
|
|
||||||
time: number;
|
|
||||||
entities: IEntityState[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 实体状态
|
|
||||||
interface IEntityState {
|
|
||||||
netId: number;
|
|
||||||
pos?: Vec2;
|
|
||||||
rot?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成消息
|
|
||||||
interface MsgSpawn {
|
|
||||||
netId: number;
|
|
||||||
ownerId: number;
|
|
||||||
prefab: string;
|
|
||||||
pos: Vec2;
|
|
||||||
rot: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 销毁消息
|
|
||||||
interface MsgDespawn {
|
|
||||||
netId: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 输入消息
|
|
||||||
interface MsgInput {
|
|
||||||
input: IPlayerInput;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 玩家输入
|
|
||||||
interface IPlayerInput {
|
|
||||||
seq?: number;
|
|
||||||
moveDir?: Vec2;
|
|
||||||
actions?: string[];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### API 类型
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 加入请求
|
|
||||||
interface ReqJoin {
|
|
||||||
playerName: string;
|
|
||||||
roomId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加入响应
|
|
||||||
interface ResJoin {
|
|
||||||
clientId: number;
|
|
||||||
roomId: string;
|
|
||||||
playerCount: number;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 蓝图节点
|
|
||||||
|
|
||||||
网络模块提供了可视化脚本支持的蓝图节点:
|
|
||||||
|
|
||||||
- `IsLocalPlayer` - 检查实体是否为本地玩家
|
|
||||||
- `IsServer` - 检查是否运行在服务器端
|
|
||||||
- `HasAuthority` - 检查是否有权限控制实体
|
|
||||||
- `GetNetworkId` - 获取实体的网络 ID
|
|
||||||
- `GetLocalPlayerId` - 获取本地玩家 ID
|
|
||||||
|
|
||||||
## 服务令牌
|
## 服务令牌
|
||||||
|
|
||||||
@@ -588,142 +145,15 @@ import {
|
|||||||
NetworkInputSystemToken
|
NetworkInputSystemToken
|
||||||
} from '@esengine/network';
|
} from '@esengine/network';
|
||||||
|
|
||||||
// 获取服务
|
|
||||||
const networkService = services.get(NetworkServiceToken);
|
const networkService = services.get(NetworkServiceToken);
|
||||||
```
|
```
|
||||||
|
|
||||||
## 实际示例
|
## 蓝图节点
|
||||||
|
|
||||||
### 完整的多人游戏客户端
|
网络模块提供可视化脚本支持:
|
||||||
|
|
||||||
```typescript
|
- `IsLocalPlayer` - 检查实体是否为本地玩家
|
||||||
import { Core, Scene, EntitySystem, Matcher, Entity } from '@esengine/ecs-framework';
|
- `IsServer` - 检查是否运行在服务器端
|
||||||
import {
|
- `HasAuthority` - 检查是否有权限控制实体
|
||||||
NetworkPlugin,
|
- `GetNetworkId` - 获取实体的网络 ID
|
||||||
NetworkIdentity,
|
- `GetLocalPlayerId` - 获取本地玩家 ID
|
||||||
NetworkTransform
|
|
||||||
} from '@esengine/network';
|
|
||||||
|
|
||||||
// 定义游戏场景
|
|
||||||
class GameScene extends Scene {
|
|
||||||
initialize(): void {
|
|
||||||
this.name = 'MultiplayerGame';
|
|
||||||
// 网络系统由 NetworkPlugin 自动添加
|
|
||||||
// 添加自定义系统
|
|
||||||
this.addSystem(new LocalInputHandler());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化
|
|
||||||
async function initGame() {
|
|
||||||
Core.create({ debug: false });
|
|
||||||
|
|
||||||
const scene = new GameScene();
|
|
||||||
Core.setScene(scene);
|
|
||||||
|
|
||||||
// 安装网络插件
|
|
||||||
const networkPlugin = new NetworkPlugin();
|
|
||||||
await Core.installPlugin(networkPlugin);
|
|
||||||
|
|
||||||
// 注册玩家预制体
|
|
||||||
networkPlugin.registerPrefab('player', (scene, spawn) => {
|
|
||||||
const entity = scene.createEntity(`player_${spawn.netId}`);
|
|
||||||
|
|
||||||
const identity = entity.addComponent(new NetworkIdentity());
|
|
||||||
identity.netId = spawn.netId;
|
|
||||||
identity.ownerId = spawn.ownerId;
|
|
||||||
identity.bIsLocalPlayer = spawn.ownerId === networkPlugin.networkService.localClientId;
|
|
||||||
|
|
||||||
entity.addComponent(new NetworkTransform());
|
|
||||||
|
|
||||||
// 如果是本地玩家,添加输入标记
|
|
||||||
if (identity.bIsLocalPlayer) {
|
|
||||||
entity.addComponent(new LocalInputComponent());
|
|
||||||
}
|
|
||||||
|
|
||||||
return entity;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 连接服务器
|
|
||||||
const success = await networkPlugin.connect('ws://localhost:3000', 'Player1');
|
|
||||||
if (success) {
|
|
||||||
console.log('已连接!');
|
|
||||||
} else {
|
|
||||||
console.error('连接失败');
|
|
||||||
}
|
|
||||||
|
|
||||||
return networkPlugin;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 游戏循环
|
|
||||||
function gameLoop(deltaTime: number) {
|
|
||||||
Core.update(deltaTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
initGame();
|
|
||||||
```
|
|
||||||
|
|
||||||
### 处理输入
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
class LocalInputHandler extends EntitySystem {
|
|
||||||
private _networkPlugin: NetworkPlugin | null = null;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super(Matcher.empty().all(NetworkIdentity, LocalInputComponent));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected onAddedToScene(): void {
|
|
||||||
// 获取 NetworkPlugin 引用
|
|
||||||
this._networkPlugin = Core.getPlugin(NetworkPlugin);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected processEntity(entity: Entity, dt: number): void {
|
|
||||||
if (!this._networkPlugin) return;
|
|
||||||
|
|
||||||
const identity = entity.getComponent(NetworkIdentity)!;
|
|
||||||
if (!identity.bIsLocalPlayer) return;
|
|
||||||
|
|
||||||
// 读取键盘输入
|
|
||||||
let moveX = 0;
|
|
||||||
let moveY = 0;
|
|
||||||
|
|
||||||
if (keyboard.isPressed('A')) moveX -= 1;
|
|
||||||
if (keyboard.isPressed('D')) moveX += 1;
|
|
||||||
if (keyboard.isPressed('W')) moveY += 1;
|
|
||||||
if (keyboard.isPressed('S')) moveY -= 1;
|
|
||||||
|
|
||||||
if (moveX !== 0 || moveY !== 0) {
|
|
||||||
this._networkPlugin.sendMoveInput(moveX, moveY);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keyboard.isJustPressed('Space')) {
|
|
||||||
this._networkPlugin.sendActionInput('jump');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 最佳实践
|
|
||||||
|
|
||||||
1. **合理设置同步频率**:根据游戏类型选择合适的 `tickRate`,动作游戏通常需要 20-60 Hz
|
|
||||||
|
|
||||||
2. **使用插值延迟**:设置适当的 `interpolationDelay` 来平衡延迟和平滑度
|
|
||||||
|
|
||||||
3. **客户端预测**:对于本地玩家使用客户端预测减少输入延迟
|
|
||||||
|
|
||||||
4. **预制体管理**:为每种网络实体类型注册对应的预制体工厂
|
|
||||||
|
|
||||||
5. **权限检查**:使用 `bHasAuthority` 检查是否有权限修改实体
|
|
||||||
|
|
||||||
6. **连接状态**:监听连接状态变化,处理断线重连
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
networkService.setCallbacks({
|
|
||||||
onConnected: () => console.log('已连接'),
|
|
||||||
onDisconnected: () => {
|
|
||||||
console.log('已断开');
|
|
||||||
// 处理重连逻辑
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|||||||
207
docs/src/content/docs/modules/network/server.md
Normal file
207
docs/src/content/docs/modules/network/server.md
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
---
|
||||||
|
title: "服务器端"
|
||||||
|
description: "GameServer 和 Room 管理"
|
||||||
|
---
|
||||||
|
|
||||||
|
## GameServer
|
||||||
|
|
||||||
|
GameServer 是服务器端的核心类,管理 WebSocket 连接和房间。
|
||||||
|
|
||||||
|
### 基本用法
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { GameServer } from '@esengine/network-server';
|
||||||
|
|
||||||
|
const server = new GameServer({
|
||||||
|
port: 3000,
|
||||||
|
roomConfig: {
|
||||||
|
maxPlayers: 16,
|
||||||
|
tickRate: 20
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 启动服务器
|
||||||
|
await server.start();
|
||||||
|
console.log('Server started on ws://localhost:3000');
|
||||||
|
|
||||||
|
// 停止服务器
|
||||||
|
await server.stop();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置选项
|
||||||
|
|
||||||
|
| 属性 | 类型 | 描述 |
|
||||||
|
|------|------|------|
|
||||||
|
| `port` | `number` | WebSocket 端口 |
|
||||||
|
| `roomConfig.maxPlayers` | `number` | 房间最大玩家数 |
|
||||||
|
| `roomConfig.tickRate` | `number` | 同步频率 (Hz) |
|
||||||
|
|
||||||
|
### 房间管理
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 获取或创建房间
|
||||||
|
const room = server.getOrCreateRoom('room-id');
|
||||||
|
|
||||||
|
// 获取已存在的房间
|
||||||
|
const existingRoom = server.getRoom('room-id');
|
||||||
|
|
||||||
|
// 销毁房间
|
||||||
|
server.destroyRoom('room-id');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Room
|
||||||
|
|
||||||
|
Room 类管理单个游戏房间的玩家和状态。
|
||||||
|
|
||||||
|
### API
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class Room {
|
||||||
|
readonly id: string;
|
||||||
|
readonly playerCount: number;
|
||||||
|
readonly isFull: boolean;
|
||||||
|
|
||||||
|
// 添加玩家
|
||||||
|
addPlayer(name: string, connection: Connection): IPlayer | null;
|
||||||
|
|
||||||
|
// 移除玩家
|
||||||
|
removePlayer(clientId: number): void;
|
||||||
|
|
||||||
|
// 获取玩家
|
||||||
|
getPlayer(clientId: number): IPlayer | undefined;
|
||||||
|
|
||||||
|
// 处理输入
|
||||||
|
handleInput(clientId: number, input: IPlayerInput): void;
|
||||||
|
|
||||||
|
// 销毁房间
|
||||||
|
destroy(): void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 玩家接口
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface IPlayer {
|
||||||
|
clientId: number; // 客户端 ID
|
||||||
|
name: string; // 玩家名称
|
||||||
|
connection: Connection; // 连接对象
|
||||||
|
netId: number; // 网络实体 ID
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 协议类型
|
||||||
|
|
||||||
|
### 消息类型
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 状态同步消息
|
||||||
|
interface MsgSync {
|
||||||
|
time: number;
|
||||||
|
entities: IEntityState[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 实体状态
|
||||||
|
interface IEntityState {
|
||||||
|
netId: number;
|
||||||
|
pos?: Vec2;
|
||||||
|
rot?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成消息
|
||||||
|
interface MsgSpawn {
|
||||||
|
netId: number;
|
||||||
|
ownerId: number;
|
||||||
|
prefab: string;
|
||||||
|
pos: Vec2;
|
||||||
|
rot: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 销毁消息
|
||||||
|
interface MsgDespawn {
|
||||||
|
netId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 输入消息
|
||||||
|
interface MsgInput {
|
||||||
|
input: IPlayerInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 玩家输入
|
||||||
|
interface IPlayerInput {
|
||||||
|
seq?: number;
|
||||||
|
moveDir?: Vec2;
|
||||||
|
actions?: string[];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### API 类型
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 加入请求
|
||||||
|
interface ReqJoin {
|
||||||
|
playerName: string;
|
||||||
|
roomId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加入响应
|
||||||
|
interface ResJoin {
|
||||||
|
clientId: number;
|
||||||
|
roomId: string;
|
||||||
|
playerCount: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用 CLI 创建服务端
|
||||||
|
|
||||||
|
推荐使用 ESEngine CLI 快速创建完整的游戏服务端:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir my-game-server && cd my-game-server
|
||||||
|
npm init -y
|
||||||
|
npx @esengine/cli init -p nodejs
|
||||||
|
```
|
||||||
|
|
||||||
|
生成的项目结构:
|
||||||
|
|
||||||
|
```
|
||||||
|
my-game-server/
|
||||||
|
├── src/
|
||||||
|
│ ├── index.ts # 入口文件
|
||||||
|
│ ├── server/
|
||||||
|
│ │ └── GameServer.ts # 网络服务器配置
|
||||||
|
│ └── game/
|
||||||
|
│ ├── Game.ts # ECS 游戏主类
|
||||||
|
│ ├── scenes/
|
||||||
|
│ │ └── MainScene.ts # 主场景
|
||||||
|
│ ├── components/ # ECS 组件
|
||||||
|
│ │ ├── PositionComponent.ts
|
||||||
|
│ │ └── VelocityComponent.ts
|
||||||
|
│ └── systems/ # ECS 系统
|
||||||
|
│ └── MovementSystem.ts
|
||||||
|
├── tsconfig.json
|
||||||
|
├── package.json
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
启动服务端:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 开发模式(热重载)
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# 生产模式
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
1. **合理设置同步频率**:根据游戏类型选择合适的 `tickRate`
|
||||||
|
- 回合制游戏:5-10 Hz
|
||||||
|
- 休闲游戏:10-20 Hz
|
||||||
|
- 动作游戏:20-60 Hz
|
||||||
|
|
||||||
|
2. **房间大小控制**:根据服务器性能设置合理的 `maxPlayers`
|
||||||
|
|
||||||
|
3. **连接管理**:监听玩家连接/断开事件,处理异常情况
|
||||||
|
|
||||||
|
4. **状态验证**:服务器应验证客户端输入,防止作弊
|
||||||
174
docs/src/content/docs/modules/network/sync.md
Normal file
174
docs/src/content/docs/modules/network/sync.md
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
---
|
||||||
|
title: "状态同步"
|
||||||
|
description: "插值、预测和快照缓冲区"
|
||||||
|
---
|
||||||
|
|
||||||
|
## 快照缓冲区
|
||||||
|
|
||||||
|
用于存储服务器状态快照并进行插值:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createSnapshotBuffer, type IStateSnapshot } from '@esengine/network';
|
||||||
|
|
||||||
|
const buffer = createSnapshotBuffer<IStateSnapshot>({
|
||||||
|
maxSnapshots: 30, // 最大快照数
|
||||||
|
interpolationDelay: 100 // 插值延迟 (ms)
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加快照
|
||||||
|
buffer.addSnapshot({
|
||||||
|
time: serverTime,
|
||||||
|
entities: states
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取插值状态
|
||||||
|
const interpolated = buffer.getInterpolatedState(clientTime);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置选项
|
||||||
|
|
||||||
|
| 属性 | 类型 | 描述 |
|
||||||
|
|------|------|------|
|
||||||
|
| `maxSnapshots` | `number` | 缓冲区最大快照数 |
|
||||||
|
| `interpolationDelay` | `number` | 插值延迟(毫秒) |
|
||||||
|
|
||||||
|
## 变换插值器
|
||||||
|
|
||||||
|
### 线性插值器
|
||||||
|
|
||||||
|
适用于简单的位置插值:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createTransformInterpolator } from '@esengine/network';
|
||||||
|
|
||||||
|
const interpolator = createTransformInterpolator();
|
||||||
|
|
||||||
|
// 添加状态
|
||||||
|
interpolator.addState(time, { x: 0, y: 0, rotation: 0 });
|
||||||
|
|
||||||
|
// 获取插值结果
|
||||||
|
const state = interpolator.getInterpolatedState(currentTime);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hermite 插值器
|
||||||
|
|
||||||
|
使用 Hermite 样条实现更平滑的插值,适合需要考虑速度的场景:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createHermiteTransformInterpolator } from '@esengine/network';
|
||||||
|
|
||||||
|
const interpolator = createHermiteTransformInterpolator({
|
||||||
|
bufferSize: 10
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加带速度的状态
|
||||||
|
interpolator.addState(time, {
|
||||||
|
x: 100,
|
||||||
|
y: 200,
|
||||||
|
rotation: 0,
|
||||||
|
vx: 5,
|
||||||
|
vy: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取平滑的插值结果
|
||||||
|
const state = interpolator.getInterpolatedState(currentTime);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 插值器对比
|
||||||
|
|
||||||
|
| 类型 | 优点 | 缺点 | 适用场景 |
|
||||||
|
|------|------|------|---------|
|
||||||
|
| 线性插值 | 简单、计算快 | 可能不平滑 | 简单移动 |
|
||||||
|
| Hermite 插值 | 平滑、考虑速度 | 计算量较大 | 高速移动 |
|
||||||
|
|
||||||
|
## 客户端预测
|
||||||
|
|
||||||
|
实现客户端预测和服务器校正,减少输入延迟:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createClientPrediction } from '@esengine/network';
|
||||||
|
|
||||||
|
const prediction = createClientPrediction({
|
||||||
|
maxPredictedInputs: 60,
|
||||||
|
reconciliationThreshold: 0.1
|
||||||
|
});
|
||||||
|
|
||||||
|
// 预测输入
|
||||||
|
const seq = prediction.predict(inputState, currentState, (state, input) => {
|
||||||
|
// 应用输入到状态
|
||||||
|
return applyInput(state, input);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 服务器校正
|
||||||
|
const corrected = prediction.reconcile(
|
||||||
|
serverState,
|
||||||
|
serverSeq,
|
||||||
|
(state, input) => applyInput(state, input)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 预测配置
|
||||||
|
|
||||||
|
| 属性 | 类型 | 描述 |
|
||||||
|
|------|------|------|
|
||||||
|
| `maxPredictedInputs` | `number` | 最大预测输入数 |
|
||||||
|
| `reconciliationThreshold` | `number` | 校正阈值 |
|
||||||
|
|
||||||
|
### 工作流程
|
||||||
|
|
||||||
|
```
|
||||||
|
客户端 服务器
|
||||||
|
│ │
|
||||||
|
├─ 1. 本地预测输入 ──────────────────►
|
||||||
|
│ │
|
||||||
|
├─ 2. 发送输入到服务器 │
|
||||||
|
│ │
|
||||||
|
│ ├─ 3. 处理输入
|
||||||
|
│ │
|
||||||
|
◄──────────────────── 4. 返回权威状态
|
||||||
|
│ │
|
||||||
|
├─ 5. 校正本地状态 │
|
||||||
|
│ │
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用建议
|
||||||
|
|
||||||
|
### 插值延迟设置
|
||||||
|
|
||||||
|
- **低延迟网络**(局域网):50-100ms
|
||||||
|
- **普通网络**:100-150ms
|
||||||
|
- **高延迟网络**:150-200ms
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const buffer = createSnapshotBuffer({
|
||||||
|
interpolationDelay: 100 // 根据网络情况调整
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 预测校正
|
||||||
|
|
||||||
|
对于本地玩家使用客户端预测:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 本地玩家:预测 + 校正
|
||||||
|
if (identity.bIsLocalPlayer) {
|
||||||
|
const predicted = prediction.predict(input, state, applyInput);
|
||||||
|
// 使用预测状态渲染
|
||||||
|
}
|
||||||
|
|
||||||
|
// 远程玩家:纯插值
|
||||||
|
if (!identity.bIsLocalPlayer) {
|
||||||
|
const interpolated = interpolator.getInterpolatedState(time);
|
||||||
|
// 使用插值状态渲染
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
1. **合理设置插值延迟**:太小会导致抖动,太大会增加延迟感
|
||||||
|
|
||||||
|
2. **客户端预测仅用于本地玩家**:远程玩家使用插值
|
||||||
|
|
||||||
|
3. **校正阈值**:根据游戏精度需求设置合适的阈值
|
||||||
|
|
||||||
|
4. **快照数量**:保持足够的快照以应对网络抖动
|
||||||
@@ -58,9 +58,9 @@
|
|||||||
"contributors:add": "all-contributors add",
|
"contributors:add": "all-contributors add",
|
||||||
"contributors:generate": "all-contributors generate",
|
"contributors:generate": "all-contributors generate",
|
||||||
"contributors:check": "all-contributors check",
|
"contributors:check": "all-contributors check",
|
||||||
"docs:dev": "vitepress dev docs",
|
"docs:dev": "pnpm --filter @esengine/docs dev",
|
||||||
"docs:build": "npm run docs:api && vitepress build docs",
|
"docs:build": "pnpm run docs:api && pnpm --filter @esengine/docs build",
|
||||||
"docs:preview": "vitepress preview docs",
|
"docs:preview": "pnpm --filter @esengine/docs preview",
|
||||||
"docs:api": "typedoc",
|
"docs:api": "typedoc",
|
||||||
"docs:api:watch": "typedoc --watch",
|
"docs:api:watch": "typedoc --watch",
|
||||||
"update:worker-demo": "npm run build:core && cd examples/worker-system-demo && npm run build && cd ../.. && npm run copy:worker-demo",
|
"update:worker-demo": "npm run build:core && cd examples/worker-system-demo && npm run build && cd ../.. && npm run copy:worker-demo",
|
||||||
|
|||||||
2758
pnpm-lock.yaml
generated
2758
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -9,3 +9,4 @@ packages:
|
|||||||
- 'packages/editor/plugins/*'
|
- 'packages/editor/plugins/*'
|
||||||
- 'packages/rust/*'
|
- 'packages/rust/*'
|
||||||
- 'packages/tools/*'
|
- 'packages/tools/*'
|
||||||
|
- 'docs'
|
||||||
|
|||||||
Reference in New Issue
Block a user