Compare commits

...

476 Commits

Author SHA1 Message Date
YHH
ddc7a7750e Chore/lint fixes (#212)
* fix(eslint): 修复装饰器缩进配置

* fix(eslint): 修复装饰器缩进配置

* chore: 删除未使用的导入

* chore(lint): 移除未使用的导入和变量

* chore(lint): 修复editor-app中未使用的函数参数

* chore(lint): 修复未使用的赋值变量

* chore(eslint): 将所有错误级别改为警告以通过CI

* fix(codeql): 修复GitHub Advanced Security检测到的问题
2025-11-02 23:50:41 +08:00
YHH
50a01d9dd3 chore: 统一并强化ESLint配置规则 2025-11-02 12:45:47 +08:00
YHH
793aad0a5e chore: 移除旧版ESLint配置并更新子模块 2025-11-02 12:22:27 +08:00
YHH
9c1bf8dbed refactor(core): 移除全局EventBus,实现场景级事件隔离 (#211) 2025-11-01 18:19:23 +08:00
YHH
620f3eecc7 style(core): ESLint自动修复代码格式问题 (#210) 2025-11-01 17:41:50 +08:00
YHH
4355538d8d refactor(core): 重构Scene和Entity的代码质量,消除所有Lint警告 (#209) 2025-11-01 17:18:12 +08:00
YHH
3ad5dc9ca3 refactor(core): 改进事件系统类型安全并消除 ESLint 警告 (#208) 2025-11-01 16:12:18 +08:00
YHH
57c7e7be3f feat(core):统一 Core 库的命名规范和代码风格 (#207) 2025-11-01 10:23:46 +08:00
github-actions[bot]
6778ccace4 chore(editor): bump version to 1.0.5 (#201)
Co-authored-by: esengine <18465053+esengine@users.noreply.github.com>
2025-10-31 17:42:41 +08:00
github-actions[bot]
1264232533 chore(behavior-tree): release v1.0.1 (#200)
Co-authored-by: esengine <18465053+esengine@users.noreply.github.com>
2025-10-31 17:30:12 +08:00
YHH
61813e67b6 refactor(behavior-tree)!: 迁移到 Runtime 执行器架构 (#196)
* refactor(behavior-tree)!: 迁移到 Runtime 执行器架构

* fix(behavior-tree): 修复LogAction中的ReDoS安全漏洞

* feat(behavior-tree): 完善行为树核心功能并修复类型错误
2025-10-31 17:27:38 +08:00
YHH
c58e3411fd feat(core): 启用 TypeScript 最严格的类型检查 (#199)
* feat(core):  启用 TypeScript 最严格的类型检查

* ci: 配置 Codecov 以适应类型安全改进

* fix(core): 修复 CodeQL 安全警告

* fix(core): eslint.config.mjs
2025-10-31 16:14:23 +08:00
LINGYE
011d795361 perf(core): 优化 Scene.systems getter 避免每帧重复排序 (#197) 2025-10-30 23:27:37 +08:00
YHH
3f40a04370 Merge branch 'master' of https://github.com/esengine/ecs-framework 2025-10-28 17:19:38 +08:00
YHH
fc042bb7d9 feat(editor): 添加插件多语言支持 2025-10-28 17:19:28 +08:00
github-actions[bot]
d051e52131 chore(core): release v2.2.11 (#195)
Co-authored-by: esengine <18465053+esengine@users.noreply.github.com>
2025-10-28 14:12:56 +08:00
YHH
fb4316aeb9 Merge branch 'master' of https://github.com/esengine/ecs-framework 2025-10-28 14:08:47 +08:00
YHH
683203919f refactor(core): 使用fflate替换msgpack以兼容小游戏环境 2025-10-28 14:08:34 +08:00
github-actions[bot]
a0cddbcae6 chore(core): release v2.2.10 (#194)
Co-authored-by: esengine <18465053+esengine@users.noreply.github.com>
2025-10-28 11:58:05 +08:00
YHH
4e81fc7eba fix(docs): 修复行为树文档中的死链接 2025-10-28 11:54:39 +08:00
YHH
b410e2de47 fix(core): 移除TextEncoder依赖以兼容小游戏环境 2025-10-28 11:51:57 +08:00
YHH
9868c746e1 Merge branch 'master' of https://github.com/esengine/ecs-framework 2025-10-28 11:45:48 +08:00
YHH
f0b4453a5f fix(behavior-tree): 修复插件节点执行问题并完善文档 2025-10-28 11:45:35 +08:00
github-actions[bot]
6b49471734 chore(editor-core): release v1.0.0 (#193)
Co-authored-by: esengine <18465053+esengine@users.noreply.github.com>
2025-10-27 15:10:06 +08:00
YHH
fe791e83a8 fix(ci): 添加--access public参数以支持发布公开scoped包 2025-10-27 15:08:00 +08:00
YHH
edbc9eb27f ci(editor-core): 添加npm发布流程支持 2025-10-27 15:04:31 +08:00
github-actions[bot]
2f63034d9a chore(editor): bump version to 1.0.4 (#192)
Co-authored-by: esengine <18465053+esengine@users.noreply.github.com>
Co-authored-by: YHH <359807859@qq.com>
2025-10-27 10:25:27 +08:00
YHH
dee0e0284a chore(behavior-tree): 移除CI测试脚本 2025-10-27 10:12:35 +08:00
YHH
890e591f2a fix(behavior-tree): 修复发布时缺少publishConfig导致的402错误 2025-10-27 10:07:00 +08:00
YHH
cb6561e27b fix(ci): 修复首次发布时版本号未改变的错误 2025-10-27 10:03:14 +08:00
YHH
7ef70d7f9a Merge branch 'master' of https://github.com/esengine/ecs-framework 2025-10-27 10:00:11 +08:00
YHH
86405c1dcd chore(ci): 禁用发布流程中的测试步骤 2025-10-27 10:00:02 +08:00
github-actions[bot]
60fa259285 chore(core): release v2.2.9 (#191)
Co-authored-by: esengine <18465053+esengine@users.noreply.github.com>
2025-10-27 09:56:54 +08:00
YHH
27f86eece2 chore(ci): 将semantic-release改为手动发布并支持patch/minor/major版本选择 2025-10-27 09:51:44 +08:00
YHH
4cee396ea9 chore(ci): 重构发布流程为手动触发方式并添加behavior-tree包发布支持 2025-10-27 09:47:59 +08:00
YHH
d2ad295b48 chore: 移除Dependabot自动依赖更新配置 2025-10-27 09:36:03 +08:00
YHH
009f8af4e1 Feature/ecs behavior tree (#188)
* feat(behavior-tree): 完全 ECS 化的行为树系统

* feat(editor-app): 添加行为树可视化编辑器

* chore: 移除 Cocos Creator 扩展目录

* feat(editor-app): 行为树编辑器功能增强

* fix(editor-app): 修复 TypeScript 类型错误

* feat(editor-app): 使用 FlexLayout 重构面板系统并优化资产浏览器

* feat(editor-app): 改进编辑器UI样式并修复行为树执行顺序

* feat(behavior-tree,editor-app): 添加装饰器系统并优化编辑器性能

* feat(behavior-tree,editor-app): 添加属性绑定系统

* feat(editor-app,behavior-tree): 优化编辑器UI并改进行为树功能

* feat(editor-app,behavior-tree): 添加全局黑板系统并增强资产浏览器功能

* feat(behavior-tree,editor-app): 添加运行时资产导出系统

* feat(behavior-tree,editor-app): 添加SubTree系统和资产选择器

* feat(behavior-tree,editor-app): 优化系统架构并改进编辑器文件管理

* fix(behavior-tree,editor-app): 修复SubTree节点错误显示空节点警告

* fix(editor-app): 修复局部黑板类型定义文件扩展名错误
2025-10-27 09:29:11 +08:00
LINGYE
0cd99209c4 支持集成第三方日志库 (#190)
* 更新 ILogger 签名

改为纯可变参数兼容主流日志库

* 拆分日志类型与实现

* 新增 setLoggerFactory 方法

* tweak

* getLoggerName 返回类名,默认情况下子类无需重写

* 更新日志说明文档

* 增加测试

* 使用 getSystemInstanceTypeName,避免压缩导致获取类名不一致
2025-10-26 11:53:46 +08:00
YHH
3ea55303dc Merge pull request #187 from esengine/feature/complete-iscene-interface
feat(core): 完善 IScene 接口定义
2025-10-20 17:44:14 +08:00
YHH
c458a5e036 chore(ci): 移除 pr-size-labeler workflow 2025-10-20 17:36:46 +08:00
YHH
c511725d1f chore(ci): 移除 pr-agent 和 mergify 配置 2025-10-20 17:33:30 +08:00
YHH
3876d9b92b feat(core): 完善 IScene 接口定义 2025-10-20 17:24:56 +08:00
YHH
f863c48ab0 feat(ci): 自动告知用户可使用 AI 助手 2025-10-19 10:13:03 +08:00
YHH
10096795a1 ci(deps): 优化 Dependabot 自动化流程减少维护负担 2025-10-19 10:03:35 +08:00
YHH
8b146c8d5f Merge pull request #161 from esengine/dependabot/npm_and_yarn/ws-8.18.3
chore(deps): bump ws from 8.18.2 to 8.18.3
2025-10-19 09:53:24 +08:00
YHH
1208c4ffeb Merge branch 'master' into dependabot/npm_and_yarn/ws-8.18.3 2025-10-19 09:48:30 +08:00
YHH
ec5de97973 fix(ci): 换用 gpt-4o 模型解决 token 超限问题 2025-10-19 01:08:11 +08:00
YHH
0daa92cfb7 fix(ci): 优化 AI Issue Helper 避免 token 超限 2025-10-19 01:03:55 +08:00
YHH
130f466026 fix(ci): 修复 AI Issue Helper 的 MCP 配置和响应传递问题 2025-10-19 01:01:18 +08:00
YHH
f93de87940 fix(ci): 修复 AI 响应包含代码时的 JavaScript 语法错误 2025-10-19 00:59:04 +08:00
YHH
367d97e9bb fix(ci): 修复 AI Issue Helper 的 prompt 文件读取问题 2025-10-19 00:55:47 +08:00
YHH
77701f214c fix(ci): AI Issue Helper 忽略机器人评论 2025-10-19 00:51:23 +08:00
YHH
b5b64f8c41 feat(ci): 为 AI Issue Helper 添加代码检索功能 2025-10-19 00:43:40 +08:00
YHH
ab04ad30f1 fix(ci): 修复 AI Issue Helper 的数据解析问题 2025-10-19 00:39:10 +08:00
YHH
330d9a6fdb fix(ci): 为 AI workflow 添加 models 权限 2025-10-19 00:33:12 +08:00
YHH
e762343142 fix(ci): 修复 AI 工具 workflow 的 YAML 语法错误 2025-10-19 00:26:07 +08:00
YHH
fce9e3d4d6 Merge branch 'master' of https://github.com/esengine/ecs-framework 2025-10-19 00:17:46 +08:00
YHH
0e5855ee4e ci: 添加 AI Issue 智能分析和批量处理工具 2025-10-19 00:16:05 +08:00
dependabot[bot]
ba61737bc7 chore(deps): bump ws from 8.18.2 to 8.18.3
Bumps [ws](https://github.com/websockets/ws) from 8.18.2 to 8.18.3.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.18.2...8.18.3)

---
updated-dependencies:
- dependency-name: ws
  dependency-version: 8.18.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-18 16:16:01 +00:00
YHH
bd7ea1f713 Merge pull request #173 from esengine/dependabot/npm_and_yarn/protobufjs-7.5.4
chore(deps): bump protobufjs from 7.5.3 to 7.5.4
2025-10-19 00:14:35 +08:00
YHH
1abd20edf5 Merge branch 'master' into dependabot/npm_and_yarn/protobufjs-7.5.4 2025-10-19 00:06:07 +08:00
YHH
496513c641 ci: 添加 AI 代码审查工具配置 2025-10-19 00:04:36 +08:00
YHH
848b637f45 docs: 增强 README可视化和统计展示 2025-10-18 23:45:34 +08:00
dependabot[bot]
39049601d4 chore(deps): bump protobufjs from 7.5.3 to 7.5.4
Bumps [protobufjs](https://github.com/protobufjs/protobuf.js) from 7.5.3 to 7.5.4.
- [Release notes](https://github.com/protobufjs/protobuf.js/releases)
- [Changelog](https://github.com/protobufjs/protobuf.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/protobufjs/protobuf.js/compare/protobufjs-v7.5.3...protobufjs-v7.5.4)

---
updated-dependencies:
- dependency-name: protobufjs
  dependency-version: 7.5.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-18 15:30:56 +00:00
YHH
e31cdd17d3 fix(ci): 修复 Mergify 配置语法错误 2025-10-18 23:13:14 +08:00
YHH
2a3f2d49b8 fix(ci): 放宽 commitlint scope 限制 2025-10-18 23:06:05 +08:00
YHH
ca452889d7 chore(deps): 更新 package-lock.json 2025-10-18 22:44:55 +08:00
YHH
2df501ec07 ci: 添加自动化工具提升项目质量 2025-10-18 22:32:39 +08:00
YHH
6b1e6c6fdc fix(ci): 修复发布流程改为手动触发并包含 package-lock 2025-10-18 22:18:47 +08:00
YHH
570e970e1c chore: modernize project infrastructure 2025-10-18 22:06:27 +08:00
YHH
f4e3505d52 规范项目标准,更改为MIT协议 2025-10-18 21:48:44 +08:00
YHH
9af2b9859a 新增deepwiki徽章 2025-10-18 21:16:17 +08:00
YHH
d99d314621 Merge branch 'master' of https://github.com/esengine/ecs-framework 2025-10-18 21:06:43 +08:00
YHH
c5b8b18e33 显示自上个版本发布依赖的更新内容再pr里 2025-10-18 21:06:04 +08:00
YHH
7280265a64 core发布流程里的中文pr 2025-10-18 21:03:00 +08:00
YHH
35fa0ef884 Merge pull request #153 from esengine/release/core-v2.2.8
chore(core): Release v2.2.8
2025-10-18 21:00:39 +08:00
github-actions[bot]
9c778cb71b chore(core): bump version to 2.2.8 2025-10-18 12:59:36 +00:00
YHH
4a401744c1 更新core发布流程 2025-10-18 20:56:59 +08:00
YHH
5f6b2d4d40 更新core流程 2025-10-18 20:45:45 +08:00
YHH
bf25218af2 新增发布core npm自动流程 2025-10-18 20:36:10 +08:00
YHH
1f7f9d9f84 修复端口没有跟随设置更改的问题 2025-10-18 20:21:43 +08:00
YHH
2be53a04e4 Merge pull request #152 from foxling/feature/world-manager-config
feat(core): 支持通过 Core.create() 配置 WorldManager 参数
2025-10-18 13:16:52 +08:00
LING YE
7f56ebc786 feat(core): 支持通过 Core.create() 配置 WorldManager 参数 2025-10-18 13:10:09 +08:00
YHH
a801e4f50e Merge pull request #150 from esengine/issue-149-编辑器支持设置字体大小
支持字体设置大小
2025-10-17 23:47:42 +08:00
YHH
a9f9ad9b94 支持字体设置大小 2025-10-17 23:47:04 +08:00
YHH
3cf1dab5b9 Merge pull request #148 from esengine/issue-147-Scene的构造函数不应该由用户传入性能分析器
performancemonitor由内部框架维护
2025-10-17 22:19:53 +08:00
YHH
63165bbbfc performancemonitor由内部框架维护 2025-10-17 22:13:32 +08:00
YHH
61caad2bef Merge pull request #146 from esengine/issue-132-场景序列化系统
更新图标及场景序列化系统
2025-10-17 18:17:56 +08:00
YHH
b826bbc4c7 更新图标及场景序列化系统 2025-10-17 18:13:31 +08:00
YHH
2ce7dad8d8 Merge pull request #131 from esengine/release/editor-v1.0.3
chore(editor): Release v1.0.3
2025-10-16 23:53:37 +08:00
esengine
dff400bf22 chore(editor): bump version to 1.0.3 2025-10-16 15:52:45 +00:00
YHH
27ce902344 配置createUpdaterArtifacts生成sig 2025-10-16 23:38:11 +08:00
YHH
33ee0a04c6 升级tauri-action 2025-10-16 23:20:55 +08:00
YHH
d68f6922f8 Merge pull request #129 from esengine/release/editor-v1.0.1
chore(editor): Release v1.0.1
2025-10-16 23:09:26 +08:00
esengine
f8539d7958 chore(editor): bump version to 1.0.1 2025-10-16 15:08:22 +00:00
YHH
14dc911e0a Merge branch 'master' of https://github.com/esengine/ecs-framework 2025-10-16 22:55:09 +08:00
YHH
deccb6bf84 修复editor再ci上版本冲突问题 2025-10-16 22:54:58 +08:00
YHH
dacbfcae95 Merge pull request #127 from esengine/imgbot
[ImgBot] Optimize images
2025-10-16 22:49:51 +08:00
ImgBotApp
1b69ed17b7 [ImgBot] Optimize images
*Total -- 496.42kb -> 376.40kb (24.18%)

/screenshots/main_screetshot.png -- 179.18kb -> 121.90kb (31.97%)
/screenshots/performance_profiler.png -- 59.99kb -> 41.74kb (30.43%)
/screenshots/port_manager.png -- 22.70kb -> 16.86kb (25.72%)
/screenshots/about.png -- 38.75kb -> 29.00kb (25.17%)
/screenshots/plugin_manager.png -- 38.49kb -> 29.16kb (24.25%)
/packages/editor-app/src-tauri/icons/icon.svg -- 3.52kb -> 2.76kb (21.6%)
/screenshots/settings.png -- 53.78kb -> 44.90kb (16.51%)
/packages/editor-app/src-tauri/icons/icon.png -- 33.45kb -> 29.78kb (10.98%)
/packages/editor-app/src-tauri/icons/512x512.png -- 33.45kb -> 29.78kb (10.98%)
/packages/editor-app/src-tauri/icons/256x256.png -- 13.97kb -> 12.76kb (8.68%)
/packages/editor-app/src-tauri/icons/128x128@2x.png -- 13.97kb -> 12.76kb (8.68%)
/packages/editor-app/src-tauri/icons/128x128.png -- 5.17kb -> 5.03kb (2.89%)

Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>
2025-10-16 14:45:32 +00:00
YHH
241acc9050 更新文档 2025-10-16 22:45:08 +08:00
YHH
8fa921930c Merge pull request #126 from esengine/issue-125-编辑器热更新
热更新配置
2025-10-16 22:31:26 +08:00
YHH
011e43811a 热更新配置 2025-10-16 22:26:50 +08:00
YHH
9f16debd75 新增rust编译缓存和ts构建缓存 2025-10-16 20:54:33 +08:00
YHH
92c56c439b 移除过时的工作流 2025-10-16 20:50:32 +08:00
YHH
7de6a5af0f v2.2.5 2025-10-16 20:37:16 +08:00
YHH
173a063781 Merge pull request #124 from esengine/issue-122-格式化的标准配置
所有源代码文件使用 LF (Unix 风格)
2025-10-16 20:29:21 +08:00
YHH
e04ac7c909 所有源代码文件使用 LF (Unix 风格)
prettier格式化
eslint代码质量检查
2025-10-16 20:24:45 +08:00
YHH
a6e49e1d47 修复publish-release中release_id错误问题 2025-10-16 20:07:56 +08:00
YHH
f0046c7dc2 新增icons作为编辑器图标 2025-10-16 19:52:17 +08:00
YHH
2a17c47c25 修复ts警告 2025-10-16 18:20:31 +08:00
YHH
8d741bf1b9 Merge pull request #123 from esengine/issue-119-插件化编辑器
Issue 119 插件化编辑器
2025-10-16 17:50:35 +08:00
YHH
c676006632 构建配置 2025-10-16 17:44:57 +08:00
YHH
5bcfd597b9 Merge remote-tracking branch 'remotes/origin/master' into issue-119-插件化编辑器 2025-10-16 17:37:24 +08:00
YHH
3cda3c2238 显示客户端链接的ip:port 2025-10-16 17:33:43 +08:00
YHH
43bdd7e43b 远程读取日志 2025-10-16 17:10:22 +08:00
YHH
1ec7892338 设置界面 2025-10-16 13:07:19 +08:00
YHH
6bcfd48a2f 清理调试日志 2025-10-16 12:21:18 +08:00
YHH
345ef70972 支持color类型 2025-10-16 12:00:17 +08:00
YHH
c876edca0c 调试实体和组件属性 2025-10-16 11:55:41 +08:00
YHH
fcf3def284 收集远端数据再profiler dockpanel上 2025-10-15 23:24:13 +08:00
YHH
6f1a2896dd 性能分析器及端口管理器 2025-10-15 22:30:49 +08:00
YHH
62381f4160 记录上一次操作的面板的size大小持久化 2025-10-15 20:26:40 +08:00
YHH
171805debf 禁用默认右键 2025-10-15 20:23:55 +08:00
YHH
619abcbfbc 插件管理器 2025-10-15 20:10:52 +08:00
YHH
03909924c2 app loading 2025-10-15 18:29:48 +08:00
YHH
f4ea077114 菜单栏 2025-10-15 18:24:13 +08:00
YHH
956ccf9195 2D/3D视口 2025-10-15 18:08:55 +08:00
YHH
e880925e3f 视口视图 2025-10-15 17:28:45 +08:00
YHH
0a860920ad 日志面板 2025-10-15 17:21:59 +08:00
YHH
fb7a1b1282 可动态识别属性 2025-10-15 17:15:05 +08:00
YHH
59970ef7c3 Merge pull request #121 from foxling/fix/parent-child-deserialization-bug
fix: 修复场景反序列化时子实体丢失的问题
2025-10-15 15:55:26 +08:00
LING YE
a7750c2894 fix: 修复场景反序列化时子实体丢失的问题
在场景反序列化过程中,子实体虽然保持了父子引用关系,
但未被添加到 Scene 的实体集合和查询系统中,导致查询时子实体"丢失"。
2025-10-15 15:48:54 +08:00
YHH
b69b81f63a 支持树形资源管理器 2025-10-15 10:08:15 +08:00
YHH
00fc6dfd67 Dock系统,支持Tab和拖放 2025-10-15 09:58:45 +08:00
YHH
82451e9fd3 可拖动调整大小的面板 2025-10-15 09:43:48 +08:00
YHH
d0fcc0e447 项目启动流程和资产浏览器功能 2025-10-15 09:34:44 +08:00
YHH
285279629e 优化加载脚本逻辑 2025-10-15 09:19:30 +08:00
YHH
cbfe09b5e9 组件发现和动态加载系统 2025-10-15 00:40:27 +08:00
YHH
b757c1d06c 项目打开功能 2025-10-15 00:23:19 +08:00
YHH
4550a6146a 组件属性编辑器 2025-10-15 00:15:12 +08:00
YHH
3224bb9696 国际化系统 2025-10-14 23:56:54 +08:00
YHH
3a5e73266e 组件注册与添加 2025-10-14 23:42:06 +08:00
YHH
1cf5641c4c 实体存储和管理服务 2025-10-14 23:31:09 +08:00
YHH
85dad41e60 Tauri 窗口,显示编辑器界面 2025-10-14 23:15:07 +08:00
YHH
bd839cf431 Tauri 编辑器应用框架 2025-10-14 22:53:26 +08:00
YHH
b20b2ae4ce 编辑器核心框架 2025-10-14 22:33:55 +08:00
YHH
cac6aedf78 调试插件 DebugPlugin 2025-10-14 22:12:35 +08:00
YHH
a572c80967 v2.2.4 2025-10-14 21:22:07 +08:00
YHH
a09e8261db Merge pull request #118 from esengine/issue-117-统一组件注册系统,移除ComponentTypeManager重复实现
移除了功能重复的ComponentTypeManager
2025-10-14 18:24:17 +08:00
YHH
62e8ebe926 移除了功能重复的ComponentTypeManager 2025-10-14 18:19:08 +08:00
YHH
96e0a9126f Merge pull request #116 from esengine/issue-94-响应式查询(Reactive_Query_System)/_Event-driven_Query
使用ComponentRegistry来确保与Entity.componentMask使用相同的bitIndex
2025-10-14 17:56:26 +08:00
YHH
4afb195814 Merge remote-tracking branch 'origin/master' into issue-94-响应式查询(Reactive_Query_System)/_Event-driven_Query 2025-10-14 17:51:59 +08:00
YHH
7da5366bca 修复querysystem循环依赖的问题 2025-10-14 17:50:32 +08:00
YHH
d979c38615 使用ComponentRegistry来确保与Entity.componentMask使用相同的bitIndex 2025-10-14 17:34:15 +08:00
YHH
7def06126b Merge pull request #115 from esengine/issue-114-reactive-query_文档
新增实体查询系统文档
2025-10-14 13:39:23 +08:00
YHH
9f600f88b0 新增实体查询系统文档 2025-10-14 13:33:12 +08:00
YHH
e3c4d5f0c0 Merge pull request #105 from esengine/issue-94-响应式查询(Reactive_Query_System)/_Event-driven_Query
响应式查询
2025-10-14 12:29:54 +08:00
YHH
b97f3a8431 移除了 EntitySystem.update() 中的冗余 invalidate() 调用 2025-10-14 12:08:08 +08:00
YHH
3b917a06af 修复响应式查询缓存失效和测试隔离问题 2025-10-14 11:48:04 +08:00
YHH
360106fb92 优化ReactiveQuery: 添加公共API、修复内存泄漏、提升通知性能 2025-10-13 23:55:43 +08:00
YHH
507ed5005f Merge remote-tracking branch 'origin/master' into issue-94-响应式查询(Reactive_Query_System)/_Event-driven_Query 2025-10-13 23:36:12 +08:00
YHH
8bea5d5e68 Merge remote-tracking branch 'origin/master' into issue-94-响应式查询(Reactive_Query_System)/_Event-driven_Query 2025-10-13 23:29:56 +08:00
YHH
99076adb72 Merge pull request #113 from esengine/issue-112-优化_DebugManager_使用_DI_避免依赖问题
使用DI避免依赖问题
2025-10-13 23:21:10 +08:00
YHH
d1a6230b23 新增调试配置服务用于DI 2025-10-13 23:17:10 +08:00
YHH
cfe3916934 使用DI避免依赖问题 2025-10-13 22:56:49 +08:00
YHH
d798995876 Merge pull request #111 from esengine/issue-110-_Inject_装饰器在_Cocos_Creator_环境中不执行
扩展 InjectableMetadata 接口支持属性注入,实现 @InjectProperty 装饰器
2025-10-12 23:44:22 +08:00
YHH
43e6b7bf88 扩展 InjectableMetadata 接口支持属性注入,实现 @InjectProperty 装饰器 2025-10-12 23:39:32 +08:00
YHH
9253686de1 v2.2.3 2025-10-12 21:41:09 +08:00
YHH
7e7eae2d1a Merge pull request #109 from esengine/issue-108-world和scene进行了多次更新
Revert "Merge pull request #102 from esengine/issue-74-World与Scene关系不清晰"
2025-10-12 21:39:35 +08:00
YHH
1924d979d6 Revert "Merge pull request #102 from esengine/issue-74-World与Scene关系不清晰"
This reverts commit f2b9c5cc5a, reversing
changes made to 5f507532ed.
2025-10-12 21:38:53 +08:00
YHH
ed84394301 更新序列化文档 2025-10-12 19:02:17 +08:00
YHH
bb99cf5389 v2.2.2 2025-10-12 18:56:39 +08:00
YHH
2d0700f441 Merge pull request #107 from esengine/issue-106-类型定义声明返回_Buffer导致浏览器兼容问题
修复buffer再浏览器环境不兼容的问题
2025-10-12 18:53:53 +08:00
YHH
e3ead8a695 修复buffer再浏览器环境不兼容的问题 2025-10-12 18:49:20 +08:00
YHH
942043f0b0 报告响应式查询的数量而不是传统缓存 2025-10-11 18:44:55 +08:00
YHH
23d81bca35 响应式查询 2025-10-11 18:31:20 +08:00
YHH
701f538e57 Merge pull request #104 from esengine/issue-103-WorldManager统一管理所有World实例_含默认World
Issue 103 world manager统一管理所有world实例 含默认world
2025-10-11 15:26:27 +08:00
YHH
bb3017ffc2 Merge remote-tracking branch 'origin/master' into issue-103-WorldManager统一管理所有World实例_含默认World
# Conflicts:
#	packages/core/src/Core.ts
#	packages/core/src/ECS/SceneManager.ts
2025-10-11 15:21:43 +08:00
YHH
f2b9c5cc5a Merge pull request #102 from esengine/issue-74-World与Scene关系不清晰
统一World与Scene架构,SceneManager内部使用DefaultWorld
2025-10-11 15:16:52 +08:00
YHH
532a52acfc 统一的World管理路径 2025-10-11 15:14:37 +08:00
YHH
c19b5ae9a7 统一World与Scene架构,SceneManager内部使用DefaultWorld 2025-10-11 14:44:21 +08:00
YHH
5f507532ed 统一World与Scene架构,SceneManager内部使用DefaultWorld 2025-10-11 14:27:09 +08:00
YHH
6e48f22540 更新v2.2.1文档 2025-10-11 11:33:07 +08:00
YHH
66aa9f4f20 更新文档 2025-10-11 10:48:24 +08:00
YHH
62f895efe0 v2.2.1 2025-10-11 10:46:46 +08:00
YHH
4a060e1ce3 WorldManager 现在由 ServiceContainer 统一管理 2025-10-11 10:40:10 +08:00
YHH
a0177c9163 从 tslib 导入辅助函数 2025-10-11 10:36:59 +08:00
YHH
f45af34614 更新v2.2.0文档 2025-10-11 10:16:52 +08:00
YHH
14a8d755f0 PoolManager 现在由 ServiceContainer 统一管理 2025-10-11 09:38:16 +08:00
YHH
b67ab80c75 Merge pull request #93 from esengine/issue-80-插件系统
插件系统
2025-10-11 09:32:12 +08:00
YHH
ae71af856b 插件系统 2025-10-11 09:26:36 +08:00
YHH
279c1d9bc9 Merge pull request #92 from esengine/issue-82-组件引用完整性
组件引用完整性,升级到es2021使用weakref
2025-10-11 00:30:36 +08:00
YHH
9068a109b0 降级es2020,实现 WeakRef Polyfill 2025-10-11 00:25:10 +08:00
YHH
7850fc610c 组件引用完整性,升级到es2021使用weakref 2025-10-10 23:38:48 +08:00
YHH
536871d09b Merge pull request #88 from esengine/issue-76-依赖注入
依赖注入引入DI容器
2025-10-10 22:15:24 +08:00
YHH
1af2cf5f99 Scene 构造函数注入 PerformanceMonitor 2025-10-10 22:08:10 +08:00
YHH
b13132b259 依赖注入引入DI容器 2025-10-10 21:52:43 +08:00
YHH
a1a6970ea4 Merge pull request #87 from esengine/issue-73-Core类职责过重需要进行拆分
新增ServiceContainer服务容器, 所有服务统一实现 IService 接口
2025-10-10 18:17:55 +08:00
YHH
41bbe23404 新增ServiceContainer服务容器, 所有服务统一实现 IService 接口 2025-10-10 18:13:28 +08:00
YHH
1d2a3e283e Merge pull request #86 from esengine/issue-75-组件存储策略不统一
组件存储策略不统一
2025-10-10 16:38:06 +08:00
YHH
62d7521384 移除 Entity._localComponents/强制Entity必须属于Scene/简化组件操作逻辑 2025-10-10 16:31:43 +08:00
YHH
bf14b59a28 空查询应该返回所有实体 2025-10-10 11:49:06 +08:00
YHH
0a0f64510f 更新测试用例 2025-10-10 10:58:52 +08:00
YHH
9445c735c3 对象池内存管理优化 2025-10-10 10:16:44 +08:00
YHH
7339e7ecec 新增scenemanager,重构core类减少多世界造成的性能压力 2025-10-09 23:33:11 +08:00
YHH
79f7c89e23 修复再不同环境下buffer兼容性问题 2025-10-09 17:44:15 +08:00
YHH
e724e5a1ba 更新demo 2025-10-09 17:43:46 +08:00
YHH
fdaa94a61d v2.1.52 2025-10-09 17:26:07 +08:00
YHH
6af0074c36 导航增加序列化章节 2025-10-09 17:18:14 +08:00
YHH
97a69fed09 增量序列化支持二进制 2025-10-09 17:14:18 +08:00
YHH
959879440d 更新序列化文档 2025-10-09 14:18:43 +08:00
YHH
fd1bbb0e00 新增增量序列化 2025-10-09 12:30:04 +08:00
YHH
072e68cf43 修复序列化ci测试 2025-10-08 20:58:07 +08:00
YHH
610232e6b0 core库demo更新 2025-10-08 20:52:31 +08:00
YHH
69c46f32eb 支持二进制序列化 2025-10-08 20:42:55 +08:00
YHH
06b3f92007 场景自定义序列化支持 2025-10-08 18:34:15 +08:00
YHH
c631290049 对query/entity进行安全类型扩展 2025-10-08 13:13:23 +08:00
YHH
f41c1a3ca3 冗余测试合并 2025-10-08 12:04:13 +08:00
YHH
bd6ba84087 Merge pull request #72 from 0MirageTank0/master
优化掩码数据结构,新增BitMaskHashMap类型,支持无限数量原型
2025-10-05 09:13:30 +08:00
MirageTank
1512409eb3 优化位掩码工具的输出格式
- 十六进制不再输出无意义的前导0符号
- 修正部分测试单元检测逻辑
2025-10-04 13:16:51 +08:00
MirageTank
bcb5feeb1c 实现高性能 BitMaskHashMap 并优化ArchetypeSystem
- 引入 BitMaskHashMap 类,使用双层 MurmurHash3 哈希算法提升查找性能
- 替换 ArchetypeSystem 中原有的嵌套 Map 结构为 BitMaskHashMap,支持任意数量的原型
- 验证在十万级连续键值下无哈希冲突,确保生产环境可用性
2025-10-04 10:26:19 +08:00
MirageTank
da8b7cf601 重构位掩码数据结构,修复部分方法未考虑扩展位的问题
- 所有操作均考虑扩展位、扩展长度不一致的使用场景,无感扩容掩码位
- 使用定长数组存储高低位,遍历友好,为高效哈希计算提供结构支持
- 补充相应单元测试,覆盖所有方法及分支
2025-10-03 16:55:07 +08:00
YHH
316527c459 更新实体文档(components为只读属性) 2025-10-01 00:15:19 +08:00
YHH
da70818b22 v2.1.51 2025-10-01 00:11:14 +08:00
YHH
5ea3b72b2b 使用BitMask64Data.segments扩展ComponentRegistry 2025-09-30 23:58:52 +08:00
YHH
632864b361 单一数据源 + 懒加载缓存 2025-09-30 23:37:47 +08:00
YHH
952247def0 重构 Component基类违反ECS纯粹性问题 2025-09-30 22:26:44 +08:00
YHH
51debede52 移除废弃的文件 2025-09-30 20:44:08 +08:00
YHH
ce7b731bcf Merge pull request #71 from esengine/fix/issue-70-component-mask-string
Fix/issue 70 component mask string
2025-09-30 18:19:11 +08:00
YHH
86e2dc8fdb 完全删除 ComponentIndexManager 2025-09-30 18:12:49 +08:00
YHH
78047134c2 完全移除 byComponentType 2025-09-30 18:03:03 +08:00
YHH
125a1686ab 完全删除 byComponentType 2025-09-30 17:54:22 +08:00
YHH
d542ac48b8 重构byComponentType为lazy cache模式 2025-09-30 17:07:13 +08:00
YHH
1ac0227c90 移除bymask 2025-09-30 17:01:49 +08:00
YHH
a5e70bcd99 修复QuerySystem组件掩码索引使用toString()返回[object Object]的问题 #70 2025-09-30 16:38:32 +08:00
YHH
38763de7f6 BitMask64Segment 独立类型 2025-09-30 15:56:28 +08:00
YHH
db73b077c5 支持分层 BitMask 自动扩容,避免用户超过组件后报错问题 2025-09-30 15:38:50 +08:00
YHH
0969d09da1 优化ArchetypeSystem的AND指令 2025-09-30 15:08:33 +08:00
YHH
a07108a431 修复soa测试用例 2025-09-30 13:43:12 +08:00
YHH
6693b56ab8 Merge pull request #69 from 0MirageTank0/master
优化ArchetypeSystem性能
2025-09-30 13:31:11 +08:00
MirageTank
a7349bd360 补充TypeDecorators文件对ComponentType参数类型的修改 2025-09-30 12:53:23 +08:00
MirageTank
e92c0040b5 重构原型系统以提升性能
- 将原型ID类型从字符串改为BitMask64Data,避免拼接字符串的性能损耗,降低内存占用.
- 原型ID生成不再依赖组件名称.
2025-09-30 11:57:31 +08:00
MirageTank
f448fa48c4 重构组件类型管理器相关函数参数以使用ComponentType,而不是Component. 2025-09-30 11:42:01 +08:00
YHH
aa33cad4fa 扩展typedarray存储系统,允许自动类型推断@AutoTyped 2025-09-30 11:00:05 +08:00
YHH
d0cb7d5359 v2.1.50 2025-09-30 09:59:46 +08:00
YHH
90153b98fe 更新 2025-09-30 09:51:02 +08:00
YHH
8c4e8d523e 更新微信小游戏worker文档 2025-09-30 09:37:30 +08:00
YHH
90ad4b3ec4 抽象worker接口,避免污染项目 2025-09-29 18:15:47 +08:00
YHH
62bc6b547e 支持wx/browser的worker(由于wx限制默认不开启worker) 2025-09-29 13:21:08 +08:00
YHH
be11060674 archetypesystem只负责原型管理,querysytem负责查询和缓存 2025-09-29 11:01:39 +08:00
YHH
d62bf9f7f9 entitysytem的logger返回类型更改,需要重写实现getLoggerName 2025-09-29 09:53:13 +08:00
YHH
61fcd52c65 移除eventhandler装饰器 2025-09-29 09:35:02 +08:00
YHH
2947ddeb64 不限制ci的超时时间 2025-09-29 09:04:53 +08:00
YHH
d9b752c180 修复workerentitysysten和entitysytem的logger重复问题 2025-09-29 00:01:59 +08:00
YHH
b82891caee 装饰器事件自动清理 2025-09-28 23:58:43 +08:00
YHH
05f04ef37e 允许用户自定义核心数量 2025-09-28 23:35:25 +08:00
YHH
66dc9780b9 使用coi-serviceworker用于无法控制标头的情况 2025-09-28 21:11:17 +08:00
YHH
d48b22c656 更新demo界面 2025-09-28 21:00:28 +08:00
YHH
727b1864eb 更新demo 2025-09-28 20:49:00 +08:00
YHH
de3bfd7551 新增禁用sab功能 2025-09-28 20:41:23 +08:00
YHH
dedb91379f worker系统不支持sab回退到普通worker 2025-09-28 20:22:06 +08:00
YHH
1dfcd008aa 新增worker-system文档及源码示例 2025-09-28 20:03:29 +08:00
YHH
cf2dc91af6 更新worker-demo 2025-09-28 18:29:32 +08:00
YHH
a66f80a766 新增WorkerEntitySystem系统 2025-09-28 17:36:36 +08:00
YHH
f4e49c316e getWorldManager允许传入可选配置用于覆盖默认配置 2025-09-28 15:52:46 +08:00
YHH
d1cd72bbb2 修复QuerySystem中addEntities没有更新componentIndexManager/archetypeSystem索引 2025-09-28 15:32:54 +08:00
YHH
6178851def 修复QuerySystem/ArchetypeSystem未响应实体增删Component的问题 2025-09-28 15:23:59 +08:00
YHH
945f772c30 只在有package改动的时候触发ci流程 2025-09-28 12:32:35 +08:00
YHH
b546c9c712 vitepress修改为githubpage路径 2025-09-28 12:29:43 +08:00
YHH
413ce93b31 更新文档 2025-09-28 12:26:51 +08:00
YHH
cffe32911d v2.1.49 2025-09-28 10:36:01 +08:00
YHH
4f651eb42e 优化querysystem系统(减少数组拷贝)
移除dirtytracksystem
2025-09-28 09:40:36 +08:00
YHH
6da1585b6b entitysystem实用帧缓存和长期缓存策略 2025-09-26 18:30:49 +08:00
YHH
b988e81a1b 数学库新增Vector3 2025-09-26 17:45:52 +08:00
YHH
1a1c1087d2 标记组件不符合规范的废弃属性 2025-09-26 17:45:26 +08:00
YHH
1a1549230f 更新references库 2025-09-26 13:06:39 +08:00
YHH
64ea53eba1 系统添加缓存实体机制避免频繁开销 2025-09-26 10:50:31 +08:00
YHH
5e052a7e7d 默认不增强事件,避免事件性能开销 2025-09-26 10:28:00 +08:00
YHH
cf9ea495d0 移除过时类并标记组件和实体的update为过时方法 2025-09-26 10:09:23 +08:00
YHH
9603c6423b 系统避免在同一帧内进行多次query操作 2025-09-26 09:45:22 +08:00
YHH
457eef585e Merge branch 'master' of https://github.com/esengine/ecs-framework 2025-09-26 09:38:57 +08:00
YHH
1ade449c4d 控制实体update默认不更新 2025-09-26 09:38:51 +08:00
YHH
aa9d73a810 Merge pull request #68 from snakenjq/master
场景移除时, 清理系统
2025-09-24 18:25:55 +08:00
SNDA\niujiaqun.nathan
cc266a7ba9 修改错误方法调用 2025-09-24 18:18:48 +08:00
SNDA\niujiaqun.nathan
d8ea324018 场景移除时, 清理系统 2025-09-24 18:14:22 +08:00
YHH
60566e8d78 v2.1.48 2025-09-24 16:05:22 +08:00
YHH
306d2994dc Merge pull request #67 from foxling/fix/global-log-level
修复 setGlobalLogLevel 不影响新创建 logger 的问题
2025-09-24 15:54:28 +08:00
YHH
e6a8791fc3 系统中提供更安全的事件监听器方法(避免内存泄露) 2025-09-24 11:03:37 +08:00
YHH
6cbbc06998 规范jsdoc注释 2025-09-24 10:45:33 +08:00
YHH
0b4244fd8e 修复循环依赖问题 2025-09-24 10:20:36 +08:00
LING YE
367ddfbf8a 修复 setGlobalLogLevel 方法,使其可以在后续新建的 Logger 实例中生效 2025-09-22 15:28:29 +08:00
YHH
168e028098 更新支持es5环境 2025-09-04 16:26:29 +08:00
YHH
042ded37d2 新增自定义log颜色ci测试 2025-09-04 16:25:40 +08:00
YHH
4137eb2bce 网络层完善消息队列 2025-09-04 16:25:18 +08:00
YHH
20a3f03e12 支持用户自定义log颜色 2025-09-04 16:24:38 +08:00
YHH
7792710694 避免splice开销问题,改为SwapPop + typeId → denseIndex 2025-09-03 10:56:33 +08:00
YHH
dbddbbdfb8 更新ci测试用例 2025-09-03 10:39:29 +08:00
YHH
4869f5741e bits多态改为POD+原地操作 2025-09-03 10:29:43 +08:00
YHH
bda547dd2e 把掩码从BigInt-like多态都改成Mask64 2025-09-03 00:39:00 +08:00
YHH
ef80b03a44 更改为固定64位掩码,没必要为任意精度付出性能代价 2025-09-03 00:12:59 +08:00
YHH
6e511ae949 改成 SparseSet+SwapRemove 的致密存储 2025-09-02 22:29:11 +08:00
YHH
94541d0abb 实体中的线性数组换为按组件类型ID直址的稀疏数组 2025-09-02 21:59:59 +08:00
YHH
586a0e5d14 降低ci测试覆盖率 2025-09-02 21:16:36 +08:00
YHH
814842dbaf 降低测试覆盖率导致的ci错误 2025-09-02 21:11:15 +08:00
YHH
70a993573f 修复ci报错 2025-09-02 18:04:55 +08:00
YHH
21659cbb13 交/并/差运算全部改用ID集合单次扫描 2025-09-02 17:17:07 +08:00
YHH
a44251cc55 新增world概念(多world管理多scene概念)现在支持多个world多个scene同时更新 2025-08-20 17:48:31 +08:00
YHH
69616bbddc 实现ServerRpc和ClientRpc装饰器 2025-08-20 10:32:56 +08:00
YHH
0a1d7ac083 实现SyncVar装饰器和组件同步 2025-08-20 10:16:54 +08:00
YHH
364bc4cdab update demo 2025-08-20 09:17:43 +08:00
YHH
2504eb24e1 经测试转换为wasm没有明显的效率提升,因为js和rust的交互反而有所下降
等待以后有密集计算再考虑使用,先移除了
2025-08-20 09:15:06 +08:00
YHH
bdbef0bd0d core核心rust实现 2025-08-18 20:46:47 +08:00
YHH
e4e38ee4e6 更新api文档 2025-08-15 13:47:11 +08:00
YHH
021e892e33 ci流程需要core库先编译后再测试network-shared 2025-08-15 13:32:36 +08:00
YHH
c27d5022fd 优化内部组件索引机制(更改为SparseSet索引)减少用户切换索引成本
修复内部系统初始化逻辑 - 不应该再onInitialize中初始内部entities,移动到initialize中
ci跳过cocos项目避免ci失败
soa开放更多安全类型接口
2025-08-15 12:58:55 +08:00
YHH
6730a5d625 传输层实现(客户端/服务端,链接管理和心跳机制,重连机制)
消息序列化(json序列化,消息压缩,消息ID和时间戳)
网络服务器核心(networkserver/基础room/链接状态同步)
网络客户端核心(networkclient/消息队列)
2025-08-14 23:59:00 +08:00
YHH
32092f992d 更新文档 2025-08-14 18:45:24 +08:00
YHH
a5f0c8f6b5 更新文档 2025-08-14 18:44:04 +08:00
YHH
85cd93e51a 文档更新 2025-08-14 18:38:09 +08:00
YHH
0b7e623748 新增组件/系统装饰器避免混淆
更改Set兼容web/小游戏
2025-08-14 18:35:03 +08:00
YHH
62f250b43c 重构network库(mvp版本)搭建基础设施和核心接口
定义ITransport/ISerializer/INetworkMessage接口
NetworkIdentity组件
基础事件定义
2025-08-13 13:07:40 +08:00
YHH
25136349ff 修复动态require导致的跨平台错误
新增emitter的dispose方法用于清理事件
启用composite增量编译
2025-08-13 12:18:40 +08:00
YHH
baeb047e27 支持可以任意参数 2025-08-12 11:47:18 +08:00
YHH
56dd18b983 废弃core.scene更改为setscene方法 2025-08-12 11:08:27 +08:00
YHH
86cb70a94f Merge branch 'master' of https://github.com/esengine/ecs-framework
# Conflicts:
#	packages/network-shared/package.json
2025-08-12 09:43:29 +08:00
YHH
9f76d37a82 更新network库及core库优化 2025-08-12 09:39:07 +08:00
YHH
a026ed9428 降级ws版本 2025-08-11 12:33:42 +08:00
YHH
c178e2fbcc 移除ci性能测试,github下不应该测试这些文件 2025-08-11 11:34:38 +08:00
YHH
b88bb1dc87 修复ci中的大小写问题
update gitsubmodule
2025-08-11 11:11:56 +08:00
YHH
3069e28224 add submodule electric-world 2025-08-11 11:06:28 +08:00
YHH
d69b3af99b 更新math版本 2025-08-11 10:39:14 +08:00
YHH
7398b7c6d0 修复math库tsconfig引用问题 2025-08-11 10:38:11 +08:00
YHH
5d57904d22 更新使用rollup打包
解决大小写冲突问题
2025-08-11 10:34:13 +08:00
YHH
7daf352a25 更新库rollup配置 2025-08-11 10:25:28 +08:00
YHH
6a49f6a534 npm包发布配置更改 2025-08-11 10:03:19 +08:00
YHH
5bce08683a update 2.1.30 2025-08-11 09:31:44 +08:00
YHH
edc60fc3d8 添加发布core命令 2025-08-11 09:15:14 +08:00
YHH
1361fd8a90 导出soa装饰器 2025-08-11 09:01:01 +08:00
YHH
d539bb3dd9 更新文档 2025-08-11 08:18:18 +08:00
YHH
3b9ae4f384 新增math库 2025-08-10 16:00:02 +08:00
YHH
2783448de5 重新整理网络架构,tsrpc/syncvar并行 2025-08-10 12:35:39 +08:00
YHH
6e21ff08d5 集成tsrpc代替protobuf 2025-08-09 18:56:19 +08:00
YHH
e56278e4a6 移除路径映射,只让Jest处理路径解析 2025-08-08 15:57:56 +08:00
YHH
fc9bf816dd 修复build失败问题 2025-08-08 15:47:48 +08:00
YHH
854fd7df3a 修复ci测试 2025-08-08 15:41:37 +08:00
YHH
87dd564a12 项目统一改用Logger控制管理
拆分pool类和FluentAPI
2025-08-08 11:16:00 +08:00
YHH
2d389308ea 新增syncvar高级特性,使用protobuf定义 2025-08-07 20:23:49 +08:00
YHH
ea8523be35 使用Lerna 和 monorepo管理项目结构 2025-08-07 13:29:12 +08:00
YHH
4479f0fab0 避免throw导致的中止运行,增加fallback回退json的序列化 2025-08-07 10:16:36 +08:00
YHH
7a000318a6 整合组件类型至统一的componentregistry中 2025-08-07 09:43:34 +08:00
YHH
9a08ae74b6 移除json序列化只保留protobuf 2025-08-06 17:42:12 +08:00
YHH
f3d2950df3 修复ci失败 2025-08-06 17:12:39 +08:00
YHH
8cfba4a166 新增protobuf依赖(为网络和序列化做准备)
更新readme
2025-08-06 17:04:02 +08:00
YHH
51e6bba2a7 2.1.29 2025-08-06 09:56:55 +08:00
YHH
ccbfa78070 修复了QuerySystem在销毁实体时的内存泄漏问题
实现了完整的onAdded/onRemoved回调系统
修复了override修饰符和类型兼容性问题
2025-08-06 09:39:08 +08:00
YHH
69655f1936 测试用例更新 2025-07-31 15:37:40 +08:00
YHH
6ea366cfed 优化matcher内部实现改为querysystem
完善type类型
更新文档
2025-07-31 11:56:04 +08:00
YHH
b7d17fb16d soa添加float32 2025-07-30 18:10:14 +08:00
YHH
f3dc8c6344 BigIntFactory 缓存优化
- 为 zero() 和 one() 方法添加缓存,避免重复创建对象
ComponentIndexManager 优化
  - 添加了空实体检查,跳过不必要的索引操作
  - 实现了 Set 对象池,重用 Set 实例以减少内存分配
  - 优化了自动优化检查频率,从每次操作变为每100次操作检查一次
EntityManager 优化
  - 对空实体跳过不必要的组件索引、原型系统和脏标记操作
  - 批量创建时同样应用空实体优化
2025-07-30 17:10:58 +08:00
YHH
69ec545854 优化createEntity的性能/新增批量创建实体api 2025-07-30 16:05:16 +08:00
YHH
65386ff731 优化EntitySystem初始化逻辑/防止多次初始化
增加matcher和entitysystem的测试
2025-07-30 15:42:19 +08:00
YHH
01fa33e122 新增soastorage存储器 2025-07-30 14:14:04 +08:00
YHH
0411aa9aef 2.1.28 2025-07-30 11:14:26 +08:00
YHH
4a5c890121 对bigint进行兼容处理(不支持的环境回退到兼容模式) 2025-07-30 11:11:46 +08:00
YHH
4c11fdc176 2.1.27 2025-07-29 16:10:09 +08:00
YHH
d99e7a45ea 新增更多覆盖测试 2025-07-29 15:56:40 +08:00
YHH
52528ff1b7 修复querysystem的rebuildindex方法
修复位掩码不一致问题
修复未注册组件的处理
2025-07-29 10:58:31 +08:00
YHH
4a9317f3f4 querysystem进行ci测试隔离 2025-07-29 10:39:48 +08:00
YHH
9450dd5869 修复ci报错 2025-07-29 10:25:12 +08:00
YHH
d5471e4828 修复ci报错问题 2025-07-29 09:29:29 +08:00
YHH
2f71785add 覆盖querysystem/eventbus/componentstorage测试 2025-07-29 09:08:31 +08:00
YHH
608f5030b2 对ecs目录进行更多的ci测试 2025-07-28 17:38:18 +08:00
YHH
dd8f3714ed 修复不应该jest console导致的ci失败 2025-07-28 17:20:28 +08:00
YHH
abec2b3648 querysystem内部框架维护(不需要用户手动调用事件派发)
新增test覆盖测试
2025-07-28 17:14:10 +08:00
YHH
ea06a9f07d Merge pull request #58 from esengine/develop_simple_entity
移除过度复杂的组件缓存系统 #57
2025-07-18 21:21:01 +08:00
YHH
9f54759cc5 添加-获取-移除循环CI条件放宽 #57 2025-07-18 18:29:17 +08:00
YHH
55dd5f9ed0 单元测试条件放宽 CI环境性能较低 #57 2025-07-18 18:26:47 +08:00
YHH
05455421fb 移除过度复杂的组件缓存系统 #57 2025-07-18 18:08:57 +08:00
YHH
af61067f08 修复ci导致的问题 2025-07-18 15:15:37 +08:00
YHH
19cda88248 移除子模块CI 2025-07-18 15:11:20 +08:00
YHH
0edb2738a1 优化IdentifierPool - 世代式ID池管理器 2025-07-18 14:59:00 +08:00
YHH
e1bc364525 新增实用ai编辑器 2025-07-13 22:53:30 +08:00
YHH
2925ee380d 新增mvvm示例 2025-07-08 20:23:19 +08:00
YHH
731edf5872 Merge branch 'master' of https://github.com/esengine/ecs-framework
# Conflicts:
#	.gitmodules
#	extensions/cocos/cocos-ecs/package-lock.json
#	extensions/cocos/cocos-ecs/package.json
2025-07-08 08:57:11 +08:00
YHH
7b85039b17 更新mvvm示例 2025-07-08 08:55:55 +08:00
YHH
2bc45fa574 新增子模块cocos-mvvm 2025-07-07 23:41:53 +08:00
YHH
d2b4455205 2.1.26 2025-07-07 11:49:57 +08:00
YHH
bce4a26197 构建cjs/mjs 2025-07-07 11:49:36 +08:00
YHH
1da5040d60 打包支持nodejs模块 2025-07-07 11:06:08 +08:00
YHH
afd33e053b 更新子模块及导入demo 2025-07-07 11:02:11 +08:00
YHH
171d03c006 新增snapshot快照功能 2025-07-07 09:45:36 +08:00
YHH
34d5237aaa Merge branch 'master' of https://github.com/esengine/ecs-framework
# Conflicts:
#	package-lock.json
#	package.json
2025-07-03 09:02:37 +08:00
YHH
037c3d6a05 2.1.25 2025-07-02 23:49:28 +08:00
YHH
5596ba634e 2.1.24 2025-07-02 23:49:02 +08:00
YHH
a5f69065f4 update 2025-07-02 23:48:51 +08:00
YHH
969ef249ea Merge branch 'master' of https://github.com/esengine/ecs-framework 2025-07-02 23:47:41 +08:00
YHH
a37183851f 修复queryall缓存信息错误问题 2025-07-02 23:47:30 +08:00
YHH
4cf3e1a769 2.1.24 2025-07-02 11:25:00 +08:00
YHH
354e5a2761 update submodule 2025-07-02 11:23:50 +08:00
YHH
c9fd8cc2a7 优化组件递归调用导致的性能问题/新增实体所在的场景显示 2025-07-02 09:42:49 +08:00
YHH
bb19f752a1 优化性能结构/延迟加载
新增测试代码用于测试性能
2025-07-02 00:13:29 +08:00
YHH
6bd9c1055c 2.1.23 2025-07-01 11:48:41 +08:00
YHH
dff77097c6 git commit 2025-07-01 11:48:26 +08:00
YHH
b4dc1c5661 修复type大小写问题 2025-06-30 20:43:11 +08:00
YHH
992338d924 更新性能分析器及更改部分注释 2025-06-30 20:33:45 +08:00
YHH
f88a402b0c 新增程序化地形子模块 2025-06-27 16:51:19 +08:00
YHH
5938d36149 版本更新 2025-06-26 18:00:02 +08:00
YHH
78577db3f9 更新示例教程 2025-06-25 23:17:55 +08:00
YHH
0b4a6b77e2 采矿行为树示例 2025-06-25 17:50:40 +08:00
YHH
01084a8897 更新rts示例代码(矿工自动采矿) 2025-06-24 23:51:59 +08:00
YHH
0f18a1979e rts-demo 2025-06-24 19:34:37 +08:00
YHH
68a615bc7b 移除测试代码 2025-06-23 23:48:19 +08:00
YHH
add1068c1a Merge branch 'master' of https://github.com/esengine/ecs-framework 2025-06-23 16:08:40 +08:00
YHH
7a40df9965 修复过时文档问题 2025-06-23 16:08:31 +08:00
YHH
3e6a1aa59a 更新example 2025-06-22 21:21:46 +08:00
YHH
d3fe79cf39 example update 2025-06-20 18:21:50 +08:00
YHH
48fa547c8f 更改为submodule 2025-06-19 21:04:38 +08:00
YHH
80e2f7df71 remove cocos-extensions 2025-06-19 21:00:42 +08:00
YHH
0107f1f58a remove admin-dashboard 2025-06-19 20:49:09 +08:00
YHH
d29c9a96f4 新增热更新后台管理系统 2025-06-19 18:32:32 +08:00
YHH
37d75c3281 2.1.22 2025-06-19 15:43:48 +08:00
YHH
666ded7b89 初始化只在系统真正开始工作时执行 2025-06-19 15:43:22 +08:00
YHH
73a882f75e 2.1.21 2025-06-19 15:01:20 +08:00
YHH
310f5f2349 支持先加入实体后加入系统以让matcher进行实体匹配/优化行为树节点效果及逻辑 2025-06-19 15:00:14 +08:00
YHH
8c86d6b696 更新文档及优化行为树编辑器 2025-06-19 10:38:31 +08:00
YHH
82cd163adc 清理冗余代码及频繁日志输出 2025-06-18 23:37:47 +08:00
YHH
802ee25621 新增Blackboard 2025-06-18 23:31:53 +08:00
YHH
f48ebb65ba 修复插件生成代码报错问题 2025-06-18 20:22:17 +08:00
YHH
aaa2a8ed2c 新增装饰节点选中功能 2025-06-18 18:31:29 +08:00
YHH
5a06f5420b 条件装饰节点和条件装饰器结合 2025-06-18 18:21:55 +08:00
YHH
343f5a44f2 新增根节点 2025-06-18 15:53:48 +08:00
YHH
92125aee3a 删除连线操作 2025-06-18 15:43:17 +08:00
YHH
96f651b7ca 新增cocos右键打开和保存行为树功能 2025-06-18 15:20:07 +08:00
YHH
06ea01e928 拖拽逻辑更新 2025-06-17 23:59:30 +08:00
YHH
577f1e429a 新增行为树编辑器 2025-06-17 18:28:57 +08:00
YHH
7808f64fe5 更新生成代码工具 2025-06-17 10:46:47 +08:00
YHH
e6789e49e4 2.1.20 2025-06-17 07:35:46 +08:00
YHH
797619aece 更新池利用率 2025-06-17 07:35:23 +08:00
YHH
1b5363611d 新增cocos-debug-profiler 2025-06-17 00:32:16 +08:00
YHH
103f773286 添加Cocos Creator ECS编辑器插件:完整的框架管理和模板生成功能 2025-06-16 18:32:44 +08:00
YHH
d9ef0b587e 2.1.19 2025-06-16 09:36:59 +08:00
YHH
d5b98256f0 优化bits性能及移除组件上限 2025-06-16 09:36:36 +08:00
YHH
efcceaa898 2.1.18 2025-06-12 09:47:44 +08:00
YHH
e4aad11965 update readme 2025-06-12 09:47:25 +08:00
YHH
47207fad52 2.1.17 2025-06-12 09:44:05 +08:00
YHH
202bf82896 更新场景切换状态 2025-06-12 09:43:57 +08:00
YHH
0e3274a743 移除coreevents事件派发机制 2025-06-12 09:42:35 +08:00
YHH
b06174926d 更新快速入门指南 2025-06-10 13:22:28 +08:00
YHH
abb23a3c02 2.1.16 2025-06-10 13:12:22 +08:00
YHH
0c8f232282 文档及教程更新 2025-06-10 13:12:14 +08:00
YHH
ef023d27bf 2.1.15 2025-06-10 09:50:05 +08:00
YHH
7a591825eb 先移除wasm后续再通过其他方式接入 2025-06-10 09:49:55 +08:00
YHH
e71c49d596 移除额外的打包流程 2025-06-09 18:27:20 +08:00
YHH
e6ce8995ba 2.1.14 2025-06-09 18:01:34 +08:00
YHH
f6250b6d5b 规范为cococs wasm 2025-06-09 18:00:50 +08:00
YHH
757eff2937 2.1.13 2025-06-09 15:55:26 +08:00
YHH
996a7f3ddf 重构WASM架构:移除npm包中的WASM文件,改为独立发布 - 移除自动WASM加载逻辑 - 添加手动initializeWasm API - 创建专门的WASM发布包构建脚本 - 更新Cocos Creator使用指南 2025-06-09 15:54:34 +08:00
YHH
94c050bacb 2.1.12 2025-06-09 15:47:10 +08:00
YHH
3f4aa59a29 完善Cocos Creator支持:添加手动WASM初始化API和使用指南 2025-06-09 15:46:14 +08:00
YHH
bee7cf4278 2.1.11 2025-06-09 15:32:11 +08:00
YHH
b9db6f0b40 2.1.10 2025-06-09 15:16:31 +08:00
YHH
8967cba3c7 修复WASM路径解析问题,支持Cocos Creator环境 - 改进多路径WASM加载策略 - 添加Core.disableWasm()方法 - 简化WASM初始化逻辑以避免环境兼容性问题 2025-06-09 15:15:41 +08:00
YHH
d04ad2eea9 Update build-rollup.js 2025-06-09 14:53:15 +08:00
YHH
f2d3880a06 重构项目结构:整理gitignore,移动source目录到根目录,统一依赖管理 2025-06-09 14:51:26 +08:00
YHH
ec5f70ecfc fix: 修复npm包构建和发布问题 - 将esbuild target从es2017升级到es2020以支持BigInt - 修复浏览器环境下Node.js模块依赖问题 - 添加define配置处理require等Node.js全局变量 - 成功发布@esengine/ecs-framework@2.1.7到npm 2025-06-09 13:34:46 +08:00
YHH
40b3fe7165 docs: 更新项目文档 - 添加EntityManager、事件系统、性能优化使用示例和说明 2025-06-09 13:25:10 +08:00
YHH
4095f1e946 refactor: 规范化代码注释和更新核心模块 - 移除冗余JSDoc注释,统一代码风格 2025-06-09 13:24:54 +08:00
YHH
e219fc47ba feat: 添加ECS核心功能模块 - EntityManager实体管理器、EventBus事件总线、性能优化系统 2025-06-09 13:24:24 +08:00
YHH
6e2e7a4af5 refactor: 拆分WasmCore大文件为模块化结构 - 将23KB的WasmCore.ts拆分为types/loader/fallback/core/instance五个模块 2025-06-09 13:23:46 +08:00
YHH
2e7f764d6c refactor: 细化ECS/Core目录结构 - 按功能拆分为Events/Query/Performance/Storage四个子模块 2025-06-09 13:23:29 +08:00
YHH
ce64de5b3d 更改为es2020模块适应Cocos/laya引擎 2025-06-09 10:42:19 +08:00
YHH
35ca1dd7ea 将dist目录添加到.gitignore中 - dist目录是npm包构建的输出目录,不应该提交到版本控制 2025-06-09 10:39:37 +08:00
YHH
8d0ad6b871 新增wasm以优化实体update速度 2025-06-08 21:50:50 +08:00
YHH
0aa4791cf7 更新文档并预留wasm接口 2025-06-08 10:20:51 +08:00
YHH
082c2b46d0 移除数学库 2025-06-07 21:45:11 +08:00
YHH
50420f9052 增加性能测试 2025-06-07 21:28:31 +08:00
653 changed files with 176501 additions and 18722 deletions

62
.all-contributorsrc Normal file
View File

@@ -0,0 +1,62 @@
{
"projectName": "ecs-framework",
"projectOwner": "esengine",
"repoType": "github",
"repoHost": "https://github.com",
"files": ["README.md"],
"imageSize": 100,
"commit": true,
"commitConvention": "angular",
"contributors": [
{
"login": "yhh",
"name": "Frank Huang",
"avatar_url": "https://avatars.githubusercontent.com/u/145575?v=4",
"profile": "https://github.com/yhh",
"contributions": ["code"]
}
],
"contributorsPerLine": 7,
"contributorsSortAlphabetically": false,
"badgeTemplate": "[![All Contributors](https://img.shields.io/badge/all_contributors-<%= contributors.length %>-orange.svg?style=flat-square)](#contributors)",
"contributorTemplate": "<a href=\"<%= contributor.profile %>\"><img src=\"<%= contributor.avatar_url %>\" width=\"<%= options.imageSize %>px;\" alt=\"<%= contributor.name %>\"/><br /><sub><b><%= contributor.name %></b></sub></a>",
"types": {
"code": {
"symbol": "💻",
"description": "Code",
"link": "[<%= symbol %>](<%= url %> \"Code\")"
},
"doc": {
"symbol": "📖",
"description": "Documentation",
"link": "[<%= symbol %>](<%= url %> \"Documentation\")"
},
"test": {
"symbol": "⚠️",
"description": "Tests",
"link": "[<%= symbol %>](<%= url %> \"Tests\")"
},
"bug": {
"symbol": "🐛",
"description": "Bug reports",
"link": "[<%= symbol %>](<%= url %> \"Bug reports\")"
},
"example": {
"symbol": "💡",
"description": "Examples",
"link": "[<%= symbol %>](<%= url %> \"Examples\")"
},
"design": {
"symbol": "🎨",
"description": "Design",
"link": "[<%= symbol %>](<%= url %> \"Design\")"
},
"ideas": {
"symbol": "🤔",
"description": "Ideas & Planning",
"link": "[<%= symbol %>](<%= url %> \"Ideas & Planning\")"
}
},
"skipCi": true
}

36
.coderabbit.yaml Normal file
View File

@@ -0,0 +1,36 @@
# CodeRabbit 配置文件
# https://docs.coderabbit.ai/configuration
language: "zh-CN" # 使用中文评论
reviews:
# 审查级别
profile: "chill" # "chill" 或 "strict" 或 "assertive"
# 自动审查设置
auto_review:
enabled: true
drafts: false # 草稿 PR 不自动审查
base_branches:
- master
- main
# 审查内容
request_changes_workflow: false # 不阻止 PR 合并
high_level_summary: true # 生成高层次摘要
poem: false # 不生成诗歌(可以改为 true 增加趣味)
review_status: true # 显示审查状态
# 忽略的文件
path_filters:
- "!**/*.md" # 不审查 markdown
- "!**/package-lock.json" # 不审查 lock 文件
- "!**/dist/**" # 不审查构建输出
- "!**/*.min.js" # 不审查压缩文件
# 聊天设置
chat:
auto_reply: true # 自动回复问题
# 提交建议
suggestions:
enabled: true # 启用代码建议

29
.commitlintrc.json Normal file
View File

@@ -0,0 +1,29 @@
{
"extends": ["@commitlint/config-conventional"],
"rules": {
"type-enum": [
2,
"always",
[
"feat",
"fix",
"docs",
"style",
"refactor",
"perf",
"test",
"build",
"ci",
"chore",
"revert"
]
],
"scope-enum": [
0
],
"scope-empty": [0],
"subject-empty": [2, "never"],
"subject-case": [0],
"header-max-length": [2, "always", 100]
}
}

35
.editorconfig Normal file
View File

@@ -0,0 +1,35 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
# 所有文件的默认设置
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
# TypeScript/JavaScript 文件
[*.{ts,tsx,js,jsx,mjs,cjs}]
indent_style = space
indent_size = 4
# JSON 文件
[*.json]
indent_style = space
indent_size = 2
# YAML 文件
[*.{yml,yaml}]
indent_style = space
indent_size = 2
# Markdown 文件
[*.md]
trim_trailing_whitespace = false
indent_size = 2
# 包管理文件
[{package.json,package-lock.json,tsconfig.json}]
indent_size = 2

44
.gitattributes vendored Normal file
View File

@@ -0,0 +1,44 @@
# 自动检测文本文件并规范化换行符
* text=auto
# 源代码文件强制使用 LF
*.ts text eol=lf
*.tsx text eol=lf
*.js text eol=lf
*.jsx text eol=lf
*.mjs text eol=lf
*.cjs text eol=lf
*.json text eol=lf
*.md text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
# 配置文件强制使用 LF
.gitignore text eol=lf
.gitattributes text eol=lf
.editorconfig text eol=lf
.prettierrc text eol=lf
.prettierignore text eol=lf
.eslintrc.json text eol=lf
tsconfig.json text eol=lf
# Shell 脚本强制使用 LF
*.sh text eol=lf
# Windows 批处理文件使用 CRLF
*.bat text eol=crlf
*.cmd text eol=crlf
*.ps1 text eol=crlf
# 二进制文件不转换
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.svg binary
*.woff binary
*.woff2 binary
*.ttf binary
*.eot binary
*.otf binary

130
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,130 @@
name: 🐛 Bug Report / 错误报告
description: Report a bug or issue / 报告一个错误或问题
title: "[Bug]: "
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
感谢你提交 Bug 报告!请填写以下信息帮助我们更快定位问题。
Thanks for reporting a bug! Please fill in the information below to help us locate the issue faster.
- type: textarea
id: description
attributes:
label: 问题描述 / Bug Description
description: 清晰简洁地描述遇到的问题 / A clear and concise description of the bug
placeholder: 例如当我创建超过1000个实体时游戏卡顿严重...
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: 复现步骤 / Steps to Reproduce
description: 如何复现这个问题?/ How can we reproduce this issue?
placeholder: |
1. 创建场景
2. 添加 1000 个实体
3. 运行游戏
4. 观察卡顿
value: |
1.
2.
3.
validations:
required: true
- type: textarea
id: expected
attributes:
label: 期望行为 / Expected Behavior
description: 你期望发生什么?/ What did you expect to happen?
placeholder: 游戏应该流畅运行FPS 保持在 60...
validations:
required: true
- type: textarea
id: actual
attributes:
label: 实际行为 / Actual Behavior
description: 实际发生了什么?/ What actually happened?
placeholder: FPS 降到 20游戏严重卡顿...
validations:
required: true
- type: input
id: version
attributes:
label: 版本 / Version
description: 使用的 @esengine/ecs-framework 版本 / Version of @esengine/ecs-framework
placeholder: 例如 / e.g., 2.2.8
validations:
required: true
- type: dropdown
id: platform
attributes:
label: 平台 / Platform
description: 在哪个平台遇到问题?/ Which platform did you encounter the issue?
multiple: true
options:
- Web / 浏览器
- Cocos Creator
- Laya Engine
- WeChat Mini Game / 微信小游戏
- Other / 其他
validations:
required: true
- type: textarea
id: environment
attributes:
label: 环境信息 / Environment
description: |
相关环境信息 / Relevant environment information
例如操作系统、浏览器版本、Node.js 版本等
placeholder: |
- OS: Windows 11
- Browser: Chrome 120
- Node.js: 20.10.0
value: |
- OS:
- Browser:
- Node.js:
validations:
required: false
- type: textarea
id: code
attributes:
label: 代码示例 / Code Sample
description: 如果可能,提供最小可复现代码 / If possible, provide minimal reproducible code
render: typescript
placeholder: |
import { Core, Scene, Entity } from '@esengine/ecs-framework';
// 你的代码 / Your code here
validations:
required: false
- type: textarea
id: logs
attributes:
label: 错误日志 / Error Logs
description: 相关的错误日志或截图 / Relevant error logs or screenshots
render: shell
validations:
required: false
- type: checkboxes
id: checklist
attributes:
label: 检查清单 / Checklist
options:
- label: 我已经搜索过类似的 issue / I have searched for similar issues
required: true
- label: 我使用的是最新版本 / I am using the latest version
required: false
- label: 我愿意提交 PR 修复此问题 / I am willing to submit a PR to fix this issue
required: false

17
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,17 @@
blank_issues_enabled: true
contact_links:
- name: 📚 文档 / Documentation
url: https://esengine.github.io/ecs-framework/
about: 查看完整文档和教程 / View full documentation and tutorials
- name: 🤖 AI 文档助手 / AI Documentation Assistant
url: https://deepwiki.com/esengine/ecs-framework
about: 使用 AI 助手快速找到答案 / Use AI assistant to quickly find answers
- name: 💬 QQ 交流群 / QQ Group
url: https://jq.qq.com/?_wv=1027&k=29w1Nud6
about: 加入社区交流群 / Join the community group
- name: 🌟 GitHub Discussions
url: https://github.com/esengine/ecs-framework/discussions
about: 参与社区讨论 / Join community discussions

View File

@@ -0,0 +1,90 @@
name: ✨ Feature Request / 功能建议
description: Suggest a new feature or enhancement / 建议新功能或改进
title: "[Feature]: "
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
感谢你的功能建议!请详细描述你的想法。
Thanks for your feature suggestion! Please describe your idea in detail.
- type: textarea
id: problem
attributes:
label: 问题描述 / Problem Description
description: 这个功能解决什么问题?/ What problem does this feature solve?
placeholder: 当我需要...的时候,现在很不方便,因为... / When I need to..., it's inconvenient because...
validations:
required: true
- type: textarea
id: solution
attributes:
label: 建议的解决方案 / Proposed Solution
description: 你希望如何实现这个功能?/ How would you like this feature to work?
placeholder: 可以添加一个新的 API例如... / Could add a new API, for example...
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: 其他方案 / Alternatives
description: 你考虑过哪些替代方案?/ What alternatives have you considered?
placeholder: 也可以通过...来实现,但是... / Could also achieve this by..., but...
validations:
required: false
- type: textarea
id: examples
attributes:
label: 使用示例 / Usage Example
description: 展示这个功能如何使用 / Show how this feature would be used
render: typescript
placeholder: |
// 理想的 API 设计 / Ideal API design
const pool = new ComponentPool(MyComponent, { size: 100 });
const component = pool.acquire();
validations:
required: false
- type: dropdown
id: scope
attributes:
label: 影响范围 / Scope
description: 这个功能主要影响哪个部分?/ Which part does this feature mainly affect?
options:
- Core / 核心框架
- Performance / 性能
- API Design / API 设计
- Developer Experience / 开发体验
- Documentation / 文档
- Editor / 编辑器
- Other / 其他
validations:
required: true
- type: dropdown
id: priority
attributes:
label: 优先级 / Priority
description: 你认为这个功能有多重要?/ How important do you think this feature is?
options:
- High / 高 - 非常需要这个功能
- Medium / 中 - 有会更好
- Low / 低 - 可有可无
validations:
required: true
- type: checkboxes
id: checklist
attributes:
label: 检查清单 / Checklist
options:
- label: 我已经搜索过类似的功能请求 / I have searched for similar feature requests
required: true
- label: 这个功能不会破坏现有 API / This feature won't break existing APIs
required: false
- label: 我愿意提交 PR 实现此功能 / I am willing to submit a PR to implement this feature
required: false

64
.github/ISSUE_TEMPLATE/question.yml vendored Normal file
View File

@@ -0,0 +1,64 @@
name: ❓ Question / 问题咨询
description: Ask a question about using the framework / 询问框架使用问题
title: "[Question]: "
labels: ["question"]
body:
- type: markdown
attributes:
value: |
💡 提示:如果是简单问题,可以先查看:
- [📚 文档](https://esengine.github.io/ecs-framework/)
- [📖 AI 文档助手](https://deepwiki.com/esengine/ecs-framework)
- [💬 QQ 交流群](https://jq.qq.com/?_wv=1027&k=29w1Nud6)
💡 Tip: For simple questions, please check first:
- [📚 Documentation](https://esengine.github.io/ecs-framework/)
- [📖 AI Documentation](https://deepwiki.com/esengine/ecs-framework)
- type: textarea
id: question
attributes:
label: 你的问题 / Your Question
description: 清晰描述你的问题 / Describe your question clearly
placeholder: 如何在 Cocos Creator 中使用 ECS Framework
validations:
required: true
- type: textarea
id: context
attributes:
label: 背景信息 / Context
description: 提供更多上下文帮助理解问题 / Provide more context to help understand
placeholder: |
我正在开发一个多人在线游戏...
我尝试过...但是...
validations:
required: false
- type: textarea
id: code
attributes:
label: 相关代码 / Related Code
description: 如果适用,提供相关代码片段 / If applicable, provide relevant code snippet
render: typescript
validations:
required: false
- type: input
id: version
attributes:
label: 版本 / Version
description: 使用的框架版本 / Framework version you're using
placeholder: 例如 / e.g., 2.2.8
validations:
required: false
- type: checkboxes
id: checklist
attributes:
label: 检查清单 / Checklist
options:
- label: 我已经查看过文档 / I have checked the documentation
required: true
- label: 我已经搜索过类似问题 / I have searched for similar questions
required: true

32
.github/labeler.yml vendored Normal file
View File

@@ -0,0 +1,32 @@
# 自动标签配置
# 根据 issue/PR 内容自动打标签
'bug':
- '/(bug|错误|崩溃|crash|error|exception|问题)/i'
'enhancement':
- '/(feature|功能|enhancement|improve|优化|建议)/i'
'documentation':
- '/(doc|文档|readme|guide|tutorial|教程)/i'
'question':
- '/(question|疑问|how to|如何|怎么)/i'
'performance':
- '/(performance|性能|slow|慢|lag|卡顿|optimize)/i'
'core':
- '/(@esengine\/ecs-framework|packages\/core|core package)/i'
'editor':
- '/(editor|编辑器|tauri)/i'
'network':
- '/(network|网络|multiplayer|多人)/i'
'help wanted':
- '/(help wanted|需要帮助|求助)/i'
'good first issue':
- '/(good first issue|新手友好|beginner)/i'

95
.github/labels.yml vendored Normal file
View File

@@ -0,0 +1,95 @@
# GitHub Labels 配置
# 可以使用 https://github.com/Financial-Times/github-label-sync 来同步标签
# Size Labels (PR 大小)
- name: 'size/XS'
color: '00ff00'
description: '极小的改动 (< 10 行)'
- name: 'size/S'
color: '00ff00'
description: '小改动 (10-100 行)'
- name: 'size/M'
color: 'ffff00'
description: '中等改动 (100-500 行)'
- name: 'size/L'
color: 'ff9900'
description: '大改动 (500-1000 行)'
- name: 'size/XL'
color: 'ff0000'
description: '超大改动 (> 1000 行)'
# Type Labels
- name: 'bug'
color: 'd73a4a'
description: '错误或问题'
- name: 'enhancement'
color: 'a2eeef'
description: '新功能或改进'
- name: 'documentation'
color: '0075ca'
description: '文档相关'
- name: 'question'
color: 'd876e3'
description: '问题咨询'
- name: 'performance'
color: 'ff6b6b'
description: '性能相关'
# Scope Labels
- name: 'core'
color: '5319e7'
description: '核心包相关'
- name: 'editor'
color: '5319e7'
description: '编辑器相关'
- name: 'network'
color: '5319e7'
description: '网络相关'
# Status Labels
- name: 'stale'
color: 'ededed'
description: '长时间无活动'
- name: 'wip'
color: 'fbca04'
description: '进行中'
- name: 'help wanted'
color: '008672'
description: '需要帮助'
- name: 'good first issue'
color: '7057ff'
description: '适合新手'
- name: 'quick-review'
color: '00ff00'
description: '小改动,快速 Review'
- name: 'automerge'
color: 'bfdadc'
description: '自动合并'
- name: 'pinned'
color: 'c2e0c6'
description: '置顶,不会被标记为 stale'
- name: 'security'
color: 'ee0701'
description: '安全相关,高优先级'
# Dependencies
- name: 'dependencies'
color: '0366d6'
description: '依赖更新'

View File

@@ -0,0 +1,73 @@
name: AI Batch Analyze Issues
on:
workflow_dispatch:
inputs:
mode:
description: '分析模式'
required: true
type: choice
options:
- 'recent' # 最近 10 个 issue
- 'open' # 所有打开的 issue
- 'all' # 所有 issue慎用
default: 'recent'
permissions:
issues: write
contents: read
jobs:
analyze:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
- name: Install GitHub CLI
run: |
gh --version || (curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
sudo apt update
sudo apt install gh)
- name: Batch Analyze Issues
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
MODE="${{ github.event.inputs.mode }}"
# 获取 issue 列表
if [ "$MODE" = "recent" ]; then
echo "📊 分析最近 10 个 issue..."
ISSUES=$(gh issue list --limit 10 --json number --jq '.[].number')
elif [ "$MODE" = "open" ]; then
echo "📊 分析所有打开的 issue..."
ISSUES=$(gh issue list --state open --json number --jq '.[].number')
else
echo "📊 分析所有 issue这可能需要很长时间..."
ISSUES=$(gh issue list --state all --limit 100 --json number --jq '.[].number')
fi
# 为每个 issue 添加 AI 分析评论
for issue_num in $ISSUES; do
echo "🤖 分析 Issue #$issue_num..."
# 获取 issue 内容
ISSUE_BODY=$(gh issue view $issue_num --json body --jq '.body')
ISSUE_TITLE=$(gh issue view $issue_num --json title --jq '.title')
# 添加触发评论
gh issue comment $issue_num --body "@ai-helper 请帮我分析这个 issue" || true
# 避免 API 限制
sleep 2
done
echo "✅ 批量分析完成!"
echo "查看结果https://github.com/${{ github.repository }}/issues"

61
.github/workflows/ai-helper-tip.yml vendored Normal file
View File

@@ -0,0 +1,61 @@
name: AI Helper Tip
# 对所有新创建的 issue 自动回复 AI 助手使用说明(新老用户都适用)
on:
issues:
types: [opened]
permissions:
issues: write
jobs:
tip:
runs-on: ubuntu-latest
steps:
- name: Post AI Helper Usage Tip
uses: actions/github-script@v7
with:
script: |
const message = [
"## 🤖 AI 助手可用 | AI Helper Available",
"",
"**中文说明:**",
"",
"本项目配备了 AI 智能助手,可以帮助你快速获得解答!",
"",
"**使用方法:** 在评论中提及 `@ai-helper`AI 会自动搜索项目代码并提供解决方案。",
"",
"**示例:**",
"```",
"@ai-helper 如何创建一个新的 System",
"@ai-helper 这个报错是什么原因?",
"```",
"",
"---",
"",
"**English:**",
"",
"This project has an AI assistant to help you get answers quickly!",
"",
"**How to use:** Mention `@ai-helper` in a comment, and AI will automatically search the codebase and provide solutions.",
"",
"**Examples:**",
"```",
"@ai-helper How do I create a new System?",
"@ai-helper What causes this error?",
"```",
"",
"---",
"",
"💡 *AI 助手基于代码库提供建议,复杂问题建议等待维护者回复*",
"💡 *AI suggestions are based on the codebase. For complex issues, please wait for maintainer responses*"
].join('\n');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: message
});
console.log('✅ AI helper tip posted successfully');

85
.github/workflows/ai-issue-helper.yml vendored Normal file
View File

@@ -0,0 +1,85 @@
name: AI Issue Helper
on:
issue_comment:
types: [created]
permissions:
issues: write
contents: read
models: read
jobs:
ai-helper:
runs-on: ubuntu-latest
# 只在真实用户提到 @ai-helper 时触发,忽略机器人评论
if: |
contains(github.event.comment.body, '@ai-helper') &&
github.event.comment.user.type != 'Bot'
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Get Issue Details
id: issue
uses: actions/github-script@v7
with:
script: |
const issue = await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number
});
// 限制长度,避免超过 token 限制
const maxLength = 1000;
const truncate = (str, max) => {
if (!str) return '';
return str.length > max ? str.substring(0, max) + '...[内容过长已截断]' : str;
};
core.exportVariable('ISSUE_TITLE', truncate(issue.data.title || '', 200));
core.exportVariable('ISSUE_BODY', truncate(issue.data.body || '', maxLength));
core.exportVariable('COMMENT_BODY', truncate(context.payload.comment.body || '', 500));
core.exportVariable('ISSUE_NUMBER', context.issue.number);
- name: Create Prompt
id: prompt
run: |
cat > prompt.txt << 'PROMPT_EOF'
Issue #${{ env.ISSUE_NUMBER }}
标题: ${{ env.ISSUE_TITLE }}
内容: ${{ env.ISSUE_BODY }}
评论: ${{ env.COMMENT_BODY }}
请搜索项目代码并提供解决方案。
PROMPT_EOF
- name: AI Analysis
uses: actions/ai-inference@v1
id: ai
with:
model: 'gpt-4o'
enable-github-mcp: true
max-tokens: 1500
system-prompt: |
你是 ECS Framework (TypeScript ECS 框架) 的 AI 助手。
主要代码在 packages/core/src。
搜索相关代码后,用中文简洁回答问题,包含问题分析、解决方案和代码引用。
prompt-file: prompt.txt
- name: Post AI Response
env:
AI_RESPONSE: ${{ steps.ai.outputs.response }}
uses: actions/github-script@v7
with:
script: |
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: process.env.AI_RESPONSE
});

View File

@@ -0,0 +1,56 @@
name: AI Issue Moderator
on:
issues:
types: [opened]
issue_comment:
types: [created]
permissions:
issues: write
contents: read
models: read
jobs:
moderate:
runs-on: ubuntu-latest
steps:
- name: Check Content
uses: actions/ai-inference@v1
id: check
with:
model: 'gpt-4o-mini'
system-prompt: |
你是一个内容审查助手。
检查内容是否包含:
1. 垃圾信息或广告
2. 恶意或攻击性内容
3. 与项目完全无关的内容
只返回 "SPAM" 或 "OK",不要其他内容。
prompt: |
标题:${{ github.event.issue.title || github.event.comment.body }}
内容:
${{ github.event.issue.body || github.event.comment.body }}
- name: Mark as Spam
if: contains(steps.check.outputs.response, 'SPAM')
uses: actions/github-script@v7
with:
script: |
// 添加 spam 标签
github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['spam']
});
// 添加评论
github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: '🤖 这个内容被 AI 检测为可能的垃圾内容。如果这是误判,请联系维护者。\n\n🤖 This content was detected as potential spam by AI. If this is a false positive, please contact the maintainers.'
});

160
.github/workflows/batch-label-issues.yml vendored Normal file
View File

@@ -0,0 +1,160 @@
name: Batch Label Issues
on:
workflow_dispatch:
inputs:
mode:
description: '标签模式'
required: true
type: choice
options:
- 'recent' # 最近 20 个 issue
- 'open' # 所有打开的 issue
- 'unlabeled' # 只处理没有标签的 issue
- 'all' # 所有 issue慎用
default: 'recent'
skip_labeled:
description: '跳过已有标签的 issue'
required: false
type: boolean
default: true
permissions:
issues: write
contents: read
jobs:
batch-label:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
- name: Batch Label Issues
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
MODE="${{ github.event.inputs.mode }}"
SKIP_LABELED="${{ github.event.inputs.skip_labeled }}"
echo "📊 开始批量打标签..."
echo "模式: $MODE"
echo "跳过已标签: $SKIP_LABELED"
# 获取 issue 列表
if [ "$MODE" = "recent" ]; then
echo "📋 获取最近 20 个 issue..."
ISSUES=$(gh issue list --limit 20 --json number,labels,title,body --jq '.[] | {number, labels: [.labels[].name], title, body}')
elif [ "$MODE" = "open" ]; then
echo "📋 获取所有打开的 issue..."
ISSUES=$(gh issue list --state open --json number,labels,title,body --jq '.[] | {number, labels: [.labels[].name], title, body}')
elif [ "$MODE" = "unlabeled" ]; then
echo "📋 获取没有标签的 issue..."
ISSUES=$(gh issue list --state all --json number,labels,title,body --jq '.[] | select(.labels | length == 0) | {number, labels: [.labels[].name], title, body}')
else
echo "📋 获取所有 issue限制 100 个)..."
ISSUES=$(gh issue list --state all --limit 100 --json number,labels,title,body --jq '.[] | {number, labels: [.labels[].name], title, body}')
fi
# 临时文件
echo "$ISSUES" > /tmp/issues.json
# 处理每个 issue
cat /tmp/issues.json | jq -c '.' | while read -r issue; do
ISSUE_NUM=$(echo "$issue" | jq -r '.number')
EXISTING_LABELS=$(echo "$issue" | jq -r '.labels | join(",")')
TITLE=$(echo "$issue" | jq -r '.title')
BODY=$(echo "$issue" | jq -r '.body')
echo ""
echo "🔍 处理 Issue #$ISSUE_NUM: $TITLE"
echo " 现有标签: $EXISTING_LABELS"
# 跳过已有标签的 issue
if [ "$SKIP_LABELED" = "true" ] && [ ! -z "$EXISTING_LABELS" ]; then
echo " ⏭️ 跳过(已有标签)"
continue
fi
# 分析内容并打标签
LABELS_TO_ADD=""
# 检测 bug
if echo "$TITLE $BODY" | grep -iE "(bug|错误|崩溃|crash|error|exception|问题|fix)" > /dev/null; then
LABELS_TO_ADD="$LABELS_TO_ADD bug"
echo " 🐛 检测到: bug"
fi
# 检测 feature request
if echo "$TITLE $BODY" | grep -iE "(feature|功能|enhancement|improve|优化|建议|新增|添加|add)" > /dev/null; then
LABELS_TO_ADD="$LABELS_TO_ADD enhancement"
echo " ✨ 检测到: enhancement"
fi
# 检测 question
if echo "$TITLE $BODY" | grep -iE "(question|疑问|how to|如何|怎么|为什么|why|咨询|\?|)" > /dev/null; then
LABELS_TO_ADD="$LABELS_TO_ADD question"
echo " ❓ 检测到: question"
fi
# 检测 documentation
if echo "$TITLE $BODY" | grep -iE "(doc|文档|readme|guide|tutorial|教程|说明)" > /dev/null; then
LABELS_TO_ADD="$LABELS_TO_ADD documentation"
echo " 📖 检测到: documentation"
fi
# 检测 performance
if echo "$TITLE $BODY" | grep -iE "(performance|性能|slow|慢|lag|卡顿|optimize|优化)" > /dev/null; then
LABELS_TO_ADD="$LABELS_TO_ADD performance"
echo " ⚡ 检测到: performance"
fi
# 检测 core
if echo "$TITLE $BODY" | grep -iE "(@esengine/ecs-framework|packages/core|core package|核心包)" > /dev/null; then
LABELS_TO_ADD="$LABELS_TO_ADD core"
echo " 🎯 检测到: core"
fi
# 检测 editor
if echo "$TITLE $BODY" | grep -iE "(editor|编辑器|tauri)" > /dev/null; then
LABELS_TO_ADD="$LABELS_TO_ADD editor"
echo " 🎨 检测到: editor"
fi
# 检测 network
if echo "$TITLE $BODY" | grep -iE "(network|网络|multiplayer|多人|同步)" > /dev/null; then
LABELS_TO_ADD="$LABELS_TO_ADD network"
echo " 🌐 检测到: network"
fi
# 检测 help wanted
if echo "$TITLE $BODY" | grep -iE "(help wanted|需要帮助|求助)" > /dev/null; then
LABELS_TO_ADD="$LABELS_TO_ADD help wanted"
echo " 🆘 检测到: help wanted"
fi
# 添加标签
if [ ! -z "$LABELS_TO_ADD" ]; then
echo " ✅ 添加标签: $LABELS_TO_ADD"
for label in $LABELS_TO_ADD; do
gh issue edit $ISSUE_NUM --add-label "$label" 2>&1 | grep -v "already exists" || true
done
echo " 💬 添加说明评论..."
gh issue comment $ISSUE_NUM --body $'🤖 自动标签系统检测到此 issue 并添加了相关标签。如有误判,请告知维护者。\n\n🤖 Auto-labeling system detected and labeled this issue. Please let maintainers know if this is incorrect.' || true
else
echo " 未检测到明确类型"
fi
# 避免 API 限制
sleep 1
done
echo ""
echo "✅ 批量标签完成!"
echo "查看结果: https://github.com/${{ github.repository }}/issues"

90
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,90 @@
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: Type check
run: npm run type-check
- name: Lint check
run: npm run lint
- 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

146
.github/workflows/cleanup-dependabot.yml vendored Normal file
View File

@@ -0,0 +1,146 @@
name: Cleanup Old Dependabot PRs
# 手动触发的 workflow用于清理堆积的 Dependabot PR
on:
workflow_dispatch:
inputs:
days_old:
description: '关闭多少天前创建的 PR默认 7 天)'
required: false
default: '7'
dry_run:
description: '试运行模式true=仅显示,不关闭)'
required: false
default: 'true'
type: choice
options:
- 'true'
- 'false'
jobs:
cleanup:
runs-on: ubuntu-latest
permissions:
pull-requests: write
issues: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: List and Close Old Dependabot PRs
uses: actions/github-script@v7
with:
script: |
const daysOld = parseInt('${{ github.event.inputs.days_old }}') || 7;
const dryRun = '${{ github.event.inputs.dry_run }}' === 'true';
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - daysOld);
console.log(`🔍 查找超过 ${daysOld} 天的 Dependabot PR...`);
console.log(`📅 截止日期: ${cutoffDate.toISOString()}`);
console.log(`🏃 模式: ${dryRun ? '试运行(不会实际关闭)' : '实际执行'}`);
console.log('---');
// 获取所有 Dependabot PR
const { data: pulls } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
per_page: 100
});
const dependabotPRs = pulls.filter(pr =>
pr.user.login === 'dependabot[bot]' &&
new Date(pr.created_at) < cutoffDate
);
console.log(`📊 找到 ${dependabotPRs.length} 个符合条件的 Dependabot PR`);
console.log('');
if (dependabotPRs.length === 0) {
console.log('✅ 没有需要清理的 PR');
return;
}
// 按类型分组
const byType = {
dev: [],
prod: [],
actions: [],
other: []
};
for (const pr of dependabotPRs) {
const title = pr.title.toLowerCase();
const labels = pr.labels.map(l => l.name);
let type = 'other';
if (title.includes('dev-dependencies') || title.includes('development')) {
type = 'dev';
} else if (title.includes('production-dependencies')) {
type = 'prod';
} else if (labels.includes('github-actions')) {
type = 'actions';
}
byType[type].push(pr);
}
console.log('📋 PR 分类统计:');
console.log(` 🔧 开发依赖: ${byType.dev.length} 个`);
console.log(` 📦 生产依赖: ${byType.prod.length} 个`);
console.log(` ⚙️ GitHub Actions: ${byType.actions.length} 个`);
console.log(` ❓ 其他: ${byType.other.length} 个`);
console.log('');
// 处理每个 PR
for (const pr of dependabotPRs) {
const age = Math.floor((Date.now() - new Date(pr.created_at)) / (1000 * 60 * 60 * 24));
console.log(`${dryRun ? '🔍' : '🗑️ '} #${pr.number}: ${pr.title}`);
console.log(` 创建时间: ${pr.created_at} (${age} 天前)`);
console.log(` 链接: ${pr.html_url}`);
if (!dryRun) {
await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number,
state: 'closed'
});
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body: `🤖 **自动关闭旧的 Dependabot PR**
此 PR 已超过 ${daysOld} 天未合并,已被自动关闭以清理积压。
📌 **下一步:**
- Dependabot 已配置为月度运行,届时会创建新的分组更新
- 新的 Mergify 规则会智能处理不同类型的依赖更新
- 开发依赖和 GitHub Actions 会自动合并(即使 CI 失败)
- 生产依赖需要 CI 通过才会自动合并
如果需要立即应用此更新,请手动更新依赖。
---
*此操作由仓库维护者手动触发的清理工作流执行*`
});
console.log(' ✅ 已关闭并添加说明');
} else {
console.log(' 试运行模式 - 未执行操作');
}
console.log('');
}
console.log('---');
if (dryRun) {
console.log(`✨ 试运行完成!共发现 ${dependabotPRs.length} 个待清理的 PR`);
console.log('💡 要实际执行清理,请将 dry_run 参数设为 false 重新运行');
} else {
console.log(`✅ 清理完成!已关闭 ${dependabotPRs.length} 个 Dependabot PR`);
}

45
.github/workflows/codecov.yml vendored Normal file
View File

@@ -0,0 +1,45 @@
name: Code Coverage
on:
push:
branches: [ master, main ]
pull_request:
branches: [ master, main ]
jobs:
coverage:
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: Run tests with coverage
run: |
cd packages/core
npm run test:coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./packages/core/coverage/coverage-final.json
flags: core
name: core-coverage
fail_ci_if_error: true
verbose: true
- name: Upload coverage artifact
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: packages/core/coverage/

41
.github/workflows/codeql.yml vendored Normal file
View File

@@ -0,0 +1,41 @@
name: "CodeQL"
on:
push:
branches: [ "master", "main" ]
pull_request:
branches: [ "master", "main" ]
schedule:
- cron: '0 0 * * 1' # 每周一运行
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
queries: security-and-quality
- name: Autobuild
uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"

31
.github/workflows/commitlint.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
name: Commit Lint
on:
pull_request:
types: [opened, synchronize, reopened]
permissions:
contents: read
pull-requests: read
jobs:
commitlint:
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: Install commitlint
run: |
npm install --save-dev @commitlint/config-conventional @commitlint/cli
- name: Validate PR commits
run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose

67
.github/workflows/docs.yml vendored Normal file
View 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

23
.github/workflows/issue-labeler.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
name: Issue Labeler
on:
issues:
types: [opened, edited]
permissions:
issues: write
contents: read
jobs:
label:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Label Issues
uses: github/issue-labeler@v3.4
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
configuration-path: .github/labeler.yml
enable-versioned-regex: 1

28
.github/workflows/issue-translator.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: Issue Translator
on:
issue_comment:
types: [created]
issues:
types: [opened]
permissions:
issues: write
jobs:
translate:
runs-on: ubuntu-latest
steps:
- name: Translate Issues
uses: tomsun28/issues-translate-action@v2.7
with:
IS_MODIFY_TITLE: false
# 设置为 true 会修改标题false 只在评论中添加翻译
CUSTOM_BOT_NOTE: |
<details>
<summary>🌏 Translation / 翻译</summary>
Bot detected the issue body's language is not English, translate it automatically.
机器人检测到 issue 内容非英文,自动翻译。
</details>

157
.github/workflows/release-editor.yml vendored Normal file
View File

@@ -0,0 +1,157 @@
name: Release Editor App
on:
push:
tags:
- 'editor-v*'
workflow_dispatch:
inputs:
version:
description: 'Release version (e.g., 1.0.0)'
required: true
default: '1.0.0'
jobs:
build-tauri:
strategy:
fail-fast: false
matrix:
include:
- platform: windows-latest
target: x86_64-pc-windows-msvc
arch: x64
- platform: macos-latest
target: x86_64-apple-darwin
arch: x64
- platform: macos-latest
target: aarch64-apple-darwin
arch: arm64
runs-on: ${{ matrix.platform }}
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 Rust stable
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Rust cache
uses: Swatinem/rust-cache@v2
with:
workspaces: packages/editor-app/src-tauri
cache-on-failure: true
- name: Install dependencies (Ubuntu)
if: matrix.platform == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
- name: Install frontend dependencies
run: npm ci
- name: Update version in config files (for manual trigger)
if: github.event_name == 'workflow_dispatch'
run: |
cd packages/editor-app
# 临时更新版本号用于构建(不提交到仓库)
npm version ${{ github.event.inputs.version }} --no-git-tag-version
node scripts/sync-version.js
- name: Cache TypeScript build
uses: actions/cache@v4
with:
path: |
packages/core/bin
packages/editor-core/dist
packages/behavior-tree/bin
key: ${{ runner.os }}-ts-build-${{ hashFiles('packages/core/src/**', 'packages/editor-core/src/**', 'packages/behavior-tree/src/**') }}
restore-keys: |
${{ runner.os }}-ts-build-
- name: Build core package
run: npm run build:core
- name: Build editor-core package
run: |
cd packages/editor-core
npm run build
- name: Build behavior-tree package
run: |
cd packages/behavior-tree
npm run build
- name: Build Tauri app
uses: tauri-apps/tauri-action@v0.5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
with:
projectPath: packages/editor-app
tagName: ${{ github.event_name == 'workflow_dispatch' && format('editor-v{0}', github.event.inputs.version) || github.ref_name }}
releaseName: 'ECS Editor v${{ github.event.inputs.version || github.ref_name }}'
releaseBody: 'See the assets to download this version and install.'
releaseDraft: false
prerelease: false
includeUpdaterJson: true
updaterJsonKeepUniversal: false
args: ${{ matrix.platform == 'macos-latest' && format('--target {0}', matrix.target) || '' }}
# 构建成功后,创建 PR 更新版本号
update-version-pr:
needs: build-tauri
if: github.event_name == 'workflow_dispatch' && success()
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'
- name: Update version files
run: |
cd packages/editor-app
npm version ${{ github.event.inputs.version }} --no-git-tag-version
node scripts/sync-version.js
- name: Create Pull Request
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "chore(editor): bump version to ${{ github.event.inputs.version }}"
branch: release/editor-v${{ github.event.inputs.version }}
delete-branch: true
title: "chore(editor): Release v${{ github.event.inputs.version }}"
body: |
## 🚀 Release v${{ github.event.inputs.version }}
This PR updates the editor version after successful release build.
### Changes
- ✅ Updated `packages/editor-app/package.json` → `${{ github.event.inputs.version }}`
- ✅ Updated `packages/editor-app/src-tauri/tauri.conf.json` → `${{ github.event.inputs.version }}`
### Release
- 📦 [GitHub Release](https://github.com/${{ github.repository }}/releases/tag/editor-v${{ github.event.inputs.version }})
---
*This PR was automatically created by the release workflow.*
labels: |
release
editor
automated pr

112
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,112 @@
name: Release NPM Packages
on:
workflow_dispatch:
inputs:
package:
description: '选择要发布的包'
required: true
type: choice
options:
- core
- behavior-tree
- editor-core
version_type:
description: '版本更新类型'
required: true
type: choice
options:
- patch
- minor
- major
- custom
custom_version:
description: '自定义版本号 (仅当选择 custom 时使用,例如: 2.2.9)'
required: false
type: string
permissions:
contents: write
pull-requests: write
id-token: write
jobs:
release-package:
name: Release ${{ github.event.inputs.package }} Package
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
registry-url: 'https://registry.npmjs.org'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build core package (if needed)
if: ${{ github.event.inputs.package == 'behavior-tree' || github.event.inputs.package == 'editor-core' }}
run: |
cd packages/core
npm run build
# - name: Run tests
# run: |
# cd packages/${{ github.event.inputs.package }}
# npm run test:ci
- name: Update version
id: version
run: |
cd packages/${{ github.event.inputs.package }}
if [ "${{ github.event.inputs.version_type }}" = "custom" ]; then
npm version ${{ github.event.inputs.custom_version }} --no-git-tag-version --allow-same-version
else
npm version ${{ github.event.inputs.version_type }} --no-git-tag-version
fi
NEW_VERSION=$(node -p "require('./package.json').version")
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
echo "发布版本: $NEW_VERSION"
- name: Build package
run: |
cd packages/${{ github.event.inputs.package }}
npm run build:npm
- name: Publish to npm
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
cd packages/${{ github.event.inputs.package }}/dist
npm publish --access public
- name: Create Pull Request
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "chore(${{ github.event.inputs.package }}): release v${{ steps.version.outputs.new_version }}"
branch: release/${{ github.event.inputs.package }}-v${{ steps.version.outputs.new_version }}
delete-branch: true
title: "chore(${{ github.event.inputs.package }}): Release v${{ steps.version.outputs.new_version }}"
body: |
## 🚀 Release v${{ steps.version.outputs.new_version }}
此 PR 更新 `@esengine/${{ github.event.inputs.package }}` 包的版本号
### 变更
- ✅ 已发布到 npm: [@esengine/${{ github.event.inputs.package }}@${{ steps.version.outputs.new_version }}](https://www.npmjs.com/package/@esengine/${{ github.event.inputs.package }}/v/${{ steps.version.outputs.new_version }})
- ✅ 更新 `packages/${{ github.event.inputs.package }}/package.json` → `${{ steps.version.outputs.new_version }}`
---
*此 PR 由发布工作流自动创建*
labels: |
release
${{ github.event.inputs.package }}
automated pr

43
.github/workflows/size-limit.yml vendored Normal file
View File

@@ -0,0 +1,43 @@
name: Size Limit
on:
pull_request:
branches:
- master
- main
paths:
- 'packages/core/src/**'
- 'packages/core/package.json'
- '.size-limit.json'
permissions:
contents: read
pull-requests: write
issues: write
jobs:
size:
runs-on: ubuntu-latest
steps:
- name: Checkout
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
run: |
cd packages/core
npm run build:npm
- name: Check bundle size
uses: andresz1/size-limit-action@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
skip_step: install

60
.github/workflows/stale.yml vendored Normal file
View File

@@ -0,0 +1,60 @@
name: Stale Issues and PRs
on:
schedule:
- cron: '0 0 * * *' # 每天运行一次
workflow_dispatch: # 允许手动触发
permissions:
issues: write
pull-requests: write
jobs:
stale:
runs-on: ubuntu-latest
steps:
- name: Stale Bot
uses: actions/stale@v9
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
# Issue 配置
stale-issue-message: |
这个 issue 已经 60 天没有活动了,将在 14 天后自动关闭。
如果这个问题仍然存在,请留言说明情况。
This issue has been inactive for 60 days and will be closed in 14 days.
If this issue is still relevant, please leave a comment.
close-issue-message: |
由于长时间无活动,这个 issue 已被自动关闭。
如需重新打开,请留言说明。
This issue has been automatically closed due to inactivity.
Please comment if you'd like to reopen it.
days-before-issue-stale: 60
days-before-issue-close: 14
stale-issue-label: 'stale'
exempt-issue-labels: 'pinned,security,enhancement,help wanted'
# PR 配置
stale-pr-message: |
这个 PR 已经 30 天没有活动了,将在 7 天后自动关闭。
如果你还在处理这个 PR请更新一下。
This PR has been inactive for 30 days and will be closed in 7 days.
If you're still working on it, please update it.
close-pr-message: |
由于长时间无活动,这个 PR 已被自动关闭。
如需继续,请重新打开或创建新的 PR。
This PR has been automatically closed due to inactivity.
Please reopen or create a new PR to continue.
days-before-pr-stale: 30
days-before-pr-close: 7
stale-pr-label: 'stale'
exempt-pr-labels: 'pinned,security,wip'
# 其他配置
operations-per-run: 100
remove-stale-when-updated: true
ascending: true

58
.github/workflows/welcome.yml vendored Normal file
View File

@@ -0,0 +1,58 @@
name: Welcome
on:
issues:
types: [opened]
pull_request_target:
types: [opened]
permissions:
issues: write
pull-requests: write
jobs:
welcome:
runs-on: ubuntu-latest
steps:
- name: Welcome new contributors
uses: actions/first-interaction@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
issue-message: |
👋 你好!感谢你提交第一个 issue
我们会尽快查看并回复。同时,建议你:
- 📚 查看[文档](https://esengine.github.io/ecs-framework/)
- 🤖 使用 [AI 文档助手](https://deepwiki.com/esengine/ecs-framework)
- 💬 加入 [QQ 交流群](https://jq.qq.com/?_wv=1027&k=29w1Nud6)
---
👋 Hello! Thanks for opening your first issue!
We'll review it as soon as possible. Meanwhile, you might want to:
- 📚 Check the [documentation](https://esengine.github.io/ecs-framework/)
- 🤖 Use [AI documentation assistant](https://deepwiki.com/esengine/ecs-framework)
pr-message: |
👋 你好!感谢你提交第一个 Pull Request
在我们 Review 之前,请确保:
- ✅ 代码遵循项目规范
- ✅ 通过所有测试
- ✅ 更新了相关文档
- ✅ Commit 遵循 [Conventional Commits](https://www.conventionalcommits.org/) 规范
查看完整的[贡献指南](https://github.com/esengine/ecs-framework/blob/master/CONTRIBUTING.md)。
---
👋 Hello! Thanks for your first Pull Request!
Before we review, please ensure:
- ✅ Code follows project conventions
- ✅ All tests pass
- ✅ Documentation is updated
- ✅ Commits follow [Conventional Commits](https://www.conventionalcommits.org/)
See the full [Contributing Guide](https://github.com/esengine/ecs-framework/blob/master/CONTRIBUTING.md).

91
.gitignore vendored
View File

@@ -1,11 +1,80 @@
/source/node_modules
/source/bin
/demo/bin-debug
/demo/bin-release
/.idea
/.vscode
/demo_wxgame
/demo/.wing
/demo/.idea
/demo/.vscode
/source/docs
# 依赖目录
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# 构建输出
bin/
dist/
*.tgz
# TypeScript
*.tsbuildinfo
# 临时文件
*.tmp
*.temp
.cache/
# IDE 配置
.idea/
.vscode/
*.swp
*.swo
*~
# 操作系统文件
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# 日志文件
logs/
*.log
# 环境配置
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# 测试覆盖率
coverage/
*.lcov
# 包管理器锁文件保留npm的忽略其他的
yarn.lock
pnpm-lock.yaml
# 文档生成
docs/api/
docs/build/
docs/.vitepress/cache/
docs/.vitepress/dist/
# 备份文件
*.bak
*.backup
# 演示项目构建产物
/demo/bin-debug/
/demo/bin-release/
/demo/.wing/
/demo/.idea/
/demo/.vscode/
/demo_wxgame/
# Tauri 构建产物
**/src-tauri/target/
**/src-tauri/WixTools/
**/src-tauri/gen/
# Tauri 捆绑输出
**/src-tauri/target/release/bundle/
**/src-tauri/target/debug/bundle/

18
.gitmodules vendored Normal file
View File

@@ -0,0 +1,18 @@
[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 "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 "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

40
.npmignore Normal file
View File

@@ -0,0 +1,40 @@
# 源代码文件
src/
tsconfig*.json
*.ts
!bin/**/*.d.ts
# 开发文件
dev-bin/
scripts/
.vscode/
.git/
.gitignore
# 测试文件
**/*.test.*
**/*.spec.*
**/test/
**/tests/
# 构建缓存
node_modules/
*.log
*.tmp
*.temp
# 文档草稿
docs/draft/
*.draft.md
# 编辑器文件
.DS_Store
Thumbs.db
*.swp
*.swo
*~
# 环境文件
.env
.env.local
.env.*.local

2
.nxignore Normal file
View File

@@ -0,0 +1,2 @@
examples/*/settings/**
extensions/*/settings/**

49
.prettierignore Normal file
View File

@@ -0,0 +1,49 @@
# 依赖和构建输出
node_modules/
dist/
bin/
build/
coverage/
*.min.js
*.min.css
# 编译输出
**/*.d.ts
tsconfig.tsbuildinfo
# 日志
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# 第三方库
thirdparty/
examples/lawn-mower-demo/
extensions/
# 文档生成
docs/.vitepress/cache/
docs/.vitepress/dist/
docs/api/
# 临时文件
*.tmp
*.bak
*.swp
*~
# 系统文件
.DS_Store
Thumbs.db
# 编辑器
.vscode/
.idea/
# 其他
*.backup
CHANGELOG.md
LICENSE
README.md

14
.prettierrc Normal file
View File

@@ -0,0 +1,14 @@
{
"semi": true,
"singleQuote": true,
"tabWidth": 4,
"useTabs": false,
"trailingComma": "none",
"printWidth": 120,
"arrowParens": "always",
"endOfLine": "lf",
"bracketSpacing": true,
"quoteProps": "as-needed",
"jsxSingleQuote": false,
"proseWrap": "preserve"
}

25
.size-limit.json Normal file
View File

@@ -0,0 +1,25 @@
[
{
"name": "@esengine/ecs-framework (ESM)",
"path": "packages/core/dist/esm/index.js",
"import": "*",
"limit": "50 KB",
"webpack": false,
"gzip": true
},
{
"name": "@esengine/ecs-framework (UMD)",
"path": "packages/core/dist/umd/ecs-framework.js",
"limit": "60 KB",
"webpack": false,
"gzip": true
},
{
"name": "Core Runtime (Tree-shaking)",
"path": "packages/core/dist/esm/index.js",
"import": "{ Core, Scene, Entity, Component }",
"limit": "30 KB",
"webpack": false,
"gzip": true
}
]

133
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,133 @@
# 贡献指南 / Contributing Guide
感谢你对 ECS Framework 的关注!
Thank you for your interest in contributing to ECS Framework!
## Commit 规范 / Commit Convention
本项目使用 [Conventional Commits](https://www.conventionalcommits.org/) 规范。
This project follows the [Conventional Commits](https://www.conventionalcommits.org/) specification.
### 格式 / Format
```
<type>(<scope>): <subject>
<body>
<footer>
```
### 类型 / Types
- **feat**: 新功能 / New feature
- **fix**: 错误修复 / Bug fix
- **docs**: 文档变更 / Documentation changes
- **style**: 代码格式(不影响代码运行) / Code style changes
- **refactor**: 重构(既不是新功能也不是修复) / Code refactoring
- **perf**: 性能优化 / Performance improvements
- **test**: 测试相关 / Test changes
- **build**: 构建系统或依赖变更 / Build system changes
- **ci**: CI 配置变更 / CI configuration changes
- **chore**: 其他变更 / Other changes
### 范围 / Scope
- **core**: 核心包 @esengine/ecs-framework
- **math**: 数学库包
- **network-client**: 网络客户端包
- **network-server**: 网络服务端包
- **network-shared**: 网络共享包
- **editor**: 编辑器
- **docs**: 文档
### 示例 / Examples
```bash
# 新功能
feat(core): add component pooling system
# 错误修复
fix(core): fix entity deletion memory leak
# 破坏性变更
feat(core): redesign system lifecycle
BREAKING CHANGE: System.initialize() now requires Scene parameter
```
## 自动发布 / Automatic Release
本项目使用 Semantic Release 自动发布。
This project uses Semantic Release for automatic publishing.
### 版本规则 / Versioning Rules
根据你的 commit 类型,版本号会自动更新:
Based on your commit type, the version will be automatically updated:
- `feat`: 增加 **minor** 版本 (0.x.0)
- `fix`, `perf`, `refactor`: 增加 **patch** 版本 (0.0.x)
- `BREAKING CHANGE`: 增加 **major** 版本 (x.0.0)
### 发布流程 / Release Process
1. 提交代码到 `master` 分支 / Push commits to `master` branch
2. GitHub Actions 自动运行测试 / GitHub Actions runs tests automatically
3. Semantic Release 分析 commits / Semantic Release analyzes commits
4. 自动更新版本号 / Version is automatically updated
5. 自动生成 CHANGELOG.md / CHANGELOG.md is automatically generated
6. 自动发布到 npm / Package is automatically published to npm
7. 自动创建 GitHub Release / GitHub Release is automatically created
## 开发流程 / Development Workflow
1. Fork 本仓库 / Fork this repository
2. 创建特性分支 / Create a feature branch
```bash
git checkout -b feat/my-feature
```
3. 提交你的变更 / Commit your changes
```bash
git commit -m "feat(core): add new feature"
```
4. 推送到你的 Fork / Push to your fork
```bash
git push origin feat/my-feature
```
5. 创建 Pull Request / Create a Pull Request
## 本地测试 / Local Testing
```bash
# 安装依赖
npm install
# 运行测试
npm test
# 构建
npm run build
# 代码检查
npm run lint
# 代码格式化
npm run format
```
## 问题反馈 / Issue Reporting
如果你发现了 bug 或有新功能建议,请[创建 Issue](https://github.com/esengine/ecs-framework/issues/new)。
If you find a bug or have a feature request, please [create an issue](https://github.com/esengine/ecs-framework/issues/new).
## 许可证 / License
通过贡献代码,你同意你的贡献将遵循 MIT 许可证。
By contributing, you agree that your contributions will be licensed under the MIT License.

214
LICENSE
View File

@@ -1,201 +1,21 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
MIT License
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
Copyright (c) 2025 ECS Framework
1. Definitions.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

453
README.md
View File

@@ -1,295 +1,274 @@
# ECS Framework
[![CI](https://github.com/esengine/ecs-framework/workflows/CI/badge.svg)](https://github.com/esengine/ecs-framework/actions)
[![codecov](https://codecov.io/gh/esengine/ecs-framework/graph/badge.svg)](https://codecov.io/gh/esengine/ecs-framework)
[![npm version](https://badge.fury.io/js/%40esengine%2Fecs-framework.svg)](https://badge.fury.io/js/%40esengine%2Fecs-framework)
[![npm downloads](https://img.shields.io/npm/dm/@esengine/ecs-framework.svg)](https://www.npmjs.com/package/@esengine/ecs-framework)
[![Bundle Size](https://img.shields.io/bundlephobia/minzip/@esengine/ecs-framework)](https://bundlephobia.com/package/@esengine/ecs-framework)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-3178C6?style=flat&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![All Contributors](https://img.shields.io/badge/all_contributors-0-orange.svg?style=flat-square)](#contributors)
[![GitHub stars](https://img.shields.io/github/stars/esengine/ecs-framework?style=social)](https://github.com/esengine/ecs-framework/stargazers)
[![DeepWiki](https://img.shields.io/badge/_AI_文档-DeepWiki-6366f1?style=flat&logo=gitbook&logoColor=white)](https://deepwiki.com/esengine/ecs-framework)
一个轻量级的 TypeScript ECSEntity-Component-System框架专为小游戏开发设计适用于 Laya、Cocos 等游戏引擎。
<div align="center">
## ✨ 特性
<p>一个高性能的 TypeScript ECS (Entity-Component-System) 框架,专为现代游戏开发而设计。</p>
- 🚀 **轻量级 ECS 架构** - 基于实体组件系统,提供清晰的代码结构
- 📡 **事件系统** - 内置 Emitter 事件发射器,支持类型安全的事件管理
-**定时器系统** - 完整的定时器管理,支持延迟和重复任务
- 🔍 **查询系统** - 基于位掩码的高性能实体查询
- 🛠️ **性能监控** - 内置性能监控工具,帮助优化游戏性能
- 🎯 **对象池** - 内存管理优化,减少垃圾回收压力
- 📊 **数学库** - 完整的 2D 数学运算支持
<p>A high-performance TypeScript ECS (Entity-Component-System) framework designed for modern game development.</p>
## 📦 安装
</div>
---
## 📊 项目统计 / Project Stats
<div align="center">
[![Star History Chart](https://api.star-history.com/svg?repos=esengine/ecs-framework&type=Date)](https://star-history.com/#esengine/ecs-framework&Date)
</div>
<div align="center">
<a href="https://github.com/esengine/ecs-framework/graphs/contributors">
<img src="https://contrib.rocks/image?repo=esengine/ecs-framework" />
</a>
</div>
### 📈 下载趋势 / Download Trends
<div align="center">
[![NPM Downloads](https://img.shields.io/npm/dt/@esengine/ecs-framework?label=Total%20Downloads&style=for-the-badge&color=blue)](https://www.npmjs.com/package/@esengine/ecs-framework)
[![NPM Trends](https://img.shields.io/npm/dm/@esengine/ecs-framework?label=Monthly%20Downloads&style=for-the-badge&color=success)](https://npmtrends.com/@esengine/ecs-framework)
</div>
---
## 特性
- **高性能** - 针对大规模实体优化支持SoA存储和批量处理
- **多线程计算** - Worker系统支持真正的并行处理充分利用多核CPU性能
- **类型安全** - 完整的TypeScript支持编译时类型检查
- **现代架构** - 支持多World、多Scene的分层架构设计
- **开发友好** - 内置调试工具和性能监控
- **跨平台** - 支持Cocos Creator、Laya引擎和Web平台
## 安装
```bash
npm install @esengine/ecs-framework
```
## 🚀 快速开始
### 1. 初始化框架
## 快速开始
```typescript
import { Core, CoreEvents } from '@esengine/ecs-framework';
import { Core, Scene, Component, EntitySystem, ECSComponent, ECSSystem, Matcher, Time } from '@esengine/ecs-framework';
// 创建 Core 实例
const core = Core.create(true); // true 表示开启调试模式
// 在游戏循环中更新框架
function gameLoop() {
// 发送帧更新事件
Core.emitter.emit(CoreEvents.frameUpdated);
}
```
### 2. 创建场景
```typescript
import { Scene, Vector2, EntitySystem } from '@esengine/ecs-framework';
class GameScene extends Scene {
public initialize() {
// 创建玩家实体
const player = this.createEntity("Player");
// 设置位置
player.position = new Vector2(100, 100);
// 添加自定义组件
const movement = player.addComponent(new MovementComponent());
// 添加系统
this.addEntityProcessor(new MovementSystem());
}
public onStart() {
console.log("游戏场景已启动");
// 定义组件
@ECSComponent('Position')
class Position extends Component {
constructor(public x = 0, public y = 0) {
super();
}
}
// 设置当前场景
Core.scene = new GameScene();
```
### 3. 创建组件
```typescript
import { Component, Vector2, Time } from '@esengine/ecs-framework';
class MovementComponent extends Component {
public speed: number = 100;
public direction: Vector2 = Vector2.zero;
public update() {
if (this.direction.length > 0) {
const movement = this.direction.multiply(this.speed * Time.deltaTime);
this.entity.position = this.entity.position.add(movement);
}
@ECSComponent('Velocity')
class Velocity extends Component {
constructor(public dx = 0, public dy = 0) {
super();
}
}
```
### 4. 创建系统
```typescript
import { EntitySystem, Entity } from '@esengine/ecs-framework';
// 创建系统
@ECSSystem('Movement')
class MovementSystem extends EntitySystem {
protected process(entities: Entity[]) {
constructor() {
super(Matcher.all(Position, Velocity));
}
protected process(entities: readonly Entity[]): void {
for (const entity of entities) {
const movement = entity.getComponent(MovementComponent);
if (movement) {
movement.update();
}
const position = entity.getComponent(Position)!;
const velocity = entity.getComponent(Velocity)!;
position.x += velocity.dx * Time.deltaTime;
position.y += velocity.dy * Time.deltaTime;
}
}
}
```
## 📚 核心概念
// 创建场景并启动
class GameScene extends Scene {
protected initialize(): void {
this.addSystem(new MovementSystem());
### Entity实体
实体是游戏世界中的基本对象,包含位置、旋转、缩放等基本属性,可以添加组件来扩展功能。
```typescript
import { Vector2 } from '@esengine/ecs-framework';
const entity = scene.createEntity("MyEntity");
entity.position = new Vector2(100, 200);
entity.rotation = Math.PI / 4;
entity.scale = new Vector2(2, 2);
```
### 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 = Math.max(0, this.currentHealth - damage);
if (this.currentHealth <= 0) {
this.entity.destroy();
}
}
}
```
### System系统
系统处理实体集合,实现游戏逻辑。
```typescript
import { EntitySystem, Entity } from '@esengine/ecs-framework';
class HealthSystem extends EntitySystem {
protected process(entities: Entity[]) {
for (const entity of entities) {
const health = entity.getComponent(HealthComponent);
if (health && health.currentHealth <= 0) {
entity.destroy();
}
}
}
}
```
## 🎮 高级功能
### 事件系统
```typescript
import { Core, CoreEvents } from '@esengine/ecs-framework';
// 监听事件
Core.emitter.addObserver(CoreEvents.frameUpdated, this.onFrameUpdate, this);
// 发射自定义事件
Core.emitter.emit("playerDied", { player: entity, score: 1000 });
// 移除监听
Core.emitter.removeObserver(CoreEvents.frameUpdated, this.onFrameUpdate);
```
### 定时器系统
```typescript
import { Core } from '@esengine/ecs-framework';
// 延迟执行
Core.schedule(2.0, false, this, (timer) => {
console.log("2秒后执行");
});
// 重复执行
Core.schedule(1.0, true, this, (timer) => {
console.log("每秒执行一次");
});
```
### 实体查询
```typescript
// 按名称查找
const player = scene.findEntity("Player");
// 按标签查找
const enemies = scene.findEntitiesByTag(1);
// 按ID查找
const entity = scene.findEntityById(123);
```
### 性能监控
```typescript
import { PerformanceMonitor } from '@esengine/ecs-framework';
// 获取性能数据
const monitor = PerformanceMonitor.instance;
console.log("平均FPS:", monitor.averageFPS);
console.log("内存使用:", monitor.memoryUsage);
```
## 🛠️ 开发工具
### 对象池
```typescript
// 创建对象池
class BulletPool extends es.Pool<Bullet> {
protected createObject(): Bullet {
return new Bullet();
const player = this.createEntity("Player");
player.addComponent(new Position(100, 100));
player.addComponent(new Velocity(50, 0));
}
}
const bulletPool = new BulletPool();
// 启动游戏
Core.create();
Core.setScene(new GameScene());
// 获取对象
const bullet = bulletPool.obtain();
// 释放对象
bulletPool.free(bullet);
// 游戏循环中更新
function gameLoop(deltaTime: number) {
Core.update(deltaTime);
}
```
### 实体调试
## 核心特性
```typescript
// 获取实体调试信息
const debugInfo = entity.getDebugInfo();
console.log("实体信息:", debugInfo);
- **实体查询** - 使用 Matcher API 进行高效的实体过滤
- **事件系统** - 类型安全的事件发布/订阅机制
- **性能优化** - SoA 存储优化,支持大规模实体处理
- **多线程支持** - Worker系统实现真正的并行计算充分利用多核CPU
- **多场景** - 支持 World/Scene 分层架构
- **时间管理** - 内置定时器和时间控制系统
// 获取场景统计
const stats = scene.getStats();
console.log("场景统计:", stats);
## 🏗️ 架构设计 / Architecture
```mermaid
graph TB
A[Core 核心] --> B[World 世界]
B --> C[Scene 场景]
C --> D[EntityManager 实体管理器]
C --> E[SystemManager 系统管理器]
D --> F[Entity 实体]
F --> G[Component 组件]
E --> H[EntitySystem 实体系统]
E --> I[WorkerSystem 工作线程系统]
style A fill:#e1f5ff
style B fill:#fff3e0
style C fill:#f3e5f5
style D fill:#e8f5e9
style E fill:#fff9c4
style F fill:#ffebee
style G fill:#e0f2f1
style H fill:#fce4ec
style I fill:#f1f8e9
```
## 📖 文档
## 平台支持
- [快速入门](docs/getting-started.md) - 从零开始学习框架使用
- [核心概念](docs/core-concepts.md) - 深入了解 ECS 架构和设计原理
- [查询系统使用指南](docs/query-system-usage.md) - 学习高性能查询系统的详细用法
支持主流游戏引擎和 Web 平台:
## 🔗 扩展库
- **Cocos Creator**
- **Laya 引擎**
- **原生 Web** - 浏览器环境直接运行
- **小游戏平台** - 微信、支付宝等小游戏
- [路径寻找库](https://github.com/esengine/ecs-astar) - A*、广度优先、Dijkstra、GOAP 算法
- [AI 系统](https://github.com/esengine/BehaviourTree-ai) - 行为树、效用 AI 系统
## ECS Framework Editor
## 🤝 贡献
跨平台桌面编辑器,提供可视化开发和调试工具。
欢迎提交 Issue 和 Pull Request
### 主要功能
### 开发环境设置
- **场景管理** - 可视化场景层级和实体管理
- **组件检视** - 实时查看和编辑实体组件
- **性能分析** - 内置 Profiler 监控系统性能
- **插件系统** - 可扩展的插件架构
- **远程调试** - 连接运行中的游戏进行实时调试
- **自动更新** - 支持热更新,自动获取最新版本
```bash
# 克隆项目
git clone https://github.com/esengine/ecs-framework.git
### 下载
# 进入源码目录
cd ecs-framework/source
[![Latest Release](https://img.shields.io/github/v/release/esengine/ecs-framework?label=下载最新版本&style=for-the-badge)](https://github.com/esengine/ecs-framework/releases/latest)
# 安装依赖
npm install
支持 Windows、macOS (Intel & Apple Silicon)
# 构建项目
npm run build
### 截图
# 运行测试
npm test
```
<img src="screenshots/main_screetshot.png" alt="ECS Framework Editor" width="800">
### 构建要求
<details>
<summary>查看更多截图</summary>
- Node.js >= 14.0.0
- TypeScript >= 4.0.0
**性能分析器**
<img src="screenshots/performance_profiler.png" alt="Performance Profiler" width="600">
## 📄 许可证
**插件管理**
<img src="screenshots/plugin_manager.png" alt="Plugin Manager" width="600">
本项目采用 [MIT](LICENSE) 许可证。
**设置界面**
<img src="screenshots/settings.png" alt="Settings" width="600">
## 💬 交流群
</details>
加入 QQ 群讨论:[ecs游戏框架交流](https://jq.qq.com/?_wv=1027&k=29w1Nud6)
## 示例项目
---
- [Worker系统演示](https://esengine.github.io/ecs-framework/demos/worker-system/) - 多线程物理系统演示,展示高性能并行计算
- [割草机演示](https://github.com/esengine/lawn-mower-demo) - 完整的游戏示例
**ECS Framework** - 让游戏开发更简单、更高效!
## 文档
- [📚 AI智能文档](https://deepwiki.com/esengine/ecs-framework) - AI助手随时解答你的问题
- [快速入门](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 文档
## 生态系统
- [路径寻找](https://github.com/esengine/ecs-astar) - A*、BFS、Dijkstra 算法
- [AI 系统](https://github.com/esengine/BehaviourTree-ai) - 行为树、效用 AI
## 💪 支持项目 / Support the Project
如果这个项目对你有帮助,请考虑:
If this project helps you, please consider:
<div align="center">
[![GitHub Sponsors](https://img.shields.io/badge/Sponsor-GitHub%20Sponsors-ea4aaa?style=for-the-badge&logo=github)](https://github.com/sponsors/esengine)
[![Star on GitHub](https://img.shields.io/badge/⭐_Star-on_GitHub-yellow?style=for-the-badge&logo=github)](https://github.com/esengine/ecs-framework)
</div>
- ⭐ 给项目点个 Star
- 🐛 报告 Bug 或提出新功能
- 📝 改进文档
- 💖 成为赞助者
## 社区与支持
- [问题反馈](https://github.com/esengine/ecs-framework/issues) - Bug 报告和功能建议
- [讨论区](https://github.com/esengine/ecs-framework/discussions) - 提问、分享想法
- [QQ 交流群](https://jq.qq.com/?_wv=1027&k=29w1Nud6) - ecs游戏框架交流
## 贡献者 / Contributors
感谢所有为这个项目做出贡献的人!
Thanks goes to these wonderful people:
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tbody>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/esengine"><img src="https://avatars.githubusercontent.com/esengine?s=100" width="100px;" alt="esengine"/><br /><sub><b>esengine</b></sub></a><br /><a href="#maintenance-esengine" title="Maintenance">🚧</a> <a href="https://github.com/esengine/ecs-framework/commits?author=esengine" title="Code">💻</a> <a href="#design-esengine" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/foxling"><img src="https://avatars.githubusercontent.com/foxling?s=100" width="100px;" alt="LING YE"/><br /><sub><b>LING YE</b></sub></a><br /><a href="https://github.com/esengine/ecs-framework/commits?author=foxling" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MirageTank"><img src="https://avatars.githubusercontent.com/MirageTank?s=100" width="100px;" alt="MirageTank"/><br /><sub><b>MirageTank</b></sub></a><br /><a href="https://github.com/esengine/ecs-framework/commits?author=MirageTank" title="Code">💻</a></td>
</tr>
</tbody>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
本项目遵循 [all-contributors](https://github.com/all-contributors/all-contributors) 规范。欢迎任何形式的贡献!
## 许可证
[MIT](LICENSE) © 2025 ECS Framework

View File

@@ -1,21 +1,53 @@
# Security Policy
# 安全政策
## Supported Versions
## 支持的版本
Use this section to tell people about which versions of your project are
currently being supported with security updates.
我们为以下版本提供安全更新:
| Version | Supported |
| 版本 | 支持状态 |
| ------- | ------------------ |
| 5.1.x | :white_check_mark: |
| 5.0.x | :x: |
| 4.0.x | :white_check_mark: |
| < 4.0 | :x: |
| 2.0.x | :white_check_mark: |
| 1.0.x | :x: |
## Reporting a Vulnerability
## 报告漏洞
Use this section to tell people how to report a vulnerability.
如果您发现了安全漏洞,请通过以下方式报告:
Tell them where to go, how often they can expect to get an update on a
reported vulnerability, what to expect if the vulnerability is accepted or
declined, etc.
### 报告渠道
- **邮箱**: [安全邮箱将在实际部署时提供]
- **GitHub**: 创建私有安全报告(推荐)
### 报告流程
1. **不要**在公开的 issue 中报告安全漏洞
2. 提供详细的漏洞描述,包括:
- 受影响的版本
- 复现步骤
- 潜在的影响范围
- 如果可能,提供修复建议
### 响应时间
- **确认收到**: 72小时内
- **初步评估**: 1周内
- **修复发布**: 根据严重程度通常在2-4周内
### 处理流程
1. 我们会确认漏洞的存在和严重程度
2. 开发修复方案并进行测试
3. 发布安全更新
4. 在修复发布后,会在相关渠道公布漏洞详情
### 安全最佳实践
使用 ECS Framework 时,请遵循以下安全建议:
- 始终使用最新的稳定版本
- 定期更新依赖项
- 在生产环境中禁用调试模式
- 验证所有外部输入数据
- 不要在客户端存储敏感信息
感谢您帮助保持 ECS Framework 的安全性!

53
codecov.yml Normal file
View File

@@ -0,0 +1,53 @@
# Codecov 配置文件
# https://docs.codecov.com/docs/codecov-yaml
coverage:
status:
# 项目整体覆盖率要求
project:
default:
target: auto
threshold: 1%
base: auto
# 补丁覆盖率要求(针对 PR 中的新代码)
patch:
default:
target: 50% # 降低补丁覆盖率要求到 50%
threshold: 5%
base: auto
# 精确度设置
precision: 2
round: down
range: "70...100"
# 注释设置
comment:
layout: "reach,diff,flags,tree,files"
behavior: default
require_changes: false
require_base: false
require_head: true
# 忽略的文件/目录
ignore:
- "tests/**/*"
- "**/*.test.ts"
- "**/*.spec.ts"
- "**/test/**/*"
- "**/tests/**/*"
- "bin/**/*"
- "dist/**/*"
- "node_modules/**/*"
# 标志组
flags:
core:
paths:
- packages/core/src/
carryforward: true
# GitHub Checks 配置
github_checks:
annotations: true

240
docs/.vitepress/config.mjs Normal file
View File

@@ -0,0 +1,240 @@
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: '实体查询系统', link: '/guide/entity-query' },
{
text: '系统架构 (System)',
link: '/guide/system',
items: [
{ text: 'Worker系统 (多线程)', link: '/guide/worker-system' }
]
},
{
text: '场景管理 (Scene)',
link: '/guide/scene',
items: [
{ text: 'SceneManager', link: '/guide/scene-manager' },
{ text: 'WorldManager', link: '/guide/world-manager' }
]
},
{
text: '行为树系统 (Behavior Tree)',
link: '/guide/behavior-tree/',
items: [
{ text: '快速开始', link: '/guide/behavior-tree/getting-started' },
{ text: '核心概念', link: '/guide/behavior-tree/core-concepts' },
{ text: '编辑器指南', link: '/guide/behavior-tree/editor-guide' },
{ text: '编辑器工作流', link: '/guide/behavior-tree/editor-workflow' },
{ text: '自定义动作组件', link: '/guide/behavior-tree/custom-actions' },
{ text: 'Cocos Creator集成', link: '/guide/behavior-tree/cocos-integration' },
{ text: 'Laya引擎集成', link: '/guide/behavior-tree/laya-integration' },
{ text: '高级用法', link: '/guide/behavior-tree/advanced-usage' },
{ text: '最佳实践', link: '/guide/behavior-tree/best-practices' }
]
},
{ text: '序列化系统 (Serialization)', link: '/guide/serialization' },
{ text: '事件系统 (Event)', link: '/guide/event-system' },
{ text: '时间和定时器 (Time)', link: '/guide/time-and-timers' },
{ text: '日志系统 (Logger)', link: '/guide/logging' }
]
},
{
text: '高级特性',
collapsed: false,
items: [
{ text: '服务容器 (Service Container)', link: '/guide/service-container' },
{ text: '插件系统 (Plugin System)', link: '/guide/plugin-system' }
]
},
{
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'
}
}
})

View File

@@ -1,496 +0,0 @@
# 核心概念
ECS Framework 基于 Entity-Component-System 架构模式,这是一种高度模块化和可扩展的游戏开发架构。本文档将详细介绍框架的核心概念。
## ECS 架构概述
ECS 架构将传统的面向对象设计分解为三个核心部分:
- **Entity实体** - 游戏世界中的对象,包含基本属性如位置、旋转、缩放
- **Component组件** - 包含数据和行为的功能模块
- **System系统** - 处理实体集合的逻辑处理单元
## Core核心
Core 是框架的核心管理类,负责游戏的生命周期管理。
### 创建和配置
```typescript
import { Core } from './Core';
// 创建核心实例(调试模式)
const core = Core.create(true);
// 创建核心实例(发布模式)
const core = Core.create(false);
```
### 事件系统
```typescript
import { CoreEvents } from './ECS/CoreEvents';
// 监听核心事件
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 './ECS/Scene';
// 创建场景
const scene = new Scene();
scene.name = "GameScene";
// 设置为当前场景
Core.scene = scene;
// 场景生命周期
scene.begin(); // 开始场景
scene.update(); // 更新场景
scene.end(); // 结束场景
```
## Entity实体
实体是游戏世界中的基本对象,包含位置、旋转、缩放等基本属性。
### 实体的基本属性
```typescript
import { Vector2 } from './Math/Vector2';
const entity = scene.createEntity("MyEntity");
// 位置
entity.position = new Vector2(100, 200);
entity.position = entity.position.add(new Vector2(10, 0));
// 旋转(弧度)
entity.rotation = Math.PI / 4;
// 缩放
entity.scale = new Vector2(2, 2);
// 标签(用于分类)
entity.tag = 1;
// 启用状态
entity.enabled = true;
// 活跃状态
entity.active = true;
// 更新顺序
entity.updateOrder = 10;
```
### 实体层级关系
```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 './ECS/Component';
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();
```
## Scene场景
场景是实体和系统的容器,管理游戏世界的状态。
### 场景生命周期
```typescript
class GameScene extends es.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
class MovementSystem extends es.EntitySystem {
protected process(entities: es.Entity[]) {
for (const entity of entities) {
const movement = entity.getComponent(MovementComponent);
if (movement) {
movement.update();
}
}
}
}
```
### ProcessingSystem
定期处理的系统:
```typescript
class HealthRegenerationSystem extends es.ProcessingSystem {
protected process(entities: es.Entity[]) {
for (const entity of entities) {
const health = entity.getComponent(HealthComponent);
if (health && health.currentHealth < health.maxHealth) {
health.currentHealth += 10 * es.Time.deltaTime;
}
}
}
}
```
### IntervalSystem
按时间间隔执行的系统:
```typescript
class SpawnSystem extends es.IntervalSystem {
constructor() {
super(3.0); // 每3秒执行一次
}
protected processSystem() {
// 生成敌人
const enemy = this.scene.createEntity("Enemy");
enemy.addComponent(new EnemyComponent());
}
}
```
### PassiveSystem
被动系统,不自动处理实体:
```typescript
class CollisionSystem extends es.PassiveSystem {
public checkCollisions() {
// 手动调用的碰撞检测逻辑
}
}
```
## Time时间
时间管理工具类,提供游戏时间相关功能:
```typescript
// 获取时间信息
console.log("帧时间:", es.Time.deltaTime);
console.log("总时间:", es.Time.totalTime);
console.log("帧数:", es.Time.frameCount);
console.log("时间缩放:", es.Time.timeScale);
// 设置时间缩放(慢动作效果)
es.Time.timeScale = 0.5;
// 检查时间间隔
if (es.Time.checkEvery(1.0, lastCheckTime)) {
// 每秒执行一次
}
```
## Vector2二维向量
二维向量类,提供数学运算:
```typescript
// 创建向量
const vec1 = new es.Vector2(10, 20);
const vec2 = es.Vector2.zero;
const vec3 = es.Vector2.one;
// 向量运算
const sum = vec1.add(vec2);
const diff = vec1.subtract(vec2);
const scaled = vec1.multiply(2);
const normalized = vec1.normalize();
// 向量属性
console.log("长度:", vec1.length);
console.log("长度平方:", vec1.lengthSquared);
// 静态方法
const distance = es.Vector2.distance(vec1, vec2);
const lerped = es.Vector2.lerp(vec1, vec2, 0.5);
const fromAngle = es.Vector2.fromAngle(Math.PI / 4);
```
## 性能监控
框架内置性能监控工具:
```typescript
// 获取性能监控实例
const monitor = es.PerformanceMonitor.instance;
// 查看性能数据
console.log("平均FPS:", monitor.averageFPS);
console.log("最小FPS:", monitor.minFPS);
console.log("最大FPS:", monitor.maxFPS);
console.log("内存使用:", monitor.memoryUsage);
// 重置性能数据
monitor.reset();
```
## 对象池
内存管理优化工具:
```typescript
// 创建对象池
class BulletPool extends es.Pool<Bullet> {
protected createObject(): Bullet {
return new Bullet();
}
}
const bulletPool = new BulletPool();
// 使用对象池
const bullet = bulletPool.obtain();
// 使用bullet...
bulletPool.free(bullet);
// 清空对象池
bulletPool.clear();
```
## 最佳实践
### 1. 实体设计
- 实体只包含基本属性,功能通过组件实现
- 合理使用实体层级关系
- 及时销毁不需要的实体
### 2. 组件设计
- 组件保持单一职责
- 使用生命周期方法进行初始化和清理
- 避免组件间直接依赖
### 3. 系统设计
- 系统专注于特定逻辑处理
- 合理设置系统更新顺序
- 使用被动系统处理特殊逻辑
### 4. 性能优化
- 使用对象池减少内存分配
- 监控性能数据
- 合理使用时间缩放
## 总结
ECS Framework 提供了完整的实体组件系统架构:
- **Core** 管理游戏生命周期和全局功能
- **Entity** 作为游戏对象的基础容器
- **Component** 实现具体的功能模块
- **System** 处理游戏逻辑
- **Scene** 管理游戏世界状态
通过合理使用这些核心概念,可以构建出结构清晰、易于维护的游戏代码。

23
docs/examples/index.md Normal file
View 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)

View 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系统功能包括实体管理、物理模拟和性能监控。

View File

@@ -1,625 +0,0 @@
# 快速入门
本指南将帮助您快速上手 ECS Framework这是一个轻量级的实体组件系统框架专为小游戏设计。
## 项目结构
```
ecs-framework/
├── source/
│ ├── src/ # 源代码
│ │ ├── ECS/ # ECS核心系统
│ │ ├── Math/ # 数学运算
│ │ ├── Types/ # 类型定义
│ │ └── Utils/ # 工具类
│ ├── scripts/ # 构建脚本
│ └── tsconfig.json # TypeScript配置
└── docs/ # 文档
```
## 安装和构建
### 从源码构建
```bash
# 克隆项目
git clone https://github.com/esengine/ecs-framework.git
# 进入源码目录
cd ecs-framework/source
# 编译TypeScript
npx tsc
```
### 直接使用
您可以直接将源码复制到项目中使用或者引用编译后的JavaScript文件。
## 基础设置
### 1. 导入框架
```typescript
// 导入核心类
import { Core } from './Core';
import { Entity } from './ECS/Entity';
import { Component } from './ECS/Component';
import { Scene } from './ECS/Scene';
import { QuerySystem } from './ECS/Core/QuerySystem';
import { Emitter } from './Utils/Emitter';
import { TimerManager } from './Utils/Timers/TimerManager';
```
### 2. 创建基础管理器
```typescript
class GameManager {
private core: Core;
private scene: Scene;
private querySystem: QuerySystem;
private emitter: Emitter;
private timerManager: TimerManager;
constructor() {
// 创建核心实例
this.core = Core.create(true);
// 创建场景
this.scene = new Scene();
this.scene.name = "GameScene";
// 获取场景的查询系统
this.querySystem = this.scene.querySystem;
// 获取核心的事件系统和定时器
this.emitter = Core.emitter;
this.timerManager = this.core._timerManager;
// 设置当前场景
Core.scene = this.scene;
}
public update(deltaTime: number): void {
// 更新定时器
this.timerManager.update(deltaTime);
// 更新场景
this.scene.update();
// 处理系统逻辑
this.updateSystems(deltaTime);
}
private updateSystems(deltaTime: number): void {
// 在这里添加您的系统更新逻辑
}
}
```
### 3. 游戏循环
```typescript
const gameManager = new GameManager();
let lastTime = performance.now();
function gameLoop() {
const currentTime = performance.now();
const deltaTime = (currentTime - lastTime) / 1000; // 转换为秒
lastTime = currentTime;
gameManager.update(deltaTime);
requestAnimationFrame(gameLoop);
}
// 启动游戏循环
gameLoop();
```
## 创建实体和组件
### 1. 定义组件
```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;
constructor(x: number = 0, y: number = 0) {
super();
this.x = x;
this.y = y;
}
}
// 生命值组件
class HealthComponent extends Component {
public maxHealth: number = 100;
public currentHealth: number = 100;
constructor(maxHealth: number = 100) {
super();
this.maxHealth = maxHealth;
this.currentHealth = maxHealth;
}
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;
}
}
```
### 2. 创建实体
```typescript
class GameManager {
// ... 之前的代码 ...
public createPlayer(): Entity {
const player = this.scene.createEntity("Player");
// 添加组件
player.addComponent(new PositionComponent(400, 300));
player.addComponent(new VelocityComponent(0, 0));
player.addComponent(new HealthComponent(100));
// 设置标签和更新顺序
player.tag = 1; // 玩家标签
player.updateOrder = 0;
return player;
}
public createEnemy(x: number, y: number): Entity {
const enemy = this.scene.createEntity("Enemy");
enemy.addComponent(new PositionComponent(x, y));
enemy.addComponent(new VelocityComponent(50, 0));
enemy.addComponent(new HealthComponent(50));
enemy.tag = 2; // 敌人标签
enemy.updateOrder = 1;
return enemy;
}
}
## 使
```typescript
class GameManager {
// ... 之前的代码 ...
private updateSystems(deltaTime: number): void {
this.updateMovementSystem(deltaTime);
this.updateHealthSystem(deltaTime);
this.updateCollisionSystem();
}
private updateMovementSystem(deltaTime: number): void {
// 查询所有具有位置和速度组件的实体
const movableEntities = this.querySystem.queryTwoComponents(
PositionComponent,
VelocityComponent
);
movableEntities.forEach(({ entity, component1: position, component2: velocity }) => {
// 更新位置
position.x += velocity.x * deltaTime;
position.y += velocity.y * deltaTime;
// 边界检查
if (position.x < 0 || position.x > 800) {
velocity.x = -velocity.x;
}
if (position.y < 0 || position.y > 600) {
velocity.y = -velocity.y;
}
});
}
private updateHealthSystem(deltaTime: number): void {
// 查询所有具有生命值组件的实体
const healthEntities = this.querySystem.queryComponentTyped(HealthComponent);
const deadEntities: Entity[] = [];
healthEntities.forEach(({ entity, component: health }) => {
// 检查死亡
if (health.isDead()) {
deadEntities.push(entity);
}
});
// 移除死亡实体
deadEntities.forEach(entity => {
entity.destroy();
});
}
private updateCollisionSystem(): void {
// 获取玩家
const players = this.scene.findEntitiesByTag(1); // 玩家标签
const enemies = this.scene.findEntitiesByTag(2); // 敌人标签
players.forEach(player => {
const playerPos = player.getComponent(PositionComponent);
const playerHealth = player.getComponent(HealthComponent);
if (!playerPos || !playerHealth) return;
enemies.forEach(enemy => {
const enemyPos = enemy.getComponent(PositionComponent);
if (!enemyPos) return;
// 简单的距离检测
const distance = Math.sqrt(
Math.pow(playerPos.x - enemyPos.x, 2) +
Math.pow(playerPos.y - enemyPos.y, 2)
);
if (distance < 50) { // 碰撞距离
playerHealth.takeDamage(10);
console.log(`当前生命值: ${playerHealth.currentHealth}`);
}
});
});
}
}
```
## 使用事件系统
框架内置了事件系统,用于组件间通信:
```typescript
// 定义事件类型
enum GameEvents {
PLAYER_DIED = 'playerDied',
ENEMY_SPAWNED = 'enemySpawned',
SCORE_CHANGED = 'scoreChanged'
}
class GameManager {
// ... 之前的代码 ...
constructor() {
// ... 之前的代码 ...
// 监听事件
this.emitter.on(GameEvents.PLAYER_DIED, this.onPlayerDied.bind(this));
this.emitter.on(GameEvents.ENEMY_SPAWNED, this.onEnemySpawned.bind(this));
}
private onPlayerDied(player: Entity): void {
console.log('游戏结束!');
// 重置游戏或显示游戏结束界面
}
private onEnemySpawned(enemy: Entity): void {
console.log('新敌人出现!');
}
private updateHealthSystem(deltaTime: number): void {
const healthEntities = this.querySystem.queryComponentTyped(HealthComponent);
healthEntities.forEach(({ entity, component: health }) => {
if (health.isDead()) {
// 发送死亡事件
if (entity.tag === 1) { // 玩家
this.emitter.emit(GameEvents.PLAYER_DIED, entity);
}
entity.destroy();
}
});
}
}
```
## 使用定时器
框架提供了强大的定时器系统:
```typescript
class GameManager {
// ... 之前的代码 ...
public startGame(): void {
// 创建玩家
this.createPlayer();
// 每2秒生成一个敌人
Core.schedule(2.0, true, this, (timer) => {
const x = Math.random() * 800;
const y = Math.random() * 600;
const enemy = this.createEnemy(x, y);
this.emitter.emit(GameEvents.ENEMY_SPAWNED, enemy);
});
// 5秒后增加敌人生成速度
Core.schedule(5.0, false, this, (timer) => {
console.log('游戏难度提升!');
// 可以在这里修改敌人生成间隔
});
}
}
##
```typescript
// 导入框架
import { Core } from './Core';
import { Entity } from './ECS/Entity';
import { Component } from './ECS/Component';
import { Scene } from './ECS/Scene';
import { QuerySystem } from './ECS/Core/QuerySystem';
import { Emitter } from './Utils/Emitter';
// 定义组件
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 HealthComponent extends Component {
constructor(public maxHealth: number = 100) {
super();
this.currentHealth = maxHealth;
}
public currentHealth: number;
public takeDamage(damage: number): void {
this.currentHealth = Math.max(0, this.currentHealth - damage);
}
public isDead(): boolean {
return this.currentHealth <= 0;
}
}
// 游戏事件
enum GameEvents {
PLAYER_DIED = 'playerDied',
ENEMY_SPAWNED = 'enemySpawned'
}
// 完整的游戏管理器
class SimpleGame {
private core: Core;
private scene: Scene;
private querySystem: QuerySystem;
private emitter: Emitter;
private isRunning: boolean = false;
constructor() {
this.core = Core.create(true);
this.scene = new Scene();
this.scene.name = "SimpleGame";
this.querySystem = this.scene.querySystem;
this.emitter = Core.emitter;
// 设置场景
Core.scene = this.scene;
// 监听事件
this.emitter.on(GameEvents.PLAYER_DIED, () => {
console.log('游戏结束!');
this.isRunning = false;
});
}
public start(): void {
console.log('游戏开始!');
this.isRunning = true;
// 创建玩家
this.createPlayer();
// 定期生成敌人
Core.schedule(2.0, true, this, (timer) => {
if (this.isRunning) {
this.createEnemy();
}
});
// 启动游戏循环
this.gameLoop();
}
private createPlayer(): Entity {
const player = this.scene.createEntity("Player");
player.addComponent(new PositionComponent(400, 300));
player.addComponent(new VelocityComponent(100, 0));
player.addComponent(new HealthComponent(100));
player.tag = 1; // 玩家标签
return player;
}
private createEnemy(): Entity {
const enemy = this.scene.createEntity("Enemy");
const x = Math.random() * 800;
const y = Math.random() * 600;
enemy.addComponent(new PositionComponent(x, y));
enemy.addComponent(new VelocityComponent(-50, 0));
enemy.addComponent(new HealthComponent(50));
enemy.tag = 2; // 敌人标签
this.emitter.emit(GameEvents.ENEMY_SPAWNED, enemy);
return enemy;
}
private update(deltaTime: number): void {
// 更新场景
this.scene.update();
// 更新游戏系统
this.updateMovement(deltaTime);
this.updateCollision();
this.updateHealth();
}
private updateMovement(deltaTime: number): void {
const movableEntities = this.querySystem.queryTwoComponents(
PositionComponent,
VelocityComponent
);
movableEntities.forEach(({ entity, component1: pos, component2: vel }) => {
pos.x += vel.x * deltaTime;
pos.y += vel.y * deltaTime;
// 边界检查
if (pos.x < 0 || pos.x > 800) vel.x = -vel.x;
if (pos.y < 0 || pos.y > 600) vel.y = -vel.y;
});
}
private updateCollision(): void {
const players = this.scene.findEntitiesByTag(1);
const enemies = this.scene.findEntitiesByTag(2);
players.forEach(player => {
const playerPos = player.getComponent(PositionComponent);
const playerHealth = player.getComponent(HealthComponent);
if (!playerPos || !playerHealth) return;
enemies.forEach(enemy => {
const enemyPos = enemy.getComponent(PositionComponent);
if (!enemyPos) return;
const distance = Math.sqrt(
Math.pow(playerPos.x - enemyPos.x, 2) +
Math.pow(playerPos.y - enemyPos.y, 2)
);
if (distance < 50) {
playerHealth.takeDamage(10);
console.log(`玩家生命值: ${playerHealth.currentHealth}`);
}
});
});
}
private updateHealth(): void {
const healthEntities = this.querySystem.queryComponentTyped(HealthComponent);
const deadEntities: Entity[] = [];
healthEntities.forEach(({ entity, component: health }) => {
if (health.isDead()) {
deadEntities.push(entity);
if (entity.tag === 1) { // 玩家死亡
this.emitter.emit(GameEvents.PLAYER_DIED, entity);
}
}
});
// 移除死亡实体
deadEntities.forEach(entity => {
entity.destroy();
});
}
private gameLoop(): void {
let lastTime = performance.now();
const loop = () => {
if (!this.isRunning) return;
const currentTime = performance.now();
const deltaTime = (currentTime - lastTime) / 1000;
lastTime = currentTime;
this.update(deltaTime);
requestAnimationFrame(loop);
};
loop();
}
}
// 启动游戏
const game = new SimpleGame();
game.start();
```
## 下一步
现在您已经掌握了 ECS Framework 的基础用法,可以继续学习:
- [核心概念](core-concepts.md) - 深入了解 ECS 架构和设计原理
- [查询系统使用指南](query-system-usage.md) - 学习高性能查询系统的详细用法
## 常见问题
### Q: 如何在不同游戏引擎中集成?
A: ECS Framework 是引擎无关的,您只需要:
1. 将框架源码复制到项目中
2. 在游戏引擎的主循环中调用 `scene.update()`
3. 根据需要集成渲染、输入等引擎特定功能
### Q: 如何处理输入?
A: 框架本身不提供输入处理,建议:
1. 创建一个输入组件来存储输入状态
2. 在游戏循环中更新输入状态
3. 在相关组件中读取输入状态并处理
### Q: 如何调试?
A: 框架提供了多种调试功能:
- 使用 `entity.getDebugInfo()` 查看实体信息
- 使用 `querySystem.getPerformanceReport()` 查看查询性能
- 使用 `querySystem.getStats()` 查看详细统计信息
### Q: 性能如何优化?
A: 框架已经内置了多种性能优化:
- 使用位掩码进行快速组件匹配
- 多级索引系统加速查询
- 智能缓存减少重复计算
- 批量操作减少开销
建议定期调用 `querySystem.optimizeIndexes()` 来自动优化配置。

View File

@@ -0,0 +1,392 @@
# 高级用法
本文介绍行为树系统的高级功能和使用技巧。
## 全局黑板
全局黑板在所有行为树实例之间共享数据。
### 使用全局黑板
```typescript
import { GlobalBlackboardService } from '@esengine/behavior-tree';
import { Core } from '@esengine/ecs-framework';
// 获取全局黑板服务
const globalBlackboard = Core.services.resolve(GlobalBlackboardService);
// 设置全局变量
globalBlackboard.setValue('gameState', 'playing');
globalBlackboard.setValue('playerCount', 4);
globalBlackboard.setValue('difficulty', 'hard');
// 读取全局变量
const gameState = globalBlackboard.getValue('gameState');
const playerCount = globalBlackboard.getValue<number>('playerCount');
```
### 在自定义执行器中访问全局黑板
```typescript
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '@esengine/behavior-tree';
import { GlobalBlackboardService } from '@esengine/behavior-tree';
import { Core } from '@esengine/ecs-framework';
export class CheckGameState implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const globalBlackboard = Core.services.resolve(GlobalBlackboardService);
const gameState = globalBlackboard.getValue('gameState');
if (gameState === 'paused') {
return TaskStatus.Failure;
}
return TaskStatus.Success;
}
}
```
## 性能优化
### 1. 降低更新频率
对于不需要每帧更新的AI,可以使用冷却装饰器:
```typescript
// 每0.1秒执行一次
const ai = BehaviorTreeBuilder.create('ThrottledAI')
.cooldown(0.1, 'ThrottleRoot')
.selector('MainLogic')
// AI逻辑...
.end()
.end()
.build();
```
### 2. 条件缓存
在自定义执行器中缓存昂贵的条件检查结果:
```typescript
export class CachedCheck implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { state, runtime, totalTime } = context;
const cacheTime = state.lastCheckTime || 0;
// 如果缓存未过期(1秒内),直接使用缓存结果
if (totalTime - cacheTime < 1.0) {
return state.cachedResult || TaskStatus.Failure;
}
// 执行昂贵的检查
const result = performExpensiveCheck();
const status = result ? TaskStatus.Success : TaskStatus.Failure;
// 缓存结果
state.cachedResult = status;
state.lastCheckTime = totalTime;
return status;
}
reset(context: NodeExecutionContext): void {
context.state.cachedResult = undefined;
context.state.lastCheckTime = undefined;
}
}
```
### 3. 分帧执行
将大量计算分散到多帧:
```typescript
export class ProcessLargeDataset implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { state, runtime } = context;
const data = runtime.getBlackboardValue<any[]>('dataset') || [];
let processedIndex = state.processedIndex || 0;
const batchSize = 100; // 每帧处理100个
const endIndex = Math.min(processedIndex + batchSize, data.length);
for (let i = processedIndex; i < endIndex; i++) {
processItem(data[i]);
}
state.processedIndex = endIndex;
if (endIndex >= data.length) {
return TaskStatus.Success;
}
return TaskStatus.Running;
}
reset(context: NodeExecutionContext): void {
context.state.processedIndex = 0;
}
}
```
## 调试技巧
### 1. 使用日志节点
在关键位置添加日志:
```typescript
const tree = BehaviorTreeBuilder.create('Debug')
.log('开始战斗序列', 'StartCombat')
.sequence('Combat')
.log('检查生命值', 'CheckHealth')
.blackboardCompare('health', 0, 'greater')
.log('执行攻击', 'Attack')
.end()
.build();
```
### 2. 监控黑板状态
```typescript
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
// 输出所有黑板变量
console.log('黑板变量:', runtime?.getAllBlackboardVariables());
// 输出活动节点
console.log('活动节点:', Array.from(runtime?.activeNodeIds || []));
```
### 3. 在自定义执行器中调试
```typescript
export class DebugAction implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { nodeData, runtime, state } = context;
console.log(`[${nodeData.name}] 开始执行`);
console.log('配置:', nodeData.config);
console.log('状态:', state);
console.log('黑板:', runtime.getAllBlackboardVariables());
// 执行逻辑...
return TaskStatus.Success;
}
}
```
### 4. 性能分析
测量节点执行时间:
```typescript
export class ProfiledAction implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const startTime = performance.now();
// 执行操作
doSomething();
const elapsed = performance.now() - startTime;
console.log(`[${context.nodeData.name}] 耗时: ${elapsed.toFixed(2)}ms`);
return TaskStatus.Success;
}
}
```
## 常见模式
### 1. 状态机模式
使用行为树实现状态机:
```typescript
const fsm = BehaviorTreeBuilder.create('StateMachine')
.defineBlackboardVariable('currentState', 'idle')
.selector('StateSwitch')
// Idle状态
.sequence('IdleState')
.blackboardCompare('currentState', 'idle', 'equals')
.log('执行Idle行为', 'IdleBehavior')
.end()
// Move状态
.sequence('MoveState')
.blackboardCompare('currentState', 'move', 'equals')
.log('执行Move行为', 'MoveBehavior')
.end()
// Attack状态
.sequence('AttackState')
.blackboardCompare('currentState', 'attack', 'equals')
.log('执行Attack行为', 'AttackBehavior')
.end()
.end()
.build();
```
状态转换通过修改黑板变量实现:
```typescript
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
runtime?.setBlackboardValue('currentState', 'move');
```
### 2. 优先级队列模式
按优先级执行任务:
```typescript
const tree = BehaviorTreeBuilder.create('PriorityQueue')
.selector('Priorities')
// 最高优先级:生存
.sequence('Survive')
.blackboardCompare('health', 20, 'less')
.log('治疗', 'Heal')
.end()
// 中优先级:战斗
.sequence('Combat')
.blackboardExists('nearbyEnemy')
.log('战斗', 'Fight')
.end()
// 低优先级:收集资源
.sequence('Gather')
.log('收集资源', 'CollectResources')
.end()
.end()
.build();
```
### 3. 并行任务模式
同时执行多个任务:
```typescript
const tree = BehaviorTreeBuilder.create('ParallelTasks')
.parallel('Effects', { successPolicy: 'all' })
.log('播放动画', 'PlayAnimation')
.log('播放音效', 'PlaySound')
.log('生成粒子', 'SpawnParticles')
.end()
.build();
```
### 4. 重试模式
失败时重试:
```typescript
// 使用自定义重试装饰器(参见custom-actions.md中的RetryDecorator示例)
// 或者使用UntilSuccess装饰器
const tree = BehaviorTreeBuilder.create('Retry')
.untilSuccess('RetryUntilSuccess')
.log('尝试操作', 'TryOperation')
.end()
.build();
```
### 5. 超时模式
限制任务执行时间:
```typescript
const tree = BehaviorTreeBuilder.create('Timeout')
.timeout(5.0, 'TimeLimit')
.log('长时间运行的任务', 'LongTask')
.end()
.build();
```
## 与游戏引擎集成
### Cocos Creator集成
参见[Cocos Creator集成指南](./cocos-integration.md)
### LayaAir集成
参见[LayaAir集成指南](./laya-integration.md)
## 最佳实践
### 1. 合理使用黑板
```typescript
// 好的做法:使用类型化的黑板访问
const health = runtime.getBlackboardValue<number>('health');
// 好的做法:定义所有黑板变量
const tree = BehaviorTreeBuilder.create('AI')
.defineBlackboardVariable('health', 100)
.defineBlackboardVariable('target', null)
.defineBlackboardVariable('state', 'idle')
// ...
```
### 2. 避免过深的树结构
```typescript
// 不好:嵌套过深
.selector()
.sequence()
.selector()
.sequence()
.selector()
// 太深了!
.end()
.end()
.end()
.end()
.end()
// 好:使用合理的深度
.selector()
.sequence()
.log('Action1')
.log('Action2')
.end()
.sequence()
.log('Action3')
.log('Action4')
.end()
.end()
```
### 3. 使用有意义的节点名称
```typescript
// 好的做法
.selector('CombatDecision')
.sequence('AttackEnemy')
.blackboardExists('target', 'HasTarget')
.log('执行攻击', 'Attack')
.end()
.end()
// 不好的做法
.selector('Node1')
.sequence('Node2')
.blackboardExists('target', 'Node3')
.log('Attack', 'Node4')
.end()
.end()
```
### 4. 模块化设计
将复杂逻辑分解为多个独立的行为树,在需要时组合使用。
### 5. 性能考虑
- 避免在每帧执行昂贵的操作
- 使用冷却装饰器控制执行频率
- 缓存计算结果
- 合理使用并行节点
## 下一步
- 查看[自定义节点执行器](./custom-actions.md)学习如何创建自定义节点
- 阅读[最佳实践](./best-practices.md)了解行为树设计技巧
- 参考[编辑器使用指南](./editor-guide.md)学习可视化编辑

View File

@@ -0,0 +1,506 @@
# 资产管理
本文介绍如何加载、管理和复用行为树资产。
## 为什么需要资产管理?
在实际游戏开发中,你可能会遇到以下场景:
1. **多个实体共享同一个行为树** - 100个敌人使用同一套AI逻辑
2. **动态加载行为树** - 从JSON文件加载行为树配置
3. **子树复用** - 将常用的行为片段(如"巡逻"、"追击")做成独立的子树
4. **运行时切换行为树** - 敌人在不同阶段使用不同的行为树
## BehaviorTreeAssetManager
框架提供了 `BehaviorTreeAssetManager` 服务来统一管理行为树资产。
### 核心概念
- **BehaviorTreeData行为树数据**:行为树的定义,可以被多个实体共享
- **BehaviorTreeRuntimeComponent运行时组件**:每个实体独立的运行时状态
- **AssetManager资产管理器**:统一管理所有 BehaviorTreeData
### 基本使用
```typescript
import { Core } from '@esengine/ecs-framework';
import {
BehaviorTreeAssetManager,
BehaviorTreeBuilder,
BehaviorTreeStarter
} from '@esengine/behavior-tree';
// 1. 获取资产管理器(插件已自动注册)
const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
// 2. 创建并注册行为树资产
const enemyAI = BehaviorTreeBuilder.create('EnemyAI')
.defineBlackboardVariable('health', 100)
.selector('MainBehavior')
.log('攻击')
.end()
.build();
assetManager.loadAsset(enemyAI);
// 3. 为多个实体使用同一份资产
const enemy1 = scene.createEntity('Enemy1');
const enemy2 = scene.createEntity('Enemy2');
const enemy3 = scene.createEntity('Enemy3');
// 获取共享的资产
const sharedTree = assetManager.getAsset('EnemyAI');
if (sharedTree) {
BehaviorTreeStarter.start(enemy1, sharedTree);
BehaviorTreeStarter.start(enemy2, sharedTree);
BehaviorTreeStarter.start(enemy3, sharedTree);
}
```
### 资产管理器 API
```typescript
// 加载资产
assetManager.loadAsset(treeData);
// 获取资产
const tree = assetManager.getAsset('TreeID');
// 检查资产是否存在
if (assetManager.hasAsset('TreeID')) {
// ...
}
// 卸载资产
assetManager.unloadAsset('TreeID');
// 获取所有资产ID
const allIds = assetManager.getAllAssetIds();
// 清空所有资产
assetManager.clearAll();
```
## 从文件加载行为树
### JSON 格式
行为树可以导出为 JSON 格式:
```json
{
"version": "1.0.0",
"metadata": {
"name": "EnemyAI",
"description": "敌人AI行为树"
},
"rootNodeId": "root-1",
"nodes": [
{
"id": "root-1",
"name": "RootSelector",
"nodeType": "Composite",
"data": {
"compositeType": "Selector"
},
"children": ["combat-1", "patrol-1"]
},
{
"id": "combat-1",
"name": "Combat",
"nodeType": "Action",
"data": {
"actionType": "LogAction",
"message": "攻击敌人"
},
"children": []
}
],
"blackboard": [
{
"name": "health",
"type": "number",
"defaultValue": 100
}
]
}
```
### 加载 JSON 文件
```typescript
import {
BehaviorTreeAssetSerializer,
BehaviorTreeAssetManager
} from '@esengine/behavior-tree';
async function loadTreeFromFile(filePath: string) {
const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
// 1. 读取文件内容
const jsonContent = await fetch(filePath).then(res => res.text());
// 2. 反序列化
const treeData = BehaviorTreeAssetSerializer.deserialize(jsonContent);
// 3. 加载到资产管理器
assetManager.loadAsset(treeData);
return treeData;
}
// 使用
const tree = await loadTreeFromFile('/assets/enemy-ai.btree.json');
BehaviorTreeStarter.start(entity, tree);
```
## 子树SubTree
子树允许你将常用的行为片段做成独立的树,然后在其他树中引用。
### 为什么使用子树?
1. **代码复用** - 避免重复定义相同的行为
2. **模块化** - 将复杂的行为树拆分成小的可管理单元
3. **团队协作** - 不同成员可以独立开发不同的子树
### 创建子树
```typescript
// 1. 创建巡逻子树
const patrolTree = BehaviorTreeBuilder.create('PatrolBehavior')
.sequence('Patrol')
.log('选择巡逻点', 'PickWaypoint')
.log('移动到巡逻点', 'MoveToWaypoint')
.wait(2.0, 'WaitAtWaypoint')
.end()
.build();
// 2. 创建追击子树
const chaseTree = BehaviorTreeBuilder.create('ChaseBehavior')
.sequence('Chase')
.log('锁定目标', 'LockTarget')
.log('追击目标', 'ChaseTarget')
.end()
.build();
// 3. 注册子树到资产管理器
const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
assetManager.loadAsset(patrolTree);
assetManager.loadAsset(chaseTree);
```
### 使用子树
```typescript
// 在主行为树中使用子树
const mainTree = BehaviorTreeBuilder.create('EnemyAI')
.defineBlackboardVariable('hasTarget', false)
.selector('MainBehavior')
// 条件:发现目标时执行追击子树
.sequence('CombatBranch')
.blackboardExists('hasTarget')
.subTree('ChaseBehavior', { shareBlackboard: true })
.end()
// 默认:执行巡逻子树
.subTree('PatrolBehavior', { shareBlackboard: true })
.end()
.build();
assetManager.loadAsset(mainTree);
// 启动主行为树
const enemy = scene.createEntity('Enemy');
BehaviorTreeStarter.start(enemy, mainTree);
```
### SubTree 配置选项
```typescript
.subTree('SubTreeID', {
shareBlackboard: true, // 是否共享黑板默认true
})
```
- **shareBlackboard: true** - 子树和父树共享黑板变量
- **shareBlackboard: false** - 子树使用独立的黑板
## 资源预加载
在游戏启动时预加载所有行为树资产:
```typescript
class BehaviorTreePreloader {
private assetManager: BehaviorTreeAssetManager;
constructor() {
this.assetManager = Core.services.resolve(BehaviorTreeAssetManager);
}
async preloadAll() {
// 定义所有行为树文件
const treeFiles = [
'/assets/ai/enemy-ai.btree.json',
'/assets/ai/boss-ai.btree.json',
'/assets/ai/patrol.btree.json',
'/assets/ai/chase.btree.json'
];
// 并行加载所有文件
const loadPromises = treeFiles.map(file => this.loadTree(file));
await Promise.all(loadPromises);
console.log(`已加载 ${this.assetManager.getAssetCount()} 个行为树资产`);
}
private async loadTree(filePath: string) {
const jsonContent = await fetch(filePath).then(res => res.text());
const treeData = BehaviorTreeAssetSerializer.deserialize(jsonContent);
this.assetManager.loadAsset(treeData);
}
}
// 游戏启动时调用
const preloader = new BehaviorTreePreloader();
await preloader.preloadAll();
```
## 运行时切换行为树
敌人在不同阶段使用不同的行为树:
```typescript
class EnemyAI {
private entity: Entity;
private assetManager: BehaviorTreeAssetManager;
constructor(entity: Entity) {
this.entity = entity;
this.assetManager = Core.services.resolve(BehaviorTreeAssetManager);
}
// 切换到巡逻AI
switchToPatrol() {
const tree = this.assetManager.getAsset('PatrolAI');
if (tree) {
BehaviorTreeStarter.stop(this.entity);
BehaviorTreeStarter.start(this.entity, tree);
}
}
// 切换到战斗AI
switchToCombat() {
const tree = this.assetManager.getAsset('CombatAI');
if (tree) {
BehaviorTreeStarter.stop(this.entity);
BehaviorTreeStarter.start(this.entity, tree);
}
}
// 切换到狂暴模式
switchToBerserk() {
const tree = this.assetManager.getAsset('BerserkAI');
if (tree) {
BehaviorTreeStarter.stop(this.entity);
BehaviorTreeStarter.start(this.entity, tree);
}
}
}
// 使用
const enemyAI = new EnemyAI(enemyEntity);
// Boss血量低于30%时进入狂暴
const runtime = enemyEntity.getComponent(BehaviorTreeRuntimeComponent);
const health = runtime?.getBlackboardValue<number>('health');
if (health && health < 30) {
enemyAI.switchToBerserk();
}
```
## 内存优化
### 1. 共享行为树数据
```typescript
// 好的做法100个敌人共享1份BehaviorTreeData
const sharedTree = assetManager.getAsset('EnemyAI');
for (let i = 0; i < 100; i++) {
const enemy = scene.createEntity(`Enemy${i}`);
BehaviorTreeStarter.start(enemy, sharedTree!); // 共享数据
}
// 不好的做法每个敌人创建独立的BehaviorTreeData
for (let i = 0; i < 100; i++) {
const enemy = scene.createEntity(`Enemy${i}`);
const tree = BehaviorTreeBuilder.create('EnemyAI') // 重复创建
// ... 节点定义
.build();
BehaviorTreeStarter.start(enemy, tree);
}
```
### 2. 及时卸载不用的资产
```typescript
// 关卡结束时卸载该关卡的AI
function onLevelEnd() {
assetManager.unloadAsset('Level1BossAI');
assetManager.unloadAsset('Level1EnemyAI');
}
// 加载新关卡的AI
function onLevelStart() {
const boss2AI = await loadTreeFromFile('/assets/level2-boss.btree.json');
assetManager.loadAsset(boss2AI);
}
```
## 完整示例:多敌人类型的游戏
```typescript
import { Core, Scene } from '@esengine/ecs-framework';
import {
BehaviorTreePlugin,
BehaviorTreeAssetManager,
BehaviorTreeBuilder,
BehaviorTreeStarter
} from '@esengine/behavior-tree';
async function setupGame() {
// 1. 初始化
Core.create();
const plugin = new BehaviorTreePlugin();
await Core.installPlugin(plugin);
const scene = new Scene();
plugin.setupScene(scene);
Core.setScene(scene);
const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
// 2. 创建共享的子树
const patrolTree = BehaviorTreeBuilder.create('Patrol')
.sequence('PatrolLoop')
.log('巡逻')
.wait(1.0)
.end()
.build();
const combatTree = BehaviorTreeBuilder.create('Combat')
.sequence('CombatLoop')
.log('战斗')
.end()
.build();
assetManager.loadAsset(patrolTree);
assetManager.loadAsset(combatTree);
// 3. 创建不同类型敌人的AI
const meleeEnemyAI = BehaviorTreeBuilder.create('MeleeEnemyAI')
.selector('MeleeBehavior')
.sequence('Attack')
.blackboardExists('target')
.log('近战攻击')
.end()
.subTree('Patrol')
.end()
.build();
const rangedEnemyAI = BehaviorTreeBuilder.create('RangedEnemyAI')
.selector('RangedBehavior')
.sequence('Attack')
.blackboardExists('target')
.log('远程攻击')
.end()
.subTree('Patrol')
.end()
.build();
assetManager.loadAsset(meleeEnemyAI);
assetManager.loadAsset(rangedEnemyAI);
// 4. 创建多个敌人实体
// 10个近战敌人共享同一份AI
const meleeAI = assetManager.getAsset('MeleeEnemyAI')!;
for (let i = 0; i < 10; i++) {
const enemy = scene.createEntity(`MeleeEnemy${i}`);
BehaviorTreeStarter.start(enemy, meleeAI);
}
// 5个远程敌人共享同一份AI
const rangedAI = assetManager.getAsset('RangedEnemyAI')!;
for (let i = 0; i < 5; i++) {
const enemy = scene.createEntity(`RangedEnemy${i}`);
BehaviorTreeStarter.start(enemy, rangedAI);
}
console.log(`已创建 15 个敌人,使用 ${assetManager.getAssetCount()} 个行为树资产`);
// 5. 游戏循环
setInterval(() => {
Core.update(0.016);
}, 16);
}
setupGame();
```
## 常见问题
### 如何检查资产是否已加载?
```typescript
const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
if (!assetManager.hasAsset('EnemyAI')) {
// 加载资产
const tree = await loadTreeFromFile('/assets/enemy-ai.btree.json');
assetManager.loadAsset(tree);
}
```
### 子树找不到怎么办?
确保子树已经加载到资产管理器中:
```typescript
// 1. 先加载子树
const subTree = BehaviorTreeBuilder.create('SubTreeID')
// ...
.build();
assetManager.loadAsset(subTree);
// 2. 再加载使用子树的主树
const mainTree = BehaviorTreeBuilder.create('MainTree')
.subTree('SubTreeID')
.build();
```
### 如何导出行为树为 JSON
```typescript
import { BehaviorTreeAssetSerializer } from '@esengine/behavior-tree';
const tree = BehaviorTreeBuilder.create('MyTree')
// ... 节点定义
.build();
// 序列化为JSON字符串
const json = BehaviorTreeAssetSerializer.serialize(tree);
// 保存到文件或发送到服务器
console.log(json);
```
## 下一步
- 学习[Cocos Creator 集成](./cocos-integration.md)了解如何在游戏引擎中加载资源
- 查看[自定义节点执行器](./custom-actions.md)创建自定义行为
- 阅读[最佳实践](./best-practices.md)优化你的行为树设计

View File

@@ -0,0 +1,468 @@
# 最佳实践
本文介绍行为树设计和使用的最佳实践,帮助你构建高效、可维护的AI系统。
## 行为树设计原则
### 1. 保持树的层次清晰
将复杂行为分解成清晰的层次结构:
```
Root Selector
├── Emergency (高优先级:紧急情况)
│ ├── FleeFromDanger
│ └── CallForHelp
├── Combat (中优先级:战斗)
│ ├── Attack
│ └── Defend
└── Idle (低优先级:空闲)
├── Patrol
└── Rest
```
### 2. 单一职责原则
每个节点应该只做一件事。要实现复杂动作,创建自定义执行器,参见[自定义节点执行器](./custom-actions.md)。
```typescript
// 好的设计 - 使用内置节点
.sequence('AttackSequence')
.blackboardExists('target', 'CheckTarget')
.log('瞄准', 'Aim')
.log('开火', 'Fire')
.end()
```
### 3. 使用描述性名称
节点名称应该清楚地表达其功能:
```typescript
// 好的命名
.blackboardCompare('health', 20, 'less', 'CheckHealthLow')
.log('寻找最近的医疗包', 'FindHealthPack')
.log('移动到医疗包', 'MoveToHealthPack')
// 不好的命名
.blackboardCompare('health', 20, 'less', 'C1')
.log('Do something', 'Action1')
.log('Move', 'A2')
```
## 黑板变量管理
### 1. 变量命名规范
使用清晰的命名约定:
```typescript
const tree = BehaviorTreeBuilder.create('AI')
// 状态变量
.defineBlackboardVariable('currentState', 'idle')
.defineBlackboardVariable('isMoving', false)
// 目标和引用
.defineBlackboardVariable('targetEnemy', null)
.defineBlackboardVariable('patrolPoints', [])
// 配置参数
.defineBlackboardVariable('attackRange', 5.0)
.defineBlackboardVariable('moveSpeed', 10.0)
// 临时数据
.defineBlackboardVariable('lastAttackTime', 0)
.defineBlackboardVariable('searchAttempts', 0)
// ...
.build();
```
### 2. 避免过度使用黑板
只在需要跨节点共享的数据才放入黑板。在自定义执行器中使用局部变量:
```typescript
// 好的做法 - 使用局部变量
export class CalculateAction implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
// 局部计算
const temp1 = 10;
const temp2 = 20;
const result = temp1 + temp2;
// 只保存需要共享的结果
context.runtime.setBlackboardValue('calculationResult', result);
return TaskStatus.Success;
}
}
```
### 3. 使用类型安全的访问
```typescript
export class TypeSafeAction implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { runtime } = context;
// 使用泛型进行类型安全访问
const health = runtime.getBlackboardValue<number>('health');
const target = runtime.getBlackboardValue<Entity | null>('target');
const state = runtime.getBlackboardValue<string>('currentState');
if (health !== undefined && health < 30) {
runtime.setBlackboardValue('currentState', 'flee');
}
return TaskStatus.Success;
}
}
```
## 执行器设计
### 1. 保持执行器无状态
状态必须存储在`context.state`中,而不是执行器实例:
```typescript
// 正确的做法
export class TimedAction implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
if (!context.state.startTime) {
context.state.startTime = context.totalTime;
}
const elapsed = context.totalTime - context.state.startTime;
if (elapsed >= 3.0) {
return TaskStatus.Success;
}
return TaskStatus.Running;
}
reset(context: NodeExecutionContext): void {
context.state.startTime = undefined;
}
}
```
### 2. 条件应该是无副作用的
条件检查不应该修改状态:
```typescript
// 好的做法 - 只读检查
@NodeExecutorMetadata({
implementationType: 'IsHealthLow',
nodeType: NodeType.Condition,
displayName: '检查生命值低',
category: '条件',
configSchema: {
threshold: {
type: 'number',
default: 30,
supportBinding: true
}
}
})
export class IsHealthLow implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const threshold = BindingHelper.getValue<number>(context, 'threshold', 30);
const health = context.runtime.getBlackboardValue<number>('health') || 0;
return health < threshold ? TaskStatus.Success : TaskStatus.Failure;
}
}
```
### 3. 错误处理
```typescript
export class SafeAction implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
try {
const resourceId = context.runtime.getBlackboardValue('resourceId');
if (!resourceId) {
console.error('[SafeAction] 资源ID未设置');
return TaskStatus.Failure;
}
// 执行操作...
return TaskStatus.Success;
} catch (error) {
console.error('[SafeAction] 执行失败:', error);
context.runtime.setBlackboardValue('lastError', error.message);
return TaskStatus.Failure;
}
}
}
```
## 性能优化技巧
### 1. 使用冷却装饰器
避免高频执行昂贵操作:
```typescript
const tree = BehaviorTreeBuilder.create('ThrottledAI')
.cooldown(1.0, 'ThrottleSearch') // 最多每秒执行一次
.log('昂贵的搜索操作', 'ExpensiveSearch')
.end()
.build();
```
### 2. 缓存计算结果
```typescript
export class CachedFindNearest implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { state, runtime, totalTime } = context;
// 检查缓存是否有效
const cacheTime = state.enemyCacheTime || 0;
if (totalTime - cacheTime < 0.5) { // 缓存0.5秒
const cached = runtime.getBlackboardValue('nearestEnemy');
return cached ? TaskStatus.Success : TaskStatus.Failure;
}
// 执行搜索
const nearest = findNearestEnemy();
runtime.setBlackboardValue('nearestEnemy', nearest);
state.enemyCacheTime = totalTime;
return nearest ? TaskStatus.Success : TaskStatus.Failure;
}
reset(context: NodeExecutionContext): void {
context.state.enemyCacheTime = undefined;
}
}
```
### 3. 使用早期退出
```typescript
const tree = BehaviorTreeBuilder.create('EarlyExit')
.selector('FindTarget')
// 先检查缓存的目标
.blackboardExists('cachedTarget', 'HasCachedTarget')
// 没有缓存才进行搜索(需要自定义执行器)
.log('执行昂贵的搜索', 'SearchNewTarget')
.end()
.build();
```
## 可维护性
### 1. 使用有意义的节点名称
```typescript
// 好的做法
const tree = BehaviorTreeBuilder.create('CombatAI')
.selector('CombatDecision')
.sequence('AttackEnemy')
.blackboardExists('target', 'HasTarget')
.log('执行攻击', 'Attack')
.end()
.end()
.build();
// 不好的做法
const tree = BehaviorTreeBuilder.create('AI')
.selector('Node1')
.sequence('Node2')
.blackboardExists('target', 'Node3')
.log('Attack', 'Node4')
.end()
.end()
.build();
```
### 2. 使用编辑器创建复杂树
对于复杂的AI,使用可视化编辑器:
- 更直观的结构
- 方便非程序员调整
- 易于版本控制
- 支持实时调试
### 3. 添加注释和文档
```typescript
// 为行为树添加清晰的注释
const bossAI = BehaviorTreeBuilder.create('BossAI')
.defineBlackboardVariable('phase', 1) // 1=正常, 2=狂暴, 3=濒死
.selector('MainBehavior')
// 阶段3: 生命值<20%,使用终极技能
.sequence('Phase3')
.blackboardCompare('phase', 3, 'equals')
.log('使用终极技能', 'UltimateAbility')
.end()
// 阶段2: 生命值<50%,进入狂暴
.sequence('Phase2')
.blackboardCompare('phase', 2, 'equals')
.log('进入狂暴模式', 'BerserkMode')
.end()
// 阶段1: 正常战斗
.sequence('Phase1')
.log('普通攻击', 'NormalAttack')
.end()
.end()
.build();
```
## 调试技巧
### 1. 使用日志节点
```typescript
const tree = BehaviorTreeBuilder.create('Debug')
.log('开始攻击序列', 'StartAttack')
.sequence('Attack')
.log('检查目标', 'CheckTarget')
.blackboardExists('target')
.log('执行攻击', 'DoAttack')
.end()
.build();
```
### 2. 在自定义执行器中调试
```typescript
export class DebugAction implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { nodeData, runtime, state } = context;
console.group(`[${nodeData.name}]`);
console.log('配置:', nodeData.config);
console.log('状态:', state);
console.log('黑板:', runtime.getAllBlackboardVariables());
console.log('活动节点:', Array.from(runtime.activeNodeIds));
console.groupEnd();
return TaskStatus.Success;
}
}
```
### 3. 状态可视化
```typescript
export class VisualizeState implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
if (process.env.NODE_ENV === 'development') {
console.group('AI State');
console.log('Entity:', context.entity.name);
console.log('Health:', context.runtime.getBlackboardValue('health'));
console.log('State:', context.runtime.getBlackboardValue('currentState'));
console.log('Target:', context.runtime.getBlackboardValue('target'));
console.groupEnd();
}
return TaskStatus.Success;
}
}
```
## 常见反模式
### 1. 过深的嵌套
```typescript
// 不好 - 太深的嵌套
.selector()
.sequence()
.sequence()
.sequence()
.log('太深了', 'DeepAction')
.end()
.end()
.end()
.end()
// 好 - 使用合理的深度
.selector()
.sequence()
.log('Action1')
.log('Action2')
.end()
.sequence()
.log('Action3')
.log('Action4')
.end()
.end()
```
### 2. 在执行器中存储状态
```typescript
// 错误 - 状态存储在执行器中
export class BadAction implements INodeExecutor {
private startTime = 0; // 错误!多个节点会共享这个值
execute(context: NodeExecutionContext): TaskStatus {
this.startTime = context.totalTime; // 错误!
return TaskStatus.Success;
}
}
// 正确 - 状态存储在context.state中
export class GoodAction implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
if (!context.state.startTime) {
context.state.startTime = context.totalTime; // 正确!
}
return TaskStatus.Success;
}
}
```
### 3. 频繁修改黑板
```typescript
// 不好 - 每帧都修改黑板
export class FrequentUpdate implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const pos = getCurrentPosition();
context.runtime.setBlackboardValue('position', pos); // 每帧都set
context.runtime.setBlackboardValue('velocity', getVelocity());
context.runtime.setBlackboardValue('rotation', getRotation());
return TaskStatus.Running;
}
}
// 好 - 只在需要时修改
export class SmartUpdate implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const oldPos = context.runtime.getBlackboardValue('position');
const newPos = getCurrentPosition();
// 只在位置变化时更新
if (!positionsEqual(oldPos, newPos)) {
context.runtime.setBlackboardValue('position', newPos);
}
return TaskStatus.Running;
}
}
```
## 下一步
- 学习[自定义节点执行器](./custom-actions.md)扩展行为树功能
- 探索[高级用法](./advanced-usage.md)了解更多技巧
- 参考[核心概念](./core-concepts.md)深入理解原理

View File

@@ -0,0 +1,683 @@
# Cocos Creator 集成
本教程将引导你在 Cocos Creator 项目中集成和使用行为树系统。
## 前置要求
- Cocos Creator 3.x 或更高版本
- 基本的 TypeScript 知识
- 已完成[快速开始](./getting-started.md)教程
## 安装
### 步骤1安装依赖
在你的 Cocos Creator 项目根目录下:
```bash
npm install @esengine/ecs-framework @esengine/behavior-tree
```
### 步骤2配置 tsconfig.json
确保 `tsconfig.json` 中包含以下配置:
```json
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"moduleResolution": "node"
}
}
```
## 项目结构
建议的项目结构:
```
assets/
├── scripts/
│ ├── ai/
│ │ ├── EnemyAIComponent.ts # AI 组件
│ │ └── PlayerDetector.ts # 检测器
│ ├── systems/
│ │ └── BehaviorTreeSystem.ts # 行为树系统
│ └── Main.ts # 主入口
├── resources/
│ └── behaviors/
│ ├── enemy-ai.btree.json # 行为树资产
│ └── patrol.btree.json # 子树资产
└── types/
└── enemy-ai.ts # 类型定义
```
## 初始化 ECS 和行为树
### 创建主入口组件
创建 `assets/scripts/Main.ts`
```typescript
import { _decorator, Component } from 'cc';
import { Core, Scene } from '@esengine/ecs-framework';
import { BehaviorTreePlugin } from '@esengine/behavior-tree';
const { ccclass } = _decorator;
@ccclass('Main')
export class Main extends Component {
async onLoad() {
// 初始化 ECS Core
Core.create();
// 安装行为树插件
const behaviorTreePlugin = new BehaviorTreePlugin();
await Core.installPlugin(behaviorTreePlugin);
// 创建并设置场景
const scene = new Scene();
behaviorTreePlugin.setupScene(scene);
Core.setScene(scene);
console.log('ECS 和行为树系统初始化完成');
}
update(deltaTime: number) {
// 更新 ECS会自动更新场景
Core.update(deltaTime);
}
onDestroy() {
// 清理资源
Core.destroy();
}
}
```
### 添加组件到场景
1. 在场景中创建一个空节点(命名为 `GameManager`
2. 添加 `Main` 组件到该节点
## 创建 AI 组件
创建 `assets/scripts/ai/EnemyAIComponent.ts`
```typescript
import { _decorator, Component, Node } from 'cc';
import { Core, Entity } from '@esengine/ecs-framework';
import {
BehaviorTreeBuilder,
BehaviorTreeStarter,
BehaviorTreeRuntimeComponent
} from '@esengine/behavior-tree';
const { ccclass, property } = _decorator;
@ccclass('EnemyAIComponent')
export class EnemyAIComponent extends Component {
private aiEntity: Entity | null = null;
async start() {
// 创建行为树
await this.createBehaviorTree();
}
private async createBehaviorTree() {
try {
// 获取Core管理的场景
const scene = Core.scene;
if (!scene) {
console.error('场景未初始化');
return;
}
// 使用Builder API创建行为树
const tree = BehaviorTreeBuilder.create('EnemyAI')
.defineBlackboardVariable('cocosNode', this.node)
.defineBlackboardVariable('health', 100)
.defineBlackboardVariable('playerNode', null)
.defineBlackboardVariable('detectionRange', 10)
.defineBlackboardVariable('attackRange', 2)
.selector('MainBehavior')
.sequence('Combat')
.blackboardExists('playerNode')
.blackboardCompare('health', 30, 'greater')
.log('攻击玩家', 'AttackPlayer')
.end()
.sequence('Flee')
.blackboardCompare('health', 30, 'lessOrEqual')
.log('逃跑', 'RunAway')
.end()
.log('巡逻', 'Patrol')
.end()
.build();
// 创建AI实体并启动
this.aiEntity = scene.createEntity(`AI_${this.node.name}`);
BehaviorTreeStarter.start(this.aiEntity, tree);
console.log('敌人 AI 已启动');
} catch (error) {
console.error('初始化行为树失败:', error);
}
}
onDestroy() {
// 停止 AI
if (this.aiEntity) {
BehaviorTreeStarter.stop(this.aiEntity);
}
}
}
```
## 与 Cocos 节点交互
### 创建自定义执行器
要实现与Cocos节点的交互需要创建自定义执行器
```typescript
import {
INodeExecutor,
NodeExecutionContext,
NodeExecutorMetadata
} from '@esengine/behavior-tree';
import { TaskStatus, NodeType } from '@esengine/behavior-tree';
import { Animation } from 'cc';
@NodeExecutorMetadata({
implementationType: 'PlayAnimation',
nodeType: NodeType.Action,
displayName: '播放动画',
description: '播放Cocos节点上的动画',
category: 'Cocos',
configSchema: {
animationName: {
type: 'string',
default: 'attack'
}
}
})
export class PlayAnimationAction implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const cocosNode = context.runtime.getBlackboardValue('cocosNode');
const animationName = context.nodeData.config.animationName;
if (!cocosNode) {
return TaskStatus.Failure;
}
const animation = cocosNode.getComponent(Animation);
if (animation) {
animation.play(animationName);
return TaskStatus.Success;
}
return TaskStatus.Failure;
}
}
```
## 完整示例:敌人 AI
### 行为树设计
使用编辑器创建 `enemy-ai.btree.json`
```
RootSelector
├── CombatSequence
│ ├── CheckPlayerInRange (Condition)
│ ├── CheckHealthGood (Condition)
│ └── AttackPlayer (Action)
├── FleeSequence
│ ├── CheckHealthLow (Condition)
│ └── RunAway (Action)
└── PatrolSequence
├── PickWaypoint (Action)
├── MoveToWaypoint (Action)
└── Wait (Action)
```
### 黑板变量
定义以下黑板变量:
- `cocosNode`Node - Cocos 节点引用
- `health`Number - 生命值
- `playerNode`Object - 玩家节点引用
- `detectionRange`Number - 检测范围
- `attackRange`Number - 攻击范围
- `currentWaypoint`Number - 当前路点索引
### 实现检测系统
创建 `assets/scripts/ai/PlayerDetector.ts`
```typescript
import { _decorator, Component, Node, Vec3 } from 'cc';
import { BehaviorTreeRuntimeComponent } from '@esengine/behavior-tree';
const { ccclass, property } = _decorator;
@ccclass('PlayerDetector')
export class PlayerDetector extends Component {
@property(Node)
player: Node = null;
@property
detectionRange: number = 10;
private runtime: BehaviorTreeRuntimeComponent | null = null;
start() {
// 假设AI组件在同一节点上
const aiComponent = this.node.getComponent('EnemyAIComponent') as any;
if (aiComponent && aiComponent.aiEntity) {
this.runtime = aiComponent.aiEntity.getComponent(BehaviorTreeRuntimeComponent);
}
}
update(deltaTime: number) {
if (!this.runtime || !this.player) {
return;
}
// 计算距离
const distance = Vec3.distance(this.node.position, this.player.position);
// 更新黑板
this.runtime.setBlackboardValue('playerNode', this.player);
this.runtime.setBlackboardValue('playerInRange', distance <= this.detectionRange);
this.runtime.setBlackboardValue('distanceToPlayer', distance);
}
}
```
## 资源管理
### 使用 BehaviorTreeAssetManager
框架提供了 `BehaviorTreeAssetManager` 来统一管理行为树资产,避免重复创建:
```typescript
import { Core } from '@esengine/ecs-framework';
import {
BehaviorTreeAssetManager,
BehaviorTreeBuilder,
BehaviorTreeStarter
} from '@esengine/behavior-tree';
// 获取资产管理器(插件已自动注册)
const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
// 创建并注册行为树(只创建一次)
const enemyAI = BehaviorTreeBuilder.create('EnemyAI')
.defineBlackboardVariable('health', 100)
.selector('MainBehavior')
.log('攻击')
.end()
.build();
assetManager.loadAsset(enemyAI);
// 为多个敌人实体使用同一份资产
for (let i = 0; i < 10; i++) {
const enemy = scene.createEntity(`Enemy${i}`);
const tree = assetManager.getAsset('EnemyAI')!;
BehaviorTreeStarter.start(enemy, tree); // 10个敌人共享1份数据
}
```
### 从 Cocos Creator 资源加载
#### 1. 将行为树 JSON 放入 resources 目录
```
assets/
└── resources/
└── behaviors/
├── enemy-ai.btree.json
└── boss-ai.btree.json
```
#### 2. 创建资源加载器
创建 `assets/scripts/BehaviorTreeLoader.ts`
```typescript
import { resources, JsonAsset } from 'cc';
import { Core } from '@esengine/ecs-framework';
import {
BehaviorTreeAssetManager,
BehaviorTreeAssetSerializer,
BehaviorTreeData
} from '@esengine/behavior-tree';
export class BehaviorTreeLoader {
private assetManager: BehaviorTreeAssetManager;
constructor() {
this.assetManager = Core.services.resolve(BehaviorTreeAssetManager);
}
/**
* 从 resources 目录加载行为树
* @param path 相对于 resources 的路径,不带扩展名
* @example await loader.load('behaviors/enemy-ai')
*/
async load(path: string): Promise<BehaviorTreeData | null> {
return new Promise((resolve, reject) => {
resources.load(path, JsonAsset, (err, jsonAsset) => {
if (err) {
console.error(`加载行为树失败: ${path}`, err);
reject(err);
return;
}
try {
// 反序列化 JSON 为 BehaviorTreeData
const jsonStr = JSON.stringify(jsonAsset.json);
const treeData = BehaviorTreeAssetSerializer.deserialize(jsonStr);
// 加载到资产管理器
this.assetManager.loadAsset(treeData);
console.log(`行为树已加载: ${treeData.name}`);
resolve(treeData);
} catch (error) {
console.error(`解析行为树失败: ${path}`, error);
reject(error);
}
});
});
}
/**
* 预加载所有行为树
*/
async preloadAll(paths: string[]): Promise<void> {
const promises = paths.map(path => this.load(path));
await Promise.all(promises);
console.log(`已预加载 ${paths.length} 个行为树`);
}
}
```
#### 3. 在游戏启动时预加载
修改 `Main.ts`
```typescript
import { _decorator, Component } from 'cc';
import { Core, Scene } from '@esengine/ecs-framework';
import { BehaviorTreePlugin } from '@esengine/behavior-tree';
import { BehaviorTreeLoader } from './BehaviorTreeLoader';
const { ccclass } = _decorator;
@ccclass('Main')
export class Main extends Component {
private loader: BehaviorTreeLoader | null = null;
async onLoad() {
// 初始化 ECS Core
Core.create();
// 安装行为树插件
const behaviorTreePlugin = new BehaviorTreePlugin();
await Core.installPlugin(behaviorTreePlugin);
// 创建场景
const scene = new Scene();
behaviorTreePlugin.setupScene(scene);
Core.setScene(scene);
// 创建加载器并预加载所有行为树
this.loader = new BehaviorTreeLoader();
await this.loader.preloadAll([
'behaviors/enemy-ai',
'behaviors/boss-ai',
'behaviors/patrol', // 子树
'behaviors/chase' // 子树
]);
console.log('游戏初始化完成');
}
update(deltaTime: number) {
Core.update(deltaTime);
}
onDestroy() {
Core.destroy();
}
}
```
#### 4. 在敌人组件中使用
```typescript
import { _decorator, Component } from 'cc';
import { Core, Entity } from '@esengine/ecs-framework';
import {
BehaviorTreeAssetManager,
BehaviorTreeStarter
} from '@esengine/behavior-tree';
const { ccclass, property } = _decorator;
@ccclass('EnemyAIComponent')
export class EnemyAIComponent extends Component {
@property
aiType: string = 'enemy-ai'; // 在编辑器中配置使用哪个AI
private aiEntity: Entity | null = null;
start() {
const scene = Core.scene;
if (!scene) return;
// 从资产管理器获取已加载的行为树
const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
const tree = assetManager.getAsset(this.aiType);
if (tree) {
this.aiEntity = scene.createEntity(`AI_${this.node.name}`);
BehaviorTreeStarter.start(this.aiEntity, tree);
// 设置黑板变量
const runtime = this.aiEntity.getComponent(BehaviorTreeRuntimeComponent);
runtime?.setBlackboardValue('cocosNode', this.node);
} else {
console.error(`找不到行为树资产: ${this.aiType}`);
}
}
onDestroy() {
if (this.aiEntity) {
BehaviorTreeStarter.stop(this.aiEntity);
}
}
}
```
## 调试
### 可视化调试信息
创建调试组件显示 AI 状态:
```typescript
import { _decorator, Component, Label } from 'cc';
import { BehaviorTreeRuntimeComponent } from '@esengine/behavior-tree';
const { ccclass, property } = _decorator;
@ccclass('AIDebugger')
export class AIDebugger extends Component {
@property(Label)
debugLabel: Label = null;
private runtime: BehaviorTreeRuntimeComponent | null = null;
start() {
const aiComponent = this.node.getComponent('EnemyAIComponent') as any;
if (aiComponent && aiComponent.aiEntity) {
this.runtime = aiComponent.aiEntity.getComponent(BehaviorTreeRuntimeComponent);
}
}
update() {
if (!this.runtime || !this.debugLabel) {
return;
}
const health = this.runtime.getBlackboardValue('health');
const playerNode = this.runtime.getBlackboardValue('playerNode');
this.debugLabel.string = `Health: ${health}\nHas Target: ${playerNode ? 'Yes' : 'No'}`;
}
}
```
## 性能优化
### 1. 限制行为树数量
合理控制同时运行的行为树数量:
```typescript
class AIManager {
private activeAIs: Entity[] = [];
private maxAIs: number = 20;
addAI(entity: Entity, tree: BehaviorTreeData) {
if (this.activeAIs.length >= this.maxAIs) {
// 移除最远的AI
const furthest = this.findFurthestAI();
if (furthest) {
BehaviorTreeStarter.stop(furthest);
this.activeAIs = this.activeAIs.filter(e => e !== furthest);
}
}
BehaviorTreeStarter.start(entity, tree);
this.activeAIs.push(entity);
}
removeAI(entity: Entity) {
BehaviorTreeStarter.stop(entity);
this.activeAIs = this.activeAIs.filter(e => e !== entity);
}
private findFurthestAI(): Entity | null {
// 根据距离找到最远的AI
// 实现细节略
return this.activeAIs[0];
}
}
```
### 2. 使用冷却装饰器
对于不需要每帧更新的AI使用冷却装饰器
```typescript
const tree = BehaviorTreeBuilder.create('ThrottledAI')
.cooldown(0.2, 'ThrottleRoot') // 每0.2秒执行一次
.selector('MainBehavior')
// AI逻辑...
.end()
.end()
.build();
```
### 3. 缓存计算结果
在自定义执行器中缓存昂贵的计算:
```typescript
export class CachedFindTarget implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { state, runtime, totalTime } = context;
const cacheTime = state.lastFindTime || 0;
if (totalTime - cacheTime < 1.0) {
const cached = runtime.getBlackboardValue('target');
return cached ? TaskStatus.Success : TaskStatus.Failure;
}
const target = findNearestTarget();
runtime.setBlackboardValue('target', target);
state.lastFindTime = totalTime;
return target ? TaskStatus.Success : TaskStatus.Failure;
}
}
```
## 多平台注意事项
### 性能考虑
不同平台的性能差异:
- **Web平台**: 受浏览器性能限制建议减少同时运行的AI数量
- **原生平台**: 性能较好可以运行更多AI
- **小游戏平台**: 内存受限,注意控制行为树数量和复杂度
### 平台适配
```typescript
import { sys } from 'cc';
// 根据平台调整AI数量
const maxAIs = sys.isNative ? 50 : (sys.isBrowser ? 20 : 30);
// 根据平台调整更新频率
const updateInterval = sys.isNative ? 0.016 : 0.05;
```
## 常见问题
### 行为树无法加载?
检查:
1. 资源路径是否正确(相对于 `resources` 目录)
2. 文件是否已添加到项目中
3. 检查控制台错误信息
### AI 不执行?
确保:
1. `Main` 组件的 `update` 方法被调用
2. `Scene.update()` 在每帧被调用
3. 行为树已通过 `BehaviorTreeStarter.start()` 启动
### 黑板变量不更新?
检查:
1. 变量名拼写是否正确
2. 是否在正确的时机更新变量
3. 使用 `BehaviorTreeRuntimeComponent.getBlackboardValue()``setBlackboardValue()` 方法
## 下一步
- 查看[资产管理](./asset-management.md)了解如何加载和管理行为树资产、使用子树
- 学习[高级用法](./advanced-usage.md)了解性能优化和调试技巧
- 阅读[最佳实践](./best-practices.md)优化你的 AI
- 学习[自定义节点执行器](./custom-actions.md)创建自定义行为

View File

@@ -0,0 +1,491 @@
# 核心概念
本文介绍行为树系统的核心概念和工作原理。
## 什么是行为树?
行为树(Behavior Tree)是一种用于控制AI和自动化系统的决策结构。它通过树状层次结构组织任务,从根节点开始逐层执行,直到找到合适的行为。
### 与状态机的对比
传统状态机:
- 基于状态和转换
- 状态之间的转换复杂
- 难以扩展和维护
- 不便于复用
行为树:
- 基于任务和层次结构
- 模块化、易于复用
- 可视化编辑
- 灵活的决策逻辑
## 树结构
行为树由节点组成,形成树状结构:
```
Root (根节点)
├── Selector (选择器)
│ ├── Sequence (序列)
│ │ ├── Condition (条件)
│ │ └── Action (动作)
│ └── Action (动作)
└── Sequence (序列)
├── Action (动作)
└── Wait (等待)
```
每个节点都有:
- 父节点(除了根节点)
- 零个或多个子节点
- 执行状态
- 返回结果
## 节点类型
### 复合节点(Composite)
复合节点有多个子节点,按特定规则执行它们。
#### Selector(选择器)
按顺序尝试执行子节点,直到某个子节点成功。
```typescript
const tree = BehaviorTreeBuilder.create('FindFood')
.selector('FindFoodSelector')
.log('尝试吃附近的食物', 'EatNearby')
.log('搜索食物', 'SearchFood')
.log('放弃', 'GiveUp')
.end()
.build();
```
执行逻辑:
1. 尝试第一个子节点
2. 如果返回Success,选择器成功
3. 如果返回Failure,尝试下一个子节点
4. 如果返回Running,选择器返回Running
5. 所有子节点都失败时,选择器失败
#### Sequence(序列)
按顺序执行所有子节点,直到某个子节点失败。
```typescript
const tree = BehaviorTreeBuilder.create('Attack')
.sequence('AttackSequence')
.blackboardExists('target') // 检查是否有目标
.log('瞄准', 'Aim')
.log('开火', 'Fire')
.end()
.build();
```
执行逻辑:
1. 依次执行子节点
2. 如果子节点返回Failure,序列失败
3. 如果子节点返回Running,序列返回Running
4. 如果子节点返回Success,继续下一个子节点
5. 所有子节点都成功时,序列成功
#### Parallel(并行)
同时执行多个子节点。
```typescript
const tree = BehaviorTreeBuilder.create('PlayEffects')
.parallel('Effects', {
successPolicy: 'all', // 所有任务都要成功
failurePolicy: 'one' // 任一失败则失败
})
.log('播放动画', 'PlayAnimation')
.log('播放音效', 'PlaySound')
.log('生成粒子', 'SpawnEffect')
.end()
.build();
```
策略类型:
- `successPolicy: 'all'`: 所有子节点都成功才成功
- `successPolicy: 'one'`: 任意一个子节点成功就成功
- `failurePolicy: 'all'`: 所有子节点都失败才失败
- `failurePolicy: 'one'`: 任意一个子节点失败就失败
### 装饰器节点(Decorator)
装饰器节点只有一个子节点,用于修改子节点的行为或结果。
#### Inverter(反转)
反转子节点的结果:
```typescript
const tree = BehaviorTreeBuilder.create('CheckSafe')
.inverter('NotHasEnemy')
.blackboardExists('enemy')
.end()
.build();
```
#### Repeater(重复)
重复执行子节点:
```typescript
const tree = BehaviorTreeBuilder.create('Jump3Times')
.repeater(3, 'RepeatJump')
.log('跳跃', 'Jump')
.end()
.build();
```
#### Cooldown(冷却)
限制子节点的执行频率:
```typescript
const tree = BehaviorTreeBuilder.create('UseSkill')
.cooldown(5.0, 'SkillCooldown')
.log('使用特殊技能', 'UseSpecialAbility')
.end()
.build();
```
#### Timeout(超时)
限制子节点的执行时间:
```typescript
const tree = BehaviorTreeBuilder.create('TimedTask')
.timeout(10.0, 'TaskTimeout')
.log('长时间运行的任务', 'ComplexTask')
.end()
.build();
```
### 叶节点(Leaf)
叶节点没有子节点,执行具体的任务。
#### Action(动作)
执行具体操作。内置动作节点包括:
```typescript
const tree = BehaviorTreeBuilder.create('Actions')
.sequence()
.wait(2.0) // 等待2秒
.log('Hello', 'LogAction') // 输出日志
.setBlackboardValue('score', 100) // 设置黑板值
.modifyBlackboardValue('score', 'add', 10) // 修改黑板值
.end()
.build();
```
要实现自定义动作,需要创建自定义执行器,参见[自定义节点执行器](./custom-actions.md)。
#### Condition(条件)
检查条件。内置条件节点包括:
```typescript
const tree = BehaviorTreeBuilder.create('Conditions')
.selector()
.blackboardExists('player') // 检查变量是否存在
.blackboardCompare('health', 50, 'greater') // 比较变量值
.randomProbability(0.5) // 50%概率
.end()
.build();
```
#### Wait(等待)
等待指定时间:
```typescript
const tree = BehaviorTreeBuilder.create('WaitExample')
.wait(2.0, 'Wait2Seconds')
.build();
```
## 任务状态
每个节点执行后返回以下状态之一:
### Success(成功)
任务成功完成。
```typescript
// 内置节点会根据逻辑自动返回Success
.log('任务完成') // 总是返回Success
.blackboardCompare('score', 100, 'greater') // 条件满足时返回Success
```
### Failure(失败)
任务执行失败。
```typescript
.blackboardCompare('score', 100, 'greater') // 条件不满足返回Failure
.blackboardExists('nonExistent') // 变量不存在返回Failure
```
### Running(运行中)
任务需要多帧完成,仍在执行中。
```typescript
.wait(3.0) // 等待过程中返回Running,3秒后返回Success
```
### Invalid(无效)
节点未初始化或已重置。通常不需要手动处理此状态。
## 黑板系统
黑板(Blackboard)是行为树的数据存储系统,用于在节点之间共享数据。
### 本地黑板
每个行为树实例都有自己的本地黑板:
```typescript
const tree = BehaviorTreeBuilder.create('EnemyAI')
.defineBlackboardVariable('health', 100)
.defineBlackboardVariable('target', null)
.defineBlackboardVariable('state', 'idle')
// ...
.build();
```
### 支持的数据类型
黑板支持以下数据类型:
- String字符串
- Number数字
- Boolean布尔值
- Vector2二维向量
- Vector3三维向量
- Object对象引用
- Array数组
示例:
```typescript
const tree = BehaviorTreeBuilder.create('Variables')
.defineBlackboardVariable('name', 'Enemy') // 字符串
.defineBlackboardVariable('count', 0) // 数字
.defineBlackboardVariable('isActive', true) // 布尔值
.defineBlackboardVariable('position', { x: 0, y: 0 }) // 对象(也可用于Vector2)
.defineBlackboardVariable('velocity', { x: 0, y: 0, z: 0 }) // 对象(也可用于Vector3)
.defineBlackboardVariable('items', []) // 数组
.build();
```
### 读写变量
通过`BehaviorTreeRuntimeComponent`访问黑板:
```typescript
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
// 读取变量
const health = runtime?.getBlackboardValue('health');
const target = runtime?.getBlackboardValue('target');
// 写入变量
runtime?.setBlackboardValue('health', 50);
runtime?.setBlackboardValue('lastAttackTime', Date.now());
// 获取所有变量
const allVars = runtime?.getAllBlackboardVariables();
```
也可以使用内置节点操作黑板:
```typescript
const tree = BehaviorTreeBuilder.create('BlackboardOps')
.sequence()
.setBlackboardValue('score', 100) // 设置值
.modifyBlackboardValue('score', 'add', 10) // 增加10
.blackboardCompare('score', 110, 'equals') // 检查是否等于110
.end()
.build();
```
### 全局黑板
所有行为树实例共享的黑板,通过`GlobalBlackboardService`访问:
```typescript
import { GlobalBlackboardService } from '@esengine/behavior-tree';
import { Core } from '@esengine/ecs-framework';
const globalBlackboard = Core.services.resolve(GlobalBlackboardService);
// 设置全局变量
globalBlackboard.setValue('gameState', 'playing');
globalBlackboard.setValue('difficulty', 5);
// 读取全局变量
const gameState = globalBlackboard.getValue('gameState');
```
在自定义执行器中访问全局黑板:
```typescript
import { GlobalBlackboardService } from '@esengine/behavior-tree';
import { Core } from '@esengine/ecs-framework';
export class CheckGameState implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const globalBlackboard = Core.services.resolve(GlobalBlackboardService);
const gameState = globalBlackboard.getValue('gameState');
if (gameState === 'paused') {
return TaskStatus.Failure;
}
return TaskStatus.Success;
}
}
```
## 执行流程
### 初始化
```typescript
// 1. 初始化Core和插件
Core.create();
const plugin = new BehaviorTreePlugin();
await Core.installPlugin(plugin);
// 2. 创建场景
const scene = new Scene();
plugin.setupScene(scene);
Core.setScene(scene);
// 3. 构建行为树
const tree = BehaviorTreeBuilder.create('AI')
// ... 定义节点
.build();
// 4. 创建实体并启动
const entity = scene.createEntity('AIEntity');
BehaviorTreeStarter.start(entity, tree);
```
### 更新循环
```typescript
// 每帧更新
gameLoop(() => {
const deltaTime = getDeltaTime();
Core.update(deltaTime); // Core会自动更新场景和所有行为树
});
```
### 执行顺序
```
1. 从根节点开始
2. 根节点执行其逻辑(通常是Selector或Sequence)
3. 根节点的子节点按顺序执行
4. 每个子节点可能有自己的子节点
5. 叶节点执行具体操作并返回状态
6. 状态向上传播到父节点
7. 父节点根据策略决定如何处理子节点的状态
8. 最终根节点返回整体状态
```
### 执行示例
```typescript
const tree = BehaviorTreeBuilder.create('Example')
.selector('Root') // 1. 执行选择器
.sequence('Branch1') // 2. 尝试第一个分支
.blackboardCompare('ready', true, 'equals', 'CheckReady') // 3. 条件失败
.end() // 4. 序列失败,选择器继续下一个分支
.sequence('Branch2') // 5. 尝试第二个分支
.blackboardCompare('active', true, 'equals', 'CheckActive') // 6. 条件成功
.log('执行动作', 'DoAction') // 7. 动作成功
.end() // 8. 序列成功,选择器成功
.end() // 9. 整个树成功
.build();
```
执行流程图:
```
Root(Selector)
→ Branch1(Sequence)
→ CheckReady: Failure
→ Branch1 fails
→ Branch2(Sequence)
→ CheckActive: Success
→ DoAction: Success
→ Branch2 succeeds
→ Root succeeds
```
## Runtime架构
本框架的行为树采用Runtime执行器架构:
### 核心组件
- **BehaviorTreeData**: 纯数据结构,描述行为树的结构和配置
- **BehaviorTreeRuntimeComponent**: 运行时组件,管理执行状态和黑板
- **BehaviorTreeExecutionSystem**: 执行系统,驱动行为树运行
- **INodeExecutor**: 节点执行器接口,定义节点的执行逻辑
- **NodeExecutionContext**: 执行上下文,包含执行所需的所有信息
### 架构特点
1. **数据与逻辑分离**: BehaviorTreeData是纯数据,执行逻辑在执行器中
2. **无状态执行器**: 执行器实例可以在多个节点间共享,状态存储在Runtime中
3. **类型安全**: 通过TypeScript类型系统保证类型安全
4. **高性能**: 避免不必要的对象创建,优化内存使用
### 数据流
```
BehaviorTreeBuilder
↓ (构建)
BehaviorTreeData
↓ (加载到)
BehaviorTreeAssetManager
↓ (读取)
BehaviorTreeExecutionSystem
↓ (执行)
INodeExecutor.execute(context)
↓ (返回)
TaskStatus
↓ (更新)
NodeRuntimeState
```
## 下一步
现在你已经理解了行为树的核心概念,接下来可以:
- 查看[快速开始](./getting-started.md)创建第一个行为树
- 学习[自定义节点执行器](./custom-actions.md)创建自定义节点
- 探索[高级用法](./advanced-usage.md)了解更多功能
- 阅读[最佳实践](./best-practices.md)学习设计模式

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,119 @@
# 行为树编辑器使用指南
行为树编辑器提供了可视化的方式来创建和编辑行为树。
## 启动编辑器
```bash
cd packages/editor-app
npm run tauri:dev
```
## 基本操作
### 打开行为树编辑器
通过以下方式打开行为树编辑器窗口:
1. 在资产浏览器中双击 `.btree` 文件
2. 菜单栏:`窗口` → 选择行为树编辑器相关插件
### 创建新行为树
在行为树编辑器窗口的工具栏中点击"新建"按钮(加号图标)
### 保存行为树
在行为树编辑器窗口的工具栏中点击"保存"按钮(磁盘图标)
### 添加节点
从左侧节点面板拖拽节点到画布:
- 复合节点Selector、Sequence、Parallel
- 装饰器Inverter、Repeater、UntilFail等
- 动作节点ExecuteAction、Wait等
- 条件节点Condition
### 连接节点
拖拽父节点底部的连接点到子节点顶部建立连接
### 删除节点
选中节点后按 `Delete``Backspace`
### 编辑属性
点击节点后在右侧属性面板中编辑节点参数
## 黑板变量
在黑板面板中管理共享数据:
1. 点击"添加变量"按钮
2. 输入变量名、选择类型并设置默认值
3. 在节点中通过变量名引用黑板变量
支持的变量类型:
- String字符串
- Number数字
- Boolean布尔值
- Vector2二维向量
- Vector3三维向量
- Object对象引用
- Array数组
## 导出运行时资产
### 导出步骤
1. 点击工具栏的"导出"按钮
2. 选择导出模式:
- 当前文件:仅导出当前打开的行为树
- 工作区导出:导出项目中所有行为树
3. 选择资产输出路径
4. 选择TypeScript类型定义输出路径
5. 为每个文件选择导出格式:
- 二进制:.btree.bin默认文件更小加载更快
- JSON.btree.json可读性好便于调试
6. 点击"导出"按钮
### 加载运行时资产
编辑器导出的文件是编辑器格式包含UI布局信息。当前版本中从编辑器导出的资产可以使用Builder API在代码中重新构建或者等待资产加载系统的完善。
推荐使用Builder API创建行为树
```typescript
import { BehaviorTreeBuilder, BehaviorTreeStarter } from '@esengine/behavior-tree';
import { Core, Scene } from '@esengine/ecs-framework';
// 使用Builder创建行为树
const tree = BehaviorTreeBuilder.create('EnemyAI')
.defineBlackboardVariable('health', 100)
.defineBlackboardVariable('target', null)
.selector('MainBehavior')
.sequence('AttackBranch')
.blackboardCompare('health', 50, 'greater')
.log('攻击玩家', 'Attack')
.end()
.log('逃离战斗', 'Flee')
.end()
.build();
// 启动行为树
const entity = scene.createEntity('Enemy');
BehaviorTreeStarter.start(entity, tree);
```
## 支持的操作
- `Delete` / `Backspace`:删除选中的节点或连线
- `Ctrl` + 点击:多选节点
- 框选:拖拽空白区域进行框选
- 拖拽画布:按住鼠标中键或空格键拖拽
## 下一步
- 查看[编辑器工作流](./editor-workflow.md)了解完整的开发流程
- 查看[自定义节点执行器](./custom-actions.md)学习如何扩展节点

View File

@@ -0,0 +1,253 @@
# 编辑器工作流
本教程介绍如何使用行为树编辑器创建AI并在游戏中加载使用。
## 完整流程
```
1. 启动编辑器
2. 创建行为树并定义黑板变量
3. 添加和配置节点
4. 导出JSON文件
5. 在游戏中加载并使用
```
## 使用编辑器创建
### 启动编辑器
```bash
cd packages/editor-app
npm run tauri:dev
```
### 基本操作
1. **创建行为树**`文件``新建项目` → 创建行为树文件
2. **定义黑板变量**:在黑板面板中添加共享变量
3. **添加节点**:从节点面板拖拽到画布
4. **连接节点**:拖拽连接点建立父子关系
5. **配置属性**:选中节点后在属性面板编辑
6. **导出**`文件``导出``JSON格式`
### 示例敌人AI的黑板变量
在编辑器黑板面板中定义:
```
health: Number = 100
target: Object = null
moveSpeed: Number = 5.0
attackRange: Number = 2.0
```
### 示例:行为树结构
```
Root: Selector
├── Combat Sequence
│ ├── CheckHasTarget (Condition)
│ ├── CheckInAttackRange (Condition)
│ └── ExecuteAttack (Action)
├── Patrol Sequence
│ ├── MoveToNextPatrolPoint (Action)
│ └── Wait 2s
└── Idle (Action)
```
## 在游戏中使用
### 使用Builder API创建
推荐使用Builder API在代码中创建行为树
```typescript
import { Core, Scene } from '@esengine/ecs-framework';
import {
BehaviorTreePlugin,
BehaviorTreeBuilder,
BehaviorTreeStarter,
BehaviorTreeRuntimeComponent
} from '@esengine/behavior-tree';
// 初始化
Core.create();
const plugin = new BehaviorTreePlugin();
await Core.installPlugin(plugin);
const scene = new Scene();
plugin.setupScene(scene);
Core.setScene(scene);
// 使用Builder创建行为树
const tree = BehaviorTreeBuilder.create('EnemyAI')
.defineBlackboardVariable('health', 100)
.defineBlackboardVariable('target', null)
.defineBlackboardVariable('moveSpeed', 5.0)
.selector('MainBehavior')
.sequence('AttackBranch')
.blackboardExists('target')
.blackboardCompare('health', 30, 'greater')
.log('攻击目标', 'Attack')
.end()
.log('巡逻', 'Patrol')
.end()
.build();
// 创建实体并启动行为树
const entity = scene.createEntity('Enemy');
BehaviorTreeStarter.start(entity, tree);
// 访问和修改黑板
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
runtime?.setBlackboardValue('target', someTarget);
// 游戏循环
setInterval(() => {
Core.update(0.016); // 60 FPS
}, 16);
```
## 实现自定义执行器
要扩展行为树的功能,需要创建自定义执行器(详见[自定义节点执行器](./custom-actions.md)
```typescript
import {
INodeExecutor,
NodeExecutionContext,
BindingHelper,
NodeExecutorMetadata
} from '@esengine/behavior-tree';
import { TaskStatus, NodeType } from '@esengine/behavior-tree';
@NodeExecutorMetadata({
implementationType: 'AttackAction',
nodeType: NodeType.Action,
displayName: '攻击目标',
description: '对目标造成伤害',
category: '战斗',
configSchema: {
damage: {
type: 'number',
default: 10,
supportBinding: true
}
}
})
export class AttackAction implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const damage = BindingHelper.getValue<number>(context, 'damage', 10);
const target = context.runtime.getBlackboardValue('target');
if (!target) {
return TaskStatus.Failure;
}
// 执行攻击逻辑
performAttack(context.entity, target, damage);
return TaskStatus.Success;
}
reset(context: NodeExecutionContext): void {
// 清理状态
}
}
```
## 调试技巧
### 1. 使用日志节点
在行为树中添加Log节点输出调试信息
```typescript
const tree = BehaviorTreeBuilder.create('DebugAI')
.log('开始战斗序列', 'StartCombat')
.sequence('Combat')
.blackboardCompare('health', 0, 'greater')
.log('执行攻击', 'Attack')
.end()
.build();
```
### 2. 监控黑板状态
```typescript
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
console.log('黑板变量:', runtime?.getAllBlackboardVariables());
console.log('活动节点:', Array.from(runtime?.activeNodeIds || []));
```
### 3. 在自定义执行器中调试
```typescript
export class DebugAction implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { nodeData, runtime, state } = context;
console.group(`[${nodeData.name}]`);
console.log('配置:', nodeData.config);
console.log('状态:', state);
console.log('黑板:', runtime.getAllBlackboardVariables());
console.groupEnd();
return TaskStatus.Success;
}
}
```
## 完整示例
```typescript
import { Core, Scene } from '@esengine/ecs-framework';
import {
BehaviorTreePlugin,
BehaviorTreeBuilder,
BehaviorTreeStarter,
BehaviorTreeRuntimeComponent
} from '@esengine/behavior-tree';
// 初始化
Core.create();
const plugin = new BehaviorTreePlugin();
await Core.installPlugin(plugin);
const scene = new Scene();
plugin.setupScene(scene);
Core.setScene(scene);
// 使用Builder API构建行为树
const tree = BehaviorTreeBuilder.create('EnemyAI')
.defineBlackboardVariable('health', 100)
.defineBlackboardVariable('hasTarget', false)
.selector('Root')
.sequence('Combat')
.blackboardCompare('hasTarget', true, 'equals')
.log('攻击玩家', 'Attack')
.end()
.log('空闲', 'Idle')
.end()
.build();
// 创建实体并启动
const entity = scene.createEntity('Enemy');
BehaviorTreeStarter.start(entity, tree);
// 模拟发现目标
setTimeout(() => {
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
runtime?.setBlackboardValue('hasTarget', true);
}, 2000);
// 游戏循环
setInterval(() => {
Core.update(0.016);
}, 16);
```
## 下一步
- 查看[自定义节点执行器](./custom-actions.md)学习如何创建自定义节点
- 查看[高级用法](./advanced-usage.md)了解性能优化等高级特性
- 查看[最佳实践](./best-practices.md)优化你的AI设计

View File

@@ -0,0 +1,385 @@
# 快速开始
本教程将引导你在5分钟内创建第一个行为树。
## 安装
```bash
npm install @esengine/behavior-tree
```
## 第一个行为树
让我们创建一个简单的AI行为树,实现"巡逻-发现敌人-攻击"的逻辑。
### 步骤1: 导入依赖
```typescript
import { Core, Scene, Entity } from '@esengine/ecs-framework';
import {
BehaviorTreeBuilder,
BehaviorTreeStarter,
BehaviorTreePlugin
} from '@esengine/behavior-tree';
```
### 步骤2: 初始化Core并安装插件
```typescript
Core.create();
const plugin = new BehaviorTreePlugin();
await Core.installPlugin(plugin);
```
### 步骤3: 创建场景并设置行为树系统
```typescript
const scene = new Scene();
plugin.setupScene(scene);
Core.setScene(scene);
```
### 步骤4: 构建行为树数据
```typescript
const guardAITree = BehaviorTreeBuilder.create('GuardAI')
// 定义黑板变量
.defineBlackboardVariable('health', 100)
.defineBlackboardVariable('hasEnemy', false)
.defineBlackboardVariable('patrolPoint', 0)
// 根选择器
.selector('RootSelector')
// 分支1: 如果发现敌人且生命值高,则攻击
.selector('CombatBranch')
.blackboardExists('hasEnemy', 'CheckEnemy')
.blackboardCompare('health', 30, 'greater', 'CheckHealth')
.log('守卫正在攻击敌人', 'Attack')
.end()
// 分支2: 如果生命值低,则逃跑
.selector('FleeBranch')
.blackboardCompare('health', 30, 'lessOrEqual', 'CheckLowHealth')
.log('守卫生命值过低,正在逃跑', 'Flee')
.end()
// 分支3: 默认巡逻
.selector('PatrolBranch')
.modifyBlackboardValue('patrolPoint', 'add', 1, 'IncrementPatrol')
.log('守卫正在巡逻', 'Patrol')
.wait(2.0, 'WaitAtPoint')
.end()
.end()
.build();
```
### 步骤5: 创建实体并启动行为树
```typescript
// 创建守卫实体
const guardEntity = scene.createEntity('Guard');
// 启动行为树
BehaviorTreeStarter.start(guardEntity, guardAITree);
```
### 步骤6: 运行游戏循环
```typescript
// 模拟游戏循环
setInterval(() => {
Core.update(0.1); // 传入deltaTime(秒)
}, 100); // 每100ms更新一次
```
### 步骤7: 模拟游戏事件
```typescript
// 5秒后模拟发现敌人
setTimeout(() => {
const runtime = guardEntity.getComponent(BehaviorTreeRuntimeComponent);
runtime?.setBlackboardValue('hasEnemy', true);
console.log('发现敌人!');
}, 5000);
// 10秒后模拟受伤
setTimeout(() => {
const runtime = guardEntity.getComponent(BehaviorTreeRuntimeComponent);
runtime?.setBlackboardValue('health', 20);
console.log('守卫受伤!');
}, 10000);
```
## 完整代码
```typescript
import { Core, Scene } from '@esengine/ecs-framework';
import {
BehaviorTreeBuilder,
BehaviorTreeStarter,
BehaviorTreePlugin,
BehaviorTreeRuntimeComponent
} from '@esengine/behavior-tree';
async function main() {
// 1. 创建核心并安装插件
Core.create();
const plugin = new BehaviorTreePlugin();
await Core.installPlugin(plugin);
// 2. 创建场景
const scene = new Scene();
plugin.setupScene(scene);
Core.setScene(scene);
// 3. 构建行为树数据
const guardAITree = BehaviorTreeBuilder.create('GuardAI')
.defineBlackboardVariable('health', 100)
.defineBlackboardVariable('hasEnemy', false)
.defineBlackboardVariable('patrolPoint', 0)
.selector('RootSelector')
.selector('CombatBranch')
.blackboardExists('hasEnemy')
.blackboardCompare('health', 30, 'greater')
.log('守卫正在攻击敌人')
.end()
.selector('FleeBranch')
.blackboardCompare('health', 30, 'lessOrEqual')
.log('守卫生命值过低,正在逃跑')
.end()
.selector('PatrolBranch')
.modifyBlackboardValue('patrolPoint', 'add', 1)
.log('守卫正在巡逻')
.wait(2.0)
.end()
.end()
.build();
// 4. 创建守卫实体并启动行为树
const guardEntity = scene.createEntity('Guard');
BehaviorTreeStarter.start(guardEntity, guardAITree);
// 5. 运行游戏循环
setInterval(() => {
Core.update(0.1);
}, 100);
// 6. 模拟游戏事件
setTimeout(() => {
const runtime = guardEntity.getComponent(BehaviorTreeRuntimeComponent);
runtime?.setBlackboardValue('hasEnemy', true);
console.log('发现敌人!');
}, 5000);
setTimeout(() => {
const runtime = guardEntity.getComponent(BehaviorTreeRuntimeComponent);
runtime?.setBlackboardValue('health', 20);
console.log('守卫受伤!');
}, 10000);
}
main();
```
## 运行结果
运行程序后,你会看到类似的输出:
```
守卫正在巡逻
守卫正在巡逻
守卫正在巡逻
发现敌人!
守卫正在攻击敌人
守卫正在攻击敌人
守卫受伤!
守卫生命值过低,正在逃跑
```
## 理解代码
### 黑板变量
```typescript
.defineBlackboardVariable('health', 100)
.defineBlackboardVariable('hasEnemy', false)
.defineBlackboardVariable('patrolPoint', 0)
```
黑板用于在节点之间共享数据。这里定义了三个变量:
- `health`: 守卫的生命值
- `hasEnemy`: 是否发现敌人
- `patrolPoint`: 当前巡逻点编号
### 选择器节点
```typescript
.selector('RootSelector')
// 分支1
// 分支2
// 分支3
.end()
```
选择器按顺序尝试执行子节点,直到某个子节点返回成功。类似于编程中的 `if-else if-else`
### 条件节点
```typescript
.blackboardExists('hasEnemy') // 检查变量是否存在
.blackboardCompare('health', 30, 'greater') // 比较变量值
```
条件节点用于检查黑板变量的值。
### 动作节点
```typescript
.log('守卫正在攻击敌人') // 输出日志
.wait(2.0) // 等待2秒
.modifyBlackboardValue('patrolPoint', 'add', 1) // 修改黑板值
```
动作节点执行具体的操作。
### Runtime组件
```typescript
const runtime = guardEntity.getComponent(BehaviorTreeRuntimeComponent);
runtime?.setBlackboardValue('hasEnemy', true);
runtime?.getBlackboardValue('health');
```
通过`BehaviorTreeRuntimeComponent`访问和修改黑板变量。
## 常见任务状态
行为树的每个节点返回以下状态之一:
- **Success**: 任务成功完成
- **Failure**: 任务执行失败
- **Running**: 任务正在执行,需要在后续帧继续
- **Invalid**: 无效状态(未初始化或已重置)
## 内置节点
### 复合节点
- `sequence()` - 序列节点,按顺序执行所有子节点
- `selector()` - 选择器节点,按顺序尝试子节点直到成功
- `parallel()` - 并行节点,同时执行多个子节点
- `parallelSelector()` - 并行选择器
- `randomSequence()` - 随机序列
- `randomSelector()` - 随机选择器
### 装饰器节点
- `inverter()` - 反转子节点结果
- `repeater(count)` - 重复执行子节点
- `alwaysSucceed()` - 总是返回成功
- `alwaysFail()` - 总是返回失败
- `untilSuccess()` - 重复直到成功
- `untilFail()` - 重复直到失败
- `conditional(key, value, operator)` - 条件装饰器
- `cooldown(time)` - 冷却装饰器
- `timeout(time)` - 超时装饰器
### 动作节点
- `wait(duration)` - 等待指定时间
- `log(message)` - 输出日志
- `setBlackboardValue(key, value)` - 设置黑板值
- `modifyBlackboardValue(key, operation, value)` - 修改黑板值
- `executeAction(actionName)` - 执行自定义动作
### 条件节点
- `blackboardExists(key)` - 检查变量是否存在
- `blackboardCompare(key, value, operator)` - 比较黑板值
- `randomProbability(probability)` - 随机概率
- `executeCondition(conditionName)` - 执行自定义条件
## 控制行为树
### 启动
```typescript
BehaviorTreeStarter.start(entity, treeData);
```
### 停止
```typescript
BehaviorTreeStarter.stop(entity);
```
### 暂停和恢复
```typescript
BehaviorTreeStarter.pause(entity);
// ... 一段时间后
BehaviorTreeStarter.resume(entity);
```
### 重启
```typescript
BehaviorTreeStarter.restart(entity);
```
## 下一步
现在你已经创建了第一个行为树,接下来可以:
1. 学习[核心概念](./core-concepts.md)深入理解行为树原理
2. 学习[资产管理](./asset-management.md)了解如何加载和复用行为树、使用子树
3. 查看[自定义节点执行器](./custom-actions.md)学习如何创建自定义节点
4. 根据你的场景查看集成教程:[Cocos Creator](./cocos-integration.md) 或 [Node.js](./nodejs-usage.md)
5. 查看[高级用法](./advanced-usage.md)了解更多功能
## 常见问题
### 为什么行为树不执行?
确保:
1. 已经安装了 `BehaviorTreePlugin`
2. 调用了 `plugin.setupScene(scene)`
3. 调用了 `BehaviorTreeStarter.start(entity, treeData)`
4. 在游戏循环中调用了 `Core.update(deltaTime)`
### 如何访问黑板变量?
```typescript
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
// 读取
const health = runtime?.getBlackboardValue('health');
// 写入
runtime?.setBlackboardValue('health', 50);
// 获取所有变量
const allVars = runtime?.getAllBlackboardVariables();
```
### 如何调试行为树?
使用日志节点:
```typescript
.log('到达这个节点', 'DebugLog')
```
或者在代码中监控黑板:
```typescript
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
console.log('黑板变量:', runtime?.getAllBlackboardVariables());
console.log('活动节点:', Array.from(runtime?.activeNodeIds || []));
```
### 如何使用自定义逻辑?
内置的`executeAction``executeCondition`节点只是占位符。要实现真正的自定义逻辑,你需要创建自定义执行器:
参见[自定义节点执行器](./custom-actions.md)学习如何创建。

View File

@@ -0,0 +1,197 @@
# 行为树系统
行为树(Behavior Tree)是一种用于游戏AI和自动化控制的强大工具。本框架提供了基于Runtime执行器架构的行为树系统,具有高性能、类型安全、易于扩展的特点。
## 什么是行为树?
行为树是一种层次化的任务执行结构,由多个节点组成,每个节点负责特定的任务。行为树特别适合于:
- 游戏AI(敌人、NPC行为)
- 状态机的替代方案
- 复杂的决策逻辑
- 可视化的行为设计
## 核心特性
### Runtime执行器架构
- 数据与逻辑分离
- 无状态执行器设计
- 高性能执行
- 类型安全
### 可视化编辑器
- 图形化节点编辑
- 实时预览和调试
- 拖拽式节点创建
- 属性连接和绑定
### 灵活的黑板系统
- 本地黑板(单个行为树)
- 全局黑板(所有行为树共享)
- 类型安全的变量访问
- 支持属性绑定
### 插件系统
- 自动注册机制
- 装饰器声明元数据
- 支持多语言
- 易于扩展
## 文档导航
### 入门教程
- **[快速开始](./getting-started.md)** - 5分钟上手行为树
- **[核心概念](./core-concepts.md)** - 理解行为树的基本原理
### 编辑器使用
- **[编辑器使用指南](./editor-guide.md)** - 可视化创建行为树
- **[编辑器工作流](./editor-workflow.md)** - 完整的开发流程
### 资源管理
- **[资产管理](./asset-management.md)** - 加载、管理和复用行为树资产、使用子树
### 引擎集成
- **[Cocos Creator 集成](./cocos-integration.md)** - 在 Cocos Creator 中使用行为树
- **[Laya 引擎集成](./laya-integration.md)** - 在 Laya 中使用行为树
- **[Node.js 服务端使用](./nodejs-usage.md)** - 在服务器、聊天机器人等场景中使用行为树
### 高级主题
- **[高级用法](./advanced-usage.md)** - 性能优化、调试技巧
- **[自定义节点执行器](./custom-actions.md)** - 创建自定义行为节点
- **[最佳实践](./best-practices.md)** - 行为树设计模式和技巧
## 快速示例
### 使用Builder创建
```typescript
import { Core, Scene } from '@esengine/ecs-framework';
import {
BehaviorTreeBuilder,
BehaviorTreeStarter,
BehaviorTreePlugin
} from '@esengine/behavior-tree';
// 初始化
Core.create();
const plugin = new BehaviorTreePlugin();
await Core.installPlugin(plugin);
const scene = new Scene();
plugin.setupScene(scene);
Core.setScene(scene);
// 创建行为树
const enemyAI = BehaviorTreeBuilder.create('EnemyAI')
.defineBlackboardVariable('health', 100)
.defineBlackboardVariable('target', null)
.selector('MainBehavior')
// 如果生命值高,则攻击
.sequence('AttackBranch')
.blackboardCompare('health', 50, 'greater')
.log('攻击玩家', 'Attack')
.end()
// 否则逃跑
.log('逃离战斗', 'Flee')
.end()
.build();
// 启动AI
const entity = scene.createEntity('Enemy');
BehaviorTreeStarter.start(entity, enemyAI);
```
### 使用编辑器创建
1. 打开行为树编辑器
2. 创建新的行为树资产
3. 拖拽节点到画布
4. 配置节点属性和连接
5. 保存并在代码中使用
## 架构说明
### Runtime执行器架构
本框架采用Runtime执行器架构,将节点定义和执行逻辑分离:
**核心组件:**
- `BehaviorTreeData`: 纯数据结构,描述行为树
- `BehaviorTreeRuntimeComponent`: 运行时组件,管理状态和黑板
- `BehaviorTreeExecutionSystem`: 执行系统,驱动行为树运行
- `INodeExecutor`: 节点执行器接口
- `NodeExecutionContext`: 执行上下文
**优势:**
- 数据与逻辑分离,易于序列化
- 执行器无状态,可复用
- 类型安全,编译时检查
- 高性能执行
### 自定义执行器
创建自定义节点非常简单:
```typescript
import {
INodeExecutor,
NodeExecutionContext,
BindingHelper,
NodeExecutorMetadata
} from '@esengine/behavior-tree';
import { TaskStatus, NodeType } from '@esengine/behavior-tree';
@NodeExecutorMetadata({
implementationType: 'AttackAction',
nodeType: NodeType.Action,
displayName: '攻击',
description: '攻击目标',
category: '战斗',
configSchema: {
damage: {
type: 'number',
default: 10,
supportBinding: true
}
}
})
export class AttackAction implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const damage = BindingHelper.getValue<number>(context, 'damage', 10);
const target = context.runtime.getBlackboardValue('target');
if (!target) {
return TaskStatus.Failure;
}
console.log(`造成 ${damage} 点伤害`);
return TaskStatus.Success;
}
}
```
详细说明请参见[自定义节点执行器](./custom-actions.md)。
## 下一步
建议按照以下顺序学习:
1. 阅读[快速开始](./getting-started.md)了解基础用法
2. 学习[核心概念](./core-concepts.md)理解行为树原理
3. 学习[资产管理](./asset-management.md)了解如何加载和复用行为树、使用子树
4. 根据你的场景查看集成教程:
- 客户端游戏:[Cocos Creator](./cocos-integration.md) 或 [Laya](./laya-integration.md)
- 服务端应用:[Node.js 服务端使用](./nodejs-usage.md)
5. 尝试[编辑器使用指南](./editor-guide.md)可视化创建行为树
6. 探索[高级用法](./advanced-usage.md)和[自定义节点执行器](./custom-actions.md)提升技能
## 获取帮助
- 提交 [Issue](https://github.com/esengine/ecs-framework/issues)
- 加入社区讨论
- 参考文档中的完整代码示例

View File

@@ -0,0 +1,313 @@
# Laya 引擎集成
本教程将引导你在 Laya 引擎项目中集成和使用行为树系统。
## 前置要求
- LayaAir 3.x 或更高版本
- 基本的 TypeScript 知识
- 已完成[快速开始](./getting-started.md)教程
## 安装
在你的 Laya 项目根目录下:
```bash
npm install @esengine/ecs-framework @esengine/behavior-tree
```
## 项目结构
建议的项目结构:
```
src/
├── ai/
│ ├── EnemyAI.ts
│ └── BossAI.ts
├── systems/
│ └── AISystem.ts
└── Main.ts
resources/
└── behaviors/
├── enemy.btree.json
└── boss.btree.json
```
## 初始化
### 在Main.ts中初始化
```typescript
import { Core, Scene } from '@esengine/ecs-framework';
import { BehaviorTreePlugin } from '@esengine/behavior-tree';
export class Main {
constructor() {
Laya.init(1280, 720).then(() => {
this.initECS();
this.startGame();
});
}
private async initECS() {
// 初始化 ECS
Core.create();
// 安装行为树插件
const btPlugin = new BehaviorTreePlugin();
await Core.installPlugin(btPlugin);
// 创建并设置场景
const scene = new Scene();
btPlugin.setupScene(scene);
Core.setScene(scene);
// 启动更新循环
Laya.timer.frameLoop(1, this, this.update);
}
private update() {
// Core.update会自动更新场景
Core.update(Laya.timer.delta / 1000);
}
private startGame() {
// 加载场景
}
}
new Main();
```
## 创建AI组件
```typescript
import { Core, Entity } from '@esengine/ecs-framework';
import {
BehaviorTreeBuilder,
BehaviorTreeStarter,
BehaviorTreeRuntimeComponent
} from '@esengine/behavior-tree';
export class EnemyAI extends Laya.Script {
private aiEntity: Entity;
onEnable() {
this.createBehaviorTree();
}
private createBehaviorTree() {
// 获取Core管理的场景
const scene = Core.scene;
if (!scene) {
console.error('场景未初始化');
return;
}
const sprite = this.owner as Laya.Sprite;
// 使用Builder API创建行为树
const tree = BehaviorTreeBuilder.create('EnemyAI')
.defineBlackboardVariable('layaSprite', sprite)
.defineBlackboardVariable('health', 100)
.defineBlackboardVariable('position', { x: sprite.x, y: sprite.y })
.selector('MainBehavior')
.sequence('Combat')
.blackboardCompare('health', 30, 'greater')
.log('攻击', 'Attack')
.end()
.log('巡逻', 'Patrol')
.end()
.build();
// 创建AI实体并启动
this.aiEntity = scene.createEntity(`AI_${sprite.name}`);
BehaviorTreeStarter.start(this.aiEntity, tree);
}
onDisable() {
// 停止AI
if (this.aiEntity) {
BehaviorTreeStarter.stop(this.aiEntity);
}
}
}
```
## 与Laya节点交互
要实现与Laya节点的交互需要创建自定义执行器。下面展示一个完整示例。
## 完整示例
创建一个使用自定义执行器的敌人AI系统
```typescript
import {
BehaviorTreeBuilder,
BehaviorTreeStarter,
INodeExecutor,
NodeExecutionContext,
NodeExecutorMetadata,
BehaviorTreeRuntimeComponent
} from '@esengine/behavior-tree';
import { TaskStatus, NodeType } from '@esengine/behavior-tree';
import { Core, Entity } from '@esengine/ecs-framework';
// 自定义移动执行器
@NodeExecutorMetadata({
implementationType: 'MoveToTarget',
nodeType: NodeType.Action,
displayName: '移动到目标',
category: 'Laya',
configSchema: {
speed: {
type: 'number',
default: 50,
supportBinding: true
}
}
})
export class MoveToTargetAction implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const sprite = context.runtime.getBlackboardValue('layaSprite');
const targetPos = context.runtime.getBlackboardValue('targetPosition');
const speed = context.nodeData.config.speed;
if (!sprite || !targetPos) {
return TaskStatus.Failure;
}
const dx = targetPos.x - sprite.x;
const dy = targetPos.y - sprite.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 10) {
return TaskStatus.Success;
}
sprite.x += (dx / distance) * speed * context.deltaTime;
sprite.y += (dy / distance) * speed * context.deltaTime;
return TaskStatus.Running;
}
}
export class SimpleEnemyAI extends Laya.Script {
public player: Laya.Sprite;
private aiEntity: Entity;
onEnable() {
this.buildAI();
}
private buildAI() {
const scene = Core.scene;
if (!scene) {
console.error('场景未初始化');
return;
}
const sprite = this.owner as Laya.Sprite;
const tree = BehaviorTreeBuilder.create('EnemyAI')
.defineBlackboardVariable('layaSprite', sprite)
.defineBlackboardVariable('health', 100)
.defineBlackboardVariable('player', this.player)
.defineBlackboardVariable('targetPosition', { x: 0, y: 0 })
.selector('MainBehavior')
.sequence('Attack')
.blackboardExists('player')
.log('攻击玩家', 'DoAttack')
.end()
.log('巡逻', 'Patrol')
.end()
.build();
this.aiEntity = scene.createEntity(`AI_${sprite.name}`);
BehaviorTreeStarter.start(this.aiEntity, tree);
// 可以在帧更新中修改黑板
Laya.timer.frameLoop(1, this, () => {
const runtime = this.aiEntity?.getComponent(BehaviorTreeRuntimeComponent);
if (runtime && this.player) {
runtime.setBlackboardValue('targetPosition', {
x: this.player.x,
y: this.player.y
});
}
});
}
onDisable() {
if (this.aiEntity) {
BehaviorTreeStarter.stop(this.aiEntity);
}
Laya.timer.clearAll(this);
}
}
```
## 性能优化
### 使用冷却装饰器
对于不需要每帧更新的AI使用冷却装饰器
```typescript
const tree = BehaviorTreeBuilder.create('ThrottledAI')
.cooldown(0.2, 'ThrottleRoot') // 每0.2秒执行一次
.selector('MainBehavior')
// AI逻辑...
.end()
.end()
.build();
```
### 限制同时运行的AI数量
```typescript
class AIManager {
private activeAIs: Entity[] = [];
private maxAIs: number = 20;
addAI(entity: Entity, tree: BehaviorTreeData) {
if (this.activeAIs.length >= this.maxAIs) {
const furthest = this.activeAIs.shift();
if (furthest) {
BehaviorTreeStarter.stop(furthest);
}
}
BehaviorTreeStarter.start(entity, tree);
this.activeAIs.push(entity);
}
}
```
## 常见问题
### 资源加载失败?
确保:
1. 资源路径正确
2. 资源已添加到项目中
3. 使用 `Laya.loader.load()` 加载
### AI不执行
检查:
1. `onUpdate()` 是否被调用
2. `Scene.update()` 是否执行
3. 行为树是否已启动
## 下一步
- 查看[高级用法](./advanced-usage.md)
- 学习[最佳实践](./best-practices.md)

View File

@@ -0,0 +1,580 @@
# Node.js 服务端使用
本文介绍如何在 Node.js 服务端环境(如游戏服务器、机器人、自动化工具)中使用行为树系统。
## 使用场景
行为树不仅适用于游戏客户端AI在服务端也有广泛应用
1. **游戏服务器** - NPC AI逻辑、副本关卡脚本
2. **聊天机器人** - 对话流程控制、智能回复
3. **自动化测试** - 测试用例执行流程
4. **工作流引擎** - 业务流程自动化
5. **爬虫系统** - 数据采集流程控制
## 基础设置
### 安装
```bash
npm install @esengine/ecs-framework @esengine/behavior-tree
```
### TypeScript 配置
```json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"moduleResolution": "node",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}
```
## 快速开始
### 简单的游戏服务器 NPC
```typescript
import { Core, Scene } from '@esengine/ecs-framework';
import {
BehaviorTreePlugin,
BehaviorTreeBuilder,
BehaviorTreeStarter,
BehaviorTreeRuntimeComponent
} from '@esengine/behavior-tree';
async function startServer() {
// 1. 初始化 ECS Core
Core.create();
// 2. 安装行为树插件
const plugin = new BehaviorTreePlugin();
await Core.installPlugin(plugin);
// 3. 创建场景
const scene = new Scene();
plugin.setupScene(scene);
Core.setScene(scene);
// 4. 创建 NPC 行为树
const npcAI = BehaviorTreeBuilder.create('MerchantNPC')
.defineBlackboardVariable('mood', 'friendly')
.defineBlackboardVariable('goldAmount', 1000)
.selector('NPCBehavior')
// 如果玩家触发对话
.sequence('Dialogue')
.blackboardExists('playerRequest')
.log('NPC: 欢迎光临!')
.end()
// 默认行为:闲置
.sequence('Idle')
.log('NPC: 正在整理商品...')
.wait(5.0)
.end()
.end()
.build();
// 5. 创建 NPC 实体
const npc = scene.createEntity('Merchant');
BehaviorTreeStarter.start(npc, npcAI);
// 6. 启动游戏循环20 TPS
setInterval(() => {
Core.update(0.05); // 50ms = 1/20秒
}, 50);
// 7. 模拟玩家交互
setTimeout(() => {
const runtime = npc.getComponent(BehaviorTreeRuntimeComponent);
runtime?.setBlackboardValue('playerRequest', 'buy_sword');
console.log('玩家发起交易请求');
}, 3000);
console.log('游戏服务器已启动');
}
startServer();
```
## 实战示例:聊天机器人
创建一个基于行为树的智能聊天机器人:
```typescript
import { Core, Scene, Entity } from '@esengine/ecs-framework';
import {
BehaviorTreePlugin,
BehaviorTreeBuilder,
BehaviorTreeStarter,
BehaviorTreeRuntimeComponent,
INodeExecutor,
NodeExecutionContext,
TaskStatus,
NodeType,
NodeExecutorMetadata
} from '@esengine/behavior-tree';
// 1. 创建自定义节点:回复消息
@NodeExecutorMetadata({
implementationType: 'SendMessage',
nodeType: NodeType.Action,
displayName: '发送消息',
configSchema: {
message: { type: 'string', default: '' }
}
})
class SendMessageAction implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const message = context.nodeData.config['message'] as string;
const userMessage = context.runtime.getBlackboardValue<string>('userMessage');
console.log(`[机器人回复]: ${message}`);
console.log(` 回复给: ${userMessage}`);
return TaskStatus.Success;
}
}
// 2. 创建自定义节点:匹配关键词
@NodeExecutorMetadata({
implementationType: 'MatchKeyword',
nodeType: NodeType.Condition,
displayName: '匹配关键词',
configSchema: {
keyword: { type: 'string', default: '' }
}
})
class MatchKeywordCondition implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const keyword = context.nodeData.config['keyword'] as string;
const userMessage = context.runtime.getBlackboardValue<string>('userMessage') || '';
return userMessage.includes(keyword) ? TaskStatus.Success : TaskStatus.Failure;
}
}
// 3. 创建聊天机器人类
class ChatBot {
private botEntity: Entity;
private runtime: BehaviorTreeRuntimeComponent | null = null;
constructor(scene: Scene) {
// 创建机器人行为树
const botBehavior = BehaviorTreeBuilder.create('ChatBotAI')
.defineBlackboardVariable('userMessage', '')
.defineBlackboardVariable('userName', 'Guest')
.selector('ResponseSelector')
// 问候语
.sequence('Greeting')
.executeCondition('MatchKeyword', { keyword: '你好' })
.executeAction('SendMessage', { message: '你好!我是智能助手,有什么可以帮你的吗?' })
.end()
// 帮助请求
.sequence('Help')
.executeCondition('MatchKeyword', { keyword: '帮助' })
.executeAction('SendMessage', { message: '我可以帮你回答问题、查询信息。试试问我一些问题吧!' })
.end()
// 查询天气
.sequence('Weather')
.executeCondition('MatchKeyword', { keyword: '天气' })
.executeAction('SendMessage', { message: '今天天气不错,晴天,温度适宜。' })
.end()
// 查询时间
.sequence('Time')
.executeCondition('MatchKeyword', { keyword: '时间' })
.executeAction('SendMessage', { message: `现在时间是 ${new Date().toLocaleString()}` })
.end()
// 默认回复
.executeAction('SendMessage', { message: '抱歉,我还不太理解你的意思。可以换个方式问我吗?' })
.end()
.build();
// 创建实体并启动
this.botEntity = scene.createEntity('ChatBot');
BehaviorTreeStarter.start(this.botEntity, botBehavior);
this.runtime = this.botEntity.getComponent(BehaviorTreeRuntimeComponent);
}
// 处理用户消息
async handleMessage(userName: string, message: string) {
if (this.runtime) {
this.runtime.setBlackboardValue('userName', userName);
this.runtime.setBlackboardValue('userMessage', message);
}
// 等待一帧让行为树执行
await new Promise(resolve => setTimeout(resolve, 100));
}
}
// 4. 主程序
async function main() {
// 初始化
Core.create();
const plugin = new BehaviorTreePlugin();
await Core.installPlugin(plugin);
const scene = new Scene();
plugin.setupScene(scene);
Core.setScene(scene);
// 注册自定义节点
const system = scene.getSystem(BehaviorTreeExecutionSystem);
if (system) {
const registry = system.getExecutorRegistry();
registry.register('SendMessage', new SendMessageAction());
registry.register('MatchKeyword', new MatchKeywordCondition());
}
// 创建聊天机器人
const bot = new ChatBot(scene);
// 启动更新循环
setInterval(() => {
Core.update(0.1);
}, 100);
// 模拟用户对话
console.log('\n=== 聊天机器人测试 ===\n');
await bot.handleMessage('Alice', '你好');
await new Promise(resolve => setTimeout(resolve, 200));
await bot.handleMessage('Bob', '现在几点了?');
await new Promise(resolve => setTimeout(resolve, 200));
await bot.handleMessage('Charlie', '今天天气怎么样');
await new Promise(resolve => setTimeout(resolve, 200));
await bot.handleMessage('David', '你能帮我做什么');
await new Promise(resolve => setTimeout(resolve, 200));
await bot.handleMessage('Eve', '你好吗?');
}
main();
```
## 实战示例:多人游戏服务器
### 房间管理系统
```typescript
import { Core, Scene, Entity } from '@esengine/ecs-framework';
import {
BehaviorTreePlugin,
BehaviorTreeBuilder,
BehaviorTreeStarter,
BehaviorTreeAssetManager
} from '@esengine/behavior-tree';
// 游戏房间
class GameRoom {
private scene: Scene;
private assetManager: BehaviorTreeAssetManager;
private monsters: Entity[] = [];
constructor(roomId: string) {
// 创建房间场景
this.scene = new Scene();
const plugin = new BehaviorTreePlugin();
plugin.setupScene(this.scene);
this.assetManager = Core.services.resolve(BehaviorTreeAssetManager);
// 初始化房间
this.spawnMonsters();
console.log(`房间 ${roomId} 已创建,怪物数量: ${this.monsters.length}`);
}
private spawnMonsters() {
// 从资产管理器获取怪物AI所有房间共享
const monsterAI = this.assetManager.getAsset('MonsterAI');
if (!monsterAI) return;
// 生成10个怪物
for (let i = 0; i < 10; i++) {
const monster = this.scene.createEntity(`Monster_${i}`);
BehaviorTreeStarter.start(monster, monsterAI);
this.monsters.push(monster);
}
}
update(deltaTime: number) {
this.scene.update(deltaTime);
}
destroy() {
this.monsters.forEach(m => m.destroy());
this.monsters = [];
}
}
// 房间管理器
class RoomManager {
private rooms: Map<string, GameRoom> = new Map();
createRoom(roomId: string): GameRoom {
const room = new GameRoom(roomId);
this.rooms.set(roomId, room);
return room;
}
getRoom(roomId: string): GameRoom | undefined {
return this.rooms.get(roomId);
}
destroyRoom(roomId: string) {
const room = this.rooms.get(roomId);
if (room) {
room.destroy();
this.rooms.delete(roomId);
}
}
update(deltaTime: number) {
this.rooms.forEach(room => room.update(deltaTime));
}
}
// 主程序
async function startGameServer() {
// 初始化
Core.create();
const plugin = new BehaviorTreePlugin();
await Core.installPlugin(plugin);
// 预加载怪物AI所有房间共享
const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
const monsterAI = BehaviorTreeBuilder.create('MonsterAI')
.defineBlackboardVariable('health', 100)
.selector('Behavior')
.log('攻击玩家')
.end()
.build();
assetManager.loadAsset(monsterAI);
// 创建房间管理器
const roomManager = new RoomManager();
// 模拟房间创建
roomManager.createRoom('room_1');
roomManager.createRoom('room_2');
// 服务器主循环60 TPS
setInterval(() => {
roomManager.update(1/60);
}, 1000 / 60);
console.log('游戏服务器已启动');
}
startGameServer();
```
## 性能优化
### 1. 控制更新频率
```typescript
// 不同类型的AI使用不同的更新频率
class AIManager {
private importantAIs: Entity[] = []; // Boss等重要AI60 TPS
private normalAIs: Entity[] = []; // 普通敌人20 TPS
private backgroundAIs: Entity[] = []; // 背景NPC5 TPS
update() {
// 重要AI每帧更新
this.updateAIs(this.importantAIs, 1/60);
// 普通AI每3帧更新一次
if (frameCount % 3 === 0) {
this.updateAIs(this.normalAIs, 3/60);
}
// 背景AI每12帧更新一次
if (frameCount % 12 === 0) {
this.updateAIs(this.backgroundAIs, 12/60);
}
}
}
```
### 2. 资源管理
```typescript
// 使用资产管理器避免重复创建
const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
// 预加载所有AI
const enemyAI = BehaviorTreeBuilder.create('EnemyAI').build();
const bossAI = BehaviorTreeBuilder.create('BossAI').build();
assetManager.loadAsset(enemyAI);
assetManager.loadAsset(bossAI);
// 创建1000个敌人但只使用1份BehaviorTreeData
for (let i = 0; i < 1000; i++) {
const enemy = scene.createEntity(`Enemy${i}`);
const ai = assetManager.getAsset('EnemyAI')!;
BehaviorTreeStarter.start(enemy, ai);
}
```
### 3. 使用对象池
```typescript
class EntityPool {
private pool: Entity[] = [];
private active: Entity[] = [];
spawn(scene: Scene, treeId: string): Entity {
let entity = this.pool.pop();
if (!entity) {
entity = scene.createEntity();
const tree = assetManager.getAsset(treeId)!;
BehaviorTreeStarter.start(entity, tree);
} else {
BehaviorTreeStarter.restart(entity);
}
this.active.push(entity);
return entity;
}
recycle(entity: Entity) {
BehaviorTreeStarter.pause(entity);
const index = this.active.indexOf(entity);
if (index >= 0) {
this.active.splice(index, 1);
this.pool.push(entity);
}
}
}
```
## 最佳实践
### 1. 使用环境变量控制调试
```typescript
const DEBUG = process.env.NODE_ENV === 'development';
const aiTree = BehaviorTreeBuilder.create('AI')
.selector('Main')
.when(DEBUG, builder =>
builder.log('调试信息开始AI逻辑')
)
// AI 逻辑...
.end()
.build();
```
### 2. 错误处理
```typescript
try {
const tree = BehaviorTreeBuilder.create('AI')
// ... 构建逻辑
.build();
assetManager.loadAsset(tree);
BehaviorTreeStarter.start(entity, tree);
} catch (error) {
console.error('启动AI失败:', error);
// 使用默认AI或进行降级处理
}
```
### 3. 监控和日志
```typescript
// 定期输出AI状态
setInterval(() => {
const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
const count = assetManager.getAssetCount();
const entities = scene.getEntitiesFor(Matcher.empty().all(BehaviorTreeRuntimeComponent));
console.log(`[AI监控] 行为树资产: ${count}, 活跃实体: ${entities.length}`);
}, 10000);
```
## 常见问题
### 如何与 Express/Koa 等框架集成?
```typescript
import express from 'express';
import { Core, Scene } from '@esengine/ecs-framework';
const app = express();
const scene = new Scene();
// 在单独的循环中更新ECS
setInterval(() => {
Core.update(0.016);
}, 16);
app.post('/npc/:id/interact', (req, res) => {
const npcId = req.params.id;
const npc = scene.findEntity(npcId);
if (npc) {
const runtime = npc.getComponent(BehaviorTreeRuntimeComponent);
runtime?.setBlackboardValue('playerRequest', req.body);
res.json({ success: true });
} else {
res.status(404).json({ error: 'NPC not found' });
}
});
app.listen(3000);
```
### 如何持久化行为树状态?
```typescript
// 保存状态
function saveAIState(entity: Entity) {
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
if (runtime) {
return {
treeId: runtime.treeId,
blackboard: runtime.getAllBlackboardVariables(),
activeNodes: Array.from(runtime.activeNodeIds)
};
}
}
// 恢复状态
function loadAIState(entity: Entity, savedState: any) {
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
if (runtime) {
// 恢复黑板变量
Object.entries(savedState.blackboard).forEach(([key, value]) => {
runtime.setBlackboardValue(key, value);
});
}
}
```
## 下一步
- 查看[资产管理](./asset-management.md)了解资源加载和子树
- 学习[自定义节点执行器](./custom-actions.md)创建自定义行为
- 阅读[最佳实践](./best-practices.md)优化你的服务端AI

658
docs/guide/component.md Normal file
View File

@@ -0,0 +1,658 @@
# 组件系统
在 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} 已添加实体ID: ${this.entityId}`);
this.resource = new SomeResource();
}
/**
* 组件从实体移除时调用
* 用于清理资源、断开引用等
*/
onRemovedFromEntity(): void {
console.log(`组件 ${this.constructor.name} 已移除`);
if (this.resource) {
this.resource.cleanup();
this.resource = null;
}
}
}
```
## 组件与实体的关系
组件存储了所属实体的ID (`entityId`)而不是直接引用实体对象。这是ECS数据导向设计的体现避免了循环引用。
在实际使用中,**应该在 System 中处理实体和组件的交互**,而不是在组件内部:
```typescript
@ECSComponent('Health')
class Health extends Component {
current: number;
max: number;
constructor(max: number = 100) {
super();
this.max = max;
this.current = max;
}
isDead(): boolean {
return this.current <= 0;
}
}
@ECSComponent('Damage')
class Damage extends Component {
value: number;
constructor(value: number) {
super();
this.value = value;
}
}
// 推荐:在 System 中处理逻辑
class DamageSystem extends EntitySystem {
constructor() {
super(new Matcher().all(Health, Damage));
}
process(entities: readonly Entity[]): void {
for (const entity of entities) {
const health = entity.getComponent(Health)!;
const damage = entity.getComponent(Damage)!;
health.current -= damage.value;
if (health.isDead()) {
entity.destroy();
}
// 应用伤害后移除 Damage 组件
entity.removeComponent(damage);
}
}
}
```
## 组件属性
每个组件都有一些内置属性:
```typescript
@ECSComponent('ExampleComponent')
class ExampleComponent extends Component {
someData: string = "example";
onAddedToEntity(): void {
console.log(`组件ID: ${this.id}`); // 唯一的组件ID
console.log(`所属实体ID: ${this.entityId}`); // 所属实体的ID
}
}
```
如果需要访问实体对象,应该在 System 中进行:
```typescript
class ExampleSystem extends EntitySystem {
constructor() {
super(new Matcher().all(ExampleComponent));
}
process(entities: readonly Entity[]): void {
for (const entity of entities) {
const comp = entity.getComponent(ExampleComponent)!;
console.log(`实体名称: ${entity.name}`);
console.log(`组件数据: ${comp.someData}`);
}
}
}
```
## 复杂组件示例
### 状态机组件
```typescript
enum EntityState {
Idle,
Moving,
Attacking,
Dead
}
@ECSComponent('StateMachine')
class StateMachine extends Component {
private _currentState: EntityState = EntityState.Idle;
private _previousState: EntityState = EntityState.Idle;
private _stateTimer: number = 0;
get currentState(): EntityState {
return this._currentState;
}
get previousState(): EntityState {
return this._previousState;
}
get stateTimer(): number {
return this._stateTimer;
}
changeState(newState: EntityState): void {
if (this._currentState !== newState) {
this._previousState = this._currentState;
this._currentState = newState;
this._stateTimer = 0;
}
}
updateTimer(deltaTime: number): void {
this._stateTimer += deltaTime;
}
isInState(state: EntityState): boolean {
return this._currentState === state;
}
}
```
### 配置数据组件
```typescript
interface WeaponData {
damage: number;
range: number;
fireRate: number;
ammo: number;
}
@ECSComponent('WeaponConfig')
class WeaponConfig extends Component {
data: WeaponData;
constructor(weaponData: WeaponData) {
super();
this.data = { ...weaponData }; // 深拷贝避免共享引用
}
// 提供便捷的访问方法
getDamage(): number {
return this.data.damage;
}
canFire(): boolean {
return this.data.ammo > 0;
}
consumeAmmo(): boolean {
if (this.data.ammo > 0) {
this.data.ammo--;
return true;
}
return false;
}
}
```
## 最佳实践
### 1. 保持组件简单
```typescript
// 好的组件设计 - 单一职责
@ECSComponent('Position')
class Position extends Component {
x: number = 0;
y: number = 0;
}
@ECSComponent('Velocity')
class Velocity extends Component {
dx: number = 0;
dy: number = 0;
}
// 避免的组件设计 - 职责过多
@ECSComponent('GameObject')
class GameObject extends Component {
x: number;
y: number;
dx: number;
dy: number;
health: number;
damage: number;
sprite: string;
// 太多不相关的属性
}
```
### 2. 使用构造函数初始化
```typescript
@ECSComponent('Transform')
class Transform extends Component {
x: number;
y: number;
rotation: number;
scale: number;
constructor(x = 0, y = 0, rotation = 0, scale = 1) {
super();
this.x = x;
this.y = y;
this.rotation = rotation;
this.scale = scale;
}
}
```
### 3. 明确的类型定义
```typescript
interface InventoryItem {
id: string;
name: string;
quantity: number;
type: 'weapon' | 'consumable' | 'misc';
}
@ECSComponent('Inventory')
class Inventory extends Component {
items: InventoryItem[] = [];
maxSlots: number;
constructor(maxSlots: number = 20) {
super();
this.maxSlots = maxSlots;
}
addItem(item: InventoryItem): boolean {
if (this.items.length < this.maxSlots) {
this.items.push(item);
return true;
}
return false;
}
removeItem(itemId: string): InventoryItem | null {
const index = this.items.findIndex(item => item.id === itemId);
if (index !== -1) {
return this.items.splice(index, 1)[0];
}
return null;
}
}
```
### 4. 引用其他实体
当组件需要关联其他实体时(如父子关系、跟随目标等),**推荐方式是存储实体ID**,然后在 System 中查找:
```typescript
@ECSComponent('Follower')
class Follower extends Component {
targetId: number;
followDistance: number = 50;
constructor(targetId: number) {
super();
this.targetId = targetId;
}
}
// 在 System 中查找目标实体并处理逻辑
class FollowerSystem extends EntitySystem {
constructor() {
super(new Matcher().all(Follower, Position));
}
process(entities: readonly Entity[]): void {
for (const entity of entities) {
const follower = entity.getComponent(Follower)!;
const position = entity.getComponent(Position)!;
// 通过场景查找目标实体
const target = entity.scene?.findEntityById(follower.targetId);
if (target) {
const targetPos = target.getComponent(Position);
if (targetPos) {
// 跟随逻辑
const dx = targetPos.x - position.x;
const dy = targetPos.y - position.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance > follower.followDistance) {
// 移动靠近目标
}
}
}
}
}
}
```
这种方式的优势:
- 组件保持简单,只存储基本数据类型
- 符合数据导向设计
- 在 System 中统一处理查找和逻辑
- 易于理解和维护
**避免在组件中直接存储实体引用**
```typescript
// 错误示范:直接存储实体引用
@ECSComponent('BadFollower')
class BadFollower extends Component {
target: Entity; // 实体销毁后仍持有引用,可能导致内存泄漏
}
```
## 高级特性
### EntityRef 装饰器 - 自动引用追踪
框架提供了 `@EntityRef` 装饰器用于**特殊场景**下安全地存储实体引用。这是一个高级特性,一般情况下推荐使用存储ID的方式。
#### 什么时候需要 EntityRef
在以下场景中,`@EntityRef` 可以简化代码:
1. **父子关系**: 需要在组件中直接访问父实体或子实体
2. **复杂关联**: 实体之间有多个引用关系
3. **频繁访问**: 需要在多处访问引用的实体,使用ID查找会有性能开销
#### 核心特性
`@EntityRef` 装饰器通过 **ReferenceTracker** 自动追踪引用关系:
- 当被引用的实体销毁时,所有指向它的 `@EntityRef` 属性自动设为 `null`
- 防止跨场景引用(会输出警告并拒绝设置)
- 防止引用已销毁的实体(会输出警告并设为 `null`)
- 使用 WeakRef 避免内存泄漏(自动GC支持)
- 组件移除时自动清理引用注册
#### 基本用法
```typescript
import { Component, ECSComponent, EntityRef, Entity } from '@esengine/ecs-framework';
@ECSComponent('Parent')
class ParentComponent extends Component {
@EntityRef()
parent: Entity | null = null;
}
// 使用示例
const scene = new Scene();
const parent = scene.createEntity('Parent');
const child = scene.createEntity('Child');
const comp = child.addComponent(new ParentComponent());
comp.parent = parent;
console.log(comp.parent); // Entity { name: 'Parent' }
// 当 parent 被销毁时comp.parent 自动变为 null
parent.destroy();
console.log(comp.parent); // null
```
#### 多个引用属性
一个组件可以有多个 `@EntityRef` 属性:
```typescript
@ECSComponent('Combat')
class CombatComponent extends Component {
@EntityRef()
target: Entity | null = null;
@EntityRef()
ally: Entity | null = null;
@EntityRef()
lastAttacker: Entity | null = null;
}
// 使用示例
const player = scene.createEntity('Player');
const enemy = scene.createEntity('Enemy');
const npc = scene.createEntity('NPC');
const combat = player.addComponent(new CombatComponent());
combat.target = enemy;
combat.ally = npc;
// enemy 销毁后,只有 target 变为 nullally 仍然有效
enemy.destroy();
console.log(combat.target); // null
console.log(combat.ally); // Entity { name: 'NPC' }
```
#### 安全检查
`@EntityRef` 提供了多重安全检查:
```typescript
const scene1 = new Scene();
const scene2 = new Scene();
const entity1 = scene1.createEntity('Entity1');
const entity2 = scene2.createEntity('Entity2');
const comp = entity1.addComponent(new ParentComponent());
// 跨场景引用会失败
comp.parent = entity2; // 输出错误日志comp.parent 为 null
console.log(comp.parent); // null
// 引用已销毁的实体会失败
const entity3 = scene1.createEntity('Entity3');
entity3.destroy();
comp.parent = entity3; // 输出警告日志comp.parent 为 null
console.log(comp.parent); // null
```
#### 实现原理
`@EntityRef` 使用以下机制实现自动引用追踪:
1. **ReferenceTracker**: Scene 持有一个引用追踪器,记录所有实体引用关系
2. **WeakRef**: 使用弱引用存储组件,避免循环引用导致内存泄漏
3. **属性拦截**: 通过 `Object.defineProperty` 拦截 getter/setter
4. **自动清理**: 实体销毁时,ReferenceTracker 遍历所有引用并设为 null
```typescript
// 简化的实现原理
class ReferenceTracker {
// entityId -> 引用该实体的所有组件记录
private _references: Map<number, Set<{ component: WeakRef<Component>, propertyKey: string }>>;
// 实体销毁时调用
clearReferencesTo(entityId: number): void {
const records = this._references.get(entityId);
if (records) {
for (const record of records) {
const component = record.component.deref();
if (component) {
// 将组件的引用属性设为 null
(component as any)[record.propertyKey] = null;
}
}
this._references.delete(entityId);
}
}
}
```
#### 性能考虑
`@EntityRef` 会带来一些性能开销:
- **写入开销**: 每次设置引用时需要更新 ReferenceTracker
- **内存开销**: ReferenceTracker 需要维护引用映射表
- **销毁开销**: 实体销毁时需要遍历所有引用并清理
对于大多数场景,这些开销是可以接受的。但如果有**大量实体和频繁的引用变更**,存储ID可能更高效。
#### 最佳实践
```typescript
// 推荐:适合使用 @EntityRef 的场景 - 父子关系
@ECSComponent('Transform')
class Transform extends Component {
@EntityRef()
parent: Entity | null = null;
position: { x: number, y: number } = { x: 0, y: 0 };
// 可以直接访问父实体的组件
getWorldPosition(): { x: number, y: number } {
if (!this.parent) {
return { ...this.position };
}
const parentTransform = this.parent.getComponent(Transform);
if (parentTransform) {
const parentPos = parentTransform.getWorldPosition();
return {
x: parentPos.x + this.position.x,
y: parentPos.y + this.position.y
};
}
return { ...this.position };
}
}
// 不推荐:不适合使用 @EntityRef 的场景 - 大量动态目标
@ECSComponent('AITarget')
class AITarget extends Component {
@EntityRef()
target: Entity | null = null; // 如果目标频繁变化用ID更好
updateCooldown: number = 0;
}
// 推荐这种场景用ID更好
@ECSComponent('AITarget')
class AITargetBetter extends Component {
targetId: number | null = null; // 存储ID
updateCooldown: number = 0;
}
```
#### 调试支持
ReferenceTracker 提供了调试接口:
```typescript
// 查看某个实体被哪些组件引用
const references = scene.referenceTracker.getReferencesTo(entity.id);
console.log(`实体 ${entity.name}${references.length} 个组件引用`);
// 获取完整的调试信息
const debugInfo = scene.referenceTracker.getDebugInfo();
console.log(debugInfo);
```
#### 总结
- **推荐做法**: 大部分情况使用存储ID + System查找的方式
- **EntityRef 适用场景**: 父子关系、复杂关联、组件内需要直接访问引用实体的场景
- **核心优势**: 自动清理、防止悬空引用、代码更简洁
- **注意事项**: 有性能开销,不适合大量动态引用的场景
组件是 ECS 架构的数据载体,正确设计组件能让你的游戏代码更模块化、可维护和高性能。

View File

501
docs/guide/entity-query.md Normal file
View File

@@ -0,0 +1,501 @@
# 实体查询系统
实体查询是 ECS 架构的核心功能之一。本指南将介绍如何使用 Matcher 和 QuerySystem 来查询和筛选实体。
## 核心概念
### Matcher - 查询条件描述符
Matcher 是一个链式 API,用于描述实体查询条件。它本身不执行查询,而是作为条件传递给 EntitySystem 或 QuerySystem。
### QuerySystem - 查询执行引擎
QuerySystem 负责实际执行查询,内部使用响应式查询机制自动优化性能。
## 在 EntitySystem 中使用 Matcher
这是最常见的使用方式。EntitySystem 通过 Matcher 自动筛选和处理符合条件的实体。
### 基础用法
```typescript
import { EntitySystem, Matcher, Entity, Component } from '@esengine/ecs-framework';
class PositionComponent extends Component {
public x: number = 0;
public y: number = 0;
}
class VelocityComponent extends Component {
public vx: number = 0;
public vy: number = 0;
}
class MovementSystem extends EntitySystem {
constructor() {
// 方式1: 使用 Matcher.empty().all()
super(Matcher.empty().all(PositionComponent, VelocityComponent));
// 方式2: 直接使用 Matcher.all() (等价)
// super(Matcher.all(PositionComponent, VelocityComponent));
}
protected process(entities: readonly Entity[]): void {
for (const entity of entities) {
const pos = entity.getComponent(PositionComponent)!;
const vel = entity.getComponent(VelocityComponent)!;
pos.x += vel.vx;
pos.y += vel.vy;
}
}
}
// 添加到场景
scene.addEntityProcessor(new MovementSystem());
```
### Matcher 链式 API
#### all() - 必须包含所有组件
```typescript
class HealthSystem extends EntitySystem {
constructor() {
// 实体必须同时拥有 Health 和 Position 组件
super(Matcher.empty().all(HealthComponent, PositionComponent));
}
protected process(entities: readonly Entity[]): void {
// 只处理同时拥有两个组件的实体
}
}
```
#### any() - 至少包含一个组件
```typescript
class DamageableSystem extends EntitySystem {
constructor() {
// 实体至少拥有 Health 或 Shield 其中之一
super(Matcher.any(HealthComponent, ShieldComponent));
}
protected process(entities: readonly Entity[]): void {
// 处理拥有生命值或护盾的实体
}
}
```
#### none() - 不能包含指定组件
```typescript
class AliveEntitySystem extends EntitySystem {
constructor() {
// 实体不能拥有 DeadTag 组件
super(Matcher.all(HealthComponent).none(DeadTag));
}
protected process(entities: readonly Entity[]): void {
// 只处理活着的实体
}
}
```
#### 组合条件
```typescript
class CombatSystem extends EntitySystem {
constructor() {
super(
Matcher.empty()
.all(PositionComponent, HealthComponent) // 必须有位置和生命
.any(WeaponComponent, MagicComponent) // 至少有武器或魔法
.none(DeadTag, FrozenTag) // 不能是死亡或冰冻状态
);
}
protected process(entities: readonly Entity[]): void {
// 处理可以战斗的活着的实体
}
}
```
### 按标签查询
```typescript
class PlayerSystem extends EntitySystem {
constructor() {
// 查询特定标签的实体
super(Matcher.empty().withTag(Tags.PLAYER));
}
protected process(entities: readonly Entity[]): void {
// 只处理玩家实体
}
}
```
### 按名称查询
```typescript
class BossSystem extends EntitySystem {
constructor() {
// 查询特定名称的实体
super(Matcher.empty().withName('Boss'));
}
protected process(entities: readonly Entity[]): void {
// 只处理名为 'Boss' 的实体
}
}
```
## 直接使用 QuerySystem
如果不需要创建系统,可以直接使用 Scene 的 querySystem 进行查询。
### 基础查询方法
```typescript
// 获取场景的查询系统
const querySystem = scene.querySystem;
// 查询拥有所有指定组件的实体
const result1 = querySystem.queryAll(PositionComponent, VelocityComponent);
console.log(`找到 ${result1.count} 个移动实体`);
console.log(`查询耗时: ${result1.executionTime.toFixed(2)}ms`);
// 查询拥有任意指定组件的实体
const result2 = querySystem.queryAny(WeaponComponent, MagicComponent);
console.log(`找到 ${result2.count} 个战斗单位`);
// 查询不包含指定组件的实体
const result3 = querySystem.queryNone(DeadTag);
console.log(`找到 ${result3.count} 个活着的实体`);
```
### 按标签查询
```typescript
const playerResult = querySystem.queryByTag(Tags.PLAYER);
for (const player of playerResult.entities) {
console.log('玩家:', player.name);
}
```
### 按名称查询
```typescript
const bossResult = querySystem.queryByName('Boss');
if (bossResult.count > 0) {
const boss = bossResult.entities[0];
console.log('找到Boss:', boss);
}
```
### 按单个组件查询
```typescript
const healthResult = querySystem.queryByComponent(HealthComponent);
console.log(`${healthResult.count} 个实体拥有生命值`);
```
## 性能优化
### 自动缓存
QuerySystem 内部使用响应式查询自动缓存结果,相同的查询条件会直接使用缓存:
```typescript
// 第一次查询,执行实际查询
const result1 = querySystem.queryAll(PositionComponent);
console.log('fromCache:', result1.fromCache); // false
// 第二次相同查询,使用缓存
const result2 = querySystem.queryAll(PositionComponent);
console.log('fromCache:', result2.fromCache); // true
```
### 实体变化自动更新
当实体添加/移除组件时,查询缓存会自动更新:
```typescript
// 查询拥有武器的实体
const before = querySystem.queryAll(WeaponComponent);
console.log('之前:', before.count); // 假设为 5
// 给实体添加武器
const enemy = scene.createEntity('Enemy');
enemy.addComponent(new WeaponComponent());
// 再次查询,自动包含新实体
const after = querySystem.queryAll(WeaponComponent);
console.log('之后:', after.count); // 现在是 6
```
### 查询性能统计
```typescript
const stats = querySystem.getStats();
console.log('总查询次数:', stats.queryStats.totalQueries);
console.log('缓存命中率:', stats.queryStats.cacheHitRate);
console.log('缓存大小:', stats.cacheStats.size);
```
## 实际应用场景
### 场景1: 物理系统
```typescript
class PhysicsSystem extends EntitySystem {
constructor() {
super(Matcher.empty().all(TransformComponent, RigidbodyComponent));
}
protected process(entities: readonly Entity[]): void {
for (const entity of entities) {
const transform = entity.getComponent(TransformComponent)!;
const rigidbody = entity.getComponent(RigidbodyComponent)!;
// 应用重力
rigidbody.velocity.y -= 9.8 * Time.deltaTime;
// 更新位置
transform.position.x += rigidbody.velocity.x * Time.deltaTime;
transform.position.y += rigidbody.velocity.y * Time.deltaTime;
}
}
}
```
### 场景2: 渲染系统
```typescript
class RenderSystem extends EntitySystem {
constructor() {
super(
Matcher.empty()
.all(TransformComponent, SpriteComponent)
.none(InvisibleTag) // 排除不可见实体
);
}
protected process(entities: readonly Entity[]): void {
// 按 z-order 排序
const sorted = entities.slice().sort((a, b) => {
const zA = a.getComponent(TransformComponent)!.z;
const zB = b.getComponent(TransformComponent)!.z;
return zA - zB;
});
// 渲染实体
for (const entity of sorted) {
const transform = entity.getComponent(TransformComponent)!;
const sprite = entity.getComponent(SpriteComponent)!;
renderer.drawSprite(sprite.texture, transform.position);
}
}
}
```
### 场景3: 碰撞检测
```typescript
class CollisionSystem extends EntitySystem {
constructor() {
super(Matcher.empty().all(TransformComponent, ColliderComponent));
}
protected process(entities: readonly Entity[]): void {
// 简单的 O(n²) 碰撞检测
for (let i = 0; i < entities.length; i++) {
for (let j = i + 1; j < entities.length; j++) {
this.checkCollision(entities[i], entities[j]);
}
}
}
private checkCollision(a: Entity, b: Entity): void {
const transA = a.getComponent(TransformComponent)!;
const transB = b.getComponent(TransformComponent)!;
const colliderA = a.getComponent(ColliderComponent)!;
const colliderB = b.getComponent(ColliderComponent)!;
if (this.isOverlapping(transA, colliderA, transB, colliderB)) {
// 触发碰撞事件
scene.eventSystem.emit('collision', { entityA: a, entityB: b });
}
}
private isOverlapping(...args: any[]): boolean {
// 碰撞检测逻辑
return false;
}
}
```
### 场景4: 一次性查询
```typescript
// 在系统外部执行一次性查询
class GameManager {
private scene: Scene;
public countEnemies(): number {
const result = this.scene.querySystem.queryByTag(Tags.ENEMY);
return result.count;
}
public findNearestEnemy(playerPos: Vector2): Entity | null {
const enemies = this.scene.querySystem.queryByTag(Tags.ENEMY);
let nearest: Entity | null = null;
let minDistance = Infinity;
for (const enemy of enemies.entities) {
const transform = enemy.getComponent(TransformComponent);
if (!transform) continue;
const distance = Vector2.distance(playerPos, transform.position);
if (distance < minDistance) {
minDistance = distance;
nearest = enemy;
}
}
return nearest;
}
}
```
## 最佳实践
### 1. 优先使用 EntitySystem
```typescript
// 推荐: 使用 EntitySystem
class GoodSystem extends EntitySystem {
constructor() {
super(Matcher.empty().all(HealthComponent));
}
protected process(entities: readonly Entity[]): void {
// 自动获得符合条件的实体,每帧自动更新
}
}
// 不推荐: 在 update 中手动查询
class BadSystem extends EntitySystem {
constructor() {
super(Matcher.empty());
}
protected process(entities: readonly Entity[]): void {
// 每帧手动查询,浪费性能
const result = this.scene!.querySystem.queryAll(HealthComponent);
for (const entity of result.entities) {
// ...
}
}
}
```
### 2. 合理使用 none() 排除条件
```typescript
// 排除已死亡的敌人
class EnemyAISystem extends EntitySystem {
constructor() {
super(
Matcher.empty()
.all(EnemyTag, AIComponent)
.none(DeadTag) // 不处理死亡的敌人
);
}
}
```
### 3. 使用标签优化查询
```typescript
// 不好: 查询所有实体再过滤
const allEntities = scene.querySystem.getAllEntities();
const players = allEntities.filter(e => e.hasComponent(PlayerTag));
// 好: 直接按标签查询
const players = scene.querySystem.queryByTag(Tags.PLAYER).entities;
```
### 4. 避免过于复杂的查询条件
```typescript
// 不推荐: 过于复杂
super(
Matcher.empty()
.all(A, B, C, D)
.any(E, F, G)
.none(H, I, J)
);
// 推荐: 拆分成多个简单系统
class SystemAB extends EntitySystem {
constructor() {
super(Matcher.empty().all(A, B));
}
}
class SystemCD extends EntitySystem {
constructor() {
super(Matcher.empty().all(C, D));
}
}
```
## 注意事项
### 1. 查询结果是只读的
```typescript
const result = querySystem.queryAll(PositionComponent);
// 不要修改返回的数组
result.entities.push(someEntity); // 错误!
// 如果需要修改,先复制
const mutableArray = [...result.entities];
mutableArray.push(someEntity); // 正确
```
### 2. 组件添加/移除后的查询时机
```typescript
// 创建实体并添加组件
const entity = scene.createEntity('Player');
entity.addComponent(new PositionComponent());
// 立即查询可能获取到新实体
const result = scene.querySystem.queryAll(PositionComponent);
// result.entities 包含新创建的实体
```
### 3. Matcher 是不可变的
```typescript
const matcher = Matcher.empty().all(PositionComponent);
// 链式调用返回新的 Matcher 实例
const matcher2 = matcher.any(VelocityComponent);
// matcher 本身不变
console.log(matcher === matcher2); // false
```
## 相关 API
- [Matcher](../api/classes/Matcher.md) - 查询条件描述符 API 参考
- [QuerySystem](../api/classes/QuerySystem.md) - 查询系统 API 参考
- [EntitySystem](../api/classes/EntitySystem.md) - 实体系统 API 参考
- [Entity](../api/classes/Entity.md) - 实体 API 参考

288
docs/guide/entity.md Normal file
View 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
View 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 框架中实现松耦合架构的重要工具,正确使用事件系统能让你的游戏代码更加模块化、可维护和可扩展。

View 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 });
// 从服务容器获取 WorldManagerCore 已自动创建并注册)
const worldManager = Core.services.resolve(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)
```

40
docs/guide/index.md Normal file
View File

@@ -0,0 +1,40 @@
# 指南
欢迎使用 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等环境。
## 高级特性
### [服务容器 (Service Container)](./service-container.md)
掌握依赖注入和服务管理,实现松耦合的架构设计。
### [插件系统 (Plugin System)](./plugin-system.md)
学习如何开发和使用插件,扩展框架功能,实现功能模块化。

594
docs/guide/logging.md Normal file
View File

@@ -0,0 +1,594 @@
# 日志系统
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('渲染系统启动');
}
}
```
### 集成第三方日志库
通过 `setLoggerFactory` 可以将业务代码中的日志器替换为第三方日志库(如 winston、pino、nestjs Logger 等)。
**说明**: 目前框架内部日志仍使用 ConsoleLogger自定义日志器仅影响业务代码如 EntitySystem
#### 基本用法
```typescript
import { setLoggerFactory } from '@esengine/ecs-framework';
setLoggerFactory((name?: string) => {
// 返回实现 ILogger 接口的日志器实例
return yourLogger;
});
```
#### 使用示例
```typescript
// 集成 Winston
setLoggerFactory((name?: string) => winston.createLogger({ /* ... */ }));
// 集成 Pino
setLoggerFactory((name?: string) => pino({ name }));
// 集成 NestJS Logger
setLoggerFactory((name?: string) => new Logger(name));
```
#### EntitySystem 中的使用
EntitySystem 会自动使用类名创建日志器:
```typescript
class PlayerMovementSystem extends EntitySystem {
// this.logger 自动使用 'PlayerMovementSystem' 作为名称
protected process(entities: readonly Entity[]): void {
this.logger.info(`处理 ${entities.length} 个玩家实体`);
}
}
```
### 自定义输出
```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();
```
日志系统是调试和监控应用的重要工具,正确使用日志系统能大大提高开发效率和问题排查能力。

View 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('使用降级适配器继续运行');
}
```

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

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

View 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('收到内存警告,开始清理资源');
// 清理不必要的资源
});
}
```

643
docs/guide/plugin-system.md Normal file
View File

@@ -0,0 +1,643 @@
# 插件系统
插件系统允许你以模块化的方式扩展 ECS Framework 的功能。通过插件,你可以封装特定功能(如网络同步、物理引擎、调试工具等),并在多个项目中复用。
## 概述
### 什么是插件
插件是实现了 `IPlugin` 接口的类,可以在运行时动态安装到框架中。插件可以:
- 注册自定义服务到服务容器
- 添加系统到场景
- 注册自定义组件
- 扩展框架功能
### 插件的优势
- **模块化**: 将功能封装为独立模块,提高代码可维护性
- **可复用**: 同一个插件可以在多个项目中使用
- **解耦**: 核心框架与扩展功能分离
- **热插拔**: 运行时动态安装和卸载插件
## 快速开始
### 创建第一个插件
创建一个简单的调试插件:
```typescript
import { IPlugin, Core, ServiceContainer } from '@esengine/ecs-framework';
class DebugPlugin implements IPlugin {
readonly name = 'debug-plugin';
readonly version = '1.0.0';
install(core: Core, services: ServiceContainer): void {
console.log('Debug plugin installed');
// 可以在这里注册服务、添加系统等
}
uninstall(): void {
console.log('Debug plugin uninstalled');
// 清理资源
}
}
```
### 安装插件
使用 `Core.installPlugin()` 安装插件:
```typescript
import { Core } from '@esengine/ecs-framework';
// 初始化Core
Core.create({ debug: true });
// 安装插件
await Core.installPlugin(new DebugPlugin());
// 检查插件是否已安装
if (Core.isPluginInstalled('debug-plugin')) {
console.log('Debug plugin is running');
}
```
### 卸载插件
```typescript
// 卸载插件
await Core.uninstallPlugin('debug-plugin');
```
### 获取插件实例
```typescript
// 获取已安装的插件
const plugin = Core.getPlugin('debug-plugin');
if (plugin) {
console.log(`Plugin version: ${plugin.version}`);
}
```
## 插件开发
### IPlugin 接口
所有插件必须实现 `IPlugin` 接口:
```typescript
export interface IPlugin {
// 插件唯一名称
readonly name: string;
// 插件版本建议遵循semver规范
readonly version: string;
// 依赖的其他插件(可选)
readonly dependencies?: readonly string[];
// 安装插件时调用
install(core: Core, services: ServiceContainer): void | Promise<void>;
// 卸载插件时调用
uninstall(): void | Promise<void>;
}
```
### 插件生命周期
#### install 方法
在插件安装时调用,用于初始化插件:
```typescript
class MyPlugin implements IPlugin {
readonly name = 'my-plugin';
readonly version = '1.0.0';
install(core: Core, services: ServiceContainer): void {
// 1. 注册服务
services.registerSingleton(MyService);
// 2. 访问当前场景
const scene = core.scene;
if (scene) {
// 3. 添加系统
scene.addSystem(new MySystem());
}
// 4. 其他初始化逻辑
console.log('Plugin initialized');
}
uninstall(): void {
// 清理逻辑
}
}
```
#### uninstall 方法
在插件卸载时调用,用于清理资源:
```typescript
class MyPlugin implements IPlugin {
readonly name = 'my-plugin';
readonly version = '1.0.0';
private myService?: MyService;
install(core: Core, services: ServiceContainer): void {
this.myService = new MyService();
services.registerInstance(MyService, this.myService);
}
uninstall(): void {
// 清理服务
if (this.myService) {
this.myService.dispose();
this.myService = undefined;
}
// 移除事件监听器
// 释放其他资源
}
}
```
### 异步插件
插件的 `install``uninstall` 方法都支持异步:
```typescript
class AsyncPlugin implements IPlugin {
readonly name = 'async-plugin';
readonly version = '1.0.0';
async install(core: Core, services: ServiceContainer): Promise<void> {
// 异步加载资源
const config = await fetch('/plugin-config.json').then(r => r.json());
// 使用加载的配置初始化服务
const service = new MyService(config);
services.registerInstance(MyService, service);
}
async uninstall(): Promise<void> {
// 异步清理
await this.saveState();
}
private async saveState() {
// 保存插件状态
}
}
// 使用
await Core.installPlugin(new AsyncPlugin());
```
### 注册服务
插件可以向服务容器注册自己的服务:
```typescript
import { IService } from '@esengine/ecs-framework';
class NetworkService implements IService {
connect(url: string) {
console.log(`Connecting to ${url}`);
}
dispose(): void {
console.log('Network service disposed');
}
}
class NetworkPlugin implements IPlugin {
readonly name = 'network-plugin';
readonly version = '1.0.0';
install(core: Core, services: ServiceContainer): void {
// 注册网络服务
services.registerSingleton(NetworkService);
// 解析并使用服务
const network = services.resolve(NetworkService);
network.connect('ws://localhost:8080');
}
uninstall(): void {
// 服务容器会自动调用服务的dispose方法
}
}
```
### 添加系统
插件可以向场景添加自定义系统:
```typescript
import { EntitySystem, Matcher } from '@esengine/ecs-framework';
class PhysicsSystem extends EntitySystem {
constructor() {
super(Matcher.empty().all(PhysicsBody));
}
protected process(entities: readonly Entity[]): void {
// 物理模拟逻辑
}
}
class PhysicsPlugin implements IPlugin {
readonly name = 'physics-plugin';
readonly version = '1.0.0';
private physicsSystem?: PhysicsSystem;
install(core: Core, services: ServiceContainer): void {
const scene = core.scene;
if (scene) {
this.physicsSystem = new PhysicsSystem();
scene.addSystem(this.physicsSystem);
}
}
uninstall(): void {
// 移除系统
if (this.physicsSystem) {
const scene = Core.scene;
if (scene) {
scene.removeSystem(this.physicsSystem);
}
this.physicsSystem = undefined;
}
}
}
```
## 依赖管理
### 声明依赖
插件可以声明对其他插件的依赖:
```typescript
class AdvancedPhysicsPlugin implements IPlugin {
readonly name = 'advanced-physics';
readonly version = '2.0.0';
// 声明依赖基础物理插件
readonly dependencies = ['physics-plugin'] as const;
install(core: Core, services: ServiceContainer): void {
// 可以安全地使用physics-plugin提供的服务
const physicsService = services.resolve(PhysicsService);
// ...
}
uninstall(): void {
// 清理
}
}
```
### 依赖检查
框架会自动检查依赖关系,如果依赖未满足会抛出错误:
```typescript
// 错误physics-plugin 未安装
try {
await Core.installPlugin(new AdvancedPhysicsPlugin());
} catch (error) {
console.error(error); // Plugin advanced-physics has unmet dependencies: physics-plugin
}
// 正确:先安装依赖
await Core.installPlugin(new PhysicsPlugin());
await Core.installPlugin(new AdvancedPhysicsPlugin());
```
### 卸载顺序
框架会检查依赖关系,防止卸载被其他插件依赖的插件:
```typescript
await Core.installPlugin(new PhysicsPlugin());
await Core.installPlugin(new AdvancedPhysicsPlugin());
// 错误physics-plugin 被 advanced-physics 依赖
try {
await Core.uninstallPlugin('physics-plugin');
} catch (error) {
console.error(error); // Cannot uninstall plugin physics-plugin: it is required by advanced-physics
}
// 正确:先卸载依赖它的插件
await Core.uninstallPlugin('advanced-physics');
await Core.uninstallPlugin('physics-plugin');
```
## 插件管理
### 通过 Core 管理
Core 类提供了便捷的插件管理方法:
```typescript
// 安装插件
await Core.installPlugin(myPlugin);
// 卸载插件
await Core.uninstallPlugin('plugin-name');
// 检查插件是否已安装
if (Core.isPluginInstalled('plugin-name')) {
// ...
}
// 获取插件实例
const plugin = Core.getPlugin('plugin-name');
```
### 通过 PluginManager 管理
也可以直接使用 PluginManager 服务:
```typescript
const pluginManager = Core.services.resolve(PluginManager);
// 获取所有插件
const allPlugins = pluginManager.getAllPlugins();
console.log(`Total plugins: ${allPlugins.length}`);
// 获取插件元数据
const metadata = pluginManager.getMetadata('my-plugin');
if (metadata) {
console.log(`State: ${metadata.state}`);
console.log(`Installed at: ${new Date(metadata.installedAt!)}`);
}
// 获取所有插件元数据
const allMetadata = pluginManager.getAllMetadata();
for (const meta of allMetadata) {
console.log(`${meta.name} v${meta.version} - ${meta.state}`);
}
```
## 实用插件示例
### 网络同步插件
```typescript
import { IPlugin, IService, Core, ServiceContainer } from '@esengine/ecs-framework';
class NetworkSyncService implements IService {
private ws?: WebSocket;
connect(url: string) {
this.ws = new WebSocket(url);
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
this.handleMessage(data);
};
}
private handleMessage(data: any) {
// 处理网络消息
}
dispose(): void {
if (this.ws) {
this.ws.close();
this.ws = undefined;
}
}
}
class NetworkSyncPlugin implements IPlugin {
readonly name = 'network-sync';
readonly version = '1.0.0';
install(core: Core, services: ServiceContainer): void {
// 注册网络服务
services.registerSingleton(NetworkSyncService);
// 自动连接
const network = services.resolve(NetworkSyncService);
network.connect('ws://localhost:8080');
}
uninstall(): void {
// 服务会自动dispose
}
}
```
### 性能分析插件
```typescript
class PerformanceAnalysisPlugin implements IPlugin {
readonly name = 'performance-analysis';
readonly version = '1.0.0';
private frameCount = 0;
private totalTime = 0;
install(core: Core, services: ServiceContainer): void {
const monitor = services.resolve(PerformanceMonitor);
monitor.enable();
// 定期输出性能报告
const timer = services.resolve(TimerManager);
timer.schedule(5.0, true, null, () => {
this.printReport(monitor);
});
}
uninstall(): void {
// 清理
}
private printReport(monitor: PerformanceMonitor) {
console.log('=== Performance Report ===');
console.log(`FPS: ${monitor.getFPS()}`);
console.log(`Memory: ${monitor.getMemoryUsage()} MB`);
}
}
```
## 最佳实践
### 命名规范
- 插件名称使用小写字母和连字符:`my-awesome-plugin`
- 版本号遵循语义化版本规范:`1.0.0`
```typescript
class MyPlugin implements IPlugin {
readonly name = 'my-awesome-plugin'; // 好
readonly version = '1.0.0'; // 好
}
```
### 清理资源
始终在 `uninstall` 中清理插件创建的所有资源:
```typescript
class MyPlugin implements IPlugin {
readonly name = 'my-plugin';
readonly version = '1.0.0';
private timerId?: number;
private listener?: () => void;
install(core: Core, services: ServiceContainer): void {
// 添加定时器
this.timerId = setInterval(() => {
// ...
}, 1000);
// 添加事件监听
this.listener = () => {};
window.addEventListener('resize', this.listener);
}
uninstall(): void {
// 清理定时器
if (this.timerId) {
clearInterval(this.timerId);
this.timerId = undefined;
}
// 移除事件监听
if (this.listener) {
window.removeEventListener('resize', this.listener);
this.listener = undefined;
}
}
}
```
### 错误处理
在插件中妥善处理错误,避免影响整个应用:
```typescript
class MyPlugin implements IPlugin {
readonly name = 'my-plugin';
readonly version = '1.0.0';
async install(core: Core, services: ServiceContainer): Promise<void> {
try {
// 可能失败的操作
await this.loadConfig();
} catch (error) {
console.error('Failed to load plugin config:', error);
throw error; // 重新抛出,让框架知道安装失败
}
}
async uninstall(): Promise<void> {
try {
await this.cleanup();
} catch (error) {
console.error('Failed to cleanup plugin:', error);
// 即使清理失败也不应该阻止卸载
}
}
private async loadConfig() {
// 加载配置
}
private async cleanup() {
// 清理
}
}
```
### 配置化
允许用户配置插件行为:
```typescript
interface NetworkPluginConfig {
serverUrl: string;
autoReconnect: boolean;
timeout: number;
}
class NetworkPlugin implements IPlugin {
readonly name = 'network-plugin';
readonly version = '1.0.0';
constructor(private config: NetworkPluginConfig) {}
install(core: Core, services: ServiceContainer): void {
const network = new NetworkService(this.config);
services.registerInstance(NetworkService, network);
}
uninstall(): void {
// 清理
}
}
// 使用
const plugin = new NetworkPlugin({
serverUrl: 'ws://localhost:8080',
autoReconnect: true,
timeout: 5000
});
await Core.installPlugin(plugin);
```
## 常见问题
### 插件安装失败
**问题**: 插件安装时抛出错误
**原因**:
- 依赖未满足
- install 方法中有异常
- 服务注册冲突
**解决**:
1. 检查依赖是否已安装
2. 查看错误日志
3. 确保服务名称不冲突
### 插件卸载后仍有副作用
**问题**: 卸载插件后,插件的功能仍在运行
**原因**: uninstall 方法中未正确清理资源
**解决**: 确保在 uninstall 中清理:
- 定时器
- 事件监听器
- WebSocket连接
- 系统引用
### 何时使用插件
**适合使用插件**:
- 可选功能(调试工具、性能分析)
- 第三方集成(网络库、物理引擎)
- 跨项目复用的功能模块
**不适合使用插件**:
- 核心游戏逻辑
- 简单的工具类
- 项目特定的功能
## 相关链接
- [服务容器](./service-container.md) - 在插件中使用服务容器
- [系统架构](./system.md) - 在插件中添加系统
- [快速开始](./getting-started.md) - Core 初始化和基础使用

675
docs/guide/scene-manager.md Normal file
View File

@@ -0,0 +1,675 @@
# SceneManager
SceneManager 是 ECS Framework 提供的轻量级场景管理器,适用于 95% 的游戏应用。它提供简单直观的 API支持场景切换和延迟加载。
## 适用场景
SceneManager 适合以下场景:
- 单人游戏
- 简单多人游戏
- 移动游戏
- 需要场景切换的游戏(菜单、游戏、暂停等)
- 不需要多 World 隔离的项目
## 特点
- 轻量级,零额外开销
- 简单直观的 API
- 支持延迟场景切换(避免在当前帧中途切换)
- 自动管理 ECS 流式 API
- 自动处理场景生命周期
- 集成在 Core 中,自动更新
## 基本使用
### 推荐方式:使用 Core 的静态方法
这是最简单和推荐的方式,适合大多数应用:
```typescript
import { Core, Scene } from '@esengine/ecs-framework';
// 1. 初始化 Core
Core.create({ debug: true });
// 2. 创建并设置场景
class GameScene extends Scene {
protected initialize(): void {
this.name = "GameScene";
// 添加系统
this.addSystem(new MovementSystem());
this.addSystem(new RenderSystem());
// 创建初始实体
const player = this.createEntity("Player");
player.addComponent(new Transform(400, 300));
player.addComponent(new Health(100));
}
public onStart(): void {
console.log("游戏场景已启动");
}
}
// 3. 设置场景
Core.setScene(new GameScene());
// 4. 游戏循环Core.update 会自动更新场景)
function gameLoop(deltaTime: number) {
Core.update(deltaTime); // 自动更新所有服务和场景
}
// Laya 引擎集成
Laya.timer.frameLoop(1, this, () => {
const deltaTime = Laya.timer.delta / 1000;
Core.update(deltaTime);
});
// Cocos Creator 集成
update(deltaTime: number) {
Core.update(deltaTime);
}
```
### 高级方式:直接使用 SceneManager
如果需要更多控制,可以直接使用 SceneManager
```typescript
import { Core, SceneManager, Scene } from '@esengine/ecs-framework';
// 初始化 Core
Core.create({ debug: true });
// 获取 SceneManagerCore 已自动创建并注册)
const sceneManager = Core.services.resolve(SceneManager);
// 设置场景
const gameScene = new GameScene();
sceneManager.setScene(gameScene);
// 游戏循环(仍然使用 Core.update
function gameLoop(deltaTime: number) {
Core.update(deltaTime); // Core会自动调用sceneManager.update()
}
```
**重要**:无论使用哪种方式,游戏循环中都应该只调用 `Core.update()`,它会自动更新 SceneManager 和场景。不需要手动调用 `sceneManager.update()`
## 场景切换
### 立即切换
使用 `Core.setScene()``sceneManager.setScene()` 立即切换场景:
```typescript
// 方式1使用 Core推荐
Core.setScene(new MenuScene());
// 方式2使用 SceneManager
const sceneManager = Core.services.resolve(SceneManager);
sceneManager.setScene(new MenuScene());
```
### 延迟切换
使用 `Core.loadScene()``sceneManager.loadScene()` 延迟切换场景,场景会在下一帧切换:
```typescript
// 方式1使用 Core推荐
Core.loadScene(new GameOverScene());
// 方式2使用 SceneManager
const sceneManager = Core.services.resolve(SceneManager);
sceneManager.loadScene(new GameOverScene());
```
在 System 中切换场景时,应该使用延迟切换:
```typescript
class GameOverSystem extends EntitySystem {
process(entities: readonly Entity[]): void {
const player = entities.find(e => e.name === 'Player');
const health = player?.getComponent(Health);
if (health && health.value <= 0) {
// 延迟切换到游戏结束场景(下一帧生效)
Core.loadScene(new GameOverScene());
// 当前帧继续执行,不会中断当前系统的处理
}
}
}
```
### 完整的场景切换示例
```typescript
import { Core, Scene } from '@esengine/ecs-framework';
// 初始化
Core.create({ debug: true });
// 菜单场景
class MenuScene extends Scene {
protected initialize(): void {
this.name = "MenuScene";
// 监听开始游戏事件
this.eventSystem.on('start_game', () => {
Core.loadScene(new GameScene());
});
}
public onStart(): void {
console.log("显示菜单界面");
}
public unload(): void {
console.log("菜单场景卸载");
}
}
// 游戏场景
class GameScene extends Scene {
protected initialize(): void {
this.name = "GameScene";
// 创建游戏实体
const player = this.createEntity("Player");
player.addComponent(new Transform(400, 300));
player.addComponent(new Health(100));
// 监听游戏结束事件
this.eventSystem.on('game_over', () => {
Core.loadScene(new GameOverScene());
});
}
public onStart(): void {
console.log("游戏开始");
}
public unload(): void {
console.log("游戏场景卸载");
}
}
// 游戏结束场景
class GameOverScene extends Scene {
protected initialize(): void {
this.name = "GameOverScene";
// 监听返回菜单事件
this.eventSystem.on('back_to_menu', () => {
Core.loadScene(new MenuScene());
});
}
public onStart(): void {
console.log("显示游戏结束界面");
}
}
// 开始游戏
Core.setScene(new MenuScene());
// 游戏循环
function gameLoop(deltaTime: number) {
Core.update(deltaTime); // 自动更新场景
}
```
## API 参考
### Core 静态方法(推荐)
#### Core.setScene()
立即切换场景。
```typescript
public static setScene<T extends IScene>(scene: T): T
```
**参数**
- `scene` - 要设置的场景实例
**返回**
- 返回设置的场景实例
**示例**
```typescript
const gameScene = Core.setScene(new GameScene());
console.log(gameScene.name);
```
#### Core.loadScene()
延迟加载场景(下一帧切换)。
```typescript
public static loadScene<T extends IScene>(scene: T): void
```
**参数**
- `scene` - 要加载的场景实例
**示例**
```typescript
Core.loadScene(new GameOverScene());
```
#### Core.scene
获取当前活跃的场景。
```typescript
public static get scene(): IScene | null
```
**返回**
- 当前场景实例,如果没有场景则返回 null
**示例**
```typescript
const currentScene = Core.scene;
if (currentScene) {
console.log(`当前场景: ${currentScene.name}`);
}
```
#### Core.ecsAPI
获取 ECS 流式 API。
```typescript
public static get ecsAPI(): ECSFluentAPI | null
```
**返回**
- ECS API 实例,如果当前没有场景则返回 null
**示例**
```typescript
const api = Core.ecsAPI;
if (api) {
// 查询实体
const enemies = api.find(Enemy, Transform);
// 发射事件
api.emit('game:start', { level: 1 });
}
```
### SceneManager 方法(高级)
如果需要直接使用 SceneManager可以通过服务容器获取
```typescript
const sceneManager = Core.services.resolve(SceneManager);
```
#### setScene()
立即切换场景。
```typescript
public setScene<T extends IScene>(scene: T): T
```
#### loadScene()
延迟加载场景。
```typescript
public loadScene<T extends IScene>(scene: T): void
```
#### currentScene
获取当前场景。
```typescript
public get currentScene(): IScene | null
```
#### api
获取 ECS 流式 API。
```typescript
public get api(): ECSFluentAPI | null
```
#### hasScene
检查是否有活跃场景。
```typescript
public get hasScene(): boolean
```
#### hasPendingScene
检查是否有待切换的场景。
```typescript
public get hasPendingScene(): boolean
```
## 使用 ECS 流式 API
通过 `Core.ecsAPI` 可以方便地访问场景的 ECS 功能:
```typescript
const api = Core.ecsAPI;
if (!api) {
console.error('没有活跃场景');
return;
}
// 查询实体
const players = api.find(Player, Transform);
const enemies = api.find(Enemy, Health, Transform);
// 发射事件
api.emit('player:scored', { points: 100 });
// 监听事件
api.on('enemy:died', (data) => {
console.log('敌人死亡:', data);
});
```
## 最佳实践
### 1. 使用 Core 的静态方法
```typescript
// 推荐:使用 Core 的静态方法
Core.setScene(new GameScene());
Core.loadScene(new MenuScene());
const currentScene = Core.scene;
// 不推荐:除非有特殊需求,否则不需要直接使用 SceneManager
const sceneManager = Core.services.resolve(SceneManager);
sceneManager.setScene(new GameScene());
```
### 2. 只调用 Core.update()
```typescript
// 正确:只调用 Core.update()
function gameLoop(deltaTime: number) {
Core.update(deltaTime); // 自动更新所有服务和场景
}
// 错误:不要手动调用 sceneManager.update()
function gameLoop(deltaTime: number) {
Core.update(deltaTime);
sceneManager.update(); // 重复更新,会导致问题!
}
```
### 3. 使用延迟切换避免问题
在 System 中切换场景时,应该使用 `loadScene()` 而不是 `setScene()`
```typescript
// 推荐:延迟切换
class HealthSystem extends EntitySystem {
process(entities: readonly Entity[]): void {
for (const entity of entities) {
const health = entity.getComponent(Health);
if (health.value <= 0) {
Core.loadScene(new GameOverScene());
// 当前帧继续处理其他实体
}
}
}
}
// 不推荐:立即切换可能导致问题
class HealthSystem extends EntitySystem {
process(entities: readonly Entity[]): void {
for (const entity of entities) {
const health = entity.getComponent(Health);
if (health.value <= 0) {
Core.setScene(new GameOverScene());
// 场景立即切换,当前帧的其他实体可能无法正常处理
}
}
}
}
```
### 4. 场景职责分离
每个场景应该只负责一个特定的游戏状态:
```typescript
// 好的设计 - 职责清晰
class MenuScene extends Scene {
// 只处理菜单相关逻辑
}
class GameScene extends Scene {
// 只处理游戏玩法逻辑
}
class PauseScene extends Scene {
// 只处理暂停界面逻辑
}
// 避免的设计 - 职责混乱
class MegaScene extends Scene {
// 包含菜单、游戏、暂停等所有逻辑
}
```
### 5. 资源管理
在场景的 `unload()` 方法中清理资源:
```typescript
class GameScene extends Scene {
private textures: Map<string, any> = new Map();
private sounds: Map<string, any> = new Map();
protected initialize(): void {
this.loadResources();
}
private loadResources(): void {
this.textures.set('player', loadTexture('player.png'));
this.sounds.set('bgm', loadSound('bgm.mp3'));
}
public unload(): void {
// 清理资源
this.textures.clear();
this.sounds.clear();
console.log('场景资源已清理');
}
}
```
### 6. 事件驱动的场景切换
使用事件系统来触发场景切换,保持代码解耦:
```typescript
class GameScene extends Scene {
protected initialize(): void {
// 监听场景切换事件
this.eventSystem.on('goto:menu', () => {
Core.loadScene(new MenuScene());
});
this.eventSystem.on('goto:gameover', (data) => {
Core.loadScene(new GameOverScene());
});
}
}
// 在 System 中触发事件
class GameLogicSystem extends EntitySystem {
process(entities: readonly Entity[]): void {
if (levelComplete) {
this.scene.eventSystem.emitSync('goto:gameover', {
score: 1000,
level: 5
});
}
}
}
```
## 架构层次
SceneManager 在 ECS Framework 中的位置:
```
Core (全局服务)
└── SceneManager (场景管理,自动更新)
└── Scene (当前场景)
├── EntitySystem (系统)
├── Entity (实体)
└── Component (组件)
```
## 与 WorldManager 的对比
| 特性 | SceneManager | WorldManager |
|------|--------------|--------------|
| 适用场景 | 95% 的游戏应用 | 高级多世界隔离场景 |
| 复杂度 | 简单 | 复杂 |
| 场景数量 | 单场景(可切换) | 多 World每个 World 多场景 |
| 性能开销 | 最小 | 较高 |
| 使用方式 | `Core.setScene()` | `worldManager.createWorld()` |
**何时使用 SceneManager**
- 单人游戏
- 简单的多人游戏
- 移动游戏
- 场景之间需要切换但不需要同时运行
**何时使用 WorldManager**
- MMO 游戏服务器(每个房间一个 World
- 游戏大厅系统(每个游戏房间完全隔离)
- 需要运行多个完全独立的游戏实例
## 完整示例
```typescript
import { Core, Scene, EntitySystem, Entity, Matcher } from '@esengine/ecs-framework';
// 定义组件
class Transform {
constructor(public x: number, public y: number) {}
}
class Velocity {
constructor(public vx: number, public vy: number) {}
}
class Health {
constructor(public value: number) {}
}
// 定义系统
class MovementSystem extends EntitySystem {
constructor() {
super(Matcher.all(Transform, Velocity));
}
process(entities: readonly Entity[]): void {
for (const entity of entities) {
const transform = entity.getComponent(Transform);
const velocity = entity.getComponent(Velocity);
if (transform && velocity) {
transform.x += velocity.vx;
transform.y += velocity.vy;
}
}
}
}
// 定义场景
class MenuScene extends Scene {
protected initialize(): void {
this.name = "MenuScene";
console.log("菜单场景初始化");
}
public onStart(): void {
console.log("菜单场景启动");
}
}
class GameScene extends Scene {
protected initialize(): void {
this.name = "GameScene";
// 添加系统
this.addSystem(new MovementSystem());
// 创建玩家
const player = this.createEntity("Player");
player.addComponent(new Transform(400, 300));
player.addComponent(new Velocity(0, 0));
player.addComponent(new Health(100));
// 创建敌人
for (let i = 0; i < 5; i++) {
const enemy = this.createEntity(`Enemy_${i}`);
enemy.addComponent(new Transform(
Math.random() * 800,
Math.random() * 600
));
enemy.addComponent(new Velocity(
Math.random() * 100 - 50,
Math.random() * 100 - 50
));
enemy.addComponent(new Health(50));
}
}
public onStart(): void {
console.log('游戏场景启动');
}
public unload(): void {
console.log('游戏场景卸载');
}
}
// 初始化
Core.create({ debug: true });
// 设置初始场景
Core.setScene(new MenuScene());
// 游戏循环
let lastTime = 0;
function gameLoop(currentTime: number) {
const deltaTime = (currentTime - lastTime) / 1000;
lastTime = currentTime;
// 只需要调用 Core.update它会自动更新场景
Core.update(deltaTime);
requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);
// 切换到游戏场景
setTimeout(() => {
Core.loadScene(new GameScene());
}, 3000);
```
SceneManager 为大多数游戏提供了简单而强大的场景管理能力。通过 Core 的静态方法,你可以轻松地管理场景切换。如果你需要更高级的多世界隔离功能,请参考 [WorldManager](./world-manager.md) 文档。

661
docs/guide/scene.md Normal file
View File

@@ -0,0 +1,661 @@
# 场景管理
在 ECS 架构中场景Scene是游戏世界的容器负责管理实体、系统和组件的生命周期。场景提供了完整的 ECS 运行环境。
## 基本概念
场景是 ECS 框架的核心容器,提供:
- 实体的创建、管理和销毁
- 系统的注册和执行调度
- 组件的存储和查询
- 事件系统支持
- 性能监控和调试信息
## 场景管理方式
ECS Framework 提供了两种场景管理方式:
1. **[SceneManager](./scene-manager.md)** - 适用于 95% 的游戏应用
- 单人游戏、简单多人游戏、移动游戏
- 轻量级,简单直观的 API
- 支持场景切换
2. **[WorldManager](./world-manager.md)** - 适用于高级多世界隔离场景
- MMO 游戏服务器、游戏房间系统
- 多 World 管理,每个 World 可包含多个场景
- 完全隔离的独立环境
本文档重点介绍 Scene 类本身的使用方法。关于场景管理器的详细信息,请查看对应的文档。
## 创建场景
### 继承 Scene 类
**推荐做法:继承 Scene 类来创建自定义场景**
```typescript
import { Scene, EntitySystem } from '@esengine/ecs-framework';
class GameScene extends Scene {
protected initialize(): void {
// 设置场景名称
this.name = "GameScene";
// 添加系统
this.addSystem(new MovementSystem());
this.addSystem(new RenderSystem());
this.addSystem(new PhysicsSystem());
// 创建初始实体
this.createInitialEntities();
}
private createInitialEntities(): void {
// 创建玩家
const player = this.createEntity("Player");
player.addComponent(new Position(400, 300));
player.addComponent(new Health(100));
player.addComponent(new PlayerController());
// 创建敌人
for (let i = 0; i < 5; i++) {
const enemy = this.createEntity(`Enemy_${i}`);
enemy.addComponent(new Position(Math.random() * 800, Math.random() * 600));
enemy.addComponent(new Health(50));
enemy.addComponent(new EnemyAI());
}
}
public onStart(): void {
console.log("游戏场景已启动");
// 场景启动时的逻辑
}
public unload(): void {
console.log("游戏场景已卸载");
// 场景卸载时的清理逻辑
}
}
```
### 使用场景配置
```typescript
import { ISceneConfig } from '@esengine/ecs-framework';
const config: ISceneConfig = {
name: "MainGame",
enableEntityDirectUpdate: false
};
class ConfiguredScene extends Scene {
constructor() {
super(config);
}
}
```
## 场景生命周期
场景提供了完整的生命周期管理:
```typescript
class ExampleScene extends Scene {
protected initialize(): void {
// 场景初始化:设置系统和初始实体
console.log("场景初始化");
}
public onStart(): void {
// 场景开始运行:游戏逻辑开始执行
console.log("场景开始运行");
}
public unload(): void {
// 场景卸载:清理资源
console.log("场景卸载");
}
}
// 使用场景(由框架自动管理生命周期)
const scene = new ExampleScene();
// 场景的 initialize(), begin(), update(), end() 由框架自动调用
```
**生命周期方法**
1. `initialize()` - 场景初始化,设置系统和初始实体
2. `begin()` / `onStart()` - 场景开始运行
3. `update()` - 每帧更新(由场景管理器调用)
4. `end()` / `unload()` - 场景卸载,清理资源
## 实体管理
### 创建实体
```typescript
class EntityScene extends Scene {
createGameEntities(): void {
// 创建单个实体
const player = this.createEntity("Player");
// 批量创建实体(高性能)
const bullets = this.createEntities(100, "Bullet");
// 为批量创建的实体添加组件
bullets.forEach((bullet, index) => {
bullet.addComponent(new Position(index * 10, 100));
bullet.addComponent(new Velocity(Math.random() * 200 - 100, -300));
});
}
}
```
### 查找实体
```typescript
class SearchScene extends Scene {
findEntities(): void {
// 按名称查找
const player = this.findEntity("Player");
const player2 = this.getEntityByName("Player"); // 别名方法
// 按 ID 查找
const entity = this.findEntityById(123);
// 按标签查找
const enemies = this.findEntitiesByTag(2);
const enemies2 = this.getEntitiesByTag(2); // 别名方法
if (player) {
console.log(`找到玩家: ${player.name}`);
}
console.log(`找到 ${enemies.length} 个敌人`);
}
}
```
### 销毁实体
```typescript
class DestroyScene extends Scene {
cleanupEntities(): void {
// 销毁所有实体
this.destroyAllEntities();
// 单个实体的销毁通过实体本身
const enemy = this.findEntity("Enemy_1");
if (enemy) {
enemy.destroy(); // 实体会自动从场景中移除
}
}
}
```
## 系统管理
### 添加和移除系统
```typescript
class SystemScene extends Scene {
protected initialize(): void {
// 添加系统
const movementSystem = new MovementSystem();
this.addSystem(movementSystem);
// 设置系统更新顺序
movementSystem.updateOrder = 1;
// 添加更多系统
this.addSystem(new PhysicsSystem());
this.addSystem(new RenderSystem());
}
public removeUnnecessarySystems(): void {
// 获取系统
const physicsSystem = this.getEntityProcessor(PhysicsSystem);
// 移除系统
if (physicsSystem) {
this.removeSystem(physicsSystem);
}
}
}
```
### 系统访问
```typescript
class SystemAccessScene extends Scene {
public pausePhysics(): void {
const physicsSystem = this.getEntityProcessor(PhysicsSystem);
if (physicsSystem) {
physicsSystem.enabled = false;
}
}
public getAllSystems(): EntitySystem[] {
return this.systems; // 获取所有系统
}
}
```
## 事件系统
场景内置了类型安全的事件系统:
```typescript
class EventScene extends Scene {
protected initialize(): void {
// 监听事件
this.eventSystem.on('player_died', this.onPlayerDied.bind(this));
this.eventSystem.on('enemy_spawned', this.onEnemySpawned.bind(this));
this.eventSystem.on('level_complete', this.onLevelComplete.bind(this));
}
private onPlayerDied(data: any): void {
console.log('玩家死亡事件');
// 处理玩家死亡
}
private onEnemySpawned(data: any): void {
console.log('敌人生成事件');
// 处理敌人生成
}
private onLevelComplete(data: any): void {
console.log('关卡完成事件');
// 处理关卡完成
}
public triggerGameEvent(): void {
// 发送事件(同步)
this.eventSystem.emitSync('custom_event', {
message: "这是自定义事件",
timestamp: Date.now()
});
// 发送事件(异步)
this.eventSystem.emit('async_event', {
data: "异步事件数据"
});
}
}
```
### 事件系统 API
```typescript
// 监听事件
this.eventSystem.on('event_name', callback);
// 监听一次(自动取消订阅)
this.eventSystem.once('event_name', callback);
// 取消监听
this.eventSystem.off('event_name', callback);
// 同步发送事件
this.eventSystem.emitSync('event_name', data);
// 异步发送事件
this.eventSystem.emit('event_name', data);
// 清除所有事件监听
this.eventSystem.clear();
```
## 场景统计和调试
### 获取场景统计
```typescript
class StatsScene extends Scene {
public showStats(): void {
const stats = this.getStats();
console.log(`实体数量: ${stats.entityCount}`);
console.log(`系统数量: ${stats.processorCount}`);
console.log('组件存储统计:', stats.componentStorageStats);
}
public showDebugInfo(): void {
const debugInfo = this.getDebugInfo();
console.log('场景调试信息:', debugInfo);
// 显示所有实体信息
debugInfo.entities.forEach(entity => {
console.log(`实体 ${entity.name}(${entity.id}): ${entity.componentCount} 个组件`);
console.log('组件类型:', entity.componentTypes);
});
// 显示所有系统信息
debugInfo.processors.forEach(processor => {
console.log(`系统 ${processor.name}: 处理 ${processor.entityCount} 个实体`);
});
}
}
```
## 组件查询
Scene 提供了强大的组件查询系统:
```typescript
class QueryScene extends Scene {
protected initialize(): void {
// 创建一些实体
for (let i = 0; i < 10; i++) {
const entity = this.createEntity(`Entity_${i}`);
entity.addComponent(new Transform(i * 10, 0));
entity.addComponent(new Velocity(1, 0));
if (i % 2 === 0) {
entity.addComponent(new Renderer());
}
}
}
public queryEntities(): void {
// 通过 QuerySystem 查询
const entities = this.querySystem.query([Transform, Velocity]);
console.log(`找到 ${entities.length} 个有 Transform 和 Velocity 的实体`);
// 使用 ECS 流式 API如果通过 SceneManager
// const api = sceneManager.api;
// const entities = api?.find(Transform, Velocity);
}
}
```
## 性能监控
Scene 内置了性能监控功能:
```typescript
class PerformanceScene extends Scene {
public showPerformance(): void {
// 获取性能数据
const perfData = this.performanceMonitor?.getPerformanceData();
if (perfData) {
console.log('FPS:', perfData.fps);
console.log('帧时间:', perfData.frameTime);
console.log('实体更新时间:', perfData.entityUpdateTime);
console.log('系统更新时间:', perfData.systemUpdateTime);
}
// 获取性能报告
const report = this.performanceMonitor?.generateReport();
if (report) {
console.log('性能报告:', report);
}
}
}
```
## 最佳实践
### 1. 场景职责分离
```typescript
// 好的场景设计 - 职责清晰
class MenuScene extends Scene {
// 只处理菜单相关逻辑
}
class GameScene extends Scene {
// 只处理游戏玩法逻辑
}
class InventoryScene extends Scene {
// 只处理物品栏逻辑
}
// 避免的场景设计 - 职责混乱
class MegaScene extends Scene {
// 包含菜单、游戏、物品栏等所有逻辑
}
```
### 2. 合理的系统组织
```typescript
class OrganizedScene extends Scene {
protected initialize(): void {
// 按功能和依赖关系添加系统
this.addInputSystems();
this.addLogicSystems();
this.addRenderSystems();
}
private addInputSystems(): void {
this.addSystem(new InputSystem());
}
private addLogicSystems(): void {
this.addSystem(new MovementSystem());
this.addSystem(new PhysicsSystem());
this.addSystem(new CollisionSystem());
}
private addRenderSystems(): void {
this.addSystem(new RenderSystem());
this.addSystem(new UISystem());
}
}
```
### 3. 资源管理
```typescript
class ResourceScene extends Scene {
private textures: Map<string, any> = new Map();
private sounds: Map<string, any> = new Map();
protected initialize(): void {
this.loadResources();
}
private loadResources(): void {
// 加载场景所需资源
this.textures.set('player', this.loadTexture('player.png'));
this.sounds.set('bgm', this.loadSound('bgm.mp3'));
}
public unload(): void {
// 清理资源
this.textures.clear();
this.sounds.clear();
console.log('场景资源已清理');
}
private loadTexture(path: string): any {
// 加载纹理
return null;
}
private loadSound(path: string): any {
// 加载音效
return null;
}
}
```
### 4. 事件处理规范
```typescript
class EventHandlingScene extends Scene {
protected initialize(): void {
// 集中管理事件监听
this.setupEventListeners();
}
private setupEventListeners(): void {
this.eventSystem.on('game_pause', this.onGamePause.bind(this));
this.eventSystem.on('game_resume', this.onGameResume.bind(this));
this.eventSystem.on('player_input', this.onPlayerInput.bind(this));
}
private onGamePause(): void {
// 暂停游戏逻辑
this.systems.forEach(system => {
if (system instanceof GameLogicSystem) {
system.enabled = false;
}
});
}
private onGameResume(): void {
// 恢复游戏逻辑
this.systems.forEach(system => {
if (system instanceof GameLogicSystem) {
system.enabled = true;
}
});
}
private onPlayerInput(data: any): void {
// 处理玩家输入
}
public unload(): void {
// 清理事件监听
this.eventSystem.clear();
}
}
```
### 5. 初始化顺序
```typescript
class ProperInitScene extends Scene {
protected initialize(): void {
// 1. 首先设置场景配置
this.name = "GameScene";
// 2. 然后添加系统(按依赖顺序)
this.addSystem(new InputSystem());
this.addSystem(new MovementSystem());
this.addSystem(new PhysicsSystem());
this.addSystem(new RenderSystem());
// 3. 最后创建实体
this.createEntities();
// 4. 设置事件监听
this.setupEvents();
}
private createEntities(): void {
// 创建实体
}
private setupEvents(): void {
// 设置事件监听
}
}
```
## 完整示例
```typescript
import { Scene, EntitySystem, Entity, Matcher } from '@esengine/ecs-framework';
// 定义组件
class Transform {
constructor(public x: number, public y: number) {}
}
class Velocity {
constructor(public vx: number, public vy: number) {}
}
class Health {
constructor(public value: number) {}
}
// 定义系统
class MovementSystem extends EntitySystem {
constructor() {
super(Matcher.all(Transform, Velocity));
}
process(entities: readonly Entity[]): void {
for (const entity of entities) {
const transform = entity.getComponent(Transform);
const velocity = entity.getComponent(Velocity);
if (transform && velocity) {
transform.x += velocity.vx;
transform.y += velocity.vy;
}
}
}
}
// 定义场景
class GameScene extends Scene {
protected initialize(): void {
this.name = "GameScene";
// 添加系统
this.addSystem(new MovementSystem());
// 创建玩家
const player = this.createEntity("Player");
player.addComponent(new Transform(400, 300));
player.addComponent(new Velocity(0, 0));
player.addComponent(new Health(100));
// 创建敌人
for (let i = 0; i < 5; i++) {
const enemy = this.createEntity(`Enemy_${i}`);
enemy.addComponent(new Transform(
Math.random() * 800,
Math.random() * 600
));
enemy.addComponent(new Velocity(
Math.random() * 100 - 50,
Math.random() * 100 - 50
));
enemy.addComponent(new Health(50));
}
// 设置事件监听
this.eventSystem.on('player_died', () => {
console.log('玩家死亡!');
});
}
public onStart(): void {
console.log('游戏场景启动');
}
public unload(): void {
console.log('游戏场景卸载');
this.eventSystem.clear();
}
}
// 使用场景
// 方式1通过 SceneManager推荐
import { Core, SceneManager } from '@esengine/ecs-framework';
Core.create({ debug: true });
const sceneManager = Core.services.resolve(SceneManager);
sceneManager.setScene(new GameScene());
// 方式2通过 WorldManager高级用例
import { WorldManager } from '@esengine/ecs-framework';
const worldManager = Core.services.resolve(WorldManager);
const world = worldManager.createWorld('game');
world.createScene('main', new GameScene());
world.setSceneActive('main', true);
```
## 下一步
- 了解 [SceneManager](./scene-manager.md) - 适用于大多数游戏的简单场景管理
- 了解 [WorldManager](./world-manager.md) - 适用于需要多世界隔离的高级场景
场景是 ECS 框架的核心容器,正确使用场景管理能让你的游戏架构更加清晰、模块化和易于维护。

823
docs/guide/serialization.md Normal file
View File

@@ -0,0 +1,823 @@
# 序列化系统
序列化系统提供了完整的场景、实体和组件数据持久化方案,支持全量序列化和增量序列化两种模式,适用于游戏存档、网络同步、场景编辑器、时间回溯等场景。
## 基本概念
序列化系统分为两个层次:
- **全量序列化**:序列化完整的场景状态,包括所有实体、组件和场景数据
- **增量序列化**:只序列化相对于基础快照的变更部分,大幅减少数据量
### 支持的数据格式
- **JSON格式**:人类可读,便于调试和编辑
- **Binary格式**使用MessagePack体积更小性能更高
> **📢 v2.2.2 重要变更**
>
> 从 v2.2.2 开始,二进制序列化格式返回 `Uint8Array` 而非 Node.js 的 `Buffer`,以确保浏览器兼容性:
> - `serialize({ format: 'binary' })` 返回 `string | Uint8Array`(原为 `string | Buffer`
> - `deserialize(data)` 接收 `string | Uint8Array`(原为 `string | Buffer`
> - `applyIncremental(data)` 接收 `IncrementalSnapshot | string | Uint8Array`(原为包含 `Buffer`
>
> **迁移影响**
> - ✅ **运行时兼容**Node.js 的 `Buffer` 继承自 `Uint8Array`,现有代码可直接运行
> - ⚠️ **类型检查**:如果你的 TypeScript 代码中显式使用了 `Buffer` 类型,需要改为 `Uint8Array`
> - ✅ **浏览器支持**`Uint8Array` 是标准 JavaScript 类型,所有现代浏览器都支持
## 全量序列化
### 基础用法
#### 1. 标记可序列化组件
使用 `@Serializable``@Serialize` 装饰器标记需要序列化的组件和字段:
```typescript
import { Component, ECSComponent, Serializable, Serialize } from '@esengine/ecs-framework';
@ECSComponent('Player')
@Serializable({ version: 1 })
class PlayerComponent extends Component {
@Serialize()
public name: string = '';
@Serialize()
public level: number = 1;
@Serialize()
public experience: number = 0;
@Serialize()
public position: { x: number; y: number } = { x: 0, y: 0 };
// 不使用 @Serialize() 的字段不会被序列化
private tempData: any = null;
}
```
#### 2. 序列化场景
```typescript
// JSON格式序列化
const jsonData = scene.serialize({
format: 'json',
pretty: true // 美化输出
});
// 保存到本地存储
localStorage.setItem('gameSave', jsonData);
// Binary格式序列化更小的体积
const binaryData = scene.serialize({
format: 'binary'
});
// 保存为文件Node.js环境
// 注意binaryData 是 Uint8Array 类型Node.js 的 fs 可以直接写入
fs.writeFileSync('save.bin', binaryData);
```
#### 3. 反序列化场景
```typescript
// 从JSON恢复
const saveData = localStorage.getItem('gameSave');
if (saveData) {
scene.deserialize(saveData, {
strategy: 'replace' // 替换当前场景内容
});
}
// 从Binary恢复
const binaryData = fs.readFileSync('save.bin');
scene.deserialize(binaryData, {
strategy: 'merge' // 合并到现有场景
});
```
### 序列化选项
#### SerializationOptions
```typescript
interface SceneSerializationOptions {
// 指定要序列化的组件类型(可选)
components?: ComponentType[];
// 序列化格式:'json' 或 'binary'
format?: 'json' | 'binary';
// JSON美化输出
pretty?: boolean;
// 包含元数据
includeMetadata?: boolean;
}
```
示例:
```typescript
// 只序列化特定组件类型
const saveData = scene.serialize({
format: 'json',
components: [PlayerComponent, InventoryComponent],
pretty: true,
includeMetadata: true
});
```
#### DeserializationOptions
```typescript
interface SceneDeserializationOptions {
// 反序列化策略
strategy?: 'merge' | 'replace';
// 组件类型注册表(可选,默认使用全局注册表)
componentRegistry?: Map<string, ComponentType>;
}
```
### 高级装饰器
#### 字段序列化选项
```typescript
@ECSComponent('Advanced')
@Serializable({ version: 1 })
class AdvancedComponent extends Component {
// 使用别名
@Serialize({ alias: 'playerName' })
public name: string = '';
// 自定义序列化器
@Serialize({
serializer: (value: Date) => value.toISOString(),
deserializer: (value: string) => new Date(value)
})
public createdAt: Date = new Date();
// 忽略序列化
@IgnoreSerialization()
public cachedData: any = null;
}
```
#### 集合类型序列化
```typescript
@ECSComponent('Collections')
@Serializable({ version: 1 })
class CollectionsComponent extends Component {
// Map序列化
@SerializeAsMap()
public inventory: Map<string, number> = new Map();
// Set序列化
@SerializeAsSet()
public acquiredSkills: Set<string> = new Set();
constructor() {
super();
this.inventory.set('gold', 100);
this.inventory.set('silver', 50);
this.acquiredSkills.add('attack');
this.acquiredSkills.add('defense');
}
}
```
### 场景自定义数据
除了实体和组件,还可以序列化场景级别的配置数据:
```typescript
// 设置场景数据
scene.sceneData.set('weather', 'rainy');
scene.sceneData.set('difficulty', 'hard');
scene.sceneData.set('checkpoint', { x: 100, y: 200 });
// 序列化时会自动包含场景数据
const saveData = scene.serialize({ format: 'json' });
// 反序列化后场景数据会恢复
scene.deserialize(saveData);
console.log(scene.sceneData.get('weather')); // 'rainy'
```
## 增量序列化
增量序列化只保存场景的变更部分,适用于网络同步、撤销/重做、时间回溯等需要频繁保存状态的场景。
### 基础用法
#### 1. 创建基础快照
```typescript
// 在需要开始记录变更前创建基础快照
scene.createIncrementalSnapshot();
```
#### 2. 修改场景
```typescript
// 添加实体
const enemy = scene.createEntity('Enemy');
enemy.addComponent(new PositionComponent(100, 200));
enemy.addComponent(new HealthComponent(50));
// 修改组件
const player = scene.findEntity('Player');
const pos = player.getComponent(PositionComponent);
pos.x = 300;
pos.y = 400;
// 删除组件
player.removeComponentByType(BuffComponent);
// 删除实体
const oldEntity = scene.findEntity('ToDelete');
oldEntity.destroy();
// 修改场景数据
scene.sceneData.set('score', 1000);
```
#### 3. 获取增量变更
```typescript
// 获取相对于基础快照的所有变更
const incremental = scene.serializeIncremental();
// 查看变更统计
const stats = IncrementalSerializer.getIncrementalStats(incremental);
console.log('总变更数:', stats.totalChanges);
console.log('新增实体:', stats.addedEntities);
console.log('删除实体:', stats.removedEntities);
console.log('新增组件:', stats.addedComponents);
console.log('更新组件:', stats.updatedComponents);
```
#### 4. 序列化增量数据
```typescript
// JSON格式默认
const jsonData = IncrementalSerializer.serializeIncremental(incremental, {
format: 'json'
});
// 二进制格式(更小的体积,更高性能)
const binaryData = IncrementalSerializer.serializeIncremental(incremental, {
format: 'binary'
});
// 美化JSON输出便于调试
const prettyJson = IncrementalSerializer.serializeIncremental(incremental, {
format: 'json',
pretty: true
});
// 发送或保存
socket.send(binaryData); // 网络传输使用二进制
localStorage.setItem('changes', jsonData); // 本地存储可用JSON
```
#### 5. 应用增量变更
```typescript
// 在另一个场景应用变更
const otherScene = new Scene();
// 直接应用增量对象
otherScene.applyIncremental(incremental);
// 从JSON字符串应用
const jsonData = IncrementalSerializer.serializeIncremental(incremental, { format: 'json' });
otherScene.applyIncremental(jsonData);
// 从二进制Uint8Array应用
const binaryData = IncrementalSerializer.serializeIncremental(incremental, { format: 'binary' });
otherScene.applyIncremental(binaryData);
```
### 增量快照管理
#### 更新快照基准
在应用增量变更后,可以更新快照基准:
```typescript
// 创建初始快照
scene.createIncrementalSnapshot();
// 第一次修改
entity.addComponent(new VelocityComponent(5, 0));
const incremental1 = scene.serializeIncremental();
// 更新基准(将当前状态设为新的基准)
scene.updateIncrementalSnapshot();
// 第二次修改(增量将基于更新后的基准)
entity.getComponent(VelocityComponent).dx = 10;
const incremental2 = scene.serializeIncremental();
```
#### 清除快照
```typescript
// 释放快照占用的内存
scene.clearIncrementalSnapshot();
// 检查是否有快照
if (scene.hasIncrementalSnapshot()) {
console.log('存在增量快照');
}
```
### 增量序列化选项
```typescript
interface IncrementalSerializationOptions {
// 是否进行组件数据的深度对比
// 默认true设为false可提升性能但可能漏掉组件内部字段变更
deepComponentComparison?: boolean;
// 是否跟踪场景数据变更
// 默认true
trackSceneData?: boolean;
// 是否压缩快照使用JSON序列化
// 默认false
compressSnapshot?: boolean;
// 序列化格式
// 'json': JSON格式可读性好方便调试
// 'binary': MessagePack二进制格式体积小性能高
// 默认 'json'
format?: 'json' | 'binary';
// 是否美化JSON输出仅在format='json'时有效)
// 默认false
pretty?: boolean;
}
// 使用选项
scene.createIncrementalSnapshot({
deepComponentComparison: true,
trackSceneData: true
});
```
### 增量数据结构
增量快照包含以下变更类型:
```typescript
interface IncrementalSnapshot {
version: number; // 快照版本号
timestamp: number; // 时间戳
sceneName: string; // 场景名称
baseVersion: number; // 基础版本号
entityChanges: EntityChange[]; // 实体变更
componentChanges: ComponentChange[]; // 组件变更
sceneDataChanges: SceneDataChange[]; // 场景数据变更
}
// 变更操作类型
enum ChangeOperation {
EntityAdded = 'entity_added',
EntityRemoved = 'entity_removed',
EntityUpdated = 'entity_updated',
ComponentAdded = 'component_added',
ComponentRemoved = 'component_removed',
ComponentUpdated = 'component_updated',
SceneDataUpdated = 'scene_data_updated'
}
```
## 版本迁移
当组件结构发生变化时,版本迁移系统可以自动升级旧版本的存档数据。
### 注册迁移函数
```typescript
import { VersionMigrationManager } from '@esengine/ecs-framework';
// 假设 PlayerComponent v1 有 hp 字段
// v2 改为 health 和 maxHealth 字段
// 注册从版本1到版本2的迁移
VersionMigrationManager.registerComponentMigration(
'Player',
1, // 从版本
2, // 到版本
(data) => {
// 迁移逻辑
const newData = {
...data,
health: data.hp,
maxHealth: data.hp,
};
delete newData.hp;
return newData;
}
);
```
### 使用迁移构建器
```typescript
import { MigrationBuilder } from '@esengine/ecs-framework';
new MigrationBuilder()
.forComponent('Player')
.fromVersionToVersion(2, 3)
.migrate((data) => {
// 从版本2迁移到版本3
data.experience = data.exp || 0;
delete data.exp;
return data;
});
```
### 场景级迁移
```typescript
// 注册场景级迁移
VersionMigrationManager.registerSceneMigration(
1, // 从版本
2, // 到版本
(scene) => {
// 迁移场景结构
scene.metadata = {
...scene.metadata,
migratedFrom: 1
};
return scene;
}
);
```
### 检查迁移路径
```typescript
// 检查是否可以迁移
const canMigrate = VersionMigrationManager.canMigrateComponent(
'Player',
1, // 从版本
3 // 到版本
);
if (canMigrate) {
// 可以安全迁移
scene.deserialize(oldSaveData);
}
// 获取迁移路径
const path = VersionMigrationManager.getComponentMigrationPath('Player');
console.log('可用迁移版本:', path); // [1, 2, 3]
```
## 使用场景
### 游戏存档系统
```typescript
class SaveSystem {
private static SAVE_KEY = 'game_save';
// 保存游戏
public static saveGame(scene: Scene): void {
const saveData = scene.serialize({
format: 'json',
pretty: false
});
localStorage.setItem(this.SAVE_KEY, saveData);
console.log('游戏已保存');
}
// 加载游戏
public static loadGame(scene: Scene): boolean {
const saveData = localStorage.getItem(this.SAVE_KEY);
if (saveData) {
scene.deserialize(saveData, {
strategy: 'replace'
});
console.log('游戏已加载');
return true;
}
return false;
}
// 检查是否有存档
public static hasSave(): boolean {
return localStorage.getItem(this.SAVE_KEY) !== null;
}
}
```
### 网络同步
```typescript
class NetworkSync {
private baseSnapshot?: any;
private syncInterval: number = 100; // 100ms同步一次
constructor(private scene: Scene, private socket: WebSocket) {
this.setupSync();
}
private setupSync(): void {
// 创建基础快照
this.scene.createIncrementalSnapshot();
// 定期发送增量
setInterval(() => {
this.sendIncremental();
}, this.syncInterval);
// 接收远程增量
this.socket.onmessage = (event) => {
this.receiveIncremental(event.data);
};
}
private sendIncremental(): void {
const incremental = this.scene.serializeIncremental();
const stats = IncrementalSerializer.getIncrementalStats(incremental);
// 只在有变更时发送
if (stats.totalChanges > 0) {
// 使用二进制格式减少网络传输量
const binaryData = IncrementalSerializer.serializeIncremental(incremental, {
format: 'binary'
});
this.socket.send(binaryData);
// 更新基准
this.scene.updateIncrementalSnapshot();
}
}
private receiveIncremental(data: ArrayBuffer): void {
// 直接应用二进制数据ArrayBuffer 转 Uint8Array
const uint8Array = new Uint8Array(data);
this.scene.applyIncremental(uint8Array);
}
}
```
### 撤销/重做系统
```typescript
class UndoRedoSystem {
private history: IncrementalSnapshot[] = [];
private currentIndex: number = -1;
private maxHistory: number = 50;
constructor(private scene: Scene) {
// 创建初始快照
this.scene.createIncrementalSnapshot();
this.saveState('Initial');
}
// 保存当前状态
public saveState(label: string): void {
const incremental = this.scene.serializeIncremental();
// 删除当前位置之后的历史
this.history = this.history.slice(0, this.currentIndex + 1);
// 添加新状态
this.history.push(incremental);
this.currentIndex++;
// 限制历史记录数量
if (this.history.length > this.maxHistory) {
this.history.shift();
this.currentIndex--;
}
// 更新快照基准
this.scene.updateIncrementalSnapshot();
}
// 撤销
public undo(): boolean {
if (this.currentIndex > 0) {
this.currentIndex--;
const incremental = this.history[this.currentIndex];
this.scene.applyIncremental(incremental);
return true;
}
return false;
}
// 重做
public redo(): boolean {
if (this.currentIndex < this.history.length - 1) {
this.currentIndex++;
const incremental = this.history[this.currentIndex];
this.scene.applyIncremental(incremental);
return true;
}
return false;
}
public canUndo(): boolean {
return this.currentIndex > 0;
}
public canRedo(): boolean {
return this.currentIndex < this.history.length - 1;
}
}
```
### 关卡编辑器
```typescript
class LevelEditor {
// 导出关卡
public exportLevel(scene: Scene, filename: string): void {
const levelData = scene.serialize({
format: 'json',
pretty: true,
includeMetadata: true
});
// 浏览器环境
const blob = new Blob([levelData], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
// 导入关卡
public importLevel(scene: Scene, fileContent: string): void {
scene.deserialize(fileContent, {
strategy: 'replace'
});
}
// 验证关卡数据
public validateLevel(saveData: string): boolean {
const validation = SceneSerializer.validate(saveData);
if (!validation.valid) {
console.error('关卡数据无效:', validation.errors);
return false;
}
return true;
}
// 获取关卡信息(不完全反序列化)
public getLevelInfo(saveData: string): any {
const info = SceneSerializer.getInfo(saveData);
return info;
}
}
```
## 性能优化建议
### 1. 选择合适的格式
- **开发阶段**使用JSON格式便于调试和查看
- **生产环境**使用Binary格式减少30-50%的数据大小
### 2. 按需序列化
```typescript
// 只序列化需要持久化的组件
const saveData = scene.serialize({
format: 'binary',
components: [PlayerComponent, InventoryComponent, QuestComponent]
});
```
### 3. 增量序列化优化
```typescript
// 对于高频同步,关闭深度对比以提升性能
scene.createIncrementalSnapshot({
deepComponentComparison: false // 只检测组件的添加/删除
});
```
### 4. 批量操作
```typescript
// 批量修改后再序列化
scene.entities.buffer.forEach(entity => {
// 批量修改
});
// 一次性序列化所有变更
const incremental = scene.serializeIncremental();
```
## 最佳实践
### 1. 明确序列化字段
```typescript
// 明确标记需要序列化的字段
@ECSComponent('Player')
@Serializable({ version: 1 })
class PlayerComponent extends Component {
@Serialize()
public name: string = '';
@Serialize()
public level: number = 1;
// 运行时数据不序列化
private _cachedSprite: any = null;
}
```
### 2. 使用版本控制
```typescript
// 为组件指定版本
@Serializable({ version: 2 })
class PlayerComponent extends Component {
// 版本2的字段
}
// 注册迁移函数确保兼容性
VersionMigrationManager.registerComponentMigration('Player', 1, 2, migrateV1ToV2);
```
### 3. 避免循环引用
```typescript
// 不要在组件中直接引用其他实体
@ECSComponent('Follower')
@Serializable({ version: 1 })
class FollowerComponent extends Component {
// 存储实体ID而不是实体引用
@Serialize()
public targetId: number = 0;
// 通过场景查找目标实体
public getTarget(scene: Scene): Entity | null {
return scene.entities.findEntityById(this.targetId);
}
}
```
### 4. 压缩大数据
```typescript
// 对于大型数据结构,使用自定义序列化
@ECSComponent('LargeData')
@Serializable({ version: 1 })
class LargeDataComponent extends Component {
@Serialize({
serializer: (data: LargeObject) => compressData(data),
deserializer: (data: CompressedData) => decompressData(data)
})
public data: LargeObject;
}
```
## API参考
### 全量序列化API
- [`Scene.serialize()`](/api/classes/Scene#serialize) - 序列化场景
- [`Scene.deserialize()`](/api/classes/Scene#deserialize) - 反序列化场景
- [`SceneSerializer`](/api/classes/SceneSerializer) - 场景序列化器
- [`ComponentSerializer`](/api/classes/ComponentSerializer) - 组件序列化器
### 增量序列化API
- [`Scene.createIncrementalSnapshot()`](/api/classes/Scene#createincrementalsnapshot) - 创建基础快照
- [`Scene.serializeIncremental()`](/api/classes/Scene#serializeincremental) - 获取增量变更
- [`Scene.applyIncremental()`](/api/classes/Scene#applyincremental) - 应用增量变更支持IncrementalSnapshot对象、JSON字符串或二进制Uint8Array
- [`Scene.updateIncrementalSnapshot()`](/api/classes/Scene#updateincrementalsnapshot) - 更新快照基准
- [`Scene.clearIncrementalSnapshot()`](/api/classes/Scene#clearincrementalsnapshot) - 清除快照
- [`Scene.hasIncrementalSnapshot()`](/api/classes/Scene#hasincrementalsnapshot) - 检查是否有快照
- [`IncrementalSerializer`](/api/classes/IncrementalSerializer) - 增量序列化器
- [`IncrementalSnapshot`](/api/interfaces/IncrementalSnapshot) - 增量快照接口
- [`IncrementalSerializationOptions`](/api/interfaces/IncrementalSerializationOptions) - 增量序列化选项
- [`IncrementalSerializationFormat`](/api/type-aliases/IncrementalSerializationFormat) - 序列化格式类型
### 版本迁移API
- [`VersionMigrationManager`](/api/classes/VersionMigrationManager) - 版本迁移管理器
- `VersionMigrationManager.registerComponentMigration()` - 注册组件迁移
- `VersionMigrationManager.registerSceneMigration()` - 注册场景迁移
- `VersionMigrationManager.canMigrateComponent()` - 检查是否可以迁移
- `VersionMigrationManager.getComponentMigrationPath()` - 获取迁移路径
序列化系统是构建完整游戏的重要基础设施,合理使用可以实现强大的功能,如存档系统、网络同步、关卡编辑器等。

View File

@@ -0,0 +1,589 @@
# 服务容器
服务容器ServiceContainer是 ECS Framework 的依赖注入容器,负责管理框架中所有服务的注册、解析和生命周期。通过服务容器,你可以实现松耦合的架构设计,提高代码的可测试性和可维护性。
## 概述
### 什么是服务容器
服务容器是一个轻量级的依赖注入DI容器它提供了
- **服务注册**: 将服务类型注册到容器中
- **服务解析**: 从容器中获取服务实例
- **生命周期管理**: 自动管理服务实例的创建和销毁
- **依赖注入**: 自动解析服务之间的依赖关系
### 核心概念
#### 服务Service
服务是实现了 `IService` 接口的类,必须提供 `dispose()` 方法用于资源清理:
```typescript
import { IService } from '@esengine/ecs-framework';
class MyService implements IService {
constructor() {
// 初始化逻辑
}
dispose(): void {
// 清理资源
}
}
```
#### 生命周期
服务容器支持两种生命周期:
- **Singleton单例**: 整个应用程序生命周期内只有一个实例,所有解析请求返回同一个实例
- **Transient瞬时**: 每次解析都创建新的实例
## 基础使用
### 访问服务容器
Core 类内置了服务容器,可以通过 `Core.services` 访问:
```typescript
import { Core } from '@esengine/ecs-framework';
// 初始化Core
Core.create({ debug: true });
// 访问服务容器
const container = Core.services;
```
### 注册服务
#### 注册单例服务
单例服务在首次解析时创建,之后所有解析请求都返回同一个实例:
```typescript
class DataService implements IService {
private data: Map<string, any> = new Map();
getData(key: string) {
return this.data.get(key);
}
setData(key: string, value: any) {
this.data.set(key, value);
}
dispose(): void {
this.data.clear();
}
}
// 注册单例服务
Core.services.registerSingleton(DataService);
```
#### 注册瞬时服务
瞬时服务每次解析都创建新实例,适用于无状态或短生命周期的服务:
```typescript
class CommandService implements IService {
execute(command: string) {
console.log(`Executing: ${command}`);
}
dispose(): void {
// 清理资源
}
}
// 注册瞬时服务
Core.services.registerTransient(CommandService);
```
#### 注册服务实例
直接注册已创建的实例,自动视为单例:
```typescript
const config = new ConfigService();
config.load('./config.json');
// 注册实例
Core.services.registerInstance(ConfigService, config);
```
#### 使用工厂函数注册
工厂函数允许你在创建服务时执行自定义逻辑:
```typescript
Core.services.registerSingleton(LoggerService, (container) => {
const logger = new LoggerService();
logger.setLevel('debug');
return logger;
});
```
### 解析服务
#### resolve 方法
解析服务实例,如果服务未注册会抛出异常:
```typescript
// 解析服务
const dataService = Core.services.resolve(DataService);
dataService.setData('player', { name: 'Alice', score: 100 });
// 单例服务,多次解析返回同一个实例
const same = Core.services.resolve(DataService);
console.log(same === dataService); // true
```
#### tryResolve 方法
尝试解析服务,如果未注册返回 null 而不抛出异常:
```typescript
const optional = Core.services.tryResolve(OptionalService);
if (optional) {
optional.doSomething();
}
```
#### isRegistered 方法
检查服务是否已注册:
```typescript
if (Core.services.isRegistered(DataService)) {
const service = Core.services.resolve(DataService);
}
```
## 内置服务
Core 在初始化时自动注册了以下内置服务:
### TimerManager
定时器管理器,负责管理所有游戏定时器:
```typescript
const timerManager = Core.services.resolve(TimerManager);
// 创建定时器
timerManager.schedule(1.0, false, null, (timer) => {
console.log('1秒后执行');
});
```
### PerformanceMonitor
性能监控器,监控游戏性能并提供优化建议:
```typescript
const monitor = Core.services.resolve(PerformanceMonitor);
// 启用性能监控
monitor.enable();
// 获取性能数据
const fps = monitor.getFPS();
```
### SceneManager
场景管理器,管理单场景应用的场景生命周期:
```typescript
const sceneManager = Core.services.resolve(SceneManager);
// 设置当前场景
sceneManager.setScene(new GameScene());
// 获取当前场景
const currentScene = sceneManager.currentScene;
// 延迟切换场景
sceneManager.loadScene(new MenuScene());
// 更新场景
sceneManager.update();
```
### WorldManager
世界管理器,管理多个独立的 World 实例(高级用例):
```typescript
const worldManager = Core.services.resolve(WorldManager);
// 创建独立的游戏世界
const gameWorld = worldManager.createWorld('game_room_001', {
name: 'GameRoom',
maxScenes: 5
});
// 在World中创建场景
const scene = gameWorld.createScene('battle', new BattleScene());
gameWorld.setSceneActive('battle', true);
// 更新所有World
worldManager.updateAll();
```
**适用场景**:
- SceneManager: 适用于 95% 的游戏(单人游戏、简单多人游戏)
- WorldManager: 适用于 MMO 服务器、游戏房间系统等需要完全隔离的多世界应用
### PoolManager
对象池管理器,管理所有对象池:
```typescript
const poolManager = Core.services.resolve(PoolManager);
// 创建对象池
const bulletPool = poolManager.createPool('bullets', () => new Bullet(), 100);
```
### PluginManager
插件管理器,管理插件的安装和卸载:
```typescript
const pluginManager = Core.services.resolve(PluginManager);
// 获取所有已安装的插件
const plugins = pluginManager.getAllPlugins();
```
## 依赖注入
ECS Framework 提供了装饰器来简化依赖注入。
### @Injectable 装饰器
标记类为可注入的服务:
```typescript
import { Injectable, IService } from '@esengine/ecs-framework';
@Injectable()
class GameService implements IService {
constructor() {
console.log('GameService created');
}
dispose(): void {
console.log('GameService disposed');
}
}
```
### @Inject 装饰器
在构造函数中注入依赖:
```typescript
import { Injectable, Inject, IService } from '@esengine/ecs-framework';
@Injectable()
class PlayerService implements IService {
constructor(
@Inject(DataService) private data: DataService,
@Inject(GameService) private game: GameService
) {
// data 和 game 会自动从容器中解析
}
dispose(): void {
// 清理资源
}
}
```
### 注册可注入服务
使用 `registerInjectable` 自动处理依赖注入:
```typescript
import { registerInjectable } from '@esengine/ecs-framework';
// 注册服务(会自动解析@Inject依赖
registerInjectable(Core.services, PlayerService);
// 解析时会自动注入依赖
const player = Core.services.resolve(PlayerService);
```
### @Updatable 装饰器
标记服务为可更新的,使其在每帧自动被调用:
```typescript
import { Injectable, Updatable, IService, IUpdatable } from '@esengine/ecs-framework';
@Injectable()
@Updatable() // 默认优先级为0
class PhysicsService implements IService, IUpdatable {
update(deltaTime?: number): void {
// 每帧更新物理模拟
}
dispose(): void {
// 清理资源
}
}
// 指定更新优先级(数值越小越先执行)
@Injectable()
@Updatable(10)
class RenderService implements IService, IUpdatable {
update(deltaTime?: number): void {
// 每帧渲染
}
dispose(): void {
// 清理资源
}
}
```
使用 `@Updatable` 装饰器的服务会被 Core 自动调用,无需手动管理:
```typescript
// Core.update() 会自动调用所有@Updatable服务的update方法
function gameLoop(deltaTime: number) {
Core.update(deltaTime); // 自动更新所有可更新服务
}
```
## 自定义服务
### 创建自定义服务
实现 `IService` 接口并注册到容器:
```typescript
import { IService } from '@esengine/ecs-framework';
class AudioService implements IService {
private sounds: Map<string, HTMLAudioElement> = new Map();
play(soundId: string) {
const sound = this.sounds.get(soundId);
if (sound) {
sound.play();
}
}
load(soundId: string, url: string) {
const audio = new Audio(url);
this.sounds.set(soundId, audio);
}
dispose(): void {
// 停止所有音效并清理
for (const sound of this.sounds.values()) {
sound.pause();
sound.src = '';
}
this.sounds.clear();
}
}
// 注册自定义服务
Core.services.registerSingleton(AudioService);
// 使用服务
const audio = Core.services.resolve(AudioService);
audio.load('jump', '/sounds/jump.mp3');
audio.play('jump');
```
### 服务间依赖
服务可以依赖其他服务:
```typescript
@Injectable()
class ConfigService implements IService {
private config: any = {};
get(key: string) {
return this.config[key];
}
dispose(): void {
this.config = {};
}
}
@Injectable()
class NetworkService implements IService {
constructor(
@Inject(ConfigService) private config: ConfigService
) {
// 使用配置服务
const apiUrl = this.config.get('apiUrl');
}
dispose(): void {
// 清理网络连接
}
}
// 注册服务(按依赖顺序)
registerInjectable(Core.services, ConfigService);
registerInjectable(Core.services, NetworkService);
```
## 高级用法
### 服务替换(测试)
在测试中替换真实服务为模拟服务:
```typescript
// 测试代码
class MockDataService implements IService {
getData(key: string) {
return 'mock data';
}
dispose(): void {}
}
// 注册模拟服务(用于测试)
Core.services.registerInstance(DataService, new MockDataService());
```
### 循环依赖检测
服务容器会自动检测循环依赖:
```typescript
// A 依赖 BB 依赖 A
@Injectable()
class ServiceA implements IService {
constructor(@Inject(ServiceB) b: ServiceB) {}
dispose(): void {}
}
@Injectable()
class ServiceB implements IService {
constructor(@Inject(ServiceA) a: ServiceA) {}
dispose(): void {}
}
// 解析时会抛出错误: Circular dependency detected: ServiceA -> ServiceB -> ServiceA
```
### 获取所有服务
```typescript
// 获取所有已注册的服务类型
const types = Core.services.getRegisteredServices();
// 获取所有已实例化的服务实例
const instances = Core.services.getAll();
```
### 服务清理
```typescript
// 注销单个服务
Core.services.unregister(MyService);
// 清空所有服务会调用每个服务的dispose方法
Core.services.clear();
```
## 最佳实践
### 服务命名
服务类名应该以 `Service``Manager` 结尾,清晰表达其职责:
```typescript
class PlayerService implements IService {}
class AudioManager implements IService {}
class NetworkService implements IService {}
```
### 资源清理
始终在 `dispose()` 方法中清理资源:
```typescript
class ResourceService implements IService {
private resources: Map<string, Resource> = new Map();
dispose(): void {
// 释放所有资源
for (const resource of this.resources.values()) {
resource.release();
}
this.resources.clear();
}
}
```
### 避免过度使用
不要把所有类都注册为服务,服务应该是:
- 全局单例或需要共享状态
- 需要在多处使用
- 生命周期需要管理
- 需要依赖注入
对于简单的工具类或数据类,直接创建实例即可。
### 依赖方向
保持清晰的依赖方向,避免循环依赖:
```
高层服务 -> 中层服务 -> 底层服务
GameLogic -> DataService -> ConfigService
```
## 常见问题
### 服务未注册错误
**问题**: `Error: Service MyService is not registered`
**解决**:
```typescript
// 确保服务已注册
Core.services.registerSingleton(MyService);
// 或者使用tryResolve
const service = Core.services.tryResolve(MyService);
if (!service) {
console.log('Service not found');
}
```
### 循环依赖错误
**问题**: `Circular dependency detected`
**解决**: 重新设计服务依赖关系,引入中间服务或使用事件系统解耦。
### 何时使用单例 vs 瞬时
- **单例**: 管理器类、配置、缓存、状态管理
- **瞬时**: 命令对象、请求处理器、临时任务
## 相关链接
- [插件系统](./plugin-system.md) - 使用服务容器注册插件服务
- [快速开始](./getting-started.md) - Core 初始化和基础使用
- [系统架构](./system.md) - 在系统中使用服务

650
docs/guide/system.md Normal file
View File

@@ -0,0 +1,650 @@
# 系统架构
在 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 {
// 方式1传入实例
this.addSystem(new MovementSystem());
this.addSystem(new RenderSystem());
// 方式2传入类型自动依赖注入
this.addEntityProcessor(PhysicsSystem);
// 设置系统更新顺序
const movementSystem = this.getSystem(MovementSystem);
if (movementSystem) {
movementSystem.updateOrder = 1;
}
}
}
```
### 系统依赖注入
系统实现了 `IService` 接口,支持通过依赖注入获取其他服务或系统:
```typescript
import { ECSSystem, Injectable, Inject } from '@esengine/ecs-framework';
@Injectable()
@ECSSystem('Physics')
class PhysicsSystem extends EntitySystem {
constructor(
@Inject(CollisionService) private collision: CollisionService
) {
super(Matcher.all(Transform, RigidBody));
}
protected process(entities: readonly Entity[]): void {
// 使用注入的服务
this.collision.detectCollisions(entities);
}
// 实现 IService 接口的 dispose 方法
public dispose(): void {
// 清理资源
}
}
// 使用时传入类型即可,框架会自动注入依赖
class GameScene extends Scene {
protected initialize(): void {
// 自动依赖注入
this.addEntityProcessor(PhysicsSystem);
}
}
```
注意事项:
- 使用 `@Injectable()` 装饰器标记需要依赖注入的系统
- 在构造函数参数中使用 `@Inject()` 装饰器声明依赖
- 系统必须实现 `dispose()` 方法IService 接口要求)
- 使用 `addEntityProcessor(类型)` 而不是 `addSystem(new 类型())` 来启用依赖注入
### 系统更新顺序
```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 架构的逻辑处理核心,正确设计和使用系统能让你的游戏代码更加模块化、高效和易于维护。

View 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
View 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框架提供了强大的并行计算能力让你能够充分利用现代多核处理器的性能为复杂的游戏逻辑和计算密集型任务提供了高效的解决方案。

761
docs/guide/world-manager.md Normal file
View File

@@ -0,0 +1,761 @@
# WorldManager
WorldManager 是 ECS Framework 提供的高级世界管理器用于管理多个完全隔离的游戏世界World。每个 World 都是独立的 ECS 环境,可以包含多个场景。
## 适用场景
WorldManager 适合以下高级场景:
- MMO 游戏服务器的多房间管理
- 游戏大厅系统(每个游戏房间完全隔离)
- 服务器端的多游戏实例
- 需要完全隔离的多个游戏环境
- 需要同时运行多个独立世界的应用
## 特点
- 多 World 管理,每个 World 完全独立
- 每个 World 可以包含多个 Scene
- 支持 World 的激活/停用
- 自动清理空 World
- World 级别的全局系统
- 批量操作和查询
## 基本使用
### 初始化
WorldManager 是 Core 的内置服务,通过服务容器获取:
```typescript
import { Core, WorldManager } from '@esengine/ecs-framework';
// 初始化 Core
Core.create({ debug: true });
// 从服务容器获取 WorldManagerCore 已自动创建并注册)
const worldManager = Core.services.resolve(WorldManager);
```
### 创建 World
```typescript
// 创建游戏房间 World
const room1 = worldManager.createWorld('room_001', {
name: 'GameRoom_001',
maxScenes: 5,
debug: true
});
// 激活 World
worldManager.setWorldActive('room_001', true);
// 创建更多房间
const room2 = worldManager.createWorld('room_002', {
name: 'GameRoom_002',
maxScenes: 5
});
worldManager.setWorldActive('room_002', true);
```
### 游戏循环
在游戏循环中更新所有活跃的 World
```typescript
function gameLoop(deltaTime: number) {
Core.update(deltaTime); // 更新全局服务
worldManager.updateAll(); // 更新所有活跃的 World
}
// 启动游戏循环
let lastTime = 0;
setInterval(() => {
const currentTime = Date.now();
const deltaTime = (currentTime - lastTime) / 1000;
lastTime = currentTime;
gameLoop(deltaTime);
}, 16); // 60 FPS
```
## World 管理
### 创建 World
```typescript
// 基本创建
const world = worldManager.createWorld('worldId');
// 带配置创建
const world = worldManager.createWorld('worldId', {
name: 'MyWorld',
maxScenes: 10,
autoCleanup: true,
debug: true
});
```
**配置选项IWorldConfig**
- `name?: string` - World 名称
- `maxScenes?: number` - 最大场景数量限制(默认 10
- `autoCleanup?: boolean` - 是否自动清理空场景(默认 true
- `debug?: boolean` - 是否启用调试模式(默认 false
### 获取 World
```typescript
// 通过 ID 获取
const world = worldManager.getWorld('room_001');
if (world) {
console.log(`World: ${world.name}`);
}
// 获取所有 World
const allWorlds = worldManager.getAllWorlds();
console.log(`共有 ${allWorlds.length} 个 World`);
// 获取所有 World ID
const worldIds = worldManager.getWorldIds();
console.log('World 列表:', worldIds);
// 通过名称查找
const world = worldManager.findWorldByName('GameRoom_001');
```
### 激活和停用 World
```typescript
// 激活 World开始运行和更新
worldManager.setWorldActive('room_001', true);
// 停用 World停止更新但保留数据
worldManager.setWorldActive('room_001', false);
// 检查 World 是否激活
if (worldManager.isWorldActive('room_001')) {
console.log('房间正在运行');
}
// 获取所有活跃的 World
const activeWorlds = worldManager.getActiveWorlds();
console.log(`当前有 ${activeWorlds.length} 个活跃 World`);
```
### 移除 World
```typescript
// 移除 World会自动停用并销毁
const removed = worldManager.removeWorld('room_001');
if (removed) {
console.log('World 已移除');
}
```
## World 中的场景管理
每个 World 可以包含多个 Scene 并独立管理它们的生命周期。
### 创建场景
```typescript
const world = worldManager.getWorld('room_001');
if (!world) return;
// 创建场景
const mainScene = world.createScene('main', new MainScene());
const uiScene = world.createScene('ui', new UIScene());
const hudScene = world.createScene('hud', new HUDScene());
// 激活场景
world.setSceneActive('main', true);
world.setSceneActive('ui', true);
world.setSceneActive('hud', false);
```
### 查询场景
```typescript
// 获取特定场景
const mainScene = world.getScene<MainScene>('main');
if (mainScene) {
console.log(`场景名称: ${mainScene.name}`);
}
// 获取所有场景
const allScenes = world.getAllScenes();
console.log(`World 中共有 ${allScenes.length} 个场景`);
// 获取所有场景 ID
const sceneIds = world.getSceneIds();
console.log('场景列表:', sceneIds);
// 获取活跃场景数量
const activeCount = world.getActiveSceneCount();
console.log(`当前有 ${activeCount} 个活跃场景`);
// 检查场景是否激活
if (world.isSceneActive('main')) {
console.log('主场景正在运行');
}
```
### 场景切换
World 支持多场景同时运行,也支持场景切换:
```typescript
class GameWorld {
private world: World;
constructor(worldManager: WorldManager) {
this.world = worldManager.createWorld('game', {
name: 'GameWorld',
maxScenes: 5
});
// 创建所有场景
this.world.createScene('menu', new MenuScene());
this.world.createScene('game', new GameScene());
this.world.createScene('pause', new PauseScene());
this.world.createScene('gameover', new GameOverScene());
// 激活 World
worldManager.setWorldActive('game', true);
}
public showMenu(): void {
this.deactivateAllScenes();
this.world.setSceneActive('menu', true);
}
public startGame(): void {
this.deactivateAllScenes();
this.world.setSceneActive('game', true);
}
public pauseGame(): void {
// 游戏场景继续存在但停止更新
this.world.setSceneActive('game', false);
// 显示暂停界面
this.world.setSceneActive('pause', true);
}
public resumeGame(): void {
this.world.setSceneActive('pause', false);
this.world.setSceneActive('game', true);
}
public showGameOver(): void {
this.deactivateAllScenes();
this.world.setSceneActive('gameover', true);
}
private deactivateAllScenes(): void {
const sceneIds = this.world.getSceneIds();
sceneIds.forEach(id => this.world.setSceneActive(id, false));
}
}
```
### 移除场景
```typescript
// 移除不再需要的场景
const removed = world.removeScene('oldScene');
if (removed) {
console.log('场景已移除');
}
// 场景会自动调用 end() 方法进行清理
```
## 全局系统
World 支持全局系统,这些系统在 World 级别运行,不依赖特定 Scene。
### 定义全局系统
```typescript
import { IGlobalSystem } from '@esengine/ecs-framework';
// 网络系统World 级别)
class NetworkSystem implements IGlobalSystem {
readonly name = 'NetworkSystem';
private connectionId: string;
constructor(connectionId: string) {
this.connectionId = connectionId;
}
initialize(): void {
console.log(`网络系统初始化: ${this.connectionId}`);
// 建立网络连接
}
update(deltaTime?: number): void {
// 处理网络消息,不依赖任何 Scene
// 接收和发送网络包
}
destroy(): void {
console.log(`网络系统销毁: ${this.connectionId}`);
// 关闭网络连接
}
}
// 物理系统World 级别)
class PhysicsSystem implements IGlobalSystem {
readonly name = 'PhysicsSystem';
initialize(): void {
console.log('物理系统初始化');
}
update(deltaTime?: number): void {
// 物理模拟,作用于 World 中所有场景
}
destroy(): void {
console.log('物理系统销毁');
}
}
```
### 使用全局系统
```typescript
const world = worldManager.getWorld('room_001');
if (!world) return;
// 添加全局系统
const networkSystem = world.addGlobalSystem(new NetworkSystem('conn_001'));
const physicsSystem = world.addGlobalSystem(new PhysicsSystem());
// 获取全局系统
const network = world.getGlobalSystem(NetworkSystem);
if (network) {
console.log('找到网络系统');
}
// 移除全局系统
world.removeGlobalSystem(networkSystem);
```
## 批量操作
### 更新所有 World
```typescript
// 更新所有活跃的 World应该在游戏循环中调用
worldManager.updateAll();
// 这会自动更新每个 World 的:
// 1. 全局系统
// 2. 所有活跃场景
```
### 启动和停止
```typescript
// 启动所有 World
worldManager.startAll();
// 停止所有 World
worldManager.stopAll();
// 检查是否正在运行
if (worldManager.isRunning) {
console.log('WorldManager 正在运行');
}
```
### 查找 World
```typescript
// 使用条件查找
const emptyWorlds = worldManager.findWorlds(world => {
return world.sceneCount === 0;
});
// 查找活跃的 World
const activeWorlds = worldManager.findWorlds(world => {
return world.isActive;
});
// 查找特定名称的 World
const world = worldManager.findWorldByName('GameRoom_001');
```
## 统计和监控
### 获取统计信息
```typescript
const stats = worldManager.getStats();
console.log(`总 World 数: ${stats.totalWorlds}`);
console.log(`活跃 World 数: ${stats.activeWorlds}`);
console.log(`总场景数: ${stats.totalScenes}`);
console.log(`总实体数: ${stats.totalEntities}`);
console.log(`总系统数: ${stats.totalSystems}`);
// 查看每个 World 的详细信息
stats.worlds.forEach(worldInfo => {
console.log(`World: ${worldInfo.name}`);
console.log(` 场景数: ${worldInfo.sceneCount}`);
console.log(` 是否活跃: ${worldInfo.isActive}`);
});
```
### 获取详细状态
```typescript
const status = worldManager.getDetailedStatus();
// 包含所有 World 的详细状态
status.worlds.forEach(worldStatus => {
console.log(`World ID: ${worldStatus.id}`);
console.log(`状态:`, worldStatus.status);
});
```
## 自动清理
WorldManager 支持自动清理空的 World。
### 配置清理
```typescript
// 创建带清理配置的 WorldManager
const worldManager = Core.services.resolve(WorldManager);
// WorldManager 的配置在 Core 中设置:
// {
// maxWorlds: 50,
// autoCleanup: true,
// cleanupInterval: 30000 // 30 秒
// }
```
### 手动清理
```typescript
// 手动触发清理
const cleanedCount = worldManager.cleanup();
console.log(`清理了 ${cleanedCount} 个 World`);
```
**清理条件**
- World 未激活
- 没有 Scene 或所有 Scene 都是空的
- 创建时间超过 10 分钟
## API 参考
### WorldManager API
| 方法 | 说明 |
|------|------|
| `createWorld(worldId, config?)` | 创建新 World |
| `removeWorld(worldId)` | 移除 World |
| `getWorld(worldId)` | 获取 World |
| `getAllWorlds()` | 获取所有 World |
| `getWorldIds()` | 获取所有 World ID |
| `setWorldActive(worldId, active)` | 设置 World 激活状态 |
| `isWorldActive(worldId)` | 检查 World 是否激活 |
| `getActiveWorlds()` | 获取所有活跃的 World |
| `updateAll()` | 更新所有活跃 World |
| `startAll()` | 启动所有 World |
| `stopAll()` | 停止所有 World |
| `findWorlds(predicate)` | 查找满足条件的 World |
| `findWorldByName(name)` | 根据名称查找 World |
| `getStats()` | 获取统计信息 |
| `getDetailedStatus()` | 获取详细状态信息 |
| `cleanup()` | 清理空 World |
| `destroy()` | 销毁 WorldManager |
### World API
| 方法 | 说明 |
|------|------|
| `createScene(sceneId, sceneInstance?)` | 创建并添加 Scene |
| `removeScene(sceneId)` | 移除 Scene |
| `getScene(sceneId)` | 获取 Scene |
| `getAllScenes()` | 获取所有 Scene |
| `getSceneIds()` | 获取所有 Scene ID |
| `setSceneActive(sceneId, active)` | 设置 Scene 激活状态 |
| `isSceneActive(sceneId)` | 检查 Scene 是否激活 |
| `getActiveSceneCount()` | 获取活跃 Scene 数量 |
| `addGlobalSystem(system)` | 添加全局系统 |
| `removeGlobalSystem(system)` | 移除全局系统 |
| `getGlobalSystem(type)` | 获取全局系统 |
| `start()` | 启动 World |
| `stop()` | 停止 World |
| `updateGlobalSystems()` | 更新全局系统 |
| `updateScenes()` | 更新所有激活 Scene |
| `destroy()` | 销毁 World |
| `getStatus()` | 获取 World 状态 |
| `getStats()` | 获取统计信息 |
### 属性
| 属性 | 说明 |
|------|------|
| `worldCount` | World 总数 |
| `activeWorldCount` | 活跃 World 数量 |
| `isRunning` | 是否正在运行 |
| `config` | 配置信息 |
## 完整示例
### MMO 游戏房间系统
```typescript
import { Core, WorldManager, Scene, World } from '@esengine/ecs-framework';
// 初始化
Core.create({ debug: true });
const worldManager = Core.services.resolve(WorldManager);
// 房间管理器
class RoomManager {
private worldManager: WorldManager;
private rooms: Map<string, World> = new Map();
constructor(worldManager: WorldManager) {
this.worldManager = worldManager;
}
// 创建游戏房间
public createRoom(roomId: string, maxPlayers: number): World {
const world = this.worldManager.createWorld(roomId, {
name: `Room_${roomId}`,
maxScenes: 3,
debug: true
});
// 创建房间场景
world.createScene('lobby', new LobbyScene());
world.createScene('game', new GameScene());
world.createScene('result', new ResultScene());
// 添加房间级别的系统
world.addGlobalSystem(new NetworkSystem(roomId));
world.addGlobalSystem(new RoomLogicSystem(maxPlayers));
// 激活 World 和初始场景
this.worldManager.setWorldActive(roomId, true);
world.setSceneActive('lobby', true);
this.rooms.set(roomId, world);
console.log(`房间 ${roomId} 已创建`);
return world;
}
// 玩家加入房间
public joinRoom(roomId: string, playerId: string): boolean {
const world = this.rooms.get(roomId);
if (!world) {
console.log(`房间 ${roomId} 不存在`);
return false;
}
// 在大厅场景中创建玩家实体
const lobbyScene = world.getScene('lobby');
if (lobbyScene) {
const player = lobbyScene.createEntity(`Player_${playerId}`);
// 添加玩家组件...
console.log(`玩家 ${playerId} 加入房间 ${roomId}`);
return true;
}
return false;
}
// 开始游戏
public startGame(roomId: string): void {
const world = this.rooms.get(roomId);
if (!world) return;
// 切换到游戏场景
world.setSceneActive('lobby', false);
world.setSceneActive('game', true);
console.log(`房间 ${roomId} 游戏开始`);
}
// 结束游戏
public endGame(roomId: string): void {
const world = this.rooms.get(roomId);
if (!world) return;
// 切换到结果场景
world.setSceneActive('game', false);
world.setSceneActive('result', true);
console.log(`房间 ${roomId} 游戏结束`);
}
// 关闭房间
public closeRoom(roomId: string): void {
this.worldManager.removeWorld(roomId);
this.rooms.delete(roomId);
console.log(`房间 ${roomId} 已关闭`);
}
// 获取房间列表
public getRoomList(): string[] {
return Array.from(this.rooms.keys());
}
// 获取房间统计
public getRoomStats(roomId: string) {
const world = this.rooms.get(roomId);
return world?.getStats();
}
}
// 使用房间管理器
const roomManager = new RoomManager(worldManager);
// 创建多个游戏房间
roomManager.createRoom('room_001', 4);
roomManager.createRoom('room_002', 4);
roomManager.createRoom('room_003', 2);
// 玩家加入
roomManager.joinRoom('room_001', 'player_1');
roomManager.joinRoom('room_001', 'player_2');
// 开始游戏
roomManager.startGame('room_001');
// 游戏循环
function gameLoop(deltaTime: number) {
Core.update(deltaTime);
worldManager.updateAll(); // 更新所有房间
}
// 定期清理空房间
setInterval(() => {
const stats = worldManager.getStats();
console.log(`当前房间数: ${stats.totalWorlds}`);
console.log(`活跃房间数: ${stats.activeWorlds}`);
worldManager.cleanup();
}, 60000); // 每分钟清理一次
```
## 最佳实践
### 1. 合理的 World 粒度
```typescript
// 推荐:每个独立环境一个 World
const room1 = worldManager.createWorld('room_1'); // 游戏房间1
const room2 = worldManager.createWorld('room_2'); // 游戏房间2
// 不推荐:过度使用 World
const world1 = worldManager.createWorld('ui'); // UI 不需要独立 World
const world2 = worldManager.createWorld('menu'); // 菜单不需要独立 World
```
### 2. 使用全局系统处理跨场景逻辑
```typescript
// 推荐World 级别的系统
class NetworkSystem implements IGlobalSystem {
update() {
// 网络处理不依赖场景
}
}
// 不推荐:在每个场景中重复创建
class GameScene extends Scene {
initialize() {
this.addSystem(new NetworkSystem()); // 不应该在场景级别
}
}
```
### 3. 及时清理不用的 World
```typescript
// 推荐:玩家离开时清理房间
function onPlayerLeave(roomId: string) {
const world = worldManager.getWorld(roomId);
if (world && world.sceneCount === 0) {
worldManager.removeWorld(roomId);
}
}
// 或使用自动清理
worldManager.cleanup();
```
### 4. 监控资源使用
```typescript
// 定期检查资源使用情况
setInterval(() => {
const stats = worldManager.getStats();
if (stats.totalWorlds > 100) {
console.warn('World 数量过多,考虑清理');
worldManager.cleanup();
}
if (stats.totalEntities > 10000) {
console.warn('实体数量过多,检查是否有泄漏');
}
}, 30000);
```
## 与 SceneManager 的对比
| 特性 | SceneManager | WorldManager |
|------|--------------|--------------|
| 适用场景 | 95% 的游戏应用 | 高级多世界隔离场景 |
| 复杂度 | 简单 | 复杂 |
| 场景数量 | 单场景(可切换) | 多 World每个 World 多场景 |
| 场景隔离 | 无(场景切换) | 完全隔离(每个 World 独立) |
| 性能开销 | 最小 | 较高 |
| 全局系统 | 无 | 支持World 级别) |
| 使用示例 | 单人游戏、移动游戏 | MMO 服务器、游戏房间系统 |
**何时使用 WorldManager**
- MMO 游戏服务器(每个房间一个 World
- 游戏大厅系统(每个游戏房间完全隔离)
- 需要运行多个完全独立的游戏实例
- 服务器端模拟多个游戏世界
**何时使用 SceneManager**
- 单人游戏
- 简单的多人游戏
- 移动游戏
- 场景之间需要切换但不需要同时运行
## 架构层次
WorldManager 在 ECS Framework 中的位置:
```
Core (全局服务)
└── WorldManager (世界管理)
├── World 1 (游戏房间1)
│ ├── GlobalSystem (全局系统)
│ ├── Scene 1 (场景1)
│ │ ├── EntitySystem
│ │ ├── Entity
│ │ └── Component
│ └── Scene 2 (场景2)
├── World 2 (游戏房间2)
│ ├── GlobalSystem
│ └── Scene 1
└── World 3 (游戏房间3)
```
WorldManager 为需要多世界隔离的高级应用提供了强大的管理能力。如果你的应用不需要多世界隔离,建议使用更简单的 [SceneManager](./scene-manager.md)。

23
docs/index.md Normal file
View 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: 核心功能独立打包,支持多平台
---

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

File diff suppressed because it is too large Load Diff

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

View File

@@ -1,325 +0,0 @@
# QuerySystem 使用指南
QuerySystem 是 ECS Framework 中的高性能实体查询系统,支持多级索引、智能缓存和类型安全的查询操作。
## 基本用法
### 1. 获取查询系统
```typescript
import { Scene } from './ECS/Scene';
import { Entity } from './ECS/Entity';
// 创建场景,查询系统会自动创建
const scene = new Scene();
const querySystem = scene.querySystem;
// 或者从Core获取当前场景的查询系统
import { Core } from './Core';
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.queryAllTyped(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
// 自动优化索引配置
querySystem.optimizeIndexes();
// 获取性能统计
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 } from './ECS/Systems/EntitySystem';
class MovementSystem extends EntitySystem {
public update(): 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 {
public update(): 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 {
public update(): 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. 代码组织
- 将复杂查询封装到专门的方法中
- 使用查询构建器创建可读性更好的查询
- 在系统中合理组织查询逻辑

71
eslint.config.mjs Normal file
View File

@@ -0,0 +1,71 @@
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
export default [
eslint.configs.recommended,
...tseslint.configs.recommended,
{
files: ['packages/**/src/**/*.{ts,tsx,js,jsx}'],
languageOptions: {
parser: tseslint.parser,
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
project: true,
tsconfigRootDir: import.meta.dirname
}
},
rules: {
'semi': ['warn', 'always'],
'quotes': ['warn', 'single', { avoidEscape: true }],
'indent': ['warn', 4, {
SwitchCase: 1,
ignoredNodes: [
'PropertyDefinition[decorators.length > 0]',
'TSTypeParameterInstantiation'
]
}],
'no-trailing-spaces': 'warn',
'eol-last': ['warn', 'always'],
'comma-dangle': ['warn', 'never'],
'object-curly-spacing': ['warn', 'always'],
'array-bracket-spacing': ['warn', 'never'],
'arrow-parens': ['warn', 'always'],
'no-multiple-empty-lines': ['warn', { max: 2, maxEOF: 1 }],
'no-console': 'off',
'no-empty': 'warn',
'no-case-declarations': 'warn',
'no-useless-catch': 'warn',
'no-prototype-builtins': 'warn',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unsafe-assignment': 'warn',
'@typescript-eslint/no-unsafe-member-access': 'warn',
'@typescript-eslint/no-unsafe-call': 'warn',
'@typescript-eslint/no-unsafe-return': 'warn',
'@typescript-eslint/no-unsafe-argument': 'warn',
'@typescript-eslint/no-unsafe-function-type': 'warn',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-non-null-assertion': 'off'
}
},
{
ignores: [
'node_modules/**',
'**/node_modules/**',
'dist/**',
'**/dist/**',
'bin/**',
'**/bin/**',
'build/**',
'**/build/**',
'coverage/**',
'**/coverage/**',
'thirdparty/**',
'examples/lawn-mower-demo/**',
'extensions/**',
'**/*.min.js',
'**/*.d.ts'
]
}
];

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

View 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"
}
}

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

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

View 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会处理渲染
}
}

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

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

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

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

View 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"]
}

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

26
lerna.json Normal file
View File

@@ -0,0 +1,26 @@
{
"version": "independent",
"npmClient": "npm",
"command": {
"publish": {
"registry": "https://registry.npmjs.org/",
"allowBranch": ["master", "main"],
"conventionalCommits": true,
"message": "chore(release): publish packages",
"topologicalSort": true,
"verifyAccess": true,
"verifyRegistry": true
},
"version": {
"allowBranch": ["master", "main"],
"conventionalCommits": true,
"message": "chore(release): version packages",
"push": false
}
},
"packages": [
"packages/*",
"!extensions/cocos/*/settings/**",
"!examples/*/settings/**"
]
}

27061
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More