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:
YHH
2025-12-27 13:28:49 +08:00
committed by GitHub
parent 89cdfe396b
commit d57a007a42
26 changed files with 5970 additions and 2261 deletions

View File

@@ -56,7 +56,7 @@ jobs:
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: docs/.vitepress/dist
path: docs/dist
deploy:
environment:

View File

@@ -39,7 +39,16 @@ export default defineConfig({
label: '核心概念',
translations: { en: 'Core Concepts' },
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: '组件',
@@ -132,7 +141,17 @@ export default defineConfig({
{ label: '寻路', slug: 'modules/pathfinding', translations: { en: 'Pathfinding' } },
{ label: '蓝图', slug: 'modules/blueprint', translations: { en: 'Blueprint' } },
{ 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' } },
],
},
],
},
{

View File

@@ -1,5 +1,6 @@
{
"name": "docs-new",
"name": "@esengine/docs",
"private": true,
"type": "module",
"version": "0.0.1",
"scripts": {

View File

@@ -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

View 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

View 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

View 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

View 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

View 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;
}
```

View 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);
}
});
```

View File

@@ -1,5 +1,6 @@
---
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.
@@ -24,49 +25,17 @@ npm install @esengine/network
npm install @esengine/network-server
```
## Quick Setup with CLI
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:
## Architecture
```
my-game-server/
├── src/
├── index.ts # Entry point
├── server/
│ └── GameServer.ts # Network server configuration
└── game/
├── Game.ts # ECS game class
│ ├── 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
Client Server
┌────────────────┐ ┌────────────────┐
NetworkPlugin │◄──── WS ────► │ GameServer │
├─ Service │ │ ├─ Room │
├─ SyncSystem │ │ └─ Players │
├─ SpawnSystem │ └────────────────┘
└─ InputSystem │
└────────────────┘
```
## Quick Start
@@ -75,21 +44,16 @@ npm run start
```typescript
import { Core, Scene } from '@esengine/ecs-framework';
import {
NetworkPlugin,
NetworkIdentity,
NetworkTransform
} from '@esengine/network';
import { NetworkPlugin, NetworkIdentity, NetworkTransform } from '@esengine/network';
// Define game scene
class GameScene extends Scene {
initialize(): void {
this.name = 'Game';
// Network systems are automatically added by NetworkPlugin
}
}
// Initialize Core
// Initialize
Core.create({ debug: false });
const scene = new GameScene();
Core.setScene(scene);
@@ -105,7 +69,7 @@ networkPlugin.registerPrefab('player', (scene, spawn) => {
const identity = entity.addComponent(new NetworkIdentity());
identity.netId = spawn.netId;
identity.ownerId = spawn.ownerId;
identity.isLocalPlayer = spawn.ownerId === networkPlugin.networkService.localClientId;
identity.bIsLocalPlayer = spawn.ownerId === networkPlugin.networkService.clientId;
entity.addComponent(new NetworkTransform());
return entity;
@@ -116,20 +80,10 @@ const success = await networkPlugin.connect('ws://localhost:3000', 'PlayerName')
if (success) {
console.log('Connected!');
}
// Game loop
function gameLoop(dt: number) {
Core.update(dt);
}
// Disconnect
await networkPlugin.disconnect();
```
### Server
After creating a server project with CLI, the generated code already configures GameServer:
```typescript
import { GameServer } from '@esengine/network-server';
@@ -145,436 +99,39 @@ await server.start();
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:
```
Client Server
┌────────────────┐ ┌────────────────┐
│ NetworkPlugin │◄──── WS ────► │ GameServer │
│ ├─ Service │ │ ├─ Room │
│ ├─ SyncSystem │ │ └─ Players │
│ ├─ SpawnSystem │ └────────────────┘
│ └─ InputSystem │
└────────────────┘
```bash
mkdir my-game-server && cd my-game-server
npm init -y
npx @esengine/cli init -p nodejs
```
### Components
Generated project structure:
#### 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
}
```
my-game-server/
├── src/
│ ├── index.ts
│ ├── server/
│ │ └── GameServer.ts
└── game/
│ ├── Game.ts
├── scenes/
├── components/
│ └── systems/
├── tsconfig.json
└── package.json
```
#### NetworkTransform
## Documentation
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:
- 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
- [Client Usage](/en/modules/network/client/) - NetworkPlugin, components and systems
- [Server Side](/en/modules/network/server/) - GameServer and Room management
- [State Sync](/en/modules/network/sync/) - Interpolation, prediction and snapshots
- [API Reference](/en/modules/network/api/) - Complete API documentation
## Service Tokens
@@ -588,142 +145,15 @@ import {
NetworkInputSystemToken
} from '@esengine/network';
// Get service
const networkService = services.get(NetworkServiceToken);
```
## Practical Example
## Blueprint Nodes
### Complete Multiplayer Client
The network module provides visual scripting support:
```typescript
import { Core, Scene, EntitySystem, Matcher, Entity } from '@esengine/ecs-framework';
import {
NetworkPlugin,
NetworkIdentity,
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
}
});
```
- `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

View 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

View 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

View File

@@ -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/) 组织和管理实体

View 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/) - 组件的定义和生命周期

View 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/) - 组件中的实体引用装饰器

View 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/) - 销毁和持久化

View 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/) - 场景切换和管理

View 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;
```

View 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()` 方法

View File

@@ -1,5 +1,6 @@
---
title: "网络同步系统 (Network)"
description: "基于 TSRPC 的多人游戏网络同步解决方案"
---
`@esengine/network` 提供基于 TSRPC 的客户端-服务器网络同步解决方案,用于多人游戏的实体同步、输入处理和状态插值。
@@ -24,49 +25,17 @@ npm install @esengine/network
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 # 入口文件
├── 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
客户端 服务器
┌────────────────┐ ┌────────────────┐
NetworkPlugin │◄──── WS ────► │ GameServer │
├─ Service │ │ ├─ Room │
├─ SyncSystem │ └─ Players │
├─ SpawnSystem │ └────────────────┘
└─ InputSystem │
└────────────────┘
```
## 快速开始
@@ -75,21 +44,16 @@ npm run start
```typescript
import { Core, Scene } from '@esengine/ecs-framework';
import {
NetworkPlugin,
NetworkIdentity,
NetworkTransform
} from '@esengine/network';
import { NetworkPlugin, NetworkIdentity, NetworkTransform } from '@esengine/network';
// 定义游戏场景
class GameScene extends Scene {
initialize(): void {
this.name = 'Game';
// 网络系统由 NetworkPlugin 自动添加
}
}
// 初始化 Core
// 初始化
Core.create({ debug: false });
const scene = new GameScene();
Core.setScene(scene);
@@ -105,7 +69,7 @@ networkPlugin.registerPrefab('player', (scene, spawn) => {
const identity = entity.addComponent(new NetworkIdentity());
identity.netId = spawn.netId;
identity.ownerId = spawn.ownerId;
identity.bIsLocalPlayer = spawn.ownerId === networkPlugin.networkService.localClientId;
identity.bIsLocalPlayer = spawn.ownerId === networkPlugin.networkService.clientId;
entity.addComponent(new NetworkTransform());
return entity;
@@ -116,20 +80,10 @@ const success = await networkPlugin.connect('ws://localhost:3000', 'PlayerName')
if (success) {
console.log('Connected!');
}
// 游戏循环
function gameLoop(dt: number) {
Core.update(dt);
}
// 断开连接
await networkPlugin.disconnect();
```
### 服务器端
使用 CLI 创建服务端项目后,默认生成的代码已经配置好了 GameServer
```typescript
import { GameServer } from '@esengine/network-server';
@@ -145,436 +99,39 @@ await server.start();
console.log('Server started on ws://localhost:3000');
```
## 核心概念
## 使用 CLI 快速创建
### 架构
推荐使用 ESEngine CLI 快速创建完整的游戏服务端项目:
```
客户端 服务器
┌────────────────┐ ┌────────────────┐
│ NetworkPlugin │◄──── WS ────► │ GameServer │
│ ├─ Service │ │ ├─ Room │
│ ├─ SyncSystem │ │ └─ Players │
│ ├─ SpawnSystem │ └────────────────┘
│ └─ InputSystem │
└────────────────┘
```bash
mkdir my-game-server && cd my-game-server
npm init -y
npx @esengine/cli init -p nodejs
```
### 组件
生成的项目结构:
#### NetworkIdentity
网络标识组件,每个网络同步的实体必须拥有:
```typescript
class NetworkIdentity extends Component {
netId: number; // 网络唯一 ID
ownerId: number; // 所有者客户端 ID
bIsLocalPlayer: boolean; // 是否为本地玩家
bHasAuthority: boolean; // 是否有权限控制
}
```
my-game-server/
├── src/
│ ├── index.ts
│ ├── server/
│ │ └── GameServer.ts
└── game/
│ ├── Game.ts
│ ├── scenes/
├── components/
│ └── systems/
├── tsconfig.json
└── package.json
```
#### NetworkTransform
## 文档导航
网络变换组件,用于位置和旋转同步:
```typescript
class NetworkTransform extends Component {
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
- [客户端使用](/modules/network/client/) - NetworkPlugin、组件和系统
- [服务器端](/modules/network/server/) - GameServer 和 Room 管理
- [状态同步](/modules/network/sync/) - 插值、预测和快照
- [API 参考](/modules/network/api/) - 完整 API 文档
## 服务令牌
@@ -588,142 +145,15 @@ import {
NetworkInputSystemToken
} from '@esengine/network';
// 获取服务
const networkService = services.get(NetworkServiceToken);
```
## 实际示例
## 蓝图节点
### 完整的多人游戏客户端
网络模块提供可视化脚本支持:
```typescript
import { Core, Scene, EntitySystem, Matcher, Entity } from '@esengine/ecs-framework';
import {
NetworkPlugin,
NetworkIdentity,
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('已断开');
// 处理重连逻辑
}
});
```
- `IsLocalPlayer` - 检查实体是否为本地玩家
- `IsServer` - 检查是否运行在服务器端
- `HasAuthority` - 检查是否有权限控制实体
- `GetNetworkId` - 获取实体的网络 ID
- `GetLocalPlayerId` - 获取本地玩家 ID

View 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. **状态验证**:服务器应验证客户端输入,防止作弊

View 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. **快照数量**:保持足够的快照以应对网络抖动

View File

@@ -58,9 +58,9 @@
"contributors:add": "all-contributors add",
"contributors:generate": "all-contributors generate",
"contributors:check": "all-contributors check",
"docs:dev": "vitepress dev docs",
"docs:build": "npm run docs:api && vitepress build docs",
"docs:preview": "vitepress preview docs",
"docs:dev": "pnpm --filter @esengine/docs dev",
"docs:build": "pnpm run docs:api && pnpm --filter @esengine/docs build",
"docs:preview": "pnpm --filter @esengine/docs preview",
"docs:api": "typedoc",
"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",

2758
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,3 +9,4 @@ packages:
- 'packages/editor/plugins/*'
- 'packages/rust/*'
- 'packages/tools/*'
- 'docs'