Compare commits
239 Commits
v2.1.19
...
issue-79-批
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf14b59a28 | ||
|
|
0a0f64510f | ||
|
|
9445c735c3 | ||
|
|
7339e7ecec | ||
|
|
79f7c89e23 | ||
|
|
e724e5a1ba | ||
|
|
fdaa94a61d | ||
|
|
6af0074c36 | ||
|
|
97a69fed09 | ||
|
|
959879440d | ||
|
|
fd1bbb0e00 | ||
|
|
072e68cf43 | ||
|
|
610232e6b0 | ||
|
|
69c46f32eb | ||
|
|
06b3f92007 | ||
|
|
c631290049 | ||
|
|
f41c1a3ca3 | ||
|
|
bd6ba84087 | ||
|
|
1512409eb3 | ||
|
|
bcb5feeb1c | ||
|
|
da8b7cf601 | ||
|
|
316527c459 | ||
|
|
da70818b22 | ||
|
|
5ea3b72b2b | ||
|
|
632864b361 | ||
|
|
952247def0 | ||
|
|
51debede52 | ||
|
|
ce7b731bcf | ||
|
|
86e2dc8fdb | ||
|
|
78047134c2 | ||
|
|
125a1686ab | ||
|
|
d542ac48b8 | ||
|
|
1ac0227c90 | ||
|
|
a5e70bcd99 | ||
|
|
38763de7f6 | ||
|
|
db73b077c5 | ||
|
|
0969d09da1 | ||
|
|
a07108a431 | ||
|
|
6693b56ab8 | ||
|
|
a7349bd360 | ||
|
|
e92c0040b5 | ||
|
|
f448fa48c4 | ||
|
|
aa33cad4fa | ||
|
|
d0cb7d5359 | ||
|
|
90153b98fe | ||
|
|
8c4e8d523e | ||
|
|
90ad4b3ec4 | ||
|
|
62bc6b547e | ||
|
|
be11060674 | ||
|
|
d62bf9f7f9 | ||
|
|
61fcd52c65 | ||
|
|
2947ddeb64 | ||
|
|
d9b752c180 | ||
|
|
b82891caee | ||
|
|
05f04ef37e | ||
|
|
66dc9780b9 | ||
|
|
d48b22c656 | ||
|
|
727b1864eb | ||
|
|
de3bfd7551 | ||
|
|
dedb91379f | ||
|
|
1dfcd008aa | ||
|
|
cf2dc91af6 | ||
|
|
a66f80a766 | ||
|
|
f4e49c316e | ||
|
|
d1cd72bbb2 | ||
|
|
6178851def | ||
|
|
945f772c30 | ||
|
|
b546c9c712 | ||
|
|
413ce93b31 | ||
|
|
cffe32911d | ||
|
|
4f651eb42e | ||
|
|
6da1585b6b | ||
|
|
b988e81a1b | ||
|
|
1a1c1087d2 | ||
|
|
1a1549230f | ||
|
|
64ea53eba1 | ||
|
|
5e052a7e7d | ||
|
|
cf9ea495d0 | ||
|
|
9603c6423b | ||
|
|
457eef585e | ||
|
|
1ade449c4d | ||
|
|
aa9d73a810 | ||
|
|
cc266a7ba9 | ||
|
|
d8ea324018 | ||
|
|
60566e8d78 | ||
|
|
306d2994dc | ||
|
|
e6a8791fc3 | ||
|
|
6cbbc06998 | ||
|
|
0b4244fd8e | ||
|
|
367ddfbf8a | ||
|
|
168e028098 | ||
|
|
042ded37d2 | ||
|
|
4137eb2bce | ||
|
|
20a3f03e12 | ||
|
|
7792710694 | ||
|
|
dbddbbdfb8 | ||
|
|
4869f5741e | ||
|
|
bda547dd2e | ||
|
|
ef80b03a44 | ||
|
|
6e511ae949 | ||
|
|
94541d0abb | ||
|
|
586a0e5d14 | ||
|
|
814842dbaf | ||
|
|
70a993573f | ||
|
|
21659cbb13 | ||
|
|
a44251cc55 | ||
|
|
69616bbddc | ||
|
|
0a1d7ac083 | ||
|
|
364bc4cdab | ||
|
|
2504eb24e1 | ||
|
|
bdbef0bd0d | ||
|
|
e4e38ee4e6 | ||
|
|
021e892e33 | ||
|
|
c27d5022fd | ||
|
|
6730a5d625 | ||
|
|
32092f992d | ||
|
|
a5f0c8f6b5 | ||
|
|
85cd93e51a | ||
|
|
0b7e623748 | ||
|
|
62f250b43c | ||
|
|
25136349ff | ||
|
|
baeb047e27 | ||
|
|
56dd18b983 | ||
|
|
86cb70a94f | ||
|
|
9f76d37a82 | ||
|
|
a026ed9428 | ||
|
|
c178e2fbcc | ||
|
|
b88bb1dc87 | ||
|
|
3069e28224 | ||
|
|
d69b3af99b | ||
|
|
7398b7c6d0 | ||
|
|
5d57904d22 | ||
|
|
7daf352a25 | ||
|
|
6a49f6a534 | ||
|
|
5bce08683a | ||
|
|
edc60fc3d8 | ||
|
|
1361fd8a90 | ||
|
|
d539bb3dd9 | ||
|
|
3b9ae4f384 | ||
|
|
2783448de5 | ||
|
|
6e21ff08d5 | ||
|
|
e56278e4a6 | ||
|
|
fc9bf816dd | ||
|
|
854fd7df3a | ||
|
|
87dd564a12 | ||
|
|
2d389308ea | ||
|
|
ea8523be35 | ||
|
|
4479f0fab0 | ||
|
|
7a000318a6 | ||
|
|
9a08ae74b6 | ||
|
|
f3d2950df3 | ||
|
|
8cfba4a166 | ||
|
|
51e6bba2a7 | ||
|
|
ccbfa78070 | ||
|
|
69655f1936 | ||
|
|
6ea366cfed | ||
|
|
b7d17fb16d | ||
|
|
f3dc8c6344 | ||
|
|
69ec545854 | ||
|
|
65386ff731 | ||
|
|
01fa33e122 | ||
|
|
0411aa9aef | ||
|
|
4a5c890121 | ||
|
|
4c11fdc176 | ||
|
|
d99e7a45ea | ||
|
|
52528ff1b7 | ||
|
|
4a9317f3f4 | ||
|
|
9450dd5869 | ||
|
|
d5471e4828 | ||
|
|
2f71785add | ||
|
|
608f5030b2 | ||
|
|
dd8f3714ed | ||
|
|
abec2b3648 | ||
|
|
ea06a9f07d | ||
|
|
9f54759cc5 | ||
|
|
55dd5f9ed0 | ||
|
|
05455421fb | ||
|
|
af61067f08 | ||
|
|
19cda88248 | ||
|
|
0edb2738a1 | ||
|
|
e1bc364525 | ||
|
|
2925ee380d | ||
|
|
731edf5872 | ||
|
|
7b85039b17 | ||
|
|
2bc45fa574 | ||
|
|
d2b4455205 | ||
|
|
bce4a26197 | ||
|
|
1da5040d60 | ||
|
|
afd33e053b | ||
|
|
171d03c006 | ||
|
|
34d5237aaa | ||
|
|
037c3d6a05 | ||
|
|
5596ba634e | ||
|
|
a5f69065f4 | ||
|
|
969ef249ea | ||
|
|
a37183851f | ||
|
|
4cf3e1a769 | ||
|
|
354e5a2761 | ||
|
|
c9fd8cc2a7 | ||
|
|
bb19f752a1 | ||
|
|
6bd9c1055c | ||
|
|
dff77097c6 | ||
|
|
b4dc1c5661 | ||
|
|
992338d924 | ||
|
|
f88a402b0c | ||
|
|
5938d36149 | ||
|
|
78577db3f9 | ||
|
|
0b4a6b77e2 | ||
|
|
01084a8897 | ||
|
|
0f18a1979e | ||
|
|
68a615bc7b | ||
|
|
add1068c1a | ||
|
|
7a40df9965 | ||
|
|
3e6a1aa59a | ||
|
|
d3fe79cf39 | ||
|
|
48fa547c8f | ||
|
|
80e2f7df71 | ||
|
|
0107f1f58a | ||
|
|
d29c9a96f4 | ||
|
|
37d75c3281 | ||
|
|
666ded7b89 | ||
|
|
73a882f75e | ||
|
|
310f5f2349 | ||
|
|
8c86d6b696 | ||
|
|
82cd163adc | ||
|
|
802ee25621 | ||
|
|
f48ebb65ba | ||
|
|
aaa2a8ed2c | ||
|
|
5a06f5420b | ||
|
|
343f5a44f2 | ||
|
|
92125aee3a | ||
|
|
96f651b7ca | ||
|
|
06ea01e928 | ||
|
|
577f1e429a | ||
|
|
7808f64fe5 | ||
|
|
e6789e49e4 | ||
|
|
797619aece | ||
|
|
1b5363611d | ||
|
|
103f773286 |
84
.github/workflows/ci.yml
vendored
Normal file
84
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master, main, develop ]
|
||||
paths:
|
||||
- 'packages/**'
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
- 'tsconfig.json'
|
||||
- 'jest.config.*'
|
||||
- '.github/workflows/ci.yml'
|
||||
pull_request:
|
||||
branches: [ master, main, develop ]
|
||||
paths:
|
||||
- 'packages/**'
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
- 'tsconfig.json'
|
||||
- 'jest.config.*'
|
||||
- '.github/workflows/ci.yml'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build core package first
|
||||
run: npm run build:core
|
||||
|
||||
- name: Run tests with coverage
|
||||
run: npm run test:ci
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
file: ./coverage/lcov.info
|
||||
flags: unittests
|
||||
name: codecov-umbrella
|
||||
fail_ci_if_error: false
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build project
|
||||
run: npm run build
|
||||
|
||||
- name: Build npm package
|
||||
run: npm run build:npm
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-artifacts
|
||||
path: |
|
||||
bin/
|
||||
dist/
|
||||
retention-days: 7
|
||||
67
.github/workflows/docs.yml
vendored
Normal file
67
.github/workflows/docs.yml
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
name: Deploy Documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- 'packages/**'
|
||||
- 'typedoc.json'
|
||||
- 'package.json'
|
||||
- '.github/workflows/docs.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: pages
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build core package
|
||||
run: npm run build:core
|
||||
|
||||
- name: Generate API documentation
|
||||
run: npm run docs:api
|
||||
|
||||
- name: Build documentation
|
||||
run: npm run docs:build
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: docs/.vitepress/dist
|
||||
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -55,6 +55,8 @@ pnpm-lock.yaml
|
||||
# 文档生成
|
||||
docs/api/
|
||||
docs/build/
|
||||
docs/.vitepress/cache/
|
||||
docs/.vitepress/dist/
|
||||
|
||||
# 备份文件
|
||||
*.bak
|
||||
|
||||
33
.gitmodules
vendored
Normal file
33
.gitmodules
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
[submodule "thirdparty/BehaviourTree-ai"]
|
||||
path = thirdparty/BehaviourTree-ai
|
||||
url = https://github.com/esengine/BehaviourTree-ai.git
|
||||
[submodule "thirdparty/admin-backend"]
|
||||
path = thirdparty/admin-backend
|
||||
url = https://github.com/esengine/admin-backend.git
|
||||
[submodule "extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension"]
|
||||
path = extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension
|
||||
url = https://github.com/esengine/cocos-ecs-extension.git
|
||||
[submodule "extensions/cocos/cocos-ecs/extensions/behaviour-tree"]
|
||||
path = extensions/cocos/cocos-ecs/extensions/behaviour-tree
|
||||
url = https://github.com/esengine/behaviour-tree.git
|
||||
[submodule "extensions/cocos/cocos-ecs/extensions/cocos-terrain-gen"]
|
||||
path = extensions/cocos/cocos-ecs/extensions/cocos-terrain-gen
|
||||
url = https://github.com/esengine/cocos-terrain-gen.git
|
||||
[submodule "extensions/cocos/cocos-ecs/extensions/mvvm-designer"]
|
||||
path = extensions/cocos/cocos-ecs/extensions/mvvm-designer
|
||||
url = https://github.com/esengine/mvvm-designer.git
|
||||
[submodule "thirdparty/mvvm-ui-framework"]
|
||||
path = thirdparty/mvvm-ui-framework
|
||||
url = https://github.com/esengine/mvvm-ui-framework.git
|
||||
[submodule "thirdparty/cocos-nexus"]
|
||||
path = thirdparty/cocos-nexus
|
||||
url = https://github.com/esengine/cocos-nexus.git
|
||||
[submodule "extensions/cocos/cocos-ecs/extensions/utilityai_designer"]
|
||||
path = extensions/cocos/cocos-ecs/extensions/utilityai_designer
|
||||
url = https://github.com/esengine/utilityai_designer.git
|
||||
[submodule "thirdparty/ecs-astar"]
|
||||
path = thirdparty/ecs-astar
|
||||
url = https://github.com/esengine/ecs-astar.git
|
||||
[submodule "examples/lawn-mower-demo"]
|
||||
path = examples/lawn-mower-demo
|
||||
url = https://github.com/esengine/lawn-mower-demo.git
|
||||
425
README.md
425
README.md
@@ -1,22 +1,21 @@
|
||||
# ECS Framework
|
||||
|
||||
[](https://github.com/esengine/ecs-framework/actions)
|
||||
[](https://badge.fury.io/js/%40esengine%2Fecs-framework)
|
||||
[](https://www.typescriptlang.org/)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://github.com/esengine/ecs-framework/stargazers)
|
||||
|
||||
TypeScript ECS (Entity-Component-System) 框架,专为游戏开发设计。
|
||||
|
||||
> 🤔 **什么是 ECS?** 不熟悉 ECS 架构?建议先阅读 [ECS 架构基础](docs/concepts-explained.md#ecs-架构基础) 了解核心概念
|
||||
一个高性能的 TypeScript ECS (Entity-Component-System) 框架,专为现代游戏开发而设计。
|
||||
|
||||
## 特性
|
||||
|
||||
- 🔧 **完整的 TypeScript 支持** - 强类型检查和代码提示
|
||||
- 📡 **[类型安全事件系统](docs/concepts-explained.md#事件系统)** - 事件装饰器和异步事件处理
|
||||
- 🔍 **[查询系统](docs/concepts-explained.md#实体管理)** - 流式 API 和智能缓存
|
||||
- ⚡ **[性能优化](docs/concepts-explained.md#性能优化技术)** - 组件索引、Archetype 系统、脏标记
|
||||
- 🎯 **[实体管理器](docs/concepts-explained.md#实体管理)** - 统一的实体生命周期管理
|
||||
- 🧰 **调试工具** - 内置性能监控和调试信息
|
||||
|
||||
> 📖 **不熟悉这些概念?** 查看我们的 [技术概念详解](docs/concepts-explained.md) 了解它们的作用和应用场景
|
||||
- **高性能** - 针对大规模实体优化,支持SoA存储和批量处理
|
||||
- **多线程计算** - Worker系统支持真正的并行处理,充分利用多核CPU性能
|
||||
- **类型安全** - 完整的TypeScript支持,编译时类型检查
|
||||
- **现代架构** - 支持多World、多Scene的分层架构设计
|
||||
- **开发友好** - 内置调试工具和性能监控
|
||||
- **跨平台** - 支持Cocos Creator、Laya引擎和Web平台
|
||||
|
||||
## 安装
|
||||
|
||||
@@ -26,391 +25,103 @@ npm install @esengine/ecs-framework
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 基础设置
|
||||
|
||||
```typescript
|
||||
import { Core, Scene, Entity, Component, EntitySystem } from '@esengine/ecs-framework';
|
||||
import { Core, Scene, Component, EntitySystem, ECSComponent, ECSSystem, Matcher, Time } from '@esengine/ecs-framework';
|
||||
|
||||
// 创建核心实例
|
||||
const core = Core.create(true); // 调试模式
|
||||
|
||||
// 创建场景
|
||||
const scene = new Scene();
|
||||
Core.scene = scene;
|
||||
```
|
||||
|
||||
### 定义组件
|
||||
|
||||
```typescript
|
||||
class PositionComponent extends Component {
|
||||
constructor(public x: number = 0, public y: number = 0) {
|
||||
// 定义组件
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
constructor(public x = 0, public y = 0) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
class VelocityComponent extends Component {
|
||||
constructor(public dx: number = 0, public dy: number = 0) {
|
||||
@ECSComponent('Velocity')
|
||||
class Velocity extends Component {
|
||||
constructor(public dx = 0, public dy = 0) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
class HealthComponent extends Component {
|
||||
constructor(
|
||||
public maxHealth: number = 100,
|
||||
public currentHealth: number = 100
|
||||
) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 创建实体
|
||||
|
||||
```typescript
|
||||
// 基础实体创建
|
||||
const player = scene.createEntity("Player");
|
||||
player.addComponent(new PositionComponent(100, 100));
|
||||
player.addComponent(new VelocityComponent(5, 0));
|
||||
player.addComponent(new HealthComponent(100, 100));
|
||||
|
||||
// 批量创建实体
|
||||
const enemies = scene.createEntities(50, "Enemy");
|
||||
```
|
||||
|
||||
### 创建系统
|
||||
|
||||
```typescript
|
||||
// 创建系统
|
||||
@ECSSystem('Movement')
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super();
|
||||
super(Matcher.all(Position, Velocity));
|
||||
}
|
||||
|
||||
public process(entities: Entity[]) {
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const position = entity.getComponent(PositionComponent);
|
||||
const velocity = entity.getComponent(VelocityComponent);
|
||||
|
||||
if (position && velocity) {
|
||||
position.x += velocity.dx;
|
||||
position.y += velocity.dy;
|
||||
}
|
||||
const position = entity.getComponent(Position)!;
|
||||
const velocity = entity.getComponent(Velocity)!;
|
||||
|
||||
position.x += velocity.dx * Time.deltaTime;
|
||||
position.y += velocity.dy * Time.deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加系统到场景
|
||||
scene.addEntityProcessor(new MovementSystem());
|
||||
```
|
||||
// 创建场景并启动
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.addSystem(new MovementSystem());
|
||||
|
||||
### 游戏循环
|
||||
|
||||
ECS框架需要在游戏引擎的更新循环中调用:
|
||||
|
||||
```typescript
|
||||
// 统一的API:传入deltaTime
|
||||
Core.update(deltaTime);
|
||||
```
|
||||
|
||||
**不同平台的集成示例:**
|
||||
|
||||
```typescript
|
||||
// Laya引擎
|
||||
Laya.timer.frameLoop(1, this, () => {
|
||||
const deltaTime = Laya.timer.delta / 1000; // 转换为秒
|
||||
Core.update(deltaTime);
|
||||
});
|
||||
|
||||
// Cocos Creator
|
||||
update(deltaTime: number) {
|
||||
Core.update(deltaTime);
|
||||
}
|
||||
|
||||
// 原生浏览器环境
|
||||
let lastTime = 0;
|
||||
function gameLoop(currentTime: number) {
|
||||
const deltaTime = lastTime > 0 ? (currentTime - lastTime) / 1000 : 0.016;
|
||||
lastTime = currentTime;
|
||||
Core.update(deltaTime);
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
requestAnimationFrame(gameLoop);
|
||||
```
|
||||
|
||||
## 实体管理器
|
||||
|
||||
EntityManager 提供了统一的实体管理接口:
|
||||
|
||||
```typescript
|
||||
import { EntityManager } from '@esengine/ecs-framework';
|
||||
|
||||
const entityManager = new EntityManager();
|
||||
|
||||
// 流式查询 API
|
||||
const results = entityManager
|
||||
.query()
|
||||
.withAll(PositionComponent, VelocityComponent)
|
||||
.withNone(HealthComponent)
|
||||
.withTag(1)
|
||||
.execute();
|
||||
|
||||
// 批量操作(使用Scene的方法)
|
||||
const bullets = scene.createEntities(100, "bullet");
|
||||
|
||||
// 按标签查询
|
||||
const enemies = entityManager.getEntitiesByTag(2);
|
||||
```
|
||||
|
||||
## 事件系统
|
||||
|
||||
### [基础事件](docs/concepts-explained.md#类型安全事件)
|
||||
|
||||
类型安全的事件系统,编译时检查事件名和数据类型。
|
||||
|
||||
```typescript
|
||||
import { EventBus, ECSEventType } from '@esengine/ecs-framework';
|
||||
|
||||
const eventBus = entityManager.eventBus;
|
||||
|
||||
// 监听预定义事件
|
||||
eventBus.onEntityCreated((data) => {
|
||||
console.log(`实体创建: ${data.entityName}`);
|
||||
});
|
||||
|
||||
eventBus.onComponentAdded((data) => {
|
||||
console.log(`组件添加: ${data.componentType}`);
|
||||
});
|
||||
|
||||
// 自定义事件
|
||||
eventBus.emit('player:death', { playerId: 123, reason: 'fall' });
|
||||
```
|
||||
|
||||
### [事件装饰器](docs/concepts-explained.md#事件装饰器)
|
||||
|
||||
使用装饰器语法自动注册事件监听器,减少样板代码。
|
||||
|
||||
```typescript
|
||||
import { EventHandler, ECSEventType } from '@esengine/ecs-framework';
|
||||
|
||||
class GameSystem {
|
||||
@EventHandler(ECSEventType.ENTITY_DESTROYED)
|
||||
onEntityDestroyed(data: EntityDestroyedEventData) {
|
||||
console.log('实体销毁:', data.entityName);
|
||||
}
|
||||
|
||||
@EventHandler('player:levelup')
|
||||
onPlayerLevelUp(data: { playerId: number; newLevel: number }) {
|
||||
console.log(`玩家 ${data.playerId} 升级到 ${data.newLevel} 级`);
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Position(100, 100));
|
||||
player.addComponent(new Velocity(50, 0));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
// 启动游戏
|
||||
Core.create();
|
||||
Core.setScene(new GameScene());
|
||||
|
||||
### [组件索引](docs/concepts-explained.md#组件索引系统)
|
||||
|
||||
通过建立索引避免线性搜索,将查询复杂度从 O(n) 降低到 O(1)。
|
||||
|
||||
```typescript
|
||||
// 使用Scene的查询系统进行组件索引
|
||||
const querySystem = scene.querySystem;
|
||||
|
||||
// 查询具有特定组件的实体
|
||||
const entitiesWithPosition = querySystem.queryAll(PositionComponent).entities;
|
||||
const entitiesWithVelocity = querySystem.queryAll(VelocityComponent).entities;
|
||||
|
||||
// 性能统计
|
||||
const stats = querySystem.getStats();
|
||||
console.log('查询效率:', stats.hitRate);
|
||||
```
|
||||
|
||||
**索引类型选择:**
|
||||
- **哈希索引** - 适合稳定的、大量的组件(如位置、生命值)
|
||||
- **位图索引** - 适合频繁变化的组件(如Buff、状态)
|
||||
|
||||
> 📋 详细选择指南参见 [索引类型选择指南](docs/concepts-explained.md#索引类型选择指南)
|
||||
|
||||
### [Archetype 系统](docs/concepts-explained.md#archetype-系统)
|
||||
|
||||
将具有相同组件组合的实体分组,减少查询时的组件检查开销。
|
||||
|
||||
```typescript
|
||||
// 使用查询系统的Archetype功能
|
||||
const querySystem = scene.querySystem;
|
||||
|
||||
// 查询统计
|
||||
const stats = querySystem.getStats();
|
||||
console.log('缓存命中率:', stats.hitRate);
|
||||
```
|
||||
|
||||
### [脏标记系统](docs/concepts-explained.md#脏标记系统)
|
||||
|
||||
追踪数据变化,只处理发生改变的实体,避免不必要的计算。
|
||||
|
||||
```typescript
|
||||
// 脏标记通过组件系统自动管理
|
||||
// 组件变化时会自动标记为脏数据
|
||||
|
||||
// 查询系统会自动处理脏标记优化
|
||||
const movingEntities = scene.querySystem.queryAll(PositionComponent, VelocityComponent);
|
||||
```
|
||||
|
||||
> 💡 **不确定何时使用这些优化?** 查看 [性能优化建议](docs/concepts-explained.md#性能建议) 了解适用场景
|
||||
|
||||
## API 参考
|
||||
|
||||
### 核心类
|
||||
|
||||
| 类 | 描述 |
|
||||
|---|---|
|
||||
| `Core` | 框架核心管理类 |
|
||||
| `Scene` | 场景容器,管理实体和系统 |
|
||||
| `Entity` | 实体对象,包含组件集合 |
|
||||
| `Component` | 组件基类 |
|
||||
| `EntitySystem` | 系统基类 |
|
||||
| `EntityManager` | 实体管理器 |
|
||||
|
||||
### 查询 API
|
||||
|
||||
```typescript
|
||||
entityManager
|
||||
.query()
|
||||
.withAll(...components) // 包含所有指定组件
|
||||
.withAny(...components) // 包含任意指定组件
|
||||
.withNone(...components) // 不包含指定组件
|
||||
.withTag(tag) // 包含指定标签
|
||||
.withoutTag(tag) // 不包含指定标签
|
||||
.execute() // 执行查询
|
||||
```
|
||||
|
||||
### 事件类型
|
||||
|
||||
```typescript
|
||||
enum ECSEventType {
|
||||
ENTITY_CREATED = 'entity:created',
|
||||
ENTITY_DESTROYED = 'entity:destroyed',
|
||||
COMPONENT_ADDED = 'component:added',
|
||||
COMPONENT_REMOVED = 'component:removed',
|
||||
SYSTEM_ADDED = 'system:added',
|
||||
SYSTEM_REMOVED = 'system:removed'
|
||||
// 游戏循环中更新
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime);
|
||||
}
|
||||
```
|
||||
|
||||
## 与其他框架对比
|
||||
## 核心特性
|
||||
|
||||
| 特性 | @esengine/ecs-framework | bitECS | Miniplex |
|
||||
|------|-------------------------|--------|----------|
|
||||
| TypeScript 支持 | ✅ 原生支持 | ✅ 完整支持 | ✅ 原生支持 |
|
||||
| 事件系统 | ✅ 内置+装饰器 | ❌ 需自己实现 | ✅ 响应式 |
|
||||
| 查询系统 | ✅ 流式 API | ✅ 函数式 | ✅ 响应式 |
|
||||
| 实体管理器 | ✅ 统一接口 | ❌ 低级 API | ✅ 高级接口 |
|
||||
| 性能优化 | ✅ 多重优化 | ✅ 极致性能 | ✅ React 优化 |
|
||||
| JavaScript引擎集成 | ✅ 专为JS引擎设计 | ✅ 通用设计 | ⚠️ 主要 React |
|
||||
- **实体查询** - 使用 Matcher API 进行高效的实体过滤
|
||||
- **事件系统** - 类型安全的事件发布/订阅机制
|
||||
- **性能优化** - SoA 存储优化,支持大规模实体处理
|
||||
- **多线程支持** - Worker系统实现真正的并行计算,充分利用多核CPU
|
||||
- **多场景** - 支持 World/Scene 分层架构
|
||||
- **时间管理** - 内置定时器和时间控制系统
|
||||
|
||||
**选择指南:**
|
||||
- 选择本框架:需要完整的游戏开发工具链和中文社区支持
|
||||
- 选择 bitECS:需要极致性能和最小化设计
|
||||
- 选择 Miniplex:主要用于 React 应用开发
|
||||
## 平台支持
|
||||
|
||||
## 项目结构
|
||||
支持主流游戏引擎和 Web 平台:
|
||||
|
||||
```
|
||||
ecs-framework/
|
||||
├── src/
|
||||
│ ├── ECS/ # ECS 核心系统
|
||||
│ │ ├── Core/ # 核心管理器
|
||||
│ │ ├── Systems/ # 系统类型
|
||||
│ │ └── Utils/ # ECS 工具
|
||||
│ ├── Types/ # TypeScript接口定义
|
||||
│ └── Utils/ # 通用工具
|
||||
├── docs/ # 文档
|
||||
└── scripts/ # 构建脚本
|
||||
```
|
||||
- **Cocos Creator** - 内置引擎集成支持,提供[专用调试插件](https://store.cocos.com/app/detail/7823)
|
||||
- **Laya 引擎** - 完整的生命周期管理
|
||||
- **原生 Web** - 浏览器环境直接运行
|
||||
- **小游戏平台** - 微信、支付宝等小游戏
|
||||
|
||||
|
||||
## 示例项目
|
||||
|
||||
- [Worker系统演示](https://esengine.github.io/ecs-framework/demos/worker-system/) - 多线程物理系统演示,展示高性能并行计算
|
||||
- [割草机演示](https://github.com/esengine/lawn-mower-demo) - 完整的游戏示例
|
||||
|
||||
## 文档
|
||||
|
||||
### 🎯 新手入门
|
||||
- **[📖 新手教程完整指南](docs/beginner-tutorials.md)** - 完整学习路径,从零开始 ⭐ **强烈推荐**
|
||||
- **[🚀 快速入门](docs/getting-started.md)** - 详细的入门教程,包含Laya/Cocos/Node.js集成指南 ⭐ **平台集成必读**
|
||||
- [🧠 技术概念详解](docs/concepts-explained.md) - 通俗易懂的技术概念解释 ⭐ **推荐新手阅读**
|
||||
- [🎯 位掩码使用指南](docs/bitmask-guide.md) - 位掩码概念、原理和高级使用技巧
|
||||
- [💡 使用场景示例](docs/use-cases.md) - 不同类型游戏的具体应用案例
|
||||
- [🔧 框架类型系统](docs/concepts-explained.md#框架类型系统) - TypeScript接口设计和使用指南
|
||||
- [快速入门](https://esengine.github.io/ecs-framework/guide/getting-started.html) - 详细教程和平台集成
|
||||
- [完整指南](https://esengine.github.io/ecs-framework/guide/) - ECS 概念和使用指南
|
||||
- [API 参考](https://esengine.github.io/ecs-framework/api/) - 完整 API 文档
|
||||
|
||||
### 📚 核心功能
|
||||
- [🎭 实体管理指南](docs/entity-guide.md) - 实体的创建和使用方法
|
||||
- [🧩 组件设计指南](docs/component-design-guide.md) - 如何设计高质量组件 ⭐ **设计必读**
|
||||
- [⚙️ 系统详解指南](docs/system-guide.md) - 四种系统类型的详细使用
|
||||
- [🎬 场景管理指南](docs/scene-management-guide.md) - 场景切换和数据管理
|
||||
- [⏰ 定时器系统指南](docs/timer-guide.md) - 定时器的完整使用方法
|
||||
## 生态系统
|
||||
|
||||
### API 参考
|
||||
- [核心 API 参考](docs/core-concepts.md) - 完整的 API 使用说明
|
||||
- [实体基础指南](docs/entity-guide.md) - 实体的基本概念和操作
|
||||
- [EntityManager 指南](docs/entity-manager-example.md) - 高性能查询和批量操作
|
||||
- [事件系统指南](docs/event-system-example.md) - 事件系统完整用法
|
||||
- [查询系统指南](docs/query-system-usage.md) - 查询系统使用方法
|
||||
|
||||
### 性能相关
|
||||
- [性能优化指南](docs/performance-optimization.md) - 性能优化技术和策略
|
||||
|
||||
## 构建
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# 构建项目
|
||||
npm run build
|
||||
|
||||
# 监听模式
|
||||
npm run build:watch
|
||||
|
||||
# 清理构建文件
|
||||
npm run clean
|
||||
|
||||
# 重新构建
|
||||
npm run rebuild
|
||||
```
|
||||
|
||||
## 性能监控
|
||||
|
||||
框架提供内置性能统计:
|
||||
|
||||
```typescript
|
||||
// 场景统计
|
||||
const sceneStats = scene.getStats();
|
||||
console.log('性能统计:', {
|
||||
实体数量: sceneStats.entityCount,
|
||||
系统数量: sceneStats.processorCount
|
||||
});
|
||||
|
||||
// 查询系统统计
|
||||
const queryStats = scene.querySystem.getStats();
|
||||
console.log('查询统计:', {
|
||||
缓存命中率: queryStats.hitRate + '%',
|
||||
查询次数: queryStats.queryCount
|
||||
});
|
||||
```
|
||||
|
||||
## 扩展库
|
||||
|
||||
- [路径寻找库](https://github.com/esengine/ecs-astar) - A*、BFS、Dijkstra 算法
|
||||
- [路径寻找](https://github.com/esengine/ecs-astar) - A*、BFS、Dijkstra 算法
|
||||
- [AI 系统](https://github.com/esengine/BehaviourTree-ai) - 行为树、效用 AI
|
||||
|
||||
## 社区
|
||||
## 社区与支持
|
||||
|
||||
- QQ 群:[ecs游戏框架交流](https://jq.qq.com/?_wv=1027&k=29w1Nud6)
|
||||
- GitHub:[提交 Issue](https://github.com/esengine/ecs-framework/issues)
|
||||
|
||||
## 贡献
|
||||
|
||||
欢迎提交 Pull Request 和 Issue!
|
||||
|
||||
### 开发要求
|
||||
|
||||
- Node.js >= 14.0.0
|
||||
- TypeScript >= 4.0.0
|
||||
- [问题反馈](https://github.com/esengine/ecs-framework/issues) - Bug 报告和功能建议
|
||||
- [QQ 交流群](https://jq.qq.com/?_wv=1027&k=29w1Nud6) - ecs游戏框架交流
|
||||
|
||||
## 许可证
|
||||
|
||||
[MIT](LICENSE)
|
||||
[MIT](LICENSE) © 2025 ECS Framework
|
||||
209
docs/.vitepress/config.mjs
Normal file
209
docs/.vitepress/config.mjs
Normal file
@@ -0,0 +1,209 @@
|
||||
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/core/package.json'), 'utf-8')
|
||||
)
|
||||
|
||||
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: 'ECS Framework',
|
||||
description: '高性能TypeScript ECS框架 - 为游戏开发而生',
|
||||
lang: 'zh-CN',
|
||||
|
||||
themeConfig: {
|
||||
nav: [
|
||||
{ text: '首页', link: '/' },
|
||||
{ text: '快速开始', link: '/guide/getting-started' },
|
||||
{ text: '指南', link: '/guide/' },
|
||||
{ text: 'API', link: '/api/README' },
|
||||
{
|
||||
text: '示例',
|
||||
items: [
|
||||
{ text: 'Worker系统演示', link: '/examples/worker-system-demo' },
|
||||
{ text: '割草机演示', link: 'https://github.com/esengine/lawn-mower-demo' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: `v${corePackageJson.version}`,
|
||||
link: 'https://github.com/esengine/ecs-framework/releases'
|
||||
}
|
||||
],
|
||||
|
||||
sidebar: {
|
||||
'/guide/': [
|
||||
{
|
||||
text: '开始使用',
|
||||
items: [
|
||||
{ text: '快速开始', link: '/guide/getting-started' },
|
||||
{ text: '指南概览', link: '/guide/' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '核心概念',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: '实体类 (Entity)', link: '/guide/entity' },
|
||||
{ text: '组件系统 (Component)', link: '/guide/component' },
|
||||
{
|
||||
text: '系统架构 (System)',
|
||||
link: '/guide/system',
|
||||
items: [
|
||||
{ text: 'Worker系统 (多线程)', link: '/guide/worker-system' }
|
||||
]
|
||||
},
|
||||
{ text: '场景管理 (Scene)', link: '/guide/scene' },
|
||||
{ text: '序列化系统 (Serialization)', link: '/guide/serialization' },
|
||||
{ text: '事件系统 (Event)', link: '/guide/event-system' },
|
||||
{ text: '时间和定时器 (Time)', link: '/guide/time-and-timers' },
|
||||
{ text: '日志系统 (Logger)', link: '/guide/logging' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '平台适配器',
|
||||
link: '/guide/platform-adapter',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: '浏览器适配器', link: '/guide/platform-adapter/browser' },
|
||||
{ text: '微信小游戏适配器', link: '/guide/platform-adapter/wechat-minigame' },
|
||||
{ text: 'Node.js适配器', link: '/guide/platform-adapter/nodejs' }
|
||||
]
|
||||
}
|
||||
],
|
||||
'/examples/': [
|
||||
{
|
||||
text: '示例',
|
||||
items: [
|
||||
{ text: '示例概览', link: '/examples/' },
|
||||
{ text: 'Worker系统演示', link: '/examples/worker-system-demo' }
|
||||
]
|
||||
}
|
||||
],
|
||||
'/api/': [
|
||||
{
|
||||
text: 'API 参考',
|
||||
items: [
|
||||
{ text: '概述', link: '/api/README' },
|
||||
{
|
||||
text: '核心类',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: 'Core', link: '/api/classes/Core' },
|
||||
{ text: 'Scene', link: '/api/classes/Scene' },
|
||||
{ text: 'World', link: '/api/classes/World' },
|
||||
{ text: 'Entity', link: '/api/classes/Entity' },
|
||||
{ text: 'Component', link: '/api/classes/Component' },
|
||||
{ text: 'EntitySystem', link: '/api/classes/EntitySystem' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '系统类',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'PassiveSystem', link: '/api/classes/PassiveSystem' },
|
||||
{ text: 'ProcessingSystem', link: '/api/classes/ProcessingSystem' },
|
||||
{ text: 'IntervalSystem', link: '/api/classes/IntervalSystem' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '工具类',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Matcher', link: '/api/classes/Matcher' },
|
||||
{ text: 'Time', link: '/api/classes/Time' },
|
||||
{ text: 'PerformanceMonitor', link: '/api/classes/PerformanceMonitor' },
|
||||
{ text: 'DebugManager', link: '/api/classes/DebugManager' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '接口',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'IScene', link: '/api/interfaces/IScene' },
|
||||
{ text: 'IComponent', link: '/api/interfaces/IComponent' },
|
||||
{ text: 'ISystemBase', link: '/api/interfaces/ISystemBase' },
|
||||
{ text: 'ICoreConfig', link: '/api/interfaces/ICoreConfig' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '装饰器',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: '@ECSComponent', link: '/api/functions/ECSComponent' },
|
||||
{ text: '@ECSSystem', link: '/api/functions/ECSSystem' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '枚举',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'ECSEventType', link: '/api/enumerations/ECSEventType' },
|
||||
{ text: 'LogLevel', link: '/api/enumerations/LogLevel' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
socialLinks: [
|
||||
{ icon: 'github', link: 'https://github.com/esengine/ecs-framework' }
|
||||
],
|
||||
|
||||
footer: {
|
||||
message: 'Released under the MIT License.',
|
||||
copyright: 'Copyright © 2025 ECS Framework'
|
||||
},
|
||||
|
||||
editLink: {
|
||||
pattern: 'https://github.com/esengine/ecs-framework/edit/master/docs/:path',
|
||||
text: '在 GitHub 上编辑此页'
|
||||
},
|
||||
|
||||
search: {
|
||||
provider: 'local'
|
||||
},
|
||||
|
||||
outline: {
|
||||
level: [2, 3],
|
||||
label: '目录'
|
||||
}
|
||||
},
|
||||
|
||||
head: [
|
||||
['meta', { name: 'theme-color', content: '#646cff' }],
|
||||
['link', { rel: 'icon', href: '/favicon.ico' }]
|
||||
],
|
||||
|
||||
base: '/ecs-framework/',
|
||||
cleanUrls: true,
|
||||
|
||||
markdown: {
|
||||
lineNumbers: true,
|
||||
theme: {
|
||||
light: 'github-light',
|
||||
dark: 'github-dark'
|
||||
}
|
||||
}
|
||||
})
|
||||
Binary file not shown.
@@ -1,431 +0,0 @@
|
||||
# 位掩码使用指南
|
||||
|
||||
本文档详细解释ECS框架中位掩码的概念、原理和使用方法。
|
||||
|
||||
## 目录
|
||||
|
||||
1. [什么是位掩码](#什么是位掩码)
|
||||
2. [位掩码的优势](#位掩码的优势)
|
||||
3. [基础使用方法](#基础使用方法)
|
||||
4. [高级位掩码操作](#高级位掩码操作)
|
||||
5. [实际应用场景](#实际应用场景)
|
||||
6. [性能优化技巧](#性能优化技巧)
|
||||
|
||||
## 什么是位掩码
|
||||
|
||||
### 基本概念
|
||||
|
||||
位掩码(BitMask)是一种使用二进制位来表示状态或属性的技术。在ECS框架中,每个组件类型对应一个二进制位,实体的组件组合可以用一个数字来表示。
|
||||
|
||||
### 简单例子
|
||||
|
||||
假设我们有以下组件:
|
||||
- PositionComponent → 位置 0 (二进制: 001)
|
||||
- VelocityComponent → 位置 1 (二进制: 010)
|
||||
- HealthComponent → 位置 2 (二进制: 100)
|
||||
|
||||
那么一个同时拥有Position和Health组件的实体,其位掩码就是:
|
||||
```
|
||||
001 (Position) + 100 (Health) = 101 (二进制) = 5 (十进制)
|
||||
```
|
||||
|
||||
### 可视化理解
|
||||
|
||||
```typescript
|
||||
// 组件类型对应的位位置
|
||||
PositionComponent → 位置0 → 2^0 = 1 → 二进制: 001
|
||||
VelocityComponent → 位置1 → 2^1 = 2 → 二进制: 010
|
||||
HealthComponent → 位置2 → 2^2 = 4 → 二进制: 100
|
||||
RenderComponent → 位置3 → 2^3 = 8 → 二进制: 1000
|
||||
|
||||
// 实体的组件组合示例
|
||||
实体A: Position + Velocity → 001 + 010 = 011 (二进制) = 3 (十进制)
|
||||
实体B: Position + Health → 001 + 100 = 101 (二进制) = 5 (十进制)
|
||||
实体C: Position + Velocity + Health → 001 + 010 + 100 = 111 (二进制) = 7 (十进制)
|
||||
```
|
||||
|
||||
## 位掩码的优势
|
||||
|
||||
### 1. 极快的查询速度
|
||||
|
||||
```typescript
|
||||
// 传统方式:需要遍历组件列表
|
||||
function hasComponents(entity, componentTypes) {
|
||||
for (const type of componentTypes) {
|
||||
if (!entity.hasComponent(type)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 位掩码方式:一次位运算即可
|
||||
function hasComponentsMask(entityMask, requiredMask) {
|
||||
return (entityMask & requiredMask) === requiredMask;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 内存效率
|
||||
|
||||
```typescript
|
||||
// 一个bigint可以表示64个组件的组合状态
|
||||
// 相比存储组件列表,内存使用量大大减少
|
||||
|
||||
const entity = scene.createEntity("Player");
|
||||
entity.addComponent(new PositionComponent());
|
||||
entity.addComponent(new HealthComponent());
|
||||
|
||||
// 获取位掩码(只是一个数字)
|
||||
const mask = entity.componentMask; // bigint类型
|
||||
console.log(`位掩码: ${mask}`); // 输出: 5 (二进制: 101)
|
||||
```
|
||||
|
||||
### 3. 批量操作优化
|
||||
|
||||
```typescript
|
||||
// 可以快速筛选大量实体
|
||||
const entities = scene.getAllEntities();
|
||||
const requiredMask = BigInt(0b101); // Position + Health
|
||||
|
||||
const filteredEntities = entities.filter(entity =>
|
||||
(entity.componentMask & requiredMask) === requiredMask
|
||||
);
|
||||
```
|
||||
|
||||
## 基础使用方法
|
||||
|
||||
### 获取实体的位掩码
|
||||
|
||||
```typescript
|
||||
import { Scene, Entity, Component } from '@esengine/ecs-framework';
|
||||
|
||||
// 创建组件
|
||||
class PositionComponent extends Component {
|
||||
constructor(public x: number = 0, public y: number = 0) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
class HealthComponent extends Component {
|
||||
constructor(public maxHealth: number = 100) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
// 创建实体并添加组件
|
||||
const scene = new Scene();
|
||||
const entity = scene.createEntity("Player");
|
||||
|
||||
console.log(`初始位掩码: ${entity.componentMask}`); // 0
|
||||
|
||||
entity.addComponent(new PositionComponent(100, 200));
|
||||
console.log(`添加Position后: ${entity.componentMask}`); // 可能是 1
|
||||
|
||||
entity.addComponent(new HealthComponent(100));
|
||||
console.log(`添加Health后: ${entity.componentMask}`); // 可能是 5
|
||||
|
||||
// 查看二进制表示
|
||||
console.log(`二进制表示: ${entity.componentMask.toString(2)}`);
|
||||
```
|
||||
|
||||
### 手动检查位掩码
|
||||
|
||||
```typescript
|
||||
// 检查实体是否拥有特定组件组合
|
||||
function checkEntityComponents(entity: Entity) {
|
||||
const mask = entity.componentMask;
|
||||
|
||||
// 将位掩码转换为二进制字符串查看
|
||||
const binaryString = mask.toString(2).padStart(8, '0');
|
||||
console.log(`实体组件状态: ${binaryString}`);
|
||||
|
||||
// 分析每一位
|
||||
console.log(`位0 (Position): ${(mask & 1n) !== 0n ? '有' : '无'}`);
|
||||
console.log(`位1 (Velocity): ${(mask & 2n) !== 0n ? '有' : '无'}`);
|
||||
console.log(`位2 (Health): ${(mask & 4n) !== 0n ? '有' : '无'}`);
|
||||
console.log(`位3 (Render): ${(mask & 8n) !== 0n ? '有' : '无'}`);
|
||||
}
|
||||
```
|
||||
|
||||
## 高级位掩码操作
|
||||
|
||||
### 使用BitMaskOptimizer
|
||||
|
||||
框架提供了BitMaskOptimizer类来简化位掩码操作:
|
||||
|
||||
```typescript
|
||||
import { BitMaskOptimizer } from '@esengine/ecs-framework';
|
||||
|
||||
// 获取优化器实例
|
||||
const optimizer = BitMaskOptimizer.getInstance();
|
||||
|
||||
// 注册组件类型(建议在游戏初始化时进行)
|
||||
optimizer.registerComponentType('PositionComponent');
|
||||
optimizer.registerComponentType('VelocityComponent');
|
||||
optimizer.registerComponentType('HealthComponent');
|
||||
optimizer.registerComponentType('RenderComponent');
|
||||
|
||||
// 创建单个组件的掩码
|
||||
const positionMask = optimizer.createSingleComponentMask('PositionComponent');
|
||||
console.log(`Position掩码: ${positionMask} (二进制: ${positionMask.toString(2)})`);
|
||||
|
||||
// 创建组合掩码
|
||||
const movementMask = optimizer.createCombinedMask(['PositionComponent', 'VelocityComponent']);
|
||||
console.log(`Movement掩码: ${movementMask} (二进制: ${movementMask.toString(2)})`);
|
||||
|
||||
// 检查实体是否匹配掩码
|
||||
const entity = scene.createEntity("TestEntity");
|
||||
entity.addComponent(new PositionComponent());
|
||||
entity.addComponent(new VelocityComponent());
|
||||
|
||||
const hasMovementComponents = optimizer.maskContainsAllComponents(
|
||||
entity.componentMask,
|
||||
['PositionComponent', 'VelocityComponent']
|
||||
);
|
||||
console.log(`实体拥有移动组件: ${hasMovementComponents}`);
|
||||
```
|
||||
|
||||
### 位掩码分析工具
|
||||
|
||||
```typescript
|
||||
// 分析位掩码的实用函数
|
||||
class MaskAnalyzer {
|
||||
private optimizer = BitMaskOptimizer.getInstance();
|
||||
|
||||
// 分析实体的组件组合
|
||||
analyzeEntity(entity: Entity): void {
|
||||
const mask = entity.componentMask;
|
||||
const componentNames = this.optimizer.maskToComponentNames(mask);
|
||||
const componentCount = this.optimizer.getComponentCount(mask);
|
||||
|
||||
console.log(`实体 ${entity.name} 分析:`);
|
||||
console.log(`- 位掩码: ${mask} (二进制: ${mask.toString(2)})`);
|
||||
console.log(`- 组件数量: ${componentCount}`);
|
||||
console.log(`- 组件列表: ${componentNames.join(', ')}`);
|
||||
}
|
||||
|
||||
// 比较两个实体的组件差异
|
||||
compareEntities(entityA: Entity, entityB: Entity): void {
|
||||
const maskA = entityA.componentMask;
|
||||
const maskB = entityB.componentMask;
|
||||
|
||||
const commonMask = maskA & maskB;
|
||||
const onlyInA = maskA & ~maskB;
|
||||
const onlyInB = maskB & ~maskA;
|
||||
|
||||
console.log(`实体比较:`);
|
||||
console.log(`- 共同组件: ${this.optimizer.maskToComponentNames(commonMask).join(', ')}`);
|
||||
console.log(`- 仅在A中: ${this.optimizer.maskToComponentNames(onlyInA).join(', ')}`);
|
||||
console.log(`- 仅在B中: ${this.optimizer.maskToComponentNames(onlyInB).join(', ')}`);
|
||||
}
|
||||
|
||||
// 查找具有特定组件组合的实体
|
||||
findEntitiesWithMask(entities: Entity[], requiredComponents: string[]): Entity[] {
|
||||
const requiredMask = this.optimizer.createCombinedMask(requiredComponents);
|
||||
|
||||
return entities.filter(entity =>
|
||||
(entity.componentMask & requiredMask) === requiredMask
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
const analyzer = new MaskAnalyzer();
|
||||
analyzer.analyzeEntity(entity);
|
||||
```
|
||||
|
||||
## 实际应用场景
|
||||
|
||||
### 1. 高性能实体查询
|
||||
|
||||
```typescript
|
||||
class GameSystem {
|
||||
private optimizer = BitMaskOptimizer.getInstance();
|
||||
private movementMask: bigint;
|
||||
private combatMask: bigint;
|
||||
|
||||
constructor() {
|
||||
// 预计算常用掩码
|
||||
this.movementMask = this.optimizer.createCombinedMask([
|
||||
'PositionComponent', 'VelocityComponent'
|
||||
]);
|
||||
|
||||
this.combatMask = this.optimizer.createCombinedMask([
|
||||
'PositionComponent', 'HealthComponent', 'WeaponComponent'
|
||||
]);
|
||||
}
|
||||
|
||||
// 快速查找移动实体
|
||||
findMovingEntities(entities: Entity[]): Entity[] {
|
||||
return entities.filter(entity =>
|
||||
(entity.componentMask & this.movementMask) === this.movementMask
|
||||
);
|
||||
}
|
||||
|
||||
// 快速查找战斗单位
|
||||
findCombatUnits(entities: Entity[]): Entity[] {
|
||||
return entities.filter(entity =>
|
||||
(entity.componentMask & this.combatMask) === this.combatMask
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 实体分类和管理
|
||||
|
||||
```typescript
|
||||
class EntityClassifier {
|
||||
private optimizer = BitMaskOptimizer.getInstance();
|
||||
|
||||
// 定义实体类型掩码
|
||||
private readonly ENTITY_TYPES = {
|
||||
PLAYER: this.optimizer.createCombinedMask([
|
||||
'PositionComponent', 'HealthComponent', 'InputComponent'
|
||||
]),
|
||||
ENEMY: this.optimizer.createCombinedMask([
|
||||
'PositionComponent', 'HealthComponent', 'AIComponent'
|
||||
]),
|
||||
PROJECTILE: this.optimizer.createCombinedMask([
|
||||
'PositionComponent', 'VelocityComponent', 'DamageComponent'
|
||||
]),
|
||||
PICKUP: this.optimizer.createCombinedMask([
|
||||
'PositionComponent', 'PickupComponent'
|
||||
])
|
||||
};
|
||||
|
||||
// 根据组件组合判断实体类型
|
||||
classifyEntity(entity: Entity): string {
|
||||
const mask = entity.componentMask;
|
||||
|
||||
for (const [type, typeMask] of Object.entries(this.ENTITY_TYPES)) {
|
||||
if ((mask & typeMask) === typeMask) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
return 'UNKNOWN';
|
||||
}
|
||||
|
||||
// 批量分类实体
|
||||
classifyEntities(entities: Entity[]): Map<string, Entity[]> {
|
||||
const classified = new Map<string, Entity[]>();
|
||||
|
||||
for (const entity of entities) {
|
||||
const type = this.classifyEntity(entity);
|
||||
if (!classified.has(type)) {
|
||||
classified.set(type, []);
|
||||
}
|
||||
classified.get(type)!.push(entity);
|
||||
}
|
||||
|
||||
return classified;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 性能优化技巧
|
||||
|
||||
### 1. 预计算常用掩码
|
||||
|
||||
```typescript
|
||||
class MaskCache {
|
||||
private optimizer = BitMaskOptimizer.getInstance();
|
||||
|
||||
// 预计算游戏中常用的组件组合
|
||||
public readonly COMMON_MASKS = {
|
||||
RENDERABLE: this.optimizer.createCombinedMask([
|
||||
'PositionComponent', 'RenderComponent'
|
||||
]),
|
||||
MOVABLE: this.optimizer.createCombinedMask([
|
||||
'PositionComponent', 'VelocityComponent'
|
||||
]),
|
||||
LIVING: this.optimizer.createCombinedMask([
|
||||
'HealthComponent'
|
||||
]),
|
||||
INTERACTIVE: this.optimizer.createCombinedMask([
|
||||
'PositionComponent', 'ColliderComponent'
|
||||
])
|
||||
};
|
||||
|
||||
constructor() {
|
||||
// 预计算常用组合以提升性能
|
||||
this.optimizer.precomputeCommonMasks([
|
||||
['PositionComponent', 'RenderComponent'],
|
||||
['PositionComponent', 'VelocityComponent'],
|
||||
['PositionComponent', 'HealthComponent', 'WeaponComponent']
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 位掩码调试工具
|
||||
|
||||
```typescript
|
||||
// 位掩码调试工具
|
||||
class MaskDebugger {
|
||||
private optimizer = BitMaskOptimizer.getInstance();
|
||||
|
||||
// 可视化位掩码
|
||||
visualizeMask(mask: bigint, maxBits: number = 16): string {
|
||||
const binary = mask.toString(2).padStart(maxBits, '0');
|
||||
const components = this.optimizer.maskToComponentNames(mask);
|
||||
|
||||
let visualization = `位掩码: ${mask} (二进制: ${binary})\n`;
|
||||
visualization += `组件: ${components.join(', ')}\n`;
|
||||
visualization += `可视化: `;
|
||||
|
||||
for (let i = maxBits - 1; i >= 0; i--) {
|
||||
const bit = (mask & (1n << BigInt(i))) !== 0n ? '1' : '0';
|
||||
visualization += bit;
|
||||
if (i % 4 === 0 && i > 0) visualization += ' ';
|
||||
}
|
||||
|
||||
return visualization;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 组件注册
|
||||
|
||||
```typescript
|
||||
// 在游戏初始化时注册所有组件类型
|
||||
function initializeComponentTypes() {
|
||||
const optimizer = BitMaskOptimizer.getInstance();
|
||||
|
||||
// 按使用频率注册(常用的组件分配较小的位位置)
|
||||
optimizer.registerComponentType('PositionComponent'); // 位置0
|
||||
optimizer.registerComponentType('VelocityComponent'); // 位置1
|
||||
optimizer.registerComponentType('HealthComponent'); // 位置2
|
||||
optimizer.registerComponentType('RenderComponent'); // 位置3
|
||||
// ... 其他组件
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 掩码缓存管理
|
||||
|
||||
```typescript
|
||||
// 定期清理掩码缓存以避免内存泄漏
|
||||
setInterval(() => {
|
||||
const optimizer = BitMaskOptimizer.getInstance();
|
||||
const stats = optimizer.getCacheStats();
|
||||
|
||||
// 如果缓存过大,清理一部分
|
||||
if (stats.size > 1000) {
|
||||
optimizer.clearCache();
|
||||
console.log('位掩码缓存已清理');
|
||||
}
|
||||
}, 60000); // 每分钟检查一次
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
位掩码是ECS框架中的核心优化技术,它提供了:
|
||||
|
||||
1. **极快的查询速度** - 位运算比遍历快数百倍
|
||||
2. **内存效率** - 用一个数字表示复杂的组件组合
|
||||
3. **批量操作优化** - 可以快速处理大量实体
|
||||
4. **灵活的查询构建** - 支持复杂的组件组合查询
|
||||
|
||||
通过理解和正确使用位掩码,可以显著提升游戏的性能表现。记住要在游戏初始化时注册组件类型,预计算常用掩码,并合理管理缓存。
|
||||
@@ -1,695 +0,0 @@
|
||||
# 组件设计最佳实践指南
|
||||
|
||||
组件是ECS架构的核心,设计良好的组件是构建高质量游戏的基础。本指南将教你如何设计出清晰、高效、可维护的组件。
|
||||
|
||||
## 组件设计原则
|
||||
|
||||
### 1. 数据为主,逻辑为辅
|
||||
|
||||
**核心理念:** 组件主要存储数据,复杂逻辑放在系统中处理。
|
||||
|
||||
```typescript
|
||||
// ✅ 好的设计:主要是数据
|
||||
class HealthComponent extends Component {
|
||||
public maxHealth: number;
|
||||
public currentHealth: number;
|
||||
public regenRate: number = 0;
|
||||
public lastDamageTime: number = 0;
|
||||
|
||||
constructor(maxHealth: number = 100) {
|
||||
super();
|
||||
this.maxHealth = maxHealth;
|
||||
this.currentHealth = maxHealth;
|
||||
}
|
||||
|
||||
// 简单的辅助方法是可以的
|
||||
isDead(): boolean {
|
||||
return this.currentHealth <= 0;
|
||||
}
|
||||
|
||||
getHealthPercentage(): number {
|
||||
return this.currentHealth / this.maxHealth;
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ 不好的设计:包含太多逻辑
|
||||
class BadHealthComponent extends Component {
|
||||
public maxHealth: number;
|
||||
public currentHealth: number;
|
||||
|
||||
takeDamage(damage: number) {
|
||||
this.currentHealth -= damage;
|
||||
|
||||
// 这些逻辑应该在系统中处理
|
||||
if (this.currentHealth <= 0) {
|
||||
this.entity.destroy(); // 销毁逻辑
|
||||
this.playDeathSound(); // 音效逻辑
|
||||
this.createDeathEffect(); // 特效逻辑
|
||||
this.updatePlayerScore(100); // 分数逻辑
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 单一职责原则
|
||||
|
||||
每个组件只负责一个方面的数据。
|
||||
|
||||
```typescript
|
||||
// ✅ 好的设计:单一职责
|
||||
class PositionComponent extends Component {
|
||||
public x: number = 0;
|
||||
public y: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
|
||||
class VelocityComponent extends Component {
|
||||
public x: number = 0;
|
||||
public y: number = 0;
|
||||
public maxSpeed: number = 100;
|
||||
|
||||
constructor(x: number = 0, y: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
|
||||
class RotationComponent extends Component {
|
||||
public angle: number = 0;
|
||||
public angularVelocity: number = 0;
|
||||
|
||||
constructor(angle: number = 0) {
|
||||
super();
|
||||
this.angle = angle;
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ 不好的设计:职责混乱
|
||||
class TransformComponent extends Component {
|
||||
public x: number = 0;
|
||||
public y: number = 0;
|
||||
public velocityX: number = 0;
|
||||
public velocityY: number = 0;
|
||||
public angle: number = 0;
|
||||
public scale: number = 1;
|
||||
public health: number = 100; // 和变换无关
|
||||
public ammo: number = 30; // 和变换无关
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 组合优于继承
|
||||
|
||||
使用多个小组件组合,而不是大而全的组件继承。
|
||||
|
||||
```typescript
|
||||
// ✅ 好的设计:组合方式
|
||||
class Player {
|
||||
constructor(scene: Scene) {
|
||||
const player = scene.createEntity("Player");
|
||||
|
||||
// 通过组合不同组件实现功能
|
||||
player.addComponent(new PositionComponent(100, 100));
|
||||
player.addComponent(new VelocityComponent());
|
||||
player.addComponent(new HealthComponent(100));
|
||||
player.addComponent(new PlayerInputComponent());
|
||||
player.addComponent(new WeaponComponent());
|
||||
player.addComponent(new InventoryComponent());
|
||||
|
||||
return player;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建不同类型的实体很容易
|
||||
class Enemy {
|
||||
constructor(scene: Scene) {
|
||||
const enemy = scene.createEntity("Enemy");
|
||||
|
||||
// 复用相同的组件,但组合不同
|
||||
enemy.addComponent(new PositionComponent(200, 200));
|
||||
enemy.addComponent(new VelocityComponent());
|
||||
enemy.addComponent(new HealthComponent(50));
|
||||
enemy.addComponent(new AIComponent()); // 不同:AI而不是玩家输入
|
||||
enemy.addComponent(new WeaponComponent()); // 相同:都有武器
|
||||
// 没有库存组件
|
||||
|
||||
return enemy;
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ 不好的设计:继承方式
|
||||
class GameObject {
|
||||
public x: number;
|
||||
public y: number;
|
||||
public health: number;
|
||||
// ... 很多属性
|
||||
}
|
||||
|
||||
class PlayerGameObject extends GameObject {
|
||||
public input: InputData;
|
||||
public inventory: Item[];
|
||||
// 强制继承了不需要的属性
|
||||
}
|
||||
|
||||
class EnemyGameObject extends GameObject {
|
||||
public ai: AIData;
|
||||
// 继承了不需要的库存等属性
|
||||
}
|
||||
```
|
||||
|
||||
## 常见组件类型和设计
|
||||
|
||||
### 1. 数据组件(Data Components)
|
||||
|
||||
纯数据存储,没有或很少有方法。
|
||||
|
||||
```typescript
|
||||
// 位置信息
|
||||
class PositionComponent extends Component {
|
||||
public x: number;
|
||||
public y: number;
|
||||
|
||||
constructor(x: number = 0, y: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
// 简单的辅助方法
|
||||
distanceTo(other: PositionComponent): number {
|
||||
const dx = this.x - other.x;
|
||||
const dy = this.y - other.y;
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
set(x: number, y: number) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
|
||||
// 统计信息
|
||||
class StatsComponent extends Component {
|
||||
public strength: number = 10;
|
||||
public agility: number = 10;
|
||||
public intelligence: number = 10;
|
||||
public vitality: number = 10;
|
||||
|
||||
// 计算派生属性
|
||||
getMaxHealth(): number {
|
||||
return this.vitality * 10;
|
||||
}
|
||||
|
||||
getDamage(): number {
|
||||
return this.strength * 2;
|
||||
}
|
||||
|
||||
getMoveSpeed(): number {
|
||||
return this.agility * 5;
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染信息
|
||||
class SpriteComponent extends Component {
|
||||
public textureName: string;
|
||||
public width: number;
|
||||
public height: number;
|
||||
public tint: number = 0xFFFFFF;
|
||||
public alpha: number = 1.0;
|
||||
public visible: boolean = true;
|
||||
|
||||
constructor(textureName: string, width: number = 0, height: number = 0) {
|
||||
super();
|
||||
this.textureName = textureName;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 标记组件(Tag Components)
|
||||
|
||||
用于标识实体状态或类型的空组件。
|
||||
|
||||
```typescript
|
||||
// 标记组件通常不包含数据
|
||||
class PlayerComponent extends Component {
|
||||
// 空组件,仅用于标记这是玩家实体
|
||||
}
|
||||
|
||||
class EnemyComponent extends Component {
|
||||
// 空组件,仅用于标记这是敌人实体
|
||||
}
|
||||
|
||||
class DeadComponent extends Component {
|
||||
// 标记实体已死亡
|
||||
public deathTime: number;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.deathTime = Time.totalTime;
|
||||
}
|
||||
}
|
||||
|
||||
class InvincibleComponent extends Component {
|
||||
// 标记实体无敌状态
|
||||
public duration: number;
|
||||
|
||||
constructor(duration: number = 2.0) {
|
||||
super();
|
||||
this.duration = duration;
|
||||
}
|
||||
}
|
||||
|
||||
// 使用标记组件进行查询
|
||||
class GameSystem {
|
||||
updatePlayers() {
|
||||
// 只处理玩家实体
|
||||
const players = this.scene.findEntitiesWithComponent(PlayerComponent);
|
||||
// ...
|
||||
}
|
||||
|
||||
updateEnemies() {
|
||||
// 只处理敌人实体
|
||||
const enemies = this.scene.findEntitiesWithComponent(EnemyComponent);
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 行为组件(Behavior Components)
|
||||
|
||||
包含简单行为逻辑的组件。
|
||||
|
||||
```typescript
|
||||
class WeaponComponent extends Component {
|
||||
public damage: number;
|
||||
public fireRate: number;
|
||||
public ammo: number;
|
||||
public maxAmmo: number;
|
||||
public lastFireTime: number = 0;
|
||||
|
||||
constructor(damage: number = 10, fireRate: number = 0.5) {
|
||||
super();
|
||||
this.damage = damage;
|
||||
this.fireRate = fireRate;
|
||||
this.maxAmmo = 30;
|
||||
this.ammo = this.maxAmmo;
|
||||
}
|
||||
|
||||
canFire(): boolean {
|
||||
return this.ammo > 0 &&
|
||||
Time.totalTime - this.lastFireTime >= this.fireRate;
|
||||
}
|
||||
|
||||
fire(): boolean {
|
||||
if (this.canFire()) {
|
||||
this.ammo--;
|
||||
this.lastFireTime = Time.totalTime;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
reload() {
|
||||
this.ammo = this.maxAmmo;
|
||||
}
|
||||
|
||||
getAmmoPercentage(): number {
|
||||
return this.ammo / this.maxAmmo;
|
||||
}
|
||||
}
|
||||
|
||||
class InventoryComponent extends Component {
|
||||
private items: Map<string, number> = new Map();
|
||||
public maxCapacity: number = 20;
|
||||
|
||||
addItem(itemType: string, quantity: number = 1): boolean {
|
||||
if (this.getTotalItems() + quantity > this.maxCapacity) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const current = this.items.get(itemType) || 0;
|
||||
this.items.set(itemType, current + quantity);
|
||||
return true;
|
||||
}
|
||||
|
||||
removeItem(itemType: string, quantity: number = 1): boolean {
|
||||
const current = this.items.get(itemType) || 0;
|
||||
if (current < quantity) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const newAmount = current - quantity;
|
||||
if (newAmount === 0) {
|
||||
this.items.delete(itemType);
|
||||
} else {
|
||||
this.items.set(itemType, newAmount);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
hasItem(itemType: string, quantity: number = 1): boolean {
|
||||
const current = this.items.get(itemType) || 0;
|
||||
return current >= quantity;
|
||||
}
|
||||
|
||||
getTotalItems(): number {
|
||||
let total = 0;
|
||||
this.items.forEach(quantity => total += quantity);
|
||||
return total;
|
||||
}
|
||||
|
||||
getItems(): Map<string, number> {
|
||||
return new Map(this.items); // 返回副本
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 组件通信和依赖
|
||||
|
||||
### 1. 组件间通信
|
||||
|
||||
组件间不应直接通信,通过系统或事件系统进行通信。
|
||||
|
||||
```typescript
|
||||
// ✅ 好的设计:通过事件通信
|
||||
class HealthComponent extends Component {
|
||||
public currentHealth: number;
|
||||
public maxHealth: number;
|
||||
|
||||
takeDamage(damage: number) {
|
||||
this.currentHealth -= damage;
|
||||
|
||||
// 发送事件,让其他系统响应
|
||||
Core.emitter.emit('health:damaged', {
|
||||
entity: this.entity,
|
||||
damage: damage,
|
||||
remainingHealth: this.currentHealth
|
||||
});
|
||||
|
||||
if (this.currentHealth <= 0) {
|
||||
Core.emitter.emit('health:died', {
|
||||
entity: this.entity
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 其他组件响应事件
|
||||
class AnimationComponent extends Component {
|
||||
onAddedToEntity() {
|
||||
super.onAddedToEntity();
|
||||
|
||||
// 监听受伤事件
|
||||
Core.emitter.addObserver('health:damaged', this.onDamaged, this);
|
||||
}
|
||||
|
||||
onRemovedFromEntity() {
|
||||
Core.emitter.removeObserver('health:damaged', this.onDamaged, this);
|
||||
super.onRemovedFromEntity();
|
||||
}
|
||||
|
||||
private onDamaged(data: any) {
|
||||
if (data.entity === this.entity) {
|
||||
this.playHurtAnimation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ 不好的设计:直接依赖其他组件
|
||||
class BadHealthComponent extends Component {
|
||||
takeDamage(damage: number) {
|
||||
this.currentHealth -= damage;
|
||||
|
||||
// 直接操作其他组件
|
||||
const animation = this.entity.getComponent(AnimationComponent);
|
||||
if (animation) {
|
||||
animation.playHurtAnimation(); // 紧耦合
|
||||
}
|
||||
|
||||
const sound = this.entity.getComponent(SoundComponent);
|
||||
if (sound) {
|
||||
sound.playHurtSound(); // 紧耦合
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 可选依赖
|
||||
|
||||
有时组件需要其他组件配合工作,但应该优雅处理缺失的情况。
|
||||
|
||||
```typescript
|
||||
class MovementComponent extends Component {
|
||||
public speed: number = 100;
|
||||
|
||||
update() {
|
||||
// 可选依赖:输入组件
|
||||
const input = this.entity.getComponent(InputComponent);
|
||||
const velocity = this.entity.getComponent(VelocityComponent);
|
||||
|
||||
if (input && velocity) {
|
||||
// 根据输入设置速度
|
||||
velocity.x = input.horizontal * this.speed;
|
||||
velocity.y = input.vertical * this.speed;
|
||||
}
|
||||
|
||||
// 可选依赖:AI组件
|
||||
const ai = this.entity.getComponent(AIComponent);
|
||||
if (ai && velocity && !input) {
|
||||
// AI控制移动(如果没有输入)
|
||||
velocity.x = ai.moveDirection.x * this.speed;
|
||||
velocity.y = ai.moveDirection.y * this.speed;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 组件性能优化
|
||||
|
||||
### 1. 对象池优化
|
||||
|
||||
对于频繁创建/销毁的组件,使用对象池。
|
||||
|
||||
```typescript
|
||||
class PooledBulletComponent extends Component {
|
||||
public damage: number = 10;
|
||||
public speed: number = 200;
|
||||
public direction: { x: number; y: number } = { x: 0, y: 0 };
|
||||
public lifetime: number = 5.0;
|
||||
private currentLifetime: number = 0;
|
||||
|
||||
// 重置组件状态,用于对象池
|
||||
reset() {
|
||||
this.damage = 10;
|
||||
this.speed = 200;
|
||||
this.direction.set(0, 0);
|
||||
this.lifetime = 5.0;
|
||||
this.currentLifetime = 0;
|
||||
}
|
||||
|
||||
// 配置子弹
|
||||
configure(damage: number, speed: number, direction: { x: number; y: number }) {
|
||||
this.damage = damage;
|
||||
this.speed = speed;
|
||||
this.direction = direction.copy();
|
||||
}
|
||||
|
||||
update() {
|
||||
this.currentLifetime += Time.deltaTime;
|
||||
|
||||
if (this.currentLifetime >= this.lifetime) {
|
||||
// 生命周期结束,回收到对象池
|
||||
BulletPool.release(this.entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 对象池管理
|
||||
class BulletPool {
|
||||
private static pool: Entity[] = [];
|
||||
|
||||
static get(): Entity {
|
||||
if (this.pool.length > 0) {
|
||||
const bullet = this.pool.pop()!;
|
||||
bullet.enabled = true;
|
||||
return bullet;
|
||||
} else {
|
||||
return this.createBullet();
|
||||
}
|
||||
}
|
||||
|
||||
static release(bullet: Entity) {
|
||||
bullet.enabled = false;
|
||||
bullet.getComponent(PooledBulletComponent)?.reset();
|
||||
this.pool.push(bullet);
|
||||
}
|
||||
|
||||
private static createBullet(): Entity {
|
||||
const bullet = Core.scene.createEntity("Bullet");
|
||||
bullet.addComponent(new PooledBulletComponent());
|
||||
bullet.addComponent(new PositionComponent());
|
||||
bullet.addComponent(new VelocityComponent());
|
||||
return bullet;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 数据紧凑性
|
||||
|
||||
保持组件数据紧凑,避免不必要的对象分配。
|
||||
|
||||
```typescript
|
||||
// ✅ 好的设计:紧凑的数据结构
|
||||
class ParticleComponent extends Component {
|
||||
// 使用基本类型,避免对象分配
|
||||
public x: number = 0;
|
||||
public y: number = 0;
|
||||
public velocityX: number = 0;
|
||||
public velocityY: number = 0;
|
||||
public life: number = 1.0;
|
||||
public maxLife: number = 1.0;
|
||||
public size: number = 1.0;
|
||||
public color: number = 0xFFFFFF;
|
||||
|
||||
// 计算属性,避免存储冗余数据
|
||||
get alpha(): number {
|
||||
return this.life / this.maxLife;
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ 不好的设计:过多对象分配
|
||||
class BadParticleComponent extends Component {
|
||||
public position: { x: number; y: number } = { x: 0, y: 0 }; // 对象分配
|
||||
public velocity: { x: number; y: number } = { x: 0, y: 0 }; // 对象分配
|
||||
public color: Color = new Color(); // 对象分配
|
||||
public transform: Transform = new Transform(); // 对象分配
|
||||
|
||||
// 冗余数据
|
||||
public alpha: number = 1.0;
|
||||
public life: number = 1.0;
|
||||
public maxLife: number = 1.0;
|
||||
}
|
||||
```
|
||||
|
||||
## 组件调试和测试
|
||||
|
||||
### 1. 调试友好的组件
|
||||
|
||||
```typescript
|
||||
class DebugFriendlyComponent extends Component {
|
||||
public someValue: number = 0;
|
||||
private debugName: string;
|
||||
|
||||
constructor(debugName: string = "Unknown") {
|
||||
super();
|
||||
this.debugName = debugName;
|
||||
}
|
||||
|
||||
// 提供有用的调试信息
|
||||
toString(): string {
|
||||
return `${this.constructor.name}(${this.debugName}): value=${this.someValue}`;
|
||||
}
|
||||
|
||||
// 验证组件状态
|
||||
validate(): boolean {
|
||||
if (this.someValue < 0) {
|
||||
console.warn(`${this} has invalid value: ${this.someValue}`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 获取调试信息
|
||||
getDebugInfo(): any {
|
||||
return {
|
||||
name: this.debugName,
|
||||
value: this.someValue,
|
||||
entityId: this.entity?.id,
|
||||
isValid: this.validate()
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 单元测试
|
||||
|
||||
```typescript
|
||||
// 组件测试示例
|
||||
describe('HealthComponent', () => {
|
||||
let healthComponent: HealthComponent;
|
||||
|
||||
beforeEach(() => {
|
||||
healthComponent = new HealthComponent(100);
|
||||
});
|
||||
|
||||
test('初始状态正确', () => {
|
||||
expect(healthComponent.currentHealth).toBe(100);
|
||||
expect(healthComponent.maxHealth).toBe(100);
|
||||
expect(healthComponent.isDead()).toBe(false);
|
||||
});
|
||||
|
||||
test('受伤功能正确', () => {
|
||||
healthComponent.takeDamage(30);
|
||||
expect(healthComponent.currentHealth).toBe(70);
|
||||
expect(healthComponent.getHealthPercentage()).toBe(0.7);
|
||||
});
|
||||
|
||||
test('死亡检测正确', () => {
|
||||
healthComponent.takeDamage(100);
|
||||
expect(healthComponent.isDead()).toBe(true);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## 常见问题和最佳实践
|
||||
|
||||
### Q: 组件应该有多大?
|
||||
|
||||
A: 组件应该尽可能小和专注。如果一个组件有超过10个字段,考虑拆分。
|
||||
|
||||
### Q: 组件可以包含方法吗?
|
||||
|
||||
A: 可以,但应该是简单的辅助方法。复杂逻辑应该在系统中处理。
|
||||
|
||||
### Q: 如何处理组件之间的依赖?
|
||||
|
||||
A:
|
||||
1. 优先使用组合而不是依赖
|
||||
2. 通过事件系统通信
|
||||
3. 在系统中处理组件间的协调
|
||||
|
||||
### Q: 什么时候使用继承?
|
||||
|
||||
A: 很少使用。只在有明确的"是一个"关系时使用,如:
|
||||
|
||||
```typescript
|
||||
abstract class ColliderComponent extends Component {
|
||||
abstract checkCollision(other: ColliderComponent): boolean;
|
||||
}
|
||||
|
||||
class CircleColliderComponent extends ColliderComponent {
|
||||
public radius: number;
|
||||
|
||||
checkCollision(other: ColliderComponent): boolean {
|
||||
// 圆形碰撞检测
|
||||
}
|
||||
}
|
||||
|
||||
class BoxColliderComponent extends ColliderComponent {
|
||||
public width: number;
|
||||
public height: number;
|
||||
|
||||
checkCollision(other: ColliderComponent): boolean {
|
||||
// 方形碰撞检测
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
遵循这些原则,你就能设计出高质量、易维护的组件系统!
|
||||
@@ -1,665 +0,0 @@
|
||||
# 技术概念详解
|
||||
|
||||
本文档用通俗易懂的语言解释ECS框架中的关键技术概念,帮助开发者理解这些技术的作用和应用场景。
|
||||
|
||||
## 目录
|
||||
|
||||
- [ECS 架构基础](#ecs-架构基础)
|
||||
- [性能优化技术](#性能优化技术)
|
||||
- [事件系统](#事件系统)
|
||||
- [实体管理](#实体管理)
|
||||
|
||||
## ECS 架构基础
|
||||
|
||||
### 什么是 ECS?
|
||||
|
||||
ECS (Entity-Component-System) 是一种编程架构模式,将游戏对象分解为三个独立的部分:
|
||||
|
||||
**传统面向对象方式:**
|
||||
```typescript
|
||||
// 传统继承方式 - 问题很多
|
||||
class GameObject {
|
||||
x: number; y: number;
|
||||
render() { ... }
|
||||
update() { ... }
|
||||
}
|
||||
|
||||
class Player extends GameObject {
|
||||
health: number;
|
||||
shoot() { ... }
|
||||
}
|
||||
|
||||
class Enemy extends Player { // 敌人需要射击但不需要玩家控制?
|
||||
ai() { ... }
|
||||
}
|
||||
```
|
||||
|
||||
**ECS 方式:**
|
||||
```typescript
|
||||
// 数据和逻辑分离,灵活组合
|
||||
const player = createEntity()
|
||||
.add(PositionComponent) // 位置数据
|
||||
.add(HealthComponent) // 生命值数据
|
||||
.add(PlayerInputComponent) // 玩家输入标记
|
||||
|
||||
const enemy = createEntity()
|
||||
.add(PositionComponent) // 复用位置数据
|
||||
.add(HealthComponent) // 复用生命值数据
|
||||
.add(AIComponent) // AI标记
|
||||
|
||||
// 系统处理具有特定组件的实体
|
||||
MovementSystem.process([PositionComponent, VelocityComponent]);
|
||||
```
|
||||
|
||||
### ECS 的优势
|
||||
|
||||
1. **灵活组合** - 像搭积木一样组装功能
|
||||
2. **代码复用** - 组件可以在不同实体间复用
|
||||
3. **性能优化** - 数据连续存储,缓存友好
|
||||
4. **并行处理** - 系统间相互独立,可以并行执行
|
||||
5. **易于测试** - 组件和系统可以独立测试
|
||||
|
||||
### 实际应用场景
|
||||
|
||||
**游戏开发中的例子:**
|
||||
- **RPG游戏**:玩家、NPC、怪物都有位置和生命值,但只有玩家有输入组件
|
||||
- **射击游戏**:子弹、玩家、敌人都有位置和碰撞体,但行为完全不同
|
||||
- **策略游戏**:建筑、单位、资源都是实体,通过不同组件组合实现功能
|
||||
|
||||
## 性能优化技术
|
||||
|
||||
### 组件索引系统
|
||||
|
||||
**问题:** 没有索引时,查找组件需要遍历所有实体
|
||||
```typescript
|
||||
// 慢的方式:线性搜索 O(n)
|
||||
function findEntitiesWithHealth() {
|
||||
const result = [];
|
||||
for (const entity of allEntities) { // 遍历10万个实体
|
||||
if (entity.hasComponent(HealthComponent)) {
|
||||
result.push(entity);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
**解决方案:** 建立索引,直接访问
|
||||
```typescript
|
||||
// 快的方式:索引查找 O(1)
|
||||
const healthIndex = componentIndex.get(HealthComponent);
|
||||
const entitiesWithHealth = healthIndex.getEntities(); // 直接获取
|
||||
```
|
||||
|
||||
**应用场景:**
|
||||
- 频繁查询特定组件的实体
|
||||
- 大规模实体场景(数千到数万个实体)
|
||||
- 实时游戏中的系统更新
|
||||
|
||||
### 索引类型选择指南
|
||||
|
||||
框架提供两种索引类型,选择合适的类型对性能至关重要:
|
||||
|
||||
#### 🔸 哈希索引 (Hash Index)
|
||||
|
||||
**适用场景:**
|
||||
- 实体数量较多(> 1000个)
|
||||
- 组件数据变化不频繁
|
||||
- 需要快速查找特定实体
|
||||
|
||||
**优势:**
|
||||
- 查询速度极快 O(1)
|
||||
- 内存使用相对较少
|
||||
- 适合大量实体
|
||||
|
||||
**缺点:**
|
||||
- 添加/删除组件时有额外开销
|
||||
- 不适合频繁变化的组件
|
||||
|
||||
```typescript
|
||||
// 适合哈希索引的组件
|
||||
componentIndex.setIndexType(PositionComponent, 'hash'); // 位置变化不频繁
|
||||
componentIndex.setIndexType(HealthComponent, 'hash'); // 生命值组件稳定
|
||||
componentIndex.setIndexType(PlayerComponent, 'hash'); // 玩家标记组件
|
||||
```
|
||||
|
||||
#### 🔹 位图索引 (Bitmap Index)
|
||||
|
||||
**适用场景:**
|
||||
- 组件频繁添加/删除
|
||||
- 实体数量适中(< 10000个)
|
||||
- 需要批量操作
|
||||
|
||||
**优势:**
|
||||
- 添加/删除组件极快
|
||||
- 批量查询效率高
|
||||
- 内存访问模式好
|
||||
|
||||
**缺点:**
|
||||
- 随实体数量增长,内存占用增加
|
||||
- 稀疏数据时效率降低
|
||||
|
||||
```typescript
|
||||
// 适合位图索引的组件
|
||||
componentIndex.setIndexType(BuffComponent, 'bitmap'); // Buff经常添加删除
|
||||
componentIndex.setIndexType(TemporaryComponent, 'bitmap'); // 临时组件
|
||||
componentIndex.setIndexType(StateComponent, 'bitmap'); // 状态组件变化频繁
|
||||
```
|
||||
|
||||
#### 📊 选择决策表
|
||||
|
||||
| 考虑因素 | 哈希索引 (Hash) | 位图索引 (Bitmap) |
|
||||
|---------|----------------|-------------------|
|
||||
| **实体数量** | > 1,000 | < 10,000 |
|
||||
| **组件变化频率** | 低频变化 | 高频变化 |
|
||||
| **查询频率** | 高频查询 | 中等查询 |
|
||||
| **内存使用** | 较少 | 随实体数增加 |
|
||||
| **批量操作** | 一般 | 优秀 |
|
||||
|
||||
#### 🤔 快速决策流程
|
||||
|
||||
**第一步:判断组件变化频率**
|
||||
- 组件经常添加/删除? → 选择 **位图索引**
|
||||
- 组件相对稳定? → 继续第二步
|
||||
|
||||
**第二步:判断实体数量**
|
||||
- 实体数量 > 1000? → 选择 **哈希索引**
|
||||
- 实体数量 < 1000? → 选择 **位图索引**
|
||||
|
||||
**第三步:特殊情况**
|
||||
- 需要频繁批量操作? → 选择 **位图索引**
|
||||
- 内存使用很重要? → 选择 **哈希索引**
|
||||
|
||||
#### 🎮 实际游戏中的选择示例
|
||||
|
||||
**射击游戏:**
|
||||
```typescript
|
||||
// 稳定组件用哈希索引
|
||||
componentIndex.setIndexType(PositionComponent, 'hash'); // 实体位置稳定存在
|
||||
componentIndex.setIndexType(HealthComponent, 'hash'); // 生命值组件持续存在
|
||||
componentIndex.setIndexType(WeaponComponent, 'hash'); // 武器组件不常变化
|
||||
|
||||
// 变化组件用位图索引
|
||||
componentIndex.setIndexType(BuffComponent, 'bitmap'); // Buff频繁添加删除
|
||||
componentIndex.setIndexType(ReloadingComponent, 'bitmap'); // 装弹状态临时组件
|
||||
```
|
||||
|
||||
**策略游戏:**
|
||||
```typescript
|
||||
// 大量单位,核心组件用哈希
|
||||
componentIndex.setIndexType(UnitComponent, 'hash'); // 单位类型稳定
|
||||
componentIndex.setIndexType(OwnerComponent, 'hash'); // 所属玩家稳定
|
||||
|
||||
// 状态组件用位图
|
||||
componentIndex.setIndexType(SelectedComponent, 'bitmap'); // 选中状态常变化
|
||||
componentIndex.setIndexType(MovingComponent, 'bitmap'); // 移动状态变化
|
||||
componentIndex.setIndexType(AttackingComponent, 'bitmap'); // 攻击状态临时
|
||||
```
|
||||
|
||||
**RPG游戏:**
|
||||
```typescript
|
||||
// 角色核心属性用哈希
|
||||
componentIndex.setIndexType(StatsComponent, 'hash'); // 属性组件稳定
|
||||
componentIndex.setIndexType(InventoryComponent, 'hash'); // 背包组件稳定
|
||||
componentIndex.setIndexType(LevelComponent, 'hash'); // 等级组件稳定
|
||||
|
||||
// 临时状态用位图
|
||||
componentIndex.setIndexType(StatusEffectComponent, 'bitmap'); // 状态效果变化
|
||||
componentIndex.setIndexType(QuestComponent, 'bitmap'); // 任务状态变化
|
||||
componentIndex.setIndexType(CombatComponent, 'bitmap'); // 战斗状态临时
|
||||
```
|
||||
|
||||
#### ❌ 常见选择错误
|
||||
|
||||
**错误示例1:大量实体使用位图索引**
|
||||
```typescript
|
||||
// ❌ 错误:10万个单位用位图索引,内存爆炸
|
||||
const entityCount = 100000;
|
||||
componentIndex.setIndexType(UnitComponent, 'bitmap'); // 内存占用过大!
|
||||
|
||||
// ✅ 正确:大量实体用哈希索引
|
||||
componentIndex.setIndexType(UnitComponent, 'hash');
|
||||
```
|
||||
|
||||
**错误示例2:频繁变化组件用哈希索引**
|
||||
```typescript
|
||||
// ❌ 错误:Buff频繁添加删除,哈希索引效率低
|
||||
componentIndex.setIndexType(BuffComponent, 'hash'); // 添加删除慢!
|
||||
|
||||
// ✅ 正确:变化频繁的组件用位图索引
|
||||
componentIndex.setIndexType(BuffComponent, 'bitmap');
|
||||
```
|
||||
|
||||
**错误示例3:不考虑实际使用场景**
|
||||
```typescript
|
||||
// ❌ 错误:所有组件都用同一种索引
|
||||
componentIndex.setIndexType(PositionComponent, 'hash');
|
||||
componentIndex.setIndexType(BuffComponent, 'hash'); // 应该用bitmap
|
||||
componentIndex.setIndexType(TemporaryComponent, 'hash'); // 应该用bitmap
|
||||
|
||||
// ✅ 正确:根据组件特性选择
|
||||
componentIndex.setIndexType(PositionComponent, 'hash'); // 稳定组件
|
||||
componentIndex.setIndexType(BuffComponent, 'bitmap'); // 变化组件
|
||||
componentIndex.setIndexType(TemporaryComponent, 'bitmap'); // 临时组件
|
||||
```
|
||||
|
||||
### Archetype 系统
|
||||
|
||||
**什么是 Archetype?**
|
||||
Archetype(原型)是具有相同组件组合的实体分组。
|
||||
|
||||
**没有 Archetype 的问题:**
|
||||
```typescript
|
||||
// 每次都要检查每个实体的组件组合
|
||||
for (const entity of allEntities) {
|
||||
if (entity.has(Position) && entity.has(Velocity) && !entity.has(Frozen)) {
|
||||
// 处理移动
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Archetype 的解决方案:**
|
||||
```typescript
|
||||
// 实体按组件组合自动分组
|
||||
const movableArchetype = [Position, Velocity, !Frozen];
|
||||
const movableEntities = archetypeSystem.getEntities(movableArchetype);
|
||||
// 直接处理,无需逐个检查
|
||||
```
|
||||
|
||||
**应用场景:**
|
||||
- 大量实体的游戏(RTS、MMO)
|
||||
- 频繁的实体查询操作
|
||||
- 批量处理相同类型的实体
|
||||
|
||||
### 脏标记系统
|
||||
|
||||
**什么是脏标记?**
|
||||
脏标记(Dirty Tracking)追踪哪些数据发生了变化,避免处理未变化的数据。
|
||||
|
||||
**没有脏标记的问题:**
|
||||
```typescript
|
||||
// 每帧都重新计算所有实体,即使它们没有移动
|
||||
function renderSystem() {
|
||||
for (const entity of entities) {
|
||||
updateRenderPosition(entity); // 浪费计算
|
||||
updateRenderRotation(entity); // 浪费计算
|
||||
updateRenderScale(entity); // 浪费计算
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**脏标记的解决方案:**
|
||||
```typescript
|
||||
// 只处理发生变化的实体
|
||||
function renderSystem() {
|
||||
const dirtyEntities = dirtyTracking.getDirtyEntities();
|
||||
for (const entity of dirtyEntities) {
|
||||
if (dirtyTracking.isDirty(entity, PositionComponent)) {
|
||||
updateRenderPosition(entity); // 只在需要时计算
|
||||
}
|
||||
if (dirtyTracking.isDirty(entity, RotationComponent)) {
|
||||
updateRenderRotation(entity);
|
||||
}
|
||||
}
|
||||
dirtyTracking.clearDirtyFlags();
|
||||
}
|
||||
```
|
||||
|
||||
**应用场景:**
|
||||
- 渲染系统优化(只更新变化的物体)
|
||||
- 物理系统优化(只计算移动的物体)
|
||||
- UI更新优化(只刷新变化的界面元素)
|
||||
- 网络同步优化(只发送变化的数据)
|
||||
|
||||
**实际例子:**
|
||||
```typescript
|
||||
// 游戏中的应用
|
||||
class MovementSystem {
|
||||
process() {
|
||||
// 玩家移动时标记为脏
|
||||
if (playerInput.moved) {
|
||||
dirtyTracking.markDirty(player, PositionComponent);
|
||||
}
|
||||
|
||||
// 静止的敌人不会被标记为脏,渲染系统会跳过它们
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 事件系统
|
||||
|
||||
### 类型安全事件
|
||||
|
||||
**传统事件的问题:**
|
||||
```typescript
|
||||
// 类型不安全,容易出错
|
||||
eventEmitter.emit('player_died', playerData);
|
||||
eventEmitter.on('player_dead', handler); // 事件名拼写错误!
|
||||
```
|
||||
|
||||
**类型安全事件的解决方案:**
|
||||
```typescript
|
||||
// 编译时检查,避免错误
|
||||
enum GameEvents {
|
||||
PLAYER_DIED = 'player:died',
|
||||
LEVEL_COMPLETED = 'level:completed'
|
||||
}
|
||||
|
||||
eventBus.emit(GameEvents.PLAYER_DIED, { playerId: 123 });
|
||||
eventBus.on(GameEvents.PLAYER_DIED, (data) => {
|
||||
// data 类型自动推断
|
||||
});
|
||||
```
|
||||
|
||||
### 事件装饰器
|
||||
|
||||
**什么是装饰器?**
|
||||
装饰器让你用简单的语法自动注册事件监听器。
|
||||
|
||||
**传统方式:**
|
||||
```typescript
|
||||
class GameManager {
|
||||
constructor() {
|
||||
// 手动注册事件
|
||||
eventBus.on('entity:created', this.onEntityCreated.bind(this));
|
||||
eventBus.on('entity:destroyed', this.onEntityDestroyed.bind(this));
|
||||
eventBus.on('component:added', this.onComponentAdded.bind(this));
|
||||
}
|
||||
|
||||
onEntityCreated(data) { ... }
|
||||
onEntityDestroyed(data) { ... }
|
||||
onComponentAdded(data) { ... }
|
||||
}
|
||||
```
|
||||
|
||||
**装饰器方式:**
|
||||
```typescript
|
||||
class GameManager {
|
||||
@EventHandler('entity:created')
|
||||
onEntityCreated(data) { ... } // 自动注册
|
||||
|
||||
@EventHandler('entity:destroyed')
|
||||
onEntityDestroyed(data) { ... } // 自动注册
|
||||
|
||||
@EventHandler('component:added')
|
||||
onComponentAdded(data) { ... } // 自动注册
|
||||
}
|
||||
```
|
||||
|
||||
**应用场景:**
|
||||
- 游戏状态管理
|
||||
- UI更新响应
|
||||
- 音效播放触发
|
||||
- 成就系统检查
|
||||
|
||||
## 实体管理
|
||||
|
||||
### 实体生命周期
|
||||
|
||||
**创建实体的不同方式:**
|
||||
```typescript
|
||||
// 单个创建 - 适用于重要实体
|
||||
const player = scene.createEntity("Player");
|
||||
|
||||
// 批量创建 - 适用于大量相似实体
|
||||
const bullets = scene.createEntities(100, "Bullet");
|
||||
|
||||
// 延迟创建 - 避免性能峰值
|
||||
// 分批创建大量实体以避免单帧卡顿
|
||||
for (let i = 0; i < 100; i++) {
|
||||
setTimeout(() => {
|
||||
const batch = scene.createEntities(10, "Enemy");
|
||||
// 配置批次实体...
|
||||
}, i * 16); // 每16ms创建一批
|
||||
}
|
||||
```
|
||||
|
||||
### 查询系统
|
||||
|
||||
**流式API的优势:**
|
||||
```typescript
|
||||
// 传统方式:复杂的条件判断
|
||||
const result = [];
|
||||
for (const entity of entities) {
|
||||
if (entity.has(Position) &&
|
||||
entity.has(Velocity) &&
|
||||
!entity.has(Frozen) &&
|
||||
entity.tag === EntityTag.ENEMY) {
|
||||
result.push(entity);
|
||||
}
|
||||
}
|
||||
|
||||
// 流式API:清晰表达意图
|
||||
const result = entityManager
|
||||
.query()
|
||||
.withAll(Position, Velocity)
|
||||
.withNone(Frozen)
|
||||
.withTag(EntityTag.ENEMY)
|
||||
.execute();
|
||||
```
|
||||
|
||||
### 批量操作
|
||||
|
||||
**为什么需要批量操作?**
|
||||
```typescript
|
||||
// 慢的方式:逐个处理
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const bullet = createEntity();
|
||||
bullet.addComponent(new PositionComponent());
|
||||
bullet.addComponent(new VelocityComponent());
|
||||
}
|
||||
|
||||
// 快的方式:批量处理
|
||||
const bullets = scene.createEntities(1000, "Bullet");
|
||||
bullets.forEach(bullet => {
|
||||
bullet.addComponent(new PositionComponent());
|
||||
bullet.addComponent(new VelocityComponent());
|
||||
});
|
||||
```
|
||||
|
||||
**应用场景:**
|
||||
- 生成大量子弹/粒子
|
||||
- 加载关卡时创建大量实体
|
||||
- 清理场景时删除大量实体
|
||||
|
||||
## 性能建议
|
||||
|
||||
### 什么时候使用这些优化?
|
||||
|
||||
| 实体数量 | 推荐配置 | 说明 |
|
||||
|---------|---------|------|
|
||||
| < 1,000 | 默认配置 | 简单场景,不需要特殊优化 |
|
||||
| 1,000 - 10,000 | 启用组件索引 | 中等规模,索引提升查询速度 |
|
||||
| 10,000 - 50,000 | 启用Archetype | 大规模场景,分组优化 |
|
||||
| > 50,000 | 全部优化 | 超大规模,需要所有优化技术 |
|
||||
|
||||
### 常见使用误区
|
||||
|
||||
**错误:过度优化**
|
||||
```typescript
|
||||
// 不要在小项目中使用所有优化
|
||||
const entityManager = new EntityManager();
|
||||
entityManager.enableAllOptimizations(); // 小项目不需要
|
||||
```
|
||||
|
||||
**正确:按需优化**
|
||||
```typescript
|
||||
// 根据实际需求选择优化
|
||||
if (entityCount > 10000) {
|
||||
entityManager.enableArchetypeSystem();
|
||||
}
|
||||
if (hasFrequentQueries) {
|
||||
entityManager.enableComponentIndex();
|
||||
}
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
这些技术概念可能看起来复杂,但它们解决的都是实际开发中的具体问题:
|
||||
|
||||
1. **ECS架构** - 让代码更灵活、可维护
|
||||
2. **组件索引** - 让查询更快速
|
||||
3. **Archetype系统** - 让批量操作更高效
|
||||
4. **脏标记系统** - 让更新更智能
|
||||
5. **事件系统** - 让组件间通信更安全
|
||||
6. **实体管理** - 让大规模场景成为可能
|
||||
|
||||
从简单的场景开始,随着项目复杂度增加,逐步引入这些优化技术。
|
||||
|
||||
## 框架类型系统
|
||||
|
||||
### TypeScript接口设计
|
||||
|
||||
ECS框架采用了精简的TypeScript接口设计,提供类型安全保障的同时保持实现的灵活性。
|
||||
|
||||
#### 核心接口
|
||||
|
||||
**IComponent接口**
|
||||
```typescript
|
||||
interface IComponent {
|
||||
readonly id: number;
|
||||
enabled: boolean;
|
||||
updateOrder: number;
|
||||
|
||||
onAddedToEntity(): void;
|
||||
onRemovedFromEntity(): void;
|
||||
onEnabled(): void;
|
||||
onDisabled(): void;
|
||||
update(): void;
|
||||
}
|
||||
```
|
||||
- 定义所有组件的基本契约
|
||||
- Component基类实现此接口
|
||||
- 确保组件生命周期方法的一致性
|
||||
|
||||
**ISystemBase接口**
|
||||
```typescript
|
||||
interface ISystemBase {
|
||||
readonly systemName: string;
|
||||
readonly entities: readonly any[];
|
||||
updateOrder: number;
|
||||
enabled: boolean;
|
||||
|
||||
initialize(): void;
|
||||
update(): void;
|
||||
lateUpdate?(): void;
|
||||
}
|
||||
```
|
||||
- 为EntitySystem类提供类型约束
|
||||
- 定义系统的核心执行方法
|
||||
- 支持可选的延迟更新
|
||||
|
||||
**IEventBus接口**
|
||||
```typescript
|
||||
interface IEventBus {
|
||||
emit<T>(eventType: string, data: T): void;
|
||||
emitAsync<T>(eventType: string, data: T): Promise<void>;
|
||||
on<T>(eventType: string, handler: (data: T) => void, config?: IEventListenerConfig): string;
|
||||
// ... 其他事件方法
|
||||
}
|
||||
```
|
||||
- 提供类型安全的事件系统契约
|
||||
- 支持同步和异步事件处理
|
||||
- EventBus类完整实现此接口
|
||||
|
||||
#### 事件数据接口
|
||||
|
||||
**事件数据层次结构**
|
||||
```typescript
|
||||
// 基础事件数据
|
||||
interface IEventData {
|
||||
timestamp: number;
|
||||
source?: string;
|
||||
eventId?: string;
|
||||
}
|
||||
|
||||
// 实体相关事件
|
||||
interface IEntityEventData extends IEventData {
|
||||
entityId: number;
|
||||
entityName?: string;
|
||||
entityTag?: string;
|
||||
}
|
||||
|
||||
// 组件相关事件
|
||||
interface IComponentEventData extends IEntityEventData {
|
||||
componentType: string;
|
||||
component?: IComponent;
|
||||
}
|
||||
```
|
||||
- 清晰的继承层次
|
||||
- 类型安全的事件数据传递
|
||||
- 便于事件处理器的实现
|
||||
|
||||
#### 类型别名
|
||||
|
||||
**ComponentType<T>**
|
||||
```typescript
|
||||
type ComponentType<T extends IComponent = IComponent> = new (...args: any[]) => T;
|
||||
```
|
||||
- 用于类型安全的组件操作
|
||||
- 支持泛型约束
|
||||
- 广泛用于实体和查询系统
|
||||
|
||||
### 设计原则
|
||||
|
||||
#### 1. 接口简化原则
|
||||
- 只保留实际使用的接口
|
||||
- 移除了未使用的复杂接口(如IEntityManager、IEntityQueryBuilder等)
|
||||
- 减少认知负担,提高开发效率
|
||||
|
||||
#### 2. 实现灵活性原则
|
||||
- 接口作为类型约束而非强制实现
|
||||
- 允许具体类有更丰富的实现
|
||||
- 保持向后兼容性
|
||||
|
||||
#### 3. 类型安全原则
|
||||
- 编译时类型检查
|
||||
- 泛型支持提供精确的类型推断
|
||||
- 事件系统的完整类型安全
|
||||
|
||||
### 使用指南
|
||||
|
||||
#### 在项目中使用接口
|
||||
```typescript
|
||||
// 作为类型约束
|
||||
function processComponent<T extends IComponent>(component: T) {
|
||||
if (component.enabled) {
|
||||
component.update();
|
||||
}
|
||||
}
|
||||
|
||||
// 作为参数类型
|
||||
function registerSystem(system: ISystemBase) {
|
||||
scene.addEntityProcessor(system);
|
||||
}
|
||||
|
||||
// 作为泛型约束
|
||||
function getComponent<T extends IComponent>(type: ComponentType<T>): T | null {
|
||||
return entity.getComponent(type);
|
||||
}
|
||||
```
|
||||
|
||||
#### 扩展框架接口
|
||||
```typescript
|
||||
// 如果需要扩展组件接口
|
||||
interface IAdvancedComponent extends IComponent {
|
||||
priority: number;
|
||||
category: string;
|
||||
}
|
||||
|
||||
class AdvancedComponent extends Component implements IAdvancedComponent {
|
||||
public priority: number = 0;
|
||||
public category: string = "default";
|
||||
|
||||
// 实现基础接口方法
|
||||
}
|
||||
```
|
||||
|
||||
### 接口维护
|
||||
|
||||
当前的接口设计已经过精心清理,包含:
|
||||
- **12个核心接口** - 涵盖组件、系统、事件等核心概念
|
||||
- **0个冗余接口** - 移除了所有未使用的接口定义
|
||||
- **完整的类型覆盖** - 为所有主要功能提供类型支持
|
||||
|
||||
这种设计确保了框架的类型安全性,同时保持了代码的简洁性和可维护性。
|
||||
@@ -1,620 +0,0 @@
|
||||
# 核心 API 参考
|
||||
|
||||
本文档详细介绍 ECS Framework 的核心 API 和使用方法。
|
||||
|
||||
> 🤔 **不熟悉ECS概念?** 建议先阅读 [技术概念详解](concepts-explained.md) 了解ECS架构基础和性能优化原理
|
||||
|
||||
## ECS 架构概述
|
||||
|
||||
ECS 架构将传统的面向对象设计分解为三个核心部分:
|
||||
|
||||
- **Entity(实体)** - 游戏世界中的对象,包含基本属性如位置、旋转、缩放
|
||||
- **Component(组件)** - 包含数据和行为的功能模块
|
||||
- **System(系统)** - 处理实体集合的逻辑处理单元
|
||||
|
||||
## Core(核心)
|
||||
|
||||
Core 是框架的核心管理类,负责游戏的生命周期管理。
|
||||
|
||||
### 创建和配置
|
||||
|
||||
```typescript
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
// 创建核心实例(调试模式)
|
||||
const core = Core.create(true);
|
||||
|
||||
// 创建核心实例(发布模式)
|
||||
const core = Core.create(false);
|
||||
```
|
||||
|
||||
### 事件系统
|
||||
|
||||
```typescript
|
||||
import { CoreEvents } from '@esengine/ecs-framework';
|
||||
|
||||
// 监听核心事件
|
||||
Core.emitter.addObserver(CoreEvents.frameUpdated, this.onUpdate, this);
|
||||
|
||||
// 发送帧更新事件
|
||||
Core.emitter.emit(CoreEvents.frameUpdated);
|
||||
|
||||
// 发送自定义事件
|
||||
Core.emitter.emit("customEvent", { data: "value" });
|
||||
```
|
||||
|
||||
### 定时器系统
|
||||
|
||||
```typescript
|
||||
// 延迟执行
|
||||
Core.schedule(2.0, false, this, (timer) => {
|
||||
console.log("2秒后执行");
|
||||
});
|
||||
|
||||
// 重复执行
|
||||
Core.schedule(1.0, true, this, (timer) => {
|
||||
console.log("每秒执行一次");
|
||||
});
|
||||
```
|
||||
|
||||
## Scene(场景)
|
||||
|
||||
场景是游戏世界的容器,管理实体和系统的生命周期。
|
||||
|
||||
### 创建和使用场景
|
||||
|
||||
```typescript
|
||||
import { Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// 创建场景
|
||||
const scene = new Scene();
|
||||
scene.name = "GameScene";
|
||||
|
||||
// 设置为当前场景
|
||||
Core.scene = scene;
|
||||
|
||||
// 场景生命周期
|
||||
scene.begin(); // 开始场景
|
||||
scene.update(); // 更新场景
|
||||
scene.end(); // 结束场景
|
||||
```
|
||||
|
||||
### 批量实体管理
|
||||
|
||||
```typescript
|
||||
// 批量创建实体 - 高性能
|
||||
const entities = scene.createEntities(1000, "Enemy");
|
||||
|
||||
// 批量添加实体(延迟缓存清理)
|
||||
entities.forEach(entity => {
|
||||
scene.addEntity(entity, false); // 延迟清理
|
||||
});
|
||||
scene.querySystem.clearCache(); // 手动清理缓存
|
||||
|
||||
// 获取性能统计
|
||||
const stats = scene.getStats();
|
||||
console.log(`实体数量: ${stats.entityCount}`);
|
||||
```
|
||||
|
||||
## Entity(实体)
|
||||
|
||||
实体是游戏世界中的基本对象,包含位置、旋转、缩放等基本属性。
|
||||
|
||||
### 实体的基本属性
|
||||
|
||||
```typescript
|
||||
const entity = scene.createEntity("MyEntity");
|
||||
|
||||
// 标签(用于分类)
|
||||
entity.tag = 1;
|
||||
|
||||
// 启用状态
|
||||
entity.enabled = true;
|
||||
|
||||
// 活跃状态
|
||||
entity.active = true;
|
||||
|
||||
// 更新顺序
|
||||
entity.updateOrder = 10;
|
||||
|
||||
// 注意:框架专注于ECS架构,不提供Transform相关功能
|
||||
// 位置、旋转、缩放等Transform功能需要通过组件实现
|
||||
class TransformComponent extends Component {
|
||||
public x: number = 0;
|
||||
public y: number = 0;
|
||||
public rotation: number = 0;
|
||||
public scaleX: number = 1;
|
||||
public scaleY: number = 1;
|
||||
}
|
||||
|
||||
// 使用Transform组件
|
||||
const transform = entity.addComponent(new TransformComponent());
|
||||
transform.x = 100;
|
||||
transform.y = 200;
|
||||
transform.rotation = Math.PI / 4;
|
||||
```
|
||||
|
||||
### 实体层级关系
|
||||
|
||||
```typescript
|
||||
// 添加子实体
|
||||
const parent = scene.createEntity("Parent");
|
||||
const child = scene.createEntity("Child");
|
||||
parent.addChild(child);
|
||||
|
||||
// 获取父实体
|
||||
const parentEntity = child.parent;
|
||||
|
||||
// 获取所有子实体
|
||||
const children = parent.children;
|
||||
|
||||
// 查找子实体
|
||||
const foundChild = parent.findChild("Child");
|
||||
|
||||
// 按标签查找子实体
|
||||
const taggedChildren = parent.findChildrenByTag(1);
|
||||
|
||||
// 移除子实体
|
||||
parent.removeChild(child);
|
||||
|
||||
// 移除所有子实体
|
||||
parent.removeAllChildren();
|
||||
```
|
||||
|
||||
### 实体生命周期
|
||||
|
||||
```typescript
|
||||
// 检查实体是否被销毁
|
||||
if (!entity.isDestroyed) {
|
||||
// 实体仍然有效
|
||||
}
|
||||
|
||||
// 销毁实体
|
||||
entity.destroy();
|
||||
|
||||
// 获取实体调试信息
|
||||
const debugInfo = entity.getDebugInfo();
|
||||
console.log(debugInfo);
|
||||
```
|
||||
|
||||
## Component(组件)
|
||||
|
||||
组件包含数据和行为,定义了实体的特性和能力。
|
||||
|
||||
### 创建组件
|
||||
|
||||
```typescript
|
||||
import { Component } from '@esengine/ecs-framework';
|
||||
|
||||
class HealthComponent extends Component {
|
||||
public maxHealth: number = 100;
|
||||
public currentHealth: number = 100;
|
||||
|
||||
public takeDamage(damage: number) {
|
||||
this.currentHealth -= damage;
|
||||
if (this.currentHealth <= 0) {
|
||||
this.entity.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
public heal(amount: number) {
|
||||
this.currentHealth = Math.min(this.maxHealth, this.currentHealth + amount);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 组件生命周期
|
||||
|
||||
```typescript
|
||||
class MyComponent extends Component {
|
||||
public onAddedToEntity() {
|
||||
// 组件被添加到实体时调用
|
||||
console.log("组件已添加到实体:", this.entity.name);
|
||||
}
|
||||
|
||||
public onRemovedFromEntity() {
|
||||
// 组件从实体移除时调用
|
||||
console.log("组件已从实体移除");
|
||||
}
|
||||
|
||||
public onEnabled() {
|
||||
// 组件启用时调用
|
||||
console.log("组件已启用");
|
||||
}
|
||||
|
||||
public onDisabled() {
|
||||
// 组件禁用时调用
|
||||
console.log("组件已禁用");
|
||||
}
|
||||
|
||||
public update() {
|
||||
// 每帧更新(如果组件启用)
|
||||
console.log("组件更新");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 组件管理
|
||||
|
||||
```typescript
|
||||
// 添加组件
|
||||
const health = entity.addComponent(new HealthComponent());
|
||||
|
||||
// 创建并添加组件
|
||||
const movement = entity.createComponent(MovementComponent, 200); // 传递构造参数
|
||||
|
||||
// 获取组件
|
||||
const healthComp = entity.getComponent(HealthComponent);
|
||||
|
||||
// 检查组件是否存在
|
||||
if (entity.hasComponent(HealthComponent)) {
|
||||
// 处理逻辑
|
||||
}
|
||||
|
||||
// 获取或创建组件
|
||||
const weapon = entity.getOrCreateComponent(WeaponComponent);
|
||||
|
||||
// 获取多个同类型组件
|
||||
const allHealthComps = entity.getComponents(HealthComponent);
|
||||
|
||||
// 移除组件
|
||||
entity.removeComponent(healthComp);
|
||||
|
||||
// 按类型移除组件
|
||||
entity.removeComponentByType(HealthComponent);
|
||||
|
||||
// 移除所有组件
|
||||
entity.removeAllComponents();
|
||||
```
|
||||
|
||||
### 组件对象池优化
|
||||
|
||||
```typescript
|
||||
import { Component, ComponentPoolManager } from '@esengine/ecs-framework';
|
||||
|
||||
class BulletComponent extends Component {
|
||||
public damage: number = 10;
|
||||
public speed: number = 300;
|
||||
|
||||
// 对象池重置方法
|
||||
public reset() {
|
||||
this.damage = 10;
|
||||
this.speed = 300;
|
||||
}
|
||||
}
|
||||
|
||||
// 注册组件池
|
||||
ComponentPoolManager.getInstance().registerPool(
|
||||
'BulletComponent',
|
||||
() => new BulletComponent(),
|
||||
(bullet) => bullet.reset(),
|
||||
1000
|
||||
);
|
||||
|
||||
// 使用对象池获取组件
|
||||
const bullet = ComponentPoolManager.getInstance().acquireComponent('BulletComponent');
|
||||
if (bullet) {
|
||||
entity.addComponent(bullet);
|
||||
}
|
||||
|
||||
// 释放组件回对象池
|
||||
ComponentPoolManager.getInstance().releaseComponent('BulletComponent', bullet);
|
||||
|
||||
// 预热所有组件池
|
||||
ComponentPoolManager.getInstance().prewarmAll(100);
|
||||
|
||||
// 获取池统计
|
||||
const stats = ComponentPoolManager.getInstance().getPoolStats();
|
||||
console.log('组件池统计:', stats);
|
||||
```
|
||||
|
||||
## Scene(场景)
|
||||
|
||||
场景是实体和系统的容器,管理游戏世界的状态。
|
||||
|
||||
### 场景生命周期
|
||||
|
||||
```typescript
|
||||
import { Scene } from '@esengine/ecs-framework';
|
||||
|
||||
class GameScene extends Scene {
|
||||
public initialize() {
|
||||
// 场景初始化,创建实体和系统
|
||||
this.setupEntities();
|
||||
this.setupSystems();
|
||||
}
|
||||
|
||||
public onStart() {
|
||||
// 场景开始运行时调用
|
||||
console.log("场景开始");
|
||||
}
|
||||
|
||||
public unload() {
|
||||
// 场景卸载时调用
|
||||
console.log("场景卸载");
|
||||
}
|
||||
|
||||
private setupEntities() {
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new PlayerComponent());
|
||||
}
|
||||
|
||||
private setupSystems() {
|
||||
this.addEntityProcessor(new MovementSystem());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 实体管理
|
||||
|
||||
```typescript
|
||||
// 创建实体
|
||||
const entity = scene.createEntity("MyEntity");
|
||||
|
||||
// 添加现有实体
|
||||
scene.addEntity(entity);
|
||||
|
||||
// 查找实体
|
||||
const player = scene.findEntity("Player");
|
||||
const entityById = scene.findEntityById(123);
|
||||
const entitiesByTag = scene.findEntitiesByTag(1);
|
||||
|
||||
// 销毁所有实体
|
||||
scene.destroyAllEntities();
|
||||
|
||||
// 获取场景统计信息
|
||||
const stats = scene.getStats();
|
||||
console.log("实体数量:", stats.entityCount);
|
||||
console.log("系统数量:", stats.processorCount);
|
||||
```
|
||||
|
||||
## System(系统)
|
||||
|
||||
系统处理实体集合,实现游戏的核心逻辑。
|
||||
|
||||
### EntitySystem
|
||||
|
||||
最常用的系统类型,处理实体集合:
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, Entity, Matcher } from '@esengine/ecs-framework';
|
||||
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(MovementComponent));
|
||||
}
|
||||
|
||||
protected process(entities: Entity[]) {
|
||||
for (const entity of entities) {
|
||||
const movement = entity.getComponent(MovementComponent);
|
||||
if (movement) {
|
||||
movement.update();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ProcessingSystem
|
||||
|
||||
定期处理的系统:
|
||||
|
||||
```typescript
|
||||
import { ProcessingSystem, Time, Matcher } from '@esengine/ecs-framework';
|
||||
|
||||
class HealthRegenerationSystem extends ProcessingSystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(HealthComponent));
|
||||
}
|
||||
|
||||
public processSystem() {
|
||||
// ProcessingSystem不处理具体实体,而是执行全局逻辑
|
||||
// 如果需要处理实体,应该使用EntitySystem
|
||||
this.regenerateAllPlayerHealth();
|
||||
}
|
||||
|
||||
private regenerateAllPlayerHealth() {
|
||||
// 通过场景查找所有玩家实体并恢复生命值
|
||||
const players = this.scene.findEntitiesByTag(PlayerTag);
|
||||
for (const player of players) {
|
||||
const health = player.getComponent(HealthComponent);
|
||||
if (health && health.currentHealth < health.maxHealth) {
|
||||
health.currentHealth += 10 * Time.deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### IntervalSystem
|
||||
|
||||
按时间间隔执行的系统:
|
||||
|
||||
```typescript
|
||||
import { IntervalSystem, Matcher } from '@esengine/ecs-framework';
|
||||
|
||||
class SpawnSystem extends IntervalSystem {
|
||||
constructor() {
|
||||
// IntervalSystem需要Matcher和间隔时间
|
||||
super(Matcher.empty(), 3.0); // 每3秒执行一次
|
||||
}
|
||||
|
||||
protected process(entities: Entity[]) {
|
||||
// 生成敌人
|
||||
const enemy = this.scene.createEntity("Enemy");
|
||||
enemy.addComponent(new EnemyComponent());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### PassiveSystem
|
||||
|
||||
被动系统,不自动处理实体:
|
||||
|
||||
```typescript
|
||||
import { PassiveSystem, Matcher } from '@esengine/ecs-framework';
|
||||
|
||||
class CollisionSystem extends PassiveSystem {
|
||||
constructor() {
|
||||
super(Matcher.empty());
|
||||
}
|
||||
|
||||
public checkCollisions() {
|
||||
// 手动调用的碰撞检测逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Time(时间)
|
||||
|
||||
时间管理工具类,提供游戏时间相关功能:
|
||||
|
||||
```typescript
|
||||
import { Time } from '@esengine/ecs-framework';
|
||||
|
||||
// 获取时间信息
|
||||
console.log("帧时间:", Time.deltaTime);
|
||||
console.log("总时间:", Time.totalTime);
|
||||
console.log("帧数:", Time.frameCount);
|
||||
console.log("时间缩放:", Time.timeScale);
|
||||
|
||||
// 设置时间缩放(慢动作效果)
|
||||
Time.timeScale = 0.5;
|
||||
|
||||
// 检查时间间隔
|
||||
if (Time.checkEvery(1.0, lastCheckTime)) {
|
||||
// 每秒执行一次
|
||||
}
|
||||
```
|
||||
|
||||
## 性能监控
|
||||
|
||||
框架内置性能监控工具:
|
||||
|
||||
```typescript
|
||||
import { PerformanceMonitor } from '@esengine/ecs-framework';
|
||||
|
||||
// 获取性能监控实例
|
||||
const monitor = PerformanceMonitor.instance;
|
||||
|
||||
// 查看性能数据
|
||||
console.log("平均FPS:", monitor.averageFPS);
|
||||
console.log("最小FPS:", monitor.minFPS);
|
||||
console.log("最大FPS:", monitor.maxFPS);
|
||||
console.log("内存使用:", monitor.memoryUsage);
|
||||
|
||||
// 重置性能数据
|
||||
monitor.reset();
|
||||
```
|
||||
|
||||
## 对象池
|
||||
|
||||
内存管理优化工具:
|
||||
|
||||
```typescript
|
||||
import { Pool, IPoolable } from '@esengine/ecs-framework';
|
||||
|
||||
// 定义可池化的对象(需要实现IPoolable接口)
|
||||
class Bullet implements IPoolable {
|
||||
public x: number = 0;
|
||||
public y: number = 0;
|
||||
public speed: number = 0;
|
||||
|
||||
// 重置对象状态,准备重用
|
||||
public reset(): void {
|
||||
this.x = 0;
|
||||
this.y = 0;
|
||||
this.speed = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建对象池
|
||||
const bulletPool = new Pool<Bullet>(() => new Bullet(), 100);
|
||||
|
||||
// 预热对象池
|
||||
bulletPool.warmUp(20);
|
||||
|
||||
// 使用对象池
|
||||
const bullet = bulletPool.obtain();
|
||||
bullet.x = 100;
|
||||
bullet.y = 200;
|
||||
bullet.speed = 500;
|
||||
|
||||
// 使用完后归还到池中
|
||||
bulletPool.free(bullet);
|
||||
|
||||
// 查看池统计信息
|
||||
console.log(bulletPool.getStats());
|
||||
|
||||
// 清空对象池
|
||||
bulletPool.clear();
|
||||
|
||||
// 使用静态方法(自动管理池)
|
||||
const bullet2 = Pool.obtain(Bullet);
|
||||
Pool.free(Bullet, bullet2);
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 实体设计
|
||||
|
||||
- 实体只包含基本属性,功能通过组件实现
|
||||
- 合理使用实体层级关系
|
||||
- 及时销毁不需要的实体
|
||||
|
||||
### 2. 组件设计
|
||||
|
||||
- 组件保持单一职责
|
||||
- 使用生命周期方法进行初始化和清理
|
||||
- 避免组件间直接依赖
|
||||
|
||||
### 3. 系统设计
|
||||
|
||||
- 系统专注于特定逻辑处理
|
||||
- 合理设置系统更新顺序
|
||||
- 使用被动系统处理特殊逻辑
|
||||
|
||||
### 4. 性能优化
|
||||
|
||||
- 使用对象池减少内存分配
|
||||
- 监控性能数据
|
||||
- 合理使用时间缩放
|
||||
|
||||
## 高级性能优化功能
|
||||
|
||||
### 查询系统优化
|
||||
|
||||
框架内部已集成查询优化,无需手动配置。查询系统会自动使用最优的算法:
|
||||
|
||||
```typescript
|
||||
// 查询系统会自动优化这些操作
|
||||
const movingEntities = scene.querySystem.queryAll(PositionComponent, VelocityComponent);
|
||||
const renderableEntities = scene.querySystem.queryAll(PositionComponent, RenderComponent);
|
||||
|
||||
// 获取查询统计信息
|
||||
const queryStats = scene.querySystem.getStats();
|
||||
console.log('查询统计:', queryStats);
|
||||
```
|
||||
|
||||
### 批量操作API
|
||||
|
||||
```typescript
|
||||
// 批量创建实体 - 最高性能
|
||||
const entities = scene.createEntities(10000, "Bullets");
|
||||
|
||||
// 批量查询优化
|
||||
const movingEntities = scene.querySystem.queryAll(PositionComponent, VelocityComponent).entities;
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
ECS Framework 提供了完整的实体组件系统架构:
|
||||
|
||||
- **Core** 管理游戏生命周期和全局功能
|
||||
- **Entity** 作为游戏对象的基础容器
|
||||
- **Component** 实现具体的功能模块,支持对象池优化
|
||||
- **System** 处理游戏逻辑
|
||||
- **Scene** 管理游戏世界状态,支持批量操作
|
||||
- **高级优化** 位掩码优化器、组件对象池、批量操作等
|
||||
|
||||
通过合理使用这些核心概念和优化功能,可以构建出高性能、结构清晰、易于维护的游戏代码。
|
||||
@@ -1,370 +0,0 @@
|
||||
# 实体基础指南
|
||||
|
||||
本指南介绍实体(Entity)的基本概念和基础使用方法。
|
||||
|
||||
> 📖 **需要高级实体管理?** 请参考 [EntityManager 指南](entity-manager-example.md) 了解高性能查询和批量操作
|
||||
|
||||
## 实体概述
|
||||
|
||||
实体(Entity)是 ECS 架构中的核心概念之一,它作为组件的容器存在。实体本身不包含游戏逻辑,所有功能都通过添加不同的组件来实现。
|
||||
|
||||
### 实体的特点
|
||||
|
||||
- **轻量级容器**:实体只是组件的载体,不包含具体的游戏逻辑
|
||||
- **唯一标识**:每个实体都有唯一的ID和名称
|
||||
- **层次结构**:支持父子关系,可以构建复杂的实体层次
|
||||
- **高性能查询**:基于位掩码的组件查询系统
|
||||
- **生命周期管理**:完整的创建、更新、销毁流程
|
||||
|
||||
## 创建实体
|
||||
|
||||
### 基本创建方式
|
||||
|
||||
```typescript
|
||||
import { Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// 通过场景创建实体
|
||||
const scene = new Scene();
|
||||
const entity = scene.createEntity("Player");
|
||||
|
||||
console.log(entity.name); // "Player"
|
||||
console.log(entity.id); // 唯一的数字ID
|
||||
```
|
||||
|
||||
### 批量创建实体(推荐)
|
||||
|
||||
```typescript
|
||||
import { Scene } from '@esengine/ecs-framework';
|
||||
|
||||
const scene = new Scene();
|
||||
|
||||
// 批量创建1000个实体 - 高性能
|
||||
const entities = scene.createEntities(1000, "Enemy");
|
||||
|
||||
// 批量配置
|
||||
entities.forEach((entity, index) => {
|
||||
entity.tag = 2; // 敌人标签
|
||||
// 添加组件...
|
||||
});
|
||||
```
|
||||
|
||||
### 使用流式API创建
|
||||
|
||||
```typescript
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
// 使用ECS流式API
|
||||
const entity = Core.ecsAPI
|
||||
?.entity("Enemy")
|
||||
.withComponent(new PositionComponent(100, 200))
|
||||
.withComponent(new HealthComponent(50))
|
||||
.withTag(2)
|
||||
.build();
|
||||
```
|
||||
|
||||
## 实体属性
|
||||
|
||||
### 基本属性
|
||||
|
||||
```typescript
|
||||
// 实体名称 - 用于调试和标识
|
||||
entity.name = "Player";
|
||||
|
||||
// 实体ID - 只读,场景内唯一
|
||||
console.log(entity.id); // 例如: 1
|
||||
|
||||
// 标签 - 用于分类和快速查询
|
||||
entity.tag = 1; // 玩家标签
|
||||
entity.tag = 2; // 敌人标签
|
||||
|
||||
// 更新顺序 - 控制实体在系统中的处理优先级
|
||||
entity.updateOrder = 0; // 数值越小优先级越高
|
||||
```
|
||||
|
||||
### 状态控制
|
||||
|
||||
```typescript
|
||||
// 启用状态 - 控制实体是否参与更新和处理
|
||||
entity.enabled = true; // 启用实体
|
||||
entity.enabled = false; // 禁用实体
|
||||
|
||||
// 激活状态 - 控制实体及其子实体的活跃状态
|
||||
entity.active = true; // 激活实体
|
||||
entity.active = false; // 停用实体
|
||||
|
||||
// 检查层次结构中的激活状态
|
||||
if (entity.activeInHierarchy) {
|
||||
// 实体在整个层次结构中都是激活的
|
||||
}
|
||||
|
||||
// 检查销毁状态
|
||||
if (entity.isDestroyed) {
|
||||
// 实体已被销毁
|
||||
}
|
||||
```
|
||||
|
||||
### 更新间隔
|
||||
|
||||
```typescript
|
||||
// 控制实体更新频率
|
||||
entity.updateInterval = 1; // 每帧更新
|
||||
entity.updateInterval = 2; // 每2帧更新一次
|
||||
entity.updateInterval = 5; // 每5帧更新一次
|
||||
```
|
||||
|
||||
## 组件管理
|
||||
|
||||
### 添加组件
|
||||
|
||||
```typescript
|
||||
// 创建并添加组件
|
||||
const healthComponent = entity.addComponent(new HealthComponent(100));
|
||||
|
||||
// 使用工厂方法创建组件
|
||||
const positionComponent = entity.createComponent(PositionComponent, 100, 200);
|
||||
|
||||
// 批量添加组件
|
||||
const components = entity.addComponents([
|
||||
new PositionComponent(0, 0),
|
||||
new VelocityComponent(50, 0),
|
||||
new HealthComponent(100)
|
||||
]);
|
||||
```
|
||||
|
||||
### 获取组件
|
||||
|
||||
```typescript
|
||||
// 获取单个组件
|
||||
const health = entity.getComponent(HealthComponent);
|
||||
if (health) {
|
||||
console.log(`当前生命值: ${health.currentHealth}`);
|
||||
}
|
||||
|
||||
// 获取或创建组件(如果不存在则创建)
|
||||
const position = entity.getOrCreateComponent(PositionComponent, 0, 0);
|
||||
|
||||
// 获取多个同类型组件(如果组件可以重复添加)
|
||||
const allHealthComponents = entity.getComponents(HealthComponent);
|
||||
```
|
||||
|
||||
### 检查组件
|
||||
|
||||
```typescript
|
||||
// 检查是否拥有指定组件
|
||||
if (entity.hasComponent(HealthComponent)) {
|
||||
// 实体拥有生命值组件
|
||||
}
|
||||
|
||||
// 检查组件掩码(高性能)
|
||||
const mask = entity.componentMask;
|
||||
console.log(`组件掩码: ${mask.toString(2)}`); // 二进制表示
|
||||
```
|
||||
|
||||
### 移除组件
|
||||
|
||||
```typescript
|
||||
// 移除指定组件实例
|
||||
const healthComponent = entity.getComponent(HealthComponent);
|
||||
if (healthComponent) {
|
||||
entity.removeComponent(healthComponent);
|
||||
}
|
||||
|
||||
// 按类型移除组件
|
||||
const removedHealth = entity.removeComponentByType(HealthComponent);
|
||||
|
||||
// 批量移除组件
|
||||
const removedComponents = entity.removeComponentsByTypes([
|
||||
HealthComponent,
|
||||
VelocityComponent
|
||||
]);
|
||||
|
||||
// 移除所有组件
|
||||
entity.removeAllComponents();
|
||||
```
|
||||
|
||||
## 层次结构管理
|
||||
|
||||
### 父子关系
|
||||
|
||||
```typescript
|
||||
// 创建父子实体
|
||||
const player = scene.createEntity("Player");
|
||||
const weapon = scene.createEntity("Weapon");
|
||||
const shield = scene.createEntity("Shield");
|
||||
|
||||
// 添加子实体
|
||||
player.addChild(weapon);
|
||||
player.addChild(shield);
|
||||
|
||||
// 获取父实体
|
||||
console.log(weapon.parent === player); // true
|
||||
|
||||
// 获取所有子实体
|
||||
const children = player.children;
|
||||
console.log(children.length); // 2
|
||||
|
||||
// 获取子实体数量
|
||||
console.log(player.childCount); // 2
|
||||
```
|
||||
|
||||
### 查找子实体
|
||||
|
||||
```typescript
|
||||
// 按名称查找子实体
|
||||
const weapon = player.findChild("Weapon");
|
||||
|
||||
// 递归查找子实体
|
||||
const deepChild = player.findChild("DeepChild", true);
|
||||
|
||||
// 按标签查找子实体
|
||||
const enemies = player.findChildrenByTag(2); // 查找所有敌人标签的子实体
|
||||
|
||||
// 递归按标签查找
|
||||
const allEnemies = player.findChildrenByTag(2, true);
|
||||
```
|
||||
|
||||
### 层次结构操作
|
||||
|
||||
```typescript
|
||||
// 移除子实体
|
||||
const removed = player.removeChild(weapon);
|
||||
|
||||
// 移除所有子实体
|
||||
player.removeAllChildren();
|
||||
|
||||
// 获取根实体
|
||||
const root = weapon.getRoot();
|
||||
|
||||
// 检查祖先关系
|
||||
if (player.isAncestorOf(weapon)) {
|
||||
// player 是 weapon 的祖先
|
||||
}
|
||||
|
||||
// 检查后代关系
|
||||
if (weapon.isDescendantOf(player)) {
|
||||
// weapon 是 player 的后代
|
||||
}
|
||||
|
||||
// 获取实体在层次结构中的深度
|
||||
const depth = weapon.getDepth(); // 从根实体开始计算的深度
|
||||
```
|
||||
|
||||
### 遍历子实体
|
||||
|
||||
```typescript
|
||||
// 遍历直接子实体
|
||||
player.forEachChild((child, index) => {
|
||||
console.log(`子实体 ${index}: ${child.name}`);
|
||||
});
|
||||
|
||||
// 递归遍历所有子实体
|
||||
player.forEachChild((child, index) => {
|
||||
console.log(`子实体 ${index}: ${child.name} (深度: ${child.getDepth()})`);
|
||||
}, true);
|
||||
```
|
||||
|
||||
## 实体生命周期
|
||||
|
||||
### 更新循环
|
||||
|
||||
```typescript
|
||||
// 手动更新实体(通常由场景自动调用)
|
||||
entity.update();
|
||||
|
||||
// 实体会自动调用所有组件的update方法
|
||||
class MyComponent extends Component {
|
||||
public update(): void {
|
||||
// 组件更新逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 销毁实体
|
||||
|
||||
```typescript
|
||||
// 销毁实体
|
||||
entity.destroy();
|
||||
|
||||
// 检查是否已销毁
|
||||
if (entity.isDestroyed) {
|
||||
console.log("实体已被销毁");
|
||||
}
|
||||
|
||||
// 销毁实体时会自动:
|
||||
// 1. 移除所有组件
|
||||
// 2. 从父实体中移除
|
||||
// 3. 销毁所有子实体
|
||||
// 4. 从场景中移除
|
||||
```
|
||||
|
||||
# 高级特性请参考其他指南
|
||||
|
||||
> 📚 **更多功能:**
|
||||
> - **高性能查询和批量操作** → [EntityManager 指南](entity-manager-example.md)
|
||||
> - **性能优化技术** → [性能优化指南](performance-optimization.md)
|
||||
> - **组件索引和缓存** → [技术概念详解](concepts-explained.md)
|
||||
|
||||
## 基础最佳实践
|
||||
|
||||
### 1. 合理使用标签
|
||||
|
||||
```typescript
|
||||
// 定义标签常量
|
||||
const EntityTags = {
|
||||
PLAYER: 1,
|
||||
ENEMY: 2,
|
||||
PROJECTILE: 3,
|
||||
PICKUP: 4
|
||||
} as const;
|
||||
|
||||
// 使用标签进行分类
|
||||
player.tag = EntityTags.PLAYER;
|
||||
enemy.tag = EntityTags.ENEMY;
|
||||
```
|
||||
|
||||
### 2. 正确的销毁处理
|
||||
|
||||
```typescript
|
||||
// 确保正确销毁实体
|
||||
if (!entity.isDestroyed) {
|
||||
entity.destroy(); // 自动移除组件和层次关系
|
||||
}
|
||||
|
||||
// 检查实体状态
|
||||
if (entity.isDestroyed) {
|
||||
return; // 避免操作已销毁的实体
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 组件生命周期
|
||||
|
||||
```typescript
|
||||
// 正确添加组件
|
||||
const health = entity.addComponent(new HealthComponent(100));
|
||||
|
||||
// 安全获取组件
|
||||
const healthComp = entity.getComponent(HealthComponent);
|
||||
if (healthComp && healthComp.currentHealth <= 0) {
|
||||
entity.destroy();
|
||||
}
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 实体如何实现位置、旋转等变换?
|
||||
|
||||
A: 通过添加相应的组件:
|
||||
|
||||
```typescript
|
||||
class TransformComponent extends Component {
|
||||
public position = { x: 0, y: 0 };
|
||||
public rotation = 0;
|
||||
public scale = { x: 1, y: 1 };
|
||||
}
|
||||
|
||||
entity.addComponent(new TransformComponent());
|
||||
```
|
||||
|
||||
### Q: 实体可以在场景间移动吗?
|
||||
|
||||
A: 不可以。实体与场景绑定,需要在新场景中重新创建。
|
||||
@@ -1,370 +0,0 @@
|
||||
# EntityManager 使用指南
|
||||
|
||||
本文档详细介绍 EntityManager 的使用方法和最佳实践。
|
||||
|
||||
## 目录
|
||||
|
||||
1. [基础用法](#基础用法)
|
||||
2. [查询系统](#查询系统)
|
||||
3. [实体管理](#实体管理)
|
||||
4. [性能监控](#性能监控)
|
||||
5. [最佳实践](#最佳实践)
|
||||
|
||||
## 基础用法
|
||||
|
||||
### 创建 EntityManager
|
||||
|
||||
```typescript
|
||||
import { EntityManager, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// 创建场景和实体管理器
|
||||
const scene = new Scene();
|
||||
const entityManager = new EntityManager();
|
||||
|
||||
// 批量创建实体(使用Scene方法)
|
||||
const enemies = scene.createEntities(50, "Enemy");
|
||||
|
||||
// 为实体添加组件
|
||||
enemies.forEach((enemy, index) => {
|
||||
enemy.addComponent(new PositionComponent(
|
||||
Math.random() * 800,
|
||||
Math.random() * 600
|
||||
));
|
||||
enemy.addComponent(new HealthComponent(100));
|
||||
enemy.addComponent(new VelocityComponent(
|
||||
(Math.random() - 0.5) * 100,
|
||||
(Math.random() - 0.5) * 100
|
||||
));
|
||||
enemy.tag = 2; // 敌人标签
|
||||
});
|
||||
```
|
||||
|
||||
## 查询系统
|
||||
|
||||
### 基础查询
|
||||
|
||||
```typescript
|
||||
// 按组件类型查询
|
||||
const healthEntities = entityManager.getEntitiesWithComponent(HealthComponent);
|
||||
|
||||
// 按标签查询
|
||||
const enemies = entityManager.getEntitiesByTag(2);
|
||||
const players = entityManager.getEntitiesByTag(1);
|
||||
|
||||
// 按名称查询
|
||||
const boss = entityManager.getEntityByName("BossEnemy");
|
||||
|
||||
// 获取所有实体
|
||||
const allEntities = entityManager.getAllEntities();
|
||||
```
|
||||
|
||||
### 流式查询 API
|
||||
|
||||
```typescript
|
||||
// 复杂查询条件
|
||||
const movingEnemies = entityManager
|
||||
.query()
|
||||
.withAll(PositionComponent, VelocityComponent, HealthComponent)
|
||||
.withTag(2) // 敌人标签
|
||||
.execute();
|
||||
|
||||
// 查询活跃的玩家
|
||||
const activePlayers = entityManager
|
||||
.query()
|
||||
.withAll(PositionComponent)
|
||||
.withTag(1) // 玩家标签
|
||||
.active() // 只查询活跃实体
|
||||
.execute();
|
||||
|
||||
// 排除特定组件的实体
|
||||
const nonCombatEntities = entityManager
|
||||
.query()
|
||||
.withAll(PositionComponent)
|
||||
.without(WeaponComponent, HealthComponent)
|
||||
.execute();
|
||||
|
||||
// 自定义条件查询
|
||||
const nearbyEnemies = entityManager
|
||||
.query()
|
||||
.withAll(PositionComponent)
|
||||
.withTag(2)
|
||||
.where(entity => {
|
||||
const pos = entity.getComponent(PositionComponent);
|
||||
return pos && Math.abs(pos.x - playerX) < 100;
|
||||
})
|
||||
.execute();
|
||||
```
|
||||
|
||||
## 实体管理
|
||||
|
||||
### 创建和销毁实体
|
||||
|
||||
```typescript
|
||||
// 创建单个实体
|
||||
const player = entityManager.createEntity("Player");
|
||||
player.addComponent(new PositionComponent(400, 300));
|
||||
player.addComponent(new HealthComponent(100));
|
||||
player.tag = 1;
|
||||
|
||||
// 销毁实体
|
||||
entityManager.destroyEntity(player);
|
||||
|
||||
// 按名称销毁
|
||||
entityManager.destroyEntity("Enemy_1");
|
||||
|
||||
// 按ID销毁
|
||||
entityManager.destroyEntity(123);
|
||||
```
|
||||
|
||||
### 实体查找
|
||||
|
||||
```typescript
|
||||
// 按ID查找
|
||||
const entity = entityManager.getEntity(123);
|
||||
|
||||
// 按名称查找
|
||||
const player = entityManager.getEntityByName("Player");
|
||||
|
||||
// 检查实体是否存在
|
||||
if (entity && !entity.isDestroyed) {
|
||||
// 实体有效
|
||||
}
|
||||
```
|
||||
|
||||
## 性能监控
|
||||
|
||||
### 基础统计
|
||||
|
||||
```typescript
|
||||
// 获取实体数量
|
||||
console.log('总实体数:', entityManager.entityCount);
|
||||
console.log('活跃实体数:', entityManager.activeEntityCount);
|
||||
|
||||
// 获取场景统计
|
||||
const sceneStats = scene.getStats();
|
||||
console.log('场景统计:', {
|
||||
实体数量: sceneStats.entityCount,
|
||||
系统数量: sceneStats.processorCount
|
||||
});
|
||||
|
||||
// 获取查询系统统计
|
||||
const queryStats = scene.querySystem.getStats();
|
||||
console.log('查询统计:', queryStats);
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 高效查询
|
||||
|
||||
```typescript
|
||||
// ✅ 好的做法:缓存查询结果
|
||||
class CombatSystem extends EntitySystem {
|
||||
private cachedEnemies: Entity[] = [];
|
||||
private lastUpdateFrame = 0;
|
||||
|
||||
protected process(entities: Entity[]): void {
|
||||
// 每5帧更新一次缓存
|
||||
if (Time.frameCount - this.lastUpdateFrame > 5) {
|
||||
this.cachedEnemies = this.entityManager
|
||||
.query()
|
||||
.withAll(PositionComponent, HealthComponent)
|
||||
.withTag(2)
|
||||
.execute();
|
||||
this.lastUpdateFrame = Time.frameCount;
|
||||
}
|
||||
|
||||
// 使用缓存的结果
|
||||
this.cachedEnemies.forEach(enemy => {
|
||||
// 处理敌人逻辑
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 批量操作
|
||||
|
||||
```typescript
|
||||
// ✅ 好的做法:批量创建和配置
|
||||
function createBulletWave(count: number): Entity[] {
|
||||
// 使用Scene的批量创建
|
||||
const bullets = scene.createEntities(count, "Bullet");
|
||||
|
||||
// 批量配置组件
|
||||
bullets.forEach((bullet, index) => {
|
||||
const angle = (index / count) * Math.PI * 2;
|
||||
bullet.addComponent(new PositionComponent(400, 300));
|
||||
bullet.addComponent(new VelocityComponent(
|
||||
Math.cos(angle) * 200,
|
||||
Math.sin(angle) * 200
|
||||
));
|
||||
bullet.addComponent(new BulletComponent());
|
||||
bullet.tag = 3; // 子弹标签
|
||||
});
|
||||
|
||||
return bullets;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 内存管理
|
||||
|
||||
```typescript
|
||||
// ✅ 好的做法:及时清理无用实体
|
||||
class CleanupSystem extends EntitySystem {
|
||||
protected process(entities: Entity[]): void {
|
||||
// 清理超出边界的子弹
|
||||
const bullets = this.entityManager.getEntitiesByTag(3);
|
||||
bullets.forEach(bullet => {
|
||||
const pos = bullet.getComponent(PositionComponent);
|
||||
if (pos && (pos.x < -100 || pos.x > 900 || pos.y < -100 || pos.y > 700)) {
|
||||
this.entityManager.destroyEntity(bullet);
|
||||
}
|
||||
});
|
||||
|
||||
// 清理死亡的敌人
|
||||
const deadEnemies = this.entityManager
|
||||
.query()
|
||||
.withAll(HealthComponent)
|
||||
.withTag(2)
|
||||
.where(entity => {
|
||||
const health = entity.getComponent(HealthComponent);
|
||||
return health && health.currentHealth <= 0;
|
||||
})
|
||||
.execute();
|
||||
|
||||
deadEnemies.forEach(enemy => {
|
||||
this.entityManager.destroyEntity(enemy);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 查询优化
|
||||
|
||||
```typescript
|
||||
// ✅ 好的做法:使用合适的查询方法
|
||||
class GameSystem extends EntitySystem {
|
||||
findTargetsInRange(attacker: Entity, range: number): Entity[] {
|
||||
const attackerPos = attacker.getComponent(PositionComponent);
|
||||
if (!attackerPos) return [];
|
||||
|
||||
// 先按标签快速筛选,再按距离过滤
|
||||
return this.entityManager
|
||||
.getEntitiesByTag(2) // 敌人标签
|
||||
.filter(enemy => {
|
||||
const enemyPos = enemy.getComponent(PositionComponent);
|
||||
if (!enemyPos) return false;
|
||||
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(attackerPos.x - enemyPos.x, 2) +
|
||||
Math.pow(attackerPos.y - enemyPos.y, 2)
|
||||
);
|
||||
return distance <= range;
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 完整示例
|
||||
|
||||
```typescript
|
||||
import {
|
||||
EntityManager,
|
||||
Scene,
|
||||
Entity,
|
||||
Component,
|
||||
EntitySystem,
|
||||
Matcher
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
// 组件定义
|
||||
class PositionComponent extends Component {
|
||||
constructor(public x: number = 0, public y: number = 0) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
class HealthComponent extends Component {
|
||||
constructor(
|
||||
public maxHealth: number = 100,
|
||||
public currentHealth: number = 100
|
||||
) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
// 游戏管理器
|
||||
class GameManager {
|
||||
private scene: Scene;
|
||||
private entityManager: EntityManager;
|
||||
|
||||
constructor() {
|
||||
this.scene = new Scene();
|
||||
this.entityManager = new EntityManager();
|
||||
this.setupGame();
|
||||
}
|
||||
|
||||
private setupGame(): void {
|
||||
// 创建玩家
|
||||
const player = this.entityManager.createEntity("Player");
|
||||
player.addComponent(new PositionComponent(400, 300));
|
||||
player.addComponent(new HealthComponent(100));
|
||||
player.tag = 1;
|
||||
|
||||
// 创建敌人
|
||||
const enemies = this.scene.createEntities(10, "Enemy");
|
||||
enemies.forEach(enemy => {
|
||||
enemy.addComponent(new PositionComponent(
|
||||
Math.random() * 800,
|
||||
Math.random() * 600
|
||||
));
|
||||
enemy.addComponent(new HealthComponent(50));
|
||||
enemy.tag = 2;
|
||||
});
|
||||
|
||||
// 添加系统
|
||||
this.scene.addEntityProcessor(new HealthSystem());
|
||||
}
|
||||
|
||||
public update(): void {
|
||||
this.scene.update();
|
||||
|
||||
// 输出统计信息
|
||||
console.log('实体数量:', this.entityManager.entityCount);
|
||||
console.log('活跃实体数:', this.entityManager.activeEntityCount);
|
||||
}
|
||||
}
|
||||
|
||||
// 生命值系统
|
||||
class HealthSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(HealthComponent));
|
||||
}
|
||||
|
||||
protected process(entities: Entity[]): void {
|
||||
const healthEntities = this.scene.querySystem.queryAll(HealthComponent);
|
||||
|
||||
healthEntities.entities.forEach(entity => {
|
||||
const health = entity.getComponent(HealthComponent);
|
||||
if (health && health.currentHealth <= 0) {
|
||||
console.log(`实体 ${entity.name} 死亡`);
|
||||
entity.destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 启动游戏
|
||||
const game = new GameManager();
|
||||
setInterval(() => game.update(), 16); // 60 FPS
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
EntityManager 提供了强大的实体管理功能:
|
||||
|
||||
- **创建管理**:`createEntity()`, `destroyEntity()`
|
||||
- **查询功能**:`getEntitiesWithComponent()`, `getEntitiesByTag()`, `query()`
|
||||
- **实体查找**:`getEntity()`, `getEntityByName()`
|
||||
- **统计信息**:`entityCount`, `activeEntityCount`
|
||||
|
||||
通过合理使用这些API,可以构建高性能的游戏系统。记住要及时清理无用实体,缓存频繁查询的结果,并使用合适的查询方法来优化性能。
|
||||
@@ -1,496 +0,0 @@
|
||||
# ECS事件系统使用指南
|
||||
|
||||
本文档介绍如何使用ECS框架的增强事件系统,包括类型安全的事件发布订阅、预定义的ECS事件类型和高级功能。
|
||||
|
||||
## 目录
|
||||
|
||||
1. [基础用法](#基础用法)
|
||||
2. [预定义ECS事件](#预定义ecs事件)
|
||||
3. [事件装饰器](#事件装饰器)
|
||||
4. [高级功能](#高级功能)
|
||||
5. [性能优化](#性能优化)
|
||||
6. [最佳实践](#最佳实践)
|
||||
|
||||
## 基础用法
|
||||
|
||||
### 创建事件总线
|
||||
|
||||
```typescript
|
||||
import { EventBus, GlobalEventBus } from './ECS';
|
||||
|
||||
// 方式1:创建独立的事件总线
|
||||
const eventBus = new EventBus(true); // true启用调试模式
|
||||
|
||||
// 方式2:使用全局事件总线
|
||||
const globalEventBus = GlobalEventBus.getInstance(true);
|
||||
```
|
||||
|
||||
### 基本事件发布订阅
|
||||
|
||||
```typescript
|
||||
// 定义事件数据类型
|
||||
interface PlayerDiedEvent {
|
||||
playerId: number;
|
||||
cause: string;
|
||||
position: { x: number; y: number };
|
||||
}
|
||||
|
||||
// 监听事件
|
||||
const listenerId = eventBus.on<PlayerDiedEvent>('player:died', (data) => {
|
||||
console.log(`Player ${data.playerId} died at (${data.position.x}, ${data.position.y})`);
|
||||
console.log(`Cause: ${data.cause}`);
|
||||
});
|
||||
|
||||
// 发射事件
|
||||
eventBus.emit('player:died', {
|
||||
playerId: 123,
|
||||
cause: 'enemy_attack',
|
||||
position: { x: 100, y: 200 }
|
||||
});
|
||||
|
||||
// 移除监听器
|
||||
eventBus.off('player:died', listenerId);
|
||||
```
|
||||
|
||||
### 一次性事件监听
|
||||
|
||||
```typescript
|
||||
// 只监听一次
|
||||
eventBus.once<PlayerDiedEvent>('player:died', (data) => {
|
||||
console.log('This will only be called once');
|
||||
});
|
||||
```
|
||||
|
||||
### 异步事件处理
|
||||
|
||||
```typescript
|
||||
// 异步事件监听
|
||||
eventBus.onAsync<PlayerDiedEvent>('player:died', async (data) => {
|
||||
await savePlayerDeathToDatabase(data);
|
||||
await updateLeaderboard(data.playerId);
|
||||
});
|
||||
|
||||
// 异步事件发射
|
||||
await eventBus.emitAsync('player:died', playerData);
|
||||
```
|
||||
|
||||
## 预定义ECS事件
|
||||
|
||||
框架提供了完整的ECS事件类型定义,支持实体、组件、系统等核心概念的事件。
|
||||
|
||||
### 实体事件
|
||||
|
||||
```typescript
|
||||
import { ECSEventType, IEntityEventData } from './ECS';
|
||||
|
||||
// 监听实体创建事件
|
||||
eventBus.onEntityCreated((data: IEntityEventData) => {
|
||||
console.log(`Entity created: ${data.entityName} (ID: ${data.entityId})`);
|
||||
});
|
||||
|
||||
// 监听实体销毁事件
|
||||
eventBus.on<IEntityEventData>(ECSEventType.ENTITY_DESTROYED, (data) => {
|
||||
console.log(`Entity destroyed: ${data.entityName}`);
|
||||
});
|
||||
|
||||
// 手动发射实体事件
|
||||
eventBus.emitEntityCreated({
|
||||
timestamp: Date.now(),
|
||||
source: 'GameManager',
|
||||
entityId: 123,
|
||||
entityName: 'Player',
|
||||
entityTag: 'player'
|
||||
});
|
||||
```
|
||||
|
||||
### 组件事件
|
||||
|
||||
```typescript
|
||||
import { IComponentEventData } from './ECS';
|
||||
|
||||
// 监听组件添加事件
|
||||
eventBus.onComponentAdded((data: IComponentEventData) => {
|
||||
console.log(`Component ${data.componentType} added to entity ${data.entityId}`);
|
||||
});
|
||||
|
||||
// 监听组件移除事件
|
||||
eventBus.on<IComponentEventData>(ECSEventType.COMPONENT_REMOVED, (data) => {
|
||||
console.log(`Component ${data.componentType} removed from entity ${data.entityId}`);
|
||||
});
|
||||
```
|
||||
|
||||
### 系统事件
|
||||
|
||||
```typescript
|
||||
import { ISystemEventData } from './ECS';
|
||||
|
||||
// 监听系统错误
|
||||
eventBus.onSystemError((data: ISystemEventData) => {
|
||||
console.error(`System error in ${data.systemName}: ${data.systemType}`);
|
||||
});
|
||||
|
||||
// 监听系统处理开始/结束
|
||||
eventBus.on<ISystemEventData>(ECSEventType.SYSTEM_PROCESSING_START, (data) => {
|
||||
console.log(`System ${data.systemName} started processing`);
|
||||
});
|
||||
```
|
||||
|
||||
### 性能事件
|
||||
|
||||
```typescript
|
||||
import { IPerformanceEventData } from './ECS';
|
||||
|
||||
// 监听性能警告
|
||||
eventBus.onPerformanceWarning((data: IPerformanceEventData) => {
|
||||
console.warn(`Performance warning: ${data.operation} took ${data.executionTime}ms`);
|
||||
});
|
||||
|
||||
// 监听内存使用过高
|
||||
eventBus.on<IPerformanceEventData>(ECSEventType.MEMORY_USAGE_HIGH, (data) => {
|
||||
console.warn(`High memory usage: ${data.memoryUsage}MB`);
|
||||
});
|
||||
```
|
||||
|
||||
## 事件装饰器
|
||||
|
||||
使用装饰器可以自动注册事件监听器,简化代码编写。
|
||||
|
||||
### 基础装饰器
|
||||
|
||||
```typescript
|
||||
import { EventHandler, AsyncEventHandler, EventPriority } from './ECS';
|
||||
|
||||
class GameManager {
|
||||
@EventHandler(ECSEventType.ENTITY_CREATED, { priority: EventPriority.HIGH })
|
||||
onEntityCreated(data: IEntityEventData) {
|
||||
console.log(`New entity: ${data.entityName}`);
|
||||
}
|
||||
|
||||
@AsyncEventHandler(ECSEventType.ENTITY_DESTROYED)
|
||||
async onEntityDestroyed(data: IEntityEventData) {
|
||||
await this.cleanupEntityResources(data.entityId);
|
||||
}
|
||||
|
||||
@EventHandler('custom:game:event', { once: true })
|
||||
onGameStart(data: any) {
|
||||
console.log('Game started!');
|
||||
}
|
||||
|
||||
// 需要手动调用初始化方法
|
||||
constructor() {
|
||||
// 如果类有initEventListeners方法,会自动注册装饰器定义的监听器
|
||||
if (this.initEventListeners) {
|
||||
this.initEventListeners();
|
||||
}
|
||||
}
|
||||
|
||||
private async cleanupEntityResources(entityId: number) {
|
||||
// 清理实体相关资源
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 优先级和配置
|
||||
|
||||
```typescript
|
||||
class SystemManager {
|
||||
@EventHandler(ECSEventType.SYSTEM_ERROR, {
|
||||
priority: EventPriority.CRITICAL,
|
||||
context: this
|
||||
})
|
||||
handleSystemError(data: ISystemEventData) {
|
||||
this.logError(data);
|
||||
this.restartSystem(data.systemName);
|
||||
}
|
||||
|
||||
@AsyncEventHandler(ECSEventType.PERFORMANCE_WARNING, {
|
||||
priority: EventPriority.LOW,
|
||||
async: true
|
||||
})
|
||||
async handlePerformanceWarning(data: IPerformanceEventData) {
|
||||
await this.optimizePerformance(data);
|
||||
}
|
||||
|
||||
private logError(data: ISystemEventData) {
|
||||
// 错误日志记录
|
||||
}
|
||||
|
||||
private restartSystem(systemName: string) {
|
||||
// 重启系统
|
||||
}
|
||||
|
||||
private async optimizePerformance(data: IPerformanceEventData) {
|
||||
// 性能优化逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 高级功能
|
||||
|
||||
### 事件批处理
|
||||
|
||||
```typescript
|
||||
// 设置批处理配置
|
||||
eventBus.setBatchConfig('entity:update', 100, 16); // 批量100个,延迟16ms
|
||||
|
||||
// 发射事件(会被批处理)
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
eventBus.emit('entity:update', { entityId: i, data: 'update' });
|
||||
}
|
||||
|
||||
// 手动刷新批处理队列
|
||||
eventBus.flushBatch('entity:update');
|
||||
```
|
||||
|
||||
### 事件统计和监控
|
||||
|
||||
```typescript
|
||||
// 获取单个事件统计
|
||||
const stats = eventBus.getStats('entity:created');
|
||||
console.log(`Event triggered ${stats.triggerCount} times`);
|
||||
console.log(`Average execution time: ${stats.averageExecutionTime}ms`);
|
||||
|
||||
// 获取所有事件统计
|
||||
const allStats = eventBus.getStats();
|
||||
if (allStats instanceof Map) {
|
||||
allStats.forEach((stat, eventType) => {
|
||||
console.log(`${eventType}: ${stat.triggerCount} triggers`);
|
||||
});
|
||||
}
|
||||
|
||||
// 重置统计
|
||||
eventBus.resetStats('entity:created');
|
||||
```
|
||||
|
||||
### 事件类型验证
|
||||
|
||||
```typescript
|
||||
import { EventTypeValidator } from './ECS';
|
||||
|
||||
// 检查事件类型是否有效
|
||||
if (EventTypeValidator.isValid('entity:created')) {
|
||||
eventBus.emit('entity:created', data);
|
||||
}
|
||||
|
||||
// 添加自定义事件类型
|
||||
EventTypeValidator.addCustomType('game:custom:event');
|
||||
|
||||
// 获取所有有效事件类型
|
||||
const validTypes = EventTypeValidator.getAllValidTypes();
|
||||
console.log('Valid event types:', validTypes);
|
||||
```
|
||||
|
||||
### 调试和日志
|
||||
|
||||
```typescript
|
||||
// 启用调试模式
|
||||
eventBus.setDebugMode(true);
|
||||
|
||||
// 设置最大监听器数量
|
||||
eventBus.setMaxListeners(50);
|
||||
|
||||
// 检查是否有监听器
|
||||
if (eventBus.hasListeners('entity:created')) {
|
||||
console.log('Has listeners for entity:created');
|
||||
}
|
||||
|
||||
// 获取监听器数量
|
||||
const count = eventBus.getListenerCount('entity:created');
|
||||
console.log(`${count} listeners for entity:created`);
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 事件过滤和条件监听
|
||||
|
||||
```typescript
|
||||
// 使用条件过滤减少不必要的事件处理
|
||||
eventBus.on<IEntityEventData>(ECSEventType.ENTITY_CREATED, (data) => {
|
||||
// 只处理玩家实体
|
||||
if (data.entityTag === 'player') {
|
||||
handlePlayerCreated(data);
|
||||
}
|
||||
});
|
||||
|
||||
// 更好的方式:使用具体的事件类型
|
||||
eventBus.on<IEntityEventData>('entity:player:created', handlePlayerCreated);
|
||||
```
|
||||
|
||||
### 内存管理
|
||||
|
||||
```typescript
|
||||
class EventManager {
|
||||
private listeners: string[] = [];
|
||||
|
||||
public setupListeners() {
|
||||
// 保存监听器ID以便清理
|
||||
this.listeners.push(
|
||||
eventBus.on('event1', this.handler1.bind(this)),
|
||||
eventBus.on('event2', this.handler2.bind(this))
|
||||
);
|
||||
}
|
||||
|
||||
public cleanup() {
|
||||
// 清理所有监听器
|
||||
this.listeners.forEach(id => {
|
||||
eventBus.off('event1', id);
|
||||
eventBus.off('event2', id);
|
||||
});
|
||||
this.listeners.length = 0;
|
||||
}
|
||||
|
||||
private handler1(data: any) { /* ... */ }
|
||||
private handler2(data: any) { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
### 异步事件优化
|
||||
|
||||
```typescript
|
||||
// 使用Promise.all并行处理多个异步事件
|
||||
const promises = [
|
||||
eventBus.emitAsync('save:player', playerData),
|
||||
eventBus.emitAsync('update:leaderboard', scoreData),
|
||||
eventBus.emitAsync('notify:friends', notificationData)
|
||||
];
|
||||
|
||||
await Promise.all(promises);
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 事件命名规范
|
||||
|
||||
```typescript
|
||||
// 推荐的事件命名格式:模块:对象:动作
|
||||
const EVENT_NAMES = {
|
||||
// 实体相关
|
||||
ENTITY_PLAYER_CREATED: 'entity:player:created',
|
||||
ENTITY_ENEMY_DESTROYED: 'entity:enemy:destroyed',
|
||||
|
||||
// 游戏逻辑相关
|
||||
GAME_LEVEL_COMPLETED: 'game:level:completed',
|
||||
GAME_SCORE_UPDATED: 'game:score:updated',
|
||||
|
||||
// UI相关
|
||||
UI_MENU_OPENED: 'ui:menu:opened',
|
||||
UI_BUTTON_CLICKED: 'ui:button:clicked'
|
||||
};
|
||||
```
|
||||
|
||||
### 2. 类型安全
|
||||
|
||||
```typescript
|
||||
// 定义强类型的事件数据接口
|
||||
interface GameEvents {
|
||||
'player:levelup': { playerId: number; newLevel: number; experience: number };
|
||||
'inventory:item:added': { itemId: string; quantity: number; playerId: number };
|
||||
'combat:damage:dealt': { attackerId: number; targetId: number; damage: number };
|
||||
}
|
||||
|
||||
// 创建类型安全的事件发射器
|
||||
class TypedEventBus {
|
||||
private eventBus = new EventBus();
|
||||
|
||||
emit<K extends keyof GameEvents>(eventType: K, data: GameEvents[K]) {
|
||||
this.eventBus.emit(eventType, data);
|
||||
}
|
||||
|
||||
on<K extends keyof GameEvents>(
|
||||
eventType: K,
|
||||
handler: (data: GameEvents[K]) => void
|
||||
) {
|
||||
return this.eventBus.on(eventType, handler);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 错误处理
|
||||
|
||||
```typescript
|
||||
// 在事件处理器中添加错误处理
|
||||
eventBus.on<IEntityEventData>(ECSEventType.ENTITY_CREATED, (data) => {
|
||||
try {
|
||||
processEntityCreation(data);
|
||||
} catch (error) {
|
||||
console.error('Error processing entity creation:', error);
|
||||
// 发射错误事件
|
||||
eventBus.emit(ECSEventType.ERROR_OCCURRED, {
|
||||
timestamp: Date.now(),
|
||||
source: 'EntityCreationHandler',
|
||||
error: error.message,
|
||||
context: data
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 4. 模块化事件管理
|
||||
|
||||
```typescript
|
||||
// 为不同模块创建专门的事件管理器
|
||||
class PlayerEventManager {
|
||||
constructor(private eventBus: EventBus) {
|
||||
this.setupListeners();
|
||||
}
|
||||
|
||||
private setupListeners() {
|
||||
this.eventBus.onEntityCreated(this.onPlayerCreated.bind(this));
|
||||
this.eventBus.on('player:levelup', this.onPlayerLevelUp.bind(this));
|
||||
this.eventBus.on('player:died', this.onPlayerDied.bind(this));
|
||||
}
|
||||
|
||||
private onPlayerCreated(data: IEntityEventData) {
|
||||
if (data.entityTag === 'player') {
|
||||
// 处理玩家创建逻辑
|
||||
}
|
||||
}
|
||||
|
||||
private onPlayerLevelUp(data: any) {
|
||||
// 处理玩家升级逻辑
|
||||
}
|
||||
|
||||
private onPlayerDied(data: any) {
|
||||
// 处理玩家死亡逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 与EntityManager集成
|
||||
|
||||
```typescript
|
||||
import { EntityManager } from './ECS';
|
||||
|
||||
// EntityManager会自动设置事件总线
|
||||
const entityManager = new EntityManager();
|
||||
|
||||
// 获取事件总线实例
|
||||
const eventBus = entityManager.eventBus;
|
||||
|
||||
// 监听自动发射的ECS事件
|
||||
eventBus.onEntityCreated((data) => {
|
||||
console.log('Entity created automatically:', data);
|
||||
});
|
||||
|
||||
eventBus.onComponentAdded((data) => {
|
||||
console.log('Component added automatically:', data);
|
||||
});
|
||||
|
||||
// 创建实体时会自动发射事件
|
||||
const entity = entityManager.createEntity('Player');
|
||||
|
||||
// 添加组件时会自动发射事件
|
||||
entity.addComponent(new HealthComponent(100));
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
ECS框架的事件系统提供了:
|
||||
|
||||
- **类型安全**:完整的TypeScript类型支持
|
||||
- **高性能**:批处理、缓存和优化机制
|
||||
- **易用性**:装饰器、预定义事件类型
|
||||
- **可扩展**:自定义事件类型和验证
|
||||
- **调试友好**:详细的统计信息和调试模式
|
||||
|
||||
通过合理使用事件系统,可以实现松耦合的模块化架构,提高代码的可维护性和扩展性。
|
||||
23
docs/examples/index.md
Normal file
23
docs/examples/index.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# 示例
|
||||
|
||||
这里展示了ECS Framework的各种使用示例,通过实际的演示帮助您理解框架的功能和最佳实践。
|
||||
|
||||
## 🎮 互动演示
|
||||
|
||||
### [Worker系统演示](./worker-system-demo)
|
||||
- **功能**: 展示Worker多线程物理计算和渲染优化
|
||||
- **特性**: 1000+粒子实时物理模拟、碰撞检测、性能对比
|
||||
- **技术点**: SharedArrayBuffer、Canvas 2D优化、实体生命周期管理
|
||||
|
||||
## 🔗 外部示例
|
||||
|
||||
### [割草机演示](https://github.com/esengine/lawn-mower-demo)
|
||||
- **平台**: Cocos Creator 3.x
|
||||
- **功能**: 完整的游戏演示项目
|
||||
- **特性**: 展示ECS架构在实际游戏项目中的应用
|
||||
|
||||
## 📚 更多资源
|
||||
|
||||
- [快速开始指南](/guide/getting-started)
|
||||
- [核心概念](/guide/)
|
||||
- [API文档](/api/README)
|
||||
13
docs/examples/worker-system-demo.md
Normal file
13
docs/examples/worker-system-demo.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Worker系统演示
|
||||
|
||||
这是一个展示ECS框架Worker系统功能的交互式演示。
|
||||
|
||||
## 在线演示
|
||||
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="/ecs-framework/demos/worker-system/index.html" target="_blank" style="display: inline-block; padding: 15px 30px; background: #4a9eff; color: white; text-decoration: none; border-radius: 8px; font-weight: bold; font-size: 16px; box-shadow: 0 4px 8px rgba(74, 158, 255, 0.3); transition: all 0.3s ease;">
|
||||
🚀 打开Worker系统演示
|
||||
</a>
|
||||
</div>
|
||||
|
||||
> **注意**: 演示将在新窗口中打开,展示完整的Worker系统功能,包括实体管理、物理模拟和性能监控。
|
||||
@@ -1,508 +0,0 @@
|
||||
# 快速入门
|
||||
|
||||
本指南将帮助您快速上手 ECS Framework,这是一个专业级的实体组件系统框架,采用现代化架构设计,专为高性能游戏开发打造。
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
npm install @esengine/ecs-framework
|
||||
```
|
||||
|
||||
## 更新机制说明
|
||||
|
||||
ECS框架需要在游戏引擎的更新循环中调用,并传入deltaTime:
|
||||
|
||||
```typescript
|
||||
// 统一的更新方式:让外部引擎传入deltaTime
|
||||
Core.update(deltaTime);
|
||||
```
|
||||
|
||||
**不同平台的集成方式:**
|
||||
- **Laya引擎**:使用 `Laya.timer.delta / 1000`
|
||||
- **Cocos Creator**:使用组件的 `update(deltaTime)` 参数
|
||||
- **原生浏览器**:自己计算deltaTime
|
||||
- **Node.js服务器**:自己计算deltaTime
|
||||
|
||||
**优势:**
|
||||
- 与引擎时间系统完全同步
|
||||
- 支持引擎的时间缩放和暂停功能
|
||||
- 更精确的时间控制
|
||||
- 统一的API,简化集成
|
||||
|
||||
## 平台集成
|
||||
|
||||
### Laya引擎
|
||||
|
||||
```typescript
|
||||
import { Scene as LayaScene } from "laya/display/Scene";
|
||||
import { Core, Scene as ECSScene, EntityManager, EntitySystem } from '@esengine/ecs-framework';
|
||||
|
||||
class LayaECSGame extends LayaScene {
|
||||
private ecsScene: ECSScene;
|
||||
private entityManager: EntityManager;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// 初始化ECS框架
|
||||
Core.create(true);
|
||||
this.ecsScene = new ECSScene();
|
||||
this.ecsScene.name = "LayaGameScene";
|
||||
Core.scene = this.ecsScene;
|
||||
|
||||
this.entityManager = new EntityManager();
|
||||
this.setupSystems();
|
||||
}
|
||||
|
||||
onAwake(): void {
|
||||
super.onAwake();
|
||||
// 使用Laya的帧循环更新ECS
|
||||
Laya.timer.frameLoop(1, this, this.updateECS);
|
||||
}
|
||||
|
||||
onDestroy(): void {
|
||||
Laya.timer.clear(this, this.updateECS);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
private updateECS(): void {
|
||||
// 使用Laya的deltaTime更新ECS
|
||||
const deltaTime = Laya.timer.delta / 1000; // 转换为秒
|
||||
Core.update(deltaTime);
|
||||
}
|
||||
|
||||
private setupSystems(): void {
|
||||
this.ecsScene.addEntityProcessor(new LayaRenderSystem(this));
|
||||
this.ecsScene.addEntityProcessor(new MovementSystem());
|
||||
}
|
||||
}
|
||||
|
||||
// Laya渲染系统
|
||||
class LayaRenderSystem extends EntitySystem {
|
||||
private layaScene: LayaScene;
|
||||
|
||||
constructor(layaScene: LayaScene) {
|
||||
super(Matcher.empty().all(PositionComponent, SpriteComponent));
|
||||
this.layaScene = layaScene;
|
||||
}
|
||||
|
||||
protected process(entities: Entity[]): void {
|
||||
entities.forEach(entity => {
|
||||
const pos = entity.getComponent(PositionComponent);
|
||||
const sprite = entity.getComponent(SpriteComponent);
|
||||
|
||||
if (pos && sprite && sprite.layaSprite) {
|
||||
sprite.layaSprite.x = pos.x;
|
||||
sprite.layaSprite.y = pos.y;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 使用方法
|
||||
Laya.Scene.open("GameScene.scene", false, null, null, LayaECSGame);
|
||||
```
|
||||
|
||||
### Cocos Creator
|
||||
|
||||
```typescript
|
||||
import { Component as CocosComponent, _decorator } from 'cc';
|
||||
import { Core, Scene as ECSScene, EntityManager, EntitySystem } from '@esengine/ecs-framework';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
@ccclass('ECSGameManager')
|
||||
export class ECSGameManager extends CocosComponent {
|
||||
private ecsScene: ECSScene;
|
||||
private entityManager: EntityManager;
|
||||
|
||||
start() {
|
||||
// 初始化ECS框架
|
||||
Core.create(true);
|
||||
this.ecsScene = new ECSScene();
|
||||
this.ecsScene.name = "CocosGameScene";
|
||||
Core.scene = this.ecsScene;
|
||||
|
||||
this.entityManager = new EntityManager();
|
||||
this.setupSystems();
|
||||
}
|
||||
|
||||
update(deltaTime: number) {
|
||||
// 使用Cocos Creator的deltaTime更新ECS
|
||||
Core.update(deltaTime);
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
if (this.ecsScene) {
|
||||
this.ecsScene.onDestroy();
|
||||
}
|
||||
}
|
||||
|
||||
private setupSystems(): void {
|
||||
this.ecsScene.addEntityProcessor(new CocosRenderSystem(this.node));
|
||||
this.ecsScene.addEntityProcessor(new MovementSystem());
|
||||
}
|
||||
|
||||
public getEntityManager(): EntityManager {
|
||||
return this.entityManager;
|
||||
}
|
||||
}
|
||||
|
||||
// Cocos渲染系统
|
||||
class CocosRenderSystem extends EntitySystem {
|
||||
private rootNode: Node;
|
||||
|
||||
constructor(rootNode: Node) {
|
||||
super(Matcher.empty().all(PositionComponent, SpriteComponent));
|
||||
this.rootNode = rootNode;
|
||||
}
|
||||
|
||||
protected process(entities: Entity[]): void {
|
||||
entities.forEach(entity => {
|
||||
const pos = entity.getComponent(PositionComponent);
|
||||
const sprite = entity.getComponent(SpriteComponent);
|
||||
|
||||
if (pos && sprite && sprite.cocosNode) {
|
||||
sprite.cocosNode.setPosition(pos.x, pos.y);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 将ECSGameManager脚本挂载到场景根节点
|
||||
```
|
||||
|
||||
### Node.js后端
|
||||
|
||||
```typescript
|
||||
import { Core, Scene, EntityManager, EntitySystem, Time } from '@esengine/ecs-framework';
|
||||
|
||||
class ServerGameManager {
|
||||
private scene: Scene;
|
||||
private entityManager: EntityManager;
|
||||
private isRunning: boolean = false;
|
||||
private tickRate: number = 60; // 60 TPS
|
||||
private lastUpdate: number = Date.now();
|
||||
|
||||
constructor() {
|
||||
Core.create(true);
|
||||
this.scene = new Scene();
|
||||
this.scene.name = "ServerScene";
|
||||
Core.scene = this.scene;
|
||||
|
||||
this.entityManager = new EntityManager();
|
||||
this.setupSystems();
|
||||
}
|
||||
|
||||
public start(): void {
|
||||
this.isRunning = true;
|
||||
console.log(`游戏服务器启动,TPS: ${this.tickRate}`);
|
||||
this.gameLoop();
|
||||
}
|
||||
|
||||
public stop(): void {
|
||||
this.isRunning = false;
|
||||
}
|
||||
|
||||
private gameLoop(): void {
|
||||
if (!this.isRunning) return;
|
||||
|
||||
const now = Date.now();
|
||||
const deltaTime = (now - this.lastUpdate) / 1000;
|
||||
this.lastUpdate = now;
|
||||
|
||||
// 使用计算出的deltaTime更新ECS
|
||||
Core.update(deltaTime);
|
||||
|
||||
const frameTime = 1000 / this.tickRate;
|
||||
const processingTime = Date.now() - now;
|
||||
const delay = Math.max(0, frameTime - processingTime);
|
||||
|
||||
setTimeout(() => this.gameLoop(), delay);
|
||||
}
|
||||
|
||||
private setupSystems(): void {
|
||||
this.scene.addEntityProcessor(new ServerMovementSystem());
|
||||
this.scene.addEntityProcessor(new NetworkSyncSystem());
|
||||
this.scene.addEntityProcessor(new AISystem());
|
||||
}
|
||||
|
||||
public handlePlayerInput(playerId: string, input: any): void {
|
||||
const playerEntity = this.findPlayerEntity(playerId);
|
||||
if (playerEntity) {
|
||||
const inputComp = playerEntity.getComponent(InputComponent);
|
||||
if (inputComp) {
|
||||
inputComp.updateInput(input);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public getWorldState(): any {
|
||||
const entities = this.entityManager
|
||||
.query()
|
||||
.withAll(PositionComponent, NetworkComponent)
|
||||
.execute();
|
||||
|
||||
return entities.map(entity => ({
|
||||
id: entity.id,
|
||||
position: entity.getComponent(PositionComponent),
|
||||
}));
|
||||
}
|
||||
|
||||
private findPlayerEntity(playerId: string): Entity | null {
|
||||
const players = this.entityManager
|
||||
.query()
|
||||
.withAll(PlayerComponent)
|
||||
.execute();
|
||||
|
||||
return players.find(player =>
|
||||
player.getComponent(PlayerComponent).playerId === playerId
|
||||
) || null;
|
||||
}
|
||||
}
|
||||
|
||||
// 启动服务器
|
||||
const server = new ServerGameManager();
|
||||
server.start();
|
||||
```
|
||||
|
||||
### 原生浏览器
|
||||
|
||||
```typescript
|
||||
import { Core, Scene, EntityManager, EntitySystem } from '@esengine/ecs-framework';
|
||||
|
||||
class BrowserGame {
|
||||
private scene: Scene;
|
||||
private entityManager: EntityManager;
|
||||
|
||||
constructor() {
|
||||
Core.create(true);
|
||||
this.scene = new Scene();
|
||||
this.scene.name = "BrowserScene";
|
||||
Core.scene = this.scene;
|
||||
|
||||
this.entityManager = new EntityManager();
|
||||
this.setupSystems();
|
||||
}
|
||||
|
||||
public start(): void {
|
||||
this.createEntities();
|
||||
this.gameLoop();
|
||||
}
|
||||
|
||||
private gameLoop(): void {
|
||||
let lastTime = 0;
|
||||
const update = (currentTime: number) => {
|
||||
// 计算deltaTime并更新ECS(原生浏览器环境)
|
||||
const deltaTime = lastTime > 0 ? (currentTime - lastTime) / 1000 : 0.016;
|
||||
lastTime = currentTime;
|
||||
Core.update(deltaTime);
|
||||
requestAnimationFrame(update);
|
||||
};
|
||||
requestAnimationFrame(update);
|
||||
}
|
||||
|
||||
private setupSystems(): void {
|
||||
this.scene.addEntityProcessor(new MovementSystem());
|
||||
this.scene.addEntityProcessor(new RenderSystem());
|
||||
}
|
||||
|
||||
private createEntities(): void {
|
||||
const player = this.entityManager.createEntity("Player");
|
||||
player.addComponent(new PositionComponent(400, 300));
|
||||
player.addComponent(new VelocityComponent(0, 0));
|
||||
}
|
||||
}
|
||||
|
||||
const game = new BrowserGame();
|
||||
game.start();
|
||||
```
|
||||
|
||||
## 基础组件定义
|
||||
|
||||
```typescript
|
||||
import { Component } from '@esengine/ecs-framework';
|
||||
|
||||
// 位置组件
|
||||
class PositionComponent extends Component {
|
||||
public x: number = 0;
|
||||
public y: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this.x = 0;
|
||||
this.y = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 速度组件
|
||||
class VelocityComponent extends Component {
|
||||
public x: number = 0;
|
||||
public y: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this.x = 0;
|
||||
this.y = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 生命值组件
|
||||
class HealthComponent extends Component {
|
||||
public maxHealth: number = 100;
|
||||
public currentHealth: number = 100;
|
||||
|
||||
constructor(maxHealth: number = 100) {
|
||||
super();
|
||||
this.maxHealth = maxHealth;
|
||||
this.currentHealth = maxHealth;
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this.maxHealth = 100;
|
||||
this.currentHealth = 100;
|
||||
}
|
||||
|
||||
public takeDamage(damage: number): void {
|
||||
this.currentHealth = Math.max(0, this.currentHealth - damage);
|
||||
}
|
||||
|
||||
public heal(amount: number): void {
|
||||
this.currentHealth = Math.min(this.maxHealth, this.currentHealth + amount);
|
||||
}
|
||||
|
||||
public isDead(): boolean {
|
||||
return this.currentHealth <= 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 基础系统创建
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, Entity, Matcher, Time } from '@esengine/ecs-framework';
|
||||
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(PositionComponent, VelocityComponent));
|
||||
}
|
||||
|
||||
protected process(entities: Entity[]): void {
|
||||
const movingEntities = this.scene.querySystem.queryAll(PositionComponent, VelocityComponent);
|
||||
|
||||
movingEntities.entities.forEach(entity => {
|
||||
const position = entity.getComponent(PositionComponent);
|
||||
const velocity = entity.getComponent(VelocityComponent);
|
||||
|
||||
if (position && velocity) {
|
||||
position.x += velocity.x * Time.deltaTime;
|
||||
position.y += velocity.y * Time.deltaTime;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class HealthSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(HealthComponent));
|
||||
}
|
||||
|
||||
protected process(entities: Entity[]): void {
|
||||
const healthEntities = this.scene.querySystem.queryAll(HealthComponent);
|
||||
|
||||
healthEntities.entities.forEach(entity => {
|
||||
const health = entity.getComponent(HealthComponent);
|
||||
if (health && health.currentHealth <= 0) {
|
||||
entity.destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 实体管理
|
||||
|
||||
```typescript
|
||||
// 创建实体
|
||||
const player = entityManager.createEntity("Player");
|
||||
player.addComponent(new PositionComponent(100, 100));
|
||||
player.addComponent(new VelocityComponent(5, 0));
|
||||
player.addComponent(new HealthComponent(100));
|
||||
|
||||
// 批量创建实体
|
||||
const enemies = scene.createEntities(50, "Enemy");
|
||||
enemies.forEach(enemy => {
|
||||
enemy.addComponent(new PositionComponent(Math.random() * 800, Math.random() * 600));
|
||||
enemy.addComponent(new HealthComponent(50));
|
||||
});
|
||||
|
||||
// 查询实体
|
||||
const movingEntities = entityManager
|
||||
.query()
|
||||
.withAll(PositionComponent, VelocityComponent)
|
||||
.execute();
|
||||
|
||||
const healthEntities = entityManager.getEntitiesWithComponent(HealthComponent);
|
||||
const enemiesByTag = entityManager.getEntitiesByTag(2);
|
||||
```
|
||||
|
||||
## 事件系统
|
||||
|
||||
推荐使用Scene的事件系统或EntityManager的事件系统:
|
||||
|
||||
```typescript
|
||||
// 使用EntityManager的事件系统(推荐)
|
||||
const eventBus = entityManager.eventBus;
|
||||
|
||||
// 监听ECS事件
|
||||
eventBus.onEntityCreated((data) => {
|
||||
console.log(`实体创建: ${data.entityName}`);
|
||||
});
|
||||
|
||||
eventBus.onComponentAdded((data) => {
|
||||
console.log(`组件添加: ${data.componentType}`);
|
||||
});
|
||||
|
||||
// 发射自定义事件
|
||||
eventBus.emit('player:died', { player: entity, score: 1000 });
|
||||
|
||||
// 使用装饰器自动注册事件监听器
|
||||
import { EventHandler } from '@esengine/ecs-framework';
|
||||
|
||||
class GameSystem {
|
||||
@EventHandler('player:died')
|
||||
onPlayerDied(data: { player: Entity; score: number }) {
|
||||
console.log(`玩家死亡,得分: ${data.score}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 性能监控
|
||||
|
||||
```typescript
|
||||
// 获取场景统计
|
||||
const sceneStats = scene.getStats();
|
||||
console.log('实体数量:', sceneStats.entityCount);
|
||||
console.log('系统数量:', sceneStats.processorCount);
|
||||
|
||||
// 获取查询统计
|
||||
const queryStats = scene.querySystem.getStats();
|
||||
console.log('查询统计:', queryStats);
|
||||
```
|
||||
|
||||
## 下一步
|
||||
|
||||
- [EntityManager 使用指南](entity-manager-example.md) - 详细了解实体管理器的高级功能
|
||||
- [性能优化指南](performance-optimization.md) - 深入了解三大性能优化系统
|
||||
- [核心概念](core-concepts.md) - 深入了解 ECS 架构和设计原理
|
||||
- [查询系统使用指南](query-system-usage.md) - 学习高性能查询系统的详细用法
|
||||
359
docs/guide/component.md
Normal file
359
docs/guide/component.md
Normal file
@@ -0,0 +1,359 @@
|
||||
# 组件系统
|
||||
|
||||
在 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` 装饰器**,这确保了:
|
||||
- 组件在代码混淆后仍能正确识别
|
||||
- 提供稳定的类型名称用于序列化和调试
|
||||
- 框架能正确管理组件注册
|
||||
|
||||
```typescript
|
||||
// ✅ 正确的用法
|
||||
@ECSComponent('Velocity')
|
||||
class Velocity extends Component {
|
||||
dx: number = 0;
|
||||
dy: number = 0;
|
||||
}
|
||||
|
||||
// ❌ 错误的用法 - 没有装饰器
|
||||
class BadComponent extends Component {
|
||||
// 这样定义的组件可能在生产环境出现问题
|
||||
}
|
||||
```
|
||||
|
||||
## 组件生命周期
|
||||
|
||||
组件提供了生命周期钩子,可以重写来执行特定的逻辑:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('ExampleComponent')
|
||||
class ExampleComponent extends Component {
|
||||
private resource: SomeResource | null = null;
|
||||
|
||||
/**
|
||||
* 组件被添加到实体时调用
|
||||
* 用于初始化资源、建立引用等
|
||||
*/
|
||||
onAddedToEntity(): void {
|
||||
console.log(`组件 ${this.constructor.name} 被添加到实体 ${this.entity.name}`);
|
||||
this.resource = new SomeResource();
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件从实体移除时调用
|
||||
* 用于清理资源、断开引用等
|
||||
*/
|
||||
onRemovedFromEntity(): void {
|
||||
console.log(`组件 ${this.constructor.name} 从实体 ${this.entity.name} 移除`);
|
||||
if (this.resource) {
|
||||
this.resource.cleanup();
|
||||
this.resource = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 访问实体
|
||||
|
||||
组件可以通过 `this.entity` 访问其所属的实体:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Damage')
|
||||
class Damage extends Component {
|
||||
damage: number;
|
||||
|
||||
constructor(damage: number) {
|
||||
super();
|
||||
this.damage = damage;
|
||||
}
|
||||
|
||||
// 在组件方法中访问实体和其他组件
|
||||
applyDamage(): void {
|
||||
const health = this.entity.getComponent(Health);
|
||||
if (health) {
|
||||
health.takeDamage(this.damage);
|
||||
|
||||
// 如果生命值为0,销毁实体
|
||||
if (health.isDead()) {
|
||||
this.entity.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 组件属性
|
||||
|
||||
每个组件都有一些内置属性:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('ExampleComponent')
|
||||
class ExampleComponent extends Component {
|
||||
someData: string = "example";
|
||||
|
||||
showComponentInfo(): void {
|
||||
console.log(`组件ID: ${this.id}`); // 唯一的组件ID
|
||||
console.log(`所属实体: ${this.entity.name}`); // 所属实体引用
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 复杂组件示例
|
||||
|
||||
### 状态机组件
|
||||
|
||||
```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. 避免在组件中存储实体引用
|
||||
|
||||
```typescript
|
||||
// ❌ 避免:在组件中存储其他实体的引用
|
||||
@ECSComponent('BadFollower')
|
||||
class BadFollower extends Component {
|
||||
target: Entity; // 直接引用可能导致内存泄漏
|
||||
}
|
||||
|
||||
// ✅ 推荐:存储实体ID,通过场景查找
|
||||
@ECSComponent('Follower')
|
||||
class Follower extends Component {
|
||||
targetId: number;
|
||||
followDistance: number = 50;
|
||||
|
||||
constructor(targetId: number) {
|
||||
super();
|
||||
this.targetId = targetId;
|
||||
}
|
||||
|
||||
getTarget(): Entity | null {
|
||||
return this.entity.scene?.findEntityById(this.targetId) || null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
组件是 ECS 架构的数据载体,正确设计组件能让你的游戏代码更模块化、可维护和高性能。
|
||||
288
docs/guide/entity.md
Normal file
288
docs/guide/entity.md
Normal file
@@ -0,0 +1,288 @@
|
||||
# 实体类
|
||||
|
||||
在 ECS 架构中,实体(Entity)是游戏世界中的基本对象。实体本身不包含游戏逻辑或数据,它只是一个容器,用来组合不同的组件来实现各种功能。
|
||||
|
||||
## 基本概念
|
||||
|
||||
实体是一个轻量级的对象,主要用于:
|
||||
- 作为组件的容器
|
||||
- 提供唯一标识(ID)
|
||||
- 管理组件的生命周期
|
||||
|
||||
## 创建实体
|
||||
|
||||
**重要提示:实体必须通过场景创建,不支持手动创建!**
|
||||
|
||||
实体必须通过场景的 `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 架构的核心概念之一,理解如何正确使用实体将帮助你构建高效、可维护的游戏代码。
|
||||
551
docs/guide/event-system.md
Normal file
551
docs/guide/event-system.md
Normal file
@@ -0,0 +1,551 @@
|
||||
# 事件系统
|
||||
|
||||
ECS 框架内置了强大的类型安全事件系统,支持同步/异步事件、优先级、批处理等高级功能。事件系统是实现组件间通信、系统间协作的核心机制。
|
||||
|
||||
## 基本概念
|
||||
|
||||
事件系统提供了发布-订阅模式的实现,包含以下核心概念:
|
||||
- **事件发布者**:发射事件的对象
|
||||
- **事件监听者**:监听并处理特定事件的对象
|
||||
- **事件类型**:字符串标识,用于区分不同类型的事件
|
||||
- **事件数据**:事件携带的相关信息
|
||||
|
||||
## 事件系统架构
|
||||
|
||||
框架提供了两层事件系统:
|
||||
|
||||
1. **TypeSafeEventSystem** - 底层高性能事件系统
|
||||
2. **EventBus** - 上层增强事件总线,提供更多便利功能
|
||||
|
||||
## 基本使用
|
||||
|
||||
### 在场景中使用事件系统
|
||||
|
||||
每个场景都有内置的事件系统:
|
||||
|
||||
```typescript
|
||||
class GameScene 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('score_changed', this.onScoreChanged.bind(this));
|
||||
}
|
||||
|
||||
private onPlayerDied(data: { player: Entity, cause: string }): void {
|
||||
console.log(`玩家死亡,原因: ${data.cause}`);
|
||||
// 处理玩家死亡逻辑
|
||||
}
|
||||
|
||||
private onEnemySpawned(data: { enemy: Entity, position: { x: number, y: number } }): void {
|
||||
console.log('敌人生成于:', data.position);
|
||||
// 处理敌人生成逻辑
|
||||
}
|
||||
|
||||
private onScoreChanged(data: { newScore: number, oldScore: number }): void {
|
||||
console.log(`分数变化: ${data.oldScore} -> ${data.newScore}`);
|
||||
// 更新UI显示
|
||||
}
|
||||
|
||||
// 在系统中发射事件
|
||||
someGameLogic(): void {
|
||||
// 发射同步事件
|
||||
this.eventSystem.emitSync('score_changed', {
|
||||
newScore: 1000,
|
||||
oldScore: 800
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 在系统中使用事件
|
||||
|
||||
系统可以方便地监听和发送事件:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Combat')
|
||||
class CombatSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Health, Combat));
|
||||
}
|
||||
|
||||
protected onInitialize(): void {
|
||||
// 使用系统提供的事件监听方法(自动清理)
|
||||
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'
|
||||
});
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const combat = entity.getComponent(Combat);
|
||||
if (combat && combat.shouldAttack()) {
|
||||
// 发射攻击事件
|
||||
this.scene?.eventSystem.emitSync('player_attack', {
|
||||
damage: combat.damage,
|
||||
target: combat.target
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 高级功能
|
||||
|
||||
### 一次性监听器
|
||||
|
||||
```typescript
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 只监听一次的事件
|
||||
this.eventSystem.once('game_start', this.onGameStart.bind(this));
|
||||
|
||||
// 或者使用配置对象
|
||||
this.eventSystem.on('level_complete', this.onLevelComplete.bind(this), {
|
||||
once: true // 只执行一次
|
||||
});
|
||||
}
|
||||
|
||||
private onGameStart(): void {
|
||||
console.log('游戏开始!');
|
||||
// 这个方法只会被调用一次
|
||||
}
|
||||
|
||||
private onLevelComplete(): void {
|
||||
console.log('关卡完成!');
|
||||
// 这个方法也只会被调用一次
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 优先级控制
|
||||
|
||||
```typescript
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 高优先级监听器先执行
|
||||
this.eventSystem.on('damage_dealt', this.onDamageDealt.bind(this), {
|
||||
priority: 100 // 高优先级
|
||||
});
|
||||
|
||||
// 普通优先级
|
||||
this.eventSystem.on('damage_dealt', this.updateUI.bind(this), {
|
||||
priority: 0 // 默认优先级
|
||||
});
|
||||
|
||||
// 低优先级最后执行
|
||||
this.eventSystem.on('damage_dealt', this.logDamage.bind(this), {
|
||||
priority: -100 // 低优先级
|
||||
});
|
||||
}
|
||||
|
||||
private onDamageDealt(data: any): void {
|
||||
// 最先执行 - 处理核心游戏逻辑
|
||||
}
|
||||
|
||||
private updateUI(data: any): void {
|
||||
// 中等优先级 - 更新界面
|
||||
}
|
||||
|
||||
private logDamage(data: any): void {
|
||||
// 最后执行 - 记录日志
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 异步事件处理
|
||||
|
||||
```typescript
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 监听异步事件
|
||||
this.eventSystem.onAsync('save_game', this.onSaveGame.bind(this));
|
||||
this.eventSystem.onAsync('load_data', this.onLoadData.bind(this));
|
||||
}
|
||||
|
||||
private async onSaveGame(data: { saveSlot: number }): Promise<void> {
|
||||
console.log(`开始保存游戏到槽位 ${data.saveSlot}`);
|
||||
|
||||
// 模拟异步保存操作
|
||||
await this.saveGameData(data.saveSlot);
|
||||
|
||||
console.log('游戏保存完成');
|
||||
}
|
||||
|
||||
private async onLoadData(data: { url: string }): Promise<void> {
|
||||
try {
|
||||
const response = await fetch(data.url);
|
||||
const gameData = await response.json();
|
||||
// 处理加载的数据
|
||||
} catch (error) {
|
||||
console.error('数据加载失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private async saveGameData(slot: number): Promise<void> {
|
||||
// 模拟保存操作
|
||||
return new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
|
||||
// 发射异步事件
|
||||
public async triggerSave(): Promise<void> {
|
||||
// 使用 emit 而不是 emitSync 来触发异步监听器
|
||||
await this.eventSystem.emit('save_game', { saveSlot: 1 });
|
||||
console.log('所有异步保存操作完成');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 事件统计和调试
|
||||
|
||||
```typescript
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.eventSystem.on('debug_event', this.onDebugEvent.bind(this));
|
||||
}
|
||||
|
||||
private onDebugEvent(): void {
|
||||
// 处理调试事件
|
||||
}
|
||||
|
||||
public showEventStats(): void {
|
||||
// 获取特定事件的统计信息
|
||||
const stats = this.eventSystem.getStats('debug_event') as any;
|
||||
if (stats) {
|
||||
console.log('事件统计:');
|
||||
console.log(`- 事件类型: ${stats.eventType}`);
|
||||
console.log(`- 监听器数量: ${stats.listenerCount}`);
|
||||
console.log(`- 触发次数: ${stats.triggerCount}`);
|
||||
console.log(`- 平均执行时间: ${stats.averageExecutionTime.toFixed(2)}ms`);
|
||||
}
|
||||
|
||||
// 获取所有事件统计
|
||||
const allStats = this.eventSystem.getStats() as Map<string, any>;
|
||||
console.log(`总共有 ${allStats.size} 种事件类型`);
|
||||
}
|
||||
|
||||
public resetEventStats(): void {
|
||||
// 重置特定事件的统计
|
||||
this.eventSystem.resetStats('debug_event');
|
||||
|
||||
// 或重置所有统计
|
||||
this.eventSystem.resetStats();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 全局事件总线
|
||||
|
||||
对于跨场景的事件通信,可以使用全局事件总线:
|
||||
|
||||
```typescript
|
||||
import { GlobalEventBus } from '@esengine/ecs-framework';
|
||||
|
||||
class GameManager {
|
||||
private eventBus = GlobalEventBus.getInstance();
|
||||
|
||||
constructor() {
|
||||
this.setupGlobalEvents();
|
||||
}
|
||||
|
||||
private setupGlobalEvents(): void {
|
||||
// 监听全局事件
|
||||
this.eventBus.on('player_level_up', this.onPlayerLevelUp.bind(this));
|
||||
this.eventBus.on('achievement_unlocked', this.onAchievementUnlocked.bind(this));
|
||||
this.eventBus.onAsync('upload_score', this.onUploadScore.bind(this));
|
||||
}
|
||||
|
||||
private onPlayerLevelUp(data: { level: number, experience: number }): void {
|
||||
console.log(`玩家升级到 ${data.level} 级!`);
|
||||
// 处理全局升级逻辑
|
||||
}
|
||||
|
||||
private onAchievementUnlocked(data: { achievementId: string, name: string }): void {
|
||||
console.log(`解锁成就: ${data.name}`);
|
||||
// 显示成就通知
|
||||
}
|
||||
|
||||
private async onUploadScore(data: { score: number, playerName: string }): Promise<void> {
|
||||
// 异步上传分数到服务器
|
||||
try {
|
||||
await this.uploadToServer(data);
|
||||
console.log('分数上传成功');
|
||||
} catch (error) {
|
||||
console.error('分数上传失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
public triggerGlobalEvent(): void {
|
||||
// 发射全局事件
|
||||
this.eventBus.emit('player_level_up', {
|
||||
level: 10,
|
||||
experience: 2500
|
||||
});
|
||||
}
|
||||
|
||||
private async uploadToServer(data: any): Promise<void> {
|
||||
// 模拟服务器上传
|
||||
return new Promise(resolve => setTimeout(resolve, 2000));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 批处理事件
|
||||
|
||||
对于高频事件,可以使用批处理来提升性能:
|
||||
|
||||
```typescript
|
||||
class MovementSystem extends EntitySystem {
|
||||
protected onInitialize(): void {
|
||||
// 设置位置更新事件的批处理
|
||||
this.scene?.eventSystem.setBatchConfig('position_updated', {
|
||||
batchSize: 50, // 批处理大小
|
||||
delay: 16, // 延迟时间(毫秒)
|
||||
enabled: true
|
||||
});
|
||||
|
||||
// 监听批处理事件
|
||||
this.addEventListener('position_updated:batch', this.onPositionBatch.bind(this));
|
||||
}
|
||||
|
||||
private onPositionBatch(batchData: any): void {
|
||||
console.log(`批处理位置更新,共 ${batchData.count} 个事件`);
|
||||
|
||||
// 批量处理所有位置更新
|
||||
for (const event of batchData.events) {
|
||||
this.updateMinimap(event.entityId, event.position);
|
||||
}
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const position = entity.getComponent(Position);
|
||||
if (position && position.hasChanged) {
|
||||
// 发射高频位置更新事件(会被批处理)
|
||||
this.scene?.eventSystem.emitSync('position_updated', {
|
||||
entityId: entity.id,
|
||||
position: { x: position.x, y: position.y }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateMinimap(entityId: number, position: { x: number, y: number }): void {
|
||||
// 更新小地图显示
|
||||
}
|
||||
|
||||
public flushPositionUpdates(): void {
|
||||
// 立即处理所有待处理的位置更新
|
||||
this.scene?.eventSystem.flushBatch('position_updated');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 预定义的 ECS 事件
|
||||
|
||||
框架提供了一些预定义的 ECS 生命周期事件:
|
||||
|
||||
```typescript
|
||||
import { ECSEventType } from '@esengine/ecs-framework';
|
||||
|
||||
class ECSMonitor {
|
||||
private eventBus = GlobalEventBus.getInstance();
|
||||
|
||||
constructor() {
|
||||
this.setupECSEvents();
|
||||
}
|
||||
|
||||
private setupECSEvents(): void {
|
||||
// 监听实体生命周期事件
|
||||
this.eventBus.onEntityCreated(this.onEntityCreated.bind(this));
|
||||
this.eventBus.on(ECSEventType.ENTITY_DESTROYED, this.onEntityDestroyed.bind(this));
|
||||
|
||||
// 监听组件生命周期事件
|
||||
this.eventBus.onComponentAdded(this.onComponentAdded.bind(this));
|
||||
this.eventBus.on(ECSEventType.COMPONENT_REMOVED, this.onComponentRemoved.bind(this));
|
||||
|
||||
// 监听系统事件
|
||||
this.eventBus.on(ECSEventType.SYSTEM_ADDED, this.onSystemAdded.bind(this));
|
||||
this.eventBus.onSystemError(this.onSystemError.bind(this));
|
||||
|
||||
// 监听性能警告
|
||||
this.eventBus.onPerformanceWarning(this.onPerformanceWarning.bind(this));
|
||||
}
|
||||
|
||||
private onEntityCreated(data: any): void {
|
||||
console.log(`实体创建: ${data.entityName} (ID: ${data.entity.id})`);
|
||||
}
|
||||
|
||||
private onEntityDestroyed(data: any): void {
|
||||
console.log(`实体销毁: ${data.entity.name} (ID: ${data.entity.id})`);
|
||||
}
|
||||
|
||||
private onComponentAdded(data: any): void {
|
||||
console.log(`组件添加: ${data.componentType} 到实体 ${data.entity.name}`);
|
||||
}
|
||||
|
||||
private onComponentRemoved(data: any): void {
|
||||
console.log(`组件移除: ${data.componentType} 从实体 ${data.entity.name}`);
|
||||
}
|
||||
|
||||
private onSystemAdded(data: any): void {
|
||||
console.log(`系统添加: ${data.systemName}`);
|
||||
}
|
||||
|
||||
private onSystemError(data: any): void {
|
||||
console.error(`系统错误: ${data.systemName}`, data.error);
|
||||
}
|
||||
|
||||
private onPerformanceWarning(data: any): void {
|
||||
console.warn(`性能警告: ${data.systemName} 执行时间过长 (${data.executionTime}ms)`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 事件命名规范
|
||||
|
||||
```typescript
|
||||
// ✅ 好的事件命名
|
||||
this.eventSystem.emitSync('player:health_changed', data);
|
||||
this.eventSystem.emitSync('enemy:spawned', data);
|
||||
this.eventSystem.emitSync('ui:score_updated', data);
|
||||
this.eventSystem.emitSync('game:paused', data);
|
||||
|
||||
// ❌ 避免的事件命名
|
||||
this.eventSystem.emitSync('event1', data);
|
||||
this.eventSystem.emitSync('update', data);
|
||||
this.eventSystem.emitSync('change', data);
|
||||
```
|
||||
|
||||
### 2. 类型安全的事件数据
|
||||
|
||||
```typescript
|
||||
// 定义事件数据接口
|
||||
interface PlayerHealthChangedEvent {
|
||||
entityId: number;
|
||||
oldHealth: number;
|
||||
newHealth: number;
|
||||
cause: 'damage' | 'healing';
|
||||
}
|
||||
|
||||
interface EnemySpawnedEvent {
|
||||
enemyType: string;
|
||||
position: { x: number, y: number };
|
||||
level: number;
|
||||
}
|
||||
|
||||
// 使用类型安全的事件
|
||||
class HealthSystem extends EntitySystem {
|
||||
private onHealthChanged(data: PlayerHealthChangedEvent): void {
|
||||
// TypeScript 会提供完整的类型检查
|
||||
console.log(`生命值变化: ${data.oldHealth} -> ${data.newHealth}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 避免事件循环
|
||||
|
||||
```typescript
|
||||
// ❌ 避免:可能导致无限循环
|
||||
class BadEventHandler {
|
||||
private onScoreChanged(data: any): void {
|
||||
// 在处理分数变化时又触发分数变化事件
|
||||
this.scene?.eventSystem.emitSync('score_changed', newData); // 危险!
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 正确:使用不同的事件类型或条件判断
|
||||
class GoodEventHandler {
|
||||
private isProcessingScore = false;
|
||||
|
||||
private onScoreChanged(data: any): void {
|
||||
if (this.isProcessingScore) return; // 防止循环
|
||||
|
||||
this.isProcessingScore = true;
|
||||
// 处理分数变化
|
||||
this.updateUI(data);
|
||||
this.isProcessingScore = false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 及时清理事件监听器
|
||||
|
||||
```typescript
|
||||
class TemporaryUI {
|
||||
private listenerId: string;
|
||||
|
||||
constructor(scene: Scene) {
|
||||
// 保存监听器ID用于后续清理
|
||||
this.listenerId = scene.eventSystem.on('ui_update', this.onUpdate.bind(this));
|
||||
}
|
||||
|
||||
private onUpdate(data: any): void {
|
||||
// 处理UI更新
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
// 清理事件监听器
|
||||
if (this.listenerId) {
|
||||
scene.eventSystem.off('ui_update', this.listenerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 性能考虑
|
||||
|
||||
```typescript
|
||||
class OptimizedEventHandler {
|
||||
protected onInitialize(): void {
|
||||
// 对于高频事件,使用批处理
|
||||
this.scene?.eventSystem.setBatchConfig('movement_update', {
|
||||
batchSize: 100,
|
||||
delay: 16,
|
||||
enabled: true
|
||||
});
|
||||
|
||||
// 对于低频但重要的事件,使用高优先级
|
||||
this.addEventListener('game_over', this.onGameOver.bind(this), {
|
||||
priority: 1000
|
||||
});
|
||||
|
||||
// 对于一次性事件,使用 once
|
||||
this.addEventListener('level_start', this.onLevelStart.bind(this), {
|
||||
once: true
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
事件系统是 ECS 框架中实现松耦合架构的重要工具,正确使用事件系统能让你的游戏代码更加模块化、可维护和可扩展。
|
||||
417
docs/guide/getting-started.md
Normal file
417
docs/guide/getting-started.md
Normal file
@@ -0,0 +1,417 @@
|
||||
# 快速开始
|
||||
|
||||
本指南将帮助你快速上手 ECS Framework,从安装到创建第一个 ECS 应用。
|
||||
|
||||
## 安装
|
||||
|
||||
### NPM 安装
|
||||
|
||||
```bash
|
||||
# 使用 npm
|
||||
npm install @esengine/ecs-framework
|
||||
```
|
||||
|
||||
## 初始化 Core
|
||||
|
||||
### 基础初始化
|
||||
|
||||
ECS Framework 的核心是 `Core` 类,它是一个单例模式,负责管理整个框架的生命周期。
|
||||
|
||||
```typescript
|
||||
import { Core } from '@esengine/ecs-framework'
|
||||
|
||||
// 方式1:使用配置对象(推荐)
|
||||
const core = Core.create({
|
||||
debug: true, // 启用调试模式,提供详细的日志和性能监控
|
||||
enableEntitySystems: true, // 启用实体系统,这是ECS的核心功能
|
||||
debugConfig: { // 可选:高级调试配置
|
||||
enabled: false, // 是否启用WebSocket调试服务器
|
||||
websocketUrl: 'ws://localhost:8080',
|
||||
debugFrameRate: 30, // 调试数据发送帧率
|
||||
channels: {
|
||||
entities: true,
|
||||
systems: true,
|
||||
performance: true,
|
||||
components: true,
|
||||
scenes: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 方式2:简化创建(向后兼容)
|
||||
const core = Core.create(true); // 等同于 { debug: true, enableEntitySystems: true }
|
||||
|
||||
// 方式3:生产环境配置
|
||||
const core = Core.create({
|
||||
debug: false, // 生产环境关闭调试
|
||||
enableEntitySystems: true
|
||||
});
|
||||
```
|
||||
|
||||
### Core 配置详解
|
||||
|
||||
```typescript
|
||||
interface ICoreConfig {
|
||||
/** 是否启用调试模式 - 影响日志级别和性能监控 */
|
||||
debug?: boolean;
|
||||
|
||||
/** 是否启用实体系统 - 核心ECS功能开关 */
|
||||
enableEntitySystems?: boolean;
|
||||
|
||||
/** 高级调试配置 - 用于开发工具集成 */
|
||||
debugConfig?: {
|
||||
enabled: boolean; // 是否启用调试服务器
|
||||
websocketUrl: string; // WebSocket服务器地址
|
||||
autoReconnect?: boolean; // 是否自动重连
|
||||
debugFrameRate?: 60 | 30 | 15; // 调试数据发送帧率
|
||||
channels: { // 数据通道配置
|
||||
entities: boolean; // 实体数据
|
||||
systems: boolean; // 系统数据
|
||||
performance: boolean; // 性能数据
|
||||
components: boolean; // 组件数据
|
||||
scenes: boolean; // 场景数据
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Core 实例管理
|
||||
|
||||
Core 采用单例模式,创建后可以通过静态属性获取:
|
||||
|
||||
```typescript
|
||||
// 创建实例
|
||||
const core = Core.create(true);
|
||||
|
||||
// 获取已创建的实例
|
||||
const instance = Core.Instance; // 获取当前实例,如果未创建则为 null
|
||||
```
|
||||
|
||||
### 游戏循环集成
|
||||
|
||||
**重要**: 在创建实体和系统之前,你需要先了解如何将 ECS Framework 集成到你的游戏引擎中。
|
||||
|
||||
`Core.update(deltaTime)` 是整个框架的心跳,必须在游戏引擎的每一帧中调用。它负责:
|
||||
- 更新框架内置的 Time 类
|
||||
- 更新所有全局管理器(定时器、对象池等)
|
||||
- 更新所有场景中的实体系统
|
||||
- 处理实体的创建和销毁
|
||||
- 收集性能数据(调试模式下)
|
||||
|
||||
各引擎集成示例请参考:[与游戏引擎集成](#与游戏引擎集成)
|
||||
|
||||
## 创建第一个 ECS 应用
|
||||
|
||||
### 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('Velocity')
|
||||
class Velocity extends Component {
|
||||
dx: number = 0
|
||||
dy: number = 0
|
||||
|
||||
constructor(dx: number = 0, dy: number = 0) {
|
||||
super()
|
||||
this.dx = dx
|
||||
this.dy = dy
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染组件
|
||||
@ECSComponent('Sprite')
|
||||
class Sprite extends Component {
|
||||
texture: string = ''
|
||||
width: number = 32
|
||||
height: number = 32
|
||||
|
||||
constructor(texture: string, width: number = 32, height: number = 32) {
|
||||
super()
|
||||
this.texture = texture
|
||||
this.width = width
|
||||
this.height = height
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 创建实体系统
|
||||
|
||||
系统包含游戏逻辑,处理具有特定组件的实体。ECS Framework 提供了基于 Matcher 的实体过滤机制:
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, Matcher, Time, ECSSystem } from '@esengine/ecs-framework'
|
||||
|
||||
// 移动系统 - 处理位置和速度
|
||||
@ECSSystem('MovementSystem')
|
||||
class MovementSystem extends EntitySystem {
|
||||
|
||||
constructor() {
|
||||
// 使用 Matcher 定义要处理的实体:必须同时拥有 Position 和 Velocity 组件
|
||||
super(Matcher.empty().all(Position, Velocity))
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// process 方法接收所有匹配的实体
|
||||
for (const entity of entities) {
|
||||
const position = entity.getComponent(Position)!
|
||||
const velocity = entity.getComponent(Velocity)!
|
||||
|
||||
// 更新位置(使用框架的Time类)
|
||||
position.x += velocity.dx * Time.deltaTime
|
||||
position.y += velocity.dy * Time.deltaTime
|
||||
|
||||
// 边界检查示例
|
||||
if (position.x < 0) position.x = 0
|
||||
if (position.y < 0) position.y = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染系统 - 处理可见对象
|
||||
@ECSSystem('RenderSystem')
|
||||
class RenderSystem extends EntitySystem {
|
||||
|
||||
constructor() {
|
||||
// 必须有 Position 和 Sprite,可选 Velocity(用于方向判断)
|
||||
super(Matcher.empty().all(Position, Sprite).any(Velocity))
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const position = entity.getComponent(Position)!
|
||||
const sprite = entity.getComponent(Sprite)!
|
||||
const velocity = entity.getComponent(Velocity) // 可能为 null
|
||||
|
||||
// 根据速度方向翻转精灵(可选逻辑)
|
||||
let flipX = false
|
||||
if (velocity && velocity.dx < 0) {
|
||||
flipX = true
|
||||
}
|
||||
|
||||
// 渲染逻辑(这里是伪代码)
|
||||
this.drawSprite(sprite.texture, position.x, position.y, sprite.width, sprite.height, flipX)
|
||||
}
|
||||
}
|
||||
|
||||
private drawSprite(texture: string, x: number, y: number, width: number, height: number, flipX: boolean = false) {
|
||||
// 实际的渲染实现将取决于你使用的游戏引擎
|
||||
const direction = flipX ? '←' : '→'
|
||||
console.log(`渲染 ${texture} 在位置 (${x.toFixed(1)}, ${y.toFixed(1)}) 方向: ${direction}`)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### 3. 创建场景
|
||||
|
||||
推荐继承 Scene 类来创建自定义场景:
|
||||
|
||||
```typescript
|
||||
import { Scene } from '@esengine/ecs-framework'
|
||||
|
||||
// 推荐:继承Scene创建自定义场景
|
||||
class GameScene extends Scene {
|
||||
|
||||
initialize(): void {
|
||||
// 场景初始化逻辑
|
||||
this.name = "MainScene";
|
||||
|
||||
// 添加系统到场景
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
}
|
||||
|
||||
onStart(): void {
|
||||
// 场景开始运行时的逻辑
|
||||
console.log("游戏场景已启动");
|
||||
}
|
||||
|
||||
unload(): void {
|
||||
// 场景卸载时的清理逻辑
|
||||
console.log("游戏场景已卸载");
|
||||
}
|
||||
}
|
||||
|
||||
// 创建并设置场景
|
||||
const gameScene = new GameScene();
|
||||
Core.setScene(gameScene);
|
||||
```
|
||||
|
||||
### 4. 创建实体
|
||||
|
||||
```typescript
|
||||
// 创建玩家实体
|
||||
const player = gameScene.createEntity("Player");
|
||||
player.addComponent(new Position(100, 100));
|
||||
player.addComponent(new Velocity(50, 30)); // 每秒移动 50 像素(x方向),30 像素(y方向)
|
||||
player.addComponent(new Sprite("player.png", 64, 64));
|
||||
```
|
||||
|
||||
## 场景管理
|
||||
|
||||
Core 内置了场景管理功能,使用非常简单:
|
||||
|
||||
```typescript
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始化Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 创建并设置场景
|
||||
class GameScene extends Scene {
|
||||
initialize(): void {
|
||||
this.name = "GamePlay";
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
}
|
||||
}
|
||||
|
||||
const gameScene = new GameScene();
|
||||
Core.setScene(gameScene);
|
||||
|
||||
// 游戏循环(自动更新场景)
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // 自动更新全局服务和场景
|
||||
}
|
||||
|
||||
// 切换场景
|
||||
Core.loadScene(new MenuScene()); // 延迟切换(下一帧)
|
||||
Core.setScene(new GameScene()); // 立即切换
|
||||
|
||||
// 访问当前场景
|
||||
const currentScene = Core.scene;
|
||||
|
||||
// 使用流式API
|
||||
const player = Core.ecsAPI?.createEntity('Player')
|
||||
.addComponent(Position, 100, 100)
|
||||
.addComponent(Velocity, 50, 0);
|
||||
```
|
||||
|
||||
### 高级:使用 WorldManager 管理多世界
|
||||
|
||||
仅适用于复杂的服务器端应用(MMO游戏服务器、游戏房间系统等):
|
||||
|
||||
```typescript
|
||||
import { Core, WorldManager } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始化Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 创建世界管理器(手动管理)
|
||||
const worldManager = new WorldManager();
|
||||
|
||||
// 创建多个独立的游戏世界
|
||||
const room1 = worldManager.createWorld('room_001');
|
||||
const room2 = worldManager.createWorld('room_002');
|
||||
|
||||
// 在每个世界中创建场景
|
||||
const gameScene1 = room1.createScene('game', new GameScene());
|
||||
const gameScene2 = room2.createScene('game', new GameScene());
|
||||
|
||||
// 激活场景
|
||||
room1.setSceneActive('game', true);
|
||||
room2.setSceneActive('game', true);
|
||||
|
||||
// 游戏循环(需要手动更新世界)
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // 更新全局服务
|
||||
worldManager.updateAll(); // 手动更新所有世界
|
||||
}
|
||||
```
|
||||
|
||||
## 与游戏引擎集成
|
||||
|
||||
### Laya 引擎集成
|
||||
|
||||
```typescript
|
||||
import { Stage } from "laya/display/Stage";
|
||||
import { Laya } from "Laya";
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始化 Laya
|
||||
Laya.init(800, 600).then(() => {
|
||||
// 初始化 ECS
|
||||
Core.create(true);
|
||||
Core.setScene(new GameScene());
|
||||
|
||||
// 启动游戏循环
|
||||
Laya.timer.frameLoop(1, this, () => {
|
||||
const deltaTime = Laya.timer.delta / 1000;
|
||||
Core.update(deltaTime); // 自动更新全局服务和场景
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Cocos Creator 集成
|
||||
|
||||
```typescript
|
||||
import { Component, _decorator } from 'cc';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
const { ccclass } = _decorator;
|
||||
|
||||
@ccclass('ECSGameManager')
|
||||
export class ECSGameManager extends Component {
|
||||
onLoad() {
|
||||
// 初始化 ECS
|
||||
Core.create(true);
|
||||
Core.setScene(new GameScene());
|
||||
}
|
||||
|
||||
update(deltaTime: number) {
|
||||
// 自动更新全局服务和场景
|
||||
Core.update(deltaTime);
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
// 清理资源
|
||||
Core.destroy();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 下一步
|
||||
|
||||
现在你已经成功创建了第一个 ECS 应用!接下来可以:
|
||||
|
||||
- 查看完整的 [API 文档](/api/README)
|
||||
- 探索更多[实际应用示例](/examples/)
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 为什么我的系统没有执行?
|
||||
|
||||
确保:
|
||||
1. 系统已添加到场景:`this.addSystem(system)` (在 Scene 的 initialize 方法中)
|
||||
2. 场景已设置:`Core.setScene(scene)`
|
||||
3. 游戏循环在调用:`Core.update(deltaTime)`
|
||||
|
||||
### 如何调试 ECS 应用?
|
||||
|
||||
启用调试模式:
|
||||
|
||||
```typescript
|
||||
Core.create({ debug: true })
|
||||
|
||||
// 获取调试数据
|
||||
const debugData = Core.getDebugData()
|
||||
console.log(debugData)
|
||||
```
|
||||
32
docs/guide/index.md
Normal file
32
docs/guide/index.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# 指南
|
||||
|
||||
欢迎使用 ECS Framework 指南。这里将详细介绍框架的各个核心概念和使用方法。
|
||||
|
||||
## 核心概念
|
||||
|
||||
### [实体类 (Entity)](./entity.md)
|
||||
了解 ECS 架构的基础 - 实体类的使用方法、生命周期管理和最佳实践。
|
||||
|
||||
### [组件系统 (Component)](./component.md)
|
||||
学习如何创建和使用组件,实现游戏功能的模块化设计。
|
||||
|
||||
### [系统架构 (System)](./system.md)
|
||||
掌握系统的编写方法,实现游戏逻辑的处理。
|
||||
|
||||
### [场景管理 (Scene)](./scene.md)
|
||||
了解场景的生命周期、系统管理和实体容器功能。
|
||||
|
||||
### [事件系统 (Event)](./event-system.md)
|
||||
掌握类型安全的事件系统,实现组件间通信和系统协作。
|
||||
|
||||
### [序列化系统 (Serialization)](./serialization.md)
|
||||
掌握场景、实体和组件的序列化方案,支持全量序列化和增量序列化,实现游戏存档、网络同步等功能。
|
||||
|
||||
### [时间和定时器 (Time)](./time-and-timers.md)
|
||||
学习时间管理和定时器系统,实现游戏逻辑的精确时间控制。
|
||||
|
||||
### [日志系统 (Logger)](./logging.md)
|
||||
掌握分级日志系统,用于调试、监控和错误追踪。
|
||||
|
||||
### [平台适配器 (Platform Adapter)](./platform-adapter.md)
|
||||
了解如何为不同平台实现和注册平台适配器,支持浏览器、小游戏、Node.js等环境。
|
||||
550
docs/guide/logging.md
Normal file
550
docs/guide/logging.md
Normal file
@@ -0,0 +1,550 @@
|
||||
# 日志系统
|
||||
|
||||
ECS 框架提供了功能强大的分级日志系统,支持多种日志级别、颜色输出、自定义前缀和灵活的配置选项。日志系统可以帮助开发者调试代码和监控应用运行状态。
|
||||
|
||||
## 基本概念
|
||||
|
||||
日志系统包含以下核心概念:
|
||||
- **日志级别**:Debug < Info < Warn < Error < Fatal < None
|
||||
- **日志器**:具名的日志输出器,每个模块可以有自己的日志器
|
||||
- **日志管理器**:全局管理所有日志器的单例
|
||||
- **颜色配置**:支持控制台颜色输出
|
||||
|
||||
## 日志级别
|
||||
|
||||
```typescript
|
||||
import { LogLevel } from '@esengine/ecs-framework';
|
||||
|
||||
// 日志级别从低到高
|
||||
LogLevel.Debug // 0 - 调试信息
|
||||
LogLevel.Info // 1 - 一般信息
|
||||
LogLevel.Warn // 2 - 警告信息
|
||||
LogLevel.Error // 3 - 错误信息
|
||||
LogLevel.Fatal // 4 - 致命错误
|
||||
LogLevel.None // 5 - 不输出任何日志
|
||||
```
|
||||
|
||||
## 基本使用
|
||||
|
||||
### 使用默认日志器
|
||||
|
||||
```typescript
|
||||
import { Logger } from '@esengine/ecs-framework';
|
||||
|
||||
class GameSystem extends EntitySystem {
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 输出不同级别的日志
|
||||
Logger.debug('处理实体数量:', entities.length);
|
||||
Logger.info('系统正常运行');
|
||||
Logger.warn('检测到性能问题');
|
||||
Logger.error('处理过程中发生错误', new Error('示例错误'));
|
||||
Logger.fatal('致命错误,系统即将停止');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 创建命名日志器
|
||||
|
||||
```typescript
|
||||
import { createLogger } from '@esengine/ecs-framework';
|
||||
|
||||
class MovementSystem extends EntitySystem {
|
||||
private logger = createLogger('MovementSystem');
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
this.logger.info(`处理 ${entities.length} 个移动实体`);
|
||||
|
||||
for (const entity of entities) {
|
||||
const position = entity.getComponent(Position);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
if (position && velocity) {
|
||||
position.x += velocity.dx * Time.deltaTime;
|
||||
position.y += velocity.dy * Time.deltaTime;
|
||||
|
||||
this.logger.debug(`实体 ${entity.id} 移动到位置 (${position.x}, ${position.y})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected onAdded(entity: Entity): void {
|
||||
this.logger.info(`实体 ${entity.name} 加入移动系统`);
|
||||
}
|
||||
|
||||
protected onRemoved(entity: Entity): void {
|
||||
this.logger.warn(`实体 ${entity.name} 离开移动系统`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 系统内置日志器
|
||||
|
||||
框架的各个系统都有自己的日志器:
|
||||
|
||||
```typescript
|
||||
// 框架内部使用示例
|
||||
class Scene {
|
||||
private static readonly _logger = createLogger('Scene');
|
||||
|
||||
public addSystem(system: EntitySystem): void {
|
||||
Scene._logger.info(`添加系统: ${system.systemName}`);
|
||||
// 系统添加逻辑
|
||||
}
|
||||
|
||||
public removeSystem(system: EntitySystem): void {
|
||||
Scene._logger.warn(`移除系统: ${system.systemName}`);
|
||||
// 系统移除逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 日志配置
|
||||
|
||||
### 设置全局日志级别
|
||||
|
||||
```typescript
|
||||
import { setGlobalLogLevel, LogLevel } from '@esengine/ecs-framework';
|
||||
|
||||
// 在开发环境显示所有日志
|
||||
setGlobalLogLevel(LogLevel.Debug);
|
||||
|
||||
// 在生产环境只显示警告及以上级别
|
||||
setGlobalLogLevel(LogLevel.Warn);
|
||||
|
||||
// 完全禁用日志输出
|
||||
setGlobalLogLevel(LogLevel.None);
|
||||
```
|
||||
|
||||
### 创建自定义配置的日志器
|
||||
|
||||
```typescript
|
||||
import { ConsoleLogger, LogLevel } from '@esengine/ecs-framework';
|
||||
|
||||
class CustomLoggerExample {
|
||||
private debugLogger: ConsoleLogger;
|
||||
private productionLogger: ConsoleLogger;
|
||||
|
||||
constructor() {
|
||||
// 开发环境日志器
|
||||
this.debugLogger = new ConsoleLogger({
|
||||
level: LogLevel.Debug,
|
||||
enableTimestamp: true,
|
||||
enableColors: true,
|
||||
prefix: 'DEV'
|
||||
});
|
||||
|
||||
// 生产环境日志器
|
||||
this.productionLogger = new ConsoleLogger({
|
||||
level: LogLevel.Error,
|
||||
enableTimestamp: true,
|
||||
enableColors: false,
|
||||
prefix: 'PROD'
|
||||
});
|
||||
}
|
||||
|
||||
public logDevelopmentInfo(): void {
|
||||
this.debugLogger.debug('这是调试信息');
|
||||
this.debugLogger.info('开发环境信息');
|
||||
}
|
||||
|
||||
public logProductionError(): void {
|
||||
this.productionLogger.error('生产环境错误');
|
||||
this.productionLogger.fatal('致命错误');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 颜色配置
|
||||
|
||||
### 使用预定义颜色
|
||||
|
||||
```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
|
||||
});
|
||||
```
|
||||
|
||||
### 完整颜色示例
|
||||
|
||||
```typescript
|
||||
import { LoggerManager, Colors, LogLevel } from '@esengine/ecs-framework';
|
||||
|
||||
class ColorLoggerDemo {
|
||||
private logger = createLogger('ColorDemo');
|
||||
|
||||
constructor() {
|
||||
// 设置自定义颜色
|
||||
const manager = LoggerManager.getInstance();
|
||||
manager.setGlobalColors({
|
||||
debug: Colors.CYAN,
|
||||
info: Colors.GREEN,
|
||||
warn: Colors.YELLOW,
|
||||
error: Colors.RED,
|
||||
fatal: `${Colors.BOLD}${Colors.BRIGHT_RED}`
|
||||
});
|
||||
}
|
||||
|
||||
public demonstrateColors(): void {
|
||||
this.logger.debug('这是蓝绿色的调试信息');
|
||||
this.logger.info('这是绿色的信息');
|
||||
this.logger.warn('这是黄色的警告');
|
||||
this.logger.error('这是红色的错误');
|
||||
this.logger.fatal('这是加粗的亮红色致命错误');
|
||||
}
|
||||
|
||||
public resetToDefaults(): void {
|
||||
// 重置为默认颜色
|
||||
LoggerManager.getInstance().resetColors();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 高级功能
|
||||
|
||||
### 分层日志器
|
||||
|
||||
```typescript
|
||||
import { LoggerManager } from '@esengine/ecs-framework';
|
||||
|
||||
class HierarchicalLoggingExample {
|
||||
private systemLogger = createLogger('GameSystems');
|
||||
private movementLogger: ILogger;
|
||||
private renderLogger: ILogger;
|
||||
|
||||
constructor() {
|
||||
const manager = LoggerManager.getInstance();
|
||||
|
||||
// 创建子日志器
|
||||
this.movementLogger = manager.createChildLogger('GameSystems', 'Movement');
|
||||
this.renderLogger = manager.createChildLogger('GameSystems', 'Render');
|
||||
}
|
||||
|
||||
public demonstrateHierarchy(): void {
|
||||
this.systemLogger.info('游戏系统启动');
|
||||
|
||||
// 子日志器会显示完整路径:[GameSystems.Movement]
|
||||
this.movementLogger.debug('移动系统初始化');
|
||||
|
||||
// 子日志器会显示完整路径:[GameSystems.Render]
|
||||
this.renderLogger.info('渲染系统启动');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 自定义输出
|
||||
|
||||
```typescript
|
||||
import { ConsoleLogger, LogLevel } from '@esengine/ecs-framework';
|
||||
|
||||
class CustomOutputLogger {
|
||||
private fileLogger: ConsoleLogger;
|
||||
private networkLogger: ConsoleLogger;
|
||||
|
||||
constructor() {
|
||||
// 输出到文件的日志器(模拟)
|
||||
this.fileLogger = new ConsoleLogger({
|
||||
level: LogLevel.Info,
|
||||
output: (level: LogLevel, message: string) => {
|
||||
this.writeToFile(LogLevel[level], message);
|
||||
}
|
||||
});
|
||||
|
||||
// 发送到网络的日志器(模拟)
|
||||
this.networkLogger = new ConsoleLogger({
|
||||
level: LogLevel.Error,
|
||||
output: (level: LogLevel, message: string) => {
|
||||
this.sendToServer(LogLevel[level], message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private writeToFile(level: string, message: string): void {
|
||||
// 模拟文件写入
|
||||
console.log(`[FILE] ${level}: ${message}`);
|
||||
}
|
||||
|
||||
private sendToServer(level: string, message: string): void {
|
||||
// 模拟网络发送
|
||||
console.log(`[NETWORK] ${level}: ${message}`);
|
||||
}
|
||||
|
||||
public logToFile(message: string): void {
|
||||
this.fileLogger.info(message);
|
||||
}
|
||||
|
||||
public logCriticalError(error: Error): void {
|
||||
this.networkLogger.error('Critical error occurred', error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 实际应用示例
|
||||
|
||||
### 游戏系统日志
|
||||
|
||||
```typescript
|
||||
class GameWithLogging {
|
||||
private gameLogger = createLogger('Game');
|
||||
private performanceLogger = createLogger('Performance');
|
||||
private networkLogger = createLogger('Network');
|
||||
|
||||
constructor() {
|
||||
// 在开发环境启用详细日志
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
setGlobalLogLevel(LogLevel.Debug);
|
||||
} else {
|
||||
setGlobalLogLevel(LogLevel.Warn);
|
||||
}
|
||||
}
|
||||
|
||||
public startGame(): void {
|
||||
this.gameLogger.info('游戏开始启动');
|
||||
|
||||
try {
|
||||
this.initializeSystems();
|
||||
this.loadResources();
|
||||
this.startGameLoop();
|
||||
|
||||
this.gameLogger.info('游戏启动成功');
|
||||
} catch (error) {
|
||||
this.gameLogger.fatal('游戏启动失败', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private initializeSystems(): void {
|
||||
this.gameLogger.debug('初始化游戏系统');
|
||||
|
||||
const systems = [
|
||||
new MovementSystem(),
|
||||
new RenderSystem(),
|
||||
new PhysicsSystem()
|
||||
];
|
||||
|
||||
for (const system of systems) {
|
||||
const startTime = performance.now();
|
||||
|
||||
// 初始化系统
|
||||
system.initialize();
|
||||
|
||||
const endTime = performance.now();
|
||||
this.performanceLogger.debug(
|
||||
`系统 ${system.systemName} 初始化耗时: ${(endTime - startTime).toFixed(2)}ms`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private loadResources(): void {
|
||||
this.gameLogger.info('开始加载资源');
|
||||
|
||||
const resources = ['textures', 'sounds', 'data'];
|
||||
for (const resource of resources) {
|
||||
try {
|
||||
this.loadResource(resource);
|
||||
this.gameLogger.debug(`资源 ${resource} 加载成功`);
|
||||
} catch (error) {
|
||||
this.gameLogger.error(`资源 ${resource} 加载失败`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private startGameLoop(): void {
|
||||
this.gameLogger.info('启动游戏循环');
|
||||
this.performanceLogger.debug('开始性能监控');
|
||||
}
|
||||
|
||||
private loadResource(name: string): void {
|
||||
// 模拟资源加载
|
||||
if (Math.random() < 0.1) {
|
||||
throw new Error(`Failed to load ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
public handleNetworkEvent(event: string, data: any): void {
|
||||
this.networkLogger.info(`网络事件: ${event}`, data);
|
||||
|
||||
if (event === 'connection_lost') {
|
||||
this.networkLogger.warn('网络连接丢失,尝试重连');
|
||||
} else if (event === 'sync_error') {
|
||||
this.networkLogger.error('数据同步错误', data);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 错误追踪和调试
|
||||
|
||||
```typescript
|
||||
class ErrorTrackingSystem extends EntitySystem {
|
||||
private logger = createLogger('ErrorTracker');
|
||||
private errorCounts = new Map<string, number>();
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
try {
|
||||
this.processEntity(entity);
|
||||
} catch (error) {
|
||||
this.handleError(entity, error as Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private processEntity(entity: Entity): void {
|
||||
// 模拟可能出错的实体处理
|
||||
if (Math.random() < 0.01) { // 1% 概率出错
|
||||
throw new Error(`Processing error for entity ${entity.id}`);
|
||||
}
|
||||
|
||||
this.logger.debug(`成功处理实体 ${entity.id}`);
|
||||
}
|
||||
|
||||
private handleError(entity: Entity, error: Error): void {
|
||||
const errorKey = error.message;
|
||||
const count = this.errorCounts.get(errorKey) || 0;
|
||||
this.errorCounts.set(errorKey, count + 1);
|
||||
|
||||
this.logger.error(
|
||||
`实体 ${entity.id} 处理失败 (第${count + 1}次): ${error.message}`,
|
||||
{
|
||||
entityId: entity.id,
|
||||
entityName: entity.name,
|
||||
componentCount: entity.components.length,
|
||||
errorStack: error.stack
|
||||
}
|
||||
);
|
||||
|
||||
// 如果同一类型错误发生太多次,升级为警告
|
||||
if (count >= 5) {
|
||||
this.logger.warn(`错误 "${errorKey}" 已发生 ${count + 1} 次,可能需要关注`);
|
||||
}
|
||||
|
||||
// 如果错误次数过多,升级为致命错误
|
||||
if (count >= 20) {
|
||||
this.logger.fatal(`错误 "${errorKey}" 发生次数过多,系统可能存在严重问题`);
|
||||
}
|
||||
}
|
||||
|
||||
public getErrorSummary(): void {
|
||||
this.logger.info('=== 错误统计 ===');
|
||||
for (const [error, count] of this.errorCounts) {
|
||||
this.logger.info(`${error}: ${count} 次`);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 合理的日志级别选择
|
||||
|
||||
```typescript
|
||||
class LoggingBestPractices {
|
||||
private logger = createLogger('BestPractices');
|
||||
|
||||
public demonstrateLogLevels(): void {
|
||||
// ✅ Debug - 详细的调试信息
|
||||
this.logger.debug('变量值', { x: 10, y: 20 });
|
||||
|
||||
// ✅ Info - 重要的状态变化
|
||||
this.logger.info('系统启动完成');
|
||||
|
||||
// ✅ Warn - 异常但不致命的情况
|
||||
this.logger.warn('资源未找到,使用默认值');
|
||||
|
||||
// ✅ Error - 错误但程序可以继续
|
||||
this.logger.error('保存失败,将重试', new Error('Network timeout'));
|
||||
|
||||
// ✅ Fatal - 致命错误,程序无法继续
|
||||
this.logger.fatal('内存不足,程序即将退出');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 结构化日志数据
|
||||
|
||||
```typescript
|
||||
class StructuredLogging {
|
||||
private logger = createLogger('Structured');
|
||||
|
||||
public logWithStructuredData(): void {
|
||||
// ✅ 提供结构化的上下文信息
|
||||
this.logger.info('用户操作', {
|
||||
userId: 12345,
|
||||
action: 'move',
|
||||
position: { x: 100, y: 200 },
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
// ✅ 包含相关的错误上下文
|
||||
this.logger.error('数据库查询失败', {
|
||||
query: 'SELECT * FROM users',
|
||||
parameters: { id: 123 },
|
||||
connectionId: 'conn_456',
|
||||
retryCount: 3
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 避免日志性能问题
|
||||
|
||||
```typescript
|
||||
class PerformanceConsciousLogging {
|
||||
private logger = createLogger('Performance');
|
||||
|
||||
public efficientLogging(): void {
|
||||
// ✅ 检查日志级别避免不必要的计算
|
||||
if (this.logger.debug) {
|
||||
const expensiveData = this.calculateExpensiveDebugInfo();
|
||||
this.logger.debug('详细调试信息', expensiveData);
|
||||
}
|
||||
|
||||
// ❌ 避免:总是计算昂贵的日志数据
|
||||
// this.logger.debug('调试信息', this.calculateExpensiveDebugInfo());
|
||||
}
|
||||
|
||||
private calculateExpensiveDebugInfo(): any {
|
||||
// 模拟昂贵的计算
|
||||
return { /* 复杂的调试数据 */ };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 日志配置管理
|
||||
|
||||
```typescript
|
||||
class LoggingConfiguration {
|
||||
public static setupLogging(): void {
|
||||
// 根据环境配置日志级别
|
||||
const isDevelopment = process.env.NODE_ENV === 'development';
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
if (isDevelopment) {
|
||||
setGlobalLogLevel(LogLevel.Debug);
|
||||
setLoggerColors({
|
||||
debug: Colors.CYAN,
|
||||
info: Colors.GREEN,
|
||||
warn: Colors.YELLOW,
|
||||
error: Colors.RED,
|
||||
fatal: Colors.BRIGHT_RED
|
||||
});
|
||||
} else if (isProduction) {
|
||||
setGlobalLogLevel(LogLevel.Warn);
|
||||
// 生产环境禁用颜色
|
||||
LoggerManager.getInstance().resetColors();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 在应用启动时配置日志
|
||||
LoggingConfiguration.setupLogging();
|
||||
```
|
||||
|
||||
日志系统是调试和监控应用的重要工具,正确使用日志系统能大大提高开发效率和问题排查能力。
|
||||
289
docs/guide/platform-adapter.md
Normal file
289
docs/guide/platform-adapter.md
Normal file
@@ -0,0 +1,289 @@
|
||||
# 平台适配器
|
||||
|
||||
## 概述
|
||||
|
||||
ECS框架提供了平台适配器接口,允许用户为不同的运行环境实现自定义的平台适配器。
|
||||
|
||||
**核心库只提供接口定义,平台适配器实现代码请从文档中复制使用。**
|
||||
|
||||
## 为什么不提供单独的适配器包?
|
||||
|
||||
1. **灵活性**: 不同项目对平台适配的需求可能不同,复制代码可以让用户根据需要自由修改
|
||||
2. **减少依赖**: 避免引入不必要的依赖包,保持核心框架的精简
|
||||
3. **定制化**: 用户可以根据具体的运行环境和需求进行定制
|
||||
|
||||
## 支持的平台
|
||||
|
||||
### 🌐 [浏览器适配器](./platform-adapter/browser.md)
|
||||
|
||||
支持所有现代浏览器环境,包括 Chrome、Firefox、Safari、Edge 等。
|
||||
|
||||
**特性支持**:
|
||||
- ✅ Worker (Web Worker)
|
||||
- ✅ SharedArrayBuffer (需要COOP/COEP)
|
||||
- ✅ Transferable Objects
|
||||
- ✅ Module Worker (现代浏览器)
|
||||
|
||||
**适用场景**: Web游戏、Web应用、PWA
|
||||
|
||||
---
|
||||
|
||||
### 📱 [微信小游戏适配器](./platform-adapter/wechat-minigame.md)
|
||||
|
||||
专为微信小游戏环境设计,处理微信小游戏的特殊限制和API。
|
||||
|
||||
**特性支持**:
|
||||
- ✅ Worker (最多1个,需配置game.json)
|
||||
- ❌ SharedArrayBuffer
|
||||
- ❌ Transferable Objects
|
||||
- ✅ 微信设备信息API
|
||||
|
||||
**适用场景**: 微信小游戏开发
|
||||
|
||||
---
|
||||
|
||||
### 🖥️ [Node.js适配器](./platform-adapter/nodejs.md)
|
||||
|
||||
为 Node.js 服务器环境提供支持,适用于游戏服务器和计算服务器。
|
||||
|
||||
**特性支持**:
|
||||
- ✅ Worker Threads
|
||||
- ✅ SharedArrayBuffer
|
||||
- ✅ Transferable Objects
|
||||
- ✅ 完整系统信息
|
||||
|
||||
**适用场景**: 游戏服务器、计算服务器、CLI工具
|
||||
|
||||
---
|
||||
|
||||
## 核心接口
|
||||
|
||||
### 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 接口
|
||||
|
||||
```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';
|
||||
}
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 选择合适的平台适配器
|
||||
|
||||
根据你的运行环境选择对应的适配器:
|
||||
|
||||
```typescript
|
||||
import { PlatformManager } from '@esengine/ecs-framework';
|
||||
|
||||
// 浏览器环境
|
||||
if (typeof window !== 'undefined') {
|
||||
const { BrowserAdapter } = await import('./platform/BrowserAdapter');
|
||||
PlatformManager.getInstance().registerAdapter(new BrowserAdapter());
|
||||
}
|
||||
|
||||
// 微信小游戏环境
|
||||
else if (typeof wx !== 'undefined') {
|
||||
const { WeChatMiniGameAdapter } = await import('./platform/WeChatMiniGameAdapter');
|
||||
PlatformManager.getInstance().registerAdapter(new WeChatMiniGameAdapter());
|
||||
}
|
||||
|
||||
// Node.js环境
|
||||
else if (typeof process !== 'undefined' && process.versions?.node) {
|
||||
const { NodeAdapter } = await import('./platform/NodeAdapter');
|
||||
PlatformManager.getInstance().registerAdapter(new NodeAdapter());
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 检查适配器状态
|
||||
|
||||
```typescript
|
||||
const manager = PlatformManager.getInstance();
|
||||
|
||||
// 检查是否已注册适配器
|
||||
if (manager.hasAdapter()) {
|
||||
const adapter = manager.getAdapter();
|
||||
console.log('当前平台:', adapter.name);
|
||||
console.log('平台版本:', adapter.version);
|
||||
|
||||
// 检查功能支持
|
||||
console.log('Worker支持:', manager.supportsFeature('worker'));
|
||||
console.log('SharedArrayBuffer支持:', manager.supportsFeature('shared-array-buffer'));
|
||||
}
|
||||
```
|
||||
|
||||
## 创建自定义适配器
|
||||
|
||||
如果现有的平台适配器不能满足你的需求,你可以创建自定义适配器:
|
||||
|
||||
### 1. 实现接口
|
||||
|
||||
```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 {
|
||||
// 实现你的 Worker 支持检查逻辑
|
||||
return false;
|
||||
}
|
||||
|
||||
public isSharedArrayBufferSupported(): boolean {
|
||||
// 实现你的 SharedArrayBuffer 支持检查逻辑
|
||||
return false;
|
||||
}
|
||||
|
||||
public getHardwareConcurrency(): number {
|
||||
// 返回你的平台的并发数
|
||||
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. 注册自定义适配器
|
||||
|
||||
```typescript
|
||||
import { PlatformManager } from '@esengine/ecs-framework';
|
||||
import { CustomAdapter } from './CustomAdapter';
|
||||
|
||||
const customAdapter = new CustomAdapter();
|
||||
PlatformManager.getInstance().registerAdapter(customAdapter);
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 平台检测顺序
|
||||
|
||||
建议按照以下顺序检测和注册平台适配器:
|
||||
|
||||
```typescript
|
||||
async function initializePlatform() {
|
||||
const manager = PlatformManager.getInstance();
|
||||
|
||||
try {
|
||||
// 1. 微信小游戏 (优先级最高,环境特征最明显)
|
||||
if (typeof wx !== 'undefined' && wx.getSystemInfoSync) {
|
||||
const { WeChatMiniGameAdapter } = await import('./platform/WeChatMiniGameAdapter');
|
||||
manager.registerAdapter(new WeChatMiniGameAdapter());
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Node.js 环境
|
||||
if (typeof process !== 'undefined' && process.versions?.node) {
|
||||
const { NodeAdapter } = await import('./platform/NodeAdapter');
|
||||
manager.registerAdapter(new NodeAdapter());
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 浏览器环境 (最后检测,覆盖面最广)
|
||||
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
|
||||
const { BrowserAdapter } = await import('./platform/BrowserAdapter');
|
||||
manager.registerAdapter(new BrowserAdapter());
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. 未知环境,使用默认适配器
|
||||
console.warn('未识别的平台环境,使用默认适配器');
|
||||
manager.registerAdapter(new CustomAdapter());
|
||||
|
||||
} catch (error) {
|
||||
console.error('平台适配器初始化失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 功能降级处理
|
||||
|
||||
```typescript
|
||||
function createWorkerSystem() {
|
||||
const manager = PlatformManager.getInstance();
|
||||
|
||||
if (!manager.hasAdapter()) {
|
||||
throw new Error('未注册平台适配器');
|
||||
}
|
||||
|
||||
const config: WorkerSystemConfig = {
|
||||
enableWorker: manager.supportsFeature('worker'),
|
||||
workerCount: manager.supportsFeature('worker') ?
|
||||
manager.getAdapter().getHardwareConcurrency() : 1,
|
||||
useSharedArrayBuffer: manager.supportsFeature('shared-array-buffer')
|
||||
};
|
||||
|
||||
// 如果不支持Worker,自动降级到同步处理
|
||||
if (!config.enableWorker) {
|
||||
console.info('当前平台不支持Worker,使用同步处理模式');
|
||||
}
|
||||
|
||||
return new PhysicsSystem(config);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 错误处理
|
||||
|
||||
```typescript
|
||||
try {
|
||||
await initializePlatform();
|
||||
|
||||
// 验证适配器功能
|
||||
const manager = PlatformManager.getInstance();
|
||||
const adapter = manager.getAdapter();
|
||||
|
||||
console.log(`平台适配器初始化成功: ${adapter.name} v${adapter.version}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('平台初始化失败:', error);
|
||||
|
||||
// 提供降级方案
|
||||
const fallbackAdapter = new CustomAdapter();
|
||||
PlatformManager.getInstance().registerAdapter(fallbackAdapter);
|
||||
|
||||
console.warn('使用降级适配器继续运行');
|
||||
}
|
||||
```
|
||||
370
docs/guide/platform-adapter/browser.md
Normal file
370
docs/guide/platform-adapter/browser.md
Normal file
@@ -0,0 +1,370 @@
|
||||
# 浏览器适配器
|
||||
|
||||
## 概述
|
||||
|
||||
浏览器平台适配器为标准Web浏览器环境提供支持,包括 Chrome、Firefox、Safari、Edge 等现代浏览器。
|
||||
|
||||
## 特性支持
|
||||
|
||||
- ✅ **Worker**: 支持 Web Worker 和 Module Worker
|
||||
- ✅ **SharedArrayBuffer**: 支持(需要COOP/COEP头部)
|
||||
- ✅ **Transferable Objects**: 完全支持
|
||||
- ✅ **高精度时间**: 使用 `performance.now()`
|
||||
- ✅ **基础信息**: 浏览器版本和基本配置
|
||||
|
||||
## 完整实现
|
||||
|
||||
```typescript
|
||||
import type {
|
||||
IPlatformAdapter,
|
||||
PlatformWorker,
|
||||
WorkerCreationOptions,
|
||||
PlatformConfig
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* 浏览器平台适配器
|
||||
* 支持标准Web浏览器环境
|
||||
*/
|
||||
export class BrowserAdapter implements IPlatformAdapter {
|
||||
public readonly name = 'browser';
|
||||
public readonly version: string;
|
||||
|
||||
constructor() {
|
||||
this.version = this.getBrowserInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否支持Worker
|
||||
*/
|
||||
public isWorkerSupported(): boolean {
|
||||
return typeof Worker !== 'undefined';
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否支持SharedArrayBuffer
|
||||
*/
|
||||
public isSharedArrayBufferSupported(): boolean {
|
||||
return typeof SharedArrayBuffer !== 'undefined' && this.checkSharedArrayBufferEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取硬件并发数(CPU核心数)
|
||||
*/
|
||||
public getHardwareConcurrency(): number {
|
||||
return navigator.hardwareConcurrency || 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建Worker
|
||||
*/
|
||||
public createWorker(script: string, options: WorkerCreationOptions = {}): PlatformWorker {
|
||||
if (!this.isWorkerSupported()) {
|
||||
throw new Error('浏览器不支持Worker');
|
||||
}
|
||||
|
||||
try {
|
||||
return new BrowserWorker(script, options);
|
||||
} catch (error) {
|
||||
throw new Error(`创建浏览器Worker失败: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建SharedArrayBuffer
|
||||
*/
|
||||
public createSharedArrayBuffer(length: number): SharedArrayBuffer | null {
|
||||
if (!this.isSharedArrayBufferSupported()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return new SharedArrayBuffer(length);
|
||||
} catch (error) {
|
||||
console.warn('SharedArrayBuffer创建失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取高精度时间戳
|
||||
*/
|
||||
public getHighResTimestamp(): number {
|
||||
return performance.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取平台配置
|
||||
*/
|
||||
public getPlatformConfig(): PlatformConfig {
|
||||
return {
|
||||
maxWorkerCount: this.getHardwareConcurrency(),
|
||||
supportsModuleWorker: false,
|
||||
supportsTransferableObjects: true,
|
||||
maxSharedArrayBufferSize: 1024 * 1024 * 1024, // 1GB
|
||||
workerScriptPrefix: '',
|
||||
limitations: {
|
||||
noEval: false,
|
||||
requiresWorkerInit: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取浏览器信息
|
||||
*/
|
||||
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';
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查SharedArrayBuffer是否真正可用
|
||||
*/
|
||||
private checkSharedArrayBufferEnabled(): boolean {
|
||||
try {
|
||||
new SharedArrayBuffer(8);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 浏览器Worker封装
|
||||
*/
|
||||
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已被终止');
|
||||
}
|
||||
|
||||
try {
|
||||
if (transfer && transfer.length > 0) {
|
||||
this.worker.postMessage(message, transfer);
|
||||
} else {
|
||||
this.worker.postMessage(message);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`发送消息到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('终止Worker失败:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建浏览器Worker
|
||||
*/
|
||||
private createBrowserWorker(script: string, options: WorkerCreationOptions): Worker {
|
||||
try {
|
||||
// 创建Blob URL
|
||||
const blob = new Blob([script], { type: 'application/javascript' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
// 创建Worker
|
||||
const worker = new Worker(url, {
|
||||
type: options.type || 'classic',
|
||||
credentials: options.credentials,
|
||||
name: options.name
|
||||
});
|
||||
|
||||
// 清理Blob URL(延迟清理,确保Worker已加载)
|
||||
setTimeout(() => {
|
||||
URL.revokeObjectURL(url);
|
||||
}, 1000);
|
||||
|
||||
return worker;
|
||||
} catch (error) {
|
||||
throw new Error(`无法创建浏览器Worker: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 复制代码
|
||||
|
||||
将上述代码复制到你的项目中,例如 `src/platform/BrowserAdapter.ts`。
|
||||
|
||||
### 2. 注册适配器
|
||||
|
||||
```typescript
|
||||
import { PlatformManager } from '@esengine/ecs-framework';
|
||||
import { BrowserAdapter } from './platform/BrowserAdapter';
|
||||
|
||||
// 创建并注册浏览器适配器
|
||||
const browserAdapter = new BrowserAdapter();
|
||||
PlatformManager.registerAdapter(browserAdapter);
|
||||
|
||||
// 框架会自动检测和使用合适的适配器
|
||||
```
|
||||
|
||||
### 3. 使用 WorkerEntitySystem
|
||||
|
||||
浏览器适配器与 WorkerEntitySystem 配合使用,框架会自动处理 Worker 脚本的创建:
|
||||
|
||||
```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
|
||||
};
|
||||
}
|
||||
|
||||
// 这个函数会被自动序列化并在Worker中执行
|
||||
protected workerProcess(entities, deltaTime, config) {
|
||||
return entities.map(entity => {
|
||||
// 应用重力
|
||||
entity.vy += config.gravity * deltaTime;
|
||||
|
||||
// 更新位置
|
||||
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. 验证适配器工作状态
|
||||
|
||||
```typescript
|
||||
// 验证适配器是否正常工作
|
||||
const adapter = new BrowserAdapter();
|
||||
console.log('适配器名称:', adapter.name);
|
||||
console.log('浏览器版本:', adapter.version);
|
||||
console.log('Worker支持:', adapter.isWorkerSupported());
|
||||
console.log('SharedArrayBuffer支持:', adapter.isSharedArrayBufferSupported());
|
||||
console.log('CPU核心数:', adapter.getHardwareConcurrency());
|
||||
```
|
||||
|
||||
## 重要注意事项
|
||||
|
||||
### SharedArrayBuffer 支持
|
||||
|
||||
SharedArrayBuffer 需要特殊的安全配置:
|
||||
|
||||
1. **HTTPS**: 必须在安全上下文中使用
|
||||
2. **COOP/COEP 头部**: 需要设置正确的跨域隔离头部
|
||||
|
||||
```html
|
||||
<!-- 在 HTML 中设置 -->
|
||||
<meta http-equiv="Cross-Origin-Opener-Policy" content="same-origin">
|
||||
<meta http-equiv="Cross-Origin-Embedder-Policy" content="require-corp">
|
||||
```
|
||||
|
||||
或在服务器配置中设置:
|
||||
|
||||
```
|
||||
Cross-Origin-Opener-Policy: same-origin
|
||||
Cross-Origin-Embedder-Policy: require-corp
|
||||
```
|
||||
|
||||
### 浏览器兼容性
|
||||
|
||||
- **Worker**: 所有现代浏览器支持
|
||||
- **Module Worker**: Chrome 80+, Firefox 114+
|
||||
- **SharedArrayBuffer**: Chrome 68+, Firefox 79+(需要COOP/COEP)
|
||||
- **Transferable Objects**: 所有现代浏览器支持
|
||||
|
||||
## 性能优化建议
|
||||
|
||||
1. **Worker 池**: 复用 Worker 实例,避免频繁创建和销毁
|
||||
2. **数据传输**: 使用 Transferable Objects 减少数据拷贝
|
||||
3. **SharedArrayBuffer**: 对于大量数据共享,使用 SharedArrayBuffer
|
||||
4. **模块 Worker**: 在支持的浏览器中使用模块 Worker 来更好地组织代码
|
||||
|
||||
## 调试技巧
|
||||
|
||||
```typescript
|
||||
// 检查浏览器支持情况
|
||||
const adapter = new BrowserAdapter();
|
||||
console.log('Worker支持:', adapter.isWorkerSupported());
|
||||
console.log('SharedArrayBuffer支持:', adapter.isSharedArrayBufferSupported());
|
||||
console.log('硬件并发数:', adapter.getHardwareConcurrency());
|
||||
console.log('平台配置:', adapter.getPlatformConfig());
|
||||
```
|
||||
558
docs/guide/platform-adapter/nodejs.md
Normal file
558
docs/guide/platform-adapter/nodejs.md
Normal file
@@ -0,0 +1,558 @@
|
||||
# Node.js 适配器
|
||||
|
||||
## 概述
|
||||
|
||||
Node.js 平台适配器为 Node.js 服务器环境提供支持,适用于游戏服务器、计算服务器或其他需要 ECS 架构的服务器应用。
|
||||
|
||||
## 特性支持
|
||||
|
||||
- ✅ **Worker**: 支持(通过 `worker_threads` 模块)
|
||||
- ❌ **SharedArrayBuffer**: 支持(Node.js 16.17.0+)
|
||||
- ✅ **Transferable Objects**: 完全支持
|
||||
- ✅ **高精度时间**: 使用 `process.hrtime.bigint()`
|
||||
- ✅ **设备信息**: 完整的系统和进程信息
|
||||
|
||||
## 完整实现
|
||||
|
||||
```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 平台适配器
|
||||
* 支持 Node.js 服务器环境
|
||||
*/
|
||||
export class NodeAdapter implements IPlatformAdapter {
|
||||
public readonly name = 'nodejs';
|
||||
public readonly version: string;
|
||||
|
||||
constructor() {
|
||||
this.version = process.version;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否支持Worker
|
||||
*/
|
||||
public isWorkerSupported(): boolean {
|
||||
try {
|
||||
// 检查 worker_threads 模块是否可用
|
||||
return typeof worker_threads !== 'undefined' && typeof Worker !== 'undefined';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否支持SharedArrayBuffer
|
||||
*/
|
||||
public isSharedArrayBufferSupported(): boolean {
|
||||
// Node.js 支持 SharedArrayBuffer
|
||||
return typeof SharedArrayBuffer !== 'undefined';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取硬件并发数(CPU核心数)
|
||||
*/
|
||||
public getHardwareConcurrency(): number {
|
||||
return os.cpus().length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建Worker
|
||||
*/
|
||||
public createWorker(script: string, options: WorkerCreationOptions = {}): PlatformWorker {
|
||||
if (!this.isWorkerSupported()) {
|
||||
throw new Error('Node.js环境不支持Worker Threads');
|
||||
}
|
||||
|
||||
try {
|
||||
return new NodeWorker(script, options);
|
||||
} catch (error) {
|
||||
throw new Error(`创建Node.js Worker失败: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建SharedArrayBuffer
|
||||
*/
|
||||
public createSharedArrayBuffer(length: number): SharedArrayBuffer | null {
|
||||
if (!this.isSharedArrayBufferSupported()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return new SharedArrayBuffer(length);
|
||||
} catch (error) {
|
||||
console.warn('SharedArrayBuffer创建失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取高精度时间戳(纳秒)
|
||||
*/
|
||||
public getHighResTimestamp(): number {
|
||||
// 返回毫秒,与浏览器 performance.now() 保持一致
|
||||
return Number(process.hrtime.bigint()) / 1000000;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取平台配置
|
||||
*/
|
||||
public getPlatformConfig(): PlatformConfig {
|
||||
return {
|
||||
maxWorkerCount: this.getHardwareConcurrency(),
|
||||
supportsModuleWorker: true, // Node.js 支持 ES 模块
|
||||
supportsTransferableObjects: true,
|
||||
maxSharedArrayBufferSize: this.getMaxSharedArrayBufferSize(),
|
||||
workerScriptPrefix: '',
|
||||
limitations: {
|
||||
noEval: false, // Node.js 支持 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
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Node.js设备信息
|
||||
*/
|
||||
public getDeviceInfo(): NodeDeviceInfo {
|
||||
const cpus = os.cpus();
|
||||
const networkInterfaces = os.networkInterfaces();
|
||||
const userInfo = os.userInfo();
|
||||
|
||||
return {
|
||||
// 系统信息
|
||||
platform: os.platform(),
|
||||
arch: os.arch(),
|
||||
type: os.type(),
|
||||
release: os.release(),
|
||||
version: os.version(),
|
||||
hostname: os.hostname(),
|
||||
|
||||
// CPU信息
|
||||
cpus: cpus.map(cpu => ({
|
||||
model: cpu.model,
|
||||
speed: cpu.speed,
|
||||
times: cpu.times
|
||||
})),
|
||||
|
||||
// 内存信息
|
||||
totalMemory: os.totalmem(),
|
||||
freeMemory: os.freemem(),
|
||||
usedMemory: os.totalmem() - os.freemem(),
|
||||
|
||||
// 负载信息
|
||||
loadAverage: os.loadavg(),
|
||||
|
||||
// 网络接口
|
||||
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: {
|
||||
pid: process.pid,
|
||||
ppid: process.ppid,
|
||||
version: process.version,
|
||||
versions: process.versions,
|
||||
uptime: process.uptime()
|
||||
},
|
||||
|
||||
// 用户信息
|
||||
userInfo: {
|
||||
uid: userInfo.uid,
|
||||
gid: userInfo.gid,
|
||||
username: userInfo.username,
|
||||
homedir: userInfo.homedir,
|
||||
shell: userInfo.shell
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取SharedArrayBuffer最大大小限制
|
||||
*/
|
||||
private getMaxSharedArrayBufferSize(): number {
|
||||
const totalMemory = os.totalmem();
|
||||
// 限制为系统总内存的50%
|
||||
return Math.floor(totalMemory * 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Node.js Worker封装
|
||||
*/
|
||||
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 {
|
||||
// 判断 script 是文件路径还是脚本内容
|
||||
if (this.isFilePath(script)) {
|
||||
// 直接使用文件路径
|
||||
this.scriptPath = script;
|
||||
this.isTemporaryFile = false;
|
||||
} else {
|
||||
// 将脚本内容写入临时文件
|
||||
this.scriptPath = this.writeScriptToFile(script, options.name);
|
||||
this.isTemporaryFile = true;
|
||||
}
|
||||
|
||||
// 创建Worker
|
||||
this.worker = new Worker(this.scriptPath, {
|
||||
// Node.js Worker options
|
||||
workerData: options.name ? { name: options.name } : undefined
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(`创建Node.js Worker失败: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为文件路径
|
||||
*/
|
||||
private isFilePath(script: string): boolean {
|
||||
// 检查是否看起来像文件路径
|
||||
return (script.endsWith('.js') || script.endsWith('.mjs') || script.endsWith('.ts')) &&
|
||||
!script.includes('\n') &&
|
||||
!script.includes(';') &&
|
||||
script.length < 500; // 文件路径通常不会太长
|
||||
}
|
||||
|
||||
/**
|
||||
* 将脚本内容写入临时文件
|
||||
*/
|
||||
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(`写入Worker脚本文件失败: ${(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已被终止');
|
||||
}
|
||||
|
||||
try {
|
||||
if (transfer && transfer.length > 0) {
|
||||
// Node.js Worker 支持 Transferable Objects
|
||||
this.worker.postMessage(message, transfer);
|
||||
} else {
|
||||
this.worker.postMessage(message);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`发送消息到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) => {
|
||||
// 将 Error 转换为 ErrorEvent 格式
|
||||
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';
|
||||
|
||||
// 清理临时脚本文件
|
||||
this.cleanupScriptFile();
|
||||
} catch (error) {
|
||||
console.error('终止Node.js Worker失败:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理临时脚本文件
|
||||
*/
|
||||
private cleanupScriptFile(): void {
|
||||
// 只清理临时创建的文件,不清理用户提供的文件路径
|
||||
if (this.scriptPath && this.isTemporaryFile) {
|
||||
try {
|
||||
fs.unlinkSync(this.scriptPath);
|
||||
} catch (error) {
|
||||
console.warn('清理Worker脚本文件失败:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 复制代码
|
||||
|
||||
将上述代码复制到你的项目中,例如 `src/platform/NodeAdapter.ts`。
|
||||
|
||||
### 2. 注册适配器
|
||||
|
||||
```typescript
|
||||
import { PlatformManager } from '@esengine/ecs-framework';
|
||||
import { NodeAdapter } from './platform/NodeAdapter';
|
||||
|
||||
// 检查是否在Node.js环境
|
||||
if (typeof process !== 'undefined' && process.versions && process.versions.node) {
|
||||
const nodeAdapter = new NodeAdapter();
|
||||
PlatformManager.getInstance().registerAdapter(nodeAdapter);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 使用 WorkerEntitySystem
|
||||
|
||||
Node.js 适配器与 WorkerEntitySystem 配合使用,框架会自动处理 Worker 脚本的创建:
|
||||
|
||||
```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, // 使用所有CPU核心
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
// 这个函数会被自动序列化并在Worker中执行
|
||||
protected workerProcess(entities, deltaTime, config) {
|
||||
return entities.map(entity => {
|
||||
// 应用重力
|
||||
entity.vy += config.gravity * deltaTime;
|
||||
|
||||
// 更新位置
|
||||
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. 获取系统信息
|
||||
|
||||
```typescript
|
||||
const manager = PlatformManager.getInstance();
|
||||
if (manager.hasAdapter()) {
|
||||
const adapter = manager.getAdapter();
|
||||
const deviceInfo = adapter.getDeviceInfo();
|
||||
|
||||
console.log('Node.js版本:', deviceInfo.process?.version);
|
||||
console.log('CPU核心数:', deviceInfo.cpus?.length);
|
||||
console.log('总内存:', Math.round(deviceInfo.totalMemory! / 1024 / 1024), 'MB');
|
||||
console.log('可用内存:', Math.round(deviceInfo.freeMemory! / 1024 / 1024), 'MB');
|
||||
}
|
||||
```
|
||||
|
||||
## 官方文档参考
|
||||
|
||||
Node.js Worker Threads 相关官方文档:
|
||||
|
||||
- [Worker Threads 官方文档](https://nodejs.org/api/worker_threads.html)
|
||||
- [SharedArrayBuffer 支持](https://nodejs.org/api/globals.html#class-sharedarraybuffer)
|
||||
- [OS 模块文档](https://nodejs.org/api/os.html)
|
||||
- [Process 模块文档](https://nodejs.org/api/process.html)
|
||||
|
||||
## 重要注意事项
|
||||
|
||||
### Worker Threads 要求
|
||||
|
||||
- **Node.js版本**: 需要 Node.js 10.5.0+ (建议 12+)
|
||||
- **模块类型**: 支持 CommonJS 和 ES 模块
|
||||
- **线程限制**: 理论上无限制,但建议不超过 CPU 核心数的 2 倍
|
||||
|
||||
### 性能优化建议
|
||||
|
||||
#### 1. Worker 池管理
|
||||
|
||||
```typescript
|
||||
class ServerPhysicsSystem extends WorkerEntitySystem {
|
||||
constructor() {
|
||||
const cpuCount = os.cpus().length;
|
||||
super(Matcher.all(Transform, Velocity), {
|
||||
enableWorker: true,
|
||||
workerCount: Math.min(cpuCount * 2, 16), // 最多16个Worker
|
||||
entitiesPerWorker: 1000, // 每个Worker处理1000个实体
|
||||
useSharedArrayBuffer: true,
|
||||
systemConfig: {
|
||||
gravity: 9.8,
|
||||
timeStep: 1/60
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 内存管理
|
||||
|
||||
```typescript
|
||||
class MemoryMonitor {
|
||||
public static checkMemoryUsage(): void {
|
||||
const used = process.memoryUsage();
|
||||
|
||||
console.log('内存使用情况:');
|
||||
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`);
|
||||
|
||||
// 内存使用率过高时触发警告
|
||||
if (used.heapUsed > used.heapTotal * 0.9) {
|
||||
console.warn('内存使用率过高,建议优化或重启');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 定期检查内存使用
|
||||
setInterval(() => {
|
||||
MemoryMonitor.checkMemoryUsage();
|
||||
}, 30000); // 每30秒检查一次
|
||||
```
|
||||
|
||||
#### 3. 服务器环境优化
|
||||
|
||||
```typescript
|
||||
// 设置进程标题
|
||||
process.title = 'ecs-game-server';
|
||||
|
||||
// 处理未捕获异常
|
||||
process.on('uncaughtException', (error) => {
|
||||
console.error('未捕获异常:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.error('未处理的Promise拒绝:', reason);
|
||||
});
|
||||
|
||||
// 优雅关闭
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('收到SIGTERM信号,正在关闭服务器...');
|
||||
// 清理资源
|
||||
process.exit(0);
|
||||
});
|
||||
```
|
||||
|
||||
## 调试技巧
|
||||
|
||||
```typescript
|
||||
// 检查Node.js环境支持情况
|
||||
const adapter = new NodeAdapter();
|
||||
console.log('Node.js版本:', adapter.version);
|
||||
console.log('Worker支持:', adapter.isWorkerSupported());
|
||||
console.log('SharedArrayBuffer支持:', adapter.isSharedArrayBufferSupported());
|
||||
console.log('CPU核心数:', adapter.getHardwareConcurrency());
|
||||
|
||||
// 获取详细配置
|
||||
const config = adapter.getPlatformConfig();
|
||||
console.log('平台配置:', JSON.stringify(config, null, 2));
|
||||
|
||||
// 系统资源监控
|
||||
const deviceInfo = adapter.getDeviceInfo();
|
||||
console.log('系统负载:', deviceInfo.loadAverage);
|
||||
console.log('网络接口:', Object.keys(deviceInfo.networkInterfaces!));
|
||||
```
|
||||
677
docs/guide/platform-adapter/wechat-minigame.md
Normal file
677
docs/guide/platform-adapter/wechat-minigame.md
Normal file
@@ -0,0 +1,677 @@
|
||||
# 微信小游戏适配器
|
||||
|
||||
## 概述
|
||||
|
||||
微信小游戏平台适配器专为微信小游戏环境设计,处理微信小游戏的特殊限制和API。
|
||||
|
||||
## 特性支持
|
||||
|
||||
- ✅ **Worker**: 支持(通过 `wx.createWorker` 创建,需要配置 game.json)
|
||||
- ❌ **SharedArrayBuffer**: 不支持
|
||||
- ❌ **Transferable Objects**: 不支持(只支持可序列化对象)
|
||||
- ✅ **高精度时间**: 使用 `Date.now()` 或 `wx.getPerformance()`
|
||||
- ✅ **设备信息**: 完整的微信小游戏设备信息
|
||||
|
||||
## 完整实现
|
||||
|
||||
```typescript
|
||||
import type {
|
||||
IPlatformAdapter,
|
||||
PlatformWorker,
|
||||
WorkerCreationOptions,
|
||||
PlatformConfig,
|
||||
WeChatDeviceInfo
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* 微信小游戏平台适配器
|
||||
* 支持微信小游戏环境
|
||||
*/
|
||||
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.version || 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否支持Worker
|
||||
*/
|
||||
public isWorkerSupported(): boolean {
|
||||
// 微信小游戏支持Worker,通过wx.createWorker创建
|
||||
return typeof wx !== 'undefined' && typeof wx.createWorker === 'function';
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否支持SharedArrayBuffer(不支持)
|
||||
*/
|
||||
public isSharedArrayBufferSupported(): boolean {
|
||||
return false; // 微信小游戏不支持SharedArrayBuffer
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取硬件并发数
|
||||
*/
|
||||
public getHardwareConcurrency(): number {
|
||||
// 微信小游戏官方限制:最多只能创建 1 个 Worker
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建Worker
|
||||
* @param script 脚本内容或文件路径
|
||||
* @param options Worker创建选项
|
||||
*/
|
||||
public createWorker(script: string, options: WorkerCreationOptions = {}): PlatformWorker {
|
||||
if (!this.isWorkerSupported()) {
|
||||
throw new Error('微信小游戏不支持Worker');
|
||||
}
|
||||
|
||||
try {
|
||||
return new WeChatWorker(script, options);
|
||||
} catch (error) {
|
||||
throw new Error(`创建微信Worker失败: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建SharedArrayBuffer(不支持)
|
||||
*/
|
||||
public createSharedArrayBuffer(length: number): SharedArrayBuffer | null {
|
||||
return null; // 微信小游戏不支持SharedArrayBuffer
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取高精度时间戳
|
||||
*/
|
||||
public getHighResTimestamp(): number {
|
||||
// 尝试使用微信的性能API,否则使用Date.now()
|
||||
if (typeof wx !== 'undefined' && wx.getPerformance) {
|
||||
const performance = wx.getPerformance();
|
||||
return performance.now();
|
||||
}
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取平台配置
|
||||
*/
|
||||
public getPlatformConfig(): PlatformConfig {
|
||||
return {
|
||||
maxWorkerCount: 1, // 微信小游戏最多支持 1 个 Worker
|
||||
supportsModuleWorker: false, // 不支持模块Worker
|
||||
supportsTransferableObjects: this.checkTransferableObjectsSupport(),
|
||||
maxSharedArrayBufferSize: 0,
|
||||
workerScriptPrefix: '',
|
||||
limitations: {
|
||||
noEval: true, // 微信小游戏限制eval使用
|
||||
requiresWorkerInit: false,
|
||||
memoryLimit: this.getMemoryLimit(),
|
||||
workerNotSupported: false,
|
||||
workerLimitations: [
|
||||
'最多只能创建 1 个 Worker',
|
||||
'创建新Worker前必须先调用 Worker.terminate()',
|
||||
'Worker脚本必须为项目内相对路径',
|
||||
'需要在 game.json 中配置 workers 路径',
|
||||
'使用 worker.onMessage() 而不是 self.onmessage',
|
||||
'需要基础库 1.9.90 及以上版本'
|
||||
]
|
||||
},
|
||||
extensions: {
|
||||
platform: 'wechat-minigame',
|
||||
systemInfo: this.systemInfo,
|
||||
appId: this.systemInfo.host?.appId || 'unknown'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取微信小游戏设备信息
|
||||
*/
|
||||
public getDeviceInfo(): WeChatDeviceInfo {
|
||||
return {
|
||||
// 设备基础信息
|
||||
brand: this.systemInfo.brand,
|
||||
model: this.systemInfo.model,
|
||||
platform: this.systemInfo.platform,
|
||||
system: this.systemInfo.system,
|
||||
benchmarkLevel: this.systemInfo.benchmarkLevel,
|
||||
cpuType: this.systemInfo.cpuType,
|
||||
memorySize: this.systemInfo.memorySize,
|
||||
deviceAbi: this.systemInfo.deviceAbi,
|
||||
abi: this.systemInfo.abi,
|
||||
|
||||
// 窗口信息
|
||||
screenWidth: this.systemInfo.screenWidth,
|
||||
screenHeight: this.systemInfo.screenHeight,
|
||||
screenTop: this.systemInfo.screenTop,
|
||||
windowWidth: this.systemInfo.windowWidth,
|
||||
windowHeight: this.systemInfo.windowHeight,
|
||||
pixelRatio: this.systemInfo.pixelRatio,
|
||||
statusBarHeight: this.systemInfo.statusBarHeight,
|
||||
safeArea: this.systemInfo.safeArea,
|
||||
|
||||
// 应用信息
|
||||
version: this.systemInfo.version,
|
||||
language: this.systemInfo.language,
|
||||
theme: this.systemInfo.theme,
|
||||
SDKVersion: this.systemInfo.SDKVersion,
|
||||
enableDebug: this.systemInfo.enableDebug,
|
||||
fontSizeSetting: this.systemInfo.fontSizeSetting,
|
||||
host: this.systemInfo.host
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步获取完整的平台配置
|
||||
*/
|
||||
public async getPlatformConfigAsync(): Promise<PlatformConfig> {
|
||||
// 可以在这里添加异步获取设备性能信息的逻辑
|
||||
const baseConfig = this.getPlatformConfig();
|
||||
|
||||
// 尝试获取设备性能信息
|
||||
try {
|
||||
const benchmarkLevel = await this.getBenchmarkLevel();
|
||||
baseConfig.extensions = {
|
||||
...baseConfig.extensions,
|
||||
benchmarkLevel
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn('获取性能基准失败:', error);
|
||||
}
|
||||
|
||||
return baseConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否支持Transferable Objects
|
||||
*/
|
||||
private checkTransferableObjectsSupport(): boolean {
|
||||
// 微信小游戏不支持 Transferable Objects
|
||||
// 基础库 2.20.2 之前只支持可序列化的 key-value 对象
|
||||
// 2.20.2 之后支持任意类型数据,但仍然不支持 Transferable Objects
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统信息
|
||||
*/
|
||||
private getSystemInfo(): any {
|
||||
if (typeof wx !== 'undefined' && wx.getSystemInfoSync) {
|
||||
try {
|
||||
return wx.getSystemInfoSync();
|
||||
} catch (error) {
|
||||
console.warn('获取微信系统信息失败:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取内存限制
|
||||
*/
|
||||
private getMemoryLimit(): number {
|
||||
// 微信小游戏通常有内存限制
|
||||
const memorySize = this.systemInfo.memorySize;
|
||||
if (memorySize) {
|
||||
// 解析内存大小字符串(如 "4GB")
|
||||
const match = memorySize.match(/(\d+)([GM]B)?/i);
|
||||
if (match) {
|
||||
const value = parseInt(match[1], 10);
|
||||
const unit = match[2]?.toUpperCase();
|
||||
|
||||
if (unit === 'GB') {
|
||||
return value * 1024 * 1024 * 1024;
|
||||
} else if (unit === 'MB') {
|
||||
return value * 1024 * 1024;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 默认限制为512MB
|
||||
return 512 * 1024 * 1024;
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步获取设备性能基准
|
||||
*/
|
||||
private async getBenchmarkLevel(): Promise<number> {
|
||||
return new Promise((resolve) => {
|
||||
if (typeof wx !== 'undefined' && wx.getDeviceInfo) {
|
||||
wx.getDeviceInfo({
|
||||
success: (res: any) => {
|
||||
resolve(res.benchmarkLevel || 0);
|
||||
},
|
||||
fail: () => {
|
||||
resolve(0);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resolve(this.systemInfo.benchmarkLevel || 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信Worker封装
|
||||
*/
|
||||
class WeChatWorker implements PlatformWorker {
|
||||
private _state: 'running' | 'terminated' = 'running';
|
||||
private worker: any;
|
||||
private scriptPath: string;
|
||||
private isTemporaryFile: boolean = false;
|
||||
|
||||
constructor(script: string, options: WorkerCreationOptions = {}) {
|
||||
if (typeof wx === 'undefined' || typeof wx.createWorker !== 'function') {
|
||||
throw new Error('微信小游戏不支持Worker');
|
||||
}
|
||||
|
||||
try {
|
||||
// 判断 script 是文件路径还是脚本内容
|
||||
if (this.isFilePath(script)) {
|
||||
// 直接使用文件路径
|
||||
this.scriptPath = script;
|
||||
this.isTemporaryFile = false;
|
||||
this.worker = wx.createWorker(this.scriptPath, {
|
||||
useExperimentalWorker: true // 启用实验性Worker获得更好性能
|
||||
});
|
||||
} else {
|
||||
// 微信小游戏不支持动态脚本内容,只能使用文件路径
|
||||
// 将脚本内容写入文件系统
|
||||
this.scriptPath = this.writeScriptToFile(script, options.name);
|
||||
this.isTemporaryFile = true;
|
||||
this.worker = wx.createWorker(this.scriptPath, {
|
||||
useExperimentalWorker: true
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`创建微信Worker失败: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为文件路径
|
||||
*/
|
||||
private isFilePath(script: string): boolean {
|
||||
// 简单判断:如果包含 .js 后缀且不包含换行符或分号,认为是文件路径
|
||||
return script.endsWith('.js') &&
|
||||
!script.includes('\n') &&
|
||||
!script.includes(';') &&
|
||||
script.length < 200; // 文件路径通常不会太长
|
||||
}
|
||||
|
||||
/**
|
||||
* 将脚本内容写入文件系统
|
||||
* 注意:微信小游戏不支持blob URL,只能使用文件系统
|
||||
*/
|
||||
private writeScriptToFile(script: string, name?: string): string {
|
||||
const fs = wx.getFileSystemManager();
|
||||
const fileName = name ? `worker-${name}.js` : `worker-${Date.now()}.js`;
|
||||
const filePath = `${wx.env.USER_DATA_PATH}/${fileName}`;
|
||||
|
||||
try {
|
||||
fs.writeFileSync(filePath, script, 'utf8');
|
||||
return filePath;
|
||||
} catch (error) {
|
||||
throw new Error(`写入Worker脚本文件失败: ${(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已被终止');
|
||||
}
|
||||
|
||||
try {
|
||||
// 微信小游戏 Worker 只支持可序列化对象,忽略 transfer 参数
|
||||
this.worker.postMessage(message);
|
||||
} catch (error) {
|
||||
throw new Error(`发送消息到微信Worker失败: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public onMessage(handler: (event: { data: any }) => void): void {
|
||||
// 微信小游戏使用 onMessage 方法,不是 onmessage 属性
|
||||
this.worker.onMessage((res: any) => {
|
||||
handler({ data: res });
|
||||
});
|
||||
}
|
||||
|
||||
public onError(handler: (error: ErrorEvent) => void): void {
|
||||
// 注意:微信小游戏 Worker 的错误处理可能与标准不同
|
||||
if (this.worker.onError) {
|
||||
this.worker.onError(handler);
|
||||
}
|
||||
}
|
||||
|
||||
public terminate(): void {
|
||||
if (this._state === 'running') {
|
||||
try {
|
||||
this.worker.terminate();
|
||||
this._state = 'terminated';
|
||||
|
||||
// 清理临时脚本文件
|
||||
this.cleanupScriptFile();
|
||||
} catch (error) {
|
||||
console.error('终止微信Worker失败:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理临时脚本文件
|
||||
*/
|
||||
private cleanupScriptFile(): void {
|
||||
// 只清理临时创建的文件,不清理用户提供的文件路径
|
||||
if (this.scriptPath && this.isTemporaryFile) {
|
||||
try {
|
||||
const fs = wx.getFileSystemManager();
|
||||
fs.unlinkSync(this.scriptPath);
|
||||
} catch (error) {
|
||||
console.warn('清理Worker脚本文件失败:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 复制代码
|
||||
|
||||
将上述代码复制到你的项目中,例如 `src/platform/WeChatMiniGameAdapter.ts`。
|
||||
|
||||
### 2. 注册适配器
|
||||
|
||||
```typescript
|
||||
import { PlatformManager } from '@esengine/ecs-framework';
|
||||
import { WeChatMiniGameAdapter } from './platform/WeChatMiniGameAdapter';
|
||||
|
||||
// 检查是否在微信小游戏环境
|
||||
if (typeof wx !== 'undefined') {
|
||||
const wechatAdapter = new WeChatMiniGameAdapter();
|
||||
PlatformManager.getInstance().registerAdapter(wechatAdapter);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. WorkerEntitySystem 使用方式
|
||||
|
||||
微信小游戏适配器与 WorkerEntitySystem 配合使用,自动处理 Worker 脚本创建:
|
||||
|
||||
#### 基本使用方式(推荐)
|
||||
|
||||
```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, // 微信小游戏限制只能创建1个Worker
|
||||
systemConfig: { gravity: 100, friction: 0.95 }
|
||||
});
|
||||
}
|
||||
|
||||
protected getDefaultEntityDataSize(): number {
|
||||
return 6; // id, x, y, vx, vy, mass
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
// WorkerEntitySystem 会自动将此函数序列化并写入临时文件
|
||||
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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 使用预先创建的 Worker 文件(可选)
|
||||
|
||||
如果你希望使用预先创建的 Worker 文件:
|
||||
|
||||
```typescript
|
||||
// 1. 在 game.json 中配置 Worker 路径
|
||||
/*
|
||||
{
|
||||
"workers": "workers"
|
||||
}
|
||||
*/
|
||||
|
||||
// 2. 创建 workers/physics.js 文件
|
||||
// workers/physics.js 内容:
|
||||
/*
|
||||
// 微信小游戏 Worker 使用标准的 self.onmessage
|
||||
self.onmessage = function(e) {
|
||||
const { type, id, entities, deltaTime, systemConfig } = e.data;
|
||||
|
||||
if (entities) {
|
||||
// 处理物理计算
|
||||
const results = entities.map(entity => {
|
||||
entity.vy += systemConfig.gravity * deltaTime;
|
||||
entity.x += entity.vx * deltaTime;
|
||||
entity.y += entity.vy * deltaTime;
|
||||
return entity;
|
||||
});
|
||||
|
||||
self.postMessage({ id, result: results });
|
||||
}
|
||||
};
|
||||
*/
|
||||
|
||||
// 3. 通过平台适配器直接创建(不推荐,WorkerEntitySystem会自动处理)
|
||||
const adapter = PlatformManager.getInstance().getAdapter();
|
||||
const worker = adapter.createWorker('workers/physics.js');
|
||||
```
|
||||
|
||||
### 4. 获取设备信息
|
||||
|
||||
```typescript
|
||||
const manager = PlatformManager.getInstance();
|
||||
if (manager.hasAdapter()) {
|
||||
const adapter = manager.getAdapter();
|
||||
console.log('微信设备信息:', adapter.getDeviceInfo());
|
||||
}
|
||||
```
|
||||
|
||||
## 官方文档参考
|
||||
|
||||
在使用微信小游戏 Worker 之前,建议先阅读官方文档:
|
||||
|
||||
- [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)
|
||||
- [Worker.terminate API](https://developers.weixin.qq.com/minigame/dev/api/worker/Worker.terminate.html)
|
||||
|
||||
## 重要注意事项
|
||||
|
||||
### Worker 限制和配置
|
||||
|
||||
微信小游戏的 Worker 有以下限制:
|
||||
|
||||
- **数量限制**: 最多只能创建 1 个 Worker
|
||||
- **版本要求**: 需要基础库 1.9.90 及以上版本
|
||||
- **脚本支持**: 不支持 blob URL,只能使用文件路径或写入文件系统
|
||||
- **文件路径**: Worker 脚本路径必须为绝对路径,但不能以 "/" 开头
|
||||
- **生命周期**: 创建新 Worker 前必须先调用 `Worker.terminate()` 终止当前 Worker
|
||||
- **消息处理**: Worker 内使用标准的 `self.onmessage`,主线程使用 `worker.onMessage()`
|
||||
- **实验性功能**: 支持 `useExperimentalWorker` 选项获得更好的 iOS 性能
|
||||
|
||||
#### Worker 配置(可选)
|
||||
|
||||
如果使用预先创建的 Worker 文件,需要在 `game.json` 中添加 workers 配置:
|
||||
|
||||
```json
|
||||
{
|
||||
"deviceOrientation": "portrait",
|
||||
"showStatusBar": false,
|
||||
"workers": "workers",
|
||||
"subpackages": []
|
||||
}
|
||||
```
|
||||
|
||||
**注意**: 使用 WorkerEntitySystem 时无需此配置,框架会自动将脚本写入临时文件。
|
||||
|
||||
### 内存限制
|
||||
|
||||
微信小游戏有严格的内存限制:
|
||||
|
||||
- 通常限制在 256MB - 512MB
|
||||
- 需要及时释放不用的资源
|
||||
- 避免内存泄漏
|
||||
|
||||
### API 限制
|
||||
|
||||
- 不支持 `eval()` 函数
|
||||
- 不支持 `Function` 构造器
|
||||
- DOM API 受限
|
||||
- 文件系统 API 受限
|
||||
|
||||
## 性能优化建议
|
||||
|
||||
### 1. 分帧处理
|
||||
|
||||
```typescript
|
||||
class FramedProcessor {
|
||||
private tasks: (() => void)[] = [];
|
||||
private isProcessing = false;
|
||||
|
||||
public addTask(task: () => void): void {
|
||||
this.tasks.push(task);
|
||||
if (!this.isProcessing) {
|
||||
this.processNextFrame();
|
||||
}
|
||||
}
|
||||
|
||||
private processNextFrame(): void {
|
||||
this.isProcessing = true;
|
||||
const startTime = Date.now();
|
||||
const frameTime = 16; // 16ms per frame
|
||||
|
||||
while (this.tasks.length > 0 && Date.now() - startTime < frameTime) {
|
||||
const task = this.tasks.shift();
|
||||
if (task) task();
|
||||
}
|
||||
|
||||
if (this.tasks.length > 0) {
|
||||
setTimeout(() => this.processNextFrame(), 0);
|
||||
} else {
|
||||
this.isProcessing = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 内存管理
|
||||
|
||||
```typescript
|
||||
class MemoryManager {
|
||||
private static readonly MAX_MEMORY = 256 * 1024 * 1024; // 256MB
|
||||
|
||||
public static checkMemoryUsage(): void {
|
||||
if (typeof wx !== 'undefined' && wx.getPerformance) {
|
||||
const performance = wx.getPerformance();
|
||||
const memoryInfo = performance.getEntries().find(
|
||||
(entry: any) => entry.entryType === 'memory'
|
||||
);
|
||||
|
||||
if (memoryInfo && memoryInfo.usedJSHeapSize > this.MAX_MEMORY * 0.8) {
|
||||
console.warn('内存使用率过高,建议清理资源');
|
||||
// 触发垃圾回收或资源清理
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 调试技巧
|
||||
|
||||
```typescript
|
||||
// 检查微信小游戏环境
|
||||
if (typeof wx !== 'undefined') {
|
||||
const adapter = new WeChatMiniGameAdapter();
|
||||
|
||||
console.log('微信版本:', adapter.version);
|
||||
console.log('设备信息:', adapter.getDeviceInfo());
|
||||
console.log('平台配置:', adapter.getPlatformConfig());
|
||||
|
||||
// 检查功能支持
|
||||
console.log('Worker支持:', adapter.isWorkerSupported());
|
||||
console.log('SharedArrayBuffer支持:', adapter.isSharedArrayBufferSupported());
|
||||
}
|
||||
```
|
||||
|
||||
## 微信小游戏特殊API
|
||||
|
||||
```typescript
|
||||
// 获取设备性能等级
|
||||
if (typeof wx !== 'undefined' && wx.getDeviceInfo) {
|
||||
wx.getDeviceInfo({
|
||||
success: (res) => {
|
||||
console.log('设备性能等级:', res.benchmarkLevel);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 监听内存警告
|
||||
if (typeof wx !== 'undefined' && wx.onMemoryWarning) {
|
||||
wx.onMemoryWarning(() => {
|
||||
console.warn('收到内存警告,开始清理资源');
|
||||
// 清理不必要的资源
|
||||
});
|
||||
}
|
||||
```
|
||||
577
docs/guide/scene.md
Normal file
577
docs/guide/scene.md
Normal file
@@ -0,0 +1,577 @@
|
||||
# 场景管理
|
||||
|
||||
在 ECS 架构中,场景(Scene)是游戏世界的容器,负责管理实体、系统和组件的生命周期。场景提供了完整的 ECS 运行环境。
|
||||
|
||||
## 基本概念
|
||||
|
||||
场景是 ECS 框架的核心容器,提供:
|
||||
- 实体的创建、管理和销毁
|
||||
- 系统的注册和执行调度
|
||||
- 组件的存储和查询
|
||||
- 事件系统支持
|
||||
- 性能监控和调试信息
|
||||
|
||||
## 创建场景
|
||||
|
||||
### 继承 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() 由框架自动调用
|
||||
```
|
||||
|
||||
## 实体管理
|
||||
|
||||
### 创建实体
|
||||
|
||||
```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()
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 场景统计和调试
|
||||
|
||||
### 获取场景统计
|
||||
|
||||
```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} 个实体`);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 场景集成到框架
|
||||
|
||||
ECS Framework 提供了灵活的场景管理架构,适用于不同规模的应用:
|
||||
|
||||
### 1. 使用 SceneManager(推荐大多数应用)
|
||||
|
||||
适用于 95% 的游戏应用(单人游戏、简单多人游戏、移动游戏等):
|
||||
|
||||
```typescript
|
||||
import { Core, SceneManager } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始化Core(全局服务)
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 创建场景管理器
|
||||
const sceneManager = new SceneManager();
|
||||
|
||||
// 创建游戏场景
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "GameScene";
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
}
|
||||
}
|
||||
|
||||
// 设置场景
|
||||
const gameScene = new GameScene();
|
||||
sceneManager.setScene(gameScene);
|
||||
|
||||
// 游戏循环
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // 更新全局服务
|
||||
sceneManager.update(); // 更新当前场景
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 场景切换
|
||||
|
||||
SceneManager 支持流畅的场景切换:
|
||||
|
||||
```typescript
|
||||
// 立即切换场景
|
||||
const menuScene = new MenuScene();
|
||||
sceneManager.setScene(menuScene);
|
||||
|
||||
// 延迟切换场景(在下一帧切换)
|
||||
const gameScene = new GameScene();
|
||||
sceneManager.startSceneTransition(gameScene, false);
|
||||
|
||||
// 访问当前场景
|
||||
const currentScene = sceneManager.currentScene;
|
||||
|
||||
// 访问 ECS API
|
||||
const ecsAPI = sceneManager.ecsAPI;
|
||||
const entity = ecsAPI?.createEntity('player');
|
||||
```
|
||||
|
||||
### 3. 使用 WorldManager(高级用例)
|
||||
|
||||
适用于需要完全隔离的多世界应用(MMO服务器、游戏房间系统等):
|
||||
|
||||
```typescript
|
||||
import { Core, WorldManager } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始化Core(全局服务)
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 创建世界管理器
|
||||
const worldManager = new WorldManager();
|
||||
|
||||
// 创建多个独立的游戏世界
|
||||
const gameWorld = worldManager.createWorld('game', {
|
||||
name: 'MainGame',
|
||||
maxScenes: 5
|
||||
});
|
||||
|
||||
// 在World中创建场景
|
||||
const menuScene = gameWorld.createScene('menu', new MenuScene());
|
||||
const gameScene = gameWorld.createScene('game', new GameScene());
|
||||
|
||||
// 激活场景
|
||||
gameWorld.setSceneActive('menu', true);
|
||||
|
||||
// 游戏循环
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // 更新全局服务
|
||||
worldManager.updateAll(); // 更新所有世界
|
||||
}
|
||||
```
|
||||
|
||||
## 多场景管理
|
||||
|
||||
在World中可以管理多个场景,通过激活/停用来切换:
|
||||
|
||||
```typescript
|
||||
class GameWorld extends World {
|
||||
private menuScene: Scene;
|
||||
private gameScene: Scene;
|
||||
private gameOverScene: Scene;
|
||||
|
||||
public initialize(): void {
|
||||
// 创建多个场景
|
||||
this.menuScene = this.createScene('menu', new MenuScene());
|
||||
this.gameScene = this.createScene('game', new GameScene());
|
||||
this.gameOverScene = this.createScene('gameover', new GameOverScene());
|
||||
|
||||
// 设置初始场景
|
||||
this.showMenu();
|
||||
}
|
||||
|
||||
public showMenu(): void {
|
||||
this.deactivateAllScenes();
|
||||
this.setSceneActive('menu', true);
|
||||
}
|
||||
|
||||
public startGame(): void {
|
||||
this.deactivateAllScenes();
|
||||
this.setSceneActive('game', true);
|
||||
}
|
||||
|
||||
public showGameOver(): void {
|
||||
this.deactivateAllScenes();
|
||||
this.setSceneActive('gameover', true);
|
||||
}
|
||||
|
||||
private deactivateAllScenes(): void {
|
||||
this.setSceneActive('menu', false);
|
||||
this.setSceneActive('game', false);
|
||||
this.setSceneActive('gameover', false);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 架构层次
|
||||
|
||||
ECS Framework 的架构层次清晰,职责分明:
|
||||
|
||||
```typescript
|
||||
// 架构层次:
|
||||
// Core (全局服务) → SceneManager (场景管理) → Scene → EntitySystem → Entity → Component
|
||||
// 或
|
||||
// Core (全局服务) → WorldManager (世界管理) → World → Scene → EntitySystem → Entity → Component
|
||||
|
||||
// 1. 推荐:使用 SceneManager 管理单场景/场景切换
|
||||
import { Core, SceneManager } from '@esengine/ecs-framework';
|
||||
|
||||
Core.create({ debug: true });
|
||||
const sceneManager = new SceneManager();
|
||||
sceneManager.setScene(new GameScene());
|
||||
|
||||
// 游戏循环
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // 全局服务
|
||||
sceneManager.update(); // 场景更新
|
||||
}
|
||||
|
||||
// 2. 高级:使用 WorldManager 管理多世界
|
||||
import { Core, WorldManager } from '@esengine/ecs-framework';
|
||||
|
||||
Core.create({ debug: true });
|
||||
const worldManager = new WorldManager();
|
||||
const world = worldManager.createWorld('gameWorld');
|
||||
const scene = world.createScene('mainScene', new GameScene());
|
||||
world.setSceneActive('mainScene', true);
|
||||
|
||||
// 游戏循环
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // 全局服务
|
||||
worldManager.updateAll(); // 所有世界更新
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 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 {
|
||||
// 加载场景所需资源
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// 清理资源
|
||||
this.textures.clear();
|
||||
this.sounds.clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 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 {
|
||||
// 处理玩家输入
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
场景是 ECS 框架的核心容器,正确使用场景管理能让你的游戏架构更加清晰、模块化和易于维护。
|
||||
810
docs/guide/serialization.md
Normal file
810
docs/guide/serialization.md
Normal file
@@ -0,0 +1,810 @@
|
||||
# 序列化系统
|
||||
|
||||
序列化系统提供了完整的场景、实体和组件数据持久化方案,支持全量序列化和增量序列化两种模式,适用于游戏存档、网络同步、场景编辑器、时间回溯等场景。
|
||||
|
||||
## 基本概念
|
||||
|
||||
序列化系统分为两个层次:
|
||||
|
||||
- **全量序列化**:序列化完整的场景状态,包括所有实体、组件和场景数据
|
||||
- **增量序列化**:只序列化相对于基础快照的变更部分,大幅减少数据量
|
||||
|
||||
### 支持的数据格式
|
||||
|
||||
- **JSON格式**:人类可读,便于调试和编辑
|
||||
- **Binary格式**:使用MessagePack,体积更小,性能更高
|
||||
|
||||
## 全量序列化
|
||||
|
||||
### 基础用法
|
||||
|
||||
#### 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环境)
|
||||
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
|
||||
// 设置场景数据
|
||||
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);
|
||||
|
||||
// 从二进制Buffer应用
|
||||
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 {
|
||||
// 直接应用二进制数据
|
||||
const buffer = Buffer.from(data);
|
||||
this.scene.applyIncremental(buffer);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 撤销/重做系统
|
||||
|
||||
```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字符串或二进制Buffer)
|
||||
- [`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()` - 获取迁移路径
|
||||
|
||||
序列化系统是构建完整游戏的重要基础设施,合理使用可以实现强大的功能,如存档系统、网络同步、关卡编辑器等。
|
||||
604
docs/guide/system.md
Normal file
604
docs/guide/system.md
Normal file
@@ -0,0 +1,604 @@
|
||||
# 系统架构
|
||||
|
||||
在 ECS 架构中,系统(System)是处理业务逻辑的地方。系统负责对拥有特定组件组合的实体执行操作,是 ECS 架构的逻辑处理单元。
|
||||
|
||||
## 基本概念
|
||||
|
||||
系统是继承自 `EntitySystem` 抽象基类的具体类,用于:
|
||||
- 定义实体的处理逻辑(如移动、碰撞检测、渲染等)
|
||||
- 根据组件组合筛选需要处理的实体
|
||||
- 提供生命周期管理和性能监控
|
||||
- 管理实体的添加、移除事件
|
||||
|
||||
## 系统类型
|
||||
|
||||
框架提供了几种不同类型的系统基类:
|
||||
|
||||
### EntitySystem - 基础系统
|
||||
|
||||
最基础的系统类,所有其他系统都继承自它:
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, ECSSystem, Matcher } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSSystem('Movement')
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// 使用 Matcher 定义需要处理的实体条件
|
||||
super(Matcher.all(Position, Velocity));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const position = entity.getComponent(Position);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
if (position && velocity) {
|
||||
position.x += velocity.dx * Time.deltaTime;
|
||||
position.y += velocity.dy * Time.deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ProcessingSystem - 处理系统
|
||||
|
||||
适用于不需要逐个处理实体的系统:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Physics')
|
||||
class PhysicsSystem extends ProcessingSystem {
|
||||
constructor() {
|
||||
super(); // 不需要指定 Matcher
|
||||
}
|
||||
|
||||
public processSystem(): void {
|
||||
// 执行物理世界步进
|
||||
this.physicsWorld.step(Time.deltaTime);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### PassiveSystem - 被动系统
|
||||
|
||||
被动系统不进行主动处理,主要用于监听实体的添加和移除事件:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('EntityTracker')
|
||||
class EntityTrackerSystem extends PassiveSystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Health));
|
||||
}
|
||||
|
||||
protected onAdded(entity: Entity): void {
|
||||
console.log(`生命值实体被添加: ${entity.name}`);
|
||||
}
|
||||
|
||||
protected onRemoved(entity: Entity): void {
|
||||
console.log(`生命值实体被移除: ${entity.name}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### IntervalSystem - 间隔系统
|
||||
|
||||
按固定时间间隔执行的系统:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('AutoSave')
|
||||
class AutoSaveSystem extends IntervalSystem {
|
||||
constructor() {
|
||||
// 每 5 秒执行一次
|
||||
super(5.0, Matcher.all(SaveData));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
console.log('执行自动保存...');
|
||||
// 保存游戏数据
|
||||
this.saveGameData(entities);
|
||||
}
|
||||
|
||||
private saveGameData(entities: readonly Entity[]): void {
|
||||
// 保存逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### WorkerEntitySystem - 多线程系统
|
||||
|
||||
基于Web Worker的多线程处理系统,适用于计算密集型任务,能够充分利用多核CPU性能。
|
||||
|
||||
Worker系统提供了真正的并行计算能力,支持SharedArrayBuffer优化,并具有自动降级支持。特别适合物理模拟、粒子系统、AI计算等场景。
|
||||
|
||||
**详细内容请参考:[Worker系统](/guide/worker-system)**
|
||||
|
||||
## 实体匹配器 (Matcher)
|
||||
|
||||
Matcher 用于定义系统需要处理哪些实体。它提供了灵活的条件组合:
|
||||
|
||||
### 基本匹配条件
|
||||
|
||||
```typescript
|
||||
// 必须同时拥有 Position 和 Velocity 组件
|
||||
const matcher1 = Matcher.all(Position, Velocity);
|
||||
|
||||
// 至少拥有 Health 或 Shield 组件之一
|
||||
const matcher2 = Matcher.any(Health, Shield);
|
||||
|
||||
// 不能拥有 Dead 组件
|
||||
const matcher3 = Matcher.none(Dead);
|
||||
```
|
||||
|
||||
### 复合匹配条件
|
||||
|
||||
```typescript
|
||||
// 复杂的组合条件
|
||||
const complexMatcher = Matcher.all(Position, Velocity)
|
||||
.any(Player, Enemy)
|
||||
.none(Dead, Disabled);
|
||||
|
||||
@ECSSystem('Combat')
|
||||
class CombatSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(complexMatcher);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 特殊匹配条件
|
||||
|
||||
```typescript
|
||||
// 按标签匹配
|
||||
const tagMatcher = Matcher.byTag(1); // 匹配标签为 1 的实体
|
||||
|
||||
// 按名称匹配
|
||||
const nameMatcher = Matcher.byName("Player"); // 匹配名称为 "Player" 的实体
|
||||
|
||||
// 单组件匹配
|
||||
const componentMatcher = Matcher.byComponent(Health); // 匹配拥有 Health 组件的实体
|
||||
```
|
||||
|
||||
## 系统生命周期
|
||||
|
||||
系统提供了完整的生命周期回调:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Example')
|
||||
class ExampleSystem extends EntitySystem {
|
||||
protected onInitialize(): void {
|
||||
console.log('系统初始化');
|
||||
// 系统被添加到场景时调用,用于初始化资源
|
||||
}
|
||||
|
||||
protected onBegin(): void {
|
||||
// 每帧处理开始前调用
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 主要的处理逻辑
|
||||
for (const entity of entities) {
|
||||
// 处理每个实体
|
||||
}
|
||||
}
|
||||
|
||||
protected lateProcess(entities: readonly Entity[]): void {
|
||||
// 主处理之后的后期处理
|
||||
}
|
||||
|
||||
protected onEnd(): void {
|
||||
// 每帧处理结束后调用
|
||||
}
|
||||
|
||||
protected onDestroy(): void {
|
||||
console.log('系统销毁');
|
||||
// 系统从场景移除时调用,用于清理资源
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 实体事件监听
|
||||
|
||||
系统可以监听实体的添加和移除事件:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('EnemyManager')
|
||||
class EnemyManagerSystem extends EntitySystem {
|
||||
private enemyCount = 0;
|
||||
|
||||
constructor() {
|
||||
super(Matcher.all(Enemy, Health));
|
||||
}
|
||||
|
||||
protected onAdded(entity: Entity): void {
|
||||
this.enemyCount++;
|
||||
console.log(`敌人加入战斗,当前敌人数量: ${this.enemyCount}`);
|
||||
|
||||
// 可以在这里为新敌人设置初始状态
|
||||
const health = entity.getComponent(Health);
|
||||
if (health) {
|
||||
health.current = health.max;
|
||||
}
|
||||
}
|
||||
|
||||
protected onRemoved(entity: Entity): void {
|
||||
this.enemyCount--;
|
||||
console.log(`敌人被移除,剩余敌人数量: ${this.enemyCount}`);
|
||||
|
||||
// 检查是否所有敌人都被消灭
|
||||
if (this.enemyCount === 0) {
|
||||
this.scene?.eventSystem.emitSync('all_enemies_defeated');
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 系统属性和方法
|
||||
|
||||
### 重要属性
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Example')
|
||||
class ExampleSystem extends EntitySystem {
|
||||
showSystemInfo(): void {
|
||||
console.log(`系统名称: ${this.systemName}`); // 系统名称
|
||||
console.log(`更新顺序: ${this.updateOrder}`); // 更新时序
|
||||
console.log(`是否启用: ${this.enabled}`); // 启用状态
|
||||
console.log(`实体数量: ${this.entities.length}`); // 匹配的实体数量
|
||||
console.log(`所属场景: ${this.scene?.name}`); // 所属场景
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 实体访问
|
||||
|
||||
```typescript
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 方式1:使用参数中的实体列表
|
||||
for (const entity of entities) {
|
||||
// 处理实体
|
||||
}
|
||||
|
||||
// 方式2:使用 this.entities 属性(与参数相同)
|
||||
for (const entity of this.entities) {
|
||||
// 处理实体
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 控制系统执行
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Conditional')
|
||||
class ConditionalSystem extends EntitySystem {
|
||||
private shouldProcess = true;
|
||||
|
||||
protected onCheckProcessing(): boolean {
|
||||
// 返回 false 时跳过本次处理
|
||||
return this.shouldProcess && this.entities.length > 0;
|
||||
}
|
||||
|
||||
public pause(): void {
|
||||
this.shouldProcess = false;
|
||||
}
|
||||
|
||||
public resume(): void {
|
||||
this.shouldProcess = true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 事件系统集成
|
||||
|
||||
系统可以方便地监听和发送事件:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('GameLogic')
|
||||
class GameLogicSystem extends EntitySystem {
|
||||
protected onInitialize(): void {
|
||||
// 添加事件监听器(系统销毁时自动清理)
|
||||
this.addEventListener('player_died', this.onPlayerDied.bind(this));
|
||||
this.addEventListener('level_complete', this.onLevelComplete.bind(this));
|
||||
}
|
||||
|
||||
private onPlayerDied(data: any): void {
|
||||
console.log('玩家死亡,重新开始游戏');
|
||||
// 处理玩家死亡逻辑
|
||||
}
|
||||
|
||||
private onLevelComplete(data: any): void {
|
||||
console.log('关卡完成,加载下一关');
|
||||
// 处理关卡完成逻辑
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 在处理过程中发送事件
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health);
|
||||
if (health && health.current <= 0) {
|
||||
this.scene?.eventSystem.emitSync('entity_died', { entity });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 性能监控
|
||||
|
||||
系统内置了性能监控功能:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Performance')
|
||||
class PerformanceSystem extends EntitySystem {
|
||||
protected onEnd(): void {
|
||||
// 获取性能数据
|
||||
const perfData = this.getPerformanceData();
|
||||
if (perfData) {
|
||||
console.log(`执行时间: ${perfData.executionTime.toFixed(2)}ms`);
|
||||
}
|
||||
|
||||
// 获取性能统计
|
||||
const stats = this.getPerformanceStats();
|
||||
if (stats) {
|
||||
console.log(`平均执行时间: ${stats.averageTime.toFixed(2)}ms`);
|
||||
}
|
||||
}
|
||||
|
||||
public resetPerformance(): void {
|
||||
this.resetPerformanceData();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 系统管理
|
||||
|
||||
### 添加系统到场景
|
||||
|
||||
```typescript
|
||||
// 在场景子类中添加系统
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 添加系统
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
this.addSystem(new PhysicsSystem());
|
||||
|
||||
// 设置系统更新顺序
|
||||
const movementSystem = this.getSystem(MovementSystem);
|
||||
if (movementSystem) {
|
||||
movementSystem.updateOrder = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 系统更新顺序
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Input')
|
||||
class InputSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(InputComponent));
|
||||
this.updateOrder = -100; // 输入系统优先执行
|
||||
}
|
||||
}
|
||||
|
||||
@ECSSystem('Physics')
|
||||
class PhysicsSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(RigidBody));
|
||||
this.updateOrder = 0; // 默认顺序
|
||||
}
|
||||
}
|
||||
|
||||
@ECSSystem('Render')
|
||||
class RenderSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Sprite, Transform));
|
||||
this.updateOrder = 100; // 渲染系统最后执行
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 复杂系统示例
|
||||
|
||||
### 碰撞检测系统
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Collision')
|
||||
class CollisionSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Transform, Collider));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 简单的 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(entityA: Entity, entityB: Entity): void {
|
||||
const transformA = entityA.getComponent(Transform);
|
||||
const transformB = entityB.getComponent(Transform);
|
||||
const colliderA = entityA.getComponent(Collider);
|
||||
const colliderB = entityB.getComponent(Collider);
|
||||
|
||||
if (this.isColliding(transformA, colliderA, transformB, colliderB)) {
|
||||
// 发送碰撞事件
|
||||
this.scene?.eventSystem.emitSync('collision', {
|
||||
entityA,
|
||||
entityB
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private isColliding(transformA: Transform, colliderA: Collider,
|
||||
transformB: Transform, colliderB: Collider): boolean {
|
||||
// 碰撞检测逻辑
|
||||
return false; // 简化示例
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 状态机系统
|
||||
|
||||
```typescript
|
||||
@ECSSystem('StateMachine')
|
||||
class StateMachineSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(StateMachine));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const stateMachine = entity.getComponent(StateMachine);
|
||||
if (stateMachine) {
|
||||
stateMachine.updateTimer(Time.deltaTime);
|
||||
this.updateState(entity, stateMachine);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateState(entity: Entity, stateMachine: StateMachine): void {
|
||||
switch (stateMachine.currentState) {
|
||||
case EntityState.Idle:
|
||||
this.handleIdleState(entity, stateMachine);
|
||||
break;
|
||||
case EntityState.Moving:
|
||||
this.handleMovingState(entity, stateMachine);
|
||||
break;
|
||||
case EntityState.Attacking:
|
||||
this.handleAttackingState(entity, stateMachine);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private handleIdleState(entity: Entity, stateMachine: StateMachine): void {
|
||||
// 空闲状态逻辑
|
||||
}
|
||||
|
||||
private handleMovingState(entity: Entity, stateMachine: StateMachine): void {
|
||||
// 移动状态逻辑
|
||||
}
|
||||
|
||||
private handleAttackingState(entity: Entity, stateMachine: StateMachine): void {
|
||||
// 攻击状态逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 系统单一职责
|
||||
|
||||
```typescript
|
||||
// ✅ 好的系统设计 - 职责单一
|
||||
@ECSSystem('Movement')
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Position, Velocity));
|
||||
}
|
||||
}
|
||||
|
||||
@ECSSystem('Rendering')
|
||||
class RenderingSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Sprite, Transform));
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ 避免的系统设计 - 职责过多
|
||||
@ECSSystem('GameSystem')
|
||||
class GameSystem extends EntitySystem {
|
||||
// 一个系统处理移动、渲染、音效等多种逻辑
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 使用装饰器
|
||||
|
||||
**必须使用 `@ECSSystem` 装饰器**:
|
||||
|
||||
```typescript
|
||||
// ✅ 正确的用法
|
||||
@ECSSystem('Physics')
|
||||
class PhysicsSystem extends EntitySystem {
|
||||
// 系统实现
|
||||
}
|
||||
|
||||
// ❌ 错误的用法 - 没有装饰器
|
||||
class BadSystem extends EntitySystem {
|
||||
// 这样定义的系统可能在生产环境出现问题
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 合理的更新顺序
|
||||
|
||||
```typescript
|
||||
// 按逻辑顺序设置系统的更新时序
|
||||
@ECSSystem('Input')
|
||||
class InputSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super();
|
||||
this.updateOrder = -100; // 最先处理输入
|
||||
}
|
||||
}
|
||||
|
||||
@ECSSystem('Logic')
|
||||
class GameLogicSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super();
|
||||
this.updateOrder = 0; // 处理游戏逻辑
|
||||
}
|
||||
}
|
||||
|
||||
@ECSSystem('Render')
|
||||
class RenderSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super();
|
||||
this.updateOrder = 100; // 最后进行渲染
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 避免在系统间直接引用
|
||||
|
||||
```typescript
|
||||
// ❌ 避免:系统间直接引用
|
||||
@ECSSystem('Bad')
|
||||
class BadSystem extends EntitySystem {
|
||||
private otherSystem: SomeOtherSystem; // 避免直接引用其他系统
|
||||
}
|
||||
|
||||
// ✅ 推荐:通过事件系统通信
|
||||
@ECSSystem('Good')
|
||||
class GoodSystem extends EntitySystem {
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 通过事件系统与其他系统通信
|
||||
this.scene?.eventSystem.emitSync('data_updated', { entities });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 及时清理资源
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Resource')
|
||||
class ResourceSystem extends EntitySystem {
|
||||
private resources: Map<string, any> = new Map();
|
||||
|
||||
protected onDestroy(): void {
|
||||
// 清理资源
|
||||
for (const [key, resource] of this.resources) {
|
||||
if (resource.dispose) {
|
||||
resource.dispose();
|
||||
}
|
||||
}
|
||||
this.resources.clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
系统是 ECS 架构的逻辑处理核心,正确设计和使用系统能让你的游戏代码更加模块化、高效和易于维护。
|
||||
553
docs/guide/time-and-timers.md
Normal file
553
docs/guide/time-and-timers.md
Normal file
@@ -0,0 +1,553 @@
|
||||
# 时间和定时器系统
|
||||
|
||||
ECS 框架提供了完整的时间管理和定时器系统,包括时间缩放、帧时间计算和灵活的定时器调度功能。
|
||||
|
||||
## Time 类
|
||||
|
||||
Time 类是框架的时间管理核心,提供了游戏时间相关的所有功能。
|
||||
|
||||
### 基本时间属性
|
||||
|
||||
```typescript
|
||||
import { Time } from '@esengine/ecs-framework';
|
||||
|
||||
class GameSystem extends EntitySystem {
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 获取帧时间(秒)
|
||||
const deltaTime = Time.deltaTime;
|
||||
|
||||
// 获取未缩放的帧时间
|
||||
const unscaledDelta = Time.unscaledDeltaTime;
|
||||
|
||||
// 获取游戏总时间
|
||||
const totalTime = Time.totalTime;
|
||||
|
||||
// 获取当前帧数
|
||||
const frameCount = Time.frameCount;
|
||||
|
||||
console.log(`第 ${frameCount} 帧,帧时间: ${deltaTime}s,总时间: ${totalTime}s`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 时间缩放
|
||||
|
||||
Time 类支持时间缩放功能,可以实现慢动作、快进等效果:
|
||||
|
||||
```typescript
|
||||
class TimeControlSystem extends EntitySystem {
|
||||
public enableSlowMotion(): void {
|
||||
// 设置为慢动作(50%速度)
|
||||
Time.timeScale = 0.5;
|
||||
console.log('慢动作模式启用');
|
||||
}
|
||||
|
||||
public enableFastForward(): void {
|
||||
// 设置为快进(200%速度)
|
||||
Time.timeScale = 2.0;
|
||||
console.log('快进模式启用');
|
||||
}
|
||||
|
||||
public pauseGame(): void {
|
||||
// 暂停游戏(时间静止)
|
||||
Time.timeScale = 0;
|
||||
console.log('游戏暂停');
|
||||
}
|
||||
|
||||
public resumeNormalSpeed(): void {
|
||||
// 恢复正常速度
|
||||
Time.timeScale = 1.0;
|
||||
console.log('恢复正常速度');
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// deltaTime 会受到 timeScale 影响
|
||||
const scaledDelta = Time.deltaTime; // 受时间缩放影响
|
||||
const realDelta = Time.unscaledDeltaTime; // 不受时间缩放影响
|
||||
|
||||
for (const entity of entities) {
|
||||
const movement = entity.getComponent(Movement);
|
||||
if (movement) {
|
||||
// 使用缩放时间进行游戏逻辑更新
|
||||
movement.update(scaledDelta);
|
||||
}
|
||||
|
||||
const ui = entity.getComponent(UIComponent);
|
||||
if (ui) {
|
||||
// UI 动画使用真实时间,不受游戏时间缩放影响
|
||||
ui.update(realDelta);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 时间检查工具
|
||||
|
||||
```typescript
|
||||
class CooldownSystem extends EntitySystem {
|
||||
private lastAttackTime = 0;
|
||||
private lastSpawnTime = 0;
|
||||
|
||||
constructor() {
|
||||
super(Matcher.all(Weapon));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 检查攻击冷却
|
||||
if (Time.checkEvery(1.5, this.lastAttackTime)) {
|
||||
this.performAttack();
|
||||
this.lastAttackTime = Time.totalTime;
|
||||
}
|
||||
|
||||
// 检查生成间隔
|
||||
if (Time.checkEvery(3.0, this.lastSpawnTime)) {
|
||||
this.spawnEnemy();
|
||||
this.lastSpawnTime = Time.totalTime;
|
||||
}
|
||||
}
|
||||
|
||||
private performAttack(): void {
|
||||
console.log('执行攻击!');
|
||||
}
|
||||
|
||||
private spawnEnemy(): void {
|
||||
console.log('生成敌人!');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Core.schedule 定时器系统
|
||||
|
||||
Core 提供了强大的定时器调度功能,可以创建一次性或重复执行的定时器。
|
||||
|
||||
### 基本定时器使用
|
||||
|
||||
```typescript
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 创建一次性定时器
|
||||
this.createOneTimeTimers();
|
||||
|
||||
// 创建重复定时器
|
||||
this.createRepeatingTimers();
|
||||
|
||||
// 创建带上下文的定时器
|
||||
this.createContextTimers();
|
||||
}
|
||||
|
||||
private createOneTimeTimers(): void {
|
||||
// 2秒后执行一次
|
||||
Core.schedule(2.0, false, null, (timer) => {
|
||||
console.log('2秒延迟执行');
|
||||
});
|
||||
|
||||
// 5秒后显示提示
|
||||
Core.schedule(5.0, false, this, (timer) => {
|
||||
const scene = timer.getContext<GameScene>();
|
||||
scene.showTip('游戏提示:5秒已过!');
|
||||
});
|
||||
}
|
||||
|
||||
private createRepeatingTimers(): void {
|
||||
// 每秒重复执行
|
||||
const heartbeatTimer = Core.schedule(1.0, true, null, (timer) => {
|
||||
console.log(`游戏心跳 - 总时间: ${Time.totalTime.toFixed(1)}s`);
|
||||
});
|
||||
|
||||
// 可以保存定时器引用用于后续控制
|
||||
this.saveTimerReference(heartbeatTimer);
|
||||
}
|
||||
|
||||
private createContextTimers(): void {
|
||||
const gameData = { score: 0, level: 1 };
|
||||
|
||||
// 每2秒增加分数
|
||||
Core.schedule(2.0, true, gameData, (timer) => {
|
||||
const data = timer.getContext<typeof gameData>();
|
||||
data.score += 10;
|
||||
console.log(`分数增加!当前分数: ${data.score}`);
|
||||
});
|
||||
}
|
||||
|
||||
private saveTimerReference(timer: any): void {
|
||||
// 可以稍后停止定时器
|
||||
setTimeout(() => {
|
||||
timer.stop();
|
||||
console.log('定时器已停止');
|
||||
}, 10000); // 10秒后停止
|
||||
}
|
||||
|
||||
private showTip(message: string): void {
|
||||
console.log('提示:', message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 定时器控制
|
||||
|
||||
```typescript
|
||||
class TimerControlExample {
|
||||
private attackTimer: any;
|
||||
private spawnerTimer: any;
|
||||
|
||||
public startCombat(): void {
|
||||
// 启动攻击定时器
|
||||
this.attackTimer = Core.schedule(0.5, true, this, (timer) => {
|
||||
const self = timer.getContext<TimerControlExample>();
|
||||
self.performAttack();
|
||||
});
|
||||
|
||||
// 启动敌人生成定时器
|
||||
this.spawnerTimer = Core.schedule(3.0, true, null, (timer) => {
|
||||
this.spawnEnemy();
|
||||
});
|
||||
}
|
||||
|
||||
public stopCombat(): void {
|
||||
// 停止所有战斗相关定时器
|
||||
if (this.attackTimer) {
|
||||
this.attackTimer.stop();
|
||||
console.log('攻击定时器已停止');
|
||||
}
|
||||
|
||||
if (this.spawnerTimer) {
|
||||
this.spawnerTimer.stop();
|
||||
console.log('生成定时器已停止');
|
||||
}
|
||||
}
|
||||
|
||||
public resetAttackTimer(): void {
|
||||
// 重置攻击定时器
|
||||
if (this.attackTimer) {
|
||||
this.attackTimer.reset();
|
||||
console.log('攻击定时器已重置');
|
||||
}
|
||||
}
|
||||
|
||||
private performAttack(): void {
|
||||
console.log('执行攻击');
|
||||
}
|
||||
|
||||
private spawnEnemy(): void {
|
||||
console.log('生成敌人');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 复杂定时器场景
|
||||
|
||||
```typescript
|
||||
class AdvancedTimerUsage {
|
||||
private powerUpDuration = 0;
|
||||
private powerUpActive = false;
|
||||
|
||||
public activatePowerUp(): void {
|
||||
if (this.powerUpActive) {
|
||||
console.log('能力提升已激活');
|
||||
return;
|
||||
}
|
||||
|
||||
this.powerUpActive = true;
|
||||
this.powerUpDuration = 10; // 10秒持续时间
|
||||
|
||||
console.log('能力提升激活!');
|
||||
|
||||
// 每秒更新剩余时间
|
||||
const countdownTimer = Core.schedule(1.0, true, this, (timer) => {
|
||||
const self = timer.getContext<AdvancedTimerUsage>();
|
||||
self.powerUpDuration--;
|
||||
|
||||
console.log(`能力提升剩余时间: ${self.powerUpDuration}秒`);
|
||||
|
||||
if (self.powerUpDuration <= 0) {
|
||||
self.deactivatePowerUp();
|
||||
timer.stop(); // 停止倒计时
|
||||
}
|
||||
});
|
||||
|
||||
// 能力提升结束定时器(备用)
|
||||
Core.schedule(10.0, false, this, (timer) => {
|
||||
const self = timer.getContext<AdvancedTimerUsage>();
|
||||
if (self.powerUpActive) {
|
||||
self.deactivatePowerUp();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private deactivatePowerUp(): void {
|
||||
this.powerUpActive = false;
|
||||
this.powerUpDuration = 0;
|
||||
console.log('能力提升结束');
|
||||
}
|
||||
|
||||
// 创建波次攻击定时器
|
||||
public startWaveAttack(): void {
|
||||
let waveCount = 0;
|
||||
const maxWaves = 5;
|
||||
|
||||
const waveTimer = Core.schedule(2.0, true, { waveCount, maxWaves }, (timer) => {
|
||||
const context = timer.getContext<{ waveCount: number, maxWaves: number }>();
|
||||
context.waveCount++;
|
||||
|
||||
console.log(`第 ${context.waveCount} 波攻击!`);
|
||||
|
||||
if (context.waveCount >= context.maxWaves) {
|
||||
console.log('所有波次攻击完成');
|
||||
timer.stop();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 创建条件定时器
|
||||
public startConditionalTimer(): void {
|
||||
Core.schedule(0.1, true, this, (timer) => {
|
||||
const self = timer.getContext<AdvancedTimerUsage>();
|
||||
|
||||
// 检查某个条件
|
||||
if (self.shouldStopTimer()) {
|
||||
console.log('条件满足,停止定时器');
|
||||
timer.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
// 继续执行定时器逻辑
|
||||
self.performTimerAction();
|
||||
});
|
||||
}
|
||||
|
||||
private shouldStopTimer(): boolean {
|
||||
// 检查停止条件
|
||||
return Time.totalTime > 30; // 30秒后停止
|
||||
}
|
||||
|
||||
private performTimerAction(): void {
|
||||
console.log('执行定时器动作');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 实际应用示例
|
||||
|
||||
### 技能冷却系统
|
||||
|
||||
```typescript
|
||||
class SkillCooldownSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(SkillComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const skill = entity.getComponent(SkillComponent);
|
||||
|
||||
// 更新技能冷却
|
||||
if (skill.isOnCooldown) {
|
||||
skill.cooldownRemaining -= Time.deltaTime;
|
||||
|
||||
if (skill.cooldownRemaining <= 0) {
|
||||
skill.cooldownRemaining = 0;
|
||||
skill.isOnCooldown = false;
|
||||
console.log(`技能 ${skill.name} 冷却完成`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public useSkill(entity: Entity, skillName: string): boolean {
|
||||
const skill = entity.getComponent(SkillComponent);
|
||||
|
||||
if (skill.isOnCooldown) {
|
||||
console.log(`技能 ${skillName} 还在冷却中,剩余 ${skill.cooldownRemaining.toFixed(1)}秒`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 执行技能
|
||||
this.executeSkill(entity, skill);
|
||||
|
||||
// 开始冷却
|
||||
skill.isOnCooldown = true;
|
||||
skill.cooldownRemaining = skill.cooldownDuration;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private executeSkill(entity: Entity, skill: SkillComponent): void {
|
||||
console.log(`执行技能: ${skill.name}`);
|
||||
// 技能效果逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 游戏状态定时器
|
||||
|
||||
```typescript
|
||||
class GameStateManager {
|
||||
private gamePhase = 'preparation';
|
||||
private phaseTimer: any;
|
||||
|
||||
public startGame(): void {
|
||||
this.startPreparationPhase();
|
||||
}
|
||||
|
||||
private startPreparationPhase(): void {
|
||||
this.gamePhase = 'preparation';
|
||||
console.log('准备阶段开始 - 10秒准备时间');
|
||||
|
||||
this.phaseTimer = Core.schedule(10.0, false, this, (timer) => {
|
||||
const self = timer.getContext<GameStateManager>();
|
||||
self.startCombatPhase();
|
||||
});
|
||||
}
|
||||
|
||||
private startCombatPhase(): void {
|
||||
this.gamePhase = 'combat';
|
||||
console.log('战斗阶段开始 - 60秒战斗时间');
|
||||
|
||||
this.phaseTimer = Core.schedule(60.0, false, this, (timer) => {
|
||||
const self = timer.getContext<GameStateManager>();
|
||||
self.startResultPhase();
|
||||
});
|
||||
|
||||
// 每10秒刷新一波敌人
|
||||
Core.schedule(10.0, true, null, (timer) => {
|
||||
if (this.gamePhase === 'combat') {
|
||||
this.spawnEnemyWave();
|
||||
} else {
|
||||
timer.stop(); // 战斗阶段结束时停止刷新
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private startResultPhase(): void {
|
||||
this.gamePhase = 'result';
|
||||
console.log('结算阶段开始 - 5秒结算时间');
|
||||
|
||||
this.phaseTimer = Core.schedule(5.0, false, this, (timer) => {
|
||||
const self = timer.getContext<GameStateManager>();
|
||||
self.endGame();
|
||||
});
|
||||
}
|
||||
|
||||
private endGame(): void {
|
||||
console.log('游戏结束');
|
||||
this.gamePhase = 'ended';
|
||||
}
|
||||
|
||||
private spawnEnemyWave(): void {
|
||||
console.log('刷新敌人波次');
|
||||
}
|
||||
|
||||
public getCurrentPhase(): string {
|
||||
return this.gamePhase;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 合理使用时间类型
|
||||
|
||||
```typescript
|
||||
class MovementSystem extends EntitySystem {
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const movement = entity.getComponent(Movement);
|
||||
|
||||
// ✅ 游戏逻辑使用缩放时间
|
||||
movement.position.x += movement.velocity.x * Time.deltaTime;
|
||||
|
||||
// ✅ UI动画使用真实时间(不受游戏暂停影响)
|
||||
const ui = entity.getComponent(UIAnimation);
|
||||
if (ui) {
|
||||
ui.update(Time.unscaledDeltaTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 定时器管理
|
||||
|
||||
```typescript
|
||||
class TimerManager {
|
||||
private timers: any[] = [];
|
||||
|
||||
public createManagedTimer(duration: number, repeats: boolean, callback: () => void): any {
|
||||
const timer = Core.schedule(duration, repeats, null, callback);
|
||||
this.timers.push(timer);
|
||||
return timer;
|
||||
}
|
||||
|
||||
public stopAllTimers(): void {
|
||||
for (const timer of this.timers) {
|
||||
timer.stop();
|
||||
}
|
||||
this.timers = [];
|
||||
}
|
||||
|
||||
public cleanupCompletedTimers(): void {
|
||||
this.timers = this.timers.filter(timer => !timer.isDone);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 避免过多的定时器
|
||||
|
||||
```typescript
|
||||
// ❌ 避免:为每个实体创建定时器
|
||||
class BadExample extends EntitySystem {
|
||||
protected onAdded(entity: Entity): void {
|
||||
Core.schedule(1.0, true, entity, (timer) => {
|
||||
// 每个实体一个定时器,性能差
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 推荐:在系统中统一管理时间
|
||||
class GoodExample extends EntitySystem {
|
||||
private lastUpdateTime = 0;
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 每秒执行一次逻辑
|
||||
if (Time.checkEvery(1.0, this.lastUpdateTime)) {
|
||||
this.processAllEntities(entities);
|
||||
this.lastUpdateTime = Time.totalTime;
|
||||
}
|
||||
}
|
||||
|
||||
private processAllEntities(entities: readonly Entity[]): void {
|
||||
// 批量处理所有实体
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 定时器上下文使用
|
||||
|
||||
```typescript
|
||||
interface TimerContext {
|
||||
entityId: number;
|
||||
duration: number;
|
||||
onComplete: () => void;
|
||||
}
|
||||
|
||||
class ContextualTimerExample {
|
||||
public createEntityTimer(entityId: number, duration: number, onComplete: () => void): void {
|
||||
const context: TimerContext = {
|
||||
entityId,
|
||||
duration,
|
||||
onComplete
|
||||
};
|
||||
|
||||
Core.schedule(duration, false, context, (timer) => {
|
||||
const ctx = timer.getContext<TimerContext>();
|
||||
console.log(`实体 ${ctx.entityId} 的定时器完成`);
|
||||
ctx.onComplete();
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
时间和定时器系统是游戏开发中的重要工具,正确使用这些功能能让你的游戏逻辑更加精确和可控。
|
||||
608
docs/guide/worker-system.md
Normal file
608
docs/guide/worker-system.md
Normal file
@@ -0,0 +1,608 @@
|
||||
# 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;
|
||||
}
|
||||
```
|
||||
|
||||
### 配置建议
|
||||
|
||||
```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系统为ECS框架提供了强大的并行计算能力,让你能够充分利用现代多核处理器的性能,为复杂的游戏逻辑和计算密集型任务提供了高效的解决方案。
|
||||
23
docs/index.md
Normal file
23
docs/index.md
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
layout: home
|
||||
|
||||
hero:
|
||||
name: "ECS Framework"
|
||||
text: "高性能ECS框架"
|
||||
tagline: "为Javascript游戏开发而设计"
|
||||
actions:
|
||||
- theme: brand
|
||||
text: 快速开始
|
||||
link: /guide/getting-started
|
||||
- theme: alt
|
||||
text: 查看示例
|
||||
link: https://github.com/esengine/lawn-mower-demo
|
||||
|
||||
features:
|
||||
- title: 高性能
|
||||
details: 支持大规模实体处理
|
||||
- title: 类型安全
|
||||
details: 完整的TypeScript支持,编译时类型检查
|
||||
- title: 模块化设计
|
||||
details: 核心功能独立打包,支持多平台
|
||||
---
|
||||
@@ -1,425 +0,0 @@
|
||||
# 性能优化指南
|
||||
|
||||
本文档介绍ECS框架的性能优化技术和最佳实践。
|
||||
|
||||
## 目录
|
||||
|
||||
1. [查询系统优化](#查询系统优化)
|
||||
2. [实体管理优化](#实体管理优化)
|
||||
3. [组件设计优化](#组件设计优化)
|
||||
4. [系统设计优化](#系统设计优化)
|
||||
5. [内存管理](#内存管理)
|
||||
6. [性能监控](#性能监控)
|
||||
|
||||
## 查询系统优化
|
||||
|
||||
### 使用高效的查询方法
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:使用标签查询(快速)
|
||||
const enemies = entityManager.getEntitiesByTag(2);
|
||||
|
||||
// ✅ 推荐:使用组件查询
|
||||
const healthEntities = entityManager.getEntitiesWithComponent(HealthComponent);
|
||||
|
||||
// ✅ 推荐:使用Scene的查询系统
|
||||
const movingEntities = scene.querySystem.queryAll(PositionComponent, VelocityComponent);
|
||||
|
||||
// ⚠️ 谨慎:自定义条件查询(较慢)
|
||||
const nearbyEnemies = entityManager
|
||||
.query()
|
||||
.withAll(PositionComponent)
|
||||
.where(entity => {
|
||||
const pos = entity.getComponent(PositionComponent);
|
||||
return pos && Math.abs(pos.x - playerX) < 100;
|
||||
})
|
||||
.execute();
|
||||
```
|
||||
|
||||
### 查询结果缓存
|
||||
|
||||
```typescript
|
||||
class OptimizedCombatSystem extends EntitySystem {
|
||||
private cachedEnemies: Entity[] = [];
|
||||
private lastCacheUpdate = 0;
|
||||
private cacheInterval = 5; // 每5帧更新一次
|
||||
|
||||
protected process(entities: Entity[]): void {
|
||||
// 缓存查询结果
|
||||
if (Time.frameCount - this.lastCacheUpdate >= this.cacheInterval) {
|
||||
this.cachedEnemies = this.entityManager.getEntitiesByTag(2);
|
||||
this.lastCacheUpdate = Time.frameCount;
|
||||
}
|
||||
|
||||
// 使用缓存的结果
|
||||
this.cachedEnemies.forEach(enemy => {
|
||||
this.processEnemy(enemy);
|
||||
});
|
||||
}
|
||||
|
||||
private processEnemy(enemy: Entity): void {
|
||||
// 处理敌人逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 实体管理优化
|
||||
|
||||
### 批量创建实体
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:使用Scene的批量创建
|
||||
function createEnemyWave(count: number): Entity[] {
|
||||
const enemies = scene.createEntities(count, "Enemy");
|
||||
|
||||
// 批量配置组件
|
||||
enemies.forEach((enemy, index) => {
|
||||
enemy.addComponent(new PositionComponent(
|
||||
Math.random() * 800,
|
||||
Math.random() * 600
|
||||
));
|
||||
enemy.addComponent(new HealthComponent(100));
|
||||
enemy.addComponent(new AIComponent());
|
||||
enemy.tag = 2; // 敌人标签
|
||||
});
|
||||
|
||||
return enemies;
|
||||
}
|
||||
|
||||
// ❌ 避免:循环单独创建
|
||||
function createEnemyWaveSlow(count: number): Entity[] {
|
||||
const enemies: Entity[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const enemy = entityManager.createEntity(`Enemy_${i}`);
|
||||
enemy.addComponent(new PositionComponent());
|
||||
enemy.addComponent(new HealthComponent());
|
||||
enemies.push(enemy);
|
||||
}
|
||||
return enemies;
|
||||
}
|
||||
```
|
||||
|
||||
### 实体复用策略
|
||||
|
||||
```typescript
|
||||
// 使用简单的实体复用策略
|
||||
class EntityReusableManager {
|
||||
private inactiveEntities: Entity[] = [];
|
||||
private scene: Scene;
|
||||
|
||||
constructor(scene: Scene) {
|
||||
this.scene = scene;
|
||||
}
|
||||
|
||||
// 预创建实体
|
||||
preCreateEntities(count: number, entityName: string): void {
|
||||
const entities = this.scene.createEntities(count, entityName);
|
||||
entities.forEach(entity => {
|
||||
entity.enabled = false; // 禁用但不销毁
|
||||
this.inactiveEntities.push(entity);
|
||||
});
|
||||
}
|
||||
|
||||
// 获取可复用实体
|
||||
getReusableEntity(): Entity | null {
|
||||
if (this.inactiveEntities.length > 0) {
|
||||
const entity = this.inactiveEntities.pop()!;
|
||||
entity.enabled = true;
|
||||
return entity;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 回收实体
|
||||
recycleEntity(entity: Entity): void {
|
||||
entity.enabled = false;
|
||||
entity.removeAllComponents();
|
||||
this.inactiveEntities.push(entity);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 组件设计优化
|
||||
|
||||
### 数据局部性优化
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:紧凑的数据结构
|
||||
class OptimizedPositionComponent extends Component {
|
||||
public x: number = 0;
|
||||
public y: number = 0;
|
||||
public z: number = 0;
|
||||
|
||||
// 避免对象分配
|
||||
public setPosition(x: number, y: number, z: number = 0): void {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ 避免:过多对象分配
|
||||
class SlowPositionComponent extends Component {
|
||||
public position: { x: number; y: number; z: number } = { x: 0, y: 0, z: 0 };
|
||||
public velocity: { x: number; y: number; z: number } = { x: 0, y: 0, z: 0 };
|
||||
public acceleration: { x: number; y: number; z: number } = { x: 0, y: 0, z: 0 };
|
||||
}
|
||||
```
|
||||
|
||||
### 组件池化
|
||||
|
||||
```typescript
|
||||
// 使用框架内置的组件池
|
||||
ComponentPoolManager.getInstance().registerPool(
|
||||
'BulletComponent',
|
||||
() => new BulletComponent(),
|
||||
(bullet) => bullet.reset(),
|
||||
1000
|
||||
);
|
||||
|
||||
// 获取组件
|
||||
const bullet = ComponentPoolManager.getInstance().acquireComponent('BulletComponent');
|
||||
if (bullet) {
|
||||
entity.addComponent(bullet);
|
||||
}
|
||||
|
||||
// 释放组件
|
||||
ComponentPoolManager.getInstance().releaseComponent('BulletComponent', bullet);
|
||||
```
|
||||
|
||||
## 系统设计优化
|
||||
|
||||
### 系统更新顺序优化
|
||||
|
||||
```typescript
|
||||
class OptimizedGameManager {
|
||||
private scene: Scene;
|
||||
|
||||
constructor() {
|
||||
this.scene = new Scene();
|
||||
this.setupSystems();
|
||||
}
|
||||
|
||||
private setupSystems(): void {
|
||||
// 按依赖关系排序系统
|
||||
this.scene.addEntityProcessor(new InputSystem()).updateOrder = 10;
|
||||
this.scene.addEntityProcessor(new MovementSystem()).updateOrder = 20;
|
||||
this.scene.addEntityProcessor(new CollisionSystem()).updateOrder = 30;
|
||||
this.scene.addEntityProcessor(new RenderSystem()).updateOrder = 40;
|
||||
this.scene.addEntityProcessor(new CleanupSystem()).updateOrder = 50;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 时间分片处理
|
||||
|
||||
```typescript
|
||||
class TimeSlicedAISystem extends EntitySystem {
|
||||
private aiEntities: Entity[] = [];
|
||||
private currentIndex = 0;
|
||||
private entitiesPerFrame = 10;
|
||||
|
||||
protected process(entities: Entity[]): void {
|
||||
// 获取所有AI实体
|
||||
if (this.aiEntities.length === 0) {
|
||||
this.aiEntities = this.entityManager.getEntitiesByTag(3); // AI标签
|
||||
}
|
||||
|
||||
// 每帧只处理部分实体
|
||||
const endIndex = Math.min(
|
||||
this.currentIndex + this.entitiesPerFrame,
|
||||
this.aiEntities.length
|
||||
);
|
||||
|
||||
for (let i = this.currentIndex; i < endIndex; i++) {
|
||||
this.processAI(this.aiEntities[i]);
|
||||
}
|
||||
|
||||
// 更新索引
|
||||
this.currentIndex = endIndex;
|
||||
if (this.currentIndex >= this.aiEntities.length) {
|
||||
this.currentIndex = 0;
|
||||
this.aiEntities = []; // 重新获取实体列表
|
||||
}
|
||||
}
|
||||
|
||||
private processAI(entity: Entity): void {
|
||||
// AI处理逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 内存管理
|
||||
|
||||
### 及时清理无用实体
|
||||
|
||||
```typescript
|
||||
class CleanupSystem extends EntitySystem {
|
||||
protected process(entities: Entity[]): void {
|
||||
// 清理超出边界的子弹
|
||||
const bullets = this.entityManager.getEntitiesByTag(4); // 子弹标签
|
||||
bullets.forEach(bullet => {
|
||||
const pos = bullet.getComponent(PositionComponent);
|
||||
if (pos && this.isOutOfBounds(pos)) {
|
||||
this.entityManager.destroyEntity(bullet);
|
||||
}
|
||||
});
|
||||
|
||||
// 清理死亡的实体
|
||||
const deadEntities = this.entityManager
|
||||
.query()
|
||||
.withAll(HealthComponent)
|
||||
.where(entity => {
|
||||
const health = entity.getComponent(HealthComponent);
|
||||
return health && health.currentHealth <= 0;
|
||||
})
|
||||
.execute();
|
||||
|
||||
deadEntities.forEach(entity => {
|
||||
this.entityManager.destroyEntity(entity);
|
||||
});
|
||||
}
|
||||
|
||||
private isOutOfBounds(pos: PositionComponent): boolean {
|
||||
return pos.x < -100 || pos.x > 900 || pos.y < -100 || pos.y > 700;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 实体复用管理
|
||||
|
||||
```typescript
|
||||
class GameEntityManager {
|
||||
private bulletManager: EntityReusableManager;
|
||||
private effectManager: EntityReusableManager;
|
||||
|
||||
constructor(scene: Scene) {
|
||||
this.bulletManager = new EntityReusableManager(scene);
|
||||
this.effectManager = new EntityReusableManager(scene);
|
||||
|
||||
// 预创建实体
|
||||
this.bulletManager.preCreateEntities(100, "Bullet");
|
||||
this.effectManager.preCreateEntities(50, "Effect");
|
||||
}
|
||||
|
||||
createBullet(): Entity | null {
|
||||
const bullet = this.bulletManager.getReusableEntity();
|
||||
if (bullet) {
|
||||
bullet.addComponent(new BulletComponent());
|
||||
bullet.addComponent(new PositionComponent());
|
||||
bullet.addComponent(new VelocityComponent());
|
||||
}
|
||||
return bullet;
|
||||
}
|
||||
|
||||
destroyBullet(bullet: Entity): void {
|
||||
this.bulletManager.recycleEntity(bullet);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 性能监控
|
||||
|
||||
### 基础性能统计
|
||||
|
||||
```typescript
|
||||
class PerformanceMonitor {
|
||||
private scene: Scene;
|
||||
private entityManager: EntityManager;
|
||||
|
||||
constructor(scene: Scene, entityManager: EntityManager) {
|
||||
this.scene = scene;
|
||||
this.entityManager = entityManager;
|
||||
}
|
||||
|
||||
public getPerformanceReport(): any {
|
||||
return {
|
||||
// 实体统计
|
||||
entities: {
|
||||
total: this.entityManager.entityCount,
|
||||
active: this.entityManager.activeEntityCount
|
||||
},
|
||||
|
||||
// 场景统计
|
||||
scene: this.scene.getStats(),
|
||||
|
||||
// 查询系统统计
|
||||
querySystem: this.scene.querySystem.getStats(),
|
||||
|
||||
// 内存使用
|
||||
memory: {
|
||||
used: (performance as any).memory?.usedJSHeapSize || 0,
|
||||
total: (performance as any).memory?.totalJSHeapSize || 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public logPerformance(): void {
|
||||
const report = this.getPerformanceReport();
|
||||
console.log('性能报告:', report);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 帧率监控
|
||||
|
||||
```typescript
|
||||
class FPSMonitor {
|
||||
private frameCount = 0;
|
||||
private lastTime = performance.now();
|
||||
private fps = 0;
|
||||
|
||||
public update(): void {
|
||||
this.frameCount++;
|
||||
const currentTime = performance.now();
|
||||
|
||||
if (currentTime - this.lastTime >= 1000) {
|
||||
this.fps = this.frameCount;
|
||||
this.frameCount = 0;
|
||||
this.lastTime = currentTime;
|
||||
|
||||
if (this.fps < 30) {
|
||||
console.warn(`低帧率警告: ${this.fps} FPS`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public getFPS(): number {
|
||||
return this.fps;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践总结
|
||||
|
||||
### 查询优化
|
||||
1. 优先使用标签查询和组件查询
|
||||
2. 缓存频繁使用的查询结果
|
||||
3. 避免过度使用自定义条件查询
|
||||
4. 合理设置查询缓存更新频率
|
||||
|
||||
### 实体管理
|
||||
1. 使用批量创建方法
|
||||
2. 实现实体池化减少GC压力
|
||||
3. 及时清理无用实体
|
||||
4. 合理设置实体标签
|
||||
|
||||
### 组件设计
|
||||
1. 保持组件数据紧凑
|
||||
2. 避免在组件中分配大量对象
|
||||
3. 使用组件池化
|
||||
4. 分离数据和行为
|
||||
|
||||
### 系统设计
|
||||
1. 合理安排系统更新顺序
|
||||
2. 对重计算任务使用时间分片
|
||||
3. 避免在系统中进行复杂查询
|
||||
4. 缓存系统间的共享数据
|
||||
|
||||
### 内存管理
|
||||
1. 定期清理无用实体和组件
|
||||
2. 使用对象池减少GC
|
||||
3. 监控内存使用情况
|
||||
4. 避免内存泄漏
|
||||
|
||||
通过遵循这些最佳实践,可以显著提升ECS框架的性能表现。
|
||||
118
docs/public/coi-serviceworker.js
Normal file
118
docs/public/coi-serviceworker.js
Normal file
@@ -0,0 +1,118 @@
|
||||
/*! coi-serviceworker v0.1.7 - Guido Zuidhof and contributors, licensed under MIT */
|
||||
let coepCredentialless = false;
|
||||
if (typeof window === 'undefined') {
|
||||
self.addEventListener("install", () => self.skipWaiting());
|
||||
self.addEventListener("activate", (event) => event.waitUntil(self.clients.claim()));
|
||||
|
||||
self.addEventListener("message", (ev) => {
|
||||
if (!ev.data) {
|
||||
return;
|
||||
} else if (ev.data.type === "deregister") {
|
||||
self.registration
|
||||
.unregister()
|
||||
.then(() => {
|
||||
return self.clients.matchAll();
|
||||
})
|
||||
.then(clients => {
|
||||
clients.forEach((client) => client.navigate(client.url));
|
||||
});
|
||||
} else if (ev.data.type === "coepCredentialless") {
|
||||
coepCredentialless = ev.data.value;
|
||||
}
|
||||
});
|
||||
|
||||
self.addEventListener("fetch", function (event) {
|
||||
const r = event.request;
|
||||
if (r.cache === "only-if-cached" && r.mode !== "same-origin") {
|
||||
return;
|
||||
}
|
||||
|
||||
const request = (coepCredentialless && r.mode === "no-cors")
|
||||
? new Request(r, {
|
||||
credentials: "omit",
|
||||
})
|
||||
: r;
|
||||
event.respondWith(
|
||||
fetch(request)
|
||||
.then((response) => {
|
||||
if (response.status === 0) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const newHeaders = new Headers(response.headers);
|
||||
newHeaders.set("Cross-Origin-Embedder-Policy",
|
||||
coepCredentialless ? "credentialless" : "require-corp"
|
||||
);
|
||||
if (!coepCredentialless) {
|
||||
newHeaders.set("Cross-Origin-Resource-Policy", "cross-origin");
|
||||
}
|
||||
newHeaders.set("Cross-Origin-Opener-Policy", "same-origin");
|
||||
|
||||
return new Response(response.body, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: newHeaders,
|
||||
});
|
||||
})
|
||||
.catch((e) => console.error(e))
|
||||
);
|
||||
});
|
||||
|
||||
} else {
|
||||
(() => {
|
||||
// You can customize the behavior of this script through a global `coi` variable.
|
||||
const coi = {
|
||||
shouldRegister: () => true,
|
||||
shouldDeregister: () => false,
|
||||
coepCredentialless: () => !(window.chrome || window.netscape),
|
||||
doReload: () => window.location.reload(),
|
||||
quiet: false,
|
||||
...window.coi
|
||||
};
|
||||
|
||||
const n = navigator;
|
||||
|
||||
if (n.serviceWorker && n.serviceWorker.controller) {
|
||||
n.serviceWorker.controller.postMessage({
|
||||
type: "coepCredentialless",
|
||||
value: coi.coepCredentialless(),
|
||||
});
|
||||
|
||||
if (coi.shouldDeregister()) {
|
||||
n.serviceWorker.controller.postMessage({ type: "deregister" });
|
||||
}
|
||||
}
|
||||
|
||||
// If we're already coi: do nothing. Perhaps it's due to this script doing its job, or COOP/COEP are
|
||||
// already set from the origin server. Also if the browser has no notion of crossOriginIsolated, just give up here.
|
||||
if (window.crossOriginIsolated !== false || !coi.shouldRegister()) return;
|
||||
|
||||
if (!window.isSecureContext) {
|
||||
!coi.quiet && console.log("COOP/COEP Service Worker not registered, a secure context is required.");
|
||||
return;
|
||||
}
|
||||
|
||||
// In some environments (e.g. Chrome incognito mode) this won't be available
|
||||
if (n.serviceWorker) {
|
||||
n.serviceWorker.register(window.document.currentScript.src).then(
|
||||
(registration) => {
|
||||
!coi.quiet && console.log("COOP/COEP Service Worker registered", registration.scope);
|
||||
|
||||
registration.addEventListener("updatefound", () => {
|
||||
!coi.quiet && console.log("Reloading page to make use of updated COOP/COEP Service Worker.");
|
||||
coi.doReload();
|
||||
});
|
||||
|
||||
// If the registration is active, but it's not controlling the page
|
||||
if (registration.active && !n.serviceWorker.controller) {
|
||||
!coi.quiet && console.log("Reloading page to make use of COOP/COEP Service Worker.");
|
||||
coi.doReload();
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
!coi.quiet && console.error("COOP/COEP Service Worker failed to register:", err);
|
||||
}
|
||||
);
|
||||
}
|
||||
})();
|
||||
}
|
||||
12849
docs/public/demos/worker-system/assets/index-CrID--xK.js
Normal file
12849
docs/public/demos/worker-system/assets/index-CrID--xK.js
Normal file
File diff suppressed because it is too large
Load Diff
210
docs/public/demos/worker-system/index.html
Normal file
210
docs/public/demos/worker-system/index.html
Normal file
@@ -0,0 +1,210 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ECS Framework Worker System Demo</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
font-family: Arial, sans-serif;
|
||||
background: #1a1a1a;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #4a9eff;
|
||||
}
|
||||
|
||||
.demo-area {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.canvas-container {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#gameCanvas {
|
||||
border: 2px solid #4a9eff;
|
||||
background: #000;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.controls {
|
||||
width: 300px;
|
||||
background: #2a2a2a;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.control-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.control-group input, .control-group button {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
margin-bottom: 5px;
|
||||
border: 1px solid #555;
|
||||
background: #3a3a3a;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.control-group button {
|
||||
background: #4a9eff;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.control-group button:hover {
|
||||
background: #3a8eef;
|
||||
}
|
||||
|
||||
.control-group button:disabled {
|
||||
background: #555;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.stats {
|
||||
background: #2a2a2a;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.stats h3 {
|
||||
margin-top: 0;
|
||||
color: #4a9eff;
|
||||
}
|
||||
|
||||
.stat-line {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.worker-enabled {
|
||||
color: #4eff4a;
|
||||
}
|
||||
|
||||
.worker-disabled {
|
||||
color: #ff4a4a;
|
||||
}
|
||||
|
||||
.performance-high {
|
||||
color: #4eff4a;
|
||||
}
|
||||
|
||||
.performance-medium {
|
||||
color: #ffff4a;
|
||||
}
|
||||
|
||||
.performance-low {
|
||||
color: #ff4a4a;
|
||||
}
|
||||
</style>
|
||||
<script type="module" crossorigin src="/ecs-framework/demos/worker-system/assets/index-CrID--xK.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>ECS Framework Worker System 演示</h1>
|
||||
|
||||
<div class="demo-area">
|
||||
<div class="canvas-container">
|
||||
<canvas id="gameCanvas" width="800" height="600"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<div class="control-group">
|
||||
<label>实体数量:</label>
|
||||
<input type="range" id="entityCount" min="100" max="10000" value="1000" step="100">
|
||||
<span id="entityCountValue">1000</span>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Worker 设置:</label>
|
||||
<button id="toggleWorker">禁用 Worker</button>
|
||||
<button id="toggleSAB">禁用 SharedArrayBuffer</button>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<button id="spawnParticles">生成粒子系统</button>
|
||||
<button id="clearEntities">清空所有实体</button>
|
||||
<button id="resetDemo">重置演示</button>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>物理参数:</label>
|
||||
<input type="range" id="gravity" min="0" max="500" value="100" step="10">
|
||||
<label>重力: <span id="gravityValue">100</span></label>
|
||||
|
||||
<input type="range" id="friction" min="0" max="100" value="95" step="5">
|
||||
<label>摩擦力: <span id="frictionValue">95%</span></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<h3>性能统计</h3>
|
||||
<div class="stat-line">FPS: <span id="fps">0</span></div>
|
||||
<div class="stat-line">实体数量: <span id="entityCountStat">0</span></div>
|
||||
<div class="stat-line">Worker状态: <span id="workerStatus" class="worker-disabled">未启用</span></div>
|
||||
<div class="stat-line">Worker负载: <span id="workerLoad">N/A</span></div>
|
||||
<div class="stat-line">运行模式: <span id="sabStatus" class="worker-disabled">同步模式</span></div>
|
||||
<div class="stat-line">物理系统耗时: <span id="physicsTime">0</span>ms</div>
|
||||
<div class="stat-line">渲染系统耗时: <span id="renderTime">0</span>ms</div>
|
||||
<div class="stat-line">总帧时间: <span id="frameTime">0</span>ms</div>
|
||||
<div class="stat-line">内存使用: <span id="memoryUsage">0</span>MB</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 使用 coi-serviceworker 启用 SharedArrayBuffer 支持 -->
|
||||
<script src="/ecs-framework/coi-serviceworker.js"></script>
|
||||
|
||||
<script>
|
||||
// Check SharedArrayBuffer support and display info
|
||||
function checkSharedArrayBufferSupport() {
|
||||
const hasSharedArrayBuffer = typeof SharedArrayBuffer !== 'undefined';
|
||||
const isCrossOriginIsolated = self.crossOriginIsolated || false;
|
||||
const isLocalhost = location.hostname === 'localhost' || location.hostname === '127.0.0.1';
|
||||
const isGitHubPages = location.hostname === 'esengine.github.io';
|
||||
|
||||
console.log('=== SharedArrayBuffer 支持检测 ===');
|
||||
console.log('SharedArrayBuffer 存在:', hasSharedArrayBuffer);
|
||||
console.log('跨域隔离状态:', isCrossOriginIsolated);
|
||||
console.log('是否本地环境:', isLocalhost);
|
||||
console.log('是否 GitHub Pages:', isGitHubPages);
|
||||
|
||||
if (hasSharedArrayBuffer && isCrossOriginIsolated) {
|
||||
console.log('✅ SharedArrayBuffer 功能已启用!');
|
||||
console.log('系统将使用高性能的 SharedArrayBuffer 模式');
|
||||
} else if (isGitHubPages) {
|
||||
console.log('ℹ️ 如果页面刷新,可能是 coi-serviceworker 正在设置跨域隔离');
|
||||
console.log('刷新后 SharedArrayBuffer 应该可用');
|
||||
}
|
||||
|
||||
return hasSharedArrayBuffer && isCrossOriginIsolated;
|
||||
}
|
||||
|
||||
// Run check after page load
|
||||
window.addEventListener('load', () => {
|
||||
setTimeout(checkSharedArrayBufferSupport, 1000);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,333 +0,0 @@
|
||||
# QuerySystem 使用指南
|
||||
|
||||
QuerySystem 是 ECS Framework 中的高性能实体查询系统,支持多级索引、智能缓存和类型安全的查询操作。
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 1. 获取查询系统
|
||||
|
||||
```typescript
|
||||
import { Scene, Entity } from '@esengine/ecs-framework';
|
||||
|
||||
// 创建场景,查询系统会自动创建
|
||||
const scene = new Scene();
|
||||
const querySystem = scene.querySystem;
|
||||
|
||||
// 或者从Core获取当前场景的查询系统
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
const currentQuerySystem = Core.scene?.querySystem;
|
||||
```
|
||||
|
||||
### 2. 基本查询操作
|
||||
|
||||
```typescript
|
||||
// 查询包含所有指定组件的实体
|
||||
const result = querySystem.queryAll(PositionComponent, VelocityComponent);
|
||||
console.log(`找到 ${result.count} 个实体`);
|
||||
|
||||
// 查询包含任意指定组件的实体
|
||||
const anyResult = querySystem.queryAny(HealthComponent, ManaComponent);
|
||||
|
||||
// 查询不包含指定组件的实体
|
||||
const noneResult = querySystem.queryNone(DeadComponent);
|
||||
```
|
||||
|
||||
### 3. 类型安全查询
|
||||
|
||||
```typescript
|
||||
// 类型安全的查询,返回实体和对应的组件
|
||||
const typedResult = querySystem.queryAll(PositionComponent, VelocityComponent);
|
||||
for (let i = 0; i < typedResult.entities.length; i++) {
|
||||
const entity = typedResult.entities[i];
|
||||
const [position, velocity] = typedResult.components[i];
|
||||
// position 和 velocity 都是类型安全的
|
||||
}
|
||||
|
||||
// 查询单个组件类型
|
||||
const healthEntities = querySystem.queryComponentTyped(HealthComponent);
|
||||
healthEntities.forEach(({ entity, component }) => {
|
||||
console.log(`实体 ${entity.name} 的生命值: ${component.value}`);
|
||||
});
|
||||
|
||||
// 查询两个组件类型
|
||||
const movableEntities = querySystem.queryTwoComponents(PositionComponent, VelocityComponent);
|
||||
movableEntities.forEach(({ entity, component1: position, component2: velocity }) => {
|
||||
// 更新位置
|
||||
position.x += velocity.x;
|
||||
position.y += velocity.y;
|
||||
});
|
||||
```
|
||||
|
||||
### 4. 使用查询构建器
|
||||
|
||||
```typescript
|
||||
// 创建复杂查询
|
||||
const query = querySystem.createQuery()
|
||||
.withAll(PositionComponent, RenderComponent)
|
||||
.without(HiddenComponent)
|
||||
.withTag(1) // 特定标签
|
||||
.orderByName()
|
||||
.limit(10);
|
||||
|
||||
const result = query.execute();
|
||||
|
||||
// 链式操作
|
||||
const visibleEnemies = querySystem.createQuery()
|
||||
.withAll(EnemyComponent, PositionComponent)
|
||||
.without(DeadComponent, HiddenComponent)
|
||||
.filter(entity => entity.name.startsWith('Boss'))
|
||||
.orderBy((a, b) => a.id - b.id);
|
||||
|
||||
// 迭代结果
|
||||
visibleEnemies.forEach((entity, index) => {
|
||||
console.log(`敌人 ${index}: ${entity.name}`);
|
||||
});
|
||||
```
|
||||
|
||||
### 5. 高级查询功能
|
||||
|
||||
```typescript
|
||||
// 复合查询
|
||||
const complexResult = querySystem.queryComplex(
|
||||
{
|
||||
type: QueryConditionType.ALL,
|
||||
componentTypes: [PositionComponent, VelocityComponent],
|
||||
mask: /* 位掩码 */
|
||||
},
|
||||
{
|
||||
type: QueryConditionType.NONE,
|
||||
componentTypes: [DeadComponent],
|
||||
mask: /* 位掩码 */
|
||||
}
|
||||
);
|
||||
|
||||
// 批量查询
|
||||
const batchResults = querySystem.batchQuery([
|
||||
{ type: QueryConditionType.ALL, componentTypes: [HealthComponent], mask: /* 位掩码 */ },
|
||||
{ type: QueryConditionType.ALL, componentTypes: [ManaComponent], mask: /* 位掩码 */ }
|
||||
]);
|
||||
|
||||
// 并行查询
|
||||
const parallelResults = await querySystem.parallelQuery([
|
||||
{ type: QueryConditionType.ALL, componentTypes: [PositionComponent], mask: /* 位掩码 */ },
|
||||
{ type: QueryConditionType.ALL, componentTypes: [VelocityComponent], mask: /* 位掩码 */ }
|
||||
]);
|
||||
```
|
||||
|
||||
## 场景级别的实体查询
|
||||
|
||||
除了使用QuerySystem,您还可以直接使用Scene提供的便捷查询方法:
|
||||
|
||||
### 基本场景查询
|
||||
|
||||
```typescript
|
||||
// 按名称查找实体
|
||||
const player = scene.findEntity("Player");
|
||||
const playerAlt = scene.getEntityByName("Player"); // 别名方法
|
||||
|
||||
// 按ID查找实体
|
||||
const entity = scene.findEntityById(123);
|
||||
|
||||
// 按标签查找实体
|
||||
const enemies = scene.findEntitiesByTag(2);
|
||||
const enemiesAlt = scene.getEntitiesByTag(2); // 别名方法
|
||||
|
||||
// 获取所有实体
|
||||
const allEntities = scene.entities.buffer;
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 1. 缓存管理
|
||||
|
||||
```typescript
|
||||
// 设置缓存配置
|
||||
querySystem.setCacheConfig(200, 2000); // 最大200个缓存项,2秒超时
|
||||
|
||||
// 清空缓存
|
||||
querySystem.clearCache();
|
||||
|
||||
// 预热常用查询
|
||||
const commonQueries = [
|
||||
{ type: QueryConditionType.ALL, componentTypes: [PositionComponent], mask: /* 位掩码 */ },
|
||||
{ type: QueryConditionType.ALL, componentTypes: [VelocityComponent], mask: /* 位掩码 */ }
|
||||
];
|
||||
querySystem.warmUpCache(commonQueries);
|
||||
```
|
||||
|
||||
### 2. 索引优化
|
||||
|
||||
```typescript
|
||||
// 获取性能统计
|
||||
const stats = querySystem.getStats();
|
||||
console.log(`缓存命中率: ${(stats.hitRate * 100).toFixed(1)}%`);
|
||||
console.log(`实体数量: ${stats.entityCount}`);
|
||||
|
||||
// 获取详细性能报告
|
||||
const report = querySystem.getPerformanceReport();
|
||||
console.log(report);
|
||||
```
|
||||
|
||||
### 3. 查询监听和快照
|
||||
|
||||
```typescript
|
||||
// 监听查询结果变更
|
||||
const unwatch = querySystem.watchQuery(
|
||||
{ type: QueryConditionType.ALL, componentTypes: [EnemyComponent], mask: /* 位掩码 */ },
|
||||
(entities, changeType) => {
|
||||
console.log(`敌人实体${changeType}: ${entities.length}个`);
|
||||
}
|
||||
);
|
||||
|
||||
// 取消监听
|
||||
unwatch();
|
||||
|
||||
// 创建查询快照
|
||||
const snapshot1 = querySystem.createSnapshot(
|
||||
{ type: QueryConditionType.ALL, componentTypes: [PlayerComponent], mask: /* 位掩码 */ }
|
||||
);
|
||||
|
||||
// 稍后创建另一个快照
|
||||
const snapshot2 = querySystem.createSnapshot(
|
||||
{ type: QueryConditionType.ALL, componentTypes: [PlayerComponent], mask: /* 位掩码 */ }
|
||||
);
|
||||
|
||||
// 比较快照
|
||||
const diff = querySystem.compareSnapshots(snapshot1, snapshot2);
|
||||
console.log(`新增: ${diff.added.length}, 移除: ${diff.removed.length}`);
|
||||
```
|
||||
|
||||
## 实际使用示例
|
||||
|
||||
### 移动系统示例
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, Entity, Matcher } from '@esengine/ecs-framework';
|
||||
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(PositionComponent, VelocityComponent));
|
||||
}
|
||||
|
||||
protected process(entities: Entity[]): void {
|
||||
// 查询所有可移动的实体
|
||||
const movableEntities = this.scene.querySystem.queryTwoComponents(
|
||||
PositionComponent,
|
||||
VelocityComponent
|
||||
);
|
||||
|
||||
movableEntities.forEach(({ entity, component1: position, component2: velocity }) => {
|
||||
// 更新位置
|
||||
position.x += velocity.x * Time.deltaTime;
|
||||
position.y += velocity.y * Time.deltaTime;
|
||||
|
||||
// 边界检查
|
||||
if (position.x < 0 || position.x > 800) {
|
||||
velocity.x = -velocity.x;
|
||||
}
|
||||
if (position.y < 0 || position.y > 600) {
|
||||
velocity.y = -velocity.y;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 碰撞检测示例
|
||||
|
||||
```typescript
|
||||
class CollisionSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(PositionComponent, ColliderComponent));
|
||||
}
|
||||
|
||||
protected process(entities: Entity[]): void {
|
||||
// 获取所有具有碰撞器的实体
|
||||
const collidableEntities = this.scene.querySystem.queryTwoComponents(
|
||||
PositionComponent,
|
||||
ColliderComponent
|
||||
);
|
||||
|
||||
// 检测碰撞
|
||||
for (let i = 0; i < collidableEntities.length; i++) {
|
||||
for (let j = i + 1; j < collidableEntities.length; j++) {
|
||||
const entityA = collidableEntities[i];
|
||||
const entityB = collidableEntities[j];
|
||||
|
||||
if (this.checkCollision(entityA, entityB)) {
|
||||
this.handleCollision(entityA.entity, entityB.entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private checkCollision(entityA: any, entityB: any): boolean {
|
||||
// 简单的距离检测
|
||||
const posA = entityA.component1;
|
||||
const posB = entityB.component1;
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(posA.x - posB.x, 2) + Math.pow(posA.y - posB.y, 2)
|
||||
);
|
||||
return distance < 50;
|
||||
}
|
||||
|
||||
private handleCollision(entityA: Entity, entityB: Entity): void {
|
||||
console.log(`碰撞检测: ${entityA.name} 与 ${entityB.name}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 生命值管理示例
|
||||
|
||||
```typescript
|
||||
class HealthSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(HealthComponent));
|
||||
}
|
||||
|
||||
protected process(entities: Entity[]): void {
|
||||
// 查询所有具有生命值的实体
|
||||
const healthEntities = this.scene.querySystem.queryComponentTyped(HealthComponent);
|
||||
const deadEntities: Entity[] = [];
|
||||
|
||||
healthEntities.forEach(({ entity, component: health }) => {
|
||||
// 检查死亡
|
||||
if (health.currentHealth <= 0) {
|
||||
deadEntities.push(entity);
|
||||
}
|
||||
});
|
||||
|
||||
// 移除死亡实体
|
||||
deadEntities.forEach(entity => {
|
||||
console.log(`实体 ${entity.name} 已死亡`);
|
||||
entity.destroy();
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 查询优化
|
||||
|
||||
- 尽量使用类型安全的查询方法
|
||||
- 避免在每帧都创建新的查询
|
||||
- 合理使用缓存机制
|
||||
|
||||
### 2. 性能监控
|
||||
|
||||
- 定期检查查询性能报告
|
||||
- 监控缓存命中率
|
||||
- 优化频繁使用的查询
|
||||
|
||||
### 3. 内存管理
|
||||
|
||||
- 及时清理不需要的查询监听器
|
||||
- 合理设置缓存大小
|
||||
- 避免创建过多的查询快照
|
||||
|
||||
### 4. 代码组织
|
||||
|
||||
- 将复杂查询封装到专门的方法中
|
||||
- 使用查询构建器创建可读性更好的查询
|
||||
- 在系统中合理组织查询逻辑
|
||||
@@ -1,734 +0,0 @@
|
||||
# 场景管理完整指南
|
||||
|
||||
场景(Scene)是ECS框架中管理游戏对象和系统的核心容器。本指南将详细介绍如何有效地使用场景来构建和管理你的游戏。
|
||||
|
||||
## 场景基础概念
|
||||
|
||||
### 什么是场景?
|
||||
|
||||
场景是一个完整的游戏世界容器,它包含:
|
||||
- 🎮 **实体集合** - 所有游戏对象
|
||||
- ⚙️ **系统集合** - 处理游戏逻辑的系统
|
||||
- 📊 **事件系统** - 场景内的事件通信
|
||||
- 🔍 **查询系统** - 高效的实体查询
|
||||
- 📈 **性能监控** - 场景级别的性能统计
|
||||
|
||||
```typescript
|
||||
import { Scene, Core } from '@esengine/ecs-framework';
|
||||
|
||||
// 创建场景
|
||||
const gameScene = new Scene();
|
||||
|
||||
// 设置为当前活动场景
|
||||
Core.scene = gameScene;
|
||||
```
|
||||
|
||||
### 场景的生命周期
|
||||
|
||||
```typescript
|
||||
class GameScene extends Scene {
|
||||
// 场景开始时调用
|
||||
onStart() {
|
||||
console.log("场景开始");
|
||||
this.initializeScene();
|
||||
}
|
||||
|
||||
// 场景更新时调用(每帧)
|
||||
update() {
|
||||
super.update(); // 调用父类更新
|
||||
|
||||
// 自定义更新逻辑
|
||||
this.updateGameLogic();
|
||||
}
|
||||
|
||||
// 场景结束时调用
|
||||
onDestroy() {
|
||||
console.log("场景结束");
|
||||
this.cleanup();
|
||||
super.onDestroy();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 基础场景操作
|
||||
|
||||
### 1. 创建和配置场景
|
||||
|
||||
```typescript
|
||||
class MenuScene extends Scene {
|
||||
private backgroundMusic: AudioClip;
|
||||
|
||||
onStart() {
|
||||
this.setupUI();
|
||||
this.setupSystems();
|
||||
this.setupInput();
|
||||
this.playBackgroundMusic();
|
||||
}
|
||||
|
||||
private setupUI() {
|
||||
// 创建菜单UI实体
|
||||
const titleEntity = this.createEntity("Title");
|
||||
titleEntity.addComponent(new TextComponent("我的游戏", 48));
|
||||
titleEntity.addComponent(new PositionComponent(400, 100));
|
||||
|
||||
const startButton = this.createEntity("StartButton");
|
||||
startButton.addComponent(new ButtonComponent("开始游戏"));
|
||||
startButton.addComponent(new PositionComponent(400, 300));
|
||||
|
||||
const settingsButton = this.createEntity("SettingsButton");
|
||||
settingsButton.addComponent(new ButtonComponent("设置"));
|
||||
settingsButton.addComponent(new PositionComponent(400, 400));
|
||||
|
||||
const exitButton = this.createEntity("ExitButton");
|
||||
exitButton.addComponent(new ButtonComponent("退出"));
|
||||
exitButton.addComponent(new PositionComponent(400, 500));
|
||||
}
|
||||
|
||||
private setupSystems() {
|
||||
// 添加UI相关系统
|
||||
this.addEntityProcessor(new UIRenderSystem());
|
||||
this.addEntityProcessor(new ButtonClickSystem());
|
||||
this.addEntityProcessor(new MenuTransitionSystem());
|
||||
}
|
||||
|
||||
private setupInput() {
|
||||
// 监听按钮点击事件
|
||||
this.eventBus.on('button:clicked', this.onButtonClicked, this);
|
||||
}
|
||||
|
||||
private onButtonClicked(data: { buttonName: string }) {
|
||||
switch (data.buttonName) {
|
||||
case "开始游戏":
|
||||
this.transitionToGame();
|
||||
break;
|
||||
case "设置":
|
||||
this.showSettings();
|
||||
break;
|
||||
case "退出":
|
||||
this.exitGame();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private transitionToGame() {
|
||||
// 切换到游戏场景
|
||||
const gameScene = new GameScene();
|
||||
Core.scene = gameScene;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 游戏主场景
|
||||
|
||||
```typescript
|
||||
class GameScene extends Scene {
|
||||
private player: Entity;
|
||||
private enemySpawner: Entity;
|
||||
private ui: Entity;
|
||||
|
||||
onStart() {
|
||||
this.setupWorld();
|
||||
this.setupPlayer();
|
||||
this.setupEnemies();
|
||||
this.setupSystems();
|
||||
this.setupUI();
|
||||
}
|
||||
|
||||
private setupWorld() {
|
||||
// 创建背景
|
||||
const background = this.createEntity("Background");
|
||||
background.addComponent(new SpriteComponent("background.png"));
|
||||
background.addComponent(new PositionComponent(0, 0));
|
||||
|
||||
// 创建边界
|
||||
this.createWorldBounds();
|
||||
}
|
||||
|
||||
private setupPlayer() {
|
||||
this.player = this.createEntity("Player");
|
||||
this.player.addComponent(new PositionComponent(400, 300));
|
||||
this.player.addComponent(new VelocityComponent());
|
||||
this.player.addComponent(new HealthComponent(100));
|
||||
this.player.addComponent(new SpriteComponent("player.png"));
|
||||
this.player.addComponent(new PlayerInputComponent());
|
||||
this.player.addComponent(new WeaponComponent());
|
||||
this.player.tag = EntityTags.PLAYER;
|
||||
}
|
||||
|
||||
private setupEnemies() {
|
||||
this.enemySpawner = this.createEntity("EnemySpawner");
|
||||
this.enemySpawner.addComponent(new SpawnerComponent());
|
||||
this.enemySpawner.addComponent(new PositionComponent(0, 0));
|
||||
}
|
||||
|
||||
private setupSystems() {
|
||||
// 输入系统
|
||||
this.addEntityProcessor(new PlayerInputSystem()).updateOrder = 0;
|
||||
|
||||
// 游戏逻辑系统
|
||||
this.addEntityProcessor(new MovementSystem()).updateOrder = 10;
|
||||
this.addEntityProcessor(new AISystem()).updateOrder = 15;
|
||||
this.addEntityProcessor(new WeaponSystem()).updateOrder = 20;
|
||||
this.addEntityProcessor(new CollisionSystem()).updateOrder = 30;
|
||||
this.addEntityProcessor(new HealthSystem()).updateOrder = 40;
|
||||
|
||||
// 生成和清理系统
|
||||
this.addEntityProcessor(new EnemySpawnSystem()).updateOrder = 50;
|
||||
this.addEntityProcessor(new EntityCleanupSystem()).updateOrder = 60;
|
||||
|
||||
// 渲染系统
|
||||
this.addEntityProcessor(new RenderSystem()).updateOrder = 100;
|
||||
this.addEntityProcessor(new UIRenderSystem()).updateOrder = 110;
|
||||
|
||||
// 特效和音频系统
|
||||
this.addEntityProcessor(new ParticleSystem()).updateOrder = 120;
|
||||
this.addEntityProcessor(new AudioSystem()).updateOrder = 130;
|
||||
}
|
||||
|
||||
private setupUI() {
|
||||
this.ui = this.createEntity("GameUI");
|
||||
this.ui.addComponent(new HealthBarComponent());
|
||||
this.ui.addComponent(new ScoreDisplayComponent());
|
||||
this.ui.addComponent(new AmmoDisplayComponent());
|
||||
}
|
||||
|
||||
private createWorldBounds() {
|
||||
// 创建世界边界,防止实体跑出屏幕
|
||||
const bounds = [
|
||||
{ x: 0, y: 0, width: 10, height: 600 }, // 左边界
|
||||
{ x: 790, y: 0, width: 10, height: 600 }, // 右边界
|
||||
{ x: 0, y: 0, width: 800, height: 10 }, // 上边界
|
||||
{ x: 0, y: 590, width: 800, height: 10 } // 下边界
|
||||
];
|
||||
|
||||
bounds.forEach((bound, index) => {
|
||||
const wall = this.createEntity(`Wall_${index}`);
|
||||
wall.addComponent(new PositionComponent(bound.x, bound.y));
|
||||
wall.addComponent(new ColliderComponent(bound.width, bound.height));
|
||||
wall.addComponent(new WallComponent());
|
||||
wall.tag = EntityTags.WALL;
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 场景切换和管理
|
||||
|
||||
### 1. 场景管理器
|
||||
|
||||
> **注意:** 以下的 SceneManager、TransitionManager 等是自定义的场景管理类示例,不是ECS框架提供的内置API。你可以基于这些示例实现自己的场景管理系统。
|
||||
|
||||
```typescript
|
||||
enum SceneType {
|
||||
MENU = "menu",
|
||||
GAME = "game",
|
||||
PAUSE = "pause",
|
||||
GAME_OVER = "game_over",
|
||||
SETTINGS = "settings"
|
||||
}
|
||||
|
||||
// 自定义场景管理器(示例实现)
|
||||
class SceneManager {
|
||||
private static instance: SceneManager;
|
||||
private currentScene: Scene | null = null;
|
||||
private previousScene: Scene | null = null;
|
||||
private sceneHistory: Scene[] = [];
|
||||
|
||||
static getInstance(): SceneManager {
|
||||
if (!this.instance) {
|
||||
this.instance = new SceneManager();
|
||||
}
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
switchToScene(sceneType: SceneType, data?: any) {
|
||||
// 保存当前场景到历史
|
||||
if (this.currentScene) {
|
||||
this.previousScene = this.currentScene;
|
||||
this.sceneHistory.push(this.currentScene);
|
||||
this.currentScene.onDestroy();
|
||||
}
|
||||
|
||||
// 创建新场景
|
||||
this.currentScene = this.createScene(sceneType, data);
|
||||
Core.scene = this.currentScene;
|
||||
|
||||
console.log(`切换到场景: ${sceneType}`);
|
||||
}
|
||||
|
||||
goBack(): boolean {
|
||||
if (this.sceneHistory.length > 0) {
|
||||
const previousScene = this.sceneHistory.pop()!;
|
||||
|
||||
if (this.currentScene) {
|
||||
this.currentScene.onDestroy();
|
||||
}
|
||||
|
||||
this.currentScene = previousScene;
|
||||
Core.scene = this.currentScene;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
pushScene(sceneType: SceneType, data?: any) {
|
||||
// 暂停当前场景,不销毁
|
||||
if (this.currentScene) {
|
||||
this.previousScene = this.currentScene;
|
||||
this.sceneHistory.push(this.currentScene);
|
||||
this.pauseScene(this.currentScene);
|
||||
}
|
||||
|
||||
this.currentScene = this.createScene(sceneType, data);
|
||||
Core.scene = this.currentScene;
|
||||
}
|
||||
|
||||
popScene() {
|
||||
if (this.sceneHistory.length > 0) {
|
||||
if (this.currentScene) {
|
||||
this.currentScene.onDestroy();
|
||||
}
|
||||
|
||||
this.currentScene = this.sceneHistory.pop()!;
|
||||
this.resumeScene(this.currentScene);
|
||||
Core.scene = this.currentScene;
|
||||
}
|
||||
}
|
||||
|
||||
private createScene(sceneType: SceneType, data?: any): Scene {
|
||||
switch (sceneType) {
|
||||
case SceneType.MENU:
|
||||
return new MenuScene();
|
||||
case SceneType.GAME:
|
||||
return new GameScene(data);
|
||||
case SceneType.PAUSE:
|
||||
return new PauseScene();
|
||||
case SceneType.GAME_OVER:
|
||||
return new GameOverScene(data);
|
||||
case SceneType.SETTINGS:
|
||||
return new SettingsScene();
|
||||
default:
|
||||
throw new Error(`Unknown scene type: ${sceneType}`);
|
||||
}
|
||||
}
|
||||
|
||||
private pauseScene(scene: Scene) {
|
||||
// 暂停场景的所有系统
|
||||
scene.systems.forEach(system => {
|
||||
system.enabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
private resumeScene(scene: Scene) {
|
||||
// 恢复场景的所有系统
|
||||
scene.systems.forEach(system => {
|
||||
system.enabled = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 使用场景管理器
|
||||
const sceneManager = SceneManager.getInstance();
|
||||
|
||||
// 切换场景
|
||||
sceneManager.switchToScene(SceneType.MENU);
|
||||
|
||||
// 推入场景(用于暂停菜单等)
|
||||
sceneManager.pushScene(SceneType.PAUSE);
|
||||
|
||||
// 弹出场景(返回游戏)
|
||||
sceneManager.popScene();
|
||||
```
|
||||
|
||||
### 2. 场景转场效果
|
||||
|
||||
```typescript
|
||||
class TransitionManager {
|
||||
private isTransitioning: boolean = false;
|
||||
|
||||
async fadeTransition(fromScene: Scene, toScene: Scene, duration: number = 1.0) {
|
||||
if (this.isTransitioning) return;
|
||||
|
||||
this.isTransitioning = true;
|
||||
|
||||
// 创建转场覆盖层
|
||||
const overlay = this.createFadeOverlay();
|
||||
|
||||
// 淡出当前场景
|
||||
await this.fadeOut(overlay, duration / 2);
|
||||
|
||||
// 切换场景
|
||||
fromScene.onDestroy();
|
||||
Core.scene = toScene;
|
||||
|
||||
// 淡入新场景
|
||||
await this.fadeIn(overlay, duration / 2);
|
||||
|
||||
// 清理覆盖层
|
||||
overlay.destroy();
|
||||
this.isTransitioning = false;
|
||||
}
|
||||
|
||||
async slideTransition(fromScene: Scene, toScene: Scene, direction: 'left' | 'right' | 'up' | 'down') {
|
||||
if (this.isTransitioning) return;
|
||||
|
||||
this.isTransitioning = true;
|
||||
|
||||
// 实现滑动转场效果
|
||||
const slideDistance = this.getSlideDistance(direction);
|
||||
|
||||
// 移动当前场景
|
||||
await this.slideScene(fromScene, slideDistance);
|
||||
|
||||
// 切换场景
|
||||
fromScene.onDestroy();
|
||||
Core.scene = toScene;
|
||||
|
||||
// 从相反方向滑入新场景
|
||||
await this.slideScene(toScene, -slideDistance);
|
||||
|
||||
this.isTransitioning = false;
|
||||
}
|
||||
|
||||
private createFadeOverlay(): Entity {
|
||||
const overlay = Core.scene.createEntity("TransitionOverlay");
|
||||
overlay.addComponent(new SpriteComponent("black_pixel.png"));
|
||||
overlay.addComponent(new PositionComponent(0, 0));
|
||||
|
||||
const sprite = overlay.getComponent(SpriteComponent);
|
||||
sprite.width = 800;
|
||||
sprite.height = 600;
|
||||
sprite.alpha = 0;
|
||||
|
||||
return overlay;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 场景数据管理
|
||||
|
||||
### 1. 场景间数据传递
|
||||
|
||||
```typescript
|
||||
interface GameData {
|
||||
score: number;
|
||||
level: number;
|
||||
playerName: string;
|
||||
difficulty: string;
|
||||
}
|
||||
|
||||
class GameScene extends Scene {
|
||||
private gameData: GameData;
|
||||
|
||||
constructor(data?: GameData) {
|
||||
super();
|
||||
this.gameData = data || {
|
||||
score: 0,
|
||||
level: 1,
|
||||
playerName: "Player",
|
||||
difficulty: "normal"
|
||||
};
|
||||
}
|
||||
|
||||
onStart() {
|
||||
super.onStart();
|
||||
|
||||
// 根据传入数据配置场景
|
||||
this.setupPlayerWithData();
|
||||
this.setupLevelWithDifficulty();
|
||||
}
|
||||
|
||||
private setupPlayerWithData() {
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new NameComponent(this.gameData.playerName));
|
||||
player.addComponent(new ScoreComponent(this.gameData.score));
|
||||
// ... 其他组件
|
||||
}
|
||||
|
||||
private setupLevelWithDifficulty() {
|
||||
const difficultySettings = {
|
||||
easy: { enemySpawnRate: 2.0, enemyHealth: 50 },
|
||||
normal: { enemySpawnRate: 1.5, enemyHealth: 75 },
|
||||
hard: { enemySpawnRate: 1.0, enemyHealth: 100 }
|
||||
};
|
||||
|
||||
const settings = difficultySettings[this.gameData.difficulty];
|
||||
|
||||
const spawner = this.createEntity("EnemySpawner");
|
||||
const spawnerComp = new SpawnerComponent();
|
||||
spawnerComp.spawnInterval = settings.enemySpawnRate;
|
||||
spawnerComp.enemyHealth = settings.enemyHealth;
|
||||
spawner.addComponent(spawnerComp);
|
||||
}
|
||||
|
||||
// 游戏结束时传递数据到下一个场景
|
||||
gameOver() {
|
||||
const finalScore = this.getPlayerScore();
|
||||
const sceneManager = SceneManager.getInstance();
|
||||
|
||||
sceneManager.switchToScene(SceneType.GAME_OVER, {
|
||||
score: finalScore,
|
||||
level: this.gameData.level,
|
||||
playerName: this.gameData.playerName
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class GameOverScene extends Scene {
|
||||
constructor(private gameData: GameData) {
|
||||
super();
|
||||
}
|
||||
|
||||
onStart() {
|
||||
this.displayResults();
|
||||
this.setupRestartButton();
|
||||
}
|
||||
|
||||
private displayResults() {
|
||||
const scoreText = this.createEntity("ScoreText");
|
||||
scoreText.addComponent(new TextComponent(`最终分数: ${this.gameData.score}`));
|
||||
scoreText.addComponent(new PositionComponent(400, 200));
|
||||
|
||||
const levelText = this.createEntity("LevelText");
|
||||
levelText.addComponent(new TextComponent(`到达关卡: ${this.gameData.level}`));
|
||||
levelText.addComponent(new PositionComponent(400, 250));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 持久化数据管理
|
||||
|
||||
```typescript
|
||||
class SaveManager {
|
||||
private static SAVE_KEY = "game_save_data";
|
||||
|
||||
static saveScene(scene: Scene): void {
|
||||
const saveData = {
|
||||
playerData: this.extractPlayerData(scene),
|
||||
sceneState: this.extractSceneState(scene),
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
localStorage.setItem(this.SAVE_KEY, JSON.stringify(saveData));
|
||||
console.log("游戏已保存");
|
||||
}
|
||||
|
||||
static loadScene(): Scene | null {
|
||||
const saveDataStr = localStorage.getItem(this.SAVE_KEY);
|
||||
if (!saveDataStr) return null;
|
||||
|
||||
try {
|
||||
const saveData = JSON.parse(saveDataStr);
|
||||
return this.recreateScene(saveData);
|
||||
} catch (error) {
|
||||
console.error("读取存档失败:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static extractPlayerData(scene: Scene): any {
|
||||
const player = scene.findEntitiesWithTag(EntityTags.PLAYER)[0];
|
||||
if (!player) return null;
|
||||
|
||||
return {
|
||||
position: player.getComponent(PositionComponent),
|
||||
health: player.getComponent(HealthComponent),
|
||||
inventory: player.getComponent(InventoryComponent)?.getItems(),
|
||||
score: player.getComponent(ScoreComponent)?.score
|
||||
};
|
||||
}
|
||||
|
||||
private static extractSceneState(scene: Scene): any {
|
||||
return {
|
||||
enemies: this.extractEnemiesData(scene),
|
||||
items: this.extractItemsData(scene),
|
||||
level: this.getCurrentLevel(scene)
|
||||
};
|
||||
}
|
||||
|
||||
private static recreateScene(saveData: any): Scene {
|
||||
const scene = new GameScene();
|
||||
|
||||
// 重建玩家
|
||||
this.recreatePlayer(scene, saveData.playerData);
|
||||
|
||||
// 重建场景状态
|
||||
this.recreateSceneState(scene, saveData.sceneState);
|
||||
|
||||
return scene;
|
||||
}
|
||||
}
|
||||
|
||||
// 自动保存系统
|
||||
class AutoSaveSystem extends IntervalSystem {
|
||||
constructor() {
|
||||
super(30.0); // 每30秒自动保存
|
||||
}
|
||||
|
||||
processSystem() {
|
||||
SaveManager.saveScene(this.scene);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 场景性能优化
|
||||
|
||||
### 1. 实体管理优化
|
||||
|
||||
```typescript
|
||||
class OptimizedScene extends Scene {
|
||||
private activeEntities: Set<Entity> = new Set();
|
||||
private inactiveEntities: Set<Entity> = new Set();
|
||||
|
||||
createEntity(name?: string): Entity {
|
||||
const entity = super.createEntity(name);
|
||||
this.activeEntities.add(entity);
|
||||
return entity;
|
||||
}
|
||||
|
||||
destroyEntity(entity: Entity) {
|
||||
this.activeEntities.delete(entity);
|
||||
super.destroyEntity(entity);
|
||||
}
|
||||
|
||||
// 暂时禁用实体而不销毁
|
||||
deactivateEntity(entity: Entity) {
|
||||
if (this.activeEntities.has(entity)) {
|
||||
this.activeEntities.delete(entity);
|
||||
this.inactiveEntities.add(entity);
|
||||
entity.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 重新激活实体
|
||||
activateEntity(entity: Entity) {
|
||||
if (this.inactiveEntities.has(entity)) {
|
||||
this.inactiveEntities.delete(entity);
|
||||
this.activeEntities.add(entity);
|
||||
entity.enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 只更新活跃实体
|
||||
update() {
|
||||
for (const entity of this.activeEntities) {
|
||||
if (entity.enabled) {
|
||||
entity.update();
|
||||
}
|
||||
}
|
||||
|
||||
this.updateEntitySystems();
|
||||
}
|
||||
|
||||
// 批量操作
|
||||
deactivateAllEnemies() {
|
||||
const enemies = this.findEntitiesWithTag(EntityTags.ENEMY);
|
||||
enemies.forEach(enemy => this.deactivateEntity(enemy));
|
||||
}
|
||||
|
||||
activateAllEnemies() {
|
||||
const enemies = Array.from(this.inactiveEntities)
|
||||
.filter(entity => entity.hasTag(EntityTags.ENEMY));
|
||||
enemies.forEach(enemy => this.activateEntity(enemy));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 系统性能监控
|
||||
|
||||
```typescript
|
||||
class PerformanceMonitoredScene extends Scene {
|
||||
private systemPerformance: Map<string, number[]> = new Map();
|
||||
|
||||
addEntityProcessor<T extends EntitySystem>(system: T): T {
|
||||
const wrappedSystem = this.wrapSystemWithMonitoring(system);
|
||||
return super.addEntityProcessor(wrappedSystem);
|
||||
}
|
||||
|
||||
private wrapSystemWithMonitoring<T extends EntitySystem>(system: T): T {
|
||||
const originalUpdate = system.update.bind(system);
|
||||
const systemName = system.constructor.name;
|
||||
|
||||
system.update = () => {
|
||||
const startTime = performance.now();
|
||||
originalUpdate();
|
||||
const endTime = performance.now();
|
||||
|
||||
this.recordSystemPerformance(systemName, endTime - startTime);
|
||||
};
|
||||
|
||||
return system;
|
||||
}
|
||||
|
||||
private recordSystemPerformance(systemName: string, duration: number) {
|
||||
if (!this.systemPerformance.has(systemName)) {
|
||||
this.systemPerformance.set(systemName, []);
|
||||
}
|
||||
|
||||
const records = this.systemPerformance.get(systemName)!;
|
||||
records.push(duration);
|
||||
|
||||
// 只保留最近100次记录
|
||||
if (records.length > 100) {
|
||||
records.shift();
|
||||
}
|
||||
}
|
||||
|
||||
getPerformanceReport(): any {
|
||||
const report = {};
|
||||
|
||||
this.systemPerformance.forEach((durations, systemName) => {
|
||||
const avgDuration = durations.reduce((a, b) => a + b, 0) / durations.length;
|
||||
const maxDuration = Math.max(...durations);
|
||||
const minDuration = Math.min(...durations);
|
||||
|
||||
report[systemName] = {
|
||||
average: avgDuration.toFixed(2) + 'ms',
|
||||
max: maxDuration.toFixed(2) + 'ms',
|
||||
min: minDuration.toFixed(2) + 'ms',
|
||||
samples: durations.length
|
||||
};
|
||||
});
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
// 定期输出性能报告
|
||||
private performanceReportTimer() {
|
||||
Core.schedule(5.0, true, this, () => {
|
||||
console.table(this.getPerformanceReport());
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 常见问题和最佳实践
|
||||
|
||||
### Q: 何时创建新场景?
|
||||
|
||||
A:
|
||||
- 游戏的不同阶段(菜单、游戏、设置)
|
||||
- 不同的关卡
|
||||
- 需要完全不同系统配置的情况
|
||||
- 需要清理大量实体时
|
||||
|
||||
### Q: 场景切换时如何保持数据?
|
||||
|
||||
A:
|
||||
1. 使用场景构造函数传递数据
|
||||
2. 使用全局数据管理器
|
||||
3. 使用本地存储进行持久化
|
||||
|
||||
### Q: 如何优化场景性能?
|
||||
|
||||
A:
|
||||
1. 合理使用实体的启用/禁用
|
||||
2. 监控系统性能
|
||||
3. 批量操作实体
|
||||
4. 使用对象池减少垃圾回收
|
||||
|
||||
### Q: 多个场景可以同时存在吗?
|
||||
|
||||
A: 框架同时只支持一个活跃场景,但可以通过场景栈实现多场景管理(如暂停菜单)。
|
||||
|
||||
通过合理使用场景系统,你可以构建出结构清晰、性能优良的游戏架构!
|
||||
@@ -1,515 +0,0 @@
|
||||
# 系统(System)详解指南
|
||||
|
||||
系统是ECS架构中的"S",负责处理拥有特定组件的实体。本指南详细介绍框架中的各种系统类型及其使用方法。
|
||||
|
||||
## 系统基础概念
|
||||
|
||||
### 什么是系统?
|
||||
|
||||
系统是处理游戏逻辑的地方,它们:
|
||||
- 🎯 **专注单一职责** - 每个系统只处理一种类型的逻辑
|
||||
- 🔄 **自动执行** - 系统会在每帧自动被调用
|
||||
- 📊 **基于组件过滤** - 只处理包含特定组件的实体
|
||||
- ⚡ **高性能** - 利用ECS的数据局部性优势
|
||||
|
||||
### 系统的工作原理
|
||||
|
||||
```typescript
|
||||
// 系统的基本工作流程:
|
||||
// 1. 查询符合条件的实体
|
||||
// 2. 遍历这些实体
|
||||
// 3. 读取/修改实体的组件数据
|
||||
// 4. 执行游戏逻辑
|
||||
|
||||
class MovementSystem extends EntitySystem {
|
||||
process(entities: Entity[]) {
|
||||
for (const entity of entities) {
|
||||
const position = entity.getComponent(PositionComponent);
|
||||
const velocity = entity.getComponent(VelocityComponent);
|
||||
|
||||
// 更新位置 = 当前位置 + 速度 * 时间
|
||||
position.x += velocity.x * Time.deltaTime;
|
||||
position.y += velocity.y * Time.deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 系统类型详解
|
||||
|
||||
### 1. EntitySystem - 基础系统
|
||||
|
||||
最常用的系统类型,每帧处理所有符合条件的实体。
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, Entity, Matcher } from '@esengine/ecs-framework';
|
||||
|
||||
class HealthSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// 使用Matcher指定需要的组件
|
||||
super(Matcher.empty().all(HealthComponent));
|
||||
}
|
||||
|
||||
// 主要处理逻辑
|
||||
protected process(entities: Entity[]) {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(HealthComponent);
|
||||
|
||||
// 处理生命值逻辑
|
||||
if (health.currentHealth <= 0) {
|
||||
this.handleDeath(entity);
|
||||
} else if (health.currentHealth < health.maxHealth) {
|
||||
this.handleRegeneration(health);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleDeath(entity: Entity) {
|
||||
// 添加死亡标记
|
||||
entity.addComponent(new DeadComponent());
|
||||
|
||||
// 触发死亡事件
|
||||
Core.emitter.emit('entity:died', {
|
||||
entityId: entity.id,
|
||||
entityName: entity.name
|
||||
});
|
||||
}
|
||||
|
||||
private handleRegeneration(health: HealthComponent) {
|
||||
// 缓慢恢复生命值
|
||||
health.currentHealth += health.regenRate * Time.deltaTime;
|
||||
health.currentHealth = Math.min(health.currentHealth, health.maxHealth);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**适用场景:**
|
||||
- 移动系统
|
||||
- 渲染系统
|
||||
- 碰撞检测系统
|
||||
- AI系统
|
||||
|
||||
### 2. ProcessingSystem - 简化处理系统
|
||||
|
||||
不需要处理具体实体,主要用于执行全局逻辑或不依赖特定实体的系统处理。
|
||||
|
||||
```typescript
|
||||
import { ProcessingSystem, Matcher } from '@esengine/ecs-framework';
|
||||
|
||||
class GameLogicSystem extends ProcessingSystem {
|
||||
constructor() {
|
||||
// ProcessingSystem可以不指定Matcher,或使用空Matcher
|
||||
super(Matcher.empty());
|
||||
}
|
||||
|
||||
// 处理系统逻辑(每帧执行)
|
||||
public processSystem() {
|
||||
// 执行全局游戏逻辑
|
||||
this.updateGameState();
|
||||
this.checkWinConditions();
|
||||
this.updateUI();
|
||||
}
|
||||
|
||||
private updateGameState() {
|
||||
// 更新游戏状态逻辑
|
||||
console.log("更新游戏状态");
|
||||
}
|
||||
|
||||
private checkWinConditions() {
|
||||
// 检查胜利条件
|
||||
const players = this.scene.findEntitiesByTag(EntityTags.PLAYER);
|
||||
const enemies = this.scene.findEntitiesByTag(EntityTags.ENEMY);
|
||||
|
||||
if (enemies.length === 0) {
|
||||
this.triggerVictory();
|
||||
} else if (players.length === 0) {
|
||||
this.triggerGameOver();
|
||||
}
|
||||
}
|
||||
|
||||
private updateUI() {
|
||||
// 更新UI显示
|
||||
const gameTime = Time.totalTime;
|
||||
console.log(`游戏时间: ${gameTime.toFixed(1)}秒`);
|
||||
}
|
||||
}
|
||||
|
||||
private processIdle(entity: Entity, ai: AIComponent) {
|
||||
ai.idleTimer += Time.deltaTime;
|
||||
|
||||
if (ai.idleTimer >= ai.idleTime) {
|
||||
ai.state = AIState.PATROL;
|
||||
ai.idleTimer = 0;
|
||||
}
|
||||
|
||||
// 检查附近是否有玩家
|
||||
const nearbyPlayer = this.findNearbyPlayer(entity, ai.detectionRange);
|
||||
if (nearbyPlayer) {
|
||||
ai.state = AIState.CHASE;
|
||||
ai.target = nearbyPlayer;
|
||||
}
|
||||
}
|
||||
|
||||
private processPatrol(entity: Entity, ai: AIComponent, position: PositionComponent) {
|
||||
// 简单的来回巡逻
|
||||
if (!ai.patrolTarget) {
|
||||
ai.patrolTarget = this.getNextPatrolPoint(ai);
|
||||
}
|
||||
|
||||
const direction = ai.patrolTarget.subtract(position);
|
||||
const distance = direction.length();
|
||||
|
||||
if (distance < 10) {
|
||||
ai.patrolTarget = this.getNextPatrolPoint(ai);
|
||||
} else {
|
||||
const normalized = direction.normalize();
|
||||
position.x += normalized.x * ai.moveSpeed * Time.deltaTime;
|
||||
position.y += normalized.y * ai.moveSpeed * Time.deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**适用场景:**
|
||||
- 全局游戏逻辑系统
|
||||
- 胜负判断系统
|
||||
- UI更新系统
|
||||
- 不依赖特定实体的处理
|
||||
|
||||
### 3. IntervalSystem - 间隔执行系统
|
||||
|
||||
不是每帧都执行,而是按指定间隔执行的系统,适合不需要高频更新的逻辑。
|
||||
|
||||
```typescript
|
||||
import { IntervalSystem, Matcher } from '@esengine/ecs-framework';
|
||||
|
||||
class SpawnSystem extends IntervalSystem {
|
||||
private spawnPoints: { x: number; y: number }[] = [
|
||||
{ x: 100, y: 100 },
|
||||
{ x: 700, y: 100 },
|
||||
{ x: 400, y: 500 }
|
||||
];
|
||||
|
||||
// 每2秒执行一次
|
||||
constructor() {
|
||||
// IntervalSystem需要指定Matcher和间隔时间
|
||||
super(Matcher.empty().all(SpawnerComponent), 2.0);
|
||||
}
|
||||
|
||||
// 间隔执行的逻辑(重写process方法)
|
||||
protected process(entities: Entity[]) {
|
||||
// entities就是匹配的生成器实体
|
||||
|
||||
for (const spawner of entities) {
|
||||
const spawnerComp = spawner.getComponent(SpawnerComponent);
|
||||
|
||||
if (this.shouldSpawn(spawnerComp)) {
|
||||
this.spawnEnemy(spawner, spawnerComp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private shouldSpawn(spawner: SpawnerComponent): boolean {
|
||||
// 检查是否应该生成
|
||||
const currentEnemyCount = this.getCurrentEnemyCount();
|
||||
return currentEnemyCount < spawner.maxEnemies &&
|
||||
Math.random() < spawner.spawnChance;
|
||||
}
|
||||
|
||||
private spawnEnemy(spawnerEntity: Entity, spawner: SpawnerComponent) {
|
||||
// 随机选择生成点
|
||||
const spawnPoint = this.spawnPoints[
|
||||
Math.floor(Math.random() * this.spawnPoints.length)
|
||||
];
|
||||
|
||||
// 创建敌人实体
|
||||
const enemy = this.scene.createEntity("Enemy");
|
||||
enemy.addComponent(new PositionComponent(spawnPoint.x, spawnPoint.y));
|
||||
enemy.addComponent(new HealthComponent(50));
|
||||
enemy.addComponent(new AIComponent());
|
||||
enemy.addComponent(new VelocityComponent(0, 0));
|
||||
enemy.tag = EntityTags.ENEMY;
|
||||
|
||||
// 更新生成器统计
|
||||
spawner.spawnedCount++;
|
||||
spawner.lastSpawnTime = Time.totalTime;
|
||||
|
||||
// 发送生成事件
|
||||
Core.emitter.emit('enemy:spawned', {
|
||||
enemyId: enemy.id,
|
||||
spawnPoint: spawnPoint,
|
||||
spawnerEntity: spawnerEntity.id
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**适用场景:**
|
||||
- 敌人生成系统
|
||||
- 自动保存系统
|
||||
- 资源回收系统
|
||||
- 定期数据同步
|
||||
|
||||
### 4. PassiveSystem - 被动系统
|
||||
|
||||
不主动遍历实体,而是响应事件的系统。
|
||||
|
||||
```typescript
|
||||
import { PassiveSystem, Matcher, Core } from '@esengine/ecs-framework';
|
||||
|
||||
class ScoreSystem extends PassiveSystem {
|
||||
private score: number = 0;
|
||||
private multiplier: number = 1;
|
||||
private combo: number = 0;
|
||||
|
||||
constructor() {
|
||||
// PassiveSystem也需要Matcher,即使不使用
|
||||
super(Matcher.empty());
|
||||
}
|
||||
|
||||
initialize() {
|
||||
super.initialize();
|
||||
|
||||
// 监听游戏事件(使用Core.emitter)
|
||||
Core.emitter.addObserver('enemy:killed', this.onEnemyKilled, this);
|
||||
Core.emitter.addObserver('item:collected', this.onItemCollected, this);
|
||||
Core.emitter.addObserver('combo:broken', this.onComboBroken, this);
|
||||
}
|
||||
|
||||
// PassiveSystem被移除时清理
|
||||
destroy() {
|
||||
// 清理事件监听
|
||||
Core.emitter.removeObserver('enemy:killed', this.onEnemyKilled, this);
|
||||
Core.emitter.removeObserver('item:collected', this.onItemCollected, this);
|
||||
Core.emitter.removeObserver('combo:broken', this.onComboBroken, this);
|
||||
}
|
||||
|
||||
private onEnemyKilled(data: { enemyType: string; position: { x: number; y: number } }) {
|
||||
// 根据敌人类型给分
|
||||
let baseScore = this.getScoreForEnemyType(data.enemyType);
|
||||
|
||||
// 连击奖励
|
||||
this.combo++;
|
||||
if (this.combo > 3) {
|
||||
this.multiplier = Math.min(this.combo * 0.1, 3.0); // 最多3倍
|
||||
}
|
||||
|
||||
const finalScore = Math.floor(baseScore * this.multiplier);
|
||||
this.addScore(finalScore);
|
||||
|
||||
// 显示分数奖励
|
||||
this.showScorePopup(data.position, finalScore);
|
||||
}
|
||||
|
||||
private addScore(points: number) {
|
||||
this.score += points;
|
||||
|
||||
// 发送分数更新事件
|
||||
Core.emitter.emit('score:updated', {
|
||||
score: this.score,
|
||||
points: points,
|
||||
multiplier: this.multiplier,
|
||||
combo: this.combo
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**适用场景:**
|
||||
- 分数统计系统
|
||||
- 音效播放系统
|
||||
- UI更新系统
|
||||
- 成就系统
|
||||
|
||||
## 系统管理和注册
|
||||
|
||||
### 在场景中添加系统
|
||||
|
||||
```typescript
|
||||
import { Scene, Core } from '@esengine/ecs-framework';
|
||||
|
||||
const scene = new Scene();
|
||||
|
||||
// 添加各种系统(使用addEntityProcessor方法)
|
||||
scene.addEntityProcessor(new MovementSystem());
|
||||
scene.addEntityProcessor(new GameLogicSystem());
|
||||
scene.addEntityProcessor(new SpawnSystem());
|
||||
scene.addEntityProcessor(new ScoreSystem());
|
||||
|
||||
// 设置系统的执行优先级
|
||||
const movementSystem = scene.getEntityProcessor(MovementSystem);
|
||||
if (movementSystem) {
|
||||
movementSystem.updateOrder = 10; // 数值越小越先执行
|
||||
}
|
||||
|
||||
const renderSystem = scene.getEntityProcessor(RenderSystem);
|
||||
if (renderSystem) {
|
||||
renderSystem.updateOrder = 100; // 渲染系统最后执行
|
||||
}
|
||||
|
||||
// 设置为当前场景
|
||||
Core.scene = scene;
|
||||
```
|
||||
|
||||
### 系统的启用和禁用
|
||||
|
||||
```typescript
|
||||
// 暂时禁用某个系统
|
||||
const gameLogicSystem = scene.getEntityProcessor(GameLogicSystem);
|
||||
if (gameLogicSystem) {
|
||||
gameLogicSystem.enabled = false;
|
||||
}
|
||||
|
||||
// 重新启用
|
||||
if (gameLogicSystem) {
|
||||
gameLogicSystem.enabled = true;
|
||||
}
|
||||
|
||||
// 移除系统
|
||||
scene.removeEntityProcessor(gameLogicSystem);
|
||||
```
|
||||
|
||||
## 系统设计最佳实践
|
||||
|
||||
### 1. 单一职责原则
|
||||
|
||||
```typescript
|
||||
// ✅ 好的设计:每个系统只负责一件事
|
||||
class MovementSystem extends EntitySystem {
|
||||
// 只负责移动
|
||||
}
|
||||
|
||||
class CollisionSystem extends EntitySystem {
|
||||
// 只负责碰撞检测
|
||||
}
|
||||
|
||||
class RenderSystem extends EntitySystem {
|
||||
// 只负责渲染
|
||||
}
|
||||
|
||||
// ❌ 不好的设计:一个系统做太多事情
|
||||
class GameplaySystem extends EntitySystem {
|
||||
// 既处理移动,又处理碰撞,还处理渲染...
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 合理的系统执行顺序
|
||||
|
||||
```typescript
|
||||
// 设置合理的执行顺序
|
||||
scene.addEntityProcessor(new InputSystem()).updateOrder = 0; // 输入最先
|
||||
scene.addEntityProcessor(new GameLogicSystem()).updateOrder = 10; // 游戏逻辑
|
||||
scene.addEntityProcessor(new MovementSystem()).updateOrder = 20; // 移动计算
|
||||
scene.addEntityProcessor(new CollisionSystem()).updateOrder = 30; // 碰撞检测
|
||||
scene.addEntityProcessor(new HealthSystem()).updateOrder = 40; // 生命值处理
|
||||
scene.addEntityProcessor(new RenderSystem()).updateOrder = 100; // 渲染最后
|
||||
```
|
||||
|
||||
### 3. 系统间通信
|
||||
|
||||
```typescript
|
||||
// 使用事件进行系统间通信
|
||||
class CollisionSystem extends EntitySystem {
|
||||
process(entities: Entity[]) {
|
||||
// ... 碰撞检测逻辑
|
||||
|
||||
if (collision) {
|
||||
// 发送碰撞事件,让其他系统响应
|
||||
Core.emitter.emit('collision:detected', {
|
||||
entity1: collider1,
|
||||
entity2: collider2,
|
||||
collisionPoint: point
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class HealthSystem extends PassiveSystem {
|
||||
onAddedToScene() {
|
||||
// 监听碰撞事件
|
||||
Core.emitter.addObserver('collision:detected', this.onCollision, this);
|
||||
}
|
||||
|
||||
private onCollision(data: CollisionEventData) {
|
||||
// 处理碰撞伤害
|
||||
if (data.entity1.hasComponent(HealthComponent)) {
|
||||
const health = data.entity1.getComponent(HealthComponent);
|
||||
health.takeDamage(10);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 性能优化
|
||||
|
||||
```typescript
|
||||
class OptimizedMovementSystem extends EntitySystem {
|
||||
private lastUpdateTime: number = 0;
|
||||
private readonly UPDATE_INTERVAL = 16; // 60FPS
|
||||
|
||||
process(entities: Entity[]) {
|
||||
const currentTime = Time.totalTime;
|
||||
|
||||
// 限制更新频率
|
||||
if (currentTime - this.lastUpdateTime < this.UPDATE_INTERVAL) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 批量处理
|
||||
this.processBatch(entities);
|
||||
|
||||
this.lastUpdateTime = currentTime;
|
||||
}
|
||||
|
||||
private processBatch(entities: Entity[]) {
|
||||
// 使用for循环而不是forEach,性能更好
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
const entity = entities[i];
|
||||
// 处理逻辑...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 系统的执行顺序重要吗?
|
||||
|
||||
A: 非常重要!合理的执行顺序可以避免逻辑错误:
|
||||
|
||||
```typescript
|
||||
// 正确顺序:
|
||||
// 1. 输入系统(收集玩家输入)
|
||||
// 2. AI系统(敌人决策)
|
||||
// 3. 移动系统(更新位置)
|
||||
// 4. 碰撞系统(检测碰撞)
|
||||
// 5. 渲染系统(显示画面)
|
||||
```
|
||||
|
||||
### Q: 什么时候使用哪种系统类型?
|
||||
|
||||
A:
|
||||
- **EntitySystem** - 大部分游戏逻辑(移动、AI、碰撞等)
|
||||
- **ProcessingSystem** - 复杂的单实体处理(复杂AI、粒子系统)
|
||||
- **IntervalSystem** - 不需要每帧执行的逻辑(生成器、自动保存)
|
||||
- **PassiveSystem** - 事件响应系统(分数、音效、UI更新)
|
||||
|
||||
### Q: 系统可以访问其他系统吗?
|
||||
|
||||
A: 不建议直接访问。推荐使用事件系统进行系统间通信,保持松耦合。
|
||||
|
||||
### Q: 如何调试系统性能?
|
||||
|
||||
A: 使用框架内置的性能监控:
|
||||
|
||||
```typescript
|
||||
const monitor = PerformanceMonitor.instance;
|
||||
monitor.startFrame('MovementSystem');
|
||||
// 系统逻辑...
|
||||
monitor.endFrame('MovementSystem');
|
||||
|
||||
// 查看性能报告
|
||||
console.log(monitor.getReport());
|
||||
```
|
||||
|
||||
通过合理使用这些系统类型,你可以构建出高性能、易维护的游戏逻辑!
|
||||
@@ -1,651 +0,0 @@
|
||||
# 定时器系统使用指南
|
||||
|
||||
定时器系统是游戏开发中的重要工具,用于处理延迟执行、重复任务、倒计时等功能。本指南详细介绍如何使用ECS框架的定时器系统。
|
||||
|
||||
## 定时器基础概念
|
||||
|
||||
### 什么是定时器?
|
||||
|
||||
定时器允许你:
|
||||
- ⏰ **延迟执行** - 在指定时间后执行某个操作
|
||||
- 🔄 **重复执行** - 定期重复执行某个操作
|
||||
- 🛑 **取消执行** - 在执行前取消定时器
|
||||
- 🎯 **精确控制** - 精确控制执行时机
|
||||
|
||||
### 定时器的优势
|
||||
|
||||
相比直接在游戏循环中计时,定时器系统提供:
|
||||
- 🧹 **自动管理** - 自动处理定时器的生命周期
|
||||
- 🎮 **游戏时间控制** - 支持游戏暂停、时间缩放
|
||||
- 💾 **内存优化** - 自动回收完成的定时器
|
||||
- 🔧 **易于使用** - 简单的API调用
|
||||
|
||||
## 基础定时器使用
|
||||
|
||||
### 1. 简单延迟执行
|
||||
|
||||
```typescript
|
||||
import { Core, Timer } from '@esengine/ecs-framework';
|
||||
|
||||
// 3秒后执行一次
|
||||
Core.schedule(3.0, false, this, (timer) => {
|
||||
console.log("3秒钟到了!");
|
||||
});
|
||||
|
||||
// 实际游戏例子:延迟显示提示
|
||||
class GameTutorial {
|
||||
startTutorial() {
|
||||
// 2秒后显示第一个提示
|
||||
Core.schedule(2.0, false, this, () => {
|
||||
this.showTip("欢迎来到游戏世界!");
|
||||
});
|
||||
|
||||
// 5秒后显示移动提示
|
||||
Core.schedule(5.0, false, this, () => {
|
||||
this.showTip("使用WASD键移动角色");
|
||||
});
|
||||
|
||||
// 8秒后显示攻击提示
|
||||
Core.schedule(8.0, false, this, () => {
|
||||
this.showTip("按空格键攻击敌人");
|
||||
});
|
||||
}
|
||||
|
||||
private showTip(message: string) {
|
||||
// 显示提示的逻辑
|
||||
console.log(`提示: ${message}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 重复执行
|
||||
|
||||
```typescript
|
||||
// 每1秒执行一次,持续执行
|
||||
const repeatTimer = Core.schedule(1.0, true, this, (timer) => {
|
||||
console.log("每秒执行一次");
|
||||
});
|
||||
|
||||
// 实际游戏例子:生命值恢复
|
||||
class HealthRegeneration {
|
||||
private regenTimer: ITimer;
|
||||
|
||||
startRegeneration(entity: Entity) {
|
||||
const health = entity.getComponent(HealthComponent);
|
||||
|
||||
// 每2秒恢复5点生命值
|
||||
this.regenTimer = Core.schedule(2.0, true, this, () => {
|
||||
if (health.currentHealth < health.maxHealth) {
|
||||
health.currentHealth += 5;
|
||||
health.currentHealth = Math.min(health.currentHealth, health.maxHealth);
|
||||
|
||||
console.log(`生命值恢复:${health.currentHealth}/${health.maxHealth}`);
|
||||
|
||||
// 满血时停止恢复
|
||||
if (health.currentHealth >= health.maxHealth) {
|
||||
this.stopRegeneration();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
stopRegeneration() {
|
||||
if (this.regenTimer) {
|
||||
this.regenTimer.stop();
|
||||
this.regenTimer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 获取定时器引用进行控制
|
||||
|
||||
```typescript
|
||||
import { ITimer } from '@esengine/ecs-framework';
|
||||
|
||||
class BombTimer {
|
||||
private bombTimer: ITimer;
|
||||
private explosionTime: number = 5.0;
|
||||
|
||||
startBomb(position: { x: number; y: number }) {
|
||||
console.log("炸弹已放置!5秒后爆炸...");
|
||||
|
||||
// 创建定时器并保存引用
|
||||
this.bombTimer = Core.schedule(this.explosionTime, false, this, () => {
|
||||
this.explode(position);
|
||||
});
|
||||
}
|
||||
|
||||
defuseBomb() {
|
||||
if (this.bombTimer && !this.bombTimer.isDone) {
|
||||
// 拆除炸弹
|
||||
this.bombTimer.stop();
|
||||
console.log("炸弹已被拆除!");
|
||||
}
|
||||
}
|
||||
|
||||
getRemainingTime(): number {
|
||||
if (this.bombTimer && !this.bombTimer.isDone) {
|
||||
return this.explosionTime - this.bombTimer.elapsedTime;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private explode(position: { x: number; y: number }) {
|
||||
console.log("💥 炸弹爆炸了!");
|
||||
// 爆炸效果逻辑...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 高级定时器功能
|
||||
|
||||
### 1. 定时器链 - 顺序执行多个任务
|
||||
|
||||
```typescript
|
||||
class CutsceneManager {
|
||||
playCutscene() {
|
||||
// 第一个镜头:2秒
|
||||
Core.schedule(2.0, false, this, () => {
|
||||
this.showScene("开场镜头");
|
||||
|
||||
// 第二个镜头:3秒后
|
||||
Core.schedule(3.0, false, this, () => {
|
||||
this.showScene("角色登场");
|
||||
|
||||
// 第三个镜头:2秒后
|
||||
Core.schedule(2.0, false, this, () => {
|
||||
this.showScene("背景介绍");
|
||||
|
||||
// 结束:1秒后
|
||||
Core.schedule(1.0, false, this, () => {
|
||||
this.endCutscene();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private showScene(sceneName: string) {
|
||||
console.log(`播放场景: ${sceneName}`);
|
||||
}
|
||||
|
||||
private endCutscene() {
|
||||
console.log("过场动画结束,开始游戏!");
|
||||
}
|
||||
}
|
||||
|
||||
// 更优雅的链式写法
|
||||
class ImprovedCutsceneManager {
|
||||
playCutscene() {
|
||||
this.scheduleSequence([
|
||||
{ delay: 2.0, action: () => this.showScene("开场镜头") },
|
||||
{ delay: 3.0, action: () => this.showScene("角色登场") },
|
||||
{ delay: 2.0, action: () => this.showScene("背景介绍") },
|
||||
{ delay: 1.0, action: () => this.endCutscene() }
|
||||
]);
|
||||
}
|
||||
|
||||
private scheduleSequence(sequence: Array<{delay: number, action: () => void}>) {
|
||||
let currentDelay = 0;
|
||||
|
||||
sequence.forEach(step => {
|
||||
currentDelay += step.delay;
|
||||
Core.schedule(currentDelay, false, this, step.action);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 条件定时器 - 满足条件时执行
|
||||
|
||||
```typescript
|
||||
class ConditionalTimer {
|
||||
waitForCondition(
|
||||
condition: () => boolean,
|
||||
action: () => void,
|
||||
checkInterval: number = 0.1,
|
||||
timeout: number = 10.0
|
||||
) {
|
||||
let timeElapsed = 0;
|
||||
|
||||
const checkTimer = Core.schedule(checkInterval, true, this, () => {
|
||||
timeElapsed += checkInterval;
|
||||
|
||||
if (condition()) {
|
||||
// 条件满足,执行动作并停止检查
|
||||
checkTimer.stop();
|
||||
action();
|
||||
} else if (timeElapsed >= timeout) {
|
||||
// 超时,停止检查
|
||||
checkTimer.stop();
|
||||
console.log("等待条件超时");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 使用例子
|
||||
class WaitForPlayerExample {
|
||||
waitForPlayerToReachGoal() {
|
||||
const player = this.getPlayer();
|
||||
const goalPosition = { x: 500, y: 300 };
|
||||
|
||||
this.waitForCondition(
|
||||
// 条件:玩家到达目标位置
|
||||
() => {
|
||||
const playerPos = player.getComponent(PositionComponent);
|
||||
return playerPos.distanceTo(goalPosition) < 50;
|
||||
},
|
||||
// 动作:触发下一关
|
||||
() => {
|
||||
console.log("玩家到达目标!开始下一关");
|
||||
this.loadNextLevel();
|
||||
},
|
||||
0.1, // 每0.1秒检查一次
|
||||
30.0 // 30秒后超时
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 可暂停的定时器
|
||||
|
||||
```typescript
|
||||
class PausableTimer {
|
||||
private timers: ITimer[] = [];
|
||||
private isPaused: boolean = false;
|
||||
|
||||
schedule(delay: number, repeat: boolean, callback: () => void): ITimer {
|
||||
const timer = Core.schedule(delay, repeat, this, callback);
|
||||
this.timers.push(timer);
|
||||
return timer;
|
||||
}
|
||||
|
||||
pauseAll() {
|
||||
this.isPaused = true;
|
||||
this.timers.forEach(timer => {
|
||||
if (!timer.isDone) {
|
||||
timer.stop();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
resumeAll() {
|
||||
if (!this.isPaused) return;
|
||||
|
||||
this.isPaused = false;
|
||||
// 重新启动所有未完成的定时器
|
||||
// 注意:这是简化实现,实际需要保存剩余时间
|
||||
this.timers = this.timers.filter(timer => !timer.isDone);
|
||||
}
|
||||
|
||||
clearAll() {
|
||||
this.timers.forEach(timer => timer.stop());
|
||||
this.timers = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 游戏暂停系统
|
||||
class GamePauseSystem {
|
||||
private gameTimers: PausableTimer = new PausableTimer();
|
||||
private isGamePaused: boolean = false;
|
||||
|
||||
pauseGame() {
|
||||
if (this.isGamePaused) return;
|
||||
|
||||
this.isGamePaused = true;
|
||||
this.gameTimers.pauseAll();
|
||||
|
||||
// 显示暂停菜单
|
||||
this.showPauseMenu();
|
||||
}
|
||||
|
||||
resumeGame() {
|
||||
if (!this.isGamePaused) return;
|
||||
|
||||
this.isGamePaused = false;
|
||||
this.gameTimers.resumeAll();
|
||||
|
||||
// 隐藏暂停菜单
|
||||
this.hidePauseMenu();
|
||||
}
|
||||
|
||||
scheduleGameTimer(delay: number, repeat: boolean, callback: () => void) {
|
||||
return this.gameTimers.schedule(delay, repeat, callback);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 实际游戏应用示例
|
||||
|
||||
### 1. Buff/Debuff 系统
|
||||
|
||||
```typescript
|
||||
class BuffSystem {
|
||||
applyBuff(entity: Entity, buffType: string, duration: number) {
|
||||
const buff = new BuffComponent(buffType, duration);
|
||||
entity.addComponent(buff);
|
||||
|
||||
// 应用Buff效果
|
||||
this.applyBuffEffect(entity, buffType);
|
||||
|
||||
// 设置定时器移除Buff
|
||||
Core.schedule(duration, false, this, () => {
|
||||
if (!entity.isDestroyed && entity.hasComponent(BuffComponent)) {
|
||||
this.removeBuff(entity, buffType);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`应用了 ${buffType} Buff,持续时间 ${duration} 秒`);
|
||||
}
|
||||
|
||||
private applyBuffEffect(entity: Entity, buffType: string) {
|
||||
const stats = entity.getComponent(StatsComponent);
|
||||
|
||||
switch (buffType) {
|
||||
case 'speed_boost':
|
||||
stats.moveSpeed *= 1.5;
|
||||
break;
|
||||
case 'damage_boost':
|
||||
stats.damage *= 2.0;
|
||||
break;
|
||||
case 'invincible':
|
||||
entity.addComponent(new InvincibleComponent());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private removeBuff(entity: Entity, buffType: string) {
|
||||
const buff = entity.getComponent(BuffComponent);
|
||||
if (buff && buff.buffType === buffType) {
|
||||
entity.removeComponent(buff);
|
||||
this.removeBuffEffect(entity, buffType);
|
||||
console.log(`${buffType} Buff 已过期`);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 技能冷却系统
|
||||
|
||||
```typescript
|
||||
class SkillSystem {
|
||||
private cooldowns: Map<string, number> = new Map();
|
||||
|
||||
useSkill(player: Entity, skillName: string): boolean {
|
||||
// 检查冷却
|
||||
if (this.isOnCooldown(skillName)) {
|
||||
const remainingTime = this.getCooldownRemaining(skillName);
|
||||
console.log(`技能冷却中,还需 ${remainingTime.toFixed(1)} 秒`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 执行技能
|
||||
this.executeSkill(player, skillName);
|
||||
|
||||
// 启动冷却
|
||||
const cooldownTime = this.getSkillCooldown(skillName);
|
||||
this.startCooldown(skillName, cooldownTime);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private startCooldown(skillName: string, duration: number) {
|
||||
const endTime = Time.totalTime + duration;
|
||||
this.cooldowns.set(skillName, endTime);
|
||||
|
||||
// 设置定时器清理冷却
|
||||
Core.schedule(duration, false, this, () => {
|
||||
this.cooldowns.delete(skillName);
|
||||
console.log(`技能 ${skillName} 冷却完成!`);
|
||||
});
|
||||
}
|
||||
|
||||
private isOnCooldown(skillName: string): boolean {
|
||||
const endTime = this.cooldowns.get(skillName);
|
||||
return endTime !== undefined && Time.totalTime < endTime;
|
||||
}
|
||||
|
||||
private getCooldownRemaining(skillName: string): number {
|
||||
const endTime = this.cooldowns.get(skillName);
|
||||
return endTime ? Math.max(0, endTime - Time.totalTime) : 0;
|
||||
}
|
||||
|
||||
private executeSkill(player: Entity, skillName: string) {
|
||||
switch (skillName) {
|
||||
case 'fireball':
|
||||
this.castFireball(player);
|
||||
break;
|
||||
case 'heal':
|
||||
this.castHeal(player);
|
||||
break;
|
||||
case 'dash':
|
||||
this.performDash(player);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private getSkillCooldown(skillName: string): number {
|
||||
const cooldowns = {
|
||||
'fireball': 3.0,
|
||||
'heal': 10.0,
|
||||
'dash': 5.0
|
||||
};
|
||||
return cooldowns[skillName] || 1.0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 关卡时间限制
|
||||
|
||||
```typescript
|
||||
class LevelTimer {
|
||||
private timeLimit: number;
|
||||
private timeRemaining: number;
|
||||
private timerActive: boolean = false;
|
||||
private updateTimer: ITimer;
|
||||
|
||||
startLevel(timeLimitSeconds: number) {
|
||||
this.timeLimit = timeLimitSeconds;
|
||||
this.timeRemaining = timeLimitSeconds;
|
||||
this.timerActive = true;
|
||||
|
||||
// 每秒更新倒计时
|
||||
this.updateTimer = Core.schedule(1.0, true, this, () => {
|
||||
this.updateCountdown();
|
||||
});
|
||||
|
||||
console.log(`关卡开始!时间限制:${timeLimitSeconds} 秒`);
|
||||
}
|
||||
|
||||
private updateCountdown() {
|
||||
if (!this.timerActive) return;
|
||||
|
||||
this.timeRemaining--;
|
||||
|
||||
// 更新UI显示
|
||||
this.updateTimerUI(this.timeRemaining);
|
||||
|
||||
// 时间警告
|
||||
if (this.timeRemaining === 30) {
|
||||
console.log("⚠️ 警告:还剩30秒!");
|
||||
this.playWarningSound();
|
||||
} else if (this.timeRemaining === 10) {
|
||||
console.log("🚨 紧急:还剩10秒!");
|
||||
this.playUrgentSound();
|
||||
}
|
||||
|
||||
// 时间到
|
||||
if (this.timeRemaining <= 0) {
|
||||
this.timeUp();
|
||||
}
|
||||
}
|
||||
|
||||
private timeUp() {
|
||||
this.timerActive = false;
|
||||
this.updateTimer.stop();
|
||||
|
||||
console.log("⏰ 时间到!游戏结束");
|
||||
|
||||
// 触发游戏结束
|
||||
Core.emitter.emit('level:timeout');
|
||||
}
|
||||
|
||||
completeLevel() {
|
||||
if (this.timerActive) {
|
||||
this.timerActive = false;
|
||||
this.updateTimer.stop();
|
||||
|
||||
const completionTime = this.timeLimit - this.timeRemaining;
|
||||
console.log(`🎉 关卡完成!用时:${completionTime} 秒`);
|
||||
|
||||
// 根据剩余时间给予奖励
|
||||
this.calculateTimeBonus(this.timeRemaining);
|
||||
}
|
||||
}
|
||||
|
||||
private calculateTimeBonus(timeLeft: number) {
|
||||
const bonus = Math.floor(timeLeft * 10); // 每秒剩余10分
|
||||
if (bonus > 0) {
|
||||
console.log(`时间奖励:${bonus} 分`);
|
||||
Core.emitter.emit('score:time_bonus', { bonus });
|
||||
}
|
||||
}
|
||||
|
||||
getTimeRemaining(): number {
|
||||
return this.timeRemaining;
|
||||
}
|
||||
|
||||
getTimeRemainingFormatted(): string {
|
||||
const minutes = Math.floor(this.timeRemaining / 60);
|
||||
const seconds = this.timeRemaining % 60;
|
||||
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 定时器性能优化
|
||||
|
||||
### 1. 定时器池化
|
||||
|
||||
```typescript
|
||||
class TimerPool {
|
||||
private static instance: TimerPool;
|
||||
private timerPool: ITimer[] = [];
|
||||
|
||||
static getInstance(): TimerPool {
|
||||
if (!this.instance) {
|
||||
this.instance = new TimerPool();
|
||||
}
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
getTimer(): ITimer {
|
||||
return this.timerPool.pop() || this.createTimer();
|
||||
}
|
||||
|
||||
releaseTimer(timer: ITimer) {
|
||||
timer.stop();
|
||||
this.timerPool.push(timer);
|
||||
}
|
||||
|
||||
private createTimer(): ITimer {
|
||||
// 创建新定时器的逻辑
|
||||
return new Timer();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 批量定时器管理
|
||||
|
||||
```typescript
|
||||
class BatchTimerManager {
|
||||
private timers: Set<ITimer> = new Set();
|
||||
|
||||
scheduleMany(configs: Array<{delay: number, repeat: boolean, callback: () => void}>) {
|
||||
return configs.map(config => {
|
||||
const timer = Core.schedule(config.delay, config.repeat, this, config.callback);
|
||||
this.timers.add(timer);
|
||||
return timer;
|
||||
});
|
||||
}
|
||||
|
||||
stopAll() {
|
||||
this.timers.forEach(timer => timer.stop());
|
||||
this.timers.clear();
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
// 清理已完成的定时器
|
||||
this.timers.forEach(timer => {
|
||||
if (timer.isDone) {
|
||||
this.timers.delete(timer);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 常见问题和最佳实践
|
||||
|
||||
### Q: 定时器会自动清理吗?
|
||||
|
||||
A: 是的,完成的定时器会自动清理。但如果需要提前停止,记得调用 `timer.stop()`。
|
||||
|
||||
### Q: 定时器会受到游戏暂停影响吗?
|
||||
|
||||
A: 定时器使用游戏时间,如果实现了时间缩放功能,定时器会相应调整。
|
||||
|
||||
### Q: 如何实现精确的帧同步定时器?
|
||||
|
||||
A: 使用帧计数而不是时间:
|
||||
|
||||
```typescript
|
||||
class FrameTimer {
|
||||
private frameCount: number = 0;
|
||||
private targetFrame: number;
|
||||
|
||||
scheduleFrames(frames: number, callback: () => void) {
|
||||
this.targetFrame = this.frameCount + frames;
|
||||
|
||||
const checkFrame = () => {
|
||||
this.frameCount++;
|
||||
if (this.frameCount >= this.targetFrame) {
|
||||
callback();
|
||||
} else {
|
||||
requestAnimationFrame(checkFrame);
|
||||
}
|
||||
};
|
||||
|
||||
requestAnimationFrame(checkFrame);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Q: 如何避免定时器内存泄漏?
|
||||
|
||||
A:
|
||||
1. 及时停止不需要的定时器
|
||||
2. 在对象销毁时清理所有定时器
|
||||
3. 使用弱引用避免循环引用
|
||||
|
||||
```typescript
|
||||
class SafeTimerUser {
|
||||
private timers: ITimer[] = [];
|
||||
|
||||
scheduleTimer(delay: number, callback: () => void) {
|
||||
const timer = Core.schedule(delay, false, this, callback);
|
||||
this.timers.push(timer);
|
||||
return timer;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// 清理所有定时器
|
||||
this.timers.forEach(timer => timer.stop());
|
||||
this.timers = [];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
定时器是游戏开发中非常有用的工具,合理使用可以让你的游戏逻辑更加优雅和高效!
|
||||
@@ -1,600 +0,0 @@
|
||||
# ECS框架使用场景示例
|
||||
|
||||
本文档展示ECS框架在不同类型游戏中的具体应用案例。
|
||||
|
||||
## 目录
|
||||
|
||||
1. [小型休闲游戏](#小型休闲游戏)
|
||||
2. [中型动作游戏](#中型动作游戏)
|
||||
3. [大型策略游戏](#大型策略游戏)
|
||||
4. [MMO游戏](#mmo游戏)
|
||||
|
||||
## 小型休闲游戏
|
||||
|
||||
### 场景:简单的飞机大战游戏
|
||||
|
||||
```typescript
|
||||
import {
|
||||
Scene,
|
||||
EntityManager,
|
||||
Entity,
|
||||
Component,
|
||||
EntitySystem,
|
||||
Matcher
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
// 组件定义
|
||||
class PositionComponent extends Component {
|
||||
constructor(public x: number = 0, public y: number = 0) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
class VelocityComponent extends Component {
|
||||
constructor(public x: number = 0, public y: number = 0) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
class PlayerComponent extends Component {}
|
||||
class EnemyComponent extends Component {}
|
||||
class BulletComponent extends Component {}
|
||||
|
||||
// 游戏管理器
|
||||
class PlaneWarGame {
|
||||
private scene: Scene;
|
||||
private entityManager: EntityManager;
|
||||
|
||||
constructor() {
|
||||
this.scene = new Scene();
|
||||
this.entityManager = new EntityManager();
|
||||
this.setupGame();
|
||||
}
|
||||
|
||||
private setupGame(): void {
|
||||
// 创建玩家
|
||||
const player = this.entityManager.createEntity("Player");
|
||||
player.addComponent(new PositionComponent(400, 500));
|
||||
player.addComponent(new VelocityComponent(0, 0));
|
||||
player.addComponent(new PlayerComponent());
|
||||
player.tag = 1; // 玩家标签
|
||||
|
||||
// 创建敌人
|
||||
this.spawnEnemies(5);
|
||||
|
||||
// 添加系统
|
||||
this.scene.addEntityProcessor(new MovementSystem());
|
||||
this.scene.addEntityProcessor(new CollisionSystem());
|
||||
this.scene.addEntityProcessor(new CleanupSystem());
|
||||
}
|
||||
|
||||
private spawnEnemies(count: number): void {
|
||||
const enemies = this.scene.createEntities(count, "Enemy");
|
||||
enemies.forEach((enemy, index) => {
|
||||
enemy.addComponent(new PositionComponent(
|
||||
Math.random() * 800,
|
||||
-50
|
||||
));
|
||||
enemy.addComponent(new VelocityComponent(0, 100));
|
||||
enemy.addComponent(new EnemyComponent());
|
||||
enemy.tag = 2; // 敌人标签
|
||||
});
|
||||
}
|
||||
|
||||
public update(): void {
|
||||
this.scene.update();
|
||||
}
|
||||
}
|
||||
|
||||
// 移动系统
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(PositionComponent, VelocityComponent));
|
||||
}
|
||||
|
||||
protected process(entities: Entity[]): void {
|
||||
const movingEntities = this.scene.querySystem.queryAll(
|
||||
PositionComponent,
|
||||
VelocityComponent
|
||||
);
|
||||
|
||||
movingEntities.entities.forEach(entity => {
|
||||
const pos = entity.getComponent(PositionComponent);
|
||||
const vel = entity.getComponent(VelocityComponent);
|
||||
|
||||
pos.x += vel.x * Time.deltaTime;
|
||||
pos.y += vel.y * Time.deltaTime;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
## 中型动作游戏
|
||||
|
||||
### 场景:2D平台跳跃游戏
|
||||
|
||||
```typescript
|
||||
// 更复杂的组件
|
||||
class HealthComponent extends Component {
|
||||
constructor(
|
||||
public maxHealth: number = 100,
|
||||
public currentHealth: number = 100
|
||||
) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
class AnimationComponent extends Component {
|
||||
constructor(
|
||||
public currentAnimation: string = "idle",
|
||||
public frameIndex: number = 0,
|
||||
public frameTime: number = 0
|
||||
) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
class PhysicsComponent extends Component {
|
||||
constructor(
|
||||
public mass: number = 1,
|
||||
public friction: number = 0.8,
|
||||
public isGrounded: boolean = false
|
||||
) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
// 平台游戏管理器
|
||||
class PlatformGame {
|
||||
private scene: Scene;
|
||||
private entityManager: EntityManager;
|
||||
|
||||
constructor() {
|
||||
this.scene = new Scene();
|
||||
this.entityManager = new EntityManager();
|
||||
this.setupGame();
|
||||
}
|
||||
|
||||
private setupGame(): void {
|
||||
// 创建玩家
|
||||
this.createPlayer();
|
||||
|
||||
// 创建敌人
|
||||
this.createEnemies(10);
|
||||
|
||||
// 创建平台
|
||||
this.createPlatforms();
|
||||
|
||||
// 添加系统(按更新顺序)
|
||||
this.scene.addEntityProcessor(new InputSystem()).updateOrder = 10;
|
||||
this.scene.addEntityProcessor(new PhysicsSystem()).updateOrder = 20;
|
||||
this.scene.addEntityProcessor(new AnimationSystem()).updateOrder = 30;
|
||||
this.scene.addEntityProcessor(new CombatSystem()).updateOrder = 40;
|
||||
this.scene.addEntityProcessor(new RenderSystem()).updateOrder = 50;
|
||||
}
|
||||
|
||||
private createPlayer(): void {
|
||||
const player = this.entityManager.createEntity("Player");
|
||||
player.addComponent(new PositionComponent(100, 300));
|
||||
player.addComponent(new VelocityComponent(0, 0));
|
||||
player.addComponent(new HealthComponent(100));
|
||||
player.addComponent(new AnimationComponent("idle"));
|
||||
player.addComponent(new PhysicsComponent(1, 0.8));
|
||||
player.tag = 1;
|
||||
}
|
||||
|
||||
private createEnemies(count: number): void {
|
||||
const enemies = this.scene.createEntities(count, "Enemy");
|
||||
enemies.forEach((enemy, index) => {
|
||||
enemy.addComponent(new PositionComponent(
|
||||
200 + index * 100,
|
||||
300
|
||||
));
|
||||
enemy.addComponent(new VelocityComponent(0, 0));
|
||||
enemy.addComponent(new HealthComponent(50));
|
||||
enemy.addComponent(new AnimationComponent("patrol"));
|
||||
enemy.addComponent(new PhysicsComponent(0.8, 0.9));
|
||||
enemy.tag = 2;
|
||||
});
|
||||
}
|
||||
|
||||
private createPlatforms(): void {
|
||||
const platforms = this.scene.createEntities(5, "Platform");
|
||||
platforms.forEach((platform, index) => {
|
||||
platform.addComponent(new PositionComponent(
|
||||
index * 200,
|
||||
400 + Math.random() * 100
|
||||
));
|
||||
platform.tag = 3; // 平台标签
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
## 大型策略游戏
|
||||
|
||||
### 场景:即时战略游戏
|
||||
|
||||
```typescript
|
||||
// 策略游戏组件
|
||||
class UnitComponent extends Component {
|
||||
constructor(
|
||||
public unitType: string,
|
||||
public playerId: number,
|
||||
public level: number = 1
|
||||
) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
class AIComponent extends Component {
|
||||
constructor(
|
||||
public state: string = "idle",
|
||||
public target: Entity | null = null,
|
||||
public lastDecisionTime: number = 0
|
||||
) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
class ResourceComponent extends Component {
|
||||
constructor(
|
||||
public gold: number = 0,
|
||||
public wood: number = 0,
|
||||
public food: number = 0
|
||||
) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
// 策略游戏管理器
|
||||
class StrategyGame {
|
||||
private scene: Scene;
|
||||
private entityManager: EntityManager;
|
||||
private players: Map<number, Entity> = new Map();
|
||||
|
||||
constructor() {
|
||||
this.scene = new Scene();
|
||||
this.entityManager = new EntityManager();
|
||||
this.setupGame();
|
||||
}
|
||||
|
||||
private setupGame(): void {
|
||||
// 创建玩家
|
||||
this.createPlayers(4);
|
||||
|
||||
// 为每个玩家创建初始单位
|
||||
this.players.forEach((player, playerId) => {
|
||||
this.createInitialUnits(playerId, 10);
|
||||
});
|
||||
|
||||
// 添加系统
|
||||
this.scene.addEntityProcessor(new AISystem()).updateOrder = 10;
|
||||
this.scene.addEntityProcessor(new CombatSystem()).updateOrder = 20;
|
||||
this.scene.addEntityProcessor(new ResourceSystem()).updateOrder = 30;
|
||||
this.scene.addEntityProcessor(new UnitManagementSystem()).updateOrder = 40;
|
||||
}
|
||||
|
||||
private createPlayers(count: number): void {
|
||||
for (let i = 0; i < count; i++) {
|
||||
const player = this.entityManager.createEntity(`Player_${i}`);
|
||||
player.addComponent(new ResourceComponent(1000, 500, 100));
|
||||
player.tag = 10 + i; // 玩家标签从10开始
|
||||
this.players.set(i, player);
|
||||
}
|
||||
}
|
||||
|
||||
private createInitialUnits(playerId: number, count: number): void {
|
||||
const units = this.scene.createEntities(count, `Unit_${playerId}`);
|
||||
|
||||
units.forEach((unit, index) => {
|
||||
unit.addComponent(new PositionComponent(
|
||||
playerId * 200 + Math.random() * 100,
|
||||
playerId * 200 + Math.random() * 100
|
||||
));
|
||||
unit.addComponent(new UnitComponent("warrior", playerId));
|
||||
unit.addComponent(new HealthComponent(100));
|
||||
unit.addComponent(new AIComponent());
|
||||
unit.tag = 20 + playerId; // 单位标签
|
||||
});
|
||||
}
|
||||
|
||||
// 批量单位操作
|
||||
public createArmy(playerId: number, unitType: string, count: number): Entity[] {
|
||||
const units = this.scene.createEntities(count, `${unitType}_${playerId}`);
|
||||
|
||||
// 批量配置组件
|
||||
units.forEach(unit => {
|
||||
unit.addComponent(new UnitComponent(unitType, playerId));
|
||||
unit.addComponent(new HealthComponent(100));
|
||||
unit.addComponent(new PositionComponent(
|
||||
Math.random() * 1000,
|
||||
Math.random() * 1000
|
||||
));
|
||||
unit.tag = 20 + playerId;
|
||||
});
|
||||
|
||||
return units;
|
||||
}
|
||||
|
||||
// 查询玩家的所有单位
|
||||
public getPlayerUnits(playerId: number): Entity[] {
|
||||
return this.entityManager
|
||||
.query()
|
||||
.withAll(UnitComponent)
|
||||
.withTag(20 + playerId)
|
||||
.execute();
|
||||
}
|
||||
|
||||
// 查询特定类型的单位
|
||||
public getUnitsByType(unitType: string): Entity[] {
|
||||
return this.entityManager
|
||||
.query()
|
||||
.withAll(UnitComponent)
|
||||
.where(entity => {
|
||||
const unit = entity.getComponent(UnitComponent);
|
||||
return unit && unit.unitType === unitType;
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
|
||||
// AI系统
|
||||
class AISystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(AIComponent, UnitComponent));
|
||||
}
|
||||
|
||||
protected process(entities: Entity[]): void {
|
||||
const aiUnits = this.entityManager
|
||||
.query()
|
||||
.withAll(AIComponent, UnitComponent)
|
||||
.execute();
|
||||
|
||||
aiUnits.forEach(unit => {
|
||||
this.processAI(unit);
|
||||
});
|
||||
}
|
||||
|
||||
private processAI(unit: Entity): void {
|
||||
const ai = unit.getComponent(AIComponent);
|
||||
const unitComp = unit.getComponent(UnitComponent);
|
||||
|
||||
if (!ai || !unitComp) return;
|
||||
|
||||
// 简单AI逻辑
|
||||
switch (ai.state) {
|
||||
case "idle":
|
||||
this.findTarget(unit);
|
||||
break;
|
||||
case "attack":
|
||||
this.attackTarget(unit);
|
||||
break;
|
||||
case "move":
|
||||
this.moveToTarget(unit);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private findTarget(unit: Entity): void {
|
||||
const unitComp = unit.getComponent(UnitComponent);
|
||||
if (!unitComp) return;
|
||||
|
||||
// 查找敌方单位
|
||||
const enemies = this.entityManager
|
||||
.query()
|
||||
.withAll(UnitComponent)
|
||||
.where(entity => {
|
||||
const enemyUnit = entity.getComponent(UnitComponent);
|
||||
return enemyUnit && enemyUnit.playerId !== unitComp.playerId;
|
||||
})
|
||||
.execute();
|
||||
|
||||
if (enemies.length > 0) {
|
||||
const ai = unit.getComponent(AIComponent);
|
||||
if (ai) {
|
||||
ai.target = enemies[0];
|
||||
ai.state = "attack";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private attackTarget(unit: Entity): void {
|
||||
// 攻击逻辑
|
||||
}
|
||||
|
||||
private moveToTarget(unit: Entity): void {
|
||||
// 移动逻辑
|
||||
}
|
||||
}
|
||||
|
||||
## MMO游戏
|
||||
|
||||
### 场景:大型多人在线游戏
|
||||
|
||||
```typescript
|
||||
// MMO特有组件
|
||||
class NetworkComponent extends Component {
|
||||
constructor(
|
||||
public playerId: string,
|
||||
public isLocal: boolean = false,
|
||||
public lastSyncTime: number = 0
|
||||
) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
class InventoryComponent extends Component {
|
||||
public items: Map<string, number> = new Map();
|
||||
|
||||
addItem(itemId: string, count: number): void {
|
||||
const current = this.items.get(itemId) || 0;
|
||||
this.items.set(itemId, current + count);
|
||||
}
|
||||
}
|
||||
|
||||
class GuildComponent extends Component {
|
||||
constructor(
|
||||
public guildId: string,
|
||||
public rank: string = "member"
|
||||
) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
// MMO游戏管理器
|
||||
class MMOGame {
|
||||
private scene: Scene;
|
||||
private entityManager: EntityManager;
|
||||
private localPlayerId: string;
|
||||
|
||||
constructor(localPlayerId: string) {
|
||||
this.scene = new Scene();
|
||||
this.entityManager = new EntityManager();
|
||||
this.localPlayerId = localPlayerId;
|
||||
this.setupGame();
|
||||
}
|
||||
|
||||
private setupGame(): void {
|
||||
// 添加MMO特有系统
|
||||
this.scene.addEntityProcessor(new NetworkSyncSystem()).updateOrder = 5;
|
||||
this.scene.addEntityProcessor(new PlayerSystem()).updateOrder = 10;
|
||||
this.scene.addEntityProcessor(new NPCSystem()).updateOrder = 15;
|
||||
this.scene.addEntityProcessor(new GuildSystem()).updateOrder = 20;
|
||||
this.scene.addEntityProcessor(new InventorySystem()).updateOrder = 25;
|
||||
}
|
||||
|
||||
// 创建玩家角色
|
||||
public createPlayer(playerId: string, isLocal: boolean = false): Entity {
|
||||
const player = this.entityManager.createEntity(`Player_${playerId}`);
|
||||
player.addComponent(new PositionComponent(0, 0));
|
||||
player.addComponent(new HealthComponent(1000));
|
||||
player.addComponent(new NetworkComponent(playerId, isLocal));
|
||||
player.addComponent(new InventoryComponent());
|
||||
player.tag = isLocal ? 1 : 2; // 本地玩家标签1,远程玩家标签2
|
||||
|
||||
return player;
|
||||
}
|
||||
|
||||
// 批量创建NPC
|
||||
public createNPCs(count: number): Entity[] {
|
||||
const npcs = this.scene.createEntities(count, "NPC");
|
||||
|
||||
npcs.forEach((npc, index) => {
|
||||
npc.addComponent(new PositionComponent(
|
||||
Math.random() * 2000,
|
||||
Math.random() * 2000
|
||||
));
|
||||
npc.addComponent(new HealthComponent(500));
|
||||
npc.addComponent(new AIComponent("patrol"));
|
||||
npc.tag = 3; // NPC标签
|
||||
});
|
||||
|
||||
return npcs;
|
||||
}
|
||||
|
||||
// 查询附近的玩家
|
||||
public getNearbyPlayers(centerX: number, centerY: number, radius: number): Entity[] {
|
||||
return this.entityManager
|
||||
.query()
|
||||
.withAll(PositionComponent, NetworkComponent)
|
||||
.where(entity => {
|
||||
const pos = entity.getComponent(PositionComponent);
|
||||
if (!pos) return false;
|
||||
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(pos.x - centerX, 2) +
|
||||
Math.pow(pos.y - centerY, 2)
|
||||
);
|
||||
return distance <= radius;
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
// 查询公会成员
|
||||
public getGuildMembers(guildId: string): Entity[] {
|
||||
return this.entityManager
|
||||
.query()
|
||||
.withAll(GuildComponent, NetworkComponent)
|
||||
.where(entity => {
|
||||
const guild = entity.getComponent(GuildComponent);
|
||||
return guild && guild.guildId === guildId;
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
// 获取在线玩家统计
|
||||
public getOnlinePlayerStats(): any {
|
||||
const allPlayers = this.entityManager.getEntitiesWithComponent(NetworkComponent);
|
||||
const localPlayers = this.entityManager.getEntitiesByTag(1);
|
||||
const remotePlayers = this.entityManager.getEntitiesByTag(2);
|
||||
|
||||
return {
|
||||
total: allPlayers.length,
|
||||
local: localPlayers.length,
|
||||
remote: remotePlayers.length
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 网络同步系统
|
||||
class NetworkSyncSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(NetworkComponent));
|
||||
}
|
||||
|
||||
protected process(entities: Entity[]): void {
|
||||
const networkEntities = this.entityManager.getEntitiesWithComponent(NetworkComponent);
|
||||
|
||||
networkEntities.forEach(entity => {
|
||||
const network = entity.getComponent(NetworkComponent);
|
||||
if (!network || network.isLocal) return;
|
||||
|
||||
// 同步远程实体数据
|
||||
this.syncRemoteEntity(entity);
|
||||
});
|
||||
}
|
||||
|
||||
private syncRemoteEntity(entity: Entity): void {
|
||||
// 网络同步逻辑
|
||||
const network = entity.getComponent(NetworkComponent);
|
||||
if (!network) return;
|
||||
|
||||
const currentTime = Date.now();
|
||||
if (currentTime - network.lastSyncTime > 100) { // 100ms同步一次
|
||||
// 发送同步数据
|
||||
network.lastSyncTime = currentTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
## 性能优化建议
|
||||
|
||||
### 小型游戏(< 1000实体)
|
||||
- 使用简单的查询方法
|
||||
- 不需要复杂的优化
|
||||
- 重点关注代码可读性
|
||||
|
||||
### 中型游戏(1000-10000实体)
|
||||
- 使用标签查询优化性能
|
||||
- 实现基础的对象池
|
||||
- 缓存频繁查询的结果
|
||||
|
||||
### 大型游戏(10000-100000实体)
|
||||
- 使用时间分片处理大量实体
|
||||
- 实现空间分区优化邻近查询
|
||||
- 使用批量操作减少单次调用开销
|
||||
|
||||
### MMO游戏(100000+实体)
|
||||
- 实现分区管理,只处理相关区域的实体
|
||||
- 使用异步处理避免阻塞主线程
|
||||
- 实现智能缓存和预加载机制
|
||||
|
||||
## 总结
|
||||
|
||||
ECS框架的灵活性使其能够适应各种规模的游戏开发需求:
|
||||
|
||||
1. **小型游戏**:简单直接,快速开发
|
||||
2. **中型游戏**:平衡性能和复杂度
|
||||
3. **大型游戏**:充分利用优化特性
|
||||
4. **MMO游戏**:处理海量实体和复杂交互
|
||||
|
||||
选择合适的架构模式和优化策略,可以让ECS框架在不同场景下都发挥最佳性能。
|
||||
483
examples/core-demos/index.html
Normal file
483
examples/core-demos/index.html
Normal file
@@ -0,0 +1,483 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ECS Framework Core Demos</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
background: #0f0f23;
|
||||
color: #e0e0e0;
|
||||
overflow: hidden;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* 侧边栏 */
|
||||
.sidebar {
|
||||
width: 280px;
|
||||
background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%);
|
||||
border-right: 2px solid #0a3d62;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 4px 0 20px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 30px 20px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sidebar-header h1 {
|
||||
font-size: 1.5em;
|
||||
color: white;
|
||||
margin-bottom: 8px;
|
||||
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.sidebar-header p {
|
||||
font-size: 0.85em;
|
||||
color: rgba(255,255,255,0.9);
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.demo-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
.demo-category {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.category-title {
|
||||
padding: 12px 20px;
|
||||
color: #8892b0;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.demo-item {
|
||||
padding: 14px 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border-left: 3px solid transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.demo-item:hover {
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
border-left-color: #667eea;
|
||||
}
|
||||
|
||||
.demo-item.active {
|
||||
background: rgba(102, 126, 234, 0.2);
|
||||
border-left-color: #667eea;
|
||||
}
|
||||
|
||||
.demo-icon {
|
||||
font-size: 20px;
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.demo-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.demo-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #ccd6f6;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.demo-item.active .demo-name {
|
||||
color: #667eea;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.demo-desc {
|
||||
font-size: 11px;
|
||||
color: #8892b0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: 20px;
|
||||
border-top: 1px solid rgba(255,255,255,0.1);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.github-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 20px;
|
||||
background: rgba(102, 126, 234, 0.2);
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.github-link:hover {
|
||||
background: rgba(102, 126, 234, 0.3);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* 主内容区 */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content-header {
|
||||
padding: 25px 40px;
|
||||
background: #1a1a2e;
|
||||
border-bottom: 2px solid #0a3d62;
|
||||
}
|
||||
|
||||
.content-header h2 {
|
||||
font-size: 2em;
|
||||
color: #ccd6f6;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.content-header p {
|
||||
color: #8892b0;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.demo-canvas-container {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: #0a0a15;
|
||||
}
|
||||
|
||||
#demoCanvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 控制面板 */
|
||||
.control-panel {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
width: 320px;
|
||||
background: rgba(26, 26, 46, 0.95);
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(102, 126, 234, 0.3);
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
|
||||
backdrop-filter: blur(10px);
|
||||
max-height: calc(100% - 40px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.control-panel-header {
|
||||
padding: 15px 20px;
|
||||
background: linear-gradient(135deg, rgba(102, 126, 234, 0.2) 0%, rgba(118, 75, 162, 0.2) 100%);
|
||||
border-bottom: 1px solid rgba(102, 126, 234, 0.3);
|
||||
border-radius: 12px 12px 0 0;
|
||||
}
|
||||
|
||||
.control-panel-header h3 {
|
||||
color: #667eea;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.control-panel-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.control-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.control-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.control-section h4 {
|
||||
color: #8892b0;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px 16px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
button.secondary {
|
||||
background: rgba(102, 126, 234, 0.2);
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
button.secondary:hover {
|
||||
background: rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
button.danger {
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
}
|
||||
|
||||
button.success {
|
||||
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.input-group label {
|
||||
display: block;
|
||||
color: #8892b0;
|
||||
font-size: 12px;
|
||||
margin-bottom: 6px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.input-group input {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
background: rgba(255,255,255,0.05);
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
border-radius: 6px;
|
||||
color: #ccd6f6;
|
||||
font-size: 13px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.input-group input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
background: rgba(255,255,255,0.08);
|
||||
}
|
||||
|
||||
/* 统计信息 */
|
||||
.stats-panel {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
background: rgba(255,255,255,0.03);
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(255,255,255,0.05);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #8892b0;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
color: #667eea;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Toast通知 */
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 30px;
|
||||
right: 30px;
|
||||
background: rgba(26, 26, 46, 0.98);
|
||||
border: 1px solid #667eea;
|
||||
border-radius: 8px;
|
||||
padding: 15px 20px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
|
||||
transform: translateY(150%);
|
||||
transition: transform 0.3s ease;
|
||||
z-index: 1000;
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
.toast.show {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.toast-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.toast-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.toast-message {
|
||||
color: #ccd6f6;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(255,255,255,0.05);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(102, 126, 234, 0.5);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(102, 126, 234, 0.7);
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
.loading {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 4px solid rgba(102, 126, 234, 0.2);
|
||||
border-top-color: #667eea;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
margin-top: 15px;
|
||||
color: #8892b0;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-container">
|
||||
<!-- 侧边栏 -->
|
||||
<div class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h1>🎮 ECS Core Demos</h1>
|
||||
<p>交互式演示集合</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-list" id="demoList">
|
||||
<!-- Demo列表将通过JS动态生成 -->
|
||||
</div>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<a href="https://github.com/esengine/ecs-framework" target="_blank" class="github-link">
|
||||
<span>⭐</span>
|
||||
<span>GitHub</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<div class="main-content">
|
||||
<div class="content-header">
|
||||
<h2 id="demoTitle">选择一个演示开始</h2>
|
||||
<p id="demoDescription">从左侧菜单选择一个演示查看效果</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-canvas-container">
|
||||
<canvas id="demoCanvas"></canvas>
|
||||
|
||||
<!-- 控制面板 -->
|
||||
<div class="control-panel" id="controlPanel" style="display: none;">
|
||||
<div class="control-panel-header">
|
||||
<h3>控制面板</h3>
|
||||
</div>
|
||||
<div class="control-panel-content" id="controlPanelContent">
|
||||
<!-- 控制内容将由各个demo动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载动画 -->
|
||||
<div class="loading" id="loading" style="display: none;">
|
||||
<div class="loading-spinner"></div>
|
||||
<div class="loading-text">加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast通知 -->
|
||||
<div class="toast" id="toast">
|
||||
<div class="toast-content">
|
||||
<span class="toast-icon">✅</span>
|
||||
<span class="toast-message" id="toastMessage"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
630
examples/core-demos/package-lock.json
generated
Normal file
630
examples/core-demos/package-lock.json
generated
Normal file
@@ -0,0 +1,630 @@
|
||||
{
|
||||
"name": "ecs-core-demos",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ecs-core-demos",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@esengine/ecs-framework": "file:../../packages/core"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"../../packages/core": {
|
||||
"name": "@esengine/ecs-framework",
|
||||
"version": "2.1.51",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"msgpack-lite": "^0.1.26"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.28.3",
|
||||
"@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1",
|
||||
"@babel/plugin-transform-optional-chaining": "^7.27.1",
|
||||
"@babel/preset-env": "^7.28.3",
|
||||
"@rollup/plugin-babel": "^6.0.4",
|
||||
"@rollup/plugin-commonjs": "^28.0.3",
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/msgpack-lite": "^0.1.11",
|
||||
"@types/node": "^20.19.17",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"rimraf": "^5.0.0",
|
||||
"rollup": "^4.42.0",
|
||||
"rollup-plugin-dts": "^6.2.1",
|
||||
"ts-jest": "^29.4.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
|
||||
"integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
|
||||
"integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
|
||||
"integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
|
||||
"integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
|
||||
"integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
|
||||
"integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
|
||||
"integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
|
||||
"integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
|
||||
"integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
|
||||
"integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
|
||||
"integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
|
||||
"integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
|
||||
"integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
|
||||
"integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
|
||||
"integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
|
||||
"integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
|
||||
"integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
|
||||
"integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
|
||||
"integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
|
||||
"integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
|
||||
"integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
|
||||
"integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esengine/ecs-framework": {
|
||||
"resolved": "../../packages/core",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
|
||||
"integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/android-arm": "0.18.20",
|
||||
"@esbuild/android-arm64": "0.18.20",
|
||||
"@esbuild/android-x64": "0.18.20",
|
||||
"@esbuild/darwin-arm64": "0.18.20",
|
||||
"@esbuild/darwin-x64": "0.18.20",
|
||||
"@esbuild/freebsd-arm64": "0.18.20",
|
||||
"@esbuild/freebsd-x64": "0.18.20",
|
||||
"@esbuild/linux-arm": "0.18.20",
|
||||
"@esbuild/linux-arm64": "0.18.20",
|
||||
"@esbuild/linux-ia32": "0.18.20",
|
||||
"@esbuild/linux-loong64": "0.18.20",
|
||||
"@esbuild/linux-mips64el": "0.18.20",
|
||||
"@esbuild/linux-ppc64": "0.18.20",
|
||||
"@esbuild/linux-riscv64": "0.18.20",
|
||||
"@esbuild/linux-s390x": "0.18.20",
|
||||
"@esbuild/linux-x64": "0.18.20",
|
||||
"@esbuild/netbsd-x64": "0.18.20",
|
||||
"@esbuild/openbsd-x64": "0.18.20",
|
||||
"@esbuild/sunos-x64": "0.18.20",
|
||||
"@esbuild/win32-arm64": "0.18.20",
|
||||
"@esbuild/win32-ia32": "0.18.20",
|
||||
"@esbuild/win32-x64": "0.18.20"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "3.29.5",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz",
|
||||
"integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.18.0",
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "4.5.14",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.14.tgz",
|
||||
"integrity": "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.18.10",
|
||||
"postcss": "^8.4.27",
|
||||
"rollup": "^3.27.1"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">= 14",
|
||||
"less": "*",
|
||||
"lightningcss": "^1.21.0",
|
||||
"sass": "*",
|
||||
"stylus": "*",
|
||||
"sugarss": "*",
|
||||
"terser": "^5.4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"less": {
|
||||
"optional": true
|
||||
},
|
||||
"lightningcss": {
|
||||
"optional": true
|
||||
},
|
||||
"sass": {
|
||||
"optional": true
|
||||
},
|
||||
"stylus": {
|
||||
"optional": true
|
||||
},
|
||||
"sugarss": {
|
||||
"optional": true
|
||||
},
|
||||
"terser": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
examples/core-demos/package.json
Normal file
18
examples/core-demos/package.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "ecs-core-demos",
|
||||
"version": "1.0.0",
|
||||
"description": "ECS Framework Core Demos - Multiple Interactive Examples",
|
||||
"main": "src/main.ts",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@esengine/ecs-framework": "file:../../packages/core"
|
||||
}
|
||||
}
|
||||
99
examples/core-demos/src/demos/DemoBase.ts
Normal file
99
examples/core-demos/src/demos/DemoBase.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { Scene, Core } from '@esengine/ecs-framework';
|
||||
|
||||
export interface DemoInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
category: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export abstract class DemoBase {
|
||||
protected scene: Scene;
|
||||
protected canvas: HTMLCanvasElement;
|
||||
protected ctx: CanvasRenderingContext2D;
|
||||
protected controlPanel: HTMLElement;
|
||||
protected isRunning: boolean = false;
|
||||
protected animationFrameId: number | null = null;
|
||||
protected lastTime: number = 0;
|
||||
|
||||
constructor(canvas: HTMLCanvasElement, controlPanel: HTMLElement) {
|
||||
this.canvas = canvas;
|
||||
this.ctx = canvas.getContext('2d')!;
|
||||
this.controlPanel = controlPanel;
|
||||
this.scene = new Scene({ name: this.getInfo().name });
|
||||
|
||||
// 设置canvas大小
|
||||
this.resizeCanvas();
|
||||
window.addEventListener('resize', () => this.resizeCanvas());
|
||||
}
|
||||
|
||||
abstract getInfo(): DemoInfo;
|
||||
abstract setup(): void;
|
||||
abstract createControls(): void;
|
||||
|
||||
protected resizeCanvas() {
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
this.canvas.width = rect.width;
|
||||
this.canvas.height = rect.height;
|
||||
}
|
||||
|
||||
public start() {
|
||||
if (this.isRunning) return;
|
||||
this.isRunning = true;
|
||||
this.lastTime = performance.now();
|
||||
|
||||
// 设置当前场景到Core
|
||||
Core.setScene(this.scene);
|
||||
|
||||
this.scene.begin();
|
||||
this.loop();
|
||||
}
|
||||
|
||||
public stop() {
|
||||
this.isRunning = false;
|
||||
if (this.animationFrameId !== null) {
|
||||
cancelAnimationFrame(this.animationFrameId);
|
||||
this.animationFrameId = null;
|
||||
}
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.stop();
|
||||
this.scene.end();
|
||||
}
|
||||
|
||||
protected loop = () => {
|
||||
if (!this.isRunning) return;
|
||||
|
||||
// 计算deltaTime
|
||||
const currentTime = performance.now();
|
||||
const deltaTime = (currentTime - this.lastTime) / 1000; // 转换为秒
|
||||
this.lastTime = currentTime;
|
||||
|
||||
// 更新ECS框架
|
||||
Core.update(deltaTime);
|
||||
|
||||
// 渲染
|
||||
this.render();
|
||||
|
||||
// 继续循环
|
||||
this.animationFrameId = requestAnimationFrame(this.loop);
|
||||
}
|
||||
|
||||
protected abstract render(): void;
|
||||
|
||||
protected showToast(message: string, icon: string = '✅') {
|
||||
const toast = document.getElementById('toast')!;
|
||||
const toastMessage = document.getElementById('toastMessage')!;
|
||||
const toastIcon = toast.querySelector('.toast-icon')!;
|
||||
|
||||
toastIcon.textContent = icon;
|
||||
toastMessage.textContent = message;
|
||||
|
||||
toast.classList.add('show');
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('show');
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
468
examples/core-demos/src/demos/IncrementalSerializationDemo.ts
Normal file
468
examples/core-demos/src/demos/IncrementalSerializationDemo.ts
Normal file
@@ -0,0 +1,468 @@
|
||||
import { DemoBase, DemoInfo } from './DemoBase';
|
||||
import {
|
||||
Component,
|
||||
ECSComponent,
|
||||
Entity,
|
||||
EntitySystem,
|
||||
Matcher,
|
||||
Serializable,
|
||||
Serialize,
|
||||
IncrementalSerializer
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
// ===== 组件定义 =====
|
||||
@ECSComponent('IncDemo_Position')
|
||||
@Serializable({ version: 1, typeId: 'IncDemo_Position' })
|
||||
class PositionComponent extends Component {
|
||||
@Serialize() x: number = 0;
|
||||
@Serialize() y: number = 0;
|
||||
constructor(x: number = 0, y: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('IncDemo_Velocity')
|
||||
@Serializable({ version: 1, typeId: 'IncDemo_Velocity' })
|
||||
class VelocityComponent extends Component {
|
||||
@Serialize() vx: number = 0;
|
||||
@Serialize() vy: number = 0;
|
||||
constructor(vx: number = 0, vy: number = 0) {
|
||||
super();
|
||||
this.vx = vx;
|
||||
this.vy = vy;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('IncDemo_Renderable')
|
||||
@Serializable({ version: 1, typeId: 'IncDemo_Renderable' })
|
||||
class RenderableComponent extends Component {
|
||||
@Serialize() color: string = '#ffffff';
|
||||
@Serialize() radius: number = 10;
|
||||
constructor(color: string = '#ffffff', radius: number = 10) {
|
||||
super();
|
||||
this.color = color;
|
||||
this.radius = radius;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 系统定义 =====
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(PositionComponent, VelocityComponent));
|
||||
}
|
||||
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const [pos, vel] = this.getComponents(entity, PositionComponent, VelocityComponent);
|
||||
|
||||
pos.x += vel.vx;
|
||||
pos.y += vel.vy;
|
||||
|
||||
if (pos.x < 0 || pos.x > 1200) vel.vx *= -1;
|
||||
if (pos.y < 0 || pos.y > 600) vel.vy *= -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RenderSystem extends EntitySystem {
|
||||
private canvas: HTMLCanvasElement;
|
||||
private ctx: CanvasRenderingContext2D;
|
||||
|
||||
constructor(canvas: HTMLCanvasElement) {
|
||||
super(Matcher.all(PositionComponent, RenderableComponent));
|
||||
this.canvas = canvas;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) throw new Error('Failed to get canvas context');
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
this.ctx.fillStyle = '#0a0a15';
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
for (const entity of entities) {
|
||||
const [pos, render] = this.getComponents(entity, PositionComponent, RenderableComponent);
|
||||
|
||||
this.ctx.fillStyle = render.color;
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(pos.x, pos.y, render.radius, 0, Math.PI * 2);
|
||||
this.ctx.fill();
|
||||
|
||||
this.ctx.fillStyle = 'white';
|
||||
this.ctx.font = '10px Arial';
|
||||
this.ctx.textAlign = 'center';
|
||||
this.ctx.fillText(entity.name, pos.x, pos.y - render.radius - 5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class IncrementalSerializationDemo extends DemoBase {
|
||||
private renderSystem!: RenderSystem;
|
||||
private incrementalHistory: any[] = [];
|
||||
private autoSnapshotInterval: number | null = null;
|
||||
|
||||
getInfo(): DemoInfo {
|
||||
return {
|
||||
id: 'incremental-serialization',
|
||||
name: '增量序列化',
|
||||
description: '演示增量序列化功能,只保存场景变更而非完整状态,适用于网络同步和回放系统',
|
||||
category: '核心功能',
|
||||
icon: '🔄'
|
||||
};
|
||||
}
|
||||
|
||||
setup() {
|
||||
// 创建控制面板
|
||||
this.createControls();
|
||||
|
||||
// 添加系统
|
||||
this.renderSystem = new RenderSystem(this.canvas);
|
||||
this.scene.addEntityProcessor(new MovementSystem());
|
||||
this.scene.addEntityProcessor(this.renderSystem);
|
||||
|
||||
// 创建初始实体
|
||||
this.createInitialEntities();
|
||||
|
||||
// 创建基础快照
|
||||
this.scene.createIncrementalSnapshot();
|
||||
this.addToHistory('Initial State');
|
||||
}
|
||||
|
||||
private createInitialEntities() {
|
||||
// 创建玩家
|
||||
const player = this.scene.createEntity('Player');
|
||||
player.addComponent(new PositionComponent(600, 300));
|
||||
player.addComponent(new VelocityComponent(2, 1.5));
|
||||
player.addComponent(new RenderableComponent('#4a9eff', 15));
|
||||
|
||||
// 设置场景数据
|
||||
this.scene.sceneData.set('gameTime', 0);
|
||||
this.scene.sceneData.set('score', 0);
|
||||
}
|
||||
|
||||
private createRandomEntity() {
|
||||
const entity = this.scene.createEntity(`Entity_${Date.now()}`);
|
||||
entity.addComponent(new PositionComponent(
|
||||
Math.random() * this.canvas.width,
|
||||
Math.random() * this.canvas.height
|
||||
));
|
||||
entity.addComponent(new VelocityComponent(
|
||||
(Math.random() - 0.5) * 3,
|
||||
(Math.random() - 0.5) * 3
|
||||
));
|
||||
const colors = ['#ff6b6b', '#4ecdc4', '#ffe66d', '#a8dadc', '#f1faee'];
|
||||
entity.addComponent(new RenderableComponent(
|
||||
colors[Math.floor(Math.random() * colors.length)],
|
||||
5 + Math.random() * 10
|
||||
));
|
||||
}
|
||||
|
||||
private addToHistory(label: string) {
|
||||
const incremental = this.scene.serializeIncremental();
|
||||
const stats = IncrementalSerializer.getIncrementalStats(incremental);
|
||||
|
||||
// 计算JSON和二进制格式的大小
|
||||
const jsonSize = IncrementalSerializer.getIncrementalSize(incremental, 'json');
|
||||
const binarySize = IncrementalSerializer.getIncrementalSize(incremental, 'binary');
|
||||
|
||||
this.incrementalHistory.push({
|
||||
label,
|
||||
incremental,
|
||||
stats,
|
||||
timestamp: Date.now(),
|
||||
jsonSize,
|
||||
binarySize
|
||||
});
|
||||
|
||||
this.scene.updateIncrementalSnapshot();
|
||||
this.updateHistoryPanel();
|
||||
this.updateStats();
|
||||
}
|
||||
|
||||
createControls() {
|
||||
this.controlPanel.innerHTML = `
|
||||
<div class="control-section">
|
||||
<h4>实体控制</h4>
|
||||
<div class="button-group">
|
||||
<button id="addEntity" class="secondary">添加随机实体</button>
|
||||
<button id="removeEntity" class="danger">删除最后一个实体</button>
|
||||
<button id="modifyEntity" class="secondary">修改实体数据</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-section">
|
||||
<h4>增量快照</h4>
|
||||
<div class="button-group">
|
||||
<button id="captureSnapshot" class="success">捕获当前状态</button>
|
||||
<button id="clearHistory" class="danger">清空历史</button>
|
||||
</div>
|
||||
<div style="margin-top: 10px;">
|
||||
<label>
|
||||
<input type="checkbox" id="autoSnapshot">
|
||||
自动快照(每2秒)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-section">
|
||||
<h4>场景数据控制</h4>
|
||||
<div class="input-group">
|
||||
<label>游戏时间</label>
|
||||
<input type="number" id="gameTime" value="0" step="1">
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label>分数</label>
|
||||
<input type="number" id="score" value="0" step="10">
|
||||
</div>
|
||||
<button id="updateSceneData" class="secondary">更新场景数据</button>
|
||||
</div>
|
||||
|
||||
<div class="stats-panel">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">实体数量</div>
|
||||
<div class="stat-value" id="entityCount">0</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">历史记录</div>
|
||||
<div class="stat-value" id="historyCount">0</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">JSON大小</div>
|
||||
<div class="stat-value" id="jsonSize">0B</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">二进制大小</div>
|
||||
<div class="stat-value" id="binarySize">0B</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">压缩率</div>
|
||||
<div class="stat-value" id="compressionRatio">0%</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">总变更数</div>
|
||||
<div class="stat-value" id="totalChanges">0</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-section">
|
||||
<h4>增量历史 <small style="color: #8892b0;">(点击快照查看详情)</small></h4>
|
||||
<div style="max-height: 300px; overflow-y: auto; background: rgba(0,0,0,0.3); padding: 10px; border-radius: 6px;" id="historyPanel">
|
||||
暂无历史记录
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-section">
|
||||
<h4>快照详情</h4>
|
||||
<div style="max-height: 200px; overflow-y: auto; background: rgba(0,0,0,0.3); padding: 10px; border-radius: 6px; font-family: monospace; font-size: 11px; color: #8892b0;" id="snapshotDetails">
|
||||
点击历史记录查看详情...
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.bindEvents();
|
||||
this.updateStats();
|
||||
}
|
||||
|
||||
private bindEvents() {
|
||||
document.getElementById('addEntity')!.addEventListener('click', () => {
|
||||
this.createRandomEntity();
|
||||
this.addToHistory('添加实体');
|
||||
this.showToast('添加了一个随机实体');
|
||||
});
|
||||
|
||||
document.getElementById('removeEntity')!.addEventListener('click', () => {
|
||||
const entities = this.scene.entities.buffer;
|
||||
if (entities.length > 1) {
|
||||
const lastEntity = entities[entities.length - 1];
|
||||
lastEntity.destroy();
|
||||
this.addToHistory('删除实体');
|
||||
this.showToast('删除了最后一个实体');
|
||||
} else {
|
||||
this.showToast('至少保留一个实体', '⚠️');
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('modifyEntity')!.addEventListener('click', () => {
|
||||
const entities = this.scene.entities.buffer;
|
||||
if (entities.length > 0) {
|
||||
const randomEntity = entities[Math.floor(Math.random() * entities.length)];
|
||||
const pos = randomEntity.getComponent(PositionComponent);
|
||||
if (pos) {
|
||||
pos.x = Math.random() * this.canvas.width;
|
||||
pos.y = Math.random() * this.canvas.height;
|
||||
}
|
||||
this.addToHistory('修改实体位置');
|
||||
this.showToast(`修改了 ${randomEntity.name} 的位置`);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('captureSnapshot')!.addEventListener('click', () => {
|
||||
this.addToHistory('手动快照');
|
||||
this.showToast('已捕获当前状态', '📸');
|
||||
});
|
||||
|
||||
document.getElementById('clearHistory')!.addEventListener('click', () => {
|
||||
this.incrementalHistory = [];
|
||||
this.scene.createIncrementalSnapshot();
|
||||
this.addToHistory('清空后重新开始');
|
||||
this.showToast('历史记录已清空');
|
||||
});
|
||||
|
||||
document.getElementById('autoSnapshot')!.addEventListener('change', (e) => {
|
||||
const checkbox = e.target as HTMLInputElement;
|
||||
if (checkbox.checked) {
|
||||
this.autoSnapshotInterval = window.setInterval(() => {
|
||||
this.addToHistory('自动快照');
|
||||
}, 2000);
|
||||
this.showToast('自动快照已启用', '⏱️');
|
||||
} else {
|
||||
if (this.autoSnapshotInterval !== null) {
|
||||
clearInterval(this.autoSnapshotInterval);
|
||||
this.autoSnapshotInterval = null;
|
||||
}
|
||||
this.showToast('自动快照已禁用');
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('updateSceneData')!.addEventListener('click', () => {
|
||||
const gameTime = parseInt((document.getElementById('gameTime') as HTMLInputElement).value);
|
||||
const score = parseInt((document.getElementById('score') as HTMLInputElement).value);
|
||||
|
||||
this.scene.sceneData.set('gameTime', gameTime);
|
||||
this.scene.sceneData.set('score', score);
|
||||
|
||||
this.addToHistory('更新场景数据');
|
||||
this.showToast('场景数据已更新');
|
||||
});
|
||||
}
|
||||
|
||||
private updateHistoryPanel() {
|
||||
const panel = document.getElementById('historyPanel')!;
|
||||
|
||||
if (this.incrementalHistory.length === 0) {
|
||||
panel.innerHTML = '暂无历史记录';
|
||||
return;
|
||||
}
|
||||
|
||||
panel.innerHTML = this.incrementalHistory.map((item, index) => {
|
||||
const isLatest = index === this.incrementalHistory.length - 1;
|
||||
const time = new Date(item.timestamp).toLocaleTimeString();
|
||||
|
||||
return `
|
||||
<div class="history-item" data-index="${index}" style="
|
||||
padding: 8px;
|
||||
margin: 4px 0;
|
||||
background: ${isLatest ? 'rgba(74, 158, 255, 0.2)' : 'rgba(136, 146, 176, 0.1)'};
|
||||
border-left: 3px solid ${isLatest ? '#4a9eff' : '#8892b0'};
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<div>
|
||||
<strong>${item.label}</strong>
|
||||
${isLatest ? '<span style="color: #4a9eff; margin-left: 8px;">●</span>' : ''}
|
||||
</div>
|
||||
<small style="color: #8892b0;">${time}</small>
|
||||
</div>
|
||||
<div style="font-size: 11px; color: #8892b0; margin-top: 4px;">
|
||||
实体: +${item.stats.addedEntities} -${item.stats.removedEntities} ~${item.stats.updatedEntities} |
|
||||
组件: +${item.stats.addedComponents} -${item.stats.removedComponents} ~${item.stats.updatedComponents}
|
||||
</div>
|
||||
<div style="font-size: 11px; color: #8892b0; margin-top: 2px;">
|
||||
JSON: ${this.formatBytes(item.jsonSize)} |
|
||||
Binary: ${this.formatBytes(item.binarySize)} |
|
||||
<span style="color: #4ecdc4;">节省: ${((1 - item.binarySize / item.jsonSize) * 100).toFixed(1)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// 绑定点击事件
|
||||
panel.querySelectorAll('.history-item').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
const index = parseInt(item.getAttribute('data-index')!);
|
||||
this.showSnapshotDetails(index);
|
||||
});
|
||||
});
|
||||
|
||||
// 自动滚动到底部
|
||||
panel.scrollTop = panel.scrollHeight;
|
||||
}
|
||||
|
||||
private showSnapshotDetails(index: number) {
|
||||
const item = this.incrementalHistory[index];
|
||||
const detailsPanel = document.getElementById('snapshotDetails')!;
|
||||
|
||||
const compressionRatio = ((1 - item.binarySize / item.jsonSize) * 100).toFixed(1);
|
||||
|
||||
const details = {
|
||||
版本: item.incremental.version,
|
||||
基础版本: item.incremental.baseVersion,
|
||||
时间戳: new Date(item.incremental.timestamp).toLocaleString(),
|
||||
场景名称: item.incremental.sceneName,
|
||||
格式对比: {
|
||||
JSON大小: this.formatBytes(item.jsonSize),
|
||||
二进制大小: this.formatBytes(item.binarySize),
|
||||
压缩率: `${compressionRatio}%`,
|
||||
节省字节: this.formatBytes(item.jsonSize - item.binarySize)
|
||||
},
|
||||
统计: item.stats,
|
||||
实体变更: item.incremental.entityChanges.map((c: any) => ({
|
||||
操作: c.operation,
|
||||
实体ID: c.entityId,
|
||||
实体名称: c.entityName
|
||||
})),
|
||||
组件变更: item.incremental.componentChanges.map((c: any) => ({
|
||||
操作: c.operation,
|
||||
实体ID: c.entityId,
|
||||
组件类型: c.componentType
|
||||
})),
|
||||
场景数据变更: item.incremental.sceneDataChanges.map((c: any) => ({
|
||||
键: c.key,
|
||||
值: c.value,
|
||||
已删除: c.deleted
|
||||
}))
|
||||
};
|
||||
|
||||
detailsPanel.textContent = JSON.stringify(details, null, 2);
|
||||
}
|
||||
|
||||
private updateStats() {
|
||||
document.getElementById('entityCount')!.textContent = this.scene.entities.count.toString();
|
||||
document.getElementById('historyCount')!.textContent = this.incrementalHistory.length.toString();
|
||||
|
||||
if (this.incrementalHistory.length > 0) {
|
||||
const lastItem = this.incrementalHistory[this.incrementalHistory.length - 1];
|
||||
|
||||
document.getElementById('jsonSize')!.textContent = this.formatBytes(lastItem.jsonSize);
|
||||
document.getElementById('binarySize')!.textContent = this.formatBytes(lastItem.binarySize);
|
||||
|
||||
const compressionRatio = ((1 - lastItem.binarySize / lastItem.jsonSize) * 100).toFixed(1);
|
||||
const ratioElement = document.getElementById('compressionRatio')!;
|
||||
ratioElement.textContent = `${compressionRatio}%`;
|
||||
ratioElement.style.color = parseFloat(compressionRatio) > 30 ? '#4ecdc4' : '#ffe66d';
|
||||
|
||||
document.getElementById('totalChanges')!.textContent = lastItem.stats.totalChanges.toString();
|
||||
}
|
||||
}
|
||||
|
||||
private formatBytes(bytes: number): string {
|
||||
if (bytes < 1024) return `${bytes}B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
// RenderSystem会处理渲染
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
if (this.autoSnapshotInterval !== null) {
|
||||
clearInterval(this.autoSnapshotInterval);
|
||||
}
|
||||
super.destroy();
|
||||
}
|
||||
}
|
||||
386
examples/core-demos/src/demos/SerializationDemo.ts
Normal file
386
examples/core-demos/src/demos/SerializationDemo.ts
Normal file
@@ -0,0 +1,386 @@
|
||||
import { DemoBase, DemoInfo } from './DemoBase';
|
||||
import {
|
||||
Component,
|
||||
ECSComponent,
|
||||
Entity,
|
||||
EntitySystem,
|
||||
Matcher,
|
||||
Serializable,
|
||||
Serialize,
|
||||
SerializeAsMap
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
// ===== 组件定义 =====
|
||||
@ECSComponent('SerDemo_Position')
|
||||
@Serializable({ version: 1, typeId: 'SerDemo_Position' })
|
||||
class PositionComponent extends Component {
|
||||
@Serialize() x: number = 0;
|
||||
@Serialize() y: number = 0;
|
||||
constructor(x: number = 0, y: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('SerDemo_Velocity')
|
||||
@Serializable({ version: 1, typeId: 'SerDemo_Velocity' })
|
||||
class VelocityComponent extends Component {
|
||||
@Serialize() vx: number = 0;
|
||||
@Serialize() vy: number = 0;
|
||||
constructor(vx: number = 0, vy: number = 0) {
|
||||
super();
|
||||
this.vx = vx;
|
||||
this.vy = vy;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('SerDemo_Renderable')
|
||||
@Serializable({ version: 1, typeId: 'SerDemo_Renderable' })
|
||||
class RenderableComponent extends Component {
|
||||
@Serialize() color: string = '#ffffff';
|
||||
@Serialize() radius: number = 10;
|
||||
constructor(color: string = '#ffffff', radius: number = 10) {
|
||||
super();
|
||||
this.color = color;
|
||||
this.radius = radius;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('SerDemo_Player')
|
||||
@Serializable({ version: 1, typeId: 'SerDemo_Player' })
|
||||
class PlayerComponent extends Component {
|
||||
@Serialize() name: string = 'Player';
|
||||
@Serialize() level: number = 1;
|
||||
@Serialize() health: number = 100;
|
||||
@SerializeAsMap() inventory: Map<string, number> = new Map();
|
||||
constructor(name: string = 'Player') {
|
||||
super();
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 系统定义 =====
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(PositionComponent, VelocityComponent));
|
||||
}
|
||||
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const [pos, vel] = this.getComponents(entity, PositionComponent, VelocityComponent);
|
||||
|
||||
pos.x += vel.vx;
|
||||
pos.y += vel.vy;
|
||||
|
||||
// 边界反弹
|
||||
if (pos.x < 0 || pos.x > 1200) vel.vx *= -1;
|
||||
if (pos.y < 0 || pos.y > 600) vel.vy *= -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RenderSystem extends EntitySystem {
|
||||
private canvas: HTMLCanvasElement;
|
||||
private ctx: CanvasRenderingContext2D;
|
||||
|
||||
constructor(canvas: HTMLCanvasElement) {
|
||||
super(Matcher.all(PositionComponent, RenderableComponent));
|
||||
this.canvas = canvas;
|
||||
this.ctx = canvas.getContext('2d')!;
|
||||
}
|
||||
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
// 清空画布
|
||||
this.ctx.fillStyle = '#0a0a15';
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
// 渲染所有实体
|
||||
for (const entity of entities) {
|
||||
const [pos, render] = this.getComponents(entity, PositionComponent, RenderableComponent);
|
||||
|
||||
this.ctx.fillStyle = render.color;
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(pos.x, pos.y, render.radius, 0, Math.PI * 2);
|
||||
this.ctx.fill();
|
||||
|
||||
// 如果是玩家,显示名字
|
||||
const player = entity.getComponent(PlayerComponent);
|
||||
if (player) {
|
||||
this.ctx.fillStyle = 'white';
|
||||
this.ctx.font = '12px Arial';
|
||||
this.ctx.textAlign = 'center';
|
||||
this.ctx.fillText(player.name, pos.x, pos.y - render.radius - 5);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class SerializationDemo extends DemoBase {
|
||||
private renderSystem!: RenderSystem;
|
||||
private jsonData: string = '';
|
||||
private binaryData: Buffer | null = null;
|
||||
|
||||
getInfo(): DemoInfo {
|
||||
return {
|
||||
id: 'serialization',
|
||||
name: '场景序列化',
|
||||
description: '演示场景的序列化和反序列化功能,支持JSON和二进制格式',
|
||||
category: '核心功能',
|
||||
icon: '💾'
|
||||
};
|
||||
}
|
||||
|
||||
setup() {
|
||||
// @ECSComponent装饰器会自动注册组件到ComponentRegistry
|
||||
// ComponentRegistry会被序列化系统自动使用,无需手动注册
|
||||
|
||||
// 添加系统
|
||||
this.renderSystem = new RenderSystem(this.canvas);
|
||||
this.scene.addEntityProcessor(new MovementSystem());
|
||||
this.scene.addEntityProcessor(this.renderSystem);
|
||||
|
||||
// 创建初始实体
|
||||
this.createInitialEntities();
|
||||
|
||||
// 创建控制面板
|
||||
this.createControls();
|
||||
}
|
||||
|
||||
private createInitialEntities() {
|
||||
// 创建玩家
|
||||
const player = this.scene.createEntity('Player');
|
||||
player.addComponent(new PositionComponent(600, 300));
|
||||
player.addComponent(new VelocityComponent(2, 1.5));
|
||||
player.addComponent(new RenderableComponent('#4a9eff', 15));
|
||||
const playerComp = new PlayerComponent('Hero');
|
||||
playerComp.level = 5;
|
||||
playerComp.health = 100;
|
||||
playerComp.inventory.set('sword', 1);
|
||||
playerComp.inventory.set('potion', 5);
|
||||
player.addComponent(playerComp);
|
||||
|
||||
// 创建一些随机实体
|
||||
for (let i = 0; i < 5; i++) {
|
||||
this.createRandomEntity();
|
||||
}
|
||||
|
||||
// 设置场景数据
|
||||
this.scene.sceneData.set('weather', 'sunny');
|
||||
this.scene.sceneData.set('gameTime', 12.5);
|
||||
this.scene.sceneData.set('difficulty', 'normal');
|
||||
}
|
||||
|
||||
private createRandomEntity() {
|
||||
const entity = this.scene.createEntity(`Entity_${Date.now()}`);
|
||||
entity.addComponent(new PositionComponent(
|
||||
Math.random() * this.canvas.width,
|
||||
Math.random() * this.canvas.height
|
||||
));
|
||||
entity.addComponent(new VelocityComponent(
|
||||
(Math.random() - 0.5) * 3,
|
||||
(Math.random() - 0.5) * 3
|
||||
));
|
||||
const colors = ['#ff6b6b', '#4ecdc4', '#ffe66d', '#a8dadc', '#f1faee'];
|
||||
entity.addComponent(new RenderableComponent(
|
||||
colors[Math.floor(Math.random() * colors.length)],
|
||||
5 + Math.random() * 10
|
||||
));
|
||||
}
|
||||
|
||||
createControls() {
|
||||
this.controlPanel.innerHTML = `
|
||||
<div class="control-section">
|
||||
<h4>实体控制</h4>
|
||||
<div class="button-group">
|
||||
<button id="addEntity" class="secondary">添加随机实体</button>
|
||||
<button id="clearEntities" class="danger">清空所有实体</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-section">
|
||||
<h4>序列化操作</h4>
|
||||
<div class="button-group">
|
||||
<button id="serializeJSON">序列化为JSON</button>
|
||||
<button id="serializeBinary" class="success">序列化为二进制</button>
|
||||
<button id="deserialize" class="secondary">反序列化恢复</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-section">
|
||||
<h4>本地存储</h4>
|
||||
<div class="button-group">
|
||||
<button id="saveLocal" class="success">保存到LocalStorage</button>
|
||||
<button id="loadLocal" class="secondary">从LocalStorage加载</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-section">
|
||||
<h4>场景数据</h4>
|
||||
<div class="input-group">
|
||||
<label>天气</label>
|
||||
<input type="text" id="weather" value="sunny" placeholder="sunny/rainy/snowy">
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label>游戏时间</label>
|
||||
<input type="number" id="gameTime" value="12.5" step="0.1" min="0" max="24">
|
||||
</div>
|
||||
<button id="updateSceneData" class="secondary">更新场景数据</button>
|
||||
</div>
|
||||
|
||||
<div class="stats-panel">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">实体数量</div>
|
||||
<div class="stat-value" id="entityCount">0</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">JSON大小</div>
|
||||
<div class="stat-value" id="jsonSize">0B</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">二进制大小</div>
|
||||
<div class="stat-value" id="binarySize">0B</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">压缩率</div>
|
||||
<div class="stat-value" id="compressionRatio">0%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-section">
|
||||
<h4>序列化数据预览</h4>
|
||||
<div style="max-height: 200px; overflow-y: auto; background: rgba(0,0,0,0.3); padding: 10px; border-radius: 6px; font-family: monospace; font-size: 11px; color: #8892b0; word-break: break-all;" id="dataPreview">
|
||||
点击序列化按钮查看数据...
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 绑定事件
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
private bindEvents() {
|
||||
document.getElementById('addEntity')!.addEventListener('click', () => {
|
||||
this.createRandomEntity();
|
||||
this.updateStats();
|
||||
this.showToast('添加了一个随机实体');
|
||||
});
|
||||
|
||||
document.getElementById('clearEntities')!.addEventListener('click', () => {
|
||||
this.scene.destroyAllEntities();
|
||||
this.createInitialEntities();
|
||||
this.updateStats();
|
||||
this.showToast('场景已重置');
|
||||
});
|
||||
|
||||
document.getElementById('serializeJSON')!.addEventListener('click', () => {
|
||||
this.jsonData = this.scene.serialize({ format: 'json', pretty: true }) as string;
|
||||
this.updateDataPreview(this.jsonData, 'json');
|
||||
this.updateStats();
|
||||
this.showToast('已序列化为JSON格式');
|
||||
});
|
||||
|
||||
document.getElementById('serializeBinary')!.addEventListener('click', () => {
|
||||
this.binaryData = this.scene.serialize({ format: 'binary' }) as Buffer;
|
||||
const base64 = this.binaryData.toString('base64');
|
||||
this.updateDataPreview(`Binary Data (Base64):\n${base64.substring(0, 500)}...`, 'binary');
|
||||
this.updateStats();
|
||||
this.showToast('已序列化为二进制格式', '🔐');
|
||||
});
|
||||
|
||||
document.getElementById('deserialize')!.addEventListener('click', () => {
|
||||
const data = this.binaryData || this.jsonData;
|
||||
if (!data) {
|
||||
this.showToast('请先执行序列化操作', '⚠️');
|
||||
return;
|
||||
}
|
||||
|
||||
this.scene.deserialize(data, {
|
||||
strategy: 'replace'
|
||||
// componentRegistry会自动从ComponentRegistry获取,无需手动传入
|
||||
});
|
||||
|
||||
this.updateStats();
|
||||
this.showToast('场景已恢复');
|
||||
});
|
||||
|
||||
document.getElementById('saveLocal')!.addEventListener('click', () => {
|
||||
const jsonData = this.scene.serialize({ format: 'json' }) as string;
|
||||
localStorage.setItem('ecs_demo_scene', jsonData);
|
||||
this.showToast('已保存到LocalStorage', '💾');
|
||||
});
|
||||
|
||||
document.getElementById('loadLocal')!.addEventListener('click', () => {
|
||||
const data = localStorage.getItem('ecs_demo_scene');
|
||||
if (!data) {
|
||||
this.showToast('LocalStorage中没有保存的场景', '⚠️');
|
||||
return;
|
||||
}
|
||||
|
||||
this.scene.deserialize(data, {
|
||||
strategy: 'replace'
|
||||
// componentRegistry会自动从ComponentRegistry获取,无需手动传入
|
||||
});
|
||||
|
||||
this.updateStats();
|
||||
this.showToast('已从LocalStorage加载', '📂');
|
||||
});
|
||||
|
||||
document.getElementById('updateSceneData')!.addEventListener('click', () => {
|
||||
const weather = (document.getElementById('weather') as HTMLInputElement).value;
|
||||
const gameTime = parseFloat((document.getElementById('gameTime') as HTMLInputElement).value);
|
||||
|
||||
this.scene.sceneData.set('weather', weather);
|
||||
this.scene.sceneData.set('gameTime', gameTime);
|
||||
|
||||
this.showToast('场景数据已更新');
|
||||
});
|
||||
|
||||
// 初始更新统计
|
||||
this.updateStats();
|
||||
}
|
||||
|
||||
private updateDataPreview(data: string, format: string) {
|
||||
const preview = document.getElementById('dataPreview')!;
|
||||
if (format === 'json') {
|
||||
const truncated = data.length > 1000 ? data.substring(0, 1000) + '\n...(truncated)' : data;
|
||||
preview.textContent = truncated;
|
||||
} else {
|
||||
preview.textContent = data;
|
||||
}
|
||||
}
|
||||
|
||||
private updateStats() {
|
||||
const entityCount = this.scene.entities.count;
|
||||
document.getElementById('entityCount')!.textContent = entityCount.toString();
|
||||
|
||||
// 计算JSON大小
|
||||
if (this.jsonData) {
|
||||
const jsonSize = new Blob([this.jsonData]).size;
|
||||
document.getElementById('jsonSize')!.textContent = this.formatBytes(jsonSize);
|
||||
}
|
||||
|
||||
// 计算二进制大小
|
||||
if (this.binaryData) {
|
||||
const binarySize = this.binaryData.length;
|
||||
document.getElementById('binarySize')!.textContent = this.formatBytes(binarySize);
|
||||
|
||||
// 计算压缩率
|
||||
if (this.jsonData) {
|
||||
const jsonSize = new Blob([this.jsonData]).size;
|
||||
const ratio = ((1 - binarySize / jsonSize) * 100).toFixed(1);
|
||||
document.getElementById('compressionRatio')!.textContent = `${ratio}%`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private formatBytes(bytes: number): string {
|
||||
if (bytes < 1024) return `${bytes}B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
// RenderSystem会处理渲染
|
||||
}
|
||||
}
|
||||
832
examples/core-demos/src/demos/WorkerSystemDemo.ts
Normal file
832
examples/core-demos/src/demos/WorkerSystemDemo.ts
Normal file
@@ -0,0 +1,832 @@
|
||||
import { DemoBase, DemoInfo } from './DemoBase';
|
||||
import { Component, ECSComponent, WorkerEntitySystem, EntitySystem, Matcher, Entity, ECSSystem, PlatformManager, Time } from '@esengine/ecs-framework';
|
||||
import { BrowserAdapter } from '../platform/BrowserAdapter';
|
||||
|
||||
// ============ 组件定义 ============
|
||||
|
||||
@ECSComponent('WorkerDemo_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;
|
||||
}
|
||||
|
||||
set(x: number, y: number): void {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('WorkerDemo_Velocity')
|
||||
class Velocity extends Component {
|
||||
dx: number = 0;
|
||||
dy: number = 0;
|
||||
|
||||
constructor(dx: number = 0, dy: number = 0) {
|
||||
super();
|
||||
this.dx = dx;
|
||||
this.dy = dy;
|
||||
}
|
||||
|
||||
set(dx: number, dy: number): void {
|
||||
this.dx = dx;
|
||||
this.dy = dy;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('WorkerDemo_Physics')
|
||||
class Physics extends Component {
|
||||
mass: number = 1;
|
||||
bounce: number = 0.8;
|
||||
friction: number = 0.95;
|
||||
|
||||
constructor(mass: number = 1, bounce: number = 0.8, friction: number = 0.95) {
|
||||
super();
|
||||
this.mass = mass;
|
||||
this.bounce = bounce;
|
||||
this.friction = friction;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('WorkerDemo_Renderable')
|
||||
class Renderable extends Component {
|
||||
color: string = '#ffffff';
|
||||
size: number = 5;
|
||||
shape: 'circle' | 'square' = 'circle';
|
||||
|
||||
constructor(color: string = '#ffffff', size: number = 5, shape: 'circle' | 'square' = 'circle') {
|
||||
super();
|
||||
this.color = color;
|
||||
this.size = size;
|
||||
this.shape = shape;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('WorkerDemo_Lifetime')
|
||||
class Lifetime extends Component {
|
||||
maxAge: number = 5;
|
||||
currentAge: number = 0;
|
||||
|
||||
constructor(maxAge: number = 5) {
|
||||
super();
|
||||
this.maxAge = maxAge;
|
||||
this.currentAge = 0;
|
||||
}
|
||||
|
||||
isDead(): boolean {
|
||||
return this.currentAge >= this.maxAge;
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 系统定义 ============
|
||||
|
||||
interface PhysicsEntityData {
|
||||
id: number;
|
||||
x: number;
|
||||
y: number;
|
||||
dx: number;
|
||||
dy: number;
|
||||
mass: number;
|
||||
bounce: number;
|
||||
friction: number;
|
||||
radius: number;
|
||||
}
|
||||
|
||||
interface PhysicsConfig {
|
||||
gravity: number;
|
||||
canvasWidth: number;
|
||||
canvasHeight: number;
|
||||
groundFriction: number;
|
||||
}
|
||||
|
||||
@ECSSystem('PhysicsWorkerSystem')
|
||||
class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsEntityData> {
|
||||
private physicsConfig: PhysicsConfig;
|
||||
|
||||
constructor(enableWorker: boolean, canvasWidth: number, canvasHeight: number) {
|
||||
const defaultConfig = {
|
||||
gravity: 100,
|
||||
canvasWidth,
|
||||
canvasHeight,
|
||||
groundFriction: 0.98
|
||||
};
|
||||
|
||||
const isSharedArrayBufferAvailable = typeof SharedArrayBuffer !== 'undefined' && self.crossOriginIsolated;
|
||||
|
||||
super(
|
||||
Matcher.empty().all(Position, Velocity, Physics),
|
||||
{
|
||||
enableWorker,
|
||||
workerCount: isSharedArrayBufferAvailable ? (navigator.hardwareConcurrency || 2) : 1,
|
||||
systemConfig: defaultConfig,
|
||||
useSharedArrayBuffer: true
|
||||
}
|
||||
);
|
||||
|
||||
this.physicsConfig = defaultConfig;
|
||||
}
|
||||
|
||||
protected extractEntityData(entity: Entity): PhysicsEntityData {
|
||||
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,
|
||||
bounce: physics.bounce,
|
||||
friction: physics.friction,
|
||||
radius: renderable.size
|
||||
};
|
||||
}
|
||||
|
||||
protected workerProcess(
|
||||
entities: PhysicsEntityData[],
|
||||
deltaTime: number,
|
||||
systemConfig?: PhysicsConfig
|
||||
): PhysicsEntityData[] {
|
||||
const config = systemConfig || this.physicsConfig;
|
||||
const result = entities.map(e => ({ ...e }));
|
||||
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
const entity = result[i];
|
||||
|
||||
entity.dy += config.gravity * deltaTime;
|
||||
entity.x += entity.dx * deltaTime;
|
||||
entity.y += entity.dy * deltaTime;
|
||||
|
||||
if (entity.x <= entity.radius) {
|
||||
entity.x = entity.radius;
|
||||
entity.dx = -entity.dx * entity.bounce;
|
||||
} else if (entity.x >= config.canvasWidth - entity.radius) {
|
||||
entity.x = config.canvasWidth - entity.radius;
|
||||
entity.dx = -entity.dx * entity.bounce;
|
||||
}
|
||||
|
||||
if (entity.y <= entity.radius) {
|
||||
entity.y = entity.radius;
|
||||
entity.dy = -entity.dy * entity.bounce;
|
||||
} else if (entity.y >= config.canvasHeight - entity.radius) {
|
||||
entity.y = config.canvasHeight - entity.radius;
|
||||
entity.dy = -entity.dy * entity.bounce;
|
||||
entity.dx *= config.groundFriction;
|
||||
}
|
||||
|
||||
entity.dx *= entity.friction;
|
||||
entity.dy *= entity.friction;
|
||||
}
|
||||
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
for (let j = i + 1; j < result.length; j++) {
|
||||
const ball1 = result[i];
|
||||
const ball2 = result[j];
|
||||
|
||||
const dx = ball2.x - ball1.x;
|
||||
const dy = ball2.y - ball1.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
const minDistance = ball1.radius + ball2.radius;
|
||||
|
||||
if (distance < minDistance && distance > 0) {
|
||||
const nx = dx / distance;
|
||||
const ny = dy / distance;
|
||||
|
||||
const overlap = minDistance - distance;
|
||||
const separationX = nx * overlap * 0.5;
|
||||
const separationY = ny * overlap * 0.5;
|
||||
|
||||
ball1.x -= separationX;
|
||||
ball1.y -= separationY;
|
||||
ball2.x += separationX;
|
||||
ball2.y += separationY;
|
||||
|
||||
const relativeVelocityX = ball2.dx - ball1.dx;
|
||||
const relativeVelocityY = ball2.dy - ball1.dy;
|
||||
const velocityAlongNormal = relativeVelocityX * nx + relativeVelocityY * ny;
|
||||
|
||||
if (velocityAlongNormal > 0) continue;
|
||||
|
||||
const restitution = (ball1.bounce + ball2.bounce) * 0.5;
|
||||
const impulseScalar = -(1 + restitution) * velocityAlongNormal / (1/ball1.mass + 1/ball2.mass);
|
||||
|
||||
const impulseX = impulseScalar * nx;
|
||||
const impulseY = impulseScalar * ny;
|
||||
|
||||
ball1.dx -= impulseX / ball1.mass;
|
||||
ball1.dy -= impulseY / ball1.mass;
|
||||
ball2.dx += impulseX / ball2.mass;
|
||||
ball2.dy += impulseY / ball2.mass;
|
||||
|
||||
const energyLoss = 0.98;
|
||||
ball1.dx *= energyLoss;
|
||||
ball1.dy *= energyLoss;
|
||||
ball2.dx *= energyLoss;
|
||||
ball2.dy *= energyLoss;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected applyResult(entity: Entity, result: PhysicsEntityData): void {
|
||||
if (!entity || !entity.enabled) return;
|
||||
|
||||
const position = entity.getComponent(Position);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
if (!position || !velocity) return;
|
||||
|
||||
position.set(result.x, result.y);
|
||||
velocity.set(result.dx, result.dy);
|
||||
}
|
||||
|
||||
public updatePhysicsConfig(newConfig: Partial<PhysicsConfig>): void {
|
||||
Object.assign(this.physicsConfig, newConfig);
|
||||
this.updateConfig({ systemConfig: this.physicsConfig });
|
||||
}
|
||||
|
||||
public getPhysicsConfig(): PhysicsConfig {
|
||||
return { ...this.physicsConfig };
|
||||
}
|
||||
|
||||
protected getDefaultEntityDataSize(): number {
|
||||
return 9;
|
||||
}
|
||||
|
||||
protected writeEntityToBuffer(entityData: PhysicsEntityData, offset: number): void {
|
||||
const sharedArray = (this as any).sharedFloatArray as Float32Array;
|
||||
if (!sharedArray) return;
|
||||
|
||||
// 在第一个位置存储当前实体数量
|
||||
const currentEntityCount = Math.floor(offset / 9) + 1;
|
||||
sharedArray[0] = currentEntityCount;
|
||||
|
||||
// 数据从索引9开始存储(第一个9个位置用作元数据区域)
|
||||
const dataOffset = offset + 9;
|
||||
sharedArray[dataOffset + 0] = entityData.id;
|
||||
sharedArray[dataOffset + 1] = entityData.x;
|
||||
sharedArray[dataOffset + 2] = entityData.y;
|
||||
sharedArray[dataOffset + 3] = entityData.dx;
|
||||
sharedArray[dataOffset + 4] = entityData.dy;
|
||||
sharedArray[dataOffset + 5] = entityData.mass;
|
||||
sharedArray[dataOffset + 6] = entityData.bounce;
|
||||
sharedArray[dataOffset + 7] = entityData.friction;
|
||||
sharedArray[dataOffset + 8] = entityData.radius;
|
||||
}
|
||||
|
||||
protected readEntityFromBuffer(offset: number): PhysicsEntityData | null {
|
||||
const sharedArray = (this as any).sharedFloatArray as Float32Array;
|
||||
if (!sharedArray) return null;
|
||||
|
||||
// 数据从索引9开始存储
|
||||
const dataOffset = offset + 9;
|
||||
return {
|
||||
id: sharedArray[dataOffset + 0],
|
||||
x: sharedArray[dataOffset + 1],
|
||||
y: sharedArray[dataOffset + 2],
|
||||
dx: sharedArray[dataOffset + 3],
|
||||
dy: sharedArray[dataOffset + 4],
|
||||
mass: sharedArray[dataOffset + 5],
|
||||
bounce: sharedArray[dataOffset + 6],
|
||||
friction: sharedArray[dataOffset + 7],
|
||||
radius: sharedArray[dataOffset + 8]
|
||||
};
|
||||
}
|
||||
|
||||
protected getSharedArrayBufferProcessFunction(): any {
|
||||
return function(sharedFloatArray: Float32Array, startIndex: number, endIndex: number, deltaTime: number, systemConfig?: any) {
|
||||
const config = systemConfig || {
|
||||
gravity: 100,
|
||||
canvasWidth: 800,
|
||||
canvasHeight: 600,
|
||||
groundFriction: 0.98
|
||||
};
|
||||
|
||||
const actualEntityCount = sharedFloatArray[0];
|
||||
|
||||
// 基础物理更新
|
||||
for (let i = startIndex; i < endIndex && i < actualEntityCount; i++) {
|
||||
const offset = i * 9 + 9;
|
||||
|
||||
const id = sharedFloatArray[offset + 0];
|
||||
if (id === 0) continue;
|
||||
|
||||
let x = sharedFloatArray[offset + 1];
|
||||
let y = sharedFloatArray[offset + 2];
|
||||
let dx = sharedFloatArray[offset + 3];
|
||||
let dy = sharedFloatArray[offset + 4];
|
||||
const bounce = sharedFloatArray[offset + 6];
|
||||
const friction = sharedFloatArray[offset + 7];
|
||||
const radius = sharedFloatArray[offset + 8];
|
||||
|
||||
// 应用重力
|
||||
dy += config.gravity * deltaTime;
|
||||
|
||||
// 更新位置
|
||||
x += dx * deltaTime;
|
||||
y += dy * deltaTime;
|
||||
|
||||
// 边界碰撞
|
||||
if (x <= radius) {
|
||||
x = radius;
|
||||
dx = -dx * bounce;
|
||||
} else if (x >= config.canvasWidth - radius) {
|
||||
x = config.canvasWidth - radius;
|
||||
dx = -dx * bounce;
|
||||
}
|
||||
|
||||
if (y <= radius) {
|
||||
y = radius;
|
||||
dy = -dy * bounce;
|
||||
} else if (y >= config.canvasHeight - radius) {
|
||||
y = config.canvasHeight - radius;
|
||||
dy = -dy * bounce;
|
||||
dx *= config.groundFriction;
|
||||
}
|
||||
|
||||
// 空气阻力
|
||||
dx *= friction;
|
||||
dy *= friction;
|
||||
|
||||
// 写回数据
|
||||
sharedFloatArray[offset + 1] = x;
|
||||
sharedFloatArray[offset + 2] = y;
|
||||
sharedFloatArray[offset + 3] = dx;
|
||||
sharedFloatArray[offset + 4] = dy;
|
||||
}
|
||||
|
||||
// 碰撞检测
|
||||
for (let i = startIndex; i < endIndex && i < actualEntityCount; i++) {
|
||||
const offset1 = i * 9 + 9;
|
||||
const id1 = sharedFloatArray[offset1 + 0];
|
||||
if (id1 === 0) continue;
|
||||
|
||||
let x1 = sharedFloatArray[offset1 + 1];
|
||||
let y1 = sharedFloatArray[offset1 + 2];
|
||||
let dx1 = sharedFloatArray[offset1 + 3];
|
||||
let dy1 = sharedFloatArray[offset1 + 4];
|
||||
const mass1 = sharedFloatArray[offset1 + 5];
|
||||
const bounce1 = sharedFloatArray[offset1 + 6];
|
||||
const radius1 = sharedFloatArray[offset1 + 8];
|
||||
|
||||
for (let j = 0; j < actualEntityCount; j++) {
|
||||
if (i === j) continue;
|
||||
|
||||
const offset2 = j * 9 + 9;
|
||||
const id2 = sharedFloatArray[offset2 + 0];
|
||||
if (id2 === 0) continue;
|
||||
|
||||
const x2 = sharedFloatArray[offset2 + 1];
|
||||
const y2 = sharedFloatArray[offset2 + 2];
|
||||
const dx2 = sharedFloatArray[offset2 + 3];
|
||||
const dy2 = sharedFloatArray[offset2 + 4];
|
||||
const mass2 = sharedFloatArray[offset2 + 5];
|
||||
const bounce2 = sharedFloatArray[offset2 + 6];
|
||||
const radius2 = sharedFloatArray[offset2 + 8];
|
||||
|
||||
if (isNaN(x2) || isNaN(y2) || isNaN(radius2) || radius2 <= 0) continue;
|
||||
|
||||
const deltaX = x2 - x1;
|
||||
const deltaY = y2 - y1;
|
||||
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
||||
const minDistance = radius1 + radius2;
|
||||
|
||||
if (distance < minDistance && distance > 0) {
|
||||
const nx = deltaX / distance;
|
||||
const ny = deltaY / distance;
|
||||
|
||||
const overlap = minDistance - distance;
|
||||
const separationX = nx * overlap * 0.5;
|
||||
const separationY = ny * overlap * 0.5;
|
||||
|
||||
x1 -= separationX;
|
||||
y1 -= separationY;
|
||||
|
||||
const relativeVelocityX = dx2 - dx1;
|
||||
const relativeVelocityY = dy2 - dy1;
|
||||
const velocityAlongNormal = relativeVelocityX * nx + relativeVelocityY * ny;
|
||||
|
||||
if (velocityAlongNormal > 0) continue;
|
||||
|
||||
const restitution = (bounce1 + bounce2) * 0.5;
|
||||
const impulseScalar = -(1 + restitution) * velocityAlongNormal / (1/mass1 + 1/mass2);
|
||||
|
||||
const impulseX = impulseScalar * nx;
|
||||
const impulseY = impulseScalar * ny;
|
||||
|
||||
dx1 -= impulseX / mass1;
|
||||
dy1 -= impulseY / mass1;
|
||||
|
||||
const energyLoss = 0.98;
|
||||
dx1 *= energyLoss;
|
||||
dy1 *= energyLoss;
|
||||
}
|
||||
}
|
||||
|
||||
sharedFloatArray[offset1 + 1] = x1;
|
||||
sharedFloatArray[offset1 + 2] = y1;
|
||||
sharedFloatArray[offset1 + 3] = dx1;
|
||||
sharedFloatArray[offset1 + 4] = dy1;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ECSSystem('RenderSystem')
|
||||
class RenderSystem extends EntitySystem {
|
||||
private canvas: HTMLCanvasElement;
|
||||
private ctx: CanvasRenderingContext2D;
|
||||
|
||||
constructor(canvas: HTMLCanvasElement) {
|
||||
super(Matcher.all(Position, Renderable));
|
||||
this.canvas = canvas;
|
||||
this.ctx = canvas.getContext('2d')!;
|
||||
}
|
||||
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
this.ctx.fillStyle = '#000';
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
for (const entity of entities) {
|
||||
const position = this.requireComponent(entity, Position);
|
||||
const renderable = this.requireComponent(entity, Renderable);
|
||||
|
||||
this.ctx.fillStyle = renderable.color;
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(position.x, position.y, renderable.size, 0, Math.PI * 2);
|
||||
this.ctx.fill();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ECSSystem('LifetimeSystem')
|
||||
class LifetimeSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Lifetime));
|
||||
}
|
||||
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
const deltaTime = Time.deltaTime;
|
||||
|
||||
for (const entity of entities) {
|
||||
const lifetime = this.requireComponent(entity, Lifetime);
|
||||
|
||||
lifetime.currentAge += deltaTime;
|
||||
if (lifetime.isDead()) {
|
||||
entity.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Demo类 ============
|
||||
|
||||
export class WorkerSystemDemo extends DemoBase {
|
||||
private physicsSystem!: PhysicsWorkerSystem;
|
||||
private renderSystem!: RenderSystem;
|
||||
private lifetimeSystem!: LifetimeSystem;
|
||||
private currentFPS = 0;
|
||||
private frameCount = 0;
|
||||
private fpsUpdateTime = 0;
|
||||
private elements: { [key: string]: HTMLElement } = {};
|
||||
|
||||
getInfo(): DemoInfo {
|
||||
return {
|
||||
id: 'worker-system',
|
||||
name: 'Worker System',
|
||||
description: '演示 ECS 框架中的多线程物理计算能力',
|
||||
category: '核心功能',
|
||||
icon: '⚙️'
|
||||
};
|
||||
}
|
||||
|
||||
setup(): void {
|
||||
// 注册浏览器平台适配器
|
||||
const browserAdapter = new BrowserAdapter();
|
||||
PlatformManager.getInstance().registerAdapter(browserAdapter);
|
||||
|
||||
// 初始化系统
|
||||
this.physicsSystem = new PhysicsWorkerSystem(true, this.canvas.width, this.canvas.height);
|
||||
this.renderSystem = new RenderSystem(this.canvas);
|
||||
this.lifetimeSystem = new LifetimeSystem();
|
||||
|
||||
this.physicsSystem.updateOrder = 1;
|
||||
this.lifetimeSystem.updateOrder = 2;
|
||||
this.renderSystem.updateOrder = 3;
|
||||
|
||||
this.scene.addSystem(this.physicsSystem);
|
||||
this.scene.addSystem(this.lifetimeSystem);
|
||||
this.scene.addSystem(this.renderSystem);
|
||||
|
||||
// 创建控制面板
|
||||
this.createControls();
|
||||
|
||||
// 初始化UI元素引用
|
||||
this.initializeUIElements();
|
||||
this.bindEvents();
|
||||
|
||||
// 生成初始实体
|
||||
this.spawnInitialEntities(1000);
|
||||
}
|
||||
|
||||
createControls(): void {
|
||||
this.controlPanel.innerHTML = `
|
||||
<div style="background: #2a2a2a; padding: 20px; border-radius: 8px; height: 100%; overflow-y: auto;">
|
||||
<div style="margin-bottom: 15px;">
|
||||
<label style="display: block; margin-bottom: 5px; color: #ccc;">实体数量:</label>
|
||||
<input type="range" id="entityCount" min="100" max="10000" value="1000" step="100"
|
||||
style="width: 100%; margin-bottom: 5px;">
|
||||
<span id="entityCountValue" style="color: #fff;">1000</span>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 15px;">
|
||||
<label style="display: block; margin-bottom: 5px; color: #ccc;">Worker 设置:</label>
|
||||
<button id="toggleWorker" style="width: 100%; padding: 8px; margin-bottom: 5px;
|
||||
background: #4a9eff; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
||||
禁用 Worker
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 15px;">
|
||||
<button id="spawnParticles" style="width: 100%; padding: 8px; margin-bottom: 5px;
|
||||
background: #4a9eff; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
||||
生成粒子爆炸
|
||||
</button>
|
||||
<button id="clearEntities" style="width: 100%; padding: 8px; margin-bottom: 5px;
|
||||
background: #4a9eff; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
||||
清空所有实体
|
||||
</button>
|
||||
<button id="resetDemo" style="width: 100%; padding: 8px;
|
||||
background: #4a9eff; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
||||
重置演示
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 15px;">
|
||||
<label style="display: block; margin-bottom: 5px; color: #ccc;">物理参数:</label>
|
||||
<input type="range" id="gravity" min="0" max="500" value="100" step="10"
|
||||
style="width: 100%; margin-bottom: 5px;">
|
||||
<label style="color: #ccc;">重力: <span id="gravityValue">100</span></label>
|
||||
|
||||
<input type="range" id="friction" min="0" max="100" value="95" step="5"
|
||||
style="width: 100%; margin-top: 10px; margin-bottom: 5px;">
|
||||
<label style="color: #ccc;">摩擦力: <span id="frictionValue">95%</span></label>
|
||||
</div>
|
||||
|
||||
<div style="background: #1a1a1a; padding: 15px; border-radius: 8px; font-family: monospace; font-size: 12px;">
|
||||
<h3 style="margin-top: 0; color: #4a9eff;">性能统计</h3>
|
||||
<div style="margin: 5px 0; color: #ccc;">FPS: <span id="fps" style="color: #4eff4a;">0</span></div>
|
||||
<div style="margin: 5px 0; color: #ccc;">实体数量: <span id="entityCountStat" style="color: #fff;">0</span></div>
|
||||
<div style="margin: 5px 0; color: #ccc;">Worker状态: <span id="workerStatus" style="color: #ff4a4a;">未启用</span></div>
|
||||
<div style="margin: 5px 0; color: #ccc;">Worker负载: <span id="workerLoad" style="color: #fff;">N/A</span></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected render(): void {
|
||||
this.frameCount++;
|
||||
const currentTime = performance.now();
|
||||
|
||||
if (currentTime - this.fpsUpdateTime >= 1000) {
|
||||
this.currentFPS = this.frameCount;
|
||||
this.frameCount = 0;
|
||||
this.fpsUpdateTime = currentTime;
|
||||
}
|
||||
|
||||
this.updateUI();
|
||||
}
|
||||
|
||||
private initializeUIElements(): void {
|
||||
const elementIds = [
|
||||
'entityCount', 'entityCountValue', 'toggleWorker',
|
||||
'gravity', 'gravityValue', 'friction', 'frictionValue', 'spawnParticles',
|
||||
'clearEntities', 'resetDemo', 'fps', 'entityCountStat', 'workerStatus', 'workerLoad'
|
||||
];
|
||||
|
||||
for (const id of elementIds) {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
this.elements[id] = element;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bindEvents(): void {
|
||||
if (this.elements.entityCount && this.elements.entityCountValue) {
|
||||
const slider = this.elements.entityCount as HTMLInputElement;
|
||||
slider.addEventListener('input', () => {
|
||||
this.elements.entityCountValue.textContent = slider.value;
|
||||
});
|
||||
|
||||
slider.addEventListener('change', () => {
|
||||
const count = parseInt(slider.value);
|
||||
this.spawnInitialEntities(count);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.elements.toggleWorker) {
|
||||
this.elements.toggleWorker.addEventListener('click', () => {
|
||||
const workerEnabled = this.toggleWorker();
|
||||
this.elements.toggleWorker.textContent = workerEnabled ? '禁用 Worker' : '启用 Worker';
|
||||
});
|
||||
}
|
||||
|
||||
if (this.elements.gravity && this.elements.gravityValue) {
|
||||
const slider = this.elements.gravity as HTMLInputElement;
|
||||
slider.addEventListener('input', () => {
|
||||
this.elements.gravityValue.textContent = slider.value;
|
||||
});
|
||||
|
||||
slider.addEventListener('change', () => {
|
||||
const gravity = parseInt(slider.value);
|
||||
this.updateWorkerConfig({ gravity });
|
||||
});
|
||||
}
|
||||
|
||||
if (this.elements.friction && this.elements.frictionValue) {
|
||||
const slider = this.elements.friction as HTMLInputElement;
|
||||
slider.addEventListener('input', () => {
|
||||
const value = parseInt(slider.value);
|
||||
this.elements.frictionValue.textContent = `${value}%`;
|
||||
});
|
||||
|
||||
slider.addEventListener('change', () => {
|
||||
const friction = parseInt(slider.value) / 100;
|
||||
this.updateWorkerConfig({ friction });
|
||||
});
|
||||
}
|
||||
|
||||
if (this.elements.spawnParticles) {
|
||||
this.elements.spawnParticles.addEventListener('click', () => {
|
||||
const centerX = this.canvas.width / 2;
|
||||
const centerY = this.canvas.height / 2;
|
||||
this.spawnParticleExplosion(centerX, centerY, 100);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.elements.clearEntities) {
|
||||
this.elements.clearEntities.addEventListener('click', () => {
|
||||
this.clearAllEntities();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.elements.resetDemo) {
|
||||
this.elements.resetDemo.addEventListener('click', () => {
|
||||
(this.elements.entityCount as HTMLInputElement).value = '1000';
|
||||
this.elements.entityCountValue.textContent = '1000';
|
||||
(this.elements.gravity as HTMLInputElement).value = '100';
|
||||
this.elements.gravityValue.textContent = '100';
|
||||
(this.elements.friction as HTMLInputElement).value = '95';
|
||||
this.elements.frictionValue.textContent = '95%';
|
||||
|
||||
this.spawnInitialEntities(1000);
|
||||
this.updateWorkerConfig({ gravity: 100, friction: 0.95 });
|
||||
});
|
||||
}
|
||||
|
||||
this.canvas.addEventListener('click', (event) => {
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
const x = event.clientX - rect.left;
|
||||
const y = event.clientY - rect.top;
|
||||
this.spawnParticleExplosion(x, y, 30);
|
||||
});
|
||||
}
|
||||
|
||||
private updateUI(): void {
|
||||
const workerInfo = this.physicsSystem.getWorkerInfo();
|
||||
|
||||
if (this.elements.fps) {
|
||||
this.elements.fps.textContent = this.currentFPS.toString();
|
||||
}
|
||||
|
||||
if (this.elements.entityCountStat) {
|
||||
this.elements.entityCountStat.textContent = this.scene.entities.count.toString();
|
||||
}
|
||||
|
||||
if (this.elements.workerStatus) {
|
||||
if (workerInfo.enabled) {
|
||||
this.elements.workerStatus.textContent = `启用 (${workerInfo.workerCount} Workers)`;
|
||||
this.elements.workerStatus.style.color = '#4eff4a';
|
||||
} else {
|
||||
this.elements.workerStatus.textContent = '禁用';
|
||||
this.elements.workerStatus.style.color = '#ff4a4a';
|
||||
}
|
||||
}
|
||||
|
||||
if (this.elements.workerLoad) {
|
||||
const entityCount = this.scene.entities.count;
|
||||
if (workerInfo.enabled && entityCount > 0) {
|
||||
const entitiesPerWorker = Math.ceil(entityCount / workerInfo.workerCount);
|
||||
this.elements.workerLoad.textContent = `${entitiesPerWorker}/Worker (共${workerInfo.workerCount}个)`;
|
||||
} else {
|
||||
this.elements.workerLoad.textContent = 'N/A';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private spawnInitialEntities(count: number = 1000): void {
|
||||
this.clearAllEntities();
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
this.createParticle();
|
||||
}
|
||||
}
|
||||
|
||||
private createParticle(): void {
|
||||
const entity = this.scene.createEntity(`Particle_${Date.now()}_${Math.random()}`);
|
||||
|
||||
const x = Math.random() * (this.canvas.width - 20) + 10;
|
||||
const y = Math.random() * (this.canvas.height - 20) + 10;
|
||||
const dx = (Math.random() - 0.5) * 200;
|
||||
const dy = (Math.random() - 0.5) * 200;
|
||||
const mass = Math.random() * 3 + 2;
|
||||
const bounce = 0.85 + Math.random() * 0.15;
|
||||
const friction = 0.998 + Math.random() * 0.002;
|
||||
|
||||
const colors = [
|
||||
'#ff4444', '#44ff44', '#4444ff', '#ffff44', '#ff44ff', '#44ffff', '#ffffff',
|
||||
'#ff8844', '#88ff44', '#4488ff', '#ff4488', '#88ff88', '#8888ff', '#ffaa44'
|
||||
];
|
||||
const color = colors[Math.floor(Math.random() * colors.length)];
|
||||
const size = Math.random() * 6 + 3;
|
||||
|
||||
entity.addComponent(new Position(x, y));
|
||||
entity.addComponent(new Velocity(dx, dy));
|
||||
entity.addComponent(new Physics(mass, bounce, friction));
|
||||
entity.addComponent(new Renderable(color, size, 'circle'));
|
||||
entity.addComponent(new Lifetime(5 + Math.random() * 10));
|
||||
}
|
||||
|
||||
private spawnParticleExplosion(centerX: number, centerY: number, count: number = 50): void {
|
||||
for (let i = 0; i < count; i++) {
|
||||
const entity = this.scene.createEntity(`Explosion_${Date.now()}_${i}`);
|
||||
|
||||
const angle = (Math.PI * 2 * i) / count + (Math.random() - 0.5) * 0.5;
|
||||
const distance = Math.random() * 30;
|
||||
const x = centerX + Math.cos(angle) * distance;
|
||||
const y = centerY + Math.sin(angle) * distance;
|
||||
|
||||
const speed = 100 + Math.random() * 150;
|
||||
const dx = Math.cos(angle) * speed;
|
||||
const dy = Math.sin(angle) * speed;
|
||||
const mass = 0.5 + Math.random() * 1;
|
||||
const bounce = 0.8 + Math.random() * 0.2;
|
||||
|
||||
const colors = ['#ffaa00', '#ff6600', '#ff0066', '#ff3300', '#ffff00'];
|
||||
const color = colors[Math.floor(Math.random() * colors.length)];
|
||||
const size = Math.random() * 4 + 2;
|
||||
|
||||
entity.addComponent(new Position(x, y));
|
||||
entity.addComponent(new Velocity(dx, dy));
|
||||
entity.addComponent(new Physics(mass, bounce, 0.999));
|
||||
entity.addComponent(new Renderable(color, size, 'circle'));
|
||||
entity.addComponent(new Lifetime(2 + Math.random() * 3));
|
||||
}
|
||||
}
|
||||
|
||||
private clearAllEntities(): void {
|
||||
const entities = [...this.scene.entities.buffer];
|
||||
for (const entity of entities) {
|
||||
entity.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
private toggleWorker(): boolean {
|
||||
const workerInfo = this.physicsSystem.getWorkerInfo();
|
||||
const newWorkerEnabled = !workerInfo.enabled;
|
||||
|
||||
// 保存当前物理配置
|
||||
const currentConfig = this.physicsSystem.getPhysicsConfig();
|
||||
|
||||
this.scene.removeSystem(this.physicsSystem);
|
||||
this.physicsSystem = new PhysicsWorkerSystem(newWorkerEnabled, this.canvas.width, this.canvas.height);
|
||||
this.physicsSystem.updateOrder = 1;
|
||||
|
||||
// 恢复物理配置
|
||||
this.physicsSystem.updatePhysicsConfig(currentConfig);
|
||||
|
||||
this.scene.addSystem(this.physicsSystem);
|
||||
|
||||
return newWorkerEnabled;
|
||||
}
|
||||
|
||||
private updateWorkerConfig(config: { gravity?: number; friction?: number }): void {
|
||||
if (config.gravity !== undefined || config.friction !== undefined) {
|
||||
const physicsConfig = this.physicsSystem.getPhysicsConfig();
|
||||
this.physicsSystem.updatePhysicsConfig({
|
||||
gravity: config.gravity ?? physicsConfig.gravity,
|
||||
groundFriction: config.friction ?? physicsConfig.groundFriction
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
13
examples/core-demos/src/demos/index.ts
Normal file
13
examples/core-demos/src/demos/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { DemoBase } from './DemoBase';
|
||||
import { SerializationDemo } from './SerializationDemo';
|
||||
import { IncrementalSerializationDemo } from './IncrementalSerializationDemo';
|
||||
import { WorkerSystemDemo } from './WorkerSystemDemo';
|
||||
|
||||
export { DemoBase, SerializationDemo, IncrementalSerializationDemo, WorkerSystemDemo };
|
||||
|
||||
// Demo注册表
|
||||
export const DEMO_REGISTRY: typeof DemoBase[] = [
|
||||
SerializationDemo,
|
||||
IncrementalSerializationDemo,
|
||||
WorkerSystemDemo
|
||||
];
|
||||
171
examples/core-demos/src/main.ts
Normal file
171
examples/core-demos/src/main.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { DEMO_REGISTRY, DemoBase } from './demos';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
class DemoManager {
|
||||
private demos: Map<string, typeof DemoBase> = new Map();
|
||||
private currentDemo: DemoBase | null = null;
|
||||
private canvas: HTMLCanvasElement;
|
||||
private controlPanel: HTMLElement;
|
||||
|
||||
constructor() {
|
||||
// 初始化ECS Core
|
||||
Core.create({
|
||||
debug: true,
|
||||
enableEntitySystems: true
|
||||
});
|
||||
|
||||
this.canvas = document.getElementById('demoCanvas') as HTMLCanvasElement;
|
||||
this.controlPanel = document.getElementById('controlPanel') as HTMLElement;
|
||||
|
||||
// 注册所有demos
|
||||
for (const DemoClass of DEMO_REGISTRY) {
|
||||
const tempInstance = new DemoClass(this.canvas, this.controlPanel);
|
||||
const info = tempInstance.getInfo();
|
||||
this.demos.set(info.id, DemoClass);
|
||||
tempInstance.destroy();
|
||||
}
|
||||
|
||||
// 渲染demo列表
|
||||
this.renderDemoList();
|
||||
|
||||
// 自动加载第一个demo
|
||||
const firstDemo = DEMO_REGISTRY[0];
|
||||
if (firstDemo) {
|
||||
const tempInstance = new firstDemo(this.canvas, this.controlPanel);
|
||||
const info = tempInstance.getInfo();
|
||||
tempInstance.destroy();
|
||||
this.loadDemo(info.id);
|
||||
}
|
||||
}
|
||||
|
||||
private renderDemoList() {
|
||||
const demoList = document.getElementById('demoList')!;
|
||||
|
||||
// 按分类组织demos
|
||||
const categories = new Map<string, typeof DemoBase[]>();
|
||||
|
||||
for (const DemoClass of DEMO_REGISTRY) {
|
||||
const tempInstance = new DemoClass(this.canvas, this.controlPanel);
|
||||
const info = tempInstance.getInfo();
|
||||
tempInstance.destroy();
|
||||
|
||||
if (!categories.has(info.category)) {
|
||||
categories.set(info.category, []);
|
||||
}
|
||||
categories.get(info.category)!.push(DemoClass);
|
||||
}
|
||||
|
||||
// 渲染分类和demos
|
||||
let html = '';
|
||||
for (const [category, demoClasses] of categories) {
|
||||
html += `<div class="demo-category">`;
|
||||
html += `<div class="category-title">${category}</div>`;
|
||||
|
||||
for (const DemoClass of demoClasses) {
|
||||
const tempInstance = new DemoClass(this.canvas, this.controlPanel);
|
||||
const info = tempInstance.getInfo();
|
||||
tempInstance.destroy();
|
||||
|
||||
html += `
|
||||
<div class="demo-item" data-demo-id="${info.id}">
|
||||
<div class="demo-icon">${info.icon}</div>
|
||||
<div class="demo-info">
|
||||
<div class="demo-name">${info.name}</div>
|
||||
<div class="demo-desc">${info.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
html += `</div>`;
|
||||
}
|
||||
|
||||
demoList.innerHTML = html;
|
||||
|
||||
// 绑定点击事件
|
||||
demoList.querySelectorAll('.demo-item').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
const demoId = item.getAttribute('data-demo-id')!;
|
||||
this.loadDemo(demoId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private loadDemo(demoId: string) {
|
||||
// 停止并销毁当前demo
|
||||
if (this.currentDemo) {
|
||||
this.currentDemo.destroy();
|
||||
this.currentDemo = null;
|
||||
}
|
||||
|
||||
// 显示加载动画
|
||||
const loading = document.getElementById('loading')!;
|
||||
loading.style.display = 'block';
|
||||
|
||||
// 延迟加载,给用户反馈
|
||||
setTimeout(() => {
|
||||
const DemoClass = this.demos.get(demoId);
|
||||
if (!DemoClass) {
|
||||
console.error(`Demo ${demoId} not found`);
|
||||
loading.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 创建新demo
|
||||
this.currentDemo = new DemoClass(this.canvas, this.controlPanel);
|
||||
const info = this.currentDemo.getInfo();
|
||||
|
||||
// 更新页面标题和描述
|
||||
document.getElementById('demoTitle')!.textContent = info.name;
|
||||
document.getElementById('demoDescription')!.textContent = info.description;
|
||||
|
||||
// 设置demo
|
||||
this.currentDemo.setup();
|
||||
|
||||
// 显示控制面板
|
||||
this.controlPanel.style.display = 'block';
|
||||
|
||||
// 启动demo
|
||||
this.currentDemo.start();
|
||||
|
||||
// 更新菜单选中状态
|
||||
document.querySelectorAll('.demo-item').forEach(item => {
|
||||
item.classList.remove('active');
|
||||
if (item.getAttribute('data-demo-id') === demoId) {
|
||||
item.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
loading.style.display = 'none';
|
||||
|
||||
console.log(`✅ Demo "${info.name}" loaded successfully`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to load demo ${demoId}:`, error);
|
||||
loading.style.display = 'none';
|
||||
this.showError('加载演示失败:' + (error as Error).message);
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
private showError(message: string) {
|
||||
const toast = document.getElementById('toast')!;
|
||||
const toastMessage = document.getElementById('toastMessage')!;
|
||||
const toastIcon = toast.querySelector('.toast-icon')!;
|
||||
|
||||
toastIcon.textContent = '❌';
|
||||
toastMessage.textContent = message;
|
||||
toast.style.borderColor = '#f5576c';
|
||||
|
||||
toast.classList.add('show');
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('show');
|
||||
toast.style.borderColor = '#667eea';
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
new DemoManager();
|
||||
});
|
||||
204
examples/core-demos/src/platform/BrowserAdapter.ts
Normal file
204
examples/core-demos/src/platform/BrowserAdapter.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import type {
|
||||
IPlatformAdapter,
|
||||
PlatformWorker,
|
||||
WorkerCreationOptions,
|
||||
PlatformConfig
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* 浏览器平台适配器
|
||||
*/
|
||||
export class BrowserAdapter implements IPlatformAdapter {
|
||||
public readonly name = 'browser';
|
||||
public readonly version: string;
|
||||
|
||||
constructor() {
|
||||
this.version = this.getBrowserInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否支持Worker
|
||||
*/
|
||||
public isWorkerSupported(): boolean {
|
||||
return typeof Worker !== 'undefined';
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否支持SharedArrayBuffer
|
||||
*/
|
||||
public isSharedArrayBufferSupported(): boolean {
|
||||
return typeof SharedArrayBuffer !== 'undefined' && this.checkSharedArrayBufferEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取硬件并发数(CPU核心数)
|
||||
*/
|
||||
public getHardwareConcurrency(): number {
|
||||
return navigator.hardwareConcurrency || 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建Worker
|
||||
*/
|
||||
public createWorker(script: string, options: WorkerCreationOptions = {}): PlatformWorker {
|
||||
if (!this.isWorkerSupported()) {
|
||||
throw new Error('浏览器不支持Worker');
|
||||
}
|
||||
|
||||
try {
|
||||
return new BrowserWorker(script, options);
|
||||
} catch (error) {
|
||||
throw new Error(`创建浏览器Worker失败: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建SharedArrayBuffer
|
||||
*/
|
||||
public createSharedArrayBuffer(length: number): SharedArrayBuffer | null {
|
||||
if (!this.isSharedArrayBufferSupported()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return new SharedArrayBuffer(length);
|
||||
} catch (error) {
|
||||
console.warn('SharedArrayBuffer创建失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取高精度时间戳
|
||||
*/
|
||||
public getHighResTimestamp(): number {
|
||||
return performance.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取平台配置
|
||||
*/
|
||||
public getPlatformConfig(): PlatformConfig {
|
||||
return {
|
||||
maxWorkerCount: this.getHardwareConcurrency(),
|
||||
supportsModuleWorker: false,
|
||||
supportsTransferableObjects: true,
|
||||
maxSharedArrayBufferSize: 1024 * 1024 * 1024, // 1GB
|
||||
workerScriptPrefix: '',
|
||||
limitations: {
|
||||
noEval: false,
|
||||
requiresWorkerInit: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取浏览器信息
|
||||
*/
|
||||
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.]+)/);
|
||||
return match ? `Firefox ${match[1]}` : 'Firefox';
|
||||
} else if (userAgent.includes('Safari')) {
|
||||
const match = userAgent.match(/Version\/([0-9.]+)/);
|
||||
return match ? `Safari ${match[1]}` : 'Safari';
|
||||
}
|
||||
return 'Unknown Browser';
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查SharedArrayBuffer是否真正可用
|
||||
*/
|
||||
private checkSharedArrayBufferEnabled(): boolean {
|
||||
try {
|
||||
new SharedArrayBuffer(8);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 浏览器Worker封装
|
||||
*/
|
||||
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已被终止');
|
||||
}
|
||||
|
||||
try {
|
||||
if (transfer && transfer.length > 0) {
|
||||
this.worker.postMessage(message, transfer);
|
||||
} else {
|
||||
this.worker.postMessage(message);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`发送消息到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('终止Worker失败:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建浏览器Worker
|
||||
*/
|
||||
private createBrowserWorker(script: string, options: WorkerCreationOptions): Worker {
|
||||
try {
|
||||
// 创建Blob URL
|
||||
const blob = new Blob([script], { type: 'application/javascript' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
// 创建Worker
|
||||
const worker = new Worker(url, {
|
||||
type: options.type || 'classic',
|
||||
credentials: options.credentials,
|
||||
name: options.name
|
||||
});
|
||||
|
||||
// 清理Blob URL(延迟清理,确保Worker已加载)
|
||||
setTimeout(() => {
|
||||
URL.revokeObjectURL(url);
|
||||
}, 1000);
|
||||
|
||||
return worker;
|
||||
} catch (error) {
|
||||
throw new Error(`无法创建浏览器Worker: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
21
examples/core-demos/tsconfig.json
Normal file
21
examples/core-demos/tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
15
examples/core-demos/vite.config.ts
Normal file
15
examples/core-demos/vite.config.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
server: {
|
||||
port: 3003,
|
||||
headers: {
|
||||
'Cross-Origin-Opener-Policy': 'same-origin',
|
||||
'Cross-Origin-Embedder-Policy': 'require-corp'
|
||||
}
|
||||
},
|
||||
build: {
|
||||
target: 'es2020',
|
||||
outDir: 'dist'
|
||||
}
|
||||
});
|
||||
1
examples/lawn-mower-demo
Submodule
1
examples/lawn-mower-demo
Submodule
Submodule examples/lawn-mower-demo added at 5a4976b192
@@ -0,0 +1,2 @@
|
||||
[InternetShortcut]
|
||||
URL=https://docs.cocos.com/creator/manual/en/scripting/setup.html#custom-script-template
|
||||
24
extensions/cocos/cocos-ecs/.gitignore
vendored
Normal file
24
extensions/cocos/cocos-ecs/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
|
||||
#///////////////////////////
|
||||
# Cocos Creator 3D Project
|
||||
#///////////////////////////
|
||||
library/
|
||||
temp/
|
||||
local/
|
||||
build/
|
||||
profiles/
|
||||
native
|
||||
#//////////////////////////
|
||||
# NPM
|
||||
#//////////////////////////
|
||||
node_modules/
|
||||
|
||||
#//////////////////////////
|
||||
# VSCode
|
||||
#//////////////////////////
|
||||
.vscode/
|
||||
|
||||
#//////////////////////////
|
||||
# WebStorm
|
||||
#//////////////////////////
|
||||
.idea/
|
||||
14
extensions/cocos/cocos-ecs/assets/resources.meta
Normal file
14
extensions/cocos/cocos-ecs/assets/resources.meta
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "2a691dda-d56d-4a72-9fef-111a999415db",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {
|
||||
"isBundle": true,
|
||||
"bundleConfigID": "default",
|
||||
"bundleName": "resources",
|
||||
"priority": 8
|
||||
}
|
||||
}
|
||||
9
extensions/cocos/cocos-ecs/assets/resources/effects.meta
Normal file
9
extensions/cocos/cocos-ecs/assets/resources/effects.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "8c25761f-50d6-498b-a95f-d863bf1fbff1",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "3a66cbbc-6612-4408-838b-875d0bb2e9a3",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node_15iffhg4p",
|
||||
"type": "root",
|
||||
"name": "根节点",
|
||||
"description": "行为树的根节点,每棵树只能有一个根节点",
|
||||
"children": [
|
||||
"node_o6tsnrxyg"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "node_o6tsnrxyg",
|
||||
"type": "selector",
|
||||
"name": "选择器",
|
||||
"description": "按顺序执行子节点,任一成功则整体成功",
|
||||
"properties": {
|
||||
"abortType": "LowerPriority"
|
||||
},
|
||||
"children": [
|
||||
"node_tljchzbno",
|
||||
"node_txhx0hau5",
|
||||
"node_r9kvcwv8u",
|
||||
"node_520hedw22"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "node_tljchzbno",
|
||||
"type": "conditional-decorator",
|
||||
"name": "休息条件装饰器",
|
||||
"description": "基于条件执行子节点(拖拽条件节点到此装饰器来配置条件)",
|
||||
"properties": {
|
||||
"conditionType": "blackboardCompare",
|
||||
"executeWhenTrue": true,
|
||||
"abortType": "LowerPriority",
|
||||
"shouldReevaluate": true,
|
||||
"variableName": "{{isLowStamina}}",
|
||||
"operator": "equal",
|
||||
"compareValue": "true"
|
||||
},
|
||||
"children": [
|
||||
"node_ulp8qx68h"
|
||||
],
|
||||
"condition": {
|
||||
"type": "blackboard-value-comparison",
|
||||
"properties": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node_txhx0hau5",
|
||||
"type": "conditional-decorator",
|
||||
"name": "存储条件装饰器",
|
||||
"description": "基于条件执行子节点(拖拽条件节点到此装饰器来配置条件)",
|
||||
"properties": {
|
||||
"conditionType": "blackboardCompare",
|
||||
"executeWhenTrue": true,
|
||||
"abortType": "LowerPriority",
|
||||
"shouldReevaluate": true,
|
||||
"variableName": "{{hasOre}}",
|
||||
"operator": "equal",
|
||||
"compareValue": "true"
|
||||
},
|
||||
"children": [
|
||||
"node_dhsz8rgl1"
|
||||
],
|
||||
"condition": {
|
||||
"type": "blackboard-value-comparison",
|
||||
"properties": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node_r9kvcwv8u",
|
||||
"type": "conditional-decorator",
|
||||
"name": "挖矿条件装饰器",
|
||||
"description": "基于条件执行子节点(拖拽条件节点到此装饰器来配置条件)",
|
||||
"properties": {
|
||||
"conditionType": "blackboardCompare",
|
||||
"executeWhenTrue": true,
|
||||
"abortType": "LowerPriority",
|
||||
"shouldReevaluate": true,
|
||||
"variableName": "{{isLowStamina}}",
|
||||
"operator": "equal",
|
||||
"compareValue": "false"
|
||||
},
|
||||
"children": [
|
||||
"node_zguxml6u7"
|
||||
],
|
||||
"condition": {
|
||||
"type": "blackboard-value-comparison",
|
||||
"properties": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node_ulp8qx68h",
|
||||
"type": "sequence",
|
||||
"name": "序列器",
|
||||
"description": "按顺序执行子节点,任一失败则整体失败",
|
||||
"properties": {
|
||||
"abortType": "None"
|
||||
},
|
||||
"children": [
|
||||
"node_0fgq85ovw",
|
||||
"node_9v13vpqyr"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "node_0fgq85ovw",
|
||||
"type": "event-action",
|
||||
"name": "回家休息",
|
||||
"description": "执行已注册的事件处理函数(推荐)",
|
||||
"properties": {
|
||||
"eventName": "go-home-rest",
|
||||
"parameters": "{}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node_9v13vpqyr",
|
||||
"type": "event-action",
|
||||
"name": "恢复体力",
|
||||
"description": "执行已注册的事件处理函数(推荐)",
|
||||
"properties": {
|
||||
"eventName": "recover-stamina",
|
||||
"parameters": "{}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node_ui4ja9mlj",
|
||||
"type": "event-action",
|
||||
"name": "前往仓库存储",
|
||||
"description": "执行已注册的事件处理函数(推荐)",
|
||||
"properties": {
|
||||
"eventName": "store-ore",
|
||||
"parameters": "{}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node_969njccy2",
|
||||
"type": "event-action",
|
||||
"name": "挖掘金矿",
|
||||
"description": "执行已注册的事件处理函数(推荐)",
|
||||
"properties": {
|
||||
"eventName": "mine-gold-ore",
|
||||
"parameters": "{}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node_520hedw22",
|
||||
"type": "event-action",
|
||||
"name": "默认待机",
|
||||
"description": "执行已注册的事件处理函数(推荐)",
|
||||
"properties": {
|
||||
"eventName": "idle-behavior",
|
||||
"parameters": "{}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node_o5c7hv5wx",
|
||||
"type": "set-blackboard-value",
|
||||
"name": "设置黑板变量",
|
||||
"description": "设置黑板变量的值",
|
||||
"properties": {
|
||||
"variableName": "{{hasOre}}",
|
||||
"value": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node_zf0sgkqev",
|
||||
"type": "set-blackboard-value",
|
||||
"name": "设置黑板变量",
|
||||
"description": "设置黑板变量的值",
|
||||
"properties": {
|
||||
"variableName": "{{hasOre}}",
|
||||
"value": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node_dhsz8rgl1",
|
||||
"type": "sequence",
|
||||
"name": "序列器",
|
||||
"description": "按顺序执行子节点,任一失败则整体失败",
|
||||
"properties": {
|
||||
"abortType": "None"
|
||||
},
|
||||
"children": [
|
||||
"node_ui4ja9mlj",
|
||||
"node_o5c7hv5wx"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "node_zguxml6u7",
|
||||
"type": "sequence",
|
||||
"name": "序列器",
|
||||
"description": "按顺序执行子节点,任一失败则整体失败",
|
||||
"properties": {
|
||||
"abortType": "None"
|
||||
},
|
||||
"children": [
|
||||
"node_969njccy2",
|
||||
"node_zf0sgkqev"
|
||||
]
|
||||
}
|
||||
],
|
||||
"blackboard": [
|
||||
{
|
||||
"name": "unitType",
|
||||
"type": "string",
|
||||
"value": "miner",
|
||||
"description": "单位类型",
|
||||
"group": "基础属性"
|
||||
},
|
||||
{
|
||||
"name": "currentHealth",
|
||||
"type": "number",
|
||||
"value": 100,
|
||||
"description": "当前生命值",
|
||||
"group": "基础属性"
|
||||
},
|
||||
{
|
||||
"name": "maxHealth",
|
||||
"type": "number",
|
||||
"value": 100,
|
||||
"description": "最大生命值",
|
||||
"group": "基础属性"
|
||||
},
|
||||
{
|
||||
"name": "stamina",
|
||||
"type": "number",
|
||||
"value": 100,
|
||||
"description": "当前体力值 - 挖矿会消耗体力",
|
||||
"group": "体力系统"
|
||||
},
|
||||
{
|
||||
"name": "maxStamina",
|
||||
"type": "number",
|
||||
"value": 100,
|
||||
"description": "最大体力值",
|
||||
"group": "体力系统"
|
||||
},
|
||||
{
|
||||
"name": "staminaPercentage",
|
||||
"type": "number",
|
||||
"value": 1,
|
||||
"description": "体力百分比",
|
||||
"group": "体力系统"
|
||||
},
|
||||
{
|
||||
"name": "isLowStamina",
|
||||
"type": "boolean",
|
||||
"value": false,
|
||||
"description": "是否低体力 - 体力低于20%时为true",
|
||||
"group": "体力系统"
|
||||
},
|
||||
{
|
||||
"name": "isResting",
|
||||
"type": "boolean",
|
||||
"value": false,
|
||||
"description": "是否正在休息",
|
||||
"group": "体力系统"
|
||||
},
|
||||
{
|
||||
"name": "homePosition",
|
||||
"type": "vector3",
|
||||
"value": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"description": "家的位置 - 矿工休息的地方",
|
||||
"group": "体力系统"
|
||||
},
|
||||
{
|
||||
"name": "hasOre",
|
||||
"type": "boolean",
|
||||
"value": false,
|
||||
"description": "是否携带矿石",
|
||||
"group": "工作状态"
|
||||
},
|
||||
{
|
||||
"name": "currentCommand",
|
||||
"type": "string",
|
||||
"value": "mine",
|
||||
"description": "当前命令",
|
||||
"group": "工作状态"
|
||||
},
|
||||
{
|
||||
"name": "hasTarget",
|
||||
"type": "boolean",
|
||||
"value": false,
|
||||
"description": "是否有目标",
|
||||
"group": "工作状态"
|
||||
},
|
||||
{
|
||||
"name": "targetPosition",
|
||||
"type": "vector3",
|
||||
"value": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"description": "目标位置",
|
||||
"group": "移动属性"
|
||||
},
|
||||
{
|
||||
"name": "isMoving",
|
||||
"type": "boolean",
|
||||
"value": false,
|
||||
"description": "是否正在移动",
|
||||
"group": "移动属性"
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"name": "behavior-tree",
|
||||
"created": "2025-06-25T14:06:55.596Z",
|
||||
"version": "1.0",
|
||||
"exportType": "clean"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"ver": "2.0.1",
|
||||
"importer": "json",
|
||||
"imported": true,
|
||||
"uuid": "598e1450-8c7a-46c7-9540-398f9809d627",
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
1395
extensions/cocos/cocos-ecs/assets/resources/miner-stamina-ai.btree
Normal file
1395
extensions/cocos/cocos-ecs/assets/resources/miner-stamina-ai.btree
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"ver": "1.0.0",
|
||||
"importer": "*",
|
||||
"imported": true,
|
||||
"uuid": "24c6e7e6-4ff0-4e7b-b470-9468bfa66b5d",
|
||||
"files": [
|
||||
".btree",
|
||||
".json"
|
||||
],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
9
extensions/cocos/cocos-ecs/assets/resources/prefabs.meta
Normal file
9
extensions/cocos/cocos-ecs/assets/resources/prefabs.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "2bf3ded8-4054-4d8f-a367-c76b21eaf538",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"ver": "1.1.50",
|
||||
"importer": "prefab",
|
||||
"imported": true,
|
||||
"uuid": "51a6e245-2983-4258-be9f-9e21378f7f9f",
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {},
|
||||
"userData": {
|
||||
"syncNodeName": "Panel_Node"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "4fd895f7-6b0f-4357-aa3a-7c2e88ffac9a",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "829183be-61a1-4494-bf64-3df359c0e8e7",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
9
extensions/cocos/cocos-ecs/assets/scenes.meta
Normal file
9
extensions/cocos/cocos-ecs/assets/scenes.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "240e4a78-e55f-47a8-84de-39220bba1321",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,757 @@
|
||||
[
|
||||
{
|
||||
"__type__": "cc.SceneAsset",
|
||||
"_name": "behaviour-example-scene",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"_native": "",
|
||||
"scene": {
|
||||
"__id__": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Scene",
|
||||
"_name": "behaviour-example-scene",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"_parent": null,
|
||||
"_children": [
|
||||
{
|
||||
"__id__": 2
|
||||
},
|
||||
{
|
||||
"__id__": 5
|
||||
},
|
||||
{
|
||||
"__id__": 7
|
||||
},
|
||||
{
|
||||
"__id__": 8
|
||||
},
|
||||
{
|
||||
"__id__": 14
|
||||
}
|
||||
],
|
||||
"_active": true,
|
||||
"_components": [],
|
||||
"_prefab": null,
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_lrot": {
|
||||
"__type__": "cc.Quat",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"w": 1
|
||||
},
|
||||
"_lscale": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"z": 1
|
||||
},
|
||||
"_mobility": 0,
|
||||
"_layer": 1073741824,
|
||||
"_euler": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"autoReleaseAssets": false,
|
||||
"_globals": {
|
||||
"__id__": 16
|
||||
},
|
||||
"_id": "ff354f0b-c2f5-4dea-8ffb-0152d175d11c"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
"_name": "Main Light",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"_parent": {
|
||||
"__id__": 1
|
||||
},
|
||||
"_children": [],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 3
|
||||
}
|
||||
],
|
||||
"_prefab": null,
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_lrot": {
|
||||
"__type__": "cc.Quat",
|
||||
"x": -0.06397656665577071,
|
||||
"y": -0.44608233363525845,
|
||||
"z": -0.8239028751062036,
|
||||
"w": -0.3436591377065261
|
||||
},
|
||||
"_lscale": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"z": 1
|
||||
},
|
||||
"_mobility": 0,
|
||||
"_layer": 1073741824,
|
||||
"_euler": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": -117.894,
|
||||
"y": -194.909,
|
||||
"z": 38.562
|
||||
},
|
||||
"_id": "c0y6F5f+pAvI805TdmxIjx"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.DirectionalLight",
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"node": {
|
||||
"__id__": 2
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": null,
|
||||
"_color": {
|
||||
"__type__": "cc.Color",
|
||||
"r": 255,
|
||||
"g": 250,
|
||||
"b": 240,
|
||||
"a": 255
|
||||
},
|
||||
"_useColorTemperature": false,
|
||||
"_colorTemperature": 6550,
|
||||
"_staticSettings": {
|
||||
"__id__": 4
|
||||
},
|
||||
"_visibility": -325058561,
|
||||
"_illuminanceHDR": 65000,
|
||||
"_illuminance": 65000,
|
||||
"_illuminanceLDR": 1.6927083333333335,
|
||||
"_shadowEnabled": false,
|
||||
"_shadowPcf": 0,
|
||||
"_shadowBias": 0.00001,
|
||||
"_shadowNormalBias": 0,
|
||||
"_shadowSaturation": 1,
|
||||
"_shadowDistance": 50,
|
||||
"_shadowInvisibleOcclusionRange": 200,
|
||||
"_csmLevel": 4,
|
||||
"_csmLayerLambda": 0.75,
|
||||
"_csmOptimizationMode": 2,
|
||||
"_csmAdvancedOptions": false,
|
||||
"_csmLayersTransition": false,
|
||||
"_csmTransitionRange": 0.05,
|
||||
"_shadowFixedArea": false,
|
||||
"_shadowNear": 0.1,
|
||||
"_shadowFar": 10,
|
||||
"_shadowOrthoSize": 5,
|
||||
"_id": "597uMYCbhEtJQc0ffJlcgA"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.StaticLightSettings",
|
||||
"_baked": false,
|
||||
"_editorOnly": false,
|
||||
"_castShadow": false
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
"_name": "Main Camera",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"_parent": {
|
||||
"__id__": 1
|
||||
},
|
||||
"_children": [],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 6
|
||||
}
|
||||
],
|
||||
"_prefab": null,
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": -10,
|
||||
"y": 10,
|
||||
"z": 10
|
||||
},
|
||||
"_lrot": {
|
||||
"__type__": "cc.Quat",
|
||||
"x": -0.27781593346944056,
|
||||
"y": -0.36497167621709875,
|
||||
"z": -0.11507512748638377,
|
||||
"w": 0.8811195706053617
|
||||
},
|
||||
"_lscale": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"z": 1
|
||||
},
|
||||
"_mobility": 0,
|
||||
"_layer": 1073741824,
|
||||
"_euler": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": -35,
|
||||
"y": -45,
|
||||
"z": 0
|
||||
},
|
||||
"_id": "c9DMICJLFO5IeO07EPon7U"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Camera",
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"node": {
|
||||
"__id__": 5
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": null,
|
||||
"_projection": 1,
|
||||
"_priority": 0,
|
||||
"_fov": 45,
|
||||
"_fovAxis": 0,
|
||||
"_orthoHeight": 10,
|
||||
"_near": 1,
|
||||
"_far": 1000,
|
||||
"_color": {
|
||||
"__type__": "cc.Color",
|
||||
"r": 51,
|
||||
"g": 51,
|
||||
"b": 51,
|
||||
"a": 255
|
||||
},
|
||||
"_depth": 1,
|
||||
"_stencil": 0,
|
||||
"_clearFlags": 14,
|
||||
"_rect": {
|
||||
"__type__": "cc.Rect",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 1,
|
||||
"height": 1
|
||||
},
|
||||
"_aperture": 19,
|
||||
"_shutter": 7,
|
||||
"_iso": 0,
|
||||
"_screenScale": 1,
|
||||
"_visibility": 1822425087,
|
||||
"_targetTexture": null,
|
||||
"_postProcess": null,
|
||||
"_usePostProcess": false,
|
||||
"_cameraType": -1,
|
||||
"_trackingType": 0,
|
||||
"_id": "7dWQTpwS5LrIHnc1zAPUtf"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
"_name": "GameWorld",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"_parent": {
|
||||
"__id__": 1
|
||||
},
|
||||
"_children": [],
|
||||
"_active": true,
|
||||
"_components": [],
|
||||
"_prefab": null,
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_lrot": {
|
||||
"__type__": "cc.Quat",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"w": 1
|
||||
},
|
||||
"_lscale": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"z": 1
|
||||
},
|
||||
"_mobility": 0,
|
||||
"_layer": 1073741824,
|
||||
"_euler": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_id": "8b9QorrGZIl64tVv0Z0vRQ"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
"_name": "Canvas",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"_parent": {
|
||||
"__id__": 1
|
||||
},
|
||||
"_children": [
|
||||
{
|
||||
"__id__": 9
|
||||
}
|
||||
],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 11
|
||||
},
|
||||
{
|
||||
"__id__": 12
|
||||
},
|
||||
{
|
||||
"__id__": 13
|
||||
}
|
||||
],
|
||||
"_prefab": null,
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 640,
|
||||
"y": 360,
|
||||
"z": 0
|
||||
},
|
||||
"_lrot": {
|
||||
"__type__": "cc.Quat",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"w": 1
|
||||
},
|
||||
"_lscale": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"z": 1
|
||||
},
|
||||
"_mobility": 0,
|
||||
"_layer": 1073741824,
|
||||
"_euler": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_id": "4edRVPFLtIz5pR5edsryvx"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
"_name": "Camera",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"_parent": {
|
||||
"__id__": 8
|
||||
},
|
||||
"_children": [],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 10
|
||||
}
|
||||
],
|
||||
"_prefab": null,
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 1000
|
||||
},
|
||||
"_lrot": {
|
||||
"__type__": "cc.Quat",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"w": 1
|
||||
},
|
||||
"_lscale": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"z": 1
|
||||
},
|
||||
"_mobility": 0,
|
||||
"_layer": 1073741824,
|
||||
"_euler": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_id": "dfyZdh0bxJop4PyQrmHEP6"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Camera",
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"node": {
|
||||
"__id__": 9
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": null,
|
||||
"_projection": 0,
|
||||
"_priority": 1073741824,
|
||||
"_fov": 45,
|
||||
"_fovAxis": 0,
|
||||
"_orthoHeight": 360,
|
||||
"_near": 1,
|
||||
"_far": 2000,
|
||||
"_color": {
|
||||
"__type__": "cc.Color",
|
||||
"r": 0,
|
||||
"g": 0,
|
||||
"b": 0,
|
||||
"a": 255
|
||||
},
|
||||
"_depth": 1,
|
||||
"_stencil": 0,
|
||||
"_clearFlags": 6,
|
||||
"_rect": {
|
||||
"__type__": "cc.Rect",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 1,
|
||||
"height": 1
|
||||
},
|
||||
"_aperture": 19,
|
||||
"_shutter": 7,
|
||||
"_iso": 0,
|
||||
"_screenScale": 1,
|
||||
"_visibility": 41943040,
|
||||
"_targetTexture": null,
|
||||
"_postProcess": null,
|
||||
"_usePostProcess": false,
|
||||
"_cameraType": -1,
|
||||
"_trackingType": 0,
|
||||
"_id": "48lLOhLY5Onqokj70aNP+E"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.UITransform",
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"node": {
|
||||
"__id__": 8
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": null,
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
"width": 1280,
|
||||
"height": 720
|
||||
},
|
||||
"_anchorPoint": {
|
||||
"__type__": "cc.Vec2",
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
},
|
||||
"_id": "c3qBrLTLNImoltQDlZ6coz"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Canvas",
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"node": {
|
||||
"__id__": 8
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": null,
|
||||
"_cameraComponent": {
|
||||
"__id__": 10
|
||||
},
|
||||
"_alignCanvasWithScreen": true,
|
||||
"_id": "9d3SdE3ORAOZ6AG/imW6NO"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Widget",
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"node": {
|
||||
"__id__": 8
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": null,
|
||||
"_alignFlags": 45,
|
||||
"_target": null,
|
||||
"_left": 0,
|
||||
"_right": 0,
|
||||
"_top": 0,
|
||||
"_bottom": 0,
|
||||
"_horizontalCenter": 0,
|
||||
"_verticalCenter": 0,
|
||||
"_isAbsLeft": true,
|
||||
"_isAbsRight": true,
|
||||
"_isAbsTop": true,
|
||||
"_isAbsBottom": true,
|
||||
"_isAbsHorizontalCenter": true,
|
||||
"_isAbsVerticalCenter": true,
|
||||
"_originalWidth": 0,
|
||||
"_originalHeight": 0,
|
||||
"_alignMode": 2,
|
||||
"_lockFlags": 0,
|
||||
"_id": "4a8iJypC1J8pMml467hQ6c"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
"_name": "RTSDemo",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"_parent": {
|
||||
"__id__": 1
|
||||
},
|
||||
"_children": [],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 15
|
||||
}
|
||||
],
|
||||
"_prefab": null,
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_lrot": {
|
||||
"__type__": "cc.Quat",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"w": 1
|
||||
},
|
||||
"_lscale": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"z": 1
|
||||
},
|
||||
"_mobility": 0,
|
||||
"_layer": 1073741824,
|
||||
"_euler": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_id": "89cmsd2gNNsq155xC7mob8"
|
||||
},
|
||||
{
|
||||
"__type__": "c33869Km+9Bb7dw/OyRztvE",
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"node": {
|
||||
"__id__": 14
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": null,
|
||||
"minerCount": 1,
|
||||
"goldMineCount": 3,
|
||||
"_id": "86AIY7iYlMNqJsDC/+LIMU"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.SceneGlobals",
|
||||
"ambient": {
|
||||
"__id__": 17
|
||||
},
|
||||
"shadows": {
|
||||
"__id__": 18
|
||||
},
|
||||
"_skybox": {
|
||||
"__id__": 19
|
||||
},
|
||||
"fog": {
|
||||
"__id__": 20
|
||||
},
|
||||
"octree": {
|
||||
"__id__": 21
|
||||
},
|
||||
"skin": {
|
||||
"__id__": 22
|
||||
},
|
||||
"lightProbeInfo": {
|
||||
"__id__": 23
|
||||
},
|
||||
"postSettings": {
|
||||
"__id__": 24
|
||||
},
|
||||
"bakedWithStationaryMainLight": false,
|
||||
"bakedWithHighpLightmap": false
|
||||
},
|
||||
{
|
||||
"__type__": "cc.AmbientInfo",
|
||||
"_skyColorHDR": {
|
||||
"__type__": "cc.Vec4",
|
||||
"x": 0.2,
|
||||
"y": 0.5,
|
||||
"z": 0.8,
|
||||
"w": 0.520833125
|
||||
},
|
||||
"_skyColor": {
|
||||
"__type__": "cc.Vec4",
|
||||
"x": 0.2,
|
||||
"y": 0.5,
|
||||
"z": 0.8,
|
||||
"w": 0.520833125
|
||||
},
|
||||
"_skyIllumHDR": 20000,
|
||||
"_skyIllum": 20000,
|
||||
"_groundAlbedoHDR": {
|
||||
"__type__": "cc.Vec4",
|
||||
"x": 0.2,
|
||||
"y": 0.2,
|
||||
"z": 0.2,
|
||||
"w": 1
|
||||
},
|
||||
"_groundAlbedo": {
|
||||
"__type__": "cc.Vec4",
|
||||
"x": 0.2,
|
||||
"y": 0.2,
|
||||
"z": 0.2,
|
||||
"w": 1
|
||||
},
|
||||
"_skyColorLDR": {
|
||||
"__type__": "cc.Vec4",
|
||||
"x": 0.452588,
|
||||
"y": 0.607642,
|
||||
"z": 0.755699,
|
||||
"w": 0
|
||||
},
|
||||
"_skyIllumLDR": 0.8,
|
||||
"_groundAlbedoLDR": {
|
||||
"__type__": "cc.Vec4",
|
||||
"x": 0.618555,
|
||||
"y": 0.577848,
|
||||
"z": 0.544564,
|
||||
"w": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"__type__": "cc.ShadowsInfo",
|
||||
"_enabled": false,
|
||||
"_type": 0,
|
||||
"_normal": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
"y": 1,
|
||||
"z": 0
|
||||
},
|
||||
"_distance": 0,
|
||||
"_planeBias": 1,
|
||||
"_shadowColor": {
|
||||
"__type__": "cc.Color",
|
||||
"r": 76,
|
||||
"g": 76,
|
||||
"b": 76,
|
||||
"a": 255
|
||||
},
|
||||
"_maxReceived": 4,
|
||||
"_size": {
|
||||
"__type__": "cc.Vec2",
|
||||
"x": 1024,
|
||||
"y": 1024
|
||||
}
|
||||
},
|
||||
{
|
||||
"__type__": "cc.SkyboxInfo",
|
||||
"_envLightingType": 0,
|
||||
"_envmapHDR": {
|
||||
"__uuid__": "d032ac98-05e1-4090-88bb-eb640dcb5fc1@b47c0",
|
||||
"__expectedType__": "cc.TextureCube"
|
||||
},
|
||||
"_envmap": {
|
||||
"__uuid__": "d032ac98-05e1-4090-88bb-eb640dcb5fc1@b47c0",
|
||||
"__expectedType__": "cc.TextureCube"
|
||||
},
|
||||
"_envmapLDR": {
|
||||
"__uuid__": "6f01cf7f-81bf-4a7e-bd5d-0afc19696480@b47c0",
|
||||
"__expectedType__": "cc.TextureCube"
|
||||
},
|
||||
"_diffuseMapHDR": null,
|
||||
"_diffuseMapLDR": null,
|
||||
"_enabled": true,
|
||||
"_useHDR": true,
|
||||
"_editableMaterial": null,
|
||||
"_reflectionHDR": null,
|
||||
"_reflectionLDR": null,
|
||||
"_rotationAngle": 0
|
||||
},
|
||||
{
|
||||
"__type__": "cc.FogInfo",
|
||||
"_type": 0,
|
||||
"_fogColor": {
|
||||
"__type__": "cc.Color",
|
||||
"r": 200,
|
||||
"g": 200,
|
||||
"b": 200,
|
||||
"a": 255
|
||||
},
|
||||
"_enabled": false,
|
||||
"_fogDensity": 0.3,
|
||||
"_fogStart": 0.5,
|
||||
"_fogEnd": 300,
|
||||
"_fogAtten": 5,
|
||||
"_fogTop": 1.5,
|
||||
"_fogRange": 1.2,
|
||||
"_accurate": false
|
||||
},
|
||||
{
|
||||
"__type__": "cc.OctreeInfo",
|
||||
"_enabled": false,
|
||||
"_minPos": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": -1024,
|
||||
"y": -1024,
|
||||
"z": -1024
|
||||
},
|
||||
"_maxPos": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 1024,
|
||||
"y": 1024,
|
||||
"z": 1024
|
||||
},
|
||||
"_depth": 8
|
||||
},
|
||||
{
|
||||
"__type__": "cc.SkinInfo",
|
||||
"_enabled": true,
|
||||
"_blurRadius": 0.01,
|
||||
"_sssIntensity": 3
|
||||
},
|
||||
{
|
||||
"__type__": "cc.LightProbeInfo",
|
||||
"_giScale": 1,
|
||||
"_giSamples": 1024,
|
||||
"_bounces": 2,
|
||||
"_reduceRinging": 0,
|
||||
"_showProbe": true,
|
||||
"_showWireframe": true,
|
||||
"_showConvex": false,
|
||||
"_data": null,
|
||||
"_lightProbeSphereVolume": 1
|
||||
},
|
||||
{
|
||||
"__type__": "cc.PostSettingsInfo",
|
||||
"_toneMappingType": 0
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"ver": "1.1.50",
|
||||
"importer": "scene",
|
||||
"imported": true,
|
||||
"uuid": "ff354f0b-c2f5-4dea-8ffb-0152d175d11c",
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
2676
extensions/cocos/cocos-ecs/assets/scenes/scene.scene
Normal file
2676
extensions/cocos/cocos-ecs/assets/scenes/scene.scene
Normal file
File diff suppressed because it is too large
Load Diff
11
extensions/cocos/cocos-ecs/assets/scenes/scene.scene.meta
Normal file
11
extensions/cocos/cocos-ecs/assets/scenes/scene.scene.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"ver": "1.1.50",
|
||||
"importer": "scene",
|
||||
"imported": true,
|
||||
"uuid": "fcbf2917-6d43-4528-8829-7ee089594879",
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
9
extensions/cocos/cocos-ecs/assets/scripts.meta
Normal file
9
extensions/cocos/cocos-ecs/assets/scripts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "1556cd72-9618-4f9f-b9e7-28152a33bde9",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
130
extensions/cocos/cocos-ecs/assets/scripts/RTSDemo.ts
Normal file
130
extensions/cocos/cocos-ecs/assets/scripts/RTSDemo.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { _decorator, Component, Node, Vec3, Color } from 'cc';
|
||||
import { SimplePrefabFactory } from './components/SimplePrefabFactory';
|
||||
import { BehaviorTreeComponent } from './components/BehaviorTreeComponent';
|
||||
import { StatusUIManager } from './components/StatusUIManager';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/**
|
||||
* 矿工AI演示场景
|
||||
*/
|
||||
@ccclass('SimpleMinerDemo')
|
||||
export class SimpleMinerDemo extends Component {
|
||||
|
||||
@property
|
||||
minerCount: number = 1;
|
||||
|
||||
@property
|
||||
goldMineCount: number = 3;
|
||||
|
||||
private miners: Node[] = [];
|
||||
private goldMines: Node[] = [];
|
||||
private warehouse: Node | null = null;
|
||||
private ground: Node | null = null;
|
||||
private totalOresCollected: number = 0;
|
||||
private warehouseUI: any = null;
|
||||
|
||||
start() {
|
||||
this.createWorld();
|
||||
this.createWarehouse();
|
||||
this.createGoldMines();
|
||||
this.createMiners();
|
||||
}
|
||||
|
||||
private createWorld() {
|
||||
this.ground = SimplePrefabFactory.createGround(new Vec3(20, 0.2, 20));
|
||||
this.node.addChild(this.ground);
|
||||
this.ground.setWorldPosition(new Vec3(0, 0, 0));
|
||||
}
|
||||
|
||||
private createWarehouse() {
|
||||
this.warehouse = SimplePrefabFactory.createBuilding('Warehouse', new Vec3(2, 2, 2), Color.GRAY);
|
||||
this.node.addChild(this.warehouse);
|
||||
this.warehouse.setWorldPosition(new Vec3(0, 1, 0));
|
||||
this.createWarehouseUI();
|
||||
}
|
||||
|
||||
private createGoldMines() {
|
||||
for (let i = 0; i < this.goldMineCount; i++) {
|
||||
const angle = (i / this.goldMineCount) * Math.PI * 2;
|
||||
const radius = 6 + Math.random() * 2;
|
||||
const position = new Vec3(
|
||||
Math.cos(angle) * radius,
|
||||
0.8,
|
||||
Math.sin(angle) * radius
|
||||
);
|
||||
|
||||
const goldMine = SimplePrefabFactory.createResource(`GoldMine_${i + 1}`, Color.YELLOW);
|
||||
this.node.addChild(goldMine);
|
||||
goldMine.setWorldPosition(position);
|
||||
goldMine.setScale(new Vec3(1.2, 1.2, 1.2));
|
||||
this.goldMines.push(goldMine);
|
||||
}
|
||||
}
|
||||
|
||||
private createMiners() {
|
||||
for (let i = 0; i < this.minerCount; i++) {
|
||||
const angle = (i / this.minerCount) * Math.PI * 2;
|
||||
const radius = 3;
|
||||
const position = new Vec3(
|
||||
Math.cos(angle) * radius,
|
||||
1,
|
||||
Math.sin(angle) * radius
|
||||
);
|
||||
|
||||
const miner = SimplePrefabFactory.createUnit(`Miner_${i + 1}`, Color.BLUE);
|
||||
this.node.addChild(miner);
|
||||
miner.setWorldPosition(position);
|
||||
|
||||
const behaviorTree = miner.addComponent(BehaviorTreeComponent);
|
||||
behaviorTree.behaviorTreeFile = 'miner-stamina-ai.bt';
|
||||
behaviorTree.debugMode = true;
|
||||
|
||||
this.scheduleOnce(() => {
|
||||
const blackboard = behaviorTree.getBlackboard();
|
||||
if (blackboard) {
|
||||
blackboard.setValue('homePosition', position.clone());
|
||||
}
|
||||
}, 0.5);
|
||||
|
||||
this.miners.push(miner);
|
||||
}
|
||||
}
|
||||
|
||||
public getAllGoldMines(): Node[] {
|
||||
return this.goldMines.filter(mine => mine && mine.isValid);
|
||||
}
|
||||
|
||||
public getWarehouse(): Node | null {
|
||||
return this.warehouse;
|
||||
}
|
||||
|
||||
public mineGoldOre(miner: Node): boolean {
|
||||
this.totalOresCollected++;
|
||||
this.updateWarehouseUI();
|
||||
return true;
|
||||
}
|
||||
|
||||
public getTotalOresCollected(): number {
|
||||
return this.totalOresCollected;
|
||||
}
|
||||
|
||||
private createWarehouseUI() {
|
||||
if (!this.warehouse) return;
|
||||
|
||||
this.warehouseUI = StatusUIManager.createWarehouseUI(this.warehouse);
|
||||
if (this.warehouseUI) {
|
||||
this.updateWarehouseUI();
|
||||
}
|
||||
}
|
||||
|
||||
private updateWarehouseUI() {
|
||||
if (this.warehouseUI && this.warehouseUI.warehouseCountLabel) {
|
||||
this.warehouseUI.warehouseCountLabel.string = `🏭 总存储: ${this.totalOresCollected}`;
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
this.unscheduleAllCallbacks();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "c3386f4a-9bef-416f-b770-fcec91cedbc4",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "d07d95ad-f180-4b6e-9d0a-7248e75ec795",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,518 @@
|
||||
import { Node, resources, JsonAsset, Component, _decorator, Vec3, tween, instantiate, Prefab } from 'cc';
|
||||
import { BehaviorTree, BehaviorTreeBuilder, Blackboard, TaskStatus, BehaviorTreeJSONConfig, EventRegistry, IBehaviorTreeContext, ActionResult } from '@esengine/ai';
|
||||
import { MinerStatusUI } from './MinerStatusUI';
|
||||
import { StatusUIManager } from './StatusUIManager';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
@ccclass('BehaviorTreeComponent')
|
||||
export class BehaviorTreeComponent extends Component {
|
||||
|
||||
@property
|
||||
behaviorTreeFile: string = '';
|
||||
|
||||
@property
|
||||
autoStart: boolean = true;
|
||||
|
||||
@property
|
||||
debugMode: boolean = false;
|
||||
|
||||
@property
|
||||
showStatusUI: boolean = true;
|
||||
|
||||
@property(Prefab)
|
||||
statusUIPrefab: Prefab | null = null;
|
||||
|
||||
private behaviorTree: BehaviorTree<any> | null = null;
|
||||
private statusUI: MinerStatusUI | null = null;
|
||||
private blackboard: Blackboard | null = null;
|
||||
private context: any = null;
|
||||
private eventRegistry: EventRegistry | null = null;
|
||||
private isLoaded: boolean = false;
|
||||
private isRunning: boolean = false;
|
||||
|
||||
private actionStates: Map<string, {
|
||||
isExecuting: boolean;
|
||||
startTime: number;
|
||||
duration: number;
|
||||
}> = new Map();
|
||||
|
||||
start() {
|
||||
if (this.autoStart && this.behaviorTreeFile) {
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
if (this.showStatusUI) {
|
||||
this.createStatusUI();
|
||||
}
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
if (!this.behaviorTreeFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.loadBehaviorTree();
|
||||
this.isLoaded = true;
|
||||
this.isRunning = true;
|
||||
} catch (error) {
|
||||
// 静默处理
|
||||
}
|
||||
}
|
||||
|
||||
private async loadBehaviorTree(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let jsonPath = this.behaviorTreeFile;
|
||||
resources.load(jsonPath, JsonAsset, (err, asset) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const treeData = asset.json as BehaviorTreeJSONConfig;
|
||||
this.buildBehaviorTree(treeData);
|
||||
resolve();
|
||||
} catch (buildError) {
|
||||
reject(buildError);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private buildBehaviorTree(treeData: BehaviorTreeJSONConfig) {
|
||||
this.eventRegistry = new EventRegistry();
|
||||
this.setupEventHandlers();
|
||||
|
||||
const baseContext = {
|
||||
node: this.node,
|
||||
component: this,
|
||||
eventRegistry: this.eventRegistry
|
||||
};
|
||||
|
||||
const result = BehaviorTreeBuilder.fromBehaviorTreeConfig(treeData, baseContext);
|
||||
this.behaviorTree = result.tree;
|
||||
this.blackboard = result.blackboard;
|
||||
this.context = result.context;
|
||||
|
||||
this.initializeBlackboard();
|
||||
}
|
||||
|
||||
private setupEventHandlers() {
|
||||
if (!this.eventRegistry) return;
|
||||
|
||||
this.eventRegistry.registerAction('go-home-rest', (context, params) => {
|
||||
return this.handleGoHomeRest(context, params);
|
||||
});
|
||||
|
||||
this.eventRegistry.registerAction('recover-stamina', (context, params) => {
|
||||
return this.handleRecoverStamina(context, params);
|
||||
});
|
||||
|
||||
this.eventRegistry.registerAction('store-ore', (context, params) => {
|
||||
return this.handleStoreOre(context, params);
|
||||
});
|
||||
|
||||
this.eventRegistry.registerAction('mine-gold-ore', (context, params) => {
|
||||
return this.handleMineGoldOre(context, params);
|
||||
});
|
||||
|
||||
this.eventRegistry.registerAction('idle-behavior', (context, params) => {
|
||||
return this.handleIdleBehavior(context, params);
|
||||
});
|
||||
}
|
||||
|
||||
private initializeBlackboard() {
|
||||
if (!this.blackboard) return;
|
||||
|
||||
this.blackboard.setValue('stamina', 100);
|
||||
this.blackboard.setValue('staminaPercentage', 1.0);
|
||||
this.blackboard.setValue('isLowStamina', false);
|
||||
this.blackboard.setValue('hasOre', false);
|
||||
this.blackboard.setValue('isResting', false);
|
||||
this.blackboard.setValue('homePosition', this.node.worldPosition);
|
||||
}
|
||||
|
||||
|
||||
private createStatusUI() {
|
||||
if (!this.statusUIPrefab) {
|
||||
this.createSimpleStatusUI();
|
||||
return;
|
||||
}
|
||||
|
||||
const uiNode = instantiate(this.statusUIPrefab);
|
||||
const canvas = this.node.scene?.getChildByName('Canvas');
|
||||
if (canvas) {
|
||||
canvas.addChild(uiNode);
|
||||
this.statusUI = uiNode.getComponent(MinerStatusUI);
|
||||
if (this.statusUI) {
|
||||
this.statusUI.setFollowTarget(this.node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private createSimpleStatusUI() {
|
||||
this.statusUI = StatusUIManager.createStatusUIForMiner(this.node);
|
||||
}
|
||||
|
||||
private updateStatusUI() {
|
||||
if (!this.statusUI || !this.blackboard) return;
|
||||
|
||||
const stamina = this.blackboard.getValue('stamina') || 0;
|
||||
const maxStamina = this.blackboard.getValue('maxStamina') || 100;
|
||||
const hasOre = this.blackboard.getValue('hasOre') || false;
|
||||
const isResting = this.blackboard.getValue('isResting') || false;
|
||||
|
||||
// 更新体力
|
||||
this.statusUI.updateStamina(stamina, maxStamina);
|
||||
|
||||
// 更新状态文本
|
||||
let status = '';
|
||||
if (isResting) {
|
||||
status = '😴休息中';
|
||||
} else if (hasOre) {
|
||||
status = '🚚运输中';
|
||||
} else {
|
||||
status = '⛏️挖矿中';
|
||||
}
|
||||
this.statusUI.updateStatus(status);
|
||||
|
||||
// 获取仓库矿石总数
|
||||
const gameManager = this.node.parent?.getComponent('SimpleMinerDemo');
|
||||
const warehouseTotal = (gameManager as any)?.getTotalOresCollected() || 0;
|
||||
|
||||
// 更新矿石数量显示
|
||||
this.statusUI.updateOreCount(hasOre, warehouseTotal);
|
||||
|
||||
// 更新动作进度
|
||||
this.updateActionProgressUI();
|
||||
}
|
||||
|
||||
private updateActionProgressUI() {
|
||||
if (!this.statusUI) return;
|
||||
|
||||
let actionName = '';
|
||||
let progress = 0;
|
||||
|
||||
// 检查当前正在执行的动作
|
||||
for (const [key, state] of this.actionStates.entries()) {
|
||||
if (state.isExecuting) {
|
||||
const elapsed = Date.now() - state.startTime;
|
||||
progress = Math.min(elapsed / state.duration, 1.0);
|
||||
|
||||
switch (key) {
|
||||
case 'mine-gold-ore':
|
||||
actionName = '⛏️ 挖掘中';
|
||||
break;
|
||||
case 'store-ore':
|
||||
actionName = '📦 存储中';
|
||||
break;
|
||||
case 'recover-stamina':
|
||||
actionName = '💤 恢复体力';
|
||||
break;
|
||||
default:
|
||||
actionName = key;
|
||||
}
|
||||
break; // 只显示第一个正在执行的动作
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有正在执行的动作,清空进度显示
|
||||
this.statusUI.updateActionProgress(actionName, progress);
|
||||
}
|
||||
|
||||
// ==================== 行为树事件处理器 ====================
|
||||
|
||||
/**
|
||||
* 清理动作状态 - 当动作被中止时调用
|
||||
*/
|
||||
private clearActionState(actionKey: string) {
|
||||
if (this.actionStates.has(actionKey)) {
|
||||
this.actionStates.delete(actionKey);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 回家休息 - 包含体力恢复逻辑
|
||||
*/
|
||||
private handleGoHomeRest(context: any, params: any): ActionResult {
|
||||
const blackboard = this.blackboard;
|
||||
if (!blackboard) return 'failure';
|
||||
|
||||
// 检查是否已经在家了
|
||||
const homePos = blackboard.getValue('homePosition') || this.node.worldPosition;
|
||||
const distance = Vec3.distance(this.node.worldPosition, homePos);
|
||||
|
||||
|
||||
|
||||
if (distance > 1.0) {
|
||||
// 还没到家,继续移动
|
||||
this.moveToPosition(homePos, 2.0);
|
||||
return 'running';
|
||||
} else {
|
||||
this.clearActionState('mine-gold-ore');
|
||||
this.clearActionState('store-ore');
|
||||
blackboard.setValue('isResting', true);
|
||||
const actionKey = 'go-home-rest';
|
||||
const currentTime = Date.now();
|
||||
|
||||
// 初始化休息状态
|
||||
if (!this.actionStates.has(actionKey)) {
|
||||
this.actionStates.set(actionKey, {
|
||||
isExecuting: true,
|
||||
startTime: currentTime,
|
||||
duration: 2000 // 2秒恢复一次
|
||||
});
|
||||
return 'running';
|
||||
}
|
||||
|
||||
const actionState = this.actionStates.get(actionKey)!;
|
||||
const elapsed = currentTime - actionState.startTime;
|
||||
|
||||
if (elapsed >= actionState.duration) {
|
||||
const currentStamina = blackboard.getValue('stamina');
|
||||
const newStamina = Math.min(100, currentStamina + 10);
|
||||
|
||||
blackboard.setValue('stamina', newStamina);
|
||||
blackboard.setValue('staminaPercentage', newStamina / 100);
|
||||
|
||||
if (newStamina >= 80) {
|
||||
blackboard.setValue('isResting', false);
|
||||
blackboard.setValue('isLowStamina', false);
|
||||
this.actionStates.delete(actionKey);
|
||||
return 'success';
|
||||
}
|
||||
|
||||
actionState.startTime = currentTime;
|
||||
}
|
||||
|
||||
return 'running';
|
||||
}
|
||||
}
|
||||
|
||||
private handleRecoverStamina(context: any, params: any): ActionResult {
|
||||
return 'success';
|
||||
}
|
||||
|
||||
private handleMineGoldOre(context: any, params: any): ActionResult {
|
||||
const blackboard = this.blackboard;
|
||||
if (!blackboard) return 'failure';
|
||||
|
||||
const hasOre = blackboard.getValue('hasOre');
|
||||
const isLowStamina = blackboard.getValue('isLowStamina');
|
||||
const isResting = blackboard.getValue('isResting');
|
||||
|
||||
if (hasOre || isLowStamina || isResting) {
|
||||
return 'failure';
|
||||
}
|
||||
|
||||
const gameManager = this.node.parent?.getComponent('SimpleMinerDemo');
|
||||
const goldMines = (gameManager as any)?.getAllGoldMines();
|
||||
if (!goldMines?.length) return 'failure';
|
||||
|
||||
let nearestMine = goldMines[0];
|
||||
let minDistance = Vec3.distance(this.node.worldPosition, nearestMine.worldPosition);
|
||||
|
||||
for (const mine of goldMines) {
|
||||
const distance = Vec3.distance(this.node.worldPosition, mine.worldPosition);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
nearestMine = mine;
|
||||
}
|
||||
}
|
||||
|
||||
if (minDistance > 2.0) {
|
||||
this.moveToPosition(nearestMine.worldPosition, 2.0);
|
||||
return 'running';
|
||||
} else {
|
||||
const actionKey = 'mine-gold-ore';
|
||||
const currentTime = Date.now();
|
||||
|
||||
if (!this.actionStates.has(actionKey)) {
|
||||
this.actionStates.set(actionKey, {
|
||||
isExecuting: true,
|
||||
startTime: currentTime,
|
||||
duration: 3000
|
||||
});
|
||||
return 'running';
|
||||
}
|
||||
|
||||
const actionState = this.actionStates.get(actionKey)!;
|
||||
const elapsed = currentTime - actionState.startTime;
|
||||
|
||||
if (elapsed >= actionState.duration) {
|
||||
const currentStamina = blackboard.getValue('stamina');
|
||||
const newStamina = Math.max(0, currentStamina - 15);
|
||||
|
||||
blackboard.setValue('stamina', newStamina);
|
||||
blackboard.setValue('staminaPercentage', newStamina / 100);
|
||||
blackboard.setValue('hasOre', true);
|
||||
blackboard.setValue('isLowStamina', newStamina < 20);
|
||||
|
||||
this.actionStates.delete(actionKey);
|
||||
return 'failure';
|
||||
}
|
||||
|
||||
return 'running';
|
||||
}
|
||||
}
|
||||
|
||||
private handleStoreOre(context: any, params: any): ActionResult {
|
||||
const blackboard = this.blackboard;
|
||||
if (!blackboard) return 'failure';
|
||||
|
||||
const hasOre = blackboard.getValue('hasOre');
|
||||
if (!hasOre) {
|
||||
return 'failure';
|
||||
}
|
||||
|
||||
const isLowStamina = blackboard.getValue('isLowStamina');
|
||||
if (isLowStamina) {
|
||||
return 'failure';
|
||||
}
|
||||
|
||||
this.clearActionState('mine-gold-ore');
|
||||
const gameManager = this.node.parent?.getComponent('SimpleMinerDemo');
|
||||
const warehouse = (gameManager as any)?.getWarehouse();
|
||||
if (!warehouse) return 'failure';
|
||||
|
||||
const distance = Vec3.distance(this.node.worldPosition, warehouse.worldPosition);
|
||||
|
||||
if (distance > 2.0) {
|
||||
this.moveToPosition(warehouse.worldPosition, 2.0);
|
||||
return 'running';
|
||||
} else {
|
||||
const actionKey = 'store-ore';
|
||||
const currentTime = Date.now();
|
||||
|
||||
if (!this.actionStates.has(actionKey)) {
|
||||
this.actionStates.set(actionKey, {
|
||||
isExecuting: true,
|
||||
startTime: currentTime,
|
||||
duration: 1500
|
||||
});
|
||||
return 'running';
|
||||
}
|
||||
|
||||
const actionState = this.actionStates.get(actionKey)!;
|
||||
const elapsed = currentTime - actionState.startTime;
|
||||
|
||||
if (elapsed >= actionState.duration) {
|
||||
blackboard.setValue('hasOre', false);
|
||||
(gameManager as any).mineGoldOre(this.node);
|
||||
this.actionStates.delete(actionKey);
|
||||
return 'success';
|
||||
}
|
||||
|
||||
return 'running';
|
||||
}
|
||||
}
|
||||
|
||||
private handleIdleBehavior(context: any, params: any): ActionResult {
|
||||
return 'success';
|
||||
}
|
||||
|
||||
private moveToPosition(targetPos: Vec3, duration: number) {
|
||||
tween(this.node).stop();
|
||||
tween(this.node).to(duration, { worldPosition: targetPos }).start();
|
||||
}
|
||||
|
||||
update(deltaTime: number) {
|
||||
if (this.behaviorTree && this.isRunning) {
|
||||
this.behaviorTree.tick(deltaTime);
|
||||
}
|
||||
|
||||
if (this.showStatusUI) {
|
||||
this.updateStatusUI();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取黑板
|
||||
*/
|
||||
getBlackboard(): Blackboard | null {
|
||||
return this.blackboard;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取行为树
|
||||
*/
|
||||
getBehaviorTree(): BehaviorTree<any> | null {
|
||||
return this.behaviorTree;
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停行为树
|
||||
*/
|
||||
pause() {
|
||||
this.isRunning = false;
|
||||
if (this.debugMode) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复行为树
|
||||
*/
|
||||
resume() {
|
||||
if (this.isLoaded) {
|
||||
this.isRunning = true;
|
||||
if (this.debugMode) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止行为树
|
||||
*/
|
||||
stop() {
|
||||
this.isRunning = false;
|
||||
if (this.behaviorTree) {
|
||||
this.behaviorTree.reset();
|
||||
}
|
||||
if (this.debugMode) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新加载行为树
|
||||
*/
|
||||
async reload() {
|
||||
this.stop();
|
||||
await this.initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置行为树状态
|
||||
*/
|
||||
reset() {
|
||||
if (this.behaviorTree) {
|
||||
this.behaviorTree.reset();
|
||||
}
|
||||
if (this.debugMode) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
this.stop();
|
||||
if (this.eventRegistry) {
|
||||
this.eventRegistry.clear();
|
||||
}
|
||||
|
||||
// 清理UI
|
||||
if (this.statusUI) {
|
||||
this.statusUI.node.destroy();
|
||||
this.statusUI = null;
|
||||
}
|
||||
|
||||
this.behaviorTree = null;
|
||||
this.blackboard = null;
|
||||
this.context = null;
|
||||
this.eventRegistry = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "8efd182b-9891-4903-bef2-eb07b5184263",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
import { _decorator, Component, resources, JsonAsset, Vec3 } from 'cc';
|
||||
import { BehaviorTree, BehaviorTreeBuilder, Blackboard, BehaviorTreeJSONConfig, ExecutionContext, EventRegistry, ActionResult } from '@esengine/ai';
|
||||
import { UnitController } from './UnitController';
|
||||
import { RTSBehaviorHandler } from './RTSBehaviorHandler';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/**
|
||||
* 游戏执行上下文接口
|
||||
* 继承框架的ExecutionContext,添加游戏特定的属性
|
||||
*/
|
||||
interface GameExecutionContext extends ExecutionContext {
|
||||
unitController: UnitController;
|
||||
gameObject: any;
|
||||
eventRegistry?: EventRegistry;
|
||||
// 确保继承索引签名
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* 行为树管理器 - 使用@esengine/ai包管理行为树
|
||||
*/
|
||||
@ccclass('BehaviorTreeManager')
|
||||
export class BehaviorTreeManager extends Component {
|
||||
|
||||
@property
|
||||
debugMode: boolean = true;
|
||||
|
||||
@property
|
||||
tickInterval: number = 0.1; // 行为树更新间隔(秒)- 10fps更新频率,平衡性能和响应性
|
||||
|
||||
private behaviorTree: BehaviorTree<GameExecutionContext> | null = null;
|
||||
private blackboard: Blackboard | null = null;
|
||||
private context: GameExecutionContext | null = null;
|
||||
private eventRegistry: EventRegistry | null = null;
|
||||
private isLoaded: boolean = false;
|
||||
private isRunning: boolean = false;
|
||||
private lastTickTime: number = 0;
|
||||
private unitController: UnitController | null = null;
|
||||
private currentBehaviorTreeName: string = '';
|
||||
private behaviorHandler: RTSBehaviorHandler | null = null;
|
||||
|
||||
/**
|
||||
* 初始化行为树
|
||||
*/
|
||||
async initializeBehaviorTree(behaviorTreeName: string, unitController: UnitController) {
|
||||
this.currentBehaviorTreeName = behaviorTreeName;
|
||||
this.unitController = unitController;
|
||||
|
||||
// 获取RTSBehaviorHandler组件
|
||||
this.behaviorHandler = this.getComponent(RTSBehaviorHandler);
|
||||
if (!this.behaviorHandler) {
|
||||
console.error(`BehaviorTreeManager: 未找到RTSBehaviorHandler组件 - ${this.node.name}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.loadBehaviorTree(behaviorTreeName);
|
||||
this.setupBlackboard();
|
||||
this.isLoaded = true;
|
||||
this.isRunning = true;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`行为树初始化失败: ${behaviorTreeName}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载行为树文件
|
||||
*/
|
||||
private async loadBehaviorTree(behaviorTreeName: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const jsonPath = `${behaviorTreeName}.bt`;
|
||||
|
||||
|
||||
resources.load(jsonPath, JsonAsset, (err, asset) => {
|
||||
if (err) {
|
||||
console.error(`加载行为树文件失败: ${jsonPath}`, err);
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const behaviorTreeData = asset.json as BehaviorTreeJSONConfig;
|
||||
|
||||
// 创建执行上下文
|
||||
this.blackboard = new Blackboard();
|
||||
this.eventRegistry = this.createEventRegistry();
|
||||
this.context = {
|
||||
blackboard: this.blackboard,
|
||||
unitController: this.unitController!,
|
||||
gameObject: this.node,
|
||||
eventRegistry: this.eventRegistry
|
||||
} as GameExecutionContext;
|
||||
|
||||
// 从JSON数据创建行为树
|
||||
const buildResult = BehaviorTreeBuilder.fromBehaviorTreeConfig<GameExecutionContext>(behaviorTreeData, this.context);
|
||||
this.behaviorTree = buildResult.tree;
|
||||
this.blackboard = buildResult.blackboard;
|
||||
|
||||
resolve();
|
||||
} catch (parseError) {
|
||||
console.error(`创建行为树失败: ${jsonPath}`, parseError);
|
||||
reject(parseError);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建事件注册表
|
||||
*/
|
||||
private createEventRegistry(): EventRegistry {
|
||||
const registry = new EventRegistry();
|
||||
|
||||
// 注册体力系统矿工行为事件处理器
|
||||
const eventHandlers = {
|
||||
// 矿工体力系统核心行为
|
||||
'mine-gold-ore': (context: any, params?: any) => this.callBehaviorHandler('onMineGoldOre', params),
|
||||
'store-ore': (context: any, params?: any) => this.callBehaviorHandler('onStoreOre', params),
|
||||
'go-home-rest': (context: any, params?: any) => this.callBehaviorHandler('onGoHomeRest', params),
|
||||
'recover-stamina': (context: any, params?: any) => this.callBehaviorHandler('onRecoverStamina', params),
|
||||
'idle-behavior': (context: any, params?: any) => this.callBehaviorHandler('onIdleBehavior', params)
|
||||
};
|
||||
|
||||
// 将事件处理器注册到EventRegistry
|
||||
Object.entries(eventHandlers).forEach(([eventName, handler]) => {
|
||||
registry.registerAction(eventName, handler);
|
||||
});
|
||||
|
||||
return registry;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用行为处理器的方法
|
||||
*/
|
||||
private callBehaviorHandler(methodName: string, params: any = {}): ActionResult {
|
||||
if (!this.behaviorHandler) {
|
||||
console.error(`BehaviorTreeManager: RTSBehaviorHandler未初始化 - ${this.node.name}`);
|
||||
return 'failure';
|
||||
}
|
||||
|
||||
try {
|
||||
// 直接调用RTSBehaviorHandler的方法
|
||||
const method = (this.behaviorHandler as any)[methodName];
|
||||
if (typeof method === 'function') {
|
||||
const result = method.call(this.behaviorHandler, params);
|
||||
return result || 'success'; // 确保有返回值
|
||||
} else {
|
||||
console.error(`BehaviorTreeManager: 方法不存在: ${methodName}`);
|
||||
return 'failure';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`BehaviorTreeManager: 调用方法失败: ${methodName}`, error);
|
||||
return 'failure';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置黑板基础信息
|
||||
*/
|
||||
private setupBlackboard() {
|
||||
if (!this.unitController || !this.blackboard) return;
|
||||
|
||||
// 设置矿工基础信息
|
||||
this.blackboard.setValue('unitType', this.unitController.unitType);
|
||||
this.blackboard.setValue('currentHealth', this.unitController.currentHealth);
|
||||
this.blackboard.setValue('maxHealth', this.unitController.maxHealth);
|
||||
this.blackboard.setValue('currentCommand', 'mine');
|
||||
this.blackboard.setValue('hasOre', false);
|
||||
this.blackboard.setValue('hasTarget', false);
|
||||
this.blackboard.setValue('targetPosition', null);
|
||||
this.blackboard.setValue('isMoving', false);
|
||||
|
||||
// 设置体力系统信息
|
||||
this.blackboard.setValue('stamina', this.unitController.currentStamina);
|
||||
this.blackboard.setValue('maxStamina', this.unitController.maxStamina);
|
||||
this.blackboard.setValue('staminaPercentage', this.unitController.currentStamina / this.unitController.maxStamina);
|
||||
this.blackboard.setValue('isLowStamina', this.unitController.currentStamina < this.unitController.maxStamina * 0.2);
|
||||
this.blackboard.setValue('isResting', false);
|
||||
this.blackboard.setValue('homePosition', this.unitController.homePosition);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新黑板值
|
||||
*/
|
||||
updateBlackboardValue(key: string, value: any) {
|
||||
if (this.blackboard) {
|
||||
this.blackboard.setValue(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取黑板值
|
||||
*/
|
||||
getBlackboardValue(key: string): any {
|
||||
return this.blackboard?.getValue(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取黑板
|
||||
*/
|
||||
getBlackboard(): Blackboard | null {
|
||||
return this.blackboard;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取行为树
|
||||
*/
|
||||
getBehaviorTree(): BehaviorTree<GameExecutionContext> | null {
|
||||
return this.behaviorTree;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新行为树
|
||||
*/
|
||||
update(deltaTime: number) {
|
||||
if (!this.isLoaded || !this.isRunning || !this.behaviorTree || !this.blackboard) return;
|
||||
|
||||
// 控制更新频率
|
||||
this.lastTickTime += deltaTime;
|
||||
if (this.lastTickTime < this.tickInterval) return;
|
||||
|
||||
this.lastTickTime = 0;
|
||||
|
||||
// 更新矿工状态信息
|
||||
if (this.unitController) {
|
||||
// 基础属性
|
||||
this.blackboard.setValue('currentHealth', this.unitController.currentHealth);
|
||||
this.blackboard.setValue('currentCommand', this.unitController.currentCommand);
|
||||
this.blackboard.setValue('hasTarget', this.unitController.targetPosition && !this.unitController.targetPosition.equals(Vec3.ZERO));
|
||||
this.blackboard.setValue('targetPosition', this.unitController.targetPosition);
|
||||
this.blackboard.setValue('isMoving', this.unitController.targetPosition && !this.unitController.targetPosition.equals(Vec3.ZERO));
|
||||
|
||||
// 体力系统状态
|
||||
this.blackboard.setValue('stamina', this.unitController.currentStamina);
|
||||
this.blackboard.setValue('maxStamina', this.unitController.maxStamina);
|
||||
this.blackboard.setValue('staminaPercentage', this.unitController.currentStamina / this.unitController.maxStamina);
|
||||
this.blackboard.setValue('isLowStamina', this.unitController.currentStamina < this.unitController.maxStamina * 0.2);
|
||||
this.blackboard.setValue('homePosition', this.unitController.homePosition);
|
||||
}
|
||||
|
||||
// 执行行为树
|
||||
try {
|
||||
this.behaviorTree.tick(deltaTime);
|
||||
} catch (error) {
|
||||
console.error(`行为树执行错误: ${this.node.name}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停行为树
|
||||
*/
|
||||
pause() {
|
||||
this.isRunning = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复行为树
|
||||
*/
|
||||
resume() {
|
||||
if (this.isLoaded) {
|
||||
this.isRunning = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止行为树
|
||||
*/
|
||||
stop() {
|
||||
this.isRunning = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新加载行为树
|
||||
*/
|
||||
async reloadBehaviorTree() {
|
||||
if (this.currentBehaviorTreeName && this.unitController) {
|
||||
this.stop();
|
||||
await this.initializeBehaviorTree(this.currentBehaviorTreeName, this.unitController);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置行为树
|
||||
*/
|
||||
reset() {
|
||||
if (this.behaviorTree) {
|
||||
this.behaviorTree.reset();
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
this.stop();
|
||||
this.behaviorTree = null;
|
||||
this.blackboard = null;
|
||||
this.context = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "891c88fc-282d-4791-a961-8d85244bfee7",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import { Component, _decorator, Label, ProgressBar, Node, UITransform, Canvas, find, Camera, Vec3, director, Color, Layers, Graphics } from 'cc';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/**
|
||||
* 矿工状态UI组件
|
||||
*/
|
||||
@ccclass('MinerStatusUI')
|
||||
export class MinerStatusUI extends Component {
|
||||
|
||||
nameLabel: Label | null = null;
|
||||
statusLabel: Label | null = null;
|
||||
staminaBar: ProgressBar | null = null;
|
||||
actionProgressBar: ProgressBar | null = null;
|
||||
actionLabel: Label | null = null;
|
||||
oreCountLabel: Label | null = null;
|
||||
warehouseCountLabel: Label | null = null;
|
||||
|
||||
@property
|
||||
followTarget: Node | null = null;
|
||||
|
||||
@property
|
||||
yOffset: number = 100;
|
||||
|
||||
private camera: Camera | null = null;
|
||||
private canvas: Canvas | null = null;
|
||||
|
||||
start() {
|
||||
this.node.layer = Layers.Enum.UI_2D;
|
||||
|
||||
this.camera = find('Main Camera')?.getComponent(Camera) || director.getScene()?.getComponentInChildren(Camera);
|
||||
this.canvas = find('Canvas')?.getComponent(Canvas) || director.getScene()?.getComponentInChildren(Canvas);
|
||||
|
||||
|
||||
|
||||
if (this.nameLabel && this.followTarget) {
|
||||
this.nameLabel.string = this.followTarget.name;
|
||||
}
|
||||
|
||||
this.updateStamina(100, 100);
|
||||
this.updateStatus('待机中');
|
||||
this.updateActionProgress('', 0);
|
||||
}
|
||||
|
||||
update() {
|
||||
if (this.followTarget && this.camera && this.canvas) {
|
||||
this.updateUIPosition();
|
||||
}
|
||||
}
|
||||
|
||||
private updateUIPosition() {
|
||||
if (!this.followTarget || !this.camera || !this.canvas) return;
|
||||
|
||||
const targetWorldPos = this.followTarget.worldPosition.clone();
|
||||
// 根据目标类型设置不同的Y偏移
|
||||
if (this.followTarget.name.includes('Warehouse')) {
|
||||
targetWorldPos.y += 3.0; // 仓库偏移更高
|
||||
} else {
|
||||
targetWorldPos.y += 2.0; // 矿工偏移
|
||||
}
|
||||
|
||||
// 将世界坐标直接转换为UI坐标
|
||||
const uiPos = new Vec3();
|
||||
this.camera.convertToUINode(targetWorldPos, this.canvas.node, uiPos);
|
||||
this.node.setPosition(uiPos);
|
||||
}
|
||||
|
||||
setFollowTarget(target: Node) {
|
||||
this.followTarget = target;
|
||||
if (this.nameLabel) {
|
||||
this.nameLabel.string = target.name;
|
||||
}
|
||||
}
|
||||
|
||||
updateStamina(current: number, max: number) {
|
||||
if (this.staminaBar) {
|
||||
this.staminaBar.progress = current / max;
|
||||
}
|
||||
|
||||
if (this.staminaBar) {
|
||||
const percentage = current / max;
|
||||
const fillNode = this.staminaBar.node.getChildByName('Bar');
|
||||
if (fillNode) {
|
||||
const graphics = fillNode.getComponent(Graphics);
|
||||
if (graphics) {
|
||||
let color: Color;
|
||||
if (percentage > 0.6) {
|
||||
color = new Color(0, 255, 0, 255);
|
||||
} else if (percentage > 0.3) {
|
||||
color = new Color(255, 255, 0, 255);
|
||||
} else {
|
||||
color = new Color(255, 0, 0, 255);
|
||||
}
|
||||
|
||||
graphics.clear();
|
||||
graphics.fillColor = color;
|
||||
graphics.rect(-75, -4, 150 * percentage, 8);
|
||||
graphics.fill();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateStatus(status: string) {
|
||||
if (this.statusLabel) {
|
||||
this.statusLabel.string = status;
|
||||
}
|
||||
}
|
||||
|
||||
updateActionProgress(actionName: string, progress: number) {
|
||||
if (this.actionLabel) {
|
||||
this.actionLabel.string = actionName;
|
||||
}
|
||||
|
||||
if (this.actionProgressBar) {
|
||||
this.actionProgressBar.progress = Math.max(0, Math.min(1, progress));
|
||||
this.actionProgressBar.node.active = actionName !== '' && progress > 0;
|
||||
}
|
||||
}
|
||||
|
||||
setVisible(visible: boolean) {
|
||||
this.node.active = visible;
|
||||
}
|
||||
|
||||
updateOreCount(hasOre: boolean, warehouseTotal: number) {
|
||||
if (this.oreCountLabel) {
|
||||
this.oreCountLabel.string = hasOre ? '💎1' : '💎0';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "5f877c25-5c26-49c6-bbb5-7ff36323e0a1",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
import { _decorator, Component, Vec3, Node } from 'cc';
|
||||
import { UnitController } from './UnitController';
|
||||
|
||||
const { ccclass } = _decorator;
|
||||
|
||||
/**
|
||||
* 矿工体力系统行为处理器 - 处理挖矿、休息、存储的完整循环
|
||||
* 展示体力驱动的工作-休息循环系统
|
||||
*/
|
||||
@ccclass('RTSBehaviorHandler')
|
||||
export class RTSBehaviorHandler extends Component {
|
||||
|
||||
private unitController: UnitController | null = null;
|
||||
private minerDemo: any = null; // MinerDemo组件引用
|
||||
private lastActionTime: number = 0;
|
||||
private actionCooldown: number = 0.5; // 动作冷却时间,避免频繁切换
|
||||
private minerIndex: number = -1; // 矿工索引,用于找到对应的家
|
||||
|
||||
start() {
|
||||
this.unitController = this.getComponent(UnitController);
|
||||
// 获取场景中的MinerDemo组件
|
||||
this.minerDemo = this.node.parent?.getComponent('MinerDemo');
|
||||
|
||||
if (!this.unitController) {
|
||||
console.error('RTSBehaviorHandler: 未找到UnitController组件');
|
||||
}
|
||||
if (!this.minerDemo) {
|
||||
console.error('RTSBehaviorHandler: 未找到MinerDemo组件');
|
||||
}
|
||||
|
||||
// 从节点名称中提取矿工索引
|
||||
const match = this.node.name.match(/Miner_(\d+)/);
|
||||
if (match) {
|
||||
this.minerIndex = parseInt(match[1]) - 1; // 转换为0基索引
|
||||
}
|
||||
|
||||
this.lastActionTime = Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查动作冷却
|
||||
*/
|
||||
private isActionOnCooldown(): boolean {
|
||||
return (Date.now() - this.lastActionTime) < (this.actionCooldown * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新动作时间
|
||||
*/
|
||||
private updateActionTime() {
|
||||
this.lastActionTime = Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 挖掘金矿(永不枯竭)
|
||||
* @param params 事件参数,包含黑板变量值
|
||||
*/
|
||||
onMineGoldOre(params: any = {}): string {
|
||||
if (!this.unitController || !this.minerDemo) {
|
||||
return 'failure';
|
||||
}
|
||||
|
||||
// 检查体力是否充足
|
||||
if (this.unitController.currentStamina < this.unitController.staminaCostPerMining) {
|
||||
return 'failure';
|
||||
}
|
||||
|
||||
// 检查是否已经携带矿石
|
||||
const hasOre = this.unitController.getBlackboardValue('hasOre');
|
||||
if (hasOre) {
|
||||
return 'failure';
|
||||
}
|
||||
|
||||
// 动作冷却检查
|
||||
if (this.isActionOnCooldown()) {
|
||||
return 'running';
|
||||
}
|
||||
|
||||
// 获取所有金矿
|
||||
const goldMines = this.minerDemo.getAllGoldMines();
|
||||
if (goldMines.length === 0) {
|
||||
return 'failure';
|
||||
}
|
||||
|
||||
// 寻找最近的金矿
|
||||
const currentPos = this.node.worldPosition;
|
||||
let nearestMine: Node | null = null;
|
||||
let minDistance = Infinity;
|
||||
|
||||
for (const mine of goldMines) {
|
||||
if (!mine || !mine.isValid) continue;
|
||||
|
||||
const distance = Vec3.distance(currentPos, mine.worldPosition);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
nearestMine = mine;
|
||||
}
|
||||
}
|
||||
|
||||
if (!nearestMine) {
|
||||
return 'failure';
|
||||
}
|
||||
|
||||
// 检查是否已经到达金矿位置
|
||||
if (minDistance < 2.0) {
|
||||
// 检查是否正在移动
|
||||
const isMoving = this.unitController.getBlackboardValue('isMoving');
|
||||
if (isMoving) {
|
||||
return 'running';
|
||||
}
|
||||
|
||||
// 消耗体力
|
||||
this.unitController.currentStamina = Math.max(0, this.unitController.currentStamina - this.unitController.staminaCostPerMining);
|
||||
|
||||
// 设置携带矿石状态
|
||||
this.unitController.setBlackboardValue('hasOre', true);
|
||||
|
||||
// 通知演示管理器
|
||||
this.minerDemo.mineGoldOre(this.node);
|
||||
|
||||
// 清除移动目标
|
||||
this.unitController.clearTarget();
|
||||
this.unitController.setBlackboardValue('isMoving', false);
|
||||
|
||||
this.updateActionTime();
|
||||
return 'success';
|
||||
} else {
|
||||
// 设置移动目标
|
||||
this.unitController.setTarget(nearestMine.worldPosition);
|
||||
return 'running';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 前往仓库存储矿石
|
||||
* @param params 事件参数,包含黑板变量值
|
||||
*/
|
||||
onStoreOre(params: any = {}): string {
|
||||
if (!this.unitController || !this.minerDemo) {
|
||||
return 'failure';
|
||||
}
|
||||
|
||||
// 检查是否携带矿石
|
||||
const hasOre = this.unitController.getBlackboardValue('hasOre');
|
||||
if (!hasOre) {
|
||||
return 'failure';
|
||||
}
|
||||
|
||||
// 动作冷却检查
|
||||
if (this.isActionOnCooldown()) {
|
||||
return 'running';
|
||||
}
|
||||
|
||||
const warehouse = this.minerDemo.getWarehouse();
|
||||
if (!warehouse || !warehouse.isValid) {
|
||||
return 'failure';
|
||||
}
|
||||
|
||||
// 计算到仓库的距离
|
||||
const currentPos = this.node.worldPosition;
|
||||
const warehousePos = warehouse.worldPosition;
|
||||
const distance = Vec3.distance(currentPos, warehousePos);
|
||||
|
||||
// 检查是否已经到达仓库
|
||||
if (distance < 2.5) {
|
||||
// 检查是否正在移动
|
||||
const isMoving = this.unitController.getBlackboardValue('isMoving');
|
||||
if (isMoving) {
|
||||
return 'running';
|
||||
}
|
||||
|
||||
// 清除携带矿石状态
|
||||
this.unitController.setBlackboardValue('hasOre', false);
|
||||
|
||||
// 清除移动目标
|
||||
this.unitController.clearTarget();
|
||||
this.unitController.setBlackboardValue('isMoving', false);
|
||||
|
||||
this.updateActionTime();
|
||||
return 'success';
|
||||
} else {
|
||||
// 设置移动目标
|
||||
this.unitController.setTarget(warehousePos);
|
||||
return 'running';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 回家休息
|
||||
* @param params 事件参数,包含黑板变量值
|
||||
*/
|
||||
onGoHomeRest(params: any = {}): string {
|
||||
if (!this.unitController || !this.minerDemo) {
|
||||
return 'failure';
|
||||
}
|
||||
|
||||
// 动作冷却检查
|
||||
if (this.isActionOnCooldown()) {
|
||||
return 'running';
|
||||
}
|
||||
|
||||
// 获取矿工的家
|
||||
const home = this.minerDemo.getMinerHome(this.minerIndex);
|
||||
if (!home || !home.isValid) {
|
||||
return 'failure';
|
||||
}
|
||||
|
||||
// 计算到家的距离
|
||||
const currentPos = this.node.worldPosition;
|
||||
const homePos = home.worldPosition;
|
||||
const distance = Vec3.distance(currentPos, homePos);
|
||||
|
||||
// 检查是否已经到达家
|
||||
if (distance < 2.0) {
|
||||
// 检查是否正在移动
|
||||
const isMoving = this.unitController.getBlackboardValue('isMoving');
|
||||
if (isMoving) {
|
||||
return 'running';
|
||||
}
|
||||
|
||||
// 设置休息状态
|
||||
this.unitController.setBlackboardValue('isResting', true);
|
||||
|
||||
// 清除移动目标
|
||||
this.unitController.clearTarget();
|
||||
this.unitController.setBlackboardValue('isMoving', false);
|
||||
|
||||
this.updateActionTime();
|
||||
return 'success';
|
||||
} else {
|
||||
// 设置移动目标
|
||||
this.unitController.setTarget(homePos);
|
||||
return 'running';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复体力
|
||||
* @param params 事件参数,包含黑板变量值
|
||||
*/
|
||||
onRecoverStamina(params: any = {}): string {
|
||||
if (!this.unitController) {
|
||||
return 'failure';
|
||||
}
|
||||
|
||||
// 检查是否在家中
|
||||
const isResting = this.unitController.getBlackboardValue('isResting');
|
||||
if (!isResting) {
|
||||
return 'failure';
|
||||
}
|
||||
|
||||
// 恢复体力
|
||||
const oldStamina = this.unitController.currentStamina;
|
||||
this.unitController.currentStamina = Math.min(this.unitController.maxStamina,
|
||||
this.unitController.currentStamina + this.unitController.staminaRecoveryRate * 0.1); // 每次恢复2点体力
|
||||
|
||||
const isFullyRested = this.unitController.currentStamina >= this.unitController.maxStamina;
|
||||
|
||||
if (isFullyRested) {
|
||||
// 清除休息状态
|
||||
this.unitController.setBlackboardValue('isResting', false);
|
||||
|
||||
// 通知演示管理器
|
||||
this.minerDemo.completeRestCycle();
|
||||
|
||||
this.updateActionTime();
|
||||
return 'success';
|
||||
} else {
|
||||
// 体力还在恢复中
|
||||
return 'running';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 待机行为
|
||||
* @param params 事件参数,包含黑板变量值
|
||||
*/
|
||||
onIdleBehavior(params: any = {}): string {
|
||||
if (!this.unitController) {
|
||||
return 'failure';
|
||||
}
|
||||
|
||||
// 清除移动目标,确保停止移动
|
||||
this.unitController.clearTarget();
|
||||
this.unitController.setBlackboardValue('isMoving', false);
|
||||
|
||||
|
||||
|
||||
return 'success';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取矿工状态摘要
|
||||
*/
|
||||
getMinerStatus(): string {
|
||||
if (!this.unitController) return 'Unknown';
|
||||
|
||||
const hasOre = this.unitController.getBlackboardValue('hasOre');
|
||||
const isMoving = this.unitController.getBlackboardValue('isMoving');
|
||||
const isResting = this.unitController.getBlackboardValue('isResting');
|
||||
const stamina = this.unitController.currentStamina;
|
||||
const maxStamina = this.unitController.maxStamina;
|
||||
|
||||
let status = '';
|
||||
if (isResting) {
|
||||
status = '😴休息中';
|
||||
} else if (hasOre) {
|
||||
status = isMoving ? '🚚运输中' : '📦携带矿石';
|
||||
} else {
|
||||
status = isMoving ? '🚶移动中' : '⛏️挖矿';
|
||||
}
|
||||
|
||||
return `${status} (体力:${stamina.toFixed(0)}/${maxStamina})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调试信息
|
||||
*/
|
||||
getDebugInfo(): any {
|
||||
if (!this.unitController) return {};
|
||||
|
||||
return {
|
||||
name: this.node.name,
|
||||
hasOre: this.unitController.getBlackboardValue('hasOre'),
|
||||
isMoving: this.unitController.getBlackboardValue('isMoving'),
|
||||
isResting: this.unitController.getBlackboardValue('isResting'),
|
||||
stamina: this.unitController.currentStamina,
|
||||
maxStamina: this.unitController.maxStamina,
|
||||
staminaPercentage: this.unitController.currentStamina / this.unitController.maxStamina,
|
||||
isLowStamina: this.unitController.currentStamina < this.unitController.maxStamina * 0.2,
|
||||
status: this.getMinerStatus()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "739ff9ee-42d5-4542-bb5b-3e7611c729e2",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import { _decorator, Component, Node, Vec3, MeshRenderer, BoxCollider, RigidBody, Material, Color, primitives, utils } from 'cc';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/**
|
||||
* 简单预制体工厂
|
||||
*/
|
||||
@ccclass('SimplePrefabFactory')
|
||||
export class SimplePrefabFactory extends Component {
|
||||
|
||||
/**
|
||||
* 创建单位节点
|
||||
*/
|
||||
static createUnit(name: string, color: Color = Color.WHITE): Node {
|
||||
const unit = new Node(name);
|
||||
|
||||
// 添加网格渲染器
|
||||
const meshRenderer = unit.addComponent(MeshRenderer);
|
||||
|
||||
// 创建立方体网格
|
||||
const mesh = utils.createMesh(primitives.box({ width: 1, height: 1, length: 1 }));
|
||||
meshRenderer.mesh = mesh;
|
||||
|
||||
// 创建材质
|
||||
const material = new Material();
|
||||
material.initialize({ effectName: 'builtin-unlit' });
|
||||
material.setProperty('mainColor', color);
|
||||
meshRenderer.material = material;
|
||||
|
||||
// 添加碰撞器
|
||||
const collider = unit.addComponent(BoxCollider);
|
||||
collider.size = new Vec3(1, 1, 1);
|
||||
|
||||
// 添加刚体
|
||||
const rigidBody = unit.addComponent(RigidBody);
|
||||
rigidBody.type = RigidBody.Type.KINEMATIC;
|
||||
|
||||
return unit;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建建筑节点
|
||||
*/
|
||||
static createBuilding(name: string, size: Vec3 = new Vec3(2, 2, 2), color: Color = Color.GRAY): Node {
|
||||
const building = new Node(name);
|
||||
|
||||
// 添加网格渲染器
|
||||
const meshRenderer = building.addComponent(MeshRenderer);
|
||||
|
||||
// 创建立方体网格
|
||||
const mesh = utils.createMesh(primitives.box({
|
||||
width: size.x,
|
||||
height: size.y,
|
||||
length: size.z
|
||||
}));
|
||||
meshRenderer.mesh = mesh;
|
||||
|
||||
// 创建材质
|
||||
const material = new Material();
|
||||
material.initialize({ effectName: 'builtin-unlit' });
|
||||
material.setProperty('mainColor', color);
|
||||
meshRenderer.material = material;
|
||||
|
||||
// 添加碰撞器
|
||||
const collider = building.addComponent(BoxCollider);
|
||||
collider.size = size;
|
||||
|
||||
return building;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建资源节点
|
||||
*/
|
||||
static createResource(name: string, color: Color = Color.YELLOW): Node {
|
||||
const resource = new Node(name);
|
||||
|
||||
// 添加网格渲染器
|
||||
const meshRenderer = resource.addComponent(MeshRenderer);
|
||||
|
||||
// 创建球体网格
|
||||
const mesh = utils.createMesh(primitives.sphere(0.5));
|
||||
meshRenderer.mesh = mesh;
|
||||
|
||||
// 创建材质
|
||||
const material = new Material();
|
||||
material.initialize({ effectName: 'builtin-unlit' });
|
||||
material.setProperty('mainColor', color);
|
||||
meshRenderer.material = material;
|
||||
|
||||
// 添加碰撞器
|
||||
const collider = resource.addComponent(BoxCollider);
|
||||
collider.size = new Vec3(1, 1, 1);
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建地面节点
|
||||
*/
|
||||
static createGround(size: Vec3 = new Vec3(50, 0.1, 50), color: Color = new Color(100, 150, 100, 255)): Node {
|
||||
const ground = new Node('Ground');
|
||||
|
||||
// 添加网格渲染器
|
||||
const meshRenderer = ground.addComponent(MeshRenderer);
|
||||
|
||||
// 创建平面网格
|
||||
const mesh = utils.createMesh(primitives.box({
|
||||
width: size.x,
|
||||
height: size.y,
|
||||
length: size.z
|
||||
}));
|
||||
meshRenderer.mesh = mesh;
|
||||
|
||||
// 创建材质
|
||||
const material = new Material();
|
||||
material.initialize({ effectName: 'builtin-unlit' });
|
||||
material.setProperty('mainColor', color);
|
||||
meshRenderer.material = material;
|
||||
|
||||
// 添加碰撞器
|
||||
const collider = ground.addComponent(BoxCollider);
|
||||
collider.size = size;
|
||||
|
||||
return ground;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "ac45cfc7-cf47-4315-bdf0-ba002b45b4b6",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
import { Component, _decorator, Node, Label, ProgressBar, UITransform, Widget, Canvas, find, director, Color, Sprite, Layers, Graphics } from 'cc';
|
||||
import { MinerStatusUI } from './MinerStatusUI';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/**
|
||||
* 状态UI管理器
|
||||
* 负责创建和管理游戏对象的状态显示界面
|
||||
*/
|
||||
@ccclass('StatusUIManager')
|
||||
export class StatusUIManager extends Component {
|
||||
|
||||
/**
|
||||
* 为矿工创建状态显示UI
|
||||
*/
|
||||
static createStatusUIForMiner(miner: Node): MinerStatusUI | null {
|
||||
const canvas = find('Canvas') || director.getScene()?.getChildByName('Canvas');
|
||||
if (!canvas) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const uiRoot = new Node(`${miner.name}_StatusUI`);
|
||||
canvas.addChild(uiRoot);
|
||||
|
||||
const uiTransform = uiRoot.addComponent(UITransform);
|
||||
uiTransform.setContentSize(200, 100);
|
||||
|
||||
const borderNode = new Node('Border');
|
||||
uiRoot.addChild(borderNode);
|
||||
const borderTransform = borderNode.addComponent(UITransform);
|
||||
borderTransform.setContentSize(202, 102);
|
||||
const borderGraphics = borderNode.addComponent(Graphics);
|
||||
borderGraphics.fillColor = new Color(100, 100, 100, 120);
|
||||
borderGraphics.rect(-101, -51, 202, 102);
|
||||
borderGraphics.fill();
|
||||
|
||||
const borderWidget = borderNode.addComponent(Widget);
|
||||
borderWidget.isAlignTop = true;
|
||||
borderWidget.isAlignBottom = true;
|
||||
borderWidget.isAlignLeft = true;
|
||||
borderWidget.isAlignRight = true;
|
||||
borderWidget.top = -1;
|
||||
borderWidget.bottom = -1;
|
||||
borderWidget.left = -1;
|
||||
borderWidget.right = -1;
|
||||
borderWidget.updateAlignment();
|
||||
|
||||
const backgroundNode = new Node('Background');
|
||||
uiRoot.addChild(backgroundNode);
|
||||
const backgroundTransform = backgroundNode.addComponent(UITransform);
|
||||
backgroundTransform.setContentSize(200, 100);
|
||||
const backgroundGraphics = backgroundNode.addComponent(Graphics);
|
||||
backgroundGraphics.fillColor = new Color(0, 0, 0, 100);
|
||||
backgroundGraphics.rect(-100, -50, 200, 100);
|
||||
backgroundGraphics.fill();
|
||||
|
||||
const statusUI = uiRoot.addComponent(MinerStatusUI);
|
||||
statusUI.setFollowTarget(miner);
|
||||
|
||||
const nameNode = new Node('NameLabel');
|
||||
uiRoot.addChild(nameNode);
|
||||
const nameTransform = nameNode.addComponent(UITransform);
|
||||
nameTransform.setContentSize(200, 25);
|
||||
const nameLabel = nameNode.addComponent(Label);
|
||||
nameLabel.string = miner.name;
|
||||
nameLabel.fontSize = 16;
|
||||
nameLabel.color = new Color(255, 255, 255, 255);
|
||||
|
||||
const nameWidget = nameNode.addComponent(Widget);
|
||||
nameWidget.isAlignTop = true;
|
||||
nameWidget.top = 0;
|
||||
nameWidget.isAlignHorizontalCenter = true;
|
||||
nameWidget.updateAlignment();
|
||||
|
||||
// 创建状态标签
|
||||
const statusNode = new Node('StatusLabel');
|
||||
uiRoot.addChild(statusNode);
|
||||
const statusTransform = statusNode.addComponent(UITransform);
|
||||
statusTransform.setContentSize(200, 20);
|
||||
const statusLabel = statusNode.addComponent(Label);
|
||||
statusLabel.string = '待机中';
|
||||
statusLabel.fontSize = 14;
|
||||
statusLabel.color = new Color(200, 200, 200, 255);
|
||||
|
||||
// 设置状态标签位置
|
||||
const statusWidget = statusNode.addComponent(Widget);
|
||||
statusWidget.isAlignTop = true;
|
||||
statusWidget.top = 25;
|
||||
statusWidget.isAlignHorizontalCenter = true;
|
||||
statusWidget.updateAlignment();
|
||||
|
||||
// 创建体力进度条
|
||||
const staminaBarNode = new Node('StaminaBar');
|
||||
uiRoot.addChild(staminaBarNode);
|
||||
const staminaBarTransform = staminaBarNode.addComponent(UITransform);
|
||||
staminaBarTransform.setContentSize(150, 8);
|
||||
const staminaBar = staminaBarNode.addComponent(ProgressBar);
|
||||
staminaBar.progress = 1.0;
|
||||
|
||||
// 创建体力进度条背景
|
||||
const staminaBgNode = new Node('Background');
|
||||
staminaBarNode.addChild(staminaBgNode);
|
||||
const staminaBgTransform = staminaBgNode.addComponent(UITransform);
|
||||
staminaBgTransform.setContentSize(150, 8);
|
||||
const staminaBgGraphics = staminaBgNode.addComponent(Graphics);
|
||||
staminaBgGraphics.fillColor = new Color(50, 50, 50, 255);
|
||||
staminaBgGraphics.rect(-75, -4, 150, 8);
|
||||
staminaBgGraphics.fill();
|
||||
|
||||
// 创建体力进度条填充
|
||||
const staminaFillNode = new Node('Bar');
|
||||
staminaBarNode.addChild(staminaFillNode);
|
||||
const staminaFillTransform = staminaFillNode.addComponent(UITransform);
|
||||
staminaFillTransform.setContentSize(150, 8);
|
||||
const staminaFillGraphics = staminaFillNode.addComponent(Graphics);
|
||||
staminaFillGraphics.fillColor = new Color(0, 255, 0, 255);
|
||||
staminaFillGraphics.rect(-75, -4, 150, 8);
|
||||
staminaFillGraphics.fill();
|
||||
|
||||
// 设置体力进度条位置
|
||||
const staminaWidget = staminaBarNode.addComponent(Widget);
|
||||
staminaWidget.isAlignTop = true;
|
||||
staminaWidget.top = 45;
|
||||
staminaWidget.isAlignHorizontalCenter = true;
|
||||
staminaWidget.updateAlignment();
|
||||
|
||||
// 创建动作进度条
|
||||
const actionBarNode = new Node('ActionProgressBar');
|
||||
uiRoot.addChild(actionBarNode);
|
||||
const actionBarTransform = actionBarNode.addComponent(UITransform);
|
||||
actionBarTransform.setContentSize(150, 6);
|
||||
const actionBar = actionBarNode.addComponent(ProgressBar);
|
||||
actionBar.progress = 0;
|
||||
actionBarNode.active = false; // 初始隐藏
|
||||
|
||||
// 创建动作进度条背景
|
||||
const actionBgNode = new Node('Background');
|
||||
actionBarNode.addChild(actionBgNode);
|
||||
const actionBgTransform = actionBgNode.addComponent(UITransform);
|
||||
actionBgTransform.setContentSize(150, 6);
|
||||
const actionBgGraphics = actionBgNode.addComponent(Graphics);
|
||||
actionBgGraphics.fillColor = new Color(50, 50, 50, 255);
|
||||
actionBgGraphics.rect(-75, -3, 150, 6);
|
||||
actionBgGraphics.fill();
|
||||
|
||||
// 创建动作进度条填充
|
||||
const actionFillNode = new Node('Bar');
|
||||
actionBarNode.addChild(actionFillNode);
|
||||
const actionFillTransform = actionFillNode.addComponent(UITransform);
|
||||
actionFillTransform.setContentSize(150, 6);
|
||||
const actionFillGraphics = actionFillNode.addComponent(Graphics);
|
||||
actionFillGraphics.fillColor = new Color(255, 255, 0, 255);
|
||||
actionFillGraphics.rect(-75, -3, 150, 6);
|
||||
actionFillGraphics.fill();
|
||||
|
||||
// 设置动作进度条位置
|
||||
const actionWidget = actionBarNode.addComponent(Widget);
|
||||
actionWidget.isAlignTop = true;
|
||||
actionWidget.top = 55;
|
||||
actionWidget.isAlignHorizontalCenter = true;
|
||||
actionWidget.updateAlignment();
|
||||
|
||||
// 创建动作标签
|
||||
const actionLabelNode = new Node('ActionLabel');
|
||||
uiRoot.addChild(actionLabelNode);
|
||||
const actionLabelTransform = actionLabelNode.addComponent(UITransform);
|
||||
actionLabelTransform.setContentSize(200, 15);
|
||||
const actionLabel = actionLabelNode.addComponent(Label);
|
||||
actionLabel.string = '';
|
||||
actionLabel.fontSize = 12;
|
||||
actionLabel.color = new Color(255, 255, 0, 255);
|
||||
|
||||
// 设置动作标签位置
|
||||
const actionLabelWidget = actionLabelNode.addComponent(Widget);
|
||||
actionLabelWidget.isAlignTop = true;
|
||||
actionLabelWidget.top = 65;
|
||||
actionLabelWidget.isAlignHorizontalCenter = true;
|
||||
actionLabelWidget.updateAlignment();
|
||||
|
||||
// 创建矿石数量标签
|
||||
const oreCountNode = new Node('OreCountLabel');
|
||||
uiRoot.addChild(oreCountNode);
|
||||
const oreCountTransform = oreCountNode.addComponent(UITransform);
|
||||
oreCountTransform.setContentSize(100, 15);
|
||||
const oreCountLabel = oreCountNode.addComponent(Label);
|
||||
oreCountLabel.string = '💎0';
|
||||
oreCountLabel.fontSize = 12;
|
||||
oreCountLabel.color = new Color(255, 215, 0, 255); // 金色
|
||||
|
||||
// 设置矿石数量标签位置(居中显示)
|
||||
const oreCountWidget = oreCountNode.addComponent(Widget);
|
||||
oreCountWidget.isAlignTop = true;
|
||||
oreCountWidget.top = 80;
|
||||
oreCountWidget.isAlignHorizontalCenter = true;
|
||||
oreCountWidget.updateAlignment();
|
||||
|
||||
statusUI.nameLabel = nameLabel;
|
||||
statusUI.statusLabel = statusLabel;
|
||||
statusUI.staminaBar = staminaBar;
|
||||
statusUI.actionProgressBar = actionBar;
|
||||
statusUI.actionLabel = actionLabel;
|
||||
statusUI.oreCountLabel = oreCountLabel;
|
||||
statusUI.warehouseCountLabel = null;
|
||||
|
||||
StatusUIManager.setNodeLayerRecursively(uiRoot, Layers.Enum.UI_2D);
|
||||
return statusUI;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归设置节点及其子节点的层级
|
||||
*/
|
||||
private static setNodeLayerRecursively(node: Node, layer: number) {
|
||||
node.layer = layer;
|
||||
for (const child of node.children) {
|
||||
StatusUIManager.setNodeLayerRecursively(child, layer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从矿工名字中提取索引号
|
||||
*/
|
||||
private static extractMinerIndex(minerName: string): number {
|
||||
const match = minerName.match(/Miner_(\d+)/);
|
||||
if (match) {
|
||||
return parseInt(match[1]) - 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为仓库创建存储量显示UI
|
||||
*/
|
||||
static createWarehouseUI(warehouse: Node): MinerStatusUI | null {
|
||||
const canvas = find('Canvas') || director.getScene()?.getChildByName('Canvas');
|
||||
if (!canvas) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const uiRoot = new Node(`${warehouse.name}_StorageUI`);
|
||||
canvas.addChild(uiRoot);
|
||||
|
||||
const uiTransform = uiRoot.addComponent(UITransform);
|
||||
uiTransform.setContentSize(120, 40);
|
||||
|
||||
const backgroundNode = new Node('Background');
|
||||
uiRoot.addChild(backgroundNode);
|
||||
const backgroundTransform = backgroundNode.addComponent(UITransform);
|
||||
backgroundTransform.setContentSize(120, 40);
|
||||
const backgroundGraphics = backgroundNode.addComponent(Graphics);
|
||||
backgroundGraphics.fillColor = new Color(0, 0, 0, 120);
|
||||
backgroundGraphics.rect(-60, -20, 120, 40);
|
||||
backgroundGraphics.fill();
|
||||
|
||||
const storageNode = new Node('StorageLabel');
|
||||
uiRoot.addChild(storageNode);
|
||||
const storageTransform = storageNode.addComponent(UITransform);
|
||||
storageTransform.setContentSize(120, 30);
|
||||
const storageLabel = storageNode.addComponent(Label);
|
||||
storageLabel.string = '🏭 总存储: 0';
|
||||
storageLabel.fontSize = 14;
|
||||
storageLabel.color = new Color(255, 255, 255, 255);
|
||||
|
||||
const storageWidget = storageNode.addComponent(Widget);
|
||||
storageWidget.isAlignHorizontalCenter = true;
|
||||
storageWidget.isAlignVerticalCenter = true;
|
||||
storageWidget.updateAlignment();
|
||||
|
||||
const statusUI = uiRoot.addComponent(MinerStatusUI);
|
||||
statusUI.setFollowTarget(warehouse);
|
||||
|
||||
statusUI.nameLabel = null;
|
||||
statusUI.statusLabel = null;
|
||||
statusUI.staminaBar = null;
|
||||
statusUI.actionProgressBar = null;
|
||||
statusUI.actionLabel = null;
|
||||
statusUI.oreCountLabel = null;
|
||||
statusUI.warehouseCountLabel = storageLabel;
|
||||
|
||||
StatusUIManager.setNodeLayerRecursively(uiRoot, Layers.Enum.UI_2D);
|
||||
return statusUI;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "7478e794-dd80-4661-9421-8e147d33c51e",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
import { _decorator, Component, Node, Vec3, MeshRenderer, Color, tween } from 'cc';
|
||||
import { BehaviorTreeManager } from './BehaviorTreeManager';
|
||||
import { RTSBehaviorHandler } from './RTSBehaviorHandler';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/**
|
||||
* 单位配置接口
|
||||
*/
|
||||
export interface UnitConfig {
|
||||
unitType: string;
|
||||
behaviorTreeName: string;
|
||||
maxHealth: number;
|
||||
moveSpeed: number;
|
||||
attackRange: number;
|
||||
attackDamage: number;
|
||||
color: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 单位控制器
|
||||
*/
|
||||
@ccclass('UnitController')
|
||||
export class UnitController extends Component {
|
||||
|
||||
@property
|
||||
showDebugInfo: boolean = true;
|
||||
|
||||
// 单位属性
|
||||
public unitType: string = '';
|
||||
public maxHealth: number = 100;
|
||||
public currentHealth: number = 100;
|
||||
public moveSpeed: number = 1.5;
|
||||
public attackRange: number = 2;
|
||||
public attackDamage: number = 25;
|
||||
public isSelected: boolean = false;
|
||||
public currentCommand: string = 'idle';
|
||||
public targetPosition: Vec3 = Vec3.ZERO.clone();
|
||||
public targetNode: Node | null = null;
|
||||
public lastAttackTime: number = 0;
|
||||
public attackCooldown: number = 1.5;
|
||||
public color: string = 'white';
|
||||
|
||||
// 体力系统属性
|
||||
public maxStamina: number = 100;
|
||||
public currentStamina: number = 100;
|
||||
public homePosition: Vec3 = Vec3.ZERO.clone();
|
||||
public staminaRecoveryRate: number = 20; // 每秒恢复的体力
|
||||
public staminaCostPerMining: number = 15; // 每次挖矿消耗的体力
|
||||
|
||||
// 移动状态管理
|
||||
private isMoving: boolean = false;
|
||||
private moveStartTime: number = 0;
|
||||
private lastTargetUpdateTime: number = 0;
|
||||
|
||||
private behaviorTreeManager: BehaviorTreeManager | null = null;
|
||||
private behaviorHandler: Component | null = null;
|
||||
private meshRenderer: MeshRenderer | null = null;
|
||||
|
||||
onLoad() {
|
||||
this.meshRenderer = this.getComponent(MeshRenderer);
|
||||
|
||||
// 创建行为树管理器
|
||||
this.behaviorTreeManager = this.addComponent(BehaviorTreeManager);
|
||||
|
||||
// 添加RTS行为处理器
|
||||
try {
|
||||
// 添加RTSBehaviorHandler组件
|
||||
this.behaviorHandler = this.addComponent(RTSBehaviorHandler);
|
||||
} catch (error) {
|
||||
console.warn('RTSBehaviorHandler组件添加失败', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置单位配置
|
||||
*/
|
||||
setup(config: UnitConfig) {
|
||||
this.unitType = config.unitType;
|
||||
this.maxHealth = config.maxHealth;
|
||||
this.currentHealth = config.maxHealth;
|
||||
this.moveSpeed = config.moveSpeed;
|
||||
this.attackRange = config.attackRange;
|
||||
this.attackDamage = config.attackDamage;
|
||||
this.color = config.color;
|
||||
|
||||
// 设置材质颜色
|
||||
this.setUnitColor(config.color);
|
||||
|
||||
// 设置节点名称显示单位类型
|
||||
this.node.name = `${config.unitType.toUpperCase()}_${this.node.name}`;
|
||||
|
||||
// 初始化行为树
|
||||
if (this.behaviorTreeManager) {
|
||||
this.behaviorTreeManager.initializeBehaviorTree(config.behaviorTreeName, this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置单位颜色
|
||||
*/
|
||||
private setUnitColor(colorName: string) {
|
||||
if (!this.meshRenderer || !this.meshRenderer.material) return;
|
||||
|
||||
const colorMap: { [key: string]: Color } = {
|
||||
'red': Color.RED,
|
||||
'green': Color.GREEN,
|
||||
'blue': Color.BLUE,
|
||||
'yellow': Color.YELLOW,
|
||||
'white': Color.WHITE,
|
||||
'cyan': Color.CYAN,
|
||||
'magenta': Color.MAGENTA
|
||||
};
|
||||
|
||||
const color = colorMap[colorName] || Color.WHITE;
|
||||
this.meshRenderer.material.setProperty('mainColor', color);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置选择状态
|
||||
*/
|
||||
setSelected(selected: boolean) {
|
||||
this.isSelected = selected;
|
||||
|
||||
// 视觉效果
|
||||
if (selected) {
|
||||
this.showSelectionEffect();
|
||||
} else {
|
||||
this.hideSelectionEffect();
|
||||
}
|
||||
|
||||
// 更新行为树黑板
|
||||
if (this.behaviorTreeManager) {
|
||||
this.behaviorTreeManager.updateBlackboardValue('isSelected', selected);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示选择效果
|
||||
*/
|
||||
private showSelectionEffect() {
|
||||
// 添加选择圈效果
|
||||
tween(this.node)
|
||||
.to(0.3, { scale: new Vec3(1.1, 1.1, 1.1) })
|
||||
.to(0.3, { scale: Vec3.ONE })
|
||||
.union()
|
||||
.repeatForever()
|
||||
.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏选择效果
|
||||
*/
|
||||
private hideSelectionEffect() {
|
||||
// 停止所有缩放动画
|
||||
tween(this.node).stop();
|
||||
this.node.setScale(Vec3.ONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布命令
|
||||
*/
|
||||
issueCommand(command: string, target?: Vec3 | Node) {
|
||||
this.currentCommand = command;
|
||||
|
||||
// 设置目标
|
||||
if (target instanceof Vec3) {
|
||||
this.targetPosition = target.clone();
|
||||
this.targetNode = null;
|
||||
} else if (target instanceof Node) {
|
||||
this.targetPosition = target.worldPosition.clone();
|
||||
this.targetNode = target;
|
||||
}
|
||||
|
||||
// 更新行为树黑板
|
||||
if (this.behaviorTreeManager) {
|
||||
this.behaviorTreeManager.updateBlackboardValue('currentCommand', command);
|
||||
this.behaviorTreeManager.updateBlackboardValue('hasTarget', target !== undefined);
|
||||
this.behaviorTreeManager.updateBlackboardValue('targetPosition', this.targetPosition);
|
||||
|
||||
if (target instanceof Node) {
|
||||
this.behaviorTreeManager.updateBlackboardValue('targetType',
|
||||
target.name.includes('Resource') ? 'resource' :
|
||||
target.name.includes('Building') ? 'building' : 'unit');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置黑板变量值
|
||||
*/
|
||||
setBlackboardValue(key: string, value: any) {
|
||||
if (this.behaviorTreeManager) {
|
||||
this.behaviorTreeManager.updateBlackboardValue(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取黑板变量值
|
||||
*/
|
||||
getBlackboardValue(key: string): any {
|
||||
return this.behaviorTreeManager?.getBlackboardValue(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置移动目标
|
||||
*/
|
||||
setTarget(position: Vec3) {
|
||||
this.targetPosition = position.clone();
|
||||
this.isMoving = true;
|
||||
this.moveStartTime = Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除移动目标
|
||||
*/
|
||||
clearTarget() {
|
||||
this.targetPosition = Vec3.ZERO.clone();
|
||||
this.isMoving = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 受到伤害
|
||||
*/
|
||||
takeDamage(damage: number) {
|
||||
this.currentHealth = Math.max(0, this.currentHealth - damage);
|
||||
|
||||
// 更新行为树黑板
|
||||
if (this.behaviorTreeManager) {
|
||||
this.behaviorTreeManager.updateBlackboardValue('currentHealth', this.currentHealth);
|
||||
this.behaviorTreeManager.updateBlackboardValue('healthPercentage', this.currentHealth / this.maxHealth);
|
||||
this.behaviorTreeManager.updateBlackboardValue('isLowHealth', this.currentHealth < this.maxHealth * 0.3);
|
||||
}
|
||||
|
||||
// 视觉效果
|
||||
this.showDamageEffect();
|
||||
|
||||
if (this.currentHealth <= 0) {
|
||||
this.die();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示受伤效果
|
||||
*/
|
||||
private showDamageEffect() {
|
||||
if (!this.meshRenderer || !this.meshRenderer.material) return;
|
||||
|
||||
// 闪红效果
|
||||
const originalColor = this.meshRenderer.material.getProperty('mainColor') as Color;
|
||||
this.meshRenderer.material.setProperty('mainColor', Color.RED);
|
||||
|
||||
this.scheduleOnce(() => {
|
||||
if (this.meshRenderer && this.meshRenderer.material) {
|
||||
this.meshRenderer.material.setProperty('mainColor', originalColor);
|
||||
}
|
||||
}, 0.2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 单位死亡
|
||||
*/
|
||||
private die() {
|
||||
console.log(`单位 ${this.node.name} 死亡`);
|
||||
|
||||
// 播放死亡动画后销毁节点
|
||||
tween(this.node)
|
||||
.to(0.5, { scale: Vec3.ZERO })
|
||||
.call(() => {
|
||||
this.node.destroy();
|
||||
})
|
||||
.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动到目标位置(只在水平面移动,不改变Y轴)
|
||||
*/
|
||||
moveToTarget(targetPos: Vec3, speed?: number, deltaTime?: number): boolean {
|
||||
const currentPos = this.node.worldPosition;
|
||||
const distance = Vec3.distance(currentPos, targetPos);
|
||||
|
||||
if (distance < 0.5) {
|
||||
this.isMoving = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
const actualSpeed = speed || this.moveSpeed;
|
||||
const actualDeltaTime = deltaTime || 0.016;
|
||||
const direction = new Vec3();
|
||||
Vec3.subtract(direction, targetPos, currentPos);
|
||||
direction.normalize();
|
||||
|
||||
const moveDistance = actualSpeed * actualDeltaTime;
|
||||
const newPosition = new Vec3();
|
||||
Vec3.scaleAndAdd(newPosition, currentPos, direction, moveDistance);
|
||||
|
||||
this.node.setWorldPosition(newPosition);
|
||||
this.isMoving = true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 攻击目标
|
||||
*/
|
||||
attackTarget(): boolean {
|
||||
const currentTime = Date.now();
|
||||
if (currentTime - this.lastAttackTime < this.attackCooldown * 1000) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.targetNode && this.targetNode.isValid) {
|
||||
const distance = Vec3.distance(this.node.worldPosition, this.targetNode.worldPosition);
|
||||
if (distance <= this.attackRange) {
|
||||
this.lastAttackTime = currentTime;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
update(deltaTime: number) {
|
||||
if (this.behaviorTreeManager) {
|
||||
this.behaviorTreeManager.update(deltaTime);
|
||||
}
|
||||
|
||||
if (this.isMoving && !this.targetPosition.equals(Vec3.ZERO)) {
|
||||
const reached = this.moveToTarget(this.targetPosition, this.moveSpeed, deltaTime);
|
||||
if (reached) {
|
||||
this.clearTarget();
|
||||
}
|
||||
}
|
||||
|
||||
// 调试信息显示
|
||||
if (this.showDebugInfo) {
|
||||
this.updateDebugInfo();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新调试信息
|
||||
*/
|
||||
private updateDebugInfo() {
|
||||
// 可以在这里添加调试信息的显示逻辑
|
||||
// 比如在单位上方显示状态文本等
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
// 停止所有动画
|
||||
tween(this.node).stop();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "4ac64480-2d09-4de6-a22c-add022790676",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
9
extensions/cocos/cocos-ecs/assets/scripts/ecs.meta
Normal file
9
extensions/cocos/cocos-ecs/assets/scripts/ecs.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "a1f43720-46e1-4d07-b56a-c9307e45726c",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
103
extensions/cocos/cocos-ecs/assets/scripts/ecs/ECSManager.ts
Normal file
103
extensions/cocos/cocos-ecs/assets/scripts/ecs/ECSManager.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import { Component, _decorator } from 'cc';
|
||||
import { GameScene } from './scenes/GameScene';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/**
|
||||
* ECS管理器 - Cocos Creator组件
|
||||
* 将此组件添加到场景中的任意节点上即可启动ECS框架
|
||||
*
|
||||
* 使用说明:
|
||||
* 1. 在Cocos Creator场景中创建一个空节点
|
||||
* 2. 将此ECSManager组件添加到该节点
|
||||
* 3. 运行场景即可自动启动ECS框架
|
||||
*/
|
||||
@ccclass('ECSManager')
|
||||
export class ECSManager extends Component {
|
||||
|
||||
@property({
|
||||
tooltip: '是否启用调试模式(建议开发阶段开启)'
|
||||
})
|
||||
public debugMode: boolean = true;
|
||||
|
||||
private isInitialized: boolean = false;
|
||||
|
||||
/**
|
||||
* 组件启动时初始化ECS
|
||||
*/
|
||||
start() {
|
||||
this.initializeECS();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化ECS框架
|
||||
*/
|
||||
private initializeECS(): void {
|
||||
if (this.isInitialized) return;
|
||||
|
||||
// ECS框架初始化开始
|
||||
|
||||
try {
|
||||
// 1. 创建Core实例,启用调试功能
|
||||
if (this.debugMode) {
|
||||
Core.create({
|
||||
debug: true,
|
||||
enableEntitySystems: true,
|
||||
debugConfig: {
|
||||
enabled: true,
|
||||
websocketUrl: 'ws://localhost:8080',
|
||||
autoReconnect: true,
|
||||
debugFrameRate: 30,
|
||||
channels: {
|
||||
entities: true,
|
||||
systems: true,
|
||||
performance: true,
|
||||
components: true,
|
||||
scenes: true
|
||||
}
|
||||
}
|
||||
});
|
||||
console.log('✅ ECS调试模式已启用');
|
||||
} else {
|
||||
Core.create({
|
||||
debug: false,
|
||||
enableEntitySystems: true
|
||||
});
|
||||
console.log('ℹ️ ECS调试模式已禁用');
|
||||
}
|
||||
|
||||
// 2. 创建游戏场景
|
||||
const gameScene = new GameScene();
|
||||
|
||||
// 3. 设置为当前场景(会自动调用scene.begin())
|
||||
Core.setScene(gameScene);
|
||||
|
||||
this.isInitialized = true;
|
||||
// ECS框架初始化完成
|
||||
|
||||
} catch (error) {
|
||||
console.error('ECS框架初始化失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 每帧更新ECS框架
|
||||
*/
|
||||
update(deltaTime: number) {
|
||||
if (this.isInitialized) {
|
||||
// 更新ECS核心系统
|
||||
Core.update(deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件销毁时清理ECS
|
||||
*/
|
||||
onDestroy() {
|
||||
if (this.isInitialized) {
|
||||
// ECS框架清理
|
||||
this.isInitialized = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "b89656f0-6320-4b6d-81cd-447bf811230c",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
153
extensions/cocos/cocos-ecs/assets/scripts/ecs/README.md
Normal file
153
extensions/cocos/cocos-ecs/assets/scripts/ecs/README.md
Normal file
@@ -0,0 +1,153 @@
|
||||
# ECS框架启动模板
|
||||
|
||||
欢迎使用ECS框架!这是一个最基础的启动模板,帮助您快速开始ECS项目开发。
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
```
|
||||
ecs/
|
||||
├── components/ # 组件目录(请在此添加您的组件)
|
||||
├── systems/ # 系统目录(请在此添加您的系统)
|
||||
├── scenes/ # 场景目录
|
||||
│ └── GameScene.ts # 主游戏场景
|
||||
├── ECSManager.ts # ECS管理器组件
|
||||
└── README.md # 本文档
|
||||
```
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 启动ECS框架
|
||||
|
||||
ECS框架已经配置完成!您只需要:
|
||||
|
||||
1. 在Cocos Creator中打开您的场景
|
||||
2. 创建一个空节点(例如命名为"ECSManager")
|
||||
3. 将 `ECSManager` 组件添加到该节点
|
||||
4. 运行场景,ECS框架将自动启动
|
||||
|
||||
### 2. 查看控制台输出
|
||||
|
||||
如果一切正常,您将在控制台看到:
|
||||
|
||||
```
|
||||
🎮 正在初始化ECS框架...
|
||||
🔧 ECS调试模式已启用,可在Cocos Creator扩展面板中查看调试信息
|
||||
🎯 游戏场景已创建
|
||||
✅ ECS框架初始化成功!
|
||||
🚀 游戏场景已启动
|
||||
```
|
||||
|
||||
### 3. 使用调试面板
|
||||
|
||||
ECS框架已启用调试功能,您可以:
|
||||
|
||||
1. 在Cocos Creator编辑器菜单中选择 "扩展" → "ECS Framework" → "调试面板"
|
||||
2. 调试面板将显示实时的ECS运行状态:
|
||||
- 实体数量和状态
|
||||
- 系统执行信息
|
||||
- 性能监控数据
|
||||
- 组件统计信息
|
||||
|
||||
**注意**:调试功能会消耗一定性能,正式发布时建议关闭调试模式。
|
||||
|
||||
## 📚 下一步开发
|
||||
|
||||
### 创建您的第一个组件
|
||||
|
||||
在 `components/` 目录下创建组件:
|
||||
|
||||
```typescript
|
||||
// components/PositionComponent.ts
|
||||
import { Component } from '@esengine/ecs-framework';
|
||||
import { Vec3 } from 'cc';
|
||||
|
||||
export class PositionComponent extends Component {
|
||||
public position: Vec3 = new Vec3();
|
||||
|
||||
constructor(x: number = 0, y: number = 0, z: number = 0) {
|
||||
super();
|
||||
this.position.set(x, y, z);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 创建您的第一个系统
|
||||
|
||||
在 `systems/` 目录下创建系统:
|
||||
|
||||
```typescript
|
||||
// systems/MovementSystem.ts
|
||||
import { EntitySystem, Entity, Matcher } from '@esengine/ecs-framework';
|
||||
import { PositionComponent } from '../components/PositionComponent';
|
||||
|
||||
export class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(PositionComponent));
|
||||
}
|
||||
|
||||
protected process(entities: Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const position = entity.getComponent(PositionComponent);
|
||||
if (position) {
|
||||
// TODO: 在这里编写移动逻辑
|
||||
console.log(`实体 ${entity.name} 位置: ${position.position}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 在场景中注册系统
|
||||
|
||||
在 `scenes/GameScene.ts` 的 `initialize()` 方法中添加:
|
||||
|
||||
```typescript
|
||||
import { MovementSystem } from '../systems/MovementSystem';
|
||||
|
||||
public initialize(): void {
|
||||
super.initialize();
|
||||
this.name = "MainGameScene";
|
||||
|
||||
// 添加系统
|
||||
this.addEntityProcessor(new MovementSystem());
|
||||
|
||||
// 创建测试实体
|
||||
const testEntity = this.createEntity("TestEntity");
|
||||
testEntity.addComponent(new PositionComponent(0, 0, 0));
|
||||
}
|
||||
```
|
||||
|
||||
## 🔗 学习资源
|
||||
|
||||
- [ECS框架完整文档](https://github.com/esengine/ecs-framework)
|
||||
- [ECS概念详解](https://github.com/esengine/ecs-framework/blob/master/docs/concepts-explained.md)
|
||||
- [新手教程](https://github.com/esengine/ecs-framework/blob/master/docs/beginner-tutorials.md)
|
||||
- [组件设计指南](https://github.com/esengine/ecs-framework/blob/master/docs/component-design-guide.md)
|
||||
- [系统开发指南](https://github.com/esengine/ecs-framework/blob/master/docs/system-guide.md)
|
||||
|
||||
## 💡 开发提示
|
||||
|
||||
1. **组件只存储数据**:避免在组件中编写复杂逻辑
|
||||
2. **系统处理逻辑**:所有业务逻辑应该在系统中实现
|
||||
3. **使用Matcher过滤实体**:系统通过Matcher指定需要处理的实体类型
|
||||
4. **性能优化**:大量实体时考虑使用位掩码查询和组件索引
|
||||
|
||||
## ❓ 常见问题
|
||||
|
||||
### Q: 如何创建实体?
|
||||
A: 在场景中使用 `this.createEntity("实体名称")`
|
||||
|
||||
### Q: 如何给实体添加组件?
|
||||
A: 使用 `entity.addComponent(new YourComponent())`
|
||||
|
||||
### Q: 如何获取实体的组件?
|
||||
A: 使用 `entity.getComponent(YourComponent)`
|
||||
|
||||
### Q: 如何删除实体?
|
||||
A: 使用 `entity.destroy()` 或 `this.destroyEntity(entity)`
|
||||
|
||||
---
|
||||
|
||||
🎮 **开始您的ECS开发之旅吧!**
|
||||
|
||||
如有问题,请查阅官方文档或提交Issue。
|
||||
11
extensions/cocos/cocos-ecs/assets/scripts/ecs/README.md.meta
Normal file
11
extensions/cocos/cocos-ecs/assets/scripts/ecs/README.md.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"ver": "1.0.1",
|
||||
"importer": "text",
|
||||
"imported": true,
|
||||
"uuid": "ca94b460-6c6a-4f72-9ec1-ab5fcd2e0e0a",
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "3c7bd2b3-6781-482c-be41-21f3dde0e2ab",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
import { Component } from '@esengine/ecs-framework';
|
||||
import { Vec3 } from 'cc';
|
||||
|
||||
/**
|
||||
* AI组件 - 复杂的AI行为和状态管理
|
||||
*/
|
||||
export class AIComponent extends Component {
|
||||
/** AI状态 */
|
||||
public currentState: 'idle' | 'patrol' | 'chase' | 'attack' | 'flee' | 'dead' = 'idle';
|
||||
|
||||
/** 目标ID(避免循环引用) */
|
||||
public targetId: number | null = null;
|
||||
|
||||
/** AI伙伴ID列表(避免循环引用) */
|
||||
public allyIds: number[] = [];
|
||||
|
||||
/** 敌人ID列表 */
|
||||
public enemyIds: number[] = [];
|
||||
|
||||
/** 复杂的AI配置 */
|
||||
public config: {
|
||||
personality: {
|
||||
aggression: number; // 攻击性 0-1
|
||||
curiosity: number; // 好奇心 0-1
|
||||
loyalty: number; // 忠诚度 0-1
|
||||
intelligence: number; // 智力 0-1
|
||||
};
|
||||
capabilities: {
|
||||
sightRange: number;
|
||||
hearingRange: number;
|
||||
movementSpeed: number;
|
||||
attackDamage: number;
|
||||
health: number;
|
||||
};
|
||||
behaviorTree: {
|
||||
rootNode: BehaviorNode;
|
||||
blackboard: Map<string, any>;
|
||||
executionHistory: BehaviorExecution[];
|
||||
};
|
||||
memory: {
|
||||
lastSeenEnemyPosition: Vec3 | null;
|
||||
lastSeenEnemyTime: number;
|
||||
knownLocations: Array<{
|
||||
position: Vec3;
|
||||
type: 'safe' | 'danger' | 'resource' | 'patrol';
|
||||
confidence: number;
|
||||
lastVisited: number;
|
||||
}>;
|
||||
relationships: Map<number, {
|
||||
entityId: number;
|
||||
relationship: 'ally' | 'enemy' | 'neutral';
|
||||
trustLevel: number;
|
||||
lastInteraction: number;
|
||||
interactionHistory: Array<{
|
||||
type: 'friendly' | 'hostile' | 'neutral';
|
||||
timestamp: number;
|
||||
impact: number;
|
||||
}>;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
|
||||
/** 状态机 */
|
||||
public stateMachine: {
|
||||
states: Map<string, AIState>;
|
||||
transitions: Map<string, Array<{
|
||||
targetState: string;
|
||||
condition: () => boolean;
|
||||
priority: number;
|
||||
}>>;
|
||||
stateHistory: Array<{
|
||||
state: string;
|
||||
enterTime: number;
|
||||
exitTime: number;
|
||||
data: any;
|
||||
}>;
|
||||
};
|
||||
|
||||
/** 感知系统 */
|
||||
public perception: {
|
||||
visibleEntities: Array<{
|
||||
entityId: number;
|
||||
position: Vec3;
|
||||
distance: number;
|
||||
angle: number;
|
||||
lastSeen: number;
|
||||
componentId?: number; // 使用组件ID避免循环引用
|
||||
}>;
|
||||
audibleSounds: Array<{
|
||||
source: Vec3;
|
||||
volume: number;
|
||||
type: string;
|
||||
timestamp: number;
|
||||
}>;
|
||||
tacticleInfo: Array<{
|
||||
entityId: number;
|
||||
contactPoint: Vec3;
|
||||
force: number;
|
||||
timestamp: number;
|
||||
}>;
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// 初始化AI配置
|
||||
this.config = {
|
||||
personality: {
|
||||
aggression: Math.random(),
|
||||
curiosity: Math.random(),
|
||||
loyalty: Math.random(),
|
||||
intelligence: Math.random()
|
||||
},
|
||||
capabilities: {
|
||||
sightRange: 100 + Math.random() * 100,
|
||||
hearingRange: 50 + Math.random() * 50,
|
||||
movementSpeed: 80 + Math.random() * 40,
|
||||
attackDamage: 10 + Math.random() * 20,
|
||||
health: 80 + Math.random() * 40
|
||||
},
|
||||
behaviorTree: {
|
||||
rootNode: new BehaviorNode('root'),
|
||||
blackboard: new Map(),
|
||||
executionHistory: []
|
||||
},
|
||||
memory: {
|
||||
lastSeenEnemyPosition: null,
|
||||
lastSeenEnemyTime: 0,
|
||||
knownLocations: [],
|
||||
relationships: new Map()
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化状态机
|
||||
this.stateMachine = {
|
||||
states: new Map(),
|
||||
transitions: new Map(),
|
||||
stateHistory: []
|
||||
};
|
||||
|
||||
// 初始化感知系统
|
||||
this.perception = {
|
||||
visibleEntities: [],
|
||||
audibleSounds: [],
|
||||
tacticleInfo: []
|
||||
};
|
||||
|
||||
this.initializeStateMachine();
|
||||
this.initializeBehaviorTree();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化状态机
|
||||
*/
|
||||
private initializeStateMachine(): void {
|
||||
// 添加基本状态
|
||||
this.stateMachine.states.set('idle', new AIState('idle', this));
|
||||
this.stateMachine.states.set('patrol', new AIState('patrol', this));
|
||||
this.stateMachine.states.set('chase', new AIState('chase', this));
|
||||
this.stateMachine.states.set('attack', new AIState('attack', this));
|
||||
this.stateMachine.states.set('flee', new AIState('flee', this));
|
||||
|
||||
// 设置状态转换
|
||||
this.stateMachine.transitions.set('idle', [
|
||||
{ targetState: 'patrol', condition: () => Math.random() > 0.8, priority: 1 },
|
||||
{ targetState: 'chase', condition: () => this.perception.visibleEntities.length > 0, priority: 3 }
|
||||
]);
|
||||
|
||||
this.stateMachine.transitions.set('patrol', [
|
||||
{ targetState: 'idle', condition: () => Math.random() > 0.9, priority: 1 },
|
||||
{ targetState: 'chase', condition: () => this.hasVisibleEnemies(), priority: 3 }
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化行为树
|
||||
*/
|
||||
private initializeBehaviorTree(): void {
|
||||
const root = this.config.behaviorTree.rootNode;
|
||||
|
||||
// 构建简单的行为树结构
|
||||
const selectorNode = new BehaviorNode('selector');
|
||||
const sequenceNode = new BehaviorNode('sequence');
|
||||
const conditionNode = new BehaviorNode('condition');
|
||||
const actionNode = new BehaviorNode('action');
|
||||
|
||||
root.addChild(selectorNode);
|
||||
selectorNode.addChild(sequenceNode);
|
||||
sequenceNode.addChild(conditionNode);
|
||||
sequenceNode.addChild(actionNode);
|
||||
|
||||
// 设置黑板数据
|
||||
this.config.behaviorTree.blackboard.set('lastPatrolPoint', new Vec3());
|
||||
this.config.behaviorTree.blackboard.set('alertLevel', 0);
|
||||
this.config.behaviorTree.blackboard.set('energy', 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加盟友(避免循环引用)
|
||||
*/
|
||||
public addAlly(allyEntityId: number): void {
|
||||
if (!this.allyIds.includes(allyEntityId)) {
|
||||
this.allyIds.push(allyEntityId);
|
||||
|
||||
// 更新关系记录
|
||||
this.config.memory.relationships.set(allyEntityId, {
|
||||
entityId: allyEntityId,
|
||||
relationship: 'ally',
|
||||
trustLevel: 0.8,
|
||||
lastInteraction: Date.now(),
|
||||
interactionHistory: []
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置目标(避免循环引用)
|
||||
*/
|
||||
public setTarget(targetEntityId: number): void {
|
||||
this.targetId = targetEntityId;
|
||||
// 不再需要双向引用
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新感知信息
|
||||
*/
|
||||
public updatePerception(deltaTime: number): void {
|
||||
// 清理过期的感知信息
|
||||
const currentTime = Date.now();
|
||||
this.perception.visibleEntities = this.perception.visibleEntities.filter(
|
||||
entity => currentTime - entity.lastSeen < 5000
|
||||
);
|
||||
|
||||
this.perception.audibleSounds = this.perception.audibleSounds.filter(
|
||||
sound => currentTime - sound.timestamp < 2000
|
||||
);
|
||||
|
||||
// 更新记忆中的位置信息
|
||||
this.config.memory.knownLocations.forEach(location => {
|
||||
location.confidence *= 0.999; // 随时间衰减可信度
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有可见敌人
|
||||
*/
|
||||
private hasVisibleEnemies(): boolean {
|
||||
return this.perception.visibleEntities.some(entity =>
|
||||
this.config.memory.relationships.get(entity.entityId)?.relationship === 'enemy'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置组件
|
||||
*/
|
||||
public reset(): void {
|
||||
// 清理ID数组(不再需要处理循环引用)
|
||||
this.allyIds = [];
|
||||
this.enemyIds = [];
|
||||
this.targetId = null;
|
||||
this.currentState = 'idle';
|
||||
|
||||
this.config.behaviorTree.blackboard.clear();
|
||||
this.config.memory.relationships.clear();
|
||||
this.perception.visibleEntities = [];
|
||||
this.perception.audibleSounds = [];
|
||||
this.perception.tacticleInfo = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 行为树节点
|
||||
*/
|
||||
class BehaviorNode {
|
||||
public name: string;
|
||||
public children: BehaviorNode[] = [];
|
||||
public parent: BehaviorNode | null = null;
|
||||
public data: Map<string, any> = new Map();
|
||||
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public addChild(child: BehaviorNode): void {
|
||||
this.children.push(child);
|
||||
child.parent = this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 行为执行记录
|
||||
*/
|
||||
interface BehaviorExecution {
|
||||
nodeName: string;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
result: 'success' | 'failure' | 'running';
|
||||
data: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* AI状态
|
||||
*/
|
||||
class AIState {
|
||||
public name: string;
|
||||
public owner: AIComponent;
|
||||
public enterTime: number = 0;
|
||||
public data: Map<string, any> = new Map();
|
||||
|
||||
constructor(name: string, owner: AIComponent) {
|
||||
this.name = name;
|
||||
this.owner = owner;
|
||||
}
|
||||
|
||||
public enter(): void {
|
||||
this.enterTime = Date.now();
|
||||
}
|
||||
|
||||
public exit(): void {
|
||||
// 记录状态历史
|
||||
this.owner.stateMachine.stateHistory.push({
|
||||
state: this.name,
|
||||
enterTime: this.enterTime,
|
||||
exitTime: Date.now(),
|
||||
data: Object.fromEntries(this.data)
|
||||
});
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user