Compare commits

...

21 Commits

Author SHA1 Message Date
github-actions[bot]
fac4bc19c5 chore: release packages (#360)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-27 10:57:16 +08:00
YHH
aed91dbe45 feat(cli): add update command for ESEngine packages (#359)
* feat(cli): add update command for ESEngine packages

- Add 'update' command to check and update @esengine/* packages
- Support --check flag to only show available updates without installing
- Support --yes flag to skip confirmation prompt
- Display package update status with current vs latest version comparison
- Preserve version prefix (^ or ~) when updating
- Bump version to 1.4.0

* chore: add changeset for CLI update command

* fix(cli): handle 'latest' tag in update command

- Treat 'latest' and '*' version tags as needing update
- Pin to specific version (^x.x.x) when updating from 'latest'
- Show '(pin version)' hint in update status output

* fix(cli): minimize file system race condition in update command

Re-read package.json immediately before writing to reduce the window
for potential race conditions between reading and writing.

* fix(cli): use atomic file write to avoid race condition

Write to temp file first, then rename for atomic update.
2025-12-27 10:54:04 +08:00
github-actions[bot]
c7f8208b6f chore: release packages (#358)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-27 10:31:43 +08:00
yhh
5131ec3c52 chore: update pnpm-lock.yaml 2025-12-27 10:28:35 +08:00
yhh
7d74623710 fix(core): 配置 publishConfig.directory 确保从 dist 目录发布 2025-12-27 10:27:00 +08:00
github-actions[bot]
044463dd5f chore: release packages (#357)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-27 09:55:30 +08:00
YHH
ce2db4e48a fix(core): 修复 World cleanup 在打包环境下的兼容性问题 (#356)
- 使用 forEach 替代 spread + for...of 解构模式,避免某些打包工具转换后的兼容性问题
- 重构 World 和 WorldManager 类,提升代码质量
- 提取默认配置为常量
- 统一双语注释格式
2025-12-27 09:51:04 +08:00
github-actions[bot]
0a88c6f2fc chore: release packages (#355)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-27 00:20:04 +08:00
yhh
b0b95c60b4 fix: 从 ignore 列表移除 network-server 以支持版本发布 2025-12-27 00:17:44 +08:00
yhh
683ac7a7d4 Merge branch 'master' of https://github.com/esengine/esengine 2025-12-27 00:16:12 +08:00
YHH
1e240e86f2 feat(cli): 增强 Node.js 服务端适配器 (#354)
* docs(network): 添加网络模块文档和 CLI 支持

- 添加中英文网络模块文档
- 将 network、network-protocols、network-server 加入 CLI 模块列表

* feat(cli): 增强 Node.js 服务端适配器

- 添加 @esengine/network-server 依赖支持
- 生成完整的 ECS 游戏服务器项目结构
- 修复 network-server 包支持 ESM/CJS 双格式
- 添加 ws@8.18.0 解决 Node.js 24 兼容性问题
- 组件使用 @ECSComponent 装饰器注册
- tsconfig 启用 experimentalDecorators
2025-12-27 00:13:58 +08:00
yhh
4d6c2fe7ff Merge branch 'master' of https://github.com/esengine/esengine 2025-12-26 23:21:17 +08:00
github-actions[bot]
67c06720c5 chore: release packages (#353)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-26 23:17:14 +08:00
YHH
33e98b9a75 fix(cli): 修复 Cocos Creator 3.x 项目检测逻辑 (#352)
* docs(network): 添加网络模块文档和 CLI 支持

- 添加中英文网络模块文档
- 将 network、network-protocols、network-server 加入 CLI 模块列表

* fix(cli): 修复 Cocos Creator 3.x 项目检测逻辑

- 重构检测代码,提取通用辅助函数
- 优先检查 package.json 中的 creator.version 字段
- 添加 .creator 和 settings 目录检测
- 使用 getMajorVersion 统一版本号解析

* chore: add changeset
2025-12-26 23:14:23 +08:00
yhh
a42f2412d7 Merge branch 'master' of https://github.com/esengine/esengine 2025-12-26 23:04:47 +08:00
yhh
fdb19a33fb docs(network): 添加网络模块文档和 CLI 支持
- 添加中英文网络模块文档
- 将 network、network-protocols、network-server 加入 CLI 模块列表
2025-12-26 23:01:07 +08:00
github-actions[bot]
1e31e9101b chore: release packages (#351)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-26 22:32:12 +08:00
yhh
d66c18041e fix(spatial): 修复 GridAOI 可见性更新问题
- 修复 addObserver 时现有观察者无法检测到新实体的问题
- 修复实体远距离移动时观察者可见性未正确更新的问题
- 重构 demos 抽取公共测试工具
2025-12-26 22:23:03 +08:00
yhh
881ffad3bc feat(tools): 添加 CLI 模块管理命令和文档验证 demos
- CLI 新增 list/add/remove 命令管理项目模块
- 创建 demos 包验证模块文档正确性
- 包含 Timer/FSM/Pathfinding/Procgen/Spatial 5个模块的完整测试
2025-12-26 22:09:01 +08:00
YHH
4a16e30794 docs(modules): 添加框架模块文档 (#350)
* docs(modules): 添加框架模块文档

添加以下模块的完整文档:
- FSM (状态机): 状态定义、转换条件、优先级、事件监听
- Timer (定时器): 定时器调度、冷却系统、服务令牌
- Spatial (空间索引): GridSpatialIndex、AOI 兴趣区域管理
- Pathfinding (寻路): A* 算法、网格地图、导航网格、路径平滑
- Procgen (程序化生成): 噪声函数、种子随机数、加权随机

所有文档均基于实际源码 API 编写,包含:
- 快速开始示例
- 完整 API 参考
- 实际使用案例
- 蓝图节点说明
- 最佳实践建议

* docs(modules): 添加 Blueprint 模块文档和所有模块英文版

新增中文文档:
- Blueprint (蓝图可视化脚本): VM、自定义节点、组合系统、触发器

新增英文文档 (docs/en/modules/):
- FSM: State machine API, transitions, ECS integration
- Timer: Timers, cooldowns, service tokens
- Spatial: Grid spatial index, AOI management
- Pathfinding: A*, grid map, NavMesh, path smoothing
- Procgen: Noise functions, seeded random, weighted random
- Blueprint: Visual scripting, custom nodes, composition

所有文档均基于实际源码 API 编写。
2025-12-26 20:02:21 +08:00
YHH
76691cc198 docs: 重构文档结构,添加独立模块区域 (#349)
* docs: 重构文档结构,添加独立模块区域

- 新增 /modules/ 目录用于功能模块文档
- 移动 behavior-tree 从 /guide/ 到 /modules/
- 添加模块总览页面
- 更新导航栏添加"模块"入口
- 更新侧边栏:模块区域独立侧边栏
- 更新 i18n 配置支持新模块

* style(docs): 提高文字对比度
2025-12-26 19:15:08 +08:00
71 changed files with 9478 additions and 844 deletions

View File

@@ -33,7 +33,6 @@
"@esengine/physics-rapier2d",
"@esengine/rapier2d",
"@esengine/world-streaming",
"@esengine/network-server",
"@esengine/editor-core",
"@esengine/editor-runtime",
"@esengine/editor-app",

View File

@@ -49,21 +49,6 @@ function createSidebar(t, prefix = '') {
{ text: t.sidebar.persistentEntity, link: `${prefix}/guide/persistent-entity` }
]
},
{
text: t.sidebar.behaviorTree,
link: `${prefix}/guide/behavior-tree/`,
items: [
{ text: t.sidebar.btGettingStarted, link: `${prefix}/guide/behavior-tree/getting-started` },
{ text: t.sidebar.btCoreConcepts, link: `${prefix}/guide/behavior-tree/core-concepts` },
{ text: t.sidebar.btEditorGuide, link: `${prefix}/guide/behavior-tree/editor-guide` },
{ text: t.sidebar.btEditorWorkflow, link: `${prefix}/guide/behavior-tree/editor-workflow` },
{ text: t.sidebar.btCustomActions, link: `${prefix}/guide/behavior-tree/custom-actions` },
{ text: t.sidebar.btCocosIntegration, link: `${prefix}/guide/behavior-tree/cocos-integration` },
{ text: t.sidebar.btLayaIntegration, link: `${prefix}/guide/behavior-tree/laya-integration` },
{ text: t.sidebar.btAdvancedUsage, link: `${prefix}/guide/behavior-tree/advanced-usage` },
{ text: t.sidebar.btBestPractices, link: `${prefix}/guide/behavior-tree/best-practices` }
]
},
{ text: t.sidebar.serialization, link: `${prefix}/guide/serialization` },
{ text: t.sidebar.eventSystem, link: `${prefix}/guide/event-system` },
{ text: t.sidebar.timeAndTimers, link: `${prefix}/guide/time-and-timers` },
@@ -89,6 +74,64 @@ function createSidebar(t, prefix = '') {
]
}
],
// 模块总览侧边栏 | Modules overview sidebar
[`${prefix}/modules/`]: [
{
text: t.sidebar.modulesOverview,
link: `${prefix}/modules/`,
items: [
{
text: t.sidebar.aiModules,
collapsed: false,
items: [
{ text: t.sidebar.behaviorTree, link: `${prefix}/modules/behavior-tree/` },
{ text: t.sidebar.fsm, link: `${prefix}/modules/fsm/` }
]
},
{
text: t.sidebar.gameplayModules,
collapsed: false,
items: [
{ text: t.sidebar.timer, link: `${prefix}/modules/timer/` },
{ text: t.sidebar.spatial, link: `${prefix}/modules/spatial/` },
{ text: t.sidebar.pathfinding, link: `${prefix}/modules/pathfinding/` }
]
},
{
text: t.sidebar.toolModules,
collapsed: false,
items: [
{ text: t.sidebar.blueprint, link: `${prefix}/modules/blueprint/` },
{ text: t.sidebar.procgen, link: `${prefix}/modules/procgen/` }
]
},
{
text: t.sidebar.networkModules,
collapsed: false,
items: [
{ text: t.sidebar.network, link: `${prefix}/modules/network/` }
]
}
]
}
],
// 行为树模块侧边栏 | Behavior tree module sidebar
[`${prefix}/modules/behavior-tree/`]: [
{
text: t.sidebar.behaviorTree,
items: [
{ text: t.sidebar.btGettingStarted, link: `${prefix}/modules/behavior-tree/getting-started` },
{ text: t.sidebar.btCoreConcepts, link: `${prefix}/modules/behavior-tree/core-concepts` },
{ text: t.sidebar.btEditorGuide, link: `${prefix}/modules/behavior-tree/editor-guide` },
{ text: t.sidebar.btEditorWorkflow, link: `${prefix}/modules/behavior-tree/editor-workflow` },
{ text: t.sidebar.btCustomActions, link: `${prefix}/modules/behavior-tree/custom-actions` },
{ text: t.sidebar.btCocosIntegration, link: `${prefix}/modules/behavior-tree/cocos-integration` },
{ text: t.sidebar.btLayaIntegration, link: `${prefix}/modules/behavior-tree/laya-integration` },
{ text: t.sidebar.btAdvancedUsage, link: `${prefix}/modules/behavior-tree/advanced-usage` },
{ text: t.sidebar.btBestPractices, link: `${prefix}/modules/behavior-tree/best-practices` }
]
}
],
[`${prefix}/examples/`]: [
{
text: t.sidebar.examples,
@@ -173,6 +216,7 @@ function createNav(t, prefix = '') {
{ text: t.nav.home, link: `${prefix}/` },
{ text: t.nav.quickStart, link: `${prefix}/guide/getting-started` },
{ text: t.nav.guide, link: `${prefix}/guide/` },
{ text: t.nav.modules, link: `${prefix}/modules/` },
{ text: t.nav.api, link: `${prefix}/api/README` },
{
text: t.nav.examples,

View File

@@ -3,6 +3,7 @@
"home": "Home",
"quickStart": "Quick Start",
"guide": "Guide",
"modules": "Modules",
"api": "API",
"examples": "Examples",
"workerDemo": "Worker System Demo",
@@ -54,7 +55,26 @@
"utilities": "Utilities",
"interfaces": "Interfaces",
"decorators": "Decorators",
"enums": "Enums"
"enums": "Enums",
"modulesOverview": "Modules Overview",
"aiModules": "AI Modules",
"gameplayModules": "Gameplay",
"toolModules": "Tools",
"networkModules": "Network",
"fsm": "State Machine (FSM)",
"fsmOverview": "Overview",
"timer": "Timer System",
"timerOverview": "Overview",
"spatial": "Spatial Index",
"spatialOverview": "Overview",
"pathfinding": "Pathfinding",
"pathfindingOverview": "Overview",
"blueprint": "Visual Scripting",
"blueprintOverview": "Overview",
"procgen": "Procedural Generation",
"procgenOverview": "Overview",
"network": "Network Sync",
"networkOverview": "Overview"
},
"home": {
"title": "ESEngine - High-performance TypeScript ECS Framework",

View File

@@ -3,6 +3,7 @@
"home": "首页",
"quickStart": "快速开始",
"guide": "指南",
"modules": "模块",
"api": "API",
"examples": "示例",
"workerDemo": "Worker系统演示",
@@ -54,7 +55,26 @@
"utilities": "工具类",
"interfaces": "接口",
"decorators": "装饰器",
"enums": "枚举"
"enums": "枚举",
"modulesOverview": "模块总览",
"aiModules": "AI 模块",
"gameplayModules": "游戏逻辑",
"toolModules": "工具模块",
"networkModules": "网络模块",
"fsm": "状态机 (FSM)",
"fsmOverview": "概述",
"timer": "定时器系统",
"timerOverview": "概述",
"spatial": "空间索引",
"spatialOverview": "概述",
"pathfinding": "寻路系统",
"pathfindingOverview": "概述",
"blueprint": "可视化脚本",
"blueprintOverview": "概述",
"procgen": "程序化生成",
"procgenOverview": "概述",
"network": "网络同步",
"networkOverview": "概述"
},
"home": {
"title": "ESEngine - 高性能 TypeScript ECS 框架",

View File

@@ -2,23 +2,24 @@
color-scheme: dark;
--vp-nav-height: 64px;
--es-bg-base: #1e1e1e;
--es-bg-elevated: #252526;
--es-bg-overlay: #2d2d2d;
--es-bg-input: #3c3c3c;
--es-bg-inset: #181818;
--es-bg-base: #1a1a1a;
--es-bg-elevated: #222222;
--es-bg-overlay: #2a2a2a;
--es-bg-input: #333333;
--es-bg-inset: #151515;
--es-bg-hover: #2a2d2e;
--es-bg-active: #37373d;
--es-bg-sidebar: #262626;
--es-bg-card: #2a2a2a;
--es-bg-header: #2d2d2d;
--es-bg-sidebar: #1e1e1e;
--es-bg-card: #242424;
--es-bg-header: #1e1e1e;
--es-text-primary: #cccccc;
--es-text-secondary: #9d9d9d;
--es-text-tertiary: #6a6a6a;
/* 提高文字对比度 | Improve text contrast */
--es-text-primary: #e0e0e0;
--es-text-secondary: #b0b0b0;
--es-text-tertiary: #888888;
--es-text-inverse: #ffffff;
--es-text-muted: #aaaaaa;
--es-text-dim: #6a6a6a;
--es-text-muted: #c0c0c0;
--es-text-dim: #888888;
--es-font-xs: 11px;
--es-font-sm: 12px;

View File

@@ -0,0 +1,404 @@
# Blueprint Visual Scripting
`@esengine/blueprint` provides a full-featured visual scripting system supporting node-based programming, event-driven execution, and blueprint composition.
## Installation
```bash
npm install @esengine/blueprint
```
## Quick Start
```typescript
import {
createBlueprintSystem,
createBlueprintComponentData,
NodeRegistry,
RegisterNode
} from '@esengine/blueprint';
// Create blueprint system
const blueprintSystem = createBlueprintSystem(scene);
// Load blueprint asset
const blueprint = await loadBlueprintAsset('player.bp');
// Create blueprint component data
const componentData = createBlueprintComponentData();
componentData.blueprintAsset = blueprint;
// Update in game loop
function gameLoop(dt: number) {
blueprintSystem.process(entities, dt);
}
```
## Core Concepts
### Blueprint Asset Structure
Blueprints are saved as `.bp` files:
```typescript
interface BlueprintAsset {
version: number; // Format version
type: 'blueprint'; // Asset type
metadata: BlueprintMetadata; // Metadata
variables: BlueprintVariable[]; // Variable definitions
nodes: BlueprintNode[]; // Node instances
connections: BlueprintConnection[]; // Connections
}
```
### Node Categories
| Category | Description | Color |
|----------|-------------|-------|
| `event` | Event nodes (entry points) | Red |
| `flow` | Flow control | Gray |
| `entity` | Entity operations | Blue |
| `component` | Component access | Cyan |
| `math` | Math operations | Green |
| `logic` | Logic operations | Red |
| `variable` | Variable access | Purple |
| `time` | Time utilities | Cyan |
| `debug` | Debug utilities | Gray |
### Pin Types
Nodes connect through pins:
```typescript
interface BlueprintPinDefinition {
name: string; // Pin name
type: PinDataType; // Data type
direction: 'input' | 'output';
isExec?: boolean; // Execution pin
defaultValue?: unknown;
}
type PinDataType =
| 'exec' // Execution flow
| 'boolean' // Boolean
| 'number' // Number
| 'string' // String
| 'vector2' // 2D vector
| 'vector3' // 3D vector
| 'entity' // Entity reference
| 'component' // Component reference
| 'any'; // Any type
```
### Variable Scopes
```typescript
type VariableScope =
| 'local' // Per execution
| 'instance' // Per entity
| 'global'; // Shared globally
```
## Virtual Machine API
### BlueprintVM
The virtual machine executes blueprint graphs:
```typescript
import { BlueprintVM } from '@esengine/blueprint';
const vm = new BlueprintVM(blueprintAsset, entity, scene);
vm.start(); // Start (triggers BeginPlay)
vm.tick(deltaTime); // Update (triggers Tick)
vm.stop(); // Stop (triggers EndPlay)
vm.pause();
vm.resume();
// Trigger events
vm.triggerEvent('EventCollision', { other: otherEntity });
vm.triggerCustomEvent('OnDamage', { amount: 50 });
// Debug mode
vm.debug = true;
```
### Execution Context
```typescript
interface ExecutionContext {
blueprint: BlueprintAsset;
entity: Entity;
scene: IScene;
deltaTime: number;
time: number;
getInput<T>(nodeId: string, pinName: string): T;
setOutput(nodeId: string, pinName: string, value: unknown): void;
getVariable<T>(name: string): T;
setVariable(name: string, value: unknown): void;
}
```
### Execution Result
```typescript
interface ExecutionResult {
outputs?: Record<string, unknown>; // Output values
nextExec?: string | null; // Next exec pin
delay?: number; // Delay execution (ms)
yield?: boolean; // Pause until next frame
error?: string; // Error message
}
```
## Custom Nodes
### Define Node Template
```typescript
import { BlueprintNodeTemplate } from '@esengine/blueprint';
const MyNodeTemplate: BlueprintNodeTemplate = {
type: 'MyCustomNode',
title: 'My Custom Node',
category: 'custom',
description: 'A custom node example',
keywords: ['custom', 'example'],
inputs: [
{ name: 'exec', type: 'exec', direction: 'input', isExec: true },
{ name: 'value', type: 'number', direction: 'input', defaultValue: 0 }
],
outputs: [
{ name: 'exec', type: 'exec', direction: 'output', isExec: true },
{ name: 'result', type: 'number', direction: 'output' }
]
};
```
### Implement Node Executor
```typescript
import { INodeExecutor, RegisterNode } from '@esengine/blueprint';
@RegisterNode(MyNodeTemplate)
class MyNodeExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const value = context.getInput<number>(node.id, 'value');
const result = value * 2;
return {
outputs: { result },
nextExec: 'exec'
};
}
}
```
### Registration Methods
```typescript
// Method 1: Decorator
@RegisterNode(MyNodeTemplate)
class MyNodeExecutor implements INodeExecutor { ... }
// Method 2: Manual registration
NodeRegistry.instance.register(MyNodeTemplate, new MyNodeExecutor());
```
## Node Registry
```typescript
import { NodeRegistry } from '@esengine/blueprint';
const registry = NodeRegistry.instance;
const allTemplates = registry.getAllTemplates();
const mathNodes = registry.getTemplatesByCategory('math');
const results = registry.searchTemplates('add');
if (registry.has('MyCustomNode')) { ... }
```
## Built-in Nodes
### Event Nodes
| Node | Description |
|------|-------------|
| `EventBeginPlay` | Triggered on blueprint start |
| `EventTick` | Triggered every frame |
| `EventEndPlay` | Triggered on blueprint stop |
| `EventCollision` | Triggered on collision |
| `EventInput` | Triggered on input |
| `EventTimer` | Triggered by timer |
### Time Nodes
| Node | Description |
|------|-------------|
| `Delay` | Delay execution |
| `GetDeltaTime` | Get frame delta |
| `GetTime` | Get total runtime |
### Math Nodes
| Node | Description |
|------|-------------|
| `Add`, `Subtract`, `Multiply`, `Divide` | Basic operations |
| `Abs`, `Clamp`, `Lerp`, `Min`, `Max` | Utility functions |
### Debug Nodes
| Node | Description |
|------|-------------|
| `Print` | Print to console |
## Blueprint Composition
### Blueprint Fragments
Encapsulate reusable logic as fragments:
```typescript
import { createFragment } from '@esengine/blueprint';
const healthFragment = createFragment('HealthSystem', {
inputs: [
{ name: 'damage', type: 'number', internalNodeId: 'input1', internalPinName: 'value' }
],
outputs: [
{ name: 'isDead', type: 'boolean', internalNodeId: 'output1', internalPinName: 'value' }
],
graph: { nodes: [...], connections: [...], variables: [...] }
});
```
### Compose Blueprints
```typescript
import { createComposer, FragmentRegistry } from '@esengine/blueprint';
// Register fragments
FragmentRegistry.instance.register('health', healthFragment);
FragmentRegistry.instance.register('movement', movementFragment);
// Create composer
const composer = createComposer('PlayerBlueprint');
// Add fragments to slots
composer.addFragment(healthFragment, 'slot1', { position: { x: 0, y: 0 } });
composer.addFragment(movementFragment, 'slot2', { position: { x: 400, y: 0 } });
// Connect slots
composer.connect('slot1', 'onDeath', 'slot2', 'disable');
// Validate
const validation = composer.validate();
if (!validation.isValid) {
console.error(validation.errors);
}
// Compile to blueprint
const blueprint = composer.compile();
```
## Trigger System
### Define Trigger Conditions
```typescript
import { TriggerCondition, TriggerDispatcher } from '@esengine/blueprint';
const lowHealthCondition: TriggerCondition = {
type: 'comparison',
left: { type: 'variable', name: 'health' },
operator: '<',
right: { type: 'constant', value: 20 }
};
```
### Use Trigger Dispatcher
```typescript
const dispatcher = new TriggerDispatcher();
dispatcher.register('lowHealth', lowHealthCondition, (context) => {
context.triggerEvent('OnLowHealth');
});
dispatcher.evaluate(context);
```
## ECS Integration
### Using Blueprint System
```typescript
import { createBlueprintSystem } from '@esengine/blueprint';
class GameScene {
private blueprintSystem: BlueprintSystem;
initialize() {
this.blueprintSystem = createBlueprintSystem(this.scene);
}
update(dt: number) {
this.blueprintSystem.process(this.entities, dt);
}
}
```
### Triggering Blueprint Events
```typescript
import { triggerBlueprintEvent, triggerCustomBlueprintEvent } from '@esengine/blueprint';
triggerBlueprintEvent(entity, 'Collision', { other: otherEntity });
triggerCustomBlueprintEvent(entity, 'OnPickup', { item: itemEntity });
```
## Serialization
### Save Blueprint
```typescript
import { validateBlueprintAsset } from '@esengine/blueprint';
function saveBlueprint(blueprint: BlueprintAsset, path: string): void {
if (!validateBlueprintAsset(blueprint)) {
throw new Error('Invalid blueprint structure');
}
const json = JSON.stringify(blueprint, null, 2);
fs.writeFileSync(path, json);
}
```
### Load Blueprint
```typescript
async function loadBlueprint(path: string): Promise<BlueprintAsset> {
const json = await fs.readFile(path, 'utf-8');
const asset = JSON.parse(json);
if (!validateBlueprintAsset(asset)) {
throw new Error('Invalid blueprint file');
}
return asset;
}
```
## Best Practices
1. **Use fragments for reusable logic**
2. **Choose appropriate variable scopes**
- `local`: Temporary calculations
- `instance`: Entity state (e.g., health)
- `global`: Game-wide state
3. **Avoid infinite loops** - VM has max steps per frame (default 1000)
4. **Debug techniques**
- Enable `vm.debug = true` for execution logs
- Use Print nodes for intermediate values
5. **Performance optimization**
- Pure nodes (`isPure: true`) cache outputs
- Avoid heavy computation in Tick

View File

@@ -0,0 +1,316 @@
# State Machine (FSM)
`@esengine/fsm` provides a type-safe finite state machine implementation for characters, AI, or any scenario requiring state management.
## Installation
```bash
npm install @esengine/fsm
```
## Quick Start
```typescript
import { createStateMachine } from '@esengine/fsm';
// Define state types
type PlayerState = 'idle' | 'walk' | 'run' | 'jump';
// Create state machine
const fsm = createStateMachine<PlayerState>('idle');
// Define states with callbacks
fsm.defineState('idle', {
onEnter: (ctx, from) => console.log(`Entered idle from ${from}`),
onExit: (ctx, to) => console.log(`Exiting idle to ${to}`),
onUpdate: (ctx, dt) => { /* Update every frame */ }
});
fsm.defineState('walk', {
onEnter: () => console.log('Started walking')
});
// Manual transition
fsm.transition('walk');
console.log(fsm.current); // 'walk'
```
## Core Concepts
### State Configuration
Each state can be configured with the following callbacks:
```typescript
interface StateConfig<TState, TContext> {
name: TState; // State name
onEnter?: (context: TContext, from: TState | null) => void; // Enter callback
onExit?: (context: TContext, to: TState) => void; // Exit callback
onUpdate?: (context: TContext, deltaTime: number) => void; // Update callback
tags?: string[]; // State tags
metadata?: Record<string, unknown>; // Metadata
}
```
### Transition Conditions
Define conditional state transitions:
```typescript
interface Context {
isMoving: boolean;
isRunning: boolean;
isGrounded: boolean;
}
const fsm = createStateMachine<PlayerState, Context>('idle', {
context: { isMoving: false, isRunning: false, isGrounded: true }
});
// Define transition conditions
fsm.defineTransition('idle', 'walk', (ctx) => ctx.isMoving);
fsm.defineTransition('walk', 'run', (ctx) => ctx.isRunning);
fsm.defineTransition('walk', 'idle', (ctx) => !ctx.isMoving);
// Automatically evaluate and execute matching transitions
fsm.evaluateTransitions();
```
### Transition Priority
When multiple transitions are valid, higher priority executes first:
```typescript
// Higher priority number = higher priority
fsm.defineTransition('idle', 'attack', (ctx) => ctx.isAttacking, 10);
fsm.defineTransition('idle', 'walk', (ctx) => ctx.isMoving, 1);
// If both conditions are met, 'attack' (priority 10) is tried first
```
## API Reference
### createStateMachine
```typescript
function createStateMachine<TState extends string, TContext = unknown>(
initialState: TState,
options?: StateMachineOptions<TContext>
): IStateMachine<TState, TContext>
```
**Parameters:**
- `initialState` - Initial state
- `options.context` - Context object, accessible in callbacks
- `options.maxHistorySize` - Maximum history entries (default 100)
- `options.enableHistory` - Enable history tracking (default true)
### State Machine Properties
| Property | Type | Description |
|----------|------|-------------|
| `current` | `TState` | Current state |
| `previous` | `TState \| null` | Previous state |
| `context` | `TContext` | Context object |
| `isTransitioning` | `boolean` | Whether currently transitioning |
| `currentStateDuration` | `number` | Current state duration (ms) |
### State Machine Methods
#### State Definition
```typescript
// Define state
fsm.defineState('idle', {
onEnter: (ctx, from) => {},
onExit: (ctx, to) => {},
onUpdate: (ctx, dt) => {}
});
// Check if state exists
fsm.hasState('idle'); // true
// Get state configuration
fsm.getStateConfig('idle');
// Get all states
fsm.getStates(); // ['idle', 'walk', ...]
```
#### Transition Operations
```typescript
// Define transition
fsm.defineTransition('idle', 'walk', condition, priority);
// Remove transition
fsm.removeTransition('idle', 'walk');
// Get transitions from state
fsm.getTransitionsFrom('idle');
// Check if transition is possible
fsm.canTransition('walk'); // true/false
// Manual transition
fsm.transition('walk');
// Force transition (ignore conditions)
fsm.transition('walk', true);
// Auto-evaluate transition conditions
fsm.evaluateTransitions();
```
#### Lifecycle
```typescript
// Update state machine (calls current state's onUpdate)
fsm.update(deltaTime);
// Reset state machine
fsm.reset(); // Reset to current state
fsm.reset('idle'); // Reset to specified state
```
#### Event Listeners
```typescript
// Listen to entering specific state
const unsubscribe = fsm.onEnter('walk', (from) => {
console.log(`Entered walk from ${from}`);
});
// Listen to exiting specific state
fsm.onExit('walk', (to) => {
console.log(`Exiting walk to ${to}`);
});
// Listen to any state change
fsm.onChange((event) => {
console.log(`${event.from} -> ${event.to} at ${event.timestamp}`);
});
// Unsubscribe
unsubscribe();
```
#### Debugging
```typescript
// Get state history
const history = fsm.getHistory();
// [{ from: 'idle', to: 'walk', timestamp: 1234567890 }, ...]
// Clear history
fsm.clearHistory();
// Get debug info
const info = fsm.getDebugInfo();
// { current, previous, duration, stateCount, transitionCount, historySize }
```
## Practical Examples
### Character State Machine
```typescript
import { createStateMachine } from '@esengine/fsm';
type CharacterState = 'idle' | 'walk' | 'run' | 'jump' | 'fall' | 'attack';
interface CharacterContext {
velocity: { x: number; y: number };
isGrounded: boolean;
isAttacking: boolean;
speed: number;
}
const characterFSM = createStateMachine<CharacterState, CharacterContext>('idle', {
context: {
velocity: { x: 0, y: 0 },
isGrounded: true,
isAttacking: false,
speed: 0
}
});
// Define states
characterFSM.defineState('idle', {
onEnter: (ctx) => { ctx.speed = 0; }
});
characterFSM.defineState('walk', {
onEnter: (ctx) => { ctx.speed = 100; }
});
characterFSM.defineState('run', {
onEnter: (ctx) => { ctx.speed = 200; }
});
// Define transitions
characterFSM.defineTransition('idle', 'walk', (ctx) => Math.abs(ctx.velocity.x) > 0);
characterFSM.defineTransition('walk', 'idle', (ctx) => ctx.velocity.x === 0);
characterFSM.defineTransition('walk', 'run', (ctx) => Math.abs(ctx.velocity.x) > 150);
// Jump has highest priority
characterFSM.defineTransition('idle', 'jump', (ctx) => !ctx.isGrounded, 10);
characterFSM.defineTransition('walk', 'jump', (ctx) => !ctx.isGrounded, 10);
// Game loop usage
function gameUpdate(dt: number) {
// Update context
characterFSM.context.velocity.x = getInputVelocity();
characterFSM.context.isGrounded = checkGrounded();
// Evaluate transitions
characterFSM.evaluateTransitions();
// Update current state
characterFSM.update(dt);
}
```
### ECS Integration
```typescript
import { Component, EntitySystem, Matcher } from '@esengine/ecs-framework';
import { createStateMachine, type IStateMachine } from '@esengine/fsm';
// State machine component
class FSMComponent extends Component {
fsm: IStateMachine<string>;
constructor(initialState: string) {
super();
this.fsm = createStateMachine(initialState);
}
}
// State machine system
class FSMSystem extends EntitySystem {
constructor() {
super(Matcher.all(FSMComponent));
}
protected processEntity(entity: Entity, dt: number): void {
const fsmComp = entity.getComponent(FSMComponent);
fsmComp.fsm.evaluateTransitions();
fsmComp.fsm.update(dt);
}
}
```
## Blueprint Nodes
The FSM module provides blueprint nodes for visual scripting:
- `GetCurrentState` - Get current state
- `TransitionTo` - Transition to specified state
- `CanTransition` - Check if transition is possible
- `IsInState` - Check if in specified state
- `WasInState` - Check if was ever in specified state
- `GetStateDuration` - Get state duration
- `EvaluateTransitions` - Evaluate transition conditions
- `ResetStateMachine` - Reset state machine

54
docs/en/modules/index.md Normal file
View File

@@ -0,0 +1,54 @@
# Modules
ESEngine provides a rich set of modules that can be imported as needed.
## Module List
### AI Modules
| Module | Package | Description |
|--------|---------|-------------|
| [Behavior Tree](/en/modules/behavior-tree/) | `@esengine/behavior-tree` | AI behavior tree with visual editor |
| [State Machine](/en/modules/fsm/) | `@esengine/fsm` | Finite state machine for character/AI states |
### Gameplay
| Module | Package | Description |
|--------|---------|-------------|
| [Timer](/en/modules/timer/) | `@esengine/timer` | Timer and cooldown system |
| [Spatial](/en/modules/spatial/) | `@esengine/spatial` | Spatial queries, AOI management |
| [Pathfinding](/en/modules/pathfinding/) | `@esengine/pathfinding` | A* pathfinding, NavMesh navigation |
### Tools
| Module | Package | Description |
|--------|---------|-------------|
| [Blueprint](/en/modules/blueprint/) | `@esengine/blueprint` | Visual scripting system |
| [Procgen](/en/modules/procgen/) | `@esengine/procgen` | Noise functions, random utilities |
### Network
| Module | Package | Description |
|--------|---------|-------------|
| [Network](/en/modules/network/) | `@esengine/network` | Multiplayer game networking |
## Installation
All modules can be installed independently:
```bash
# Install a single module
npm install @esengine/behavior-tree
# Or use CLI to add to existing project
npx @esengine/cli add behavior-tree
```
## Platform Compatibility
All modules are pure TypeScript and compatible with:
- Cocos Creator 3.x
- Laya 3.x
- Node.js
- Browser

View File

@@ -0,0 +1,727 @@
# Network System
`@esengine/network` provides a TSRPC-based client-server network synchronization solution for multiplayer games, including entity synchronization, input handling, and state interpolation.
## Overview
The network module consists of three packages:
| Package | Description |
|---------|-------------|
| `@esengine/network` | Client-side ECS plugin |
| `@esengine/network-protocols` | Shared protocol definitions |
| `@esengine/network-server` | Server-side implementation |
## Installation
```bash
# Client
npm install @esengine/network
# Server
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:
```
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
```
## Quick Start
### Client
```typescript
import { Core, Scene } from '@esengine/ecs-framework';
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
Core.create({ debug: false });
const scene = new GameScene();
Core.setScene(scene);
// Install network plugin
const networkPlugin = new NetworkPlugin();
await Core.installPlugin(networkPlugin);
// Register prefab factory
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());
return entity;
});
// Connect to server
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';
const server = new GameServer({
port: 3000,
roomConfig: {
maxPlayers: 16,
tickRate: 20
}
});
await server.start();
console.log('Server started on ws://localhost:3000');
```
## Core Concepts
### Architecture
```
Client Server
┌────────────────┐ ┌────────────────┐
│ NetworkPlugin │◄──── WS ────► │ GameServer │
│ ├─ Service │ │ ├─ Room │
│ ├─ SyncSystem │ │ └─ Players │
│ ├─ SpawnSystem │ └────────────────┘
│ └─ InputSystem │
└────────────────┘
```
### 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:
- Receives server state snapshots
- Stores states in snapshot buffer
- Performs interpolation for remote entities
#### NetworkSpawnSystem
Handles network entity spawning and despawning:
- Listens for Spawn/Despawn messages
- Creates entities using registered prefab factories
- Manages networked entity lifecycle
#### NetworkInputSystem
Handles local player input sending:
- Collects local player input
- Sends input to server
- Supports movement and action inputs
## API Reference
### NetworkPlugin
```typescript
class NetworkPlugin {
constructor(config: INetworkPluginConfig);
// Install plugin
install(services: ServiceContainer): void;
// Connect to server
connect(playerName: string, roomId?: string): Promise<void>;
// Disconnect
disconnect(): void;
// Register prefab factory
registerPrefab(prefab: string, factory: PrefabFactory): void;
// Properties
readonly localPlayerId: number | null;
readonly isConnected: boolean;
}
```
**Configuration:**
| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `serverUrl` | `string` | Yes | WebSocket server URL |
### NetworkService
Network service managing WebSocket connections:
```typescript
class NetworkService {
// Connection state
readonly state: ENetworkState;
readonly isConnected: boolean;
readonly clientId: number | null;
readonly roomId: string | null;
// Connection control
connect(serverUrl: string): Promise<void>;
disconnect(): void;
// Join room
join(playerName: string, roomId?: string): Promise<ResJoin>;
// Send input
sendInput(input: IPlayerInput): void;
// Event callbacks
setCallbacks(callbacks: Partial<INetworkCallbacks>): void;
}
```
**Network state enum:**
```typescript
enum ENetworkState {
Disconnected = 'disconnected',
Connecting = 'connecting',
Connected = 'connected',
Joining = 'joining',
Joined = 'joined'
}
```
**Callbacks interface:**
```typescript
interface INetworkCallbacks {
onConnected?: () => void;
onDisconnected?: () => void;
onJoined?: (clientId: number, roomId: string) => void;
onSync?: (msg: MsgSync) => void;
onSpawn?: (msg: MsgSpawn) => void;
onDespawn?: (msg: MsgDespawn) => void;
}
```
### Prefab Factory
```typescript
type PrefabFactory = (scene: Scene, spawn: MsgSpawn) => Entity;
```
Register prefab factories for network entity creation:
```typescript
networkPlugin.registerPrefab('enemy', (scene, spawn) => {
const entity = scene.createEntity(`enemy_${spawn.netId}`);
const identity = entity.addComponent(new NetworkIdentity());
identity.netId = spawn.netId;
identity.ownerId = spawn.ownerId;
entity.addComponent(new NetworkTransform());
entity.addComponent(new EnemyComponent());
return entity;
});
```
### Input System
#### NetworkInputSystem
```typescript
class NetworkInputSystem extends EntitySystem {
// Add movement input
addMoveInput(x: number, y: number): void;
// Add action input
addActionInput(action: string): void;
// Clear input
clearInput(): void;
}
```
Usage example:
```typescript
// Send input via NetworkPlugin (recommended)
networkPlugin.sendMoveInput(0, 1); // Movement
networkPlugin.sendActionInput('jump'); // Action
// Or use inputSystem directly
const inputSystem = networkPlugin.inputSystem;
if (keyboard.isPressed('W')) {
inputSystem.addMoveInput(0, 1);
}
if (keyboard.isPressed('Space')) {
inputSystem.addActionInput('jump');
}
```
## State Synchronization
### Snapshot Buffer
Stores server state snapshots for interpolation:
```typescript
import { createSnapshotBuffer, type IStateSnapshot } from '@esengine/network';
const buffer = createSnapshotBuffer<IStateSnapshot>({
maxSnapshots: 30, // Max snapshots
interpolationDelay: 100 // Interpolation delay (ms)
});
// Add snapshot
buffer.addSnapshot({
time: serverTime,
entities: states
});
// Get interpolated state
const interpolated = buffer.getInterpolatedState(clientTime);
```
### Transform Interpolators
#### Linear Interpolator
```typescript
import { createTransformInterpolator } from '@esengine/network';
const interpolator = createTransformInterpolator();
// Add state
interpolator.addState(time, { x: 0, y: 0, rotation: 0 });
// Get interpolated result
const state = interpolator.getInterpolatedState(currentTime);
```
#### Hermite Interpolator
Uses Hermite splines for smoother interpolation:
```typescript
import { createHermiteTransformInterpolator } from '@esengine/network';
const interpolator = createHermiteTransformInterpolator({
bufferSize: 10
});
// Add state with velocity
interpolator.addState(time, {
x: 100,
y: 200,
rotation: 0,
vx: 5,
vy: 0
});
// Get smooth interpolated result
const state = interpolator.getInterpolatedState(currentTime);
```
### Client Prediction
Implement client-side prediction with server reconciliation:
```typescript
import { createClientPrediction } from '@esengine/network';
const prediction = createClientPrediction({
maxPredictedInputs: 60,
reconciliationThreshold: 0.1
});
// Predict input
const seq = prediction.predict(inputState, currentState, (state, input) => {
// Apply input to state
return applyInput(state, input);
});
// Server reconciliation
const corrected = prediction.reconcile(
serverState,
serverSeq,
(state, input) => applyInput(state, input)
);
```
## Server Side
### GameServer
```typescript
import { GameServer } from '@esengine/network-server';
const server = new GameServer({
port: 3000,
roomConfig: {
maxPlayers: 16, // Max players per room
tickRate: 20 // Sync rate (Hz)
}
});
// Start server
await server.start();
// Get room
const room = server.getOrCreateRoom('room-id');
// Stop server
await server.stop();
```
### Room
```typescript
class Room {
readonly id: string;
readonly playerCount: number;
readonly isFull: boolean;
// Add player
addPlayer(name: string, connection: Connection): IPlayer | null;
// Remove player
removePlayer(clientId: number): void;
// Get player
getPlayer(clientId: number): IPlayer | undefined;
// Handle input
handleInput(clientId: number, input: IPlayerInput): void;
// Destroy room
destroy(): void;
}
```
**Player interface:**
```typescript
interface IPlayer {
clientId: number; // Client ID
name: string; // Player name
connection: Connection; // Connection object
netId: number; // Network entity ID
}
```
## Protocol Types
### Message Types
```typescript
// State sync message
interface MsgSync {
time: number;
entities: IEntityState[];
}
// Entity state
interface IEntityState {
netId: number;
pos?: Vec2;
rot?: number;
}
// Spawn message
interface MsgSpawn {
netId: number;
ownerId: number;
prefab: string;
pos: Vec2;
rot: number;
}
// Despawn message
interface MsgDespawn {
netId: number;
}
// Input message
interface MsgInput {
input: IPlayerInput;
}
// Player input
interface IPlayerInput {
seq?: number;
moveDir?: Vec2;
actions?: string[];
}
```
### API Types
```typescript
// Join request
interface ReqJoin {
playerName: string;
roomId?: string;
}
// Join response
interface ResJoin {
clientId: number;
roomId: string;
playerCount: number;
}
```
## Blueprint Nodes
The network module provides blueprint nodes for visual scripting:
- `IsLocalPlayer` - Check if entity is local player
- `IsServer` - Check if running on server
- `HasAuthority` - Check if has authority over entity
- `GetNetworkId` - Get entity's network ID
- `GetLocalPlayerId` - Get local player ID
## Service Tokens
For dependency injection:
```typescript
import {
NetworkServiceToken,
NetworkSyncSystemToken,
NetworkSpawnSystemToken,
NetworkInputSystemToken
} from '@esengine/network';
// Get service
const networkService = services.get(NetworkServiceToken);
```
## Practical Example
### Complete Multiplayer Client
```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
}
});
```

View File

@@ -0,0 +1,299 @@
# Pathfinding System
`@esengine/pathfinding` provides a complete 2D pathfinding solution including A* algorithm, grid maps, navigation meshes, and path smoothing.
## Installation
```bash
npm install @esengine/pathfinding
```
## Quick Start
### Grid Map Pathfinding
```typescript
import { createGridMap, createAStarPathfinder } from '@esengine/pathfinding';
// Create 20x20 grid
const grid = createGridMap(20, 20);
// Set obstacles
grid.setWalkable(5, 5, false);
grid.setWalkable(5, 6, false);
// Create pathfinder
const pathfinder = createAStarPathfinder(grid);
// Find path
const result = pathfinder.findPath(0, 0, 15, 15);
if (result.found) {
console.log('Path found!');
console.log('Path:', result.path);
console.log('Cost:', result.cost);
}
```
### NavMesh Pathfinding
```typescript
import { createNavMesh } from '@esengine/pathfinding';
const navmesh = createNavMesh();
// Add polygon areas
navmesh.addPolygon([
{ x: 0, y: 0 }, { x: 10, y: 0 },
{ x: 10, y: 10 }, { x: 0, y: 10 }
]);
navmesh.addPolygon([
{ x: 10, y: 0 }, { x: 20, y: 0 },
{ x: 20, y: 10 }, { x: 10, y: 10 }
]);
// Auto-build connections
navmesh.build();
// Find path
const result = navmesh.findPath(1, 1, 18, 8);
```
## Core Concepts
### IPathResult
```typescript
interface IPathResult {
readonly found: boolean; // Path found
readonly path: readonly IPoint[];// Path points
readonly cost: number; // Total cost
readonly nodesSearched: number; // Nodes searched
}
```
### IPathfindingOptions
```typescript
interface IPathfindingOptions {
maxNodes?: number; // Max search nodes (default 10000)
heuristicWeight?: number; // Heuristic weight (>1 faster but may be suboptimal)
allowDiagonal?: boolean; // Allow diagonal movement (default true)
avoidCorners?: boolean; // Avoid corner cutting (default true)
}
```
## Heuristic Functions
| Function | Use Case | Description |
|----------|----------|-------------|
| `manhattanDistance` | 4-directional | Manhattan distance |
| `euclideanDistance` | Any direction | Euclidean distance |
| `chebyshevDistance` | 8-directional | Diagonal cost = 1 |
| `octileDistance` | 8-directional | Diagonal cost = √2 (default) |
## Grid Map API
### createGridMap
```typescript
function createGridMap(
width: number,
height: number,
options?: IGridMapOptions
): GridMap
```
**Options:**
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `allowDiagonal` | `boolean` | `true` | Allow diagonal movement |
| `diagonalCost` | `number` | `√2` | Diagonal movement cost |
| `avoidCorners` | `boolean` | `true` | Avoid corner cutting |
| `heuristic` | `HeuristicFunction` | `octileDistance` | Heuristic function |
### Map Operations
```typescript
// Check/set walkability
grid.isWalkable(x, y);
grid.setWalkable(x, y, false);
// Set movement cost (e.g., swamp, sand)
grid.setCost(x, y, 2);
// Set rectangle region
grid.setRectWalkable(0, 0, 5, 5, false);
// Load from array (0=walkable, non-0=blocked)
grid.loadFromArray([
[0, 0, 0, 1, 0],
[0, 1, 0, 1, 0]
]);
// Load from string (.=walkable, #=blocked)
grid.loadFromString(`
.....
.#.#.
`);
// Export and reset
console.log(grid.toString());
grid.reset();
```
## A* Pathfinder API
```typescript
const pathfinder = createAStarPathfinder(grid);
const result = pathfinder.findPath(
startX, startY,
endX, endY,
{ maxNodes: 5000, heuristicWeight: 1.5 }
);
// Pathfinder is reusable
pathfinder.findPath(0, 0, 10, 10);
pathfinder.findPath(5, 5, 15, 15);
```
## NavMesh API
```typescript
const navmesh = createNavMesh();
// Add convex polygons
const id1 = navmesh.addPolygon(vertices1);
const id2 = navmesh.addPolygon(vertices2);
// Auto-detect shared edges
navmesh.build();
// Or manually set connections
navmesh.setConnection(id1, id2, {
left: { x: 10, y: 0 },
right: { x: 10, y: 10 }
});
// Query and pathfind
const polygon = navmesh.findPolygonAt(5, 5);
navmesh.isWalkable(5, 5);
const result = navmesh.findPath(1, 1, 18, 8);
```
## Path Smoothing
### Line of Sight Smoothing
Remove unnecessary waypoints:
```typescript
import { createLineOfSightSmoother } from '@esengine/pathfinding';
const smoother = createLineOfSightSmoother();
const smoothedPath = smoother.smooth(result.path, grid);
```
### Curve Smoothing
Catmull-Rom spline:
```typescript
import { createCatmullRomSmoother } from '@esengine/pathfinding';
const smoother = createCatmullRomSmoother(5, 0.5);
const curvedPath = smoother.smooth(result.path, grid);
```
### Combined Smoothing
```typescript
import { createCombinedSmoother } from '@esengine/pathfinding';
const smoother = createCombinedSmoother(5, 0.5);
const finalPath = smoother.smooth(result.path, grid);
```
### Line of Sight Functions
```typescript
import { bresenhamLineOfSight, raycastLineOfSight } from '@esengine/pathfinding';
const hasLOS = bresenhamLineOfSight(x1, y1, x2, y2, grid);
const hasLOS2 = raycastLineOfSight(x1, y1, x2, y2, grid, 0.5);
```
## Practical Examples
### Dynamic Obstacles
```typescript
class DynamicPathfinding {
private grid: GridMap;
private pathfinder: AStarPathfinder;
private dynamicObstacles: Set<string> = new Set();
addDynamicObstacle(x: number, y: number): void {
this.dynamicObstacles.add(`${x},${y}`);
this.grid.setWalkable(x, y, false);
}
removeDynamicObstacle(x: number, y: number): void {
this.dynamicObstacles.delete(`${x},${y}`);
this.grid.setWalkable(x, y, true);
}
}
```
### Terrain Costs
```typescript
const grid = createGridMap(50, 50);
// Normal ground - cost 1 (default)
// Sand - cost 2
for (let y = 10; y < 20; y++) {
for (let x = 0; x < 50; x++) {
grid.setCost(x, y, 2);
}
}
// Swamp - cost 4
for (let y = 30; y < 35; y++) {
for (let x = 20; x < 30; x++) {
grid.setCost(x, y, 4);
}
}
```
## Blueprint Nodes
- `FindPath` - Find path
- `FindPathSmooth` - Find and smooth path
- `IsWalkable` - Check walkability
- `GetPathLength` - Get path point count
- `GetPathDistance` - Get total path distance
- `GetPathPoint` - Get specific path point
- `MoveAlongPath` - Move along path
- `HasLineOfSight` - Check line of sight
## Performance Tips
1. **Limit search range**: `{ maxNodes: 1000 }`
2. **Use heuristic weight**: `{ heuristicWeight: 1.5 }` (faster but may not be optimal)
3. **Reuse pathfinder instances**
4. **Use NavMesh for complex terrain**
5. **Choose appropriate heuristic for movement type**
## Grid vs NavMesh
| Feature | GridMap | NavMesh |
|---------|---------|---------|
| Use Case | Regular tile maps | Complex polygon terrain |
| Memory | Higher (width × height) | Lower (polygon count) |
| Precision | Grid-aligned | Continuous coordinates |
| Dynamic Updates | Easy | Requires rebuild |
| Setup Complexity | Simple | More complex |

View File

@@ -0,0 +1,396 @@
# Procedural Generation (Procgen)
`@esengine/procgen` provides core tools for procedural content generation, including noise functions, seeded random numbers, and various random utilities.
## Installation
```bash
npm install @esengine/procgen
```
## Quick Start
### Noise Generation
```typescript
import { createPerlinNoise, createFBM } from '@esengine/procgen';
// Create Perlin noise
const perlin = createPerlinNoise(12345); // seed
// Sample 2D noise
const value = perlin.noise2D(x * 0.1, y * 0.1);
console.log(value); // [-1, 1]
// Use FBM for more natural results
const fbm = createFBM(perlin, {
octaves: 6,
persistence: 0.5
});
const height = fbm.noise2D(x * 0.01, y * 0.01);
```
### Seeded Random
```typescript
import { createSeededRandom } from '@esengine/procgen';
// Create deterministic random generator
const rng = createSeededRandom(42);
// Same seed always produces same sequence
console.log(rng.next()); // 0.xxx
console.log(rng.nextInt(1, 100)); // 1-100
console.log(rng.nextBool(0.3)); // 30% true
```
### Weighted Random
```typescript
import { createWeightedRandom, createSeededRandom } from '@esengine/procgen';
const rng = createSeededRandom(42);
const loot = createWeightedRandom([
{ value: 'common', weight: 60 },
{ value: 'uncommon', weight: 25 },
{ value: 'rare', weight: 10 },
{ value: 'legendary', weight: 5 }
]);
const drop = loot.pick(rng);
console.log(drop); // Likely 'common'
```
## Noise Functions
### Perlin Noise
Classic gradient noise, output range [-1, 1]:
```typescript
import { createPerlinNoise } from '@esengine/procgen';
const perlin = createPerlinNoise(seed);
const value2D = perlin.noise2D(x, y);
const value3D = perlin.noise3D(x, y, z);
```
### Simplex Noise
Faster than Perlin, less directional bias:
```typescript
import { createSimplexNoise } from '@esengine/procgen';
const simplex = createSimplexNoise(seed);
const value = simplex.noise2D(x, y);
```
### Worley Noise
Cell-based noise for stone, cell textures:
```typescript
import { createWorleyNoise } from '@esengine/procgen';
const worley = createWorleyNoise(seed);
const distance = worley.noise2D(x, y);
```
### FBM (Fractal Brownian Motion)
Layer multiple noise octaves for richer detail:
```typescript
import { createPerlinNoise, createFBM } from '@esengine/procgen';
const baseNoise = createPerlinNoise(seed);
const fbm = createFBM(baseNoise, {
octaves: 6, // Layer count (more = richer detail)
lacunarity: 2.0, // Frequency multiplier
persistence: 0.5, // Amplitude decay
frequency: 1.0, // Initial frequency
amplitude: 1.0 // Initial amplitude
});
// Standard FBM
const value = fbm.noise2D(x, y);
// Ridged FBM (for mountains)
const ridged = fbm.ridged2D(x, y);
// Turbulence
const turb = fbm.turbulence2D(x, y);
// Billowed (for clouds)
const cloud = fbm.billowed2D(x, y);
```
## Seeded Random API
### SeededRandom
Deterministic PRNG based on xorshift128+:
```typescript
import { createSeededRandom } from '@esengine/procgen';
const rng = createSeededRandom(42);
```
### Basic Methods
```typescript
rng.next(); // [0, 1) float
rng.nextInt(1, 10); // [min, max] integer
rng.nextFloat(0, 100); // [min, max) float
rng.nextBool(); // 50%
rng.nextBool(0.3); // 30%
rng.reset(); // Reset to initial state
```
### Distribution Methods
```typescript
// Normal distribution (Gaussian)
rng.nextGaussian(); // mean 0, stdDev 1
rng.nextGaussian(100, 15); // mean 100, stdDev 15
// Exponential distribution
rng.nextExponential(); // λ = 1
rng.nextExponential(0.5); // λ = 0.5
```
### Geometry Methods
```typescript
// Uniform point in circle
const point = rng.nextPointInCircle(50); // { x, y }
// Point on circle edge
const edge = rng.nextPointOnCircle(50); // { x, y }
// Uniform point in sphere
const point3D = rng.nextPointInSphere(50); // { x, y, z }
// Random direction vector
const dir = rng.nextDirection2D(); // { x, y }, length 1
```
## Weighted Random API
### WeightedRandom
Precomputed cumulative weights for efficient selection:
```typescript
import { createWeightedRandom } from '@esengine/procgen';
const selector = createWeightedRandom([
{ value: 'apple', weight: 5 },
{ value: 'banana', weight: 3 },
{ value: 'cherry', weight: 2 }
]);
const result = selector.pick(rng);
const result2 = selector.pickRandom(); // Uses Math.random
console.log(selector.getProbability(0)); // 0.5 (5/10)
console.log(selector.size); // 3
console.log(selector.totalWeight); // 10
```
### Convenience Functions
```typescript
import { weightedPick, weightedPickFromMap } from '@esengine/procgen';
const item = weightedPick([
{ value: 'a', weight: 1 },
{ value: 'b', weight: 2 }
], rng);
const item2 = weightedPickFromMap({
'common': 60,
'rare': 30,
'epic': 10
}, rng);
```
## Shuffle and Sampling
### shuffle / shuffleCopy
Fisher-Yates shuffle:
```typescript
import { shuffle, shuffleCopy } from '@esengine/procgen';
const arr = [1, 2, 3, 4, 5];
shuffle(arr, rng); // In-place
const shuffled = shuffleCopy(arr, rng); // Copy
```
### pickOne
```typescript
import { pickOne } from '@esengine/procgen';
const item = pickOne(['a', 'b', 'c', 'd'], rng);
```
### sample / sampleWithReplacement
```typescript
import { sample, sampleWithReplacement } from '@esengine/procgen';
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const unique = sample(arr, 3, rng); // 3 unique
const withRep = sampleWithReplacement(arr, 5, rng); // 5 with replacement
```
### randomIntegers
```typescript
import { randomIntegers } from '@esengine/procgen';
// 5 unique random integers from 1-100
const nums = randomIntegers(1, 100, 5, rng);
```
### weightedSample
```typescript
import { weightedSample } from '@esengine/procgen';
const items = ['A', 'B', 'C', 'D', 'E'];
const weights = [10, 8, 6, 4, 2];
const selected = weightedSample(items, weights, 3, rng);
```
## Practical Examples
### Procedural Terrain
```typescript
import { createPerlinNoise, createFBM } from '@esengine/procgen';
class TerrainGenerator {
private fbm: FBM;
private moistureFbm: FBM;
constructor(seed: number) {
const heightNoise = createPerlinNoise(seed);
const moistureNoise = createPerlinNoise(seed + 1000);
this.fbm = createFBM(heightNoise, {
octaves: 8,
persistence: 0.5,
frequency: 0.01
});
this.moistureFbm = createFBM(moistureNoise, {
octaves: 4,
persistence: 0.6,
frequency: 0.02
});
}
getHeight(x: number, y: number): number {
let height = this.fbm.noise2D(x, y);
height += this.fbm.ridged2D(x * 0.5, y * 0.5) * 0.3;
return (height + 1) * 0.5; // Normalize to [0, 1]
}
getBiome(x: number, y: number): string {
const height = this.getHeight(x, y);
const moisture = (this.moistureFbm.noise2D(x, y) + 1) * 0.5;
if (height < 0.3) return 'water';
if (height < 0.4) return 'beach';
if (height > 0.8) return 'mountain';
if (moisture < 0.3) return 'desert';
if (moisture > 0.7) return 'forest';
return 'grassland';
}
}
```
### Loot System
```typescript
import { createSeededRandom, createWeightedRandom } from '@esengine/procgen';
class LootSystem {
private rng: SeededRandom;
private raritySelector: WeightedRandom<string>;
constructor(seed: number) {
this.rng = createSeededRandom(seed);
this.raritySelector = createWeightedRandom([
{ value: 'common', weight: 60 },
{ value: 'uncommon', weight: 25 },
{ value: 'rare', weight: 10 },
{ value: 'legendary', weight: 5 }
]);
}
generateLoot(count: number): LootItem[] {
const loot: LootItem[] = [];
for (let i = 0; i < count; i++) {
const rarity = this.raritySelector.pick(this.rng);
// Get item from rarity table...
loot.push(item);
}
return loot;
}
}
```
## Blueprint Nodes
### Noise Nodes
- `SampleNoise2D` - Sample 2D noise
- `SampleFBM` - Sample FBM noise
### Random Nodes
- `SeededRandom` - Generate random float
- `SeededRandomInt` - Generate random integer
- `WeightedPick` - Weighted random selection
- `ShuffleArray` - Shuffle array
- `PickRandom` - Pick random element
- `SampleArray` - Sample from array
- `RandomPointInCircle` - Random point in circle
## Best Practices
1. **Use seeds for reproducibility**
```typescript
const seed = Date.now();
const rng = createSeededRandom(seed);
saveSeed(seed);
```
2. **Precompute weighted selectors**
```typescript
// Good: Create once, use many times
const selector = createWeightedRandom(items);
for (let i = 0; i < 1000; i++) {
selector.pick(rng);
}
```
3. **Choose appropriate noise**
- Perlin: Smooth terrain, clouds
- Simplex: Performance-critical
- Worley: Cell textures, stone
- FBM: Natural multi-detail effects
4. **Tune FBM parameters**
- `octaves`: More = richer detail, higher cost
- `persistence`: 0.5 is common, higher = more high-frequency detail
- `lacunarity`: Usually 2, controls frequency growth

View File

@@ -0,0 +1,322 @@
# Spatial Index System
`@esengine/spatial` provides efficient spatial querying and indexing, including range queries, nearest neighbor queries, raycasting, and AOI (Area of Interest) management.
## Installation
```bash
npm install @esengine/spatial
```
## Quick Start
### Spatial Index
```typescript
import { createGridSpatialIndex } from '@esengine/spatial';
// Create spatial index (cell size 100)
const spatialIndex = createGridSpatialIndex<Entity>(100);
// Insert objects
spatialIndex.insert(player, { x: 100, y: 200 });
spatialIndex.insert(enemy1, { x: 150, y: 250 });
spatialIndex.insert(enemy2, { x: 500, y: 600 });
// Find objects within radius
const nearby = spatialIndex.findInRadius({ x: 100, y: 200 }, 100);
console.log(nearby); // [player, enemy1]
// Find nearest object
const nearest = spatialIndex.findNearest({ x: 100, y: 200 });
console.log(nearest); // enemy1
// Update position
spatialIndex.update(player, { x: 120, y: 220 });
```
### AOI (Area of Interest)
```typescript
import { createGridAOI } from '@esengine/spatial';
// Create AOI manager
const aoi = createGridAOI<Entity>(100);
// Add observers
aoi.addObserver(player, { x: 100, y: 100 }, { viewRange: 200 });
aoi.addObserver(npc, { x: 150, y: 150 }, { viewRange: 150 });
// Listen to enter/exit events
aoi.addListener((event) => {
if (event.type === 'enter') {
console.log(`${event.observer} saw ${event.target}`);
} else if (event.type === 'exit') {
console.log(`${event.target} left ${event.observer}'s view`);
}
});
// Update position (triggers enter/exit events)
aoi.updatePosition(player, { x: 200, y: 200 });
// Get visible entities
const visible = aoi.getEntitiesInView(player);
```
## Core Concepts
### Spatial Index vs AOI
| Feature | SpatialIndex | AOI |
|---------|--------------|-----|
| Purpose | General spatial queries | Entity visibility tracking |
| Events | No event notification | Enter/exit events |
| Direction | One-way query | Two-way tracking |
| Use Cases | Collision, range attacks | MMO sync, NPC AI perception |
### IBounds
```typescript
interface IBounds {
readonly minX: number;
readonly minY: number;
readonly maxX: number;
readonly maxY: number;
}
```
### IRaycastHit
```typescript
interface IRaycastHit<T> {
readonly target: T; // Hit object
readonly point: IVector2; // Hit point
readonly normal: IVector2;// Hit normal
readonly distance: number;// Distance from origin
}
```
## Spatial Index API
### createGridSpatialIndex
```typescript
function createGridSpatialIndex<T>(cellSize?: number): GridSpatialIndex<T>
```
**Choosing cellSize:**
- Too small: High memory, reduced query efficiency
- Too large: Many objects per cell, slow iteration
- Recommended: 1-2x average object spacing
### Management Methods
```typescript
spatialIndex.insert(entity, position);
spatialIndex.remove(entity);
spatialIndex.update(entity, newPosition);
spatialIndex.clear();
```
### Query Methods
#### findInRadius
```typescript
const enemies = spatialIndex.findInRadius(
{ x: 100, y: 200 },
50,
(entity) => entity.type === 'enemy' // Optional filter
);
```
#### findInRect
```typescript
import { createBounds } from '@esengine/spatial';
const bounds = createBounds(0, 0, 200, 200);
const entities = spatialIndex.findInRect(bounds);
```
#### findNearest
```typescript
const nearest = spatialIndex.findNearest(
playerPosition,
500, // maxDistance
(entity) => entity.type === 'enemy'
);
```
#### findKNearest
```typescript
const nearestEnemies = spatialIndex.findKNearest(
playerPosition,
5, // k
500, // maxDistance
(entity) => entity.type === 'enemy'
);
```
#### raycast / raycastFirst
```typescript
const hits = spatialIndex.raycast(origin, direction, maxDistance);
const firstHit = spatialIndex.raycastFirst(origin, direction, maxDistance);
```
## AOI API
### createGridAOI
```typescript
function createGridAOI<T>(cellSize?: number): GridAOI<T>
```
### Observer Management
```typescript
// Add observer
aoi.addObserver(player, position, {
viewRange: 200,
observable: true // Can be seen by others
});
// Remove observer
aoi.removeObserver(player);
// Update position
aoi.updatePosition(player, newPosition);
// Update view range
aoi.updateViewRange(player, 300);
```
### Query Methods
```typescript
// Get entities in observer's view
const visible = aoi.getEntitiesInView(player);
// Get observers who can see entity
const observers = aoi.getObserversOf(monster);
// Check visibility
if (aoi.canSee(player, enemy)) { ... }
```
### Event System
```typescript
// Global event listener
aoi.addListener((event) => {
switch (event.type) {
case 'enter': /* entered view */ break;
case 'exit': /* left view */ break;
}
});
// Entity-specific listener
aoi.addEntityListener(player, (event) => {
if (event.type === 'enter') {
sendToClient(player, 'entity_enter', event.target);
}
});
```
## Utility Functions
### Bounds Creation
```typescript
import {
createBounds,
createBoundsFromCenter,
createBoundsFromCircle
} from '@esengine/spatial';
const bounds1 = createBounds(0, 0, 100, 100);
const bounds2 = createBoundsFromCenter({ x: 50, y: 50 }, 100, 100);
const bounds3 = createBoundsFromCircle({ x: 50, y: 50 }, 50);
```
### Geometry Checks
```typescript
import {
isPointInBounds,
boundsIntersect,
boundsIntersectsCircle,
distance,
distanceSquared
} from '@esengine/spatial';
if (isPointInBounds(point, bounds)) { ... }
if (boundsIntersect(boundsA, boundsB)) { ... }
if (boundsIntersectsCircle(bounds, center, radius)) { ... }
const dist = distance(pointA, pointB);
const distSq = distanceSquared(pointA, pointB); // Faster
```
## Practical Examples
### Range Attack Detection
```typescript
class CombatSystem {
private spatialIndex: ISpatialIndex<Entity>;
dealAreaDamage(center: IVector2, radius: number, damage: number): void {
const targets = this.spatialIndex.findInRadius(
center, radius,
(entity) => entity.hasComponent(HealthComponent)
);
for (const target of targets) {
target.getComponent(HealthComponent).takeDamage(damage);
}
}
}
```
### MMO Sync System
```typescript
class SyncSystem {
private aoi: IAOIManager<Player>;
constructor() {
this.aoi = createGridAOI<Player>(100);
this.aoi.addListener((event) => {
const packet = this.createSyncPacket(event);
this.sendToPlayer(event.observer, packet);
});
}
onPlayerMove(player: Player, newPosition: IVector2): void {
this.aoi.updatePosition(player, newPosition);
}
}
```
## Blueprint Nodes
### Spatial Query Nodes
- `FindInRadius`, `FindInRect`, `FindNearest`, `FindKNearest`
- `Raycast`, `RaycastFirst`
### AOI Nodes
- `GetEntitiesInView`, `GetObserversOf`, `CanSee`
- `OnEntityEnterView`, `OnEntityExitView`
## Service Tokens
```typescript
import { SpatialIndexToken, AOIManagerToken } from '@esengine/spatial';
services.register(SpatialIndexToken, createGridSpatialIndex(100));
services.register(AOIManagerToken, createGridAOI(100));
```

View File

@@ -0,0 +1,352 @@
# Timer System
`@esengine/timer` provides a flexible timer and cooldown system for delayed execution, repeating tasks, skill cooldowns, and more.
## Installation
```bash
npm install @esengine/timer
```
## Quick Start
```typescript
import { createTimerService } from '@esengine/timer';
// Create timer service
const timerService = createTimerService();
// One-time timer (executes after 1 second)
const handle = timerService.schedule('myTimer', 1000, () => {
console.log('Timer fired!');
});
// Repeating timer (every 100ms)
timerService.scheduleRepeating('heartbeat', 100, () => {
console.log('Tick');
});
// Cooldown system (5 second cooldown)
timerService.startCooldown('skill_fireball', 5000);
if (timerService.isCooldownReady('skill_fireball')) {
useFireball();
timerService.startCooldown('skill_fireball', 5000);
}
// Update in game loop
function gameLoop(deltaTime: number) {
timerService.update(deltaTime);
}
```
## Core Concepts
### Timer vs Cooldown
| Feature | Timer | Cooldown |
|---------|-------|----------|
| Purpose | Delayed code execution | Rate limiting |
| Callback | Has callback function | No callback |
| Repeat | Supports repeating | One-time |
| Query | Query remaining time | Query progress/ready status |
### TimerHandle
Handle object returned when scheduling a timer:
```typescript
interface TimerHandle {
readonly id: string; // Timer ID
readonly isValid: boolean; // Whether valid (not cancelled)
cancel(): void; // Cancel timer
}
```
### TimerInfo
Timer information object:
```typescript
interface TimerInfo {
readonly id: string; // Timer ID
readonly remaining: number; // Remaining time (ms)
readonly repeating: boolean; // Whether repeating
readonly interval?: number; // Interval (repeating only)
}
```
### CooldownInfo
Cooldown information object:
```typescript
interface CooldownInfo {
readonly id: string; // Cooldown ID
readonly duration: number; // Total duration (ms)
readonly remaining: number; // Remaining time (ms)
readonly progress: number; // Progress (0-1, 0=started, 1=finished)
readonly isReady: boolean; // Whether ready
}
```
## API Reference
### createTimerService
```typescript
function createTimerService(config?: TimerServiceConfig): ITimerService
```
**Configuration:**
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `maxTimers` | `number` | `0` | Maximum timer count (0 = unlimited) |
| `maxCooldowns` | `number` | `0` | Maximum cooldown count (0 = unlimited) |
### Timer API
#### schedule
Schedule a one-time timer:
```typescript
const handle = timerService.schedule('explosion', 2000, () => {
createExplosion();
});
// Cancel early
handle.cancel();
```
#### scheduleRepeating
Schedule a repeating timer:
```typescript
// Execute every second
timerService.scheduleRepeating('regen', 1000, () => {
player.hp += 5;
});
// Execute immediately once, then repeat every second
timerService.scheduleRepeating('tick', 1000, () => {
console.log('Tick');
}, true); // immediate = true
```
#### cancel / cancelById
Cancel timers:
```typescript
// Cancel by handle
handle.cancel();
// or
timerService.cancel(handle);
// Cancel by ID
timerService.cancelById('regen');
```
#### hasTimer
Check if timer exists:
```typescript
if (timerService.hasTimer('explosion')) {
console.log('Explosion is pending');
}
```
#### getTimerInfo
Get timer information:
```typescript
const info = timerService.getTimerInfo('explosion');
if (info) {
console.log(`Remaining: ${info.remaining}ms`);
console.log(`Repeating: ${info.repeating}`);
}
```
### Cooldown API
#### startCooldown
Start a cooldown:
```typescript
timerService.startCooldown('skill_fireball', 5000);
```
#### isCooldownReady / isOnCooldown
Check cooldown status:
```typescript
if (timerService.isCooldownReady('skill_fireball')) {
castFireball();
timerService.startCooldown('skill_fireball', 5000);
}
if (timerService.isOnCooldown('skill_fireball')) {
console.log('On cooldown...');
}
```
#### getCooldownProgress / getCooldownRemaining
Get cooldown progress:
```typescript
// Progress 0-1 (0=started, 1=complete)
const progress = timerService.getCooldownProgress('skill_fireball');
console.log(`Progress: ${(progress * 100).toFixed(0)}%`);
// Remaining time (ms)
const remaining = timerService.getCooldownRemaining('skill_fireball');
console.log(`Remaining: ${(remaining / 1000).toFixed(1)}s`);
```
#### getCooldownInfo
Get complete cooldown info:
```typescript
const info = timerService.getCooldownInfo('skill_fireball');
if (info) {
console.log(`Duration: ${info.duration}ms`);
console.log(`Remaining: ${info.remaining}ms`);
console.log(`Progress: ${info.progress}`);
console.log(`Ready: ${info.isReady}`);
}
```
#### resetCooldown / clearAllCooldowns
Reset cooldowns:
```typescript
// Reset single cooldown
timerService.resetCooldown('skill_fireball');
// Clear all cooldowns (e.g., on respawn)
timerService.clearAllCooldowns();
```
### Lifecycle
#### update
Update timer service (call every frame):
```typescript
function gameLoop(deltaTime: number) {
timerService.update(deltaTime); // deltaTime in ms
}
```
#### clear
Clear all timers and cooldowns:
```typescript
timerService.clear();
```
### Debug Properties
```typescript
console.log(timerService.activeTimerCount);
console.log(timerService.activeCooldownCount);
const timerIds = timerService.getActiveTimerIds();
const cooldownIds = timerService.getActiveCooldownIds();
```
## Practical Examples
### Skill Cooldown System
```typescript
import { createTimerService, type ITimerService } from '@esengine/timer';
class SkillSystem {
private timerService: ITimerService;
private skills: Map<string, SkillData> = new Map();
constructor() {
this.timerService = createTimerService();
}
useSkill(skillId: string): boolean {
const skill = this.skills.get(skillId);
if (!skill) return false;
if (!this.timerService.isCooldownReady(skillId)) {
const remaining = this.timerService.getCooldownRemaining(skillId);
console.log(`Skill ${skillId} on cooldown, ${remaining}ms remaining`);
return false;
}
this.executeSkill(skill);
this.timerService.startCooldown(skillId, skill.cooldown);
return true;
}
update(dt: number): void {
this.timerService.update(dt);
}
}
```
### DOT Effects
```typescript
class EffectSystem {
private timerService: ITimerService;
applyDOT(target: Entity, damage: number, duration: number): void {
const dotId = `dot_${target.id}_${Date.now()}`;
let elapsed = 0;
this.timerService.scheduleRepeating(dotId, 1000, () => {
elapsed += 1000;
target.takeDamage(damage);
if (elapsed >= duration) {
this.timerService.cancelById(dotId);
}
});
}
}
```
## Blueprint Nodes
### Cooldown Nodes
- `StartCooldown` - Start cooldown
- `IsCooldownReady` - Check if cooldown is ready
- `GetCooldownProgress` - Get cooldown progress
- `GetCooldownInfo` - Get cooldown info
- `ResetCooldown` - Reset cooldown
### Timer Nodes
- `HasTimer` - Check if timer exists
- `CancelTimer` - Cancel timer
- `GetTimerRemaining` - Get timer remaining time
## Service Token
For dependency injection:
```typescript
import { TimerServiceToken, createTimerService } from '@esengine/timer';
services.register(TimerServiceToken, createTimerService());
const timerService = services.get(TimerServiceToken);
```

View File

@@ -0,0 +1,507 @@
# 蓝图可视化脚本 (Blueprint)
`@esengine/blueprint` 提供了一个功能完整的可视化脚本系统,支持节点式编程、事件驱动和蓝图组合。
## 安装
```bash
npm install @esengine/blueprint
```
## 快速开始
```typescript
import {
createBlueprintSystem,
createBlueprintComponentData,
NodeRegistry,
RegisterNode
} from '@esengine/blueprint';
// 创建蓝图系统
const blueprintSystem = createBlueprintSystem(scene);
// 加载蓝图资产
const blueprint = await loadBlueprintAsset('player.bp');
// 创建蓝图组件数据
const componentData = createBlueprintComponentData();
componentData.blueprintAsset = blueprint;
// 在游戏循环中更新
function gameLoop(dt: number) {
blueprintSystem.process(entities, dt);
}
```
## 核心概念
### 蓝图资产结构
蓝图保存为 `.bp` 文件,包含以下结构:
```typescript
interface BlueprintAsset {
version: number; // 格式版本
type: 'blueprint'; // 资产类型
metadata: BlueprintMetadata; // 元数据
variables: BlueprintVariable[]; // 变量定义
nodes: BlueprintNode[]; // 节点实例
connections: BlueprintConnection[]; // 连接
}
```
### 节点类型
节点按功能分为以下类别:
| 类别 | 说明 | 颜色 |
|------|------|------|
| `event` | 事件节点(入口点) | 红色 |
| `flow` | 流程控制 | 灰色 |
| `entity` | 实体操作 | 蓝色 |
| `component` | 组件访问 | 青色 |
| `math` | 数学运算 | 绿色 |
| `logic` | 逻辑运算 | 红色 |
| `variable` | 变量访问 | 紫色 |
| `time` | 时间工具 | 青色 |
| `debug` | 调试工具 | 灰色 |
### 引脚类型
节点通过引脚连接:
```typescript
interface BlueprintPinDefinition {
name: string; // 引脚名称
type: PinDataType; // 数据类型
direction: 'input' | 'output';
isExec?: boolean; // 是否是执行引脚
defaultValue?: unknown;
}
// 支持的数据类型
type PinDataType =
| 'exec' // 执行流
| 'boolean' // 布尔值
| 'number' // 数字
| 'string' // 字符串
| 'vector2' // 2D 向量
| 'vector3' // 3D 向量
| 'entity' // 实体引用
| 'component' // 组件引用
| 'any'; // 任意类型
```
### 变量作用域
```typescript
type VariableScope =
| 'local' // 每次执行独立
| 'instance' // 每个实体独立
| 'global'; // 全局共享
```
## 虚拟机 API
### BlueprintVM
蓝图虚拟机负责执行蓝图图:
```typescript
import { BlueprintVM } from '@esengine/blueprint';
// 创建 VM
const vm = new BlueprintVM(blueprintAsset, entity, scene);
// 启动(触发 BeginPlay
vm.start();
// 每帧更新(触发 Tick
vm.tick(deltaTime);
// 停止(触发 EndPlay
vm.stop();
// 暂停/恢复
vm.pause();
vm.resume();
// 触发事件
vm.triggerEvent('EventCollision', { other: otherEntity });
vm.triggerCustomEvent('OnDamage', { amount: 50 });
// 调试模式
vm.debug = true;
```
### 执行上下文
```typescript
interface ExecutionContext {
blueprint: BlueprintAsset; // 蓝图资产
entity: Entity; // 当前实体
scene: IScene; // 当前场景
deltaTime: number; // 帧间隔时间
time: number; // 总运行时间
// 获取输入值
getInput<T>(nodeId: string, pinName: string): T;
// 设置输出值
setOutput(nodeId: string, pinName: string, value: unknown): void;
// 变量访问
getVariable<T>(name: string): T;
setVariable(name: string, value: unknown): void;
}
```
### 执行结果
```typescript
interface ExecutionResult {
outputs?: Record<string, unknown>; // 输出值
nextExec?: string | null; // 下一个执行引脚
delay?: number; // 延迟执行(毫秒)
yield?: boolean; // 暂停到下一帧
error?: string; // 错误信息
}
```
## 自定义节点
### 定义节点模板
```typescript
import { BlueprintNodeTemplate } from '@esengine/blueprint';
const MyNodeTemplate: BlueprintNodeTemplate = {
type: 'MyCustomNode',
title: 'My Custom Node',
category: 'custom',
description: 'A custom node example',
keywords: ['custom', 'example'],
inputs: [
{ name: 'exec', type: 'exec', direction: 'input', isExec: true },
{ name: 'value', type: 'number', direction: 'input', defaultValue: 0 }
],
outputs: [
{ name: 'exec', type: 'exec', direction: 'output', isExec: true },
{ name: 'result', type: 'number', direction: 'output' }
]
};
```
### 实现节点执行器
```typescript
import { INodeExecutor, RegisterNode } from '@esengine/blueprint';
@RegisterNode(MyNodeTemplate)
class MyNodeExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
// 获取输入
const value = context.getInput<number>(node.id, 'value');
// 执行逻辑
const result = value * 2;
// 返回结果
return {
outputs: { result },
nextExec: 'exec' // 继续执行
};
}
}
```
### 使用装饰器注册
```typescript
// 方式 1: 使用装饰器
@RegisterNode(MyNodeTemplate)
class MyNodeExecutor implements INodeExecutor { ... }
// 方式 2: 手动注册
NodeRegistry.instance.register(MyNodeTemplate, new MyNodeExecutor());
```
## 节点注册表
```typescript
import { NodeRegistry } from '@esengine/blueprint';
// 获取单例
const registry = NodeRegistry.instance;
// 获取所有模板
const allTemplates = registry.getAllTemplates();
// 按类别获取
const mathNodes = registry.getTemplatesByCategory('math');
// 搜索节点
const results = registry.searchTemplates('add');
// 检查是否存在
if (registry.has('MyCustomNode')) { ... }
```
## 内置节点
### 事件节点
| 节点 | 说明 |
|------|------|
| `EventBeginPlay` | 蓝图启动时触发 |
| `EventTick` | 每帧触发 |
| `EventEndPlay` | 蓝图停止时触发 |
| `EventCollision` | 碰撞时触发 |
| `EventInput` | 输入事件触发 |
| `EventTimer` | 定时器触发 |
| `EventMessage` | 自定义消息触发 |
### 时间节点
| 节点 | 说明 |
|------|------|
| `Delay` | 延迟执行 |
| `GetDeltaTime` | 获取帧间隔 |
| `GetTime` | 获取运行时间 |
### 数学节点
| 节点 | 说明 |
|------|------|
| `Add` | 加法 |
| `Subtract` | 减法 |
| `Multiply` | 乘法 |
| `Divide` | 除法 |
| `Abs` | 绝对值 |
| `Clamp` | 限制范围 |
| `Lerp` | 线性插值 |
| `Min` / `Max` | 最小/最大值 |
### 调试节点
| 节点 | 说明 |
|------|------|
| `Print` | 打印到控制台 |
## 蓝图组合
### 蓝图片段
将可复用的逻辑封装为片段:
```typescript
import { createFragment } from '@esengine/blueprint';
const healthFragment = createFragment('HealthSystem', {
inputs: [
{ name: 'damage', type: 'number', internalNodeId: 'input1', internalPinName: 'value' }
],
outputs: [
{ name: 'isDead', type: 'boolean', internalNodeId: 'output1', internalPinName: 'value' }
],
graph: {
nodes: [...],
connections: [...],
variables: [...]
}
});
```
### 组合蓝图
```typescript
import { createComposer, FragmentRegistry } from '@esengine/blueprint';
// 注册片段
FragmentRegistry.instance.register('health', healthFragment);
FragmentRegistry.instance.register('movement', movementFragment);
// 创建组合器
const composer = createComposer('PlayerBlueprint');
// 添加片段到槽位
composer.addFragment(healthFragment, 'slot1', { position: { x: 0, y: 0 } });
composer.addFragment(movementFragment, 'slot2', { position: { x: 400, y: 0 } });
// 连接槽位
composer.connect('slot1', 'onDeath', 'slot2', 'disable');
// 验证
const validation = composer.validate();
if (!validation.isValid) {
console.error(validation.errors);
}
// 编译成蓝图
const blueprint = composer.compile();
```
## 触发器系统
### 定义触发条件
```typescript
import { TriggerCondition, TriggerDispatcher } from '@esengine/blueprint';
const lowHealthCondition: TriggerCondition = {
type: 'comparison',
left: { type: 'variable', name: 'health' },
operator: '<',
right: { type: 'constant', value: 20 }
};
```
### 使用触发器分发器
```typescript
const dispatcher = new TriggerDispatcher();
// 注册触发器
dispatcher.register('lowHealth', lowHealthCondition, (context) => {
context.triggerEvent('OnLowHealth');
});
// 每帧评估
dispatcher.evaluate(context);
```
## 与 ECS 集成
### 使用蓝图系统
```typescript
import { createBlueprintSystem } from '@esengine/blueprint';
class GameScene {
private blueprintSystem: BlueprintSystem;
initialize() {
this.blueprintSystem = createBlueprintSystem(this.scene);
}
update(dt: number) {
// 处理所有带蓝图组件的实体
this.blueprintSystem.process(this.entities, dt);
}
}
```
### 触发蓝图事件
```typescript
import { triggerBlueprintEvent, triggerCustomBlueprintEvent } from '@esengine/blueprint';
// 触发内置事件
triggerBlueprintEvent(entity, 'Collision', { other: otherEntity });
// 触发自定义事件
triggerCustomBlueprintEvent(entity, 'OnPickup', { item: itemEntity });
```
## 实际示例
### 玩家控制蓝图
```typescript
// 定义输入处理节点
const InputMoveTemplate: BlueprintNodeTemplate = {
type: 'InputMove',
title: 'Get Movement Input',
category: 'input',
inputs: [],
outputs: [
{ name: 'direction', type: 'vector2', direction: 'output' }
],
isPure: true
};
@RegisterNode(InputMoveTemplate)
class InputMoveExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const input = context.scene.services.get(InputServiceToken);
const direction = {
x: input.getAxis('horizontal'),
y: input.getAxis('vertical')
};
return { outputs: { direction } };
}
}
```
### 状态切换逻辑
```typescript
// 在蓝图中实现状态机逻辑
const stateBlueprint = createEmptyBlueprint('PlayerState');
// 添加状态变量
stateBlueprint.variables.push({
name: 'currentState',
type: 'string',
defaultValue: 'idle',
scope: 'instance'
});
// 在 Tick 事件中检查状态转换
// ... 通过节点连接实现
```
## 序列化
### 保存蓝图
```typescript
import { validateBlueprintAsset } from '@esengine/blueprint';
function saveBlueprint(blueprint: BlueprintAsset, path: string): void {
if (!validateBlueprintAsset(blueprint)) {
throw new Error('Invalid blueprint structure');
}
const json = JSON.stringify(blueprint, null, 2);
fs.writeFileSync(path, json);
}
```
### 加载蓝图
```typescript
async function loadBlueprint(path: string): Promise<BlueprintAsset> {
const json = await fs.readFile(path, 'utf-8');
const asset = JSON.parse(json);
if (!validateBlueprintAsset(asset)) {
throw new Error('Invalid blueprint file');
}
return asset;
}
```
## 最佳实践
1. **使用片段复用逻辑**
- 将通用逻辑封装为片段
- 通过组合器构建复杂蓝图
2. **合理使用变量作用域**
- `local`: 临时计算结果
- `instance`: 实体状态(如生命值)
- `global`: 游戏全局状态
3. **避免无限循环**
- VM 有每帧最大执行步数限制(默认 1000
- 使用 Delay 节点打断长执行链
4. **调试技巧**
- 启用 `vm.debug = true` 查看执行日志
- 使用 Print 节点输出中间值
5. **性能优化**
- 纯节点(`isPure: true`)的输出会被缓存
- 避免在 Tick 中执行重计算

337
docs/modules/fsm/index.md Normal file
View File

@@ -0,0 +1,337 @@
# 状态机 (FSM)
`@esengine/fsm` 提供了一个类型安全的有限状态机实现用于角色、AI 或任何需要状态管理的场景。
## 安装
```bash
npm install @esengine/fsm
```
## 快速开始
```typescript
import { createStateMachine } from '@esengine/fsm';
// 定义状态类型
type PlayerState = 'idle' | 'walk' | 'run' | 'jump';
// 创建状态机
const fsm = createStateMachine<PlayerState>('idle');
// 定义状态和回调
fsm.defineState('idle', {
onEnter: (ctx, from) => console.log(`${from} 进入 idle`),
onExit: (ctx, to) => console.log(`从 idle 退出到 ${to}`),
onUpdate: (ctx, dt) => { /* 每帧更新 */ }
});
fsm.defineState('walk', {
onEnter: () => console.log('开始行走')
});
// 手动切换状态
fsm.transition('walk');
console.log(fsm.current); // 'walk'
```
## 核心概念
### 状态配置
每个状态可以配置以下回调:
```typescript
interface StateConfig<TState, TContext> {
name: TState; // 状态名称
onEnter?: (context: TContext, from: TState | null) => void; // 进入回调
onExit?: (context: TContext, to: TState) => void; // 退出回调
onUpdate?: (context: TContext, deltaTime: number) => void; // 更新回调
tags?: string[]; // 状态标签
metadata?: Record<string, unknown>; // 元数据
}
```
### 转换条件
可以定义带条件的状态转换:
```typescript
interface Context {
isMoving: boolean;
isRunning: boolean;
isGrounded: boolean;
}
const fsm = createStateMachine<PlayerState, Context>('idle', {
context: { isMoving: false, isRunning: false, isGrounded: true }
});
// 定义转换条件
fsm.defineTransition('idle', 'walk', (ctx) => ctx.isMoving);
fsm.defineTransition('walk', 'run', (ctx) => ctx.isRunning);
fsm.defineTransition('walk', 'idle', (ctx) => !ctx.isMoving);
// 自动评估并执行满足条件的转换
fsm.evaluateTransitions();
```
### 转换优先级
当多个转换条件同时满足时,优先级高的先执行:
```typescript
// 优先级数字越大越优先
fsm.defineTransition('idle', 'attack', (ctx) => ctx.isAttacking, 10);
fsm.defineTransition('idle', 'walk', (ctx) => ctx.isMoving, 1);
// 如果同时满足,会先尝试 attack优先级 10
```
## API 参考
### createStateMachine
```typescript
function createStateMachine<TState extends string, TContext = unknown>(
initialState: TState,
options?: StateMachineOptions<TContext>
): IStateMachine<TState, TContext>
```
**参数:**
- `initialState` - 初始状态
- `options.context` - 上下文对象,在回调中可访问
- `options.maxHistorySize` - 最大历史记录数(默认 100
- `options.enableHistory` - 是否启用历史记录(默认 true
### 状态机属性
| 属性 | 类型 | 描述 |
|------|------|------|
| `current` | `TState` | 当前状态 |
| `previous` | `TState \| null` | 上一个状态 |
| `context` | `TContext` | 上下文对象 |
| `isTransitioning` | `boolean` | 是否正在转换中 |
| `currentStateDuration` | `number` | 当前状态持续时间(毫秒) |
### 状态机方法
#### 状态定义
```typescript
// 定义状态
fsm.defineState('idle', {
onEnter: (ctx, from) => {},
onExit: (ctx, to) => {},
onUpdate: (ctx, dt) => {}
});
// 检查状态是否存在
fsm.hasState('idle'); // true
// 获取状态配置
fsm.getStateConfig('idle');
// 获取所有状态
fsm.getStates(); // ['idle', 'walk', ...]
```
#### 转换操作
```typescript
// 定义转换
fsm.defineTransition('idle', 'walk', condition, priority);
// 移除转换
fsm.removeTransition('idle', 'walk');
// 获取从某状态出发的所有转换
fsm.getTransitionsFrom('idle');
// 检查是否可以转换
fsm.canTransition('walk'); // true/false
// 手动转换
fsm.transition('walk');
// 强制转换(忽略条件)
fsm.transition('walk', true);
// 自动评估转换条件
fsm.evaluateTransitions();
```
#### 生命周期
```typescript
// 更新状态机(调用当前状态的 onUpdate
fsm.update(deltaTime);
// 重置状态机
fsm.reset(); // 重置到当前状态
fsm.reset('idle'); // 重置到指定状态
```
#### 事件监听
```typescript
// 监听进入特定状态
const unsubscribe = fsm.onEnter('walk', (from) => {
console.log(`${from} 进入 walk`);
});
// 监听退出特定状态
fsm.onExit('walk', (to) => {
console.log(`从 walk 退出到 ${to}`);
});
// 监听任意状态变化
fsm.onChange((event) => {
console.log(`${event.from} -> ${event.to} at ${event.timestamp}`);
});
// 取消订阅
unsubscribe();
```
#### 调试
```typescript
// 获取状态历史
const history = fsm.getHistory();
// [{ from: 'idle', to: 'walk', timestamp: 1234567890 }, ...]
// 清除历史
fsm.clearHistory();
// 获取调试信息
const info = fsm.getDebugInfo();
// { current, previous, duration, stateCount, transitionCount, historySize }
```
## 实际示例
### 角色状态机
```typescript
import { createStateMachine } from '@esengine/fsm';
type CharacterState = 'idle' | 'walk' | 'run' | 'jump' | 'fall' | 'attack';
interface CharacterContext {
velocity: { x: number; y: number };
isGrounded: boolean;
isAttacking: boolean;
speed: number;
}
const characterFSM = createStateMachine<CharacterState, CharacterContext>('idle', {
context: {
velocity: { x: 0, y: 0 },
isGrounded: true,
isAttacking: false,
speed: 0
}
});
// 定义状态
characterFSM.defineState('idle', {
onEnter: (ctx) => {
ctx.speed = 0;
},
onUpdate: (ctx, dt) => {
// 播放待机动画
}
});
characterFSM.defineState('walk', {
onEnter: (ctx) => {
ctx.speed = 100;
}
});
characterFSM.defineState('run', {
onEnter: (ctx) => {
ctx.speed = 200;
}
});
characterFSM.defineState('jump', {
onEnter: (ctx) => {
ctx.velocity.y = -300;
ctx.isGrounded = false;
}
});
// 定义转换
characterFSM.defineTransition('idle', 'walk', (ctx) => Math.abs(ctx.velocity.x) > 0);
characterFSM.defineTransition('walk', 'idle', (ctx) => ctx.velocity.x === 0);
characterFSM.defineTransition('walk', 'run', (ctx) => Math.abs(ctx.velocity.x) > 150);
characterFSM.defineTransition('run', 'walk', (ctx) => Math.abs(ctx.velocity.x) <= 150);
// 跳跃有最高优先级
characterFSM.defineTransition('idle', 'jump', (ctx) => !ctx.isGrounded, 10);
characterFSM.defineTransition('walk', 'jump', (ctx) => !ctx.isGrounded, 10);
characterFSM.defineTransition('run', 'jump', (ctx) => !ctx.isGrounded, 10);
characterFSM.defineTransition('jump', 'fall', (ctx) => ctx.velocity.y > 0);
characterFSM.defineTransition('fall', 'idle', (ctx) => ctx.isGrounded);
// 游戏循环中使用
function gameUpdate(dt: number) {
// 更新上下文
characterFSM.context.velocity.x = getInputVelocity();
characterFSM.context.isGrounded = checkGrounded();
// 评估状态转换
characterFSM.evaluateTransitions();
// 更新当前状态
characterFSM.update(dt);
}
```
### 与 ECS 集成
```typescript
import { Component, EntitySystem, Matcher } from '@esengine/ecs-framework';
import { createStateMachine, type IStateMachine } from '@esengine/fsm';
// 状态机组件
class FSMComponent extends Component {
fsm: IStateMachine<string>;
constructor(initialState: string) {
super();
this.fsm = createStateMachine(initialState);
}
}
// 状态机系统
class FSMSystem extends EntitySystem {
constructor() {
super(Matcher.all(FSMComponent));
}
protected processEntity(entity: Entity, dt: number): void {
const fsmComp = entity.getComponent(FSMComponent);
fsmComp.fsm.evaluateTransitions();
fsmComp.fsm.update(dt);
}
}
```
## 蓝图节点
FSM 模块提供了可视化脚本支持的蓝图节点:
- `GetCurrentState` - 获取当前状态
- `TransitionTo` - 转换到指定状态
- `CanTransition` - 检查是否可以转换
- `IsInState` - 检查是否在指定状态
- `WasInState` - 检查是否曾在指定状态
- `GetStateDuration` - 获取状态持续时间
- `EvaluateTransitions` - 评估转换条件
- `ResetStateMachine` - 重置状态机

54
docs/modules/index.md Normal file
View File

@@ -0,0 +1,54 @@
# 功能模块
ESEngine 提供了丰富的功能模块,可以按需引入到你的项目中。
## 模块列表
### AI 模块
| 模块 | 包名 | 描述 |
|------|------|------|
| [行为树](/modules/behavior-tree/) | `@esengine/behavior-tree` | AI 行为树系统,支持可视化编辑 |
| [状态机](/modules/fsm/) | `@esengine/fsm` | 有限状态机,用于角色/AI 状态管理 |
### 游戏逻辑
| 模块 | 包名 | 描述 |
|------|------|------|
| [定时器](/modules/timer/) | `@esengine/timer` | 定时器和冷却系统 |
| [空间索引](/modules/spatial/) | `@esengine/spatial` | 空间查询、AOI 兴趣区域管理 |
| [寻路系统](/modules/pathfinding/) | `@esengine/pathfinding` | A* 寻路、NavMesh 导航网格 |
### 工具模块
| 模块 | 包名 | 描述 |
|------|------|------|
| [可视化脚本](/modules/blueprint/) | `@esengine/blueprint` | 蓝图可视化脚本系统 |
| [程序化生成](/modules/procgen/) | `@esengine/procgen` | 噪声函数、随机工具 |
### 网络模块
| 模块 | 包名 | 描述 |
|------|------|------|
| [网络同步](/modules/network/) | `@esengine/network` | 多人游戏网络同步 |
## 安装
所有模块都可以独立安装:
```bash
# 安装单个模块
npm install @esengine/behavior-tree
# 或使用 CLI 添加到现有项目
npx @esengine/cli add behavior-tree
```
## 平台兼容性
所有功能模块都是纯 TypeScript 实现,兼容:
- Cocos Creator 3.x
- Laya 3.x
- Node.js
- 浏览器

View File

@@ -0,0 +1,727 @@
# 网络同步系统 (Network)
`@esengine/network` 提供基于 TSRPC 的客户端-服务器网络同步解决方案,用于多人游戏的实体同步、输入处理和状态插值。
## 概述
网络模块由三个包组成:
| 包名 | 描述 |
|------|------|
| `@esengine/network` | 客户端 ECS 插件 |
| `@esengine/network-protocols` | 共享协议定义 |
| `@esengine/network-server` | 服务器端实现 |
## 安装
```bash
# 客户端
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
```
## 快速开始
### 客户端
```typescript
import { Core, Scene } from '@esengine/ecs-framework';
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);
// 安装网络插件
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.isLocalPlayer = spawn.ownerId === networkPlugin.networkService.localClientId;
entity.addComponent(new NetworkTransform());
return entity;
});
// 连接服务器
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';
const server = new GameServer({
port: 3000,
roomConfig: {
maxPlayers: 16,
tickRate: 20
}
});
await server.start();
console.log('Server started on ws://localhost:3000');
```
## 核心概念
### 架构
```
客户端 服务器
┌────────────────┐ ┌────────────────┐
│ NetworkPlugin │◄──── WS ────► │ GameServer │
│ ├─ Service │ │ ├─ Room │
│ ├─ SyncSystem │ │ └─ Players │
│ ├─ SpawnSystem │ └────────────────┘
│ └─ InputSystem │
└────────────────┘
```
### 组件
#### NetworkIdentity
网络标识组件,每个网络同步的实体必须拥有:
```typescript
class NetworkIdentity extends Component {
netId: number; // 网络唯一 ID
ownerId: number; // 所有者客户端 ID
bIsLocalPlayer: boolean; // 是否为本地玩家
bHasAuthority: boolean; // 是否有权限控制
}
```
#### 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
## 服务令牌
用于依赖注入:
```typescript
import {
NetworkServiceToken,
NetworkSyncSystemToken,
NetworkSpawnSystemToken,
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.isLocalPlayer = spawn.ownerId === networkPlugin.networkService.localClientId;
entity.addComponent(new NetworkTransform());
// 如果是本地玩家,添加输入标记
if (identity.isLocalPlayer) {
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.isLocalPlayer) 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('已断开');
// 处理重连逻辑
}
});
```

View File

@@ -0,0 +1,502 @@
# 寻路系统 (Pathfinding)
`@esengine/pathfinding` 提供了完整的 2D 寻路解决方案,包括 A* 算法、网格地图、导航网格和路径平滑。
## 安装
```bash
npm install @esengine/pathfinding
```
## 快速开始
### 网格地图寻路
```typescript
import { createGridMap, createAStarPathfinder } from '@esengine/pathfinding';
// 创建 20x20 的网格地图
const grid = createGridMap(20, 20);
// 设置障碍物
grid.setWalkable(5, 5, false);
grid.setWalkable(5, 6, false);
grid.setWalkable(5, 7, false);
// 创建寻路器
const pathfinder = createAStarPathfinder(grid);
// 查找路径
const result = pathfinder.findPath(0, 0, 15, 15);
if (result.found) {
console.log('找到路径!');
console.log('路径点:', result.path);
console.log('总代价:', result.cost);
console.log('搜索节点数:', result.nodesSearched);
}
```
### 导航网格寻路
```typescript
import { createNavMesh } from '@esengine/pathfinding';
// 创建导航网格
const navmesh = createNavMesh();
// 添加多边形区域
navmesh.addPolygon([
{ x: 0, y: 0 }, { x: 10, y: 0 },
{ x: 10, y: 10 }, { x: 0, y: 10 }
]);
navmesh.addPolygon([
{ x: 10, y: 0 }, { x: 20, y: 0 },
{ x: 20, y: 10 }, { x: 10, y: 10 }
]);
// 自动建立连接
navmesh.build();
// 寻路
const result = navmesh.findPath(1, 1, 18, 8);
```
## 核心概念
### IPoint - 坐标点
```typescript
interface IPoint {
readonly x: number;
readonly y: number;
}
```
### IPathResult - 寻路结果
```typescript
interface IPathResult {
readonly found: boolean; // 是否找到路径
readonly path: readonly IPoint[]; // 路径点列表
readonly cost: number; // 路径总代价
readonly nodesSearched: number; // 搜索的节点数
}
```
### IPathfindingOptions - 寻路配置
```typescript
interface IPathfindingOptions {
maxNodes?: number; // 最大搜索节点数(默认 10000
heuristicWeight?: number; // 启发式权重(>1 更快但可能非最优)
allowDiagonal?: boolean; // 是否允许对角移动(默认 true
avoidCorners?: boolean; // 是否避免穿角(默认 true
}
```
## 启发式函数
模块提供了四种启发式函数:
| 函数 | 适用场景 | 说明 |
|------|----------|------|
| `manhattanDistance` | 4方向移动 | 曼哈顿距离,只考虑水平/垂直 |
| `euclideanDistance` | 任意方向 | 欧几里得距离,直线距离 |
| `chebyshevDistance` | 8方向移动 | 切比雪夫距离,对角线代价为 1 |
| `octileDistance` | 8方向移动 | 八角距离,对角线代价为 √2默认 |
```typescript
import { manhattanDistance, octileDistance } from '@esengine/pathfinding';
// 自定义启发式
const grid = createGridMap(20, 20, {
heuristic: manhattanDistance // 使用曼哈顿距离
});
```
## 网格地图 API
### createGridMap
```typescript
function createGridMap(
width: number,
height: number,
options?: IGridMapOptions
): GridMap
```
**配置选项:**
| 属性 | 类型 | 默认值 | 描述 |
|------|------|--------|------|
| `allowDiagonal` | `boolean` | `true` | 允许对角移动 |
| `diagonalCost` | `number` | `√2` | 对角移动代价 |
| `avoidCorners` | `boolean` | `true` | 避免穿角 |
| `heuristic` | `HeuristicFunction` | `octileDistance` | 启发式函数 |
### 地图操作
```typescript
// 检查/设置可通行性
grid.isWalkable(x, y);
grid.setWalkable(x, y, false);
// 设置移动代价(如沼泽、沙地)
grid.setCost(x, y, 2); // 代价为 2默认 1
// 设置矩形区域
grid.setRectWalkable(0, 0, 5, 5, false);
// 从数组加载0=可通行非0=障碍)
grid.loadFromArray([
[0, 0, 0, 1, 0],
[0, 1, 0, 1, 0],
[0, 1, 0, 0, 0]
]);
// 从字符串加载(.=可通行,#=障碍)
grid.loadFromString(`
.....
.#.#.
.#...
`);
// 导出为字符串
console.log(grid.toString());
// 重置所有节点为可通行
grid.reset();
```
### 方向常量
```typescript
import { DIRECTIONS_4, DIRECTIONS_8 } from '@esengine/pathfinding';
// 4方向上下左右
DIRECTIONS_4 // [{ dx: 0, dy: -1 }, { dx: 1, dy: 0 }, ...]
// 8方向含对角线
DIRECTIONS_8 // [{ dx: 0, dy: -1 }, { dx: 1, dy: -1 }, ...]
```
## A* 寻路器 API
### createAStarPathfinder
```typescript
function createAStarPathfinder(map: IPathfindingMap): AStarPathfinder
```
### findPath
```typescript
const result = pathfinder.findPath(
startX, startY,
endX, endY,
{
maxNodes: 5000, // 限制搜索节点数
heuristicWeight: 1.5 // 加速但可能非最优
}
);
```
### 重用寻路器
```typescript
// 寻路器可重用,内部会自动清理状态
pathfinder.findPath(0, 0, 10, 10);
pathfinder.findPath(5, 5, 15, 15);
// 手动清理(可选)
pathfinder.clear();
```
## 导航网格 API
### createNavMesh
```typescript
function createNavMesh(): NavMesh
```
### 构建导航网格
```typescript
const navmesh = createNavMesh();
// 添加凸多边形
const id1 = navmesh.addPolygon([
{ x: 0, y: 0 }, { x: 10, y: 0 },
{ x: 10, y: 10 }, { x: 0, y: 10 }
]);
const id2 = navmesh.addPolygon([
{ x: 10, y: 0 }, { x: 20, y: 0 },
{ x: 20, y: 10 }, { x: 10, y: 10 }
]);
// 方式1自动检测共享边并建立连接
navmesh.build();
// 方式2手动设置连接
navmesh.setConnection(id1, id2, {
left: { x: 10, y: 0 },
right: { x: 10, y: 10 }
});
```
### 查询和寻路
```typescript
// 查找包含点的多边形
const polygon = navmesh.findPolygonAt(5, 5);
// 检查位置是否可通行
navmesh.isWalkable(5, 5);
// 寻路(内部使用漏斗算法优化路径)
const result = navmesh.findPath(1, 1, 18, 8);
```
## 路径平滑 API
### 视线简化
移除不必要的中间点:
```typescript
import { createLineOfSightSmoother } from '@esengine/pathfinding';
const smoother = createLineOfSightSmoother();
const smoothedPath = smoother.smooth(result.path, grid);
// 原路径: [(0,0), (1,1), (2,2), (3,3), (4,4)]
// 简化后: [(0,0), (4,4)]
```
### 曲线平滑
使用 Catmull-Rom 样条曲线:
```typescript
import { createCatmullRomSmoother } from '@esengine/pathfinding';
const smoother = createCatmullRomSmoother(
5, // segments - 每段插值点数
0.5 // tension - 张力 (0-1)
);
const curvedPath = smoother.smooth(result.path, grid);
```
### 组合平滑
先简化再曲线平滑:
```typescript
import { createCombinedSmoother } from '@esengine/pathfinding';
const smoother = createCombinedSmoother(5, 0.5);
const finalPath = smoother.smooth(result.path, grid);
```
### 视线检测函数
```typescript
import { bresenhamLineOfSight, raycastLineOfSight } from '@esengine/pathfinding';
// Bresenham 算法(快速,网格对齐)
const hasLOS = bresenhamLineOfSight(x1, y1, x2, y2, grid);
// 射线投射(精确,支持浮点坐标)
const hasLOS = raycastLineOfSight(x1, y1, x2, y2, grid, 0.5);
```
## 实际示例
### 游戏角色移动
```typescript
class MovementSystem {
private grid: GridMap;
private pathfinder: AStarPathfinder;
private smoother: CombinedSmoother;
constructor(width: number, height: number) {
this.grid = createGridMap(width, height);
this.pathfinder = createAStarPathfinder(this.grid);
this.smoother = createCombinedSmoother();
}
findPath(from: IPoint, to: IPoint): IPoint[] | null {
const result = this.pathfinder.findPath(
from.x, from.y,
to.x, to.y
);
if (!result.found) {
return null;
}
// 平滑路径
return this.smoother.smooth(result.path, this.grid);
}
setObstacle(x: number, y: number): void {
this.grid.setWalkable(x, y, false);
}
setTerrain(x: number, y: number, cost: number): void {
this.grid.setCost(x, y, cost);
}
}
```
### 动态障碍物
```typescript
class DynamicPathfinding {
private grid: GridMap;
private pathfinder: AStarPathfinder;
private dynamicObstacles: Set<string> = new Set();
addDynamicObstacle(x: number, y: number): void {
const key = `${x},${y}`;
if (!this.dynamicObstacles.has(key)) {
this.dynamicObstacles.add(key);
this.grid.setWalkable(x, y, false);
}
}
removeDynamicObstacle(x: number, y: number): void {
const key = `${x},${y}`;
if (this.dynamicObstacles.has(key)) {
this.dynamicObstacles.delete(key);
this.grid.setWalkable(x, y, true);
}
}
findPath(from: IPoint, to: IPoint): IPathResult {
return this.pathfinder.findPath(from.x, from.y, to.x, to.y);
}
}
```
### 不同地形代价
```typescript
// 设置不同地形的移动代价
const grid = createGridMap(50, 50);
// 普通地面 - 代价 1默认
// 沙地 - 代价 2
for (let y = 10; y < 20; y++) {
for (let x = 0; x < 50; x++) {
grid.setCost(x, y, 2);
}
}
// 沼泽 - 代价 4
for (let y = 30; y < 35; y++) {
for (let x = 20; x < 30; x++) {
grid.setCost(x, y, 4);
}
}
// 寻路时会自动考虑地形代价
const result = pathfinder.findPath(0, 0, 49, 49);
```
### 分层寻路
对于大型地图,使用层级化寻路:
```typescript
class HierarchicalPathfinding {
private coarseGrid: GridMap; // 粗粒度网格
private fineGrid: GridMap; // 细粒度网格
private coarsePathfinder: AStarPathfinder;
private finePathfinder: AStarPathfinder;
private cellSize = 10;
findPath(from: IPoint, to: IPoint): IPoint[] {
// 1. 在粗粒度网格上寻路
const coarseFrom = this.toCoarse(from);
const coarseTo = this.toCoarse(to);
const coarseResult = this.coarsePathfinder.findPath(
coarseFrom.x, coarseFrom.y,
coarseTo.x, coarseTo.y
);
if (!coarseResult.found) {
return [];
}
// 2. 在每个粗粒度单元内进行细粒度寻路
const finePath: IPoint[] = [];
// ... 详细实现略
return finePath;
}
private toCoarse(p: IPoint): IPoint {
return {
x: Math.floor(p.x / this.cellSize),
y: Math.floor(p.y / this.cellSize)
};
}
}
```
## 蓝图节点
Pathfinding 模块提供了可视化脚本支持的蓝图节点:
- `FindPath` - 查找路径
- `FindPathSmooth` - 查找并平滑路径
- `IsWalkable` - 检查位置是否可通行
- `GetPathLength` - 获取路径点数
- `GetPathDistance` - 获取路径总距离
- `GetPathPoint` - 获取路径上的指定点
- `MoveAlongPath` - 沿路径移动
- `HasLineOfSight` - 检查视线
## 性能优化
1. **限制搜索范围**
```typescript
pathfinder.findPath(x1, y1, x2, y2, { maxNodes: 1000 });
```
2. **使用启发式权重**
```typescript
// 权重 > 1 会更快但可能不是最优路径
pathfinder.findPath(x1, y1, x2, y2, { heuristicWeight: 1.5 });
```
3. **复用寻路器实例**
```typescript
// 创建一次,多次使用
const pathfinder = createAStarPathfinder(grid);
```
4. **使用导航网格**
- 对于复杂地形NavMesh 比网格寻路更高效
- 多边形数量远少于网格单元格数量
5. **选择合适的启发式**
- 4方向移动用 `manhattanDistance`
- 8方向移动用 `octileDistance`(默认)
## 网格 vs 导航网格
| 特性 | GridMap | NavMesh |
|------|---------|---------|
| 适用场景 | 规则瓦片地图 | 复杂多边形地形 |
| 内存占用 | 较高 (width × height) | 较低 (多边形数) |
| 精度 | 网格对齐 | 连续坐标 |
| 动态修改 | 容易 | 需要重建 |
| 设置复杂度 | 简单 | 较复杂 |

View File

@@ -0,0 +1,557 @@
# 程序化生成 (Procgen)
`@esengine/procgen` 提供了程序化内容生成的核心工具,包括噪声函数、种子随机数和各种随机工具。
## 安装
```bash
npm install @esengine/procgen
```
## 快速开始
### 噪声生成
```typescript
import { createPerlinNoise, createFBM } from '@esengine/procgen';
// 创建 Perlin 噪声
const perlin = createPerlinNoise(12345); // 种子
// 采样 2D 噪声
const value = perlin.noise2D(x * 0.1, y * 0.1);
console.log(value); // [-1, 1]
// 使用 FBM 获得更自然的效果
const fbm = createFBM(perlin, {
octaves: 6,
persistence: 0.5
});
const height = fbm.noise2D(x * 0.01, y * 0.01);
```
### 种子随机数
```typescript
import { createSeededRandom } from '@esengine/procgen';
// 创建确定性随机数生成器
const rng = createSeededRandom(42);
// 相同种子总是产生相同序列
console.log(rng.next()); // 0.xxx
console.log(rng.nextInt(1, 100)); // 1-100
console.log(rng.nextBool(0.3)); // 30% true
```
### 加权随机
```typescript
import { createWeightedRandom, createSeededRandom } from '@esengine/procgen';
const rng = createSeededRandom(42);
// 创建加权选择器
const loot = createWeightedRandom([
{ value: 'common', weight: 60 },
{ value: 'uncommon', weight: 25 },
{ value: 'rare', weight: 10 },
{ value: 'legendary', weight: 5 }
]);
// 随机选择
const drop = loot.pick(rng);
console.log(drop); // 大概率是 'common'
```
## 噪声函数
### Perlin 噪声
经典的梯度噪声,输出范围 [-1, 1]
```typescript
import { createPerlinNoise } from '@esengine/procgen';
const perlin = createPerlinNoise(seed);
// 2D 噪声
const value2D = perlin.noise2D(x, y);
// 3D 噪声
const value3D = perlin.noise3D(x, y, z);
```
### Simplex 噪声
比 Perlin 更快、更少方向性偏差:
```typescript
import { createSimplexNoise } from '@esengine/procgen';
const simplex = createSimplexNoise(seed);
const value = simplex.noise2D(x, y);
```
### Worley 噪声
基于细胞的噪声,适合生成石头、细胞等纹理:
```typescript
import { createWorleyNoise } from '@esengine/procgen';
const worley = createWorleyNoise(seed);
// 返回到最近点的距离
const distance = worley.noise2D(x, y);
```
### FBM (分形布朗运动)
叠加多层噪声创建更丰富的细节:
```typescript
import { createPerlinNoise, createFBM } from '@esengine/procgen';
const baseNoise = createPerlinNoise(seed);
const fbm = createFBM(baseNoise, {
octaves: 6, // 层数(越多细节越丰富)
lacunarity: 2.0, // 频率倍增因子
persistence: 0.5, // 振幅衰减因子
frequency: 1.0, // 初始频率
amplitude: 1.0 // 初始振幅
});
// 标准 FBM
const value = fbm.noise2D(x, y);
// Ridged FBM脊状适合山脉
const ridged = fbm.ridged2D(x, y);
// Turbulence湍流
const turb = fbm.turbulence2D(x, y);
// Billowed膨胀适合云朵
const cloud = fbm.billowed2D(x, y);
```
## 种子随机数 API
### SeededRandom
基于 xorshift128+ 算法的确定性伪随机数生成器:
```typescript
import { createSeededRandom } from '@esengine/procgen';
const rng = createSeededRandom(42);
```
### 基础方法
```typescript
// [0, 1) 浮点数
rng.next();
// [min, max] 整数
rng.nextInt(1, 10);
// [min, max) 浮点数
rng.nextFloat(0, 100);
// 布尔值(可指定概率)
rng.nextBool(); // 50%
rng.nextBool(0.3); // 30%
// 重置到初始状态
rng.reset();
```
### 分布方法
```typescript
// 正态分布(高斯分布)
rng.nextGaussian(); // 均值 0, 标准差 1
rng.nextGaussian(100, 15); // 均值 100, 标准差 15
// 指数分布
rng.nextExponential(); // λ = 1
rng.nextExponential(0.5); // λ = 0.5
```
### 几何方法
```typescript
// 圆内均匀分布的点
const point = rng.nextPointInCircle(50); // { x, y }
// 圆周上的点
const edge = rng.nextPointOnCircle(50); // { x, y }
// 球内均匀分布的点
const point3D = rng.nextPointInSphere(50); // { x, y, z }
// 随机方向向量
const dir = rng.nextDirection2D(); // { x, y },长度为 1
```
## 加权随机 API
### WeightedRandom
预计算累积权重,高效随机选择:
```typescript
import { createWeightedRandom } from '@esengine/procgen';
const selector = createWeightedRandom([
{ value: 'apple', weight: 5 },
{ value: 'banana', weight: 3 },
{ value: 'cherry', weight: 2 }
]);
// 使用种子随机数
const result = selector.pick(rng);
// 使用 Math.random
const result2 = selector.pickRandom();
// 获取概率
console.log(selector.getProbability(0)); // 0.5 (5/10)
console.log(selector.size); // 3
console.log(selector.totalWeight); // 10
```
### 便捷函数
```typescript
import { weightedPick, weightedPickFromMap } from '@esengine/procgen';
// 从数组选择
const item = weightedPick([
{ value: 'a', weight: 1 },
{ value: 'b', weight: 2 }
], rng);
// 从对象选择
const item2 = weightedPickFromMap({
'common': 60,
'rare': 30,
'epic': 10
}, rng);
```
## 洗牌和采样 API
### shuffle / shuffleCopy
Fisher-Yates 洗牌算法:
```typescript
import { shuffle, shuffleCopy } from '@esengine/procgen';
const arr = [1, 2, 3, 4, 5];
// 原地洗牌
shuffle(arr, rng);
// 创建洗牌副本(不修改原数组)
const shuffled = shuffleCopy(arr, rng);
```
### pickOne
随机选择一个元素:
```typescript
import { pickOne } from '@esengine/procgen';
const items = ['a', 'b', 'c', 'd'];
const item = pickOne(items, rng);
```
### sample / sampleWithReplacement
采样:
```typescript
import { sample, sampleWithReplacement } from '@esengine/procgen';
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// 采样 3 个不重复元素
const unique = sample(arr, 3, rng);
// 采样 5 个(可重复)
const withRep = sampleWithReplacement(arr, 5, rng);
```
### randomIntegers
生成范围内的随机整数数组:
```typescript
import { randomIntegers } from '@esengine/procgen';
// 从 1-100 中随机选 5 个不重复的数
const nums = randomIntegers(1, 100, 5, rng);
```
### weightedSample
按权重采样(不重复):
```typescript
import { weightedSample } from '@esengine/procgen';
const items = ['A', 'B', 'C', 'D', 'E'];
const weights = [10, 8, 6, 4, 2];
// 按权重选 3 个
const selected = weightedSample(items, weights, 3, rng);
```
## 实际示例
### 程序化地形生成
```typescript
import { createPerlinNoise, createFBM } from '@esengine/procgen';
class TerrainGenerator {
private fbm: FBM;
private moistureFbm: FBM;
constructor(seed: number) {
const heightNoise = createPerlinNoise(seed);
const moistureNoise = createPerlinNoise(seed + 1000);
this.fbm = createFBM(heightNoise, {
octaves: 8,
persistence: 0.5,
frequency: 0.01
});
this.moistureFbm = createFBM(moistureNoise, {
octaves: 4,
persistence: 0.6,
frequency: 0.02
});
}
getHeight(x: number, y: number): number {
// 基础高度
let height = this.fbm.noise2D(x, y);
// 添加山脉
height += this.fbm.ridged2D(x * 0.5, y * 0.5) * 0.3;
return (height + 1) * 0.5; // 归一化到 [0, 1]
}
getBiome(x: number, y: number): string {
const height = this.getHeight(x, y);
const moisture = (this.moistureFbm.noise2D(x, y) + 1) * 0.5;
if (height < 0.3) return 'water';
if (height < 0.4) return 'beach';
if (height > 0.8) return 'mountain';
if (moisture < 0.3) return 'desert';
if (moisture > 0.7) return 'forest';
return 'grassland';
}
}
```
### 战利品系统
```typescript
import { createSeededRandom, createWeightedRandom, sample } from '@esengine/procgen';
interface LootItem {
id: string;
rarity: string;
}
class LootSystem {
private rng: SeededRandom;
private raritySelector: WeightedRandom<string>;
private lootTables: Map<string, LootItem[]> = new Map();
constructor(seed: number) {
this.rng = createSeededRandom(seed);
this.raritySelector = createWeightedRandom([
{ value: 'common', weight: 60 },
{ value: 'uncommon', weight: 25 },
{ value: 'rare', weight: 10 },
{ value: 'legendary', weight: 5 }
]);
// 初始化战利品表
this.lootTables.set('common', [/* ... */]);
this.lootTables.set('rare', [/* ... */]);
// ...
}
generateLoot(count: number): LootItem[] {
const loot: LootItem[] = [];
for (let i = 0; i < count; i++) {
const rarity = this.raritySelector.pick(this.rng);
const table = this.lootTables.get(rarity)!;
const item = pickOne(table, this.rng);
loot.push(item);
}
return loot;
}
// 保证可重现
setSeed(seed: number): void {
this.rng = createSeededRandom(seed);
}
}
```
### 程序化敌人放置
```typescript
import { createSeededRandom } from '@esengine/procgen';
class EnemySpawner {
private rng: SeededRandom;
constructor(seed: number) {
this.rng = createSeededRandom(seed);
}
spawnEnemiesInArea(
centerX: number,
centerY: number,
radius: number,
count: number
): Array<{ x: number; y: number; type: string }> {
const enemies: Array<{ x: number; y: number; type: string }> = [];
for (let i = 0; i < count; i++) {
// 在圆内生成位置
const pos = this.rng.nextPointInCircle(radius);
// 随机选择敌人类型
const type = this.rng.nextBool(0.2) ? 'elite' : 'normal';
enemies.push({
x: centerX + pos.x,
y: centerY + pos.y,
type
});
}
return enemies;
}
}
```
### 程序化关卡布局
```typescript
import { createSeededRandom, shuffle } from '@esengine/procgen';
interface Room {
x: number;
y: number;
width: number;
height: number;
type: 'start' | 'combat' | 'treasure' | 'boss';
}
class DungeonGenerator {
private rng: SeededRandom;
constructor(seed: number) {
this.rng = createSeededRandom(seed);
}
generate(roomCount: number): Room[] {
const rooms: Room[] = [];
// 生成房间
for (let i = 0; i < roomCount; i++) {
rooms.push({
x: this.rng.nextInt(0, 100),
y: this.rng.nextInt(0, 100),
width: this.rng.nextInt(5, 15),
height: this.rng.nextInt(5, 15),
type: 'combat'
});
}
// 随机分配特殊房间
shuffle(rooms, this.rng);
rooms[0].type = 'start';
rooms[1].type = 'treasure';
rooms[rooms.length - 1].type = 'boss';
return rooms;
}
}
```
## 蓝图节点
Procgen 模块提供了可视化脚本支持的蓝图节点:
### 噪声节点
- `SampleNoise2D` - 采样 2D 噪声
- `SampleFBM` - 采样 FBM 噪声
### 随机节点
- `SeededRandom` - 生成随机浮点数
- `SeededRandomInt` - 生成随机整数
- `WeightedPick` - 加权随机选择
- `ShuffleArray` - 洗牌数组
- `PickRandom` - 随机选择元素
- `SampleArray` - 采样数组
- `RandomPointInCircle` - 圆内随机点
## 最佳实践
1. **使用种子保证可重现性**
```typescript
// 保存种子以便重现相同结果
const seed = Date.now();
const rng = createSeededRandom(seed);
saveSeed(seed);
```
2. **预计算加权选择器**
```typescript
// 好:创建一次,多次使用
const selector = createWeightedRandom(items);
for (let i = 0; i < 1000; i++) {
selector.pick(rng);
}
// 不好:每次都创建
for (let i = 0; i < 1000; i++) {
weightedPick(items, rng);
}
```
3. **选择合适的噪声函数**
- Perlin平滑过渡的地形、云彩
- Simplex性能要求高的场景
- Worley细胞、石头纹理
- FBM需要多层细节的自然效果
4. **调整 FBM 参数**
- `octaves`:越多细节越丰富,但性能开销越大
- `persistence`0.5 是常用值,越大高频细节越明显
- `lacunarity`:通常为 2控制频率增长速度

View File

@@ -0,0 +1,600 @@
# 空间索引系统 (Spatial)
`@esengine/spatial` 提供了高效的空间查询和索引功能,包括范围查询、最近邻查询、射线检测和 AOI兴趣区域管理。
## 安装
```bash
npm install @esengine/spatial
```
## 快速开始
### 空间索引
```typescript
import { createGridSpatialIndex } from '@esengine/spatial';
// 创建空间索引(网格单元格大小为 100
const spatialIndex = createGridSpatialIndex<Entity>(100);
// 插入对象
spatialIndex.insert(player, { x: 100, y: 200 });
spatialIndex.insert(enemy1, { x: 150, y: 250 });
spatialIndex.insert(enemy2, { x: 500, y: 600 });
// 查找半径内的对象
const nearby = spatialIndex.findInRadius({ x: 100, y: 200 }, 100);
console.log(nearby); // [player, enemy1]
// 查找最近的对象
const nearest = spatialIndex.findNearest({ x: 100, y: 200 });
console.log(nearest); // enemy1
// 更新位置
spatialIndex.update(player, { x: 120, y: 220 });
```
### AOI 兴趣区域
```typescript
import { createGridAOI } from '@esengine/spatial';
// 创建 AOI 管理器
const aoi = createGridAOI<Entity>(100);
// 添加观察者(玩家)
aoi.addObserver(player, { x: 100, y: 100 }, { viewRange: 200 });
aoi.addObserver(npc, { x: 150, y: 150 }, { viewRange: 150 });
// 监听进入/离开事件
aoi.addListener((event) => {
if (event.type === 'enter') {
console.log(`${event.observer} 看到了 ${event.target}`);
} else if (event.type === 'exit') {
console.log(`${event.target} 离开了 ${event.observer} 的视野`);
}
});
// 更新位置(会自动触发进入/离开事件)
aoi.updatePosition(player, { x: 200, y: 200 });
// 获取视野内的实体
const visible = aoi.getEntitiesInView(player);
```
## 核心概念
### 空间索引 vs AOI
| 特性 | 空间索引 (SpatialIndex) | AOI (Area of Interest) |
|------|------------------------|------------------------|
| 用途 | 通用空间查询 | 实体可见性追踪 |
| 事件 | 无事件通知 | 进入/离开事件 |
| 方向 | 单向查询 | 双向追踪(谁看到谁) |
| 场景 | 碰撞检测、范围攻击 | MMO 同步、NPC AI 感知 |
### IBounds 边界框
```typescript
interface IBounds {
readonly minX: number;
readonly minY: number;
readonly maxX: number;
readonly maxY: number;
}
```
### IRaycastHit 射线检测结果
```typescript
interface IRaycastHit<T> {
readonly target: T; // 命中的对象
readonly point: IVector2; // 命中点坐标
readonly normal: IVector2; // 命中点法线
readonly distance: number; // 距离射线起点的距离
}
```
## 空间索引 API
### createGridSpatialIndex
```typescript
function createGridSpatialIndex<T>(cellSize?: number): GridSpatialIndex<T>
```
创建基于均匀网格的空间索引。
**参数:**
- `cellSize` - 网格单元格大小(默认 100
**选择合适的 cellSize**
- 太小:内存占用高,查询效率降低
- 太大:单元格内对象过多,遍历耗时
- 建议:设置为对象平均分布间距的 1-2 倍
### 管理方法
#### insert
插入对象到索引:
```typescript
spatialIndex.insert(enemy, { x: 100, y: 200 });
```
#### remove
移除对象:
```typescript
spatialIndex.remove(enemy);
```
#### update
更新对象位置:
```typescript
spatialIndex.update(enemy, { x: 150, y: 250 });
```
#### clear
清空索引:
```typescript
spatialIndex.clear();
```
### 查询方法
#### findInRadius
查找圆形范围内的所有对象:
```typescript
// 查找中心点 (100, 200) 半径 50 内的所有敌人
const enemies = spatialIndex.findInRadius(
{ x: 100, y: 200 },
50,
(entity) => entity.type === 'enemy' // 可选过滤器
);
```
#### findInRect
查找矩形区域内的所有对象:
```typescript
import { createBounds } from '@esengine/spatial';
const bounds = createBounds(0, 0, 200, 200);
const entities = spatialIndex.findInRect(bounds);
```
#### findNearest
查找最近的对象:
```typescript
// 查找最近的敌人(最大搜索距离 500
const nearest = spatialIndex.findNearest(
playerPosition,
500, // maxDistance
(entity) => entity.type === 'enemy'
);
if (nearest) {
attackTarget(nearest);
}
```
#### findKNearest
查找最近的 K 个对象:
```typescript
// 查找最近的 5 个敌人
const nearestEnemies = spatialIndex.findKNearest(
playerPosition,
5, // k
500, // maxDistance
(entity) => entity.type === 'enemy'
);
```
#### raycast
射线检测(返回所有命中):
```typescript
const hits = spatialIndex.raycast(
origin, // 射线起点
direction, // 射线方向(应归一化)
maxDistance, // 最大检测距离
filter // 可选过滤器
);
// hits 按距离排序
for (const hit of hits) {
console.log(`命中 ${hit.target} at ${hit.point}, 距离 ${hit.distance}`);
}
```
#### raycastFirst
射线检测(仅返回第一个命中):
```typescript
const hit = spatialIndex.raycastFirst(origin, direction, 1000);
if (hit) {
dealDamage(hit.target, calculateDamage(hit.distance));
}
```
### 属性
```typescript
// 获取索引中的对象数量
console.log(spatialIndex.count);
// 获取所有对象
const all = spatialIndex.getAll();
```
## AOI 兴趣区域 API
### createGridAOI
```typescript
function createGridAOI<T>(cellSize?: number): GridAOI<T>
```
创建基于网格的 AOI 管理器。
**参数:**
- `cellSize` - 网格单元格大小(建议为平均视野范围的 1-2 倍)
### 观察者管理
#### addObserver
添加观察者:
```typescript
aoi.addObserver(player, position, {
viewRange: 200, // 视野范围
observable: true // 是否可被其他观察者看到(默认 true
});
// NPC 只观察不被观察
aoi.addObserver(camera, position, {
viewRange: 500,
observable: false
});
```
#### removeObserver
移除观察者:
```typescript
aoi.removeObserver(player);
```
#### updatePosition
更新位置(自动触发进入/离开事件):
```typescript
aoi.updatePosition(player, newPosition);
```
#### updateViewRange
更新视野范围:
```typescript
// 获得增益后视野扩大
aoi.updateViewRange(player, 300);
```
### 查询方法
#### getEntitiesInView
获取观察者视野内的所有实体:
```typescript
const visible = aoi.getEntitiesInView(player);
for (const entity of visible) {
updateEntityForPlayer(player, entity);
}
```
#### getObserversOf
获取能看到指定实体的所有观察者:
```typescript
const observers = aoi.getObserversOf(monster);
for (const observer of observers) {
notifyMonsterMoved(observer, monster);
}
```
#### canSee
检查是否可见:
```typescript
if (aoi.canSee(player, enemy)) {
enemy.showHealthBar();
}
```
### 事件系统
#### 全局事件监听
```typescript
aoi.addListener((event) => {
switch (event.type) {
case 'enter':
console.log(`${event.observer} 看到了 ${event.target}`);
break;
case 'exit':
console.log(`${event.target} 离开了 ${event.observer} 的视野`);
break;
}
});
```
#### 实体特定事件监听
```typescript
// 只监听特定玩家的视野事件
aoi.addEntityListener(player, (event) => {
if (event.type === 'enter') {
sendToClient(player, 'entity_enter', event.target);
} else if (event.type === 'exit') {
sendToClient(player, 'entity_exit', event.target);
}
});
```
#### 事件类型
```typescript
interface IAOIEvent<T> {
type: 'enter' | 'exit' | 'update';
observer: T; // 观察者(谁看到了变化)
target: T; // 目标(发生变化的对象)
position: IVector2; // 目标位置
}
```
## 工具函数
### 边界框创建
```typescript
import {
createBounds,
createBoundsFromCenter,
createBoundsFromCircle
} from '@esengine/spatial';
// 从角点创建
const bounds1 = createBounds(0, 0, 100, 100);
// 从中心点和尺寸创建
const bounds2 = createBoundsFromCenter({ x: 50, y: 50 }, 100, 100);
// 从圆形创建(包围盒)
const bounds3 = createBoundsFromCircle({ x: 50, y: 50 }, 50);
```
### 几何检测
```typescript
import {
isPointInBounds,
boundsIntersect,
boundsIntersectsCircle,
distance,
distanceSquared
} from '@esengine/spatial';
// 点在边界内?
if (isPointInBounds(point, bounds)) { ... }
// 两个边界框相交?
if (boundsIntersect(boundsA, boundsB)) { ... }
// 边界框与圆形相交?
if (boundsIntersectsCircle(bounds, center, radius)) { ... }
// 距离计算
const dist = distance(pointA, pointB);
const distSq = distanceSquared(pointA, pointB); // 更快,避免 sqrt
```
## 实际示例
### 范围攻击检测
```typescript
class CombatSystem {
private spatialIndex: ISpatialIndex<Entity>;
dealAreaDamage(center: IVector2, radius: number, damage: number): void {
const targets = this.spatialIndex.findInRadius(
center,
radius,
(entity) => entity.hasComponent(HealthComponent)
);
for (const target of targets) {
const health = target.getComponent(HealthComponent);
health.takeDamage(damage);
}
}
findNearestEnemy(position: IVector2, team: string): Entity | null {
return this.spatialIndex.findNearest(
position,
undefined, // 无距离限制
(entity) => {
const teamComp = entity.getComponent(TeamComponent);
return teamComp && teamComp.team !== team;
}
);
}
}
```
### MMO 同步系统
```typescript
class SyncSystem {
private aoi: IAOIManager<Player>;
constructor() {
this.aoi = createGridAOI<Player>(100);
// 监听进入/离开事件
this.aoi.addListener((event) => {
const packet = this.createSyncPacket(event);
this.sendToPlayer(event.observer, packet);
});
}
onPlayerJoin(player: Player): void {
this.aoi.addObserver(player, player.position, {
viewRange: player.viewRange
});
}
onPlayerMove(player: Player, newPosition: IVector2): void {
this.aoi.updatePosition(player, newPosition);
}
onPlayerLeave(player: Player): void {
this.aoi.removeObserver(player);
}
// 广播给所有能看到某玩家的其他玩家
broadcastToObservers(player: Player, packet: Packet): void {
const observers = this.aoi.getObserversOf(player);
for (const observer of observers) {
this.sendToPlayer(observer, packet);
}
}
}
```
### NPC AI 感知
```typescript
class AIPerceptionSystem {
private aoi: IAOIManager<Entity>;
constructor() {
this.aoi = createGridAOI<Entity>(50);
}
setupNPC(npc: Entity): void {
const perception = npc.getComponent(PerceptionComponent);
this.aoi.addObserver(npc, npc.position, {
viewRange: perception.range
});
// 监听该 NPC 的感知事件
this.aoi.addEntityListener(npc, (event) => {
const ai = npc.getComponent(AIComponent);
if (event.type === 'enter') {
ai.onTargetDetected(event.target);
} else if (event.type === 'exit') {
ai.onTargetLost(event.target);
}
});
}
update(): void {
// 更新所有 NPC 位置
for (const npc of this.npcs) {
this.aoi.updatePosition(npc, npc.position);
}
}
}
```
## 蓝图节点
### 空间查询节点
- `FindInRadius` - 查找半径内的对象
- `FindInRect` - 查找矩形内的对象
- `FindNearest` - 查找最近的对象
- `FindKNearest` - 查找最近的 K 个对象
- `Raycast` - 射线检测
- `RaycastFirst` - 射线检测(仅第一个)
### AOI 节点
- `GetEntitiesInView` - 获取视野内实体
- `GetObserversOf` - 获取观察者
- `CanSee` - 检查可见性
- `OnEntityEnterView` - 进入视野事件
- `OnEntityExitView` - 离开视野事件
## 服务令牌
在依赖注入场景中使用:
```typescript
import {
SpatialIndexToken,
SpatialQueryToken,
AOIManagerToken,
createGridSpatialIndex,
createGridAOI
} from '@esengine/spatial';
// 注册服务
services.register(SpatialIndexToken, createGridSpatialIndex(100));
services.register(AOIManagerToken, createGridAOI(100));
// 获取服务
const spatialIndex = services.get(SpatialIndexToken);
const aoiManager = services.get(AOIManagerToken);
```
## 性能优化
1. **选择合适的 cellSize**
- 太小:内存占用高,单元格数量多
- 太大:单元格内对象多,遍历慢
- 经验法则:对象平均间距的 1-2 倍
2. **使用过滤器减少结果**
```typescript
// 在空间查询阶段就过滤,而不是事后过滤
spatialIndex.findInRadius(center, radius, (e) => e.type === 'enemy');
```
3. **使用 distanceSquared 代替 distance**
```typescript
// 避免 sqrt 计算
if (distanceSquared(a, b) < threshold * threshold) { ... }
```
4. **批量更新优化**
```typescript
// 如果有大量对象同时移动,考虑禁用事件后批量更新
```

479
docs/modules/timer/index.md Normal file
View File

@@ -0,0 +1,479 @@
# 定时器系统 (Timer)
`@esengine/timer` 提供了一个灵活的定时器和冷却系统,用于游戏中的延迟执行、重复任务、技能冷却等场景。
## 安装
```bash
npm install @esengine/timer
```
## 快速开始
```typescript
import { createTimerService } from '@esengine/timer';
// 创建定时器服务
const timerService = createTimerService();
// 一次性定时器1秒后执行
const handle = timerService.schedule('myTimer', 1000, () => {
console.log('Timer fired!');
});
// 重复定时器每100毫秒执行
timerService.scheduleRepeating('heartbeat', 100, () => {
console.log('Tick');
});
// 冷却系统5秒冷却
timerService.startCooldown('skill_fireball', 5000);
if (timerService.isCooldownReady('skill_fireball')) {
// 可以使用技能
useFireball();
timerService.startCooldown('skill_fireball', 5000);
}
// 游戏循环中更新
function gameLoop(deltaTime: number) {
timerService.update(deltaTime);
}
```
## 核心概念
### 定时器 vs 冷却
| 特性 | 定时器 (Timer) | 冷却 (Cooldown) |
|------|---------------|-----------------|
| 用途 | 延迟执行代码 | 限制操作频率 |
| 回调 | 有回调函数 | 无回调函数 |
| 重复 | 支持重复执行 | 一次性 |
| 查询 | 查询剩余时间 | 查询进度/是否就绪 |
### TimerHandle
调度定时器后返回的句柄对象,用于控制定时器:
```typescript
interface TimerHandle {
readonly id: string; // 定时器 ID
readonly isValid: boolean; // 是否有效(未被取消)
cancel(): void; // 取消定时器
}
```
### TimerInfo
定时器信息对象:
```typescript
interface TimerInfo {
readonly id: string; // 定时器 ID
readonly remaining: number; // 剩余时间(毫秒)
readonly repeating: boolean; // 是否重复执行
readonly interval?: number; // 间隔时间(仅重复定时器)
}
```
### CooldownInfo
冷却信息对象:
```typescript
interface CooldownInfo {
readonly id: string; // 冷却 ID
readonly duration: number; // 总持续时间(毫秒)
readonly remaining: number; // 剩余时间(毫秒)
readonly progress: number; // 进度0-10=刚开始1=结束)
readonly isReady: boolean; // 是否已就绪
}
```
## API 参考
### createTimerService
```typescript
function createTimerService(config?: TimerServiceConfig): ITimerService
```
**配置选项:**
| 属性 | 类型 | 默认值 | 描述 |
|------|------|--------|------|
| `maxTimers` | `number` | `0` | 最大定时器数量0 表示无限制) |
| `maxCooldowns` | `number` | `0` | 最大冷却数量0 表示无限制) |
### 定时器 API
#### schedule
调度一次性定时器:
```typescript
const handle = timerService.schedule('explosion', 2000, () => {
createExplosion();
});
// 提前取消
handle.cancel();
```
#### scheduleRepeating
调度重复定时器:
```typescript
// 每秒执行
timerService.scheduleRepeating('regen', 1000, () => {
player.hp += 5;
});
// 立即执行一次,然后每秒重复
timerService.scheduleRepeating('tick', 1000, () => {
console.log('Tick');
}, true); // immediate = true
```
#### cancel / cancelById
取消定时器:
```typescript
// 通过句柄取消
handle.cancel();
// 或
timerService.cancel(handle);
// 通过 ID 取消
timerService.cancelById('regen');
```
#### hasTimer
检查定时器是否存在:
```typescript
if (timerService.hasTimer('explosion')) {
console.log('Explosion is pending');
}
```
#### getTimerInfo
获取定时器信息:
```typescript
const info = timerService.getTimerInfo('explosion');
if (info) {
console.log(`剩余时间: ${info.remaining}ms`);
console.log(`是否重复: ${info.repeating}`);
}
```
### 冷却 API
#### startCooldown
开始冷却:
```typescript
// 5秒冷却
timerService.startCooldown('skill_fireball', 5000);
```
#### isCooldownReady / isOnCooldown
检查冷却状态:
```typescript
if (timerService.isCooldownReady('skill_fireball')) {
// 可以使用技能
castFireball();
timerService.startCooldown('skill_fireball', 5000);
} else {
console.log('技能还在冷却中');
}
// 或使用 isOnCooldown
if (timerService.isOnCooldown('skill_fireball')) {
console.log('冷却中...');
}
```
#### getCooldownProgress / getCooldownRemaining
获取冷却进度:
```typescript
// 进度 0-10=刚开始1=完成)
const progress = timerService.getCooldownProgress('skill_fireball');
console.log(`冷却进度: ${(progress * 100).toFixed(0)}%`);
// 剩余时间(毫秒)
const remaining = timerService.getCooldownRemaining('skill_fireball');
console.log(`剩余时间: ${(remaining / 1000).toFixed(1)}s`);
```
#### getCooldownInfo
获取完整冷却信息:
```typescript
const info = timerService.getCooldownInfo('skill_fireball');
if (info) {
console.log(`总时长: ${info.duration}ms`);
console.log(`剩余: ${info.remaining}ms`);
console.log(`进度: ${info.progress}`);
console.log(`就绪: ${info.isReady}`);
}
```
#### resetCooldown / clearAllCooldowns
重置冷却:
```typescript
// 重置单个冷却
timerService.resetCooldown('skill_fireball');
// 清除所有冷却(例如角色复活时)
timerService.clearAllCooldowns();
```
### 生命周期
#### update
更新定时器服务(需要每帧调用):
```typescript
function gameLoop(deltaTime: number) {
// deltaTime 单位是毫秒
timerService.update(deltaTime);
}
```
#### clear
清除所有定时器和冷却:
```typescript
timerService.clear();
```
### 调试属性
```typescript
// 获取活跃定时器数量
console.log(timerService.activeTimerCount);
// 获取活跃冷却数量
console.log(timerService.activeCooldownCount);
// 获取所有活跃定时器 ID
const timerIds = timerService.getActiveTimerIds();
// 获取所有活跃冷却 ID
const cooldownIds = timerService.getActiveCooldownIds();
```
## 实际示例
### 技能冷却系统
```typescript
import { createTimerService, type ITimerService } from '@esengine/timer';
class SkillSystem {
private timerService: ITimerService;
private skills: Map<string, SkillData> = new Map();
constructor() {
this.timerService = createTimerService();
}
registerSkill(id: string, data: SkillData): void {
this.skills.set(id, data);
}
useSkill(skillId: string): boolean {
const skill = this.skills.get(skillId);
if (!skill) return false;
// 检查冷却
if (!this.timerService.isCooldownReady(skillId)) {
const remaining = this.timerService.getCooldownRemaining(skillId);
console.log(`技能 ${skillId} 冷却中,剩余 ${remaining}ms`);
return false;
}
// 使用技能
this.executeSkill(skill);
// 开始冷却
this.timerService.startCooldown(skillId, skill.cooldown);
return true;
}
getSkillCooldownProgress(skillId: string): number {
return this.timerService.getCooldownProgress(skillId);
}
update(dt: number): void {
this.timerService.update(dt);
}
}
interface SkillData {
cooldown: number;
// ... other properties
}
```
### 延迟和定时效果
```typescript
class EffectSystem {
private timerService: ITimerService;
constructor(timerService: ITimerService) {
this.timerService = timerService;
}
// 延迟爆炸
scheduleExplosion(position: { x: number; y: number }, delay: number): void {
this.timerService.schedule(`explosion_${Date.now()}`, delay, () => {
this.createExplosion(position);
});
}
// DOT 伤害(每秒造成伤害)
applyDOT(target: Entity, damage: number, duration: number): void {
const dotId = `dot_${target.id}_${Date.now()}`;
let elapsed = 0;
this.timerService.scheduleRepeating(dotId, 1000, () => {
elapsed += 1000;
target.takeDamage(damage);
if (elapsed >= duration) {
this.timerService.cancelById(dotId);
}
});
}
// BUFF 效果(持续一段时间)
applyBuff(target: Entity, buffId: string, duration: number): void {
target.addBuff(buffId);
this.timerService.schedule(`buff_expire_${buffId}`, duration, () => {
target.removeBuff(buffId);
});
}
}
```
### 与 ECS 集成
```typescript
import { Component, EntitySystem, Matcher } from '@esengine/ecs-framework';
import { createTimerService, type ITimerService } from '@esengine/timer';
// 定时器组件
class TimerComponent extends Component {
timerService: ITimerService;
constructor() {
super();
this.timerService = createTimerService();
}
}
// 定时器系统
class TimerSystem extends EntitySystem {
constructor() {
super(Matcher.all(TimerComponent));
}
protected processEntity(entity: Entity, dt: number): void {
const timer = entity.getComponent(TimerComponent);
timer.timerService.update(dt);
}
}
// 冷却组件(用于共享冷却)
class CooldownComponent extends Component {
constructor(public timerService: ITimerService) {
super();
}
}
```
## 蓝图节点
Timer 模块提供了可视化脚本支持的蓝图节点:
### 冷却节点
- `StartCooldown` - 开始冷却
- `IsCooldownReady` - 检查冷却是否就绪
- `GetCooldownProgress` - 获取冷却进度
- `GetCooldownInfo` - 获取详细冷却信息
- `ResetCooldown` - 重置冷却
### 定时器节点
- `HasTimer` - 检查定时器是否存在
- `CancelTimer` - 取消定时器
- `GetTimerRemaining` - 获取定时器剩余时间
## 服务令牌
在依赖注入场景中使用:
```typescript
import { TimerServiceToken, createTimerService } from '@esengine/timer';
// 注册服务
services.register(TimerServiceToken, createTimerService());
// 获取服务
const timerService = services.get(TimerServiceToken);
```
## 最佳实践
1. **使用有意义的 ID**:使用描述性的 ID 便于调试和管理
```typescript
// 好
timerService.startCooldown('skill_fireball', 5000);
// 不好
timerService.startCooldown('cd1', 5000);
```
2. **避免重复 ID**:相同 ID 的定时器会覆盖之前的
```typescript
// 使用唯一 ID
const uniqueId = `explosion_${entity.id}_${Date.now()}`;
timerService.schedule(uniqueId, 1000, callback);
```
3. **及时清理**:在适当时机清理不需要的定时器和冷却
```typescript
// 实体销毁时
onDestroy() {
this.timerService.cancelById(this.timerId);
}
```
4. **配置限制**:在生产环境考虑设置最大数量限制
```typescript
const timerService = createTimerService({
maxTimers: 1000,
maxCooldowns: 500
});
```

View File

@@ -0,0 +1,15 @@
# @esengine/behavior-tree
## 1.0.3
### Patch Changes
- Updated dependencies [[`7d74623`](https://github.com/esengine/esengine/commit/7d746237100084ac3456f1af92ff664db4e50cc8)]:
- @esengine/ecs-framework@2.4.4
## 1.0.2
### Patch Changes
- Updated dependencies [[`ce2db4e`](https://github.com/esengine/esengine/commit/ce2db4e48a7cdac44265420ef16e83f6424f4dea)]:
- @esengine/ecs-framework@2.4.3

View File

@@ -1,6 +1,6 @@
{
"name": "@esengine/behavior-tree",
"version": "1.0.1",
"version": "1.0.3",
"description": "ECS-based AI behavior tree system - works with any ECS framework (ESEngine, Cocos, Laya, etc.)",
"main": "dist/index.js",
"module": "dist/index.js",

View File

@@ -0,0 +1,15 @@
# @esengine/blueprint
## 1.0.2
### Patch Changes
- Updated dependencies [[`7d74623`](https://github.com/esengine/esengine/commit/7d746237100084ac3456f1af92ff664db4e50cc8)]:
- @esengine/ecs-framework@2.4.4
## 1.0.1
### Patch Changes
- Updated dependencies [[`ce2db4e`](https://github.com/esengine/esengine/commit/ce2db4e48a7cdac44265420ef16e83f6424f4dea)]:
- @esengine/ecs-framework@2.4.3

View File

@@ -1,6 +1,6 @@
{
"name": "@esengine/blueprint",
"version": "1.0.0",
"version": "1.0.2",
"description": "Visual scripting system - works with any ECS framework (ESEngine, Cocos, Laya, etc.)",
"main": "dist/index.js",
"module": "dist/index.js",

View File

@@ -0,0 +1,16 @@
# @esengine/ecs-framework
## 2.4.4
### Patch Changes
- [`7d74623`](https://github.com/esengine/esengine/commit/7d746237100084ac3456f1af92ff664db4e50cc8) Thanks [@esengine](https://github.com/esengine)! - fix(core): 修复 npm 发布目录配置,确保从 dist 目录发布以保持与 Cocos Creator 的兼容性
## 2.4.3
### Patch Changes
- [#356](https://github.com/esengine/esengine/pull/356) [`ce2db4e`](https://github.com/esengine/esengine/commit/ce2db4e48a7cdac44265420ef16e83f6424f4dea) Thanks [@esengine](https://github.com/esengine)! - fix(core): 修复 World cleanup 在打包环境下的兼容性问题
- 使用 forEach 替代 spread + for...of 解构模式,避免某些打包工具(如 Cocos Creator转换后的兼容性问题
- 重构 World 和 WorldManager 类,提升代码质量
- 提取默认配置为常量,统一双语注释格式

View File

@@ -1,6 +1,6 @@
{
"name": "@esengine/ecs-framework",
"version": "2.4.2",
"version": "2.4.4",
"description": "用于Laya、Cocos Creator等JavaScript游戏引擎的高性能ECS框架",
"main": "dist/index.cjs",
"module": "dist/index.mjs",
@@ -70,7 +70,8 @@
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
"registry": "https://registry.npmjs.org/",
"directory": "dist"
},
"repository": {
"type": "git",

View File

@@ -13,7 +13,7 @@ const logger = createLogger('World');
* @zh 全局系统是在World级别运行的系统不依赖特定Scene
* @en Global systems run at World level and don't depend on specific Scene
*/
export type IGlobalSystem = {
export interface IGlobalSystem {
/**
* @zh 系统名称
* @en System name
@@ -49,7 +49,7 @@ export type IGlobalSystem = {
* @zh World配置接口
* @en World configuration interface
*/
export type IWorldConfig = {
export interface IWorldConfig {
/**
* @zh World名称
* @en World name
@@ -77,12 +77,23 @@ export type IWorldConfig = {
/**
* @zh 自动清理阈值毫秒空Scene超过此时间后将被自动清理
* @en Auto cleanup threshold (ms), empty scenes exceeding this time will be auto-cleaned
*
* @default 300000 (5 minutes)
*/
cleanupThresholdMs?: number;
}
/**
* @zh World默认配置
* @en World default configuration
*/
const DEFAULT_CONFIG: Required<IWorldConfig> = {
name: 'World',
debug: false,
maxScenes: 10,
autoCleanup: true,
cleanupThresholdMs: 5 * 60 * 1000
};
/**
* @zh World类 - ECS世界管理器
* @en World class - ECS world manager
@@ -101,67 +112,66 @@ export type IWorldConfig = {
* - World.services: World-level services (independent per World)
* - Scene.services: Scene-level services (independent per Scene)
*
* @zh 这种设计允许创建独立的游戏世界,如:
* - 游戏房间每个房间一个World
* - 不同的游戏模式
* - 独立的模拟环境
* @en This design allows creating independent game worlds like:
* - Game rooms (one World per room)
* - Different game modes
* - Independent simulation environments
*
* @example
* ```typescript
* // @zh 创建游戏房间的World | @en Create World for game room
* const roomWorld = new World({ name: 'Room_001' });
*
* // @zh 注册World级别的服务 | @en Register World-level service
* roomWorld.services.registerSingleton(RoomManager);
*
* // @zh 在World中创建Scene | @en Create Scene in World
* const gameScene = roomWorld.createScene('game', new Scene());
* const uiScene = roomWorld.createScene('ui', new Scene());
*
* // @zh 在Scene中使用World级别的服务 | @en Use World-level service in Scene
* const roomManager = roomWorld.services.resolve(RoomManager);
*
* // @zh 更新整个World | @en Update entire World
* roomWorld.update(deltaTime);
* const gameScene = roomWorld.createScene('game');
* roomWorld.setSceneActive('game', true);
* roomWorld.start();
* ```
*/
export class World {
public readonly name: string;
private readonly _config: IWorldConfig;
private readonly _scenes: Map<string, IScene> = new Map();
private readonly _activeScenes: Set<string> = new Set();
private readonly _config: Required<IWorldConfig>;
private readonly _scenes = new Map<string, IScene>();
private readonly _activeScenes = new Set<string>();
private readonly _globalSystems: IGlobalSystem[] = [];
private readonly _services: ServiceContainer;
private _isActive: boolean = false;
private _createdAt: number;
private readonly _createdAt: number;
private _isActive = false;
constructor(config: IWorldConfig = {}) {
this._config = {
name: 'World',
debug: false,
maxScenes: 10,
autoCleanup: true,
cleanupThresholdMs: 5 * 60 * 1000,
...config
};
this.name = this._config.name!;
this._config = { ...DEFAULT_CONFIG, ...config };
this.name = this._config.name;
this._createdAt = Date.now();
this._services = new ServiceContainer();
}
/**
* @zh World级别的服务容器用于管理World范围内的全局服务
* @en World-level service container for managing World-scoped global services
* @zh World级别的服务容器
* @en World-level service container
*/
public get services(): ServiceContainer {
return this._services;
}
/**
* @zh 检查World是否激活
* @en Check if World is active
*/
public get isActive(): boolean {
return this._isActive;
}
/**
* @zh 获取Scene数量
* @en Get scene count
*/
public get sceneCount(): number {
return this._scenes.size;
}
/**
* @zh 获取创建时间
* @en Get creation time
*/
public get createdAt(): number {
return this._createdAt;
}
/**
* @zh 创建并添加Scene到World
* @en Create and add Scene to World
@@ -169,32 +179,21 @@ export class World {
* @param sceneName - @zh Scene名称 @en Scene name
* @param sceneInstance - @zh Scene实例可选@en Scene instance (optional)
* @returns @zh 创建的Scene实例 @en Created Scene instance
* @throws @zh 名称为空、重复或超出限制时抛出错误 @en Throws if name is empty, duplicate, or limit exceeded
*/
public createScene<T extends IScene>(sceneName: string, sceneInstance?: T): T {
if (!sceneName || typeof sceneName !== 'string' || sceneName.trim() === '') {
throw new Error('Scene name不能为空');
}
this.validateSceneName(sceneName);
if (this._scenes.has(sceneName)) {
throw new Error(`Scene name '${sceneName}' 已存在于World '${this.name}' 中`);
}
if (this._scenes.size >= this._config.maxScenes!) {
throw new Error(`World '${this.name}' 已达到最大Scene数量限制: ${this._config.maxScenes}`);
}
const scene = sceneInstance || (new Scene() as unknown as T);
const scene = sceneInstance ?? new Scene() as unknown as T;
if (this._config.debug) {
const performanceMonitor = new PerformanceMonitor();
performanceMonitor.enable();
scene.services.registerInstance(PerformanceMonitor, performanceMonitor);
const monitor = new PerformanceMonitor();
monitor.enable();
scene.services.registerInstance(PerformanceMonitor, monitor);
}
(scene as { id?: string }).id = sceneName;
if (!scene.name) {
scene.name = sceneName;
}
scene.name ||= sceneName;
this._scenes.set(sceneName, scene);
scene.initialize();
@@ -211,9 +210,7 @@ export class World {
*/
public removeScene(sceneName: string): boolean {
const scene = this._scenes.get(sceneName);
if (!scene) {
return false;
}
if (!scene) return false;
if (this._activeScenes.has(sceneName)) {
this.setSceneActive(sceneName, false);
@@ -221,11 +218,20 @@ export class World {
scene.end();
this._scenes.delete(sceneName);
logger.info(`从World '${this.name}' 中移除Scene: ${sceneName}`);
return true;
}
/**
* @zh 移除所有Scene
* @en Remove all Scenes
*/
public removeAllScenes(): void {
this._scenes.forEach((_, name) => this.removeScene(name));
logger.info(`从World '${this.name}' 中移除所有Scene`);
}
/**
* @zh 获取Scene
* @en Get Scene
@@ -234,36 +240,31 @@ export class World {
* @returns @zh Scene实例或null @en Scene instance or null
*/
public getScene<T extends IScene>(sceneName: string): T | null {
return this._scenes.get(sceneName) as T || null;
return (this._scenes.get(sceneName) as T) ?? null;
}
/**
* 获取所有Scene ID
* @zh 获取所有Scene ID
* @en Get all Scene IDs
*/
public getSceneIds(): string[] {
return Array.from(this._scenes.keys());
}
/**
* 获取所有Scene
* @zh 获取所有Scene
* @en Get all Scenes
*/
public getAllScenes(): IScene[] {
return Array.from(this._scenes.values());
}
/**
* 移除所有Scene
*/
public removeAllScenes(): void {
const sceneNames = Array.from(this._scenes.keys());
for (const sceneName of sceneNames) {
this.removeScene(sceneName);
}
logger.info(`从World '${this.name}' 中移除所有Scene`);
}
/**
* 设置Scene激活状态
* @zh 设置Scene激活状态
* @en Set Scene active state
*
* @param sceneName - @zh Scene名称 @en Scene name
* @param active - @zh 是否激活 @en Whether to activate
*/
public setSceneActive(sceneName: string, active: boolean): void {
const scene = this._scenes.get(sceneName);
@@ -283,22 +284,27 @@ export class World {
}
/**
* 检查Scene是否激活
* @zh 检查Scene是否激活
* @en Check if Scene is active
*/
public isSceneActive(sceneName: string): boolean {
return this._activeScenes.has(sceneName);
}
/**
* 获取活跃Scene数量
* @zh 获取活跃Scene数量
* @en Get active Scene count
*/
public getActiveSceneCount(): number {
return this._activeScenes.size;
}
/**
* 添加全局System
* 全局System会在所有激活Scene之前更新
* @zh 添加全局System
* @en Add global System
*
* @param system - @zh 全局System实例 @en Global System instance
* @returns @zh 添加的System实例 @en Added System instance
*/
public addGlobalSystem<T extends IGlobalSystem>(system: T): T {
if (this._globalSystems.includes(system)) {
@@ -307,132 +313,77 @@ export class World {
this._globalSystems.push(system);
system.initialize?.();
logger.debug(`在World '${this.name}' 中添加全局System: ${system.name}`);
return system;
}
/**
* 移除全局System
* @zh 移除全局System
* @en Remove global System
*
* @param system - @zh 要移除的System @en System to remove
* @returns @zh 是否成功移除 @en Whether removal was successful
*/
public removeGlobalSystem(system: IGlobalSystem): boolean {
const index = this._globalSystems.indexOf(system);
if (index === -1) {
return false;
}
if (index === -1) return false;
this._globalSystems.splice(index, 1);
system.reset?.();
logger.debug(`从World '${this.name}' 中移除全局System: ${system.name}`);
return true;
}
/**
* 获取全局System
* @zh 获取全局System
* @en Get global System
*
* @param type - @zh System类型 @en System type
* @returns @zh System实例或null @en System instance or null
*/
public getGlobalSystem<T extends IGlobalSystem>(type: new (...args: any[]) => T): T | null {
for (const system of this._globalSystems) {
if (system instanceof type) {
return system as T;
}
}
return null;
public getGlobalSystem<T extends IGlobalSystem>(type: new (...args: unknown[]) => T): T | null {
return (this._globalSystems.find(s => s instanceof type) as T) ?? null;
}
/**
* 启动World
* @zh 启动World
* @en Start World
*/
public start(): void {
if (this._isActive) {
return;
}
if (this._isActive) return;
this._isActive = true;
for (const system of this._globalSystems) {
system.initialize?.();
}
this._globalSystems.forEach(s => s.initialize?.());
logger.info(`启动World: ${this.name}`);
}
/**
* 停止World
* @zh 停止World
* @en Stop World
*/
public stop(): void {
if (!this._isActive) {
return;
}
for (const sceneName of this._activeScenes) {
this.setSceneActive(sceneName, false);
}
for (const system of this._globalSystems) {
system.reset?.();
}
if (!this._isActive) return;
this._activeScenes.forEach(name => this.setSceneActive(name, false));
this._globalSystems.forEach(s => s.reset?.());
this._isActive = false;
logger.info(`停止World: ${this.name}`);
}
/**
* @zh 更新World中的全局System
* @en Update global systems in World
*
* @internal Called by Core.update()
*/
public updateGlobalSystems(): void {
if (!this._isActive) {
return;
}
for (const system of this._globalSystems) {
system.update?.();
}
}
/**
* @zh 更新World中的所有激活Scene
* @en Update all active scenes in World
*
* @internal Called by Core.update()
*/
public updateScenes(): void {
if (!this._isActive) {
return;
}
for (const sceneName of this._activeScenes) {
const scene = this._scenes.get(sceneName);
scene?.update?.();
}
if (this._config.autoCleanup) {
this.cleanup();
}
}
/**
* 销毁World
* @zh 销毁World
* @en Destroy World
*/
public destroy(): void {
logger.info(`销毁World: ${this.name}`);
this.stop();
this.removeAllScenes();
for (const sceneName of Array.from(this._scenes.keys())) {
this.removeScene(sceneName);
}
for (const system of this._globalSystems) {
if (system.destroy) {
system.destroy();
} else {
system.reset?.();
}
}
this._globalSystems.forEach(s => s.destroy?.() ?? s.reset?.());
this._globalSystems.length = 0;
this._services.clear();
@@ -440,10 +391,49 @@ export class World {
this._activeScenes.clear();
}
/**
* 获取World状态
* @zh 更新World中的全局System
* @en Update global systems in World
* @internal
*/
public updateGlobalSystems(): void {
if (!this._isActive) return;
this._globalSystems.forEach(s => s.update?.());
}
/**
* @zh 更新World中的所有激活Scene
* @en Update all active scenes in World
* @internal
*/
public updateScenes(): void {
if (!this._isActive) return;
this._activeScenes.forEach(name => {
this._scenes.get(name)?.update?.();
});
if (this._config.autoCleanup) {
this.cleanup();
}
}
/**
* @zh 获取World状态
* @en Get World status
*/
public getStatus() {
const scenes: Array<{ id: string; name: string; isActive: boolean }> = [];
this._scenes.forEach((scene, id) => {
scenes.push({
id,
name: scene.name || id,
isActive: this._activeScenes.has(id)
});
});
return {
name: this.name,
isActive: this._isActive,
@@ -452,46 +442,61 @@ export class World {
globalSystemCount: this._globalSystems.length,
createdAt: this._createdAt,
config: { ...this._config },
scenes: Array.from(this._scenes.keys()).map((sceneName) => ({
id: sceneName,
isActive: this._activeScenes.has(sceneName),
name: this._scenes.get(sceneName)?.name || sceneName
}))
scenes
};
}
/**
* 获取World统计信息
* @zh 获取World统计信息
* @en Get World statistics
*/
public getStats() {
const stats = {
totalEntities: 0,
totalSystems: this._globalSystems.length,
let totalEntities = 0;
let totalSystems = this._globalSystems.length;
this._scenes.forEach(scene => {
totalEntities += scene.entities?.count ?? 0;
totalSystems += scene.systems?.length ?? 0;
});
return {
totalEntities,
totalSystems,
memoryUsage: 0,
performance: {
averageUpdateTime: 0,
maxUpdateTime: 0
}
};
}
for (const scene of this._scenes.values()) {
stats.totalEntities += scene.entities?.count ?? 0;
stats.totalSystems += scene.systems?.length ?? 0;
/**
* @zh 验证Scene名称
* @en Validate Scene name
*/
private validateSceneName(sceneName: string): void {
if (!sceneName?.trim()) {
throw new Error('Scene name不能为空');
}
if (this._scenes.has(sceneName)) {
throw new Error(`Scene name '${sceneName}' 已存在于World '${this.name}' 中`);
}
if (this._scenes.size >= this._config.maxScenes) {
throw new Error(`World '${this.name}' 已达到最大Scene数量限制: ${this._config.maxScenes}`);
}
return stats;
}
/**
* @zh 检查Scene是否可以被自动清理
* @en Check if a scene is eligible for auto cleanup
*/
private _isSceneCleanupCandidate(sceneName: string, scene: IScene): boolean {
private isCleanupCandidate(sceneName: string, scene: IScene): boolean {
const elapsed = Date.now() - this._createdAt;
return !this._activeScenes.has(sceneName) &&
scene.entities != null &&
scene.entities.count === 0 &&
elapsed > this._config.cleanupThresholdMs!;
elapsed > this._config.cleanupThresholdMs;
}
/**
@@ -499,33 +504,18 @@ export class World {
* @en Execute auto cleanup operation
*/
private cleanup(): void {
const candidates = [...this._scenes.entries()]
.filter(([name, scene]) => this._isSceneCleanupCandidate(name, scene));
const toRemove: string[] = [];
for (const [sceneName] of candidates) {
this.removeScene(sceneName);
logger.debug(`自动清理空Scene: ${sceneName} from World ${this.name}`);
}
this._scenes.forEach((scene, name) => {
if (this.isCleanupCandidate(name, scene)) {
toRemove.push(name);
}
});
toRemove.forEach(name => {
this.removeScene(name);
logger.debug(`自动清理空Scene: ${name} from World ${this.name}`);
});
}
/**
* 检查World是否激活
*/
public get isActive(): boolean {
return this._isActive;
}
/**
* 获取Scene数量
*/
public get sceneCount(): number {
return this._scenes.size;
}
/**
* 获取创建时间
*/
public get createdAt(): number {
return this._createdAt;
}
}

View File

@@ -5,80 +5,85 @@ import type { IService } from '../Core/ServiceContainer';
const logger = createLogger('WorldManager');
/**
* WorldManager配置接口
* @zh WorldManager配置接口
* @en WorldManager configuration interface
*/
export type IWorldManagerConfig = {
export interface IWorldManagerConfig {
/**
* 最大World数量
* @zh 最大World数量
* @en Maximum number of worlds
*/
maxWorlds?: number;
/**
* 是否自动清理空World
* @zh 是否自动清理空World
* @en Auto cleanup empty worlds
*/
autoCleanup?: boolean;
/**
* 清理间隔(帧数)
* @zh 清理间隔(帧数)
* @en Cleanup interval in frames
*/
cleanupFrameInterval?: number;
/**
* 是否启用调试模式
* @zh 是否启用调试模式
* @en Enable debug mode
*/
debug?: boolean;
}
/**
* World管理器 - 管理所有World实例
* @zh WorldManager默认配置
* @en WorldManager default configuration
*/
const DEFAULT_CONFIG: Required<IWorldManagerConfig> = {
maxWorlds: 50,
autoCleanup: true,
cleanupFrameInterval: 1800,
debug: false
};
/**
* @zh 清理阈值(毫秒)
* @en Cleanup threshold in milliseconds
*/
const CLEANUP_THRESHOLD_MS = 10 * 60 * 1000; // 10 minutes
/**
* @zh World管理器 - 管理所有World实例
* @en World Manager - Manages all World instances
*
* WorldManager负责管理多个独立的World实例。
* @zh WorldManager负责管理多个独立的World实例。
* 每个World都是独立的ECS环境可以包含多个Scene。
* @en WorldManager is responsible for managing multiple independent World instances.
* Each World is an isolated ECS environment that can contain multiple Scenes.
*
* 适用场景:
* @zh 适用场景:
* - MMO游戏的多房间管理
* - 服务器端的多游戏实例
* - 需要完全隔离的多个游戏环境
* @en Use cases:
* - Multi-room management for MMO games
* - Multiple game instances on server-side
* - Completely isolated game environments
*
* @example
* ```typescript
* // 创建WorldManager实例
* const worldManager = new WorldManager({
* maxWorlds: 100,
* autoCleanup: true
* });
*
* // 创建游戏房间World
* const room1 = worldManager.createWorld('room_001', {
* name: 'GameRoom_001',
* maxScenes: 5
* });
* room1.setActive(true);
*
* // 游戏循环
* function gameLoop(deltaTime: number) {
* Core.update(deltaTime);
* worldManager.updateAll(); // 更新所有活跃World
* }
* const worldManager = new WorldManager({ maxWorlds: 100 });
* const room = worldManager.createWorld('room_001');
* worldManager.setWorldActive('room_001', true);
* ```
*/
export class WorldManager implements IService {
private readonly _config: Required<IWorldManagerConfig>;
private readonly _worlds: Map<string, World> = new Map();
private _isRunning: boolean = false;
private _framesSinceCleanup: number = 0;
private readonly _worlds = new Map<string, World>();
private _isRunning = true;
private _framesSinceCleanup = 0;
public constructor(config: IWorldManagerConfig = {}) {
this._config = {
maxWorlds: 50,
autoCleanup: true,
cleanupFrameInterval: 1800, // 1800帧
debug: false,
...config
};
// 默认启动运行状态
this._isRunning = true;
constructor(config: IWorldManagerConfig = {}) {
this._config = { ...DEFAULT_CONFIG, ...config };
logger.info('WorldManager已初始化', {
maxWorlds: this._config.maxWorlds,
@@ -88,27 +93,57 @@ export class WorldManager implements IService {
}
/**
* 创建新World
* @zh 获取World总数
* @en Get total world count
*/
public get worldCount(): number {
return this._worlds.size;
}
/**
* @zh 获取激活World数量
* @en Get active world count
*/
public get activeWorldCount(): number {
let count = 0;
this._worlds.forEach(world => {
if (world.isActive) count++;
});
return count;
}
/**
* @zh 检查是否正在运行
* @en Check if running
*/
public get isRunning(): boolean {
return this._isRunning;
}
/**
* @zh 获取配置
* @en Get configuration
*/
public get config(): IWorldManagerConfig {
return { ...this._config };
}
/**
* @zh 创建新World
* @en Create new World
*
* @param worldName - @zh World名称 @en World name
* @param config - @zh World配置 @en World configuration
* @returns @zh 创建的World实例 @en Created World instance
* @throws @zh 名称为空、重复或超出限制时抛出错误 @en Throws if name is empty, duplicate, or limit exceeded
*/
public createWorld(worldName: string, config?: IWorldConfig): World {
if (!worldName || typeof worldName !== 'string' || worldName.trim() === '') {
throw new Error('World name不能为空');
}
this.validateWorldName(worldName);
if (this._worlds.has(worldName)) {
throw new Error(`World name '${worldName}' 已存在`);
}
if (this._worlds.size >= this._config.maxWorlds!) {
throw new Error(`已达到最大World数量限制: ${this._config.maxWorlds}`);
}
// 优先级config.debug > WorldManager.debug > 默认
const worldConfig: IWorldConfig = {
...config,
name: worldName,
debug: config?.debug ?? this._config.debug ?? false,
...(config?.maxScenes !== undefined && { maxScenes: config.maxScenes }),
...(config?.autoCleanup !== undefined && { autoCleanup: config.autoCleanup })
debug: config?.debug ?? this._config.debug
};
const world = new World(worldConfig);
@@ -118,45 +153,56 @@ export class WorldManager implements IService {
}
/**
* 移除World
* @zh 移除World
* @en Remove World
*
* @param worldName - @zh World名称 @en World name
* @returns @zh 是否成功移除 @en Whether removal was successful
*/
public removeWorld(worldName: string): boolean {
const world = this._worlds.get(worldName);
if (!world) {
return false;
}
if (!world) return false;
// 销毁World
world.destroy();
this._worlds.delete(worldName);
logger.info(`移除World: ${worldName}`);
return true;
}
/**
* 获取World
* @zh 获取World
* @en Get World
*
* @param worldName - @zh World名称 @en World name
* @returns @zh World实例或null @en World instance or null
*/
public getWorld(worldName: string): World | null {
return this._worlds.get(worldName) || null;
return this._worlds.get(worldName) ?? null;
}
/**
* 获取所有World ID
* @zh 获取所有World ID
* @en Get all World IDs
*/
public getWorldIds(): string[] {
return Array.from(this._worlds.keys());
}
/**
* 获取所有World
* @zh 获取所有World
* @en Get all Worlds
*/
public getAllWorlds(): World[] {
return Array.from(this._worlds.values());
}
/**
* 设置World激活状态
* @zh 设置World激活状态
* @en Set World active state
*
* @param worldName - @zh World名称 @en World name
* @param active - @zh 是否激活 @en Whether to activate
*/
public setWorldActive(worldName: string, active: boolean): void {
const world = this._worlds.get(worldName);
@@ -175,204 +221,84 @@ export class WorldManager implements IService {
}
/**
* 检查World是否激活
* @zh 检查World是否激活
* @en Check if World is active
*/
public isWorldActive(worldName: string): boolean {
const world = this._worlds.get(worldName);
return world?.isActive ?? false;
return this._worlds.get(worldName)?.isActive ?? false;
}
/**
* 更新所有活跃的World
*
* 应该在每帧的游戏循环中调用。
* 会自动更新所有活跃World的全局系统和场景。
*
* @example
* ```typescript
* function gameLoop(deltaTime: number) {
* Core.update(deltaTime); // 更新全局服务
* worldManager.updateAll(); // 更新所有World
* }
* ```
*/
public updateAll(): void {
if (!this._isRunning) return;
for (const world of this._worlds.values()) {
if (world.isActive) {
// 更新World的全局System
world.updateGlobalSystems();
// 更新World中的所有Scene
world.updateScenes();
}
}
// 基于帧的自动清理
if (this._config.autoCleanup) {
this._framesSinceCleanup++;
if (this._framesSinceCleanup >= this._config.cleanupFrameInterval) {
this.cleanup();
this._framesSinceCleanup = 0; // 重置计数器
if (this._config.debug) {
logger.debug(`执行定期清理World (间隔: ${this._config.cleanupFrameInterval} 帧)`);
}
}
}
}
/**
* 获取所有激活的World
* @zh 获取所有激活的World
* @en Get all active Worlds
*/
public getActiveWorlds(): World[] {
const activeWorlds: World[] = [];
for (const world of this._worlds.values()) {
if (world.isActive) {
activeWorlds.push(world);
}
}
return activeWorlds;
const result: World[] = [];
this._worlds.forEach(world => {
if (world.isActive) result.push(world);
});
return result;
}
/**
* 启动所有World
* @zh 查找满足条件的World
* @en Find Worlds matching predicate
*
* @param predicate - @zh 过滤条件 @en Filter predicate
*/
public findWorlds(predicate: (world: World) => boolean): World[] {
const result: World[] = [];
this._worlds.forEach(world => {
if (predicate(world)) result.push(world);
});
return result;
}
/**
* @zh 根据名称查找World
* @en Find World by name
*
* @param name - @zh World名称 @en World name
*/
public findWorldByName(name: string): World | null {
let found: World | null = null;
this._worlds.forEach(world => {
if (world.name === name) found = world;
});
return found;
}
/**
* @zh 启动所有World
* @en Start all Worlds
*/
public startAll(): void {
this._isRunning = true;
for (const world of this._worlds.values()) {
world.start();
}
this._worlds.forEach(world => world.start());
logger.info('启动所有World');
}
/**
* 停止所有World
* @zh 停止所有World
* @en Stop all Worlds
*/
public stopAll(): void {
this._isRunning = false;
for (const world of this._worlds.values()) {
world.stop();
}
this._worlds.forEach(world => world.stop());
logger.info('停止所有World');
}
/**
* 查找满足条件的World
*/
public findWorlds(predicate: (world: World) => boolean): World[] {
const results: World[] = [];
for (const world of this._worlds.values()) {
if (predicate(world)) {
results.push(world);
}
}
return results;
}
/**
* 根据名称查找World
*/
public findWorldByName(name: string): World | null {
for (const world of this._worlds.values()) {
if (world.name === name) {
return world;
}
}
return null;
}
/**
* 获取WorldManager统计信息
*/
public getStats() {
const stats = {
totalWorlds: this._worlds.size,
activeWorlds: this.activeWorldCount,
totalScenes: 0,
totalEntities: 0,
totalSystems: 0,
memoryUsage: 0,
isRunning: this._isRunning,
config: { ...this._config },
worlds: [] as any[]
};
for (const [worldName, world] of this._worlds) {
const worldStats = world.getStats();
stats.totalScenes += worldStats.totalSystems; // World的getStats可能需要调整
stats.totalEntities += worldStats.totalEntities;
stats.totalSystems += worldStats.totalSystems;
stats.worlds.push({
id: worldName,
name: world.name,
isActive: world.isActive,
sceneCount: world.sceneCount,
...worldStats
});
}
return stats;
}
/**
* 获取详细状态信息
*/
public getDetailedStatus() {
return {
...this.getStats(),
worlds: Array.from(this._worlds.entries()).map(([worldName, world]) => ({
id: worldName,
isActive: world.isActive,
status: world.getStatus()
}))
};
}
/**
* 清理空World
*/
public cleanup(): number {
const worldsToRemove: string[] = [];
for (const [worldName, world] of this._worlds) {
if (this.shouldCleanupWorld(world)) {
worldsToRemove.push(worldName);
}
}
for (const worldName of worldsToRemove) {
this.removeWorld(worldName);
}
if (worldsToRemove.length > 0) {
logger.debug(`清理了 ${worldsToRemove.length} 个World`);
}
return worldsToRemove.length;
}
/**
* 销毁WorldManager
* @zh 销毁WorldManager
* @en Destroy WorldManager
*/
public destroy(): void {
logger.info('正在销毁WorldManager...');
// 停止所有World
this.stopAll();
// 销毁所有World
const worldNames = Array.from(this._worlds.keys());
for (const worldName of worldNames) {
this.removeWorld(worldName);
}
worldNames.forEach(name => this.removeWorld(name));
this._worlds.clear();
this._isRunning = false;
@@ -381,67 +307,178 @@ export class WorldManager implements IService {
}
/**
* 实现 IService 接口的 dispose 方法
* 调用 destroy 方法进行清理
* @zh 实现 IService 接口的 dispose 方法
* @en Implement IService dispose method
*/
public dispose(): void {
this.destroy();
}
/**
* 判断World是否应该被清理
* 清理策略:
* 1. World未激活
* 2. 没有Scene或所有Scene都是空的
* 3. 创建时间超过10分钟
* @zh 更新所有活跃的World
* @en Update all active Worlds
*
* @zh 应该在每帧的游戏循环中调用
* @en Should be called in each frame of game loop
*/
private shouldCleanupWorld(world: World): boolean {
if (world.isActive) {
return false;
public updateAll(): void {
if (!this._isRunning) return;
this._worlds.forEach(world => {
if (world.isActive) {
world.updateGlobalSystems();
world.updateScenes();
}
});
this.processAutoCleanup();
}
/**
* @zh 获取WorldManager统计信息
* @en Get WorldManager statistics
*/
public getStats() {
let totalScenes = 0;
let totalEntities = 0;
let totalSystems = 0;
const worldsList: Array<{
id: string;
name: string;
isActive: boolean;
sceneCount: number;
totalEntities: number;
totalSystems: number;
}> = [];
this._worlds.forEach((world, worldName) => {
const worldStats = world.getStats();
totalScenes += world.sceneCount;
totalEntities += worldStats.totalEntities;
totalSystems += worldStats.totalSystems;
worldsList.push({
id: worldName,
name: world.name,
isActive: world.isActive,
sceneCount: world.sceneCount,
...worldStats
});
});
return {
totalWorlds: this._worlds.size,
activeWorlds: this.activeWorldCount,
totalScenes,
totalEntities,
totalSystems,
memoryUsage: 0,
isRunning: this._isRunning,
config: { ...this._config },
worlds: worldsList
};
}
/**
* @zh 获取详细状态信息
* @en Get detailed status information
*/
public getDetailedStatus() {
const worlds: Array<{
id: string;
isActive: boolean;
status: ReturnType<World['getStatus']>;
}> = [];
this._worlds.forEach((world, worldName) => {
worlds.push({
id: worldName,
isActive: world.isActive,
status: world.getStatus()
});
});
return { ...this.getStats(), worlds };
}
/**
* @zh 清理空World
* @en Cleanup empty Worlds
*
* @returns @zh 清理的World数量 @en Number of cleaned up Worlds
*/
public cleanup(): number {
const toRemove: string[] = [];
this._worlds.forEach((world, worldName) => {
if (this.isCleanupCandidate(world)) {
toRemove.push(worldName);
}
});
toRemove.forEach(name => this.removeWorld(name));
if (toRemove.length > 0) {
logger.debug(`清理了 ${toRemove.length} 个World`);
}
return toRemove.length;
}
/**
* @zh 验证World名称
* @en Validate World name
*/
private validateWorldName(worldName: string): void {
if (!worldName?.trim()) {
throw new Error('World name不能为空');
}
if (this._worlds.has(worldName)) {
throw new Error(`World name '${worldName}' 已存在`);
}
if (this._worlds.size >= this._config.maxWorlds) {
throw new Error(`已达到最大World数量限制: ${this._config.maxWorlds}`);
}
}
/**
* @zh 处理自动清理
* @en Process auto cleanup
*/
private processAutoCleanup(): void {
if (!this._config.autoCleanup) return;
this._framesSinceCleanup++;
if (this._framesSinceCleanup >= this._config.cleanupFrameInterval) {
this.cleanup();
this._framesSinceCleanup = 0;
if (this._config.debug) {
logger.debug(`执行定期清理World (间隔: ${this._config.cleanupFrameInterval} 帧)`);
}
}
}
/**
* @zh 判断World是否应该被清理
* @en Check if World should be cleaned up
*
* @zh 清理策略:未激活 + (无Scene或全空Scene) + 创建超过10分钟
* @en Cleanup policy: inactive + (no scenes or all empty) + created over 10 minutes ago
*/
private isCleanupCandidate(world: World): boolean {
if (world.isActive) return false;
const age = Date.now() - world.createdAt;
const isOldEnough = age > 10 * 60 * 1000; // 10分钟
if (age <= CLEANUP_THRESHOLD_MS) return false;
if (world.sceneCount === 0) {
return isOldEnough;
}
if (world.sceneCount === 0) return true;
// 检查是否所有Scene都是空的
const allScenes = world.getAllScenes();
const hasEntities = allScenes.some((scene) => scene.entities && scene.entities.count > 0);
return !hasEntities && isOldEnough;
}
const hasEntities = world.getAllScenes().some(
scene => scene.entities && scene.entities.count > 0
);
/**
* 获取World总数
*/
public get worldCount(): number {
return this._worlds.size;
}
/**
* 获取激活World数量
*/
public get activeWorldCount(): number {
let count = 0;
for (const world of this._worlds.values()) {
if (world.isActive) count++;
}
return count;
}
/**
* 检查是否正在运行
*/
public get isRunning(): boolean {
return this._isRunning;
}
/**
* 获取配置
*/
public get config(): IWorldManagerConfig {
return { ...this._config };
return !hasEntities;
}
}

View File

@@ -1,5 +1,21 @@
# @esengine/fsm
## 1.0.3
### Patch Changes
- Updated dependencies [[`7d74623`](https://github.com/esengine/esengine/commit/7d746237100084ac3456f1af92ff664db4e50cc8)]:
- @esengine/ecs-framework@2.4.4
- @esengine/blueprint@1.0.2
## 1.0.2
### Patch Changes
- Updated dependencies [[`ce2db4e`](https://github.com/esengine/esengine/commit/ce2db4e48a7cdac44265420ef16e83f6424f4dea)]:
- @esengine/ecs-framework@2.4.3
- @esengine/blueprint@1.0.1
## 1.0.1
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@esengine/fsm",
"version": "1.0.1",
"version": "1.0.3",
"description": "Finite State Machine for ECS Framework / ECS 框架的有限状态机",
"type": "module",
"main": "./dist/index.js",

View File

@@ -1,5 +1,21 @@
# @esengine/pathfinding
## 1.0.3
### Patch Changes
- Updated dependencies [[`7d74623`](https://github.com/esengine/esengine/commit/7d746237100084ac3456f1af92ff664db4e50cc8)]:
- @esengine/ecs-framework@2.4.4
- @esengine/blueprint@1.0.2
## 1.0.2
### Patch Changes
- Updated dependencies [[`ce2db4e`](https://github.com/esengine/esengine/commit/ce2db4e48a7cdac44265420ef16e83f6424f4dea)]:
- @esengine/ecs-framework@2.4.3
- @esengine/blueprint@1.0.1
## 1.0.1
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@esengine/pathfinding",
"version": "1.0.1",
"version": "1.0.3",
"description": "寻路系统 | Pathfinding System - A*, Grid, NavMesh",
"type": "module",
"main": "./dist/index.js",

View File

@@ -1,5 +1,21 @@
# @esengine/procgen
## 1.0.3
### Patch Changes
- Updated dependencies [[`7d74623`](https://github.com/esengine/esengine/commit/7d746237100084ac3456f1af92ff664db4e50cc8)]:
- @esengine/ecs-framework@2.4.4
- @esengine/blueprint@1.0.2
## 1.0.2
### Patch Changes
- Updated dependencies [[`ce2db4e`](https://github.com/esengine/esengine/commit/ce2db4e48a7cdac44265420ef16e83f6424f4dea)]:
- @esengine/ecs-framework@2.4.3
- @esengine/blueprint@1.0.1
## 1.0.1
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@esengine/procgen",
"version": "1.0.1",
"version": "1.0.3",
"description": "Procedural generation tools for ECS Framework / ECS 框架的程序化生成工具",
"type": "module",
"main": "./dist/index.js",

View File

@@ -1,5 +1,29 @@
# @esengine/spatial
## 1.0.4
### Patch Changes
- Updated dependencies [[`7d74623`](https://github.com/esengine/esengine/commit/7d746237100084ac3456f1af92ff664db4e50cc8)]:
- @esengine/ecs-framework@2.4.4
- @esengine/blueprint@1.0.2
## 1.0.3
### Patch Changes
- Updated dependencies [[`ce2db4e`](https://github.com/esengine/esengine/commit/ce2db4e48a7cdac44265420ef16e83f6424f4dea)]:
- @esengine/ecs-framework@2.4.3
- @esengine/blueprint@1.0.1
## 1.0.2
### Patch Changes
- [`d66c180`](https://github.com/esengine/esengine/commit/d66c18041ebffa67b4dd12a026075e22dc1f5d36) Thanks [@esengine](https://github.com/esengine)! - fix(spatial): 修复 GridAOI 可见性更新问题
- 修复 `addObserver` 时现有观察者无法检测到新实体的问题
- 修复实体远距离移动时观察者可见性未正确更新的问题
## 1.0.1
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@esengine/spatial",
"version": "1.0.1",
"version": "1.0.4",
"description": "Spatial query and indexing system for ECS Framework / ECS 框架的空间查询和索引系统",
"type": "module",
"main": "./dist/index.js",

View File

@@ -107,8 +107,13 @@ export class GridAOI<T> implements IAOIManager<T> {
this._observers.set(entity, data);
this._addToCell(cellKey, data);
// Initial visibility check
// Initial visibility check for this observer
this._updateVisibility(data);
// Notify other observers about this new entity
if (data.observable) {
this._updateObserversOfEntity(data);
}
}
/**
@@ -398,40 +403,32 @@ export class GridAOI<T> implements IAOIManager<T> {
* @en Update other observers' visibility of an entity
*/
private _updateObserversOfEntity(movedData: AOIObserverData<T>): void {
const cellRadius = Math.ceil(this._getMaxViewRange() / this._cellSize) + 1;
const centerCell = this._getCellCoords(movedData.position);
// Check all observers for visibility changes
// This handles both: observers who can now see the entity (enter)
// and observers who could see it before but can't anymore (exit)
for (const [, otherData] of this._observers) {
if (otherData === movedData) continue;
for (let dx = -cellRadius; dx <= cellRadius; dx++) {
for (let dy = -cellRadius; dy <= cellRadius; dy++) {
const cellKey = `${centerCell.x + dx},${centerCell.y + dy}`;
const cell = this._cells.get(cellKey);
if (!cell) continue;
const distSq = distanceSquared(otherData.position, movedData.position);
const wasVisible = otherData.visibleEntities.has(movedData.entity);
const isVisible = distSq <= otherData.viewRangeSq;
for (const otherData of cell) {
if (otherData === movedData) continue;
const distSq = distanceSquared(otherData.position, movedData.position);
const wasVisible = otherData.visibleEntities.has(movedData.entity);
const isVisible = distSq <= otherData.viewRangeSq;
if (isVisible && !wasVisible) {
otherData.visibleEntities.add(movedData.entity);
this._emitEvent({
type: 'enter',
observer: otherData.entity,
target: movedData.entity,
position: movedData.position
}, otherData);
} else if (!isVisible && wasVisible) {
otherData.visibleEntities.delete(movedData.entity);
this._emitEvent({
type: 'exit',
observer: otherData.entity,
target: movedData.entity,
position: movedData.position
}, otherData);
}
}
if (isVisible && !wasVisible) {
otherData.visibleEntities.add(movedData.entity);
this._emitEvent({
type: 'enter',
observer: otherData.entity,
target: movedData.entity,
position: movedData.position
}, otherData);
} else if (!isVisible && wasVisible) {
otherData.visibleEntities.delete(movedData.entity);
this._emitEvent({
type: 'exit',
observer: otherData.entity,
target: movedData.entity,
position: movedData.position
}, otherData);
}
}
}

View File

@@ -1,5 +1,21 @@
# @esengine/timer
## 1.0.3
### Patch Changes
- Updated dependencies [[`7d74623`](https://github.com/esengine/esengine/commit/7d746237100084ac3456f1af92ff664db4e50cc8)]:
- @esengine/ecs-framework@2.4.4
- @esengine/blueprint@1.0.2
## 1.0.2
### Patch Changes
- Updated dependencies [[`ce2db4e`](https://github.com/esengine/esengine/commit/ce2db4e48a7cdac44265420ef16e83f6424f4dea)]:
- @esengine/ecs-framework@2.4.3
- @esengine/blueprint@1.0.1
## 1.0.1
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@esengine/timer",
"version": "1.0.1",
"version": "1.0.3",
"description": "Timer and cooldown system for ECS Framework / ECS 框架的定时器和冷却系统",
"type": "module",
"main": "./dist/index.js",

View File

@@ -0,0 +1,17 @@
# @esengine/network-server
## 1.0.2
### Patch Changes
- [#354](https://github.com/esengine/esengine/pull/354) [`1e240e8`](https://github.com/esengine/esengine/commit/1e240e86f2f75672c3609c9d86238a9ec08ebb4e) Thanks [@esengine](https://github.com/esengine)! - feat(cli): 增强 Node.js 服务端适配器
**@esengine/cli:**
- 添加 @esengine/network-server 依赖支持
- 生成完整的 ECS 游戏服务器项目结构
- 组件使用 @ECSComponent 装饰器注册
- tsconfig 启用 experimentalDecorators
**@esengine/network-server:**
- 支持 ESM/CJS 双格式导出
- 添加 ws@8.18.0 解决 Node.js 24 兼容性问题

View File

@@ -1,15 +1,21 @@
{
"name": "@esengine/network-server",
"version": "1.0.0",
"version": "1.0.2",
"description": "TSRPC-based network server for ESEngine",
"type": "module",
"main": "dist/index.js",
"main": "dist/index.cjs",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
}
},
"files": [
@@ -22,7 +28,8 @@
},
"dependencies": {
"@esengine/network-protocols": "workspace:*",
"tsrpc": "^3.4.15"
"tsrpc": "^3.4.15",
"ws": "^8.18.0"
},
"devDependencies": {
"tsup": "^8.5.1",

View File

@@ -2,7 +2,7 @@ import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts', 'src/main.ts'],
format: ['esm'],
format: ['esm', 'cjs'],
dts: true,
sourcemap: true,
clean: true,

View File

@@ -1,5 +1,50 @@
# @esengine/cli
## 1.5.0
### Minor Changes
- [#359](https://github.com/esengine/esengine/pull/359) [`aed91db`](https://github.com/esengine/esengine/commit/aed91dbe4507459f8ac0563ee933e44fd4c9fea6) Thanks [@esengine](https://github.com/esengine)! - feat(cli): 添加 update 命令用于更新 ESEngine 包
- 新增 `esengine update` 命令检查并更新 @esengine/\* 包到最新版本
- 支持 `--check` 参数仅检查可用更新而不安装
- 支持 `--yes` 参数跳过确认提示
- 显示包更新状态,对比当前版本与最新版本
- 更新时保留版本前缀(^ 或 ~
## 1.3.0
### Minor Changes
- [#354](https://github.com/esengine/esengine/pull/354) [`1e240e8`](https://github.com/esengine/esengine/commit/1e240e86f2f75672c3609c9d86238a9ec08ebb4e) Thanks [@esengine](https://github.com/esengine)! - feat(cli): 增强 Node.js 服务端适配器
**@esengine/cli:**
- 添加 @esengine/network-server 依赖支持
- 生成完整的 ECS 游戏服务器项目结构
- 组件使用 @ECSComponent 装饰器注册
- tsconfig 启用 experimentalDecorators
**@esengine/network-server:**
- 支持 ESM/CJS 双格式导出
- 添加 ws@8.18.0 解决 Node.js 24 兼容性问题
## 1.2.1
### Patch Changes
- [#352](https://github.com/esengine/esengine/pull/352) [`33e98b9`](https://github.com/esengine/esengine/commit/33e98b9a750f9fe684c36f1937c1afa38da36315) Thanks [@esengine](https://github.com/esengine)! - fix(cli): 修复 Cocos Creator 3.x 项目检测逻辑
- 优先检查 package.json 中的 creator.version 字段
- 添加 .creator 和 settings 目录检测
- 重构检测代码,提取通用辅助函数
## 1.2.0
### Minor Changes
- [`d66c180`](https://github.com/esengine/esengine/commit/d66c18041ebffa67b4dd12a026075e22dc1f5d36) Thanks [@esengine](https://github.com/esengine)! - feat(cli): 添加模块管理命令
- 新增 `list` 命令:按分类显示可用模块
- 新增 `add [modules...]` 命令:添加模块到项目,支持交互式选择
- 新增 `remove [modules...]` 命令:从项目移除模块,支持确认提示
## 1.1.0
### Minor Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@esengine/cli",
"version": "1.1.0",
"version": "1.5.0",
"description": "CLI tool for adding ESEngine ECS framework to existing projects",
"type": "module",
"main": "dist/index.js",

View File

@@ -7,11 +7,12 @@ import type { FileEntry, PlatformAdapter, ProjectConfig } from './types.js';
export const nodejsAdapter: PlatformAdapter = {
id: 'nodejs',
name: 'Node.js',
description: 'Generate standalone Node.js project with ECS (for servers, CLI tools, simulations)',
description: 'Generate Node.js game server with ECS and networking',
getDependencies() {
return {
'@esengine/ecs-framework': 'latest'
'@esengine/ecs-framework': 'latest',
'@esengine/network-server': 'latest'
};
},
@@ -33,147 +34,96 @@ export const nodejsAdapter: PlatformAdapter = {
},
generateFiles(config: ProjectConfig): FileEntry[] {
const files: FileEntry[] = [];
files.push({
path: 'src/index.ts',
content: generateIndex(config)
});
files.push({
path: 'src/Game.ts',
content: generateGame(config)
});
files.push({
path: 'src/components/PositionComponent.ts',
content: generatePositionComponent()
});
files.push({
path: 'src/systems/MovementSystem.ts',
content: generateMovementSystem()
});
files.push({
path: 'tsconfig.json',
content: generateTsConfig()
});
files.push({
path: 'README.md',
content: generateReadme(config)
});
return files;
return [
{ path: 'src/index.ts', content: generateIndex(config) },
{ path: 'src/server/GameServer.ts', content: generateGameServer(config) },
{ path: 'src/game/Game.ts', content: generateGame(config) },
{ path: 'src/game/scenes/MainScene.ts', content: generateMainScene(config) },
{ path: 'src/game/components/PositionComponent.ts', content: generatePositionComponent() },
{ path: 'src/game/components/VelocityComponent.ts', content: generateVelocityComponent() },
{ path: 'src/game/systems/MovementSystem.ts', content: generateMovementSystem() },
{ path: 'tsconfig.json', content: generateTsConfig() },
{ path: 'README.md', content: generateReadme(config) }
];
}
};
function generateIndex(config: ProjectConfig): string {
return `import { Game } from './Game.js';
return `import { createGameServer } from './server/GameServer';
const game = new Game();
const PORT = Number(process.env.PORT) || 3000;
async function main() {
const server = createGameServer({ port: PORT });
await server.start();
console.log('========================================');
console.log(' ${config.name} Server');
console.log('========================================');
console.log(\` WebSocket: ws://localhost:\${PORT}\`);
console.log(' Press Ctrl+C to stop');
console.log('========================================');
}
// Handle graceful shutdown
process.on('SIGINT', () => {
console.log('\\nShutting down...');
game.stop();
process.exit(0);
});
process.on('SIGTERM', () => {
game.stop();
process.exit(0);
});
main().catch(console.error);
`;
}
// Start the game
game.start();
function generateGameServer(config: ProjectConfig): string {
return `import { GameServer, type IServerConfig } from '@esengine/network-server';
import { Game } from '../game/Game';
console.log('[${config.name}] Game started. Press Ctrl+C to stop.');
/**
* @zh 创建游戏服务器
* @en Create game server
*/
export function createGameServer(config: Partial<IServerConfig> = {}): GameServer {
const server = new GameServer({
port: config.port ?? 3000,
roomConfig: {
maxPlayers: 16,
tickRate: 20,
...config.roomConfig
}
});
// 初始化 ECS 游戏逻辑
const game = new Game();
game.start();
return server;
}
`;
}
function generateGame(config: ProjectConfig): string {
return `import { Core, Scene, type ICoreConfig } from '@esengine/ecs-framework';
import { MovementSystem } from './systems/MovementSystem.js';
return `import { Core, type ICoreConfig } from '@esengine/ecs-framework';
import { MainScene } from './scenes/MainScene';
/**
* Game configuration options
*/
export interface GameOptions {
/** @zh 调试模式 @en Debug mode */
debug?: boolean;
/** @zh 目标帧率 @en Target FPS */
targetFPS?: number;
/** @zh 远程调试配置 @en Remote debug configuration */
remoteDebug?: {
/** @zh 启用远程调试 @en Enable remote debugging */
enabled: boolean;
/** @zh WebSocket地址 @en WebSocket URL */
url: string;
/** @zh 自动重连 @en Auto reconnect */
autoReconnect?: boolean;
};
}
/**
* Game Scene - Define your game systems here
*/
class GameScene extends Scene {
initialize(): void {
this.name = '${config.name}';
this.addSystem(new MovementSystem());
// Add more systems here...
}
onStart(): void {
// Create your initial entities here
}
}
/**
* Main game class with ECS game loop
*
* Features:
* - Configurable debug mode and FPS
* - Remote debugging via WebSocket
* - Fixed timestep game loop
* - Graceful start/stop
* @zh 游戏主类
* @en Main game class
*/
export class Game {
private readonly _scene: GameScene;
private readonly _targetFPS: number;
private _scene: MainScene;
private _running = false;
private _tickInterval: ReturnType<typeof setInterval> | null = null;
private _lastTime = 0;
private _targetFPS = 60;
get scene() { return this._scene; }
get running() { return this._running; }
constructor(options: GameOptions = {}) {
const { debug = false, targetFPS = 60, remoteDebug } = options;
constructor(options: { debug?: boolean; targetFPS?: number } = {}) {
const { debug = false, targetFPS = 60 } = options;
this._targetFPS = targetFPS;
const config: ICoreConfig = { debug };
// 配置远程调试
if (remoteDebug?.enabled && remoteDebug.url) {
config.debugConfig = {
enabled: true,
websocketUrl: remoteDebug.url,
autoReconnect: remoteDebug.autoReconnect ?? true,
channels: {
entities: true,
systems: true,
performance: true,
components: true,
scenes: true
}
};
}
Core.create(config);
this._scene = new GameScene();
this._scene = new MainScene();
Core.setScene(this._scene);
}
@@ -203,11 +153,38 @@ export class Game {
`;
}
function generateMainScene(config: ProjectConfig): string {
return `import { Scene } from '@esengine/ecs-framework';
import { MovementSystem } from '../systems/MovementSystem';
/**
* @zh 主场景
* @en Main scene
*/
export class MainScene extends Scene {
initialize(): void {
this.name = '${config.name}';
// 注册系统
this.addSystem(new MovementSystem());
// 添加更多系统...
}
onStart(): void {
// 创建初始实体
console.log('[MainScene] Scene started');
}
}
`;
}
function generatePositionComponent(): string {
return `import { Component, ECSComponent } from '@esengine/ecs-framework';
/**
* Position component - stores entity position
* @zh 位置组件
* @en Position component
*/
@ECSComponent('Position')
export class PositionComponent extends Component {
@@ -219,31 +196,61 @@ export class PositionComponent extends Component {
this.x = x;
this.y = y;
}
reset(): void {
this.x = 0;
this.y = 0;
}
}
`;
}
function generateVelocityComponent(): string {
return `import { Component, ECSComponent } from '@esengine/ecs-framework';
/**
* @zh 速度组件
* @en Velocity component
*/
@ECSComponent('Velocity')
export class VelocityComponent extends Component {
vx = 0;
vy = 0;
constructor(vx = 0, vy = 0) {
super();
this.vx = vx;
this.vy = vy;
}
reset(): void {
this.vx = 0;
this.vy = 0;
}
}
`;
}
function generateMovementSystem(): string {
return `import { EntitySystem, Matcher, Entity, Time, ECSSystem } from '@esengine/ecs-framework';
import { PositionComponent } from '../components/PositionComponent.js';
return `import { EntitySystem, Matcher, Entity, Time } from '@esengine/ecs-framework';
import { PositionComponent } from '../components/PositionComponent';
import { VelocityComponent } from '../components/VelocityComponent';
/**
* Movement system - processes entities with PositionComponent
*
* Customize this system for your game logic.
* @zh 移动系统
* @en Movement system
*/
@ECSSystem('MovementSystem')
export class MovementSystem extends EntitySystem {
constructor() {
super(Matcher.empty().all(PositionComponent));
super(Matcher.empty().all(PositionComponent, VelocityComponent));
}
protected process(entities: readonly Entity[]): void {
for (const entity of entities) {
const position = entity.getComponent(PositionComponent)!;
// Update position using Time.deltaTime
// position.x += velocity.dx * Time.deltaTime;
}
protected processEntity(entity: Entity, dt: number): void {
const pos = entity.getComponent(PositionComponent)!;
const vel = entity.getComponent(VelocityComponent)!;
pos.x += vel.vx * dt;
pos.y += vel.vy * dt;
}
}
`;
@@ -253,8 +260,8 @@ function generateTsConfig(): string {
return `{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"module": "CommonJS",
"moduleResolution": "Node",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
@@ -263,7 +270,9 @@ function generateTsConfig(): string {
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"sourceMap": true
"sourceMap": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
@@ -274,75 +283,54 @@ function generateTsConfig(): string {
function generateReadme(config: ProjectConfig): string {
return `# ${config.name}
A Node.js project using ESEngine ECS framework.
Node.js 游戏服务器,基于 ESEngine ECS 框架。
## Quick Start
## 快速开始
\`\`\`bash
# Install dependencies
# 安装依赖
npm install
# Run in development mode (with hot reload)
# 开发模式(热重载)
npm run dev
# Build and run
# 构建并运行
npm run build:start
\`\`\`
## Project Structure
## 项目结构
\`\`\`
src/
├── index.ts # Entry point
├── Game.ts # Game loop and ECS setup
├── components/ # ECS components (data)
│ └── PositionComponent.ts
└── systems/ # ECS systems (logic)
── MovementSystem.ts
├── index.ts # 入口文件
├── server/
│ └── GameServer.ts # 网络服务器配置
└── game/
├── Game.ts # ECS 游戏主类
── scenes/
│ └── MainScene.ts # 主场景
├── components/ # ECS 组件
│ ├── PositionComponent.ts
│ └── VelocityComponent.ts
└── systems/ # ECS 系统
└── MovementSystem.ts
\`\`\`
## Creating Components
## 客户端连接
\`\`\`typescript
import { Component } from '@esengine/ecs-framework';
import { NetworkPlugin } from '@esengine/network';
export class HealthComponent extends Component {
current = 100;
max = 100;
const networkPlugin = new NetworkPlugin({
serverUrl: 'ws://localhost:3000'
});
reset(): void {
this.current = this.max;
}
}
await networkPlugin.connect('PlayerName');
\`\`\`
## Creating Systems
## 文档
\`\`\`typescript
import { EntitySystem, Matcher, Entity } from '@esengine/ecs-framework';
import { HealthComponent } from '../components/HealthComponent.js';
export class HealthSystem extends EntitySystem {
constructor() {
super(Matcher.all(HealthComponent));
}
protected processEntity(entity: Entity, dt: number): void {
const health = entity.getComponent(HealthComponent)!;
// Your logic here
}
}
\`\`\`
## Use Cases
- Game servers
- CLI tools with complex logic
- Simulations
- Automated testing
## Documentation
- [ESEngine ECS Framework](https://github.com/esengine/esengine)
- [ESEngine 文档](https://esengine.github.io/esengine/)
- [Network 模块](https://esengine.github.io/esengine/modules/network/)
`;
}

View File

@@ -8,8 +8,9 @@ import * as path from 'node:path';
import { execSync } from 'node:child_process';
import { getPlatformChoices, getPlatforms, getAdapter } from './adapters/index.js';
import type { PlatformType, ProjectConfig } from './adapters/types.js';
import { AVAILABLE_MODULES, getModuleById, getAllModuleIds, type ModuleInfo } from './modules.js';
const VERSION = '1.0.0';
const VERSION = '1.4.0';
/**
* @zh 打印 Logo
@@ -25,52 +26,68 @@ function printLogo(): void {
console.log();
}
// =============================================================================
// 项目检测 | Project Detection
// =============================================================================
/**
* @zh 检测是否存在 *.laya 文件
* @en Check if *.laya file exists
* @zh 检查文件或目录是否存在
* @en Check if file or directory exists
*/
function hasLayaProjectFile(cwd: string): boolean {
const exists = (cwd: string, ...paths: string[]): boolean =>
paths.some(p => fs.existsSync(path.join(cwd, p)));
/**
* @zh 安全读取 JSON 文件
* @en Safely read JSON file
*/
function readJson<T = Record<string, unknown>>(filePath: string): T | null {
try {
const files = fs.readdirSync(cwd);
return files.some(f => f.endsWith('.laya'));
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
} catch {
return null;
}
}
/**
* @zh 检查目录中是否有匹配后缀的文件
* @en Check if directory contains files with matching extension
*/
function hasFileWithExt(cwd: string, ext: string): boolean {
try {
return fs.readdirSync(cwd).some(f => f.endsWith(ext));
} catch {
return false;
}
}
/**
* @zh 检测 Cocos Creator 版本
* @en Detect Cocos Creator version
* @zh 从 package.json 获取 Cocos Creator 版本
* @en Get Cocos Creator version from package.json
*/
function detectCocosVersion(cwd: string): 'cocos' | 'cocos2' | null {
// Cocos 3.x: 检查 cc.config.json 或 extensions 目录
if (fs.existsSync(path.join(cwd, 'cc.config.json')) ||
fs.existsSync(path.join(cwd, 'extensions'))) {
return 'cocos';
}
function getCocosVersionFromPackage(cwd: string): string | null {
const pkg = readJson<{ creator?: { version?: string } }>(path.join(cwd, 'package.json'));
return pkg?.creator?.version ?? null;
}
// 检查 project.json 中的版本号
const projectJsonPath = path.join(cwd, 'project.json');
if (fs.existsSync(projectJsonPath)) {
try {
const project = JSON.parse(fs.readFileSync(projectJsonPath, 'utf-8'));
// Cocos 2.x project.json 有 engine-version 字段
if (project['engine-version'] || project.engine) {
const version = project['engine-version'] || project.engine || '';
// 2.x 版本格式: "cocos-creator-js-2.4.x" 或 "2.4.x"
if (version.includes('2.') || version.startsWith('2')) {
return 'cocos2';
}
}
// 有 project.json 但没有版本信息,假设是 3.x
return 'cocos';
} catch {
// 解析失败,假设是 3.x
return 'cocos';
}
}
/**
* @zh 从 project.json 获取 Cocos 2.x 版本
* @en Get Cocos 2.x version from project.json
*/
function getCocos2VersionFromProject(cwd: string): string | null {
const project = readJson<{ 'engine-version'?: string; engine?: string }>(
path.join(cwd, 'project.json')
);
return project?.['engine-version'] ?? project?.engine ?? null;
}
return null;
/**
* @zh 判断版本号属于哪个大版本
* @en Determine major version from version string
*/
function getMajorVersion(version: string): number | null {
const match = version.match(/^(\d+)\./);
return match ? parseInt(match[1], 10) : null;
}
/**
@@ -78,23 +95,35 @@ function detectCocosVersion(cwd: string): 'cocos' | 'cocos2' | null {
* @en Detect project type
*/
function detectProjectType(cwd: string): PlatformType | null {
// Laya: 检查 *.laya 文件.laya 目录laya.json
if (hasLayaProjectFile(cwd) ||
fs.existsSync(path.join(cwd, '.laya')) ||
fs.existsSync(path.join(cwd, 'laya.json'))) {
// Laya: *.laya 文件.laya 目录laya.json
if (hasFileWithExt(cwd, '.laya') || exists(cwd, '.laya', 'laya.json')) {
return 'laya';
}
// Cocos Creator: 检查 assets 目录
if (fs.existsSync(path.join(cwd, 'assets'))) {
const cocosVersion = detectCocosVersion(cwd);
if (cocosVersion) {
return cocosVersion;
}
// Cocos Creator: 检查 package.json 中的 creator.version
const cocosVersion = getCocosVersionFromPackage(cwd);
if (cocosVersion) {
const major = getMajorVersion(cocosVersion);
if (major === 2) return 'cocos2';
if (major && major >= 3) return 'cocos';
}
// Node.js: 检查 package.json
if (fs.existsSync(path.join(cwd, 'package.json'))) {
// Cocos 3.x: .creator 目录、settings 目录、cc.config.json、extensions 目录
if (exists(cwd, '.creator', 'settings', 'cc.config.json', 'extensions')) {
return 'cocos';
}
// Cocos 2.x: project.json 中的 engine-version
const cocos2Version = getCocos2VersionFromProject(cwd);
if (cocos2Version) {
if (cocos2Version.includes('2.') || cocos2Version.startsWith('2')) {
return 'cocos2';
}
return 'cocos';
}
// Node.js: 有 package.json 但不是 Cocos
if (exists(cwd, 'package.json')) {
return 'nodejs';
}
@@ -297,12 +326,451 @@ async function initCommand(options: { platform?: string }): Promise<void> {
console.log();
}
// =========================================================================
// Module Management Commands
// =========================================================================
/**
* @zh 列出可用模块
* @en List available modules
*/
function listCommand(options: { category?: string }): void {
printLogo();
console.log(chalk.bold(' Available Modules:\n'));
const categories = ['core', 'ai', 'utility', 'physics', 'rendering', 'network'] as const;
const categoryNames: Record<string, string> = {
core: '核心 | Core',
ai: 'AI',
utility: '工具 | Utility',
physics: '物理 | Physics',
rendering: '渲染 | Rendering',
network: '网络 | Network'
};
for (const category of categories) {
const modules = AVAILABLE_MODULES.filter(m => m.category === category);
if (modules.length === 0) continue;
if (options.category && options.category !== category) continue;
console.log(chalk.cyan(` ─── ${categoryNames[category]} ───`));
for (const mod of modules) {
console.log(` ${chalk.green(mod.id.padEnd(15))} ${chalk.gray(mod.package)}`);
console.log(` ${' '.repeat(15)} ${chalk.dim(mod.description)}`);
}
console.log();
}
console.log(chalk.gray(' Use `esengine add <module>` to add a module to your project.'));
console.log();
}
/**
* @zh 添加模块到项目
* @en Add module to project
*/
async function addCommand(moduleIds: string[], options: { yes?: boolean }): Promise<void> {
printLogo();
const cwd = process.cwd();
const packageJsonPath = path.join(cwd, 'package.json');
if (!fs.existsSync(packageJsonPath)) {
console.log(chalk.red(' ✗ No package.json found. Run `npm init` first.'));
process.exit(1);
}
// Validate modules
const validModules: ModuleInfo[] = [];
const invalidIds: string[] = [];
for (const id of moduleIds) {
const mod = getModuleById(id);
if (mod) {
validModules.push(mod);
} else {
invalidIds.push(id);
}
}
if (invalidIds.length > 0) {
console.log(chalk.red(` ✗ Unknown module(s): ${invalidIds.join(', ')}`));
console.log(chalk.gray(` Available: ${getAllModuleIds().join(', ')}`));
process.exit(1);
}
if (validModules.length === 0) {
// Interactive selection
const response = await prompts({
type: 'multiselect',
name: 'modules',
message: 'Select modules to add:',
choices: AVAILABLE_MODULES.map(m => ({
title: `${m.id} - ${m.description}`,
value: m.id,
selected: false
})),
min: 1
}, {
onCancel: () => {
console.log(chalk.yellow('\n Cancelled.'));
process.exit(0);
}
});
for (const id of response.modules) {
const mod = getModuleById(id);
if (mod) validModules.push(mod);
}
}
if (validModules.length === 0) {
console.log(chalk.yellow(' No modules selected.'));
return;
}
console.log(chalk.bold('\n Adding modules:\n'));
for (const mod of validModules) {
console.log(` ${chalk.green('+')} ${mod.package}`);
}
// Confirm
if (!options.yes) {
const confirm = await prompts({
type: 'confirm',
name: 'proceed',
message: 'Proceed with installation?',
initial: true
});
if (!confirm.proceed) {
console.log(chalk.yellow('\n Cancelled.'));
return;
}
}
// Install
console.log();
const deps: Record<string, string> = {};
for (const mod of validModules) {
deps[mod.package] = mod.version;
}
const success = installDependencies(cwd, deps);
if (success) {
console.log(chalk.bold('\n Done!'));
console.log(chalk.gray('\n Import modules in your code:'));
for (const mod of validModules) {
console.log(chalk.cyan(` import { ... } from '${mod.package}';`));
}
}
console.log();
}
/**
* @zh 从项目移除模块
* @en Remove module from project
*/
async function removeCommand(moduleIds: string[], options: { yes?: boolean }): Promise<void> {
printLogo();
const cwd = process.cwd();
const packageJsonPath = path.join(cwd, 'package.json');
if (!fs.existsSync(packageJsonPath)) {
console.log(chalk.red(' ✗ No package.json found.'));
process.exit(1);
}
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
const deps = pkg.dependencies || {};
// Find installed modules
const installed = AVAILABLE_MODULES.filter(m => deps[m.package]);
if (installed.length === 0) {
console.log(chalk.yellow(' No ESEngine modules installed.'));
return;
}
// Validate modules to remove
let toRemove: ModuleInfo[] = [];
if (moduleIds.length === 0) {
// Interactive selection
const response = await prompts({
type: 'multiselect',
name: 'modules',
message: 'Select modules to remove:',
choices: installed.map(m => ({
title: `${m.id} - ${m.package}`,
value: m.id
})),
min: 1
}, {
onCancel: () => {
console.log(chalk.yellow('\n Cancelled.'));
process.exit(0);
}
});
for (const id of response.modules) {
const mod = getModuleById(id);
if (mod) toRemove.push(mod);
}
} else {
for (const id of moduleIds) {
const mod = getModuleById(id);
if (mod && deps[mod.package]) {
toRemove.push(mod);
} else if (!mod) {
console.log(chalk.yellow(` ⚠ Unknown module: ${id}`));
} else {
console.log(chalk.yellow(` ⚠ Module not installed: ${id}`));
}
}
}
if (toRemove.length === 0) {
console.log(chalk.yellow(' No modules to remove.'));
return;
}
console.log(chalk.bold('\n Removing modules:\n'));
for (const mod of toRemove) {
console.log(` ${chalk.red('-')} ${mod.package}`);
}
// Confirm
if (!options.yes) {
const confirm = await prompts({
type: 'confirm',
name: 'proceed',
message: 'Proceed with removal?',
initial: true
});
if (!confirm.proceed) {
console.log(chalk.yellow('\n Cancelled.'));
return;
}
}
// Remove from package.json
for (const mod of toRemove) {
delete deps[mod.package];
}
pkg.dependencies = deps;
fs.writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2), 'utf-8');
// Run uninstall
const pm = detectPackageManager(cwd);
const packages = toRemove.map(m => m.package).join(' ');
const uninstallCmd = pm === 'pnpm'
? `pnpm remove ${packages}`
: pm === 'yarn'
? `yarn remove ${packages}`
: `npm uninstall ${packages}`;
console.log(chalk.gray(`\n Running ${uninstallCmd}...`));
try {
execSync(uninstallCmd, { cwd, stdio: 'inherit' });
console.log(chalk.bold('\n Done!'));
} catch {
console.log(chalk.yellow(`\n ⚠ Failed to run uninstall. Modules removed from package.json.`));
}
console.log();
}
/**
* @zh 获取 npm 包的最新版本
* @en Get latest version of npm package
*/
function getLatestVersion(packageName: string): string | null {
try {
const result = execSync(`npm view ${packageName} version`, {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe']
}).trim();
return result || null;
} catch {
return null;
}
}
/**
* @zh 比较版本号,返回是否有更新
* @en Compare versions, return true if newer version available
*/
function isNewerVersion(current: string, latest: string): boolean {
const cleanCurrent = current.replace(/^\^|~/, '');
// "latest" 标签视为需要更新(固定到具体版本)
if (cleanCurrent === 'latest' || cleanCurrent === '*') {
return true;
}
const currentParts = cleanCurrent.split('.').map(Number);
const latestParts = latest.split('.').map(Number);
for (let i = 0; i < 3; i++) {
const c = currentParts[i] || 0;
const l = latestParts[i] || 0;
if (l > c) return true;
if (l < c) return false;
}
return false;
}
/**
* @zh 更新项目中的 ESEngine 模块
* @en Update ESEngine modules in project
*/
async function updateCommand(moduleIds: string[], options: { yes?: boolean; check?: boolean }): Promise<void> {
printLogo();
const cwd = process.cwd();
const packageJsonPath = path.join(cwd, 'package.json');
if (!fs.existsSync(packageJsonPath)) {
console.log(chalk.red(' ✗ No package.json found.'));
process.exit(1);
}
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
const deps = pkg.dependencies || {};
// Find installed @esengine packages
const esenginePackages: { name: string; current: string; latest: string | null }[] = [];
console.log(chalk.gray(' Checking for updates...\n'));
for (const [name, version] of Object.entries(deps)) {
if (name.startsWith('@esengine/')) {
// If specific modules provided, filter
if (moduleIds.length > 0) {
const mod = getModuleById(moduleIds.find(id => {
const m = getModuleById(id);
return m?.package === name;
}) || '');
if (!mod || mod.package !== name) continue;
}
const latest = getLatestVersion(name);
esenginePackages.push({
name,
current: version as string,
latest
});
}
}
if (esenginePackages.length === 0) {
console.log(chalk.yellow(' No ESEngine packages found in dependencies.'));
return;
}
// Display update status
const updatable: { name: string; current: string; latest: string }[] = [];
console.log(chalk.bold(' Package Status:\n'));
for (const pkg of esenginePackages) {
const currentClean = pkg.current.replace(/^\^|~/, '');
const isLatestTag = currentClean === 'latest' || currentClean === '*';
if (pkg.latest === null) {
console.log(` ${chalk.gray(pkg.name)}`);
console.log(` ${chalk.red('✗')} Unable to fetch latest version`);
} else if (isNewerVersion(pkg.current, pkg.latest)) {
console.log(` ${chalk.cyan(pkg.name)}`);
if (isLatestTag) {
console.log(` ${chalk.yellow(currentClean)}${chalk.green(`^${pkg.latest}`)} ${chalk.gray('(pin version)')}`);
} else {
console.log(` ${chalk.yellow(currentClean)}${chalk.green(pkg.latest)}`);
}
updatable.push({ name: pkg.name, current: pkg.current, latest: pkg.latest });
} else {
console.log(` ${chalk.gray(pkg.name)}`);
console.log(` ${chalk.green('✓')} ${currentClean} (up to date)`);
}
}
if (updatable.length === 0) {
console.log(chalk.bold('\n All packages are up to date!'));
return;
}
// Check-only mode
if (options.check) {
console.log(chalk.bold(`\n ${updatable.length} package(s) can be updated.`));
console.log(chalk.gray(' Run `esengine update` to update.'));
return;
}
// Confirm update
if (!options.yes) {
console.log();
const confirm = await prompts({
type: 'confirm',
name: 'proceed',
message: `Update ${updatable.length} package(s)?`,
initial: true
});
if (!confirm.proceed) {
console.log(chalk.yellow('\n Cancelled.'));
return;
}
}
// Update package.json (re-read to minimize race condition)
console.log(chalk.bold('\n Updating packages...\n'));
const updates: Record<string, string> = {};
for (const upd of updatable) {
const cleanCurrent = upd.current.replace(/^\^|~/, '');
const isLatestTag = cleanCurrent === 'latest' || cleanCurrent === '*';
const prefix = isLatestTag ? '^' : (upd.current.startsWith('^') ? '^' : upd.current.startsWith('~') ? '~' : '');
updates[upd.name] = `${prefix}${upd.latest}`;
console.log(` ${chalk.green('↑')} ${upd.name}${prefix}${upd.latest}`);
}
// Atomic update: write to temp file then rename
const tempPath = `${packageJsonPath}.tmp`;
const freshPkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
freshPkg.dependencies = { ...freshPkg.dependencies, ...updates };
fs.writeFileSync(tempPath, JSON.stringify(freshPkg, null, 2), 'utf-8');
fs.renameSync(tempPath, packageJsonPath);
// Run install
const pm = detectPackageManager(cwd);
const installCmd = pm === 'pnpm' ? 'pnpm install' : pm === 'yarn' ? 'yarn' : 'npm install';
console.log(chalk.gray(`\n Running ${installCmd}...`));
try {
execSync(installCmd, { cwd, stdio: 'inherit' });
console.log(chalk.bold('\n Done! All packages updated.'));
} catch {
console.log(chalk.yellow(`\n ⚠ Failed to run install. package.json has been updated.`));
console.log(chalk.gray(` Run \`${installCmd}\` manually.`));
}
console.log();
}
// =========================================================================
// CLI Setup
// =========================================================================
// Setup CLI
const program = new Command();
program
.name('esengine')
.description('CLI tool for adding ESEngine ECS to your project')
.description('CLI tool for ESEngine ECS framework')
.version(VERSION);
program
@@ -311,10 +779,38 @@ program
.option('-p, --platform <platform>', 'Target platform (cocos, cocos2, laya, nodejs)')
.action(initCommand);
// Default command: run init
program
.command('list')
.alias('ls')
.description('List available modules')
.option('-c, --category <category>', 'Filter by category (core, ai, utility, physics, rendering, network)')
.action(listCommand);
program
.command('add [modules...]')
.description('Add modules to your project')
.option('-y, --yes', 'Skip confirmation')
.action(addCommand);
program
.command('remove [modules...]')
.alias('rm')
.description('Remove modules from your project')
.option('-y, --yes', 'Skip confirmation')
.action(removeCommand);
program
.command('update [modules...]')
.alias('up')
.description('Update ESEngine packages to latest versions')
.option('-y, --yes', 'Skip confirmation')
.option('-c, --check', 'Only check for updates, do not install')
.action(updateCommand);
// Default command: show help
program
.action(() => {
initCommand({});
program.help();
});
program.parse();

View File

@@ -0,0 +1,150 @@
/**
* @zh ESEngine 可用模块定义
* @en ESEngine Available Modules Definition
*/
export interface ModuleInfo {
id: string;
name: string;
package: string;
version: string;
description: string;
category: 'core' | 'ai' | 'physics' | 'rendering' | 'network' | 'utility';
dependencies?: string[];
}
/**
* @zh 可用模块列表
* @en Available modules list
*/
export const AVAILABLE_MODULES: ModuleInfo[] = [
// Core
{
id: 'core',
name: 'ECS Core',
package: '@esengine/ecs-framework',
version: 'latest',
description: 'ECS 核心框架 | Core ECS framework',
category: 'core'
},
{
id: 'math',
name: 'Math',
package: '@esengine/ecs-framework-math',
version: 'latest',
description: '数学库 (向量、矩阵) | Math library (vectors, matrices)',
category: 'core'
},
// AI
{
id: 'fsm',
name: 'FSM',
package: '@esengine/fsm',
version: 'latest',
description: '有限状态机 | Finite State Machine',
category: 'ai'
},
{
id: 'behavior-tree',
name: 'Behavior Tree',
package: '@esengine/behavior-tree',
version: 'latest',
description: '行为树 AI 系统 | Behavior Tree AI system',
category: 'ai'
},
{
id: 'pathfinding',
name: 'Pathfinding',
package: '@esengine/pathfinding',
version: 'latest',
description: '寻路系统 (A*, NavMesh) | Pathfinding (A*, NavMesh)',
category: 'ai'
},
// Utility
{
id: 'timer',
name: 'Timer',
package: '@esengine/timer',
version: 'latest',
description: '定时器和冷却系统 | Timer and cooldown system',
category: 'utility'
},
{
id: 'spatial',
name: 'Spatial',
package: '@esengine/spatial',
version: 'latest',
description: '空间索引和 AOI 系统 | Spatial index and AOI system',
category: 'utility'
},
{
id: 'procgen',
name: 'Procgen',
package: '@esengine/procgen',
version: 'latest',
description: '程序化生成 (噪声、随机) | Procedural generation',
category: 'utility'
},
{
id: 'blueprint',
name: 'Blueprint',
package: '@esengine/blueprint',
version: 'latest',
description: '可视化脚本系统 | Visual scripting system',
category: 'utility'
},
// Network
{
id: 'network',
name: 'Network',
package: '@esengine/network',
version: 'latest',
description: '网络同步客户端 | Network sync client',
category: 'network',
dependencies: ['network-protocols']
},
{
id: 'network-protocols',
name: 'Network Protocols',
package: '@esengine/network-protocols',
version: 'latest',
description: '网络共享协议 | Shared network protocols',
category: 'network'
},
{
id: 'network-server',
name: 'Network Server',
package: '@esengine/network-server',
version: 'latest',
description: '网络游戏服务器 | Network game server',
category: 'network',
dependencies: ['network-protocols']
}
];
/**
* @zh 获取模块信息
* @en Get module info by id
*/
export function getModuleById(id: string): ModuleInfo | undefined {
return AVAILABLE_MODULES.find(m => m.id === id);
}
/**
* @zh 按分类获取模块
* @en Get modules by category
*/
export function getModulesByCategory(category: ModuleInfo['category']): ModuleInfo[] {
return AVAILABLE_MODULES.filter(m => m.category === category);
}
/**
* @zh 获取所有模块 ID
* @en Get all module IDs
*/
export function getAllModuleIds(): string[] {
return AVAILABLE_MODULES.map(m => m.id);
}

View File

@@ -0,0 +1,30 @@
# @esengine/demos
## 1.0.3
### Patch Changes
- Updated dependencies []:
- @esengine/fsm@1.0.3
- @esengine/pathfinding@1.0.3
- @esengine/procgen@1.0.3
- @esengine/spatial@1.0.4
- @esengine/timer@1.0.3
## 1.0.2
### Patch Changes
- Updated dependencies []:
- @esengine/fsm@1.0.2
- @esengine/pathfinding@1.0.2
- @esengine/procgen@1.0.2
- @esengine/spatial@1.0.3
- @esengine/timer@1.0.2
## 1.0.1
### Patch Changes
- Updated dependencies [[`d66c180`](https://github.com/esengine/esengine/commit/d66c18041ebffa67b4dd12a026075e22dc1f5d36)]:
- @esengine/spatial@1.0.2

View File

@@ -0,0 +1,26 @@
{
"name": "@esengine/demos",
"version": "1.0.3",
"private": true,
"description": "Demo tests for ESEngine modules documentation",
"type": "module",
"scripts": {
"test": "tsx src/index.ts",
"test:timer": "tsx src/timer.demo.ts",
"test:fsm": "tsx src/fsm.demo.ts",
"test:pathfinding": "tsx src/pathfinding.demo.ts",
"test:procgen": "tsx src/procgen.demo.ts",
"test:spatial": "tsx src/spatial.demo.ts"
},
"dependencies": {
"@esengine/timer": "workspace:*",
"@esengine/fsm": "workspace:*",
"@esengine/pathfinding": "workspace:*",
"@esengine/procgen": "workspace:*",
"@esengine/spatial": "workspace:*"
},
"devDependencies": {
"tsx": "^4.7.0",
"typescript": "^5.8.3"
}
}

View File

@@ -0,0 +1,163 @@
/**
* FSM Module Demo - Tests APIs from docs/modules/fsm/index.md
*/
import { createStateMachine } from '@esengine/fsm';
import { assert, section, demoHeader, demoFooter } from './utils.js';
type PlayerState = 'idle' | 'walk' | 'run' | 'jump';
export async function runFSMDemo(): Promise<void> {
demoHeader('FSM Module Demo');
// 1. Basic Creation
section('1. createStateMachine()');
const fsm = createStateMachine<PlayerState>('idle');
assert(fsm !== null, 'State machine created');
assert(fsm.current === 'idle', 'Initial state is idle');
// 2. Define States
section('2. defineState()');
let enterCalled = false;
let exitCalled = false;
let updateCalled = false;
fsm.defineState('idle', {
onEnter: () => { enterCalled = true; },
onExit: () => { exitCalled = true; },
onUpdate: () => { updateCalled = true; }
});
fsm.defineState('walk', {});
fsm.defineState('run', {});
fsm.defineState('jump', {});
assert(fsm.hasState('idle'), 'hasState() returns true');
assert(fsm.hasState('walk'), 'walk state exists');
// 3. Manual Transition
section('3. transition()');
fsm.transition('walk');
assert(fsm.current === 'walk', 'Transitioned to walk');
assert(exitCalled, 'onExit called on idle');
assert(fsm.previous === 'idle', 'previous is idle');
// 4. State with Context
section('4. Context Support');
interface Context {
speed: number;
isMoving: boolean;
}
const fsmCtx = createStateMachine<PlayerState, Context>('idle', {
context: { speed: 0, isMoving: false }
});
fsmCtx.defineState('idle', {
onEnter: (ctx) => { ctx.speed = 0; }
});
fsmCtx.defineState('walk', {
onEnter: (ctx) => { ctx.speed = 100; }
});
fsmCtx.transition('walk');
assert(fsmCtx.context.speed === 100, 'Context updated on enter');
// 5. Transition Conditions
section('5. defineTransition() with conditions');
const fsmTrans = createStateMachine<PlayerState, Context>('idle', {
context: { speed: 0, isMoving: false }
});
fsmTrans.defineState('idle', {});
fsmTrans.defineState('walk', {});
fsmTrans.defineTransition('idle', 'walk', (ctx) => ctx.isMoving);
fsmTrans.evaluateTransitions();
assert(fsmTrans.current === 'idle', 'No transition when condition false');
fsmTrans.context.isMoving = true;
fsmTrans.evaluateTransitions();
assert(fsmTrans.current === 'walk', 'Transitions when condition true');
// 6. Transition Priority
section('6. Transition Priority');
const fsmPri = createStateMachine<'a' | 'b' | 'c'>('a');
fsmPri.defineState('a', {});
fsmPri.defineState('b', {});
fsmPri.defineState('c', {});
fsmPri.defineTransition('a', 'b', () => true, 1);
fsmPri.defineTransition('a', 'c', () => true, 10);
fsmPri.evaluateTransitions();
assert(fsmPri.current === 'c', 'Higher priority (10) wins');
// 7. Update
section('7. update()');
const fsmUpdate = createStateMachine<PlayerState>('idle');
let updateCount = 0;
fsmUpdate.defineState('idle', {
onUpdate: () => { updateCount++; }
});
fsmUpdate.update(16);
fsmUpdate.update(16);
assert(updateCount === 2, 'onUpdate called on each update');
// 8. Event Listeners
section('8. Event Listeners');
const fsmEvents = createStateMachine<PlayerState>('idle');
fsmEvents.defineState('idle', {});
fsmEvents.defineState('walk', {});
let enterEvent = false;
let exitEvent = false;
let changeEvent = false;
fsmEvents.onEnter('walk', () => { enterEvent = true; });
fsmEvents.onExit('idle', () => { exitEvent = true; });
fsmEvents.onChange(() => { changeEvent = true; });
fsmEvents.transition('walk');
assert(enterEvent, 'onEnter listener called');
assert(exitEvent, 'onExit listener called');
assert(changeEvent, 'onChange listener called');
// 9. getStates / getTransitionsFrom
section('9. Query Methods');
const states = fsmEvents.getStates();
assert(states.length >= 2, 'getStates() returns states');
// 10. canTransition
section('10. canTransition()');
const fsmCan = createStateMachine<PlayerState, Context>('idle', {
context: { speed: 0, isMoving: false }
});
fsmCan.defineState('idle', {});
fsmCan.defineState('walk', {});
fsmCan.defineTransition('idle', 'walk', (ctx) => ctx.isMoving);
assert(!fsmCan.canTransition('walk'), 'Cannot transition when condition false');
fsmCan.context.isMoving = true;
assert(fsmCan.canTransition('walk'), 'Can transition when condition true');
// 11. Reset
section('11. reset()');
fsmCan.transition('walk');
fsmCan.reset('idle');
assert(fsmCan.current === 'idle', 'Reset to idle');
// 12. History
section('12. getHistory()');
const fsmHist = createStateMachine<PlayerState>('idle', { enableHistory: true });
fsmHist.defineState('idle', {});
fsmHist.defineState('walk', {});
fsmHist.defineState('run', {});
fsmHist.transition('walk');
fsmHist.transition('run');
const history = fsmHist.getHistory();
assert(history.length >= 2, 'History recorded');
demoFooter('FSM Demo');
}
runFSMDemo().catch(console.error);

View File

@@ -0,0 +1,75 @@
/**
* ESEngine Module Demos - Run all demos to verify documentation
*/
import { runTimerDemo } from './timer.demo.js';
import { runFSMDemo } from './fsm.demo.js';
import { runPathfindingDemo } from './pathfinding.demo.js';
import { runProcgenDemo } from './procgen.demo.js';
import { runSpatialDemo } from './spatial.demo.js';
async function runAllDemos(): Promise<void> {
console.log('\n');
console.log('╔═══════════════════════════════════════════════════════════╗');
console.log('║ ESEngine Module Documentation Tests ║');
console.log('╚═══════════════════════════════════════════════════════════╝');
console.log('\n');
const demos = [
{ name: 'Timer', fn: runTimerDemo },
{ name: 'FSM', fn: runFSMDemo },
{ name: 'Pathfinding', fn: runPathfindingDemo },
{ name: 'Procgen', fn: runProcgenDemo },
{ name: 'Spatial', fn: runSpatialDemo },
];
const results: { name: string; passed: boolean; error?: string }[] = [];
for (const demo of demos) {
try {
await demo.fn();
results.push({ name: demo.name, passed: true });
} catch (error) {
results.push({
name: demo.name,
passed: false,
error: error instanceof Error ? error.message : String(error)
});
}
}
// Summary
console.log('\n');
console.log('╔═══════════════════════════════════════════════════════════╗');
console.log('║ Summary ║');
console.log('╚═══════════════════════════════════════════════════════════╝');
console.log('\n');
let allPassed = true;
for (const result of results) {
if (result.passed) {
console.log(`${result.name}: PASSED`);
} else {
console.log(`${result.name}: FAILED - ${result.error}`);
allPassed = false;
}
}
console.log('\n');
if (allPassed) {
console.log(' ══════════════════════════════════════');
console.log(' ALL DOCUMENTATION TESTS PASSED ✓');
console.log(' ══════════════════════════════════════');
} else {
console.log(' ══════════════════════════════════════');
console.log(' SOME TESTS FAILED ✗');
console.log(' ══════════════════════════════════════');
process.exit(1);
}
console.log('\n');
}
runAllDemos().catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});

View File

@@ -0,0 +1,152 @@
/**
* Pathfinding Module Demo - Tests APIs from docs/modules/pathfinding/index.md
*/
import {
createGridMap,
createAStarPathfinder,
createLineOfSightSmoother,
createCatmullRomSmoother,
manhattanDistance,
octileDistance
} from '@esengine/pathfinding';
import { assert, section, demoHeader, demoFooter } from './utils.js';
export async function runPathfindingDemo(): Promise<void> {
demoHeader('Pathfinding Module Demo');
// 1. Create Grid Map
section('1. createGridMap()');
const grid = createGridMap(20, 20);
assert(grid !== null, 'Grid created');
assert(grid.width === 20, 'Width is 20');
assert(grid.height === 20, 'Height is 20');
// 2. Walkability
section('2. setWalkable() / isWalkable()');
assert(grid.isWalkable(5, 5), 'Initially walkable');
grid.setWalkable(5, 5, false);
assert(!grid.isWalkable(5, 5), 'Set to not walkable');
grid.setWalkable(5, 5, true);
assert(grid.isWalkable(5, 5), 'Restored to walkable');
// 3. Set Obstacles
section('3. Setting Obstacles');
grid.setWalkable(5, 5, false);
grid.setWalkable(5, 6, false);
grid.setWalkable(5, 7, false);
assert(!grid.isWalkable(5, 6), 'Obstacle set');
// 4. Create Pathfinder
section('4. createAStarPathfinder()');
const pathfinder = createAStarPathfinder(grid);
assert(pathfinder !== null, 'Pathfinder created');
// 5. Find Path
section('5. findPath()');
const result = pathfinder.findPath(0, 0, 15, 15);
assert(result.found, 'Path found');
assert(result.path.length > 0, `Path has ${result.path.length} points`);
assert(result.cost > 0, `Path cost: ${result.cost.toFixed(2)}`);
assert(result.nodesSearched > 0, `Searched ${result.nodesSearched} nodes`);
// 6. Path Blocked
section('6. Path Blocked');
// Create a wall
for (let y = 0; y < 20; y++) {
grid.setWalkable(10, y, false);
}
const blocked = pathfinder.findPath(0, 0, 15, 15);
assert(!blocked.found, 'No path when fully blocked');
// Clear wall
for (let y = 0; y < 20; y++) {
grid.setWalkable(10, y, true);
}
// 7. Movement Cost
section('7. setCost()');
const gridCost = createGridMap(10, 10);
gridCost.setCost(5, 5, 10); // High cost tile
const costResult = createAStarPathfinder(gridCost).findPath(0, 0, 9, 9);
assert(costResult.found, 'Path found with cost');
// 8. Heuristics
section('8. Heuristic Functions');
const d1 = manhattanDistance({ x: 0, y: 0 }, { x: 3, y: 4 });
assert(d1 === 7, `Manhattan distance: ${d1}`);
const d2 = octileDistance({ x: 0, y: 0 }, { x: 3, y: 4 });
assert(d2 > 0, `Octile distance: ${d2.toFixed(2)}`);
// 9. Grid Options
section('9. Grid Options');
const gridOpts = createGridMap(10, 10, {
allowDiagonal: false,
heuristic: manhattanDistance
});
assert(gridOpts !== null, 'Grid with options created');
// 10. Path Smoothing - Line of Sight
section('10. Line of Sight Smoother');
const gridSmooth = createGridMap(20, 20);
const pf = createAStarPathfinder(gridSmooth);
const rawPath = pf.findPath(0, 0, 10, 10);
const losSmoother = createLineOfSightSmoother();
const smoothed = losSmoother.smooth(rawPath.path, gridSmooth);
assert(smoothed.length <= rawPath.path.length, `Smoothed: ${rawPath.path.length} -> ${smoothed.length} points`);
// 11. Catmull-Rom Smoother
section('11. Catmull-Rom Smoother');
const crSmoother = createCatmullRomSmoother(5, 0.5);
const curved = crSmoother.smooth(rawPath.path, gridSmooth);
assert(curved.length >= rawPath.path.length, `Curved path has ${curved.length} points`);
// 12. loadFromArray
section('12. loadFromArray()');
const gridArr = createGridMap(5, 3);
gridArr.loadFromArray([
[0, 0, 0, 1, 0],
[0, 1, 0, 1, 0],
[0, 1, 0, 0, 0]
]);
assert(!gridArr.isWalkable(3, 0), 'Loaded obstacle at (3,0)');
assert(!gridArr.isWalkable(1, 1), 'Loaded obstacle at (1,1)');
assert(gridArr.isWalkable(0, 0), 'Loaded walkable at (0,0)');
// 13. loadFromString
section('13. loadFromString()');
const gridStr = createGridMap(5, 3);
gridStr.loadFromString(`
.....
.#.#.
.#...
`);
assert(!gridStr.isWalkable(1, 1), 'Loaded # as obstacle');
assert(gridStr.isWalkable(0, 0), 'Loaded . as walkable');
// 14. setRectWalkable
section('14. setRectWalkable()');
const gridRect = createGridMap(10, 10);
gridRect.setRectWalkable(2, 2, 4, 4, false);
assert(!gridRect.isWalkable(3, 3), 'Rect set as obstacle');
assert(gridRect.isWalkable(0, 0), 'Outside rect is walkable');
// 15. Pathfinder Options
section('15. Pathfinder Options');
const limitedResult = pathfinder.findPath(0, 0, 15, 15, {
maxNodes: 100,
heuristicWeight: 1.5
});
assert(limitedResult !== null, 'findPath with options works');
// 16. Reset Grid
section('16. reset()');
grid.reset();
assert(grid.isWalkable(5, 5), 'Grid reset - all walkable');
demoFooter('Pathfinding Demo');
}
runPathfindingDemo().catch(console.error);

View File

@@ -0,0 +1,196 @@
/**
* Procgen Module Demo - Tests APIs from docs/modules/procgen/index.md
*/
import {
createPerlinNoise,
createSimplexNoise,
createWorleyNoise,
createFBM,
createSeededRandom,
createWeightedRandom,
shuffle,
shuffleCopy,
pickOne,
sample,
sampleWithReplacement,
weightedPick,
weightedPickFromMap
} from '@esengine/procgen';
import { assert, section, demoHeader, demoFooter } from './utils.js';
export async function runProcgenDemo(): Promise<void> {
demoHeader('Procgen Module Demo');
// 1. Perlin Noise
section('1. createPerlinNoise()');
const perlin = createPerlinNoise(12345);
assert(perlin !== null, 'Perlin noise created');
const val2d = perlin.noise2D(0.5, 0.5);
assert(val2d >= -1 && val2d <= 1, `2D noise value in [-1,1]: ${val2d.toFixed(3)}`);
const val3d = perlin.noise3D(0.5, 0.5, 0.5);
assert(val3d >= -1 && val3d <= 1, `3D noise value in [-1,1]: ${val3d.toFixed(3)}`);
// 2. Simplex Noise
section('2. createSimplexNoise()');
const simplex = createSimplexNoise(12345);
const sval = simplex.noise2D(0.5, 0.5);
assert(sval >= -1 && sval <= 1, `Simplex value: ${sval.toFixed(3)}`);
// 3. Worley Noise
section('3. createWorleyNoise()');
const worley = createWorleyNoise(12345);
const wval = worley.noise2D(0.5, 0.5);
assert(wval >= 0, `Worley distance: ${wval.toFixed(3)}`);
// 4. FBM
section('4. createFBM()');
const fbm = createFBM(perlin, {
octaves: 6,
lacunarity: 2.0,
persistence: 0.5
});
const fbmVal = fbm.noise2D(0.1, 0.1);
assert(typeof fbmVal === 'number', `FBM value: ${fbmVal.toFixed(3)}`);
const ridged = fbm.ridged2D(0.1, 0.1);
assert(typeof ridged === 'number', `Ridged FBM: ${ridged.toFixed(3)}`);
const turb = fbm.turbulence2D(0.1, 0.1);
assert(turb >= 0, `Turbulence: ${turb.toFixed(3)}`);
// 5. Seeded Random
section('5. createSeededRandom()');
const rng = createSeededRandom(42);
assert(rng !== null, 'RNG created');
const r1 = rng.next();
assert(r1 >= 0 && r1 < 1, `next() in [0,1): ${r1.toFixed(3)}`);
const r2 = rng.nextInt(1, 10);
assert(r2 >= 1 && r2 <= 10, `nextInt(1,10): ${r2}`);
const r3 = rng.nextFloat(0, 100);
assert(r3 >= 0 && r3 < 100, `nextFloat(0,100): ${r3.toFixed(2)}`);
const r4 = rng.nextBool();
assert(typeof r4 === 'boolean', `nextBool(): ${r4}`);
const r5 = rng.nextBool(0.9);
assert(typeof r5 === 'boolean', `nextBool(0.9): ${r5}`);
// 6. Deterministic
section('6. Deterministic Sequences');
const rng1 = createSeededRandom(42);
const rng2 = createSeededRandom(42);
const seq1 = [rng1.next(), rng1.next(), rng1.next()];
const seq2 = [rng2.next(), rng2.next(), rng2.next()];
assert(seq1[0] === seq2[0] && seq1[1] === seq2[1] && seq1[2] === seq2[2],
'Same seed produces same sequence');
// 7. Distributions
section('7. Distribution Methods');
const rngDist = createSeededRandom(42);
const gauss = rngDist.nextGaussian();
assert(typeof gauss === 'number', `nextGaussian(): ${gauss.toFixed(3)}`);
const gauss2 = rngDist.nextGaussian(100, 15);
assert(typeof gauss2 === 'number', `nextGaussian(100,15): ${gauss2.toFixed(1)}`);
const exp = rngDist.nextExponential();
assert(exp >= 0, `nextExponential(): ${exp.toFixed(3)}`);
// 8. Geometry Methods
section('8. Geometry Methods');
const rngGeo = createSeededRandom(42);
const pointCircle = rngGeo.nextPointInCircle(50);
assert(pointCircle.x !== undefined && pointCircle.y !== undefined,
`nextPointInCircle: (${pointCircle.x.toFixed(1)}, ${pointCircle.y.toFixed(1)})`);
const pointOnCircle = rngGeo.nextPointOnCircle(50);
const dist = Math.sqrt(pointOnCircle.x ** 2 + pointOnCircle.y ** 2);
assert(Math.abs(dist - 50) < 0.01, `nextPointOnCircle radius ~50: ${dist.toFixed(2)}`);
const dir = rngGeo.nextDirection2D();
const len = Math.sqrt(dir.x ** 2 + dir.y ** 2);
assert(Math.abs(len - 1) < 0.01, `nextDirection2D length ~1: ${len.toFixed(3)}`);
// 9. Weighted Random
section('9. createWeightedRandom()');
const rngW = createSeededRandom(42);
const loot = createWeightedRandom([
{ value: 'common', weight: 60 },
{ value: 'rare', weight: 30 },
{ value: 'epic', weight: 10 }
]);
assert(loot.size === 3, 'Has 3 items');
assert(loot.totalWeight === 100, 'Total weight is 100');
assert(loot.getProbability(0) === 0.6, 'Common probability is 0.6');
const picked = loot.pick(rngW);
assert(['common', 'rare', 'epic'].includes(picked), `Picked: ${picked}`);
// 10. Shuffle
section('10. shuffle() / shuffleCopy()');
const rngS = createSeededRandom(42);
const arr = [1, 2, 3, 4, 5];
const copy = shuffleCopy(arr, rngS);
assert(copy.length === 5, 'Shuffled copy has same length');
assert(arr[0] === 1, 'Original unchanged');
shuffle(arr, rngS);
assert(arr.length === 5, 'In-place shuffle preserves length');
// 11. pickOne
section('11. pickOne()');
const rngP = createSeededRandom(42);
const items = ['a', 'b', 'c', 'd'];
const picked2 = pickOne(items, rngP);
assert(items.includes(picked2), `Picked: ${picked2}`);
// 12. sample
section('12. sample() / sampleWithReplacement()');
const rngSamp = createSeededRandom(42);
const nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const sampled = sample(nums, 3, rngSamp);
assert(sampled.length === 3, 'Sampled 3 items');
assert(new Set(sampled).size === 3, 'All unique');
const withRep = sampleWithReplacement(nums, 5, rngSamp);
assert(withRep.length === 5, 'Sampled 5 with replacement');
// 13. weightedPick
section('13. weightedPick() / weightedPickFromMap()');
const rngWP = createSeededRandom(42);
const item = weightedPick([
{ value: 'a', weight: 1 },
{ value: 'b', weight: 2 }
], rngWP);
assert(['a', 'b'].includes(item), `weightedPick: ${item}`);
const item2 = weightedPickFromMap({
'common': 60,
'rare': 30
}, rngWP);
assert(['common', 'rare'].includes(item2), `weightedPickFromMap: ${item2}`);
// 14. Reset
section('14. reset()');
const rngReset = createSeededRandom(42);
const first = rngReset.next();
rngReset.next();
rngReset.next();
rngReset.reset();
const afterReset = rngReset.next();
assert(first === afterReset, 'Reset restores initial state');
demoFooter('Procgen Demo');
}
runProcgenDemo().catch(console.error);

View File

@@ -0,0 +1,224 @@
/**
* Spatial Module Demo - Tests APIs from docs/modules/spatial/index.md
*/
import {
createGridSpatialIndex,
createGridAOI,
createBounds,
createBoundsFromCenter,
createBoundsFromCircle,
isPointInBounds,
boundsIntersect,
distance,
distanceSquared
} from '@esengine/spatial';
import { assert, section, demoHeader, demoFooter } from './utils.js';
interface Entity {
id: number;
type: string;
}
export async function runSpatialDemo(): Promise<void> {
demoHeader('Spatial Module Demo');
// 1. Create Spatial Index
section('1. createGridSpatialIndex()');
const spatial = createGridSpatialIndex<Entity>(100);
assert(spatial !== null, 'Spatial index created');
assert(spatial.count === 0, 'Initially empty');
// 2. Insert
section('2. insert()');
const player: Entity = { id: 1, type: 'player' };
const enemy1: Entity = { id: 2, type: 'enemy' };
const enemy2: Entity = { id: 3, type: 'enemy' };
spatial.insert(player, { x: 100, y: 200 });
spatial.insert(enemy1, { x: 150, y: 250 });
spatial.insert(enemy2, { x: 500, y: 600 });
assert(spatial.count === 3, 'Count is 3');
// 3. findInRadius
section('3. findInRadius()');
const nearby = spatial.findInRadius({ x: 100, y: 200 }, 100);
assert(nearby.length === 2, `Found ${nearby.length} entities in radius`);
assert(nearby.includes(player), 'Found player');
assert(nearby.includes(enemy1), 'Found enemy1');
assert(!nearby.includes(enemy2), 'enemy2 is too far');
// 4. findInRadius with filter
section('4. findInRadius() with filter');
const enemies = spatial.findInRadius(
{ x: 100, y: 200 },
100,
(e) => e.type === 'enemy'
);
assert(enemies.length === 1, 'Found 1 enemy');
assert(enemies[0] === enemy1, 'Found enemy1');
// 5. findNearest
section('5. findNearest()');
const nearest = spatial.findNearest({ x: 100, y: 200 });
assert(nearest === player || nearest === enemy1, 'Found nearest entity');
const nearestEnemy = spatial.findNearest(
{ x: 100, y: 200 },
undefined,
(e) => e.type === 'enemy'
);
assert(nearestEnemy === enemy1, 'Found nearest enemy');
// 6. findKNearest
section('6. findKNearest()');
const k2 = spatial.findKNearest({ x: 100, y: 200 }, 2, 1000);
assert(k2.length === 2, 'Found 2 nearest');
// 7. Update position
section('7. update()');
spatial.update(player, { x: 400, y: 400 });
const afterMove = spatial.findInRadius({ x: 100, y: 200 }, 100);
assert(!afterMove.includes(player), 'Player moved away');
// 8. Remove
section('8. remove()');
spatial.remove(enemy2);
assert(spatial.count === 2, 'Count is 2 after remove');
// 9. findInRect
section('9. findInRect()');
const bounds = createBounds(0, 0, 200, 300);
spatial.update(player, { x: 100, y: 200 });
const inRect = spatial.findInRect(bounds);
assert(inRect.length >= 1, `Found ${inRect.length} in rect`);
// 10. Raycast
section('10. raycast()');
spatial.update(player, { x: 100, y: 0 });
spatial.update(enemy1, { x: 100, y: 200 });
const hits = spatial.raycast(
{ x: 100, y: -100 },
{ x: 0, y: 1 },
500
);
assert(hits.length >= 1, `Raycast hit ${hits.length} entities`);
// 11. raycastFirst
section('11. raycastFirst()');
const firstHit = spatial.raycastFirst(
{ x: 100, y: -100 },
{ x: 0, y: 1 },
500
);
if (firstHit) {
assert(firstHit.target !== null, 'Hit has target');
assert(firstHit.distance >= 0, `Hit distance: ${firstHit.distance.toFixed(1)}`);
}
// 12. Clear
section('12. clear()');
spatial.clear();
assert(spatial.count === 0, 'Cleared');
// =========================================================================
// AOI Tests
// =========================================================================
// 13. Create AOI
section('13. createGridAOI()');
const aoi = createGridAOI<Entity>(100);
assert(aoi !== null, 'AOI created');
// 14. Add Observers
section('14. addObserver()');
const p1: Entity = { id: 1, type: 'player' };
const p2: Entity = { id: 2, type: 'player' };
aoi.addObserver(p1, { x: 100, y: 100 }, { viewRange: 200 });
aoi.addObserver(p2, { x: 150, y: 150 }, { viewRange: 200 });
// 15. getEntitiesInView
section('15. getEntitiesInView()');
const visible = aoi.getEntitiesInView(p1);
assert(visible.includes(p2), 'p1 can see p2');
// 16. canSee
section('16. canSee()');
assert(aoi.canSee(p1, p2), 'p1 can see p2');
// 17. updatePosition
section('17. updatePosition()');
aoi.updatePosition(p2, { x: 1000, y: 1000 });
assert(!aoi.canSee(p1, p2), 'p1 cannot see p2 after move');
// 18. getObserversOf
section('18. getObserversOf()');
aoi.updatePosition(p2, { x: 120, y: 120 });
const observers = aoi.getObserversOf(p2);
assert(observers.includes(p1), 'p1 observes p2');
// 19. Event Listener
section('19. addListener()');
let eventCount = 0;
aoi.addListener((event) => {
eventCount++;
});
aoi.updatePosition(p2, { x: 2000, y: 2000 }); // Should trigger exit
aoi.updatePosition(p2, { x: 130, y: 130 }); // Should trigger enter
assert(eventCount >= 1, `Events triggered: ${eventCount}`);
// 20. updateViewRange
section('20. updateViewRange()');
aoi.updateViewRange(p1, 50);
aoi.updatePosition(p2, { x: 200, y: 200 });
assert(!aoi.canSee(p1, p2), 'Cannot see after view range reduced');
// 21. removeObserver
section('21. removeObserver()');
aoi.removeObserver(p2);
const afterRemove = aoi.getEntitiesInView(p1);
assert(!afterRemove.includes(p2), 'p2 removed from AOI');
// =========================================================================
// Utility Functions
// =========================================================================
// 22. Bounds Creation
section('22. Bounds Creation');
const b1 = createBounds(0, 0, 100, 100);
assert(b1.minX === 0 && b1.maxX === 100, 'createBounds works');
const b2 = createBoundsFromCenter({ x: 50, y: 50 }, 100, 100);
assert(b2.minX === 0 && b2.maxX === 100, 'createBoundsFromCenter works');
const b3 = createBoundsFromCircle({ x: 50, y: 50 }, 50);
assert(b3.minX === 0 && b3.maxX === 100, 'createBoundsFromCircle works');
// 23. Point in Bounds
section('23. isPointInBounds()');
assert(isPointInBounds({ x: 50, y: 50 }, b1), 'Point inside');
assert(!isPointInBounds({ x: 150, y: 150 }, b1), 'Point outside');
// 24. Bounds Intersect
section('24. boundsIntersect()');
const ba = createBounds(0, 0, 100, 100);
const bb = createBounds(50, 50, 150, 150);
const bc = createBounds(200, 200, 300, 300);
assert(boundsIntersect(ba, bb), 'Overlapping bounds intersect');
assert(!boundsIntersect(ba, bc), 'Separate bounds do not intersect');
// 25. Distance
section('25. distance() / distanceSquared()');
const d = distance({ x: 0, y: 0 }, { x: 3, y: 4 });
assert(Math.abs(d - 5) < 0.001, `Distance: ${d}`);
const dsq = distanceSquared({ x: 0, y: 0 }, { x: 3, y: 4 });
assert(dsq === 25, `Distance squared: ${dsq}`);
demoFooter('Spatial Demo');
}
runSpatialDemo().catch(console.error);

View File

@@ -0,0 +1,107 @@
/**
* Timer Module Demo - Tests APIs from docs/modules/timer/index.md
*/
import { createTimerService } from '@esengine/timer';
import { assert, section, demoHeader, demoFooter } from './utils.js';
export async function runTimerDemo(): Promise<void> {
demoHeader('Timer Module Demo');
// 1. Basic Creation
section('1. createTimerService()');
const timerService = createTimerService();
assert(timerService !== null, 'Service created');
assert(timerService.activeTimerCount === 0, 'Initial timer count is 0');
assert(timerService.activeCooldownCount === 0, 'Initial cooldown count is 0');
// 2. One-shot Timer
section('2. schedule() - One-shot Timer');
let fired = false;
const handle = timerService.schedule('test', 100, () => { fired = true; });
assert(handle.id === 'test', 'Handle.id correct');
assert(handle.isValid === true, 'Handle.isValid is true');
assert(timerService.hasTimer('test'), 'hasTimer() returns true');
timerService.update(50);
assert(!fired, 'Timer not fired at 50ms');
timerService.update(60);
assert(fired, 'Timer fired after 110ms');
assert(!timerService.hasTimer('test'), 'Timer removed after firing');
// 3. Repeating Timer
section('3. scheduleRepeating()');
let count = 0;
timerService.scheduleRepeating('repeat', 50, () => { count++; });
timerService.update(50);
assert(count === 1, 'Fires once at 50ms');
timerService.update(50);
assert(count === 2, 'Fires twice at 100ms');
timerService.cancelById('repeat');
timerService.update(100);
assert(count === 2, 'Stopped after cancel');
// 4. Timer Cancellation
section('4. cancel()');
let cancelled = false;
const h = timerService.schedule('cancel', 1000, () => { cancelled = true; });
h.cancel();
assert(!h.isValid, 'Handle invalid after cancel');
timerService.update(2000);
assert(!cancelled, 'Cancelled timer does not fire');
// 5. Timer Info
section('5. getTimerInfo()');
timerService.schedule('info', 500, () => {});
const info = timerService.getTimerInfo('info');
assert(info !== null, 'Returns info object');
assert(info!.id === 'info', 'Info.id correct');
assert(info!.repeating === false, 'Info.repeating is false');
timerService.cancelById('info');
// 6. Cooldown System
section('6. Cooldown API');
timerService.startCooldown('skill', 200);
assert(!timerService.isCooldownReady('skill'), 'Not ready initially');
assert(timerService.isOnCooldown('skill'), 'isOnCooldown true');
timerService.update(100);
const progress = timerService.getCooldownProgress('skill');
assert(progress >= 0.4 && progress <= 0.6, `Progress ~0.5 (got ${progress.toFixed(2)})`);
timerService.update(150);
assert(timerService.isCooldownReady('skill'), 'Ready after duration');
// 7. Cooldown Info
section('7. getCooldownInfo()');
timerService.startCooldown('cd', 300);
timerService.update(150);
const cdInfo = timerService.getCooldownInfo('cd');
assert(cdInfo !== null, 'Returns cooldown info');
assert(cdInfo!.duration === 300, 'Duration is 300');
assert(!cdInfo!.isReady, 'isReady is false');
// 8. Reset Cooldown
section('8. resetCooldown()');
timerService.startCooldown('reset', 500);
timerService.update(100);
timerService.resetCooldown('reset');
assert(timerService.isCooldownReady('reset'), 'Ready after reset');
// 9. Clear All
section('9. clear()');
timerService.schedule('t1', 1000, () => {});
timerService.startCooldown('c1', 1000);
timerService.clear();
assert(timerService.activeTimerCount === 0, 'Timers cleared');
assert(timerService.activeCooldownCount === 0, 'Cooldowns cleared');
// 10. Config Options
section('10. Config Options');
const limited = createTimerService({ maxTimers: 2, maxCooldowns: 1 });
assert(limited !== null, 'Created with config');
demoFooter('Timer Demo');
}
runTimerDemo().catch(console.error);

View File

@@ -0,0 +1,41 @@
/**
* @zh Demo 测试工具函数
* @en Demo test utility functions
*/
/**
* @zh 断言条件为真,否则抛出错误
* @en Assert condition is true, otherwise throw error
*/
export function assert(condition: boolean, message: string): void {
if (!condition) throw new Error(`FAILED: ${message}`);
console.log(`${message}`);
}
/**
* @zh 打印测试章节标题
* @en Print test section header
*/
export function section(name: string): void {
console.log(`\n▶ ${name}`);
}
/**
* @zh 打印 Demo 开始标题
* @en Print demo start header
*/
export function demoHeader(name: string): void {
console.log('═══════════════════════════════════════');
console.log(` ${name}`);
console.log('═══════════════════════════════════════');
}
/**
* @zh 打印 Demo 结束标题
* @en Print demo end header
*/
export function demoFooter(name: string): void {
console.log('\n═══════════════════════════════════════');
console.log(` ${name}: ALL TESTS PASSED ✓`);
console.log('═══════════════════════════════════════\n');
}

View File

@@ -0,0 +1,12 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"declaration": false,
"module": "ESNext",
"moduleResolution": "bundler",
"target": "ES2022"
},
"include": ["src/**/*"]
}

113
pnpm-lock.yaml generated
View File

@@ -185,7 +185,7 @@ importers:
version: link:../../engine/ecs-engine-bindgen
'@esengine/ecs-framework':
specifier: workspace:*
version: link:../../framework/core
version: link:../../framework/core/dist
'@esengine/editor-core':
specifier: workspace:*
version: link:../editor-core
@@ -385,7 +385,7 @@ importers:
version: link:../plugins/asset-system-editor
'@esengine/ecs-framework':
specifier: workspace:*
version: link:../../framework/core
version: link:../../framework/core/dist
'@esengine/engine-core':
specifier: workspace:*
version: link:../../engine/engine-core
@@ -476,7 +476,7 @@ importers:
devDependencies:
'@esengine/ecs-framework':
specifier: workspace:*
version: link:../../framework/core
version: link:../../framework/core/dist
'@esengine/ecs-framework-math':
specifier: workspace:*
version: link:../../framework/math
@@ -550,7 +550,7 @@ importers:
version: link:../../../tools/build-config
'@esengine/ecs-framework':
specifier: workspace:*
version: link:../../../framework/core
version: link:../../../framework/core/dist
'@esengine/editor-core':
specifier: workspace:*
version: link:../../editor-core
@@ -596,7 +596,7 @@ importers:
version: link:../../../tools/build-config
'@esengine/ecs-framework':
specifier: workspace:*
version: link:../../../framework/core
version: link:../../../framework/core/dist
'@esengine/editor-core':
specifier: workspace:*
version: link:../../editor-core
@@ -639,7 +639,7 @@ importers:
version: link:../../../tools/build-config
'@esengine/ecs-framework':
specifier: workspace:*
version: link:../../../framework/core
version: link:../../../framework/core/dist
'@esengine/editor-core':
specifier: workspace:*
version: link:../../editor-core
@@ -676,7 +676,7 @@ importers:
version: link:../../../tools/build-config
'@esengine/ecs-framework':
specifier: workspace:*
version: link:../../../framework/core
version: link:../../../framework/core/dist
'@esengine/editor-core':
specifier: workspace:*
version: link:../../editor-core
@@ -713,7 +713,7 @@ importers:
version: link:../../../tools/build-config
'@esengine/ecs-framework':
specifier: workspace:*
version: link:../../../framework/core
version: link:../../../framework/core/dist
'@esengine/editor-core':
specifier: workspace:*
version: link:../../editor-core
@@ -756,7 +756,7 @@ importers:
version: link:../../../tools/build-config
'@esengine/ecs-framework':
specifier: workspace:*
version: link:../../../framework/core
version: link:../../../framework/core/dist
'@esengine/editor-core':
specifier: workspace:*
version: link:../../editor-core
@@ -833,7 +833,7 @@ importers:
version: link:../../../tools/build-config
'@esengine/ecs-framework':
specifier: workspace:*
version: link:../../../framework/core
version: link:../../../framework/core/dist
'@esengine/editor-core':
specifier: workspace:*
version: link:../../editor-core
@@ -876,7 +876,7 @@ importers:
version: link:../../../tools/build-config
'@esengine/ecs-framework':
specifier: workspace:*
version: link:../../../framework/core
version: link:../../../framework/core/dist
'@esengine/editor-core':
specifier: workspace:*
version: link:../../editor-core
@@ -913,7 +913,7 @@ importers:
version: link:../../../tools/build-config
'@esengine/ecs-framework':
specifier: workspace:*
version: link:../../../framework/core
version: link:../../../framework/core/dist
'@esengine/editor-core':
specifier: workspace:*
version: link:../../editor-core
@@ -953,7 +953,7 @@ importers:
version: link:../../../tools/build-config
'@esengine/ecs-framework':
specifier: workspace:*
version: link:../../../framework/core
version: link:../../../framework/core/dist
'@esengine/editor-core':
specifier: workspace:*
version: link:../../editor-core
@@ -993,7 +993,7 @@ importers:
version: link:../../../tools/build-config
'@esengine/ecs-framework':
specifier: workspace:*
version: link:../../../framework/core
version: link:../../../framework/core/dist
'@esengine/editor-core':
specifier: workspace:*
version: link:../../editor-core
@@ -1036,7 +1036,7 @@ importers:
version: link:../../../tools/build-config
'@esengine/ecs-framework':
specifier: workspace:*
version: link:../../../framework/core
version: link:../../../framework/core/dist
'@esengine/editor-core':
specifier: workspace:*
version: link:../../editor-core
@@ -1079,7 +1079,7 @@ importers:
version: link:../../tools/build-config
'@esengine/ecs-framework':
specifier: workspace:*
version: link:../../framework/core
version: link:../../framework/core/dist
'@esengine/engine-core':
specifier: workspace:*
version: link:../engine-core
@@ -1100,7 +1100,7 @@ importers:
version: link:../asset-system
'@esengine/ecs-framework':
specifier: workspace:*
version: link:../../framework/core
version: link:../../framework/core/dist
'@esengine/ecs-framework-math':
specifier: workspace:*
version: link:../../framework/math
@@ -1135,7 +1135,7 @@ importers:
dependencies:
'@esengine/ecs-framework':
specifier: workspace:*
version: link:../../framework/core
version: link:../../framework/core/dist
'@esengine/ecs-framework-math':
specifier: workspace:*
version: link:../../framework/math
@@ -1166,7 +1166,7 @@ importers:
version: link:../../tools/build-config
'@esengine/ecs-framework':
specifier: workspace:*
version: link:../../framework/core
version: link:../../framework/core/dist
'@esengine/engine-core':
specifier: workspace:*
version: link:../engine-core
@@ -1215,7 +1215,7 @@ importers:
version: link:../asset-system
'@esengine/ecs-framework':
specifier: workspace:*
version: link:../../framework/core
version: link:../../framework/core/dist
'@esengine/engine-core':
specifier: workspace:*
version: link:../engine-core
@@ -1251,7 +1251,7 @@ importers:
dependencies:
'@esengine/ecs-framework':
specifier: workspace:*
version: link:../../framework/core
version: link:../../framework/core/dist
'@esengine/platform-common':
specifier: workspace:*
version: link:../platform-common
@@ -1291,7 +1291,7 @@ importers:
version: link:../ecs-engine-bindgen
'@esengine/ecs-framework':
specifier: workspace:*
version: link:../../framework/core
version: link:../../framework/core/dist
'@esengine/ecs-framework-math':
specifier: workspace:*
version: link:../../framework/math
@@ -1329,7 +1329,7 @@ importers:
version: link:../../tools/build-config
'@esengine/ecs-framework':
specifier: workspace:*
version: link:../../framework/core
version: link:../../framework/core/dist
'@esengine/engine-core':
specifier: workspace:*
version: link:../engine-core
@@ -1357,7 +1357,7 @@ importers:
version: link:../../tools/build-config
'@esengine/ecs-framework':
specifier: workspace:*
version: link:../core
version: link:../core/dist
'@types/jest':
specifier: ^29.5.14
version: 29.5.14
@@ -1391,7 +1391,7 @@ importers:
version: link:../../tools/build-config
'@esengine/ecs-framework':
specifier: workspace:*
version: link:../core
version: link:../core/dist
'@types/node':
specifier: ^20.19.17
version: 20.19.27
@@ -1474,6 +1474,7 @@ importers:
typescript-eslint:
specifier: ^8.46.1
version: 8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
publishDirectory: dist
packages/framework/fsm:
dependencies:
@@ -1489,7 +1490,7 @@ importers:
version: link:../../tools/build-config
'@esengine/ecs-framework':
specifier: workspace:*
version: link:../core
version: link:../core/dist
'@types/node':
specifier: ^20.19.17
version: 20.19.27
@@ -1559,7 +1560,7 @@ importers:
version: link:../../tools/build-config
'@esengine/ecs-framework':
specifier: workspace:*
version: link:../core
version: link:../core/dist
rimraf:
specifier: ^5.0.5
version: 5.0.10
@@ -1599,7 +1600,7 @@ importers:
version: link:../blueprint
'@esengine/ecs-framework':
specifier: workspace:*
version: link:../core
version: link:../core/dist
'@esengine/ecs-framework-math':
specifier: workspace:*
version: link:../math
@@ -1624,7 +1625,7 @@ importers:
version: link:../../tools/build-config
'@esengine/ecs-framework':
specifier: workspace:*
version: link:../core
version: link:../core/dist
'@types/node':
specifier: ^20.19.17
version: 20.19.27
@@ -1652,7 +1653,7 @@ importers:
version: link:../../tools/build-config
'@esengine/ecs-framework':
specifier: workspace:*
version: link:../core
version: link:../core/dist
'@esengine/ecs-framework-math':
specifier: workspace:*
version: link:../math
@@ -1683,7 +1684,7 @@ importers:
version: link:../../tools/build-config
'@esengine/ecs-framework':
specifier: workspace:*
version: link:../core
version: link:../core/dist
'@types/node':
specifier: ^20.19.17
version: 20.19.27
@@ -1705,6 +1706,9 @@ importers:
tsrpc:
specifier: ^3.4.15
version: 3.4.21
ws:
specifier: ^8.18.0
version: 8.18.3
devDependencies:
tsup:
specifier: ^8.5.1
@@ -1727,7 +1731,7 @@ importers:
version: link:../../tools/build-config
'@esengine/ecs-framework':
specifier: workspace:*
version: link:../../framework/core
version: link:../../framework/core/dist
'@esengine/ecs-framework-math':
specifier: workspace:*
version: link:../../framework/math
@@ -1763,7 +1767,7 @@ importers:
version: link:../../tools/build-config
'@esengine/ecs-framework':
specifier: workspace:*
version: link:../../framework/core
version: link:../../framework/core/dist
'@esengine/engine-core':
specifier: workspace:*
version: link:../../engine/engine-core
@@ -1784,7 +1788,7 @@ importers:
version: link:../../tools/build-config
'@esengine/ecs-framework':
specifier: workspace:*
version: link:../../framework/core
version: link:../../framework/core/dist
'@esengine/ecs-framework-math':
specifier: workspace:*
version: link:../../framework/math
@@ -1815,7 +1819,7 @@ importers:
version: link:../../tools/build-config
'@esengine/ecs-framework':
specifier: workspace:*
version: link:../../framework/core
version: link:../../framework/core/dist
'@types/node':
specifier: ^20.19.17
version: 20.19.27
@@ -1846,7 +1850,7 @@ importers:
version: link:../../engine/ecs-engine-bindgen
'@esengine/ecs-framework':
specifier: workspace:*
version: link:../../framework/core
version: link:../../framework/core/dist
'@esengine/engine-core':
specifier: workspace:*
version: link:../../engine/engine-core
@@ -1876,7 +1880,7 @@ importers:
version: link:../../engine/ecs-engine-bindgen
'@esengine/ecs-framework':
specifier: workspace:*
version: link:../../framework/core
version: link:../../framework/core/dist
'@esengine/engine-core':
specifier: workspace:*
version: link:../../engine/engine-core
@@ -1907,7 +1911,7 @@ importers:
version: link:../../engine/ecs-engine-bindgen
'@esengine/ecs-framework':
specifier: workspace:*
version: link:../../framework/core
version: link:../../framework/core/dist
'@esengine/ecs-framework-math':
specifier: workspace:*
version: link:../../framework/math
@@ -1937,7 +1941,7 @@ importers:
version: link:../../tools/build-config
'@esengine/ecs-framework':
specifier: workspace:*
version: link:../../framework/core
version: link:../../framework/core/dist
'@esengine/engine-core':
specifier: workspace:*
version: link:../../engine/engine-core
@@ -1971,7 +1975,7 @@ importers:
version: link:../../engine/ecs-engine-bindgen
'@esengine/ecs-framework':
specifier: workspace:*
version: link:../../framework/core
version: link:../../framework/core/dist
'@esengine/ecs-framework-math':
specifier: workspace:*
version: link:../../framework/math
@@ -2001,7 +2005,7 @@ importers:
dependencies:
'@esengine/ecs-framework':
specifier: workspace:*
version: link:../../framework/core
version: link:../../framework/core/dist
'@esengine/engine-core':
specifier: workspace:*
version: link:../../engine/engine-core
@@ -2056,6 +2060,31 @@ importers:
specifier: ^5.8.3
version: 5.9.3
packages/tools/demos:
dependencies:
'@esengine/fsm':
specifier: workspace:*
version: link:../../framework/fsm
'@esengine/pathfinding':
specifier: workspace:*
version: link:../../framework/pathfinding
'@esengine/procgen':
specifier: workspace:*
version: link:../../framework/procgen
'@esengine/spatial':
specifier: workspace:*
version: link:../../framework/spatial
'@esengine/timer':
specifier: workspace:*
version: link:../../framework/timer
devDependencies:
tsx:
specifier: ^4.7.0
version: 4.21.0
typescript:
specifier: ^5.8.3
version: 5.9.3
packages/tools/sdk:
dependencies:
'@esengine/asset-system':
@@ -2072,7 +2101,7 @@ importers:
version: link:../../rendering/camera
'@esengine/ecs-framework':
specifier: workspace:*
version: link:../../framework/core
version: link:../../framework/core/dist
'@esengine/ecs-framework-math':
specifier: workspace:*
version: link:../../framework/math