Compare commits
125 Commits
@esengine/
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6aef13d93 | ||
|
|
470abb8750 | ||
|
|
2f95758911 | ||
|
|
2e9f36b656 | ||
|
|
c188a36f2b | ||
|
|
50681553b5 | ||
|
|
4e66bd8e2b | ||
|
|
7caa69a22e | ||
|
|
5a5daf7565 | ||
|
|
3415737fcc | ||
|
|
876312deb2 | ||
|
|
a790fc9e92 | ||
|
|
fa593a3c69 | ||
|
|
e7d95dfdaf | ||
|
|
bffe90b6a1 | ||
|
|
e90a42b1c9 | ||
|
|
30173f0764 | ||
|
|
12da6bd609 | ||
|
|
6b5b4efa72 | ||
|
|
51334dfc50 | ||
|
|
2035355e22 | ||
|
|
9e5f037d5d | ||
|
|
43be62b4cb | ||
|
|
c902dd7291 | ||
|
|
0d33cf0097 | ||
|
|
45de62e453 | ||
|
|
b983cbf87a | ||
|
|
34583b23af | ||
|
|
f2c3a24404 | ||
|
|
3bfb8a1c9b | ||
|
|
2ee8d87647 | ||
|
|
2d537dc10c | ||
|
|
c2acd14fce | ||
|
|
7f631793d4 | ||
|
|
2e84942ea1 | ||
|
|
d0057333a7 | ||
|
|
54c8ff4d8f | ||
|
|
caf3be72cd | ||
|
|
ec3e449681 | ||
|
|
b95a46edaf | ||
|
|
f493f2d6cc | ||
|
|
6970394717 | ||
|
|
0e4b66aac4 | ||
|
|
7399e91a5b | ||
|
|
c84addaa0b | ||
|
|
61da38faf5 | ||
|
|
f333b81298 | ||
|
|
69bb6bd946 | ||
|
|
3b6fc8266f | ||
|
|
db22bd3028 | ||
|
|
b80e967829 | ||
|
|
9e87eb39b9 | ||
|
|
ff549f3c2a | ||
|
|
15c1d98305 | ||
|
|
4a3d8c3962 | ||
|
|
0f5aa633d8 | ||
|
|
85171a0a5c | ||
|
|
35d81880a7 | ||
|
|
71022abc99 | ||
|
|
87f71e2251 | ||
|
|
b9ea8d14cf | ||
|
|
10d0fb1d5c | ||
|
|
71e111415f | ||
|
|
0de45279e6 | ||
|
|
cc6f12d470 | ||
|
|
902c0a1074 | ||
|
|
d3e489aad3 | ||
|
|
12051d987f | ||
|
|
b38fe5ebf4 | ||
|
|
f01ce1e320 | ||
|
|
094133a71a | ||
|
|
3e5b7783be | ||
|
|
ebcb4d00a8 | ||
|
|
d2af9caae9 | ||
|
|
bb696c6a60 | ||
|
|
ffd35a71cd | ||
|
|
1f3a76aabe | ||
|
|
ddc7d1f726 | ||
|
|
04b08f3f07 | ||
|
|
d9969d0b08 | ||
|
|
bdbbf8a80a | ||
|
|
1368473c71 | ||
|
|
b28169b186 | ||
|
|
e2598b2292 | ||
|
|
2e3889abed | ||
|
|
d21caa974e | ||
|
|
a08a84b7db | ||
|
|
449bd420a6 | ||
|
|
1f297ac769 | ||
|
|
4cf868a769 | ||
|
|
afdeb00b4d | ||
|
|
764ce67742 | ||
|
|
61a13baca2 | ||
|
|
1cfa64aa0f | ||
|
|
3b978384c7 | ||
|
|
10c3891abd | ||
|
|
18af48a0fc | ||
|
|
d4cef828e1 | ||
|
|
2d46ccf896 | ||
|
|
fb8bde6485 | ||
|
|
30437dc5d5 | ||
|
|
9f84c2f870 | ||
|
|
e9ea52d9b3 | ||
|
|
0662b07445 | ||
|
|
838cda91aa | ||
|
|
a000cc07d7 | ||
|
|
1316d7de49 | ||
|
|
9c41181875 | ||
|
|
9f3f9a547a | ||
|
|
18df9d1cda | ||
|
|
9a4b3388e0 | ||
|
|
66d5dc27f7 | ||
|
|
8a3e54cb45 | ||
|
|
b6f1235239 | ||
|
|
41529f6fbb | ||
|
|
7dadacc8f9 | ||
|
|
7940f581a6 | ||
|
|
8605888f11 | ||
|
|
d57a007a42 | ||
|
|
89cdfe396b | ||
|
|
fac4bc19c5 | ||
|
|
aed91dbe45 | ||
|
|
c7f8208b6f | ||
|
|
5131ec3c52 | ||
|
|
7d74623710 |
@@ -49,7 +49,6 @@
|
||||
"@esengine/material-editor",
|
||||
"@esengine/shader-editor",
|
||||
"@esengine/world-streaming-editor",
|
||||
"@esengine/node-editor",
|
||||
"@esengine/sdk",
|
||||
"@esengine/worker-generator",
|
||||
"@esengine/engine"
|
||||
|
||||
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@@ -63,15 +63,16 @@ jobs:
|
||||
- name: Build framework packages
|
||||
run: |
|
||||
pnpm --filter @esengine/ecs-framework build
|
||||
pnpm --filter @esengine/blueprint build
|
||||
pnpm --filter @esengine/ecs-framework-math build
|
||||
pnpm --filter @esengine/behavior-tree build
|
||||
pnpm --filter @esengine/blueprint build
|
||||
pnpm --filter @esengine/fsm build
|
||||
pnpm --filter @esengine/timer build
|
||||
pnpm --filter @esengine/spatial build
|
||||
pnpm --filter @esengine/procgen build
|
||||
pnpm --filter @esengine/pathfinding build
|
||||
pnpm --filter @esengine/network-protocols build
|
||||
pnpm --filter @esengine/rpc build
|
||||
pnpm --filter @esengine/network build
|
||||
|
||||
# 类型检查 (仅 framework 包)
|
||||
|
||||
2
.github/workflows/docs.yml
vendored
2
.github/workflows/docs.yml
vendored
@@ -56,7 +56,7 @@ jobs:
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: docs/.vitepress/dist
|
||||
path: docs/dist
|
||||
|
||||
deploy:
|
||||
environment:
|
||||
|
||||
9
.github/workflows/release-changesets.yml
vendored
9
.github/workflows/release-changesets.yml
vendored
@@ -45,17 +45,24 @@ jobs:
|
||||
run: |
|
||||
# Only build packages managed by Changesets (not in ignore list)
|
||||
pnpm --filter "@esengine/ecs-framework" build
|
||||
pnpm --filter "@esengine/blueprint" build
|
||||
pnpm --filter "@esengine/ecs-framework-math" build
|
||||
pnpm --filter "@esengine/behavior-tree" build
|
||||
pnpm --filter "@esengine/blueprint" build
|
||||
pnpm --filter "@esengine/fsm" build
|
||||
pnpm --filter "@esengine/timer" build
|
||||
pnpm --filter "@esengine/spatial" build
|
||||
pnpm --filter "@esengine/procgen" build
|
||||
pnpm --filter "@esengine/pathfinding" build
|
||||
pnpm --filter "@esengine/network-protocols" build
|
||||
pnpm --filter "@esengine/rpc" build
|
||||
pnpm --filter "@esengine/network" build
|
||||
pnpm --filter "@esengine/server" build
|
||||
pnpm --filter "@esengine/database-drivers" build
|
||||
pnpm --filter "@esengine/database" build
|
||||
pnpm --filter "@esengine/transaction" build
|
||||
pnpm --filter "@esengine/cli" build
|
||||
pnpm --filter "create-esengine-server" build
|
||||
pnpm --filter "@esengine/node-editor" build
|
||||
|
||||
- name: Create Release Pull Request or Publish
|
||||
id: changesets
|
||||
|
||||
22
README.md
22
README.md
@@ -49,7 +49,12 @@ npm install @esengine/ecs-framework
|
||||
| **Timer** | Timer and cooldown systems | No |
|
||||
| **Spatial** | Spatial indexing and queries (QuadTree, Grid) | No |
|
||||
| **Pathfinding** | A* and navigation mesh pathfinding | No |
|
||||
| **Network** | Client/server networking with TSRPC | No |
|
||||
| **Procgen** | Procedural generation (noise, random, sampling) | No |
|
||||
| **RPC** | High-performance RPC communication framework | No |
|
||||
| **Server** | Game server framework with rooms, auth, rate limiting | No |
|
||||
| **Network** | Client networking with prediction, AOI, delta compression | No |
|
||||
| **Transaction** | Game transaction system with Redis/Memory storage | No |
|
||||
| **World Streaming** | Open world chunk loading and streaming | No |
|
||||
|
||||
> All framework modules can be used standalone with any rendering engine.
|
||||
|
||||
@@ -199,7 +204,12 @@ npm install @esengine/fsm # State machines
|
||||
npm install @esengine/timer # Timers & cooldowns
|
||||
npm install @esengine/spatial # Spatial indexing
|
||||
npm install @esengine/pathfinding # Pathfinding
|
||||
npm install @esengine/network # Networking
|
||||
npm install @esengine/procgen # Procedural generation
|
||||
npm install @esengine/rpc # RPC framework
|
||||
npm install @esengine/server # Game server
|
||||
npm install @esengine/network # Client networking
|
||||
npm install @esengine/transaction # Transaction system
|
||||
npm install @esengine/world-streaming # World streaming
|
||||
```
|
||||
|
||||
### ESEngine Runtime (Optional)
|
||||
@@ -218,6 +228,7 @@ If you want a complete engine solution with rendering:
|
||||
A visual editor built with Tauri for scene management:
|
||||
|
||||
- Download from [Releases](https://github.com/esengine/esengine/releases)
|
||||
- [Build from source](./packages/editor/editor-app/README.md)
|
||||
- Supports behavior tree editing, tilemap painting, visual scripting
|
||||
|
||||
## Project Structure
|
||||
@@ -235,7 +246,11 @@ esengine/
|
||||
│ │ ├── spatial/ # Spatial queries
|
||||
│ │ ├── pathfinding/ # Pathfinding
|
||||
│ │ ├── procgen/ # Procedural generation
|
||||
│ │ └── network/ # Networking
|
||||
│ │ ├── rpc/ # RPC framework
|
||||
│ │ ├── server/ # Game server
|
||||
│ │ ├── network/ # Client networking
|
||||
│ │ ├── transaction/ # Transaction system
|
||||
│ │ └── world-streaming/ # World streaming
|
||||
│ │
|
||||
│ ├── engine/ # ESEngine runtime
|
||||
│ ├── rendering/ # Rendering modules
|
||||
@@ -267,6 +282,7 @@ pnpm test
|
||||
|
||||
- [ECS Framework Guide](./packages/framework/core/README.md)
|
||||
- [Behavior Tree Guide](./packages/framework/behavior-tree/README.md)
|
||||
- [Editor Setup Guide](./packages/editor/editor-app/README.md) ([中文](./packages/editor/editor-app/README_CN.md))
|
||||
- [API Reference](https://esengine.cn/api/README)
|
||||
|
||||
## Community
|
||||
|
||||
22
README_CN.md
22
README_CN.md
@@ -49,7 +49,12 @@ npm install @esengine/ecs-framework
|
||||
| **定时器** | 定时器和冷却系统 | 否 |
|
||||
| **空间索引** | 空间查询(四叉树、网格) | 否 |
|
||||
| **寻路** | A* 和导航网格寻路 | 否 |
|
||||
| **网络** | 客户端/服务端网络通信 (TSRPC) | 否 |
|
||||
| **程序化生成** | 噪声、随机、采样等生成算法 | 否 |
|
||||
| **RPC** | 高性能 RPC 通信框架 | 否 |
|
||||
| **服务端** | 游戏服务器框架,支持房间、认证、速率限制 | 否 |
|
||||
| **网络** | 客户端网络,支持预测、AOI、增量压缩 | 否 |
|
||||
| **事务系统** | 游戏事务系统,支持 Redis/内存存储 | 否 |
|
||||
| **世界流送** | 开放世界分块加载和流送 | 否 |
|
||||
|
||||
> 所有框架模块都可以独立使用,无需依赖特定渲染引擎。
|
||||
|
||||
@@ -199,7 +204,12 @@ npm install @esengine/fsm # 状态机
|
||||
npm install @esengine/timer # 定时器和冷却
|
||||
npm install @esengine/spatial # 空间索引
|
||||
npm install @esengine/pathfinding # 寻路
|
||||
npm install @esengine/network # 网络
|
||||
npm install @esengine/procgen # 程序化生成
|
||||
npm install @esengine/rpc # RPC 框架
|
||||
npm install @esengine/server # 游戏服务器
|
||||
npm install @esengine/network # 客户端网络
|
||||
npm install @esengine/transaction # 事务系统
|
||||
npm install @esengine/world-streaming # 世界流送
|
||||
```
|
||||
|
||||
### ESEngine 运行时(可选)
|
||||
@@ -218,6 +228,7 @@ npm install @esengine/network # 网络
|
||||
基于 Tauri 构建的可视化编辑器:
|
||||
|
||||
- 从 [Releases](https://github.com/esengine/esengine/releases) 下载
|
||||
- [从源码构建](./packages/editor/editor-app/README.md)
|
||||
- 支持行为树编辑、Tilemap 绘制、可视化脚本
|
||||
|
||||
## 项目结构
|
||||
@@ -235,7 +246,11 @@ esengine/
|
||||
│ │ ├── spatial/ # 空间查询
|
||||
│ │ ├── pathfinding/ # 寻路
|
||||
│ │ ├── procgen/ # 程序化生成
|
||||
│ │ └── network/ # 网络
|
||||
│ │ ├── rpc/ # RPC 框架
|
||||
│ │ ├── server/ # 游戏服务器
|
||||
│ │ ├── network/ # 客户端网络
|
||||
│ │ ├── transaction/ # 事务系统
|
||||
│ │ └── world-streaming/ # 世界流送
|
||||
│ │
|
||||
│ ├── engine/ # ESEngine 运行时
|
||||
│ ├── rendering/ # 渲染模块
|
||||
@@ -267,6 +282,7 @@ pnpm test
|
||||
|
||||
- [ECS 框架指南](./packages/framework/core/README.md)
|
||||
- [行为树指南](./packages/framework/behavior-tree/README.md)
|
||||
- [编辑器启动指南](./packages/editor/editor-app/README_CN.md) ([English](./packages/editor/editor-app/README.md))
|
||||
- [API 参考](https://esengine.cn/api/README)
|
||||
|
||||
## 社区
|
||||
|
||||
21
docs/.gitignore
vendored
Normal file
21
docs/.gitignore
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# build output
|
||||
dist/
|
||||
# generated types
|
||||
.astro/
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
@@ -1,329 +0,0 @@
|
||||
import { defineConfig } from 'vitepress'
|
||||
import Icons from 'unplugin-icons/vite'
|
||||
import { readFileSync } from 'fs'
|
||||
import { join, dirname } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const corePackageJson = JSON.parse(
|
||||
readFileSync(join(__dirname, '../../packages/framework/core/package.json'), 'utf-8')
|
||||
)
|
||||
|
||||
// Import i18n messages
|
||||
import en from './i18n/en.json' with { type: 'json' }
|
||||
import zh from './i18n/zh.json' with { type: 'json' }
|
||||
|
||||
// 创建侧边栏配置 | Create sidebar config
|
||||
// prefix: 路径前缀,如 '' 或 '/en' | Path prefix like '' or '/en'
|
||||
function createSidebar(t, prefix = '') {
|
||||
return {
|
||||
[`${prefix}/guide/`]: [
|
||||
{
|
||||
text: t.sidebar.gettingStarted,
|
||||
items: [
|
||||
{ text: t.sidebar.quickStart, link: `${prefix}/guide/getting-started` },
|
||||
{ text: t.sidebar.guideOverview, link: `${prefix}/guide/` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.coreConcepts,
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: t.sidebar.entity, link: `${prefix}/guide/entity` },
|
||||
{ text: t.sidebar.hierarchy, link: `${prefix}/guide/hierarchy` },
|
||||
{ text: t.sidebar.component, link: `${prefix}/guide/component` },
|
||||
{ text: t.sidebar.entityQuery, link: `${prefix}/guide/entity-query` },
|
||||
{
|
||||
text: t.sidebar.system,
|
||||
link: `${prefix}/guide/system`,
|
||||
items: [
|
||||
{ text: t.sidebar.workerSystem, link: `${prefix}/guide/worker-system` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.scene,
|
||||
link: `${prefix}/guide/scene`,
|
||||
items: [
|
||||
{ text: t.sidebar.sceneManager, link: `${prefix}/guide/scene-manager` },
|
||||
{ text: t.sidebar.worldManager, link: `${prefix}/guide/world-manager` },
|
||||
{ text: t.sidebar.persistentEntity, link: `${prefix}/guide/persistent-entity` }
|
||||
]
|
||||
},
|
||||
{ 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` },
|
||||
{ text: t.sidebar.logging, link: `${prefix}/guide/logging` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.advancedFeatures,
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: t.sidebar.serviceContainer, link: `${prefix}/guide/service-container` },
|
||||
{ text: t.sidebar.pluginSystem, link: `${prefix}/guide/plugin-system` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.platformAdapters,
|
||||
link: `${prefix}/guide/platform-adapter`,
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: t.sidebar.browserAdapter, link: `${prefix}/guide/platform-adapter/browser` },
|
||||
{ text: t.sidebar.wechatAdapter, link: `${prefix}/guide/platform-adapter/wechat-minigame` },
|
||||
{ text: t.sidebar.nodejsAdapter, link: `${prefix}/guide/platform-adapter/nodejs` }
|
||||
]
|
||||
}
|
||||
],
|
||||
// 模块总览侧边栏 | 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,
|
||||
items: [
|
||||
{ text: t.sidebar.examplesOverview, link: `${prefix}/examples/` },
|
||||
{ text: t.nav.workerDemo, link: `${prefix}/examples/worker-system-demo` }
|
||||
]
|
||||
}
|
||||
],
|
||||
[`${prefix}/api/`]: [
|
||||
{
|
||||
text: t.sidebar.apiReference,
|
||||
items: [
|
||||
{ text: t.sidebar.overview, link: `${prefix}/api/README` },
|
||||
{
|
||||
text: t.sidebar.coreClasses,
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: 'Core', link: `${prefix}/api/classes/Core` },
|
||||
{ text: 'Scene', link: `${prefix}/api/classes/Scene` },
|
||||
{ text: 'World', link: `${prefix}/api/classes/World` },
|
||||
{ text: 'Entity', link: `${prefix}/api/classes/Entity` },
|
||||
{ text: 'Component', link: `${prefix}/api/classes/Component` },
|
||||
{ text: 'EntitySystem', link: `${prefix}/api/classes/EntitySystem` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.systemClasses,
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'PassiveSystem', link: `${prefix}/api/classes/PassiveSystem` },
|
||||
{ text: 'ProcessingSystem', link: `${prefix}/api/classes/ProcessingSystem` },
|
||||
{ text: 'IntervalSystem', link: `${prefix}/api/classes/IntervalSystem` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.utilities,
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Matcher', link: `${prefix}/api/classes/Matcher` },
|
||||
{ text: 'Time', link: `${prefix}/api/classes/Time` },
|
||||
{ text: 'PerformanceMonitor', link: `${prefix}/api/classes/PerformanceMonitor` },
|
||||
{ text: 'DebugManager', link: `${prefix}/api/classes/DebugManager` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.interfaces,
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'IScene', link: `${prefix}/api/interfaces/IScene` },
|
||||
{ text: 'IComponent', link: `${prefix}/api/interfaces/IComponent` },
|
||||
{ text: 'ISystemBase', link: `${prefix}/api/interfaces/ISystemBase` },
|
||||
{ text: 'ICoreConfig', link: `${prefix}/api/interfaces/ICoreConfig` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.decorators,
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: '@ECSComponent', link: `${prefix}/api/functions/ECSComponent` },
|
||||
{ text: '@ECSSystem', link: `${prefix}/api/functions/ECSSystem` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.enums,
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'ECSEventType', link: `${prefix}/api/enumerations/ECSEventType` },
|
||||
{ text: 'LogLevel', link: `${prefix}/api/enumerations/LogLevel` }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// 创建导航配置 | Create nav config
|
||||
// prefix: 路径前缀,如 '' 或 '/en' | Path prefix like '' or '/en'
|
||||
function createNav(t, prefix = '') {
|
||||
return [
|
||||
{ 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,
|
||||
items: [
|
||||
{ text: t.nav.workerDemo, link: `${prefix}/examples/worker-system-demo` },
|
||||
{ text: t.nav.lawnMowerDemo, link: 'https://github.com/esengine/lawn-mower-demo' }
|
||||
]
|
||||
},
|
||||
{ text: t.nav.changelog, link: `${prefix}/changelog` },
|
||||
{
|
||||
text: `v${corePackageJson.version}`,
|
||||
link: 'https://github.com/esengine/esengine/releases'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
vite: {
|
||||
plugins: [
|
||||
Icons({
|
||||
compiler: 'vue3',
|
||||
autoInstall: true
|
||||
})
|
||||
],
|
||||
server: {
|
||||
fs: {
|
||||
allow: ['..']
|
||||
},
|
||||
middlewareMode: false,
|
||||
headers: {
|
||||
'Cross-Origin-Embedder-Policy': 'require-corp',
|
||||
'Cross-Origin-Opener-Policy': 'same-origin'
|
||||
}
|
||||
}
|
||||
},
|
||||
title: 'ESEngine',
|
||||
appearance: 'force-dark',
|
||||
|
||||
locales: {
|
||||
root: {
|
||||
label: '简体中文',
|
||||
lang: 'zh-CN',
|
||||
description: '高性能 TypeScript ECS 框架 - 为游戏开发而生',
|
||||
themeConfig: {
|
||||
nav: createNav(zh, ''),
|
||||
sidebar: createSidebar(zh, ''),
|
||||
editLink: {
|
||||
pattern: 'https://github.com/esengine/esengine/edit/master/docs/:path',
|
||||
text: zh.common.editOnGithub
|
||||
},
|
||||
outline: {
|
||||
level: [2, 3],
|
||||
label: zh.common.onThisPage
|
||||
}
|
||||
}
|
||||
},
|
||||
en: {
|
||||
label: 'English',
|
||||
lang: 'en',
|
||||
link: '/en/',
|
||||
description: 'High-performance TypeScript ECS Framework for Game Development',
|
||||
themeConfig: {
|
||||
nav: createNav(en, '/en'),
|
||||
sidebar: createSidebar(en, '/en'),
|
||||
editLink: {
|
||||
pattern: 'https://github.com/esengine/esengine/edit/master/docs/:path',
|
||||
text: en.common.editOnGithub
|
||||
},
|
||||
outline: {
|
||||
level: [2, 3],
|
||||
label: en.common.onThisPage
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
themeConfig: {
|
||||
siteTitle: 'ESEngine',
|
||||
|
||||
socialLinks: [
|
||||
{ icon: 'github', link: 'https://github.com/esengine/esengine' }
|
||||
],
|
||||
|
||||
footer: {
|
||||
message: 'Released under the MIT License.',
|
||||
copyright: 'Copyright © 2025 ECS Framework'
|
||||
},
|
||||
|
||||
search: {
|
||||
provider: 'local'
|
||||
}
|
||||
},
|
||||
|
||||
head: [
|
||||
['meta', { name: 'theme-color', content: '#646cff' }],
|
||||
['link', { rel: 'icon', href: '/favicon.ico' }]
|
||||
],
|
||||
|
||||
base: '/',
|
||||
cleanUrls: true,
|
||||
ignoreDeadLinks: true,
|
||||
|
||||
markdown: {
|
||||
lineNumbers: true,
|
||||
theme: {
|
||||
light: 'github-light',
|
||||
dark: 'github-dark'
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,107 +0,0 @@
|
||||
{
|
||||
"nav": {
|
||||
"home": "Home",
|
||||
"quickStart": "Quick Start",
|
||||
"guide": "Guide",
|
||||
"modules": "Modules",
|
||||
"api": "API",
|
||||
"examples": "Examples",
|
||||
"workerDemo": "Worker System Demo",
|
||||
"lawnMowerDemo": "Lawn Mower Demo",
|
||||
"changelog": "Changelog"
|
||||
},
|
||||
"sidebar": {
|
||||
"gettingStarted": "Getting Started",
|
||||
"quickStart": "Quick Start",
|
||||
"guideOverview": "Guide Overview",
|
||||
"coreConcepts": "Core Concepts",
|
||||
"entity": "Entity",
|
||||
"hierarchy": "Hierarchy",
|
||||
"component": "Component",
|
||||
"entityQuery": "Entity Query",
|
||||
"system": "System",
|
||||
"workerSystem": "Worker System (Multithreading)",
|
||||
"scene": "Scene",
|
||||
"sceneManager": "SceneManager",
|
||||
"worldManager": "WorldManager",
|
||||
"persistentEntity": "Persistent Entity",
|
||||
"behaviorTree": "Behavior Tree",
|
||||
"btGettingStarted": "Getting Started",
|
||||
"btCoreConcepts": "Core Concepts",
|
||||
"btEditorGuide": "Editor Guide",
|
||||
"btEditorWorkflow": "Editor Workflow",
|
||||
"btCustomActions": "Custom Actions",
|
||||
"btCocosIntegration": "Cocos Creator Integration",
|
||||
"btLayaIntegration": "Laya Engine Integration",
|
||||
"btAdvancedUsage": "Advanced Usage",
|
||||
"btBestPractices": "Best Practices",
|
||||
"serialization": "Serialization",
|
||||
"eventSystem": "Event System",
|
||||
"timeAndTimers": "Time and Timers",
|
||||
"logging": "Logging",
|
||||
"advancedFeatures": "Advanced Features",
|
||||
"serviceContainer": "Service Container",
|
||||
"pluginSystem": "Plugin System",
|
||||
"platformAdapters": "Platform Adapters",
|
||||
"browserAdapter": "Browser Adapter",
|
||||
"wechatAdapter": "WeChat Mini Game Adapter",
|
||||
"nodejsAdapter": "Node.js Adapter",
|
||||
"examples": "Examples",
|
||||
"examplesOverview": "Examples Overview",
|
||||
"apiReference": "API Reference",
|
||||
"overview": "Overview",
|
||||
"coreClasses": "Core Classes",
|
||||
"systemClasses": "System Classes",
|
||||
"utilities": "Utilities",
|
||||
"interfaces": "Interfaces",
|
||||
"decorators": "Decorators",
|
||||
"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",
|
||||
"quickLinks": "Quick Links",
|
||||
"viewDocs": "View Docs",
|
||||
"getStarted": "Get Started",
|
||||
"getStartedDesc": "From installation to your first ECS app, learn the core concepts in 5 minutes.",
|
||||
"aiSystem": "AI System",
|
||||
"behaviorTreeEditor": "Visual Behavior Tree Editor",
|
||||
"behaviorTreeDesc": "Built-in AI behavior tree system with visual editing and real-time debugging.",
|
||||
"coreFeatures": "Core Features",
|
||||
"ecsArchitecture": "High-performance ECS Architecture",
|
||||
"ecsArchitectureDesc": "Data-driven entity component system for large-scale entity processing with cache-friendly memory layout.",
|
||||
"typeSupport": "Full Type Support",
|
||||
"typeSupportDesc": "100% TypeScript with complete type definitions and compile-time checking for the best development experience.",
|
||||
"visualBehaviorTree": "Visual Behavior Tree",
|
||||
"visualBehaviorTreeDesc": "Built-in AI behavior tree system with visual editor, custom nodes, and real-time debugging.",
|
||||
"multiPlatform": "Multi-Platform Support",
|
||||
"multiPlatformDesc": "Support for browsers, Node.js, WeChat Mini Games, and seamless integration with major game engines.",
|
||||
"modularDesign": "Modular Design",
|
||||
"modularDesignDesc": "Core features packaged independently, import only what you need. Support for custom plugin extensions.",
|
||||
"devTools": "Developer Tools",
|
||||
"devToolsDesc": "Built-in performance monitoring, debugging tools, serialization system, and complete development toolchain.",
|
||||
"learnMore": "Learn more →"
|
||||
},
|
||||
"common": {
|
||||
"editOnGithub": "Edit this page on GitHub",
|
||||
"onThisPage": "On this page"
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import en from './en.json'
|
||||
import zh from './zh.json'
|
||||
|
||||
export const messages = { en, zh }
|
||||
|
||||
export type Locale = 'en' | 'zh'
|
||||
|
||||
export function getLocaleMessages(locale: Locale) {
|
||||
return messages[locale] || messages.en
|
||||
}
|
||||
|
||||
// Helper to get nested key value
|
||||
export function t(messages: typeof en, key: string): string {
|
||||
const keys = key.split('.')
|
||||
let result: any = messages
|
||||
for (const k of keys) {
|
||||
result = result?.[k]
|
||||
if (result === undefined) return key
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
{
|
||||
"nav": {
|
||||
"home": "首页",
|
||||
"quickStart": "快速开始",
|
||||
"guide": "指南",
|
||||
"modules": "模块",
|
||||
"api": "API",
|
||||
"examples": "示例",
|
||||
"workerDemo": "Worker系统演示",
|
||||
"lawnMowerDemo": "割草机演示",
|
||||
"changelog": "更新日志"
|
||||
},
|
||||
"sidebar": {
|
||||
"gettingStarted": "开始使用",
|
||||
"quickStart": "快速开始",
|
||||
"guideOverview": "指南概览",
|
||||
"coreConcepts": "核心概念",
|
||||
"entity": "实体类 (Entity)",
|
||||
"hierarchy": "层级系统 (Hierarchy)",
|
||||
"component": "组件系统 (Component)",
|
||||
"entityQuery": "实体查询系统",
|
||||
"system": "系统架构 (System)",
|
||||
"workerSystem": "Worker系统 (多线程)",
|
||||
"scene": "场景管理 (Scene)",
|
||||
"sceneManager": "SceneManager",
|
||||
"worldManager": "WorldManager",
|
||||
"persistentEntity": "持久化实体 (Persistent Entity)",
|
||||
"behaviorTree": "行为树系统 (Behavior Tree)",
|
||||
"btGettingStarted": "快速开始",
|
||||
"btCoreConcepts": "核心概念",
|
||||
"btEditorGuide": "编辑器指南",
|
||||
"btEditorWorkflow": "编辑器工作流",
|
||||
"btCustomActions": "自定义动作组件",
|
||||
"btCocosIntegration": "Cocos Creator集成",
|
||||
"btLayaIntegration": "Laya引擎集成",
|
||||
"btAdvancedUsage": "高级用法",
|
||||
"btBestPractices": "最佳实践",
|
||||
"serialization": "序列化系统 (Serialization)",
|
||||
"eventSystem": "事件系统 (Event)",
|
||||
"timeAndTimers": "时间和定时器 (Time)",
|
||||
"logging": "日志系统 (Logger)",
|
||||
"advancedFeatures": "高级特性",
|
||||
"serviceContainer": "服务容器 (Service Container)",
|
||||
"pluginSystem": "插件系统 (Plugin System)",
|
||||
"platformAdapters": "平台适配器",
|
||||
"browserAdapter": "浏览器适配器",
|
||||
"wechatAdapter": "微信小游戏适配器",
|
||||
"nodejsAdapter": "Node.js适配器",
|
||||
"examples": "示例",
|
||||
"examplesOverview": "示例概览",
|
||||
"apiReference": "API 参考",
|
||||
"overview": "概述",
|
||||
"coreClasses": "核心类",
|
||||
"systemClasses": "系统类",
|
||||
"utilities": "工具类",
|
||||
"interfaces": "接口",
|
||||
"decorators": "装饰器",
|
||||
"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 框架",
|
||||
"quickLinks": "快速入口",
|
||||
"viewDocs": "查看文档",
|
||||
"getStarted": "快速开始",
|
||||
"getStartedDesc": "从安装到创建第一个 ECS 应用,快速了解核心概念。",
|
||||
"aiSystem": "AI 系统",
|
||||
"behaviorTreeEditor": "行为树可视化编辑器",
|
||||
"behaviorTreeDesc": "内置 AI 行为树系统,支持可视化编辑和实时调试。",
|
||||
"coreFeatures": "核心特性",
|
||||
"ecsArchitecture": "高性能 ECS 架构",
|
||||
"ecsArchitectureDesc": "基于数据驱动的实体组件系统,支持大规模实体处理,缓存友好的内存布局。",
|
||||
"typeSupport": "完整类型支持",
|
||||
"typeSupportDesc": "100% TypeScript 编写,完整的类型定义和编译时检查,提供最佳的开发体验。",
|
||||
"visualBehaviorTree": "可视化行为树",
|
||||
"visualBehaviorTreeDesc": "内置 AI 行为树系统,提供可视化编辑器,支持自定义节点和实时调试。",
|
||||
"multiPlatform": "多平台支持",
|
||||
"multiPlatformDesc": "支持浏览器、Node.js、微信小游戏等多平台,可与主流游戏引擎无缝集成。",
|
||||
"modularDesign": "模块化设计",
|
||||
"modularDesignDesc": "核心功能独立打包,按需引入。支持自定义插件扩展,灵活适配不同项目。",
|
||||
"devTools": "开发者工具",
|
||||
"devToolsDesc": "内置性能监控、调试工具、序列化系统等,提供完整的开发工具链。",
|
||||
"learnMore": "了解更多 →"
|
||||
},
|
||||
"common": {
|
||||
"editOnGithub": "在 GitHub 上编辑此页",
|
||||
"onThisPage": "在这个页面上"
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
title: String,
|
||||
description: String,
|
||||
icon: String,
|
||||
link: String,
|
||||
image: String
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a :href="link" class="feature-card">
|
||||
<div class="card-image" v-if="image">
|
||||
<img :src="image" :alt="title" />
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="card-icon" v-if="icon && !image">{{ icon }}</div>
|
||||
<h3 class="card-title">{{ title }}</h3>
|
||||
<p class="card-description">{{ description }}</p>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.feature-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--es-bg-elevated, #252526);
|
||||
border: 1px solid var(--es-border-default, #3e3e42);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
border-color: var(--es-primary, #007acc);
|
||||
background: var(--es-bg-overlay, #2d2d2d);
|
||||
}
|
||||
|
||||
.card-image {
|
||||
width: 100%;
|
||||
height: 160px;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #1e3a5f 0%, #1e1e1e 100%);
|
||||
}
|
||||
|
||||
.card-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.feature-card:hover .card-image img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 16px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 12px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--es-bg-input, #3c3c3c);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--es-text-inverse, #ffffff);
|
||||
margin: 0 0 8px 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.card-description {
|
||||
font-size: 12px;
|
||||
color: var(--es-text-secondary, #9d9d9d);
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -1,422 +0,0 @@
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
const canvasRef = ref(null)
|
||||
let animationId = null
|
||||
let particles = []
|
||||
let animationStartTime = null
|
||||
let glowStartTime = null
|
||||
|
||||
// ESEngine 粒子颜色 - VS Code 风格配色(与编辑器统一)
|
||||
const colors = ['#569CD6', '#4EC9B0', '#9CDCFE', '#C586C0', '#DCDCAA']
|
||||
|
||||
class Particle {
|
||||
constructor(x, y, targetX, targetY) {
|
||||
this.x = x
|
||||
this.y = y
|
||||
this.targetX = targetX
|
||||
this.targetY = targetY
|
||||
this.size = Math.random() * 2 + 1.5
|
||||
this.alpha = Math.random() * 0.5 + 0.5
|
||||
this.color = colors[Math.floor(Math.random() * colors.length)]
|
||||
}
|
||||
}
|
||||
|
||||
function createParticles(canvas, text, fontSize) {
|
||||
const tempCanvas = document.createElement('canvas')
|
||||
const tempCtx = tempCanvas.getContext('2d')
|
||||
if (!tempCtx) return []
|
||||
|
||||
tempCtx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||
const textMetrics = tempCtx.measureText(text)
|
||||
const textWidth = textMetrics.width
|
||||
const textHeight = fontSize
|
||||
|
||||
tempCanvas.width = textWidth + 40
|
||||
tempCanvas.height = textHeight + 40
|
||||
tempCtx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||
tempCtx.textAlign = 'center'
|
||||
tempCtx.textBaseline = 'middle'
|
||||
tempCtx.fillStyle = '#ffffff'
|
||||
tempCtx.fillText(text, tempCanvas.width / 2, tempCanvas.height / 2)
|
||||
|
||||
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height)
|
||||
const pixels = imageData.data
|
||||
const newParticles = []
|
||||
const gap = 3
|
||||
|
||||
const width = canvas.width / (window.devicePixelRatio || 1)
|
||||
const height = canvas.height / (window.devicePixelRatio || 1)
|
||||
const offsetX = (width - tempCanvas.width) / 2
|
||||
const offsetY = (height - tempCanvas.height) / 2
|
||||
|
||||
for (let y = 0; y < tempCanvas.height; y += gap) {
|
||||
for (let x = 0; x < tempCanvas.width; x += gap) {
|
||||
const index = (y * tempCanvas.width + x) * 4
|
||||
const alpha = pixels[index + 3] || 0
|
||||
|
||||
if (alpha > 128) {
|
||||
const angle = Math.random() * Math.PI * 2
|
||||
const distance = Math.random() * Math.max(width, height)
|
||||
|
||||
newParticles.push(new Particle(
|
||||
width / 2 + Math.cos(angle) * distance,
|
||||
height / 2 + Math.sin(angle) * distance,
|
||||
offsetX + x,
|
||||
offsetY + y
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newParticles
|
||||
}
|
||||
|
||||
function easeOutQuart(t) {
|
||||
return 1 - Math.pow(1 - t, 4)
|
||||
}
|
||||
|
||||
function easeOutCubic(t) {
|
||||
return 1 - Math.pow(1 - t, 3)
|
||||
}
|
||||
|
||||
function animate(canvas, ctx) {
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
const width = canvas.width / dpr
|
||||
const height = canvas.height / dpr
|
||||
|
||||
const currentTime = performance.now()
|
||||
const duration = 2500
|
||||
const glowDuration = 600
|
||||
|
||||
const elapsed = currentTime - animationStartTime
|
||||
const progress = Math.min(elapsed / duration, 1)
|
||||
const easedProgress = easeOutQuart(progress)
|
||||
|
||||
// 透明背景
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
|
||||
// 计算发光进度
|
||||
let glowProgress = 0
|
||||
if (progress >= 1) {
|
||||
if (glowStartTime === null) {
|
||||
glowStartTime = currentTime
|
||||
}
|
||||
glowProgress = Math.min((currentTime - glowStartTime) / glowDuration, 1)
|
||||
glowProgress = easeOutCubic(glowProgress)
|
||||
}
|
||||
|
||||
const text = 'ESEngine'
|
||||
const fontSize = Math.min(width / 4, height / 3, 80)
|
||||
const textY = height / 2
|
||||
|
||||
for (const particle of particles) {
|
||||
const moveProgress = Math.min(easedProgress * 1.2, 1)
|
||||
const currentX = particle.x + (particle.targetX - particle.x) * moveProgress
|
||||
const currentY = particle.y + (particle.targetY - particle.y) * moveProgress
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.arc(currentX, currentY, particle.size, 0, Math.PI * 2)
|
||||
ctx.fillStyle = particle.color
|
||||
ctx.globalAlpha = particle.alpha * (1 - glowProgress * 0.3)
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
ctx.globalAlpha = 1
|
||||
|
||||
if (glowProgress > 0) {
|
||||
ctx.save()
|
||||
ctx.shadowColor = '#3b9eff'
|
||||
ctx.shadowBlur = 30 * glowProgress
|
||||
ctx.fillStyle = `rgba(255, 255, 255, ${glowProgress})`
|
||||
ctx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillText(text, width / 2, textY)
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
if (glowProgress >= 1) {
|
||||
const breathe = 0.8 + Math.sin(currentTime / 1000) * 0.2
|
||||
ctx.save()
|
||||
ctx.shadowColor = '#3b9eff'
|
||||
ctx.shadowBlur = 20 * breathe
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillText(text, width / 2, textY)
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
animationId = requestAnimationFrame(() => animate(canvas, ctx))
|
||||
}
|
||||
|
||||
function initCanvas() {
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas) return
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
const container = canvas.parentElement
|
||||
const width = container.offsetWidth
|
||||
const height = container.offsetHeight
|
||||
|
||||
canvas.width = width * dpr
|
||||
canvas.height = height * dpr
|
||||
canvas.style.width = `${width}px`
|
||||
canvas.style.height = `${height}px`
|
||||
ctx.scale(dpr, dpr)
|
||||
|
||||
const text = 'ESEngine'
|
||||
const fontSize = Math.min(width / 4, height / 3, 80)
|
||||
|
||||
particles = createParticles(canvas, text, fontSize)
|
||||
animationStartTime = performance.now()
|
||||
glowStartTime = null
|
||||
|
||||
if (animationId) {
|
||||
cancelAnimationFrame(animationId)
|
||||
}
|
||||
|
||||
animate(canvas, ctx)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initCanvas()
|
||||
window.addEventListener('resize', initCanvas)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (animationId) {
|
||||
cancelAnimationFrame(animationId)
|
||||
}
|
||||
window.removeEventListener('resize', initCanvas)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="hero-section">
|
||||
<div class="hero-container">
|
||||
<!-- 左侧文字区域 -->
|
||||
<div class="hero-text">
|
||||
<div class="hero-logo">
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="16" cy="16" r="14" stroke="#9147ff" stroke-width="2"/>
|
||||
<path d="M10 10h8v2h-6v3h5v2h-5v3h6v2h-8v-12z" fill="#9147ff"/>
|
||||
</svg>
|
||||
<span>ESENGINE</span>
|
||||
</div>
|
||||
<h1 class="hero-title">
|
||||
我们构建框架。<br/>
|
||||
而你将创造游戏。
|
||||
</h1>
|
||||
<p class="hero-description">
|
||||
ESEngine 是一个高性能的 TypeScript ECS 框架,为游戏开发者提供现代化的实体组件系统。
|
||||
无论是 2D 还是 3D 游戏,都能帮助你快速构建可扩展的游戏架构。
|
||||
</p>
|
||||
<div class="hero-actions">
|
||||
<a href="/guide/getting-started" class="btn-primary">开始使用</a>
|
||||
<a href="https://github.com/esengine/esengine" class="btn-secondary" target="_blank">了解更多</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧粒子动画区域 -->
|
||||
<div class="hero-visual">
|
||||
<div class="visual-container">
|
||||
<canvas ref="canvasRef" class="particle-canvas"></canvas>
|
||||
<div class="visual-label">
|
||||
<span class="label-title">Entity Component System</span>
|
||||
<span class="label-subtitle">High Performance Framework</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.hero-section {
|
||||
background: #0d0d0d;
|
||||
padding: 80px 0;
|
||||
min-height: calc(100vh - 64px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.hero-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 48px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.2fr;
|
||||
gap: 64px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 左侧文字 */
|
||||
.hero-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.hero-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: #ffffff;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
line-height: 1.2;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.hero-description {
|
||||
font-size: 1.125rem;
|
||||
color: #707070;
|
||||
line-height: 1.7;
|
||||
margin: 0;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.btn-primary,
|
||||
.btn-secondary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 14px 28px;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
font-size: 0.9375rem;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #3b9eff;
|
||||
color: #ffffff;
|
||||
border: 1px solid #3b9eff;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #5aadff;
|
||||
border-color: #5aadff;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #1a1a1a;
|
||||
color: #a0a0a0;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #252525;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.hero-visual {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.visual-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
aspect-ratio: 4 / 3;
|
||||
background: linear-gradient(135deg, #1a2a3a 0%, #1a1a1a 50%, #0d0d0d 100%);
|
||||
border-radius: 12px;
|
||||
border: 1px solid #2a2a2a;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20px 60px rgba(59, 158, 255, 0.1);
|
||||
}
|
||||
|
||||
.particle-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.visual-label {
|
||||
position: absolute;
|
||||
bottom: 24px;
|
||||
left: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.label-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.label-subtitle {
|
||||
font-size: 0.875rem;
|
||||
color: #737373;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 1024px) {
|
||||
.hero-container {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 48px;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.hero-section {
|
||||
padding: 48px 0;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 2.25rem;
|
||||
}
|
||||
|
||||
.hero-description {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.visual-container {
|
||||
max-width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.hero-title {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn-primary,
|
||||
.btn-secondary {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,422 +0,0 @@
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
const canvasRef = ref(null)
|
||||
let animationId = null
|
||||
let particles = []
|
||||
let animationStartTime = null
|
||||
let glowStartTime = null
|
||||
|
||||
// ESEngine particle colors - VS Code style colors (unified with editor)
|
||||
const colors = ['#569CD6', '#4EC9B0', '#9CDCFE', '#C586C0', '#DCDCAA']
|
||||
|
||||
class Particle {
|
||||
constructor(x, y, targetX, targetY) {
|
||||
this.x = x
|
||||
this.y = y
|
||||
this.targetX = targetX
|
||||
this.targetY = targetY
|
||||
this.size = Math.random() * 2 + 1.5
|
||||
this.alpha = Math.random() * 0.5 + 0.5
|
||||
this.color = colors[Math.floor(Math.random() * colors.length)]
|
||||
}
|
||||
}
|
||||
|
||||
function createParticles(canvas, text, fontSize) {
|
||||
const tempCanvas = document.createElement('canvas')
|
||||
const tempCtx = tempCanvas.getContext('2d')
|
||||
if (!tempCtx) return []
|
||||
|
||||
tempCtx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||
const textMetrics = tempCtx.measureText(text)
|
||||
const textWidth = textMetrics.width
|
||||
const textHeight = fontSize
|
||||
|
||||
tempCanvas.width = textWidth + 40
|
||||
tempCanvas.height = textHeight + 40
|
||||
tempCtx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||
tempCtx.textAlign = 'center'
|
||||
tempCtx.textBaseline = 'middle'
|
||||
tempCtx.fillStyle = '#ffffff'
|
||||
tempCtx.fillText(text, tempCanvas.width / 2, tempCanvas.height / 2)
|
||||
|
||||
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height)
|
||||
const pixels = imageData.data
|
||||
const newParticles = []
|
||||
const gap = 3
|
||||
|
||||
const width = canvas.width / (window.devicePixelRatio || 1)
|
||||
const height = canvas.height / (window.devicePixelRatio || 1)
|
||||
const offsetX = (width - tempCanvas.width) / 2
|
||||
const offsetY = (height - tempCanvas.height) / 2
|
||||
|
||||
for (let y = 0; y < tempCanvas.height; y += gap) {
|
||||
for (let x = 0; x < tempCanvas.width; x += gap) {
|
||||
const index = (y * tempCanvas.width + x) * 4
|
||||
const alpha = pixels[index + 3] || 0
|
||||
|
||||
if (alpha > 128) {
|
||||
const angle = Math.random() * Math.PI * 2
|
||||
const distance = Math.random() * Math.max(width, height)
|
||||
|
||||
newParticles.push(new Particle(
|
||||
width / 2 + Math.cos(angle) * distance,
|
||||
height / 2 + Math.sin(angle) * distance,
|
||||
offsetX + x,
|
||||
offsetY + y
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newParticles
|
||||
}
|
||||
|
||||
function easeOutQuart(t) {
|
||||
return 1 - Math.pow(1 - t, 4)
|
||||
}
|
||||
|
||||
function easeOutCubic(t) {
|
||||
return 1 - Math.pow(1 - t, 3)
|
||||
}
|
||||
|
||||
function animate(canvas, ctx) {
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
const width = canvas.width / dpr
|
||||
const height = canvas.height / dpr
|
||||
|
||||
const currentTime = performance.now()
|
||||
const duration = 2500
|
||||
const glowDuration = 600
|
||||
|
||||
const elapsed = currentTime - animationStartTime
|
||||
const progress = Math.min(elapsed / duration, 1)
|
||||
const easedProgress = easeOutQuart(progress)
|
||||
|
||||
// Transparent background
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
|
||||
// Calculate glow progress
|
||||
let glowProgress = 0
|
||||
if (progress >= 1) {
|
||||
if (glowStartTime === null) {
|
||||
glowStartTime = currentTime
|
||||
}
|
||||
glowProgress = Math.min((currentTime - glowStartTime) / glowDuration, 1)
|
||||
glowProgress = easeOutCubic(glowProgress)
|
||||
}
|
||||
|
||||
const text = 'ESEngine'
|
||||
const fontSize = Math.min(width / 4, height / 3, 80)
|
||||
const textY = height / 2
|
||||
|
||||
for (const particle of particles) {
|
||||
const moveProgress = Math.min(easedProgress * 1.2, 1)
|
||||
const currentX = particle.x + (particle.targetX - particle.x) * moveProgress
|
||||
const currentY = particle.y + (particle.targetY - particle.y) * moveProgress
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.arc(currentX, currentY, particle.size, 0, Math.PI * 2)
|
||||
ctx.fillStyle = particle.color
|
||||
ctx.globalAlpha = particle.alpha * (1 - glowProgress * 0.3)
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
ctx.globalAlpha = 1
|
||||
|
||||
if (glowProgress > 0) {
|
||||
ctx.save()
|
||||
ctx.shadowColor = '#3b9eff'
|
||||
ctx.shadowBlur = 30 * glowProgress
|
||||
ctx.fillStyle = `rgba(255, 255, 255, ${glowProgress})`
|
||||
ctx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillText(text, width / 2, textY)
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
if (glowProgress >= 1) {
|
||||
const breathe = 0.8 + Math.sin(currentTime / 1000) * 0.2
|
||||
ctx.save()
|
||||
ctx.shadowColor = '#3b9eff'
|
||||
ctx.shadowBlur = 20 * breathe
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillText(text, width / 2, textY)
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
animationId = requestAnimationFrame(() => animate(canvas, ctx))
|
||||
}
|
||||
|
||||
function initCanvas() {
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas) return
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
const container = canvas.parentElement
|
||||
const width = container.offsetWidth
|
||||
const height = container.offsetHeight
|
||||
|
||||
canvas.width = width * dpr
|
||||
canvas.height = height * dpr
|
||||
canvas.style.width = `${width}px`
|
||||
canvas.style.height = `${height}px`
|
||||
ctx.scale(dpr, dpr)
|
||||
|
||||
const text = 'ESEngine'
|
||||
const fontSize = Math.min(width / 4, height / 3, 80)
|
||||
|
||||
particles = createParticles(canvas, text, fontSize)
|
||||
animationStartTime = performance.now()
|
||||
glowStartTime = null
|
||||
|
||||
if (animationId) {
|
||||
cancelAnimationFrame(animationId)
|
||||
}
|
||||
|
||||
animate(canvas, ctx)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initCanvas()
|
||||
window.addEventListener('resize', initCanvas)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (animationId) {
|
||||
cancelAnimationFrame(animationId)
|
||||
}
|
||||
window.removeEventListener('resize', initCanvas)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="hero-section">
|
||||
<div class="hero-container">
|
||||
<!-- Left text area -->
|
||||
<div class="hero-text">
|
||||
<div class="hero-logo">
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="16" cy="16" r="14" stroke="#9147ff" stroke-width="2"/>
|
||||
<path d="M10 10h8v2h-6v3h5v2h-5v3h6v2h-8v-12z" fill="#9147ff"/>
|
||||
</svg>
|
||||
<span>ESENGINE</span>
|
||||
</div>
|
||||
<h1 class="hero-title">
|
||||
We build the framework.<br/>
|
||||
You create the game.
|
||||
</h1>
|
||||
<p class="hero-description">
|
||||
ESEngine is a high-performance TypeScript ECS framework for game developers.
|
||||
Whether 2D or 3D games, it helps you build scalable game architecture quickly.
|
||||
</p>
|
||||
<div class="hero-actions">
|
||||
<a href="/en/guide/getting-started" class="btn-primary">Get Started</a>
|
||||
<a href="https://github.com/esengine/esengine" class="btn-secondary" target="_blank">Learn More</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right particle animation area -->
|
||||
<div class="hero-visual">
|
||||
<div class="visual-container">
|
||||
<canvas ref="canvasRef" class="particle-canvas"></canvas>
|
||||
<div class="visual-label">
|
||||
<span class="label-title">Entity Component System</span>
|
||||
<span class="label-subtitle">High Performance Framework</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.hero-section {
|
||||
background: #0d0d0d;
|
||||
padding: 80px 0;
|
||||
min-height: calc(100vh - 64px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.hero-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 48px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.2fr;
|
||||
gap: 64px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Left text */
|
||||
.hero-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.hero-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: #ffffff;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
line-height: 1.2;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.hero-description {
|
||||
font-size: 1.125rem;
|
||||
color: #707070;
|
||||
line-height: 1.7;
|
||||
margin: 0;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.btn-primary,
|
||||
.btn-secondary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 14px 28px;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
font-size: 0.9375rem;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #3b9eff;
|
||||
color: #ffffff;
|
||||
border: 1px solid #3b9eff;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #5aadff;
|
||||
border-color: #5aadff;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #1a1a1a;
|
||||
color: #a0a0a0;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #252525;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.hero-visual {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.visual-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
aspect-ratio: 4 / 3;
|
||||
background: linear-gradient(135deg, #1a2a3a 0%, #1a1a1a 50%, #0d0d0d 100%);
|
||||
border-radius: 12px;
|
||||
border: 1px solid #2a2a2a;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20px 60px rgba(59, 158, 255, 0.1);
|
||||
}
|
||||
|
||||
.particle-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.visual-label {
|
||||
position: absolute;
|
||||
bottom: 24px;
|
||||
left: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.label-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.label-subtitle {
|
||||
font-size: 0.875rem;
|
||||
color: #737373;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 1024px) {
|
||||
.hero-container {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 48px;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.hero-section {
|
||||
padding: 48px 0;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 2.25rem;
|
||||
}
|
||||
|
||||
.hero-description {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.visual-container {
|
||||
max-width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.hero-title {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn-primary,
|
||||
.btn-secondary {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,595 +0,0 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--vp-nav-height: 64px;
|
||||
|
||||
--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: #1e1e1e;
|
||||
--es-bg-card: #242424;
|
||||
--es-bg-header: #1e1e1e;
|
||||
|
||||
/* 提高文字对比度 | Improve text contrast */
|
||||
--es-text-primary: #e0e0e0;
|
||||
--es-text-secondary: #b0b0b0;
|
||||
--es-text-tertiary: #888888;
|
||||
--es-text-inverse: #ffffff;
|
||||
--es-text-muted: #c0c0c0;
|
||||
--es-text-dim: #888888;
|
||||
|
||||
--es-font-xs: 11px;
|
||||
--es-font-sm: 12px;
|
||||
--es-font-base: 13px;
|
||||
--es-font-md: 14px;
|
||||
--es-font-lg: 16px;
|
||||
|
||||
--es-border-default: #3a3a3a;
|
||||
--es-border-subtle: #1a1a1a;
|
||||
--es-border-strong: #4a4a4a;
|
||||
|
||||
--es-primary: #3b82f6;
|
||||
--es-primary-hover: #2563eb;
|
||||
--es-success: #4ade80;
|
||||
--es-warning: #f59e0b;
|
||||
--es-error: #ef4444;
|
||||
--es-info: #3b82f6;
|
||||
|
||||
--es-selected: #3d5a80;
|
||||
--es-selected-hover: #4a6a90;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--es-bg-base) !important;
|
||||
}
|
||||
|
||||
html,
|
||||
html.dark {
|
||||
--vp-c-bg: var(--es-bg-base);
|
||||
--vp-c-bg-soft: var(--es-bg-elevated);
|
||||
--vp-c-bg-mute: var(--es-bg-overlay);
|
||||
--vp-c-bg-alt: var(--es-bg-sidebar);
|
||||
--vp-c-text-1: var(--es-text-primary);
|
||||
--vp-c-text-2: var(--es-text-tertiary);
|
||||
--vp-c-text-3: var(--es-text-muted);
|
||||
--vp-c-divider: var(--es-border-default);
|
||||
--vp-c-divider-light: var(--es-border-subtle);
|
||||
}
|
||||
|
||||
html:not(.dark) {
|
||||
--vp-c-bg: var(--es-bg-base) !important;
|
||||
--vp-c-bg-soft: var(--es-bg-elevated) !important;
|
||||
--vp-c-bg-mute: var(--es-bg-overlay) !important;
|
||||
--vp-c-bg-alt: var(--es-bg-sidebar) !important;
|
||||
--vp-c-text-1: var(--es-text-primary) !important;
|
||||
--vp-c-text-2: var(--es-text-tertiary) !important;
|
||||
--vp-c-text-3: var(--es-text-muted) !important;
|
||||
}
|
||||
|
||||
.VPNav {
|
||||
background: var(--es-bg-header) !important;
|
||||
border-bottom: 1px solid var(--es-border-subtle) !important;
|
||||
}
|
||||
|
||||
.VPNav .VPNavBar {
|
||||
background: var(--es-bg-header) !important;
|
||||
}
|
||||
|
||||
.VPNav .VPNavBar .wrapper {
|
||||
background: var(--es-bg-header) !important;
|
||||
}
|
||||
|
||||
.VPNav .VPNavBar::before,
|
||||
.VPNav .VPNavBar::after {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.VPNavBar {
|
||||
background: var(--es-bg-header) !important;
|
||||
}
|
||||
|
||||
.VPNavBar::before {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.VPNavBarTitle .title {
|
||||
color: var(--es-text-primary);
|
||||
font-weight: 500;
|
||||
font-size: var(--es-font-base);
|
||||
}
|
||||
|
||||
.VPNavBarMenuLink {
|
||||
color: var(--es-text-secondary) !important;
|
||||
font-size: var(--es-font-sm) !important;
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
|
||||
.VPNavBarMenuLink:hover {
|
||||
color: var(--es-text-primary) !important;
|
||||
}
|
||||
|
||||
.VPNavBarMenuLink.active {
|
||||
color: var(--es-text-primary) !important;
|
||||
}
|
||||
|
||||
.VPNavBarSearch .DocSearch-Button {
|
||||
background: var(--es-bg-input) !important;
|
||||
border: 1px solid var(--es-border-default) !important;
|
||||
border-radius: 2px;
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
.VPSidebar {
|
||||
background: var(--es-bg-sidebar) !important;
|
||||
border-right: 1px solid var(--es-border-subtle) !important;
|
||||
}
|
||||
|
||||
.VPSidebarItem.level-0 > .item {
|
||||
padding: 8px 0 4px 0;
|
||||
}
|
||||
|
||||
.VPSidebarItem.level-0 > .item > .text {
|
||||
font-weight: 600;
|
||||
font-size: var(--es-font-xs);
|
||||
color: var(--es-text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.VPSidebarItem .link {
|
||||
padding: 4px 8px;
|
||||
margin: 1px 0;
|
||||
border-radius: 2px;
|
||||
color: var(--es-text-primary);
|
||||
font-size: var(--es-font-sm);
|
||||
transition: all 0.1s ease;
|
||||
border-left: 2px solid transparent;
|
||||
}
|
||||
|
||||
.VPSidebarItem .link:hover {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
color: var(--es-text-inverse);
|
||||
}
|
||||
|
||||
.VPSidebarItem.is-active > .item > .link {
|
||||
background: var(--es-selected);
|
||||
color: var(--es-text-inverse);
|
||||
border-left: 2px solid var(--es-primary);
|
||||
}
|
||||
|
||||
.VPSidebarItem.is-active > .item > .link:hover {
|
||||
background: var(--es-selected-hover);
|
||||
}
|
||||
|
||||
.VPSidebarItem.level-1 .link {
|
||||
padding-left: 20px;
|
||||
font-size: var(--es-font-sm);
|
||||
}
|
||||
|
||||
.VPSidebarItem.level-2 .link {
|
||||
padding-left: 32px;
|
||||
font-size: var(--es-font-sm);
|
||||
}
|
||||
|
||||
.VPSidebarItem .caret {
|
||||
color: var(--es-text-secondary);
|
||||
}
|
||||
|
||||
.VPSidebarItem .caret:hover {
|
||||
color: var(--es-text-primary);
|
||||
}
|
||||
|
||||
.VPContent {
|
||||
background: var(--es-bg-card) !important;
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
.VPContent.has-sidebar {
|
||||
background: var(--es-bg-card) !important;
|
||||
}
|
||||
|
||||
/* 首页布局修复 | Home page layout fix */
|
||||
.VPPage {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
.Layout > .VPContent {
|
||||
padding-top: var(--vp-nav-height) !important;
|
||||
}
|
||||
|
||||
.VPDoc {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.VPNavBar .content {
|
||||
background: var(--es-bg-header) !important;
|
||||
}
|
||||
|
||||
.VPNavBar .content-body {
|
||||
background: var(--es-bg-header) !important;
|
||||
}
|
||||
|
||||
.VPNavBar .divider {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.VPLocalNav {
|
||||
background: var(--es-bg-header) !important;
|
||||
border-bottom: 1px solid var(--es-border-subtle) !important;
|
||||
}
|
||||
|
||||
.VPNavScreenMenu {
|
||||
background: var(--es-bg-base) !important;
|
||||
}
|
||||
|
||||
.VPNavScreen {
|
||||
background: var(--es-bg-base) !important;
|
||||
}
|
||||
|
||||
.curtain {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.VPNav .curtain,
|
||||
.VPNavBar .curtain {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
[class*="curtain"] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.VPNav > div::before,
|
||||
.VPNav > div::after {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.vp-doc {
|
||||
color: var(--es-text-primary);
|
||||
}
|
||||
|
||||
.vp-doc h1 {
|
||||
font-size: var(--es-font-lg);
|
||||
font-weight: 600;
|
||||
color: var(--es-text-inverse);
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
margin-bottom: 16px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.vp-doc h2 {
|
||||
font-size: var(--es-font-md);
|
||||
font-weight: 600;
|
||||
color: var(--es-text-inverse);
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
margin-top: 32px;
|
||||
margin-bottom: 12px;
|
||||
padding: 6px 12px;
|
||||
background: var(--es-bg-header);
|
||||
border-left: 3px solid var(--es-primary);
|
||||
}
|
||||
|
||||
.vp-doc h3 {
|
||||
font-size: var(--es-font-base);
|
||||
font-weight: 600;
|
||||
color: var(--es-text-primary);
|
||||
margin-top: 20px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.vp-doc p {
|
||||
color: var(--es-text-primary);
|
||||
line-height: 1.7;
|
||||
font-size: var(--es-font-base);
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.vp-doc ul,
|
||||
.vp-doc ol {
|
||||
padding-left: 20px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.vp-doc li {
|
||||
line-height: 1.7;
|
||||
margin: 4px 0;
|
||||
color: var(--es-text-primary);
|
||||
font-size: var(--es-font-base);
|
||||
}
|
||||
|
||||
.vp-doc li::marker {
|
||||
color: var(--es-text-secondary);
|
||||
}
|
||||
|
||||
.vp-doc strong {
|
||||
color: var(--es-text-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.vp-doc a {
|
||||
color: var(--es-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.vp-doc a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.VPDocAside {
|
||||
padding-left: 16px;
|
||||
border-left: 1px solid var(--es-border-subtle);
|
||||
}
|
||||
|
||||
.VPDocAsideOutline {
|
||||
padding: 0;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.VPDocAsideOutline .content {
|
||||
border: none !important;
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
.VPDocAsideOutline .outline-title {
|
||||
font-size: var(--es-font-xs);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--es-text-secondary);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.VPDocAsideOutline .outline-link {
|
||||
color: var(--es-text-secondary);
|
||||
font-size: var(--es-font-xs);
|
||||
padding: 4px 0;
|
||||
line-height: 1.4;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.VPDocAsideOutline .outline-link:hover {
|
||||
color: var(--es-text-primary);
|
||||
}
|
||||
|
||||
.VPDocAsideOutline .outline-link.active {
|
||||
color: var(--es-primary);
|
||||
}
|
||||
|
||||
.VPDocAsideOutline .outline-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div[class*='language-'] {
|
||||
background: var(--es-bg-inset) !important;
|
||||
border: 1px solid var(--es-border-default);
|
||||
border-radius: 2px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.vp-code-group .tabs {
|
||||
background: var(--es-bg-header);
|
||||
border-bottom: 1px solid var(--es-border-subtle);
|
||||
}
|
||||
|
||||
.vp-doc :not(pre) > code {
|
||||
background: var(--es-bg-input);
|
||||
color: var(--es-primary);
|
||||
padding: 2px 6px;
|
||||
border-radius: 2px;
|
||||
font-size: var(--es-font-xs);
|
||||
}
|
||||
|
||||
.vp-doc table {
|
||||
display: table;
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-collapse: collapse;
|
||||
margin: 16px 0;
|
||||
font-size: var(--es-font-sm);
|
||||
}
|
||||
|
||||
.vp-doc tr {
|
||||
border-bottom: 1px solid var(--es-border-subtle);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.vp-doc tr:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.vp-doc tr:hover {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
.vp-doc th {
|
||||
background: var(--es-bg-header);
|
||||
font-weight: 600;
|
||||
font-size: var(--es-font-xs);
|
||||
color: var(--es-text-secondary);
|
||||
text-align: left;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--es-border-subtle);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.vp-doc td {
|
||||
font-size: var(--es-font-sm);
|
||||
color: var(--es-text-primary);
|
||||
padding: 8px 12px;
|
||||
vertical-align: top;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.vp-doc td:first-child {
|
||||
font-weight: 500;
|
||||
color: var(--es-text-primary);
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.vp-doc .warning,
|
||||
.vp-doc .custom-block.warning {
|
||||
background: rgba(245, 158, 11, 0.08);
|
||||
border: none;
|
||||
border-left: 3px solid var(--es-warning);
|
||||
border-radius: 0 2px 2px 0;
|
||||
padding: 10px 12px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.vp-doc .warning .custom-block-title,
|
||||
.vp-doc .custom-block.warning .custom-block-title {
|
||||
color: var(--es-warning);
|
||||
font-weight: 600;
|
||||
font-size: var(--es-font-xs);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.vp-doc .warning p {
|
||||
color: var(--es-text-primary);
|
||||
margin: 0;
|
||||
font-size: var(--es-font-xs);
|
||||
}
|
||||
|
||||
.vp-doc .tip,
|
||||
.vp-doc .custom-block.tip {
|
||||
background: rgba(59, 130, 246, 0.08);
|
||||
border: none;
|
||||
border-left: 3px solid var(--es-primary);
|
||||
border-radius: 0 2px 2px 0;
|
||||
padding: 10px 12px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.vp-doc .tip .custom-block-title,
|
||||
.vp-doc .custom-block.tip .custom-block-title {
|
||||
color: var(--es-primary);
|
||||
font-weight: 600;
|
||||
font-size: var(--es-font-xs);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.vp-doc .tip p {
|
||||
color: var(--es-text-primary);
|
||||
margin: 0;
|
||||
font-size: var(--es-font-xs);
|
||||
}
|
||||
|
||||
.vp-doc .info,
|
||||
.vp-doc .custom-block.info {
|
||||
background: rgba(74, 222, 128, 0.08);
|
||||
border: none;
|
||||
border-left: 3px solid var(--es-success);
|
||||
border-radius: 0 2px 2px 0;
|
||||
padding: 10px 12px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.vp-doc .info .custom-block-title,
|
||||
.vp-doc .custom-block.info .custom-block-title {
|
||||
color: var(--es-success);
|
||||
font-weight: 600;
|
||||
font-size: var(--es-font-xs);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.vp-doc .danger,
|
||||
.vp-doc .custom-block.danger {
|
||||
background: rgba(239, 68, 68, 0.08);
|
||||
border: none;
|
||||
border-left: 3px solid var(--es-error);
|
||||
border-radius: 0 2px 2px 0;
|
||||
padding: 10px 12px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.vp-doc .danger .custom-block-title,
|
||||
.vp-doc .custom-block.danger .custom-block-title {
|
||||
color: var(--es-error);
|
||||
font-weight: 600;
|
||||
font-size: var(--es-font-xs);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.vp-doc .card {
|
||||
background: var(--es-bg-sidebar);
|
||||
border: 1px solid var(--es-border-subtle);
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.vp-doc .card-title {
|
||||
font-size: var(--es-font-sm);
|
||||
font-weight: 600;
|
||||
color: var(--es-text-primary);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.vp-doc .card-description {
|
||||
font-size: var(--es-font-xs);
|
||||
color: var(--es-text-muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.vp-doc .tag {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
background: transparent;
|
||||
border: 1px solid var(--es-border-default);
|
||||
border-radius: 2px;
|
||||
color: var(--es-text-secondary);
|
||||
font-size: var(--es-font-xs);
|
||||
margin-right: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.VPFooter {
|
||||
background: var(--es-bg-sidebar) !important;
|
||||
border-top: 1px solid var(--es-border-subtle) !important;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--es-bg-card);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--es-border-strong);
|
||||
border-radius: 4px;
|
||||
border: 2px solid var(--es-bg-card);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #5a5a5a;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.home-container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.home-section {
|
||||
padding: 32px 0;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.VPDoc .content {
|
||||
padding: 16px !important;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
import ParticleHero from './components/ParticleHero.vue'
|
||||
import ParticleHeroEn from './components/ParticleHeroEn.vue'
|
||||
import FeatureCard from './components/FeatureCard.vue'
|
||||
import './custom.css'
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
enhanceApp({ app }) {
|
||||
app.component('ParticleHero', ParticleHero)
|
||||
app.component('ParticleHeroEn', ParticleHeroEn)
|
||||
app.component('FeatureCard', FeatureCard)
|
||||
}
|
||||
}
|
||||
49
docs/README.md
Normal file
49
docs/README.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Starlight Starter Kit: Basics
|
||||
|
||||
[](https://starlight.astro.build)
|
||||
|
||||
```
|
||||
npm create astro@latest -- --template starlight
|
||||
```
|
||||
|
||||
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
||||
|
||||
## 🚀 Project Structure
|
||||
|
||||
Inside of your Astro + Starlight project, you'll see the following folders and files:
|
||||
|
||||
```
|
||||
.
|
||||
├── public/
|
||||
├── src/
|
||||
│ ├── assets/
|
||||
│ ├── content/
|
||||
│ │ └── docs/
|
||||
│ └── content.config.ts
|
||||
├── astro.config.mjs
|
||||
├── package.json
|
||||
└── tsconfig.json
|
||||
```
|
||||
|
||||
Starlight looks for `.md` or `.mdx` files in the `src/content/docs/` directory. Each file is exposed as a route based on its file name.
|
||||
|
||||
Images can be added to `src/assets/` and embedded in Markdown with a relative link.
|
||||
|
||||
Static assets, like favicons, can be placed in the `public/` directory.
|
||||
|
||||
## 🧞 Commands
|
||||
|
||||
All commands are run from the root of the project, from a terminal:
|
||||
|
||||
| Command | Action |
|
||||
| :------------------------ | :----------------------------------------------- |
|
||||
| `npm install` | Installs dependencies |
|
||||
| `npm run dev` | Starts local dev server at `localhost:4321` |
|
||||
| `npm run build` | Build your production site to `./dist/` |
|
||||
| `npm run preview` | Preview your build locally, before deploying |
|
||||
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
|
||||
| `npm run astro -- --help` | Get help using the Astro CLI |
|
||||
|
||||
## 👀 Want to learn more?
|
||||
|
||||
Check out [Starlight’s docs](https://starlight.astro.build/), read [the Astro documentation](https://docs.astro.build), or jump into the [Astro Discord server](https://astro.build/chat).
|
||||
@@ -1,663 +0,0 @@
|
||||
# ESEngine 材质系统统一架构重构方案
|
||||
|
||||
## 问题概述
|
||||
|
||||
当前 UI 和 Scene (Sprite) 两套渲染系统存在大量代码重复:
|
||||
|
||||
| 重复项 | Sprite | UI | 重复度 |
|
||||
|--------|--------|----|----|
|
||||
| 材质属性覆盖接口 | `MaterialPropertyOverride` | `UIMaterialPropertyOverride` | 100% |
|
||||
| 材质方法 (12个) | `SpriteComponent` | `UIRenderComponent` | 100% |
|
||||
| ShinyEffect 组件 | `ShinyEffectComponent` | `UIShinyEffectComponent` | 99% |
|
||||
| ShinyEffect 系统 | `ShinyEffectSystem` | `UIShinyEffectSystem` | 98% |
|
||||
|
||||
**根本原因**:缺乏统一的材质覆盖接口抽象层。
|
||||
|
||||
---
|
||||
|
||||
## 一、统一材质覆盖接口
|
||||
|
||||
### 1.1 定义通用接口
|
||||
|
||||
在 `@esengine/material-system` 包中定义统一接口:
|
||||
|
||||
```typescript
|
||||
// packages/material-system/src/interfaces/IMaterialOverridable.ts
|
||||
|
||||
/**
|
||||
* Material property override definition.
|
||||
* 材质属性覆盖定义。
|
||||
*/
|
||||
export interface MaterialPropertyOverride {
|
||||
type: 'float' | 'vec2' | 'vec3' | 'vec4' | 'color' | 'int';
|
||||
value: number | number[];
|
||||
}
|
||||
|
||||
export type MaterialOverrides = Record<string, MaterialPropertyOverride>;
|
||||
|
||||
/**
|
||||
* Interface for components that support material property overrides.
|
||||
* 支持材质属性覆盖的组件接口。
|
||||
*/
|
||||
export interface IMaterialOverridable {
|
||||
/** Material GUID for asset reference | 材质资产引用的 GUID */
|
||||
materialGuid: string;
|
||||
|
||||
/** Current material overrides | 当前材质覆盖 */
|
||||
readonly materialOverrides: MaterialOverrides;
|
||||
|
||||
/** Get current material ID | 获取当前材质 ID */
|
||||
getMaterialId(): number;
|
||||
|
||||
/** Set material ID | 设置材质 ID */
|
||||
setMaterialId(id: number): void;
|
||||
|
||||
// Uniform setters
|
||||
setOverrideFloat(name: string, value: number): this;
|
||||
setOverrideVec2(name: string, x: number, y: number): this;
|
||||
setOverrideVec3(name: string, x: number, y: number, z: number): this;
|
||||
setOverrideVec4(name: string, x: number, y: number, z: number, w: number): this;
|
||||
setOverrideColor(name: string, r: number, g: number, b: number, a?: number): this;
|
||||
setOverrideInt(name: string, value: number): this;
|
||||
|
||||
// Uniform getters
|
||||
getOverride(name: string): MaterialPropertyOverride | undefined;
|
||||
removeOverride(name: string): this;
|
||||
clearOverrides(): this;
|
||||
hasOverrides(): boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 创建 Mixin 实现
|
||||
|
||||
使用 Mixin 模式避免代码重复:
|
||||
|
||||
```typescript
|
||||
// packages/material-system/src/mixins/MaterialOverridableMixin.ts
|
||||
|
||||
import type { MaterialPropertyOverride, MaterialOverrides } from '../interfaces/IMaterialOverridable';
|
||||
|
||||
/**
|
||||
* Mixin that provides material override functionality.
|
||||
* 提供材质覆盖功能的 Mixin。
|
||||
*/
|
||||
export function MaterialOverridableMixin<TBase extends new (...args: any[]) => {}>(Base: TBase) {
|
||||
return class extends Base {
|
||||
materialGuid: string = '';
|
||||
private _materialId: number = 0;
|
||||
private _materialOverrides: MaterialOverrides = {};
|
||||
|
||||
get materialOverrides(): MaterialOverrides {
|
||||
return this._materialOverrides;
|
||||
}
|
||||
|
||||
getMaterialId(): number {
|
||||
return this._materialId;
|
||||
}
|
||||
|
||||
setMaterialId(id: number): void {
|
||||
this._materialId = id;
|
||||
}
|
||||
|
||||
setOverrideFloat(name: string, value: number): this {
|
||||
this._materialOverrides[name] = { type: 'float', value };
|
||||
return this;
|
||||
}
|
||||
|
||||
setOverrideVec2(name: string, x: number, y: number): this {
|
||||
this._materialOverrides[name] = { type: 'vec2', value: [x, y] };
|
||||
return this;
|
||||
}
|
||||
|
||||
setOverrideVec3(name: string, x: number, y: number, z: number): this {
|
||||
this._materialOverrides[name] = { type: 'vec3', value: [x, y, z] };
|
||||
return this;
|
||||
}
|
||||
|
||||
setOverrideVec4(name: string, x: number, y: number, z: number, w: number): this {
|
||||
this._materialOverrides[name] = { type: 'vec4', value: [x, y, z, w] };
|
||||
return this;
|
||||
}
|
||||
|
||||
setOverrideColor(name: string, r: number, g: number, b: number, a: number = 1.0): this {
|
||||
this._materialOverrides[name] = { type: 'color', value: [r, g, b, a] };
|
||||
return this;
|
||||
}
|
||||
|
||||
setOverrideInt(name: string, value: number): this {
|
||||
this._materialOverrides[name] = { type: 'int', value: Math.floor(value) };
|
||||
return this;
|
||||
}
|
||||
|
||||
getOverride(name: string): MaterialPropertyOverride | undefined {
|
||||
return this._materialOverrides[name];
|
||||
}
|
||||
|
||||
removeOverride(name: string): this {
|
||||
delete this._materialOverrides[name];
|
||||
return this;
|
||||
}
|
||||
|
||||
clearOverrides(): this {
|
||||
this._materialOverrides = {};
|
||||
return this;
|
||||
}
|
||||
|
||||
hasOverrides(): boolean {
|
||||
return Object.keys(this._materialOverrides).length > 0;
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、Shader Property 元数据系统
|
||||
|
||||
### 2.1 定义属性元数据接口
|
||||
|
||||
```typescript
|
||||
// packages/material-system/src/interfaces/IShaderProperty.ts
|
||||
|
||||
/**
|
||||
* Shader property UI metadata.
|
||||
* 着色器属性 UI 元数据。
|
||||
*/
|
||||
export interface ShaderPropertyMeta {
|
||||
/** Property type | 属性类型 */
|
||||
type: 'float' | 'vec2' | 'vec3' | 'vec4' | 'color' | 'int' | 'texture';
|
||||
|
||||
/** Display label (supports i18n key) | 显示标签(支持 i18n 键) */
|
||||
label: string;
|
||||
|
||||
/** Property group for organization | 属性分组 */
|
||||
group?: string;
|
||||
|
||||
/** Default value | 默认值 */
|
||||
default?: number | number[] | string;
|
||||
|
||||
// Numeric constraints
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
|
||||
/** UI hints | UI 提示 */
|
||||
hint?: 'range' | 'angle' | 'hdr' | 'normal';
|
||||
|
||||
/** Tooltip description | 工具提示描述 */
|
||||
tooltip?: string;
|
||||
|
||||
/** Whether to hide in inspector | 是否在检查器中隐藏 */
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extended shader definition with property metadata.
|
||||
* 带属性元数据的扩展着色器定义。
|
||||
*/
|
||||
export interface ShaderAssetDefinition {
|
||||
/** Shader name | 着色器名称 */
|
||||
name: string;
|
||||
|
||||
/** Display name for UI | UI 显示名称 */
|
||||
displayName?: string;
|
||||
|
||||
/** Shader description | 着色器描述 */
|
||||
description?: string;
|
||||
|
||||
/** Vertex shader source (inline or path) | 顶点着色器源(内联或路径)*/
|
||||
vertexSource: string;
|
||||
|
||||
/** Fragment shader source (inline or path) | 片段着色器源(内联或路径)*/
|
||||
fragmentSource: string;
|
||||
|
||||
/** Property metadata for inspector | 检查器属性元数据 */
|
||||
properties?: Record<string, ShaderPropertyMeta>;
|
||||
|
||||
/** Render queue / order | 渲染队列/顺序 */
|
||||
renderQueue?: number;
|
||||
|
||||
/** Preset blend mode | 预设混合模式 */
|
||||
blendMode?: 'alpha' | 'additive' | 'multiply' | 'opaque';
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 .shader 资产文件格式
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "esengine://schemas/shader.json",
|
||||
"version": 1,
|
||||
"name": "Shiny",
|
||||
"displayName": "闪光效果 | Shiny Effect",
|
||||
"description": "扫光高亮动画着色器 | Sweeping highlight animation shader",
|
||||
|
||||
"vertexSource": "./shaders/sprite.vert",
|
||||
"fragmentSource": "./shaders/shiny.frag",
|
||||
|
||||
"blendMode": "alpha",
|
||||
"renderQueue": 2000,
|
||||
|
||||
"properties": {
|
||||
"u_shinyProgress": {
|
||||
"type": "float",
|
||||
"label": "进度 | Progress",
|
||||
"group": "Animation",
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"hidden": true
|
||||
},
|
||||
"u_shinyWidth": {
|
||||
"type": "float",
|
||||
"label": "宽度 | Width",
|
||||
"group": "Effect",
|
||||
"default": 0.25,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"tooltip": "闪光带宽度 | Width of the shiny band"
|
||||
},
|
||||
"u_shinyRotation": {
|
||||
"type": "float",
|
||||
"label": "角度 | Rotation",
|
||||
"group": "Effect",
|
||||
"default": 2.25,
|
||||
"min": 0,
|
||||
"max": 6.28,
|
||||
"step": 0.01,
|
||||
"hint": "angle"
|
||||
},
|
||||
"u_shinySoftness": {
|
||||
"type": "float",
|
||||
"label": "柔和度 | Softness",
|
||||
"group": "Effect",
|
||||
"default": 1.0,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01
|
||||
},
|
||||
"u_shinyBrightness": {
|
||||
"type": "float",
|
||||
"label": "亮度 | Brightness",
|
||||
"group": "Effect",
|
||||
"default": 1.0,
|
||||
"min": 0,
|
||||
"max": 2,
|
||||
"step": 0.01
|
||||
},
|
||||
"u_shinyGloss": {
|
||||
"type": "float",
|
||||
"label": "光泽度 | Gloss",
|
||||
"group": "Effect",
|
||||
"default": 1.0,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"tooltip": "0=白色高光, 1=带颜色 | 0=white shine, 1=color-tinted"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、统一效果组件/系统架构
|
||||
|
||||
### 3.1 抽取通用 ShinyEffect 基类
|
||||
|
||||
```typescript
|
||||
// packages/material-system/src/effects/BaseShinyEffect.ts
|
||||
|
||||
import { Component, Property, Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* Base shiny effect configuration (shared between UI and Sprite).
|
||||
* 基础闪光效果配置(UI 和 Sprite 共享)。
|
||||
*/
|
||||
export abstract class BaseShinyEffect extends Component {
|
||||
// ============= Effect Parameters =============
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Width', min: 0, max: 1, step: 0.01 })
|
||||
public width: number = 0.25;
|
||||
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Rotation', min: 0, max: 360, step: 1 })
|
||||
public rotation: number = 129;
|
||||
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Softness', min: 0, max: 1, step: 0.01 })
|
||||
public softness: number = 1.0;
|
||||
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Brightness', min: 0, max: 2, step: 0.01 })
|
||||
public brightness: number = 1.0;
|
||||
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Gloss', min: 0, max: 2, step: 0.01 })
|
||||
public gloss: number = 1.0;
|
||||
|
||||
// ============= Animation Settings =============
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Play' })
|
||||
public play: boolean = true;
|
||||
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Loop' })
|
||||
public loop: boolean = true;
|
||||
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Duration', min: 0.1, step: 0.1 })
|
||||
public duration: number = 2.0;
|
||||
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Loop Delay', min: 0, step: 0.1 })
|
||||
public loopDelay: number = 2.0;
|
||||
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Initial Delay', min: 0, step: 0.1 })
|
||||
public initialDelay: number = 0;
|
||||
|
||||
// ============= Runtime State =============
|
||||
public progress: number = 0;
|
||||
public elapsedTime: number = 0;
|
||||
public inDelay: boolean = false;
|
||||
public delayRemaining: number = 0;
|
||||
public initialDelayProcessed: boolean = false;
|
||||
|
||||
reset(): void {
|
||||
this.progress = 0;
|
||||
this.elapsedTime = 0;
|
||||
this.inDelay = false;
|
||||
this.delayRemaining = 0;
|
||||
this.initialDelayProcessed = false;
|
||||
}
|
||||
|
||||
start(): void {
|
||||
this.reset();
|
||||
this.play = true;
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
this.play = false;
|
||||
}
|
||||
|
||||
getRotationRadians(): number {
|
||||
return this.rotation * Math.PI / 180;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 通用动画更新逻辑
|
||||
|
||||
```typescript
|
||||
// packages/material-system/src/effects/ShinyEffectAnimator.ts
|
||||
|
||||
import type { BaseShinyEffect } from './BaseShinyEffect';
|
||||
import type { IMaterialOverridable } from '../interfaces/IMaterialOverridable';
|
||||
import { BuiltInShaders } from '../types';
|
||||
|
||||
/**
|
||||
* Shared animator logic for shiny effect.
|
||||
* 闪光效果共享的动画逻辑。
|
||||
*/
|
||||
export class ShinyEffectAnimator {
|
||||
/**
|
||||
* Update animation state.
|
||||
* 更新动画状态。
|
||||
*/
|
||||
static updateAnimation(shiny: BaseShinyEffect, deltaTime: number): void {
|
||||
if (!shiny.initialDelayProcessed && shiny.initialDelay > 0) {
|
||||
shiny.delayRemaining = shiny.initialDelay;
|
||||
shiny.inDelay = true;
|
||||
shiny.initialDelayProcessed = true;
|
||||
}
|
||||
|
||||
if (shiny.inDelay) {
|
||||
shiny.delayRemaining -= deltaTime;
|
||||
if (shiny.delayRemaining <= 0) {
|
||||
shiny.inDelay = false;
|
||||
shiny.elapsedTime = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
shiny.elapsedTime += deltaTime;
|
||||
shiny.progress = Math.min(shiny.elapsedTime / shiny.duration, 1.0);
|
||||
|
||||
if (shiny.progress >= 1.0) {
|
||||
if (shiny.loop) {
|
||||
shiny.inDelay = true;
|
||||
shiny.delayRemaining = shiny.loopDelay;
|
||||
shiny.progress = 0;
|
||||
shiny.elapsedTime = 0;
|
||||
} else {
|
||||
shiny.play = false;
|
||||
shiny.progress = 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply material overrides.
|
||||
* 应用材质覆盖。
|
||||
*/
|
||||
static applyMaterialOverrides(shiny: BaseShinyEffect, target: IMaterialOverridable): void {
|
||||
if (target.getMaterialId() === 0) {
|
||||
target.setMaterialId(BuiltInShaders.Shiny);
|
||||
}
|
||||
|
||||
target.setOverrideFloat('u_shinyProgress', shiny.progress);
|
||||
target.setOverrideFloat('u_shinyWidth', shiny.width);
|
||||
target.setOverrideFloat('u_shinyRotation', shiny.getRotationRadians());
|
||||
target.setOverrideFloat('u_shinySoftness', shiny.softness);
|
||||
target.setOverrideFloat('u_shinyBrightness', shiny.brightness);
|
||||
target.setOverrideFloat('u_shinyGloss', shiny.gloss);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、Material Inspector 设计
|
||||
|
||||
### 4.1 组件架构
|
||||
|
||||
```
|
||||
MaterialPropertiesEditor (容器组件)
|
||||
├── ShaderSelector (着色器选择器)
|
||||
├── PropertyGroup (属性分组)
|
||||
│ ├── FloatProperty (浮点属性)
|
||||
│ ├── VectorProperty (向量属性)
|
||||
│ ├── ColorProperty (颜色属性)
|
||||
│ └── TextureProperty (纹理属性)
|
||||
└── OverrideIndicator (覆盖指示器)
|
||||
```
|
||||
|
||||
### 4.2 核心组件
|
||||
|
||||
```typescript
|
||||
// packages/editor-app/src/components/inspectors/material/MaterialPropertiesEditor.tsx
|
||||
|
||||
interface MaterialPropertiesEditorProps {
|
||||
/** Target component implementing IMaterialOverridable */
|
||||
target: IMaterialOverridable;
|
||||
/** Current shader definition with property metadata */
|
||||
shaderDef?: ShaderAssetDefinition;
|
||||
/** Callback when property changes */
|
||||
onChange?: (name: string, value: MaterialPropertyOverride) => void;
|
||||
}
|
||||
|
||||
export const MaterialPropertiesEditor: React.FC<MaterialPropertiesEditorProps> = ({
|
||||
target,
|
||||
shaderDef,
|
||||
onChange
|
||||
}) => {
|
||||
// Group properties by their group field
|
||||
const groupedProps = useMemo(() => {
|
||||
if (!shaderDef?.properties) return {};
|
||||
|
||||
const groups: Record<string, Array<[string, ShaderPropertyMeta]>> = {};
|
||||
for (const [name, meta] of Object.entries(shaderDef.properties)) {
|
||||
if (meta.hidden) continue;
|
||||
const group = meta.group || 'Default';
|
||||
if (!groups[group]) groups[group] = [];
|
||||
groups[group].push([name, meta]);
|
||||
}
|
||||
return groups;
|
||||
}, [shaderDef]);
|
||||
|
||||
return (
|
||||
<div className="material-properties-editor">
|
||||
<ShaderSelector
|
||||
currentShaderId={target.getMaterialId()}
|
||||
onSelect={(id) => target.setMaterialId(id)}
|
||||
/>
|
||||
|
||||
{Object.entries(groupedProps).map(([group, props]) => (
|
||||
<PropertyGroup key={group} title={group}>
|
||||
{props.map(([name, meta]) => (
|
||||
<PropertyField
|
||||
key={name}
|
||||
name={name}
|
||||
meta={meta}
|
||||
value={target.getOverride(name)?.value ?? meta.default}
|
||||
onChange={(value) => {
|
||||
applyOverride(target, name, meta.type, value);
|
||||
onChange?.(name, target.getOverride(name)!);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</PropertyGroup>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、实施计划
|
||||
|
||||
### Phase 1: 接口层 (1-2 天)
|
||||
|
||||
1. **创建 IMaterialOverridable 接口** (`packages/material-system/src/interfaces/`)
|
||||
2. **创建 MaterialOverridableMixin** (`packages/material-system/src/mixins/`)
|
||||
3. **导出新接口** (`packages/material-system/src/index.ts`)
|
||||
|
||||
### Phase 2: 重构现有组件 (2-3 天)
|
||||
|
||||
1. **修改 SpriteComponent**:实现 `IMaterialOverridable`,使用 Mixin
|
||||
2. **修改 UIRenderComponent**:实现 `IMaterialOverridable`,使用 Mixin
|
||||
3. **删除重复代码**:移除各组件中的重复材质方法
|
||||
|
||||
### Phase 3: 统一效果系统 (2-3 天)
|
||||
|
||||
1. **创建 BaseShinyEffect** (`packages/material-system/src/effects/`)
|
||||
2. **创建 ShinyEffectAnimator** (`packages/material-system/src/effects/`)
|
||||
3. **重构 ShinyEffectComponent**:继承 BaseShinyEffect
|
||||
4. **重构 UIShinyEffectComponent**:继承 BaseShinyEffect
|
||||
5. **重构系统**:使用 ShinyEffectAnimator
|
||||
|
||||
### Phase 4: Shader Property 系统 (2-3 天)
|
||||
|
||||
1. **定义 ShaderPropertyMeta 接口**
|
||||
2. **扩展 ShaderDefinition** 添加 properties 字段
|
||||
3. **创建 ShaderLoader** 支持 .shader 文件
|
||||
4. **注册内置着色器属性元数据**
|
||||
|
||||
### Phase 5: Material Inspector (3-4 天)
|
||||
|
||||
1. **创建 MaterialPropertiesEditor 组件**
|
||||
2. **创建 PropertyField 组件** (Float, Vector, Color, Texture)
|
||||
3. **集成到现有 Inspector 系统**
|
||||
4. **支持实时预览**
|
||||
|
||||
---
|
||||
|
||||
## 六、文件修改清单
|
||||
|
||||
| 优先级 | 包 | 文件 | 操作 |
|
||||
|--------|-----|------|------|
|
||||
| P0 | material-system | `src/interfaces/IMaterialOverridable.ts` | 新建 |
|
||||
| P0 | material-system | `src/mixins/MaterialOverridableMixin.ts` | 新建 |
|
||||
| P0 | material-system | `src/interfaces/IShaderProperty.ts` | 新建 |
|
||||
| P1 | material-system | `src/effects/BaseShinyEffect.ts` | 新建 |
|
||||
| P1 | material-system | `src/effects/ShinyEffectAnimator.ts` | 新建 |
|
||||
| P1 | sprite | `src/SpriteComponent.ts` | 重构 |
|
||||
| P1 | ui | `src/components/UIRenderComponent.ts` | 重构 |
|
||||
| P2 | sprite | `src/ShinyEffectComponent.ts` | 重构 |
|
||||
| P2 | ui | `src/components/UIShinyEffectComponent.ts` | 重构 |
|
||||
| P2 | sprite | `src/systems/ShinyEffectSystem.ts` | 重构 |
|
||||
| P2 | ui | `src/systems/render/UIShinyEffectSystem.ts` | 重构 |
|
||||
| P3 | material-system | `src/loaders/ShaderLoader.ts` | 扩展 |
|
||||
| P3 | editor-app | `src/components/inspectors/material/*` | 新建 |
|
||||
|
||||
---
|
||||
|
||||
## 七、Transform 组件统一(可选)
|
||||
|
||||
### 7.1 现状分析
|
||||
|
||||
| 特性 | TransformComponent | UITransformComponent |
|
||||
|------|-------------------|---------------------|
|
||||
| **坐标系** | 绝对坐标 (position.x/y/z) | 相对锚点坐标 (x/y + anchor) |
|
||||
| **尺寸** | ❌ 无 | ✅ width/height + 约束 |
|
||||
| **锚点系统** | ❌ 无 | ✅ anchorMin/Max |
|
||||
| **3D 支持** | ✅ IVector3 | ❌ 纯 2D |
|
||||
| **可见性** | ❌ 无 | ✅ visible, alpha |
|
||||
|
||||
### 7.2 结论
|
||||
|
||||
**不建议完全合并**,但可提取公共基类:
|
||||
|
||||
```typescript
|
||||
// packages/engine-core/src/interfaces/ITransformBase.ts
|
||||
|
||||
export interface ITransformBase {
|
||||
/** 旋转角度(度) | Rotation in degrees */
|
||||
rotation: number;
|
||||
|
||||
/** X 缩放 | Scale X */
|
||||
scaleX: number;
|
||||
|
||||
/** Y 缩放 | Scale Y */
|
||||
scaleY: number;
|
||||
|
||||
/** 本地到世界矩阵 | Local to world matrix */
|
||||
readonly localToWorldMatrix: Matrix2D;
|
||||
|
||||
/** 是否需要更新 | Dirty flag */
|
||||
isDirty: boolean;
|
||||
|
||||
/** 世界坐标 X | World position X */
|
||||
readonly worldX: number;
|
||||
|
||||
/** 世界坐标 Y | World position Y */
|
||||
readonly worldY: number;
|
||||
|
||||
/** 世界旋转 | World rotation */
|
||||
readonly worldRotation: number;
|
||||
|
||||
/** 世界缩放 X | World scale X */
|
||||
readonly worldScaleX: number;
|
||||
|
||||
/** 世界缩放 Y | World scale Y */
|
||||
readonly worldScaleY: number;
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3 收益
|
||||
|
||||
- 渲染系统可以统一处理 `ITransformBase`
|
||||
- 减少 SpriteRenderSystem 和 UIRenderSystem 的重复
|
||||
- Gizmo 系统可以共享变换操作逻辑
|
||||
|
||||
---
|
||||
|
||||
## 八、向后兼容性
|
||||
|
||||
1. **接口兼容**:现有组件的 API 保持不变
|
||||
2. **序列化兼容**:不改变现有序列化格式
|
||||
3. **渐进迁移**:可分阶段进行,不影响现有功能
|
||||
383
docs/astro.config.mjs
Normal file
383
docs/astro.config.mjs
Normal file
@@ -0,0 +1,383 @@
|
||||
// @ts-check
|
||||
import { defineConfig } from 'astro/config';
|
||||
import starlight from '@astrojs/starlight';
|
||||
import vue from '@astrojs/vue';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import rehypeRaw from 'rehype-raw';
|
||||
|
||||
export default defineConfig({
|
||||
markdown: {
|
||||
rehypePlugins: [rehypeRaw],
|
||||
},
|
||||
integrations: [
|
||||
starlight({
|
||||
title: 'ESEngine',
|
||||
logo: {
|
||||
src: './src/assets/logo.svg',
|
||||
replacesTitle: false,
|
||||
},
|
||||
social: [
|
||||
{ icon: 'github', label: 'GitHub', href: 'https://github.com/esengine/esengine' }
|
||||
],
|
||||
defaultLocale: 'root',
|
||||
locales: {
|
||||
root: {
|
||||
label: '简体中文',
|
||||
lang: 'zh-CN',
|
||||
},
|
||||
en: {
|
||||
label: 'English',
|
||||
lang: 'en',
|
||||
},
|
||||
},
|
||||
sidebar: [
|
||||
{
|
||||
label: '快速开始',
|
||||
translations: { en: 'Getting Started' },
|
||||
items: [
|
||||
{ label: '快速入门', slug: 'guide/getting-started', translations: { en: 'Quick Start' } },
|
||||
{ label: '指南概览', slug: 'guide', translations: { en: 'Guide Overview' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '核心概念',
|
||||
translations: { en: 'Core Concepts' },
|
||||
items: [
|
||||
{
|
||||
label: '实体',
|
||||
translations: { en: 'Entity' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'guide/entity', translations: { en: 'Overview' } },
|
||||
{ label: '组件操作', slug: 'guide/entity/component-operations', translations: { en: 'Component Operations' } },
|
||||
{ label: '实体句柄', slug: 'guide/entity/entity-handle', translations: { en: 'Entity Handle' } },
|
||||
{ label: '生命周期', slug: 'guide/entity/lifecycle', translations: { en: 'Lifecycle' } },
|
||||
],
|
||||
},
|
||||
{ label: '层级结构', slug: 'guide/hierarchy', translations: { en: 'Hierarchy' } },
|
||||
{
|
||||
label: '组件',
|
||||
translations: { en: 'Component' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'guide/component', translations: { en: 'Overview' } },
|
||||
{ label: '生命周期', slug: 'guide/component/lifecycle', translations: { en: 'Lifecycle' } },
|
||||
{ label: 'EntityRef 装饰器', slug: 'guide/component/entity-ref', translations: { en: 'EntityRef' } },
|
||||
{ label: '最佳实践', slug: 'guide/component/best-practices', translations: { en: 'Best Practices' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '实体查询',
|
||||
translations: { en: 'Entity Query' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'guide/entity-query', translations: { en: 'Overview' } },
|
||||
{ label: 'Matcher API', slug: 'guide/entity-query/matcher-api', translations: { en: 'Matcher API' } },
|
||||
{ label: '编译查询', slug: 'guide/entity-query/compiled-query', translations: { en: 'Compiled Query' } },
|
||||
{ label: '最佳实践', slug: 'guide/entity-query/best-practices', translations: { en: 'Best Practices' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '系统',
|
||||
translations: { en: 'System' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'guide/system', translations: { en: 'Overview' } },
|
||||
{ label: '系统类型', slug: 'guide/system/types', translations: { en: 'System Types' } },
|
||||
{ label: '生命周期', slug: 'guide/system/lifecycle', translations: { en: 'Lifecycle' } },
|
||||
{ label: '命令缓冲区', slug: 'guide/system/command-buffer', translations: { en: 'Command Buffer' } },
|
||||
{ label: '系统调度', slug: 'guide/system/scheduling', translations: { en: 'Scheduling' } },
|
||||
{ label: '变更检测', slug: 'guide/system/change-detection', translations: { en: 'Change Detection' } },
|
||||
{ label: '最佳实践', slug: 'guide/system/best-practices', translations: { en: 'Best Practices' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '场景',
|
||||
translations: { en: 'Scene' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'guide/scene', translations: { en: 'Overview' } },
|
||||
{ label: '生命周期', slug: 'guide/scene/lifecycle', translations: { en: 'Lifecycle' } },
|
||||
{ label: '实体管理', slug: 'guide/scene/entity-management', translations: { en: 'Entity Management' } },
|
||||
{ label: '系统管理', slug: 'guide/scene/system-management', translations: { en: 'System Management' } },
|
||||
{ label: '事件系统', slug: 'guide/scene/events', translations: { en: 'Events' } },
|
||||
{ label: '调试与监控', slug: 'guide/scene/debugging', translations: { en: 'Debugging' } },
|
||||
{ label: '最佳实践', slug: 'guide/scene/best-practices', translations: { en: 'Best Practices' } },
|
||||
],
|
||||
},
|
||||
{ label: '场景管理器', slug: 'guide/scene-manager', translations: { en: 'SceneManager' } },
|
||||
{ label: '持久实体', slug: 'guide/persistent-entity', translations: { en: 'Persistent Entity' } },
|
||||
{
|
||||
label: '序列化',
|
||||
translations: { en: 'Serialization' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'guide/serialization', translations: { en: 'Overview' } },
|
||||
{ label: '装饰器与继承', slug: 'guide/serialization/decorators', translations: { en: 'Decorators & Inheritance' } },
|
||||
{ label: '增量序列化', slug: 'guide/serialization/incremental', translations: { en: 'Incremental' } },
|
||||
{ label: '版本迁移', slug: 'guide/serialization/migration', translations: { en: 'Migration' } },
|
||||
{ label: '使用场景', slug: 'guide/serialization/use-cases', translations: { en: 'Use Cases' } },
|
||||
],
|
||||
},
|
||||
{ label: '事件系统', slug: 'guide/event-system', translations: { en: 'Event System' } },
|
||||
{ label: '时间与定时器', slug: 'guide/time-and-timers', translations: { en: 'Time & Timers' } },
|
||||
{ label: '日志系统', slug: 'guide/logging', translations: { en: 'Logging' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '高级功能',
|
||||
translations: { en: 'Advanced Features' },
|
||||
items: [
|
||||
{
|
||||
label: '服务容器',
|
||||
translations: { en: 'Service Container' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'guide/service-container', translations: { en: 'Overview' } },
|
||||
{ label: '内置服务', slug: 'guide/service-container/built-in-services', translations: { en: 'Built-in Services' } },
|
||||
{ label: '依赖注入', slug: 'guide/service-container/dependency-injection', translations: { en: 'Dependency Injection' } },
|
||||
{ label: 'PluginServiceRegistry', slug: 'guide/service-container/plugin-service-registry', translations: { en: 'PluginServiceRegistry' } },
|
||||
{ label: '高级用法', slug: 'guide/service-container/advanced', translations: { en: 'Advanced' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '插件系统',
|
||||
translations: { en: 'Plugin System' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'guide/plugin-system', translations: { en: 'Overview' } },
|
||||
{ label: '插件开发', slug: 'guide/plugin-system/development', translations: { en: 'Development' } },
|
||||
{ label: '服务与系统', slug: 'guide/plugin-system/services-systems', translations: { en: 'Services & Systems' } },
|
||||
{ label: '依赖管理', slug: 'guide/plugin-system/dependencies', translations: { en: 'Dependencies' } },
|
||||
{ label: '插件管理', slug: 'guide/plugin-system/management', translations: { en: 'Management' } },
|
||||
{ label: '示例插件', slug: 'guide/plugin-system/examples', translations: { en: 'Examples' } },
|
||||
{ label: '最佳实践', slug: 'guide/plugin-system/best-practices', translations: { en: 'Best Practices' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Worker 系统',
|
||||
translations: { en: 'Worker System' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'guide/worker-system', translations: { en: 'Overview' } },
|
||||
{ label: '配置选项', slug: 'guide/worker-system/configuration', translations: { en: 'Configuration' } },
|
||||
{ label: '完整示例', slug: 'guide/worker-system/examples', translations: { en: 'Examples' } },
|
||||
{ label: '微信小游戏', slug: 'guide/worker-system/wechat', translations: { en: 'WeChat' } },
|
||||
{ label: '最佳实践', slug: 'guide/worker-system/best-practices', translations: { en: 'Best Practices' } },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '平台适配器',
|
||||
translations: { en: 'Platform Adapters' },
|
||||
items: [
|
||||
{ label: '概览', slug: 'guide/platform-adapter', translations: { en: 'Overview' } },
|
||||
{ label: '浏览器', slug: 'guide/platform-adapter/browser', translations: { en: 'Browser' } },
|
||||
{ label: '微信小游戏', slug: 'guide/platform-adapter/wechat-minigame', translations: { en: 'WeChat Mini Game' } },
|
||||
{ label: 'Node.js', slug: 'guide/platform-adapter/nodejs', translations: { en: 'Node.js' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '模块',
|
||||
translations: { en: 'Modules' },
|
||||
items: [
|
||||
{ label: '模块总览', slug: 'modules', translations: { en: 'Modules Overview' } },
|
||||
{
|
||||
label: '行为树',
|
||||
translations: { en: 'Behavior Tree' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'modules/behavior-tree', translations: { en: 'Overview' } },
|
||||
{ label: '快速开始', slug: 'modules/behavior-tree/getting-started', translations: { en: 'Getting Started' } },
|
||||
{ label: '核心概念', slug: 'modules/behavior-tree/core-concepts', translations: { en: 'Core Concepts' } },
|
||||
{ label: '编辑器指南', slug: 'modules/behavior-tree/editor-guide', translations: { en: 'Editor Guide' } },
|
||||
{ label: '编辑器工作流', slug: 'modules/behavior-tree/editor-workflow', translations: { en: 'Editor Workflow' } },
|
||||
{ label: '资产管理', slug: 'modules/behavior-tree/asset-management', translations: { en: 'Asset Management' } },
|
||||
{ label: '自定义节点', slug: 'modules/behavior-tree/custom-actions', translations: { en: 'Custom Actions' } },
|
||||
{ label: '高级用法', slug: 'modules/behavior-tree/advanced-usage', translations: { en: 'Advanced Usage' } },
|
||||
{ label: '最佳实践', slug: 'modules/behavior-tree/best-practices', translations: { en: 'Best Practices' } },
|
||||
{ label: 'Cocos 集成', slug: 'modules/behavior-tree/cocos-integration', translations: { en: 'Cocos Integration' } },
|
||||
{ label: 'Laya 集成', slug: 'modules/behavior-tree/laya-integration', translations: { en: 'Laya Integration' } },
|
||||
{ label: 'Node.js 使用', slug: 'modules/behavior-tree/nodejs-usage', translations: { en: 'Node.js Usage' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '状态机',
|
||||
translations: { en: 'FSM' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'modules/fsm', translations: { en: 'Overview' } },
|
||||
{ label: 'API 参考', slug: 'modules/fsm/api', translations: { en: 'API Reference' } },
|
||||
{ label: '实际示例', slug: 'modules/fsm/examples', translations: { en: 'Examples' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '定时器',
|
||||
translations: { en: 'Timer' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'modules/timer', translations: { en: 'Overview' } },
|
||||
{ label: 'API 参考', slug: 'modules/timer/api', translations: { en: 'API Reference' } },
|
||||
{ label: '实际示例', slug: 'modules/timer/examples', translations: { en: 'Examples' } },
|
||||
{ label: '最佳实践', slug: 'modules/timer/best-practices', translations: { en: 'Best Practices' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '空间索引',
|
||||
translations: { en: 'Spatial' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'modules/spatial', translations: { en: 'Overview' } },
|
||||
{ label: '空间索引 API', slug: 'modules/spatial/spatial-index', translations: { en: 'Spatial Index API' } },
|
||||
{ label: 'AOI 兴趣区域', slug: 'modules/spatial/aoi', translations: { en: 'AOI' } },
|
||||
{ label: '实际示例', slug: 'modules/spatial/examples', translations: { en: 'Examples' } },
|
||||
{ label: '工具与优化', slug: 'modules/spatial/utilities', translations: { en: 'Utilities' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '寻路',
|
||||
translations: { en: 'Pathfinding' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'modules/pathfinding', translations: { en: 'Overview' } },
|
||||
{ label: '网格地图 API', slug: 'modules/pathfinding/grid-map', translations: { en: 'Grid Map API' } },
|
||||
{ label: '导航网格 API', slug: 'modules/pathfinding/navmesh', translations: { en: 'NavMesh API' } },
|
||||
{ label: '路径平滑', slug: 'modules/pathfinding/smoothing', translations: { en: 'Path Smoothing' } },
|
||||
{ label: '实际示例', slug: 'modules/pathfinding/examples', translations: { en: 'Examples' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '蓝图',
|
||||
translations: { en: 'Blueprint' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'modules/blueprint', translations: { en: 'Overview' } },
|
||||
{ label: '编辑器使用指南', slug: 'modules/blueprint/editor-guide', translations: { en: 'Editor Guide' } },
|
||||
{ label: 'Cocos Creator 编辑器', slug: 'modules/blueprint/cocos-editor', translations: { en: 'Cocos Creator Editor' } },
|
||||
{ label: '虚拟机 API', slug: 'modules/blueprint/vm', translations: { en: 'VM API' } },
|
||||
{ label: '自定义节点', slug: 'modules/blueprint/custom-nodes', translations: { en: 'Custom Nodes' } },
|
||||
{ label: '内置节点', slug: 'modules/blueprint/nodes', translations: { en: 'Built-in Nodes' } },
|
||||
{ label: '蓝图组合', slug: 'modules/blueprint/composition', translations: { en: 'Composition' } },
|
||||
{ label: '实际示例', slug: 'modules/blueprint/examples', translations: { en: 'Examples' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '数学库',
|
||||
translations: { en: 'Math' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'modules/math', translations: { en: 'Overview' } },
|
||||
{ label: '蓝图节点', slug: 'modules/math/blueprint-nodes', translations: { en: 'Blueprint Nodes' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '程序生成',
|
||||
translations: { en: 'Procgen' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'modules/procgen', translations: { en: 'Overview' } },
|
||||
{ label: '噪声函数', slug: 'modules/procgen/noise', translations: { en: 'Noise Functions' } },
|
||||
{ label: '种子随机数', slug: 'modules/procgen/random', translations: { en: 'Seeded Random' } },
|
||||
{ label: '采样工具', slug: 'modules/procgen/sampling', translations: { en: 'Sampling' } },
|
||||
{ label: '实际示例', slug: 'modules/procgen/examples', translations: { en: 'Examples' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'RPC 通信',
|
||||
translations: { en: 'RPC' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'modules/rpc', translations: { en: 'Overview' } },
|
||||
{ label: '服务端', slug: 'modules/rpc/server', translations: { en: 'Server' } },
|
||||
{ label: '客户端', slug: 'modules/rpc/client', translations: { en: 'Client' } },
|
||||
{ label: '编解码', slug: 'modules/rpc/codec', translations: { en: 'Codec' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '网络同步',
|
||||
translations: { en: 'Network' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'modules/network', translations: { en: 'Overview' } },
|
||||
{ label: '客户端', slug: 'modules/network/client', translations: { en: 'Client' } },
|
||||
{ label: '服务器', slug: 'modules/network/server', translations: { en: 'Server' } },
|
||||
{ label: 'HTTP 路由', slug: 'modules/network/http', translations: { en: 'HTTP Routing' } },
|
||||
{ label: '认证系统', slug: 'modules/network/auth', translations: { en: 'Authentication' } },
|
||||
{ label: '速率限制', slug: 'modules/network/rate-limit', translations: { en: 'Rate Limiting' } },
|
||||
{ label: '分布式房间', slug: 'modules/network/distributed', translations: { en: 'Distributed Rooms' } },
|
||||
{ label: '状态同步', slug: 'modules/network/sync', translations: { en: 'State Sync' } },
|
||||
{ label: '定点数同步', slug: 'modules/network/fixed-point', translations: { en: 'Fixed-Point Sync' } },
|
||||
{ label: '客户端预测', slug: 'modules/network/prediction', translations: { en: 'Prediction' } },
|
||||
{ label: 'AOI 兴趣区域', slug: 'modules/network/aoi', translations: { en: 'AOI' } },
|
||||
{ label: '增量压缩', slug: 'modules/network/delta', translations: { en: 'Delta Compression' } },
|
||||
{ label: 'API 参考', slug: 'modules/network/api', translations: { en: 'API Reference' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '事务系统',
|
||||
translations: { en: 'Transaction' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'modules/transaction', translations: { en: 'Overview' } },
|
||||
{ label: '核心概念', slug: 'modules/transaction/core', translations: { en: 'Core Concepts' } },
|
||||
{ label: '存储层', slug: 'modules/transaction/storage', translations: { en: 'Storage Layer' } },
|
||||
{ label: '操作', slug: 'modules/transaction/operations', translations: { en: 'Operations' } },
|
||||
{ label: '分布式事务', slug: 'modules/transaction/distributed', translations: { en: 'Distributed' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '数据库',
|
||||
translations: { en: 'Database' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'modules/database', translations: { en: 'Overview' } },
|
||||
{ label: '仓储模式', slug: 'modules/database/repository', translations: { en: 'Repository' } },
|
||||
{ label: '用户仓储', slug: 'modules/database/user', translations: { en: 'User Repository' } },
|
||||
{ label: '查询构建器', slug: 'modules/database/query', translations: { en: 'Query Builder' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '数据库驱动',
|
||||
translations: { en: 'Database Drivers' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'modules/database-drivers', translations: { en: 'Overview' } },
|
||||
{ label: 'MongoDB', slug: 'modules/database-drivers/mongo', translations: { en: 'MongoDB' } },
|
||||
{ label: 'Redis', slug: 'modules/database-drivers/redis', translations: { en: 'Redis' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '世界流式加载',
|
||||
translations: { en: 'World Streaming' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'modules/world-streaming', translations: { en: 'Overview' } },
|
||||
{ label: '区块管理', slug: 'modules/world-streaming/chunk-manager', translations: { en: 'Chunk Manager' } },
|
||||
{ label: '流式系统', slug: 'modules/world-streaming/streaming-system', translations: { en: 'Streaming System' } },
|
||||
{ label: '序列化', slug: 'modules/world-streaming/serialization', translations: { en: 'Serialization' } },
|
||||
{ label: '实际示例', slug: 'modules/world-streaming/examples', translations: { en: 'Examples' } },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '示例',
|
||||
translations: { en: 'Examples' },
|
||||
items: [
|
||||
{ label: '示例总览', slug: 'examples', translations: { en: 'Examples Overview' } },
|
||||
{ label: 'Worker 系统演示', slug: 'examples/worker-system-demo', translations: { en: 'Worker System Demo' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'API 参考',
|
||||
translations: { en: 'API Reference' },
|
||||
autogenerate: { directory: 'api' },
|
||||
},
|
||||
{
|
||||
label: '更新日志',
|
||||
translations: { en: 'Changelog' },
|
||||
items: [
|
||||
{ label: '@esengine/ecs-framework', link: 'https://github.com/esengine/esengine/blob/master/packages/framework/core/CHANGELOG.md', attrs: { target: '_blank' } },
|
||||
{ label: '@esengine/behavior-tree', link: 'https://github.com/esengine/esengine/blob/master/packages/framework/behavior-tree/CHANGELOG.md', attrs: { target: '_blank' } },
|
||||
{ label: '@esengine/fsm', link: 'https://github.com/esengine/esengine/blob/master/packages/framework/fsm/CHANGELOG.md', attrs: { target: '_blank' } },
|
||||
{ label: '@esengine/timer', link: 'https://github.com/esengine/esengine/blob/master/packages/framework/timer/CHANGELOG.md', attrs: { target: '_blank' } },
|
||||
{ label: '@esengine/network', link: 'https://github.com/esengine/esengine/blob/master/packages/framework/network/CHANGELOG.md', attrs: { target: '_blank' } },
|
||||
{ label: '@esengine/transaction', link: 'https://github.com/esengine/esengine/blob/master/packages/framework/transaction/CHANGELOG.md', attrs: { target: '_blank' } },
|
||||
{ label: '@esengine/rpc', link: 'https://github.com/esengine/esengine/blob/master/packages/framework/rpc/CHANGELOG.md', attrs: { target: '_blank' } },
|
||||
{ label: '@esengine/cli', link: 'https://github.com/esengine/esengine/blob/master/packages/tools/cli/CHANGELOG.md', attrs: { target: '_blank' } },
|
||||
],
|
||||
},
|
||||
],
|
||||
customCss: ['./src/styles/custom.css'],
|
||||
head: [
|
||||
{ tag: 'meta', attrs: { name: 'theme-color', content: '#646cff' } },
|
||||
],
|
||||
components: {
|
||||
Head: './src/components/Head.astro',
|
||||
ThemeSelect: './src/components/ThemeSelect.astro',
|
||||
},
|
||||
}),
|
||||
vue(),
|
||||
],
|
||||
vite: {
|
||||
plugins: [tailwindcss()],
|
||||
},
|
||||
});
|
||||
@@ -1,293 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
本文档记录 `@esengine/ecs-framework` 核心库的版本更新历史。
|
||||
|
||||
---
|
||||
|
||||
## v2.4.2 (2025-12-25)
|
||||
|
||||
### Features
|
||||
|
||||
- **IncrementalSerializer 实体过滤**: 增量序列化支持 `entityFilter` 选项 (#335)
|
||||
- 创建快照时可按条件过滤实体
|
||||
- 支持按标签、组件类型等自定义过滤逻辑
|
||||
- 适用于只同步部分实体的场景(如只同步玩家)
|
||||
|
||||
```typescript
|
||||
// 只快照玩家实体
|
||||
const snapshot = IncrementalSerializer.createSnapshot(scene, {
|
||||
entityFilter: (entity) => entity.tag === PLAYER_TAG
|
||||
});
|
||||
|
||||
// 只快照有特定组件的实体
|
||||
const snapshot = IncrementalSerializer.createSnapshot(scene, {
|
||||
entityFilter: (entity) => entity.hasComponent(PlayerMarker)
|
||||
});
|
||||
```
|
||||
|
||||
### Refactor
|
||||
|
||||
- 优化 `PlatformWorkerPool` 代码规范,提取为独立模块 (#335)
|
||||
- 优化 `WorkerEntitySystem` 实现,改进代码结构 (#334)
|
||||
- 代码规范化与依赖清理 (#317)
|
||||
- 代码结构优化,添加 `GlobalTypes.ts` 统一类型定义 (#316)
|
||||
|
||||
---
|
||||
|
||||
## v2.4.1 (2025-12-23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复 `IntervalSystem` 时间累加 bug,间隔计时更加准确
|
||||
- 修复 Cocos Creator 兼容性问题,类型导出更完整
|
||||
|
||||
### Documentation
|
||||
|
||||
- 新增 `Core.paused` 属性文档说明
|
||||
|
||||
---
|
||||
|
||||
## v2.4.0 (2025-12-15)
|
||||
|
||||
### Features
|
||||
|
||||
- **EntityHandle 实体句柄**: 轻量级实体引用抽象 (#304)
|
||||
- 28位索引 + 20位代数(generation)设计,高效复用已销毁实体槽位
|
||||
- `EntityHandleManager` 管理句柄生命周期和有效性验证
|
||||
- 支持句柄转换为实体引用,检测悬空引用
|
||||
|
||||
- **SystemScheduler 系统调度器**: 声明式系统调度 (#304)
|
||||
- 新增 `@Stage(name)` 装饰器指定系统执行阶段
|
||||
- 新增 `@Before(SystemClass)` / `@After(SystemClass)` 装饰器声明系统依赖
|
||||
- 新增 `@InSet(setName)` 装饰器将系统归入逻辑分组
|
||||
- 基于拓扑排序自动解析执行顺序,检测循环依赖
|
||||
|
||||
- **EpochManager 变更检测**: 帧级变更追踪机制 (#304)
|
||||
- 跟踪组件添加/修改时间戳(epoch)
|
||||
- 支持查询"自上次检查以来变化的组件"
|
||||
- 适用于脏检测、增量更新等优化场景
|
||||
|
||||
- **CompiledQuery 编译查询**: 预编译类型安全查询 (#304)
|
||||
- 编译时生成优化的查询逻辑,减少运行时开销
|
||||
- 完整的 TypeScript 类型推断支持
|
||||
- 支持 `With`、`Without`、`Changed` 等查询条件组合
|
||||
|
||||
- **PluginServiceRegistry**: 类型安全的插件服务注册表 (#300)
|
||||
- 通过 `Core.pluginServices` 访问
|
||||
- 支持 `ServiceToken<T>` 模式获取服务
|
||||
|
||||
- **组件自动注册**: `@ECSComponent` 装饰器增强 (#302)
|
||||
- 装饰器现在自动注册到 `ComponentRegistry`
|
||||
- 解决 `Decorators ↔ ComponentRegistry` 循环依赖
|
||||
- 新建 `ComponentTypeUtils.ts` 作为底层无依赖模块
|
||||
|
||||
### API Changes
|
||||
|
||||
- `EntitySystem` 添加 `getBefore()` / `getAfter()` / `getSets()` getter 方法
|
||||
- `Entity` 添加 `markDirty()` 辅助方法用于手动触发变更检测
|
||||
- `IScene` 添加 `epochManager` 属性
|
||||
- `CommandBuffer.pendingCount` 修正为返回实际操作数(而非实体数)
|
||||
|
||||
### Documentation
|
||||
|
||||
- 更新系统调度文档,添加声明式依赖配置章节
|
||||
- 更新实体查询文档,添加编译查询使用说明
|
||||
|
||||
---
|
||||
|
||||
## v2.3.2 (2025-12-08)
|
||||
|
||||
### Features
|
||||
|
||||
- **微信小游戏 Worker 支持**: 添加对微信小游戏平台 Worker 的完整支持 (#297)
|
||||
- 新增 `workerScriptPath` 配置项,支持预编译 Worker 脚本路径
|
||||
- 修复微信小游戏 Worker 消息格式差异(`res` 直接是数据,无需 `.data`)
|
||||
- 适用于微信小游戏等不支持动态脚本的平台
|
||||
|
||||
### New Package
|
||||
|
||||
- **@esengine/worker-generator** `v1.0.2`: CLI 工具,从 `WorkerEntitySystem` 子类自动生成 Worker 文件
|
||||
- 自动扫描并提取 `workerProcess` 方法体
|
||||
- 支持 `--wechat` 模式,使用 TypeScript 编译器转换为 ES5 语法
|
||||
- 读取代码中的 `workerScriptPath` 配置,生成到指定路径
|
||||
- 生成 `worker-mapping.json` 映射文件
|
||||
|
||||
### Documentation
|
||||
|
||||
- 更新 Worker 系统文档,添加微信小游戏支持章节
|
||||
- 新增英文版 Worker 系统文档 (`docs/en/guide/worker-system.md`)
|
||||
|
||||
---
|
||||
|
||||
## v2.3.1 (2025-12-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **类型导出修复**: 修复 v2.3.0 中的类型导出问题
|
||||
- 解决 `ServiceToken` 跨包类型兼容性问题
|
||||
- 修复 `editor-app` 和 `behavior-tree-editor` 中的类型引用
|
||||
|
||||
---
|
||||
|
||||
## v2.3.0 (2025-12-06) ⚠️ DEPRECATED
|
||||
|
||||
> **警告**: 此版本存在类型导出问题,请升级到 v2.3.1 或更高版本。
|
||||
>
|
||||
> **Warning**: This version has type export issues. Please upgrade to v2.3.1 or later.
|
||||
|
||||
### Features
|
||||
|
||||
- **持久化实体**: 添加实体跨场景迁移支持 (#285)
|
||||
- 新增 `EEntityLifecyclePolicy` 枚举(`SceneLocal`/`Persistent`)
|
||||
- Entity 添加 `setPersistent()`、`setSceneLocal()`、`isPersistent` API
|
||||
- Scene 添加 `findPersistentEntities()`、`extractPersistentEntities()`、`receiveMigratedEntities()`
|
||||
- `SceneManager.setScene()` 自动处理持久化实体迁移
|
||||
- 适用场景:全局管理器、玩家角色、跨场景状态保持
|
||||
|
||||
- **CommandBuffer 延迟命令系统**: 在帧末统一执行实体操作 (#281)
|
||||
- 支持延迟添加/移除组件、销毁实体、设置实体激活状态
|
||||
- 每个系统拥有独立的 `commands` 属性
|
||||
- 避免在迭代过程中修改实体列表,防止迭代问题
|
||||
- Scene 在 `lateUpdate` 后自动刷新所有命令缓冲区
|
||||
|
||||
### Performance
|
||||
|
||||
- **ReactiveQuery 快照优化**: 优化实体查询迭代性能 (#281)
|
||||
- 添加快照机制,避免每帧拷贝数组
|
||||
- 只在实体列表变化时创建新快照
|
||||
- 静态场景下多个系统共享同一快照
|
||||
|
||||
---
|
||||
|
||||
## v2.2.21 (2025-12-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **迭代安全修复**: 修复 `process`/`lateProcess` 迭代时组件变化导致跳过实体的问题 (#272)
|
||||
- 在系统处理过程中添加/移除组件不再导致实体被意外跳过
|
||||
|
||||
### Performance
|
||||
|
||||
- **HierarchySystem 性能优化**: 优化层级系统避免每帧遍历所有实体 (#279)
|
||||
- 使用脏实体集合代替每帧遍历所有实体
|
||||
- 静态场景下 `process()` 从 O(n) 优化为 O(1)
|
||||
- 1000 实体静态场景: 81.79μs → 0.07μs (快 1168 倍)
|
||||
- 10000 实体静态场景: 939.43μs → 0.56μs (快 1677 倍)
|
||||
- 服务端模拟 (100房间 x 100实体): 2.7ms → 1.4ms 每 tick
|
||||
|
||||
---
|
||||
|
||||
## v2.2.20 (2025-12-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **系统 onAdded 回调修复**: 修复系统 `onAdded` 回调受注册顺序影响的问题 (#270)
|
||||
- 系统初始化时会对已存在的匹配实体触发 `onAdded` 回调
|
||||
- 新增 `matchesEntity(entity)` 方法,用于检查实体是否匹配系统的查询条件
|
||||
- Scene 新增 `notifySystemsEntityAdded/Removed` 方法,确保所有系统都能收到实体变更通知
|
||||
|
||||
---
|
||||
|
||||
## v2.2.19 (2025-12-03)
|
||||
|
||||
### Features
|
||||
|
||||
- **系统稳定排序**: 添加系统稳定排序支持 (#257)
|
||||
- 新增 `addOrder` 属性,用于 `updateOrder` 相同时的稳定排序
|
||||
- 确保相同优先级的系统按添加顺序执行
|
||||
|
||||
- **模块配置**: 添加 `module.json` 配置文件 (#256)
|
||||
- 定义模块 ID、名称、版本等元信息
|
||||
- 支持模块依赖声明和导出配置
|
||||
|
||||
---
|
||||
|
||||
## v2.2.18 (2025-11-30)
|
||||
|
||||
### Features
|
||||
|
||||
- **高级性能分析器**: 实现全新的性能分析 SDK (#248)
|
||||
- `ProfilerSDK`: 统一的性能分析接口
|
||||
- 手动采样标记 (`beginSample`/`endSample`)
|
||||
- 自动作用域测量 (`measure`/`measureAsync`)
|
||||
- 调用层级追踪和调用图生成
|
||||
- 计数器和仪表支持
|
||||
- `AdvancedProfilerCollector`: 高级性能数据收集器
|
||||
- 帧时间统计和历史记录
|
||||
- 内存快照和 GC 检测
|
||||
- 长任务检测 (Long Task API)
|
||||
- 性能报告生成
|
||||
- `DebugManager`: 调试管理器
|
||||
- 统一的调试工具入口
|
||||
- 性能分析器集成
|
||||
|
||||
- **属性装饰器增强**: 扩展 `@serialize` 装饰器功能 (#247)
|
||||
- 支持更多序列化选项配置
|
||||
|
||||
### Improvements
|
||||
|
||||
- **EntitySystem 测试覆盖**: 添加完整的系统测试用例 (#240)
|
||||
- 覆盖实体查询、缓存、生命周期等场景
|
||||
|
||||
- **Matcher 增强**: 优化匹配器功能 (#240)
|
||||
- 改进匹配逻辑和性能
|
||||
|
||||
---
|
||||
|
||||
## v2.2.17 (2025-11-28)
|
||||
|
||||
### Features
|
||||
|
||||
- **ComponentRegistry 增强**: 添加组件注册表新功能 (#244)
|
||||
- 支持通过名称注册和查询组件类型
|
||||
- 添加组件掩码缓存优化性能
|
||||
|
||||
- **序列化装饰器改进**: 增强 `@serialize` 装饰器 (#244)
|
||||
- 支持更灵活的序列化配置
|
||||
- 改进嵌套对象序列化
|
||||
|
||||
- **EntitySystem 生命周期**: 新增系统生命周期方法 (#244)
|
||||
- `onSceneStart()`: 场景开始时调用
|
||||
- `onSceneStop()`: 场景停止时调用
|
||||
|
||||
---
|
||||
|
||||
## v2.2.16 (2025-11-27)
|
||||
|
||||
### Features
|
||||
|
||||
- **组件生命周期**: 添加组件生命周期回调支持 (#237)
|
||||
- `onDeserialized()`: 组件从场景文件加载或快照恢复后调用,用于恢复运行时数据
|
||||
|
||||
- **ServiceContainer 增强**: 改进服务容器功能 (#237)
|
||||
- 支持 `Symbol.for()` 模式的服务标识
|
||||
- 新增 `@InjectProperty` 属性注入装饰器
|
||||
- 改进服务解析和生命周期管理
|
||||
|
||||
- **SceneSerializer 增强**: 场景序列化器新功能 (#237)
|
||||
- 支持更多组件类型的序列化
|
||||
- 改进反序列化错误处理
|
||||
|
||||
- **属性装饰器扩展**: 扩展 `@serialize` 装饰器 (#238)
|
||||
- 支持 `@range`、`@slider` 等编辑器提示
|
||||
- 支持 `@dropdown`、`@color` 等 UI 类型
|
||||
- 支持 `@asset` 资源引用类型
|
||||
|
||||
### Improvements
|
||||
|
||||
- **Matcher 测试**: 添加 Matcher 匹配器测试用例 (#240)
|
||||
- **EntitySystem 测试**: 添加实体系统完整测试覆盖 (#240)
|
||||
|
||||
---
|
||||
|
||||
## 版本说明
|
||||
|
||||
- **主版本号**: 重大不兼容更新
|
||||
- **次版本号**: 新功能添加(向后兼容)
|
||||
- **修订版本号**: Bug 修复和小改进
|
||||
|
||||
## 相关链接
|
||||
|
||||
- [GitHub Releases](https://github.com/esengine/esengine/releases)
|
||||
- [NPM Package](https://www.npmjs.com/package/@esengine/ecs-framework)
|
||||
- [文档首页](./index.md)
|
||||
@@ -1,291 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
This document records the version update history of the `@esengine/ecs-framework` core library.
|
||||
|
||||
---
|
||||
|
||||
## v2.4.2 (2025-12-25)
|
||||
|
||||
### Features
|
||||
|
||||
- **IncrementalSerializer Entity Filter**: Incremental serialization supports `entityFilter` option (#335)
|
||||
- Filter entities by condition when creating snapshots
|
||||
- Support custom filter logic by tag, component type, etc.
|
||||
- Suitable for scenarios that only sync partial entities (e.g., only sync players)
|
||||
|
||||
```typescript
|
||||
// Only snapshot player entities
|
||||
const snapshot = IncrementalSerializer.createSnapshot(scene, {
|
||||
entityFilter: (entity) => entity.tag === PLAYER_TAG
|
||||
});
|
||||
|
||||
// Only snapshot entities with specific component
|
||||
const snapshot = IncrementalSerializer.createSnapshot(scene, {
|
||||
entityFilter: (entity) => entity.hasComponent(PlayerMarker)
|
||||
});
|
||||
```
|
||||
|
||||
### Refactor
|
||||
|
||||
- Optimize `PlatformWorkerPool` code style, extract as standalone module (#335)
|
||||
- Optimize `WorkerEntitySystem` implementation, improve code structure (#334)
|
||||
- Code standardization and dependency cleanup (#317)
|
||||
- Code structure optimization, add `GlobalTypes.ts` for unified type definitions (#316)
|
||||
|
||||
---
|
||||
|
||||
## v2.4.1 (2025-12-23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix `IntervalSystem` time accumulation bug, interval timing is now more accurate
|
||||
- Fix Cocos Creator compatibility issue, more complete type exports
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add `Core.paused` property documentation
|
||||
|
||||
---
|
||||
|
||||
## v2.4.0 (2025-12-15)
|
||||
|
||||
### Features
|
||||
|
||||
- **EntityHandle**: Lightweight entity reference abstraction (#304)
|
||||
- 28-bit index + 20-bit generation design for efficient reuse of destroyed entity slots
|
||||
- `EntityHandleManager` manages handle lifecycle and validity verification
|
||||
- Support handle-to-entity conversion with dangling reference detection
|
||||
|
||||
- **SystemScheduler**: Declarative system scheduling (#304)
|
||||
- New `@Stage(name)` decorator to specify system execution stage
|
||||
- New `@Before(SystemClass)` / `@After(SystemClass)` decorators to declare dependencies
|
||||
- New `@InSet(setName)` decorator to group systems logically
|
||||
- Automatic execution order resolution via topological sort with cycle detection
|
||||
|
||||
- **EpochManager**: Frame-level change detection mechanism (#304)
|
||||
- Track component add/modify timestamps (epochs)
|
||||
- Support querying "components changed since last check"
|
||||
- Suitable for dirty checking, incremental updates, and other optimization scenarios
|
||||
|
||||
- **CompiledQuery**: Pre-compiled type-safe queries (#304)
|
||||
- Compile-time generated optimized query logic, reducing runtime overhead
|
||||
- Full TypeScript type inference support
|
||||
- Support `With`, `Without`, `Changed` and other query condition combinations
|
||||
|
||||
- **PluginServiceRegistry**: Type-safe plugin service registry (#300)
|
||||
- Accessible via `Core.pluginServices`
|
||||
- Support `ServiceToken<T>` pattern for service retrieval
|
||||
|
||||
- **Component Auto-Registration**: `@ECSComponent` decorator enhancement (#302)
|
||||
- Decorator now automatically registers to `ComponentRegistry`
|
||||
- Resolved `Decorators ↔ ComponentRegistry` circular dependency
|
||||
- New `ComponentTypeUtils.ts` as low-level dependency-free module
|
||||
|
||||
### API Changes
|
||||
|
||||
- `EntitySystem` adds `getBefore()` / `getAfter()` / `getSets()` getter methods
|
||||
- `Entity` adds `markDirty()` helper method for manual change detection triggering
|
||||
- `IScene` adds `epochManager` property
|
||||
- `CommandBuffer.pendingCount` corrected to return actual operation count (not entity count)
|
||||
|
||||
### Documentation
|
||||
|
||||
- Updated system scheduling documentation with declarative dependency configuration
|
||||
- Updated entity query documentation with compiled query usage
|
||||
|
||||
---
|
||||
|
||||
## v2.3.2 (2025-12-08)
|
||||
|
||||
### Features
|
||||
|
||||
- **WeChat Mini Game Worker Support**: Add complete Worker support for WeChat Mini Game platform (#297)
|
||||
- New `workerScriptPath` config option for pre-compiled Worker script path
|
||||
- Fix WeChat Mini Game Worker message format difference (`res` is data directly, no `.data` wrapper)
|
||||
- Applicable to WeChat Mini Game and other platforms that don't support dynamic scripts
|
||||
|
||||
### New Package
|
||||
|
||||
- **@esengine/worker-generator** `v1.0.2`: CLI tool to auto-generate Worker files from `WorkerEntitySystem` subclasses
|
||||
- Automatically scan and extract `workerProcess` method body
|
||||
- Support `--wechat` mode, use TypeScript compiler to convert to ES5 syntax
|
||||
- Read `workerScriptPath` config from code, generate to specified path
|
||||
- Generate `worker-mapping.json` mapping file
|
||||
|
||||
### Documentation
|
||||
|
||||
- Updated Worker system documentation with WeChat Mini Game support section
|
||||
- Added English Worker system documentation (`docs/en/guide/worker-system.md`)
|
||||
|
||||
---
|
||||
|
||||
## v2.3.1 (2025-12-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Type export fix**: Fix type export issues in v2.3.0
|
||||
- Resolve `ServiceToken` cross-package type compatibility issues
|
||||
- Fix type references in `editor-app` and `behavior-tree-editor`
|
||||
|
||||
---
|
||||
|
||||
## v2.3.0 (2025-12-06) ⚠️ DEPRECATED
|
||||
|
||||
> **Warning**: This version has type export issues. Please upgrade to v2.3.1 or later.
|
||||
|
||||
### Features
|
||||
|
||||
- **Persistent Entity**: Add entity cross-scene migration support (#285)
|
||||
- New `EEntityLifecyclePolicy` enum (`SceneLocal`/`Persistent`)
|
||||
- Entity adds `setPersistent()`, `setSceneLocal()`, `isPersistent` API
|
||||
- Scene adds `findPersistentEntities()`, `extractPersistentEntities()`, `receiveMigratedEntities()`
|
||||
- `SceneManager.setScene()` automatically handles persistent entity migration
|
||||
- Use cases: global managers, player characters, cross-scene state persistence
|
||||
|
||||
- **CommandBuffer Deferred Command System**: Execute entity operations uniformly at end of frame (#281)
|
||||
- Support deferred add/remove components, destroy entities, set entity active state
|
||||
- Each system has its own `commands` property
|
||||
- Avoid modifying entity list during iteration, preventing iteration issues
|
||||
- Scene automatically flushes all command buffers after `lateUpdate`
|
||||
|
||||
### Performance
|
||||
|
||||
- **ReactiveQuery Snapshot Optimization**: Optimize entity query iteration performance (#281)
|
||||
- Add snapshot mechanism to avoid copying arrays every frame
|
||||
- Only create new snapshots when entity list changes
|
||||
- Multiple systems share the same snapshot in static scenes
|
||||
|
||||
---
|
||||
|
||||
## v2.2.21 (2025-12-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Iteration safety fix**: Fix issue where component changes during `process`/`lateProcess` iteration caused entities to be skipped (#272)
|
||||
- Adding/removing components during system processing no longer causes entities to be unexpectedly skipped
|
||||
|
||||
### Performance
|
||||
|
||||
- **HierarchySystem optimization**: Optimize hierarchy system to avoid iterating all entities every frame (#279)
|
||||
- Use dirty entity set instead of iterating all entities
|
||||
- Static scene `process()` optimized from O(n) to O(1)
|
||||
- 1000 entities static scene: 81.79μs → 0.07μs (1168x faster)
|
||||
- 10000 entities static scene: 939.43μs → 0.56μs (1677x faster)
|
||||
- Server simulation (100 rooms x 100 entities): 2.7ms → 1.4ms per tick
|
||||
|
||||
---
|
||||
|
||||
## v2.2.20 (2025-12-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **System onAdded callback fix**: Fix issue where system `onAdded` callback was affected by registration order (#270)
|
||||
- System initialization now triggers `onAdded` callback for existing matching entities
|
||||
- Added `matchesEntity(entity)` method to check if an entity matches the system's query condition
|
||||
- Scene added `notifySystemsEntityAdded/Removed` methods to ensure all systems receive entity change notifications
|
||||
|
||||
---
|
||||
|
||||
## v2.2.19 (2025-12-03)
|
||||
|
||||
### Features
|
||||
|
||||
- **System stable sorting**: Add stable sorting support for systems (#257)
|
||||
- Added `addOrder` property for stable sorting when `updateOrder` is the same
|
||||
- Ensures systems with same priority execute in add order
|
||||
|
||||
- **Module configuration**: Add `module.json` configuration file (#256)
|
||||
- Define module ID, name, version and other metadata
|
||||
- Support module dependency declaration and export configuration
|
||||
|
||||
---
|
||||
|
||||
## v2.2.18 (2025-11-30)
|
||||
|
||||
### Features
|
||||
|
||||
- **Advanced Performance Profiler**: Implement new performance analysis SDK (#248)
|
||||
- `ProfilerSDK`: Unified performance analysis interface
|
||||
- Manual sampling markers (`beginSample`/`endSample`)
|
||||
- Automatic scope measurement (`measure`/`measureAsync`)
|
||||
- Call hierarchy tracking and call graph generation
|
||||
- Counter and gauge support
|
||||
- `AdvancedProfilerCollector`: Advanced performance data collector
|
||||
- Frame time statistics and history
|
||||
- Memory snapshots and GC detection
|
||||
- Long task detection (Long Task API)
|
||||
- Performance report generation
|
||||
- `DebugManager`: Debug manager
|
||||
- Unified debug tool entry point
|
||||
- Profiler integration
|
||||
|
||||
- **Property decorator enhancement**: Extend `@serialize` decorator functionality (#247)
|
||||
- Support more serialization option configurations
|
||||
|
||||
### Improvements
|
||||
|
||||
- **EntitySystem test coverage**: Add complete system test cases (#240)
|
||||
- Cover entity query, cache, lifecycle scenarios
|
||||
|
||||
- **Matcher enhancement**: Optimize matcher functionality (#240)
|
||||
- Improved matching logic and performance
|
||||
|
||||
---
|
||||
|
||||
## v2.2.17 (2025-11-28)
|
||||
|
||||
### Features
|
||||
|
||||
- **ComponentRegistry enhancement**: Add new component registry features (#244)
|
||||
- Support registering and querying component types by name
|
||||
- Add component mask caching for performance optimization
|
||||
|
||||
- **Serialization decorator improvements**: Enhance `@serialize` decorator (#244)
|
||||
- Support more flexible serialization configuration
|
||||
- Improved nested object serialization
|
||||
|
||||
- **EntitySystem lifecycle**: Add new system lifecycle methods (#244)
|
||||
- `onSceneStart()`: Called when scene starts
|
||||
- `onSceneStop()`: Called when scene stops
|
||||
|
||||
---
|
||||
|
||||
## v2.2.16 (2025-11-27)
|
||||
|
||||
### Features
|
||||
|
||||
- **Component lifecycle**: Add component lifecycle callback support (#237)
|
||||
- `onDeserialized()`: Called after component is loaded from scene file or snapshot restore, used to restore runtime data
|
||||
|
||||
- **ServiceContainer enhancement**: Improve service container functionality (#237)
|
||||
- Support `Symbol.for()` pattern for service identifiers
|
||||
- Added `@InjectProperty` property injection decorator
|
||||
- Improved service resolution and lifecycle management
|
||||
|
||||
- **SceneSerializer enhancement**: New scene serializer features (#237)
|
||||
- Support serialization of more component types
|
||||
- Improved deserialization error handling
|
||||
|
||||
- **Property decorator extension**: Extend `@serialize` decorator (#238)
|
||||
- Support `@range`, `@slider` and other editor hints
|
||||
- Support `@dropdown`, `@color` and other UI types
|
||||
- Support `@asset` resource reference type
|
||||
|
||||
### Improvements
|
||||
|
||||
- **Matcher tests**: Add Matcher test cases (#240)
|
||||
- **EntitySystem tests**: Add complete entity system test coverage (#240)
|
||||
|
||||
---
|
||||
|
||||
## Version Notes
|
||||
|
||||
- **Major version**: Breaking changes
|
||||
- **Minor version**: New features (backward compatible)
|
||||
- **Patch version**: Bug fixes and improvements
|
||||
|
||||
## Related Links
|
||||
|
||||
- [GitHub Releases](https://github.com/esengine/esengine/releases)
|
||||
- [NPM Package](https://www.npmjs.com/package/@esengine/ecs-framework)
|
||||
- [Documentation Home](./index.md)
|
||||
@@ -1,444 +0,0 @@
|
||||
# Entity
|
||||
|
||||
In ECS architecture, an Entity is the basic object in the game world. An entity itself does not contain game logic or data - it's just a container that combines different components to achieve various functionalities.
|
||||
|
||||
## Basic Concepts
|
||||
|
||||
An entity is a lightweight object mainly used for:
|
||||
- Serving as a container for components
|
||||
- Providing a unique identifier (ID)
|
||||
- Managing component lifecycle
|
||||
|
||||
::: tip About Parent-Child Hierarchy
|
||||
Parent-child hierarchy relationships between entities are managed through `HierarchyComponent` and `HierarchySystem`, not built-in Entity properties. This design follows ECS composition principles - only entities that need hierarchy relationships add this component.
|
||||
|
||||
See [Hierarchy System](./hierarchy.md) documentation.
|
||||
:::
|
||||
|
||||
## Creating Entities
|
||||
|
||||
**Important: Entities must be created through Scene, manual creation is not supported!**
|
||||
|
||||
Entities must be created through the scene's `createEntity()` method to ensure:
|
||||
- Entity is properly added to the scene's entity management system
|
||||
- Entity is added to the query system for system use
|
||||
- Entity gets the correct scene reference
|
||||
- Related lifecycle events are triggered
|
||||
|
||||
```typescript
|
||||
// Correct way: create entity through scene
|
||||
const player = scene.createEntity("Player");
|
||||
|
||||
// Wrong way: manually create entity
|
||||
// const entity = new Entity("MyEntity", 1); // System cannot manage such entities
|
||||
```
|
||||
|
||||
## Adding Components
|
||||
|
||||
Entities gain functionality by adding components:
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent } from '@esengine/ecs-framework';
|
||||
|
||||
// Define position component
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
x: number = 0;
|
||||
y: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
|
||||
// Define health component
|
||||
@ECSComponent('Health')
|
||||
class Health extends Component {
|
||||
current: number = 100;
|
||||
max: number = 100;
|
||||
|
||||
constructor(max: number = 100) {
|
||||
super();
|
||||
this.max = max;
|
||||
this.current = max;
|
||||
}
|
||||
}
|
||||
|
||||
// Add components to entity
|
||||
const player = scene.createEntity("Player");
|
||||
player.addComponent(new Position(100, 200));
|
||||
player.addComponent(new Health(150));
|
||||
```
|
||||
|
||||
## Getting Components
|
||||
|
||||
```typescript
|
||||
// Get component (pass component class, not instance)
|
||||
const position = player.getComponent(Position); // Returns Position | null
|
||||
const health = player.getComponent(Health); // Returns Health | null
|
||||
|
||||
// Check if component exists
|
||||
if (position) {
|
||||
console.log(`Player position: x=${position.x}, y=${position.y}`);
|
||||
}
|
||||
|
||||
// Check if entity has a component
|
||||
if (player.hasComponent(Position)) {
|
||||
console.log("Player has position component");
|
||||
}
|
||||
|
||||
// Get all component instances (read-only property)
|
||||
const allComponents = player.components; // readonly Component[]
|
||||
|
||||
// Get all components of specified type (supports multiple components of same type)
|
||||
const allHealthComponents = player.getComponents(Health); // Health[]
|
||||
|
||||
// Get or create component (creates automatically if not exists)
|
||||
const position = player.getOrCreateComponent(Position, 0, 0); // Pass constructor arguments
|
||||
const health = player.getOrCreateComponent(Health, 100); // Returns existing if present, creates new if not
|
||||
```
|
||||
|
||||
## Removing Components
|
||||
|
||||
```typescript
|
||||
// Method 1: Remove by component type
|
||||
const removedHealth = player.removeComponentByType(Health);
|
||||
if (removedHealth) {
|
||||
console.log("Health component removed");
|
||||
}
|
||||
|
||||
// Method 2: Remove by component instance
|
||||
const healthComponent = player.getComponent(Health);
|
||||
if (healthComponent) {
|
||||
player.removeComponent(healthComponent);
|
||||
}
|
||||
|
||||
// Batch remove multiple component types
|
||||
const removedComponents = player.removeComponentsByTypes([Position, Health]);
|
||||
|
||||
// Check if component was removed
|
||||
if (!player.hasComponent(Health)) {
|
||||
console.log("Health component has been removed");
|
||||
}
|
||||
```
|
||||
|
||||
## Finding Entities
|
||||
|
||||
Scene provides multiple ways to find entities:
|
||||
|
||||
### Find by Name
|
||||
|
||||
```typescript
|
||||
// Find single entity
|
||||
const player = scene.findEntity("Player");
|
||||
// Or use alias method
|
||||
const player2 = scene.getEntityByName("Player");
|
||||
|
||||
if (player) {
|
||||
console.log("Found player entity");
|
||||
}
|
||||
```
|
||||
|
||||
### Find by ID
|
||||
|
||||
```typescript
|
||||
// Find by entity ID
|
||||
const entity = scene.findEntityById(123);
|
||||
```
|
||||
|
||||
### Find by Tag
|
||||
|
||||
Entities support a tag system for quick categorization and lookup:
|
||||
|
||||
```typescript
|
||||
// Set tags
|
||||
player.tag = 1; // Player tag
|
||||
enemy.tag = 2; // Enemy tag
|
||||
|
||||
// Find all entities by tag
|
||||
const players = scene.findEntitiesByTag(1);
|
||||
const enemies = scene.findEntitiesByTag(2);
|
||||
// Or use alias method
|
||||
const allPlayers = scene.getEntitiesByTag(1);
|
||||
```
|
||||
|
||||
## Entity Lifecycle
|
||||
|
||||
```typescript
|
||||
// Destroy entity
|
||||
player.destroy();
|
||||
|
||||
// Check if entity is destroyed
|
||||
if (player.isDestroyed) {
|
||||
console.log("Entity has been destroyed");
|
||||
}
|
||||
```
|
||||
|
||||
## Entity Events
|
||||
|
||||
Component changes on entities trigger events:
|
||||
|
||||
```typescript
|
||||
// Listen for component added event
|
||||
scene.eventSystem.on('component:added', (data) => {
|
||||
console.log('Component added:', data);
|
||||
});
|
||||
|
||||
// Listen for entity created event
|
||||
scene.eventSystem.on('entity:created', (data) => {
|
||||
console.log('Entity created:', data.entityName);
|
||||
});
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Batch Entity Creation
|
||||
|
||||
The framework provides high-performance batch creation methods:
|
||||
|
||||
```typescript
|
||||
// Batch create 100 bullet entities (high-performance version)
|
||||
const bullets = scene.createEntities(100, "Bullet");
|
||||
|
||||
// Add components to each bullet
|
||||
bullets.forEach((bullet, index) => {
|
||||
bullet.addComponent(new Position(Math.random() * 800, Math.random() * 600));
|
||||
bullet.addComponent(new Velocity(Math.random() * 100 - 50, Math.random() * 100 - 50));
|
||||
});
|
||||
```
|
||||
|
||||
`createEntities()` method will:
|
||||
- Batch allocate entity IDs
|
||||
- Batch add to entity list
|
||||
- Optimize query system updates
|
||||
- Reduce system cache clearing times
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Appropriate Component Granularity
|
||||
|
||||
```typescript
|
||||
// Good practice: single-purpose components
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
x: number = 0;
|
||||
y: number = 0;
|
||||
}
|
||||
|
||||
@ECSComponent('Velocity')
|
||||
class Velocity extends Component {
|
||||
dx: number = 0;
|
||||
dy: number = 0;
|
||||
}
|
||||
|
||||
// Avoid: overly complex components
|
||||
@ECSComponent('Player')
|
||||
class Player extends Component {
|
||||
// Avoid including too many unrelated properties in one component
|
||||
x: number;
|
||||
y: number;
|
||||
health: number;
|
||||
inventory: Item[];
|
||||
skills: Skill[];
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Use Decorators
|
||||
|
||||
Always use `@ECSComponent` decorator:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Transform')
|
||||
class Transform extends Component {
|
||||
// Component implementation
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Proper Naming
|
||||
|
||||
```typescript
|
||||
// Clear entity naming
|
||||
const mainCharacter = scene.createEntity("MainCharacter");
|
||||
const enemy1 = scene.createEntity("Goblin_001");
|
||||
const collectible = scene.createEntity("HealthPotion");
|
||||
```
|
||||
|
||||
### 4. Timely Cleanup
|
||||
|
||||
```typescript
|
||||
// Destroy entities that are no longer needed
|
||||
if (enemy.getComponent(Health).current <= 0) {
|
||||
enemy.destroy();
|
||||
}
|
||||
```
|
||||
|
||||
## Debugging Entities
|
||||
|
||||
The framework provides debugging features to help development:
|
||||
|
||||
```typescript
|
||||
// Get entity debug info
|
||||
const debugInfo = entity.getDebugInfo();
|
||||
console.log('Entity info:', debugInfo);
|
||||
|
||||
// List all components of entity
|
||||
entity.components.forEach(component => {
|
||||
console.log('Component:', component.constructor.name);
|
||||
});
|
||||
```
|
||||
|
||||
Entities are one of the core concepts in ECS architecture. Understanding how to use entities correctly will help you build efficient, maintainable game code.
|
||||
|
||||
## Entity Handle (EntityHandle)
|
||||
|
||||
Entity handles provide a safe way to reference entities, solving the "referencing destroyed entity" problem.
|
||||
|
||||
### Problem Scenario
|
||||
|
||||
Suppose your AI system needs to track a target enemy:
|
||||
|
||||
```typescript
|
||||
// Wrong approach: directly store entity reference
|
||||
class AISystem extends EntitySystem {
|
||||
private targetEnemy: Entity | null = null;
|
||||
|
||||
setTarget(enemy: Entity) {
|
||||
this.targetEnemy = enemy;
|
||||
}
|
||||
|
||||
process() {
|
||||
if (this.targetEnemy) {
|
||||
// Dangerous! Enemy might be destroyed, but reference still exists
|
||||
// Worse: this memory location might be reused by a new entity
|
||||
const health = this.targetEnemy.getComponent(Health);
|
||||
// Might operate on the wrong entity!
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Correct Approach Using Handles
|
||||
|
||||
Each entity is automatically assigned a handle when created, accessible via `entity.handle`:
|
||||
|
||||
```typescript
|
||||
import { EntityHandle, NULL_HANDLE, isValidHandle } from '@esengine/ecs-framework';
|
||||
|
||||
class AISystem extends EntitySystem {
|
||||
// Store handle instead of entity reference
|
||||
private targetHandle: EntityHandle = NULL_HANDLE;
|
||||
|
||||
setTarget(enemy: Entity) {
|
||||
// Save enemy's handle
|
||||
this.targetHandle = enemy.handle;
|
||||
}
|
||||
|
||||
process() {
|
||||
if (!isValidHandle(this.targetHandle)) {
|
||||
return; // No target
|
||||
}
|
||||
|
||||
// Get entity through handle (automatically checks validity)
|
||||
const enemy = this.scene.findEntityByHandle(this.targetHandle);
|
||||
|
||||
if (!enemy) {
|
||||
// Enemy was destroyed, clear reference
|
||||
this.targetHandle = NULL_HANDLE;
|
||||
return;
|
||||
}
|
||||
|
||||
// Safe operation
|
||||
const health = enemy.getComponent(Health);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Complete Example: Skill Target Locking
|
||||
|
||||
```typescript
|
||||
import {
|
||||
EntitySystem, Entity, EntityHandle, NULL_HANDLE, isValidHandle
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
@ECSSystem('SkillTargeting')
|
||||
class SkillTargetingSystem extends EntitySystem {
|
||||
// Store handles for multiple targets
|
||||
private lockedTargets: Map<Entity, EntityHandle> = new Map();
|
||||
|
||||
// Lock target
|
||||
lockTarget(caster: Entity, target: Entity) {
|
||||
this.lockedTargets.set(caster, target.handle);
|
||||
}
|
||||
|
||||
// Get locked target
|
||||
getLockedTarget(caster: Entity): Entity | null {
|
||||
const handle = this.lockedTargets.get(caster);
|
||||
|
||||
if (!handle || !isValidHandle(handle)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// findEntityByHandle checks if handle is valid
|
||||
const target = this.scene.findEntityByHandle(handle);
|
||||
|
||||
if (!target) {
|
||||
// Target died, clear lock
|
||||
this.lockedTargets.delete(caster);
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
// Cast skill
|
||||
castSkill(caster: Entity) {
|
||||
const target = this.getLockedTarget(caster);
|
||||
|
||||
if (!target) {
|
||||
console.log('Target lost, skill cancelled');
|
||||
return;
|
||||
}
|
||||
|
||||
// Safely deal damage to target
|
||||
const health = target.getComponent(Health);
|
||||
if (health) {
|
||||
health.current -= 10;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Handle vs Entity Reference
|
||||
|
||||
| Scenario | Recommended Approach |
|
||||
|----------|---------------------|
|
||||
| Temporary use within same frame | Use `Entity` reference directly |
|
||||
| Cross-frame storage (e.g., AI target, skill target) | Use `EntityHandle` |
|
||||
| Needs serialization | Use `EntityHandle` (numeric type) |
|
||||
| Network synchronization | Use `EntityHandle` (can be transmitted directly) |
|
||||
|
||||
### API Quick Reference
|
||||
|
||||
```typescript
|
||||
// Get entity's handle
|
||||
const handle = entity.handle;
|
||||
|
||||
// Check if handle is non-null
|
||||
if (isValidHandle(handle)) { ... }
|
||||
|
||||
// Get entity through handle (automatically checks validity)
|
||||
const entity = scene.findEntityByHandle(handle);
|
||||
|
||||
// Check if entity corresponding to handle is alive
|
||||
const alive = scene.handleManager.isAlive(handle);
|
||||
|
||||
// Null handle constant
|
||||
const emptyHandle = NULL_HANDLE;
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Learn about [Hierarchy System](./hierarchy.md) to establish parent-child relationships
|
||||
- Learn about [Component System](./component.md) to add functionality to entities
|
||||
- Learn about [Scene Management](./scene.md) to organize and manage entities
|
||||
@@ -1,364 +0,0 @@
|
||||
# Scene Management
|
||||
|
||||
In the ECS architecture, a Scene is a container for the game world, responsible for managing the lifecycle of entities, systems, and components. Scenes provide a complete ECS runtime environment.
|
||||
|
||||
## Basic Concepts
|
||||
|
||||
Scene is the core container of the ECS framework, providing:
|
||||
- Entity creation, management, and destruction
|
||||
- System registration and execution scheduling
|
||||
- Component storage and querying
|
||||
- Event system support
|
||||
- Performance monitoring and debugging information
|
||||
|
||||
## Scene Management Options
|
||||
|
||||
ECS Framework provides two scene management approaches:
|
||||
|
||||
1. **[SceneManager](./scene-manager)** - Suitable for 95% of game applications
|
||||
- Single-player games, simple multiplayer games, mobile games
|
||||
- Lightweight, simple and intuitive API
|
||||
- Supports scene transitions
|
||||
|
||||
2. **[WorldManager](./world-manager)** - Suitable for advanced multi-world isolation scenarios
|
||||
- MMO game servers, game room systems
|
||||
- Multi-World management, each World can contain multiple scenes
|
||||
- Completely isolated independent environments
|
||||
|
||||
This document focuses on the usage of the Scene class itself. For detailed information about scene managers, please refer to the corresponding documentation.
|
||||
|
||||
## Creating a Scene
|
||||
|
||||
### Inheriting the Scene Class
|
||||
|
||||
**Recommended: Inherit the Scene class to create custom scenes**
|
||||
|
||||
```typescript
|
||||
import { Scene, EntitySystem } from '@esengine/ecs-framework';
|
||||
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Set scene name
|
||||
this.name = "GameScene";
|
||||
|
||||
// Add systems
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
this.addSystem(new PhysicsSystem());
|
||||
|
||||
// Create initial entities
|
||||
this.createInitialEntities();
|
||||
}
|
||||
|
||||
private createInitialEntities(): void {
|
||||
// Create player
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Position(400, 300));
|
||||
player.addComponent(new Health(100));
|
||||
player.addComponent(new PlayerController());
|
||||
|
||||
// Create enemies
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const enemy = this.createEntity(`Enemy_${i}`);
|
||||
enemy.addComponent(new Position(Math.random() * 800, Math.random() * 600));
|
||||
enemy.addComponent(new Health(50));
|
||||
enemy.addComponent(new EnemyAI());
|
||||
}
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log("Game scene started");
|
||||
// Logic when scene starts
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
console.log("Game scene unloaded");
|
||||
// Cleanup logic when scene unloads
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using Scene Configuration
|
||||
|
||||
```typescript
|
||||
import { ISceneConfig } from '@esengine/ecs-framework';
|
||||
|
||||
const config: ISceneConfig = {
|
||||
name: "MainGame",
|
||||
enableEntityDirectUpdate: false
|
||||
};
|
||||
|
||||
class ConfiguredScene extends Scene {
|
||||
constructor() {
|
||||
super(config);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Scene Lifecycle
|
||||
|
||||
Scene provides complete lifecycle management:
|
||||
|
||||
```typescript
|
||||
class ExampleScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Scene initialization: setup systems and initial entities
|
||||
console.log("Scene initializing");
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
// Scene starts running: game logic begins execution
|
||||
console.log("Scene starting");
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// Scene unloading: cleanup resources
|
||||
console.log("Scene unloading");
|
||||
}
|
||||
}
|
||||
|
||||
// Using scenes (lifecycle automatically managed by framework)
|
||||
const scene = new ExampleScene();
|
||||
// Scene's initialize(), begin(), update(), end() are automatically called by the framework
|
||||
```
|
||||
|
||||
**Lifecycle Methods**:
|
||||
|
||||
1. `initialize()` - Scene initialization, setup systems and initial entities
|
||||
2. `begin()` / `onStart()` - Scene starts running
|
||||
3. `update()` - Per-frame update (called by scene manager)
|
||||
4. `end()` / `unload()` - Scene unloading, cleanup resources
|
||||
|
||||
## Entity Management
|
||||
|
||||
### Creating Entities
|
||||
|
||||
```typescript
|
||||
class EntityScene extends Scene {
|
||||
createGameEntities(): void {
|
||||
// Create single entity
|
||||
const player = this.createEntity("Player");
|
||||
|
||||
// Batch create entities (high performance)
|
||||
const bullets = this.createEntities(100, "Bullet");
|
||||
|
||||
// Add components to batch-created entities
|
||||
bullets.forEach((bullet, index) => {
|
||||
bullet.addComponent(new Position(index * 10, 100));
|
||||
bullet.addComponent(new Velocity(Math.random() * 200 - 100, -300));
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Finding Entities
|
||||
|
||||
```typescript
|
||||
class SearchScene extends Scene {
|
||||
findEntities(): void {
|
||||
// Find by name
|
||||
const player = this.findEntity("Player");
|
||||
const player2 = this.getEntityByName("Player"); // Alias method
|
||||
|
||||
// Find by ID
|
||||
const entity = this.findEntityById(123);
|
||||
|
||||
// Find by tag
|
||||
const enemies = this.findEntitiesByTag(2);
|
||||
const enemies2 = this.getEntitiesByTag(2); // Alias method
|
||||
|
||||
if (player) {
|
||||
console.log(`Found player: ${player.name}`);
|
||||
}
|
||||
|
||||
console.log(`Found ${enemies.length} enemies`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Destroying Entities
|
||||
|
||||
```typescript
|
||||
class DestroyScene extends Scene {
|
||||
cleanupEntities(): void {
|
||||
// Destroy all entities
|
||||
this.destroyAllEntities();
|
||||
|
||||
// Single entity destruction through the entity itself
|
||||
const enemy = this.findEntity("Enemy_1");
|
||||
if (enemy) {
|
||||
enemy.destroy(); // Entity is automatically removed from the scene
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## System Management
|
||||
|
||||
### Adding and Removing Systems
|
||||
|
||||
```typescript
|
||||
class SystemScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Add systems
|
||||
const movementSystem = new MovementSystem();
|
||||
this.addSystem(movementSystem);
|
||||
|
||||
// Set system update order
|
||||
movementSystem.updateOrder = 1;
|
||||
|
||||
// Add more systems
|
||||
this.addSystem(new PhysicsSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
}
|
||||
|
||||
public removeUnnecessarySystems(): void {
|
||||
// Get system
|
||||
const physicsSystem = this.getEntityProcessor(PhysicsSystem);
|
||||
|
||||
// Remove system
|
||||
if (physicsSystem) {
|
||||
this.removeSystem(physicsSystem);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Event System
|
||||
|
||||
Scene has a built-in type-safe event system:
|
||||
|
||||
```typescript
|
||||
class EventScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Listen to events
|
||||
this.eventSystem.on('player_died', this.onPlayerDied.bind(this));
|
||||
this.eventSystem.on('enemy_spawned', this.onEnemySpawned.bind(this));
|
||||
this.eventSystem.on('level_complete', this.onLevelComplete.bind(this));
|
||||
}
|
||||
|
||||
private onPlayerDied(data: any): void {
|
||||
console.log('Player died event');
|
||||
// Handle player death
|
||||
}
|
||||
|
||||
private onEnemySpawned(data: any): void {
|
||||
console.log('Enemy spawned event');
|
||||
// Handle enemy spawn
|
||||
}
|
||||
|
||||
private onLevelComplete(data: any): void {
|
||||
console.log('Level complete event');
|
||||
// Handle level completion
|
||||
}
|
||||
|
||||
public triggerGameEvent(): void {
|
||||
// Send event (synchronous)
|
||||
this.eventSystem.emitSync('custom_event', {
|
||||
message: "This is a custom event",
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
// Send event (asynchronous)
|
||||
this.eventSystem.emit('async_event', {
|
||||
data: "Async event data"
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Scene Responsibility Separation
|
||||
|
||||
```typescript
|
||||
// Good scene design - clear responsibilities
|
||||
class MenuScene extends Scene {
|
||||
// Only handles menu-related logic
|
||||
}
|
||||
|
||||
class GameScene extends Scene {
|
||||
// Only handles gameplay logic
|
||||
}
|
||||
|
||||
class InventoryScene extends Scene {
|
||||
// Only handles inventory logic
|
||||
}
|
||||
|
||||
// Avoid this design - mixed responsibilities
|
||||
class MegaScene extends Scene {
|
||||
// Contains menu, game, inventory, and all other logic
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Proper System Organization
|
||||
|
||||
```typescript
|
||||
class OrganizedScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Add systems by function and dependencies
|
||||
this.addInputSystems();
|
||||
this.addLogicSystems();
|
||||
this.addRenderSystems();
|
||||
}
|
||||
|
||||
private addInputSystems(): void {
|
||||
this.addSystem(new InputSystem());
|
||||
}
|
||||
|
||||
private addLogicSystems(): void {
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new PhysicsSystem());
|
||||
this.addSystem(new CollisionSystem());
|
||||
}
|
||||
|
||||
private addRenderSystems(): void {
|
||||
this.addSystem(new RenderSystem());
|
||||
this.addSystem(new UISystem());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Resource Management
|
||||
|
||||
```typescript
|
||||
class ResourceScene extends Scene {
|
||||
private textures: Map<string, any> = new Map();
|
||||
private sounds: Map<string, any> = new Map();
|
||||
|
||||
protected initialize(): void {
|
||||
this.loadResources();
|
||||
}
|
||||
|
||||
private loadResources(): void {
|
||||
// Load resources needed by the scene
|
||||
this.textures.set('player', this.loadTexture('player.png'));
|
||||
this.sounds.set('bgm', this.loadSound('bgm.mp3'));
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// Cleanup resources
|
||||
this.textures.clear();
|
||||
this.sounds.clear();
|
||||
console.log('Scene resources cleaned up');
|
||||
}
|
||||
|
||||
private loadTexture(path: string): any {
|
||||
// Load texture
|
||||
return null;
|
||||
}
|
||||
|
||||
private loadSound(path: string): any {
|
||||
// Load sound
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Learn about [SceneManager](./scene-manager) - Simple scene management for most games
|
||||
- Learn about [WorldManager](./world-manager) - For scenarios requiring multi-world isolation
|
||||
- Learn about [Persistent Entity](./persistent-entity) - Keep entities across scene transitions (v2.3.0+)
|
||||
|
||||
Scene is the core container of the ECS framework. Proper scene management makes your game architecture clearer, more modular, and easier to maintain.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,570 +0,0 @@
|
||||
# Worker System
|
||||
|
||||
The Worker System (WorkerEntitySystem) is a multi-threaded processing system based on Web Workers in the ECS framework. It's designed for compute-intensive tasks, fully utilizing multi-core CPU performance for true parallel computing.
|
||||
|
||||
## Core Features
|
||||
|
||||
- **True Parallel Computing**: Execute compute-intensive tasks in background threads using Web Workers
|
||||
- **Automatic Load Balancing**: Automatically distribute workload based on CPU core count
|
||||
- **SharedArrayBuffer Optimization**: Zero-copy data sharing for improved large-scale computation performance
|
||||
- **Graceful Degradation**: Automatic fallback to main thread processing when Workers are not supported
|
||||
- **Type Safety**: Full TypeScript support and type checking
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Simple Physics System Example
|
||||
|
||||
```typescript
|
||||
interface PhysicsData {
|
||||
id: number;
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
mass: number;
|
||||
radius: number;
|
||||
}
|
||||
|
||||
@ECSSystem('Physics')
|
||||
class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsData> {
|
||||
constructor() {
|
||||
super(Matcher.all(Position, Velocity, Physics), {
|
||||
enableWorker: true, // Enable Worker parallel processing
|
||||
workerCount: 8, // Worker count, auto-limited to hardware capacity
|
||||
entitiesPerWorker: 100, // Entities per Worker
|
||||
useSharedArrayBuffer: true, // Enable SharedArrayBuffer optimization
|
||||
entityDataSize: 7, // Data size per entity
|
||||
maxEntities: 10000, // Maximum entity count
|
||||
systemConfig: { // Configuration passed to Worker
|
||||
gravity: 100,
|
||||
friction: 0.95
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Data extraction: Convert Entity to serializable data
|
||||
protected extractEntityData(entity: Entity): PhysicsData {
|
||||
const position = entity.getComponent(Position);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
const physics = entity.getComponent(Physics);
|
||||
|
||||
return {
|
||||
id: entity.id,
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
vx: velocity.x,
|
||||
vy: velocity.y,
|
||||
mass: physics.mass,
|
||||
radius: physics.radius
|
||||
};
|
||||
}
|
||||
|
||||
// Worker processing function: Pure function executed in Worker
|
||||
protected workerProcess(
|
||||
entities: PhysicsData[],
|
||||
deltaTime: number,
|
||||
config: any
|
||||
): PhysicsData[] {
|
||||
return entities.map(entity => {
|
||||
// Apply gravity
|
||||
entity.vy += config.gravity * deltaTime;
|
||||
|
||||
// Update position
|
||||
entity.x += entity.vx * deltaTime;
|
||||
entity.y += entity.vy * deltaTime;
|
||||
|
||||
// Apply friction
|
||||
entity.vx *= config.friction;
|
||||
entity.vy *= config.friction;
|
||||
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
// Apply results: Apply Worker processing results back to Entity
|
||||
protected applyResult(entity: Entity, result: PhysicsData): void {
|
||||
const position = entity.getComponent(Position);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
position.x = result.x;
|
||||
position.y = result.y;
|
||||
velocity.x = result.vx;
|
||||
velocity.y = result.vy;
|
||||
}
|
||||
|
||||
// SharedArrayBuffer optimization support
|
||||
protected getDefaultEntityDataSize(): number {
|
||||
return 7; // id, x, y, vx, vy, mass, radius
|
||||
}
|
||||
|
||||
protected writeEntityToBuffer(entityData: PhysicsData, offset: number): void {
|
||||
if (!this.sharedFloatArray) return;
|
||||
|
||||
this.sharedFloatArray[offset + 0] = entityData.id;
|
||||
this.sharedFloatArray[offset + 1] = entityData.x;
|
||||
this.sharedFloatArray[offset + 2] = entityData.y;
|
||||
this.sharedFloatArray[offset + 3] = entityData.vx;
|
||||
this.sharedFloatArray[offset + 4] = entityData.vy;
|
||||
this.sharedFloatArray[offset + 5] = entityData.mass;
|
||||
this.sharedFloatArray[offset + 6] = entityData.radius;
|
||||
}
|
||||
|
||||
protected readEntityFromBuffer(offset: number): PhysicsData | null {
|
||||
if (!this.sharedFloatArray) return null;
|
||||
|
||||
return {
|
||||
id: this.sharedFloatArray[offset + 0],
|
||||
x: this.sharedFloatArray[offset + 1],
|
||||
y: this.sharedFloatArray[offset + 2],
|
||||
vx: this.sharedFloatArray[offset + 3],
|
||||
vy: this.sharedFloatArray[offset + 4],
|
||||
mass: this.sharedFloatArray[offset + 5],
|
||||
radius: this.sharedFloatArray[offset + 6]
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
The Worker system supports rich configuration options:
|
||||
|
||||
```typescript
|
||||
interface WorkerSystemConfig {
|
||||
/** Enable Worker parallel processing */
|
||||
enableWorker?: boolean;
|
||||
/** Worker count, defaults to CPU core count, auto-limited to system maximum */
|
||||
workerCount?: number;
|
||||
/** Entities per Worker for load distribution control */
|
||||
entitiesPerWorker?: number;
|
||||
/** System configuration data passed to Worker */
|
||||
systemConfig?: any;
|
||||
/** Enable SharedArrayBuffer optimization */
|
||||
useSharedArrayBuffer?: boolean;
|
||||
/** Float32 count per entity in SharedArrayBuffer */
|
||||
entityDataSize?: number;
|
||||
/** Maximum entity count (for SharedArrayBuffer pre-allocation) */
|
||||
maxEntities?: number;
|
||||
/** Pre-compiled Worker script path (for platforms like WeChat Mini Game that don't support dynamic scripts) */
|
||||
workerScriptPath?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration Recommendations
|
||||
|
||||
```typescript
|
||||
constructor() {
|
||||
super(matcher, {
|
||||
// Decide based on task complexity
|
||||
enableWorker: this.shouldUseWorker(),
|
||||
|
||||
// Worker count: System auto-limits to hardware capacity
|
||||
workerCount: 8, // Request 8 Workers, actual count limited by CPU cores
|
||||
|
||||
// Entities per Worker (optional)
|
||||
entitiesPerWorker: 200, // Precise load distribution control
|
||||
|
||||
// Enable SharedArrayBuffer for many simple calculations
|
||||
useSharedArrayBuffer: this.entityCount > 1000,
|
||||
|
||||
// Set according to actual data structure
|
||||
entityDataSize: 8, // Ensure it matches data structure
|
||||
|
||||
// Estimated maximum entity count
|
||||
maxEntities: 10000,
|
||||
|
||||
// Global configuration passed to Worker
|
||||
systemConfig: {
|
||||
gravity: 9.8,
|
||||
friction: 0.95,
|
||||
worldBounds: { width: 1920, height: 1080 }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private shouldUseWorker(): boolean {
|
||||
// Decide based on entity count and complexity
|
||||
return this.expectedEntityCount > 100;
|
||||
}
|
||||
|
||||
// Get system info
|
||||
getSystemInfo() {
|
||||
const info = this.getWorkerInfo();
|
||||
console.log(`Worker count: ${info.workerCount}/${info.maxSystemWorkerCount}`);
|
||||
console.log(`Entities per Worker: ${info.entitiesPerWorker || 'auto'}`);
|
||||
console.log(`Current mode: ${info.currentMode}`);
|
||||
}
|
||||
```
|
||||
|
||||
## Processing Modes
|
||||
|
||||
The Worker system supports two processing modes:
|
||||
|
||||
### 1. Traditional Worker Mode
|
||||
|
||||
Data is serialized and passed between main thread and Workers:
|
||||
|
||||
```typescript
|
||||
// Suitable for: Complex computation logic, moderate entity count
|
||||
constructor() {
|
||||
super(matcher, {
|
||||
enableWorker: true,
|
||||
useSharedArrayBuffer: false, // Use traditional mode
|
||||
workerCount: 2
|
||||
});
|
||||
}
|
||||
|
||||
protected workerProcess(entities: EntityData[], deltaTime: number): EntityData[] {
|
||||
// Complex algorithm logic
|
||||
return entities.map(entity => {
|
||||
// AI decisions, pathfinding, etc.
|
||||
return this.complexAILogic(entity, deltaTime);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 2. SharedArrayBuffer Mode
|
||||
|
||||
Zero-copy data sharing, suitable for many simple calculations:
|
||||
|
||||
```typescript
|
||||
// Suitable for: Many entities with simple calculations
|
||||
constructor() {
|
||||
super(matcher, {
|
||||
enableWorker: true,
|
||||
useSharedArrayBuffer: true, // Enable shared memory
|
||||
entityDataSize: 6,
|
||||
maxEntities: 10000
|
||||
});
|
||||
}
|
||||
|
||||
protected getSharedArrayBufferProcessFunction(): SharedArrayBufferProcessFunction {
|
||||
return function(sharedFloatArray: Float32Array, startIndex: number, endIndex: number, deltaTime: number, config: any) {
|
||||
const entitySize = 6;
|
||||
for (let i = startIndex; i < endIndex; i++) {
|
||||
const offset = i * entitySize;
|
||||
|
||||
// Read data
|
||||
let x = sharedFloatArray[offset];
|
||||
let y = sharedFloatArray[offset + 1];
|
||||
let vx = sharedFloatArray[offset + 2];
|
||||
let vy = sharedFloatArray[offset + 3];
|
||||
|
||||
// Physics calculations
|
||||
vy += config.gravity * deltaTime;
|
||||
x += vx * deltaTime;
|
||||
y += vy * deltaTime;
|
||||
|
||||
// Write back data
|
||||
sharedFloatArray[offset] = x;
|
||||
sharedFloatArray[offset + 1] = y;
|
||||
sharedFloatArray[offset + 2] = vx;
|
||||
sharedFloatArray[offset + 3] = vy;
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
The Worker system is particularly suitable for:
|
||||
|
||||
### 1. Physics Simulation
|
||||
- **Gravity systems**: Gravity calculations for many entities
|
||||
- **Collision detection**: Complex collision algorithms
|
||||
- **Fluid simulation**: Particle fluid systems
|
||||
- **Cloth simulation**: Vertex physics calculations
|
||||
|
||||
### 2. AI Computation
|
||||
- **Pathfinding**: A*, Dijkstra algorithms
|
||||
- **Behavior trees**: Complex AI decision logic
|
||||
- **Swarm intelligence**: Boid, fish school algorithms
|
||||
- **Neural networks**: Simple AI inference
|
||||
|
||||
### 3. Data Processing
|
||||
- **Bulk entity updates**: State machines, lifecycle management
|
||||
- **Statistical calculations**: Game data analysis
|
||||
- **Image processing**: Texture generation, effect calculations
|
||||
- **Audio processing**: Sound synthesis, spectrum analysis
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Worker Function Requirements
|
||||
|
||||
```typescript
|
||||
// Recommended: Worker processing function is a pure function
|
||||
protected workerProcess(entities: PhysicsData[], deltaTime: number, config: any): PhysicsData[] {
|
||||
// Only use parameters and standard JavaScript APIs
|
||||
return entities.map(entity => {
|
||||
// Pure computation logic, no external state dependencies
|
||||
entity.y += entity.velocity * deltaTime;
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
// Avoid: Using external references in Worker function
|
||||
protected workerProcess(entities: PhysicsData[], deltaTime: number): PhysicsData[] {
|
||||
// this and external variables are not available in Worker
|
||||
return entities.map(entity => {
|
||||
entity.y += this.someProperty; // Error
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Data Design
|
||||
|
||||
```typescript
|
||||
// Recommended: Reasonable data design
|
||||
interface SimplePhysicsData {
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
// Keep data structure simple for easy serialization
|
||||
}
|
||||
|
||||
// Avoid: Complex nested objects
|
||||
interface ComplexData {
|
||||
transform: {
|
||||
position: { x: number; y: number };
|
||||
rotation: { angle: number };
|
||||
};
|
||||
// Complex nested structures increase serialization overhead
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Worker Count Control
|
||||
|
||||
```typescript
|
||||
// Recommended: Flexible Worker configuration
|
||||
constructor() {
|
||||
super(matcher, {
|
||||
// Specify needed Worker count, system auto-limits to hardware capacity
|
||||
workerCount: 8, // Request 8 Workers
|
||||
entitiesPerWorker: 100, // 100 entities per Worker
|
||||
enableWorker: this.shouldUseWorker(), // Conditional enable
|
||||
});
|
||||
}
|
||||
|
||||
private shouldUseWorker(): boolean {
|
||||
// Decide based on entity count and complexity
|
||||
return this.expectedEntityCount > 100;
|
||||
}
|
||||
|
||||
// Get actual Worker info
|
||||
checkWorkerConfiguration() {
|
||||
const info = this.getWorkerInfo();
|
||||
console.log(`Requested Workers: 8`);
|
||||
console.log(`Actual Workers: ${info.workerCount}`);
|
||||
console.log(`System maximum: ${info.maxSystemWorkerCount}`);
|
||||
console.log(`Entities per Worker: ${info.entitiesPerWorker || 'auto'}`);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Performance Monitoring
|
||||
|
||||
```typescript
|
||||
// Recommended: Performance monitoring
|
||||
public getPerformanceMetrics(): WorkerPerformanceMetrics {
|
||||
return {
|
||||
...this.getWorkerInfo(),
|
||||
entityCount: this.entities.length,
|
||||
averageProcessTime: this.getAverageProcessTime(),
|
||||
workerUtilization: this.getWorkerUtilization()
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Optimization Tips
|
||||
|
||||
### 1. Compute Intensity Assessment
|
||||
Only use Workers for compute-intensive tasks to avoid thread overhead for simple calculations.
|
||||
|
||||
### 2. Data Transfer Optimization
|
||||
- Use SharedArrayBuffer to reduce serialization overhead
|
||||
- Keep data structures simple and flat
|
||||
- Avoid frequent large data transfers
|
||||
|
||||
### 3. Degradation Strategy
|
||||
Always provide main thread fallback to ensure normal operation in environments without Worker support.
|
||||
|
||||
### 4. Memory Management
|
||||
Clean up Worker pools and shared buffers promptly to avoid memory leaks.
|
||||
|
||||
### 5. Load Balancing
|
||||
Use `entitiesPerWorker` parameter to precisely control load distribution, avoiding idle Workers while others are overloaded.
|
||||
|
||||
## WeChat Mini Game Support
|
||||
|
||||
WeChat Mini Game has special Worker limitations and doesn't support dynamic Worker script creation. ESEngine provides the `@esengine/worker-generator` CLI tool to solve this problem.
|
||||
|
||||
### WeChat Mini Game Worker Limitations
|
||||
|
||||
| Feature | Browser | WeChat Mini Game |
|
||||
|---------|---------|------------------|
|
||||
| Dynamic scripts (Blob URL) | Supported | Not supported |
|
||||
| Worker count | Multiple | Maximum 1 |
|
||||
| Script source | Any | Must be in code package |
|
||||
| SharedArrayBuffer | Requires COOP/COEP | Limited support |
|
||||
|
||||
### Using Worker Generator CLI
|
||||
|
||||
#### 1. Install the Tool
|
||||
|
||||
```bash
|
||||
pnpm add -D @esengine/worker-generator
|
||||
```
|
||||
|
||||
#### 2. Configure workerScriptPath
|
||||
|
||||
Configure `workerScriptPath` in your WorkerEntitySystem subclass:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Physics')
|
||||
class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsData> {
|
||||
constructor() {
|
||||
super(Matcher.all(Position, Velocity, Physics), {
|
||||
enableWorker: true,
|
||||
workerScriptPath: 'workers/physics-worker.js', // Specify Worker file path
|
||||
systemConfig: {
|
||||
gravity: 100,
|
||||
friction: 0.95
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected workerProcess(
|
||||
entities: PhysicsData[],
|
||||
deltaTime: number,
|
||||
config: any
|
||||
): PhysicsData[] {
|
||||
// Physics calculation logic
|
||||
return entities.map(entity => {
|
||||
entity.vy += config.gravity * deltaTime;
|
||||
entity.x += entity.vx * deltaTime;
|
||||
entity.y += entity.vy * deltaTime;
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
// ... other methods
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Generate Worker Files
|
||||
|
||||
Run the CLI tool to automatically extract `workerProcess` functions and generate WeChat Mini Game compatible Worker files:
|
||||
|
||||
```bash
|
||||
# Basic usage
|
||||
npx esengine-worker-gen --src ./src --wechat
|
||||
|
||||
# Full options
|
||||
npx esengine-worker-gen \
|
||||
--src ./src \ # Source directory
|
||||
--wechat \ # Generate WeChat Mini Game compatible code
|
||||
--mapping \ # Generate worker-mapping.json
|
||||
--verbose # Verbose output
|
||||
```
|
||||
|
||||
The CLI tool will:
|
||||
1. Scan source directory for all `WorkerEntitySystem` subclasses
|
||||
2. Read each class's `workerScriptPath` configuration
|
||||
3. Extract `workerProcess` method body
|
||||
4. Convert to ES5 syntax (WeChat Mini Game compatible)
|
||||
5. Generate to configured path
|
||||
|
||||
#### 4. Configure game.json
|
||||
|
||||
Configure workers directory in WeChat Mini Game's `game.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"deviceOrientation": "portrait",
|
||||
"workers": "workers"
|
||||
}
|
||||
```
|
||||
|
||||
#### 5. Project Structure
|
||||
|
||||
```
|
||||
your-game/
|
||||
├── game.js
|
||||
├── game.json # Configure "workers": "workers"
|
||||
├── src/
|
||||
│ └── systems/
|
||||
│ └── PhysicsSystem.ts # workerScriptPath: 'workers/physics-worker.js'
|
||||
└── workers/
|
||||
├── physics-worker.js # Auto-generated
|
||||
└── worker-mapping.json # Auto-generated
|
||||
```
|
||||
|
||||
### Temporarily Disabling Workers
|
||||
|
||||
If you need to temporarily disable Workers (e.g., for debugging), there are two ways:
|
||||
|
||||
#### Method 1: Configuration Disable
|
||||
|
||||
```typescript
|
||||
constructor() {
|
||||
super(matcher, {
|
||||
enableWorker: false, // Disable Worker, use main thread processing
|
||||
// ...
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
#### Method 2: Platform Adapter Disable
|
||||
|
||||
Return Worker not supported in custom platform adapter:
|
||||
|
||||
```typescript
|
||||
class MyPlatformAdapter implements IPlatformAdapter {
|
||||
isWorkerSupported(): boolean {
|
||||
return false; // Return false to disable Worker
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Important Notes
|
||||
|
||||
1. **Re-run CLI tool after each `workerProcess` modification** to generate new Worker files
|
||||
|
||||
2. **Worker functions must be pure functions**, cannot depend on `this` or external variables:
|
||||
```typescript
|
||||
// Correct: Only use parameters
|
||||
protected workerProcess(entities, deltaTime, config) {
|
||||
return entities.map(e => {
|
||||
e.y += config.gravity * deltaTime;
|
||||
return e;
|
||||
});
|
||||
}
|
||||
|
||||
// Wrong: Using this
|
||||
protected workerProcess(entities, deltaTime, config) {
|
||||
return entities.map(e => {
|
||||
e.y += this.gravity * deltaTime; // Cannot access this in Worker
|
||||
return e;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
3. **Pass configuration data via `systemConfig`**, not class properties
|
||||
|
||||
4. **Developer tool warnings can be ignored**:
|
||||
- `getNetworkType:fail not support` - WeChat DevTools internal behavior
|
||||
- `SharedArrayBuffer will require cross-origin isolation` - Development environment warning, won't appear on real devices
|
||||
|
||||
## Online Demo
|
||||
|
||||
See the complete Worker system demo: [Worker System Demo](https://esengine.github.io/ecs-framework/demos/worker-system/)
|
||||
|
||||
The demo showcases:
|
||||
- Multi-threaded physics computation
|
||||
- Real-time performance comparison
|
||||
- SharedArrayBuffer optimization
|
||||
- Parallel processing of many entities
|
||||
|
||||
The Worker system provides powerful parallel computing capabilities for the ECS framework, allowing you to fully utilize modern multi-core processor performance, offering efficient solutions for complex game logic and compute-intensive tasks.
|
||||
317
docs/en/index.md
317
docs/en/index.md
@@ -1,317 +0,0 @@
|
||||
---
|
||||
layout: page
|
||||
title: ESEngine - High-performance TypeScript ECS Framework
|
||||
---
|
||||
|
||||
<ParticleHeroEn />
|
||||
|
||||
<section class="news-section">
|
||||
<div class="news-container">
|
||||
<div class="news-header">
|
||||
<h2 class="news-title">Quick Links</h2>
|
||||
<a href="/en/guide/" class="news-more">View Docs</a>
|
||||
</div>
|
||||
<div class="news-grid">
|
||||
<a href="/en/guide/getting-started" class="news-card">
|
||||
<div class="news-card-image" style="background: linear-gradient(135deg, #1e3a5f 0%, #1e1e1e 100%);">
|
||||
<div class="news-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24"><path fill="#4fc1ff" d="M12 3L1 9l4 2.18v6L12 21l7-3.82v-6l2-1.09V17h2V9zm6.82 6L12 12.72L5.18 9L12 5.28zM17 16l-5 2.72L7 16v-3.73L12 15l5-2.73z"/></svg>
|
||||
</div>
|
||||
<span class="news-badge">Quick Start</span>
|
||||
</div>
|
||||
<div class="news-card-content">
|
||||
<h3>Get Started in 5 Minutes</h3>
|
||||
<p>From installation to your first ECS app, learn the core concepts quickly.</p>
|
||||
</div>
|
||||
</a>
|
||||
<a href="/en/guide/behavior-tree/" class="news-card">
|
||||
<div class="news-card-image" style="background: linear-gradient(135deg, #1e3a5f 0%, #1e1e1e 100%);">
|
||||
<div class="news-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24"><path fill="#4ec9b0" d="M12 2a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m3 20h-1v-7l-2-2l-2 2v7H9v-7.5l-2 2V22H6v-6l3-3l1-3.5c-.3.4-.6.7-1 1L6 9v1H4V8l5-3c.5-.3 1.1-.5 1.7-.5H11c.6 0 1.2.2 1.7.5l5 3v2h-2V9l-3 1.5c-.4-.3-.7-.6-1-1l1 3.5l3 3v6Z"/></svg>
|
||||
</div>
|
||||
<span class="news-badge">AI System</span>
|
||||
</div>
|
||||
<div class="news-card-content">
|
||||
<h3>Visual Behavior Tree Editor</h3>
|
||||
<p>Built-in AI behavior tree system with visual editing and real-time debugging.</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="features-section">
|
||||
<div class="features-container">
|
||||
<h2 class="features-title">Core Features</h2>
|
||||
<div class="features-grid">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#4fc1ff" d="M13 2.05v2.02c3.95.49 7 3.85 7 7.93c0 1.45-.39 2.79-1.06 3.95l1.59 1.09A9.94 9.94 0 0 0 22 12c0-5.18-3.95-9.45-9-9.95M12 19c-3.87 0-7-3.13-7-7c0-3.53 2.61-6.43 6-6.92V2.05c-5.06.5-9 4.76-9 9.95c0 5.52 4.47 10 9.99 10c3.31 0 6.24-1.61 8.06-4.09l-1.6-1.1A7.93 7.93 0 0 1 12 19"/><path fill="#4fc1ff" d="M12 6a6 6 0 0 0-6 6c0 3.31 2.69 6 6 6a6 6 0 0 0 0-12m0 10c-2.21 0-4-1.79-4-4s1.79-4 4-4s4 1.79 4 4s-1.79 4-4 4"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">High-performance ECS Architecture</h3>
|
||||
<p class="feature-desc">Data-driven entity component system for large-scale entity processing with cache-friendly memory layout.</p>
|
||||
<a href="/en/guide/entity" class="feature-link">Learn more</a>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#569cd6" d="M3 3h18v18H3zm16.525 13.707c0-.795-.272-1.425-.816-1.89c-.544-.465-1.404-.804-2.58-1.016l-1.704-.296c-.616-.104-1.052-.26-1.308-.468c-.256-.21-.384-.468-.384-.776c0-.392.168-.7.504-.924c.336-.224.8-.336 1.392-.336c.56 0 1.008.124 1.344.372c.336.248.536.584.6 1.008h2.016c-.08-.96-.464-1.716-1.152-2.268c-.688-.552-1.6-.828-2.736-.828c-1.2 0-2.148.3-2.844.9c-.696.6-1.044 1.38-1.044 2.34c0 .76.252 1.368.756 1.824c.504.456 1.308.792 2.412.996l1.704.312c.624.12 1.068.28 1.332.48c.264.2.396.46.396.78c0 .424-.192.756-.576.996c-.384.24-.9.36-1.548.36c-.672 0-1.2-.14-1.584-.42c-.384-.28-.608-.668-.672-1.164H8.868c.048 1.016.46 1.808 1.236 2.376c.776.568 1.796.852 3.06.852c1.24 0 2.22-.292 2.94-.876c.72-.584 1.08-1.364 1.08-2.34z"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">Full Type Support</h3>
|
||||
<p class="feature-desc">100% TypeScript with complete type definitions and compile-time checking for the best development experience.</p>
|
||||
<a href="/en/guide/component" class="feature-link">Learn more</a>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#4ec9b0" d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10s10-4.5 10-10S17.5 2 12 2m0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8s8 3.59 8 8s-3.59 8-8 8m-5-8l4-4v3h4v2h-4v3z"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">Visual Behavior Tree</h3>
|
||||
<p class="feature-desc">Built-in AI behavior tree system with visual editor, custom nodes, and real-time debugging.</p>
|
||||
<a href="/en/guide/behavior-tree/" class="feature-link">Learn more</a>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#c586c0" d="M4 6h18V4H4c-1.1 0-2 .9-2 2v11H0v3h14v-3H4zm19 2h-6c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h6c.55 0 1-.45 1-1V9c0-.55-.45-1-1-1m-1 9h-4v-7h4z"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">Multi-Platform Support</h3>
|
||||
<p class="feature-desc">Support for browsers, Node.js, WeChat Mini Games, and seamless integration with major game engines.</p>
|
||||
<a href="/en/guide/platform-adapter" class="feature-link">Learn more</a>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#dcdcaa" d="M4 3h6v2H4v14h6v2H4c-1.1 0-2-.9-2-2V5c0-1.1.9-2 2-2m9 0h6c1.1 0 2 .9 2 2v14c0 1.1-.9 2-2 2h-6v-2h6V5h-6zm-1 7h4v2h-4z"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">Modular Design</h3>
|
||||
<p class="feature-desc">Core features packaged independently, import only what you need. Support for custom plugin extensions.</p>
|
||||
<a href="/en/guide/plugin-system" class="feature-link">Learn more</a>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#9cdcfe" d="M22.7 19l-9.1-9.1c.9-2.3.4-5-1.5-6.9c-2-2-5-2.4-7.4-1.3L9 6L6 9L1.6 4.7C.4 7.1.9 10.1 2.9 12.1c1.9 1.9 4.6 2.4 6.9 1.5l9.1 9.1c.4.4 1 .4 1.4 0l2.3-2.3c.5-.4.5-1.1.1-1.4"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">Developer Tools</h3>
|
||||
<p class="feature-desc">Built-in performance monitoring, debugging tools, serialization system, and complete development toolchain.</p>
|
||||
<a href="/en/guide/logging" class="feature-link">Learn more</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style scoped>
|
||||
/* Home page specific styles */
|
||||
.news-section {
|
||||
background: #0d0d0d;
|
||||
padding: 64px 0;
|
||||
border-top: 1px solid #2a2a2a;
|
||||
}
|
||||
|
||||
.news-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 48px;
|
||||
}
|
||||
|
||||
.news-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.news-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.news-more {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 20px;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 6px;
|
||||
color: #a0a0a0;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.news-more:hover {
|
||||
background: #252525;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.news-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.news-card {
|
||||
display: flex;
|
||||
background: #1f1f1f;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.news-card:hover {
|
||||
border-color: #3b9eff;
|
||||
}
|
||||
|
||||
.news-card-image {
|
||||
width: 200px;
|
||||
min-height: 140px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.news-icon {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.news-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
background: transparent;
|
||||
border: 1px solid #3a3a3a;
|
||||
border-radius: 16px;
|
||||
color: #a0a0a0;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.news-card-content {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.news-card-content h3 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.news-card-content p {
|
||||
font-size: 0.875rem;
|
||||
color: #707070;
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.features-section {
|
||||
background: #0d0d0d;
|
||||
padding: 64px 0;
|
||||
}
|
||||
|
||||
.features-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 48px;
|
||||
}
|
||||
|
||||
.features-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
margin: 0 0 32px 0;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: #1f1f1f;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
border-color: #3b9eff;
|
||||
background: #252525;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: #0d0d0d;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.feature-desc {
|
||||
font-size: 14px;
|
||||
color: #707070;
|
||||
line-height: 1.7;
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
|
||||
.feature-link {
|
||||
font-size: 14px;
|
||||
color: #3b9eff;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.feature-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.news-container,
|
||||
.features-container {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.news-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.news-card {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.news-card-image {
|
||||
width: 100%;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,404 +0,0 @@
|
||||
# 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
|
||||
@@ -1,727 +0,0 @@
|
||||
# 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
|
||||
}
|
||||
});
|
||||
```
|
||||
@@ -1,322 +0,0 @@
|
||||
# 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));
|
||||
```
|
||||
@@ -1,352 +0,0 @@
|
||||
# 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);
|
||||
```
|
||||
@@ -1,725 +0,0 @@
|
||||
# 组件系统
|
||||
|
||||
在 ECS 架构中,组件(Component)是数据和行为的载体。组件定义了实体具有的属性和功能,是 ECS 架构的核心构建块。
|
||||
|
||||
## 基本概念
|
||||
|
||||
组件是继承自 `Component` 抽象基类的具体类,用于:
|
||||
- 存储实体的数据(如位置、速度、健康值等)
|
||||
- 定义与数据相关的行为方法
|
||||
- 提供生命周期回调钩子
|
||||
- 支持序列化和调试
|
||||
|
||||
## 创建组件
|
||||
|
||||
### 基础组件定义
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
x: number = 0;
|
||||
y: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('Health')
|
||||
class Health extends Component {
|
||||
current: number;
|
||||
max: number;
|
||||
|
||||
constructor(max: number = 100) {
|
||||
super();
|
||||
this.max = max;
|
||||
this.current = max;
|
||||
}
|
||||
|
||||
// 组件可以包含行为方法
|
||||
takeDamage(damage: number): void {
|
||||
this.current = Math.max(0, this.current - damage);
|
||||
}
|
||||
|
||||
heal(amount: number): void {
|
||||
this.current = Math.min(this.max, this.current + amount);
|
||||
}
|
||||
|
||||
isDead(): boolean {
|
||||
return this.current <= 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### @ECSComponent 装饰器
|
||||
|
||||
`@ECSComponent` 是组件类必须使用的装饰器,它为组件提供了类型标识和元数据管理。
|
||||
|
||||
#### 为什么必须使用
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| **类型识别** | 提供稳定的类型名称,代码混淆后仍能正确识别 |
|
||||
| **序列化支持** | 序列化/反序列化时使用该名称作为类型标识 |
|
||||
| **组件注册** | 自动注册到 ComponentRegistry,分配唯一的位掩码 |
|
||||
| **调试支持** | 在调试工具和日志中显示可读的组件名称 |
|
||||
|
||||
#### 基本语法
|
||||
|
||||
```typescript
|
||||
@ECSComponent(typeName: string)
|
||||
```
|
||||
|
||||
- `typeName`: 组件的类型名称,建议使用与类名相同或相近的名称
|
||||
|
||||
#### 使用示例
|
||||
|
||||
```typescript
|
||||
// ✅ 正确的用法
|
||||
@ECSComponent('Velocity')
|
||||
class Velocity extends Component {
|
||||
dx: number = 0;
|
||||
dy: number = 0;
|
||||
}
|
||||
|
||||
// ✅ 推荐:类型名与类名保持一致
|
||||
@ECSComponent('PlayerController')
|
||||
class PlayerController extends Component {
|
||||
speed: number = 5;
|
||||
}
|
||||
|
||||
// ❌ 错误的用法 - 没有装饰器
|
||||
class BadComponent extends Component {
|
||||
// 这样定义的组件可能在生产环境出现问题:
|
||||
// 1. 代码压缩后类名变化,无法正确序列化
|
||||
// 2. 组件未注册到框架,查询和匹配可能失效
|
||||
}
|
||||
```
|
||||
|
||||
#### 与 @Serializable 配合使用
|
||||
|
||||
当组件需要支持序列化时,`@ECSComponent` 和 `@Serializable` 需要一起使用:
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent, Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSComponent('Player')
|
||||
@Serializable({ version: 1 })
|
||||
class PlayerComponent extends Component {
|
||||
@Serialize()
|
||||
name: string = '';
|
||||
|
||||
@Serialize()
|
||||
level: number = 1;
|
||||
|
||||
// 不使用 @Serialize() 的字段不会被序列化
|
||||
private _cachedData: any = null;
|
||||
}
|
||||
```
|
||||
|
||||
> **注意**:`@ECSComponent` 的 `typeName` 和 `@Serializable` 的 `typeId` 可以不同。如果 `@Serializable` 没有指定 `typeId`,则默认使用 `@ECSComponent` 的 `typeName`。
|
||||
|
||||
#### 组件类型名的唯一性
|
||||
|
||||
每个组件的类型名应该是唯一的:
|
||||
|
||||
```typescript
|
||||
// ❌ 错误:两个组件使用相同的类型名
|
||||
@ECSComponent('Health')
|
||||
class HealthComponent extends Component { }
|
||||
|
||||
@ECSComponent('Health') // 冲突!
|
||||
class EnemyHealthComponent extends Component { }
|
||||
|
||||
// ✅ 正确:使用不同的类型名
|
||||
@ECSComponent('PlayerHealth')
|
||||
class PlayerHealthComponent extends Component { }
|
||||
|
||||
@ECSComponent('EnemyHealth')
|
||||
class EnemyHealthComponent extends Component { }
|
||||
```
|
||||
|
||||
## 组件生命周期
|
||||
|
||||
组件提供了生命周期钩子,可以重写来执行特定的逻辑:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('ExampleComponent')
|
||||
class ExampleComponent extends Component {
|
||||
private resource: SomeResource | null = null;
|
||||
|
||||
/**
|
||||
* 组件被添加到实体时调用
|
||||
* 用于初始化资源、建立引用等
|
||||
*/
|
||||
onAddedToEntity(): void {
|
||||
console.log(`组件 ${this.constructor.name} 已添加,实体ID: ${this.entityId}`);
|
||||
this.resource = new SomeResource();
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件从实体移除时调用
|
||||
* 用于清理资源、断开引用等
|
||||
*/
|
||||
onRemovedFromEntity(): void {
|
||||
console.log(`组件 ${this.constructor.name} 已移除`);
|
||||
if (this.resource) {
|
||||
this.resource.cleanup();
|
||||
this.resource = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 组件与实体的关系
|
||||
|
||||
组件存储了所属实体的ID (`entityId`),而不是直接引用实体对象。这是ECS数据导向设计的体现,避免了循环引用。
|
||||
|
||||
在实际使用中,**应该在 System 中处理实体和组件的交互**,而不是在组件内部:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Health')
|
||||
class Health extends Component {
|
||||
current: number;
|
||||
max: number;
|
||||
|
||||
constructor(max: number = 100) {
|
||||
super();
|
||||
this.max = max;
|
||||
this.current = max;
|
||||
}
|
||||
|
||||
isDead(): boolean {
|
||||
return this.current <= 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('Damage')
|
||||
class Damage extends Component {
|
||||
value: number;
|
||||
|
||||
constructor(value: number) {
|
||||
super();
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
// 推荐:在 System 中处理逻辑
|
||||
class DamageSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(new Matcher().all(Health, Damage));
|
||||
}
|
||||
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health)!;
|
||||
const damage = entity.getComponent(Damage)!;
|
||||
|
||||
health.current -= damage.value;
|
||||
|
||||
if (health.isDead()) {
|
||||
entity.destroy();
|
||||
}
|
||||
|
||||
// 应用伤害后移除 Damage 组件
|
||||
entity.removeComponent(damage);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 组件属性
|
||||
|
||||
每个组件都有一些内置属性:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('ExampleComponent')
|
||||
class ExampleComponent extends Component {
|
||||
someData: string = "example";
|
||||
|
||||
onAddedToEntity(): void {
|
||||
console.log(`组件ID: ${this.id}`); // 唯一的组件ID
|
||||
console.log(`所属实体ID: ${this.entityId}`); // 所属实体的ID
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
如果需要访问实体对象,应该在 System 中进行:
|
||||
|
||||
```typescript
|
||||
class ExampleSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(new Matcher().all(ExampleComponent));
|
||||
}
|
||||
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const comp = entity.getComponent(ExampleComponent)!;
|
||||
console.log(`实体名称: ${entity.name}`);
|
||||
console.log(`组件数据: ${comp.someData}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 复杂组件示例
|
||||
|
||||
### 状态机组件
|
||||
|
||||
```typescript
|
||||
enum EntityState {
|
||||
Idle,
|
||||
Moving,
|
||||
Attacking,
|
||||
Dead
|
||||
}
|
||||
|
||||
@ECSComponent('StateMachine')
|
||||
class StateMachine extends Component {
|
||||
private _currentState: EntityState = EntityState.Idle;
|
||||
private _previousState: EntityState = EntityState.Idle;
|
||||
private _stateTimer: number = 0;
|
||||
|
||||
get currentState(): EntityState {
|
||||
return this._currentState;
|
||||
}
|
||||
|
||||
get previousState(): EntityState {
|
||||
return this._previousState;
|
||||
}
|
||||
|
||||
get stateTimer(): number {
|
||||
return this._stateTimer;
|
||||
}
|
||||
|
||||
changeState(newState: EntityState): void {
|
||||
if (this._currentState !== newState) {
|
||||
this._previousState = this._currentState;
|
||||
this._currentState = newState;
|
||||
this._stateTimer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
updateTimer(deltaTime: number): void {
|
||||
this._stateTimer += deltaTime;
|
||||
}
|
||||
|
||||
isInState(state: EntityState): boolean {
|
||||
return this._currentState === state;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 配置数据组件
|
||||
|
||||
```typescript
|
||||
interface WeaponData {
|
||||
damage: number;
|
||||
range: number;
|
||||
fireRate: number;
|
||||
ammo: number;
|
||||
}
|
||||
|
||||
@ECSComponent('WeaponConfig')
|
||||
class WeaponConfig extends Component {
|
||||
data: WeaponData;
|
||||
|
||||
constructor(weaponData: WeaponData) {
|
||||
super();
|
||||
this.data = { ...weaponData }; // 深拷贝避免共享引用
|
||||
}
|
||||
|
||||
// 提供便捷的访问方法
|
||||
getDamage(): number {
|
||||
return this.data.damage;
|
||||
}
|
||||
|
||||
canFire(): boolean {
|
||||
return this.data.ammo > 0;
|
||||
}
|
||||
|
||||
consumeAmmo(): boolean {
|
||||
if (this.data.ammo > 0) {
|
||||
this.data.ammo--;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 保持组件简单
|
||||
|
||||
```typescript
|
||||
// 好的组件设计 - 单一职责
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
x: number = 0;
|
||||
y: number = 0;
|
||||
}
|
||||
|
||||
@ECSComponent('Velocity')
|
||||
class Velocity extends Component {
|
||||
dx: number = 0;
|
||||
dy: number = 0;
|
||||
}
|
||||
|
||||
// 避免的组件设计 - 职责过多
|
||||
@ECSComponent('GameObject')
|
||||
class GameObject extends Component {
|
||||
x: number;
|
||||
y: number;
|
||||
dx: number;
|
||||
dy: number;
|
||||
health: number;
|
||||
damage: number;
|
||||
sprite: string;
|
||||
// 太多不相关的属性
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 使用构造函数初始化
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Transform')
|
||||
class Transform extends Component {
|
||||
x: number;
|
||||
y: number;
|
||||
rotation: number;
|
||||
scale: number;
|
||||
|
||||
constructor(x = 0, y = 0, rotation = 0, scale = 1) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.rotation = rotation;
|
||||
this.scale = scale;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 明确的类型定义
|
||||
|
||||
```typescript
|
||||
interface InventoryItem {
|
||||
id: string;
|
||||
name: string;
|
||||
quantity: number;
|
||||
type: 'weapon' | 'consumable' | 'misc';
|
||||
}
|
||||
|
||||
@ECSComponent('Inventory')
|
||||
class Inventory extends Component {
|
||||
items: InventoryItem[] = [];
|
||||
maxSlots: number;
|
||||
|
||||
constructor(maxSlots: number = 20) {
|
||||
super();
|
||||
this.maxSlots = maxSlots;
|
||||
}
|
||||
|
||||
addItem(item: InventoryItem): boolean {
|
||||
if (this.items.length < this.maxSlots) {
|
||||
this.items.push(item);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
removeItem(itemId: string): InventoryItem | null {
|
||||
const index = this.items.findIndex(item => item.id === itemId);
|
||||
if (index !== -1) {
|
||||
return this.items.splice(index, 1)[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 引用其他实体
|
||||
|
||||
当组件需要关联其他实体时(如父子关系、跟随目标等),**推荐方式是存储实体ID**,然后在 System 中查找:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Follower')
|
||||
class Follower extends Component {
|
||||
targetId: number;
|
||||
followDistance: number = 50;
|
||||
|
||||
constructor(targetId: number) {
|
||||
super();
|
||||
this.targetId = targetId;
|
||||
}
|
||||
}
|
||||
|
||||
// 在 System 中查找目标实体并处理逻辑
|
||||
class FollowerSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(new Matcher().all(Follower, Position));
|
||||
}
|
||||
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const follower = entity.getComponent(Follower)!;
|
||||
const position = entity.getComponent(Position)!;
|
||||
|
||||
// 通过场景查找目标实体
|
||||
const target = entity.scene?.findEntityById(follower.targetId);
|
||||
if (target) {
|
||||
const targetPos = target.getComponent(Position);
|
||||
if (targetPos) {
|
||||
// 跟随逻辑
|
||||
const dx = targetPos.x - position.x;
|
||||
const dy = targetPos.y - position.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance > follower.followDistance) {
|
||||
// 移动靠近目标
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这种方式的优势:
|
||||
- 组件保持简单,只存储基本数据类型
|
||||
- 符合数据导向设计
|
||||
- 在 System 中统一处理查找和逻辑
|
||||
- 易于理解和维护
|
||||
|
||||
**避免在组件中直接存储实体引用**:
|
||||
|
||||
```typescript
|
||||
// 错误示范:直接存储实体引用
|
||||
@ECSComponent('BadFollower')
|
||||
class BadFollower extends Component {
|
||||
target: Entity; // 实体销毁后仍持有引用,可能导致内存泄漏
|
||||
}
|
||||
```
|
||||
|
||||
## 高级特性
|
||||
|
||||
### EntityRef 装饰器 - 自动引用追踪
|
||||
|
||||
框架提供了 `@EntityRef` 装饰器用于**特殊场景**下安全地存储实体引用。这是一个高级特性,一般情况下推荐使用存储ID的方式。
|
||||
|
||||
#### 什么时候需要 EntityRef?
|
||||
|
||||
在以下场景中,`@EntityRef` 可以简化代码:
|
||||
|
||||
1. **父子关系**: 需要在组件中直接访问父实体或子实体
|
||||
2. **复杂关联**: 实体之间有多个引用关系
|
||||
3. **频繁访问**: 需要在多处访问引用的实体,使用ID查找会有性能开销
|
||||
|
||||
#### 核心特性
|
||||
|
||||
`@EntityRef` 装饰器通过 **ReferenceTracker** 自动追踪引用关系:
|
||||
|
||||
- 当被引用的实体销毁时,所有指向它的 `@EntityRef` 属性自动设为 `null`
|
||||
- 防止跨场景引用(会输出警告并拒绝设置)
|
||||
- 防止引用已销毁的实体(会输出警告并设为 `null`)
|
||||
- 使用 WeakRef 避免内存泄漏(自动GC支持)
|
||||
- 组件移除时自动清理引用注册
|
||||
|
||||
#### 基本用法
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent, EntityRef, Entity } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSComponent('Parent')
|
||||
class ParentComponent extends Component {
|
||||
@EntityRef()
|
||||
parent: Entity | null = null;
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
const scene = new Scene();
|
||||
const parent = scene.createEntity('Parent');
|
||||
const child = scene.createEntity('Child');
|
||||
|
||||
const comp = child.addComponent(new ParentComponent());
|
||||
comp.parent = parent;
|
||||
|
||||
console.log(comp.parent); // Entity { name: 'Parent' }
|
||||
|
||||
// 当 parent 被销毁时,comp.parent 自动变为 null
|
||||
parent.destroy();
|
||||
console.log(comp.parent); // null
|
||||
```
|
||||
|
||||
#### 多个引用属性
|
||||
|
||||
一个组件可以有多个 `@EntityRef` 属性:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Combat')
|
||||
class CombatComponent extends Component {
|
||||
@EntityRef()
|
||||
target: Entity | null = null;
|
||||
|
||||
@EntityRef()
|
||||
ally: Entity | null = null;
|
||||
|
||||
@EntityRef()
|
||||
lastAttacker: Entity | null = null;
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
const player = scene.createEntity('Player');
|
||||
const enemy = scene.createEntity('Enemy');
|
||||
const npc = scene.createEntity('NPC');
|
||||
|
||||
const combat = player.addComponent(new CombatComponent());
|
||||
combat.target = enemy;
|
||||
combat.ally = npc;
|
||||
|
||||
// enemy 销毁后,只有 target 变为 null,ally 仍然有效
|
||||
enemy.destroy();
|
||||
console.log(combat.target); // null
|
||||
console.log(combat.ally); // Entity { name: 'NPC' }
|
||||
```
|
||||
|
||||
#### 安全检查
|
||||
|
||||
`@EntityRef` 提供了多重安全检查:
|
||||
|
||||
```typescript
|
||||
const scene1 = new Scene();
|
||||
const scene2 = new Scene();
|
||||
|
||||
const entity1 = scene1.createEntity('Entity1');
|
||||
const entity2 = scene2.createEntity('Entity2');
|
||||
|
||||
const comp = entity1.addComponent(new ParentComponent());
|
||||
|
||||
// 跨场景引用会失败
|
||||
comp.parent = entity2; // 输出错误日志,comp.parent 为 null
|
||||
console.log(comp.parent); // null
|
||||
|
||||
// 引用已销毁的实体会失败
|
||||
const entity3 = scene1.createEntity('Entity3');
|
||||
entity3.destroy();
|
||||
comp.parent = entity3; // 输出警告日志,comp.parent 为 null
|
||||
console.log(comp.parent); // null
|
||||
```
|
||||
|
||||
#### 实现原理
|
||||
|
||||
`@EntityRef` 使用以下机制实现自动引用追踪:
|
||||
|
||||
1. **ReferenceTracker**: Scene 持有一个引用追踪器,记录所有实体引用关系
|
||||
2. **WeakRef**: 使用弱引用存储组件,避免循环引用导致内存泄漏
|
||||
3. **属性拦截**: 通过 `Object.defineProperty` 拦截 getter/setter
|
||||
4. **自动清理**: 实体销毁时,ReferenceTracker 遍历所有引用并设为 null
|
||||
|
||||
```typescript
|
||||
// 简化的实现原理
|
||||
class ReferenceTracker {
|
||||
// entityId -> 引用该实体的所有组件记录
|
||||
private _references: Map<number, Set<{ component: WeakRef<Component>, propertyKey: string }>>;
|
||||
|
||||
// 实体销毁时调用
|
||||
clearReferencesTo(entityId: number): void {
|
||||
const records = this._references.get(entityId);
|
||||
if (records) {
|
||||
for (const record of records) {
|
||||
const component = record.component.deref();
|
||||
if (component) {
|
||||
// 将组件的引用属性设为 null
|
||||
(component as any)[record.propertyKey] = null;
|
||||
}
|
||||
}
|
||||
this._references.delete(entityId);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 性能考虑
|
||||
|
||||
`@EntityRef` 会带来一些性能开销:
|
||||
|
||||
- **写入开销**: 每次设置引用时需要更新 ReferenceTracker
|
||||
- **内存开销**: ReferenceTracker 需要维护引用映射表
|
||||
- **销毁开销**: 实体销毁时需要遍历所有引用并清理
|
||||
|
||||
对于大多数场景,这些开销是可以接受的。但如果有**大量实体和频繁的引用变更**,存储ID可能更高效。
|
||||
|
||||
#### 最佳实践
|
||||
|
||||
```typescript
|
||||
// 推荐:适合使用 @EntityRef 的场景 - 父子关系
|
||||
@ECSComponent('Transform')
|
||||
class Transform extends Component {
|
||||
@EntityRef()
|
||||
parent: Entity | null = null;
|
||||
|
||||
position: { x: number, y: number } = { x: 0, y: 0 };
|
||||
|
||||
// 可以直接访问父实体的组件
|
||||
getWorldPosition(): { x: number, y: number } {
|
||||
if (!this.parent) {
|
||||
return { ...this.position };
|
||||
}
|
||||
|
||||
const parentTransform = this.parent.getComponent(Transform);
|
||||
if (parentTransform) {
|
||||
const parentPos = parentTransform.getWorldPosition();
|
||||
return {
|
||||
x: parentPos.x + this.position.x,
|
||||
y: parentPos.y + this.position.y
|
||||
};
|
||||
}
|
||||
|
||||
return { ...this.position };
|
||||
}
|
||||
}
|
||||
|
||||
// 不推荐:不适合使用 @EntityRef 的场景 - 大量动态目标
|
||||
@ECSComponent('AITarget')
|
||||
class AITarget extends Component {
|
||||
@EntityRef()
|
||||
target: Entity | null = null; // 如果目标频繁变化,用ID更好
|
||||
|
||||
updateCooldown: number = 0;
|
||||
}
|
||||
|
||||
// 推荐:这种场景用ID更好
|
||||
@ECSComponent('AITarget')
|
||||
class AITargetBetter extends Component {
|
||||
targetId: number | null = null; // 存储ID
|
||||
updateCooldown: number = 0;
|
||||
}
|
||||
```
|
||||
|
||||
#### 调试支持
|
||||
|
||||
ReferenceTracker 提供了调试接口:
|
||||
|
||||
```typescript
|
||||
// 查看某个实体被哪些组件引用
|
||||
const references = scene.referenceTracker.getReferencesTo(entity.id);
|
||||
console.log(`实体 ${entity.name} 被 ${references.length} 个组件引用`);
|
||||
|
||||
// 获取完整的调试信息
|
||||
const debugInfo = scene.referenceTracker.getDebugInfo();
|
||||
console.log(debugInfo);
|
||||
```
|
||||
|
||||
#### 总结
|
||||
|
||||
- **推荐做法**: 大部分情况使用存储ID + System查找的方式
|
||||
- **EntityRef 适用场景**: 父子关系、复杂关联、组件内需要直接访问引用实体的场景
|
||||
- **核心优势**: 自动清理、防止悬空引用、代码更简洁
|
||||
- **注意事项**: 有性能开销,不适合大量动态引用的场景
|
||||
|
||||
组件是 ECS 架构的数据载体,正确设计组件能让你的游戏代码更模块化、可维护和高性能。
|
||||
@@ -1,750 +0,0 @@
|
||||
# 实体查询系统
|
||||
|
||||
实体查询是 ECS 架构的核心功能之一。本指南将介绍如何使用 Matcher 和 QuerySystem 来查询和筛选实体。
|
||||
|
||||
## 核心概念
|
||||
|
||||
### Matcher - 查询条件描述符
|
||||
|
||||
Matcher 是一个链式 API,用于描述实体查询条件。它本身不执行查询,而是作为条件传递给 EntitySystem 或 QuerySystem。
|
||||
|
||||
### QuerySystem - 查询执行引擎
|
||||
|
||||
QuerySystem 负责实际执行查询,内部使用响应式查询机制自动优化性能。
|
||||
|
||||
## 在 EntitySystem 中使用 Matcher
|
||||
|
||||
这是最常见的使用方式。EntitySystem 通过 Matcher 自动筛选和处理符合条件的实体。
|
||||
|
||||
### 基础用法
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, Matcher, Entity, Component } from '@esengine/ecs-framework';
|
||||
|
||||
class PositionComponent extends Component {
|
||||
public x: number = 0;
|
||||
public y: number = 0;
|
||||
}
|
||||
|
||||
class VelocityComponent extends Component {
|
||||
public vx: number = 0;
|
||||
public vy: number = 0;
|
||||
}
|
||||
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// 方式1: 使用 Matcher.empty().all()
|
||||
super(Matcher.empty().all(PositionComponent, VelocityComponent));
|
||||
|
||||
// 方式2: 直接使用 Matcher.all() (等价)
|
||||
// super(Matcher.all(PositionComponent, VelocityComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const pos = entity.getComponent(PositionComponent)!;
|
||||
const vel = entity.getComponent(VelocityComponent)!;
|
||||
|
||||
pos.x += vel.vx;
|
||||
pos.y += vel.vy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加到场景
|
||||
scene.addEntityProcessor(new MovementSystem());
|
||||
```
|
||||
|
||||
### Matcher 链式 API
|
||||
|
||||
#### all() - 必须包含所有组件
|
||||
|
||||
```typescript
|
||||
class HealthSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// 实体必须同时拥有 Health 和 Position 组件
|
||||
super(Matcher.empty().all(HealthComponent, PositionComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 只处理同时拥有两个组件的实体
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### any() - 至少包含一个组件
|
||||
|
||||
```typescript
|
||||
class DamageableSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// 实体至少拥有 Health 或 Shield 其中之一
|
||||
super(Matcher.any(HealthComponent, ShieldComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 处理拥有生命值或护盾的实体
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### none() - 不能包含指定组件
|
||||
|
||||
```typescript
|
||||
class AliveEntitySystem extends EntitySystem {
|
||||
constructor() {
|
||||
// 实体不能拥有 DeadTag 组件
|
||||
super(Matcher.all(HealthComponent).none(DeadTag));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 只处理活着的实体
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 组合条件
|
||||
|
||||
```typescript
|
||||
class CombatSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(
|
||||
Matcher.empty()
|
||||
.all(PositionComponent, HealthComponent) // 必须有位置和生命
|
||||
.any(WeaponComponent, MagicComponent) // 至少有武器或魔法
|
||||
.none(DeadTag, FrozenTag) // 不能是死亡或冰冻状态
|
||||
);
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 处理可以战斗的活着的实体
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### nothing() - 不匹配任何实体
|
||||
|
||||
用于创建只需要生命周期方法(`onBegin`、`onEnd`)但不需要处理实体的系统。
|
||||
|
||||
```typescript
|
||||
class FrameTimerSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// 不匹配任何实体
|
||||
super(Matcher.nothing());
|
||||
}
|
||||
|
||||
protected onBegin(): void {
|
||||
// 每帧开始时执行
|
||||
Performance.markFrameStart();
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 永远不会被调用,因为没有匹配的实体
|
||||
}
|
||||
|
||||
protected onEnd(): void {
|
||||
// 每帧结束时执行
|
||||
Performance.markFrameEnd();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### empty() vs nothing() 的区别
|
||||
|
||||
| 方法 | 行为 | 使用场景 |
|
||||
|------|------|----------|
|
||||
| `Matcher.empty()` | 匹配**所有**实体 | 需要处理场景中所有实体 |
|
||||
| `Matcher.nothing()` | 不匹配**任何**实体 | 只需要生命周期回调,不处理实体 |
|
||||
|
||||
```typescript
|
||||
// empty() - 返回场景中的所有实体
|
||||
class AllEntitiesSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty());
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// entities 包含场景中的所有实体
|
||||
console.log(`场景中共有 ${entities.length} 个实体`);
|
||||
}
|
||||
}
|
||||
|
||||
// nothing() - 不返回任何实体
|
||||
class NoEntitiesSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.nothing());
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// entities 永远是空数组,此方法不会被调用
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 按标签查询
|
||||
|
||||
```typescript
|
||||
class PlayerSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// 查询特定标签的实体
|
||||
super(Matcher.empty().withTag(Tags.PLAYER));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 只处理玩家实体
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 按名称查询
|
||||
|
||||
```typescript
|
||||
class BossSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// 查询特定名称的实体
|
||||
super(Matcher.empty().withName('Boss'));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 只处理名为 'Boss' 的实体
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 直接使用 QuerySystem
|
||||
|
||||
如果不需要创建系统,可以直接使用 Scene 的 querySystem 进行查询。
|
||||
|
||||
### 基础查询方法
|
||||
|
||||
```typescript
|
||||
// 获取场景的查询系统
|
||||
const querySystem = scene.querySystem;
|
||||
|
||||
// 查询拥有所有指定组件的实体
|
||||
const result1 = querySystem.queryAll(PositionComponent, VelocityComponent);
|
||||
console.log(`找到 ${result1.count} 个移动实体`);
|
||||
console.log(`查询耗时: ${result1.executionTime.toFixed(2)}ms`);
|
||||
|
||||
// 查询拥有任意指定组件的实体
|
||||
const result2 = querySystem.queryAny(WeaponComponent, MagicComponent);
|
||||
console.log(`找到 ${result2.count} 个战斗单位`);
|
||||
|
||||
// 查询不包含指定组件的实体
|
||||
const result3 = querySystem.queryNone(DeadTag);
|
||||
console.log(`找到 ${result3.count} 个活着的实体`);
|
||||
```
|
||||
|
||||
### 按标签查询
|
||||
|
||||
```typescript
|
||||
const playerResult = querySystem.queryByTag(Tags.PLAYER);
|
||||
for (const player of playerResult.entities) {
|
||||
console.log('玩家:', player.name);
|
||||
}
|
||||
```
|
||||
|
||||
### 按名称查询
|
||||
|
||||
```typescript
|
||||
const bossResult = querySystem.queryByName('Boss');
|
||||
if (bossResult.count > 0) {
|
||||
const boss = bossResult.entities[0];
|
||||
console.log('找到Boss:', boss);
|
||||
}
|
||||
```
|
||||
|
||||
### 按单个组件查询
|
||||
|
||||
```typescript
|
||||
const healthResult = querySystem.queryByComponent(HealthComponent);
|
||||
console.log(`有 ${healthResult.count} 个实体拥有生命值`);
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 自动缓存
|
||||
|
||||
QuerySystem 内部使用响应式查询自动缓存结果,相同的查询条件会直接使用缓存:
|
||||
|
||||
```typescript
|
||||
// 第一次查询,执行实际查询
|
||||
const result1 = querySystem.queryAll(PositionComponent);
|
||||
console.log('fromCache:', result1.fromCache); // false
|
||||
|
||||
// 第二次相同查询,使用缓存
|
||||
const result2 = querySystem.queryAll(PositionComponent);
|
||||
console.log('fromCache:', result2.fromCache); // true
|
||||
```
|
||||
|
||||
### 实体变化自动更新
|
||||
|
||||
当实体添加/移除组件时,查询缓存会自动更新:
|
||||
|
||||
```typescript
|
||||
// 查询拥有武器的实体
|
||||
const before = querySystem.queryAll(WeaponComponent);
|
||||
console.log('之前:', before.count); // 假设为 5
|
||||
|
||||
// 给实体添加武器
|
||||
const enemy = scene.createEntity('Enemy');
|
||||
enemy.addComponent(new WeaponComponent());
|
||||
|
||||
// 再次查询,自动包含新实体
|
||||
const after = querySystem.queryAll(WeaponComponent);
|
||||
console.log('之后:', after.count); // 现在是 6
|
||||
```
|
||||
|
||||
### 查询性能统计
|
||||
|
||||
```typescript
|
||||
const stats = querySystem.getStats();
|
||||
console.log('总查询次数:', stats.queryStats.totalQueries);
|
||||
console.log('缓存命中率:', stats.queryStats.cacheHitRate);
|
||||
console.log('缓存大小:', stats.cacheStats.size);
|
||||
```
|
||||
|
||||
## 实际应用场景
|
||||
|
||||
### 场景1: 物理系统
|
||||
|
||||
```typescript
|
||||
class PhysicsSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(TransformComponent, RigidbodyComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const transform = entity.getComponent(TransformComponent)!;
|
||||
const rigidbody = entity.getComponent(RigidbodyComponent)!;
|
||||
|
||||
// 应用重力
|
||||
rigidbody.velocity.y -= 9.8 * Time.deltaTime;
|
||||
|
||||
// 更新位置
|
||||
transform.position.x += rigidbody.velocity.x * Time.deltaTime;
|
||||
transform.position.y += rigidbody.velocity.y * Time.deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 场景2: 渲染系统
|
||||
|
||||
```typescript
|
||||
class RenderSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(
|
||||
Matcher.empty()
|
||||
.all(TransformComponent, SpriteComponent)
|
||||
.none(InvisibleTag) // 排除不可见实体
|
||||
);
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 按 z-order 排序
|
||||
const sorted = entities.slice().sort((a, b) => {
|
||||
const zA = a.getComponent(TransformComponent)!.z;
|
||||
const zB = b.getComponent(TransformComponent)!.z;
|
||||
return zA - zB;
|
||||
});
|
||||
|
||||
// 渲染实体
|
||||
for (const entity of sorted) {
|
||||
const transform = entity.getComponent(TransformComponent)!;
|
||||
const sprite = entity.getComponent(SpriteComponent)!;
|
||||
|
||||
renderer.drawSprite(sprite.texture, transform.position);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 场景3: 碰撞检测
|
||||
|
||||
```typescript
|
||||
class CollisionSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(TransformComponent, ColliderComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 简单的 O(n²) 碰撞检测
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
for (let j = i + 1; j < entities.length; j++) {
|
||||
this.checkCollision(entities[i], entities[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private checkCollision(a: Entity, b: Entity): void {
|
||||
const transA = a.getComponent(TransformComponent)!;
|
||||
const transB = b.getComponent(TransformComponent)!;
|
||||
const colliderA = a.getComponent(ColliderComponent)!;
|
||||
const colliderB = b.getComponent(ColliderComponent)!;
|
||||
|
||||
if (this.isOverlapping(transA, colliderA, transB, colliderB)) {
|
||||
// 触发碰撞事件
|
||||
scene.eventSystem.emit('collision', { entityA: a, entityB: b });
|
||||
}
|
||||
}
|
||||
|
||||
private isOverlapping(...args: any[]): boolean {
|
||||
// 碰撞检测逻辑
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 场景4: 一次性查询
|
||||
|
||||
```typescript
|
||||
// 在系统外部执行一次性查询
|
||||
class GameManager {
|
||||
private scene: Scene;
|
||||
|
||||
public countEnemies(): number {
|
||||
const result = this.scene.querySystem.queryByTag(Tags.ENEMY);
|
||||
return result.count;
|
||||
}
|
||||
|
||||
public findNearestEnemy(playerPos: Vector2): Entity | null {
|
||||
const enemies = this.scene.querySystem.queryByTag(Tags.ENEMY);
|
||||
|
||||
let nearest: Entity | null = null;
|
||||
let minDistance = Infinity;
|
||||
|
||||
for (const enemy of enemies.entities) {
|
||||
const transform = enemy.getComponent(TransformComponent);
|
||||
if (!transform) continue;
|
||||
|
||||
const distance = Vector2.distance(playerPos, transform.position);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
nearest = enemy;
|
||||
}
|
||||
}
|
||||
|
||||
return nearest;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 编译查询 (CompiledQuery)
|
||||
|
||||
> **v2.4.0+**
|
||||
|
||||
CompiledQuery 是一个轻量级的查询工具,提供类型安全的组件访问和变更检测支持。适合临时查询、工具开发和简单的迭代场景。
|
||||
|
||||
### 基本用法
|
||||
|
||||
```typescript
|
||||
// 创建编译查询
|
||||
const query = scene.querySystem.compile(Position, Velocity);
|
||||
|
||||
// 类型安全的遍历 - 组件参数自动推断类型
|
||||
query.forEach((entity, pos, vel) => {
|
||||
pos.x += vel.vx * deltaTime;
|
||||
pos.y += vel.vy * deltaTime;
|
||||
});
|
||||
|
||||
// 获取实体数量
|
||||
console.log(`匹配实体数: ${query.count}`);
|
||||
|
||||
// 获取第一个匹配的实体
|
||||
const first = query.first();
|
||||
if (first) {
|
||||
const [entity, pos, vel] = first;
|
||||
console.log(`第一个实体: ${entity.name}`);
|
||||
}
|
||||
```
|
||||
|
||||
### 变更检测
|
||||
|
||||
CompiledQuery 支持基于 epoch 的变更检测:
|
||||
|
||||
```typescript
|
||||
class RenderSystem extends EntitySystem {
|
||||
private _query: CompiledQuery<[typeof Transform, typeof Sprite]>;
|
||||
private _lastEpoch = 0;
|
||||
|
||||
protected onInitialize(): void {
|
||||
this._query = this.scene!.querySystem.compile(Transform, Sprite);
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 只处理 Transform 或 Sprite 发生变化的实体
|
||||
this._query.forEachChanged(this._lastEpoch, (entity, transform, sprite) => {
|
||||
this.updateRenderData(entity, transform, sprite);
|
||||
});
|
||||
|
||||
// 保存当前 epoch 作为下次检查的起点
|
||||
this._lastEpoch = this.scene!.epochManager.current;
|
||||
}
|
||||
|
||||
private updateRenderData(entity: Entity, transform: Transform, sprite: Sprite): void {
|
||||
// 更新渲染数据
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 函数式 API
|
||||
|
||||
CompiledQuery 提供了丰富的函数式 API:
|
||||
|
||||
```typescript
|
||||
const query = scene.querySystem.compile(Position, Health);
|
||||
|
||||
// map - 转换实体数据
|
||||
const positions = query.map((entity, pos, health) => ({
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
healthPercent: health.current / health.max
|
||||
}));
|
||||
|
||||
// filter - 过滤实体
|
||||
const lowHealthEntities = query.filter((entity, pos, health) => {
|
||||
return health.current < health.max * 0.2;
|
||||
});
|
||||
|
||||
// find - 查找第一个匹配的实体
|
||||
const target = query.find((entity, pos, health) => {
|
||||
return health.current > 0 && pos.x > 100;
|
||||
});
|
||||
|
||||
// toArray - 转换为数组
|
||||
const allData = query.toArray();
|
||||
for (const [entity, pos, health] of allData) {
|
||||
console.log(`${entity.name}: ${pos.x}, ${pos.y}`);
|
||||
}
|
||||
|
||||
// any/empty - 检查是否有匹配
|
||||
if (query.any()) {
|
||||
console.log('有匹配的实体');
|
||||
}
|
||||
if (query.empty()) {
|
||||
console.log('没有匹配的实体');
|
||||
}
|
||||
```
|
||||
|
||||
### CompiledQuery vs EntitySystem
|
||||
|
||||
| 特性 | CompiledQuery | EntitySystem |
|
||||
|------|---------------|--------------|
|
||||
| **用途** | 轻量级查询工具 | 完整的系统逻辑 |
|
||||
| **生命周期** | 无 | 完整 (onInitialize, onDestroy 等) |
|
||||
| **调度集成** | 无 | 支持 @Stage, @Before, @After |
|
||||
| **变更检测** | forEachChanged | forEachChanged |
|
||||
| **事件监听** | 无 | addEventListener |
|
||||
| **命令缓冲** | 无 | this.commands |
|
||||
| **类型安全组件** | forEach 参数自动推断 | 需要手动 getComponent |
|
||||
| **适用场景** | 临时查询、工具、原型 | 核心游戏逻辑 |
|
||||
|
||||
**选择建议**:
|
||||
|
||||
- 使用 **EntitySystem** 处理核心游戏逻辑(移动、战斗、AI 等)
|
||||
- 使用 **CompiledQuery** 进行一次性查询、工具开发或简单迭代
|
||||
|
||||
### CompiledQuery API 参考
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `forEach(callback)` | 遍历所有匹配实体,类型安全的组件参数 |
|
||||
| `forEachChanged(sinceEpoch, callback)` | 只遍历变更的实体 |
|
||||
| `first()` | 获取第一个匹配的实体和组件 |
|
||||
| `toArray()` | 转换为 [entity, ...components] 数组 |
|
||||
| `map(callback)` | 映射转换 |
|
||||
| `filter(predicate)` | 过滤实体 |
|
||||
| `find(predicate)` | 查找第一个满足条件的实体 |
|
||||
| `any()` | 是否有任何匹配 |
|
||||
| `empty()` | 是否没有匹配 |
|
||||
| `count` | 匹配的实体数量 |
|
||||
| `entities` | 匹配的实体列表(只读) |
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 优先使用 EntitySystem
|
||||
|
||||
```typescript
|
||||
// 推荐: 使用 EntitySystem
|
||||
class GoodSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(HealthComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 自动获得符合条件的实体,每帧自动更新
|
||||
}
|
||||
}
|
||||
|
||||
// 不推荐: 在 update 中手动查询
|
||||
class BadSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty());
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 每帧手动查询,浪费性能
|
||||
const result = this.scene!.querySystem.queryAll(HealthComponent);
|
||||
for (const entity of result.entities) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 合理使用 none() 排除条件
|
||||
|
||||
```typescript
|
||||
// 排除已死亡的敌人
|
||||
class EnemyAISystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(
|
||||
Matcher.empty()
|
||||
.all(EnemyTag, AIComponent)
|
||||
.none(DeadTag) // 不处理死亡的敌人
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 使用标签优化查询
|
||||
|
||||
```typescript
|
||||
// 不好: 查询所有实体再过滤
|
||||
const allEntities = scene.querySystem.getAllEntities();
|
||||
const players = allEntities.filter(e => e.hasComponent(PlayerTag));
|
||||
|
||||
// 好: 直接按标签查询
|
||||
const players = scene.querySystem.queryByTag(Tags.PLAYER).entities;
|
||||
```
|
||||
|
||||
### 4. 避免过于复杂的查询条件
|
||||
|
||||
```typescript
|
||||
// 不推荐: 过于复杂
|
||||
super(
|
||||
Matcher.empty()
|
||||
.all(A, B, C, D)
|
||||
.any(E, F, G)
|
||||
.none(H, I, J)
|
||||
);
|
||||
|
||||
// 推荐: 拆分成多个简单系统
|
||||
class SystemAB extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(A, B));
|
||||
}
|
||||
}
|
||||
|
||||
class SystemCD extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(C, D));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
### 1. 查询结果是只读的
|
||||
|
||||
```typescript
|
||||
const result = querySystem.queryAll(PositionComponent);
|
||||
|
||||
// 不要修改返回的数组
|
||||
result.entities.push(someEntity); // 错误!
|
||||
|
||||
// 如果需要修改,先复制
|
||||
const mutableArray = [...result.entities];
|
||||
mutableArray.push(someEntity); // 正确
|
||||
```
|
||||
|
||||
### 2. 组件添加/移除后的查询时机
|
||||
|
||||
```typescript
|
||||
// 创建实体并添加组件
|
||||
const entity = scene.createEntity('Player');
|
||||
entity.addComponent(new PositionComponent());
|
||||
|
||||
// 立即查询可能获取到新实体
|
||||
const result = scene.querySystem.queryAll(PositionComponent);
|
||||
// result.entities 包含新创建的实体
|
||||
```
|
||||
|
||||
### 3. Matcher 是不可变的
|
||||
|
||||
```typescript
|
||||
const matcher = Matcher.empty().all(PositionComponent);
|
||||
|
||||
// 链式调用返回新的 Matcher 实例
|
||||
const matcher2 = matcher.any(VelocityComponent);
|
||||
|
||||
// matcher 本身不变
|
||||
console.log(matcher === matcher2); // false
|
||||
```
|
||||
|
||||
## Matcher API 快速参考
|
||||
|
||||
### 静态创建方法
|
||||
|
||||
| 方法 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `Matcher.all(...types)` | 必须包含所有指定组件 | `Matcher.all(Position, Velocity)` |
|
||||
| `Matcher.any(...types)` | 至少包含一个指定组件 | `Matcher.any(Health, Shield)` |
|
||||
| `Matcher.none(...types)` | 不能包含任何指定组件 | `Matcher.none(Dead)` |
|
||||
| `Matcher.byTag(tag)` | 按标签查询 | `Matcher.byTag(1)` |
|
||||
| `Matcher.byName(name)` | 按名称查询 | `Matcher.byName("Player")` |
|
||||
| `Matcher.byComponent(type)` | 按单个组件查询 | `Matcher.byComponent(Health)` |
|
||||
| `Matcher.empty()` | 创建空匹配器(匹配所有实体) | `Matcher.empty()` |
|
||||
| `Matcher.nothing()` | 不匹配任何实体 | `Matcher.nothing()` |
|
||||
| `Matcher.complex()` | 创建复杂查询构建器 | `Matcher.complex()` |
|
||||
|
||||
### 链式方法
|
||||
|
||||
| 方法 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `.all(...types)` | 添加必须包含的组件 | `.all(Position)` |
|
||||
| `.any(...types)` | 添加可选组件(至少一个) | `.any(Weapon, Magic)` |
|
||||
| `.none(...types)` | 添加排除的组件 | `.none(Dead)` |
|
||||
| `.exclude(...types)` | `.none()` 的别名 | `.exclude(Disabled)` |
|
||||
| `.one(...types)` | `.any()` 的别名 | `.one(Player, Enemy)` |
|
||||
| `.withTag(tag)` | 添加标签条件 | `.withTag(1)` |
|
||||
| `.withName(name)` | 添加名称条件 | `.withName("Boss")` |
|
||||
| `.withComponent(type)` | 添加单组件条件 | `.withComponent(Health)` |
|
||||
|
||||
### 实用方法
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `.getCondition()` | 获取查询条件(只读) |
|
||||
| `.isEmpty()` | 检查是否为空条件 |
|
||||
| `.isNothing()` | 检查是否为 nothing 匹配器 |
|
||||
| `.clone()` | 克隆匹配器 |
|
||||
| `.reset()` | 重置所有条件 |
|
||||
| `.toString()` | 获取字符串表示 |
|
||||
|
||||
### 常用组合示例
|
||||
|
||||
```typescript
|
||||
// 基础移动系统
|
||||
Matcher.all(Position, Velocity)
|
||||
|
||||
// 可攻击的活着的实体
|
||||
Matcher.all(Position, Health)
|
||||
.any(Weapon, Magic)
|
||||
.none(Dead, Disabled)
|
||||
|
||||
// 所有带标签的敌人
|
||||
Matcher.byTag(Tags.ENEMY)
|
||||
.all(AIComponent)
|
||||
|
||||
// 只需要生命周期的系统
|
||||
Matcher.nothing()
|
||||
```
|
||||
|
||||
## 相关 API
|
||||
|
||||
- [Matcher](../api/classes/Matcher.md) - 查询条件描述符 API 参考
|
||||
- [QuerySystem](../api/classes/QuerySystem.md) - 查询系统 API 参考
|
||||
- [EntitySystem](../api/classes/EntitySystem.md) - 实体系统 API 参考
|
||||
- [Entity](../api/classes/Entity.md) - 实体 API 参考
|
||||
@@ -1,446 +0,0 @@
|
||||
# 实体类
|
||||
|
||||
在 ECS 架构中,实体(Entity)是游戏世界中的基本对象。实体本身不包含游戏逻辑或数据,它只是一个容器,用来组合不同的组件来实现各种功能。
|
||||
|
||||
## 基本概念
|
||||
|
||||
实体是一个轻量级的对象,主要用于:
|
||||
- 作为组件的容器
|
||||
- 提供唯一标识(ID)
|
||||
- 管理组件的生命周期
|
||||
|
||||
::: tip 关于父子层级关系
|
||||
实体间的父子层级关系通过 `HierarchyComponent` 和 `HierarchySystem` 管理,而非 Entity 内置属性。这种设计遵循 ECS 组合原则 —— 只有需要层级关系的实体才添加此组件。
|
||||
|
||||
详见 [层级系统](./hierarchy.md) 文档。
|
||||
:::
|
||||
|
||||
## 创建实体
|
||||
|
||||
**重要提示:实体必须通过场景创建,不支持手动创建!**
|
||||
|
||||
实体必须通过场景的 `createEntity()` 方法来创建,这样才能确保:
|
||||
- 实体被正确添加到场景的实体管理系统中
|
||||
- 实体被添加到查询系统中,供系统使用
|
||||
- 实体获得正确的场景引用
|
||||
- 触发相关的生命周期事件
|
||||
|
||||
```typescript
|
||||
// 正确的方式:通过场景创建实体
|
||||
const player = scene.createEntity("Player");
|
||||
|
||||
// ❌ 错误的方式:手动创建实体
|
||||
// const entity = new Entity("MyEntity", 1); // 这样创建的实体系统无法管理
|
||||
```
|
||||
|
||||
## 添加组件
|
||||
|
||||
实体通过添加组件来获得功能:
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent } from '@esengine/ecs-framework';
|
||||
|
||||
// 定义位置组件
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
x: number = 0;
|
||||
y: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
|
||||
// 定义健康组件
|
||||
@ECSComponent('Health')
|
||||
class Health extends Component {
|
||||
current: number = 100;
|
||||
max: number = 100;
|
||||
|
||||
constructor(max: number = 100) {
|
||||
super();
|
||||
this.max = max;
|
||||
this.current = max;
|
||||
}
|
||||
}
|
||||
|
||||
// 给实体添加组件
|
||||
const player = scene.createEntity("Player");
|
||||
player.addComponent(new Position(100, 200));
|
||||
player.addComponent(new Health(150));
|
||||
```
|
||||
|
||||
## 获取组件
|
||||
|
||||
```typescript
|
||||
// 获取组件(传入组件类,不是实例)
|
||||
const position = player.getComponent(Position); // 返回 Position | null
|
||||
const health = player.getComponent(Health); // 返回 Health | null
|
||||
|
||||
// 检查组件是否存在
|
||||
if (position) {
|
||||
console.log(`玩家位置: x=${position.x}, y=${position.y}`);
|
||||
}
|
||||
|
||||
// 检查是否有某个组件
|
||||
if (player.hasComponent(Position)) {
|
||||
console.log("玩家有位置组件");
|
||||
}
|
||||
|
||||
// 获取所有组件实例(只读属性)
|
||||
const allComponents = player.components; // readonly Component[]
|
||||
|
||||
// 获取指定类型的所有组件(支持同类型多组件)
|
||||
const allHealthComponents = player.getComponents(Health); // Health[]
|
||||
|
||||
// 获取或创建组件(如果不存在则自动创建)
|
||||
const position = player.getOrCreateComponent(Position, 0, 0); // 传入构造参数
|
||||
const health = player.getOrCreateComponent(Health, 100); // 如果存在则返回现有的,不存在则创建新的
|
||||
```
|
||||
|
||||
## 移除组件
|
||||
|
||||
```typescript
|
||||
// 方式1:通过组件类型移除
|
||||
const removedHealth = player.removeComponentByType(Health);
|
||||
if (removedHealth) {
|
||||
console.log("健康组件已被移除");
|
||||
}
|
||||
|
||||
// 方式2:通过组件实例移除
|
||||
const healthComponent = player.getComponent(Health);
|
||||
if (healthComponent) {
|
||||
player.removeComponent(healthComponent);
|
||||
}
|
||||
|
||||
// 批量移除多种组件类型
|
||||
const removedComponents = player.removeComponentsByTypes([Position, Health]);
|
||||
|
||||
// 检查组件是否被移除
|
||||
if (!player.hasComponent(Health)) {
|
||||
console.log("健康组件已被移除");
|
||||
}
|
||||
```
|
||||
|
||||
## 实体查找
|
||||
|
||||
场景提供了多种方式来查找实体:
|
||||
|
||||
### 通过名称查找
|
||||
|
||||
```typescript
|
||||
// 查找单个实体
|
||||
const player = scene.findEntity("Player");
|
||||
// 或使用别名方法
|
||||
const player2 = scene.getEntityByName("Player");
|
||||
|
||||
if (player) {
|
||||
console.log("找到玩家实体");
|
||||
}
|
||||
```
|
||||
|
||||
### 通过 ID 查找
|
||||
|
||||
```typescript
|
||||
// 通过实体 ID 查找
|
||||
const entity = scene.findEntityById(123);
|
||||
```
|
||||
|
||||
### 通过标签查找
|
||||
|
||||
实体支持标签系统,用于快速分类和查找:
|
||||
|
||||
```typescript
|
||||
// 设置标签
|
||||
player.tag = 1; // 玩家标签
|
||||
enemy.tag = 2; // 敌人标签
|
||||
|
||||
// 通过标签查找所有相关实体
|
||||
const players = scene.findEntitiesByTag(1);
|
||||
const enemies = scene.findEntitiesByTag(2);
|
||||
// 或使用别名方法
|
||||
const allPlayers = scene.getEntitiesByTag(1);
|
||||
```
|
||||
|
||||
|
||||
## 实体生命周期
|
||||
|
||||
```typescript
|
||||
// 销毁实体
|
||||
player.destroy();
|
||||
|
||||
// 检查实体是否已销毁
|
||||
if (player.isDestroyed) {
|
||||
console.log("实体已被销毁");
|
||||
}
|
||||
```
|
||||
|
||||
## 实体事件
|
||||
|
||||
实体的组件变化会触发事件:
|
||||
|
||||
```typescript
|
||||
// 监听组件添加事件
|
||||
scene.eventSystem.on('component:added', (data) => {
|
||||
console.log('组件已添加:', data);
|
||||
});
|
||||
|
||||
// 监听实体创建事件
|
||||
scene.eventSystem.on('entity:created', (data) => {
|
||||
console.log('实体已创建:', data.entityName);
|
||||
});
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
|
||||
### 批量创建实体
|
||||
|
||||
框架提供了高性能的批量创建方法:
|
||||
|
||||
```typescript
|
||||
// 批量创建 100 个子弹实体(高性能版本)
|
||||
const bullets = scene.createEntities(100, "Bullet");
|
||||
|
||||
// 为每个子弹添加组件
|
||||
bullets.forEach((bullet, index) => {
|
||||
bullet.addComponent(new Position(Math.random() * 800, Math.random() * 600));
|
||||
bullet.addComponent(new Velocity(Math.random() * 100 - 50, Math.random() * 100 - 50));
|
||||
});
|
||||
```
|
||||
|
||||
`createEntities()` 方法会:
|
||||
- 批量分配实体 ID
|
||||
- 批量添加到实体列表
|
||||
- 优化查询系统更新
|
||||
- 减少系统缓存清理次数
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 合理的组件粒度
|
||||
|
||||
```typescript
|
||||
// 好的做法:功能单一的组件
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
x: number = 0;
|
||||
y: number = 0;
|
||||
}
|
||||
|
||||
@ECSComponent('Velocity')
|
||||
class Velocity extends Component {
|
||||
dx: number = 0;
|
||||
dy: number = 0;
|
||||
}
|
||||
|
||||
// 避免:功能过于复杂的组件
|
||||
@ECSComponent('Player')
|
||||
class Player extends Component {
|
||||
// 避免在一个组件中包含太多不相关的属性
|
||||
x: number;
|
||||
y: number;
|
||||
health: number;
|
||||
inventory: Item[];
|
||||
skills: Skill[];
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 使用装饰器
|
||||
|
||||
始终使用 `@ECSComponent` 装饰器:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Transform')
|
||||
class Transform extends Component {
|
||||
// 组件实现
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 合理命名
|
||||
|
||||
```typescript
|
||||
// 清晰的实体命名
|
||||
const mainCharacter = scene.createEntity("MainCharacter");
|
||||
const enemy1 = scene.createEntity("Goblin_001");
|
||||
const collectible = scene.createEntity("HealthPotion");
|
||||
```
|
||||
|
||||
### 4. 及时清理
|
||||
|
||||
```typescript
|
||||
// 不再需要的实体应该及时销毁
|
||||
if (enemy.getComponent(Health).current <= 0) {
|
||||
enemy.destroy();
|
||||
}
|
||||
```
|
||||
|
||||
## 调试实体
|
||||
|
||||
框架提供了调试功能来帮助开发:
|
||||
|
||||
```typescript
|
||||
// 获取实体调试信息
|
||||
const debugInfo = entity.getDebugInfo();
|
||||
console.log('实体信息:', debugInfo);
|
||||
|
||||
// 列出实体的所有组件
|
||||
entity.components.forEach(component => {
|
||||
console.log('组件:', component.constructor.name);
|
||||
});
|
||||
```
|
||||
|
||||
实体是 ECS 架构的核心概念之一,理解如何正确使用实体将帮助你构建高效、可维护的游戏代码。
|
||||
|
||||
## 实体句柄 (EntityHandle)
|
||||
|
||||
实体句柄是一种安全的实体引用方式,用于解决"引用已销毁实体"的问题。
|
||||
|
||||
### 问题场景
|
||||
|
||||
假设你的 AI 系统需要追踪一个目标敌人:
|
||||
|
||||
```typescript
|
||||
// 错误做法:直接存储实体引用
|
||||
class AISystem extends EntitySystem {
|
||||
private targetEnemy: Entity | null = null;
|
||||
|
||||
setTarget(enemy: Entity) {
|
||||
this.targetEnemy = enemy;
|
||||
}
|
||||
|
||||
process() {
|
||||
if (this.targetEnemy) {
|
||||
// 危险!敌人可能已被销毁,但引用还在
|
||||
// 更糟糕:这个内存位置可能被新实体复用了
|
||||
const health = this.targetEnemy.getComponent(Health);
|
||||
// 可能操作了错误的实体!
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用句柄的正确做法
|
||||
|
||||
每个实体创建时会自动分配一个句柄,通过 `entity.handle` 获取:
|
||||
|
||||
```typescript
|
||||
import { EntityHandle, NULL_HANDLE, isValidHandle } from '@esengine/ecs-framework';
|
||||
|
||||
class AISystem extends EntitySystem {
|
||||
// 存储句柄而非实体引用
|
||||
private targetHandle: EntityHandle = NULL_HANDLE;
|
||||
|
||||
setTarget(enemy: Entity) {
|
||||
// 保存敌人的句柄
|
||||
this.targetHandle = enemy.handle;
|
||||
}
|
||||
|
||||
process() {
|
||||
if (!isValidHandle(this.targetHandle)) {
|
||||
return; // 没有目标
|
||||
}
|
||||
|
||||
// 通过句柄获取实体(自动检测是否有效)
|
||||
const enemy = this.scene.findEntityByHandle(this.targetHandle);
|
||||
|
||||
if (!enemy) {
|
||||
// 敌人已被销毁,清空引用
|
||||
this.targetHandle = NULL_HANDLE;
|
||||
return;
|
||||
}
|
||||
|
||||
// 安全操作
|
||||
const health = enemy.getComponent(Health);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 完整示例:技能目标锁定
|
||||
|
||||
```typescript
|
||||
import {
|
||||
EntitySystem, Entity, EntityHandle, NULL_HANDLE, isValidHandle
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
@ECSSystem('SkillTargeting')
|
||||
class SkillTargetingSystem extends EntitySystem {
|
||||
// 存储多个目标的句柄
|
||||
private lockedTargets: Map<Entity, EntityHandle> = new Map();
|
||||
|
||||
// 锁定目标
|
||||
lockTarget(caster: Entity, target: Entity) {
|
||||
this.lockedTargets.set(caster, target.handle);
|
||||
}
|
||||
|
||||
// 获取锁定的目标
|
||||
getLockedTarget(caster: Entity): Entity | null {
|
||||
const handle = this.lockedTargets.get(caster);
|
||||
|
||||
if (!handle || !isValidHandle(handle)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// findEntityByHandle 会检查句柄是否有效
|
||||
const target = this.scene.findEntityByHandle(handle);
|
||||
|
||||
if (!target) {
|
||||
// 目标已死亡,清除锁定
|
||||
this.lockedTargets.delete(caster);
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
// 释放技能
|
||||
castSkill(caster: Entity) {
|
||||
const target = this.getLockedTarget(caster);
|
||||
|
||||
if (!target) {
|
||||
console.log('目标丢失,技能取消');
|
||||
return;
|
||||
}
|
||||
|
||||
// 安全地对目标造成伤害
|
||||
const health = target.getComponent(Health);
|
||||
if (health) {
|
||||
health.current -= 10;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 句柄 vs 实体引用
|
||||
|
||||
| 场景 | 推荐方式 |
|
||||
|-----|---------|
|
||||
| 同一帧内临时使用 | 直接用 `Entity` 引用 |
|
||||
| 跨帧存储(如 AI 目标、技能目标) | 使用 `EntityHandle` |
|
||||
| 需要序列化保存 | 使用 `EntityHandle`(数字类型) |
|
||||
| 网络同步 | 使用 `EntityHandle`(可直接传输) |
|
||||
|
||||
### API 速查
|
||||
|
||||
```typescript
|
||||
// 获取实体的句柄
|
||||
const handle = entity.handle;
|
||||
|
||||
// 检查句柄是否非空
|
||||
if (isValidHandle(handle)) { ... }
|
||||
|
||||
// 通过句柄获取实体(自动检测有效性)
|
||||
const entity = scene.findEntityByHandle(handle);
|
||||
|
||||
// 检查句柄对应的实体是否存活
|
||||
const alive = scene.handleManager.isAlive(handle);
|
||||
|
||||
// 空句柄常量
|
||||
const emptyHandle = NULL_HANDLE;
|
||||
```
|
||||
|
||||
## 下一步
|
||||
|
||||
- 了解 [层级系统](./hierarchy.md) 建立实体间的父子关系
|
||||
- 了解 [组件系统](./component.md) 为实体添加功能
|
||||
- 了解 [场景管理](./scene.md) 组织和管理实体
|
||||
@@ -1,643 +0,0 @@
|
||||
# 插件系统
|
||||
|
||||
插件系统允许你以模块化的方式扩展 ECS Framework 的功能。通过插件,你可以封装特定功能(如网络同步、物理引擎、调试工具等),并在多个项目中复用。
|
||||
|
||||
## 概述
|
||||
|
||||
### 什么是插件
|
||||
|
||||
插件是实现了 `IPlugin` 接口的类,可以在运行时动态安装到框架中。插件可以:
|
||||
|
||||
- 注册自定义服务到服务容器
|
||||
- 添加系统到场景
|
||||
- 注册自定义组件
|
||||
- 扩展框架功能
|
||||
|
||||
### 插件的优势
|
||||
|
||||
- **模块化**: 将功能封装为独立模块,提高代码可维护性
|
||||
- **可复用**: 同一个插件可以在多个项目中使用
|
||||
- **解耦**: 核心框架与扩展功能分离
|
||||
- **热插拔**: 运行时动态安装和卸载插件
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 创建第一个插件
|
||||
|
||||
创建一个简单的调试插件:
|
||||
|
||||
```typescript
|
||||
import { IPlugin, Core, ServiceContainer } from '@esengine/ecs-framework';
|
||||
|
||||
class DebugPlugin implements IPlugin {
|
||||
readonly name = 'debug-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
console.log('Debug plugin installed');
|
||||
|
||||
// 可以在这里注册服务、添加系统等
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
console.log('Debug plugin uninstalled');
|
||||
// 清理资源
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 安装插件
|
||||
|
||||
使用 `Core.installPlugin()` 安装插件:
|
||||
|
||||
```typescript
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始化Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 安装插件
|
||||
await Core.installPlugin(new DebugPlugin());
|
||||
|
||||
// 检查插件是否已安装
|
||||
if (Core.isPluginInstalled('debug-plugin')) {
|
||||
console.log('Debug plugin is running');
|
||||
}
|
||||
```
|
||||
|
||||
### 卸载插件
|
||||
|
||||
```typescript
|
||||
// 卸载插件
|
||||
await Core.uninstallPlugin('debug-plugin');
|
||||
```
|
||||
|
||||
### 获取插件实例
|
||||
|
||||
```typescript
|
||||
// 获取已安装的插件
|
||||
const plugin = Core.getPlugin('debug-plugin');
|
||||
if (plugin) {
|
||||
console.log(`Plugin version: ${plugin.version}`);
|
||||
}
|
||||
```
|
||||
|
||||
## 插件开发
|
||||
|
||||
### IPlugin 接口
|
||||
|
||||
所有插件必须实现 `IPlugin` 接口:
|
||||
|
||||
```typescript
|
||||
export interface IPlugin {
|
||||
// 插件唯一名称
|
||||
readonly name: string;
|
||||
|
||||
// 插件版本(建议遵循semver规范)
|
||||
readonly version: string;
|
||||
|
||||
// 依赖的其他插件(可选)
|
||||
readonly dependencies?: readonly string[];
|
||||
|
||||
// 安装插件时调用
|
||||
install(core: Core, services: ServiceContainer): void | Promise<void>;
|
||||
|
||||
// 卸载插件时调用
|
||||
uninstall(): void | Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
### 插件生命周期
|
||||
|
||||
#### install 方法
|
||||
|
||||
在插件安装时调用,用于初始化插件:
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// 1. 注册服务
|
||||
services.registerSingleton(MyService);
|
||||
|
||||
// 2. 访问当前场景
|
||||
const scene = core.scene;
|
||||
if (scene) {
|
||||
// 3. 添加系统
|
||||
scene.addSystem(new MySystem());
|
||||
}
|
||||
|
||||
// 4. 其他初始化逻辑
|
||||
console.log('Plugin initialized');
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 清理逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### uninstall 方法
|
||||
|
||||
在插件卸载时调用,用于清理资源:
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-plugin';
|
||||
readonly version = '1.0.0';
|
||||
private myService?: MyService;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
this.myService = new MyService();
|
||||
services.registerInstance(MyService, this.myService);
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 清理服务
|
||||
if (this.myService) {
|
||||
this.myService.dispose();
|
||||
this.myService = undefined;
|
||||
}
|
||||
|
||||
// 移除事件监听器
|
||||
// 释放其他资源
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 异步插件
|
||||
|
||||
插件的 `install` 和 `uninstall` 方法都支持异步:
|
||||
|
||||
```typescript
|
||||
class AsyncPlugin implements IPlugin {
|
||||
readonly name = 'async-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
async install(core: Core, services: ServiceContainer): Promise<void> {
|
||||
// 异步加载资源
|
||||
const config = await fetch('/plugin-config.json').then(r => r.json());
|
||||
|
||||
// 使用加载的配置初始化服务
|
||||
const service = new MyService(config);
|
||||
services.registerInstance(MyService, service);
|
||||
}
|
||||
|
||||
async uninstall(): Promise<void> {
|
||||
// 异步清理
|
||||
await this.saveState();
|
||||
}
|
||||
|
||||
private async saveState() {
|
||||
// 保存插件状态
|
||||
}
|
||||
}
|
||||
|
||||
// 使用
|
||||
await Core.installPlugin(new AsyncPlugin());
|
||||
```
|
||||
|
||||
### 注册服务
|
||||
|
||||
插件可以向服务容器注册自己的服务:
|
||||
|
||||
```typescript
|
||||
import { IService } from '@esengine/ecs-framework';
|
||||
|
||||
class NetworkService implements IService {
|
||||
connect(url: string) {
|
||||
console.log(`Connecting to ${url}`);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
console.log('Network service disposed');
|
||||
}
|
||||
}
|
||||
|
||||
class NetworkPlugin implements IPlugin {
|
||||
readonly name = 'network-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// 注册网络服务
|
||||
services.registerSingleton(NetworkService);
|
||||
|
||||
// 解析并使用服务
|
||||
const network = services.resolve(NetworkService);
|
||||
network.connect('ws://localhost:8080');
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 服务容器会自动调用服务的dispose方法
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 添加系统
|
||||
|
||||
插件可以向场景添加自定义系统:
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, Matcher } from '@esengine/ecs-framework';
|
||||
|
||||
class PhysicsSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(PhysicsBody));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 物理模拟逻辑
|
||||
}
|
||||
}
|
||||
|
||||
class PhysicsPlugin implements IPlugin {
|
||||
readonly name = 'physics-plugin';
|
||||
readonly version = '1.0.0';
|
||||
private physicsSystem?: PhysicsSystem;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
const scene = core.scene;
|
||||
if (scene) {
|
||||
this.physicsSystem = new PhysicsSystem();
|
||||
scene.addSystem(this.physicsSystem);
|
||||
}
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 移除系统
|
||||
if (this.physicsSystem) {
|
||||
const scene = Core.scene;
|
||||
if (scene) {
|
||||
scene.removeSystem(this.physicsSystem);
|
||||
}
|
||||
this.physicsSystem = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 依赖管理
|
||||
|
||||
### 声明依赖
|
||||
|
||||
插件可以声明对其他插件的依赖:
|
||||
|
||||
```typescript
|
||||
class AdvancedPhysicsPlugin implements IPlugin {
|
||||
readonly name = 'advanced-physics';
|
||||
readonly version = '2.0.0';
|
||||
|
||||
// 声明依赖基础物理插件
|
||||
readonly dependencies = ['physics-plugin'] as const;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// 可以安全地使用physics-plugin提供的服务
|
||||
const physicsService = services.resolve(PhysicsService);
|
||||
// ...
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 清理
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 依赖检查
|
||||
|
||||
框架会自动检查依赖关系,如果依赖未满足会抛出错误:
|
||||
|
||||
```typescript
|
||||
// 错误:physics-plugin 未安装
|
||||
try {
|
||||
await Core.installPlugin(new AdvancedPhysicsPlugin());
|
||||
} catch (error) {
|
||||
console.error(error); // Plugin advanced-physics has unmet dependencies: physics-plugin
|
||||
}
|
||||
|
||||
// 正确:先安装依赖
|
||||
await Core.installPlugin(new PhysicsPlugin());
|
||||
await Core.installPlugin(new AdvancedPhysicsPlugin());
|
||||
```
|
||||
|
||||
### 卸载顺序
|
||||
|
||||
框架会检查依赖关系,防止卸载被其他插件依赖的插件:
|
||||
|
||||
```typescript
|
||||
await Core.installPlugin(new PhysicsPlugin());
|
||||
await Core.installPlugin(new AdvancedPhysicsPlugin());
|
||||
|
||||
// 错误:physics-plugin 被 advanced-physics 依赖
|
||||
try {
|
||||
await Core.uninstallPlugin('physics-plugin');
|
||||
} catch (error) {
|
||||
console.error(error); // Cannot uninstall plugin physics-plugin: it is required by advanced-physics
|
||||
}
|
||||
|
||||
// 正确:先卸载依赖它的插件
|
||||
await Core.uninstallPlugin('advanced-physics');
|
||||
await Core.uninstallPlugin('physics-plugin');
|
||||
```
|
||||
|
||||
## 插件管理
|
||||
|
||||
### 通过 Core 管理
|
||||
|
||||
Core 类提供了便捷的插件管理方法:
|
||||
|
||||
```typescript
|
||||
// 安装插件
|
||||
await Core.installPlugin(myPlugin);
|
||||
|
||||
// 卸载插件
|
||||
await Core.uninstallPlugin('plugin-name');
|
||||
|
||||
// 检查插件是否已安装
|
||||
if (Core.isPluginInstalled('plugin-name')) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// 获取插件实例
|
||||
const plugin = Core.getPlugin('plugin-name');
|
||||
```
|
||||
|
||||
### 通过 PluginManager 管理
|
||||
|
||||
也可以直接使用 PluginManager 服务:
|
||||
|
||||
```typescript
|
||||
const pluginManager = Core.services.resolve(PluginManager);
|
||||
|
||||
// 获取所有插件
|
||||
const allPlugins = pluginManager.getAllPlugins();
|
||||
console.log(`Total plugins: ${allPlugins.length}`);
|
||||
|
||||
// 获取插件元数据
|
||||
const metadata = pluginManager.getMetadata('my-plugin');
|
||||
if (metadata) {
|
||||
console.log(`State: ${metadata.state}`);
|
||||
console.log(`Installed at: ${new Date(metadata.installedAt!)}`);
|
||||
}
|
||||
|
||||
// 获取所有插件元数据
|
||||
const allMetadata = pluginManager.getAllMetadata();
|
||||
for (const meta of allMetadata) {
|
||||
console.log(`${meta.name} v${meta.version} - ${meta.state}`);
|
||||
}
|
||||
```
|
||||
|
||||
## 实用插件示例
|
||||
|
||||
### 网络同步插件
|
||||
|
||||
```typescript
|
||||
import { IPlugin, IService, Core, ServiceContainer } from '@esengine/ecs-framework';
|
||||
|
||||
class NetworkSyncService implements IService {
|
||||
private ws?: WebSocket;
|
||||
|
||||
connect(url: string) {
|
||||
this.ws = new WebSocket(url);
|
||||
this.ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
this.handleMessage(data);
|
||||
};
|
||||
}
|
||||
|
||||
private handleMessage(data: any) {
|
||||
// 处理网络消息
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
this.ws = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NetworkSyncPlugin implements IPlugin {
|
||||
readonly name = 'network-sync';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// 注册网络服务
|
||||
services.registerSingleton(NetworkSyncService);
|
||||
|
||||
// 自动连接
|
||||
const network = services.resolve(NetworkSyncService);
|
||||
network.connect('ws://localhost:8080');
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 服务会自动dispose
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 性能分析插件
|
||||
|
||||
```typescript
|
||||
class PerformanceAnalysisPlugin implements IPlugin {
|
||||
readonly name = 'performance-analysis';
|
||||
readonly version = '1.0.0';
|
||||
private frameCount = 0;
|
||||
private totalTime = 0;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
const monitor = services.resolve(PerformanceMonitor);
|
||||
monitor.enable();
|
||||
|
||||
// 定期输出性能报告
|
||||
const timer = services.resolve(TimerManager);
|
||||
timer.schedule(5.0, true, null, () => {
|
||||
this.printReport(monitor);
|
||||
});
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 清理
|
||||
}
|
||||
|
||||
private printReport(monitor: PerformanceMonitor) {
|
||||
console.log('=== Performance Report ===');
|
||||
console.log(`FPS: ${monitor.getFPS()}`);
|
||||
console.log(`Memory: ${monitor.getMemoryUsage()} MB`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 命名规范
|
||||
|
||||
- 插件名称使用小写字母和连字符:`my-awesome-plugin`
|
||||
- 版本号遵循语义化版本规范:`1.0.0`
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-awesome-plugin'; // 好
|
||||
readonly version = '1.0.0'; // 好
|
||||
}
|
||||
```
|
||||
|
||||
### 清理资源
|
||||
|
||||
始终在 `uninstall` 中清理插件创建的所有资源:
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-plugin';
|
||||
readonly version = '1.0.0';
|
||||
private timerId?: number;
|
||||
private listener?: () => void;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// 添加定时器
|
||||
this.timerId = setInterval(() => {
|
||||
// ...
|
||||
}, 1000);
|
||||
|
||||
// 添加事件监听
|
||||
this.listener = () => {};
|
||||
window.addEventListener('resize', this.listener);
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 清理定时器
|
||||
if (this.timerId) {
|
||||
clearInterval(this.timerId);
|
||||
this.timerId = undefined;
|
||||
}
|
||||
|
||||
// 移除事件监听
|
||||
if (this.listener) {
|
||||
window.removeEventListener('resize', this.listener);
|
||||
this.listener = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 错误处理
|
||||
|
||||
在插件中妥善处理错误,避免影响整个应用:
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
async install(core: Core, services: ServiceContainer): Promise<void> {
|
||||
try {
|
||||
// 可能失败的操作
|
||||
await this.loadConfig();
|
||||
} catch (error) {
|
||||
console.error('Failed to load plugin config:', error);
|
||||
throw error; // 重新抛出,让框架知道安装失败
|
||||
}
|
||||
}
|
||||
|
||||
async uninstall(): Promise<void> {
|
||||
try {
|
||||
await this.cleanup();
|
||||
} catch (error) {
|
||||
console.error('Failed to cleanup plugin:', error);
|
||||
// 即使清理失败也不应该阻止卸载
|
||||
}
|
||||
}
|
||||
|
||||
private async loadConfig() {
|
||||
// 加载配置
|
||||
}
|
||||
|
||||
private async cleanup() {
|
||||
// 清理
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 配置化
|
||||
|
||||
允许用户配置插件行为:
|
||||
|
||||
```typescript
|
||||
interface NetworkPluginConfig {
|
||||
serverUrl: string;
|
||||
autoReconnect: boolean;
|
||||
timeout: number;
|
||||
}
|
||||
|
||||
class NetworkPlugin implements IPlugin {
|
||||
readonly name = 'network-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
constructor(private config: NetworkPluginConfig) {}
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
const network = new NetworkService(this.config);
|
||||
services.registerInstance(NetworkService, network);
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 清理
|
||||
}
|
||||
}
|
||||
|
||||
// 使用
|
||||
const plugin = new NetworkPlugin({
|
||||
serverUrl: 'ws://localhost:8080',
|
||||
autoReconnect: true,
|
||||
timeout: 5000
|
||||
});
|
||||
|
||||
await Core.installPlugin(plugin);
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 插件安装失败
|
||||
|
||||
**问题**: 插件安装时抛出错误
|
||||
|
||||
**原因**:
|
||||
- 依赖未满足
|
||||
- install 方法中有异常
|
||||
- 服务注册冲突
|
||||
|
||||
**解决**:
|
||||
1. 检查依赖是否已安装
|
||||
2. 查看错误日志
|
||||
3. 确保服务名称不冲突
|
||||
|
||||
### 插件卸载后仍有副作用
|
||||
|
||||
**问题**: 卸载插件后,插件的功能仍在运行
|
||||
|
||||
**原因**: uninstall 方法中未正确清理资源
|
||||
|
||||
**解决**: 确保在 uninstall 中清理:
|
||||
- 定时器
|
||||
- 事件监听器
|
||||
- WebSocket连接
|
||||
- 系统引用
|
||||
|
||||
### 何时使用插件
|
||||
|
||||
**适合使用插件**:
|
||||
- 可选功能(调试工具、性能分析)
|
||||
- 第三方集成(网络库、物理引擎)
|
||||
- 跨项目复用的功能模块
|
||||
|
||||
**不适合使用插件**:
|
||||
- 核心游戏逻辑
|
||||
- 简单的工具类
|
||||
- 项目特定的功能
|
||||
|
||||
## 相关链接
|
||||
|
||||
- [服务容器](./service-container.md) - 在插件中使用服务容器
|
||||
- [系统架构](./system.md) - 在插件中添加系统
|
||||
- [快速开始](./getting-started.md) - Core 初始化和基础使用
|
||||
@@ -1,681 +0,0 @@
|
||||
# SceneManager
|
||||
|
||||
SceneManager 是 ECS Framework 提供的轻量级场景管理器,适用于 95% 的游戏应用。它提供简单直观的 API,支持场景切换和延迟加载。
|
||||
|
||||
## 适用场景
|
||||
|
||||
SceneManager 适合以下场景:
|
||||
- 单人游戏
|
||||
- 简单多人游戏
|
||||
- 移动游戏
|
||||
- 需要场景切换的游戏(菜单、游戏、暂停等)
|
||||
- 不需要多 World 隔离的项目
|
||||
|
||||
## 特点
|
||||
|
||||
- 轻量级,零额外开销
|
||||
- 简单直观的 API
|
||||
- 支持延迟场景切换(避免在当前帧中途切换)
|
||||
- 自动管理 ECS 流式 API
|
||||
- 自动处理场景生命周期
|
||||
- 集成在 Core 中,自动更新
|
||||
- 支持[持久化实体](./persistent-entity.md)跨场景迁移(v2.3.0+)
|
||||
|
||||
## 基本使用
|
||||
|
||||
### 推荐方式:使用 Core 的静态方法
|
||||
|
||||
这是最简单和推荐的方式,适合大多数应用:
|
||||
|
||||
```typescript
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// 1. 初始化 Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 2. 创建并设置场景
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "GameScene";
|
||||
|
||||
// 添加系统
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
|
||||
// 创建初始实体
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Transform(400, 300));
|
||||
player.addComponent(new Health(100));
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log("游戏场景已启动");
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 设置场景
|
||||
Core.setScene(new GameScene());
|
||||
|
||||
// 4. 游戏循环(Core.update 会自动更新场景)
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // 自动更新所有服务和场景
|
||||
}
|
||||
|
||||
// Laya 引擎集成
|
||||
Laya.timer.frameLoop(1, this, () => {
|
||||
const deltaTime = Laya.timer.delta / 1000;
|
||||
Core.update(deltaTime);
|
||||
});
|
||||
|
||||
// Cocos Creator 集成
|
||||
update(deltaTime: number) {
|
||||
Core.update(deltaTime);
|
||||
}
|
||||
```
|
||||
|
||||
### 高级方式:直接使用 SceneManager
|
||||
|
||||
如果需要更多控制,可以直接使用 SceneManager:
|
||||
|
||||
```typescript
|
||||
import { Core, SceneManager, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始化 Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 获取 SceneManager(Core 已自动创建并注册)
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
|
||||
// 设置场景
|
||||
const gameScene = new GameScene();
|
||||
sceneManager.setScene(gameScene);
|
||||
|
||||
// 游戏循环(仍然使用 Core.update)
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // Core会自动调用sceneManager.update()
|
||||
}
|
||||
```
|
||||
|
||||
**重要**:无论使用哪种方式,游戏循环中都应该只调用 `Core.update()`,它会自动更新 SceneManager 和场景。不需要手动调用 `sceneManager.update()`。
|
||||
|
||||
## 场景切换
|
||||
|
||||
### 立即切换
|
||||
|
||||
使用 `Core.setScene()` 或 `sceneManager.setScene()` 立即切换场景:
|
||||
|
||||
```typescript
|
||||
// 方式1:使用 Core(推荐)
|
||||
Core.setScene(new MenuScene());
|
||||
|
||||
// 方式2:使用 SceneManager
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
sceneManager.setScene(new MenuScene());
|
||||
```
|
||||
|
||||
### 延迟切换
|
||||
|
||||
使用 `Core.loadScene()` 或 `sceneManager.loadScene()` 延迟切换场景,场景会在下一帧切换:
|
||||
|
||||
```typescript
|
||||
// 方式1:使用 Core(推荐)
|
||||
Core.loadScene(new GameOverScene());
|
||||
|
||||
// 方式2:使用 SceneManager
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
sceneManager.loadScene(new GameOverScene());
|
||||
```
|
||||
|
||||
在 System 中切换场景时,应该使用延迟切换:
|
||||
|
||||
```typescript
|
||||
class GameOverSystem extends EntitySystem {
|
||||
process(entities: readonly Entity[]): void {
|
||||
const player = entities.find(e => e.name === 'Player');
|
||||
const health = player?.getComponent(Health);
|
||||
|
||||
if (health && health.value <= 0) {
|
||||
// 延迟切换到游戏结束场景(下一帧生效)
|
||||
Core.loadScene(new GameOverScene());
|
||||
// 当前帧继续执行,不会中断当前系统的处理
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 完整的场景切换示例
|
||||
|
||||
```typescript
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始化
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 菜单场景
|
||||
class MenuScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "MenuScene";
|
||||
|
||||
// 监听开始游戏事件
|
||||
this.eventSystem.on('start_game', () => {
|
||||
Core.loadScene(new GameScene());
|
||||
});
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log("显示菜单界面");
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
console.log("菜单场景卸载");
|
||||
}
|
||||
}
|
||||
|
||||
// 游戏场景
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "GameScene";
|
||||
|
||||
// 创建游戏实体
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Transform(400, 300));
|
||||
player.addComponent(new Health(100));
|
||||
|
||||
// 监听游戏结束事件
|
||||
this.eventSystem.on('game_over', () => {
|
||||
Core.loadScene(new GameOverScene());
|
||||
});
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log("游戏开始");
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
console.log("游戏场景卸载");
|
||||
}
|
||||
}
|
||||
|
||||
// 游戏结束场景
|
||||
class GameOverScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "GameOverScene";
|
||||
|
||||
// 监听返回菜单事件
|
||||
this.eventSystem.on('back_to_menu', () => {
|
||||
Core.loadScene(new MenuScene());
|
||||
});
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log("显示游戏结束界面");
|
||||
}
|
||||
}
|
||||
|
||||
// 开始游戏
|
||||
Core.setScene(new MenuScene());
|
||||
|
||||
// 游戏循环
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // 自动更新场景
|
||||
}
|
||||
```
|
||||
|
||||
## API 参考
|
||||
|
||||
### Core 静态方法(推荐)
|
||||
|
||||
#### Core.setScene()
|
||||
|
||||
立即切换场景。
|
||||
|
||||
```typescript
|
||||
public static setScene<T extends IScene>(scene: T): T
|
||||
```
|
||||
|
||||
**参数**:
|
||||
- `scene` - 要设置的场景实例
|
||||
|
||||
**返回**:
|
||||
- 返回设置的场景实例
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
const gameScene = Core.setScene(new GameScene());
|
||||
console.log(gameScene.name);
|
||||
```
|
||||
|
||||
#### Core.loadScene()
|
||||
|
||||
延迟加载场景(下一帧切换)。
|
||||
|
||||
```typescript
|
||||
public static loadScene<T extends IScene>(scene: T): void
|
||||
```
|
||||
|
||||
**参数**:
|
||||
- `scene` - 要加载的场景实例
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
Core.loadScene(new GameOverScene());
|
||||
```
|
||||
|
||||
#### Core.scene
|
||||
|
||||
获取当前活跃的场景。
|
||||
|
||||
```typescript
|
||||
public static get scene(): IScene | null
|
||||
```
|
||||
|
||||
**返回**:
|
||||
- 当前场景实例,如果没有场景则返回 null
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
const currentScene = Core.scene;
|
||||
if (currentScene) {
|
||||
console.log(`当前场景: ${currentScene.name}`);
|
||||
}
|
||||
```
|
||||
|
||||
#### Core.ecsAPI
|
||||
|
||||
获取 ECS 流式 API。
|
||||
|
||||
```typescript
|
||||
public static get ecsAPI(): ECSFluentAPI | null
|
||||
```
|
||||
|
||||
**返回**:
|
||||
- ECS API 实例,如果当前没有场景则返回 null
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
const api = Core.ecsAPI;
|
||||
if (api) {
|
||||
// 查询实体
|
||||
const enemies = api.find(Enemy, Transform);
|
||||
|
||||
// 发射事件
|
||||
api.emit('game:start', { level: 1 });
|
||||
}
|
||||
```
|
||||
|
||||
### SceneManager 方法(高级)
|
||||
|
||||
如果需要直接使用 SceneManager,可以通过服务容器获取:
|
||||
|
||||
```typescript
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
```
|
||||
|
||||
#### setScene()
|
||||
|
||||
立即切换场景。
|
||||
|
||||
```typescript
|
||||
public setScene<T extends IScene>(scene: T): T
|
||||
```
|
||||
|
||||
#### loadScene()
|
||||
|
||||
延迟加载场景。
|
||||
|
||||
```typescript
|
||||
public loadScene<T extends IScene>(scene: T): void
|
||||
```
|
||||
|
||||
#### currentScene
|
||||
|
||||
获取当前场景。
|
||||
|
||||
```typescript
|
||||
public get currentScene(): IScene | null
|
||||
```
|
||||
|
||||
#### api
|
||||
|
||||
获取 ECS 流式 API。
|
||||
|
||||
```typescript
|
||||
public get api(): ECSFluentAPI | null
|
||||
```
|
||||
|
||||
#### hasScene
|
||||
|
||||
检查是否有活跃场景。
|
||||
|
||||
```typescript
|
||||
public get hasScene(): boolean
|
||||
```
|
||||
|
||||
#### hasPendingScene
|
||||
|
||||
检查是否有待切换的场景。
|
||||
|
||||
```typescript
|
||||
public get hasPendingScene(): boolean
|
||||
```
|
||||
|
||||
## 使用 ECS 流式 API
|
||||
|
||||
通过 `Core.ecsAPI` 可以方便地访问场景的 ECS 功能:
|
||||
|
||||
```typescript
|
||||
const api = Core.ecsAPI;
|
||||
if (!api) {
|
||||
console.error('没有活跃场景');
|
||||
return;
|
||||
}
|
||||
|
||||
// 查询实体
|
||||
const players = api.find(Player, Transform);
|
||||
const enemies = api.find(Enemy, Health, Transform);
|
||||
|
||||
// 发射事件
|
||||
api.emit('player:scored', { points: 100 });
|
||||
|
||||
// 监听事件
|
||||
api.on('enemy:died', (data) => {
|
||||
console.log('敌人死亡:', data);
|
||||
});
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 使用 Core 的静态方法
|
||||
|
||||
```typescript
|
||||
// 推荐:使用 Core 的静态方法
|
||||
Core.setScene(new GameScene());
|
||||
Core.loadScene(new MenuScene());
|
||||
const currentScene = Core.scene;
|
||||
|
||||
// 不推荐:除非有特殊需求,否则不需要直接使用 SceneManager
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
sceneManager.setScene(new GameScene());
|
||||
```
|
||||
|
||||
### 2. 只调用 Core.update()
|
||||
|
||||
```typescript
|
||||
// 正确:只调用 Core.update()
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // 自动更新所有服务和场景
|
||||
}
|
||||
|
||||
// 错误:不要手动调用 sceneManager.update()
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime);
|
||||
sceneManager.update(); // 重复更新,会导致问题!
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 使用延迟切换避免问题
|
||||
|
||||
在 System 中切换场景时,应该使用 `loadScene()` 而不是 `setScene()`:
|
||||
|
||||
```typescript
|
||||
// 推荐:延迟切换
|
||||
class HealthSystem extends EntitySystem {
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health);
|
||||
if (health.value <= 0) {
|
||||
Core.loadScene(new GameOverScene());
|
||||
// 当前帧继续处理其他实体
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 不推荐:立即切换可能导致问题
|
||||
class HealthSystem extends EntitySystem {
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health);
|
||||
if (health.value <= 0) {
|
||||
Core.setScene(new GameOverScene());
|
||||
// 场景立即切换,当前帧的其他实体可能无法正常处理
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 场景职责分离
|
||||
|
||||
每个场景应该只负责一个特定的游戏状态:
|
||||
|
||||
```typescript
|
||||
// 好的设计 - 职责清晰
|
||||
class MenuScene extends Scene {
|
||||
// 只处理菜单相关逻辑
|
||||
}
|
||||
|
||||
class GameScene extends Scene {
|
||||
// 只处理游戏玩法逻辑
|
||||
}
|
||||
|
||||
class PauseScene extends Scene {
|
||||
// 只处理暂停界面逻辑
|
||||
}
|
||||
|
||||
// 避免的设计 - 职责混乱
|
||||
class MegaScene extends Scene {
|
||||
// 包含菜单、游戏、暂停等所有逻辑
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 资源管理
|
||||
|
||||
在场景的 `unload()` 方法中清理资源:
|
||||
|
||||
```typescript
|
||||
class GameScene extends Scene {
|
||||
private textures: Map<string, any> = new Map();
|
||||
private sounds: Map<string, any> = new Map();
|
||||
|
||||
protected initialize(): void {
|
||||
this.loadResources();
|
||||
}
|
||||
|
||||
private loadResources(): void {
|
||||
this.textures.set('player', loadTexture('player.png'));
|
||||
this.sounds.set('bgm', loadSound('bgm.mp3'));
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// 清理资源
|
||||
this.textures.clear();
|
||||
this.sounds.clear();
|
||||
console.log('场景资源已清理');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. 事件驱动的场景切换
|
||||
|
||||
使用事件系统来触发场景切换,保持代码解耦:
|
||||
|
||||
```typescript
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 监听场景切换事件
|
||||
this.eventSystem.on('goto:menu', () => {
|
||||
Core.loadScene(new MenuScene());
|
||||
});
|
||||
|
||||
this.eventSystem.on('goto:gameover', (data) => {
|
||||
Core.loadScene(new GameOverScene());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 在 System 中触发事件
|
||||
class GameLogicSystem extends EntitySystem {
|
||||
process(entities: readonly Entity[]): void {
|
||||
if (levelComplete) {
|
||||
this.scene.eventSystem.emitSync('goto:gameover', {
|
||||
score: 1000,
|
||||
level: 5
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 架构层次
|
||||
|
||||
SceneManager 在 ECS Framework 中的位置:
|
||||
|
||||
```
|
||||
Core (全局服务)
|
||||
└── SceneManager (场景管理,自动更新)
|
||||
└── Scene (当前场景)
|
||||
├── EntitySystem (系统)
|
||||
├── Entity (实体)
|
||||
└── Component (组件)
|
||||
```
|
||||
|
||||
## 与 WorldManager 的对比
|
||||
|
||||
| 特性 | SceneManager | WorldManager |
|
||||
|------|--------------|--------------|
|
||||
| 适用场景 | 95% 的游戏应用 | 高级多世界隔离场景 |
|
||||
| 复杂度 | 简单 | 复杂 |
|
||||
| 场景数量 | 单场景(可切换) | 多 World,每个 World 多场景 |
|
||||
| 性能开销 | 最小 | 较高 |
|
||||
| 使用方式 | `Core.setScene()` | `worldManager.createWorld()` |
|
||||
|
||||
**何时使用 SceneManager**:
|
||||
- 单人游戏
|
||||
- 简单的多人游戏
|
||||
- 移动游戏
|
||||
- 场景之间需要切换但不需要同时运行
|
||||
|
||||
**何时使用 WorldManager**:
|
||||
- MMO 游戏服务器(每个房间一个 World)
|
||||
- 游戏大厅系统(每个游戏房间完全隔离)
|
||||
- 需要运行多个完全独立的游戏实例
|
||||
|
||||
## 完整示例
|
||||
|
||||
```typescript
|
||||
import { Core, Scene, EntitySystem, Entity, Matcher } from '@esengine/ecs-framework';
|
||||
|
||||
// 定义组件
|
||||
class Transform {
|
||||
constructor(public x: number, public y: number) {}
|
||||
}
|
||||
|
||||
class Velocity {
|
||||
constructor(public vx: number, public vy: number) {}
|
||||
}
|
||||
|
||||
class Health {
|
||||
constructor(public value: number) {}
|
||||
}
|
||||
|
||||
// 定义系统
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Transform, Velocity));
|
||||
}
|
||||
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const transform = entity.getComponent(Transform);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
if (transform && velocity) {
|
||||
transform.x += velocity.vx;
|
||||
transform.y += velocity.vy;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 定义场景
|
||||
class MenuScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "MenuScene";
|
||||
console.log("菜单场景初始化");
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log("菜单场景启动");
|
||||
}
|
||||
}
|
||||
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "GameScene";
|
||||
|
||||
// 添加系统
|
||||
this.addSystem(new MovementSystem());
|
||||
|
||||
// 创建玩家
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Transform(400, 300));
|
||||
player.addComponent(new Velocity(0, 0));
|
||||
player.addComponent(new Health(100));
|
||||
|
||||
// 创建敌人
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const enemy = this.createEntity(`Enemy_${i}`);
|
||||
enemy.addComponent(new Transform(
|
||||
Math.random() * 800,
|
||||
Math.random() * 600
|
||||
));
|
||||
enemy.addComponent(new Velocity(
|
||||
Math.random() * 100 - 50,
|
||||
Math.random() * 100 - 50
|
||||
));
|
||||
enemy.addComponent(new Health(50));
|
||||
}
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log('游戏场景启动');
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
console.log('游戏场景卸载');
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 设置初始场景
|
||||
Core.setScene(new MenuScene());
|
||||
|
||||
// 游戏循环
|
||||
let lastTime = 0;
|
||||
function gameLoop(currentTime: number) {
|
||||
const deltaTime = (currentTime - lastTime) / 1000;
|
||||
lastTime = currentTime;
|
||||
|
||||
// 只需要调用 Core.update,它会自动更新场景
|
||||
Core.update(deltaTime);
|
||||
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
requestAnimationFrame(gameLoop);
|
||||
|
||||
// 切换到游戏场景
|
||||
setTimeout(() => {
|
||||
Core.loadScene(new GameScene());
|
||||
}, 3000);
|
||||
```
|
||||
|
||||
SceneManager 为大多数游戏提供了简单而强大的场景管理能力。通过 Core 的静态方法,你可以轻松地管理场景切换。
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [持久化实体](./persistent-entity.md) - 了解如何让实体跨场景保持状态
|
||||
- [WorldManager](./world-manager.md) - 了解更高级的多世界隔离功能
|
||||
@@ -1,662 +0,0 @@
|
||||
# 场景管理
|
||||
|
||||
在 ECS 架构中,场景(Scene)是游戏世界的容器,负责管理实体、系统和组件的生命周期。场景提供了完整的 ECS 运行环境。
|
||||
|
||||
## 基本概念
|
||||
|
||||
场景是 ECS 框架的核心容器,提供:
|
||||
- 实体的创建、管理和销毁
|
||||
- 系统的注册和执行调度
|
||||
- 组件的存储和查询
|
||||
- 事件系统支持
|
||||
- 性能监控和调试信息
|
||||
|
||||
## 场景管理方式
|
||||
|
||||
ECS Framework 提供了两种场景管理方式:
|
||||
|
||||
1. **[SceneManager](./scene-manager.md)** - 适用于 95% 的游戏应用
|
||||
- 单人游戏、简单多人游戏、移动游戏
|
||||
- 轻量级,简单直观的 API
|
||||
- 支持场景切换
|
||||
|
||||
2. **[WorldManager](./world-manager.md)** - 适用于高级多世界隔离场景
|
||||
- MMO 游戏服务器、游戏房间系统
|
||||
- 多 World 管理,每个 World 可包含多个场景
|
||||
- 完全隔离的独立环境
|
||||
|
||||
本文档重点介绍 Scene 类本身的使用方法。关于场景管理器的详细信息,请查看对应的文档。
|
||||
|
||||
## 创建场景
|
||||
|
||||
### 继承 Scene 类
|
||||
|
||||
**推荐做法:继承 Scene 类来创建自定义场景**
|
||||
|
||||
```typescript
|
||||
import { Scene, EntitySystem } from '@esengine/ecs-framework';
|
||||
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 设置场景名称
|
||||
this.name = "GameScene";
|
||||
|
||||
// 添加系统
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
this.addSystem(new PhysicsSystem());
|
||||
|
||||
// 创建初始实体
|
||||
this.createInitialEntities();
|
||||
}
|
||||
|
||||
private createInitialEntities(): void {
|
||||
// 创建玩家
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Position(400, 300));
|
||||
player.addComponent(new Health(100));
|
||||
player.addComponent(new PlayerController());
|
||||
|
||||
// 创建敌人
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const enemy = this.createEntity(`Enemy_${i}`);
|
||||
enemy.addComponent(new Position(Math.random() * 800, Math.random() * 600));
|
||||
enemy.addComponent(new Health(50));
|
||||
enemy.addComponent(new EnemyAI());
|
||||
}
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log("游戏场景已启动");
|
||||
// 场景启动时的逻辑
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
console.log("游戏场景已卸载");
|
||||
// 场景卸载时的清理逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用场景配置
|
||||
|
||||
```typescript
|
||||
import { ISceneConfig } from '@esengine/ecs-framework';
|
||||
|
||||
const config: ISceneConfig = {
|
||||
name: "MainGame",
|
||||
enableEntityDirectUpdate: false
|
||||
};
|
||||
|
||||
class ConfiguredScene extends Scene {
|
||||
constructor() {
|
||||
super(config);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 场景生命周期
|
||||
|
||||
场景提供了完整的生命周期管理:
|
||||
|
||||
```typescript
|
||||
class ExampleScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 场景初始化:设置系统和初始实体
|
||||
console.log("场景初始化");
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
// 场景开始运行:游戏逻辑开始执行
|
||||
console.log("场景开始运行");
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// 场景卸载:清理资源
|
||||
console.log("场景卸载");
|
||||
}
|
||||
}
|
||||
|
||||
// 使用场景(由框架自动管理生命周期)
|
||||
const scene = new ExampleScene();
|
||||
// 场景的 initialize(), begin(), update(), end() 由框架自动调用
|
||||
```
|
||||
|
||||
**生命周期方法**:
|
||||
|
||||
1. `initialize()` - 场景初始化,设置系统和初始实体
|
||||
2. `begin()` / `onStart()` - 场景开始运行
|
||||
3. `update()` - 每帧更新(由场景管理器调用)
|
||||
4. `end()` / `unload()` - 场景卸载,清理资源
|
||||
|
||||
## 实体管理
|
||||
|
||||
### 创建实体
|
||||
|
||||
```typescript
|
||||
class EntityScene extends Scene {
|
||||
createGameEntities(): void {
|
||||
// 创建单个实体
|
||||
const player = this.createEntity("Player");
|
||||
|
||||
// 批量创建实体(高性能)
|
||||
const bullets = this.createEntities(100, "Bullet");
|
||||
|
||||
// 为批量创建的实体添加组件
|
||||
bullets.forEach((bullet, index) => {
|
||||
bullet.addComponent(new Position(index * 10, 100));
|
||||
bullet.addComponent(new Velocity(Math.random() * 200 - 100, -300));
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 查找实体
|
||||
|
||||
```typescript
|
||||
class SearchScene extends Scene {
|
||||
findEntities(): void {
|
||||
// 按名称查找
|
||||
const player = this.findEntity("Player");
|
||||
const player2 = this.getEntityByName("Player"); // 别名方法
|
||||
|
||||
// 按 ID 查找
|
||||
const entity = this.findEntityById(123);
|
||||
|
||||
// 按标签查找
|
||||
const enemies = this.findEntitiesByTag(2);
|
||||
const enemies2 = this.getEntitiesByTag(2); // 别名方法
|
||||
|
||||
if (player) {
|
||||
console.log(`找到玩家: ${player.name}`);
|
||||
}
|
||||
|
||||
console.log(`找到 ${enemies.length} 个敌人`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 销毁实体
|
||||
|
||||
```typescript
|
||||
class DestroyScene extends Scene {
|
||||
cleanupEntities(): void {
|
||||
// 销毁所有实体
|
||||
this.destroyAllEntities();
|
||||
|
||||
// 单个实体的销毁通过实体本身
|
||||
const enemy = this.findEntity("Enemy_1");
|
||||
if (enemy) {
|
||||
enemy.destroy(); // 实体会自动从场景中移除
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 系统管理
|
||||
|
||||
### 添加和移除系统
|
||||
|
||||
```typescript
|
||||
class SystemScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 添加系统
|
||||
const movementSystem = new MovementSystem();
|
||||
this.addSystem(movementSystem);
|
||||
|
||||
// 设置系统更新顺序
|
||||
movementSystem.updateOrder = 1;
|
||||
|
||||
// 添加更多系统
|
||||
this.addSystem(new PhysicsSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
}
|
||||
|
||||
public removeUnnecessarySystems(): void {
|
||||
// 获取系统
|
||||
const physicsSystem = this.getEntityProcessor(PhysicsSystem);
|
||||
|
||||
// 移除系统
|
||||
if (physicsSystem) {
|
||||
this.removeSystem(physicsSystem);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 系统访问
|
||||
|
||||
```typescript
|
||||
class SystemAccessScene extends Scene {
|
||||
public pausePhysics(): void {
|
||||
const physicsSystem = this.getEntityProcessor(PhysicsSystem);
|
||||
if (physicsSystem) {
|
||||
physicsSystem.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
public getAllSystems(): EntitySystem[] {
|
||||
return this.systems; // 获取所有系统
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 事件系统
|
||||
|
||||
场景内置了类型安全的事件系统:
|
||||
|
||||
```typescript
|
||||
class EventScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 监听事件
|
||||
this.eventSystem.on('player_died', this.onPlayerDied.bind(this));
|
||||
this.eventSystem.on('enemy_spawned', this.onEnemySpawned.bind(this));
|
||||
this.eventSystem.on('level_complete', this.onLevelComplete.bind(this));
|
||||
}
|
||||
|
||||
private onPlayerDied(data: any): void {
|
||||
console.log('玩家死亡事件');
|
||||
// 处理玩家死亡
|
||||
}
|
||||
|
||||
private onEnemySpawned(data: any): void {
|
||||
console.log('敌人生成事件');
|
||||
// 处理敌人生成
|
||||
}
|
||||
|
||||
private onLevelComplete(data: any): void {
|
||||
console.log('关卡完成事件');
|
||||
// 处理关卡完成
|
||||
}
|
||||
|
||||
public triggerGameEvent(): void {
|
||||
// 发送事件(同步)
|
||||
this.eventSystem.emitSync('custom_event', {
|
||||
message: "这是自定义事件",
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
// 发送事件(异步)
|
||||
this.eventSystem.emit('async_event', {
|
||||
data: "异步事件数据"
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 事件系统 API
|
||||
|
||||
```typescript
|
||||
// 监听事件
|
||||
this.eventSystem.on('event_name', callback);
|
||||
|
||||
// 监听一次(自动取消订阅)
|
||||
this.eventSystem.once('event_name', callback);
|
||||
|
||||
// 取消监听
|
||||
this.eventSystem.off('event_name', callback);
|
||||
|
||||
// 同步发送事件
|
||||
this.eventSystem.emitSync('event_name', data);
|
||||
|
||||
// 异步发送事件
|
||||
this.eventSystem.emit('event_name', data);
|
||||
|
||||
// 清除所有事件监听
|
||||
this.eventSystem.clear();
|
||||
```
|
||||
|
||||
## 场景统计和调试
|
||||
|
||||
### 获取场景统计
|
||||
|
||||
```typescript
|
||||
class StatsScene extends Scene {
|
||||
public showStats(): void {
|
||||
const stats = this.getStats();
|
||||
console.log(`实体数量: ${stats.entityCount}`);
|
||||
console.log(`系统数量: ${stats.processorCount}`);
|
||||
console.log('组件存储统计:', stats.componentStorageStats);
|
||||
}
|
||||
|
||||
public showDebugInfo(): void {
|
||||
const debugInfo = this.getDebugInfo();
|
||||
console.log('场景调试信息:', debugInfo);
|
||||
|
||||
// 显示所有实体信息
|
||||
debugInfo.entities.forEach(entity => {
|
||||
console.log(`实体 ${entity.name}(${entity.id}): ${entity.componentCount} 个组件`);
|
||||
console.log('组件类型:', entity.componentTypes);
|
||||
});
|
||||
|
||||
// 显示所有系统信息
|
||||
debugInfo.processors.forEach(processor => {
|
||||
console.log(`系统 ${processor.name}: 处理 ${processor.entityCount} 个实体`);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 组件查询
|
||||
|
||||
Scene 提供了强大的组件查询系统:
|
||||
|
||||
```typescript
|
||||
class QueryScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 创建一些实体
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const entity = this.createEntity(`Entity_${i}`);
|
||||
entity.addComponent(new Transform(i * 10, 0));
|
||||
entity.addComponent(new Velocity(1, 0));
|
||||
if (i % 2 === 0) {
|
||||
entity.addComponent(new Renderer());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public queryEntities(): void {
|
||||
// 通过 QuerySystem 查询
|
||||
const entities = this.querySystem.query([Transform, Velocity]);
|
||||
console.log(`找到 ${entities.length} 个有 Transform 和 Velocity 的实体`);
|
||||
|
||||
// 使用 ECS 流式 API(如果通过 SceneManager)
|
||||
// const api = sceneManager.api;
|
||||
// const entities = api?.find(Transform, Velocity);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 性能监控
|
||||
|
||||
Scene 内置了性能监控功能:
|
||||
|
||||
```typescript
|
||||
class PerformanceScene extends Scene {
|
||||
public showPerformance(): void {
|
||||
// 获取性能数据
|
||||
const perfData = this.performanceMonitor?.getPerformanceData();
|
||||
if (perfData) {
|
||||
console.log('FPS:', perfData.fps);
|
||||
console.log('帧时间:', perfData.frameTime);
|
||||
console.log('实体更新时间:', perfData.entityUpdateTime);
|
||||
console.log('系统更新时间:', perfData.systemUpdateTime);
|
||||
}
|
||||
|
||||
// 获取性能报告
|
||||
const report = this.performanceMonitor?.generateReport();
|
||||
if (report) {
|
||||
console.log('性能报告:', report);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 场景职责分离
|
||||
|
||||
```typescript
|
||||
// 好的场景设计 - 职责清晰
|
||||
class MenuScene extends Scene {
|
||||
// 只处理菜单相关逻辑
|
||||
}
|
||||
|
||||
class GameScene extends Scene {
|
||||
// 只处理游戏玩法逻辑
|
||||
}
|
||||
|
||||
class InventoryScene extends Scene {
|
||||
// 只处理物品栏逻辑
|
||||
}
|
||||
|
||||
// 避免的场景设计 - 职责混乱
|
||||
class MegaScene extends Scene {
|
||||
// 包含菜单、游戏、物品栏等所有逻辑
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 合理的系统组织
|
||||
|
||||
```typescript
|
||||
class OrganizedScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 按功能和依赖关系添加系统
|
||||
this.addInputSystems();
|
||||
this.addLogicSystems();
|
||||
this.addRenderSystems();
|
||||
}
|
||||
|
||||
private addInputSystems(): void {
|
||||
this.addSystem(new InputSystem());
|
||||
}
|
||||
|
||||
private addLogicSystems(): void {
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new PhysicsSystem());
|
||||
this.addSystem(new CollisionSystem());
|
||||
}
|
||||
|
||||
private addRenderSystems(): void {
|
||||
this.addSystem(new RenderSystem());
|
||||
this.addSystem(new UISystem());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 资源管理
|
||||
|
||||
```typescript
|
||||
class ResourceScene extends Scene {
|
||||
private textures: Map<string, any> = new Map();
|
||||
private sounds: Map<string, any> = new Map();
|
||||
|
||||
protected initialize(): void {
|
||||
this.loadResources();
|
||||
}
|
||||
|
||||
private loadResources(): void {
|
||||
// 加载场景所需资源
|
||||
this.textures.set('player', this.loadTexture('player.png'));
|
||||
this.sounds.set('bgm', this.loadSound('bgm.mp3'));
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// 清理资源
|
||||
this.textures.clear();
|
||||
this.sounds.clear();
|
||||
console.log('场景资源已清理');
|
||||
}
|
||||
|
||||
private loadTexture(path: string): any {
|
||||
// 加载纹理
|
||||
return null;
|
||||
}
|
||||
|
||||
private loadSound(path: string): any {
|
||||
// 加载音效
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 事件处理规范
|
||||
|
||||
```typescript
|
||||
class EventHandlingScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 集中管理事件监听
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
private setupEventListeners(): void {
|
||||
this.eventSystem.on('game_pause', this.onGamePause.bind(this));
|
||||
this.eventSystem.on('game_resume', this.onGameResume.bind(this));
|
||||
this.eventSystem.on('player_input', this.onPlayerInput.bind(this));
|
||||
}
|
||||
|
||||
private onGamePause(): void {
|
||||
// 暂停游戏逻辑
|
||||
this.systems.forEach(system => {
|
||||
if (system instanceof GameLogicSystem) {
|
||||
system.enabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private onGameResume(): void {
|
||||
// 恢复游戏逻辑
|
||||
this.systems.forEach(system => {
|
||||
if (system instanceof GameLogicSystem) {
|
||||
system.enabled = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private onPlayerInput(data: any): void {
|
||||
// 处理玩家输入
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// 清理事件监听
|
||||
this.eventSystem.clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 初始化顺序
|
||||
|
||||
```typescript
|
||||
class ProperInitScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 1. 首先设置场景配置
|
||||
this.name = "GameScene";
|
||||
|
||||
// 2. 然后添加系统(按依赖顺序)
|
||||
this.addSystem(new InputSystem());
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new PhysicsSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
|
||||
// 3. 最后创建实体
|
||||
this.createEntities();
|
||||
|
||||
// 4. 设置事件监听
|
||||
this.setupEvents();
|
||||
}
|
||||
|
||||
private createEntities(): void {
|
||||
// 创建实体
|
||||
}
|
||||
|
||||
private setupEvents(): void {
|
||||
// 设置事件监听
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 完整示例
|
||||
|
||||
```typescript
|
||||
import { Scene, EntitySystem, Entity, Matcher } from '@esengine/ecs-framework';
|
||||
|
||||
// 定义组件
|
||||
class Transform {
|
||||
constructor(public x: number, public y: number) {}
|
||||
}
|
||||
|
||||
class Velocity {
|
||||
constructor(public vx: number, public vy: number) {}
|
||||
}
|
||||
|
||||
class Health {
|
||||
constructor(public value: number) {}
|
||||
}
|
||||
|
||||
// 定义系统
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Transform, Velocity));
|
||||
}
|
||||
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const transform = entity.getComponent(Transform);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
if (transform && velocity) {
|
||||
transform.x += velocity.vx;
|
||||
transform.y += velocity.vy;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 定义场景
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "GameScene";
|
||||
|
||||
// 添加系统
|
||||
this.addSystem(new MovementSystem());
|
||||
|
||||
// 创建玩家
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Transform(400, 300));
|
||||
player.addComponent(new Velocity(0, 0));
|
||||
player.addComponent(new Health(100));
|
||||
|
||||
// 创建敌人
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const enemy = this.createEntity(`Enemy_${i}`);
|
||||
enemy.addComponent(new Transform(
|
||||
Math.random() * 800,
|
||||
Math.random() * 600
|
||||
));
|
||||
enemy.addComponent(new Velocity(
|
||||
Math.random() * 100 - 50,
|
||||
Math.random() * 100 - 50
|
||||
));
|
||||
enemy.addComponent(new Health(50));
|
||||
}
|
||||
|
||||
// 设置事件监听
|
||||
this.eventSystem.on('player_died', () => {
|
||||
console.log('玩家死亡!');
|
||||
});
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log('游戏场景启动');
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
console.log('游戏场景卸载');
|
||||
this.eventSystem.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// 使用场景
|
||||
// 方式1:通过 SceneManager(推荐)
|
||||
import { Core, SceneManager } from '@esengine/ecs-framework';
|
||||
|
||||
Core.create({ debug: true });
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
sceneManager.setScene(new GameScene());
|
||||
|
||||
// 方式2:通过 WorldManager(高级用例)
|
||||
import { WorldManager } from '@esengine/ecs-framework';
|
||||
|
||||
const worldManager = Core.services.resolve(WorldManager);
|
||||
const world = worldManager.createWorld('game');
|
||||
world.createScene('main', new GameScene());
|
||||
world.setSceneActive('main', true);
|
||||
```
|
||||
|
||||
## 下一步
|
||||
|
||||
- 了解 [SceneManager](./scene-manager.md) - 适用于大多数游戏的简单场景管理
|
||||
- 了解 [WorldManager](./world-manager.md) - 适用于需要多世界隔离的高级场景
|
||||
- 了解 [持久化实体](./persistent-entity.md) - 让实体跨场景保持状态(v2.3.0+)
|
||||
|
||||
场景是 ECS 框架的核心容器,正确使用场景管理能让你的游戏架构更加清晰、模块化和易于维护。
|
||||
@@ -1,923 +0,0 @@
|
||||
# 序列化系统
|
||||
|
||||
序列化系统提供了完整的场景、实体和组件数据持久化方案,支持全量序列化和增量序列化两种模式,适用于游戏存档、网络同步、场景编辑器、时间回溯等场景。
|
||||
|
||||
## 基本概念
|
||||
|
||||
序列化系统分为两个层次:
|
||||
|
||||
- **全量序列化**:序列化完整的场景状态,包括所有实体、组件和场景数据
|
||||
- **增量序列化**:只序列化相对于基础快照的变更部分,大幅减少数据量
|
||||
|
||||
### 支持的数据格式
|
||||
|
||||
- **JSON格式**:人类可读,便于调试和编辑
|
||||
- **Binary格式**:使用MessagePack,体积更小,性能更高
|
||||
|
||||
> **📢 v2.2.2 重要变更**
|
||||
>
|
||||
> 从 v2.2.2 开始,二进制序列化格式返回 `Uint8Array` 而非 Node.js 的 `Buffer`,以确保浏览器兼容性:
|
||||
> - `serialize({ format: 'binary' })` 返回 `string | Uint8Array`(原为 `string | Buffer`)
|
||||
> - `deserialize(data)` 接收 `string | Uint8Array`(原为 `string | Buffer`)
|
||||
> - `applyIncremental(data)` 接收 `IncrementalSnapshot | string | Uint8Array`(原为包含 `Buffer`)
|
||||
>
|
||||
> **迁移影响**:
|
||||
> - ✅ **运行时兼容**:Node.js 的 `Buffer` 继承自 `Uint8Array`,现有代码可直接运行
|
||||
> - ⚠️ **类型检查**:如果你的 TypeScript 代码中显式使用了 `Buffer` 类型,需要改为 `Uint8Array`
|
||||
> - ✅ **浏览器支持**:`Uint8Array` 是标准 JavaScript 类型,所有现代浏览器都支持
|
||||
|
||||
## 全量序列化
|
||||
|
||||
### 基础用法
|
||||
|
||||
#### 1. 标记可序列化组件
|
||||
|
||||
使用 `@Serializable` 和 `@Serialize` 装饰器标记需要序列化的组件和字段:
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent, Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSComponent('Player')
|
||||
@Serializable({ version: 1 })
|
||||
class PlayerComponent extends Component {
|
||||
@Serialize()
|
||||
public name: string = '';
|
||||
|
||||
@Serialize()
|
||||
public level: number = 1;
|
||||
|
||||
@Serialize()
|
||||
public experience: number = 0;
|
||||
|
||||
@Serialize()
|
||||
public position: { x: number; y: number } = { x: 0, y: 0 };
|
||||
|
||||
// 不使用 @Serialize() 的字段不会被序列化
|
||||
private tempData: any = null;
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 序列化场景
|
||||
|
||||
```typescript
|
||||
// JSON格式序列化
|
||||
const jsonData = scene.serialize({
|
||||
format: 'json',
|
||||
pretty: true // 美化输出
|
||||
});
|
||||
|
||||
// 保存到本地存储
|
||||
localStorage.setItem('gameSave', jsonData);
|
||||
|
||||
// Binary格式序列化(更小的体积)
|
||||
const binaryData = scene.serialize({
|
||||
format: 'binary'
|
||||
});
|
||||
|
||||
// 保存为文件(Node.js环境)
|
||||
// 注意:binaryData 是 Uint8Array 类型,Node.js 的 fs 可以直接写入
|
||||
fs.writeFileSync('save.bin', binaryData);
|
||||
```
|
||||
|
||||
#### 3. 反序列化场景
|
||||
|
||||
```typescript
|
||||
// 从JSON恢复
|
||||
const saveData = localStorage.getItem('gameSave');
|
||||
if (saveData) {
|
||||
scene.deserialize(saveData, {
|
||||
strategy: 'replace' // 替换当前场景内容
|
||||
});
|
||||
}
|
||||
|
||||
// 从Binary恢复
|
||||
const binaryData = fs.readFileSync('save.bin');
|
||||
scene.deserialize(binaryData, {
|
||||
strategy: 'merge' // 合并到现有场景
|
||||
});
|
||||
```
|
||||
|
||||
### 序列化选项
|
||||
|
||||
#### SerializationOptions
|
||||
|
||||
```typescript
|
||||
interface SceneSerializationOptions {
|
||||
// 指定要序列化的组件类型(可选)
|
||||
components?: ComponentType[];
|
||||
|
||||
// 序列化格式:'json' 或 'binary'
|
||||
format?: 'json' | 'binary';
|
||||
|
||||
// JSON美化输出
|
||||
pretty?: boolean;
|
||||
|
||||
// 包含元数据
|
||||
includeMetadata?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
示例:
|
||||
|
||||
```typescript
|
||||
// 只序列化特定组件类型
|
||||
const saveData = scene.serialize({
|
||||
format: 'json',
|
||||
components: [PlayerComponent, InventoryComponent],
|
||||
pretty: true,
|
||||
includeMetadata: true
|
||||
});
|
||||
```
|
||||
|
||||
#### DeserializationOptions
|
||||
|
||||
```typescript
|
||||
interface SceneDeserializationOptions {
|
||||
// 反序列化策略
|
||||
strategy?: 'merge' | 'replace';
|
||||
|
||||
// 组件类型注册表(可选,默认使用全局注册表)
|
||||
componentRegistry?: Map<string, ComponentType>;
|
||||
}
|
||||
```
|
||||
|
||||
### 高级装饰器
|
||||
|
||||
#### 字段序列化选项
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Advanced')
|
||||
@Serializable({ version: 1 })
|
||||
class AdvancedComponent extends Component {
|
||||
// 使用别名
|
||||
@Serialize({ alias: 'playerName' })
|
||||
public name: string = '';
|
||||
|
||||
// 自定义序列化器
|
||||
@Serialize({
|
||||
serializer: (value: Date) => value.toISOString(),
|
||||
deserializer: (value: string) => new Date(value)
|
||||
})
|
||||
public createdAt: Date = new Date();
|
||||
|
||||
// 忽略序列化
|
||||
@IgnoreSerialization()
|
||||
public cachedData: any = null;
|
||||
}
|
||||
```
|
||||
|
||||
#### 集合类型序列化
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Collections')
|
||||
@Serializable({ version: 1 })
|
||||
class CollectionsComponent extends Component {
|
||||
// Map序列化
|
||||
@SerializeAsMap()
|
||||
public inventory: Map<string, number> = new Map();
|
||||
|
||||
// Set序列化
|
||||
@SerializeAsSet()
|
||||
public acquiredSkills: Set<string> = new Set();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.inventory.set('gold', 100);
|
||||
this.inventory.set('silver', 50);
|
||||
this.acquiredSkills.add('attack');
|
||||
this.acquiredSkills.add('defense');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 组件继承与序列化
|
||||
|
||||
框架完整支持组件类的继承,子类会自动继承父类的序列化字段,同时可以添加自己的字段。
|
||||
|
||||
#### 基础继承
|
||||
|
||||
```typescript
|
||||
// 基类组件
|
||||
@ECSComponent('Collider2DBase')
|
||||
@Serializable({ version: 1, typeId: 'Collider2DBase' })
|
||||
abstract class Collider2DBase extends Component {
|
||||
@Serialize()
|
||||
public friction: number = 0.5;
|
||||
|
||||
@Serialize()
|
||||
public restitution: number = 0.0;
|
||||
|
||||
@Serialize()
|
||||
public isTrigger: boolean = false;
|
||||
}
|
||||
|
||||
// 子类组件 - 自动继承父类的序列化字段
|
||||
@ECSComponent('BoxCollider2D')
|
||||
@Serializable({ version: 1, typeId: 'BoxCollider2D' })
|
||||
class BoxCollider2DComponent extends Collider2DBase {
|
||||
@Serialize()
|
||||
public width: number = 1.0;
|
||||
|
||||
@Serialize()
|
||||
public height: number = 1.0;
|
||||
}
|
||||
|
||||
// 另一个子类组件
|
||||
@ECSComponent('CircleCollider2D')
|
||||
@Serializable({ version: 1, typeId: 'CircleCollider2D' })
|
||||
class CircleCollider2DComponent extends Collider2DBase {
|
||||
@Serialize()
|
||||
public radius: number = 0.5;
|
||||
}
|
||||
```
|
||||
|
||||
#### 继承规则
|
||||
|
||||
1. **字段继承**:子类自动继承父类所有被 `@Serialize()` 标记的字段
|
||||
2. **独立元数据**:每个子类维护独立的序列化元数据,修改子类不会影响父类或其他子类
|
||||
3. **typeId 区分**:使用 `typeId` 选项为每个类指定唯一标识,确保反序列化时能正确识别组件类型
|
||||
|
||||
#### 使用 typeId 的重要性
|
||||
|
||||
当使用组件继承时,**强烈建议**为每个类设置唯一的 `typeId`:
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:明确指定 typeId
|
||||
@Serializable({ version: 1, typeId: 'BoxCollider2D' })
|
||||
class BoxCollider2DComponent extends Collider2DBase { }
|
||||
|
||||
@Serializable({ version: 1, typeId: 'CircleCollider2D' })
|
||||
class CircleCollider2DComponent extends Collider2DBase { }
|
||||
|
||||
// ⚠️ 不推荐:依赖类名作为 typeId
|
||||
// 在代码压缩后类名可能变化,导致反序列化失败
|
||||
@Serializable({ version: 1 })
|
||||
class BoxCollider2DComponent extends Collider2DBase { }
|
||||
```
|
||||
|
||||
#### 子类覆盖父类字段
|
||||
|
||||
子类可以重新声明父类的字段以修改其序列化选项:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('SpecialCollider')
|
||||
@Serializable({ version: 1, typeId: 'SpecialCollider' })
|
||||
class SpecialColliderComponent extends Collider2DBase {
|
||||
// 覆盖父类字段,使用不同的别名
|
||||
@Serialize({ alias: 'fric' })
|
||||
public override friction: number = 0.8;
|
||||
|
||||
@Serialize()
|
||||
public specialProperty: string = '';
|
||||
}
|
||||
```
|
||||
|
||||
#### 忽略继承的字段
|
||||
|
||||
使用 `@IgnoreSerialization()` 可以在子类中忽略从父类继承的字段:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('TriggerOnly')
|
||||
@Serializable({ version: 1, typeId: 'TriggerOnly' })
|
||||
class TriggerOnlyCollider extends Collider2DBase {
|
||||
// 忽略父类的 friction 和 restitution 字段
|
||||
// 因为 Trigger 不需要物理材质属性
|
||||
@IgnoreSerialization()
|
||||
public override friction: number = 0;
|
||||
|
||||
@IgnoreSerialization()
|
||||
public override restitution: number = 0;
|
||||
}
|
||||
```
|
||||
|
||||
### 场景自定义数据
|
||||
|
||||
除了实体和组件,还可以序列化场景级别的配置数据:
|
||||
|
||||
```typescript
|
||||
// 设置场景数据
|
||||
scene.sceneData.set('weather', 'rainy');
|
||||
scene.sceneData.set('difficulty', 'hard');
|
||||
scene.sceneData.set('checkpoint', { x: 100, y: 200 });
|
||||
|
||||
// 序列化时会自动包含场景数据
|
||||
const saveData = scene.serialize({ format: 'json' });
|
||||
|
||||
// 反序列化后场景数据会恢复
|
||||
scene.deserialize(saveData);
|
||||
console.log(scene.sceneData.get('weather')); // 'rainy'
|
||||
```
|
||||
|
||||
## 增量序列化
|
||||
|
||||
增量序列化只保存场景的变更部分,适用于网络同步、撤销/重做、时间回溯等需要频繁保存状态的场景。
|
||||
|
||||
### 基础用法
|
||||
|
||||
#### 1. 创建基础快照
|
||||
|
||||
```typescript
|
||||
// 在需要开始记录变更前创建基础快照
|
||||
scene.createIncrementalSnapshot();
|
||||
```
|
||||
|
||||
#### 2. 修改场景
|
||||
|
||||
```typescript
|
||||
// 添加实体
|
||||
const enemy = scene.createEntity('Enemy');
|
||||
enemy.addComponent(new PositionComponent(100, 200));
|
||||
enemy.addComponent(new HealthComponent(50));
|
||||
|
||||
// 修改组件
|
||||
const player = scene.findEntity('Player');
|
||||
const pos = player.getComponent(PositionComponent);
|
||||
pos.x = 300;
|
||||
pos.y = 400;
|
||||
|
||||
// 删除组件
|
||||
player.removeComponentByType(BuffComponent);
|
||||
|
||||
// 删除实体
|
||||
const oldEntity = scene.findEntity('ToDelete');
|
||||
oldEntity.destroy();
|
||||
|
||||
// 修改场景数据
|
||||
scene.sceneData.set('score', 1000);
|
||||
```
|
||||
|
||||
#### 3. 获取增量变更
|
||||
|
||||
```typescript
|
||||
// 获取相对于基础快照的所有变更
|
||||
const incremental = scene.serializeIncremental();
|
||||
|
||||
// 查看变更统计
|
||||
const stats = IncrementalSerializer.getIncrementalStats(incremental);
|
||||
console.log('总变更数:', stats.totalChanges);
|
||||
console.log('新增实体:', stats.addedEntities);
|
||||
console.log('删除实体:', stats.removedEntities);
|
||||
console.log('新增组件:', stats.addedComponents);
|
||||
console.log('更新组件:', stats.updatedComponents);
|
||||
```
|
||||
|
||||
#### 4. 序列化增量数据
|
||||
|
||||
```typescript
|
||||
// JSON格式(默认)
|
||||
const jsonData = IncrementalSerializer.serializeIncremental(incremental, {
|
||||
format: 'json'
|
||||
});
|
||||
|
||||
// 二进制格式(更小的体积,更高性能)
|
||||
const binaryData = IncrementalSerializer.serializeIncremental(incremental, {
|
||||
format: 'binary'
|
||||
});
|
||||
|
||||
// 美化JSON输出(便于调试)
|
||||
const prettyJson = IncrementalSerializer.serializeIncremental(incremental, {
|
||||
format: 'json',
|
||||
pretty: true
|
||||
});
|
||||
|
||||
// 发送或保存
|
||||
socket.send(binaryData); // 网络传输使用二进制
|
||||
localStorage.setItem('changes', jsonData); // 本地存储可用JSON
|
||||
```
|
||||
|
||||
#### 5. 应用增量变更
|
||||
|
||||
```typescript
|
||||
// 在另一个场景应用变更
|
||||
const otherScene = new Scene();
|
||||
|
||||
// 直接应用增量对象
|
||||
otherScene.applyIncremental(incremental);
|
||||
|
||||
// 从JSON字符串应用
|
||||
const jsonData = IncrementalSerializer.serializeIncremental(incremental, { format: 'json' });
|
||||
otherScene.applyIncremental(jsonData);
|
||||
|
||||
// 从二进制Uint8Array应用
|
||||
const binaryData = IncrementalSerializer.serializeIncremental(incremental, { format: 'binary' });
|
||||
otherScene.applyIncremental(binaryData);
|
||||
```
|
||||
|
||||
### 增量快照管理
|
||||
|
||||
#### 更新快照基准
|
||||
|
||||
在应用增量变更后,可以更新快照基准:
|
||||
|
||||
```typescript
|
||||
// 创建初始快照
|
||||
scene.createIncrementalSnapshot();
|
||||
|
||||
// 第一次修改
|
||||
entity.addComponent(new VelocityComponent(5, 0));
|
||||
const incremental1 = scene.serializeIncremental();
|
||||
|
||||
// 更新基准(将当前状态设为新的基准)
|
||||
scene.updateIncrementalSnapshot();
|
||||
|
||||
// 第二次修改(增量将基于更新后的基准)
|
||||
entity.getComponent(VelocityComponent).dx = 10;
|
||||
const incremental2 = scene.serializeIncremental();
|
||||
```
|
||||
|
||||
#### 清除快照
|
||||
|
||||
```typescript
|
||||
// 释放快照占用的内存
|
||||
scene.clearIncrementalSnapshot();
|
||||
|
||||
// 检查是否有快照
|
||||
if (scene.hasIncrementalSnapshot()) {
|
||||
console.log('存在增量快照');
|
||||
}
|
||||
```
|
||||
|
||||
### 增量序列化选项
|
||||
|
||||
```typescript
|
||||
interface IncrementalSerializationOptions {
|
||||
// 是否进行组件数据的深度对比
|
||||
// 默认true,设为false可提升性能但可能漏掉组件内部字段变更
|
||||
deepComponentComparison?: boolean;
|
||||
|
||||
// 是否跟踪场景数据变更
|
||||
// 默认true
|
||||
trackSceneData?: boolean;
|
||||
|
||||
// 是否压缩快照(使用JSON序列化)
|
||||
// 默认false
|
||||
compressSnapshot?: boolean;
|
||||
|
||||
// 序列化格式
|
||||
// 'json': JSON格式(可读性好,方便调试)
|
||||
// 'binary': MessagePack二进制格式(体积小,性能高)
|
||||
// 默认 'json'
|
||||
format?: 'json' | 'binary';
|
||||
|
||||
// 是否美化JSON输出(仅在format='json'时有效)
|
||||
// 默认false
|
||||
pretty?: boolean;
|
||||
}
|
||||
|
||||
// 使用选项
|
||||
scene.createIncrementalSnapshot({
|
||||
deepComponentComparison: true,
|
||||
trackSceneData: true
|
||||
});
|
||||
```
|
||||
|
||||
### 增量数据结构
|
||||
|
||||
增量快照包含以下变更类型:
|
||||
|
||||
```typescript
|
||||
interface IncrementalSnapshot {
|
||||
version: number; // 快照版本号
|
||||
timestamp: number; // 时间戳
|
||||
sceneName: string; // 场景名称
|
||||
baseVersion: number; // 基础版本号
|
||||
entityChanges: EntityChange[]; // 实体变更
|
||||
componentChanges: ComponentChange[]; // 组件变更
|
||||
sceneDataChanges: SceneDataChange[]; // 场景数据变更
|
||||
}
|
||||
|
||||
// 变更操作类型
|
||||
enum ChangeOperation {
|
||||
EntityAdded = 'entity_added',
|
||||
EntityRemoved = 'entity_removed',
|
||||
EntityUpdated = 'entity_updated',
|
||||
ComponentAdded = 'component_added',
|
||||
ComponentRemoved = 'component_removed',
|
||||
ComponentUpdated = 'component_updated',
|
||||
SceneDataUpdated = 'scene_data_updated'
|
||||
}
|
||||
```
|
||||
|
||||
## 版本迁移
|
||||
|
||||
当组件结构发生变化时,版本迁移系统可以自动升级旧版本的存档数据。
|
||||
|
||||
### 注册迁移函数
|
||||
|
||||
```typescript
|
||||
import { VersionMigrationManager } from '@esengine/ecs-framework';
|
||||
|
||||
// 假设 PlayerComponent v1 有 hp 字段
|
||||
// v2 改为 health 和 maxHealth 字段
|
||||
|
||||
// 注册从版本1到版本2的迁移
|
||||
VersionMigrationManager.registerComponentMigration(
|
||||
'Player',
|
||||
1, // 从版本
|
||||
2, // 到版本
|
||||
(data) => {
|
||||
// 迁移逻辑
|
||||
const newData = {
|
||||
...data,
|
||||
health: data.hp,
|
||||
maxHealth: data.hp,
|
||||
};
|
||||
delete newData.hp;
|
||||
return newData;
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### 使用迁移构建器
|
||||
|
||||
```typescript
|
||||
import { MigrationBuilder } from '@esengine/ecs-framework';
|
||||
|
||||
new MigrationBuilder()
|
||||
.forComponent('Player')
|
||||
.fromVersionToVersion(2, 3)
|
||||
.migrate((data) => {
|
||||
// 从版本2迁移到版本3
|
||||
data.experience = data.exp || 0;
|
||||
delete data.exp;
|
||||
return data;
|
||||
});
|
||||
```
|
||||
|
||||
### 场景级迁移
|
||||
|
||||
```typescript
|
||||
// 注册场景级迁移
|
||||
VersionMigrationManager.registerSceneMigration(
|
||||
1, // 从版本
|
||||
2, // 到版本
|
||||
(scene) => {
|
||||
// 迁移场景结构
|
||||
scene.metadata = {
|
||||
...scene.metadata,
|
||||
migratedFrom: 1
|
||||
};
|
||||
return scene;
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### 检查迁移路径
|
||||
|
||||
```typescript
|
||||
// 检查是否可以迁移
|
||||
const canMigrate = VersionMigrationManager.canMigrateComponent(
|
||||
'Player',
|
||||
1, // 从版本
|
||||
3 // 到版本
|
||||
);
|
||||
|
||||
if (canMigrate) {
|
||||
// 可以安全迁移
|
||||
scene.deserialize(oldSaveData);
|
||||
}
|
||||
|
||||
// 获取迁移路径
|
||||
const path = VersionMigrationManager.getComponentMigrationPath('Player');
|
||||
console.log('可用迁移版本:', path); // [1, 2, 3]
|
||||
```
|
||||
|
||||
## 使用场景
|
||||
|
||||
### 游戏存档系统
|
||||
|
||||
```typescript
|
||||
class SaveSystem {
|
||||
private static SAVE_KEY = 'game_save';
|
||||
|
||||
// 保存游戏
|
||||
public static saveGame(scene: Scene): void {
|
||||
const saveData = scene.serialize({
|
||||
format: 'json',
|
||||
pretty: false
|
||||
});
|
||||
|
||||
localStorage.setItem(this.SAVE_KEY, saveData);
|
||||
console.log('游戏已保存');
|
||||
}
|
||||
|
||||
// 加载游戏
|
||||
public static loadGame(scene: Scene): boolean {
|
||||
const saveData = localStorage.getItem(this.SAVE_KEY);
|
||||
if (saveData) {
|
||||
scene.deserialize(saveData, {
|
||||
strategy: 'replace'
|
||||
});
|
||||
console.log('游戏已加载');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否有存档
|
||||
public static hasSave(): boolean {
|
||||
return localStorage.getItem(this.SAVE_KEY) !== null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 网络同步
|
||||
|
||||
```typescript
|
||||
class NetworkSync {
|
||||
private baseSnapshot?: any;
|
||||
private syncInterval: number = 100; // 100ms同步一次
|
||||
|
||||
constructor(private scene: Scene, private socket: WebSocket) {
|
||||
this.setupSync();
|
||||
}
|
||||
|
||||
private setupSync(): void {
|
||||
// 创建基础快照
|
||||
this.scene.createIncrementalSnapshot();
|
||||
|
||||
// 定期发送增量
|
||||
setInterval(() => {
|
||||
this.sendIncremental();
|
||||
}, this.syncInterval);
|
||||
|
||||
// 接收远程增量
|
||||
this.socket.onmessage = (event) => {
|
||||
this.receiveIncremental(event.data);
|
||||
};
|
||||
}
|
||||
|
||||
private sendIncremental(): void {
|
||||
const incremental = this.scene.serializeIncremental();
|
||||
const stats = IncrementalSerializer.getIncrementalStats(incremental);
|
||||
|
||||
// 只在有变更时发送
|
||||
if (stats.totalChanges > 0) {
|
||||
// 使用二进制格式减少网络传输量
|
||||
const binaryData = IncrementalSerializer.serializeIncremental(incremental, {
|
||||
format: 'binary'
|
||||
});
|
||||
this.socket.send(binaryData);
|
||||
|
||||
// 更新基准
|
||||
this.scene.updateIncrementalSnapshot();
|
||||
}
|
||||
}
|
||||
|
||||
private receiveIncremental(data: ArrayBuffer): void {
|
||||
// 直接应用二进制数据(ArrayBuffer 转 Uint8Array)
|
||||
const uint8Array = new Uint8Array(data);
|
||||
this.scene.applyIncremental(uint8Array);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 撤销/重做系统
|
||||
|
||||
```typescript
|
||||
class UndoRedoSystem {
|
||||
private history: IncrementalSnapshot[] = [];
|
||||
private currentIndex: number = -1;
|
||||
private maxHistory: number = 50;
|
||||
|
||||
constructor(private scene: Scene) {
|
||||
// 创建初始快照
|
||||
this.scene.createIncrementalSnapshot();
|
||||
this.saveState('Initial');
|
||||
}
|
||||
|
||||
// 保存当前状态
|
||||
public saveState(label: string): void {
|
||||
const incremental = this.scene.serializeIncremental();
|
||||
|
||||
// 删除当前位置之后的历史
|
||||
this.history = this.history.slice(0, this.currentIndex + 1);
|
||||
|
||||
// 添加新状态
|
||||
this.history.push(incremental);
|
||||
this.currentIndex++;
|
||||
|
||||
// 限制历史记录数量
|
||||
if (this.history.length > this.maxHistory) {
|
||||
this.history.shift();
|
||||
this.currentIndex--;
|
||||
}
|
||||
|
||||
// 更新快照基准
|
||||
this.scene.updateIncrementalSnapshot();
|
||||
}
|
||||
|
||||
// 撤销
|
||||
public undo(): boolean {
|
||||
if (this.currentIndex > 0) {
|
||||
this.currentIndex--;
|
||||
const incremental = this.history[this.currentIndex];
|
||||
this.scene.applyIncremental(incremental);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 重做
|
||||
public redo(): boolean {
|
||||
if (this.currentIndex < this.history.length - 1) {
|
||||
this.currentIndex++;
|
||||
const incremental = this.history[this.currentIndex];
|
||||
this.scene.applyIncremental(incremental);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public canUndo(): boolean {
|
||||
return this.currentIndex > 0;
|
||||
}
|
||||
|
||||
public canRedo(): boolean {
|
||||
return this.currentIndex < this.history.length - 1;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 关卡编辑器
|
||||
|
||||
```typescript
|
||||
class LevelEditor {
|
||||
// 导出关卡
|
||||
public exportLevel(scene: Scene, filename: string): void {
|
||||
const levelData = scene.serialize({
|
||||
format: 'json',
|
||||
pretty: true,
|
||||
includeMetadata: true
|
||||
});
|
||||
|
||||
// 浏览器环境
|
||||
const blob = new Blob([levelData], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// 导入关卡
|
||||
public importLevel(scene: Scene, fileContent: string): void {
|
||||
scene.deserialize(fileContent, {
|
||||
strategy: 'replace'
|
||||
});
|
||||
}
|
||||
|
||||
// 验证关卡数据
|
||||
public validateLevel(saveData: string): boolean {
|
||||
const validation = SceneSerializer.validate(saveData);
|
||||
if (!validation.valid) {
|
||||
console.error('关卡数据无效:', validation.errors);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 获取关卡信息(不完全反序列化)
|
||||
public getLevelInfo(saveData: string): any {
|
||||
const info = SceneSerializer.getInfo(saveData);
|
||||
return info;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 性能优化建议
|
||||
|
||||
### 1. 选择合适的格式
|
||||
|
||||
- **开发阶段**:使用JSON格式,便于调试和查看
|
||||
- **生产环境**:使用Binary格式,减少30-50%的数据大小
|
||||
|
||||
### 2. 按需序列化
|
||||
|
||||
```typescript
|
||||
// 只序列化需要持久化的组件
|
||||
const saveData = scene.serialize({
|
||||
format: 'binary',
|
||||
components: [PlayerComponent, InventoryComponent, QuestComponent]
|
||||
});
|
||||
```
|
||||
|
||||
### 3. 增量序列化优化
|
||||
|
||||
```typescript
|
||||
// 对于高频同步,关闭深度对比以提升性能
|
||||
scene.createIncrementalSnapshot({
|
||||
deepComponentComparison: false // 只检测组件的添加/删除
|
||||
});
|
||||
```
|
||||
|
||||
### 4. 批量操作
|
||||
|
||||
```typescript
|
||||
// 批量修改后再序列化
|
||||
scene.entities.buffer.forEach(entity => {
|
||||
// 批量修改
|
||||
});
|
||||
|
||||
// 一次性序列化所有变更
|
||||
const incremental = scene.serializeIncremental();
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 明确序列化字段
|
||||
|
||||
```typescript
|
||||
// 明确标记需要序列化的字段
|
||||
@ECSComponent('Player')
|
||||
@Serializable({ version: 1 })
|
||||
class PlayerComponent extends Component {
|
||||
@Serialize()
|
||||
public name: string = '';
|
||||
|
||||
@Serialize()
|
||||
public level: number = 1;
|
||||
|
||||
// 运行时数据不序列化
|
||||
private _cachedSprite: any = null;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 使用版本控制
|
||||
|
||||
```typescript
|
||||
// 为组件指定版本
|
||||
@Serializable({ version: 2 })
|
||||
class PlayerComponent extends Component {
|
||||
// 版本2的字段
|
||||
}
|
||||
|
||||
// 注册迁移函数确保兼容性
|
||||
VersionMigrationManager.registerComponentMigration('Player', 1, 2, migrateV1ToV2);
|
||||
```
|
||||
|
||||
### 3. 避免循环引用
|
||||
|
||||
```typescript
|
||||
// 不要在组件中直接引用其他实体
|
||||
@ECSComponent('Follower')
|
||||
@Serializable({ version: 1 })
|
||||
class FollowerComponent extends Component {
|
||||
// 存储实体ID而不是实体引用
|
||||
@Serialize()
|
||||
public targetId: number = 0;
|
||||
|
||||
// 通过场景查找目标实体
|
||||
public getTarget(scene: Scene): Entity | null {
|
||||
return scene.entities.findEntityById(this.targetId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 压缩大数据
|
||||
|
||||
```typescript
|
||||
// 对于大型数据结构,使用自定义序列化
|
||||
@ECSComponent('LargeData')
|
||||
@Serializable({ version: 1 })
|
||||
class LargeDataComponent extends Component {
|
||||
@Serialize({
|
||||
serializer: (data: LargeObject) => compressData(data),
|
||||
deserializer: (data: CompressedData) => decompressData(data)
|
||||
})
|
||||
public data: LargeObject;
|
||||
}
|
||||
```
|
||||
|
||||
## API参考
|
||||
|
||||
### 全量序列化API
|
||||
|
||||
- [`Scene.serialize()`](/api/classes/Scene#serialize) - 序列化场景
|
||||
- [`Scene.deserialize()`](/api/classes/Scene#deserialize) - 反序列化场景
|
||||
- [`SceneSerializer`](/api/classes/SceneSerializer) - 场景序列化器
|
||||
- [`ComponentSerializer`](/api/classes/ComponentSerializer) - 组件序列化器
|
||||
|
||||
### 增量序列化API
|
||||
|
||||
- [`Scene.createIncrementalSnapshot()`](/api/classes/Scene#createincrementalsnapshot) - 创建基础快照
|
||||
- [`Scene.serializeIncremental()`](/api/classes/Scene#serializeincremental) - 获取增量变更
|
||||
- [`Scene.applyIncremental()`](/api/classes/Scene#applyincremental) - 应用增量变更(支持IncrementalSnapshot对象、JSON字符串或二进制Uint8Array)
|
||||
- [`Scene.updateIncrementalSnapshot()`](/api/classes/Scene#updateincrementalsnapshot) - 更新快照基准
|
||||
- [`Scene.clearIncrementalSnapshot()`](/api/classes/Scene#clearincrementalsnapshot) - 清除快照
|
||||
- [`Scene.hasIncrementalSnapshot()`](/api/classes/Scene#hasincrementalsnapshot) - 检查是否有快照
|
||||
- [`IncrementalSerializer`](/api/classes/IncrementalSerializer) - 增量序列化器
|
||||
- [`IncrementalSnapshot`](/api/interfaces/IncrementalSnapshot) - 增量快照接口
|
||||
- [`IncrementalSerializationOptions`](/api/interfaces/IncrementalSerializationOptions) - 增量序列化选项
|
||||
- [`IncrementalSerializationFormat`](/api/type-aliases/IncrementalSerializationFormat) - 序列化格式类型
|
||||
|
||||
### 版本迁移API
|
||||
|
||||
- [`VersionMigrationManager`](/api/classes/VersionMigrationManager) - 版本迁移管理器
|
||||
- `VersionMigrationManager.registerComponentMigration()` - 注册组件迁移
|
||||
- `VersionMigrationManager.registerSceneMigration()` - 注册场景迁移
|
||||
- `VersionMigrationManager.canMigrateComponent()` - 检查是否可以迁移
|
||||
- `VersionMigrationManager.getComponentMigrationPath()` - 获取迁移路径
|
||||
|
||||
序列化系统是构建完整游戏的重要基础设施,合理使用可以实现强大的功能,如存档系统、网络同步、关卡编辑器等。
|
||||
@@ -1,828 +0,0 @@
|
||||
# 服务容器
|
||||
|
||||
服务容器(ServiceContainer)是 ECS Framework 的依赖注入容器,负责管理框架中所有服务的注册、解析和生命周期。通过服务容器,你可以实现松耦合的架构设计,提高代码的可测试性和可维护性。
|
||||
|
||||
## 概述
|
||||
|
||||
### 什么是服务容器
|
||||
|
||||
服务容器是一个轻量级的依赖注入(DI)容器,它提供了:
|
||||
|
||||
- **服务注册**: 将服务类型注册到容器中
|
||||
- **服务解析**: 从容器中获取服务实例
|
||||
- **生命周期管理**: 自动管理服务实例的创建和销毁
|
||||
- **依赖注入**: 自动解析服务之间的依赖关系
|
||||
|
||||
### 核心概念
|
||||
|
||||
#### 服务(Service)
|
||||
|
||||
服务是实现了 `IService` 接口的类,必须提供 `dispose()` 方法用于资源清理:
|
||||
|
||||
```typescript
|
||||
import { IService } from '@esengine/ecs-framework';
|
||||
|
||||
class MyService implements IService {
|
||||
constructor() {
|
||||
// 初始化逻辑
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
// 清理资源
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 服务标识符(ServiceIdentifier)
|
||||
|
||||
服务标识符用于在容器中唯一标识一个服务,支持两种类型:
|
||||
|
||||
- **类构造函数**: 直接使用服务类作为标识符,适用于具体实现类
|
||||
- **Symbol**: 使用 Symbol 作为标识符,适用于接口抽象(推荐用于插件和跨包场景)
|
||||
|
||||
```typescript
|
||||
// 方式1: 使用类作为标识符
|
||||
Core.services.registerSingleton(DataService);
|
||||
const data = Core.services.resolve(DataService);
|
||||
|
||||
// 方式2: 使用 Symbol 作为标识符(推荐用于接口)
|
||||
const IFileSystem = Symbol.for('IFileSystem');
|
||||
Core.services.registerInstance(IFileSystem, new TauriFileSystem());
|
||||
const fs = Core.services.resolve<IFileSystem>(IFileSystem);
|
||||
```
|
||||
|
||||
> **提示**: 使用 `Symbol.for()` 而非 `Symbol()` 可确保跨包/跨模块共享同一个标识符。详见[高级用法 - 接口与 Symbol 标识符模式](#接口与-symbol-标识符模式)。
|
||||
|
||||
#### 生命周期
|
||||
|
||||
服务容器支持两种生命周期:
|
||||
|
||||
- **Singleton(单例)**: 整个应用程序生命周期内只有一个实例,所有解析请求返回同一个实例
|
||||
- **Transient(瞬时)**: 每次解析都创建新的实例
|
||||
|
||||
## 基础使用
|
||||
|
||||
### 访问服务容器
|
||||
|
||||
ECS Framework 提供了三级服务容器:
|
||||
|
||||
> **版本说明**:World 服务容器功能在 v2.2.13+ 版本中可用
|
||||
|
||||
#### Core 级别服务容器
|
||||
|
||||
应用程序全局服务容器,可以通过 `Core.services` 访问:
|
||||
|
||||
```typescript
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始化Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 访问全局服务容器
|
||||
const container = Core.services;
|
||||
```
|
||||
|
||||
#### World 级别服务容器
|
||||
|
||||
每个 World 拥有独立的服务容器,用于管理 World 范围内的服务:
|
||||
|
||||
```typescript
|
||||
import { World } from '@esengine/ecs-framework';
|
||||
|
||||
// 创建 World
|
||||
const world = new World({ name: 'GameWorld' });
|
||||
|
||||
// 访问 World 级别的服务容器
|
||||
const worldContainer = world.services;
|
||||
|
||||
// 注册 World 级别的服务
|
||||
world.services.registerSingleton(RoomManager);
|
||||
```
|
||||
|
||||
#### Scene 级别服务容器
|
||||
|
||||
每个 Scene 拥有独立的服务容器,用于管理 Scene 范围内的服务:
|
||||
|
||||
```typescript
|
||||
// 访问 Scene 级别的服务容器
|
||||
const sceneContainer = scene.services;
|
||||
|
||||
// 注册 Scene 级别的服务
|
||||
scene.services.registerSingleton(PhysicsSystem);
|
||||
```
|
||||
|
||||
#### 服务容器层级
|
||||
|
||||
```
|
||||
Core.services (应用程序全局)
|
||||
└─ World.services (World 级别)
|
||||
└─ Scene.services (Scene 级别)
|
||||
```
|
||||
|
||||
不同级别的服务容器是独立的,服务不会自动向上或向下查找。选择合适的容器级别:
|
||||
|
||||
- **Core.services**: 应用程序级别的全局服务(配置、插件管理器等)
|
||||
- **World.services**: World 级别的服务(房间管理器、多人游戏状态等)
|
||||
- **Scene.services**: Scene 级别的服务(ECS 系统、场景特定逻辑等)
|
||||
|
||||
### 注册服务
|
||||
|
||||
#### 注册单例服务
|
||||
|
||||
单例服务在首次解析时创建,之后所有解析请求都返回同一个实例:
|
||||
|
||||
```typescript
|
||||
class DataService implements IService {
|
||||
private data: Map<string, any> = new Map();
|
||||
|
||||
getData(key: string) {
|
||||
return this.data.get(key);
|
||||
}
|
||||
|
||||
setData(key: string, value: any) {
|
||||
this.data.set(key, value);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.data.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// 注册单例服务
|
||||
Core.services.registerSingleton(DataService);
|
||||
```
|
||||
|
||||
#### 注册瞬时服务
|
||||
|
||||
瞬时服务每次解析都创建新实例,适用于无状态或短生命周期的服务:
|
||||
|
||||
```typescript
|
||||
class CommandService implements IService {
|
||||
execute(command: string) {
|
||||
console.log(`Executing: ${command}`);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
// 清理资源
|
||||
}
|
||||
}
|
||||
|
||||
// 注册瞬时服务
|
||||
Core.services.registerTransient(CommandService);
|
||||
```
|
||||
|
||||
#### 注册服务实例
|
||||
|
||||
直接注册已创建的实例,自动视为单例:
|
||||
|
||||
```typescript
|
||||
const config = new ConfigService();
|
||||
config.load('./config.json');
|
||||
|
||||
// 注册实例
|
||||
Core.services.registerInstance(ConfigService, config);
|
||||
```
|
||||
|
||||
#### 使用工厂函数注册
|
||||
|
||||
工厂函数允许你在创建服务时执行自定义逻辑:
|
||||
|
||||
```typescript
|
||||
Core.services.registerSingleton(LoggerService, (container) => {
|
||||
const logger = new LoggerService();
|
||||
logger.setLevel('debug');
|
||||
return logger;
|
||||
});
|
||||
```
|
||||
|
||||
### 解析服务
|
||||
|
||||
#### resolve 方法
|
||||
|
||||
解析服务实例,如果服务未注册会抛出异常:
|
||||
|
||||
```typescript
|
||||
// 解析服务
|
||||
const dataService = Core.services.resolve(DataService);
|
||||
dataService.setData('player', { name: 'Alice', score: 100 });
|
||||
|
||||
// 单例服务,多次解析返回同一个实例
|
||||
const same = Core.services.resolve(DataService);
|
||||
console.log(same === dataService); // true
|
||||
```
|
||||
|
||||
#### tryResolve 方法
|
||||
|
||||
尝试解析服务,如果未注册返回 null 而不抛出异常:
|
||||
|
||||
```typescript
|
||||
const optional = Core.services.tryResolve(OptionalService);
|
||||
if (optional) {
|
||||
optional.doSomething();
|
||||
}
|
||||
```
|
||||
|
||||
#### isRegistered 方法
|
||||
|
||||
检查服务是否已注册:
|
||||
|
||||
```typescript
|
||||
if (Core.services.isRegistered(DataService)) {
|
||||
const service = Core.services.resolve(DataService);
|
||||
}
|
||||
```
|
||||
|
||||
## 内置服务
|
||||
|
||||
Core 在初始化时自动注册了以下内置服务:
|
||||
|
||||
### TimerManager
|
||||
|
||||
定时器管理器,负责管理所有游戏定时器:
|
||||
|
||||
```typescript
|
||||
const timerManager = Core.services.resolve(TimerManager);
|
||||
|
||||
// 创建定时器
|
||||
timerManager.schedule(1.0, false, null, (timer) => {
|
||||
console.log('1秒后执行');
|
||||
});
|
||||
```
|
||||
|
||||
### PerformanceMonitor
|
||||
|
||||
性能监控器,监控游戏性能并提供优化建议:
|
||||
|
||||
```typescript
|
||||
const monitor = Core.services.resolve(PerformanceMonitor);
|
||||
|
||||
// 启用性能监控
|
||||
monitor.enable();
|
||||
|
||||
// 获取性能数据
|
||||
const fps = monitor.getFPS();
|
||||
```
|
||||
|
||||
### SceneManager
|
||||
|
||||
场景管理器,管理单场景应用的场景生命周期:
|
||||
|
||||
```typescript
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
|
||||
// 设置当前场景
|
||||
sceneManager.setScene(new GameScene());
|
||||
|
||||
// 获取当前场景
|
||||
const currentScene = sceneManager.currentScene;
|
||||
|
||||
// 延迟切换场景
|
||||
sceneManager.loadScene(new MenuScene());
|
||||
|
||||
// 更新场景
|
||||
sceneManager.update();
|
||||
```
|
||||
|
||||
### WorldManager
|
||||
|
||||
世界管理器,管理多个独立的 World 实例(高级用例):
|
||||
|
||||
```typescript
|
||||
const worldManager = Core.services.resolve(WorldManager);
|
||||
|
||||
// 创建独立的游戏世界
|
||||
const gameWorld = worldManager.createWorld('game_room_001', {
|
||||
name: 'GameRoom',
|
||||
maxScenes: 5
|
||||
});
|
||||
|
||||
// 在World中创建场景
|
||||
const scene = gameWorld.createScene('battle', new BattleScene());
|
||||
gameWorld.setSceneActive('battle', true);
|
||||
|
||||
// 更新所有World
|
||||
worldManager.updateAll();
|
||||
```
|
||||
|
||||
**适用场景**:
|
||||
- SceneManager: 适用于 95% 的游戏(单人游戏、简单多人游戏)
|
||||
- WorldManager: 适用于 MMO 服务器、游戏房间系统等需要完全隔离的多世界应用
|
||||
|
||||
### PoolManager
|
||||
|
||||
对象池管理器,管理所有对象池:
|
||||
|
||||
```typescript
|
||||
const poolManager = Core.services.resolve(PoolManager);
|
||||
|
||||
// 创建对象池
|
||||
const bulletPool = poolManager.createPool('bullets', () => new Bullet(), 100);
|
||||
```
|
||||
|
||||
### PluginManager
|
||||
|
||||
插件管理器,管理插件的安装和卸载:
|
||||
|
||||
```typescript
|
||||
const pluginManager = Core.services.resolve(PluginManager);
|
||||
|
||||
// 获取所有已安装的插件
|
||||
const plugins = pluginManager.getAllPlugins();
|
||||
```
|
||||
|
||||
## 依赖注入
|
||||
|
||||
ECS Framework 提供了装饰器来简化依赖注入。
|
||||
|
||||
### @Injectable 装饰器
|
||||
|
||||
标记类为可注入的服务:
|
||||
|
||||
```typescript
|
||||
import { Injectable, IService } from '@esengine/ecs-framework';
|
||||
|
||||
@Injectable()
|
||||
class GameService implements IService {
|
||||
constructor() {
|
||||
console.log('GameService created');
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
console.log('GameService disposed');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### @InjectProperty 装饰器
|
||||
|
||||
通过属性装饰器注入依赖。注入时机是在构造函数执行后、`onInitialize()` 调用前完成:
|
||||
|
||||
```typescript
|
||||
import { Injectable, InjectProperty, IService } from '@esengine/ecs-framework';
|
||||
|
||||
@Injectable()
|
||||
class PlayerService implements IService {
|
||||
@InjectProperty(DataService)
|
||||
private data!: DataService;
|
||||
|
||||
@InjectProperty(GameService)
|
||||
private game!: GameService;
|
||||
|
||||
dispose(): void {
|
||||
// 清理资源
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在 EntitySystem 中使用属性注入:
|
||||
|
||||
```typescript
|
||||
@Injectable()
|
||||
class CombatSystem extends EntitySystem {
|
||||
@InjectProperty(TimeService)
|
||||
private timeService!: TimeService;
|
||||
|
||||
@InjectProperty(AudioService)
|
||||
private audio!: AudioService;
|
||||
|
||||
constructor() {
|
||||
super(Matcher.all(Health, Attack));
|
||||
}
|
||||
|
||||
onInitialize(): void {
|
||||
// 此时属性已注入完成,可以安全使用
|
||||
console.log('Delta time:', this.timeService.getDeltaTime());
|
||||
}
|
||||
|
||||
processEntity(entity: Entity): void {
|
||||
// 使用注入的服务
|
||||
this.audio.playSound('attack');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **注意**: 属性声明时使用 `!` 断言(如 `private data!: DataService`),表示该属性会在使用前被注入。
|
||||
|
||||
### 注册可注入服务
|
||||
|
||||
使用 `registerInjectable` 自动处理依赖注入:
|
||||
|
||||
```typescript
|
||||
import { registerInjectable } from '@esengine/ecs-framework';
|
||||
|
||||
// 注册服务(会自动解析 @InjectProperty 依赖)
|
||||
registerInjectable(Core.services, PlayerService);
|
||||
|
||||
// 解析时会自动注入属性依赖
|
||||
const player = Core.services.resolve(PlayerService);
|
||||
```
|
||||
|
||||
### @Updatable 装饰器
|
||||
|
||||
标记服务为可更新的,使其在每帧自动被调用:
|
||||
|
||||
```typescript
|
||||
import { Injectable, Updatable, IService, IUpdatable } from '@esengine/ecs-framework';
|
||||
|
||||
@Injectable()
|
||||
@Updatable() // 默认优先级为0
|
||||
class PhysicsService implements IService, IUpdatable {
|
||||
update(deltaTime?: number): void {
|
||||
// 每帧更新物理模拟
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
// 清理资源
|
||||
}
|
||||
}
|
||||
|
||||
// 指定更新优先级(数值越小越先执行)
|
||||
@Injectable()
|
||||
@Updatable(10)
|
||||
class RenderService implements IService, IUpdatable {
|
||||
update(deltaTime?: number): void {
|
||||
// 每帧渲染
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
// 清理资源
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
使用 `@Updatable` 装饰器的服务会被 Core 自动调用,无需手动管理:
|
||||
|
||||
```typescript
|
||||
// Core.update() 会自动调用所有@Updatable服务的update方法
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // 自动更新所有可更新服务
|
||||
}
|
||||
```
|
||||
|
||||
## 自定义服务
|
||||
|
||||
### 创建自定义服务
|
||||
|
||||
实现 `IService` 接口并注册到容器:
|
||||
|
||||
```typescript
|
||||
import { IService } from '@esengine/ecs-framework';
|
||||
|
||||
class AudioService implements IService {
|
||||
private sounds: Map<string, HTMLAudioElement> = new Map();
|
||||
|
||||
play(soundId: string) {
|
||||
const sound = this.sounds.get(soundId);
|
||||
if (sound) {
|
||||
sound.play();
|
||||
}
|
||||
}
|
||||
|
||||
load(soundId: string, url: string) {
|
||||
const audio = new Audio(url);
|
||||
this.sounds.set(soundId, audio);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
// 停止所有音效并清理
|
||||
for (const sound of this.sounds.values()) {
|
||||
sound.pause();
|
||||
sound.src = '';
|
||||
}
|
||||
this.sounds.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// 注册自定义服务
|
||||
Core.services.registerSingleton(AudioService);
|
||||
|
||||
// 使用服务
|
||||
const audio = Core.services.resolve(AudioService);
|
||||
audio.load('jump', '/sounds/jump.mp3');
|
||||
audio.play('jump');
|
||||
```
|
||||
|
||||
### 服务间依赖
|
||||
|
||||
服务可以依赖其他服务:
|
||||
|
||||
```typescript
|
||||
@Injectable()
|
||||
class ConfigService implements IService {
|
||||
private config: any = {};
|
||||
|
||||
get(key: string) {
|
||||
return this.config[key];
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.config = {};
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
class NetworkService implements IService {
|
||||
constructor(
|
||||
@Inject(ConfigService) private config: ConfigService
|
||||
) {
|
||||
// 使用配置服务
|
||||
const apiUrl = this.config.get('apiUrl');
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
// 清理网络连接
|
||||
}
|
||||
}
|
||||
|
||||
// 注册服务(按依赖顺序)
|
||||
registerInjectable(Core.services, ConfigService);
|
||||
registerInjectable(Core.services, NetworkService);
|
||||
```
|
||||
|
||||
## 高级用法
|
||||
|
||||
### 接口与 Symbol 标识符模式
|
||||
|
||||
在大型项目或需要跨平台适配的游戏中,推荐使用"接口 + Symbol.for 标识符"模式。这种模式实现了真正的依赖倒置,让代码依赖于抽象而非具体实现。
|
||||
|
||||
#### 为什么使用 Symbol.for
|
||||
|
||||
- **跨包共享**: `Symbol.for('key')` 在全局 Symbol 注册表中创建/获取 Symbol,确保不同包中使用相同的标识符
|
||||
- **接口解耦**: 消费者只依赖接口定义,不依赖具体实现类
|
||||
- **可替换实现**: 可以在运行时注入不同的实现(如测试 Mock、不同平台适配)
|
||||
|
||||
#### 定义接口和标识符
|
||||
|
||||
以音频服务为例,游戏需要在不同平台(Web、微信小游戏、原生App)使用不同的音频实现:
|
||||
|
||||
```typescript
|
||||
// IAudioService.ts - 定义接口和标识符
|
||||
export interface IAudioService {
|
||||
dispose(): void;
|
||||
playSound(id: string): void;
|
||||
playMusic(id: string, loop?: boolean): void;
|
||||
stopMusic(): void;
|
||||
setVolume(volume: number): void;
|
||||
preload(id: string, url: string): Promise<void>;
|
||||
}
|
||||
|
||||
// 使用 Symbol.for 确保跨包共享同一个 Symbol
|
||||
export const IAudioService = Symbol.for('IAudioService');
|
||||
```
|
||||
|
||||
#### 实现接口
|
||||
|
||||
```typescript
|
||||
// WebAudioService.ts - Web 平台实现
|
||||
import { IAudioService } from './IAudioService';
|
||||
|
||||
export class WebAudioService implements IAudioService {
|
||||
private audioContext: AudioContext;
|
||||
private sounds: Map<string, AudioBuffer> = new Map();
|
||||
|
||||
constructor() {
|
||||
this.audioContext = new AudioContext();
|
||||
}
|
||||
|
||||
playSound(id: string): void {
|
||||
const buffer = this.sounds.get(id);
|
||||
if (buffer) {
|
||||
const source = this.audioContext.createBufferSource();
|
||||
source.buffer = buffer;
|
||||
source.connect(this.audioContext.destination);
|
||||
source.start();
|
||||
}
|
||||
}
|
||||
|
||||
async preload(id: string, url: string): Promise<void> {
|
||||
const response = await fetch(url);
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
|
||||
this.sounds.set(id, audioBuffer);
|
||||
}
|
||||
|
||||
// ... 其他方法实现
|
||||
|
||||
dispose(): void {
|
||||
this.audioContext.close();
|
||||
this.sounds.clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// WechatAudioService.ts - 微信小游戏平台实现
|
||||
export class WechatAudioService implements IAudioService {
|
||||
private innerAudioContexts: Map<string, WechatMinigame.InnerAudioContext> = new Map();
|
||||
|
||||
playSound(id: string): void {
|
||||
const ctx = this.innerAudioContexts.get(id);
|
||||
if (ctx) {
|
||||
ctx.play();
|
||||
}
|
||||
}
|
||||
|
||||
async preload(id: string, url: string): Promise<void> {
|
||||
const ctx = wx.createInnerAudioContext();
|
||||
ctx.src = url;
|
||||
this.innerAudioContexts.set(id, ctx);
|
||||
}
|
||||
|
||||
// ... 其他方法实现
|
||||
|
||||
dispose(): void {
|
||||
for (const ctx of this.innerAudioContexts.values()) {
|
||||
ctx.destroy();
|
||||
}
|
||||
this.innerAudioContexts.clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 注册和使用
|
||||
|
||||
```typescript
|
||||
import { IAudioService } from './IAudioService';
|
||||
import { WebAudioService } from './WebAudioService';
|
||||
import { WechatAudioService } from './WechatAudioService';
|
||||
|
||||
// 根据平台注册不同实现
|
||||
if (typeof wx !== 'undefined') {
|
||||
Core.services.registerInstance(IAudioService, new WechatAudioService());
|
||||
} else {
|
||||
Core.services.registerInstance(IAudioService, new WebAudioService());
|
||||
}
|
||||
|
||||
// 业务代码中使用 - 不关心具体实现
|
||||
const audio = Core.services.resolve<IAudioService>(IAudioService);
|
||||
await audio.preload('explosion', '/sounds/explosion.mp3');
|
||||
audio.playSound('explosion');
|
||||
```
|
||||
|
||||
#### 跨模块使用
|
||||
|
||||
```typescript
|
||||
// 在游戏系统中使用
|
||||
import { IAudioService } from '@mygame/core';
|
||||
|
||||
class CombatSystem extends EntitySystem {
|
||||
private audio: IAudioService;
|
||||
|
||||
initialize(): void {
|
||||
// 获取音频服务,不需要知道具体实现
|
||||
this.audio = this.scene.services.resolve<IAudioService>(IAudioService);
|
||||
}
|
||||
|
||||
onEntityDeath(entity: Entity): void {
|
||||
this.audio.playSound('death');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Symbol vs Symbol.for
|
||||
|
||||
```typescript
|
||||
// Symbol() - 每次创建唯一的 Symbol
|
||||
const sym1 = Symbol('test');
|
||||
const sym2 = Symbol('test');
|
||||
console.log(sym1 === sym2); // false - 不同的 Symbol
|
||||
|
||||
// Symbol.for() - 在全局注册表中共享
|
||||
const sym3 = Symbol.for('test');
|
||||
const sym4 = Symbol.for('test');
|
||||
console.log(sym3 === sym4); // true - 同一个 Symbol
|
||||
|
||||
// 跨包场景
|
||||
// package-a/index.ts
|
||||
export const IMyService = Symbol.for('IMyService');
|
||||
|
||||
// package-b/index.ts (不同的包)
|
||||
const IMyService = Symbol.for('IMyService');
|
||||
// 与 package-a 中的是同一个 Symbol!
|
||||
```
|
||||
|
||||
### 循环依赖检测
|
||||
|
||||
服务容器会自动检测循环依赖:
|
||||
|
||||
```typescript
|
||||
// A 依赖 B,B 依赖 A
|
||||
@Injectable()
|
||||
class ServiceA implements IService {
|
||||
constructor(@Inject(ServiceB) b: ServiceB) {}
|
||||
dispose(): void {}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
class ServiceB implements IService {
|
||||
constructor(@Inject(ServiceA) a: ServiceA) {}
|
||||
dispose(): void {}
|
||||
}
|
||||
|
||||
// 解析时会抛出错误: Circular dependency detected: ServiceA -> ServiceB -> ServiceA
|
||||
```
|
||||
|
||||
### 获取所有服务
|
||||
|
||||
```typescript
|
||||
// 获取所有已注册的服务类型
|
||||
const types = Core.services.getRegisteredServices();
|
||||
|
||||
// 获取所有已实例化的服务实例
|
||||
const instances = Core.services.getAll();
|
||||
```
|
||||
|
||||
### 服务清理
|
||||
|
||||
```typescript
|
||||
// 注销单个服务
|
||||
Core.services.unregister(MyService);
|
||||
|
||||
// 清空所有服务(会调用每个服务的dispose方法)
|
||||
Core.services.clear();
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 服务命名
|
||||
|
||||
服务类名应该以 `Service` 或 `Manager` 结尾,清晰表达其职责:
|
||||
|
||||
```typescript
|
||||
class PlayerService implements IService {}
|
||||
class AudioManager implements IService {}
|
||||
class NetworkService implements IService {}
|
||||
```
|
||||
|
||||
### 资源清理
|
||||
|
||||
始终在 `dispose()` 方法中清理资源:
|
||||
|
||||
```typescript
|
||||
class ResourceService implements IService {
|
||||
private resources: Map<string, Resource> = new Map();
|
||||
|
||||
dispose(): void {
|
||||
// 释放所有资源
|
||||
for (const resource of this.resources.values()) {
|
||||
resource.release();
|
||||
}
|
||||
this.resources.clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 避免过度使用
|
||||
|
||||
不要把所有类都注册为服务,服务应该是:
|
||||
|
||||
- 全局单例或需要共享状态
|
||||
- 需要在多处使用
|
||||
- 生命周期需要管理
|
||||
- 需要依赖注入
|
||||
|
||||
对于简单的工具类或数据类,直接创建实例即可。
|
||||
|
||||
### 依赖方向
|
||||
|
||||
保持清晰的依赖方向,避免循环依赖:
|
||||
|
||||
```
|
||||
高层服务 -> 中层服务 -> 底层服务
|
||||
GameLogic -> DataService -> ConfigService
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 服务未注册错误
|
||||
|
||||
**问题**: `Error: Service MyService is not registered`
|
||||
|
||||
**解决**:
|
||||
```typescript
|
||||
// 确保服务已注册
|
||||
Core.services.registerSingleton(MyService);
|
||||
|
||||
// 或者使用tryResolve
|
||||
const service = Core.services.tryResolve(MyService);
|
||||
if (!service) {
|
||||
console.log('Service not found');
|
||||
}
|
||||
```
|
||||
|
||||
### 循环依赖错误
|
||||
|
||||
**问题**: `Circular dependency detected`
|
||||
|
||||
**解决**: 重新设计服务依赖关系,引入中间服务或使用事件系统解耦。
|
||||
|
||||
### 何时使用单例 vs 瞬时
|
||||
|
||||
- **单例**: 管理器类、配置、缓存、状态管理
|
||||
- **瞬时**: 命令对象、请求处理器、临时任务
|
||||
|
||||
## 相关链接
|
||||
|
||||
- [插件系统](./plugin-system.md) - 使用服务容器注册插件服务
|
||||
- [快速开始](./getting-started.md) - Core 初始化和基础使用
|
||||
- [系统架构](./system.md) - 在系统中使用服务
|
||||
1161
docs/guide/system.md
1161
docs/guide/system.md
File diff suppressed because it is too large
Load Diff
@@ -1,772 +0,0 @@
|
||||
# Worker系统
|
||||
|
||||
Worker系统(WorkerEntitySystem)是ECS框架中基于Web Worker的多线程处理系统,专为计算密集型任务设计,能够充分利用多核CPU性能,实现真正的并行计算。
|
||||
|
||||
## 核心特性
|
||||
|
||||
- **真正的并行计算**:利用Web Worker在后台线程执行计算密集型任务
|
||||
- **自动负载均衡**:根据CPU核心数自动分配工作负载
|
||||
- **SharedArrayBuffer优化**:零拷贝数据共享,提升大规模计算性能
|
||||
- **降级支持**:不支持Worker时自动回退到主线程处理
|
||||
- **类型安全**:完整的TypeScript支持和类型检查
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 简单的物理系统示例
|
||||
|
||||
```typescript
|
||||
interface PhysicsData {
|
||||
id: number;
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
mass: number;
|
||||
radius: number;
|
||||
}
|
||||
|
||||
@ECSSystem('Physics')
|
||||
class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsData> {
|
||||
constructor() {
|
||||
super(Matcher.all(Position, Velocity, Physics), {
|
||||
enableWorker: true, // 启用Worker并行处理
|
||||
workerCount: 8, // Worker数量,系统会自动限制在硬件支持范围内
|
||||
entitiesPerWorker: 100, // 每个Worker处理的实体数量
|
||||
useSharedArrayBuffer: true, // 启用SharedArrayBuffer优化
|
||||
entityDataSize: 7, // 每个实体数据大小
|
||||
maxEntities: 10000, // 最大实体数量
|
||||
systemConfig: { // 传递给Worker的配置
|
||||
gravity: 100,
|
||||
friction: 0.95
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 数据提取:将Entity转换为可序列化的数据
|
||||
protected extractEntityData(entity: Entity): PhysicsData {
|
||||
const position = entity.getComponent(Position);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
const physics = entity.getComponent(Physics);
|
||||
|
||||
return {
|
||||
id: entity.id,
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
vx: velocity.x,
|
||||
vy: velocity.y,
|
||||
mass: physics.mass,
|
||||
radius: physics.radius
|
||||
};
|
||||
}
|
||||
|
||||
// Worker处理函数:纯函数,在Worker中执行
|
||||
protected workerProcess(
|
||||
entities: PhysicsData[],
|
||||
deltaTime: number,
|
||||
config: any
|
||||
): PhysicsData[] {
|
||||
return entities.map(entity => {
|
||||
// 应用重力
|
||||
entity.vy += config.gravity * deltaTime;
|
||||
|
||||
// 更新位置
|
||||
entity.x += entity.vx * deltaTime;
|
||||
entity.y += entity.vy * deltaTime;
|
||||
|
||||
// 应用摩擦力
|
||||
entity.vx *= config.friction;
|
||||
entity.vy *= config.friction;
|
||||
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
// 结果应用:将Worker处理结果应用回Entity
|
||||
protected applyResult(entity: Entity, result: PhysicsData): void {
|
||||
const position = entity.getComponent(Position);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
position.x = result.x;
|
||||
position.y = result.y;
|
||||
velocity.x = result.vx;
|
||||
velocity.y = result.vy;
|
||||
}
|
||||
|
||||
// SharedArrayBuffer优化支持
|
||||
protected getDefaultEntityDataSize(): number {
|
||||
return 7; // id, x, y, vx, vy, mass, radius
|
||||
}
|
||||
|
||||
protected writeEntityToBuffer(entityData: PhysicsData, offset: number): void {
|
||||
if (!this.sharedFloatArray) return;
|
||||
|
||||
this.sharedFloatArray[offset + 0] = entityData.id;
|
||||
this.sharedFloatArray[offset + 1] = entityData.x;
|
||||
this.sharedFloatArray[offset + 2] = entityData.y;
|
||||
this.sharedFloatArray[offset + 3] = entityData.vx;
|
||||
this.sharedFloatArray[offset + 4] = entityData.vy;
|
||||
this.sharedFloatArray[offset + 5] = entityData.mass;
|
||||
this.sharedFloatArray[offset + 6] = entityData.radius;
|
||||
}
|
||||
|
||||
protected readEntityFromBuffer(offset: number): PhysicsData | null {
|
||||
if (!this.sharedFloatArray) return null;
|
||||
|
||||
return {
|
||||
id: this.sharedFloatArray[offset + 0],
|
||||
x: this.sharedFloatArray[offset + 1],
|
||||
y: this.sharedFloatArray[offset + 2],
|
||||
vx: this.sharedFloatArray[offset + 3],
|
||||
vy: this.sharedFloatArray[offset + 4],
|
||||
mass: this.sharedFloatArray[offset + 5],
|
||||
radius: this.sharedFloatArray[offset + 6]
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 配置选项
|
||||
|
||||
Worker系统支持丰富的配置选项:
|
||||
|
||||
```typescript
|
||||
interface WorkerSystemConfig {
|
||||
/** 是否启用Worker并行处理 */
|
||||
enableWorker?: boolean;
|
||||
/** Worker数量,默认为CPU核心数,自动限制在系统最大值内 */
|
||||
workerCount?: number;
|
||||
/** 每个Worker处理的实体数量,用于控制负载分布 */
|
||||
entitiesPerWorker?: number;
|
||||
/** 系统配置数据,会传递给Worker */
|
||||
systemConfig?: any;
|
||||
/** 是否使用SharedArrayBuffer优化 */
|
||||
useSharedArrayBuffer?: boolean;
|
||||
/** 每个实体在SharedArrayBuffer中占用的Float32数量 */
|
||||
entityDataSize?: number;
|
||||
/** 最大实体数量(用于预分配SharedArrayBuffer) */
|
||||
maxEntities?: number;
|
||||
/** 预编译的Worker脚本路径(用于微信小游戏等不支持动态脚本的平台) */
|
||||
workerScriptPath?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### 配置建议
|
||||
|
||||
```typescript
|
||||
constructor() {
|
||||
super(matcher, {
|
||||
// 根据任务复杂度决定是否启用
|
||||
enableWorker: this.shouldUseWorker(),
|
||||
|
||||
// Worker数量:系统会自动限制在硬件支持范围内
|
||||
workerCount: 8, // 请求8个Worker,实际数量受CPU核心数限制
|
||||
|
||||
// 每个Worker处理的实体数量(可选)
|
||||
entitiesPerWorker: 200, // 精确控制负载分布
|
||||
|
||||
// 大量简单计算时启用SharedArrayBuffer
|
||||
useSharedArrayBuffer: this.entityCount > 1000,
|
||||
|
||||
// 根据实际数据结构设置
|
||||
entityDataSize: 8, // 确保与数据结构匹配
|
||||
|
||||
// 预估最大实体数量
|
||||
maxEntities: 10000,
|
||||
|
||||
// 传递给Worker的全局配置
|
||||
systemConfig: {
|
||||
gravity: 9.8,
|
||||
friction: 0.95,
|
||||
worldBounds: { width: 1920, height: 1080 }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private shouldUseWorker(): boolean {
|
||||
// 根据实体数量和计算复杂度决定
|
||||
return this.expectedEntityCount > 100;
|
||||
}
|
||||
|
||||
// 获取系统信息
|
||||
getSystemInfo() {
|
||||
const info = this.getWorkerInfo();
|
||||
console.log(`Worker数量: ${info.workerCount}/${info.maxSystemWorkerCount}`);
|
||||
console.log(`每Worker实体数: ${info.entitiesPerWorker || '自动分配'}`);
|
||||
console.log(`当前模式: ${info.currentMode}`);
|
||||
}
|
||||
```
|
||||
|
||||
## 处理模式
|
||||
|
||||
Worker系统支持两种处理模式:
|
||||
|
||||
### 1. 传统Worker模式
|
||||
|
||||
数据通过序列化在主线程和Worker间传递:
|
||||
|
||||
```typescript
|
||||
// 适用于:复杂计算逻辑,实体数量适中
|
||||
constructor() {
|
||||
super(matcher, {
|
||||
enableWorker: true,
|
||||
useSharedArrayBuffer: false, // 使用传统模式
|
||||
workerCount: 2
|
||||
});
|
||||
}
|
||||
|
||||
protected workerProcess(entities: EntityData[], deltaTime: number): EntityData[] {
|
||||
// 复杂的算法逻辑
|
||||
return entities.map(entity => {
|
||||
// AI决策、路径规划等复杂计算
|
||||
return this.complexAILogic(entity, deltaTime);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 2. SharedArrayBuffer模式
|
||||
|
||||
零拷贝数据共享,适合大量简单计算:
|
||||
|
||||
```typescript
|
||||
// 适用于:大量实体的简单计算
|
||||
constructor() {
|
||||
super(matcher, {
|
||||
enableWorker: true,
|
||||
useSharedArrayBuffer: true, // 启用共享内存
|
||||
entityDataSize: 6,
|
||||
maxEntities: 10000
|
||||
});
|
||||
}
|
||||
|
||||
protected getSharedArrayBufferProcessFunction(): SharedArrayBufferProcessFunction {
|
||||
return function(sharedFloatArray: Float32Array, startIndex: number, endIndex: number, deltaTime: number, config: any) {
|
||||
const entitySize = 6;
|
||||
for (let i = startIndex; i < endIndex; i++) {
|
||||
const offset = i * entitySize;
|
||||
|
||||
// 读取数据
|
||||
let x = sharedFloatArray[offset];
|
||||
let y = sharedFloatArray[offset + 1];
|
||||
let vx = sharedFloatArray[offset + 2];
|
||||
let vy = sharedFloatArray[offset + 3];
|
||||
|
||||
// 物理计算
|
||||
vy += config.gravity * deltaTime;
|
||||
x += vx * deltaTime;
|
||||
y += vy * deltaTime;
|
||||
|
||||
// 写回数据
|
||||
sharedFloatArray[offset] = x;
|
||||
sharedFloatArray[offset + 1] = y;
|
||||
sharedFloatArray[offset + 2] = vx;
|
||||
sharedFloatArray[offset + 3] = vy;
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## 完整示例:粒子物理系统
|
||||
|
||||
一个包含碰撞检测的完整粒子物理系统:
|
||||
|
||||
```typescript
|
||||
interface ParticleData {
|
||||
id: number;
|
||||
x: number;
|
||||
y: number;
|
||||
dx: number;
|
||||
dy: number;
|
||||
mass: number;
|
||||
radius: number;
|
||||
bounce: number;
|
||||
friction: number;
|
||||
}
|
||||
|
||||
@ECSSystem('ParticlePhysics')
|
||||
class ParticlePhysicsWorkerSystem extends WorkerEntitySystem<ParticleData> {
|
||||
constructor() {
|
||||
super(Matcher.all(Position, Velocity, Physics, Renderable), {
|
||||
enableWorker: true,
|
||||
workerCount: 6, // 请求6个Worker,自动限制在CPU核心数内
|
||||
entitiesPerWorker: 150, // 每个Worker处理150个粒子
|
||||
useSharedArrayBuffer: true,
|
||||
entityDataSize: 9,
|
||||
maxEntities: 5000,
|
||||
systemConfig: {
|
||||
gravity: 100,
|
||||
canvasWidth: 800,
|
||||
canvasHeight: 600,
|
||||
groundFriction: 0.98
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected extractEntityData(entity: Entity): ParticleData {
|
||||
const position = entity.getComponent(Position);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
const physics = entity.getComponent(Physics);
|
||||
const renderable = entity.getComponent(Renderable);
|
||||
|
||||
return {
|
||||
id: entity.id,
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
dx: velocity.dx,
|
||||
dy: velocity.dy,
|
||||
mass: physics.mass,
|
||||
radius: renderable.size,
|
||||
bounce: physics.bounce,
|
||||
friction: physics.friction
|
||||
};
|
||||
}
|
||||
|
||||
protected workerProcess(
|
||||
entities: ParticleData[],
|
||||
deltaTime: number,
|
||||
config: any
|
||||
): ParticleData[] {
|
||||
const result = entities.map(e => ({ ...e }));
|
||||
|
||||
// 基础物理更新
|
||||
for (const particle of result) {
|
||||
// 应用重力
|
||||
particle.dy += config.gravity * deltaTime;
|
||||
|
||||
// 更新位置
|
||||
particle.x += particle.dx * deltaTime;
|
||||
particle.y += particle.dy * deltaTime;
|
||||
|
||||
// 边界碰撞
|
||||
if (particle.x <= particle.radius) {
|
||||
particle.x = particle.radius;
|
||||
particle.dx = -particle.dx * particle.bounce;
|
||||
} else if (particle.x >= config.canvasWidth - particle.radius) {
|
||||
particle.x = config.canvasWidth - particle.radius;
|
||||
particle.dx = -particle.dx * particle.bounce;
|
||||
}
|
||||
|
||||
if (particle.y <= particle.radius) {
|
||||
particle.y = particle.radius;
|
||||
particle.dy = -particle.dy * particle.bounce;
|
||||
} else if (particle.y >= config.canvasHeight - particle.radius) {
|
||||
particle.y = config.canvasHeight - particle.radius;
|
||||
particle.dy = -particle.dy * particle.bounce;
|
||||
particle.dx *= config.groundFriction;
|
||||
}
|
||||
|
||||
// 空气阻力
|
||||
particle.dx *= particle.friction;
|
||||
particle.dy *= particle.friction;
|
||||
}
|
||||
|
||||
// 粒子间碰撞检测(O(n²)算法)
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
for (let j = i + 1; j < result.length; j++) {
|
||||
const p1 = result[i];
|
||||
const p2 = result[j];
|
||||
|
||||
const dx = p2.x - p1.x;
|
||||
const dy = p2.y - p1.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
const minDistance = p1.radius + p2.radius;
|
||||
|
||||
if (distance < minDistance && distance > 0) {
|
||||
// 分离粒子
|
||||
const nx = dx / distance;
|
||||
const ny = dy / distance;
|
||||
const overlap = minDistance - distance;
|
||||
|
||||
p1.x -= nx * overlap * 0.5;
|
||||
p1.y -= ny * overlap * 0.5;
|
||||
p2.x += nx * overlap * 0.5;
|
||||
p2.y += ny * overlap * 0.5;
|
||||
|
||||
// 弹性碰撞
|
||||
const relativeVelocityX = p2.dx - p1.dx;
|
||||
const relativeVelocityY = p2.dy - p1.dy;
|
||||
const velocityAlongNormal = relativeVelocityX * nx + relativeVelocityY * ny;
|
||||
|
||||
if (velocityAlongNormal > 0) continue;
|
||||
|
||||
const restitution = (p1.bounce + p2.bounce) * 0.5;
|
||||
const impulseScalar = -(1 + restitution) * velocityAlongNormal / (1/p1.mass + 1/p2.mass);
|
||||
|
||||
p1.dx -= impulseScalar * nx / p1.mass;
|
||||
p1.dy -= impulseScalar * ny / p1.mass;
|
||||
p2.dx += impulseScalar * nx / p2.mass;
|
||||
p2.dy += impulseScalar * ny / p2.mass;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected applyResult(entity: Entity, result: ParticleData): void {
|
||||
if (!entity?.enabled) return;
|
||||
|
||||
const position = entity.getComponent(Position);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
if (position && velocity) {
|
||||
position.set(result.x, result.y);
|
||||
velocity.set(result.dx, result.dy);
|
||||
}
|
||||
}
|
||||
|
||||
protected getDefaultEntityDataSize(): number {
|
||||
return 9;
|
||||
}
|
||||
|
||||
protected writeEntityToBuffer(data: ParticleData, offset: number): void {
|
||||
if (!this.sharedFloatArray) return;
|
||||
|
||||
this.sharedFloatArray[offset + 0] = data.id;
|
||||
this.sharedFloatArray[offset + 1] = data.x;
|
||||
this.sharedFloatArray[offset + 2] = data.y;
|
||||
this.sharedFloatArray[offset + 3] = data.dx;
|
||||
this.sharedFloatArray[offset + 4] = data.dy;
|
||||
this.sharedFloatArray[offset + 5] = data.mass;
|
||||
this.sharedFloatArray[offset + 6] = data.radius;
|
||||
this.sharedFloatArray[offset + 7] = data.bounce;
|
||||
this.sharedFloatArray[offset + 8] = data.friction;
|
||||
}
|
||||
|
||||
protected readEntityFromBuffer(offset: number): ParticleData | null {
|
||||
if (!this.sharedFloatArray) return null;
|
||||
|
||||
return {
|
||||
id: this.sharedFloatArray[offset + 0],
|
||||
x: this.sharedFloatArray[offset + 1],
|
||||
y: this.sharedFloatArray[offset + 2],
|
||||
dx: this.sharedFloatArray[offset + 3],
|
||||
dy: this.sharedFloatArray[offset + 4],
|
||||
mass: this.sharedFloatArray[offset + 5],
|
||||
radius: this.sharedFloatArray[offset + 6],
|
||||
bounce: this.sharedFloatArray[offset + 7],
|
||||
friction: this.sharedFloatArray[offset + 8]
|
||||
};
|
||||
}
|
||||
|
||||
// 性能监控
|
||||
public getPerformanceInfo(): {
|
||||
enabled: boolean;
|
||||
workerCount: number;
|
||||
entitiesPerWorker?: number;
|
||||
maxSystemWorkerCount: number;
|
||||
entityCount: number;
|
||||
isProcessing: boolean;
|
||||
currentMode: string;
|
||||
} {
|
||||
const workerInfo = this.getWorkerInfo();
|
||||
return {
|
||||
...workerInfo,
|
||||
entityCount: this.entities.length
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 适用场景
|
||||
|
||||
Worker系统特别适合以下场景:
|
||||
|
||||
### 1. 物理模拟
|
||||
- **重力系统**:大量实体的重力计算
|
||||
- **碰撞检测**:复杂的碰撞算法
|
||||
- **流体模拟**:粒子流体系统
|
||||
- **布料模拟**:顶点物理计算
|
||||
|
||||
### 2. AI计算
|
||||
- **路径寻找**:A*、Dijkstra等算法
|
||||
- **行为树**:复杂的AI决策逻辑
|
||||
- **群体智能**:鸟群、鱼群算法
|
||||
- **神经网络**:简单的AI推理
|
||||
|
||||
### 3. 数据处理
|
||||
- **大量实体更新**:状态机、生命周期管理
|
||||
- **统计计算**:游戏数据分析
|
||||
- **图像处理**:纹理生成、效果计算
|
||||
- **音频处理**:音效合成、频谱分析
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. Worker函数要求
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:Worker处理函数是纯函数
|
||||
protected workerProcess(entities: PhysicsData[], deltaTime: number, config: any): PhysicsData[] {
|
||||
// 只使用参数和标准JavaScript API
|
||||
return entities.map(entity => {
|
||||
// 纯计算逻辑,不依赖外部状态
|
||||
entity.y += entity.velocity * deltaTime;
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
// ❌ 避免:在Worker函数中使用外部引用
|
||||
protected workerProcess(entities: PhysicsData[], deltaTime: number): PhysicsData[] {
|
||||
// this 和外部变量在Worker中不可用
|
||||
return entities.map(entity => {
|
||||
entity.y += this.someProperty; // ❌ 错误
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 数据设计
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:合理的数据设计
|
||||
interface SimplePhysicsData {
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
// 保持数据结构简单,便于序列化
|
||||
}
|
||||
|
||||
// ❌ 避免:复杂的嵌套对象
|
||||
interface ComplexData {
|
||||
transform: {
|
||||
position: { x: number; y: number };
|
||||
rotation: { angle: number };
|
||||
};
|
||||
// 复杂嵌套结构增加序列化开销
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Worker数量控制
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:灵活的Worker配置
|
||||
constructor() {
|
||||
super(matcher, {
|
||||
// 直接指定需要的Worker数量,系统会自动限制在硬件支持范围内
|
||||
workerCount: 8, // 请求8个Worker
|
||||
entitiesPerWorker: 100, // 每个Worker处理100个实体
|
||||
enableWorker: this.shouldUseWorker(), // 条件启用
|
||||
});
|
||||
}
|
||||
|
||||
private shouldUseWorker(): boolean {
|
||||
// 根据实体数量和复杂度决定是否使用Worker
|
||||
return this.expectedEntityCount > 100;
|
||||
}
|
||||
|
||||
// 获取实际使用的Worker信息
|
||||
checkWorkerConfiguration() {
|
||||
const info = this.getWorkerInfo();
|
||||
console.log(`请求Worker数量: 8`);
|
||||
console.log(`实际Worker数量: ${info.workerCount}`);
|
||||
console.log(`系统最大支持: ${info.maxSystemWorkerCount}`);
|
||||
console.log(`每Worker实体数: ${info.entitiesPerWorker || '自动分配'}`);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 性能监控
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:性能监控
|
||||
public getPerformanceMetrics(): WorkerPerformanceMetrics {
|
||||
return {
|
||||
...this.getWorkerInfo(),
|
||||
entityCount: this.entities.length,
|
||||
averageProcessTime: this.getAverageProcessTime(),
|
||||
workerUtilization: this.getWorkerUtilization()
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## 性能优化建议
|
||||
|
||||
### 1. 计算密集度评估
|
||||
只对计算密集型任务使用Worker,避免在简单计算上增加线程开销。
|
||||
|
||||
### 2. 数据传输优化
|
||||
- 使用SharedArrayBuffer减少序列化开销
|
||||
- 保持数据结构简单和扁平
|
||||
- 避免频繁的大数据传输
|
||||
|
||||
### 3. 降级策略
|
||||
始终提供主线程回退方案,确保在不支持Worker的环境中正常运行。
|
||||
|
||||
### 4. 内存管理
|
||||
及时清理Worker池和共享缓冲区,避免内存泄漏。
|
||||
|
||||
### 5. 负载均衡
|
||||
使用 `entitiesPerWorker` 参数精确控制负载分布,避免某些Worker空闲而其他Worker过载。
|
||||
|
||||
## 在线演示
|
||||
|
||||
查看完整的Worker系统演示:[Worker系统演示](https://esengine.github.io/ecs-framework/demos/worker-system/)
|
||||
|
||||
该演示展示了:
|
||||
- 多线程物理计算
|
||||
- 实时性能对比
|
||||
- SharedArrayBuffer优化
|
||||
- 大量实体的并行处理
|
||||
|
||||
## 微信小游戏支持
|
||||
|
||||
微信小游戏对 Worker 有特殊限制,不支持动态创建 Worker 脚本。ESEngine 提供了 `@esengine/worker-generator` CLI 工具来解决这个问题。
|
||||
|
||||
### 微信小游戏 Worker 限制
|
||||
|
||||
| 特性 | 浏览器 | 微信小游戏 |
|
||||
|------|--------|-----------|
|
||||
| 动态脚本 (Blob URL) | ✅ 支持 | ❌ 不支持 |
|
||||
| Worker 数量 | 多个 | 最多 1 个 |
|
||||
| 脚本来源 | 任意 | 必须是代码包内文件 |
|
||||
| SharedArrayBuffer | 需要 COOP/COEP | 有限支持 |
|
||||
|
||||
### 使用 Worker Generator CLI
|
||||
|
||||
#### 1. 安装工具
|
||||
|
||||
```bash
|
||||
pnpm add -D @esengine/worker-generator
|
||||
```
|
||||
|
||||
#### 2. 配置 workerScriptPath
|
||||
|
||||
在你的 WorkerEntitySystem 子类中配置 `workerScriptPath`:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Physics')
|
||||
class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsData> {
|
||||
constructor() {
|
||||
super(Matcher.all(Position, Velocity, Physics), {
|
||||
enableWorker: true,
|
||||
workerScriptPath: 'workers/physics-worker.js', // 指定 Worker 文件路径
|
||||
systemConfig: {
|
||||
gravity: 100,
|
||||
friction: 0.95
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected workerProcess(
|
||||
entities: PhysicsData[],
|
||||
deltaTime: number,
|
||||
config: any
|
||||
): PhysicsData[] {
|
||||
// 物理计算逻辑
|
||||
return entities.map(entity => {
|
||||
entity.vy += config.gravity * deltaTime;
|
||||
entity.x += entity.vx * deltaTime;
|
||||
entity.y += entity.vy * deltaTime;
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
// ... 其他方法
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. 生成 Worker 文件
|
||||
|
||||
运行 CLI 工具自动提取 `workerProcess` 函数并生成兼容微信小游戏的 Worker 文件:
|
||||
|
||||
```bash
|
||||
# 基本用法
|
||||
npx esengine-worker-gen --src ./src --wechat
|
||||
|
||||
# 完整选项
|
||||
npx esengine-worker-gen \
|
||||
--src ./src \ # 源码目录
|
||||
--wechat \ # 生成微信小游戏兼容代码
|
||||
--mapping \ # 生成 worker-mapping.json
|
||||
--verbose # 详细输出
|
||||
```
|
||||
|
||||
CLI 工具会:
|
||||
1. 扫描源码目录,找到所有 `WorkerEntitySystem` 子类
|
||||
2. 读取每个类的 `workerScriptPath` 配置
|
||||
3. 提取 `workerProcess` 方法体
|
||||
4. 转换为 ES5 语法(微信小游戏兼容)
|
||||
5. 生成到配置的路径
|
||||
|
||||
#### 4. 配置 game.json
|
||||
|
||||
在微信小游戏的 `game.json` 中配置 workers 目录:
|
||||
|
||||
```json
|
||||
{
|
||||
"deviceOrientation": "portrait",
|
||||
"workers": "workers"
|
||||
}
|
||||
```
|
||||
|
||||
#### 5. 项目结构
|
||||
|
||||
```
|
||||
your-game/
|
||||
├── game.js
|
||||
├── game.json # 配置 "workers": "workers"
|
||||
├── src/
|
||||
│ └── systems/
|
||||
│ └── PhysicsSystem.ts # workerScriptPath: 'workers/physics-worker.js'
|
||||
└── workers/
|
||||
├── physics-worker.js # 自动生成
|
||||
└── worker-mapping.json # 自动生成
|
||||
```
|
||||
|
||||
### 临时禁用 Worker
|
||||
|
||||
如果需要临时禁用 Worker(例如调试时),有两种方式:
|
||||
|
||||
#### 方式 1:配置禁用
|
||||
|
||||
```typescript
|
||||
constructor() {
|
||||
super(matcher, {
|
||||
enableWorker: false, // 禁用 Worker,使用主线程处理
|
||||
// ...
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
#### 方式 2:平台适配器禁用
|
||||
|
||||
在自定义平台适配器中返回不支持 Worker:
|
||||
|
||||
```typescript
|
||||
class MyPlatformAdapter implements IPlatformAdapter {
|
||||
isWorkerSupported(): boolean {
|
||||
return false; // 返回 false 禁用 Worker
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 注意事项
|
||||
|
||||
1. **每次修改 `workerProcess` 后都需要重新运行 CLI 工具**生成新的 Worker 文件
|
||||
|
||||
2. **Worker 函数必须是纯函数**,不能依赖 `this` 或外部变量:
|
||||
```typescript
|
||||
// ✅ 正确:只使用参数
|
||||
protected workerProcess(entities, deltaTime, config) {
|
||||
return entities.map(e => {
|
||||
e.y += config.gravity * deltaTime;
|
||||
return e;
|
||||
});
|
||||
}
|
||||
|
||||
// ❌ 错误:使用 this
|
||||
protected workerProcess(entities, deltaTime, config) {
|
||||
return entities.map(e => {
|
||||
e.y += this.gravity * deltaTime; // Worker 中无法访问 this
|
||||
return e;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
3. **配置数据通过 `systemConfig` 传递**,而不是类属性
|
||||
|
||||
4. **开发者工具中的警告可以忽略**:
|
||||
- `getNetworkType:fail not support` - 微信开发者工具内部行为
|
||||
- `SharedArrayBuffer will require cross-origin isolation` - 开发环境警告,真机不会出现
|
||||
|
||||
Worker系统为ECS框架提供了强大的并行计算能力,让你能够充分利用现代多核处理器的性能,为复杂的游戏逻辑和计算密集型任务提供了高效的解决方案。
|
||||
@@ -1,761 +0,0 @@
|
||||
# WorldManager
|
||||
|
||||
WorldManager 是 ECS Framework 提供的高级世界管理器,用于管理多个完全隔离的游戏世界(World)。每个 World 都是独立的 ECS 环境,可以包含多个场景。
|
||||
|
||||
## 适用场景
|
||||
|
||||
WorldManager 适合以下高级场景:
|
||||
- MMO 游戏服务器的多房间管理
|
||||
- 游戏大厅系统(每个游戏房间完全隔离)
|
||||
- 服务器端的多游戏实例
|
||||
- 需要完全隔离的多个游戏环境
|
||||
- 需要同时运行多个独立世界的应用
|
||||
|
||||
## 特点
|
||||
|
||||
- 多 World 管理,每个 World 完全独立
|
||||
- 每个 World 可以包含多个 Scene
|
||||
- 支持 World 的激活/停用
|
||||
- 自动清理空 World
|
||||
- World 级别的全局系统
|
||||
- 批量操作和查询
|
||||
|
||||
## 基本使用
|
||||
|
||||
### 初始化
|
||||
|
||||
WorldManager 是 Core 的内置服务,通过服务容器获取:
|
||||
|
||||
```typescript
|
||||
import { Core, WorldManager } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始化 Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 从服务容器获取 WorldManager(Core 已自动创建并注册)
|
||||
const worldManager = Core.services.resolve(WorldManager);
|
||||
```
|
||||
|
||||
### 创建 World
|
||||
|
||||
```typescript
|
||||
// 创建游戏房间 World
|
||||
const room1 = worldManager.createWorld('room_001', {
|
||||
name: 'GameRoom_001',
|
||||
maxScenes: 5,
|
||||
debug: true
|
||||
});
|
||||
|
||||
// 激活 World
|
||||
worldManager.setWorldActive('room_001', true);
|
||||
|
||||
// 创建更多房间
|
||||
const room2 = worldManager.createWorld('room_002', {
|
||||
name: 'GameRoom_002',
|
||||
maxScenes: 5
|
||||
});
|
||||
|
||||
worldManager.setWorldActive('room_002', true);
|
||||
```
|
||||
|
||||
### 游戏循环
|
||||
|
||||
在游戏循环中更新所有活跃的 World:
|
||||
|
||||
```typescript
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // 更新全局服务
|
||||
worldManager.updateAll(); // 更新所有活跃的 World
|
||||
}
|
||||
|
||||
// 启动游戏循环
|
||||
let lastTime = 0;
|
||||
setInterval(() => {
|
||||
const currentTime = Date.now();
|
||||
const deltaTime = (currentTime - lastTime) / 1000;
|
||||
lastTime = currentTime;
|
||||
|
||||
gameLoop(deltaTime);
|
||||
}, 16); // 60 FPS
|
||||
```
|
||||
|
||||
## World 管理
|
||||
|
||||
### 创建 World
|
||||
|
||||
```typescript
|
||||
// 基本创建
|
||||
const world = worldManager.createWorld('worldId');
|
||||
|
||||
// 带配置创建
|
||||
const world = worldManager.createWorld('worldId', {
|
||||
name: 'MyWorld',
|
||||
maxScenes: 10,
|
||||
autoCleanup: true,
|
||||
debug: true
|
||||
});
|
||||
```
|
||||
|
||||
**配置选项(IWorldConfig)**:
|
||||
- `name?: string` - World 名称
|
||||
- `maxScenes?: number` - 最大场景数量限制(默认 10)
|
||||
- `autoCleanup?: boolean` - 是否自动清理空场景(默认 true)
|
||||
- `debug?: boolean` - 是否启用调试模式(默认 false)
|
||||
|
||||
### 获取 World
|
||||
|
||||
```typescript
|
||||
// 通过 ID 获取
|
||||
const world = worldManager.getWorld('room_001');
|
||||
if (world) {
|
||||
console.log(`World: ${world.name}`);
|
||||
}
|
||||
|
||||
// 获取所有 World
|
||||
const allWorlds = worldManager.getAllWorlds();
|
||||
console.log(`共有 ${allWorlds.length} 个 World`);
|
||||
|
||||
// 获取所有 World ID
|
||||
const worldIds = worldManager.getWorldIds();
|
||||
console.log('World 列表:', worldIds);
|
||||
|
||||
// 通过名称查找
|
||||
const world = worldManager.findWorldByName('GameRoom_001');
|
||||
```
|
||||
|
||||
### 激活和停用 World
|
||||
|
||||
```typescript
|
||||
// 激活 World(开始运行和更新)
|
||||
worldManager.setWorldActive('room_001', true);
|
||||
|
||||
// 停用 World(停止更新但保留数据)
|
||||
worldManager.setWorldActive('room_001', false);
|
||||
|
||||
// 检查 World 是否激活
|
||||
if (worldManager.isWorldActive('room_001')) {
|
||||
console.log('房间正在运行');
|
||||
}
|
||||
|
||||
// 获取所有活跃的 World
|
||||
const activeWorlds = worldManager.getActiveWorlds();
|
||||
console.log(`当前有 ${activeWorlds.length} 个活跃 World`);
|
||||
```
|
||||
|
||||
### 移除 World
|
||||
|
||||
```typescript
|
||||
// 移除 World(会自动停用并销毁)
|
||||
const removed = worldManager.removeWorld('room_001');
|
||||
if (removed) {
|
||||
console.log('World 已移除');
|
||||
}
|
||||
```
|
||||
|
||||
## World 中的场景管理
|
||||
|
||||
每个 World 可以包含多个 Scene 并独立管理它们的生命周期。
|
||||
|
||||
### 创建场景
|
||||
|
||||
```typescript
|
||||
const world = worldManager.getWorld('room_001');
|
||||
if (!world) return;
|
||||
|
||||
// 创建场景
|
||||
const mainScene = world.createScene('main', new MainScene());
|
||||
const uiScene = world.createScene('ui', new UIScene());
|
||||
const hudScene = world.createScene('hud', new HUDScene());
|
||||
|
||||
// 激活场景
|
||||
world.setSceneActive('main', true);
|
||||
world.setSceneActive('ui', true);
|
||||
world.setSceneActive('hud', false);
|
||||
```
|
||||
|
||||
### 查询场景
|
||||
|
||||
```typescript
|
||||
// 获取特定场景
|
||||
const mainScene = world.getScene<MainScene>('main');
|
||||
if (mainScene) {
|
||||
console.log(`场景名称: ${mainScene.name}`);
|
||||
}
|
||||
|
||||
// 获取所有场景
|
||||
const allScenes = world.getAllScenes();
|
||||
console.log(`World 中共有 ${allScenes.length} 个场景`);
|
||||
|
||||
// 获取所有场景 ID
|
||||
const sceneIds = world.getSceneIds();
|
||||
console.log('场景列表:', sceneIds);
|
||||
|
||||
// 获取活跃场景数量
|
||||
const activeCount = world.getActiveSceneCount();
|
||||
console.log(`当前有 ${activeCount} 个活跃场景`);
|
||||
|
||||
// 检查场景是否激活
|
||||
if (world.isSceneActive('main')) {
|
||||
console.log('主场景正在运行');
|
||||
}
|
||||
```
|
||||
|
||||
### 场景切换
|
||||
|
||||
World 支持多场景同时运行,也支持场景切换:
|
||||
|
||||
```typescript
|
||||
class GameWorld {
|
||||
private world: World;
|
||||
|
||||
constructor(worldManager: WorldManager) {
|
||||
this.world = worldManager.createWorld('game', {
|
||||
name: 'GameWorld',
|
||||
maxScenes: 5
|
||||
});
|
||||
|
||||
// 创建所有场景
|
||||
this.world.createScene('menu', new MenuScene());
|
||||
this.world.createScene('game', new GameScene());
|
||||
this.world.createScene('pause', new PauseScene());
|
||||
this.world.createScene('gameover', new GameOverScene());
|
||||
|
||||
// 激活 World
|
||||
worldManager.setWorldActive('game', true);
|
||||
}
|
||||
|
||||
public showMenu(): void {
|
||||
this.deactivateAllScenes();
|
||||
this.world.setSceneActive('menu', true);
|
||||
}
|
||||
|
||||
public startGame(): void {
|
||||
this.deactivateAllScenes();
|
||||
this.world.setSceneActive('game', true);
|
||||
}
|
||||
|
||||
public pauseGame(): void {
|
||||
// 游戏场景继续存在但停止更新
|
||||
this.world.setSceneActive('game', false);
|
||||
// 显示暂停界面
|
||||
this.world.setSceneActive('pause', true);
|
||||
}
|
||||
|
||||
public resumeGame(): void {
|
||||
this.world.setSceneActive('pause', false);
|
||||
this.world.setSceneActive('game', true);
|
||||
}
|
||||
|
||||
public showGameOver(): void {
|
||||
this.deactivateAllScenes();
|
||||
this.world.setSceneActive('gameover', true);
|
||||
}
|
||||
|
||||
private deactivateAllScenes(): void {
|
||||
const sceneIds = this.world.getSceneIds();
|
||||
sceneIds.forEach(id => this.world.setSceneActive(id, false));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 移除场景
|
||||
|
||||
```typescript
|
||||
// 移除不再需要的场景
|
||||
const removed = world.removeScene('oldScene');
|
||||
if (removed) {
|
||||
console.log('场景已移除');
|
||||
}
|
||||
|
||||
// 场景会自动调用 end() 方法进行清理
|
||||
```
|
||||
|
||||
## 全局系统
|
||||
|
||||
World 支持全局系统,这些系统在 World 级别运行,不依赖特定 Scene。
|
||||
|
||||
### 定义全局系统
|
||||
|
||||
```typescript
|
||||
import { IGlobalSystem } from '@esengine/ecs-framework';
|
||||
|
||||
// 网络系统(World 级别)
|
||||
class NetworkSystem implements IGlobalSystem {
|
||||
readonly name = 'NetworkSystem';
|
||||
|
||||
private connectionId: string;
|
||||
|
||||
constructor(connectionId: string) {
|
||||
this.connectionId = connectionId;
|
||||
}
|
||||
|
||||
initialize(): void {
|
||||
console.log(`网络系统初始化: ${this.connectionId}`);
|
||||
// 建立网络连接
|
||||
}
|
||||
|
||||
update(deltaTime?: number): void {
|
||||
// 处理网络消息,不依赖任何 Scene
|
||||
// 接收和发送网络包
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
console.log(`网络系统销毁: ${this.connectionId}`);
|
||||
// 关闭网络连接
|
||||
}
|
||||
}
|
||||
|
||||
// 物理系统(World 级别)
|
||||
class PhysicsSystem implements IGlobalSystem {
|
||||
readonly name = 'PhysicsSystem';
|
||||
|
||||
initialize(): void {
|
||||
console.log('物理系统初始化');
|
||||
}
|
||||
|
||||
update(deltaTime?: number): void {
|
||||
// 物理模拟,作用于 World 中所有场景
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
console.log('物理系统销毁');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用全局系统
|
||||
|
||||
```typescript
|
||||
const world = worldManager.getWorld('room_001');
|
||||
if (!world) return;
|
||||
|
||||
// 添加全局系统
|
||||
const networkSystem = world.addGlobalSystem(new NetworkSystem('conn_001'));
|
||||
const physicsSystem = world.addGlobalSystem(new PhysicsSystem());
|
||||
|
||||
// 获取全局系统
|
||||
const network = world.getGlobalSystem(NetworkSystem);
|
||||
if (network) {
|
||||
console.log('找到网络系统');
|
||||
}
|
||||
|
||||
// 移除全局系统
|
||||
world.removeGlobalSystem(networkSystem);
|
||||
```
|
||||
|
||||
## 批量操作
|
||||
|
||||
### 更新所有 World
|
||||
|
||||
```typescript
|
||||
// 更新所有活跃的 World(应该在游戏循环中调用)
|
||||
worldManager.updateAll();
|
||||
|
||||
// 这会自动更新每个 World 的:
|
||||
// 1. 全局系统
|
||||
// 2. 所有活跃场景
|
||||
```
|
||||
|
||||
### 启动和停止
|
||||
|
||||
```typescript
|
||||
// 启动所有 World
|
||||
worldManager.startAll();
|
||||
|
||||
// 停止所有 World
|
||||
worldManager.stopAll();
|
||||
|
||||
// 检查是否正在运行
|
||||
if (worldManager.isRunning) {
|
||||
console.log('WorldManager 正在运行');
|
||||
}
|
||||
```
|
||||
|
||||
### 查找 World
|
||||
|
||||
```typescript
|
||||
// 使用条件查找
|
||||
const emptyWorlds = worldManager.findWorlds(world => {
|
||||
return world.sceneCount === 0;
|
||||
});
|
||||
|
||||
// 查找活跃的 World
|
||||
const activeWorlds = worldManager.findWorlds(world => {
|
||||
return world.isActive;
|
||||
});
|
||||
|
||||
// 查找特定名称的 World
|
||||
const world = worldManager.findWorldByName('GameRoom_001');
|
||||
```
|
||||
|
||||
## 统计和监控
|
||||
|
||||
### 获取统计信息
|
||||
|
||||
```typescript
|
||||
const stats = worldManager.getStats();
|
||||
|
||||
console.log(`总 World 数: ${stats.totalWorlds}`);
|
||||
console.log(`活跃 World 数: ${stats.activeWorlds}`);
|
||||
console.log(`总场景数: ${stats.totalScenes}`);
|
||||
console.log(`总实体数: ${stats.totalEntities}`);
|
||||
console.log(`总系统数: ${stats.totalSystems}`);
|
||||
|
||||
// 查看每个 World 的详细信息
|
||||
stats.worlds.forEach(worldInfo => {
|
||||
console.log(`World: ${worldInfo.name}`);
|
||||
console.log(` 场景数: ${worldInfo.sceneCount}`);
|
||||
console.log(` 是否活跃: ${worldInfo.isActive}`);
|
||||
});
|
||||
```
|
||||
|
||||
### 获取详细状态
|
||||
|
||||
```typescript
|
||||
const status = worldManager.getDetailedStatus();
|
||||
|
||||
// 包含所有 World 的详细状态
|
||||
status.worlds.forEach(worldStatus => {
|
||||
console.log(`World ID: ${worldStatus.id}`);
|
||||
console.log(`状态:`, worldStatus.status);
|
||||
});
|
||||
```
|
||||
|
||||
## 自动清理
|
||||
|
||||
WorldManager 支持自动清理空的 World。
|
||||
|
||||
### 配置清理
|
||||
|
||||
```typescript
|
||||
// 创建带清理配置的 WorldManager
|
||||
const worldManager = Core.services.resolve(WorldManager);
|
||||
|
||||
// WorldManager 的配置在 Core 中设置:
|
||||
// {
|
||||
// maxWorlds: 50,
|
||||
// autoCleanup: true,
|
||||
// cleanupFrameInterval: 1800 // 间隔多少帧清理闲置 World
|
||||
// }
|
||||
```
|
||||
|
||||
### 手动清理
|
||||
|
||||
```typescript
|
||||
// 手动触发清理
|
||||
const cleanedCount = worldManager.cleanup();
|
||||
console.log(`清理了 ${cleanedCount} 个 World`);
|
||||
```
|
||||
|
||||
**清理条件**:
|
||||
- World 未激活
|
||||
- 没有 Scene 或所有 Scene 都是空的
|
||||
- 创建时间超过 10 分钟
|
||||
|
||||
## API 参考
|
||||
|
||||
### WorldManager API
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `createWorld(worldId, config?)` | 创建新 World |
|
||||
| `removeWorld(worldId)` | 移除 World |
|
||||
| `getWorld(worldId)` | 获取 World |
|
||||
| `getAllWorlds()` | 获取所有 World |
|
||||
| `getWorldIds()` | 获取所有 World ID |
|
||||
| `setWorldActive(worldId, active)` | 设置 World 激活状态 |
|
||||
| `isWorldActive(worldId)` | 检查 World 是否激活 |
|
||||
| `getActiveWorlds()` | 获取所有活跃的 World |
|
||||
| `updateAll()` | 更新所有活跃 World |
|
||||
| `startAll()` | 启动所有 World |
|
||||
| `stopAll()` | 停止所有 World |
|
||||
| `findWorlds(predicate)` | 查找满足条件的 World |
|
||||
| `findWorldByName(name)` | 根据名称查找 World |
|
||||
| `getStats()` | 获取统计信息 |
|
||||
| `getDetailedStatus()` | 获取详细状态信息 |
|
||||
| `cleanup()` | 清理空 World |
|
||||
| `destroy()` | 销毁 WorldManager |
|
||||
|
||||
### World API
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `createScene(sceneId, sceneInstance?)` | 创建并添加 Scene |
|
||||
| `removeScene(sceneId)` | 移除 Scene |
|
||||
| `getScene(sceneId)` | 获取 Scene |
|
||||
| `getAllScenes()` | 获取所有 Scene |
|
||||
| `getSceneIds()` | 获取所有 Scene ID |
|
||||
| `setSceneActive(sceneId, active)` | 设置 Scene 激活状态 |
|
||||
| `isSceneActive(sceneId)` | 检查 Scene 是否激活 |
|
||||
| `getActiveSceneCount()` | 获取活跃 Scene 数量 |
|
||||
| `addGlobalSystem(system)` | 添加全局系统 |
|
||||
| `removeGlobalSystem(system)` | 移除全局系统 |
|
||||
| `getGlobalSystem(type)` | 获取全局系统 |
|
||||
| `start()` | 启动 World |
|
||||
| `stop()` | 停止 World |
|
||||
| `updateGlobalSystems()` | 更新全局系统 |
|
||||
| `updateScenes()` | 更新所有激活 Scene |
|
||||
| `destroy()` | 销毁 World |
|
||||
| `getStatus()` | 获取 World 状态 |
|
||||
| `getStats()` | 获取统计信息 |
|
||||
|
||||
### 属性
|
||||
|
||||
| 属性 | 说明 |
|
||||
|------|------|
|
||||
| `worldCount` | World 总数 |
|
||||
| `activeWorldCount` | 活跃 World 数量 |
|
||||
| `isRunning` | 是否正在运行 |
|
||||
| `config` | 配置信息 |
|
||||
|
||||
## 完整示例
|
||||
|
||||
### MMO 游戏房间系统
|
||||
|
||||
```typescript
|
||||
import { Core, WorldManager, Scene, World } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始化
|
||||
Core.create({ debug: true });
|
||||
const worldManager = Core.services.resolve(WorldManager);
|
||||
|
||||
// 房间管理器
|
||||
class RoomManager {
|
||||
private worldManager: WorldManager;
|
||||
private rooms: Map<string, World> = new Map();
|
||||
|
||||
constructor(worldManager: WorldManager) {
|
||||
this.worldManager = worldManager;
|
||||
}
|
||||
|
||||
// 创建游戏房间
|
||||
public createRoom(roomId: string, maxPlayers: number): World {
|
||||
const world = this.worldManager.createWorld(roomId, {
|
||||
name: `Room_${roomId}`,
|
||||
maxScenes: 3,
|
||||
debug: true
|
||||
});
|
||||
|
||||
// 创建房间场景
|
||||
world.createScene('lobby', new LobbyScene());
|
||||
world.createScene('game', new GameScene());
|
||||
world.createScene('result', new ResultScene());
|
||||
|
||||
// 添加房间级别的系统
|
||||
world.addGlobalSystem(new NetworkSystem(roomId));
|
||||
world.addGlobalSystem(new RoomLogicSystem(maxPlayers));
|
||||
|
||||
// 激活 World 和初始场景
|
||||
this.worldManager.setWorldActive(roomId, true);
|
||||
world.setSceneActive('lobby', true);
|
||||
|
||||
this.rooms.set(roomId, world);
|
||||
console.log(`房间 ${roomId} 已创建`);
|
||||
|
||||
return world;
|
||||
}
|
||||
|
||||
// 玩家加入房间
|
||||
public joinRoom(roomId: string, playerId: string): boolean {
|
||||
const world = this.rooms.get(roomId);
|
||||
if (!world) {
|
||||
console.log(`房间 ${roomId} 不存在`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 在大厅场景中创建玩家实体
|
||||
const lobbyScene = world.getScene('lobby');
|
||||
if (lobbyScene) {
|
||||
const player = lobbyScene.createEntity(`Player_${playerId}`);
|
||||
// 添加玩家组件...
|
||||
console.log(`玩家 ${playerId} 加入房间 ${roomId}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// 开始游戏
|
||||
public startGame(roomId: string): void {
|
||||
const world = this.rooms.get(roomId);
|
||||
if (!world) return;
|
||||
|
||||
// 切换到游戏场景
|
||||
world.setSceneActive('lobby', false);
|
||||
world.setSceneActive('game', true);
|
||||
|
||||
console.log(`房间 ${roomId} 游戏开始`);
|
||||
}
|
||||
|
||||
// 结束游戏
|
||||
public endGame(roomId: string): void {
|
||||
const world = this.rooms.get(roomId);
|
||||
if (!world) return;
|
||||
|
||||
// 切换到结果场景
|
||||
world.setSceneActive('game', false);
|
||||
world.setSceneActive('result', true);
|
||||
|
||||
console.log(`房间 ${roomId} 游戏结束`);
|
||||
}
|
||||
|
||||
// 关闭房间
|
||||
public closeRoom(roomId: string): void {
|
||||
this.worldManager.removeWorld(roomId);
|
||||
this.rooms.delete(roomId);
|
||||
console.log(`房间 ${roomId} 已关闭`);
|
||||
}
|
||||
|
||||
// 获取房间列表
|
||||
public getRoomList(): string[] {
|
||||
return Array.from(this.rooms.keys());
|
||||
}
|
||||
|
||||
// 获取房间统计
|
||||
public getRoomStats(roomId: string) {
|
||||
const world = this.rooms.get(roomId);
|
||||
return world?.getStats();
|
||||
}
|
||||
}
|
||||
|
||||
// 使用房间管理器
|
||||
const roomManager = new RoomManager(worldManager);
|
||||
|
||||
// 创建多个游戏房间
|
||||
roomManager.createRoom('room_001', 4);
|
||||
roomManager.createRoom('room_002', 4);
|
||||
roomManager.createRoom('room_003', 2);
|
||||
|
||||
// 玩家加入
|
||||
roomManager.joinRoom('room_001', 'player_1');
|
||||
roomManager.joinRoom('room_001', 'player_2');
|
||||
|
||||
// 开始游戏
|
||||
roomManager.startGame('room_001');
|
||||
|
||||
// 游戏循环
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime);
|
||||
worldManager.updateAll(); // 更新所有房间
|
||||
}
|
||||
|
||||
// 定期清理空房间
|
||||
setInterval(() => {
|
||||
const stats = worldManager.getStats();
|
||||
console.log(`当前房间数: ${stats.totalWorlds}`);
|
||||
console.log(`活跃房间数: ${stats.activeWorlds}`);
|
||||
|
||||
worldManager.cleanup();
|
||||
}, 60000); // 每分钟清理一次
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 合理的 World 粒度
|
||||
|
||||
```typescript
|
||||
// 推荐:每个独立环境一个 World
|
||||
const room1 = worldManager.createWorld('room_1'); // 游戏房间1
|
||||
const room2 = worldManager.createWorld('room_2'); // 游戏房间2
|
||||
|
||||
// 不推荐:过度使用 World
|
||||
const world1 = worldManager.createWorld('ui'); // UI 不需要独立 World
|
||||
const world2 = worldManager.createWorld('menu'); // 菜单不需要独立 World
|
||||
```
|
||||
|
||||
### 2. 使用全局系统处理跨场景逻辑
|
||||
|
||||
```typescript
|
||||
// 推荐:World 级别的系统
|
||||
class NetworkSystem implements IGlobalSystem {
|
||||
update() {
|
||||
// 网络处理不依赖场景
|
||||
}
|
||||
}
|
||||
|
||||
// 不推荐:在每个场景中重复创建
|
||||
class GameScene extends Scene {
|
||||
initialize() {
|
||||
this.addSystem(new NetworkSystem()); // 不应该在场景级别
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 及时清理不用的 World
|
||||
|
||||
```typescript
|
||||
// 推荐:玩家离开时清理房间
|
||||
function onPlayerLeave(roomId: string) {
|
||||
const world = worldManager.getWorld(roomId);
|
||||
if (world && world.sceneCount === 0) {
|
||||
worldManager.removeWorld(roomId);
|
||||
}
|
||||
}
|
||||
|
||||
// 或使用自动清理
|
||||
worldManager.cleanup();
|
||||
```
|
||||
|
||||
### 4. 监控资源使用
|
||||
|
||||
```typescript
|
||||
// 定期检查资源使用情况
|
||||
setInterval(() => {
|
||||
const stats = worldManager.getStats();
|
||||
|
||||
if (stats.totalWorlds > 100) {
|
||||
console.warn('World 数量过多,考虑清理');
|
||||
worldManager.cleanup();
|
||||
}
|
||||
|
||||
if (stats.totalEntities > 10000) {
|
||||
console.warn('实体数量过多,检查是否有泄漏');
|
||||
}
|
||||
}, 30000);
|
||||
```
|
||||
|
||||
## 与 SceneManager 的对比
|
||||
|
||||
| 特性 | SceneManager | WorldManager |
|
||||
|------|--------------|--------------|
|
||||
| 适用场景 | 95% 的游戏应用 | 高级多世界隔离场景 |
|
||||
| 复杂度 | 简单 | 复杂 |
|
||||
| 场景数量 | 单场景(可切换) | 多 World,每个 World 多场景 |
|
||||
| 场景隔离 | 无(场景切换) | 完全隔离(每个 World 独立) |
|
||||
| 性能开销 | 最小 | 较高 |
|
||||
| 全局系统 | 无 | 支持(World 级别) |
|
||||
| 使用示例 | 单人游戏、移动游戏 | MMO 服务器、游戏房间系统 |
|
||||
|
||||
**何时使用 WorldManager**:
|
||||
- MMO 游戏服务器(每个房间一个 World)
|
||||
- 游戏大厅系统(每个游戏房间完全隔离)
|
||||
- 需要运行多个完全独立的游戏实例
|
||||
- 服务器端模拟多个游戏世界
|
||||
|
||||
**何时使用 SceneManager**:
|
||||
- 单人游戏
|
||||
- 简单的多人游戏
|
||||
- 移动游戏
|
||||
- 场景之间需要切换但不需要同时运行
|
||||
|
||||
## 架构层次
|
||||
|
||||
WorldManager 在 ECS Framework 中的位置:
|
||||
|
||||
```
|
||||
Core (全局服务)
|
||||
└── WorldManager (世界管理)
|
||||
├── World 1 (游戏房间1)
|
||||
│ ├── GlobalSystem (全局系统)
|
||||
│ ├── Scene 1 (场景1)
|
||||
│ │ ├── EntitySystem
|
||||
│ │ ├── Entity
|
||||
│ │ └── Component
|
||||
│ └── Scene 2 (场景2)
|
||||
├── World 2 (游戏房间2)
|
||||
│ ├── GlobalSystem
|
||||
│ └── Scene 1
|
||||
└── World 3 (游戏房间3)
|
||||
```
|
||||
|
||||
WorldManager 为需要多世界隔离的高级应用提供了强大的管理能力。如果你的应用不需要多世界隔离,建议使用更简单的 [SceneManager](./scene-manager.md)。
|
||||
317
docs/index.md
317
docs/index.md
@@ -1,317 +0,0 @@
|
||||
---
|
||||
layout: page
|
||||
title: ESEngine - 高性能 TypeScript ECS 框架
|
||||
---
|
||||
|
||||
<ParticleHero />
|
||||
|
||||
<section class="news-section">
|
||||
<div class="news-container">
|
||||
<div class="news-header">
|
||||
<h2 class="news-title">快速入口</h2>
|
||||
<a href="/guide/" class="news-more">查看文档</a>
|
||||
</div>
|
||||
<div class="news-grid">
|
||||
<a href="/guide/getting-started" class="news-card">
|
||||
<div class="news-card-image" style="background: linear-gradient(135deg, #1e3a5f 0%, #1e1e1e 100%);">
|
||||
<div class="news-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24"><path fill="#4fc1ff" d="M12 3L1 9l4 2.18v6L12 21l7-3.82v-6l2-1.09V17h2V9zm6.82 6L12 12.72L5.18 9L12 5.28zM17 16l-5 2.72L7 16v-3.73L12 15l5-2.73z"/></svg>
|
||||
</div>
|
||||
<span class="news-badge">快速开始</span>
|
||||
</div>
|
||||
<div class="news-card-content">
|
||||
<h3>5 分钟上手 ESEngine</h3>
|
||||
<p>从安装到创建第一个 ECS 应用,快速了解核心概念。</p>
|
||||
</div>
|
||||
</a>
|
||||
<a href="/guide/behavior-tree/" class="news-card">
|
||||
<div class="news-card-image" style="background: linear-gradient(135deg, #1e3a5f 0%, #1e1e1e 100%);">
|
||||
<div class="news-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24"><path fill="#4ec9b0" d="M12 2a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m3 20h-1v-7l-2-2l-2 2v7H9v-7.5l-2 2V22H6v-6l3-3l1-3.5c-.3.4-.6.7-1 1L6 9v1H4V8l5-3c.5-.3 1.1-.5 1.7-.5H11c.6 0 1.2.2 1.7.5l5 3v2h-2V9l-3 1.5c-.4-.3-.7-.6-1-1l1 3.5l3 3v6Z"/></svg>
|
||||
</div>
|
||||
<span class="news-badge">AI 系统</span>
|
||||
</div>
|
||||
<div class="news-card-content">
|
||||
<h3>行为树可视化编辑器</h3>
|
||||
<p>内置 AI 行为树系统,支持可视化编辑和实时调试。</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="features-section">
|
||||
<div class="features-container">
|
||||
<h2 class="features-title">核心特性</h2>
|
||||
<div class="features-grid">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#4fc1ff" d="M13 2.05v2.02c3.95.49 7 3.85 7 7.93c0 1.45-.39 2.79-1.06 3.95l1.59 1.09A9.94 9.94 0 0 0 22 12c0-5.18-3.95-9.45-9-9.95M12 19c-3.87 0-7-3.13-7-7c0-3.53 2.61-6.43 6-6.92V2.05c-5.06.5-9 4.76-9 9.95c0 5.52 4.47 10 9.99 10c3.31 0 6.24-1.61 8.06-4.09l-1.6-1.1A7.93 7.93 0 0 1 12 19"/><path fill="#4fc1ff" d="M12 6a6 6 0 0 0-6 6c0 3.31 2.69 6 6 6a6 6 0 0 0 0-12m0 10c-2.21 0-4-1.79-4-4s1.79-4 4-4s4 1.79 4 4s-1.79 4-4 4"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">高性能 ECS 架构</h3>
|
||||
<p class="feature-desc">基于数据驱动的实体组件系统,支持大规模实体处理,缓存友好的内存布局。</p>
|
||||
<a href="/guide/entity" class="feature-link">了解更多 →</a>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#569cd6" d="M3 3h18v18H3zm16.525 13.707c0-.795-.272-1.425-.816-1.89c-.544-.465-1.404-.804-2.58-1.016l-1.704-.296c-.616-.104-1.052-.26-1.308-.468c-.256-.21-.384-.468-.384-.776c0-.392.168-.7.504-.924c.336-.224.8-.336 1.392-.336c.56 0 1.008.124 1.344.372c.336.248.536.584.6 1.008h2.016c-.08-.96-.464-1.716-1.152-2.268c-.688-.552-1.6-.828-2.736-.828c-1.2 0-2.148.3-2.844.9c-.696.6-1.044 1.38-1.044 2.34c0 .76.252 1.368.756 1.824c.504.456 1.308.792 2.412.996l1.704.312c.624.12 1.068.28 1.332.48c.264.2.396.46.396.78c0 .424-.192.756-.576.996c-.384.24-.9.36-1.548.36c-.672 0-1.2-.14-1.584-.42c-.384-.28-.608-.668-.672-1.164H8.868c.048 1.016.46 1.808 1.236 2.376c.776.568 1.796.852 3.06.852c1.24 0 2.22-.292 2.94-.876c.72-.584 1.08-1.364 1.08-2.34z"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">完整类型支持</h3>
|
||||
<p class="feature-desc">100% TypeScript 编写,完整的类型定义和编译时检查,提供最佳的开发体验。</p>
|
||||
<a href="/guide/component" class="feature-link">了解更多 →</a>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#4ec9b0" d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10s10-4.5 10-10S17.5 2 12 2m0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8s8 3.59 8 8s-3.59 8-8 8m-5-8l4-4v3h4v2h-4v3z"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">可视化行为树</h3>
|
||||
<p class="feature-desc">内置 AI 行为树系统,提供可视化编辑器,支持自定义节点和实时调试。</p>
|
||||
<a href="/guide/behavior-tree/" class="feature-link">了解更多 →</a>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#c586c0" d="M4 6h18V4H4c-1.1 0-2 .9-2 2v11H0v3h14v-3H4zm19 2h-6c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h6c.55 0 1-.45 1-1V9c0-.55-.45-1-1-1m-1 9h-4v-7h4z"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">多平台支持</h3>
|
||||
<p class="feature-desc">支持浏览器、Node.js、微信小游戏等多平台,可与主流游戏引擎无缝集成。</p>
|
||||
<a href="/guide/platform-adapter" class="feature-link">了解更多 →</a>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#dcdcaa" d="M4 3h6v2H4v14h6v2H4c-1.1 0-2-.9-2-2V5c0-1.1.9-2 2-2m9 0h6c1.1 0 2 .9 2 2v14c0 1.1-.9 2-2 2h-6v-2h6V5h-6zm-1 7h4v2h-4z"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">模块化设计</h3>
|
||||
<p class="feature-desc">核心功能独立打包,按需引入。支持自定义插件扩展,灵活适配不同项目。</p>
|
||||
<a href="/guide/plugin-system" class="feature-link">了解更多 →</a>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#9cdcfe" d="M22.7 19l-9.1-9.1c.9-2.3.4-5-1.5-6.9c-2-2-5-2.4-7.4-1.3L9 6L6 9L1.6 4.7C.4 7.1.9 10.1 2.9 12.1c1.9 1.9 4.6 2.4 6.9 1.5l9.1 9.1c.4.4 1 .4 1.4 0l2.3-2.3c.5-.4.5-1.1.1-1.4"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">开发者工具</h3>
|
||||
<p class="feature-desc">内置性能监控、调试工具、序列化系统等,提供完整的开发工具链。</p>
|
||||
<a href="/guide/logging" class="feature-link">了解更多 →</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style scoped>
|
||||
/* 首页专用样式 | Home page specific styles */
|
||||
.news-section {
|
||||
background: #0d0d0d;
|
||||
padding: 64px 0;
|
||||
border-top: 1px solid #2a2a2a;
|
||||
}
|
||||
|
||||
.news-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 48px;
|
||||
}
|
||||
|
||||
.news-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.news-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.news-more {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 20px;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 6px;
|
||||
color: #a0a0a0;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.news-more:hover {
|
||||
background: #252525;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.news-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.news-card {
|
||||
display: flex;
|
||||
background: #1f1f1f;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.news-card:hover {
|
||||
border-color: #3b9eff;
|
||||
}
|
||||
|
||||
.news-card-image {
|
||||
width: 200px;
|
||||
min-height: 140px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.news-icon {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.news-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
background: transparent;
|
||||
border: 1px solid #3a3a3a;
|
||||
border-radius: 16px;
|
||||
color: #a0a0a0;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.news-card-content {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.news-card-content h3 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.news-card-content p {
|
||||
font-size: 0.875rem;
|
||||
color: #707070;
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.features-section {
|
||||
background: #0d0d0d;
|
||||
padding: 64px 0;
|
||||
}
|
||||
|
||||
.features-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 48px;
|
||||
}
|
||||
|
||||
.features-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
margin: 0 0 32px 0;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: #1f1f1f;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
border-color: #3b9eff;
|
||||
background: #252525;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: #0d0d0d;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.feature-desc {
|
||||
font-size: 14px;
|
||||
color: #707070;
|
||||
line-height: 1.7;
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
|
||||
.feature-link {
|
||||
font-size: 14px;
|
||||
color: #3b9eff;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.feature-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.news-container,
|
||||
.features-container {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.news-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.news-card {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.news-card-image {
|
||||
width: 100%;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,507 +0,0 @@
|
||||
# 蓝图可视化脚本 (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 中执行重计算
|
||||
@@ -1,337 +0,0 @@
|
||||
# 状态机 (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` - 重置状态机
|
||||
@@ -1,727 +0,0 @@
|
||||
# 网络同步系统 (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('已断开');
|
||||
// 处理重连逻辑
|
||||
}
|
||||
});
|
||||
```
|
||||
@@ -1,502 +0,0 @@
|
||||
# 寻路系统 (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) | 较低 (多边形数) |
|
||||
| 精度 | 网格对齐 | 连续坐标 |
|
||||
| 动态修改 | 容易 | 需要重建 |
|
||||
| 设置复杂度 | 简单 | 较复杂 |
|
||||
@@ -1,557 +0,0 @@
|
||||
# 程序化生成 (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,控制频率增长速度
|
||||
@@ -1,600 +0,0 @@
|
||||
# 空间索引系统 (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
|
||||
// 如果有大量对象同时移动,考虑禁用事件后批量更新
|
||||
```
|
||||
@@ -1,479 +0,0 @@
|
||||
# 定时器系统 (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-1,0=刚开始,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-1(0=刚开始,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
|
||||
});
|
||||
```
|
||||
23
docs/package.json
Normal file
23
docs/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "@esengine/docs",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/starlight": "^0.37.1",
|
||||
"@astrojs/vue": "^5.1.3",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"astro": "^5.6.1",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"sharp": "^0.34.2",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"vue": "^3.5.26"
|
||||
}
|
||||
}
|
||||
1
docs/public/favicon.svg
Normal file
1
docs/public/favicon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill-rule="evenodd" d="M81 36 64 0 47 36l-1 2-9-10a6 6 0 0 0-9 9l10 10h-2L0 64l36 17h2L28 91a6 6 0 1 0 9 9l9-10 1 2 17 36 17-36v-2l9 10a6 6 0 1 0 9-9l-9-9 2-1 36-17-36-17-2-1 9-9a6 6 0 1 0-9-9l-9 10v-2Zm-17 2-2 5c-4 8-11 15-19 19l-5 2 5 2c8 4 15 11 19 19l2 5 2-5c4-8 11-15 19-19l5-2-5-2c-8-4-15-11-19-19l-2-5Z" clip-rule="evenodd"/><path d="M118 19a6 6 0 0 0-9-9l-3 3a6 6 0 1 0 9 9l3-3Zm-96 4c-2 2-6 2-9 0l-3-3a6 6 0 1 1 9-9l3 3c3 2 3 6 0 9Zm0 82c-2-2-6-2-9 0l-3 3a6 6 0 1 0 9 9l3-3c3-2 3-6 0-9Zm96 4a6 6 0 0 1-9 9l-3-3a6 6 0 1 1 9-9l3 3Z"/><style>path{fill:#000}@media (prefers-color-scheme:dark){path{fill:#fff}}</style></svg>
|
||||
|
After Width: | Height: | Size: 696 B |
462
docs/public/js/blueprint-graph.js
Normal file
462
docs/public/js/blueprint-graph.js
Normal file
@@ -0,0 +1,462 @@
|
||||
/**
|
||||
* Blueprint Graph Renderer
|
||||
* Custom layout algorithm designed specifically for blueprint-style graphs
|
||||
*/
|
||||
(function() {
|
||||
const PIN_COLORS = {
|
||||
exec: '#ffffff',
|
||||
entity: '#00a0e0',
|
||||
component: '#7030c0',
|
||||
float: '#7ecd32',
|
||||
int: '#1cc4c4',
|
||||
bool: '#8c0000',
|
||||
string: '#e060e0',
|
||||
any: '#707070'
|
||||
};
|
||||
|
||||
const HEADER_CLASSES = {
|
||||
event: 'event',
|
||||
function: 'function',
|
||||
pure: 'pure',
|
||||
flow: 'flow',
|
||||
math: 'math',
|
||||
time: 'time',
|
||||
debug: 'debug',
|
||||
variable: 'variable'
|
||||
};
|
||||
|
||||
const H_GAP = 50; // Horizontal gap between columns
|
||||
const V_GAP = 25; // Vertical gap between nodes
|
||||
const START_X = 20;
|
||||
const START_Y = 20;
|
||||
|
||||
function estimateNodeSize(node) {
|
||||
const headerHeight = 28; // Match CSS: min-height 28px
|
||||
const pinRowHeight = 22; // Match CSS: pin row ~22px
|
||||
const bodyPadding = 12; // Top + bottom padding
|
||||
|
||||
// Count all pins in body (each pin is its own row now)
|
||||
const inputExecCount = node.inputs ? node.inputs.filter(p => p.type === 'exec').length : 0;
|
||||
const inputDataCount = node.inputs ? node.inputs.filter(p => p.type !== 'exec').length : 0;
|
||||
const outputExecCount = node.outputs ? node.outputs.filter(p => p.type === 'exec' && !p.inHeader).length : 0;
|
||||
const outputDataCount = node.outputs ? node.outputs.filter(p => p.type !== 'exec' && !p.inHeader).length : 0;
|
||||
const totalPins = inputExecCount + inputDataCount + outputExecCount + outputDataCount;
|
||||
|
||||
// Calculate height: header + body padding + all pin rows
|
||||
const bodyHeight = totalPins > 0 ? bodyPadding + (totalPins * pinRowHeight) : 0;
|
||||
const height = headerHeight + bodyHeight;
|
||||
|
||||
// Calculate width based on content
|
||||
let maxLabelLen = node.title.length;
|
||||
if (node.inputs) {
|
||||
node.inputs.forEach(p => {
|
||||
const len = (p.label || '').length + (p.value ? String(p.value).length + 3 : 0);
|
||||
maxLabelLen = Math.max(maxLabelLen, len);
|
||||
});
|
||||
}
|
||||
if (node.outputs) {
|
||||
node.outputs.forEach(p => {
|
||||
maxLabelLen = Math.max(maxLabelLen, (p.label || '').length);
|
||||
});
|
||||
}
|
||||
|
||||
const width = Math.max(110, Math.min(170, maxLabelLen * 8 + 40));
|
||||
return { width, height };
|
||||
}
|
||||
|
||||
/**
|
||||
* Smart blueprint layout algorithm
|
||||
*
|
||||
* Uses weighted graph analysis:
|
||||
* - All connections matter (exec has higher weight)
|
||||
* - Topological sort for X ordering
|
||||
* - Force-directed optimization for Y positions
|
||||
*/
|
||||
function autoLayout(graphData, maxWidth) {
|
||||
const nodes = graphData.nodes;
|
||||
const connections = graphData.connections;
|
||||
|
||||
if (nodes.length === 0) return { positions: {}, sizes: {} };
|
||||
|
||||
// Calculate node sizes
|
||||
const nodeSizes = {};
|
||||
nodes.forEach(n => { nodeSizes[n.id] = estimateNodeSize(n); });
|
||||
|
||||
// Build maps
|
||||
const pinToNode = {};
|
||||
const nodeById = {};
|
||||
nodes.forEach(n => {
|
||||
nodeById[n.id] = n;
|
||||
(n.inputs || []).forEach(p => { pinToNode[p.id] = n.id; });
|
||||
(n.outputs || []).forEach(p => { pinToNode[p.id] = n.id; });
|
||||
});
|
||||
|
||||
// Build weighted adjacency: outgoing[nodeId] = [{to, weight}]
|
||||
const outgoing = {};
|
||||
const incoming = {};
|
||||
nodes.forEach(n => { outgoing[n.id] = []; incoming[n.id] = []; });
|
||||
|
||||
connections.forEach(c => {
|
||||
const from = pinToNode[c.from];
|
||||
const to = pinToNode[c.to];
|
||||
if (!from || !to || from === to) return;
|
||||
|
||||
const weight = c.type === 'exec' ? 3 : 1;
|
||||
outgoing[from].push({ to, weight });
|
||||
incoming[to].push({ from, weight });
|
||||
});
|
||||
|
||||
// Calculate node "depth" using weighted longest path
|
||||
const nodeDepth = {};
|
||||
const visited = new Set();
|
||||
const inProcess = new Set();
|
||||
|
||||
function calcDepth(nodeId) {
|
||||
if (visited.has(nodeId)) return nodeDepth[nodeId];
|
||||
if (inProcess.has(nodeId)) return 0; // Cycle detected
|
||||
|
||||
inProcess.add(nodeId);
|
||||
|
||||
let maxPrevDepth = -1;
|
||||
incoming[nodeId].forEach(({ from, weight }) => {
|
||||
const prevDepth = calcDepth(from);
|
||||
maxPrevDepth = Math.max(maxPrevDepth, prevDepth);
|
||||
});
|
||||
|
||||
inProcess.delete(nodeId);
|
||||
visited.add(nodeId);
|
||||
nodeDepth[nodeId] = maxPrevDepth + 1;
|
||||
return nodeDepth[nodeId];
|
||||
}
|
||||
|
||||
// Calculate depth for all nodes
|
||||
nodes.forEach(n => calcDepth(n.id));
|
||||
|
||||
// Group nodes by depth (column)
|
||||
const columnNodes = {};
|
||||
nodes.forEach(n => {
|
||||
const depth = nodeDepth[n.id];
|
||||
if (!columnNodes[depth]) columnNodes[depth] = [];
|
||||
columnNodes[depth].push(n.id);
|
||||
});
|
||||
|
||||
// Sort columns
|
||||
const sortedColumns = Object.keys(columnNodes).map(Number).sort((a, b) => a - b);
|
||||
|
||||
// Calculate X positions
|
||||
const columnX = {};
|
||||
let currentX = START_X;
|
||||
sortedColumns.forEach(col => {
|
||||
columnX[col] = currentX;
|
||||
let maxW = 0;
|
||||
columnNodes[col].forEach(id => {
|
||||
maxW = Math.max(maxW, nodeSizes[id].width);
|
||||
});
|
||||
currentX += maxW + H_GAP;
|
||||
});
|
||||
|
||||
// Initialize Y positions - simple stacking first
|
||||
const positions = {};
|
||||
sortedColumns.forEach(col => {
|
||||
let y = START_Y;
|
||||
columnNodes[col].forEach(id => {
|
||||
positions[id] = { x: columnX[col], y };
|
||||
y += nodeSizes[id].height + V_GAP;
|
||||
});
|
||||
});
|
||||
|
||||
// Force-directed optimization for Y positions (few iterations)
|
||||
for (let iter = 0; iter < 5; iter++) {
|
||||
const forces = {};
|
||||
nodes.forEach(n => { forces[n.id] = 0; });
|
||||
|
||||
// Calculate forces from connections
|
||||
connections.forEach(c => {
|
||||
const from = pinToNode[c.from];
|
||||
const to = pinToNode[c.to];
|
||||
if (!from || !to || from === to) return;
|
||||
|
||||
const weight = c.type === 'exec' ? 2 : 1;
|
||||
const fromY = positions[from].y + nodeSizes[from].height / 2;
|
||||
const toY = positions[to].y + nodeSizes[to].height / 2;
|
||||
const diff = toY - fromY;
|
||||
|
||||
// Pull nodes toward each other
|
||||
forces[from] += diff * 0.1 * weight;
|
||||
forces[to] -= diff * 0.1 * weight;
|
||||
});
|
||||
|
||||
// Apply forces
|
||||
nodes.forEach(n => {
|
||||
positions[n.id].y += forces[n.id];
|
||||
positions[n.id].y = Math.max(START_Y, positions[n.id].y);
|
||||
});
|
||||
|
||||
// Resolve overlaps within columns
|
||||
sortedColumns.forEach(col => {
|
||||
const nodesInCol = columnNodes[col];
|
||||
nodesInCol.sort((a, b) => positions[a].y - positions[b].y);
|
||||
|
||||
for (let i = 1; i < nodesInCol.length; i++) {
|
||||
const prevId = nodesInCol[i - 1];
|
||||
const currId = nodesInCol[i];
|
||||
const minY = positions[prevId].y + nodeSizes[prevId].height + V_GAP;
|
||||
if (positions[currId].y < minY) {
|
||||
positions[currId].y = minY;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return { positions, sizes: nodeSizes };
|
||||
}
|
||||
|
||||
function renderPinSvg(type, filled = true) {
|
||||
const color = PIN_COLORS[type] || PIN_COLORS.any;
|
||||
if (type === 'exec') {
|
||||
return `<svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="${filled ? '#fff' : 'none'}" stroke="${filled ? 'none' : '#fff'}" stroke-width="2"/></svg>`;
|
||||
}
|
||||
return `<svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="${filled ? color : 'none'}" stroke="${filled ? 'none' : color}" stroke-width="2"/></svg>`;
|
||||
}
|
||||
|
||||
function renderNode(node, position, size) {
|
||||
const isEvent = node.category === 'event';
|
||||
const headerClass = HEADER_CLASSES[node.category] || 'function';
|
||||
|
||||
let html = `<div class="bp-node" style="left: ${position.x}px; top: ${position.y}px; width: ${size.width}px;">`;
|
||||
html += `<div class="bp-node-header ${headerClass}">`;
|
||||
if (isEvent) html += `<span class="bp-node-header-icon"></span>`;
|
||||
html += `<span class="bp-node-header-title">${node.title}</span>`;
|
||||
|
||||
const headerExec = node.outputs && node.outputs.find(p => p.type === 'exec' && p.inHeader);
|
||||
if (headerExec) {
|
||||
html += `<span class="bp-header-exec" data-pin="${headerExec.id}">${renderPinSvg('exec')}</span>`;
|
||||
}
|
||||
html += `</div>`;
|
||||
|
||||
// Separate exec and data pins (matching node-editor order)
|
||||
const inputExecPins = (node.inputs || []).filter(p => p.type === 'exec');
|
||||
const inputDataPins = (node.inputs || []).filter(p => p.type !== 'exec');
|
||||
const outputExecPins = (node.outputs || []).filter(p => p.type === 'exec' && !p.inHeader);
|
||||
const outputDataPins = (node.outputs || []).filter(p => p.type !== 'exec' && !p.inHeader);
|
||||
|
||||
const hasBody = inputExecPins.length > 0 || inputDataPins.length > 0 ||
|
||||
outputDataPins.length > 0 || outputExecPins.length > 0;
|
||||
|
||||
if (hasBody) {
|
||||
html += `<div class="bp-node-body">`;
|
||||
|
||||
// Input exec pins first
|
||||
inputExecPins.forEach(pin => {
|
||||
const filled = pin.connected !== false;
|
||||
html += `<div class="bp-pin-row input">`;
|
||||
html += `<span class="bp-pin" data-pin="${pin.id}">${renderPinSvg(pin.type, filled)}</span>`;
|
||||
html += `<span class="bp-pin-label">${pin.label || ''}</span>`;
|
||||
html += `</div>`;
|
||||
});
|
||||
|
||||
// Input data pins
|
||||
inputDataPins.forEach(pin => {
|
||||
const filled = pin.connected !== false;
|
||||
html += `<div class="bp-pin-row input">`;
|
||||
html += `<span class="bp-pin" data-pin="${pin.id}">${renderPinSvg(pin.type, filled)}</span>`;
|
||||
html += `<span class="bp-pin-label">${pin.label || ''}</span>`;
|
||||
if (pin.value !== undefined) html += `<span class="bp-pin-value">${pin.value}</span>`;
|
||||
html += `</div>`;
|
||||
});
|
||||
|
||||
// Output data pins (pin first, then label - CSS row-reverse will flip them)
|
||||
outputDataPins.forEach(pin => {
|
||||
html += `<div class="bp-pin-row output">`;
|
||||
html += `<span class="bp-pin" data-pin="${pin.id}">${renderPinSvg(pin.type)}</span>`;
|
||||
html += `<span class="bp-pin-label">${pin.label || ''}</span>`;
|
||||
html += `</div>`;
|
||||
});
|
||||
|
||||
// Output exec pins
|
||||
outputExecPins.forEach(pin => {
|
||||
html += `<div class="bp-pin-row output">`;
|
||||
html += `<span class="bp-pin" data-pin="${pin.id}">${renderPinSvg(pin.type)}</span>`;
|
||||
html += `<span class="bp-pin-label">${pin.label || ''}</span>`;
|
||||
html += `</div>`;
|
||||
});
|
||||
|
||||
html += `</div>`;
|
||||
}
|
||||
|
||||
html += `</div>`;
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup drag-to-scroll for graph container
|
||||
* Works with native overflow:auto scrolling
|
||||
*/
|
||||
function setupDragScroll(container) {
|
||||
let isDragging = false;
|
||||
let startX = 0, startY = 0;
|
||||
let scrollLeft = 0, scrollTop = 0;
|
||||
|
||||
container.addEventListener('mousedown', (e) => {
|
||||
if (e.button !== 0) return;
|
||||
isDragging = true;
|
||||
startX = e.pageX - container.offsetLeft;
|
||||
startY = e.pageY - container.offsetTop;
|
||||
scrollLeft = container.scrollLeft;
|
||||
scrollTop = container.scrollTop;
|
||||
container.style.cursor = 'grabbing';
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
container.addEventListener('mousemove', (e) => {
|
||||
if (!isDragging) return;
|
||||
const x = e.pageX - container.offsetLeft;
|
||||
const y = e.pageY - container.offsetTop;
|
||||
container.scrollLeft = scrollLeft - (x - startX);
|
||||
container.scrollTop = scrollTop - (y - startY);
|
||||
});
|
||||
|
||||
container.addEventListener('mouseup', () => {
|
||||
isDragging = false;
|
||||
container.style.cursor = 'grab';
|
||||
});
|
||||
|
||||
container.addEventListener('mouseleave', () => {
|
||||
isDragging = false;
|
||||
container.style.cursor = 'grab';
|
||||
});
|
||||
}
|
||||
|
||||
function renderConnections(container, graphData) {
|
||||
const svg = container.querySelector('.bp-connections');
|
||||
if (!svg) return;
|
||||
|
||||
const content = container.querySelector('.bp-graph-content') || container;
|
||||
const graphRect = content.getBoundingClientRect();
|
||||
|
||||
graphData.connections.forEach(c => {
|
||||
const fromPin = container.querySelector(`[data-pin="${c.from}"]`);
|
||||
const toPin = container.querySelector(`[data-pin="${c.to}"]`);
|
||||
if (!fromPin || !toPin) return;
|
||||
|
||||
const fromRect = fromPin.getBoundingClientRect();
|
||||
const toRect = toPin.getBoundingClientRect();
|
||||
|
||||
const x1 = fromRect.left - graphRect.left + fromRect.width / 2;
|
||||
const y1 = fromRect.top - graphRect.top + fromRect.height / 2;
|
||||
const x2 = toRect.left - graphRect.left + toRect.width / 2;
|
||||
const y2 = toRect.top - graphRect.top + toRect.height / 2;
|
||||
|
||||
// Simple bezier curve
|
||||
const dx = Math.abs(x2 - x1) * 0.5;
|
||||
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||
path.setAttribute('d', `M${x1},${y1} C${x1+dx},${y1} ${x2-dx},${y2} ${x2},${y2}`);
|
||||
path.setAttribute('class', `bp-conn ${c.type || 'exec'}`);
|
||||
svg.appendChild(path);
|
||||
});
|
||||
}
|
||||
|
||||
function initBlueprintGraphs() {
|
||||
document.querySelectorAll('.bp-graph[data-graph]').forEach(container => {
|
||||
try {
|
||||
const graphData = JSON.parse(container.dataset.graph);
|
||||
if (!graphData.nodes || graphData.nodes.length === 0) {
|
||||
console.warn('Blueprint graph has no nodes');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get container width for layout calculation
|
||||
let containerWidth = container.parentElement?.offsetWidth || 0;
|
||||
if (containerWidth < 200) {
|
||||
containerWidth = 650;
|
||||
}
|
||||
|
||||
const { positions, sizes } = autoLayout(graphData, containerWidth - 30);
|
||||
|
||||
let maxX = 0, maxY = 0;
|
||||
graphData.nodes.forEach(n => {
|
||||
const pos = positions[n.id];
|
||||
const size = sizes[n.id];
|
||||
if (pos && size) {
|
||||
maxX = Math.max(maxX, pos.x + size.width);
|
||||
maxY = Math.max(maxY, pos.y + size.height);
|
||||
}
|
||||
});
|
||||
|
||||
// Add generous padding to ensure all nodes visible
|
||||
maxX += 80;
|
||||
maxY += 80;
|
||||
|
||||
// Set minimum height but allow natural expansion
|
||||
const containerHeight = Math.max(maxY, 200);
|
||||
container.style.minHeight = containerHeight + 'px';
|
||||
|
||||
let html = `<div class="bp-graph-content" style="width:${maxX}px;height:${maxY}px;position:relative;">`;
|
||||
html += `<svg class="bp-connections" width="${maxX}" height="${maxY}"></svg>`;
|
||||
graphData.nodes.forEach(n => {
|
||||
if (positions[n.id] && sizes[n.id]) {
|
||||
html += renderNode(n, positions[n.id], sizes[n.id]);
|
||||
}
|
||||
});
|
||||
html += `</div>`;
|
||||
container.innerHTML = html;
|
||||
|
||||
// Setup drag-to-scroll
|
||||
setupDragScroll(container);
|
||||
|
||||
requestAnimationFrame(() => renderConnections(container, graphData));
|
||||
} catch (e) {
|
||||
console.error('Blueprint graph error:', e);
|
||||
}
|
||||
});
|
||||
|
||||
// Legacy format
|
||||
document.querySelectorAll('.bp-graph:not([data-graph])').forEach(graph => {
|
||||
const nodes = graph.querySelectorAll('.bp-node');
|
||||
let maxX = 0, maxY = 0;
|
||||
nodes.forEach(node => {
|
||||
const left = parseInt(node.style.left) || 0;
|
||||
const top = parseInt(node.style.top) || 0;
|
||||
const width = parseInt(node.style.width) || 150;
|
||||
maxX = Math.max(maxX, left + width + 40);
|
||||
maxY = Math.max(maxY, top + node.offsetHeight + 40);
|
||||
});
|
||||
// Don't set fixed width - let CSS handle it
|
||||
graph.style.minHeight = Math.max(maxY, 120) + 'px';
|
||||
|
||||
const svg = graph.querySelector('.bp-connections');
|
||||
if (!svg) return;
|
||||
svg.setAttribute('width', maxX);
|
||||
svg.setAttribute('height', Math.max(maxY, 120));
|
||||
|
||||
const conns = JSON.parse(graph.dataset.connections || '[]');
|
||||
const graphRect = graph.getBoundingClientRect();
|
||||
|
||||
conns.forEach(c => {
|
||||
const fromPin = graph.querySelector(`[data-pin="${c.from}"]`);
|
||||
const toPin = graph.querySelector(`[data-pin="${c.to}"]`);
|
||||
if (!fromPin || !toPin) return;
|
||||
|
||||
const fromRect = fromPin.getBoundingClientRect();
|
||||
const toRect = toPin.getBoundingClientRect();
|
||||
|
||||
const x1 = fromRect.left - graphRect.left + fromRect.width / 2;
|
||||
const y1 = fromRect.top - graphRect.top + fromRect.height / 2;
|
||||
const x2 = toRect.left - graphRect.left + toRect.width / 2;
|
||||
const y2 = toRect.top - graphRect.top + toRect.height / 2;
|
||||
|
||||
const dx = Math.abs(x2 - x1) * 0.5;
|
||||
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||
path.setAttribute('d', `M${x1},${y1} C${x1+dx},${y1} ${x2-dx},${y2} ${x2},${y2}`);
|
||||
path.setAttribute('class', `bp-conn ${c.type || 'exec'}`);
|
||||
svg.appendChild(path);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initBlueprintGraphs);
|
||||
} else {
|
||||
initBlueprintGraphs();
|
||||
}
|
||||
})();
|
||||
BIN
docs/src/assets/houston.webp
Normal file
BIN
docs/src/assets/houston.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 96 KiB |
45
docs/src/assets/logo.svg
Normal file
45
docs/src/assets/logo.svg
Normal file
@@ -0,0 +1,45 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<defs>
|
||||
<!-- Dark gradient background -->
|
||||
<linearGradient id="bgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#2d2d2d"/>
|
||||
<stop offset="100%" style="stop-color:#1a1a1a"/>
|
||||
</linearGradient>
|
||||
|
||||
<!-- Clean white text -->
|
||||
<linearGradient id="whiteGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#ffffff"/>
|
||||
<stop offset="100%" style="stop-color:#e8e8e8"/>
|
||||
</linearGradient>
|
||||
|
||||
<!-- Subtle inner shadow -->
|
||||
<filter id="innerShadow">
|
||||
<feOffset dx="0" dy="2"/>
|
||||
<feGaussianBlur stdDeviation="1" result="offset-blur"/>
|
||||
<feComposite operator="out" in="SourceGraphic" in2="offset-blur" result="inverse"/>
|
||||
<feFlood flood-color="black" flood-opacity="0.2" result="color"/>
|
||||
<feComposite operator="in" in="color" in2="inverse" result="shadow"/>
|
||||
<feComposite operator="over" in="shadow" in2="SourceGraphic"/>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- Background -->
|
||||
<rect width="512" height="512" fill="url(#bgGrad)"/>
|
||||
|
||||
<!-- Subtle border -->
|
||||
<rect x="1" y="1" width="510" height="510" fill="none" stroke="#3d3d3d" stroke-width="2"/>
|
||||
|
||||
<!-- ES Text -->
|
||||
<g filter="url(#innerShadow)">
|
||||
<!-- E -->
|
||||
<polygon points="72,120 72,392 240,392 240,340 140,340 140,282 220,282 220,230 140,230 140,172 240,172 240,120"
|
||||
fill="url(#whiteGrad)"/>
|
||||
|
||||
<!-- S -->
|
||||
<path d="M 280 172 Q 280 120 340 120 L 420 120 Q 450 120 450 160 L 450 186 L 398 186 L 398 168 Q 398 158 384 158 L 350 158 Q 320 158 320 188 Q 320 218 350 218 L 400 218 Q 450 218 450 274 L 450 332 Q 450 392 390 392 L 310 392 Q 270 392 270 340 L 270 314 L 322 314 L 322 340 Q 322 354 340 354 L 384 354 Q 404 354 404 324 L 404 290 Q 404 260 380 260 L 330 260 Q 280 260 280 208 Z"
|
||||
fill="url(#whiteGrad)"/>
|
||||
</g>
|
||||
|
||||
<!-- Accent line -->
|
||||
<rect x="72" y="424" width="368" height="4" fill="#ffffff" opacity="0.6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
23
docs/src/components/Head.astro
Normal file
23
docs/src/components/Head.astro
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
import type { Props } from '@astrojs/starlight/props';
|
||||
import Default from '@astrojs/starlight/components/Head.astro';
|
||||
---
|
||||
|
||||
<Default {...Astro.props}><slot /></Default>
|
||||
|
||||
<!-- Preload fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<!-- Force dark mode -->
|
||||
<script is:inline>
|
||||
document.documentElement.dataset.theme = 'dark';
|
||||
localStorage.setItem('starlight-theme', 'dark');
|
||||
</script>
|
||||
|
||||
<!-- Blueprint graph visualization -->
|
||||
<script is:inline src="/js/blueprint-graph.js"></script>
|
||||
3
docs/src/components/ThemeSelect.astro
Normal file
3
docs/src/components/ThemeSelect.astro
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
// Empty component to disable theme selection (dark mode only)
|
||||
---
|
||||
7
docs/src/content.config.ts
Normal file
7
docs/src/content.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineCollection } from 'astro:content';
|
||||
import { docsLoader } from '@astrojs/starlight/loaders';
|
||||
import { docsSchema } from '@astrojs/starlight/schema';
|
||||
|
||||
export const collections = {
|
||||
docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
|
||||
};
|
||||
64
docs/src/content/docs/api/index.md
Normal file
64
docs/src/content/docs/api/index.md
Normal file
@@ -0,0 +1,64 @@
|
||||
---
|
||||
title: API 参考
|
||||
description: ESEngine 完整 API 文档
|
||||
---
|
||||
|
||||
# API 参考
|
||||
|
||||
ESEngine 提供完整的 TypeScript API 文档,涵盖所有核心类、接口和方法。
|
||||
|
||||
## 核心模块
|
||||
|
||||
### 基础类
|
||||
|
||||
| 类名 | 描述 |
|
||||
|------|------|
|
||||
| [Core](/api/classes/Core) | 框架核心单例,管理整个 ECS 生命周期 |
|
||||
| [Scene](/api/classes/Scene) | 场景类,包含实体和系统 |
|
||||
| [World](/api/classes/World) | 游戏世界,可包含多个场景 |
|
||||
| [Entity](/api/classes/Entity) | 实体类,组件的容器 |
|
||||
| [Component](/api/classes/Component) | 组件基类,纯数据容器 |
|
||||
|
||||
### 系统类
|
||||
|
||||
| 类名 | 描述 |
|
||||
|------|------|
|
||||
| [EntitySystem](/api/classes/EntitySystem) | 实体系统基类 |
|
||||
| [ProcessingSystem](/api/classes/ProcessingSystem) | 处理系统,逐个处理实体 |
|
||||
| [IntervalSystem](/api/classes/IntervalSystem) | 间隔执行系统 |
|
||||
| [PassiveSystem](/api/classes/PassiveSystem) | 被动系统,不自动执行 |
|
||||
|
||||
### 工具类
|
||||
|
||||
| 类名 | 描述 |
|
||||
|------|------|
|
||||
| [Matcher](/api/classes/Matcher) | 实体匹配器,用于过滤实体 |
|
||||
| [Time](/api/classes/Time) | 时间管理器 |
|
||||
| [PerformanceMonitor](/api/classes/PerformanceMonitor) | 性能监控 |
|
||||
|
||||
## 装饰器
|
||||
|
||||
| 装饰器 | 描述 |
|
||||
|--------|------|
|
||||
| [@ECSComponent](/api/functions/ECSComponent) | 组件装饰器,用于注册组件 |
|
||||
| [@ECSSystem](/api/functions/ECSSystem) | 系统装饰器,用于注册系统 |
|
||||
|
||||
## 枚举
|
||||
|
||||
| 枚举 | 描述 |
|
||||
|------|------|
|
||||
| [ECSEventType](/api/enumerations/ECSEventType) | ECS 事件类型 |
|
||||
| [LogLevel](/api/enumerations/LogLevel) | 日志级别 |
|
||||
|
||||
## 接口
|
||||
|
||||
| 接口 | 描述 |
|
||||
|------|------|
|
||||
| [IScene](/api/interfaces/IScene) | 场景接口 |
|
||||
| [IComponent](/api/interfaces/IComponent) | 组件接口 |
|
||||
| [ISystemBase](/api/interfaces/ISystemBase) | 系统基础接口 |
|
||||
| [ICoreConfig](/api/interfaces/ICoreConfig) | Core 配置接口 |
|
||||
|
||||
:::tip[API 文档生成]
|
||||
完整 API 文档由 TypeDoc 自动生成,详见 GitHub 仓库中的 `/docs/api` 目录。
|
||||
:::
|
||||
20
docs/src/content/docs/en/examples/index.md
Normal file
20
docs/src/content/docs/en/examples/index.md
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
title: "Examples"
|
||||
description: "ESEngine example projects and demos"
|
||||
---
|
||||
|
||||
Explore example projects to learn ESEngine best practices.
|
||||
|
||||
## Available Examples
|
||||
|
||||
### [Worker System Demo](/en/examples/worker-system-demo/)
|
||||
Demonstrates how to use Web Workers for parallel processing, offloading heavy computations from the main thread.
|
||||
|
||||
## External Examples
|
||||
|
||||
### [Lawn Mower Demo](https://github.com/esengine/lawn-mower-demo)
|
||||
A complete game demo showcasing ESEngine features including:
|
||||
- Entity-Component-System architecture
|
||||
- Behavior tree AI
|
||||
- Scene management
|
||||
- Platform adaptation
|
||||
312
docs/src/content/docs/en/guide/component/best-practices.md
Normal file
312
docs/src/content/docs/en/guide/component/best-practices.md
Normal file
@@ -0,0 +1,312 @@
|
||||
---
|
||||
title: "Best Practices"
|
||||
description: "Component design patterns and complex examples"
|
||||
---
|
||||
|
||||
## Design Principles
|
||||
|
||||
### 1. Keep Components Simple
|
||||
|
||||
```typescript
|
||||
// ✅ Good component design - single responsibility
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
x: number = 0;
|
||||
y: number = 0;
|
||||
}
|
||||
|
||||
@ECSComponent('Velocity')
|
||||
class Velocity extends Component {
|
||||
dx: number = 0;
|
||||
dy: number = 0;
|
||||
}
|
||||
|
||||
// ❌ Avoid this design - too many responsibilities
|
||||
@ECSComponent('GameObject')
|
||||
class GameObject extends Component {
|
||||
x: number;
|
||||
y: number;
|
||||
dx: number;
|
||||
dy: number;
|
||||
health: number;
|
||||
damage: number;
|
||||
sprite: string;
|
||||
// Too many unrelated properties
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Use Constructor for Initialization
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Transform')
|
||||
class Transform extends Component {
|
||||
x: number;
|
||||
y: number;
|
||||
rotation: number;
|
||||
scale: number;
|
||||
|
||||
constructor(x = 0, y = 0, rotation = 0, scale = 1) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.rotation = rotation;
|
||||
this.scale = scale;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Clear Type Definitions
|
||||
|
||||
```typescript
|
||||
interface InventoryItem {
|
||||
id: string;
|
||||
name: string;
|
||||
quantity: number;
|
||||
type: 'weapon' | 'consumable' | 'misc';
|
||||
}
|
||||
|
||||
@ECSComponent('Inventory')
|
||||
class Inventory extends Component {
|
||||
items: InventoryItem[] = [];
|
||||
maxSlots: number;
|
||||
|
||||
constructor(maxSlots: number = 20) {
|
||||
super();
|
||||
this.maxSlots = maxSlots;
|
||||
}
|
||||
|
||||
addItem(item: InventoryItem): boolean {
|
||||
if (this.items.length < this.maxSlots) {
|
||||
this.items.push(item);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
removeItem(itemId: string): InventoryItem | null {
|
||||
const index = this.items.findIndex(item => item.id === itemId);
|
||||
if (index !== -1) {
|
||||
return this.items.splice(index, 1)[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Referencing Other Entities
|
||||
|
||||
When components need to reference other entities (like parent-child relationships, follow targets), **the recommended approach is to store entity IDs**, then look up in System:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Follower')
|
||||
class Follower extends Component {
|
||||
targetId: number;
|
||||
followDistance: number = 50;
|
||||
|
||||
constructor(targetId: number) {
|
||||
super();
|
||||
this.targetId = targetId;
|
||||
}
|
||||
}
|
||||
|
||||
// Look up target entity and handle logic in System
|
||||
class FollowerSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(new Matcher().all(Follower, Position));
|
||||
}
|
||||
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const follower = entity.getComponent(Follower)!;
|
||||
const position = entity.getComponent(Position)!;
|
||||
|
||||
// Look up target entity through scene
|
||||
const target = entity.scene?.findEntityById(follower.targetId);
|
||||
if (target) {
|
||||
const targetPos = target.getComponent(Position);
|
||||
if (targetPos) {
|
||||
// Follow logic
|
||||
const dx = targetPos.x - position.x;
|
||||
const dy = targetPos.y - position.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance > follower.followDistance) {
|
||||
// Move closer to target
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Advantages of this approach:
|
||||
- Components stay simple, only store basic data types
|
||||
- Follows data-oriented design
|
||||
- Unified lookup and logic handling in System
|
||||
- Easy to understand and maintain
|
||||
|
||||
**Avoid storing entity references directly in components**:
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong example: Storing entity reference directly
|
||||
@ECSComponent('BadFollower')
|
||||
class BadFollower extends Component {
|
||||
target: Entity; // Still holds reference after entity destroyed, may cause memory leak
|
||||
}
|
||||
```
|
||||
|
||||
## Complex Component Examples
|
||||
|
||||
### State Machine Component
|
||||
|
||||
```typescript
|
||||
enum EntityState {
|
||||
Idle,
|
||||
Moving,
|
||||
Attacking,
|
||||
Dead
|
||||
}
|
||||
|
||||
@ECSComponent('StateMachine')
|
||||
class StateMachine extends Component {
|
||||
private _currentState: EntityState = EntityState.Idle;
|
||||
private _previousState: EntityState = EntityState.Idle;
|
||||
private _stateTimer: number = 0;
|
||||
|
||||
get currentState(): EntityState {
|
||||
return this._currentState;
|
||||
}
|
||||
|
||||
get previousState(): EntityState {
|
||||
return this._previousState;
|
||||
}
|
||||
|
||||
get stateTimer(): number {
|
||||
return this._stateTimer;
|
||||
}
|
||||
|
||||
changeState(newState: EntityState): void {
|
||||
if (this._currentState !== newState) {
|
||||
this._previousState = this._currentState;
|
||||
this._currentState = newState;
|
||||
this._stateTimer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
updateTimer(deltaTime: number): void {
|
||||
this._stateTimer += deltaTime;
|
||||
}
|
||||
|
||||
isInState(state: EntityState): boolean {
|
||||
return this._currentState === state;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration Data Component
|
||||
|
||||
```typescript
|
||||
interface WeaponData {
|
||||
damage: number;
|
||||
range: number;
|
||||
fireRate: number;
|
||||
ammo: number;
|
||||
}
|
||||
|
||||
@ECSComponent('WeaponConfig')
|
||||
class WeaponConfig extends Component {
|
||||
data: WeaponData;
|
||||
|
||||
constructor(weaponData: WeaponData) {
|
||||
super();
|
||||
this.data = { ...weaponData }; // Deep copy to avoid shared reference
|
||||
}
|
||||
|
||||
// Provide convenience methods
|
||||
getDamage(): number {
|
||||
return this.data.damage;
|
||||
}
|
||||
|
||||
canFire(): boolean {
|
||||
return this.data.ammo > 0;
|
||||
}
|
||||
|
||||
consumeAmmo(): boolean {
|
||||
if (this.data.ammo > 0) {
|
||||
this.data.ammo--;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Tag Components
|
||||
|
||||
```typescript
|
||||
// Tag components: No data, only for identification
|
||||
@ECSComponent('Player')
|
||||
class PlayerTag extends Component {}
|
||||
|
||||
@ECSComponent('Enemy')
|
||||
class EnemyTag extends Component {}
|
||||
|
||||
@ECSComponent('Dead')
|
||||
class DeadTag extends Component {}
|
||||
|
||||
// Use tags for querying
|
||||
class EnemySystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(EnemyTag, Health).none(DeadTag));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Buffer Components
|
||||
|
||||
```typescript
|
||||
// Event/command buffer component
|
||||
@ECSComponent('DamageBuffer')
|
||||
class DamageBuffer extends Component {
|
||||
damages: { amount: number; source: number; timestamp: number }[] = [];
|
||||
|
||||
addDamage(amount: number, sourceId: number): void {
|
||||
this.damages.push({
|
||||
amount,
|
||||
source: sourceId,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.damages.length = 0;
|
||||
}
|
||||
|
||||
getTotalDamage(): number {
|
||||
return this.damages.reduce((sum, d) => sum + d.amount, 0);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## FAQ
|
||||
|
||||
### Q: How large should a component be?
|
||||
|
||||
**A**: Follow single responsibility principle. If a component contains unrelated data, split into multiple components.
|
||||
|
||||
### Q: Can components have methods?
|
||||
|
||||
**A**: Yes, but they should be data-related helper methods (like `isDead()`), not business logic. Business logic goes in Systems.
|
||||
|
||||
### Q: How to handle dependencies between components?
|
||||
|
||||
**A**: Handle inter-component interactions in Systems, don't directly access other components within a component.
|
||||
|
||||
### Q: When to use EntityRef?
|
||||
|
||||
**A**: Only when you need frequent access to referenced entity and the reference relationship is stable (like parent-child). Storing IDs is better for most cases.
|
||||
|
||||
---
|
||||
|
||||
Components are the data carriers of ECS architecture. Properly designing components makes your game code more modular, maintainable, and performant.
|
||||
228
docs/src/content/docs/en/guide/component/entity-ref.md
Normal file
228
docs/src/content/docs/en/guide/component/entity-ref.md
Normal file
@@ -0,0 +1,228 @@
|
||||
---
|
||||
title: "EntityRef Decorator"
|
||||
description: "Safe entity reference tracking mechanism"
|
||||
---
|
||||
|
||||
The framework provides the `@EntityRef` decorator for **special scenarios** to safely store entity references. This is an advanced feature; storing IDs is recommended for most cases.
|
||||
|
||||
## When Do You Need EntityRef?
|
||||
|
||||
`@EntityRef` can simplify code in these scenarios:
|
||||
|
||||
1. **Parent-Child Relationships**: Need to directly access parent or child entities in components
|
||||
2. **Complex Associations**: Multiple reference relationships between entities
|
||||
3. **Frequent Access**: Need to access referenced entity in multiple places, ID lookup has performance overhead
|
||||
|
||||
## Core Features
|
||||
|
||||
The `@EntityRef` decorator automatically tracks references through **ReferenceTracker**:
|
||||
|
||||
- When the referenced entity is destroyed, all `@EntityRef` properties pointing to it are automatically set to `null`
|
||||
- Prevents cross-scene references (outputs warning and refuses to set)
|
||||
- Prevents references to destroyed entities (outputs warning and sets to `null`)
|
||||
- Uses WeakRef to avoid memory leaks (automatic GC support)
|
||||
- Automatically cleans up reference registration when component is removed
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent, EntityRef, Entity } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSComponent('Parent')
|
||||
class ParentComponent extends Component {
|
||||
@EntityRef()
|
||||
parent: Entity | null = null;
|
||||
}
|
||||
|
||||
// Usage example
|
||||
const scene = new Scene();
|
||||
const parent = scene.createEntity('Parent');
|
||||
const child = scene.createEntity('Child');
|
||||
|
||||
const comp = child.addComponent(new ParentComponent());
|
||||
comp.parent = parent;
|
||||
|
||||
console.log(comp.parent); // Entity { name: 'Parent' }
|
||||
|
||||
// When parent is destroyed, comp.parent automatically becomes null
|
||||
parent.destroy();
|
||||
console.log(comp.parent); // null
|
||||
```
|
||||
|
||||
## Multiple Reference Properties
|
||||
|
||||
A component can have multiple `@EntityRef` properties:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Combat')
|
||||
class CombatComponent extends Component {
|
||||
@EntityRef()
|
||||
target: Entity | null = null;
|
||||
|
||||
@EntityRef()
|
||||
ally: Entity | null = null;
|
||||
|
||||
@EntityRef()
|
||||
lastAttacker: Entity | null = null;
|
||||
}
|
||||
|
||||
// Usage example
|
||||
const player = scene.createEntity('Player');
|
||||
const enemy = scene.createEntity('Enemy');
|
||||
const npc = scene.createEntity('NPC');
|
||||
|
||||
const combat = player.addComponent(new CombatComponent());
|
||||
combat.target = enemy;
|
||||
combat.ally = npc;
|
||||
|
||||
// After enemy is destroyed, only target becomes null, ally remains valid
|
||||
enemy.destroy();
|
||||
console.log(combat.target); // null
|
||||
console.log(combat.ally); // Entity { name: 'NPC' }
|
||||
```
|
||||
|
||||
## Safety Checks
|
||||
|
||||
`@EntityRef` provides multiple safety checks:
|
||||
|
||||
```typescript
|
||||
const scene1 = new Scene();
|
||||
const scene2 = new Scene();
|
||||
|
||||
const entity1 = scene1.createEntity('Entity1');
|
||||
const entity2 = scene2.createEntity('Entity2');
|
||||
|
||||
const comp = entity1.addComponent(new ParentComponent());
|
||||
|
||||
// Cross-scene reference fails
|
||||
comp.parent = entity2; // Outputs error log, comp.parent is null
|
||||
console.log(comp.parent); // null
|
||||
|
||||
// Reference to destroyed entity fails
|
||||
const entity3 = scene1.createEntity('Entity3');
|
||||
entity3.destroy();
|
||||
comp.parent = entity3; // Outputs warning log, comp.parent is null
|
||||
console.log(comp.parent); // null
|
||||
```
|
||||
|
||||
## Implementation Principle
|
||||
|
||||
`@EntityRef` uses the following mechanisms for automatic reference tracking:
|
||||
|
||||
1. **ReferenceTracker**: Scene holds a reference tracker that records all entity reference relationships
|
||||
2. **WeakRef**: Uses weak references to store components, avoiding memory leaks from circular references
|
||||
3. **Property Interception**: Intercepts getter/setter through `Object.defineProperty`
|
||||
4. **Automatic Cleanup**: When entity is destroyed, ReferenceTracker traverses all references and sets them to null
|
||||
|
||||
```typescript
|
||||
// Simplified implementation principle
|
||||
class ReferenceTracker {
|
||||
// entityId -> all component records referencing this entity
|
||||
private _references: Map<number, Set<{ component: WeakRef<Component>, propertyKey: string }>>;
|
||||
|
||||
// Called when entity is destroyed
|
||||
clearReferencesTo(entityId: number): void {
|
||||
const records = this._references.get(entityId);
|
||||
if (records) {
|
||||
for (const record of records) {
|
||||
const component = record.component.deref();
|
||||
if (component) {
|
||||
// Set component's reference property to null
|
||||
(component as any)[record.propertyKey] = null;
|
||||
}
|
||||
}
|
||||
this._references.delete(entityId);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
`@EntityRef` introduces some performance overhead:
|
||||
|
||||
- **Write Overhead**: Need to update ReferenceTracker each time a reference is set
|
||||
- **Memory Overhead**: ReferenceTracker needs to maintain reference mapping table
|
||||
- **Destroy Overhead**: Need to traverse all references and clean up when entity is destroyed
|
||||
|
||||
For most scenarios, this overhead is acceptable. But with **many entities and frequent reference changes**, storing IDs may be more efficient.
|
||||
|
||||
## Debug Support
|
||||
|
||||
ReferenceTracker provides debug interfaces:
|
||||
|
||||
```typescript
|
||||
// View which components reference an entity
|
||||
const references = scene.referenceTracker.getReferencesTo(entity.id);
|
||||
console.log(`Entity ${entity.name} is referenced by ${references.length} components`);
|
||||
|
||||
// Get complete debug info
|
||||
const debugInfo = scene.referenceTracker.getDebugInfo();
|
||||
console.log(debugInfo);
|
||||
```
|
||||
|
||||
## Comparison with Storing IDs
|
||||
|
||||
### Storing IDs (Recommended for Most Cases)
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Follower')
|
||||
class Follower extends Component {
|
||||
targetId: number | null = null;
|
||||
}
|
||||
|
||||
// Look up in System
|
||||
class FollowerSystem extends EntitySystem {
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const follower = entity.getComponent(Follower)!;
|
||||
const target = entity.scene?.findEntityById(follower.targetId);
|
||||
if (target) {
|
||||
// Follow logic
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using EntityRef (For Complex Associations)
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Transform')
|
||||
class Transform extends Component {
|
||||
@EntityRef()
|
||||
parent: Entity | null = null;
|
||||
|
||||
position: { x: number, y: number } = { x: 0, y: 0 };
|
||||
|
||||
// Can directly access parent entity's component
|
||||
getWorldPosition(): { x: number, y: number } {
|
||||
if (!this.parent) {
|
||||
return { ...this.position };
|
||||
}
|
||||
|
||||
const parentTransform = this.parent.getComponent(Transform);
|
||||
if (parentTransform) {
|
||||
const parentPos = parentTransform.getWorldPosition();
|
||||
return {
|
||||
x: parentPos.x + this.position.x,
|
||||
y: parentPos.y + this.position.y
|
||||
};
|
||||
}
|
||||
|
||||
return { ...this.position };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
| Approach | Use Case | Pros | Cons |
|
||||
|----------|----------|------|------|
|
||||
| Store ID | Most cases | Simple, no extra overhead | Need to lookup in System |
|
||||
| @EntityRef | Parent-child, complex associations | Auto-cleanup, cleaner code | Has performance overhead |
|
||||
|
||||
- **Recommended**: Use store ID + System lookup for most cases
|
||||
- **EntityRef Use Cases**: Parent-child relationships, complex associations, when component needs direct access to referenced entity
|
||||
- **Core Advantage**: Automatic cleanup, prevents dangling references, cleaner code
|
||||
- **Considerations**: Has performance overhead, not suitable for many dynamic references
|
||||
226
docs/src/content/docs/en/guide/component/index.md
Normal file
226
docs/src/content/docs/en/guide/component/index.md
Normal file
@@ -0,0 +1,226 @@
|
||||
---
|
||||
title: "Component System"
|
||||
description: "ECS component basics and creation methods"
|
||||
---
|
||||
|
||||
In ECS architecture, Components are carriers of data and behavior. Components define the properties and functionality that entities possess, and are the core building blocks of ECS architecture.
|
||||
|
||||
## Basic Concepts
|
||||
|
||||
Components are concrete classes that inherit from the `Component` abstract base class, used for:
|
||||
- Storing entity data (such as position, velocity, health, etc.)
|
||||
- Defining behavior methods related to the data
|
||||
- Providing lifecycle callback hooks
|
||||
- Supporting serialization and debugging
|
||||
|
||||
## Creating Components
|
||||
|
||||
### Basic Component Definition
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
x: number = 0;
|
||||
y: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('Health')
|
||||
class Health extends Component {
|
||||
current: number;
|
||||
max: number;
|
||||
|
||||
constructor(max: number = 100) {
|
||||
super();
|
||||
this.max = max;
|
||||
this.current = max;
|
||||
}
|
||||
|
||||
// Components can contain behavior methods
|
||||
takeDamage(damage: number): void {
|
||||
this.current = Math.max(0, this.current - damage);
|
||||
}
|
||||
|
||||
heal(amount: number): void {
|
||||
this.current = Math.min(this.max, this.current + amount);
|
||||
}
|
||||
|
||||
isDead(): boolean {
|
||||
return this.current <= 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## @ECSComponent Decorator
|
||||
|
||||
`@ECSComponent` is a required decorator for component classes, providing type identification and metadata management.
|
||||
|
||||
### Why It's Required
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| **Type Identification** | Provides stable type name that remains correct after code obfuscation |
|
||||
| **Serialization Support** | Uses this name as type identifier during serialization/deserialization |
|
||||
| **Component Registration** | Auto-registers to ComponentRegistry, assigns unique bitmask |
|
||||
| **Debug Support** | Shows readable component names in debug tools and logs |
|
||||
|
||||
### Basic Syntax
|
||||
|
||||
```typescript
|
||||
@ECSComponent(typeName: string)
|
||||
```
|
||||
|
||||
- `typeName`: Component's type name, recommended to use same or similar name as class name
|
||||
|
||||
### Usage Examples
|
||||
|
||||
```typescript
|
||||
// ✅ Correct usage
|
||||
@ECSComponent('Velocity')
|
||||
class Velocity extends Component {
|
||||
dx: number = 0;
|
||||
dy: number = 0;
|
||||
}
|
||||
|
||||
// ✅ Recommended: Keep type name consistent with class name
|
||||
@ECSComponent('PlayerController')
|
||||
class PlayerController extends Component {
|
||||
speed: number = 5;
|
||||
}
|
||||
|
||||
// ❌ Wrong usage - no decorator
|
||||
class BadComponent extends Component {
|
||||
// Components defined this way may have issues in production:
|
||||
// 1. Class name changes after minification, can't serialize correctly
|
||||
// 2. Component not registered to framework, queries may fail
|
||||
}
|
||||
```
|
||||
|
||||
### Using with @Serializable
|
||||
|
||||
When components need serialization support, use `@ECSComponent` and `@Serializable` together:
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent, Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSComponent('Player')
|
||||
@Serializable({ version: 1 })
|
||||
class PlayerComponent extends Component {
|
||||
@Serialize()
|
||||
name: string = '';
|
||||
|
||||
@Serialize()
|
||||
level: number = 1;
|
||||
|
||||
// Fields without @Serialize() won't be serialized
|
||||
private _cachedData: any = null;
|
||||
}
|
||||
```
|
||||
|
||||
> **Note**: `@ECSComponent`'s `typeName` and `@Serializable`'s `typeId` can differ. If `@Serializable` doesn't specify `typeId`, it defaults to `@ECSComponent`'s `typeName`.
|
||||
|
||||
### Type Name Uniqueness
|
||||
|
||||
Each component's type name should be unique:
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong: Two components using the same type name
|
||||
@ECSComponent('Health')
|
||||
class HealthComponent extends Component { }
|
||||
|
||||
@ECSComponent('Health') // Conflict!
|
||||
class EnemyHealthComponent extends Component { }
|
||||
|
||||
// ✅ Correct: Use different type names
|
||||
@ECSComponent('PlayerHealth')
|
||||
class PlayerHealthComponent extends Component { }
|
||||
|
||||
@ECSComponent('EnemyHealth')
|
||||
class EnemyHealthComponent extends Component { }
|
||||
```
|
||||
|
||||
## Component Properties
|
||||
|
||||
Each component has some built-in properties:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('ExampleComponent')
|
||||
class ExampleComponent extends Component {
|
||||
someData: string = "example";
|
||||
|
||||
onAddedToEntity(): void {
|
||||
console.log(`Component ID: ${this.id}`); // Unique component ID
|
||||
console.log(`Entity ID: ${this.entityId}`); // Owning entity's ID
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Component and Entity Relationship
|
||||
|
||||
Components store the owning entity's ID (`entityId`), not a direct entity reference. This is a reflection of ECS's data-oriented design, avoiding circular references.
|
||||
|
||||
In practice, **entity and component interactions should be handled in Systems**, not within components:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Health')
|
||||
class Health extends Component {
|
||||
current: number;
|
||||
max: number;
|
||||
|
||||
constructor(max: number = 100) {
|
||||
super();
|
||||
this.max = max;
|
||||
this.current = max;
|
||||
}
|
||||
|
||||
isDead(): boolean {
|
||||
return this.current <= 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('Damage')
|
||||
class Damage extends Component {
|
||||
value: number;
|
||||
|
||||
constructor(value: number) {
|
||||
super();
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Recommended: Handle logic in System
|
||||
class DamageSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(new Matcher().all(Health, Damage));
|
||||
}
|
||||
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health)!;
|
||||
const damage = entity.getComponent(Damage)!;
|
||||
|
||||
health.current -= damage.value;
|
||||
|
||||
if (health.isDead()) {
|
||||
entity.destroy();
|
||||
}
|
||||
|
||||
// Remove Damage component after applying damage
|
||||
entity.removeComponent(damage);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## More Topics
|
||||
|
||||
- [Lifecycle](/en/guide/component/lifecycle) - Component lifecycle hooks
|
||||
- [EntityRef Decorator](/en/guide/component/entity-ref) - Safe entity references
|
||||
- [Best Practices](/en/guide/component/best-practices) - Component design patterns and examples
|
||||
182
docs/src/content/docs/en/guide/component/lifecycle.md
Normal file
182
docs/src/content/docs/en/guide/component/lifecycle.md
Normal file
@@ -0,0 +1,182 @@
|
||||
---
|
||||
title: "Component Lifecycle"
|
||||
description: "Component lifecycle hooks and events"
|
||||
---
|
||||
|
||||
Components provide lifecycle hooks that can be overridden to execute specific logic.
|
||||
|
||||
## Lifecycle Methods
|
||||
|
||||
```typescript
|
||||
@ECSComponent('ExampleComponent')
|
||||
class ExampleComponent extends Component {
|
||||
private resource: SomeResource | null = null;
|
||||
|
||||
/**
|
||||
* Called when component is added to entity
|
||||
* Use for initializing resources, establishing references, etc.
|
||||
*/
|
||||
onAddedToEntity(): void {
|
||||
console.log(`Component ${this.constructor.name} added, Entity ID: ${this.entityId}`);
|
||||
this.resource = new SomeResource();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when component is removed from entity
|
||||
* Use for cleaning up resources, breaking references, etc.
|
||||
*/
|
||||
onRemovedFromEntity(): void {
|
||||
console.log(`Component ${this.constructor.name} removed`);
|
||||
if (this.resource) {
|
||||
this.resource.cleanup();
|
||||
this.resource = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Lifecycle Order
|
||||
|
||||
```
|
||||
Entity created
|
||||
↓
|
||||
addComponent() called
|
||||
↓
|
||||
onAddedToEntity() triggered
|
||||
↓
|
||||
Component in normal use...
|
||||
↓
|
||||
removeComponent() or entity.destroy() called
|
||||
↓
|
||||
onRemovedFromEntity() triggered
|
||||
↓
|
||||
Component removed/destroyed
|
||||
```
|
||||
|
||||
## Practical Use Cases
|
||||
|
||||
### Resource Management
|
||||
|
||||
```typescript
|
||||
@ECSComponent('TextureComponent')
|
||||
class TextureComponent extends Component {
|
||||
private _texture: Texture | null = null;
|
||||
texturePath: string = '';
|
||||
|
||||
onAddedToEntity(): void {
|
||||
// Load texture resource
|
||||
this._texture = TextureManager.load(this.texturePath);
|
||||
}
|
||||
|
||||
onRemovedFromEntity(): void {
|
||||
// Release texture resource
|
||||
if (this._texture) {
|
||||
TextureManager.release(this._texture);
|
||||
this._texture = null;
|
||||
}
|
||||
}
|
||||
|
||||
get texture(): Texture | null {
|
||||
return this._texture;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Event Listening
|
||||
|
||||
```typescript
|
||||
@ECSComponent('InputListener')
|
||||
class InputListener extends Component {
|
||||
private _boundHandler: ((e: KeyboardEvent) => void) | null = null;
|
||||
|
||||
onAddedToEntity(): void {
|
||||
this._boundHandler = this.handleKeyDown.bind(this);
|
||||
window.addEventListener('keydown', this._boundHandler);
|
||||
}
|
||||
|
||||
onRemovedFromEntity(): void {
|
||||
if (this._boundHandler) {
|
||||
window.removeEventListener('keydown', this._boundHandler);
|
||||
this._boundHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
private handleKeyDown(e: KeyboardEvent): void {
|
||||
// Handle keyboard input
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Registering with External Systems
|
||||
|
||||
```typescript
|
||||
@ECSComponent('PhysicsBody')
|
||||
class PhysicsBody extends Component {
|
||||
private _body: PhysicsWorld.Body | null = null;
|
||||
|
||||
onAddedToEntity(): void {
|
||||
// Create rigid body in physics world
|
||||
this._body = PhysicsWorld.createBody({
|
||||
entityId: this.entityId,
|
||||
type: 'dynamic'
|
||||
});
|
||||
}
|
||||
|
||||
onRemovedFromEntity(): void {
|
||||
// Remove rigid body from physics world
|
||||
if (this._body) {
|
||||
PhysicsWorld.removeBody(this._body);
|
||||
this._body = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
### Avoid Accessing Other Components in Lifecycle
|
||||
|
||||
```typescript
|
||||
@ECSComponent('BadComponent')
|
||||
class BadComponent extends Component {
|
||||
onAddedToEntity(): void {
|
||||
// ⚠️ Not recommended: Other components may not be added yet
|
||||
const other = this.entity?.getComponent(OtherComponent);
|
||||
if (other) {
|
||||
// May be null
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Recommended: Use System to Handle Inter-Component Interactions
|
||||
|
||||
```typescript
|
||||
@ECSSystem('InitializationSystem')
|
||||
class InitializationSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(ComponentA, ComponentB));
|
||||
}
|
||||
|
||||
// Use onAdded event to ensure both components exist
|
||||
onAdded(entity: Entity): void {
|
||||
const a = entity.getComponent(ComponentA)!;
|
||||
const b = entity.getComponent(ComponentB)!;
|
||||
// Safely initialize interaction
|
||||
a.linkTo(b);
|
||||
}
|
||||
|
||||
onRemoved(entity: Entity): void {
|
||||
// Cleanup
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Comparison with System Lifecycle
|
||||
|
||||
| Feature | Component Lifecycle | System Lifecycle |
|
||||
|---------|---------------------|------------------|
|
||||
| Trigger Timing | When component added/removed | When match conditions met |
|
||||
| Use Case | Resource init/cleanup | Business logic processing |
|
||||
| Access Other Components | Not recommended | Safe |
|
||||
| Access Scene | Limited | Full |
|
||||
225
docs/src/content/docs/en/guide/entity-query/best-practices.md
Normal file
225
docs/src/content/docs/en/guide/entity-query/best-practices.md
Normal file
@@ -0,0 +1,225 @@
|
||||
---
|
||||
title: "Best Practices"
|
||||
description: "Entity query optimization and practical applications"
|
||||
---
|
||||
|
||||
## Design Principles
|
||||
|
||||
### 1. Prefer EntitySystem
|
||||
|
||||
```typescript
|
||||
// ✅ Recommended: Use EntitySystem
|
||||
class GoodSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(HealthComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Automatically get matching entities, updated each frame
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ Not recommended: Manual query in update
|
||||
class BadSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty());
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Manual query each frame wastes performance
|
||||
const result = this.scene!.querySystem.queryAll(HealthComponent);
|
||||
for (const entity of result.entities) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Use none() for Exclusions
|
||||
|
||||
```typescript
|
||||
// Exclude dead enemies
|
||||
class EnemyAISystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(
|
||||
Matcher.empty()
|
||||
.all(EnemyTag, AIComponent)
|
||||
.none(DeadTag) // Don't process dead enemies
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Use Tags for Query Optimization
|
||||
|
||||
```typescript
|
||||
// ❌ Bad: Query all entities then filter
|
||||
const allEntities = scene.querySystem.getAllEntities();
|
||||
const players = allEntities.filter(e => e.hasComponent(PlayerTag));
|
||||
|
||||
// ✅ Good: Query directly by tag
|
||||
const players = scene.querySystem.queryByTag(Tags.PLAYER).entities;
|
||||
```
|
||||
|
||||
### 4. Avoid Overly Complex Query Conditions
|
||||
|
||||
```typescript
|
||||
// ❌ Not recommended: Too complex
|
||||
super(
|
||||
Matcher.empty()
|
||||
.all(A, B, C, D)
|
||||
.any(E, F, G)
|
||||
.none(H, I, J)
|
||||
);
|
||||
|
||||
// ✅ Recommended: Split into multiple simple systems
|
||||
class SystemAB extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(A, B));
|
||||
}
|
||||
}
|
||||
|
||||
class SystemCD extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(C, D));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Practical Applications
|
||||
|
||||
### Scenario 1: Physics System
|
||||
|
||||
```typescript
|
||||
class PhysicsSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(TransformComponent, RigidbodyComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const transform = entity.getComponent(TransformComponent)!;
|
||||
const rigidbody = entity.getComponent(RigidbodyComponent)!;
|
||||
|
||||
// Apply gravity
|
||||
rigidbody.velocity.y -= 9.8 * Time.deltaTime;
|
||||
|
||||
// Update position
|
||||
transform.position.x += rigidbody.velocity.x * Time.deltaTime;
|
||||
transform.position.y += rigidbody.velocity.y * Time.deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Scenario 2: Render System
|
||||
|
||||
```typescript
|
||||
class RenderSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(
|
||||
Matcher.empty()
|
||||
.all(TransformComponent, SpriteComponent)
|
||||
.none(InvisibleTag) // Exclude invisible entities
|
||||
);
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Sort by z-order
|
||||
const sorted = entities.slice().sort((a, b) => {
|
||||
const zA = a.getComponent(TransformComponent)!.z;
|
||||
const zB = b.getComponent(TransformComponent)!.z;
|
||||
return zA - zB;
|
||||
});
|
||||
|
||||
// Render entities
|
||||
for (const entity of sorted) {
|
||||
const transform = entity.getComponent(TransformComponent)!;
|
||||
const sprite = entity.getComponent(SpriteComponent)!;
|
||||
|
||||
renderer.drawSprite(sprite.texture, transform.position);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Scenario 3: Collision Detection
|
||||
|
||||
```typescript
|
||||
class CollisionSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(TransformComponent, ColliderComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Simple O(n²) collision detection
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
for (let j = i + 1; j < entities.length; j++) {
|
||||
this.checkCollision(entities[i], entities[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private checkCollision(a: Entity, b: Entity): void {
|
||||
const transA = a.getComponent(TransformComponent)!;
|
||||
const transB = b.getComponent(TransformComponent)!;
|
||||
const colliderA = a.getComponent(ColliderComponent)!;
|
||||
const colliderB = b.getComponent(ColliderComponent)!;
|
||||
|
||||
if (this.isOverlapping(transA, colliderA, transB, colliderB)) {
|
||||
// Trigger collision event
|
||||
scene.eventSystem.emit('collision', { entityA: a, entityB: b });
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Scenario 4: One-Time Query
|
||||
|
||||
```typescript
|
||||
// Execute one-time query outside of systems
|
||||
class GameManager {
|
||||
private scene: Scene;
|
||||
|
||||
public countEnemies(): number {
|
||||
const result = this.scene.querySystem.queryByTag(Tags.ENEMY);
|
||||
return result.count;
|
||||
}
|
||||
|
||||
public findNearestEnemy(playerPos: Vector2): Entity | null {
|
||||
const enemies = this.scene.querySystem.queryByTag(Tags.ENEMY);
|
||||
|
||||
let nearest: Entity | null = null;
|
||||
let minDistance = Infinity;
|
||||
|
||||
for (const enemy of enemies.entities) {
|
||||
const transform = enemy.getComponent(TransformComponent);
|
||||
if (!transform) continue;
|
||||
|
||||
const distance = Vector2.distance(playerPos, transform.position);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
nearest = enemy;
|
||||
}
|
||||
}
|
||||
|
||||
return nearest;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Statistics
|
||||
|
||||
```typescript
|
||||
const stats = querySystem.getStats();
|
||||
console.log('Total queries:', stats.queryStats.totalQueries);
|
||||
console.log('Cache hit rate:', stats.queryStats.cacheHitRate);
|
||||
console.log('Cache size:', stats.cacheStats.size);
|
||||
```
|
||||
|
||||
## Related APIs
|
||||
|
||||
- [Matcher](/api/classes/Matcher/) - Query condition descriptor API reference
|
||||
- [QuerySystem](/api/classes/QuerySystem/) - Query system API reference
|
||||
- [EntitySystem](/api/classes/EntitySystem/) - Entity system API reference
|
||||
- [Entity](/api/classes/Entity/) - Entity API reference
|
||||
133
docs/src/content/docs/en/guide/entity-query/compiled-query.md
Normal file
133
docs/src/content/docs/en/guide/entity-query/compiled-query.md
Normal file
@@ -0,0 +1,133 @@
|
||||
---
|
||||
title: "Compiled Query"
|
||||
description: "CompiledQuery type-safe query tool"
|
||||
---
|
||||
|
||||
> **v2.4.0+**
|
||||
|
||||
CompiledQuery is a lightweight query tool providing type-safe component access and change detection support. Suitable for ad-hoc queries, tool development, and simple iteration scenarios.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```typescript
|
||||
// Create compiled query
|
||||
const query = scene.querySystem.compile(Position, Velocity);
|
||||
|
||||
// Type-safe iteration - component parameters auto-infer types
|
||||
query.forEach((entity, pos, vel) => {
|
||||
pos.x += vel.vx * deltaTime;
|
||||
pos.y += vel.vy * deltaTime;
|
||||
});
|
||||
|
||||
// Get entity count
|
||||
console.log(`Matched entities: ${query.count}`);
|
||||
|
||||
// Get first matched entity
|
||||
const first = query.first();
|
||||
if (first) {
|
||||
const [entity, pos, vel] = first;
|
||||
console.log(`First entity: ${entity.name}`);
|
||||
}
|
||||
```
|
||||
|
||||
## Change Detection
|
||||
|
||||
CompiledQuery supports epoch-based change detection:
|
||||
|
||||
```typescript
|
||||
class RenderSystem extends EntitySystem {
|
||||
private _query: CompiledQuery<[typeof Transform, typeof Sprite]>;
|
||||
private _lastEpoch = 0;
|
||||
|
||||
protected onInitialize(): void {
|
||||
this._query = this.scene!.querySystem.compile(Transform, Sprite);
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Only process entities where Transform or Sprite changed
|
||||
this._query.forEachChanged(this._lastEpoch, (entity, transform, sprite) => {
|
||||
this.updateRenderData(entity, transform, sprite);
|
||||
});
|
||||
|
||||
// Save current epoch as next check starting point
|
||||
this._lastEpoch = this.scene!.epochManager.current;
|
||||
}
|
||||
|
||||
private updateRenderData(entity: Entity, transform: Transform, sprite: Sprite): void {
|
||||
// Update render data
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Functional API
|
||||
|
||||
CompiledQuery provides rich functional APIs:
|
||||
|
||||
```typescript
|
||||
const query = scene.querySystem.compile(Position, Health);
|
||||
|
||||
// map - Transform entity data
|
||||
const positions = query.map((entity, pos, health) => ({
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
healthPercent: health.current / health.max
|
||||
}));
|
||||
|
||||
// filter - Filter entities
|
||||
const lowHealthEntities = query.filter((entity, pos, health) => {
|
||||
return health.current < health.max * 0.2;
|
||||
});
|
||||
|
||||
// find - Find first matching entity
|
||||
const target = query.find((entity, pos, health) => {
|
||||
return health.current > 0 && pos.x > 100;
|
||||
});
|
||||
|
||||
// toArray - Convert to array
|
||||
const allData = query.toArray();
|
||||
for (const [entity, pos, health] of allData) {
|
||||
console.log(`${entity.name}: ${pos.x}, ${pos.y}`);
|
||||
}
|
||||
|
||||
// any/empty - Check for matches
|
||||
if (query.any()) {
|
||||
console.log('Has matching entities');
|
||||
}
|
||||
if (query.empty()) {
|
||||
console.log('No matching entities');
|
||||
}
|
||||
```
|
||||
|
||||
## CompiledQuery vs EntitySystem
|
||||
|
||||
| Feature | CompiledQuery | EntitySystem |
|
||||
|---------|---------------|--------------|
|
||||
| **Purpose** | Lightweight query tool | Complete system logic |
|
||||
| **Lifecycle** | None | Full (onInitialize, onDestroy, etc.) |
|
||||
| **Scheduling Integration** | None | Supports @Stage, @Before, @After |
|
||||
| **Change Detection** | forEachChanged | forEachChanged |
|
||||
| **Event Listening** | None | addEventListener |
|
||||
| **Command Buffer** | None | this.commands |
|
||||
| **Type-Safe Components** | forEach params auto-infer | Need manual getComponent |
|
||||
| **Use Cases** | Ad-hoc queries, tools, prototypes | Core game logic |
|
||||
|
||||
**Selection Advice**:
|
||||
|
||||
- Use **EntitySystem** for core game logic (movement, combat, AI, etc.)
|
||||
- Use **CompiledQuery** for one-time queries, tool development, or simple iteration
|
||||
|
||||
## API Reference
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `forEach(callback)` | Iterate all matched entities, type-safe component params |
|
||||
| `forEachChanged(sinceEpoch, callback)` | Only iterate changed entities |
|
||||
| `first()` | Get first matched entity and components |
|
||||
| `toArray()` | Convert to [entity, ...components] array |
|
||||
| `map(callback)` | Map transformation |
|
||||
| `filter(predicate)` | Filter entities |
|
||||
| `find(predicate)` | Find first entity meeting condition |
|
||||
| `any()` | Whether any matches exist |
|
||||
| `empty()` | Whether no matches exist |
|
||||
| `count` | Number of matched entities |
|
||||
| `entities` | Matched entity list (read-only) |
|
||||
244
docs/src/content/docs/en/guide/entity-query/index.md
Normal file
244
docs/src/content/docs/en/guide/entity-query/index.md
Normal file
@@ -0,0 +1,244 @@
|
||||
---
|
||||
title: "Entity Query System"
|
||||
description: "ECS entity query core concepts and basic usage"
|
||||
---
|
||||
|
||||
Entity querying is one of the core features of ECS architecture. This guide introduces how to use Matcher and QuerySystem to query and filter entities.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Matcher - Query Condition Descriptor
|
||||
|
||||
Matcher is a chainable API used to describe entity query conditions. It doesn't execute queries itself but passes conditions to EntitySystem or QuerySystem.
|
||||
|
||||
### QuerySystem - Query Execution Engine
|
||||
|
||||
QuerySystem is responsible for actually executing queries, using reactive query mechanisms internally for automatic performance optimization.
|
||||
|
||||
## Using Matcher in EntitySystem
|
||||
|
||||
This is the most common usage. EntitySystem automatically filters and processes entities matching conditions through Matcher.
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, Matcher, Entity, Component } from '@esengine/ecs-framework';
|
||||
|
||||
class PositionComponent extends Component {
|
||||
public x: number = 0;
|
||||
public y: number = 0;
|
||||
}
|
||||
|
||||
class VelocityComponent extends Component {
|
||||
public vx: number = 0;
|
||||
public vy: number = 0;
|
||||
}
|
||||
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// Method 1: Use Matcher.empty().all()
|
||||
super(Matcher.empty().all(PositionComponent, VelocityComponent));
|
||||
|
||||
// Method 2: Use Matcher.all() directly (equivalent)
|
||||
// super(Matcher.all(PositionComponent, VelocityComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const pos = entity.getComponent(PositionComponent)!;
|
||||
const vel = entity.getComponent(VelocityComponent)!;
|
||||
|
||||
pos.x += vel.vx;
|
||||
pos.y += vel.vy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add to scene
|
||||
scene.addEntityProcessor(new MovementSystem());
|
||||
```
|
||||
|
||||
### Matcher Chainable API
|
||||
|
||||
#### all() - Must Include All Components
|
||||
|
||||
```typescript
|
||||
class HealthSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// Entity must have both Health and Position components
|
||||
super(Matcher.empty().all(HealthComponent, PositionComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Only process entities with both components
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### any() - Include At Least One Component
|
||||
|
||||
```typescript
|
||||
class DamageableSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// Entity must have at least Health or Shield
|
||||
super(Matcher.any(HealthComponent, ShieldComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Process entities with health or shield
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### none() - Must Not Include Components
|
||||
|
||||
```typescript
|
||||
class AliveEntitySystem extends EntitySystem {
|
||||
constructor() {
|
||||
// Entity must not have DeadTag component
|
||||
super(Matcher.all(HealthComponent).none(DeadTag));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Only process living entities
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Combined Conditions
|
||||
|
||||
```typescript
|
||||
class CombatSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(
|
||||
Matcher.empty()
|
||||
.all(PositionComponent, HealthComponent) // Must have position and health
|
||||
.any(WeaponComponent, MagicComponent) // At least weapon or magic
|
||||
.none(DeadTag, FrozenTag) // Not dead or frozen
|
||||
);
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Process living entities that can fight
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### nothing() - Match No Entities
|
||||
|
||||
Used for systems that only need lifecycle methods (`onBegin`, `onEnd`) but don't process entities.
|
||||
|
||||
```typescript
|
||||
class FrameTimerSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// Match no entities
|
||||
super(Matcher.nothing());
|
||||
}
|
||||
|
||||
protected onBegin(): void {
|
||||
// Execute at frame start
|
||||
Performance.markFrameStart();
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Never called because no matching entities
|
||||
}
|
||||
|
||||
protected onEnd(): void {
|
||||
// Execute at frame end
|
||||
Performance.markFrameEnd();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### empty() vs nothing()
|
||||
|
||||
| Method | Behavior | Use Case |
|
||||
|--------|----------|----------|
|
||||
| `Matcher.empty()` | Match **all** entities | Process all entities in scene |
|
||||
| `Matcher.nothing()` | Match **no** entities | Only need lifecycle callbacks |
|
||||
|
||||
## Using QuerySystem Directly
|
||||
|
||||
If you don't need to create a system, you can use Scene's querySystem directly.
|
||||
|
||||
### Basic Query Methods
|
||||
|
||||
```typescript
|
||||
// Get scene's query system
|
||||
const querySystem = scene.querySystem;
|
||||
|
||||
// Query entities with all specified components
|
||||
const result1 = querySystem.queryAll(PositionComponent, VelocityComponent);
|
||||
console.log(`Found ${result1.count} moving entities`);
|
||||
console.log(`Query time: ${result1.executionTime.toFixed(2)}ms`);
|
||||
|
||||
// Query entities with any specified component
|
||||
const result2 = querySystem.queryAny(WeaponComponent, MagicComponent);
|
||||
console.log(`Found ${result2.count} combat units`);
|
||||
|
||||
// Query entities without specified components
|
||||
const result3 = querySystem.queryNone(DeadTag);
|
||||
console.log(`Found ${result3.count} living entities`);
|
||||
```
|
||||
|
||||
### Query by Tag and Name
|
||||
|
||||
```typescript
|
||||
// Query by tag
|
||||
const playerResult = querySystem.queryByTag(Tags.PLAYER);
|
||||
for (const player of playerResult.entities) {
|
||||
console.log('Player:', player.name);
|
||||
}
|
||||
|
||||
// Query by name
|
||||
const bossResult = querySystem.queryByName('Boss');
|
||||
if (bossResult.count > 0) {
|
||||
const boss = bossResult.entities[0];
|
||||
console.log('Found Boss:', boss);
|
||||
}
|
||||
|
||||
// Query by single component
|
||||
const healthResult = querySystem.queryByComponent(HealthComponent);
|
||||
console.log(`${healthResult.count} entities have health`);
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Automatic Caching
|
||||
|
||||
QuerySystem uses reactive queries internally with automatic caching:
|
||||
|
||||
```typescript
|
||||
// First query, executes actual query
|
||||
const result1 = querySystem.queryAll(PositionComponent);
|
||||
console.log('fromCache:', result1.fromCache); // false
|
||||
|
||||
// Second same query, uses cache
|
||||
const result2 = querySystem.queryAll(PositionComponent);
|
||||
console.log('fromCache:', result2.fromCache); // true
|
||||
```
|
||||
|
||||
### Automatic Updates on Entity Changes
|
||||
|
||||
Query cache updates automatically when entities add/remove components:
|
||||
|
||||
```typescript
|
||||
// Query entities with weapons
|
||||
const before = querySystem.queryAll(WeaponComponent);
|
||||
console.log('Before:', before.count); // Assume 5
|
||||
|
||||
// Add weapon to entity
|
||||
const enemy = scene.createEntity('Enemy');
|
||||
enemy.addComponent(new WeaponComponent());
|
||||
|
||||
// Query again, automatically includes new entity
|
||||
const after = querySystem.queryAll(WeaponComponent);
|
||||
console.log('After:', after.count); // Now 6
|
||||
```
|
||||
|
||||
## More Topics
|
||||
|
||||
- [Matcher API](/en/guide/entity-query/matcher-api) - Complete Matcher API reference
|
||||
- [Compiled Query](/en/guide/entity-query/compiled-query) - CompiledQuery advanced usage
|
||||
- [Best Practices](/en/guide/entity-query/best-practices) - Query optimization and practical applications
|
||||
118
docs/src/content/docs/en/guide/entity-query/matcher-api.md
Normal file
118
docs/src/content/docs/en/guide/entity-query/matcher-api.md
Normal file
@@ -0,0 +1,118 @@
|
||||
---
|
||||
title: "Matcher API"
|
||||
description: "Complete Matcher API reference"
|
||||
---
|
||||
|
||||
## Static Creation Methods
|
||||
|
||||
| Method | Description | Example |
|
||||
|--------|-------------|---------|
|
||||
| `Matcher.all(...types)` | Must include all specified components | `Matcher.all(Position, Velocity)` |
|
||||
| `Matcher.any(...types)` | Include at least one specified component | `Matcher.any(Health, Shield)` |
|
||||
| `Matcher.none(...types)` | Must not include any specified components | `Matcher.none(Dead)` |
|
||||
| `Matcher.byTag(tag)` | Query by tag | `Matcher.byTag(1)` |
|
||||
| `Matcher.byName(name)` | Query by name | `Matcher.byName("Player")` |
|
||||
| `Matcher.byComponent(type)` | Query by single component | `Matcher.byComponent(Health)` |
|
||||
| `Matcher.empty()` | Create empty matcher (matches all entities) | `Matcher.empty()` |
|
||||
| `Matcher.nothing()` | Match no entities | `Matcher.nothing()` |
|
||||
| `Matcher.complex()` | Create complex query builder | `Matcher.complex()` |
|
||||
|
||||
## Chainable Methods
|
||||
|
||||
| Method | Description | Example |
|
||||
|--------|-------------|---------|
|
||||
| `.all(...types)` | Add required components | `.all(Position)` |
|
||||
| `.any(...types)` | Add optional components (at least one) | `.any(Weapon, Magic)` |
|
||||
| `.none(...types)` | Add excluded components | `.none(Dead)` |
|
||||
| `.exclude(...types)` | Alias for `.none()` | `.exclude(Disabled)` |
|
||||
| `.one(...types)` | Alias for `.any()` | `.one(Player, Enemy)` |
|
||||
| `.withTag(tag)` | Add tag condition | `.withTag(1)` |
|
||||
| `.withName(name)` | Add name condition | `.withName("Boss")` |
|
||||
| `.withComponent(type)` | Add single component condition | `.withComponent(Health)` |
|
||||
|
||||
## Utility Methods
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `.getCondition()` | Get query condition (read-only) |
|
||||
| `.isEmpty()` | Check if empty condition |
|
||||
| `.isNothing()` | Check if nothing matcher |
|
||||
| `.clone()` | Clone matcher |
|
||||
| `.reset()` | Reset all conditions |
|
||||
| `.toString()` | Get string representation |
|
||||
|
||||
## Common Combination Examples
|
||||
|
||||
```typescript
|
||||
// Basic movement system
|
||||
Matcher.all(Position, Velocity)
|
||||
|
||||
// Attackable living entities
|
||||
Matcher.all(Position, Health)
|
||||
.any(Weapon, Magic)
|
||||
.none(Dead, Disabled)
|
||||
|
||||
// All tagged enemies
|
||||
Matcher.byTag(Tags.ENEMY)
|
||||
.all(AIComponent)
|
||||
|
||||
// System only needing lifecycle
|
||||
Matcher.nothing()
|
||||
```
|
||||
|
||||
## Query by Tag
|
||||
|
||||
```typescript
|
||||
class PlayerSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// Query entities with specific tag
|
||||
super(Matcher.empty().withTag(Tags.PLAYER));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Only process player entities
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Query by Name
|
||||
|
||||
```typescript
|
||||
class BossSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// Query entities with specific name
|
||||
super(Matcher.empty().withName('Boss'));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Only process entities named 'Boss'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
### Matcher is Immutable
|
||||
|
||||
```typescript
|
||||
const matcher = Matcher.empty().all(PositionComponent);
|
||||
|
||||
// Chain calls return new Matcher instances
|
||||
const matcher2 = matcher.any(VelocityComponent);
|
||||
|
||||
// Original matcher unchanged
|
||||
console.log(matcher === matcher2); // false
|
||||
```
|
||||
|
||||
### Query Results are Read-Only
|
||||
|
||||
```typescript
|
||||
const result = querySystem.queryAll(PositionComponent);
|
||||
|
||||
// Don't modify returned array
|
||||
result.entities.push(someEntity); // Wrong!
|
||||
|
||||
// If modification needed, copy first
|
||||
const mutableArray = [...result.entities];
|
||||
mutableArray.push(someEntity); // Correct
|
||||
```
|
||||
273
docs/src/content/docs/en/guide/entity/component-operations.md
Normal file
273
docs/src/content/docs/en/guide/entity/component-operations.md
Normal file
@@ -0,0 +1,273 @@
|
||||
---
|
||||
title: "Component Operations"
|
||||
description: "Detailed guide to adding, getting, and removing entity components"
|
||||
---
|
||||
|
||||
Entities gain functionality by adding components. This section details all component operation APIs.
|
||||
|
||||
## Adding Components
|
||||
|
||||
### addComponent
|
||||
|
||||
Add an already-created component instance:
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
x: number = 0;
|
||||
y: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
|
||||
const player = scene.createEntity("Player");
|
||||
const position = new Position(100, 200);
|
||||
player.addComponent(position);
|
||||
```
|
||||
|
||||
### createComponent
|
||||
|
||||
Pass the component type and constructor arguments directly—the entity creates the instance (recommended):
|
||||
|
||||
```typescript
|
||||
// Create and add component
|
||||
const position = player.createComponent(Position, 100, 200);
|
||||
const health = player.createComponent(Health, 150);
|
||||
|
||||
// Equivalent to:
|
||||
// const position = new Position(100, 200);
|
||||
// player.addComponent(position);
|
||||
```
|
||||
|
||||
### addComponents
|
||||
|
||||
Add multiple components at once:
|
||||
|
||||
```typescript
|
||||
const components = player.addComponents([
|
||||
new Position(100, 200),
|
||||
new Health(150),
|
||||
new Velocity(0, 0)
|
||||
]);
|
||||
```
|
||||
|
||||
:::note[Important Notes]
|
||||
- An entity cannot have two components of the same type—an exception will be thrown
|
||||
- The entity must be added to a scene before adding components
|
||||
:::
|
||||
|
||||
## Getting Components
|
||||
|
||||
### getComponent
|
||||
|
||||
Get a component of a specific type:
|
||||
|
||||
```typescript
|
||||
// Returns Position | null
|
||||
const position = player.getComponent(Position);
|
||||
|
||||
if (position) {
|
||||
position.x += 10;
|
||||
position.y += 20;
|
||||
}
|
||||
```
|
||||
|
||||
### hasComponent
|
||||
|
||||
Check if an entity has a specific component type:
|
||||
|
||||
```typescript
|
||||
if (player.hasComponent(Position)) {
|
||||
const position = player.getComponent(Position)!;
|
||||
// Use ! because we confirmed it exists
|
||||
}
|
||||
```
|
||||
|
||||
### getComponents
|
||||
|
||||
Get all components of a specific type (for multi-component scenarios):
|
||||
|
||||
```typescript
|
||||
const allHealthComponents = player.getComponents(Health);
|
||||
```
|
||||
|
||||
### getComponentByType
|
||||
|
||||
Get components with inheritance support using `instanceof` checking:
|
||||
|
||||
```typescript
|
||||
// Find CompositeNodeComponent or any subclass
|
||||
const composite = entity.getComponentByType(CompositeNodeComponent);
|
||||
if (composite) {
|
||||
// composite could be SequenceNode, SelectorNode, etc.
|
||||
}
|
||||
```
|
||||
|
||||
Difference from `getComponent()`:
|
||||
|
||||
| Method | Lookup Method | Performance | Use Case |
|
||||
|--------|---------------|-------------|----------|
|
||||
| `getComponent` | Exact type match (bitmask) | High | Known exact type |
|
||||
| `getComponentByType` | `instanceof` check | Lower | Need inheritance support |
|
||||
|
||||
### getOrCreateComponent
|
||||
|
||||
Get or create a component—automatically creates if it doesn't exist:
|
||||
|
||||
```typescript
|
||||
// Ensure entity has Position component
|
||||
const position = player.getOrCreateComponent(Position, 0, 0);
|
||||
position.x = 100;
|
||||
|
||||
// If exists, returns existing component
|
||||
// If not, creates new component with (0, 0) args
|
||||
```
|
||||
|
||||
### components Property
|
||||
|
||||
Get all entity components (read-only):
|
||||
|
||||
```typescript
|
||||
const allComponents = player.components; // readonly Component[]
|
||||
|
||||
allComponents.forEach(component => {
|
||||
console.log(component.constructor.name);
|
||||
});
|
||||
```
|
||||
|
||||
## Removing Components
|
||||
|
||||
### removeComponent
|
||||
|
||||
Remove by component instance:
|
||||
|
||||
```typescript
|
||||
const healthComponent = player.getComponent(Health);
|
||||
if (healthComponent) {
|
||||
player.removeComponent(healthComponent);
|
||||
}
|
||||
```
|
||||
|
||||
### removeComponentByType
|
||||
|
||||
Remove by component type:
|
||||
|
||||
```typescript
|
||||
const removedHealth = player.removeComponentByType(Health);
|
||||
if (removedHealth) {
|
||||
console.log("Health component removed");
|
||||
}
|
||||
```
|
||||
|
||||
### removeComponentsByTypes
|
||||
|
||||
Remove multiple component types at once:
|
||||
|
||||
```typescript
|
||||
const removedComponents = player.removeComponentsByTypes([
|
||||
Position,
|
||||
Health,
|
||||
Velocity
|
||||
]);
|
||||
```
|
||||
|
||||
### removeAllComponents
|
||||
|
||||
Remove all components:
|
||||
|
||||
```typescript
|
||||
player.removeAllComponents();
|
||||
```
|
||||
|
||||
## Change Detection
|
||||
|
||||
### markDirty
|
||||
|
||||
Mark components as modified for frame-level change detection:
|
||||
|
||||
```typescript
|
||||
const pos = entity.getComponent(Position)!;
|
||||
pos.x = 100;
|
||||
entity.markDirty(pos);
|
||||
|
||||
// Or mark multiple components
|
||||
const vel = entity.getComponent(Velocity)!;
|
||||
entity.markDirty(pos, vel);
|
||||
```
|
||||
|
||||
Use with reactive queries:
|
||||
|
||||
```typescript
|
||||
// Query for components modified this frame
|
||||
const changedQuery = scene.createReactiveQuery({
|
||||
all: [Position],
|
||||
changed: [Position] // Only match modified this frame
|
||||
});
|
||||
|
||||
for (const entity of changedQuery.getEntities()) {
|
||||
// Handle entities with position changes
|
||||
}
|
||||
```
|
||||
|
||||
## Component Mask
|
||||
|
||||
Each entity maintains a component bitmask for efficient `hasComponent` checks:
|
||||
|
||||
```typescript
|
||||
// Get component mask (internal use)
|
||||
const mask = entity.componentMask;
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
constructor(public x = 0, public y = 0) { super(); }
|
||||
}
|
||||
|
||||
@ECSComponent('Health')
|
||||
class Health extends Component {
|
||||
constructor(public current = 100, public max = 100) { super(); }
|
||||
}
|
||||
|
||||
// Create entity and add components
|
||||
const player = scene.createEntity("Player");
|
||||
player.createComponent(Position, 100, 200);
|
||||
player.createComponent(Health, 150, 150);
|
||||
|
||||
// Get and modify component
|
||||
const position = player.getComponent(Position);
|
||||
if (position) {
|
||||
position.x += 10;
|
||||
player.markDirty(position);
|
||||
}
|
||||
|
||||
// Get or create component
|
||||
const velocity = player.getOrCreateComponent(Velocity, 0, 0);
|
||||
|
||||
// Check component existence
|
||||
if (player.hasComponent(Health)) {
|
||||
const health = player.getComponent(Health)!;
|
||||
health.current -= 10;
|
||||
}
|
||||
|
||||
// Remove component
|
||||
player.removeComponentByType(Velocity);
|
||||
|
||||
// List all components
|
||||
console.log(player.components.map(c => c.constructor.name));
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Entity Handle](/en/guide/entity/entity-handle/) - Safe cross-frame entity references
|
||||
- [Component System](/en/guide/component/) - Component definition and lifecycle
|
||||
265
docs/src/content/docs/en/guide/entity/entity-handle.md
Normal file
265
docs/src/content/docs/en/guide/entity/entity-handle.md
Normal file
@@ -0,0 +1,265 @@
|
||||
---
|
||||
title: "Entity Handle"
|
||||
description: "Using EntityHandle to safely reference entities and avoid referencing destroyed entities"
|
||||
---
|
||||
|
||||
Entity handles (EntityHandle) provide a safe way to reference entities, solving the "referencing destroyed entities" problem.
|
||||
|
||||
## The Problem
|
||||
|
||||
Imagine your AI system needs to track a target enemy:
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong: Storing entity reference directly
|
||||
class AISystem extends EntitySystem {
|
||||
private targetEnemy: Entity | null = null;
|
||||
|
||||
setTarget(enemy: Entity) {
|
||||
this.targetEnemy = enemy;
|
||||
}
|
||||
|
||||
process() {
|
||||
if (this.targetEnemy) {
|
||||
// Danger! Enemy might be destroyed but reference still exists
|
||||
// Worse: This memory location might be reused by a new entity
|
||||
const health = this.targetEnemy.getComponent(Health);
|
||||
// Might operate on wrong entity!
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## What is EntityHandle
|
||||
|
||||
EntityHandle is a numeric entity identifier containing:
|
||||
- **Index**: Entity's position in the array
|
||||
- **Generation**: Number of times the entity slot has been reused
|
||||
|
||||
When an entity is destroyed, even if its index is reused by a new entity, the generation increases, invalidating old handles.
|
||||
|
||||
```typescript
|
||||
import { EntityHandle, NULL_HANDLE, isValidHandle } from '@esengine/ecs-framework';
|
||||
|
||||
// Each entity gets a handle when created
|
||||
const handle: EntityHandle = entity.handle;
|
||||
|
||||
// Null handle constant
|
||||
const emptyHandle = NULL_HANDLE;
|
||||
|
||||
// Check if handle is non-null
|
||||
if (isValidHandle(handle)) {
|
||||
// Handle is valid
|
||||
}
|
||||
```
|
||||
|
||||
## The Correct Approach
|
||||
|
||||
```typescript
|
||||
import { EntityHandle, NULL_HANDLE, isValidHandle } from '@esengine/ecs-framework';
|
||||
|
||||
class AISystem extends EntitySystem {
|
||||
// ✅ Store handle instead of entity reference
|
||||
private targetHandle: EntityHandle = NULL_HANDLE;
|
||||
|
||||
setTarget(enemy: Entity) {
|
||||
this.targetHandle = enemy.handle;
|
||||
}
|
||||
|
||||
process() {
|
||||
if (!isValidHandle(this.targetHandle)) {
|
||||
return; // No target
|
||||
}
|
||||
|
||||
// Get entity via handle (auto-validates)
|
||||
const enemy = this.scene.findEntityByHandle(this.targetHandle);
|
||||
|
||||
if (!enemy) {
|
||||
// Enemy destroyed, clear reference
|
||||
this.targetHandle = NULL_HANDLE;
|
||||
return;
|
||||
}
|
||||
|
||||
// Safe operation
|
||||
const health = enemy.getComponent(Health);
|
||||
if (health) {
|
||||
// Deal damage to enemy
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Getting Handle
|
||||
|
||||
```typescript
|
||||
// Get handle from entity
|
||||
const handle = entity.handle;
|
||||
```
|
||||
|
||||
### Validating Handle
|
||||
|
||||
```typescript
|
||||
import { isValidHandle, NULL_HANDLE } from '@esengine/ecs-framework';
|
||||
|
||||
// Check if handle is non-null
|
||||
if (isValidHandle(handle)) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// Check if entity is alive
|
||||
const alive = scene.handleManager.isAlive(handle);
|
||||
```
|
||||
|
||||
### Getting Entity by Handle
|
||||
|
||||
```typescript
|
||||
// Returns Entity | null
|
||||
const entity = scene.findEntityByHandle(handle);
|
||||
|
||||
if (entity) {
|
||||
// Entity exists and is valid
|
||||
}
|
||||
```
|
||||
|
||||
## Complete Example: Skill Target Locking
|
||||
|
||||
```typescript
|
||||
import {
|
||||
EntitySystem,
|
||||
Entity,
|
||||
EntityHandle,
|
||||
NULL_HANDLE,
|
||||
isValidHandle
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
@ECSSystem('SkillTargeting')
|
||||
class SkillTargetingSystem extends EntitySystem {
|
||||
// Store multiple target handles
|
||||
private lockedTargets: Map<number, EntityHandle> = new Map();
|
||||
|
||||
// Lock target
|
||||
lockTarget(casterId: number, target: Entity) {
|
||||
this.lockedTargets.set(casterId, target.handle);
|
||||
}
|
||||
|
||||
// Get locked target
|
||||
getLockedTarget(casterId: number): Entity | null {
|
||||
const handle = this.lockedTargets.get(casterId);
|
||||
|
||||
if (!handle || !isValidHandle(handle)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const target = this.scene.findEntityByHandle(handle);
|
||||
|
||||
if (!target) {
|
||||
// Target dead, clear lock
|
||||
this.lockedTargets.delete(casterId);
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
// Cast skill
|
||||
castSkill(caster: Entity) {
|
||||
const target = this.getLockedTarget(caster.id);
|
||||
|
||||
if (!target) {
|
||||
console.log('Target lost, skill cancelled');
|
||||
return;
|
||||
}
|
||||
|
||||
const health = target.getComponent(Health);
|
||||
if (health) {
|
||||
health.current -= 10;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear target for specific caster
|
||||
clearTarget(casterId: number) {
|
||||
this.lockedTargets.delete(casterId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage Guidelines
|
||||
|
||||
| Scenario | Recommended Approach |
|
||||
|----------|---------------------|
|
||||
| Same-frame temporary use | Direct `Entity` reference |
|
||||
| Cross-frame storage (AI target, skill target) | Use `EntityHandle` |
|
||||
| Serialization/save | Use `EntityHandle` (numeric type) |
|
||||
| Network sync | Use `EntityHandle` (directly transferable) |
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- EntityHandle is a numeric type with small memory footprint
|
||||
- `findEntityByHandle` is O(1) operation
|
||||
- Safer and more reliable than checking `entity.isDestroyed` every frame
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Optional Target Reference
|
||||
|
||||
```typescript
|
||||
class FollowComponent extends Component {
|
||||
private _targetHandle: EntityHandle = NULL_HANDLE;
|
||||
|
||||
setTarget(target: Entity | null) {
|
||||
this._targetHandle = target?.handle ?? NULL_HANDLE;
|
||||
}
|
||||
|
||||
getTarget(scene: IScene): Entity | null {
|
||||
if (!isValidHandle(this._targetHandle)) {
|
||||
return null;
|
||||
}
|
||||
return scene.findEntityByHandle(this._targetHandle);
|
||||
}
|
||||
|
||||
hasTarget(): boolean {
|
||||
return isValidHandle(this._targetHandle);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Multi-Target Tracking
|
||||
|
||||
```typescript
|
||||
class MultiTargetComponent extends Component {
|
||||
private targets: EntityHandle[] = [];
|
||||
|
||||
addTarget(target: Entity) {
|
||||
this.targets.push(target.handle);
|
||||
}
|
||||
|
||||
removeTarget(target: Entity) {
|
||||
const index = this.targets.indexOf(target.handle);
|
||||
if (index >= 0) {
|
||||
this.targets.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
getValidTargets(scene: IScene): Entity[] {
|
||||
const valid: Entity[] = [];
|
||||
const stillValid: EntityHandle[] = [];
|
||||
|
||||
for (const handle of this.targets) {
|
||||
const entity = scene.findEntityByHandle(handle);
|
||||
if (entity) {
|
||||
valid.push(entity);
|
||||
stillValid.push(handle);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up invalid handles
|
||||
this.targets = stillValid;
|
||||
return valid;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Lifecycle](/en/guide/entity/lifecycle/) - Entity destruction and persistence
|
||||
- [Entity Reference](/en/guide/component/entity-ref/) - Entity reference decorators in components
|
||||
174
docs/src/content/docs/en/guide/entity/index.md
Normal file
174
docs/src/content/docs/en/guide/entity/index.md
Normal file
@@ -0,0 +1,174 @@
|
||||
---
|
||||
title: "Entity Overview"
|
||||
description: "Basic concepts and usage of entities in ECS architecture"
|
||||
---
|
||||
|
||||
In ECS architecture, an Entity is a fundamental object in the game world. Entities contain no game logic or data themselves—they are simply containers that combine different components to achieve various functionalities.
|
||||
|
||||
## Basic Concepts
|
||||
|
||||
An entity is a lightweight object primarily used for:
|
||||
- Acting as a container for components
|
||||
- Providing unique identifiers (ID and persistentId)
|
||||
- Managing component lifecycles
|
||||
|
||||
:::tip[About Parent-Child Hierarchy]
|
||||
Parent-child relationships between entities are managed through `HierarchyComponent` and `HierarchySystem`, not built-in Entity properties. This design follows ECS composition principles—only entities that need hierarchy relationships add this component.
|
||||
|
||||
See the [Hierarchy System](/en/guide/hierarchy/) documentation for details.
|
||||
:::
|
||||
|
||||
## Creating Entities
|
||||
|
||||
**Entities must be created through the scene, not manually.**
|
||||
|
||||
```typescript
|
||||
// Correct: Create entity through scene
|
||||
const player = scene.createEntity("Player");
|
||||
|
||||
// ❌ Wrong: Manual creation
|
||||
// const entity = new Entity("MyEntity", 1);
|
||||
```
|
||||
|
||||
Creating through the scene ensures:
|
||||
- Entity is properly added to the scene's entity management system
|
||||
- Entity is added to the query system for use by systems
|
||||
- Entity gets the correct scene reference
|
||||
- Related lifecycle events are triggered
|
||||
|
||||
### Batch Creation
|
||||
|
||||
The framework provides high-performance batch creation:
|
||||
|
||||
```typescript
|
||||
// Batch create 100 bullet entities
|
||||
const bullets = scene.createEntities(100, "Bullet");
|
||||
|
||||
bullets.forEach((bullet, index) => {
|
||||
bullet.createComponent(Position, Math.random() * 800, Math.random() * 600);
|
||||
bullet.createComponent(Velocity, Math.random() * 100, Math.random() * 100);
|
||||
});
|
||||
```
|
||||
|
||||
`createEntities()` batches ID allocation, optimizes query system updates, and reduces system cache clearing.
|
||||
|
||||
## Entity Identifiers
|
||||
|
||||
Each entity has three types of identifiers:
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `id` | `number` | Runtime unique identifier for fast lookups |
|
||||
| `persistentId` | `string` | GUID for maintaining reference consistency during serialization |
|
||||
| `handle` | `EntityHandle` | Lightweight handle, see [Entity Handle](/en/guide/entity/entity-handle/) |
|
||||
|
||||
```typescript
|
||||
const entity = scene.createEntity("Player");
|
||||
|
||||
console.log(entity.id); // 1
|
||||
console.log(entity.persistentId); // "a1b2c3d4-..."
|
||||
console.log(entity.handle); // Numeric handle
|
||||
```
|
||||
|
||||
## Entity Properties
|
||||
|
||||
### Name and Tag
|
||||
|
||||
```typescript
|
||||
// Name - for debugging and lookup
|
||||
entity.name = "Player";
|
||||
|
||||
// Tag - for fast categorization and querying
|
||||
entity.tag = 1; // Player tag
|
||||
enemy.tag = 2; // Enemy tag
|
||||
```
|
||||
|
||||
### State Control
|
||||
|
||||
```typescript
|
||||
// Enable/disable state
|
||||
entity.enabled = false;
|
||||
|
||||
// Active state
|
||||
entity.active = false;
|
||||
|
||||
// Update order (lower values have higher priority)
|
||||
entity.updateOrder = 10;
|
||||
```
|
||||
|
||||
## Finding Entities
|
||||
|
||||
The scene provides multiple ways to find entities:
|
||||
|
||||
```typescript
|
||||
// Find by name
|
||||
const player = scene.findEntity("Player");
|
||||
// Or use alias
|
||||
const player2 = scene.getEntityByName("Player");
|
||||
|
||||
// Find by ID
|
||||
const entity = scene.findEntityById(123);
|
||||
|
||||
// Find all entities by tag
|
||||
const enemies = scene.findEntitiesByTag(2);
|
||||
// Or use alias
|
||||
const allEnemies = scene.getEntitiesByTag(2);
|
||||
|
||||
// Find by handle
|
||||
const entity = scene.findEntityByHandle(handle);
|
||||
```
|
||||
|
||||
## Entity Events
|
||||
|
||||
Entity changes trigger events:
|
||||
|
||||
```typescript
|
||||
// Listen for component additions
|
||||
scene.eventSystem.on('component:added', (data) => {
|
||||
console.log(`${data.entityName} added ${data.componentType}`);
|
||||
});
|
||||
|
||||
// Listen for component removals
|
||||
scene.eventSystem.on('component:removed', (data) => {
|
||||
console.log(`${data.entityName} removed ${data.componentType}`);
|
||||
});
|
||||
|
||||
// Listen for entity creation
|
||||
scene.eventSystem.on('entity:created', (data) => {
|
||||
console.log(`Entity created: ${data.entityName}`);
|
||||
});
|
||||
|
||||
// Listen for active state changes
|
||||
scene.eventSystem.on('entity:activeChanged', (data) => {
|
||||
console.log(`${data.entity.name} active: ${data.active}`);
|
||||
});
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
```typescript
|
||||
// Get entity debug info
|
||||
const debugInfo = entity.getDebugInfo();
|
||||
console.log(debugInfo);
|
||||
// {
|
||||
// name: "Player",
|
||||
// id: 1,
|
||||
// persistentId: "a1b2c3d4-...",
|
||||
// enabled: true,
|
||||
// active: true,
|
||||
// destroyed: false,
|
||||
// componentCount: 3,
|
||||
// componentTypes: ["Position", "Health", "Velocity"],
|
||||
// ...
|
||||
// }
|
||||
|
||||
// Entity string representation
|
||||
console.log(entity.toString());
|
||||
// "Entity[Player:1:a1b2c3d4]"
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Component Operations](/en/guide/entity/component-operations/) - Add, get, and remove components
|
||||
- [Entity Handle](/en/guide/entity/entity-handle/) - Safe entity reference method
|
||||
- [Lifecycle](/en/guide/entity/lifecycle/) - Destruction and persistence
|
||||
238
docs/src/content/docs/en/guide/entity/lifecycle.md
Normal file
238
docs/src/content/docs/en/guide/entity/lifecycle.md
Normal file
@@ -0,0 +1,238 @@
|
||||
---
|
||||
title: "Lifecycle"
|
||||
description: "Entity lifecycle management, destruction, and persistence"
|
||||
---
|
||||
|
||||
Entity lifecycle includes three phases: creation, runtime, and destruction. This section covers how to properly manage entity lifecycles.
|
||||
|
||||
## Destroying Entities
|
||||
|
||||
### Basic Destruction
|
||||
|
||||
```typescript
|
||||
// Destroy entity
|
||||
player.destroy();
|
||||
|
||||
// Check if entity is destroyed
|
||||
if (player.isDestroyed) {
|
||||
console.log("Entity has been destroyed");
|
||||
}
|
||||
```
|
||||
|
||||
When destroying an entity:
|
||||
1. All components are removed (triggering `onRemovedFromEntity` callbacks)
|
||||
2. Entity is removed from query systems
|
||||
3. Entity is removed from scene entity list
|
||||
4. All reference tracking is cleaned up
|
||||
|
||||
### Conditional Destruction
|
||||
|
||||
```typescript
|
||||
// Common pattern: Destroy when health depleted
|
||||
const health = enemy.getComponent(Health);
|
||||
if (health && health.current <= 0) {
|
||||
enemy.destroy();
|
||||
}
|
||||
```
|
||||
|
||||
### Destruction Safety
|
||||
|
||||
Destruction is idempotent—multiple calls won't cause errors:
|
||||
|
||||
```typescript
|
||||
player.destroy();
|
||||
player.destroy(); // Safe, no error
|
||||
```
|
||||
|
||||
## Persistent Entities
|
||||
|
||||
By default, entities are destroyed during scene transitions. Persistence allows entities to survive across scenes.
|
||||
|
||||
### Setting Persistence
|
||||
|
||||
```typescript
|
||||
// Method 1: Chain call
|
||||
const player = scene.createEntity('Player')
|
||||
.setPersistent()
|
||||
.createComponent(PlayerComponent);
|
||||
|
||||
// Method 2: Separate call
|
||||
player.setPersistent();
|
||||
|
||||
// Check persistence
|
||||
if (player.isPersistent) {
|
||||
console.log("This is a persistent entity");
|
||||
}
|
||||
```
|
||||
|
||||
### Removing Persistence
|
||||
|
||||
```typescript
|
||||
// Restore to scene-local entity
|
||||
player.setSceneLocal();
|
||||
```
|
||||
|
||||
### Lifecycle Policies
|
||||
|
||||
Entities have two lifecycle policies:
|
||||
|
||||
| Policy | Description |
|
||||
|--------|-------------|
|
||||
| `SceneLocal` | Default, destroyed with scene |
|
||||
| `Persistent` | Survives scene transitions |
|
||||
|
||||
```typescript
|
||||
import { EEntityLifecyclePolicy } from '@esengine/ecs-framework';
|
||||
|
||||
// Get current policy
|
||||
const policy = entity.lifecyclePolicy;
|
||||
|
||||
if (policy === EEntityLifecyclePolicy.Persistent) {
|
||||
// Persistent entity
|
||||
}
|
||||
```
|
||||
|
||||
### Use Cases
|
||||
|
||||
Persistent entities are suitable for:
|
||||
- Player characters
|
||||
- Global managers
|
||||
- UI entities
|
||||
- Game state that needs to survive scene transitions
|
||||
|
||||
```typescript
|
||||
// Player character
|
||||
const player = scene.createEntity('Player')
|
||||
.setPersistent();
|
||||
|
||||
// Game manager
|
||||
const gameManager = scene.createEntity('GameManager')
|
||||
.setPersistent()
|
||||
.createComponent(GameStateComponent);
|
||||
|
||||
// Score manager
|
||||
const scoreManager = scene.createEntity('ScoreManager')
|
||||
.setPersistent()
|
||||
.createComponent(ScoreComponent);
|
||||
```
|
||||
|
||||
## Scene Transition Behavior
|
||||
|
||||
```typescript
|
||||
// Scene manager switches scenes
|
||||
sceneManager.loadScene('Level2');
|
||||
|
||||
// During transition:
|
||||
// 1. SceneLocal entities are destroyed
|
||||
// 2. Persistent entities migrate to new scene
|
||||
// 3. New scene entities are created
|
||||
```
|
||||
|
||||
:::caution[Note]
|
||||
Persistent entities automatically migrate to the new scene during transitions, but other non-persistent entities they reference may be destroyed. Use [EntityHandle](/en/guide/entity/entity-handle/) to safely handle this situation.
|
||||
:::
|
||||
|
||||
## Reference Cleanup
|
||||
|
||||
The framework provides reference tracking that automatically cleans up references when entities are destroyed:
|
||||
|
||||
```typescript
|
||||
// Reference tracker cleans up all references to this entity on destruction
|
||||
scene.referenceTracker?.clearReferencesTo(entity.id);
|
||||
```
|
||||
|
||||
Using the `@entityRef` decorator handles this automatically:
|
||||
|
||||
```typescript
|
||||
class FollowComponent extends Component {
|
||||
@entityRef()
|
||||
targetId: number | null = null;
|
||||
}
|
||||
|
||||
// When target is destroyed, targetId is automatically set to null
|
||||
```
|
||||
|
||||
See [Component References](/en/guide/component/entity-ref/) for details.
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Destroy Unneeded Entities Promptly
|
||||
|
||||
```typescript
|
||||
// Destroy bullets that fly off screen
|
||||
if (position.x < 0 || position.x > screenWidth) {
|
||||
bullet.destroy();
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Use Object Pools Instead of Frequent Create/Destroy
|
||||
|
||||
```typescript
|
||||
class BulletPool {
|
||||
private pool: Entity[] = [];
|
||||
|
||||
acquire(scene: Scene): Entity {
|
||||
if (this.pool.length > 0) {
|
||||
const bullet = this.pool.pop()!;
|
||||
bullet.enabled = true;
|
||||
return bullet;
|
||||
}
|
||||
return scene.createEntity('Bullet');
|
||||
}
|
||||
|
||||
release(bullet: Entity) {
|
||||
bullet.enabled = false;
|
||||
this.pool.push(bullet);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Use Persistence Sparingly
|
||||
|
||||
Only use persistence for entities that truly need to survive scene transitions—too many persistent entities increase memory usage.
|
||||
|
||||
### 4. Clean Up References Before Destruction
|
||||
|
||||
```typescript
|
||||
// Notify related systems before destruction
|
||||
const aiSystem = scene.getSystem(AISystem);
|
||||
aiSystem?.clearTarget(enemy.id);
|
||||
|
||||
enemy.destroy();
|
||||
```
|
||||
|
||||
## Lifecycle Events
|
||||
|
||||
You can listen to entity destruction events:
|
||||
|
||||
```typescript
|
||||
// Method 1: Through event system
|
||||
scene.eventSystem.on('entity:destroyed', (data) => {
|
||||
console.log(`Entity ${data.entityName} destroyed`);
|
||||
});
|
||||
|
||||
// Method 2: In component
|
||||
class MyComponent extends Component {
|
||||
onRemovedFromEntity() {
|
||||
console.log('Component removed, entity may be destroying');
|
||||
// Clean up resources
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
```typescript
|
||||
// Get entity status
|
||||
const debugInfo = entity.getDebugInfo();
|
||||
console.log({
|
||||
destroyed: debugInfo.destroyed,
|
||||
enabled: debugInfo.enabled,
|
||||
active: debugInfo.active
|
||||
});
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Component Operations](/en/guide/entity/component-operations/) - Adding and removing components
|
||||
- [Scene Management](/en/guide/scene/) - Scene switching and management
|
||||
260
docs/src/content/docs/en/guide/event-system.md
Normal file
260
docs/src/content/docs/en/guide/event-system.md
Normal file
@@ -0,0 +1,260 @@
|
||||
---
|
||||
title: "Event System"
|
||||
description: "Type-safe event system with sync/async events, priorities, and batching"
|
||||
---
|
||||
|
||||
The ECS framework includes a powerful type-safe event system supporting sync/async events, priorities, batching, and more advanced features. The event system is the core mechanism for inter-component and inter-system communication.
|
||||
|
||||
## Basic Concepts
|
||||
|
||||
The event system implements a publish-subscribe pattern with these core concepts:
|
||||
- **Event Publisher**: Object that emits events
|
||||
- **Event Listener**: Object that listens for and handles specific events
|
||||
- **Event Type**: String identifier to distinguish different event types
|
||||
- **Event Data**: Related information carried by the event
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Using Event System in Scene
|
||||
|
||||
Each scene has a built-in event system:
|
||||
|
||||
```typescript
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Listen for events
|
||||
this.eventSystem.on('player_died', this.onPlayerDied.bind(this));
|
||||
this.eventSystem.on('enemy_spawned', this.onEnemySpawned.bind(this));
|
||||
this.eventSystem.on('score_changed', this.onScoreChanged.bind(this));
|
||||
}
|
||||
|
||||
private onPlayerDied(data: { player: Entity, cause: string }): void {
|
||||
console.log(`Player died, cause: ${data.cause}`);
|
||||
}
|
||||
|
||||
private onEnemySpawned(data: { enemy: Entity, position: { x: number, y: number } }): void {
|
||||
console.log('Enemy spawned at:', data.position);
|
||||
}
|
||||
|
||||
private onScoreChanged(data: { newScore: number, oldScore: number }): void {
|
||||
console.log(`Score changed: ${data.oldScore} -> ${data.newScore}`);
|
||||
}
|
||||
|
||||
// Emit events in systems
|
||||
someGameLogic(): void {
|
||||
this.eventSystem.emitSync('score_changed', {
|
||||
newScore: 1000,
|
||||
oldScore: 800
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using Events in Systems
|
||||
|
||||
Systems can conveniently listen for and send events:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Combat')
|
||||
class CombatSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Health, Combat));
|
||||
}
|
||||
|
||||
protected onInitialize(): void {
|
||||
// Use system's event listener method (auto-cleanup)
|
||||
this.addEventListener('player_attack', this.onPlayerAttack.bind(this));
|
||||
this.addEventListener('enemy_death', this.onEnemyDeath.bind(this));
|
||||
}
|
||||
|
||||
private onPlayerAttack(data: { damage: number, target: Entity }): void {
|
||||
const health = data.target.getComponent(Health);
|
||||
if (health) {
|
||||
health.current -= data.damage;
|
||||
|
||||
if (health.current <= 0) {
|
||||
this.scene?.eventSystem.emitSync('enemy_death', {
|
||||
enemy: data.target,
|
||||
killer: 'player'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private onEnemyDeath(data: { enemy: Entity, killer: string }): void {
|
||||
data.enemy.destroy();
|
||||
this.scene?.eventSystem.emitSync('experience_gained', {
|
||||
amount: 100,
|
||||
source: 'enemy_kill'
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### One-Time Listeners
|
||||
|
||||
```typescript
|
||||
// Listen only once
|
||||
this.eventSystem.once('game_start', this.onGameStart.bind(this));
|
||||
|
||||
// Or use configuration object
|
||||
this.eventSystem.on('level_complete', this.onLevelComplete.bind(this), {
|
||||
once: true
|
||||
});
|
||||
```
|
||||
|
||||
### Priority Control
|
||||
|
||||
```typescript
|
||||
// Higher priority listeners execute first
|
||||
this.eventSystem.on('damage_dealt', this.onDamageDealt.bind(this), {
|
||||
priority: 100 // High priority
|
||||
});
|
||||
|
||||
this.eventSystem.on('damage_dealt', this.updateUI.bind(this), {
|
||||
priority: 0 // Default priority
|
||||
});
|
||||
|
||||
this.eventSystem.on('damage_dealt', this.logDamage.bind(this), {
|
||||
priority: -100 // Low priority, executes last
|
||||
});
|
||||
```
|
||||
|
||||
### Async Event Handling
|
||||
|
||||
```typescript
|
||||
protected initialize(): void {
|
||||
this.eventSystem.onAsync('save_game', this.onSaveGame.bind(this));
|
||||
}
|
||||
|
||||
private async onSaveGame(data: { saveSlot: number }): Promise<void> {
|
||||
console.log(`Saving game to slot ${data.saveSlot}`);
|
||||
await this.saveGameData(data.saveSlot);
|
||||
console.log('Game saved');
|
||||
}
|
||||
|
||||
// Emit async event
|
||||
public async triggerSave(): Promise<void> {
|
||||
await this.eventSystem.emit('save_game', { saveSlot: 1 });
|
||||
console.log('All async save operations complete');
|
||||
}
|
||||
```
|
||||
|
||||
### Batch Processing
|
||||
|
||||
For high-frequency events, use batching to improve performance:
|
||||
|
||||
```typescript
|
||||
protected onInitialize(): void {
|
||||
// Configure batch processing for position updates
|
||||
this.scene?.eventSystem.setBatchConfig('position_updated', {
|
||||
batchSize: 50,
|
||||
delay: 16,
|
||||
enabled: true
|
||||
});
|
||||
|
||||
// Listen for batch events
|
||||
this.addEventListener('position_updated:batch', this.onPositionBatch.bind(this));
|
||||
}
|
||||
|
||||
private onPositionBatch(batchData: any): void {
|
||||
console.log(`Batch processing ${batchData.count} position updates`);
|
||||
for (const event of batchData.events) {
|
||||
this.updateMinimap(event.entityId, event.position);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Global Event Bus
|
||||
|
||||
For cross-scene event communication:
|
||||
|
||||
```typescript
|
||||
import { GlobalEventBus } from '@esengine/ecs-framework';
|
||||
|
||||
class GameManager {
|
||||
private eventBus = GlobalEventBus.getInstance();
|
||||
|
||||
constructor() {
|
||||
this.eventBus.on('player_level_up', this.onPlayerLevelUp.bind(this));
|
||||
this.eventBus.on('achievement_unlocked', this.onAchievementUnlocked.bind(this));
|
||||
}
|
||||
|
||||
private onPlayerLevelUp(data: { level: number }): void {
|
||||
console.log(`Player leveled up to ${data.level}!`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Event Naming Convention
|
||||
|
||||
```typescript
|
||||
// ✅ Good naming
|
||||
this.eventSystem.emitSync('player:health_changed', data);
|
||||
this.eventSystem.emitSync('enemy:spawned', data);
|
||||
this.eventSystem.emitSync('ui:score_updated', data);
|
||||
|
||||
// ❌ Avoid
|
||||
this.eventSystem.emitSync('event1', data);
|
||||
this.eventSystem.emitSync('update', data);
|
||||
```
|
||||
|
||||
### 2. Type-Safe Event Data
|
||||
|
||||
```typescript
|
||||
interface PlayerHealthChangedEvent {
|
||||
entityId: number;
|
||||
oldHealth: number;
|
||||
newHealth: number;
|
||||
cause: 'damage' | 'healing';
|
||||
}
|
||||
|
||||
class HealthSystem extends EntitySystem {
|
||||
private onHealthChanged(data: PlayerHealthChangedEvent): void {
|
||||
// TypeScript provides full type checking
|
||||
console.log(`Health changed: ${data.oldHealth} -> ${data.newHealth}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Avoid Event Loops
|
||||
|
||||
```typescript
|
||||
// ❌ Avoid: May cause infinite loop
|
||||
private onScoreChanged(data: any): void {
|
||||
this.scene?.eventSystem.emitSync('score_changed', newData); // Dangerous!
|
||||
}
|
||||
|
||||
// ✅ Correct: Use guard flag
|
||||
private isProcessingScore = false;
|
||||
|
||||
private onScoreChanged(data: any): void {
|
||||
if (this.isProcessingScore) return;
|
||||
|
||||
this.isProcessingScore = true;
|
||||
this.updateUI(data);
|
||||
this.isProcessingScore = false;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Clean Up Event Listeners
|
||||
|
||||
```typescript
|
||||
class TemporaryUI {
|
||||
private listenerId: string;
|
||||
|
||||
constructor(scene: Scene) {
|
||||
this.listenerId = scene.eventSystem.on('ui_update', this.onUpdate.bind(this));
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
if (this.listenerId) {
|
||||
scene.eventSystem.off('ui_update', this.listenerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,4 +1,6 @@
|
||||
# Quick Start
|
||||
---
|
||||
title: "Quick Start"
|
||||
---
|
||||
|
||||
This guide will help you get started with ECS Framework, from installation to creating your first ECS application.
|
||||
|
||||
202
docs/src/content/docs/en/guide/hierarchy.md
Normal file
202
docs/src/content/docs/en/guide/hierarchy.md
Normal file
@@ -0,0 +1,202 @@
|
||||
---
|
||||
title: "Hierarchy System"
|
||||
description: "Parent-child entity relationships using component-based design"
|
||||
---
|
||||
|
||||
In game development, parent-child hierarchy relationships between entities are common requirements. ECS Framework manages hierarchy relationships through a component-based approach using `HierarchyComponent` and `HierarchySystem`, fully adhering to ECS composition principles.
|
||||
|
||||
## Design Philosophy
|
||||
|
||||
### Why Not Built-in Hierarchy in Entity?
|
||||
|
||||
Traditional game object models build hierarchy into entities. ECS Framework chose a component-based approach because:
|
||||
|
||||
1. **ECS Composition Principle**: Hierarchy is a "feature" that should be added through components, not inherent to all entities
|
||||
2. **On-Demand Usage**: Only entities that need hierarchy add `HierarchyComponent`
|
||||
3. **Data-Logic Separation**: `HierarchyComponent` stores data, `HierarchySystem` handles logic
|
||||
4. **Serialization-Friendly**: Hierarchy as component data can be easily serialized/deserialized
|
||||
|
||||
## Basic Concepts
|
||||
|
||||
### HierarchyComponent
|
||||
|
||||
Component storing hierarchy relationship data:
|
||||
|
||||
```typescript
|
||||
import { HierarchyComponent } from '@esengine/ecs-framework';
|
||||
|
||||
interface HierarchyComponent {
|
||||
parentId: number | null; // Parent entity ID, null means root
|
||||
childIds: number[]; // Child entity ID list
|
||||
depth: number; // Depth in hierarchy (maintained by system)
|
||||
bActiveInHierarchy: boolean; // Active in hierarchy (maintained by system)
|
||||
}
|
||||
```
|
||||
|
||||
### HierarchySystem
|
||||
|
||||
System handling hierarchy logic, provides all hierarchy operation APIs:
|
||||
|
||||
```typescript
|
||||
import { HierarchySystem } from '@esengine/ecs-framework';
|
||||
|
||||
const hierarchySystem = scene.getEntityProcessor(HierarchySystem);
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Add System to Scene
|
||||
|
||||
```typescript
|
||||
import { Scene, HierarchySystem } from '@esengine/ecs-framework';
|
||||
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.addSystem(new HierarchySystem());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Establish Parent-Child Relationships
|
||||
|
||||
```typescript
|
||||
const parent = scene.createEntity("Parent");
|
||||
const child1 = scene.createEntity("Child1");
|
||||
const child2 = scene.createEntity("Child2");
|
||||
|
||||
const hierarchySystem = scene.getEntityProcessor(HierarchySystem);
|
||||
|
||||
// Set parent-child relationship (auto-adds HierarchyComponent)
|
||||
hierarchySystem.setParent(child1, parent);
|
||||
hierarchySystem.setParent(child2, parent);
|
||||
```
|
||||
|
||||
### Query Hierarchy
|
||||
|
||||
```typescript
|
||||
// Get parent entity
|
||||
const parentEntity = hierarchySystem.getParent(child1);
|
||||
|
||||
// Get all children
|
||||
const children = hierarchySystem.getChildren(parent);
|
||||
|
||||
// Get child count
|
||||
const count = hierarchySystem.getChildCount(parent);
|
||||
|
||||
// Check if has children
|
||||
const hasKids = hierarchySystem.hasChildren(parent);
|
||||
|
||||
// Get depth in hierarchy
|
||||
const depth = hierarchySystem.getDepth(child1); // Returns 1
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Parent-Child Operations
|
||||
|
||||
```typescript
|
||||
// Set parent
|
||||
hierarchySystem.setParent(child, parent);
|
||||
|
||||
// Move to root (no parent)
|
||||
hierarchySystem.setParent(child, null);
|
||||
|
||||
// Insert child at position
|
||||
hierarchySystem.insertChildAt(parent, child, 0);
|
||||
|
||||
// Remove child (becomes root)
|
||||
hierarchySystem.removeChild(parent, child);
|
||||
|
||||
// Remove all children
|
||||
hierarchySystem.removeAllChildren(parent);
|
||||
```
|
||||
|
||||
### Hierarchy Queries
|
||||
|
||||
```typescript
|
||||
// Get root of entity
|
||||
const root = hierarchySystem.getRoot(deepChild);
|
||||
|
||||
// Get all root entities
|
||||
const roots = hierarchySystem.getRootEntities();
|
||||
|
||||
// Check ancestor/descendant relationships
|
||||
const isAncestor = hierarchySystem.isAncestorOf(grandparent, child);
|
||||
const isDescendant = hierarchySystem.isDescendantOf(child, grandparent);
|
||||
```
|
||||
|
||||
### Hierarchy Traversal
|
||||
|
||||
```typescript
|
||||
// Find child by name
|
||||
const child = hierarchySystem.findChild(parent, "ChildName");
|
||||
|
||||
// Recursive search
|
||||
const deepChild = hierarchySystem.findChild(parent, "DeepChild", true);
|
||||
|
||||
// Find children by tag
|
||||
const tagged = hierarchySystem.findChildrenByTag(parent, TAG_ENEMY, true);
|
||||
|
||||
// Iterate children
|
||||
hierarchySystem.forEachChild(parent, (child) => {
|
||||
console.log(child.name);
|
||||
}, true); // true for recursive
|
||||
```
|
||||
|
||||
### Hierarchy State
|
||||
|
||||
```typescript
|
||||
// Check if active in hierarchy (considers all ancestors)
|
||||
const activeInHierarchy = hierarchySystem.isActiveInHierarchy(child);
|
||||
|
||||
// Get depth (root = 0)
|
||||
const depth = hierarchySystem.getDepth(entity);
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
```typescript
|
||||
class GameScene extends Scene {
|
||||
private hierarchySystem!: HierarchySystem;
|
||||
|
||||
protected initialize(): void {
|
||||
this.hierarchySystem = new HierarchySystem();
|
||||
this.addSystem(this.hierarchySystem);
|
||||
this.createPlayerHierarchy();
|
||||
}
|
||||
|
||||
private createPlayerHierarchy(): void {
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Transform(0, 0));
|
||||
|
||||
const body = this.createEntity("Body");
|
||||
body.addComponent(new Sprite("body.png"));
|
||||
this.hierarchySystem.setParent(body, player);
|
||||
|
||||
const weapon = this.createEntity("Weapon");
|
||||
weapon.addComponent(new Sprite("sword.png"));
|
||||
this.hierarchySystem.setParent(weapon, body);
|
||||
|
||||
console.log(`Player depth: ${this.hierarchySystem.getDepth(player)}`); // 0
|
||||
console.log(`Weapon depth: ${this.hierarchySystem.getDepth(weapon)}`); // 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Avoid Deep Nesting**: System limits max depth to 32 levels
|
||||
2. **Batch Operations**: Set up all parent-child relationships at once when building complex hierarchies
|
||||
3. **On-Demand Addition**: Only add `HierarchyComponent` to entities that truly need hierarchy
|
||||
4. **Cache System Reference**: Avoid getting `HierarchySystem` on every call
|
||||
|
||||
```typescript
|
||||
// Good practice
|
||||
class MySystem extends EntitySystem {
|
||||
private hierarchySystem!: HierarchySystem;
|
||||
|
||||
onAddedToScene() {
|
||||
this.hierarchySystem = this.scene!.getEntityProcessor(HierarchySystem)!;
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,4 +1,6 @@
|
||||
# Guide
|
||||
---
|
||||
title: "Guide"
|
||||
---
|
||||
|
||||
Welcome to the ECS Framework Guide. This guide covers the core concepts and usage of the framework.
|
||||
|
||||
225
docs/src/content/docs/en/guide/logging.md
Normal file
225
docs/src/content/docs/en/guide/logging.md
Normal file
@@ -0,0 +1,225 @@
|
||||
---
|
||||
title: "Logging System"
|
||||
description: "Multi-level logging with colors, prefixes, and flexible configuration"
|
||||
---
|
||||
|
||||
The ECS framework provides a powerful hierarchical logging system supporting multiple log levels, color output, custom prefixes, and flexible configuration options.
|
||||
|
||||
## Basic Concepts
|
||||
|
||||
- **Log Levels**: Debug < Info < Warn < Error < Fatal < None
|
||||
- **Logger**: Named log outputter, each module can have its own logger
|
||||
- **Logger Manager**: Singleton managing all loggers globally
|
||||
- **Color Config**: Supports console color output
|
||||
|
||||
## Log Levels
|
||||
|
||||
```typescript
|
||||
import { LogLevel } from '@esengine/ecs-framework';
|
||||
|
||||
LogLevel.Debug // 0 - Debug information
|
||||
LogLevel.Info // 1 - General information
|
||||
LogLevel.Warn // 2 - Warning information
|
||||
LogLevel.Error // 3 - Error information
|
||||
LogLevel.Fatal // 4 - Fatal errors
|
||||
LogLevel.None // 5 - No output
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Using Default Logger
|
||||
|
||||
```typescript
|
||||
import { Logger } from '@esengine/ecs-framework';
|
||||
|
||||
class GameSystem extends EntitySystem {
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
Logger.debug('Processing entities:', entities.length);
|
||||
Logger.info('System running normally');
|
||||
Logger.warn('Performance issue detected');
|
||||
Logger.error('Error during processing', new Error('Example'));
|
||||
Logger.fatal('Fatal error, system stopping');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Creating Named Logger
|
||||
|
||||
```typescript
|
||||
import { createLogger } from '@esengine/ecs-framework';
|
||||
|
||||
class MovementSystem extends EntitySystem {
|
||||
private logger = createLogger('MovementSystem');
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
this.logger.info(`Processing ${entities.length} moving entities`);
|
||||
|
||||
for (const entity of entities) {
|
||||
const position = entity.getComponent(Position);
|
||||
this.logger.debug(`Entity ${entity.id} moved to (${position.x}, ${position.y})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Log Configuration
|
||||
|
||||
### Set Global Log Level
|
||||
|
||||
```typescript
|
||||
import { setGlobalLogLevel, LogLevel } from '@esengine/ecs-framework';
|
||||
|
||||
// Development: show all logs
|
||||
setGlobalLogLevel(LogLevel.Debug);
|
||||
|
||||
// Production: show warnings and above
|
||||
setGlobalLogLevel(LogLevel.Warn);
|
||||
|
||||
// Disable all logs
|
||||
setGlobalLogLevel(LogLevel.None);
|
||||
```
|
||||
|
||||
### Custom Logger Configuration
|
||||
|
||||
```typescript
|
||||
import { ConsoleLogger, LogLevel } from '@esengine/ecs-framework';
|
||||
|
||||
// Development logger
|
||||
const debugLogger = new ConsoleLogger({
|
||||
level: LogLevel.Debug,
|
||||
enableTimestamp: true,
|
||||
enableColors: true,
|
||||
prefix: 'DEV'
|
||||
});
|
||||
|
||||
// Production logger
|
||||
const productionLogger = new ConsoleLogger({
|
||||
level: LogLevel.Error,
|
||||
enableTimestamp: true,
|
||||
enableColors: false,
|
||||
prefix: 'PROD'
|
||||
});
|
||||
```
|
||||
|
||||
## Color Configuration
|
||||
|
||||
```typescript
|
||||
import { Colors, setLoggerColors } from '@esengine/ecs-framework';
|
||||
|
||||
setLoggerColors({
|
||||
debug: Colors.BRIGHT_BLACK,
|
||||
info: Colors.BLUE,
|
||||
warn: Colors.YELLOW,
|
||||
error: Colors.RED,
|
||||
fatal: Colors.BRIGHT_RED
|
||||
});
|
||||
```
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Hierarchical Loggers
|
||||
|
||||
```typescript
|
||||
import { LoggerManager } from '@esengine/ecs-framework';
|
||||
|
||||
const manager = LoggerManager.getInstance();
|
||||
|
||||
// Create child loggers
|
||||
const movementLogger = manager.createChildLogger('GameSystems', 'Movement');
|
||||
const renderLogger = manager.createChildLogger('GameSystems', 'Render');
|
||||
|
||||
// Child logger shows full path: [GameSystems.Movement]
|
||||
movementLogger.debug('Movement system initialized');
|
||||
```
|
||||
|
||||
### Third-Party Logger Integration
|
||||
|
||||
```typescript
|
||||
import { setLoggerFactory } from '@esengine/ecs-framework';
|
||||
|
||||
// Integrate with Winston
|
||||
setLoggerFactory((name?: string) => winston.createLogger({ /* ... */ }));
|
||||
|
||||
// Integrate with Pino
|
||||
setLoggerFactory((name?: string) => pino({ name }));
|
||||
|
||||
// Integrate with NestJS Logger
|
||||
setLoggerFactory((name?: string) => new Logger(name));
|
||||
```
|
||||
|
||||
### Custom Output
|
||||
|
||||
```typescript
|
||||
const fileLogger = new ConsoleLogger({
|
||||
level: LogLevel.Info,
|
||||
output: (level: LogLevel, message: string) => {
|
||||
this.writeToFile(LogLevel[level], message);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Choose Appropriate Log Levels
|
||||
|
||||
```typescript
|
||||
// Debug - Detailed debug info
|
||||
this.logger.debug('Variable values', { x: 10, y: 20 });
|
||||
|
||||
// Info - Important state changes
|
||||
this.logger.info('System startup complete');
|
||||
|
||||
// Warn - Abnormal but non-fatal
|
||||
this.logger.warn('Resource not found, using default');
|
||||
|
||||
// Error - Errors but program can continue
|
||||
this.logger.error('Save failed, will retry', new Error('Network timeout'));
|
||||
|
||||
// Fatal - Fatal errors, program cannot continue
|
||||
this.logger.fatal('Out of memory, exiting');
|
||||
```
|
||||
|
||||
### 2. Structured Log Data
|
||||
|
||||
```typescript
|
||||
this.logger.info('User action', {
|
||||
userId: 12345,
|
||||
action: 'move',
|
||||
position: { x: 100, y: 200 },
|
||||
timestamp: Date.now()
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Avoid Performance Issues
|
||||
|
||||
```typescript
|
||||
// ✅ Check log level before expensive computation
|
||||
if (this.logger.debug) {
|
||||
const expensiveData = this.calculateExpensiveDebugInfo();
|
||||
this.logger.debug('Debug info', expensiveData);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Environment-Based Configuration
|
||||
|
||||
```typescript
|
||||
class LoggingConfiguration {
|
||||
public static setupLogging(): void {
|
||||
const isDevelopment = process.env.NODE_ENV === 'development';
|
||||
|
||||
if (isDevelopment) {
|
||||
setGlobalLogLevel(LogLevel.Debug);
|
||||
setLoggerColors({
|
||||
debug: Colors.CYAN,
|
||||
info: Colors.GREEN,
|
||||
warn: Colors.YELLOW,
|
||||
error: Colors.RED,
|
||||
fatal: Colors.BRIGHT_RED
|
||||
});
|
||||
} else {
|
||||
setGlobalLogLevel(LogLevel.Warn);
|
||||
LoggerManager.getInstance().resetColors();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
title: "persistent-entity"
|
||||
---
|
||||
|
||||
# Persistent Entity
|
||||
|
||||
> **Version**: v2.3.0+
|
||||
@@ -355,6 +359,5 @@ class GoodScene extends Scene {
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Scene](./scene) - Learn the basics of scenes
|
||||
- [SceneManager](./scene-manager) - Learn about scene transitions
|
||||
- [WorldManager](./world-manager) - Learn about multi-world management
|
||||
- [Scene](/en/guide/scene/) - Learn the basics of scenes
|
||||
- [SceneManager](/en/guide/scene-manager/) - Learn about scene transitions
|
||||
291
docs/src/content/docs/en/guide/platform-adapter.md
Normal file
291
docs/src/content/docs/en/guide/platform-adapter.md
Normal file
@@ -0,0 +1,291 @@
|
||||
---
|
||||
title: "Platform Adapter"
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The ECS framework provides a platform adapter interface that allows users to implement custom platform adapters for different runtime environments.
|
||||
|
||||
**The core library only provides interface definitions. Platform adapter implementations should be copied from the documentation.**
|
||||
|
||||
## Why No Separate Adapter Packages?
|
||||
|
||||
1. **Flexibility**: Different projects may have different platform adaptation needs. Copying code allows users to freely modify as needed
|
||||
2. **Reduce Dependencies**: Avoid introducing unnecessary dependency packages, keeping the core framework lean
|
||||
3. **Customization**: Users can customize according to specific runtime environments and requirements
|
||||
|
||||
## Supported Platforms
|
||||
|
||||
### [Browser Adapter](/en/guide/platform-adapter/browser/)
|
||||
|
||||
Supports all modern browser environments, including Chrome, Firefox, Safari, Edge, etc.
|
||||
|
||||
**Feature Support**:
|
||||
- Worker (Web Worker)
|
||||
- SharedArrayBuffer (requires COOP/COEP)
|
||||
- Transferable Objects
|
||||
- Module Worker (modern browsers)
|
||||
|
||||
**Use Cases**: Web games, Web applications, PWA
|
||||
|
||||
---
|
||||
|
||||
### [WeChat Mini Game Adapter](/en/guide/platform-adapter/wechat-minigame/)
|
||||
|
||||
Designed specifically for the WeChat Mini Game environment, handling special restrictions and APIs.
|
||||
|
||||
**Feature Support**:
|
||||
- Worker (max 1, requires game.json configuration)
|
||||
- SharedArrayBuffer (not supported)
|
||||
- Transferable Objects (not supported)
|
||||
- WeChat Device Info API
|
||||
|
||||
**Use Cases**: WeChat Mini Game development
|
||||
|
||||
---
|
||||
|
||||
### [Node.js Adapter](/en/guide/platform-adapter/nodejs/)
|
||||
|
||||
Provides support for Node.js server environments, suitable for game servers and compute servers.
|
||||
|
||||
**Feature Support**:
|
||||
- Worker Threads
|
||||
- SharedArrayBuffer
|
||||
- Transferable Objects
|
||||
- Complete system information
|
||||
|
||||
**Use Cases**: Game servers, compute servers, CLI tools
|
||||
|
||||
---
|
||||
|
||||
## Core Interfaces
|
||||
|
||||
### IPlatformAdapter
|
||||
|
||||
```typescript
|
||||
export interface IPlatformAdapter {
|
||||
readonly name: string;
|
||||
readonly version?: string;
|
||||
|
||||
isWorkerSupported(): boolean;
|
||||
isSharedArrayBufferSupported(): boolean;
|
||||
getHardwareConcurrency(): number;
|
||||
createWorker(script: string, options?: WorkerCreationOptions): PlatformWorker;
|
||||
createSharedArrayBuffer(length: number): SharedArrayBuffer | null;
|
||||
getHighResTimestamp(): number;
|
||||
getPlatformConfig(): PlatformConfig;
|
||||
getPlatformConfigAsync?(): Promise<PlatformConfig>;
|
||||
}
|
||||
```
|
||||
|
||||
### PlatformWorker Interface
|
||||
|
||||
```typescript
|
||||
export interface PlatformWorker {
|
||||
postMessage(message: any, transfer?: Transferable[]): void;
|
||||
onMessage(handler: (event: { data: any }) => void): void;
|
||||
onError(handler: (error: ErrorEvent) => void): void;
|
||||
terminate(): void;
|
||||
readonly state: 'running' | 'terminated';
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### 1. Choose the Appropriate Platform Adapter
|
||||
|
||||
Select the corresponding adapter based on your runtime environment:
|
||||
|
||||
```typescript
|
||||
import { PlatformManager } from '@esengine/ecs-framework';
|
||||
|
||||
// Browser environment
|
||||
if (typeof window !== 'undefined') {
|
||||
const { BrowserAdapter } = await import('./platform/BrowserAdapter');
|
||||
PlatformManager.getInstance().registerAdapter(new BrowserAdapter());
|
||||
}
|
||||
|
||||
// WeChat Mini Game environment
|
||||
else if (typeof wx !== 'undefined') {
|
||||
const { WeChatMiniGameAdapter } = await import('./platform/WeChatMiniGameAdapter');
|
||||
PlatformManager.getInstance().registerAdapter(new WeChatMiniGameAdapter());
|
||||
}
|
||||
|
||||
// Node.js environment
|
||||
else if (typeof process !== 'undefined' && process.versions?.node) {
|
||||
const { NodeAdapter } = await import('./platform/NodeAdapter');
|
||||
PlatformManager.getInstance().registerAdapter(new NodeAdapter());
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Check Adapter Status
|
||||
|
||||
```typescript
|
||||
const manager = PlatformManager.getInstance();
|
||||
|
||||
// Check if adapter is registered
|
||||
if (manager.hasAdapter()) {
|
||||
const adapter = manager.getAdapter();
|
||||
console.log('Current platform:', adapter.name);
|
||||
console.log('Platform version:', adapter.version);
|
||||
|
||||
// Check feature support
|
||||
console.log('Worker support:', manager.supportsFeature('worker'));
|
||||
console.log('SharedArrayBuffer support:', manager.supportsFeature('shared-array-buffer'));
|
||||
}
|
||||
```
|
||||
|
||||
## Creating Custom Adapters
|
||||
|
||||
If existing platform adapters don't meet your needs, you can create custom adapters:
|
||||
|
||||
### 1. Implement the Interface
|
||||
|
||||
```typescript
|
||||
import type { IPlatformAdapter, PlatformWorker, WorkerCreationOptions, PlatformConfig } from '@esengine/ecs-framework';
|
||||
|
||||
export class CustomAdapter implements IPlatformAdapter {
|
||||
public readonly name = 'custom';
|
||||
public readonly version = '1.0.0';
|
||||
|
||||
public isWorkerSupported(): boolean {
|
||||
// Implement your Worker support check logic
|
||||
return false;
|
||||
}
|
||||
|
||||
public isSharedArrayBufferSupported(): boolean {
|
||||
// Implement your SharedArrayBuffer support check logic
|
||||
return false;
|
||||
}
|
||||
|
||||
public getHardwareConcurrency(): number {
|
||||
// Return your platform's concurrency count
|
||||
return 1;
|
||||
}
|
||||
|
||||
public createWorker(script: string, options?: WorkerCreationOptions): PlatformWorker {
|
||||
throw new Error('Worker not supported on this platform');
|
||||
}
|
||||
|
||||
public createSharedArrayBuffer(length: number): SharedArrayBuffer | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
public getHighResTimestamp(): number {
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
public getPlatformConfig(): PlatformConfig {
|
||||
return {
|
||||
maxWorkerCount: 1,
|
||||
supportsModuleWorker: false,
|
||||
supportsTransferableObjects: false,
|
||||
limitations: {
|
||||
workerNotSupported: true
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Register Custom Adapter
|
||||
|
||||
```typescript
|
||||
import { PlatformManager } from '@esengine/ecs-framework';
|
||||
import { CustomAdapter } from './CustomAdapter';
|
||||
|
||||
const customAdapter = new CustomAdapter();
|
||||
PlatformManager.getInstance().registerAdapter(customAdapter);
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Platform Detection Order
|
||||
|
||||
Recommend detecting and registering platform adapters in this order:
|
||||
|
||||
```typescript
|
||||
async function initializePlatform() {
|
||||
const manager = PlatformManager.getInstance();
|
||||
|
||||
try {
|
||||
// 1. WeChat Mini Game (highest priority, most distinctive environment)
|
||||
if (typeof wx !== 'undefined' && wx.getSystemInfoSync) {
|
||||
const { WeChatMiniGameAdapter } = await import('./platform/WeChatMiniGameAdapter');
|
||||
manager.registerAdapter(new WeChatMiniGameAdapter());
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Node.js environment
|
||||
if (typeof process !== 'undefined' && process.versions?.node) {
|
||||
const { NodeAdapter } = await import('./platform/NodeAdapter');
|
||||
manager.registerAdapter(new NodeAdapter());
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Browser environment (last check, broadest coverage)
|
||||
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
|
||||
const { BrowserAdapter } = await import('./platform/BrowserAdapter');
|
||||
manager.registerAdapter(new BrowserAdapter());
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. Unknown environment, use default adapter
|
||||
console.warn('Unrecognized platform environment, using default adapter');
|
||||
manager.registerAdapter(new CustomAdapter());
|
||||
|
||||
} catch (error) {
|
||||
console.error('Platform adapter initialization failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Feature Degradation Handling
|
||||
|
||||
```typescript
|
||||
function createWorkerSystem() {
|
||||
const manager = PlatformManager.getInstance();
|
||||
|
||||
if (!manager.hasAdapter()) {
|
||||
throw new Error('No platform adapter registered');
|
||||
}
|
||||
|
||||
const config: WorkerSystemConfig = {
|
||||
enableWorker: manager.supportsFeature('worker'),
|
||||
workerCount: manager.supportsFeature('worker') ?
|
||||
manager.getAdapter().getHardwareConcurrency() : 1,
|
||||
useSharedArrayBuffer: manager.supportsFeature('shared-array-buffer')
|
||||
};
|
||||
|
||||
// If Worker not supported, automatically degrade to synchronous processing
|
||||
if (!config.enableWorker) {
|
||||
console.info('Current platform does not support Worker, using synchronous processing mode');
|
||||
}
|
||||
|
||||
return new PhysicsSystem(config);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Error Handling
|
||||
|
||||
```typescript
|
||||
try {
|
||||
await initializePlatform();
|
||||
|
||||
// Validate adapter functionality
|
||||
const manager = PlatformManager.getInstance();
|
||||
const adapter = manager.getAdapter();
|
||||
|
||||
console.log(`Platform adapter initialized: ${adapter.name} v${adapter.version}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Platform initialization failed:', error);
|
||||
|
||||
// Provide fallback solution
|
||||
const fallbackAdapter = new CustomAdapter();
|
||||
PlatformManager.getInstance().registerAdapter(fallbackAdapter);
|
||||
|
||||
console.warn('Using fallback adapter to continue running');
|
||||
}
|
||||
```
|
||||
372
docs/src/content/docs/en/guide/platform-adapter/browser.md
Normal file
372
docs/src/content/docs/en/guide/platform-adapter/browser.md
Normal file
@@ -0,0 +1,372 @@
|
||||
---
|
||||
title: "Browser Adapter"
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The browser platform adapter provides support for standard web browser environments, including modern browsers like Chrome, Firefox, Safari, Edge, etc.
|
||||
|
||||
## Feature Support
|
||||
|
||||
- **Worker**: Supports Web Worker and Module Worker
|
||||
- **SharedArrayBuffer**: Supported (requires COOP/COEP headers)
|
||||
- **Transferable Objects**: Fully supported
|
||||
- **High-Resolution Time**: Uses `performance.now()`
|
||||
- **Basic Info**: Browser version and basic configuration
|
||||
|
||||
## Complete Implementation
|
||||
|
||||
```typescript
|
||||
import type {
|
||||
IPlatformAdapter,
|
||||
PlatformWorker,
|
||||
WorkerCreationOptions,
|
||||
PlatformConfig
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* Browser platform adapter
|
||||
* Supports standard web browser environments
|
||||
*/
|
||||
export class BrowserAdapter implements IPlatformAdapter {
|
||||
public readonly name = 'browser';
|
||||
public readonly version: string;
|
||||
|
||||
constructor() {
|
||||
this.version = this.getBrowserInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Worker is supported
|
||||
*/
|
||||
public isWorkerSupported(): boolean {
|
||||
return typeof Worker !== 'undefined';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if SharedArrayBuffer is supported
|
||||
*/
|
||||
public isSharedArrayBufferSupported(): boolean {
|
||||
return typeof SharedArrayBuffer !== 'undefined' && this.checkSharedArrayBufferEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hardware concurrency (CPU core count)
|
||||
*/
|
||||
public getHardwareConcurrency(): number {
|
||||
return navigator.hardwareConcurrency || 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Worker
|
||||
*/
|
||||
public createWorker(script: string, options: WorkerCreationOptions = {}): PlatformWorker {
|
||||
if (!this.isWorkerSupported()) {
|
||||
throw new Error('Browser does not support Worker');
|
||||
}
|
||||
|
||||
try {
|
||||
return new BrowserWorker(script, options);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to create browser Worker: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create SharedArrayBuffer
|
||||
*/
|
||||
public createSharedArrayBuffer(length: number): SharedArrayBuffer | null {
|
||||
if (!this.isSharedArrayBufferSupported()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return new SharedArrayBuffer(length);
|
||||
} catch (error) {
|
||||
console.warn('SharedArrayBuffer creation failed:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get high-resolution timestamp
|
||||
*/
|
||||
public getHighResTimestamp(): number {
|
||||
return performance.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get platform configuration
|
||||
*/
|
||||
public getPlatformConfig(): PlatformConfig {
|
||||
return {
|
||||
maxWorkerCount: this.getHardwareConcurrency(),
|
||||
supportsModuleWorker: false,
|
||||
supportsTransferableObjects: true,
|
||||
maxSharedArrayBufferSize: 1024 * 1024 * 1024, // 1GB
|
||||
workerScriptPrefix: '',
|
||||
limitations: {
|
||||
noEval: false,
|
||||
requiresWorkerInit: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get browser information
|
||||
*/
|
||||
private getBrowserInfo(): string {
|
||||
const userAgent = navigator.userAgent;
|
||||
if (userAgent.includes('Chrome')) {
|
||||
const match = userAgent.match(/Chrome\/([0-9.]+)/);
|
||||
return match ? `Chrome ${match[1]}` : 'Chrome';
|
||||
} else if (userAgent.includes('Firefox')) {
|
||||
const match = userAgent.match(/Firefox\/([0-9.]+)/);
|
||||
if (match) return `Firefox ${match[1]}`;
|
||||
} else if (userAgent.includes('Safari')) {
|
||||
const match = userAgent.match(/Version\/([0-9.]+)/);
|
||||
if (match) return `Safari ${match[1]}`;
|
||||
}
|
||||
return 'Unknown Browser';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if SharedArrayBuffer is actually available
|
||||
*/
|
||||
private checkSharedArrayBufferEnabled(): boolean {
|
||||
try {
|
||||
new SharedArrayBuffer(8);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Browser Worker wrapper
|
||||
*/
|
||||
class BrowserWorker implements PlatformWorker {
|
||||
private _state: 'running' | 'terminated' = 'running';
|
||||
private worker: Worker;
|
||||
|
||||
constructor(script: string, options: WorkerCreationOptions = {}) {
|
||||
this.worker = this.createBrowserWorker(script, options);
|
||||
}
|
||||
|
||||
public get state(): 'running' | 'terminated' {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
public postMessage(message: any, transfer?: Transferable[]): void {
|
||||
if (this._state === 'terminated') {
|
||||
throw new Error('Worker has been terminated');
|
||||
}
|
||||
|
||||
try {
|
||||
if (transfer && transfer.length > 0) {
|
||||
this.worker.postMessage(message, transfer);
|
||||
} else {
|
||||
this.worker.postMessage(message);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to send message to Worker: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public onMessage(handler: (event: { data: any }) => void): void {
|
||||
this.worker.onmessage = (event: MessageEvent) => {
|
||||
handler({ data: event.data });
|
||||
};
|
||||
}
|
||||
|
||||
public onError(handler: (error: ErrorEvent) => void): void {
|
||||
this.worker.onerror = handler;
|
||||
}
|
||||
|
||||
public terminate(): void {
|
||||
if (this._state === 'running') {
|
||||
try {
|
||||
this.worker.terminate();
|
||||
this._state = 'terminated';
|
||||
} catch (error) {
|
||||
console.error('Failed to terminate Worker:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create browser Worker
|
||||
*/
|
||||
private createBrowserWorker(script: string, options: WorkerCreationOptions): Worker {
|
||||
try {
|
||||
// Create Blob URL
|
||||
const blob = new Blob([script], { type: 'application/javascript' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
// Create Worker
|
||||
const worker = new Worker(url, {
|
||||
type: options.type || 'classic',
|
||||
credentials: options.credentials,
|
||||
name: options.name
|
||||
});
|
||||
|
||||
// Clean up Blob URL (delayed to ensure Worker has loaded)
|
||||
setTimeout(() => {
|
||||
URL.revokeObjectURL(url);
|
||||
}, 1000);
|
||||
|
||||
return worker;
|
||||
} catch (error) {
|
||||
throw new Error(`Cannot create browser Worker: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### 1. Copy the Code
|
||||
|
||||
Copy the above code to your project, e.g., `src/platform/BrowserAdapter.ts`.
|
||||
|
||||
### 2. Register the Adapter
|
||||
|
||||
```typescript
|
||||
import { PlatformManager } from '@esengine/ecs-framework';
|
||||
import { BrowserAdapter } from './platform/BrowserAdapter';
|
||||
|
||||
// Create and register browser adapter
|
||||
const browserAdapter = new BrowserAdapter();
|
||||
PlatformManager.registerAdapter(browserAdapter);
|
||||
|
||||
// Framework will automatically detect and use the appropriate adapter
|
||||
```
|
||||
|
||||
### 3. Use WorkerEntitySystem
|
||||
|
||||
The browser adapter works with WorkerEntitySystem, and the framework automatically handles Worker script creation:
|
||||
|
||||
```typescript
|
||||
import { WorkerEntitySystem, Matcher } from '@esengine/ecs-framework';
|
||||
|
||||
class PhysicsSystem extends WorkerEntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Transform, Velocity), {
|
||||
enableWorker: true,
|
||||
workerCount: navigator.hardwareConcurrency || 4,
|
||||
useSharedArrayBuffer: true,
|
||||
systemConfig: { gravity: 9.8 }
|
||||
});
|
||||
}
|
||||
|
||||
protected getDefaultEntityDataSize(): number {
|
||||
return 6; // x, y, vx, vy, mass, radius
|
||||
}
|
||||
|
||||
protected extractEntityData(entity: Entity): PhysicsData {
|
||||
const transform = entity.getComponent(Transform);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
return {
|
||||
x: transform.x,
|
||||
y: transform.y,
|
||||
vx: velocity.x,
|
||||
vy: velocity.y,
|
||||
mass: 1,
|
||||
radius: 10
|
||||
};
|
||||
}
|
||||
|
||||
// This function is automatically serialized and executed in Worker
|
||||
protected workerProcess(entities, deltaTime, config) {
|
||||
return entities.map(entity => {
|
||||
// Apply gravity
|
||||
entity.vy += config.gravity * deltaTime;
|
||||
|
||||
// Update position
|
||||
entity.x += entity.vx * deltaTime;
|
||||
entity.y += entity.vy * deltaTime;
|
||||
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
protected applyResult(entity: Entity, result: PhysicsData): void {
|
||||
const transform = entity.getComponent(Transform);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
transform.x = result.x;
|
||||
transform.y = result.y;
|
||||
velocity.x = result.vx;
|
||||
velocity.y = result.vy;
|
||||
}
|
||||
}
|
||||
|
||||
interface PhysicsData {
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
mass: number;
|
||||
radius: number;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Verify Adapter Status
|
||||
|
||||
```typescript
|
||||
// Verify adapter is working properly
|
||||
const adapter = new BrowserAdapter();
|
||||
console.log('Adapter name:', adapter.name);
|
||||
console.log('Browser version:', adapter.version);
|
||||
console.log('Worker support:', adapter.isWorkerSupported());
|
||||
console.log('SharedArrayBuffer support:', adapter.isSharedArrayBufferSupported());
|
||||
console.log('CPU core count:', adapter.getHardwareConcurrency());
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
### SharedArrayBuffer Support
|
||||
|
||||
SharedArrayBuffer requires special security configuration:
|
||||
|
||||
1. **HTTPS**: Must be used in a secure context
|
||||
2. **COOP/COEP Headers**: Requires correct cross-origin isolation headers
|
||||
|
||||
```html
|
||||
<!-- Set in HTML -->
|
||||
<meta http-equiv="Cross-Origin-Opener-Policy" content="same-origin">
|
||||
<meta http-equiv="Cross-Origin-Embedder-Policy" content="require-corp">
|
||||
```
|
||||
|
||||
Or set in server configuration:
|
||||
|
||||
```
|
||||
Cross-Origin-Opener-Policy: same-origin
|
||||
Cross-Origin-Embedder-Policy: require-corp
|
||||
```
|
||||
|
||||
### Browser Compatibility
|
||||
|
||||
- **Worker**: All modern browsers supported
|
||||
- **Module Worker**: Chrome 80+, Firefox 114+
|
||||
- **SharedArrayBuffer**: Chrome 68+, Firefox 79+ (requires COOP/COEP)
|
||||
- **Transferable Objects**: All modern browsers supported
|
||||
|
||||
## Performance Optimization Tips
|
||||
|
||||
1. **Worker Pool**: Reuse Worker instances to avoid frequent creation and destruction
|
||||
2. **Data Transfer**: Use Transferable Objects to reduce data copying
|
||||
3. **SharedArrayBuffer**: Use SharedArrayBuffer for large data sharing
|
||||
4. **Module Worker**: Use module Workers in supported browsers for better code organization
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
```typescript
|
||||
// Check browser support
|
||||
const adapter = new BrowserAdapter();
|
||||
console.log('Worker support:', adapter.isWorkerSupported());
|
||||
console.log('SharedArrayBuffer support:', adapter.isSharedArrayBufferSupported());
|
||||
console.log('Hardware concurrency:', adapter.getHardwareConcurrency());
|
||||
console.log('Platform config:', adapter.getPlatformConfig());
|
||||
```
|
||||
560
docs/src/content/docs/en/guide/platform-adapter/nodejs.md
Normal file
560
docs/src/content/docs/en/guide/platform-adapter/nodejs.md
Normal file
@@ -0,0 +1,560 @@
|
||||
---
|
||||
title: "Node.js Adapter"
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The Node.js platform adapter provides support for Node.js server environments, suitable for game servers, compute servers, or other server applications that need ECS architecture.
|
||||
|
||||
## Feature Support
|
||||
|
||||
- **Worker**: Supported (via `worker_threads` module)
|
||||
- **SharedArrayBuffer**: Supported (Node.js 16.17.0+)
|
||||
- **Transferable Objects**: Fully supported
|
||||
- **High-Resolution Time**: Uses `process.hrtime.bigint()`
|
||||
- **Device Info**: Complete system and process information
|
||||
|
||||
## Complete Implementation
|
||||
|
||||
```typescript
|
||||
import { worker_threads, Worker, isMainThread, parentPort } from 'worker_threads';
|
||||
import * as os from 'os';
|
||||
import * as process from 'process';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import type {
|
||||
IPlatformAdapter,
|
||||
PlatformWorker,
|
||||
WorkerCreationOptions,
|
||||
PlatformConfig,
|
||||
NodeDeviceInfo
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* Node.js platform adapter
|
||||
* Supports Node.js server environments
|
||||
*/
|
||||
export class NodeAdapter implements IPlatformAdapter {
|
||||
public readonly name = 'nodejs';
|
||||
public readonly version: string;
|
||||
|
||||
constructor() {
|
||||
this.version = process.version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Worker is supported
|
||||
*/
|
||||
public isWorkerSupported(): boolean {
|
||||
try {
|
||||
// Check if worker_threads module is available
|
||||
return typeof worker_threads !== 'undefined' && typeof Worker !== 'undefined';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if SharedArrayBuffer is supported
|
||||
*/
|
||||
public isSharedArrayBufferSupported(): boolean {
|
||||
// Node.js supports SharedArrayBuffer
|
||||
return typeof SharedArrayBuffer !== 'undefined';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hardware concurrency (CPU core count)
|
||||
*/
|
||||
public getHardwareConcurrency(): number {
|
||||
return os.cpus().length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Worker
|
||||
*/
|
||||
public createWorker(script: string, options: WorkerCreationOptions = {}): PlatformWorker {
|
||||
if (!this.isWorkerSupported()) {
|
||||
throw new Error('Node.js environment does not support Worker Threads');
|
||||
}
|
||||
|
||||
try {
|
||||
return new NodeWorker(script, options);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to create Node.js Worker: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create SharedArrayBuffer
|
||||
*/
|
||||
public createSharedArrayBuffer(length: number): SharedArrayBuffer | null {
|
||||
if (!this.isSharedArrayBufferSupported()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return new SharedArrayBuffer(length);
|
||||
} catch (error) {
|
||||
console.warn('SharedArrayBuffer creation failed:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get high-resolution timestamp (nanoseconds)
|
||||
*/
|
||||
public getHighResTimestamp(): number {
|
||||
// Return milliseconds, consistent with browser performance.now()
|
||||
return Number(process.hrtime.bigint()) / 1000000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get platform configuration
|
||||
*/
|
||||
public getPlatformConfig(): PlatformConfig {
|
||||
return {
|
||||
maxWorkerCount: this.getHardwareConcurrency(),
|
||||
supportsModuleWorker: true, // Node.js supports ES modules
|
||||
supportsTransferableObjects: true,
|
||||
maxSharedArrayBufferSize: this.getMaxSharedArrayBufferSize(),
|
||||
workerScriptPrefix: '',
|
||||
limitations: {
|
||||
noEval: false, // Node.js supports eval
|
||||
requiresWorkerInit: false
|
||||
},
|
||||
extensions: {
|
||||
platform: 'nodejs',
|
||||
nodeVersion: process.version,
|
||||
v8Version: process.versions.v8,
|
||||
uvVersion: process.versions.uv,
|
||||
zlibVersion: process.versions.zlib,
|
||||
opensslVersion: process.versions.openssl,
|
||||
architecture: process.arch,
|
||||
endianness: os.endianness(),
|
||||
pid: process.pid,
|
||||
ppid: process.ppid
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Node.js device information
|
||||
*/
|
||||
public getDeviceInfo(): NodeDeviceInfo {
|
||||
const cpus = os.cpus();
|
||||
const networkInterfaces = os.networkInterfaces();
|
||||
const userInfo = os.userInfo();
|
||||
|
||||
return {
|
||||
// System info
|
||||
platform: os.platform(),
|
||||
arch: os.arch(),
|
||||
type: os.type(),
|
||||
release: os.release(),
|
||||
version: os.version(),
|
||||
hostname: os.hostname(),
|
||||
|
||||
// CPU info
|
||||
cpus: cpus.map(cpu => ({
|
||||
model: cpu.model,
|
||||
speed: cpu.speed,
|
||||
times: cpu.times
|
||||
})),
|
||||
|
||||
// Memory info
|
||||
totalMemory: os.totalmem(),
|
||||
freeMemory: os.freemem(),
|
||||
usedMemory: os.totalmem() - os.freemem(),
|
||||
|
||||
// Load info
|
||||
loadAverage: os.loadavg(),
|
||||
|
||||
// Network interfaces
|
||||
networkInterfaces: Object.fromEntries(
|
||||
Object.entries(networkInterfaces).map(([name, interfaces]) => [
|
||||
name,
|
||||
(interfaces || []).map(iface => ({
|
||||
address: iface.address,
|
||||
netmask: iface.netmask,
|
||||
family: iface.family as 'IPv4' | 'IPv6',
|
||||
mac: iface.mac,
|
||||
internal: iface.internal,
|
||||
cidr: iface.cidr,
|
||||
scopeid: iface.scopeid
|
||||
}))
|
||||
])
|
||||
),
|
||||
|
||||
// Process info
|
||||
process: {
|
||||
pid: process.pid,
|
||||
ppid: process.ppid,
|
||||
version: process.version,
|
||||
versions: process.versions,
|
||||
uptime: process.uptime()
|
||||
},
|
||||
|
||||
// User info
|
||||
userInfo: {
|
||||
uid: userInfo.uid,
|
||||
gid: userInfo.gid,
|
||||
username: userInfo.username,
|
||||
homedir: userInfo.homedir,
|
||||
shell: userInfo.shell
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get SharedArrayBuffer maximum size limit
|
||||
*/
|
||||
private getMaxSharedArrayBufferSize(): number {
|
||||
const totalMemory = os.totalmem();
|
||||
// Limit to 50% of total system memory
|
||||
return Math.floor(totalMemory * 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Node.js Worker wrapper
|
||||
*/
|
||||
class NodeWorker implements PlatformWorker {
|
||||
private _state: 'running' | 'terminated' = 'running';
|
||||
private worker: Worker;
|
||||
private isTemporaryFile: boolean = false;
|
||||
private scriptPath: string;
|
||||
|
||||
constructor(script: string, options: WorkerCreationOptions = {}) {
|
||||
try {
|
||||
// Determine if script is a file path or script content
|
||||
if (this.isFilePath(script)) {
|
||||
// Use file path directly
|
||||
this.scriptPath = script;
|
||||
this.isTemporaryFile = false;
|
||||
} else {
|
||||
// Write script content to temporary file
|
||||
this.scriptPath = this.writeScriptToFile(script, options.name);
|
||||
this.isTemporaryFile = true;
|
||||
}
|
||||
|
||||
// Create Worker
|
||||
this.worker = new Worker(this.scriptPath, {
|
||||
// Node.js Worker options
|
||||
workerData: options.name ? { name: options.name } : undefined
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to create Node.js Worker: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if string is a file path
|
||||
*/
|
||||
private isFilePath(script: string): boolean {
|
||||
// Check if it looks like a file path
|
||||
return (script.endsWith('.js') || script.endsWith('.mjs') || script.endsWith('.ts')) &&
|
||||
!script.includes('\n') &&
|
||||
!script.includes(';') &&
|
||||
script.length < 500; // File paths are typically not too long
|
||||
}
|
||||
|
||||
/**
|
||||
* Write script content to temporary file
|
||||
*/
|
||||
private writeScriptToFile(script: string, name?: string): string {
|
||||
const tmpDir = os.tmpdir();
|
||||
const fileName = name ? `worker-${name}-${Date.now()}.js` : `worker-${Date.now()}.js`;
|
||||
const filePath = path.join(tmpDir, fileName);
|
||||
|
||||
try {
|
||||
fs.writeFileSync(filePath, script, 'utf8');
|
||||
return filePath;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to write Worker script file: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public get state(): 'running' | 'terminated' {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
public postMessage(message: any, transfer?: Transferable[]): void {
|
||||
if (this._state === 'terminated') {
|
||||
throw new Error('Worker has been terminated');
|
||||
}
|
||||
|
||||
try {
|
||||
if (transfer && transfer.length > 0) {
|
||||
// Node.js Worker supports Transferable Objects
|
||||
this.worker.postMessage(message, transfer);
|
||||
} else {
|
||||
this.worker.postMessage(message);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to send message to Node.js Worker: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public onMessage(handler: (event: { data: any }) => void): void {
|
||||
this.worker.on('message', (data: any) => {
|
||||
handler({ data });
|
||||
});
|
||||
}
|
||||
|
||||
public onError(handler: (error: ErrorEvent) => void): void {
|
||||
this.worker.on('error', (error: Error) => {
|
||||
// Convert Error to ErrorEvent format
|
||||
const errorEvent = {
|
||||
message: error.message,
|
||||
filename: '',
|
||||
lineno: 0,
|
||||
colno: 0,
|
||||
error: error
|
||||
} as ErrorEvent;
|
||||
handler(errorEvent);
|
||||
});
|
||||
}
|
||||
|
||||
public terminate(): void {
|
||||
if (this._state === 'running') {
|
||||
try {
|
||||
this.worker.terminate();
|
||||
this._state = 'terminated';
|
||||
|
||||
// Clean up temporary script file
|
||||
this.cleanupScriptFile();
|
||||
} catch (error) {
|
||||
console.error('Failed to terminate Node.js Worker:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up temporary script file
|
||||
*/
|
||||
private cleanupScriptFile(): void {
|
||||
// Only clean up temporarily created files, not user-provided file paths
|
||||
if (this.scriptPath && this.isTemporaryFile) {
|
||||
try {
|
||||
fs.unlinkSync(this.scriptPath);
|
||||
} catch (error) {
|
||||
console.warn('Failed to clean up Worker script file:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### 1. Copy the Code
|
||||
|
||||
Copy the above code to your project, e.g., `src/platform/NodeAdapter.ts`.
|
||||
|
||||
### 2. Register the Adapter
|
||||
|
||||
```typescript
|
||||
import { PlatformManager } from '@esengine/ecs-framework';
|
||||
import { NodeAdapter } from './platform/NodeAdapter';
|
||||
|
||||
// Check if in Node.js environment
|
||||
if (typeof process !== 'undefined' && process.versions && process.versions.node) {
|
||||
const nodeAdapter = new NodeAdapter();
|
||||
PlatformManager.getInstance().registerAdapter(nodeAdapter);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Use WorkerEntitySystem
|
||||
|
||||
The Node.js adapter works with WorkerEntitySystem, and the framework automatically handles Worker script creation:
|
||||
|
||||
```typescript
|
||||
import { WorkerEntitySystem, Matcher } from '@esengine/ecs-framework';
|
||||
import * as os from 'os';
|
||||
|
||||
class PhysicsSystem extends WorkerEntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Transform, Velocity), {
|
||||
enableWorker: true,
|
||||
workerCount: os.cpus().length, // Use all CPU cores
|
||||
useSharedArrayBuffer: true,
|
||||
systemConfig: { gravity: 9.8 }
|
||||
});
|
||||
}
|
||||
|
||||
protected getDefaultEntityDataSize(): number {
|
||||
return 6; // x, y, vx, vy, mass, radius
|
||||
}
|
||||
|
||||
protected extractEntityData(entity: Entity): PhysicsData {
|
||||
const transform = entity.getComponent(Transform);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
return {
|
||||
x: transform.x,
|
||||
y: transform.y,
|
||||
vx: velocity.x,
|
||||
vy: velocity.y,
|
||||
mass: 1,
|
||||
radius: 10
|
||||
};
|
||||
}
|
||||
|
||||
// This function is automatically serialized and executed in Worker
|
||||
protected workerProcess(entities, deltaTime, config) {
|
||||
return entities.map(entity => {
|
||||
// Apply gravity
|
||||
entity.vy += config.gravity * deltaTime;
|
||||
|
||||
// Update position
|
||||
entity.x += entity.vx * deltaTime;
|
||||
entity.y += entity.vy * deltaTime;
|
||||
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
protected applyResult(entity: Entity, result: PhysicsData): void {
|
||||
const transform = entity.getComponent(Transform);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
transform.x = result.x;
|
||||
transform.y = result.y;
|
||||
velocity.x = result.vx;
|
||||
velocity.y = result.vy;
|
||||
}
|
||||
}
|
||||
|
||||
interface PhysicsData {
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
mass: number;
|
||||
radius: number;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Get System Information
|
||||
|
||||
```typescript
|
||||
const manager = PlatformManager.getInstance();
|
||||
if (manager.hasAdapter()) {
|
||||
const adapter = manager.getAdapter();
|
||||
const deviceInfo = adapter.getDeviceInfo();
|
||||
|
||||
console.log('Node.js version:', deviceInfo.process?.version);
|
||||
console.log('CPU core count:', deviceInfo.cpus?.length);
|
||||
console.log('Total memory:', Math.round(deviceInfo.totalMemory! / 1024 / 1024), 'MB');
|
||||
console.log('Available memory:', Math.round(deviceInfo.freeMemory! / 1024 / 1024), 'MB');
|
||||
}
|
||||
```
|
||||
|
||||
## Official Documentation Reference
|
||||
|
||||
Node.js Worker Threads related official documentation:
|
||||
|
||||
- [Worker Threads Official Docs](https://nodejs.org/api/worker_threads.html)
|
||||
- [SharedArrayBuffer Support](https://nodejs.org/api/globals.html#class-sharedarraybuffer)
|
||||
- [OS Module Docs](https://nodejs.org/api/os.html)
|
||||
- [Process Module Docs](https://nodejs.org/api/process.html)
|
||||
|
||||
## Important Notes
|
||||
|
||||
### Worker Threads Requirements
|
||||
|
||||
- **Node.js Version**: Requires Node.js 10.5.0+ (12+ recommended)
|
||||
- **Module Type**: Supports both CommonJS and ES modules
|
||||
- **Thread Limit**: Theoretically unlimited, but recommended not to exceed 2x CPU core count
|
||||
|
||||
### Performance Optimization Tips
|
||||
|
||||
#### 1. Worker Pool Management
|
||||
|
||||
```typescript
|
||||
class ServerPhysicsSystem extends WorkerEntitySystem {
|
||||
constructor() {
|
||||
const cpuCount = os.cpus().length;
|
||||
super(Matcher.all(Transform, Velocity), {
|
||||
enableWorker: true,
|
||||
workerCount: Math.min(cpuCount * 2, 16), // Max 16 Workers
|
||||
entitiesPerWorker: 1000, // 1000 entities per Worker
|
||||
useSharedArrayBuffer: true,
|
||||
systemConfig: {
|
||||
gravity: 9.8,
|
||||
timeStep: 1/60
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Memory Management
|
||||
|
||||
```typescript
|
||||
class MemoryMonitor {
|
||||
public static checkMemoryUsage(): void {
|
||||
const used = process.memoryUsage();
|
||||
|
||||
console.log('Memory usage:');
|
||||
console.log(` RSS: ${Math.round(used.rss / 1024 / 1024)} MB`);
|
||||
console.log(` Heap Used: ${Math.round(used.heapUsed / 1024 / 1024)} MB`);
|
||||
console.log(` Heap Total: ${Math.round(used.heapTotal / 1024 / 1024)} MB`);
|
||||
console.log(` External: ${Math.round(used.external / 1024 / 1024)} MB`);
|
||||
|
||||
// Trigger warning when memory usage is too high
|
||||
if (used.heapUsed > used.heapTotal * 0.9) {
|
||||
console.warn('Memory usage too high, consider optimizing or restarting');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check memory usage periodically
|
||||
setInterval(() => {
|
||||
MemoryMonitor.checkMemoryUsage();
|
||||
}, 30000); // Check every 30 seconds
|
||||
```
|
||||
|
||||
#### 3. Server Environment Optimization
|
||||
|
||||
```typescript
|
||||
// Set process title
|
||||
process.title = 'ecs-game-server';
|
||||
|
||||
// Handle uncaught exceptions
|
||||
process.on('uncaughtException', (error) => {
|
||||
console.error('Uncaught exception:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.error('Unhandled Promise rejection:', reason);
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('Received SIGTERM signal, shutting down server...');
|
||||
// Clean up resources
|
||||
process.exit(0);
|
||||
});
|
||||
```
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
```typescript
|
||||
// Check Node.js environment support
|
||||
const adapter = new NodeAdapter();
|
||||
console.log('Node.js version:', adapter.version);
|
||||
console.log('Worker support:', adapter.isWorkerSupported());
|
||||
console.log('SharedArrayBuffer support:', adapter.isSharedArrayBufferSupported());
|
||||
console.log('CPU core count:', adapter.getHardwareConcurrency());
|
||||
|
||||
// Get detailed configuration
|
||||
const config = adapter.getPlatformConfig();
|
||||
console.log('Platform config:', JSON.stringify(config, null, 2));
|
||||
|
||||
// System resource monitoring
|
||||
const deviceInfo = adapter.getDeviceInfo();
|
||||
console.log('System load:', deviceInfo.loadAverage);
|
||||
console.log('Network interfaces:', Object.keys(deviceInfo.networkInterfaces!));
|
||||
```
|
||||
@@ -0,0 +1,485 @@
|
||||
---
|
||||
title: "WeChat Mini Game Adapter"
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The WeChat Mini Game platform adapter is designed specifically for the WeChat Mini Game environment, handling special restrictions and APIs.
|
||||
|
||||
## Feature Support
|
||||
|
||||
| Feature | Support | Notes |
|
||||
|---------|---------|-------|
|
||||
| **Worker** | Supported | Requires precompiled file, configure `workerScriptPath` |
|
||||
| **SharedArrayBuffer** | Not Supported | WeChat Mini Game environment doesn't support this |
|
||||
| **Transferable Objects** | Not Supported | Only serializable objects supported |
|
||||
| **High-Resolution Time** | Supported | Uses `wx.getPerformance()` |
|
||||
| **Device Info** | Supported | Complete WeChat Mini Game device info |
|
||||
|
||||
## WorkerEntitySystem Usage
|
||||
|
||||
### Important: WeChat Mini Game Worker Restrictions
|
||||
|
||||
WeChat Mini Game Workers have the following restrictions:
|
||||
- **Worker scripts must be in the code package**, cannot be dynamically generated
|
||||
- **Must be configured in `game.json`** with `workers` directory
|
||||
- **Maximum of 1 Worker** can be created
|
||||
|
||||
Therefore, when using `WorkerEntitySystem`, there are two approaches:
|
||||
1. **Recommended: Use CLI tool** to automatically generate Worker files
|
||||
2. Manually create Worker files
|
||||
|
||||
### Method 1: Use CLI Tool for Auto-Generation (Recommended)
|
||||
|
||||
We provide the `@esengine/worker-generator` tool that can automatically extract `workerProcess` functions from your TypeScript code and generate WeChat Mini Game compatible Worker files.
|
||||
|
||||
#### Installation
|
||||
|
||||
```bash
|
||||
pnpm add -D @esengine/worker-generator
|
||||
# or
|
||||
npm install --save-dev @esengine/worker-generator
|
||||
```
|
||||
|
||||
#### Usage
|
||||
|
||||
```bash
|
||||
# Scan src directory, generate Worker files to workers directory
|
||||
npx esengine-worker-gen --src ./src --out ./workers --wechat
|
||||
|
||||
# View help
|
||||
npx esengine-worker-gen --help
|
||||
```
|
||||
|
||||
#### Parameter Reference
|
||||
|
||||
| Parameter | Description | Default |
|
||||
|-----------|-------------|---------|
|
||||
| `-s, --src <dir>` | Source code directory | `./src` |
|
||||
| `-o, --out <dir>` | Output directory | `./workers` |
|
||||
| `-w, --wechat` | Generate WeChat Mini Game compatible code | `false` |
|
||||
| `-m, --mapping` | Generate worker-mapping.json | `true` |
|
||||
| `-t, --tsconfig <path>` | TypeScript config file path | Auto-detect |
|
||||
| `-v, --verbose` | Verbose output | `false` |
|
||||
|
||||
#### Example Output
|
||||
|
||||
```
|
||||
ESEngine Worker Generator
|
||||
|
||||
Source directory: /project/src
|
||||
Output directory: /project/workers
|
||||
WeChat mode: Yes
|
||||
|
||||
Scanning for WorkerEntitySystem classes...
|
||||
|
||||
Found 1 WorkerEntitySystem class(es):
|
||||
- PhysicsSystem (src/systems/PhysicsSystem.ts)
|
||||
|
||||
Generating Worker files...
|
||||
|
||||
Successfully generated 1 Worker file(s):
|
||||
- PhysicsSystem -> workers/physics-system-worker.js
|
||||
|
||||
Usage:
|
||||
1. Copy the generated files to your project's workers/ directory
|
||||
2. Configure game.json (WeChat): { "workers": "workers" }
|
||||
3. In your System constructor, add:
|
||||
workerScriptPath: 'workers/physics-system-worker.js'
|
||||
```
|
||||
|
||||
#### Integration in Build Process
|
||||
|
||||
```json
|
||||
// package.json
|
||||
{
|
||||
"scripts": {
|
||||
"build:workers": "esengine-worker-gen --src ./src --out ./workers --wechat",
|
||||
"build": "pnpm build:workers && your-build-command"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Method 2: Manually Create Worker Files
|
||||
|
||||
If you don't want to use the CLI tool, you can manually create Worker files.
|
||||
|
||||
Create `workers/entity-worker.js` in your project:
|
||||
|
||||
```javascript
|
||||
// workers/entity-worker.js
|
||||
// WeChat Mini Game WorkerEntitySystem Generic Worker Template
|
||||
|
||||
let sharedFloatArray = null;
|
||||
|
||||
worker.onMessage(function(e) {
|
||||
const { type, id, entities, deltaTime, systemConfig, startIndex, endIndex, sharedBuffer } = e.data;
|
||||
|
||||
try {
|
||||
// Handle SharedArrayBuffer initialization
|
||||
if (type === 'init' && sharedBuffer) {
|
||||
sharedFloatArray = new Float32Array(sharedBuffer);
|
||||
worker.postMessage({ type: 'init', success: true });
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle SharedArrayBuffer data
|
||||
if (type === 'shared' && sharedFloatArray) {
|
||||
processSharedArrayBuffer(startIndex, endIndex, deltaTime, systemConfig);
|
||||
worker.postMessage({ id, result: null });
|
||||
return;
|
||||
}
|
||||
|
||||
// Traditional processing method
|
||||
if (entities) {
|
||||
const result = workerProcess(entities, deltaTime, systemConfig);
|
||||
|
||||
// Handle Promise return value
|
||||
if (result && typeof result.then === 'function') {
|
||||
result.then(function(finalResult) {
|
||||
worker.postMessage({ id, result: finalResult });
|
||||
}).catch(function(error) {
|
||||
worker.postMessage({ id, error: error.message });
|
||||
});
|
||||
} else {
|
||||
worker.postMessage({ id, result: result });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
worker.postMessage({ id, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Entity processing function - Modify this function based on your business logic
|
||||
* @param {Array} entities - Entity data array
|
||||
* @param {number} deltaTime - Frame interval time
|
||||
* @param {Object} systemConfig - System configuration
|
||||
* @returns {Array} Processed entity data
|
||||
*/
|
||||
function workerProcess(entities, deltaTime, systemConfig) {
|
||||
// ====== Write your processing logic here ======
|
||||
// Example: Physics calculation
|
||||
return entities.map(function(entity) {
|
||||
// Apply gravity
|
||||
entity.vy += (systemConfig.gravity || 100) * deltaTime;
|
||||
|
||||
// Update position
|
||||
entity.x += entity.vx * deltaTime;
|
||||
entity.y += entity.vy * deltaTime;
|
||||
|
||||
// Apply friction
|
||||
entity.vx *= (systemConfig.friction || 0.95);
|
||||
entity.vy *= (systemConfig.friction || 0.95);
|
||||
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* SharedArrayBuffer processing function (optional)
|
||||
*/
|
||||
function processSharedArrayBuffer(startIndex, endIndex, deltaTime, systemConfig) {
|
||||
if (!sharedFloatArray) return;
|
||||
|
||||
// ====== Implement SharedArrayBuffer processing logic as needed ======
|
||||
// Note: WeChat Mini Game doesn't support SharedArrayBuffer, this function typically won't be called
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Configure game.json
|
||||
|
||||
Add workers configuration in `game.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"deviceOrientation": "portrait",
|
||||
"showStatusBar": false,
|
||||
"workers": "workers"
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Use WorkerEntitySystem
|
||||
|
||||
```typescript
|
||||
import { WorkerEntitySystem, Matcher, Entity } from '@esengine/ecs-framework';
|
||||
|
||||
interface PhysicsData {
|
||||
id: number;
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
mass: number;
|
||||
}
|
||||
|
||||
class PhysicsSystem extends WorkerEntitySystem<PhysicsData> {
|
||||
constructor() {
|
||||
super(Matcher.all(Transform, Velocity), {
|
||||
enableWorker: true,
|
||||
workerCount: 1, // WeChat Mini Game limits to 1 Worker
|
||||
workerScriptPath: 'workers/entity-worker.js', // Specify precompiled Worker file
|
||||
systemConfig: {
|
||||
gravity: 100,
|
||||
friction: 0.95
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected getDefaultEntityDataSize(): number {
|
||||
return 6;
|
||||
}
|
||||
|
||||
protected extractEntityData(entity: Entity): PhysicsData {
|
||||
const transform = entity.getComponent(Transform);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
const physics = entity.getComponent(Physics);
|
||||
|
||||
return {
|
||||
id: entity.id,
|
||||
x: transform.x,
|
||||
y: transform.y,
|
||||
vx: velocity.x,
|
||||
vy: velocity.y,
|
||||
mass: physics.mass
|
||||
};
|
||||
}
|
||||
|
||||
// Note: In WeChat Mini Game, this method won't be used
|
||||
// Worker processing logic is in workers/entity-worker.js workerProcess function
|
||||
protected workerProcess(entities: PhysicsData[], deltaTime: number, config: any): PhysicsData[] {
|
||||
return entities.map(entity => {
|
||||
entity.vy += config.gravity * deltaTime;
|
||||
entity.x += entity.vx * deltaTime;
|
||||
entity.y += entity.vy * deltaTime;
|
||||
entity.vx *= config.friction;
|
||||
entity.vy *= config.friction;
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
protected applyResult(entity: Entity, result: PhysicsData): void {
|
||||
const transform = entity.getComponent(Transform);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
transform.x = result.x;
|
||||
transform.y = result.y;
|
||||
velocity.x = result.vx;
|
||||
velocity.y = result.vy;
|
||||
}
|
||||
|
||||
// SharedArrayBuffer related methods (not supported in WeChat Mini Game, can be omitted)
|
||||
protected writeEntityToBuffer(data: PhysicsData, offset: number): void {}
|
||||
protected readEntityFromBuffer(offset: number): PhysicsData | null { return null; }
|
||||
}
|
||||
```
|
||||
|
||||
### Temporarily Disable Worker (Fallback to Sync Mode)
|
||||
|
||||
If you encounter issues, you can temporarily disable Worker:
|
||||
|
||||
```typescript
|
||||
class PhysicsSystem extends WorkerEntitySystem<PhysicsData> {
|
||||
constructor() {
|
||||
super(Matcher.all(Transform, Velocity), {
|
||||
enableWorker: false, // Disable Worker, use main thread synchronous processing
|
||||
// ... other config
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Complete Adapter Implementation
|
||||
|
||||
```typescript
|
||||
import type {
|
||||
IPlatformAdapter,
|
||||
PlatformWorker,
|
||||
WorkerCreationOptions,
|
||||
PlatformConfig
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* WeChat Mini Game platform adapter
|
||||
*/
|
||||
export class WeChatMiniGameAdapter implements IPlatformAdapter {
|
||||
public readonly name = 'wechat-minigame';
|
||||
public readonly version: string;
|
||||
private systemInfo: any;
|
||||
|
||||
constructor() {
|
||||
this.systemInfo = this.getSystemInfo();
|
||||
this.version = this.systemInfo.SDKVersion || 'unknown';
|
||||
}
|
||||
|
||||
public isWorkerSupported(): boolean {
|
||||
return typeof wx !== 'undefined' && typeof wx.createWorker === 'function';
|
||||
}
|
||||
|
||||
public isSharedArrayBufferSupported(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
public getHardwareConcurrency(): number {
|
||||
return 1; // WeChat Mini Game max 1 Worker
|
||||
}
|
||||
|
||||
public createWorker(scriptPath: string, options: WorkerCreationOptions = {}): PlatformWorker {
|
||||
if (!this.isWorkerSupported()) {
|
||||
throw new Error('WeChat Mini Game environment does not support Worker');
|
||||
}
|
||||
|
||||
// scriptPath must be a file path in the code package
|
||||
const worker = wx.createWorker(scriptPath, {
|
||||
useExperimentalWorker: true
|
||||
});
|
||||
|
||||
return new WeChatWorker(worker);
|
||||
}
|
||||
|
||||
public createSharedArrayBuffer(length: number): SharedArrayBuffer | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
public getHighResTimestamp(): number {
|
||||
if (typeof wx !== 'undefined' && wx.getPerformance) {
|
||||
return wx.getPerformance().now();
|
||||
}
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
public getPlatformConfig(): PlatformConfig {
|
||||
return {
|
||||
maxWorkerCount: 1,
|
||||
supportsModuleWorker: false,
|
||||
supportsTransferableObjects: false,
|
||||
maxSharedArrayBufferSize: 0,
|
||||
workerScriptPrefix: '',
|
||||
limitations: {
|
||||
noEval: true, // Important: Mark that dynamic scripts not supported
|
||||
requiresWorkerInit: false,
|
||||
memoryLimit: 512 * 1024 * 1024,
|
||||
workerNotSupported: false,
|
||||
workerLimitations: [
|
||||
'Maximum of 1 Worker can be created',
|
||||
'Worker scripts must be in the code package',
|
||||
'workers path must be configured in game.json',
|
||||
'workerScriptPath configuration required'
|
||||
]
|
||||
},
|
||||
extensions: {
|
||||
platform: 'wechat-minigame',
|
||||
sdkVersion: this.systemInfo.SDKVersion
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private getSystemInfo(): any {
|
||||
if (typeof wx !== 'undefined' && wx.getSystemInfoSync) {
|
||||
try {
|
||||
return wx.getSystemInfoSync();
|
||||
} catch (error) {
|
||||
console.warn('Failed to get WeChat system info:', error);
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WeChat Worker wrapper
|
||||
*/
|
||||
class WeChatWorker implements PlatformWorker {
|
||||
private _state: 'running' | 'terminated' = 'running';
|
||||
private worker: any;
|
||||
|
||||
constructor(worker: any) {
|
||||
this.worker = worker;
|
||||
}
|
||||
|
||||
public get state(): 'running' | 'terminated' {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
public postMessage(message: any, transfer?: Transferable[]): void {
|
||||
if (this._state === 'terminated') {
|
||||
throw new Error('Worker has been terminated');
|
||||
}
|
||||
this.worker.postMessage(message);
|
||||
}
|
||||
|
||||
public onMessage(handler: (event: { data: any }) => void): void {
|
||||
this.worker.onMessage((res: any) => {
|
||||
handler({ data: res });
|
||||
});
|
||||
}
|
||||
|
||||
public onError(handler: (error: ErrorEvent) => void): void {
|
||||
if (this.worker.onError) {
|
||||
this.worker.onError(handler);
|
||||
}
|
||||
}
|
||||
|
||||
public terminate(): void {
|
||||
if (this._state === 'running') {
|
||||
this.worker.terminate();
|
||||
this._state = 'terminated';
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Register the Adapter
|
||||
|
||||
```typescript
|
||||
import { PlatformManager } from '@esengine/ecs-framework';
|
||||
import { WeChatMiniGameAdapter } from './platform/WeChatMiniGameAdapter';
|
||||
|
||||
// Register adapter at game startup
|
||||
if (typeof wx !== 'undefined') {
|
||||
const adapter = new WeChatMiniGameAdapter();
|
||||
PlatformManager.getInstance().registerAdapter(adapter);
|
||||
}
|
||||
```
|
||||
|
||||
## Official Documentation Reference
|
||||
|
||||
- [wx.createWorker API](https://developers.weixin.qq.com/minigame/dev/api/worker/wx.createWorker.html)
|
||||
- [Worker.postMessage API](https://developers.weixin.qq.com/minigame/dev/api/worker/Worker.postMessage.html)
|
||||
- [Worker.onMessage API](https://developers.weixin.qq.com/minigame/dev/api/worker/Worker.onMessage.html)
|
||||
|
||||
## Important Notes
|
||||
|
||||
### Worker Restrictions
|
||||
|
||||
| Restriction | Description |
|
||||
|-------------|-------------|
|
||||
| Quantity Limit | Maximum of 1 Worker can be created |
|
||||
| Version Requirement | Requires base library 1.9.90 or above |
|
||||
| Script Location | Must be in code package, dynamic generation not supported |
|
||||
| Lifecycle | Must terminate() before creating new Worker |
|
||||
|
||||
### Memory Limits
|
||||
|
||||
- Typically limited to 256MB - 512MB
|
||||
- Need to release unused resources promptly
|
||||
- Recommend listening for memory warnings:
|
||||
|
||||
```typescript
|
||||
wx.onMemoryWarning(() => {
|
||||
console.warn('Received memory warning, starting resource cleanup');
|
||||
// Clean up unnecessary resources
|
||||
});
|
||||
```
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
```typescript
|
||||
// Check Worker configuration
|
||||
const adapter = PlatformManager.getInstance().getAdapter();
|
||||
const config = adapter.getPlatformConfig();
|
||||
|
||||
console.log('Worker support:', adapter.isWorkerSupported());
|
||||
console.log('Max Worker count:', config.maxWorkerCount);
|
||||
console.log('Platform limitations:', config.limitations);
|
||||
```
|
||||
151
docs/src/content/docs/en/guide/plugin-system/best-practices.md
Normal file
151
docs/src/content/docs/en/guide/plugin-system/best-practices.md
Normal file
@@ -0,0 +1,151 @@
|
||||
---
|
||||
title: "Best Practices"
|
||||
description: "Plugin design guidelines and common issues"
|
||||
---
|
||||
|
||||
## Naming Convention
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
// Use lowercase letters and hyphens
|
||||
readonly name = 'my-awesome-plugin'; // OK
|
||||
|
||||
// Follow semantic versioning
|
||||
readonly version = '1.0.0'; // OK
|
||||
}
|
||||
```
|
||||
|
||||
## Resource Cleanup
|
||||
|
||||
Always clean up all resources created by the plugin in `uninstall`:
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-plugin';
|
||||
readonly version = '1.0.0';
|
||||
private timerId?: number;
|
||||
private listener?: () => void;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// Add timer
|
||||
this.timerId = setInterval(() => {
|
||||
// ...
|
||||
}, 1000);
|
||||
|
||||
// Add event listener
|
||||
this.listener = () => {};
|
||||
window.addEventListener('resize', this.listener);
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Clear timer
|
||||
if (this.timerId) {
|
||||
clearInterval(this.timerId);
|
||||
this.timerId = undefined;
|
||||
}
|
||||
|
||||
// Remove event listener
|
||||
if (this.listener) {
|
||||
window.removeEventListener('resize', this.listener);
|
||||
this.listener = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
async install(core: Core, services: ServiceContainer): Promise<void> {
|
||||
try {
|
||||
await this.loadConfig();
|
||||
} catch (error) {
|
||||
console.error('Failed to load plugin config:', error);
|
||||
throw error; // Re-throw to let framework know installation failed
|
||||
}
|
||||
}
|
||||
|
||||
async uninstall(): Promise<void> {
|
||||
try {
|
||||
await this.cleanup();
|
||||
} catch (error) {
|
||||
console.error('Failed to cleanup plugin:', error);
|
||||
// Don't block uninstall even if cleanup fails
|
||||
}
|
||||
}
|
||||
|
||||
private async loadConfig() { /* ... */ }
|
||||
private async cleanup() { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Allow users to configure plugin behavior:
|
||||
|
||||
```typescript
|
||||
interface NetworkPluginConfig {
|
||||
serverUrl: string;
|
||||
autoReconnect: boolean;
|
||||
timeout: number;
|
||||
}
|
||||
|
||||
class NetworkPlugin implements IPlugin {
|
||||
readonly name = 'network-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
constructor(private config: NetworkPluginConfig) {}
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
const network = new NetworkService(this.config);
|
||||
services.registerInstance(NetworkService, network);
|
||||
}
|
||||
|
||||
uninstall(): void {}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const plugin = new NetworkPlugin({
|
||||
serverUrl: 'ws://localhost:8080',
|
||||
autoReconnect: true,
|
||||
timeout: 5000
|
||||
});
|
||||
|
||||
await Core.installPlugin(plugin);
|
||||
```
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Plugin Installation Failed
|
||||
|
||||
**Causes**:
|
||||
- Dependencies not satisfied
|
||||
- Exception in install method
|
||||
- Service registration conflict
|
||||
|
||||
**Solutions**:
|
||||
1. Check if dependencies are installed
|
||||
2. Review error logs
|
||||
3. Ensure service names don't conflict
|
||||
|
||||
### Side Effects After Uninstall
|
||||
|
||||
**Cause**: Resources not properly cleaned in uninstall
|
||||
|
||||
**Solution**: Ensure uninstall cleans up:
|
||||
- Timers
|
||||
- Event listeners
|
||||
- WebSocket connections
|
||||
- System references
|
||||
|
||||
### When to Use Plugins
|
||||
|
||||
| Good for Plugins | Not Good for Plugins |
|
||||
|------------------|---------------------|
|
||||
| Optional features (debug tools, profiling) | Core game logic |
|
||||
| Third-party integration (network libs, physics) | Simple utilities |
|
||||
| Cross-project reusable modules | Project-specific features |
|
||||
106
docs/src/content/docs/en/guide/plugin-system/dependencies.md
Normal file
106
docs/src/content/docs/en/guide/plugin-system/dependencies.md
Normal file
@@ -0,0 +1,106 @@
|
||||
---
|
||||
title: "Dependency Management"
|
||||
description: "Declare and check plugin dependencies"
|
||||
---
|
||||
|
||||
## Declaring Dependencies
|
||||
|
||||
Plugins can declare dependencies on other plugins:
|
||||
|
||||
```typescript
|
||||
class AdvancedPhysicsPlugin implements IPlugin {
|
||||
readonly name = 'advanced-physics';
|
||||
readonly version = '2.0.0';
|
||||
|
||||
// Declare dependency on base physics plugin
|
||||
readonly dependencies = ['physics-plugin'] as const;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// Can safely use services from physics-plugin
|
||||
const physicsService = services.resolve(PhysicsService);
|
||||
// ...
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Cleanup
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Dependency Checking
|
||||
|
||||
The framework automatically checks dependencies and throws an error if not satisfied:
|
||||
|
||||
```typescript
|
||||
// Error: physics-plugin not installed
|
||||
try {
|
||||
await Core.installPlugin(new AdvancedPhysicsPlugin());
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
// Plugin advanced-physics has unmet dependencies: physics-plugin
|
||||
}
|
||||
|
||||
// Correct: install dependency first
|
||||
await Core.installPlugin(new PhysicsPlugin());
|
||||
await Core.installPlugin(new AdvancedPhysicsPlugin());
|
||||
```
|
||||
|
||||
## Uninstall Order
|
||||
|
||||
The framework checks dependencies to prevent uninstalling plugins required by others:
|
||||
|
||||
```typescript
|
||||
await Core.installPlugin(new PhysicsPlugin());
|
||||
await Core.installPlugin(new AdvancedPhysicsPlugin());
|
||||
|
||||
// Error: physics-plugin is required by advanced-physics
|
||||
try {
|
||||
await Core.uninstallPlugin('physics-plugin');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
// Cannot uninstall plugin physics-plugin: it is required by advanced-physics
|
||||
}
|
||||
|
||||
// Correct: uninstall dependent plugin first
|
||||
await Core.uninstallPlugin('advanced-physics');
|
||||
await Core.uninstallPlugin('physics-plugin');
|
||||
```
|
||||
|
||||
## Dependency Graph Example
|
||||
|
||||
```
|
||||
physics-plugin (base)
|
||||
↑
|
||||
advanced-physics (depends on physics-plugin)
|
||||
↑
|
||||
game-physics (depends on advanced-physics)
|
||||
```
|
||||
|
||||
Install order: `physics-plugin` → `advanced-physics` → `game-physics`
|
||||
|
||||
Uninstall order: `game-physics` → `advanced-physics` → `physics-plugin`
|
||||
|
||||
## Multiple Dependencies
|
||||
|
||||
```typescript
|
||||
class GamePlugin implements IPlugin {
|
||||
readonly name = 'game';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
// Declare multiple dependencies
|
||||
readonly dependencies = [
|
||||
'physics-plugin',
|
||||
'network-plugin',
|
||||
'audio-plugin'
|
||||
] as const;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// All dependencies are available
|
||||
const physics = services.resolve(PhysicsService);
|
||||
const network = services.resolve(NetworkService);
|
||||
const audio = services.resolve(AudioService);
|
||||
}
|
||||
|
||||
uninstall(): void {}
|
||||
}
|
||||
```
|
||||
139
docs/src/content/docs/en/guide/plugin-system/development.md
Normal file
139
docs/src/content/docs/en/guide/plugin-system/development.md
Normal file
@@ -0,0 +1,139 @@
|
||||
---
|
||||
title: "Plugin Development"
|
||||
description: "IPlugin interface and lifecycle"
|
||||
---
|
||||
|
||||
## IPlugin Interface
|
||||
|
||||
All plugins must implement the `IPlugin` interface:
|
||||
|
||||
```typescript
|
||||
export interface IPlugin {
|
||||
// Unique plugin name
|
||||
readonly name: string;
|
||||
|
||||
// Plugin version (semver recommended)
|
||||
readonly version: string;
|
||||
|
||||
// Dependencies on other plugins (optional)
|
||||
readonly dependencies?: readonly string[];
|
||||
|
||||
// Called when plugin is installed
|
||||
install(core: Core, services: ServiceContainer): void | Promise<void>;
|
||||
|
||||
// Called when plugin is uninstalled
|
||||
uninstall(): void | Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
## Lifecycle Methods
|
||||
|
||||
### install Method
|
||||
|
||||
Called when the plugin is installed, used for initialization:
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// 1. Register services
|
||||
services.registerSingleton(MyService);
|
||||
|
||||
// 2. Access current scene
|
||||
const scene = core.scene;
|
||||
if (scene) {
|
||||
// 3. Add systems
|
||||
scene.addSystem(new MySystem());
|
||||
}
|
||||
|
||||
// 4. Other initialization
|
||||
console.log('Plugin initialized');
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Cleanup logic
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### uninstall Method
|
||||
|
||||
Called when the plugin is uninstalled, used for cleanup:
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-plugin';
|
||||
readonly version = '1.0.0';
|
||||
private myService?: MyService;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
this.myService = new MyService();
|
||||
services.registerInstance(MyService, this.myService);
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Cleanup service
|
||||
if (this.myService) {
|
||||
this.myService.dispose();
|
||||
this.myService = undefined;
|
||||
}
|
||||
|
||||
// Remove event listeners
|
||||
// Release other resources
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Async Plugins
|
||||
|
||||
Both `install` and `uninstall` methods support async:
|
||||
|
||||
```typescript
|
||||
class AsyncPlugin implements IPlugin {
|
||||
readonly name = 'async-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
async install(core: Core, services: ServiceContainer): Promise<void> {
|
||||
// Async load resources
|
||||
const config = await fetch('/plugin-config.json').then(r => r.json());
|
||||
|
||||
// Initialize service with loaded config
|
||||
const service = new MyService(config);
|
||||
services.registerInstance(MyService, service);
|
||||
}
|
||||
|
||||
async uninstall(): Promise<void> {
|
||||
// Async cleanup
|
||||
await this.saveState();
|
||||
}
|
||||
|
||||
private async saveState() {
|
||||
// Save plugin state
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
await Core.installPlugin(new AsyncPlugin());
|
||||
```
|
||||
|
||||
## Lifecycle Flow
|
||||
|
||||
```
|
||||
Install: Core.installPlugin(plugin)
|
||||
↓
|
||||
Dependency check: Verify dependencies are satisfied
|
||||
↓
|
||||
Call install(): Register services, add systems
|
||||
↓
|
||||
State update: Mark as installed
|
||||
|
||||
Uninstall: Core.uninstallPlugin(name)
|
||||
↓
|
||||
Dependency check: Verify not required by other plugins
|
||||
↓
|
||||
Call uninstall(): Cleanup resources
|
||||
↓
|
||||
State update: Remove from plugin list
|
||||
```
|
||||
188
docs/src/content/docs/en/guide/plugin-system/examples.md
Normal file
188
docs/src/content/docs/en/guide/plugin-system/examples.md
Normal file
@@ -0,0 +1,188 @@
|
||||
---
|
||||
title: "Example Plugins"
|
||||
description: "Complete plugin implementation examples"
|
||||
---
|
||||
|
||||
## Network Sync Plugin
|
||||
|
||||
```typescript
|
||||
import { IPlugin, IService, Core, ServiceContainer } from '@esengine/ecs-framework';
|
||||
|
||||
class NetworkSyncService implements IService {
|
||||
private ws?: WebSocket;
|
||||
|
||||
connect(url: string) {
|
||||
this.ws = new WebSocket(url);
|
||||
this.ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
this.handleMessage(data);
|
||||
};
|
||||
}
|
||||
|
||||
private handleMessage(data: any) {
|
||||
// Handle network messages
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
this.ws = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NetworkSyncPlugin implements IPlugin {
|
||||
readonly name = 'network-sync';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// Register network service
|
||||
services.registerSingleton(NetworkSyncService);
|
||||
|
||||
// Auto connect
|
||||
const network = services.resolve(NetworkSyncService);
|
||||
network.connect('ws://localhost:8080');
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Service will auto-dispose
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Analysis Plugin
|
||||
|
||||
```typescript
|
||||
class PerformanceAnalysisPlugin implements IPlugin {
|
||||
readonly name = 'performance-analysis';
|
||||
readonly version = '1.0.0';
|
||||
private frameCount = 0;
|
||||
private totalTime = 0;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
const monitor = services.resolve(PerformanceMonitor);
|
||||
monitor.enable();
|
||||
|
||||
// Periodic performance report
|
||||
const timer = services.resolve(TimerManager);
|
||||
timer.schedule(5.0, true, null, () => {
|
||||
this.printReport(monitor);
|
||||
});
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Cleanup
|
||||
}
|
||||
|
||||
private printReport(monitor: PerformanceMonitor) {
|
||||
console.log('=== Performance Report ===');
|
||||
console.log(`FPS: ${monitor.getFPS()}`);
|
||||
console.log(`Memory: ${monitor.getMemoryUsage()} MB`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Debug Tools Plugin
|
||||
|
||||
```typescript
|
||||
class DebugToolsPlugin implements IPlugin {
|
||||
readonly name = 'debug-tools';
|
||||
readonly version = '1.0.0';
|
||||
private debugUI?: DebugUI;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// Create debug UI
|
||||
this.debugUI = new DebugUI();
|
||||
this.debugUI.mount(document.body);
|
||||
|
||||
// Register hotkey
|
||||
window.addEventListener('keydown', this.handleKeyDown);
|
||||
|
||||
// Add debug system
|
||||
const scene = core.scene;
|
||||
if (scene) {
|
||||
scene.addSystem(new DebugRenderSystem());
|
||||
}
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Remove UI
|
||||
if (this.debugUI) {
|
||||
this.debugUI.unmount();
|
||||
this.debugUI = undefined;
|
||||
}
|
||||
|
||||
// Remove event listener
|
||||
window.removeEventListener('keydown', this.handleKeyDown);
|
||||
}
|
||||
|
||||
private handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'F12') {
|
||||
this.debugUI?.toggle();
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Audio Plugin
|
||||
|
||||
```typescript
|
||||
class AudioPlugin implements IPlugin {
|
||||
readonly name = 'audio';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
constructor(private config: { volume: number }) {}
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
const audioService = new AudioService(this.config);
|
||||
services.registerInstance(AudioService, audioService);
|
||||
|
||||
// Add audio system
|
||||
const scene = core.scene;
|
||||
if (scene) {
|
||||
scene.addSystem(new AudioSystem());
|
||||
}
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Stop all audio
|
||||
const audio = Core.services.resolve(AudioService);
|
||||
audio.stopAll();
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
await Core.installPlugin(new AudioPlugin({ volume: 0.8 }));
|
||||
```
|
||||
|
||||
## Input Manager Plugin
|
||||
|
||||
```typescript
|
||||
class InputPlugin implements IPlugin {
|
||||
readonly name = 'input';
|
||||
readonly version = '1.0.0';
|
||||
private inputManager?: InputManager;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
this.inputManager = new InputManager();
|
||||
services.registerInstance(InputManager, this.inputManager);
|
||||
|
||||
// Bind default keys
|
||||
this.inputManager.bind('jump', ['Space', 'KeyW']);
|
||||
this.inputManager.bind('attack', ['MouseLeft', 'KeyJ']);
|
||||
|
||||
// Add input system
|
||||
const scene = core.scene;
|
||||
if (scene) {
|
||||
scene.addSystem(new InputSystem());
|
||||
}
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
if (this.inputManager) {
|
||||
this.inputManager.dispose();
|
||||
this.inputManager = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
85
docs/src/content/docs/en/guide/plugin-system/index.md
Normal file
85
docs/src/content/docs/en/guide/plugin-system/index.md
Normal file
@@ -0,0 +1,85 @@
|
||||
---
|
||||
title: "Plugin System"
|
||||
description: "Extend ECS Framework in a modular way"
|
||||
---
|
||||
|
||||
The plugin system allows you to extend ECS Framework functionality in a modular way. Through plugins, you can encapsulate specific features (like network sync, physics engines, debug tools) and reuse them across multiple projects.
|
||||
|
||||
## What is a Plugin
|
||||
|
||||
A plugin is a class that implements the `IPlugin` interface and can be dynamically installed into the framework at runtime. Plugins can:
|
||||
|
||||
- Register custom services to the service container
|
||||
- Add systems to scenes
|
||||
- Register custom components
|
||||
- Extend framework functionality
|
||||
|
||||
## Plugin Benefits
|
||||
|
||||
| Benefit | Description |
|
||||
|---------|-------------|
|
||||
| **Modular** | Encapsulate functionality as independent modules |
|
||||
| **Reusable** | Use the same plugin across multiple projects |
|
||||
| **Decoupled** | Separate core framework from extensions |
|
||||
| **Hot-swappable** | Dynamically install and uninstall at runtime |
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Create a Plugin
|
||||
|
||||
```typescript
|
||||
import { IPlugin, Core, ServiceContainer } from '@esengine/ecs-framework';
|
||||
|
||||
class DebugPlugin implements IPlugin {
|
||||
readonly name = 'debug-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
console.log('Debug plugin installed');
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
console.log('Debug plugin uninstalled');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Install a Plugin
|
||||
|
||||
```typescript
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
Core.create({ debug: true });
|
||||
|
||||
// Install plugin
|
||||
await Core.installPlugin(new DebugPlugin());
|
||||
|
||||
// Check if plugin is installed
|
||||
if (Core.isPluginInstalled('debug-plugin')) {
|
||||
console.log('Debug plugin is running');
|
||||
}
|
||||
```
|
||||
|
||||
### Uninstall a Plugin
|
||||
|
||||
```typescript
|
||||
await Core.uninstallPlugin('debug-plugin');
|
||||
```
|
||||
|
||||
### Get Plugin Instance
|
||||
|
||||
```typescript
|
||||
const plugin = Core.getPlugin('debug-plugin');
|
||||
if (plugin) {
|
||||
console.log(`Plugin version: ${plugin.version}`);
|
||||
}
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Development](./development/) - IPlugin interface and lifecycle
|
||||
- [Services & Systems](./services-systems/) - Register services and add systems
|
||||
- [Dependencies](./dependencies/) - Declare and check dependencies
|
||||
- [Management](./management/) - Manage via Core and PluginManager
|
||||
- [Examples](./examples/) - Complete examples
|
||||
- [Best Practices](./best-practices/) - Design guidelines
|
||||
93
docs/src/content/docs/en/guide/plugin-system/management.md
Normal file
93
docs/src/content/docs/en/guide/plugin-system/management.md
Normal file
@@ -0,0 +1,93 @@
|
||||
---
|
||||
title: "Plugin Management"
|
||||
description: "Manage plugins via Core and PluginManager"
|
||||
---
|
||||
|
||||
## Via Core
|
||||
|
||||
Core class provides convenient plugin management methods:
|
||||
|
||||
```typescript
|
||||
// Install plugin
|
||||
await Core.installPlugin(myPlugin);
|
||||
|
||||
// Uninstall plugin
|
||||
await Core.uninstallPlugin('plugin-name');
|
||||
|
||||
// Check if plugin is installed
|
||||
if (Core.isPluginInstalled('plugin-name')) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// Get plugin instance
|
||||
const plugin = Core.getPlugin('plugin-name');
|
||||
```
|
||||
|
||||
## Via PluginManager
|
||||
|
||||
You can also use the PluginManager service directly:
|
||||
|
||||
```typescript
|
||||
const pluginManager = Core.services.resolve(PluginManager);
|
||||
|
||||
// Get all plugins
|
||||
const allPlugins = pluginManager.getAllPlugins();
|
||||
console.log(`Total plugins: ${allPlugins.length}`);
|
||||
|
||||
// Get plugin metadata
|
||||
const metadata = pluginManager.getMetadata('my-plugin');
|
||||
if (metadata) {
|
||||
console.log(`State: ${metadata.state}`);
|
||||
console.log(`Installed at: ${new Date(metadata.installedAt!)}`);
|
||||
}
|
||||
|
||||
// Get all plugin metadata
|
||||
const allMetadata = pluginManager.getAllMetadata();
|
||||
for (const meta of allMetadata) {
|
||||
console.log(`${meta.name} v${meta.version} - ${meta.state}`);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Core Static Methods
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `installPlugin(plugin)` | Install plugin |
|
||||
| `uninstallPlugin(name)` | Uninstall plugin |
|
||||
| `isPluginInstalled(name)` | Check if installed |
|
||||
| `getPlugin(name)` | Get plugin instance |
|
||||
|
||||
### PluginManager Methods
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `getAllPlugins()` | Get all plugins |
|
||||
| `getMetadata(name)` | Get plugin metadata |
|
||||
| `getAllMetadata()` | Get all plugin metadata |
|
||||
|
||||
## Plugin States
|
||||
|
||||
```typescript
|
||||
enum PluginState {
|
||||
Pending = 'pending',
|
||||
Installing = 'installing',
|
||||
Installed = 'installed',
|
||||
Uninstalling = 'uninstalling',
|
||||
Failed = 'failed'
|
||||
}
|
||||
```
|
||||
|
||||
## Metadata Information
|
||||
|
||||
```typescript
|
||||
interface PluginMetadata {
|
||||
name: string;
|
||||
version: string;
|
||||
state: PluginState;
|
||||
dependencies?: string[];
|
||||
installedAt?: number;
|
||||
error?: Error;
|
||||
}
|
||||
```
|
||||
133
docs/src/content/docs/en/guide/plugin-system/services-systems.md
Normal file
133
docs/src/content/docs/en/guide/plugin-system/services-systems.md
Normal file
@@ -0,0 +1,133 @@
|
||||
---
|
||||
title: "Services & Systems"
|
||||
description: "Register services and add systems in plugins"
|
||||
---
|
||||
|
||||
## Registering Services
|
||||
|
||||
Plugins can register their own services to the service container:
|
||||
|
||||
```typescript
|
||||
import { IService } from '@esengine/ecs-framework';
|
||||
|
||||
class NetworkService implements IService {
|
||||
connect(url: string) {
|
||||
console.log(`Connecting to ${url}`);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
console.log('Network service disposed');
|
||||
}
|
||||
}
|
||||
|
||||
class NetworkPlugin implements IPlugin {
|
||||
readonly name = 'network-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// Register network service
|
||||
services.registerSingleton(NetworkService);
|
||||
|
||||
// Resolve and use service
|
||||
const network = services.resolve(NetworkService);
|
||||
network.connect('ws://localhost:8080');
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Service container will auto-call service's dispose method
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Service Registration Methods
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `registerSingleton(Type)` | Register singleton service |
|
||||
| `registerInstance(Type, instance)` | Register existing instance |
|
||||
| `registerTransient(Type)` | Create new instance per resolve |
|
||||
|
||||
## Adding Systems
|
||||
|
||||
Plugins can add custom systems to scenes:
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, Matcher } from '@esengine/ecs-framework';
|
||||
|
||||
class PhysicsSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(PhysicsBody));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Physics simulation logic
|
||||
}
|
||||
}
|
||||
|
||||
class PhysicsPlugin implements IPlugin {
|
||||
readonly name = 'physics-plugin';
|
||||
readonly version = '1.0.0';
|
||||
private physicsSystem?: PhysicsSystem;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
const scene = core.scene;
|
||||
if (scene) {
|
||||
this.physicsSystem = new PhysicsSystem();
|
||||
scene.addSystem(this.physicsSystem);
|
||||
}
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Remove system
|
||||
if (this.physicsSystem) {
|
||||
const scene = Core.scene;
|
||||
if (scene) {
|
||||
scene.removeSystem(this.physicsSystem);
|
||||
}
|
||||
this.physicsSystem = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Combined Usage
|
||||
|
||||
```typescript
|
||||
class GamePlugin implements IPlugin {
|
||||
readonly name = 'game-plugin';
|
||||
readonly version = '1.0.0';
|
||||
private systems: EntitySystem[] = [];
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// 1. Register services
|
||||
services.registerSingleton(ScoreService);
|
||||
services.registerSingleton(AudioService);
|
||||
|
||||
// 2. Add systems
|
||||
const scene = core.scene;
|
||||
if (scene) {
|
||||
const systems = [
|
||||
new InputSystem(),
|
||||
new MovementSystem(),
|
||||
new ScoringSystem()
|
||||
];
|
||||
|
||||
systems.forEach(system => {
|
||||
scene.addSystem(system);
|
||||
this.systems.push(system);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Remove all systems
|
||||
const scene = Core.scene;
|
||||
if (scene) {
|
||||
this.systems.forEach(system => {
|
||||
scene.removeSystem(system);
|
||||
});
|
||||
}
|
||||
this.systems = [];
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
title: "scene-manager"
|
||||
---
|
||||
|
||||
# SceneManager
|
||||
|
||||
SceneManager is a lightweight scene manager provided by ECS Framework, suitable for 95% of game applications. It provides a simple and intuitive API with support for scene transitions and delayed loading.
|
||||
@@ -19,7 +23,7 @@ SceneManager is suitable for:
|
||||
- Automatic ECS fluent API management
|
||||
- Automatic scene lifecycle handling
|
||||
- Integrated with Core, auto-updated
|
||||
- Supports [Persistent Entity](./persistent-entity) migration across scenes (v2.3.0+)
|
||||
- Supports [Persistent Entity](/en/guide/persistent-entity/) migration across scenes (v2.3.0+)
|
||||
|
||||
## Basic Usage
|
||||
|
||||
@@ -430,7 +434,6 @@ Core (Global Services)
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Persistent Entity](./persistent-entity) - Learn how to keep entities across scene transitions
|
||||
- [WorldManager](./world-manager) - Learn about advanced multi-world isolation features
|
||||
- [Persistent Entity](/en/guide/persistent-entity/) - Learn how to keep entities across scene transitions
|
||||
|
||||
SceneManager provides simple yet powerful scene management capabilities for most games. Through Core's static methods, you can easily manage scene transitions.
|
||||
179
docs/src/content/docs/en/guide/scene/best-practices.md
Normal file
179
docs/src/content/docs/en/guide/scene/best-practices.md
Normal file
@@ -0,0 +1,179 @@
|
||||
---
|
||||
title: "Best Practices"
|
||||
description: "Scene design patterns and complete examples"
|
||||
---
|
||||
|
||||
## Scene Responsibility Separation
|
||||
|
||||
```typescript
|
||||
// Good scene design - clear responsibilities
|
||||
class MenuScene extends Scene {
|
||||
// Only handles menu-related logic
|
||||
}
|
||||
|
||||
class GameScene extends Scene {
|
||||
// Only handles gameplay logic
|
||||
}
|
||||
|
||||
class InventoryScene extends Scene {
|
||||
// Only handles inventory logic
|
||||
}
|
||||
|
||||
// Avoid this design - mixed responsibilities
|
||||
class MegaScene extends Scene {
|
||||
// Contains menu, game, inventory, and all other logic
|
||||
}
|
||||
```
|
||||
|
||||
## Resource Management
|
||||
|
||||
```typescript
|
||||
class ResourceScene extends Scene {
|
||||
private textures: Map<string, any> = new Map();
|
||||
private sounds: Map<string, any> = new Map();
|
||||
|
||||
protected initialize(): void {
|
||||
this.loadResources();
|
||||
}
|
||||
|
||||
private loadResources(): void {
|
||||
this.textures.set('player', this.loadTexture('player.png'));
|
||||
this.sounds.set('bgm', this.loadSound('bgm.mp3'));
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// Cleanup resources
|
||||
this.textures.clear();
|
||||
this.sounds.clear();
|
||||
console.log('Scene resources cleaned up');
|
||||
}
|
||||
|
||||
private loadTexture(path: string): any { return null; }
|
||||
private loadSound(path: string): any { return null; }
|
||||
}
|
||||
```
|
||||
|
||||
## Initialization Order
|
||||
|
||||
```typescript
|
||||
class ProperInitScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 1. First set scene configuration
|
||||
this.name = "GameScene";
|
||||
|
||||
// 2. Then add systems (by dependency order)
|
||||
this.addSystem(new InputSystem());
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new PhysicsSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
|
||||
// 3. Create entities last
|
||||
this.createEntities();
|
||||
|
||||
// 4. Setup event listeners
|
||||
this.setupEvents();
|
||||
}
|
||||
|
||||
private createEntities(): void { /* ... */ }
|
||||
private setupEvents(): void { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
```typescript
|
||||
import { Scene, EntitySystem, Entity, Matcher } from '@esengine/ecs-framework';
|
||||
|
||||
// Define components
|
||||
class Transform {
|
||||
constructor(public x: number, public y: number) {}
|
||||
}
|
||||
|
||||
class Velocity {
|
||||
constructor(public vx: number, public vy: number) {}
|
||||
}
|
||||
|
||||
class Health {
|
||||
constructor(public value: number) {}
|
||||
}
|
||||
|
||||
// Define system
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Transform, Velocity));
|
||||
}
|
||||
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const transform = entity.getComponent(Transform);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
if (transform && velocity) {
|
||||
transform.x += velocity.vx;
|
||||
transform.y += velocity.vy;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Define scene
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "GameScene";
|
||||
|
||||
// Add systems
|
||||
this.addSystem(new MovementSystem());
|
||||
|
||||
// Create player
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Transform(400, 300));
|
||||
player.addComponent(new Velocity(0, 0));
|
||||
player.addComponent(new Health(100));
|
||||
|
||||
// Create enemies
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const enemy = this.createEntity(`Enemy_${i}`);
|
||||
enemy.addComponent(new Transform(
|
||||
Math.random() * 800,
|
||||
Math.random() * 600
|
||||
));
|
||||
enemy.addComponent(new Velocity(
|
||||
Math.random() * 100 - 50,
|
||||
Math.random() * 100 - 50
|
||||
));
|
||||
enemy.addComponent(new Health(50));
|
||||
}
|
||||
|
||||
// Setup event listeners
|
||||
this.eventSystem.on('player_died', () => {
|
||||
console.log('Player died!');
|
||||
});
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log('Game scene started');
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
console.log('Game scene unloaded');
|
||||
this.eventSystem.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Use scene
|
||||
import { Core, SceneManager } from '@esengine/ecs-framework';
|
||||
|
||||
Core.create({ debug: true });
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
sceneManager.setScene(new GameScene());
|
||||
```
|
||||
|
||||
## Design Principles
|
||||
|
||||
| Principle | Description |
|
||||
|-----------|-------------|
|
||||
| Single Responsibility | Each scene handles one game state |
|
||||
| Resource Cleanup | Clean up all resources in `unload()` |
|
||||
| System Order | Add systems: Input → Logic → Render |
|
||||
| Event Decoupling | Use event system for scene communication |
|
||||
| Layered Initialization | Config → Systems → Entities → Events |
|
||||
124
docs/src/content/docs/en/guide/scene/debugging.md
Normal file
124
docs/src/content/docs/en/guide/scene/debugging.md
Normal file
@@ -0,0 +1,124 @@
|
||||
---
|
||||
title: "Debugging & Monitoring"
|
||||
description: "Scene statistics, performance monitoring and debugging"
|
||||
---
|
||||
|
||||
Scene includes complete debugging and performance monitoring features.
|
||||
|
||||
## Scene Statistics
|
||||
|
||||
```typescript
|
||||
class StatsScene extends Scene {
|
||||
public showStats(): void {
|
||||
const stats = this.getStats();
|
||||
console.log(`Entity count: ${stats.entityCount}`);
|
||||
console.log(`System count: ${stats.processorCount}`);
|
||||
console.log('Component storage stats:', stats.componentStorageStats);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Debug Information
|
||||
|
||||
```typescript
|
||||
public showDebugInfo(): void {
|
||||
const debugInfo = this.getDebugInfo();
|
||||
console.log('Scene debug info:', debugInfo);
|
||||
|
||||
// Display all entity info
|
||||
debugInfo.entities.forEach(entity => {
|
||||
console.log(`Entity ${entity.name}(${entity.id}): ${entity.componentCount} components`);
|
||||
console.log('Component types:', entity.componentTypes);
|
||||
});
|
||||
|
||||
// Display all system info
|
||||
debugInfo.processors.forEach(processor => {
|
||||
console.log(`System ${processor.name}: processing ${processor.entityCount} entities`);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Monitoring
|
||||
|
||||
```typescript
|
||||
class PerformanceScene extends Scene {
|
||||
public showPerformance(): void {
|
||||
// Get performance data
|
||||
const perfData = this.performanceMonitor?.getPerformanceData();
|
||||
if (perfData) {
|
||||
console.log('FPS:', perfData.fps);
|
||||
console.log('Frame time:', perfData.frameTime);
|
||||
console.log('Entity update time:', perfData.entityUpdateTime);
|
||||
console.log('System update time:', perfData.systemUpdateTime);
|
||||
}
|
||||
|
||||
// Get performance report
|
||||
const report = this.performanceMonitor?.generateReport();
|
||||
if (report) {
|
||||
console.log('Performance report:', report);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### getStats()
|
||||
|
||||
Returns scene statistics:
|
||||
|
||||
```typescript
|
||||
interface SceneStats {
|
||||
entityCount: number;
|
||||
processorCount: number;
|
||||
componentStorageStats: ComponentStorageStats;
|
||||
}
|
||||
```
|
||||
|
||||
### getDebugInfo()
|
||||
|
||||
Returns detailed debug information:
|
||||
|
||||
```typescript
|
||||
interface DebugInfo {
|
||||
entities: EntityDebugInfo[];
|
||||
processors: ProcessorDebugInfo[];
|
||||
}
|
||||
|
||||
interface EntityDebugInfo {
|
||||
id: number;
|
||||
name: string;
|
||||
componentCount: number;
|
||||
componentTypes: string[];
|
||||
}
|
||||
|
||||
interface ProcessorDebugInfo {
|
||||
name: string;
|
||||
entityCount: number;
|
||||
}
|
||||
```
|
||||
|
||||
### performanceMonitor
|
||||
|
||||
Performance monitor interface:
|
||||
|
||||
```typescript
|
||||
interface PerformanceMonitor {
|
||||
getPerformanceData(): PerformanceData;
|
||||
generateReport(): string;
|
||||
}
|
||||
|
||||
interface PerformanceData {
|
||||
fps: number;
|
||||
frameTime: number;
|
||||
entityUpdateTime: number;
|
||||
systemUpdateTime: number;
|
||||
}
|
||||
```
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
1. **Debug mode** - Enable with `Core.create({ debug: true })`
|
||||
2. **Performance analysis** - Call `getStats()` periodically
|
||||
3. **Memory monitoring** - Check `componentStorageStats` for issues
|
||||
4. **System performance** - Use `performanceMonitor` to identify slow systems
|
||||
125
docs/src/content/docs/en/guide/scene/entity-management.md
Normal file
125
docs/src/content/docs/en/guide/scene/entity-management.md
Normal file
@@ -0,0 +1,125 @@
|
||||
---
|
||||
title: "Entity Management"
|
||||
description: "Entity creation, finding and destruction in scenes"
|
||||
---
|
||||
|
||||
Scene provides complete entity management APIs for creating, finding, and destroying entities.
|
||||
|
||||
## Creating Entities
|
||||
|
||||
### Single Entity
|
||||
|
||||
```typescript
|
||||
class EntityScene extends Scene {
|
||||
createGameEntities(): void {
|
||||
// Create named entity
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Position(400, 300));
|
||||
player.addComponent(new Health(100));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Batch Creation
|
||||
|
||||
```typescript
|
||||
class EntityScene extends Scene {
|
||||
createBullets(): void {
|
||||
// Batch create entities (high performance)
|
||||
const bullets = this.createEntities(100, "Bullet");
|
||||
|
||||
// Add components to batch-created entities
|
||||
bullets.forEach((bullet, index) => {
|
||||
bullet.addComponent(new Position(index * 10, 100));
|
||||
bullet.addComponent(new Velocity(Math.random() * 200 - 100, -300));
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Finding Entities
|
||||
|
||||
### By Name
|
||||
|
||||
```typescript
|
||||
// Find by name (returns first match)
|
||||
const player = this.findEntity("Player");
|
||||
const player2 = this.getEntityByName("Player"); // Alias
|
||||
|
||||
if (player) {
|
||||
console.log(`Found player: ${player.name}`);
|
||||
}
|
||||
```
|
||||
|
||||
### By ID
|
||||
|
||||
```typescript
|
||||
// Find by unique ID
|
||||
const entity = this.findEntityById(123);
|
||||
|
||||
if (entity) {
|
||||
console.log(`Found entity: ${entity.id}`);
|
||||
}
|
||||
```
|
||||
|
||||
### By Tag
|
||||
|
||||
```typescript
|
||||
// Find by tag (returns array)
|
||||
const enemies = this.findEntitiesByTag(2);
|
||||
const enemies2 = this.getEntitiesByTag(2); // Alias
|
||||
|
||||
console.log(`Found ${enemies.length} enemies`);
|
||||
```
|
||||
|
||||
## Destroying Entities
|
||||
|
||||
### Single Entity
|
||||
|
||||
```typescript
|
||||
const enemy = this.findEntity("Enemy_1");
|
||||
if (enemy) {
|
||||
enemy.destroy(); // Entity is automatically removed from scene
|
||||
}
|
||||
```
|
||||
|
||||
### All Entities
|
||||
|
||||
```typescript
|
||||
// Destroy all entities in scene
|
||||
this.destroyAllEntities();
|
||||
```
|
||||
|
||||
## Entity Queries
|
||||
|
||||
Scene provides a component query system:
|
||||
|
||||
```typescript
|
||||
class QueryScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Create test entities
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const entity = this.createEntity(`Entity_${i}`);
|
||||
entity.addComponent(new Transform(i * 10, 0));
|
||||
entity.addComponent(new Velocity(1, 0));
|
||||
}
|
||||
}
|
||||
|
||||
public queryEntities(): void {
|
||||
// Query through QuerySystem
|
||||
const entities = this.querySystem.query([Transform, Velocity]);
|
||||
console.log(`Found ${entities.length} entities with Transform and Velocity`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Method | Returns | Description |
|
||||
|--------|---------|-------------|
|
||||
| `createEntity(name)` | `Entity` | Create single entity |
|
||||
| `createEntities(count, prefix)` | `Entity[]` | Batch create entities |
|
||||
| `findEntity(name)` | `Entity \| undefined` | Find by name |
|
||||
| `findEntityById(id)` | `Entity \| undefined` | Find by ID |
|
||||
| `findEntitiesByTag(tag)` | `Entity[]` | Find by tag |
|
||||
| `destroyAllEntities()` | `void` | Destroy all entities |
|
||||
122
docs/src/content/docs/en/guide/scene/events.md
Normal file
122
docs/src/content/docs/en/guide/scene/events.md
Normal file
@@ -0,0 +1,122 @@
|
||||
---
|
||||
title: "Event System"
|
||||
description: "Scene's built-in type-safe event system"
|
||||
---
|
||||
|
||||
Scene includes a built-in type-safe event system for decoupled communication within scenes.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Listening to Events
|
||||
|
||||
```typescript
|
||||
class EventScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Listen to events
|
||||
this.eventSystem.on('player_died', this.onPlayerDied.bind(this));
|
||||
this.eventSystem.on('enemy_spawned', this.onEnemySpawned.bind(this));
|
||||
this.eventSystem.on('level_complete', this.onLevelComplete.bind(this));
|
||||
}
|
||||
|
||||
private onPlayerDied(data: any): void {
|
||||
console.log('Player died event');
|
||||
}
|
||||
|
||||
private onEnemySpawned(data: any): void {
|
||||
console.log('Enemy spawned event');
|
||||
}
|
||||
|
||||
private onLevelComplete(data: any): void {
|
||||
console.log('Level complete event');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Sending Events
|
||||
|
||||
```typescript
|
||||
public triggerGameEvent(): void {
|
||||
// Send event (synchronous)
|
||||
this.eventSystem.emitSync('custom_event', {
|
||||
message: "This is a custom event",
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
// Send event (asynchronous)
|
||||
this.eventSystem.emit('async_event', {
|
||||
data: "Async event data"
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `on(event, callback)` | Listen to event |
|
||||
| `once(event, callback)` | Listen once (auto-unsubscribe) |
|
||||
| `off(event, callback)` | Stop listening |
|
||||
| `emitSync(event, data)` | Send event (synchronous) |
|
||||
| `emit(event, data)` | Send event (asynchronous) |
|
||||
| `clear()` | Clear all event listeners |
|
||||
|
||||
## Event Handling Patterns
|
||||
|
||||
### Centralized Event Management
|
||||
|
||||
```typescript
|
||||
class EventHandlingScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
private setupEventListeners(): void {
|
||||
this.eventSystem.on('game_pause', this.onGamePause.bind(this));
|
||||
this.eventSystem.on('game_resume', this.onGameResume.bind(this));
|
||||
this.eventSystem.on('player_input', this.onPlayerInput.bind(this));
|
||||
}
|
||||
|
||||
private onGamePause(): void {
|
||||
// Pause game logic
|
||||
this.systems.forEach(system => {
|
||||
if (system instanceof GameLogicSystem) {
|
||||
system.enabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private onGameResume(): void {
|
||||
// Resume game logic
|
||||
this.systems.forEach(system => {
|
||||
if (system instanceof GameLogicSystem) {
|
||||
system.enabled = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private onPlayerInput(data: any): void {
|
||||
// Handle player input
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Cleanup Event Listeners
|
||||
|
||||
Clean up event listeners on scene unload to avoid memory leaks:
|
||||
|
||||
```typescript
|
||||
public unload(): void {
|
||||
// Clear all event listeners
|
||||
this.eventSystem.clear();
|
||||
}
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
| Category | Example Events |
|
||||
|----------|----------------|
|
||||
| Game State | `game_start`, `game_pause`, `game_over` |
|
||||
| Player Actions | `player_died`, `player_jump`, `player_attack` |
|
||||
| Enemy Actions | `enemy_spawned`, `enemy_killed` |
|
||||
| Level Progress | `level_start`, `level_complete` |
|
||||
| UI Interaction | `button_click`, `menu_open` |
|
||||
140
docs/src/content/docs/en/guide/scene/index.md
Normal file
140
docs/src/content/docs/en/guide/scene/index.md
Normal file
@@ -0,0 +1,140 @@
|
||||
---
|
||||
title: "Scene"
|
||||
description: "Core container of ECS framework, managing entity, system and component lifecycles"
|
||||
---
|
||||
|
||||
In the ECS architecture, a Scene is a container for the game world, responsible for managing the lifecycle of entities, systems, and components.
|
||||
|
||||
## Core Features
|
||||
|
||||
Scene is the core container of the ECS framework, providing:
|
||||
- Entity creation, management, and destruction
|
||||
- System registration and execution scheduling
|
||||
- Component storage and querying
|
||||
- Event system support
|
||||
- Performance monitoring and debugging information
|
||||
|
||||
## Scene Management Options
|
||||
|
||||
ECS Framework provides two scene management approaches:
|
||||
|
||||
| Manager | Use Case | Features |
|
||||
|---------|----------|----------|
|
||||
| **SceneManager** | 95% of games | Lightweight, scene transitions |
|
||||
| **WorldManager** | MMO servers, room systems | Multi-World, full isolation |
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Inherit Scene Class
|
||||
|
||||
```typescript
|
||||
import { Scene, EntitySystem } from '@esengine/ecs-framework';
|
||||
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "GameScene";
|
||||
|
||||
// Add systems
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
|
||||
// Create initial entities
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Position(400, 300));
|
||||
player.addComponent(new Health(100));
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log("Game scene started");
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
console.log("Game scene unloaded");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using Scene Configuration
|
||||
|
||||
```typescript
|
||||
import { ISceneConfig } from '@esengine/ecs-framework';
|
||||
|
||||
const config: ISceneConfig = {
|
||||
name: "MainGame",
|
||||
enableEntityDirectUpdate: false
|
||||
};
|
||||
|
||||
class ConfiguredScene extends Scene {
|
||||
constructor() {
|
||||
super(config);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Runtime Environment
|
||||
|
||||
For networked games, you can configure the runtime environment to distinguish between server and client logic.
|
||||
|
||||
### Global Configuration (Recommended)
|
||||
|
||||
Set the runtime environment once at the Core level - all Scenes will inherit this setting:
|
||||
|
||||
```typescript
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
// Method 1: Set in Core.create()
|
||||
Core.create({ runtimeEnvironment: 'server' });
|
||||
|
||||
// Method 2: Set static property directly
|
||||
Core.runtimeEnvironment = 'server';
|
||||
```
|
||||
|
||||
### Per-Scene Override
|
||||
|
||||
Individual scenes can override the global setting:
|
||||
|
||||
```typescript
|
||||
const clientScene = new Scene({ runtimeEnvironment: 'client' });
|
||||
```
|
||||
|
||||
### Environment Types
|
||||
|
||||
| Environment | Use Case |
|
||||
|-------------|----------|
|
||||
| `'standalone'` | Single-player games (default) |
|
||||
| `'server'` | Game server, authoritative logic |
|
||||
| `'client'` | Game client, rendering/input |
|
||||
|
||||
### Checking Environment in Systems
|
||||
|
||||
```typescript
|
||||
class CollectibleSpawnSystem extends EntitySystem {
|
||||
private checkCollections(): void {
|
||||
// Skip on client - only server handles authoritative logic
|
||||
if (!this.scene.isServer) return;
|
||||
|
||||
// Server-authoritative spawn logic...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
See [System Runtime Decorators](/en/guide/system/index#runtime-environment-decorators) for decorator-based approach.
|
||||
|
||||
### Running a Scene
|
||||
|
||||
```typescript
|
||||
import { Core, SceneManager } from '@esengine/ecs-framework';
|
||||
|
||||
Core.create({ debug: true });
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
sceneManager.setScene(new GameScene());
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Lifecycle](./lifecycle/) - Scene lifecycle methods
|
||||
- [Entity Management](./entity-management/) - Create, find, destroy entities
|
||||
- [System Management](./system-management/) - System control
|
||||
- [Events](./events/) - Scene event communication
|
||||
- [Debugging](./debugging/) - Performance and debugging
|
||||
- [Best Practices](./best-practices/) - Scene design patterns
|
||||
103
docs/src/content/docs/en/guide/scene/lifecycle.md
Normal file
103
docs/src/content/docs/en/guide/scene/lifecycle.md
Normal file
@@ -0,0 +1,103 @@
|
||||
---
|
||||
title: "Scene Lifecycle"
|
||||
description: "Scene lifecycle methods and execution order"
|
||||
---
|
||||
|
||||
Scene provides complete lifecycle management for proper resource initialization and cleanup.
|
||||
|
||||
## Lifecycle Methods
|
||||
|
||||
```typescript
|
||||
class ExampleScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 1. Scene initialization: setup systems and initial entities
|
||||
console.log("Scene initializing");
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
// 2. Scene starts running: game logic begins execution
|
||||
console.log("Scene starting");
|
||||
}
|
||||
|
||||
public update(deltaTime: number): void {
|
||||
// 3. Per-frame update (called by scene manager)
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// 4. Scene unloading: cleanup resources
|
||||
console.log("Scene unloading");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Execution Order
|
||||
|
||||
| Phase | Method | Description |
|
||||
|-------|--------|-------------|
|
||||
| Initialize | `initialize()` | Setup systems and initial entities |
|
||||
| Start | `begin()` / `onStart()` | Scene starts running |
|
||||
| Update | `update()` | Per-frame update (auto-called) |
|
||||
| End | `end()` / `unload()` | Cleanup resources |
|
||||
|
||||
## Lifecycle Example
|
||||
|
||||
```typescript
|
||||
class GameScene extends Scene {
|
||||
private resourcesLoaded = false;
|
||||
|
||||
protected initialize(): void {
|
||||
this.name = "GameScene";
|
||||
|
||||
// 1. Add systems (by dependency order)
|
||||
this.addSystem(new InputSystem());
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
|
||||
// 2. Create initial entities
|
||||
this.createPlayer();
|
||||
this.createEnemies();
|
||||
|
||||
// 3. Setup event listeners
|
||||
this.setupEvents();
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
this.resourcesLoaded = true;
|
||||
console.log("Scene resources loaded, game starting");
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// Cleanup event listeners
|
||||
this.eventSystem.clear();
|
||||
|
||||
// Cleanup other resources
|
||||
this.resourcesLoaded = false;
|
||||
console.log("Scene resources cleaned up");
|
||||
}
|
||||
|
||||
private createPlayer(): void {
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Position(400, 300));
|
||||
}
|
||||
|
||||
private createEnemies(): void {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const enemy = this.createEntity(`Enemy_${i}`);
|
||||
enemy.addComponent(new Position(Math.random() * 800, Math.random() * 600));
|
||||
}
|
||||
}
|
||||
|
||||
private setupEvents(): void {
|
||||
this.eventSystem.on('player_died', () => {
|
||||
console.log('Player died');
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
1. **initialize() called once** - For initial state setup
|
||||
2. **onStart() on scene activation** - May be called multiple times
|
||||
3. **unload() must cleanup resources** - Avoid memory leaks
|
||||
4. **update() managed by framework** - No manual calls needed
|
||||
115
docs/src/content/docs/en/guide/scene/system-management.md
Normal file
115
docs/src/content/docs/en/guide/scene/system-management.md
Normal file
@@ -0,0 +1,115 @@
|
||||
---
|
||||
title: "System Management"
|
||||
description: "System addition, removal and control in scenes"
|
||||
---
|
||||
|
||||
Scene manages system registration, execution order, and lifecycle.
|
||||
|
||||
## Adding Systems
|
||||
|
||||
```typescript
|
||||
class SystemScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Add system
|
||||
const movementSystem = new MovementSystem();
|
||||
this.addSystem(movementSystem);
|
||||
|
||||
// Set system update order (lower runs first)
|
||||
movementSystem.updateOrder = 1;
|
||||
|
||||
// Add more systems
|
||||
this.addSystem(new PhysicsSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Getting Systems
|
||||
|
||||
```typescript
|
||||
// Get system of specific type
|
||||
const physicsSystem = this.getEntityProcessor(PhysicsSystem);
|
||||
|
||||
if (physicsSystem) {
|
||||
console.log("Found physics system");
|
||||
}
|
||||
```
|
||||
|
||||
## Removing Systems
|
||||
|
||||
```typescript
|
||||
public removeUnnecessarySystems(): void {
|
||||
const physicsSystem = this.getEntityProcessor(PhysicsSystem);
|
||||
|
||||
if (physicsSystem) {
|
||||
this.removeSystem(physicsSystem);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Controlling Systems
|
||||
|
||||
### Enable/Disable Systems
|
||||
|
||||
```typescript
|
||||
public pausePhysics(): void {
|
||||
const physicsSystem = this.getEntityProcessor(PhysicsSystem);
|
||||
if (physicsSystem) {
|
||||
physicsSystem.enabled = false; // Disable system
|
||||
}
|
||||
}
|
||||
|
||||
public resumePhysics(): void {
|
||||
const physicsSystem = this.getEntityProcessor(PhysicsSystem);
|
||||
if (physicsSystem) {
|
||||
physicsSystem.enabled = true; // Enable system
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Get All Systems
|
||||
|
||||
```typescript
|
||||
public getAllSystems(): EntitySystem[] {
|
||||
return this.systems; // Get all registered systems
|
||||
}
|
||||
```
|
||||
|
||||
## System Organization Best Practice
|
||||
|
||||
Group systems by function:
|
||||
|
||||
```typescript
|
||||
class OrganizedScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Add systems by function and dependencies
|
||||
this.addInputSystems();
|
||||
this.addLogicSystems();
|
||||
this.addRenderSystems();
|
||||
}
|
||||
|
||||
private addInputSystems(): void {
|
||||
this.addSystem(new InputSystem());
|
||||
}
|
||||
|
||||
private addLogicSystems(): void {
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new PhysicsSystem());
|
||||
this.addSystem(new CollisionSystem());
|
||||
}
|
||||
|
||||
private addRenderSystems(): void {
|
||||
this.addSystem(new RenderSystem());
|
||||
this.addSystem(new UISystem());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Method | Returns | Description |
|
||||
|--------|---------|-------------|
|
||||
| `addSystem(system)` | `void` | Add system to scene |
|
||||
| `removeSystem(system)` | `void` | Remove system from scene |
|
||||
| `getEntityProcessor(Type)` | `T \| undefined` | Get system by type |
|
||||
| `systems` | `EntitySystem[]` | Get all systems |
|
||||
165
docs/src/content/docs/en/guide/serialization/decorators.md
Normal file
165
docs/src/content/docs/en/guide/serialization/decorators.md
Normal file
@@ -0,0 +1,165 @@
|
||||
---
|
||||
title: "Decorators & Inheritance"
|
||||
description: "Advanced serialization decorator usage and component inheritance"
|
||||
---
|
||||
|
||||
## Advanced Decorators
|
||||
|
||||
### Field Serialization Options
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Advanced')
|
||||
@Serializable({ version: 1 })
|
||||
class AdvancedComponent extends Component {
|
||||
// Use alias
|
||||
@Serialize({ alias: 'playerName' })
|
||||
public name: string = '';
|
||||
|
||||
// Custom serializer
|
||||
@Serialize({
|
||||
serializer: (value: Date) => value.toISOString(),
|
||||
deserializer: (value: string) => new Date(value)
|
||||
})
|
||||
public createdAt: Date = new Date();
|
||||
|
||||
// Ignore serialization
|
||||
@IgnoreSerialization()
|
||||
public cachedData: any = null;
|
||||
}
|
||||
```
|
||||
|
||||
### Collection Type Serialization
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Collections')
|
||||
@Serializable({ version: 1 })
|
||||
class CollectionsComponent extends Component {
|
||||
// Map serialization
|
||||
@SerializeAsMap()
|
||||
public inventory: Map<string, number> = new Map();
|
||||
|
||||
// Set serialization
|
||||
@SerializeAsSet()
|
||||
public acquiredSkills: Set<string> = new Set();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.inventory.set('gold', 100);
|
||||
this.inventory.set('silver', 50);
|
||||
this.acquiredSkills.add('attack');
|
||||
this.acquiredSkills.add('defense');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Component Inheritance and Serialization
|
||||
|
||||
The framework fully supports component class inheritance. Subclasses automatically inherit parent class serialization fields while adding their own.
|
||||
|
||||
### Basic Inheritance
|
||||
|
||||
```typescript
|
||||
// Base component
|
||||
@ECSComponent('Collider2DBase')
|
||||
@Serializable({ version: 1, typeId: 'Collider2DBase' })
|
||||
abstract class Collider2DBase extends Component {
|
||||
@Serialize()
|
||||
public friction: number = 0.5;
|
||||
|
||||
@Serialize()
|
||||
public restitution: number = 0.0;
|
||||
|
||||
@Serialize()
|
||||
public isTrigger: boolean = false;
|
||||
}
|
||||
|
||||
// Subclass component - automatically inherits parent's serialization fields
|
||||
@ECSComponent('BoxCollider2D')
|
||||
@Serializable({ version: 1, typeId: 'BoxCollider2D' })
|
||||
class BoxCollider2DComponent extends Collider2DBase {
|
||||
@Serialize()
|
||||
public width: number = 1.0;
|
||||
|
||||
@Serialize()
|
||||
public height: number = 1.0;
|
||||
}
|
||||
|
||||
// Another subclass component
|
||||
@ECSComponent('CircleCollider2D')
|
||||
@Serializable({ version: 1, typeId: 'CircleCollider2D' })
|
||||
class CircleCollider2DComponent extends Collider2DBase {
|
||||
@Serialize()
|
||||
public radius: number = 0.5;
|
||||
}
|
||||
```
|
||||
|
||||
### Inheritance Rules
|
||||
|
||||
1. **Field Inheritance**: Subclasses automatically inherit all `@Serialize()` marked fields from parent
|
||||
2. **Independent Metadata**: Each subclass maintains independent serialization metadata; modifying subclass doesn't affect parent or other subclasses
|
||||
3. **typeId Distinction**: Use `typeId` option to specify unique identifier for each class, ensuring correct component type recognition during deserialization
|
||||
|
||||
### Importance of Using typeId
|
||||
|
||||
When using component inheritance, it's **strongly recommended** to set a unique `typeId` for each class:
|
||||
|
||||
```typescript
|
||||
// ✅ Recommended: Explicitly specify typeId
|
||||
@Serializable({ version: 1, typeId: 'BoxCollider2D' })
|
||||
class BoxCollider2DComponent extends Collider2DBase { }
|
||||
|
||||
@Serializable({ version: 1, typeId: 'CircleCollider2D' })
|
||||
class CircleCollider2DComponent extends Collider2DBase { }
|
||||
|
||||
// ⚠️ Not recommended: Relying on class name as typeId
|
||||
// Class names may change after code minification, causing deserialization failure
|
||||
@Serializable({ version: 1 })
|
||||
class BoxCollider2DComponent extends Collider2DBase { }
|
||||
```
|
||||
|
||||
### Subclass Overriding Parent Fields
|
||||
|
||||
Subclasses can redeclare parent fields to modify their serialization options:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('SpecialCollider')
|
||||
@Serializable({ version: 1, typeId: 'SpecialCollider' })
|
||||
class SpecialColliderComponent extends Collider2DBase {
|
||||
// Override parent field with different alias
|
||||
@Serialize({ alias: 'fric' })
|
||||
public override friction: number = 0.8;
|
||||
|
||||
@Serialize()
|
||||
public specialProperty: string = '';
|
||||
}
|
||||
```
|
||||
|
||||
### Ignoring Inherited Fields
|
||||
|
||||
Use `@IgnoreSerialization()` to ignore fields inherited from parent in subclass:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('TriggerOnly')
|
||||
@Serializable({ version: 1, typeId: 'TriggerOnly' })
|
||||
class TriggerOnlyCollider extends Collider2DBase {
|
||||
// Ignore parent's friction and restitution fields
|
||||
// Because Trigger doesn't need physics material properties
|
||||
@IgnoreSerialization()
|
||||
public override friction: number = 0;
|
||||
|
||||
@IgnoreSerialization()
|
||||
public override restitution: number = 0;
|
||||
}
|
||||
```
|
||||
|
||||
## Decorator Reference
|
||||
|
||||
| Decorator | Description |
|
||||
|-----------|-------------|
|
||||
| `@Serializable({ version, typeId })` | Mark component as serializable |
|
||||
| `@Serialize()` | Mark field as serializable |
|
||||
| `@Serialize({ alias })` | Serialize field with alias |
|
||||
| `@Serialize({ serializer, deserializer })` | Custom serialization logic |
|
||||
| `@SerializeAsMap()` | Serialize Map type |
|
||||
| `@SerializeAsSet()` | Serialize Set type |
|
||||
| `@IgnoreSerialization()` | Ignore field serialization |
|
||||
228
docs/src/content/docs/en/guide/serialization/incremental.md
Normal file
228
docs/src/content/docs/en/guide/serialization/incremental.md
Normal file
@@ -0,0 +1,228 @@
|
||||
---
|
||||
title: "Incremental Serialization"
|
||||
description: "Only serialize changes in the scene"
|
||||
---
|
||||
|
||||
Incremental serialization only saves the changed parts of a scene, suitable for network synchronization, undo/redo, time rewinding, and other scenarios requiring frequent state saving.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### 1. Create Base Snapshot
|
||||
|
||||
```typescript
|
||||
// Create base snapshot before starting to record changes
|
||||
scene.createIncrementalSnapshot();
|
||||
```
|
||||
|
||||
### 2. Modify Scene
|
||||
|
||||
```typescript
|
||||
// Add entity
|
||||
const enemy = scene.createEntity('Enemy');
|
||||
enemy.addComponent(new PositionComponent(100, 200));
|
||||
enemy.addComponent(new HealthComponent(50));
|
||||
|
||||
// Modify component
|
||||
const player = scene.findEntity('Player');
|
||||
const pos = player.getComponent(PositionComponent);
|
||||
pos.x = 300;
|
||||
pos.y = 400;
|
||||
|
||||
// Remove component
|
||||
player.removeComponentByType(BuffComponent);
|
||||
|
||||
// Delete entity
|
||||
const oldEntity = scene.findEntity('ToDelete');
|
||||
oldEntity.destroy();
|
||||
|
||||
// Modify scene data
|
||||
scene.sceneData.set('score', 1000);
|
||||
```
|
||||
|
||||
### 3. Get Incremental Changes
|
||||
|
||||
```typescript
|
||||
// Get all changes relative to base snapshot
|
||||
const incremental = scene.serializeIncremental();
|
||||
|
||||
// View change statistics
|
||||
const stats = IncrementalSerializer.getIncrementalStats(incremental);
|
||||
console.log('Total changes:', stats.totalChanges);
|
||||
console.log('Added entities:', stats.addedEntities);
|
||||
console.log('Removed entities:', stats.removedEntities);
|
||||
console.log('Added components:', stats.addedComponents);
|
||||
console.log('Updated components:', stats.updatedComponents);
|
||||
```
|
||||
|
||||
### 4. Serialize Incremental Data
|
||||
|
||||
```typescript
|
||||
// JSON format (default)
|
||||
const jsonData = IncrementalSerializer.serializeIncremental(incremental, {
|
||||
format: 'json'
|
||||
});
|
||||
|
||||
// Binary format (smaller size, higher performance)
|
||||
const binaryData = IncrementalSerializer.serializeIncremental(incremental, {
|
||||
format: 'binary'
|
||||
});
|
||||
|
||||
// Pretty print JSON output (for debugging)
|
||||
const prettyJson = IncrementalSerializer.serializeIncremental(incremental, {
|
||||
format: 'json',
|
||||
pretty: true
|
||||
});
|
||||
|
||||
// Send or save
|
||||
socket.send(binaryData); // Use binary for network transmission
|
||||
localStorage.setItem('changes', jsonData); // JSON for local storage
|
||||
```
|
||||
|
||||
### 5. Apply Incremental Changes
|
||||
|
||||
```typescript
|
||||
// Apply changes to another scene
|
||||
const otherScene = new Scene();
|
||||
|
||||
// Directly apply incremental object
|
||||
otherScene.applyIncremental(incremental);
|
||||
|
||||
// Apply from JSON string
|
||||
const jsonData = IncrementalSerializer.serializeIncremental(incremental, { format: 'json' });
|
||||
otherScene.applyIncremental(jsonData);
|
||||
|
||||
// Apply from binary Uint8Array
|
||||
const binaryData = IncrementalSerializer.serializeIncremental(incremental, { format: 'binary' });
|
||||
otherScene.applyIncremental(binaryData);
|
||||
```
|
||||
|
||||
## Incremental Snapshot Management
|
||||
|
||||
### Update Snapshot Base
|
||||
|
||||
After applying incremental changes, you can update the snapshot base:
|
||||
|
||||
```typescript
|
||||
// Create initial snapshot
|
||||
scene.createIncrementalSnapshot();
|
||||
|
||||
// First modification
|
||||
entity.addComponent(new VelocityComponent(5, 0));
|
||||
const incremental1 = scene.serializeIncremental();
|
||||
|
||||
// Update base (set current state as new base)
|
||||
scene.updateIncrementalSnapshot();
|
||||
|
||||
// Second modification (incremental will be based on updated base)
|
||||
entity.getComponent(VelocityComponent).dx = 10;
|
||||
const incremental2 = scene.serializeIncremental();
|
||||
```
|
||||
|
||||
### Clear Snapshot
|
||||
|
||||
```typescript
|
||||
// Release memory used by snapshot
|
||||
scene.clearIncrementalSnapshot();
|
||||
|
||||
// Check if snapshot exists
|
||||
if (scene.hasIncrementalSnapshot()) {
|
||||
console.log('Incremental snapshot exists');
|
||||
}
|
||||
```
|
||||
|
||||
## Incremental Serialization Options
|
||||
|
||||
```typescript
|
||||
interface IncrementalSerializationOptions {
|
||||
// Whether to perform deep comparison of component data
|
||||
// Default true, set to false to improve performance but may miss internal field changes
|
||||
deepComponentComparison?: boolean;
|
||||
|
||||
// Whether to track scene data changes
|
||||
// Default true
|
||||
trackSceneData?: boolean;
|
||||
|
||||
// Whether to compress snapshot (using JSON serialization)
|
||||
// Default false
|
||||
compressSnapshot?: boolean;
|
||||
|
||||
// Serialization format
|
||||
// 'json': JSON format (readable, convenient for debugging)
|
||||
// 'binary': MessagePack binary format (smaller, higher performance)
|
||||
// Default 'json'
|
||||
format?: 'json' | 'binary';
|
||||
|
||||
// Whether to pretty print JSON output (only effective when format='json')
|
||||
// Default false
|
||||
pretty?: boolean;
|
||||
}
|
||||
|
||||
// Using options
|
||||
scene.createIncrementalSnapshot({
|
||||
deepComponentComparison: true,
|
||||
trackSceneData: true
|
||||
});
|
||||
```
|
||||
|
||||
## Incremental Data Structure
|
||||
|
||||
Incremental snapshots contain the following change types:
|
||||
|
||||
```typescript
|
||||
interface IncrementalSnapshot {
|
||||
version: number; // Snapshot version number
|
||||
timestamp: number; // Timestamp
|
||||
sceneName: string; // Scene name
|
||||
baseVersion: number; // Base version number
|
||||
entityChanges: EntityChange[]; // Entity changes
|
||||
componentChanges: ComponentChange[]; // Component changes
|
||||
sceneDataChanges: SceneDataChange[]; // Scene data changes
|
||||
}
|
||||
|
||||
// Change operation types
|
||||
enum ChangeOperation {
|
||||
EntityAdded = 'entity_added',
|
||||
EntityRemoved = 'entity_removed',
|
||||
EntityUpdated = 'entity_updated',
|
||||
ComponentAdded = 'component_added',
|
||||
ComponentRemoved = 'component_removed',
|
||||
ComponentUpdated = 'component_updated',
|
||||
SceneDataUpdated = 'scene_data_updated'
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### For High-Frequency Sync
|
||||
|
||||
```typescript
|
||||
// Disable deep comparison to improve performance
|
||||
scene.createIncrementalSnapshot({
|
||||
deepComponentComparison: false // Only detect component addition/removal
|
||||
});
|
||||
```
|
||||
|
||||
### Batch Operations
|
||||
|
||||
```typescript
|
||||
// Batch modify then serialize
|
||||
scene.entities.buffer.forEach(entity => {
|
||||
// Batch modifications
|
||||
});
|
||||
|
||||
// Serialize all changes at once
|
||||
const incremental = scene.serializeIncremental();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `scene.createIncrementalSnapshot(options?)` | Create base snapshot |
|
||||
| `scene.serializeIncremental()` | Get incremental changes |
|
||||
| `scene.applyIncremental(data)` | Apply incremental changes |
|
||||
| `scene.updateIncrementalSnapshot()` | Update snapshot base |
|
||||
| `scene.clearIncrementalSnapshot()` | Clear snapshot |
|
||||
| `scene.hasIncrementalSnapshot()` | Check if snapshot exists |
|
||||
| `IncrementalSerializer.getIncrementalStats(snapshot)` | Get change statistics |
|
||||
| `IncrementalSerializer.serializeIncremental(snapshot, options)` | Serialize incremental data |
|
||||
170
docs/src/content/docs/en/guide/serialization/index.md
Normal file
170
docs/src/content/docs/en/guide/serialization/index.md
Normal file
@@ -0,0 +1,170 @@
|
||||
---
|
||||
title: "Serialization System"
|
||||
description: "ECS serialization system overview and full serialization"
|
||||
---
|
||||
|
||||
The serialization system provides a complete solution for persisting scene, entity, and component data. It supports both full serialization and incremental serialization modes, suitable for game saves, network synchronization, scene editors, time rewinding, and more.
|
||||
|
||||
## Basic Concepts
|
||||
|
||||
The serialization system has two layers:
|
||||
|
||||
- **Full Serialization**: Serializes the complete scene state, including all entities, components, and scene data
|
||||
- **Incremental Serialization**: Only serializes changes relative to a base snapshot, greatly reducing data size
|
||||
|
||||
### Supported Data Formats
|
||||
|
||||
- **JSON Format**: Human-readable, convenient for debugging and editing
|
||||
- **Binary Format**: Uses MessagePack, smaller size and better performance
|
||||
|
||||
> **v2.2.2 Important Change**
|
||||
>
|
||||
> Starting from v2.2.2, binary serialization returns `Uint8Array` instead of Node.js `Buffer` to ensure browser compatibility:
|
||||
> - `serialize({ format: 'binary' })` returns `string | Uint8Array` (was `string | Buffer`)
|
||||
> - `deserialize(data)` accepts `string | Uint8Array` (was `string | Buffer`)
|
||||
> - `applyIncremental(data)` accepts `IncrementalSnapshot | string | Uint8Array` (was including `Buffer`)
|
||||
>
|
||||
> **Migration Impact**:
|
||||
> - **Runtime Compatible**: Node.js `Buffer` inherits from `Uint8Array`, existing code works directly
|
||||
> - **Type Checking**: If your TypeScript code explicitly uses `Buffer` type, change to `Uint8Array`
|
||||
> - **Browser Support**: `Uint8Array` is a standard JavaScript type supported by all modern browsers
|
||||
|
||||
## Full Serialization
|
||||
|
||||
### Basic Usage
|
||||
|
||||
#### 1. Mark Serializable Components
|
||||
|
||||
Use `@Serializable` and `@Serialize` decorators to mark components and fields for serialization:
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent, Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSComponent('Player')
|
||||
@Serializable({ version: 1 })
|
||||
class PlayerComponent extends Component {
|
||||
@Serialize()
|
||||
public name: string = '';
|
||||
|
||||
@Serialize()
|
||||
public level: number = 1;
|
||||
|
||||
@Serialize()
|
||||
public experience: number = 0;
|
||||
|
||||
@Serialize()
|
||||
public position: { x: number; y: number } = { x: 0, y: 0 };
|
||||
|
||||
// Fields without @Serialize() won't be serialized
|
||||
private tempData: any = null;
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Serialize Scene
|
||||
|
||||
```typescript
|
||||
// JSON format serialization
|
||||
const jsonData = scene.serialize({
|
||||
format: 'json',
|
||||
pretty: true // Pretty print output
|
||||
});
|
||||
|
||||
// Save to local storage
|
||||
localStorage.setItem('gameSave', jsonData);
|
||||
|
||||
// Binary format serialization (smaller size)
|
||||
const binaryData = scene.serialize({
|
||||
format: 'binary'
|
||||
});
|
||||
|
||||
// Save to file (Node.js environment)
|
||||
// Note: binaryData is Uint8Array type, Node.js fs can write it directly
|
||||
fs.writeFileSync('save.bin', binaryData);
|
||||
```
|
||||
|
||||
#### 3. Deserialize Scene
|
||||
|
||||
```typescript
|
||||
// Restore from JSON
|
||||
const saveData = localStorage.getItem('gameSave');
|
||||
if (saveData) {
|
||||
scene.deserialize(saveData, {
|
||||
strategy: 'replace' // Replace current scene content
|
||||
});
|
||||
}
|
||||
|
||||
// Restore from Binary
|
||||
const binaryData = fs.readFileSync('save.bin');
|
||||
scene.deserialize(binaryData, {
|
||||
strategy: 'merge' // Merge into existing scene
|
||||
});
|
||||
```
|
||||
|
||||
### Serialization Options
|
||||
|
||||
#### SerializationOptions
|
||||
|
||||
```typescript
|
||||
interface SceneSerializationOptions {
|
||||
// Component types to serialize (optional)
|
||||
components?: ComponentType[];
|
||||
|
||||
// Serialization format: 'json' or 'binary'
|
||||
format?: 'json' | 'binary';
|
||||
|
||||
// Pretty print JSON output
|
||||
pretty?: boolean;
|
||||
|
||||
// Include metadata
|
||||
includeMetadata?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```typescript
|
||||
// Only serialize specific component types
|
||||
const saveData = scene.serialize({
|
||||
format: 'json',
|
||||
components: [PlayerComponent, InventoryComponent],
|
||||
pretty: true,
|
||||
includeMetadata: true
|
||||
});
|
||||
```
|
||||
|
||||
#### DeserializationOptions
|
||||
|
||||
```typescript
|
||||
interface SceneDeserializationOptions {
|
||||
// Deserialization strategy
|
||||
strategy?: 'merge' | 'replace';
|
||||
|
||||
// Component type registry (optional, uses global registry by default)
|
||||
componentRegistry?: Map<string, ComponentType>;
|
||||
}
|
||||
```
|
||||
|
||||
### Scene Custom Data
|
||||
|
||||
Besides entities and components, you can also serialize scene-level configuration data:
|
||||
|
||||
```typescript
|
||||
// Set scene data
|
||||
scene.sceneData.set('weather', 'rainy');
|
||||
scene.sceneData.set('difficulty', 'hard');
|
||||
scene.sceneData.set('checkpoint', { x: 100, y: 200 });
|
||||
|
||||
// Scene data is automatically included when serializing
|
||||
const saveData = scene.serialize({ format: 'json' });
|
||||
|
||||
// Scene data is restored after deserialization
|
||||
scene.deserialize(saveData);
|
||||
console.log(scene.sceneData.get('weather')); // 'rainy'
|
||||
```
|
||||
|
||||
## More Topics
|
||||
|
||||
- [Decorators & Inheritance](/en/guide/serialization/decorators) - Advanced decorator usage and component inheritance
|
||||
- [Incremental Serialization](/en/guide/serialization/incremental) - Only serialize changes
|
||||
- [Version Migration](/en/guide/serialization/migration) - Handle data structure changes
|
||||
- [Use Cases](/en/guide/serialization/use-cases) - Save system, network sync, undo/redo examples
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user