Compare commits

..

312 Commits

Author SHA1 Message Date
yhh
157d0b8067 fix(docs): 修复英文文档dead links问题 2025-12-03 22:59:08 +08:00
yhh
dd1ae97de7 feat(docs): 添加中英文国际化支持 2025-12-03 22:48:22 +08:00
YHH
63f006ab62 feat: 添加跨平台运行时、资产系统和UI适配功能 (#256)
* feat(platform-common): 添加WASM加载器和环境检测API

* feat(rapier2d): 新增Rapier2D WASM绑定包

* feat(physics-rapier2d): 添加跨平台WASM加载器

* feat(asset-system): 添加运行时资产目录和bundle格式

* feat(asset-system-editor): 新增编辑器资产管理包

* feat(editor-core): 添加构建系统和模块管理

* feat(editor-app): 重构浏览器预览使用import maps

* feat(platform-web): 添加BrowserRuntime和资产读取

* feat(engine): 添加材质系统和着色器管理

* feat(material): 新增材质系统和着色器编辑器

* feat(tilemap): 增强tilemap编辑器和动画系统

* feat(modules): 添加module.json配置

* feat(core): 添加module.json和类型定义更新

* chore: 更新依赖和构建配置

* refactor(plugins): 更新插件模板使用ModuleManifest

* chore: 添加第三方依赖库

* chore: 移除BehaviourTree-ai和ecs-astar子模块

* docs: 更新README和文档主题样式

* fix: 修复Rust文档测试和添加rapier2d WASM绑定

* fix(tilemap-editor): 修复画布高DPI屏幕分辨率适配问题

* feat(ui): 添加UI屏幕适配系统(CanvasScaler/SafeArea)

* fix(ecs-engine-bindgen): 添加缺失的ecs-framework-math依赖

* fix: 添加缺失的包依赖修复CI构建

* fix: 修复CodeQL检测到的代码问题

* fix: 修复构建错误和缺失依赖

* fix: 修复类型检查错误

* fix(material-system): 修复tsconfig配置支持TypeScript项目引用

* fix(editor-core): 修复Rollup构建配置添加tauri external

* fix: 修复CodeQL检测到的代码问题

* fix: 修复CodeQL检测到的代码问题
2025-12-03 22:15:22 +08:00
YHH
caf7622aa0 Merge pull request #257 from esengine/feat/system-stable-sorting
feat(ecs): 添加系统稳定排序支持
2025-12-03 21:01:52 +08:00
yhh
d746cf3bb8 feat(ecs): 添加系统稳定排序支持 2025-12-03 20:54:34 +08:00
github-actions[bot]
88af781d78 chore(editor): bump version to 1.0.13 (#255)
Co-authored-by: esengine <18465053+esengine@users.noreply.github.com>
2025-12-02 00:07:52 +08:00
yhh
15d5d37e50 fix(docs): 修复 hierarchy.md 中的死链接
将不存在的 ./transform.md 链接替换为 ./component.md
2025-12-01 23:36:20 +08:00
imgbot[bot]
b9aaf894d7 [ImgBot] Optimize images (#252)
*Total -- 1,159.60kb -> 862.13kb (25.65%)

/screenshots/main_screetshot.png -- 175.29kb -> 84.97kb (51.52%)
/screenshots/settings.png -- 44.17kb -> 25.59kb (42.06%)
/screenshots/plugin_manager.png -- 60.70kb -> 37.42kb (38.35%)
/screenshots/about.png -- 32.89kb -> 23.61kb (28.21%)
/screenshots/performance_profiler.png -- 564.36kb -> 420.67kb (25.46%)
/packages/editor-app/src-tauri/icons/128x128.png -- 4.76kb -> 4.40kb (7.55%)
/packages/editor-app/src-tauri/icons/ios/AppIcon-512@2x.png -- 58.30kb -> 54.30kb (6.86%)
/packages/editor-app/src-tauri/icons/Square107x107Logo.png -- 3.96kb -> 3.73kb (5.75%)
/packages/editor-app/src-tauri/icons/Square142x142Logo.png -- 5.44kb -> 5.13kb (5.69%)
/packages/editor-app/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png -- 3.16kb -> 2.98kb (5.69%)
/packages/editor-app/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png -- 3.16kb -> 2.98kb (5.69%)
/packages/editor-app/src-tauri/icons/ios/AppIcon-40x40@2x-1.png -- 2.61kb -> 2.47kb (5.35%)
/packages/editor-app/src-tauri/icons/ios/AppIcon-40x40@2x.png -- 2.61kb -> 2.47kb (5.35%)
/packages/editor-app/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png -- 4.06kb -> 3.87kb (4.73%)
/packages/editor-app/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png -- 7.68kb -> 7.34kb (4.41%)
/packages/editor-app/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png -- 7.68kb -> 7.34kb (4.41%)
/packages/editor-app/src-tauri/icons/ios/AppIcon-40x40@3x.png -- 4.58kb -> 4.38kb (4.33%)
/packages/editor-app/src-tauri/icons/ios/AppIcon-60x60@2x.png -- 4.58kb -> 4.38kb (4.33%)
/packages/editor-app/src-tauri/icons/64x64.png -- 2.11kb -> 2.02kb (4.21%)
/packages/editor-app/src-tauri/icons/Square310x310Logo.png -- 13.92kb -> 13.36kb (4.04%)
/packages/editor-app/src-tauri/icons/Square89x89Logo.png -- 2.99kb -> 2.87kb (3.95%)
/packages/editor-app/src-tauri/icons/Square284x284Logo.png -- 12.53kb -> 12.04kb (3.89%)
/packages/editor-app/src-tauri/icons/Square150x150Logo.png -- 5.84kb -> 5.62kb (3.83%)
/packages/editor-app/src-tauri/icons/icon.png -- 25.58kb -> 24.61kb (3.81%)
/packages/editor-app/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png -- 6.73kb -> 6.47kb (3.75%)
/packages/editor-app/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png -- 6.27kb -> 6.03kb (3.72%)
/packages/editor-app/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png -- 14.68kb -> 14.15kb (3.59%)
/packages/editor-app/src-tauri/icons/ios/AppIcon-76x76@2x.png -- 6.06kb -> 5.85kb (3.51%)
/packages/editor-app/src-tauri/icons/128x128@2x.png -- 10.88kb -> 10.53kb (3.21%)
/packages/editor-app/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png -- 5.66kb -> 5.48kb (3.21%)
/packages/editor-app/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png -- 5.66kb -> 5.48kb (3.21%)
/packages/editor-app/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png -- 9.02kb -> 8.78kb (2.74%)
/packages/editor-app/src-tauri/icons/ios/AppIcon-60x60@3x.png -- 7.22kb -> 7.02kb (2.65%)
/packages/editor-app/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png -- 20.92kb -> 20.40kb (2.49%)
/packages/editor-app/src-tauri/icons/Square71x71Logo.png -- 2.38kb -> 2.33kb (1.81%)
/packages/editor-app/src-tauri/icons/ios/AppIcon-76x76@1x.png -- 2.52kb -> 2.47kb (1.79%)
/packages/editor-app/src-tauri/icons/ios/AppIcon-20x20@3x.png -- 2.01kb -> 1.98kb (1.46%)
/packages/editor-app/src-tauri/icons/ios/AppIcon-29x29@3x.png -- 2.84kb -> 2.81kb (1.1%)
/packages/editor-app/src-tauri/icons/ios/AppIcon-29x29@2x-1.png -- 1.91kb -> 1.89kb (0.82%)
/packages/editor-app/src-tauri/icons/ios/AppIcon-29x29@2x.png -- 1.91kb -> 1.89kb (0.82%)

Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>
Co-authored-by: ImgBotApp <ImgBotHelp@gmail.com>
Co-authored-by: YHH <359807859@qq.com>
2025-12-01 23:34:47 +08:00
YHH
460cdb5af4 Feature/docs improvement (#254)
* refactor: 编辑器/运行时架构拆分与构建系统升级

* feat(core): 层级系统重构与UI变换矩阵修复

* refactor: 移除 ecs-components 聚合包并修复跨包组件查找问题

* fix(physics): 修复跨包组件类引用问题

* feat: 统一运行时架构与浏览器运行支持

* feat(asset): 实现浏览器运行时资产加载系统

* fix: 修复文档、CodeQL安全问题和CI类型检查错误

* fix: 修复文档、CodeQL安全问题和CI类型检查错误

* fix: 修复文档、CodeQL安全问题、CI类型检查和测试错误

* test: 补齐核心模块测试用例,修复CI构建配置

* fix: 修复测试用例中的类型错误和断言问题

* fix: 修复 turbo build:npm 任务的依赖顺序问题

* fix: 修复 CI 构建错误并优化构建性能

* feat(docs): 重构文档站主题样式

* chore(ci): 升级所有 workflow 的 pnpm 版本从 v8 到 v10
2025-12-01 23:33:04 +08:00
github-actions[bot]
290bd9858e chore(core): release v2.2.18 (#253)
Co-authored-by: esengine <18465053+esengine@users.noreply.github.com>
2025-12-01 22:34:05 +08:00
YHH
b42a7b4e43 Feature/editor optimization (#251)
* refactor: 编辑器/运行时架构拆分与构建系统升级

* feat(core): 层级系统重构与UI变换矩阵修复

* refactor: 移除 ecs-components 聚合包并修复跨包组件查找问题

* fix(physics): 修复跨包组件类引用问题

* feat: 统一运行时架构与浏览器运行支持

* feat(asset): 实现浏览器运行时资产加载系统

* fix: 修复文档、CodeQL安全问题和CI类型检查错误

* fix: 修复文档、CodeQL安全问题和CI类型检查错误

* fix: 修复文档、CodeQL安全问题、CI类型检查和测试错误

* test: 补齐核心模块测试用例,修复CI构建配置

* fix: 修复测试用例中的类型错误和断言问题

* fix: 修复 turbo build:npm 任务的依赖顺序问题

* fix: 修复 CI 构建错误并优化构建性能
2025-12-01 22:28:51 +08:00
github-actions[bot]
189714c727 chore(editor): bump version to 1.0.12 (#250)
Co-authored-by: esengine <18465053+esengine@users.noreply.github.com>
2025-11-30 01:07:19 +08:00
YHH
987051acd4 Feature/advanced profiler (#249)
* feat(profiler): 实现高级性能分析器

* test(core): 添加 ProfilerSDK 和 AdvancedProfilerCollector 测试覆盖

* test(core): 添加 ProfilerSDK 和 AdvancedProfilerCollector 测试覆盖

* test(core): 添加 ProfilerSDK 和 AdvancedProfilerCollector 测试覆盖
2025-11-30 00:53:01 +08:00
YHH
374e08a79e feat(profiler): 实现高级性能分析器 (#248)
* feat(profiler): 实现高级性能分析器

* test(core): 添加 ProfilerSDK 和 AdvancedProfilerCollector 测试覆盖

* test(core): 添加 ProfilerSDK 和 AdvancedProfilerCollector 测试覆盖
2025-11-30 00:22:47 +08:00
YHH
359886c72f Feature/physics and tilemap enhancement (#247)
* feat(behavior-tree,tilemap): 修复编辑器连线缩放问题并增强插件系统

* feat(node-editor,blueprint): 新增通用节点编辑器和蓝图可视化脚本系统

* feat(editor,tilemap): 优化编辑器UI样式和Tilemap编辑器功能

* fix: 修复CodeQL安全警告和CI类型检查错误

* fix: 修复CodeQL安全警告和CI类型检查错误

* fix: 修复CodeQL安全警告和CI类型检查错误
2025-11-29 23:00:48 +08:00
yhh
f03b73b58e docs: 完善装饰器和 Matcher API 文档 2025-11-28 11:03:34 +08:00
github-actions[bot]
18d20df4da chore(core): release v2.2.17 (#246)
Co-authored-by: esengine <18465053+esengine@users.noreply.github.com>
2025-11-28 11:03:11 +08:00
github-actions[bot]
c5642a8605 chore(editor): bump version to 1.0.11 (#245)
Co-authored-by: esengine <18465053+esengine@users.noreply.github.com>
2025-11-28 11:01:04 +08:00
YHH
673f5e5855 feat(physics): 集成 Rapier2D 物理引擎并修复预览重置问题 (#244)
* feat(physics): 集成 Rapier2D 物理引擎并修复预览重置问题

* fix: 修复 CI 流程并清理代码
2025-11-28 10:32:28 +08:00
YHH
cabb625a17 Feature/UI input system fix (#243)
* feat(ui): 实现编辑器预览模式下的 UI 输入系统

* feat(platform-web): 为浏览器运行时添加 UI 输入系统绑定
2025-11-27 22:31:05 +08:00
github-actions[bot]
b8f05b79b0 chore(core): release v2.2.16 (#242)
Co-authored-by: esengine <18465053+esengine@users.noreply.github.com>
2025-11-27 21:13:09 +08:00
github-actions[bot]
b22faaac86 chore(editor): bump version to 1.0.10 (#241)
Co-authored-by: esengine <18465053+esengine@users.noreply.github.com>
2025-11-27 21:05:40 +08:00
YHH
107439d70c Feature/runtime cdn and plugin loader (#240)
* feat(ui): 完善 UI 布局系统和编辑器可视化工具

* refactor: 移除 ModuleRegistry,统一使用 PluginManager 插件系统

* fix: 修复 CodeQL 警告并提升测试覆盖率

* refactor: 分离运行时入口点,解决 runtime bundle 包含 React 的问题

* fix(ci): 添加 editor-core 和 editor-runtime 到 CI 依赖构建步骤

* docs: 完善 ServiceContainer 文档,新增 Symbol.for 模式和 @InjectProperty 说明

* fix(ci): 修复 type-check 失败问题

* fix(ci): 修复类型检查失败问题

* fix(ci): 修复类型检查失败问题

* fix(ci): behavior-tree 构建添加 @tauri-apps 外部依赖

* fix(ci): behavior-tree 添加 @tauri-apps/plugin-fs 类型依赖

* fix(ci): platform-web 添加缺失的 behavior-tree 依赖

* fix(lint): 移除正则表达式中不必要的转义字符
2025-11-27 20:42:46 +08:00
github-actions[bot]
71869b1a58 chore(editor): bump version to 1.0.9 (#239)
Co-authored-by: esengine <18465053+esengine@users.noreply.github.com>
2025-11-26 11:47:09 +08:00
yhh
9aed3134cf fix(ci): 修复 Windows 上 mkdir 命令报错 2025-11-26 11:28:14 +08:00
yhh
3ff57aff37 fix(ci): 修复 release-editor workflow 构建顺序 2025-11-26 11:22:43 +08:00
yhh
152c0541b8 fix(ci): 修复 release-editor workflow 构建顺序 2025-11-26 11:15:38 +08:00
YHH
7b14fa2da4 feat(editor): 添加 ECS UI 系统和编辑器更新优化 (#238) 2025-11-26 11:08:10 +08:00
YHH
3fb6f919f8 Feature/tilemap editor (#237)
* feat: 添加 Tilemap 编辑器插件和组件生命周期支持

* feat(editor-core): 添加声明式插件注册 API

* feat(editor-core): 改进tiledmap结构合并tileset进tiledmapeditor

* feat: 添加 editor-runtime SDK 和插件系统改进

* fix(ci): 修复SceneResourceManager里变量未使用问题
2025-11-25 22:23:19 +08:00
github-actions[bot]
551ca7805d chore(core): release v2.2.15 (#236)
Co-authored-by: esengine <18465053+esengine@users.noreply.github.com>
2025-11-23 22:52:05 +08:00
yhh
8ab25fe293 fix(ci): 使用纯 bash+node 实现版本更新绕过 workspace 协议问题 2025-11-23 22:49:54 +08:00
yhh
eea7ed9e58 fix(ci): 将 npm version 改为 pnpm version 修复 workspace 协议问题 2025-11-23 22:46:21 +08:00
yhh
0279cf6d27 fix(ci): 使用 pnpm publish 修复 workspace:* 协议不支持的问题 2025-11-23 22:42:23 +08:00
yhh
0dff1ad2ad fix(ci): 修复 npm 发布时 workspace:* 协议不支持的问题 2025-11-23 22:36:54 +08:00
yhh
95fbcca66f chore: 移除渲染系统调试日志 2025-11-23 22:26:46 +08:00
github-actions[bot]
a61baa83a7 chore(editor): bump version to 1.0.8 (#235)
Co-authored-by: esengine <18465053+esengine@users.noreply.github.com>
2025-11-23 22:21:20 +08:00
yhh
afebeecd68 fix(ci): 修复 Tauri 构建缺少 runtime 文件的问题 2025-11-23 22:08:38 +08:00
yhh
f4e9925319 fix(ci): 修复 Tauri 构建缺少 runtime 文件的问题 2025-11-23 21:57:10 +08:00
YHH
32460ac133 feat(editor): 优化编辑器UI和改进核心功能 (#234)
* feat(editor): 优化编辑器UI和改进核心功能

* feat(editor): 优化编辑器UI和改进核心功能
2025-11-23 21:45:10 +08:00
github-actions[bot]
4d95a7f044 chore(editor): bump version to 1.0.7 (#233)
Co-authored-by: esengine <18465053+esengine@users.noreply.github.com>
2025-11-23 16:30:14 +08:00
yhh
57f919fbe0 fix(ci): 移除release-editor工作流中有问题的TypeScript缓存步骤 2025-11-23 16:12:42 +08:00
yhh
1cb9a0e58f fix(ci): 修复release-editor工作流hashFiles语法错误 2025-11-23 15:47:23 +08:00
yhh
1da43ee822 fix: 修复 release-editor workflow 构建问题 2025-11-23 15:26:17 +08:00
yhh
f4c7563763 chore: 移除 network 相关包并修复 CI 问题 2025-11-23 15:13:51 +08:00
YHH
a3f7cc38b1 Feature/render pipeline (#232)
* refactor(engine): 重构2D渲染管线坐标系统

* feat(engine): 完善2D渲染管线和编辑器视口功能

* feat(editor): 实现Viewport变换工具系统

* feat(editor): 优化Inspector渲染性能并修复Gizmo变换工具显示

* feat(editor): 实现Run on Device移动预览功能

* feat(editor): 添加组件属性控制和依赖关系系统

* feat(editor): 实现动画预览功能和优化SpriteAnimator编辑器

* feat(editor): 修复SpriteAnimator动画预览功能并迁移CI到pnpm

* feat(editor): 修复SpriteAnimator动画预览并迁移到pnpm

* feat(editor): 修复SpriteAnimator动画预览并迁移到pnpm

* feat(editor): 修复SpriteAnimator动画预览并迁移到pnpm

* feat(editor): 修复SpriteAnimator动画预览并迁移到pnpm

* feat(ci): 迁移项目到pnpm并修复CI构建问题

* chore: 迁移CI工作流到pnpm并添加WASM构建支持

* chore: 迁移CI工作流到pnpm并添加WASM构建支持

* chore: 迁移CI工作流到pnpm并添加WASM构建支持

* chore: 迁移CI工作流到pnpm并添加WASM构建支持

* chore: 迁移CI工作流到pnpm并添加WASM构建支持

* chore: 迁移CI工作流到pnpm并添加WASM构建支持

* chore: 移除 network 相关包

* chore: 移除 network 相关包
2025-11-23 14:49:37 +08:00
github-actions[bot]
b15cbab313 chore(editor): bump version to 1.0.6 (#231)
Co-authored-by: esengine <18465053+esengine@users.noreply.github.com>
2025-11-21 12:42:30 +08:00
yhh
504b9ffb66 fix(ci): 添加编辑器工作流缺失的引擎构建步骤 2025-11-21 11:53:33 +08:00
github-actions[bot]
6226e3ff06 chore(core): release v2.2.14 (#230)
Co-authored-by: esengine <18465053+esengine@users.noreply.github.com>
2025-11-21 11:40:22 +08:00
YHH
2621d7f659 refactor(core): 移除@Inject参数装饰器,统一使用@InjectProperty (#229)
* refactor(core): 移除@Inject参数装饰器,统一使用@InjectProperty

* refactor(core): 移除@Inject参数装饰器,统一使用@InjectProperty
2025-11-21 11:37:55 +08:00
YHH
a768b890fd feat: 集成Rust WASM渲染引擎与TypeScript ECS框架 (#228)
* feat: 集成Rust WASM渲染引擎与TypeScript ECS框架

* feat: 增强编辑器UI功能与跨平台支持

* fix: 修复CI测试和类型检查问题

* fix: 修复CI问题并提高测试覆盖率

* fix: 修复CI问题并提高测试覆盖率
2025-11-21 10:03:18 +08:00
yhh
8b9616837d style(editor-app): 移除log信息 2025-11-20 09:51:29 +08:00
yhh
0d2948e60c feat(tools): rust工具初始化工具 2025-11-19 16:27:11 +08:00
YHH
ecfef727c8 feat: 实现可扩展的字段编辑器系统与专业资产选择器 (#227) 2025-11-19 14:54:03 +08:00
YHH
caed5428d5 refactor(editor-app): 改进架构和类型安全 (#226)
* refactor(editor-app): 改进架构和类型安全

* refactor(editor-app): 开始拆分 Inspector.tsx - 创建基础架构

* refactor(editor-app): 完成 Inspector.tsx 拆分

* refactor(editor-app): 优化 Inspector 类型定义,消除所有 any 使用

* refactor(editor): 实现可扩展的属性渲染器系统

* Potential fix for code scanning alert no. 231: Unused variable, import, function or class

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* fix(ci): 防止 Codecov 服务故障阻塞 CI 流程

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-11-18 22:28:13 +08:00
YHH
bce3a6e253 refactor(editor): 提取行为树编辑器为独立包并重构编辑器架构 (#216)
* refactor(editor): 提取行为树编辑器为独立包并重构编辑器架构

* feat(editor): 添加插件市场功能

* feat(editor): 重构插件市场以支持版本管理和ZIP打包

* feat(editor): 重构插件发布流程并修复React渲染警告

* fix(plugin): 修复插件发布和市场的路径不一致问题

* feat: 重构插件发布流程并添加插件删除功能

* fix(editor): 完善插件删除功能并修复多个关键问题

* fix(auth): 修复自动登录与手动登录的竞态条件问题

* feat(editor): 重构插件管理流程

* feat(editor): 支持 ZIP 文件直接发布插件

- 新增 PluginSourceParser 解析插件源
- 重构发布流程支持文件夹和 ZIP 两种方式
- 优化发布向导 UI

* feat(editor): 插件市场支持多版本安装

- 插件解压到项目 plugins 目录
- 新增 Tauri 后端安装/卸载命令
- 支持选择任意版本安装
- 修复打包逻辑,保留完整 dist 目录结构

* feat(editor): 个人中心支持多版本管理

- 合并同一插件的不同版本
- 添加版本历史展开/折叠功能
- 禁止有待审核 PR 时更新插件

* fix(editor): 修复 InspectorRegistry 服务注册

- InspectorRegistry 实现 IService 接口
- 注册到 Core.services 供插件使用

* feat(behavior-tree-editor): 完善插件注册和文件操作

- 添加文件创建模板和操作处理器
- 实现右键菜单创建行为树功能
- 修复文件读取权限问题(使用 Tauri 命令)
- 添加 BehaviorTreeEditorPanel 组件
- 修复 rollup 配置支持动态导入

* feat(plugin): 完善插件构建和发布流程

* fix(behavior-tree-editor): 完整恢复编辑器并修复 Toast 集成

* fix(behavior-tree-editor): 修复节点选中、连线跟随和文件加载问题并优化性能

* fix(behavior-tree-editor): 修复端口连接失败问题并优化连线样式

* refactor(behavior-tree-editor): 移除调试面板功能简化代码结构

* refactor(behavior-tree-editor): 清理冗余代码合并重复逻辑

* feat(behavior-tree-editor): 完善编辑器核心功能增强扩展性

* fix(lint): 修复ESLint错误确保CI通过

* refactor(behavior-tree-editor): 优化编辑器工具栏和编译器功能

* refactor(behavior-tree-editor): 清理技术债务,优化代码质量

* fix(editor-app): 修复字符串替换安全问题
2025-11-18 14:46:51 +08:00
YHH
eac660b1a0 refactor(core): 统一参数命名 - worldId/sceneId 改为 worldName/sceneName (#225)
* refactor(core): 统一参数命名 - worldId/sceneId 改为 worldName/sceneName

* test(core): 更新测试用例以匹配新的错误消息

* refactor(core): 提高代码覆盖率 - 添加参数验证和测试
2025-11-15 00:20:17 +08:00
github-actions[bot]
af49870084 chore(core): release v2.2.13 (#224)
Co-authored-by: esengine <18465053+esengine@users.noreply.github.com>
2025-11-14 12:13:47 +08:00
YHH
e2b316b3cc Fix/entity system dispose ondestroy (#223)
* fix(core): 修复 EntitySystem dispose 未调用 onDestroy 导致资源泄漏

* fix(core): 修复 Scene.end() 中 unload 调用时机导致用户无法清理资源
2025-11-14 12:10:59 +08:00
YHH
3a0544629d feat(core): 为 World 添加独立的服务容器 (#222)
* feat(core): 为 World 添加独立的服务容器

* test(core): 为 World 服务容器添加完整测试覆盖
2025-11-14 09:55:31 +08:00
LINGYE
609baace73 fix(logger): 移除自定义 factory 的缓存, 由使用方管理 (#221)
* fix(logger): 移除自定义 factory 的缓存, 由使用方管理

* test
2025-11-13 16:53:07 +08:00
LINGYE
b12cfba353 refactor(core): 移除 _activeWorlds 并优化 WorldManager 清理机制 (#220)
* refactor(core): 将 WorldManager 清理机制从定时器改为帧驱动

* refactor(core): 移除 WorldManager _activeWorlds 优化,简化状态管理

* test(core): 补充 WorldManager 测试用例

* docs(core): 更新 WorldManager cleanupFrameInterval 配置说明
2025-11-09 17:35:07 +08:00
LINGYE
6242c6daf3 fix(core): 修复 PerformanceMonitor 未遵循 Core debug 参数的问题 (#219)
- Core 传递 debug 配置到 WorldManager
- WorldManager 传递 debug 配置到 World
- World 在 debug=true 时为 Scene 注册并启用 PerformanceMonitor
- new Scene 的情况默认未开启,但暴露了 `performanceMonitor` 由使用者处理
2025-11-09 11:32:04 +08:00
github-actions[bot]
b5337de278 chore(core): release v2.2.12 (#218)
Co-authored-by: esengine <18465053+esengine@users.noreply.github.com>
2025-11-07 12:12:44 +08:00
YHH
3512199ff4 fix(core): 移除fflate依赖,修复TextEncoder兼容性问题 (#217)
* fix(core): 移除fflate依赖,修复TextEncoder兼容性问题

* fix(core): 移除fflate依赖,修复TextEncoder兼容性问题
2025-11-07 12:10:52 +08:00
YHH
e03b106652 refactor(editor): 优化布局管理和行为树文件处理 2025-11-04 23:53:26 +08:00
YHH
f9afa22406 refactor(editor): 重构编辑器架构并增强行为树执行可视化 2025-11-04 18:29:28 +08:00
YHH
adfc7e91b3 Refactor/clean architecture phase1 (#215)
* refactor(editor): 建立Clean Architecture领域模型层

* refactor(editor): 实现应用层架构 - 命令模式、用例和状态管理

* refactor(editor): 实现展示层核心Hooks

* refactor(editor): 实现基础设施层和展示层组件

* refactor(editor): 迁移画布和连接渲染到 Clean Architecture 组件

* feat(editor): 集成应用层架构和命令模式,实现撤销/重做功能

* refactor(editor): UI组件拆分

* refactor(editor): 提取快速创建菜单逻辑

* refactor(editor): 重构BehaviorTreeEditor,提取组件和Hook

* refactor(editor): 提取端口连接和键盘事件Hook

* refactor(editor): 提取拖放处理Hook

* refactor(editor): 提取画布交互Hook和工具函数

* refactor(editor): 完成核心重构

* fix(editor): 修复节点无法创建和连接

* refactor(behavior-tree,editor): 重构节点子节点约束系统,实现元数据驱动的架构
2025-11-03 21:22:16 +08:00
YHH
40cde9c050 fix(editor): 修复行为树删除连接时children数组未同步清理的bug (#214) 2025-11-03 09:57:18 +08:00
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
1797 changed files with 309858 additions and 62912 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

8
.github/codeql/codeql-config.yml vendored Normal file
View File

@@ -0,0 +1,8 @@
name: "CodeQL Config"
# Paths to exclude from analysis
paths-ignore:
- thirdparty
- "**/node_modules"
- "**/dist"
- "**/bin"

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"

View File

@@ -6,8 +6,9 @@ on:
paths:
- 'packages/**'
- 'package.json'
- 'package-lock.json'
- 'pnpm-lock.yaml'
- 'tsconfig.json'
- 'turbo.json'
- 'jest.config.*'
- '.github/workflows/ci.yml'
pull_request:
@@ -15,70 +16,112 @@ on:
paths:
- 'packages/**'
- 'package.json'
- 'package-lock.json'
- 'pnpm-lock.yaml'
- 'tsconfig.json'
- 'turbo.json'
- 'jest.config.*'
- '.github/workflows/ci.yml'
jobs:
test:
ci:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
cache: 'npm'
cache: 'pnpm'
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
# 缓存 Rust 编译结果
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
workspaces: packages/engine
cache-on-failure: true
# 缓存 wasm-pack
- name: Cache wasm-pack
uses: actions/cache@v4
with:
path: ~/.cargo/bin/wasm-pack
key: wasm-pack-${{ runner.os }}
- name: Install wasm-pack
run: |
if ! command -v wasm-pack &> /dev/null; then
cargo install wasm-pack
fi
- name: Install dependencies
run: npm ci
run: pnpm install --no-frozen-lockfile
- name: Build core package first
run: npm run build:core
# 缓存 Turbo
- name: Cache Turbo
uses: actions/cache@v4
with:
path: .turbo
key: turbo-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.sha }}
restore-keys: |
turbo-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
turbo-${{ runner.os }}-
# 构建所有包
- name: Build all packages
run: pnpm run build
- name: Copy WASM files to ecs-engine-bindgen
run: |
mkdir -p packages/ecs-engine-bindgen/src/wasm
cp packages/engine/pkg/es_engine.js packages/ecs-engine-bindgen/src/wasm/
cp packages/engine/pkg/es_engine.d.ts packages/ecs-engine-bindgen/src/wasm/
cp packages/engine/pkg/es_engine_bg.wasm packages/ecs-engine-bindgen/src/wasm/
cp packages/engine/pkg/es_engine_bg.wasm.d.ts packages/ecs-engine-bindgen/src/wasm/
# 类型检查
- name: Type check
run: pnpm run type-check
# Lint 检查
- name: Lint check
run: pnpm run lint
# 测试
- name: Run tests with coverage
run: npm run test:ci
run: pnpm run test:ci
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
continue-on-error: true
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
# 构建 npm 包
- name: Build npm packages
run: pnpm run build:npm
# 上传构建产物
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build-artifacts
path: |
bin/
dist/
retention-days: 7
packages/*/dist/
packages/*/bin/
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`);
}

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

@@ -0,0 +1,51 @@
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: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Run tests with coverage
run: |
cd packages/core
pnpm run test:coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
continue-on-error: true
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./packages/core/coverage/coverage-final.json
flags: core
name: core-coverage
fail_ci_if_error: false
verbose: true
- name: Upload coverage artifact
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: packages/core/coverage/

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

@@ -0,0 +1,42 @@
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
config-file: ./.github/codeql/codeql-config.yml
- name: Autobuild
uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"

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

@@ -0,0 +1,36 @@
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: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
cache: 'pnpm'
- name: Install commitlint
run: |
pnpm add -D @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

View File

@@ -29,26 +29,31 @@ jobs:
with:
fetch-depth: 0
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
cache: 'npm'
cache: 'pnpm'
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Install dependencies
run: npm ci
run: pnpm install
- name: Build core package
run: npm run build:core
run: pnpm run build:core
- name: Generate API documentation
run: npm run docs:api
run: pnpm run docs:api
- name: Build documentation
run: npm run docs:build
run: pnpm run docs:build
- name: Upload artifact
uses: actions/upload-pages-artifact@v3

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>

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

@@ -0,0 +1,159 @@
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: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
cache: 'pnpm'
- 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: pnpm install
- name: Update version in config files (for manual trigger)
if: github.event_name == 'workflow_dispatch'
run: |
cd packages/editor-app
node -e "const pkg=require('./package.json'); pkg.version='${{ github.event.inputs.version }}'; require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2)+'\n')"
node scripts/sync-version.js
- name: Install wasm-pack
run: cargo install wasm-pack
# 使用 Turborepo 自动按依赖顺序构建所有包
# 这会自动处理core -> asset-system -> editor-core -> ui -> 等等
- name: Build all packages with Turborepo
run: pnpm run build
- name: Copy WASM files to ecs-engine-bindgen
shell: bash
run: |
mkdir -p packages/ecs-engine-bindgen/src/wasm
cp packages/engine/pkg/es_engine.js packages/ecs-engine-bindgen/src/wasm/
cp packages/engine/pkg/es_engine.d.ts packages/ecs-engine-bindgen/src/wasm/
cp packages/engine/pkg/es_engine_bg.wasm packages/ecs-engine-bindgen/src/wasm/
cp packages/engine/pkg/es_engine_bg.wasm.d.ts packages/ecs-engine-bindgen/src/wasm/
- name: Bundle runtime files for Tauri
run: |
cd packages/editor-app
node scripts/bundle-runtime.mjs
- 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
node -e "const pkg=require('./package.json'); pkg.version='${{ github.event.inputs.version }}'; require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2)+'\n')"
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

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

@@ -0,0 +1,135 @@
name: Release NPM Packages
on:
workflow_dispatch:
inputs:
package:
description: '选择要发布的包'
required: true
type: choice
options:
- core
- behavior-tree
- editor-core
- node-editor
- blueprint
- tilemap
- physics-rapier2d
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: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
registry-url: 'https://registry.npmjs.org'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Build core package (if needed)
if: ${{ github.event.inputs.package != 'core' && github.event.inputs.package != 'node-editor' }}
run: |
cd packages/core
pnpm run build
- name: Build node-editor package (if needed for blueprint)
if: ${{ github.event.inputs.package == 'blueprint' }}
run: |
cd packages/node-editor
pnpm 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
NEW_VERSION=${{ github.event.inputs.custom_version }}
else
# Get current version and bump it
CURRENT=$(node -p "require('./package.json').version")
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT"
case "${{ github.event.inputs.version_type }}" in
major) NEW_VERSION="$((MAJOR+1)).0.0" ;;
minor) NEW_VERSION="$MAJOR.$((MINOR+1)).0" ;;
patch) NEW_VERSION="$MAJOR.$MINOR.$((PATCH+1))" ;;
esac
fi
# Update package.json using node
node -e "const fs=require('fs'); const pkg=JSON.parse(fs.readFileSync('package.json')); pkg.version='$NEW_VERSION'; fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2)+'\n')"
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
echo "发布版本: $NEW_VERSION"
- name: Build package
run: |
cd packages/${{ github.event.inputs.package }}
pnpm run build:npm
- name: Publish to npm
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
cd packages/${{ github.event.inputs.package }}/dist
pnpm publish --access public --no-git-checks
- 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

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

@@ -0,0 +1,48 @@
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: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Build core package
run: |
cd packages/core
pnpm 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).

17
.gitignore vendored
View File

@@ -16,6 +16,10 @@ dist/
*.tmp
*.temp
.cache/
.build-cache/
# Turborepo
.turbo/
# IDE 配置
.idea/
@@ -48,9 +52,9 @@ logs/
coverage/
*.lcov
# 包管理器锁文件保留npm的,忽略其他的
# 包管理器锁文件(忽略yarn保留pnpm
yarn.lock
pnpm-lock.yaml
package-lock.json
# 文档生成
docs/api/
@@ -69,3 +73,12 @@ docs/.vitepress/dist/
/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/

15
.gitmodules vendored
View File

@@ -4,27 +4,12 @@
[submodule "thirdparty/admin-backend"]
path = thirdparty/admin-backend
url = https://github.com/esengine/admin-backend.git
[submodule "extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension"]
path = extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension
url = https://github.com/esengine/cocos-ecs-extension.git
[submodule "extensions/cocos/cocos-ecs/extensions/behaviour-tree"]
path = extensions/cocos/cocos-ecs/extensions/behaviour-tree
url = https://github.com/esengine/behaviour-tree.git
[submodule "extensions/cocos/cocos-ecs/extensions/cocos-terrain-gen"]
path = extensions/cocos/cocos-ecs/extensions/cocos-terrain-gen
url = https://github.com/esengine/cocos-terrain-gen.git
[submodule "extensions/cocos/cocos-ecs/extensions/mvvm-designer"]
path = extensions/cocos/cocos-ecs/extensions/mvvm-designer
url = https://github.com/esengine/mvvm-designer.git
[submodule "thirdparty/mvvm-ui-framework"]
path = thirdparty/mvvm-ui-framework
url = https://github.com/esengine/mvvm-ui-framework.git
[submodule "thirdparty/cocos-nexus"]
path = thirdparty/cocos-nexus
url = https://github.com/esengine/cocos-nexus.git
[submodule "extensions/cocos/cocos-ecs/extensions/utilityai_designer"]
path = extensions/cocos/cocos-ecs/extensions/utilityai_designer
url = https://github.com/esengine/utilityai_designer.git
[submodule "thirdparty/ecs-astar"]
path = thirdparty/ecs-astar
url = https://github.com/esengine/ecs-astar.git

2
.npmrc Normal file
View File

@@ -0,0 +1,2 @@
link-workspace-packages=true
prefer-workspace-packages=true

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

130
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,130 @@
# 贡献指南 / 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**: 数学库包
- **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.

264
README.md
View File

@@ -1,49 +1,61 @@
# ECS Framework
# ESEngine
[![CI](https://github.com/esengine/ecs-framework/workflows/CI/badge.svg)](https://github.com/esengine/ecs-framework/actions)
[![npm version](https://badge.fury.io/js/%40esengine%2Fecs-framework.svg)](https://badge.fury.io/js/%40esengine%2Fecs-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)
[![GitHub stars](https://img.shields.io/github/stars/esengine/ecs-framework?style=social)](https://github.com/esengine/ecs-framework/stargazers)
**English** | [中文](./README_CN.md)
一个高性能的 TypeScript ECS (Entity-Component-System) 框架,专为现代游戏开发而设计。
**[Documentation](https://esengine.github.io/ecs-framework/) | [API Reference](https://esengine.github.io/ecs-framework/api/) | [Examples](./examples/)**
## 特性
ESEngine is a cross-platform 2D game engine for creating games from a unified interface. It provides a comprehensive set of common tools so that developers can focus on making games without having to reinvent the wheel.
- **高性能** - 针对大规模实体优化支持SoA存储和批量处理
- **多线程计算** - Worker系统支持真正的并行处理充分利用多核CPU性能
- **类型安全** - 完整的TypeScript支持编译时类型检查
- **现代架构** - 支持多World、多Scene的分层架构设计
- **开发友好** - 内置调试工具和性能监控
- **跨平台** - 支持Cocos Creator、Laya引擎和Web平台
Games can be exported to multiple platforms including Web browsers, WeChat Mini Games, and other mini-game platforms.
## 安装
## Free and Open Source
ESEngine is completely free and open source under the MIT license. No strings attached, no royalties. Your games are yours.
## Features
- **Data-Driven Architecture**: Built on Entity-Component-System (ECS) pattern for flexible and performant game logic
- **High-Performance Rendering**: Rust/WebAssembly 2D renderer with sprite batching and WebGL 2.0 backend
- **Visual Editor**: Cross-platform desktop editor with scene management, asset browser, and visual tools
- **Modular Design**: Use only what you need. Each feature is a separate module that can be included independently
- **Multi-Platform**: Deploy to Web, WeChat Mini Games, and more from a single codebase
## Getting the Engine
### Using npm
```bash
npm install @esengine/ecs-framework
```
## 快速开始
### Building from Source
See [Building from Source](#building-from-source) for detailed instructions.
### Editor Download
Pre-built editor binaries are available on the [Releases](https://github.com/esengine/ecs-framework/releases) page for Windows and macOS.
## Quick Start
```typescript
import { Core, Scene, Component, EntitySystem, ECSComponent, ECSSystem, Matcher, Time } from '@esengine/ecs-framework';
import {
Core, Scene, Entity, Component, EntitySystem,
Matcher, Time, ECSComponent, ECSSystem
} from '@esengine/ecs-framework';
// 定义组件
@ECSComponent('Position')
class Position extends Component {
constructor(public x = 0, public y = 0) {
super();
}
x = 0;
y = 0;
}
@ECSComponent('Velocity')
class Velocity extends Component {
constructor(public dx = 0, public dy = 0) {
super();
}
dx = 0;
dy = 0;
}
// 创建系统
@ECSSystem('Movement')
class MovementSystem extends EntitySystem {
constructor() {
@@ -52,76 +64,182 @@ class MovementSystem extends EntitySystem {
protected process(entities: readonly Entity[]): void {
for (const entity of entities) {
const position = entity.getComponent(Position)!;
const velocity = entity.getComponent(Velocity)!;
position.x += velocity.dx * Time.deltaTime;
position.y += velocity.dy * Time.deltaTime;
const pos = entity.getComponent(Position);
const vel = entity.getComponent(Velocity);
pos.x += vel.dx * Time.deltaTime;
pos.y += vel.dy * Time.deltaTime;
}
}
}
// 创建场景并启动
class GameScene extends Scene {
protected initialize(): void {
this.addSystem(new MovementSystem());
const player = this.createEntity("Player");
player.addComponent(new Position(100, 100));
player.addComponent(new Velocity(50, 0));
}
}
// 启动游戏
Core.create();
Core.setScene(new GameScene());
const scene = new Scene();
scene.addSystem(new MovementSystem());
const player = scene.createEntity('Player');
player.addComponent(new Position());
player.addComponent(new Velocity());
Core.setScene(scene);
// Game loop
let lastTime = 0;
function gameLoop(currentTime: number) {
const deltaTime = (currentTime - lastTime) / 1000;
lastTime = currentTime;
// 游戏循环中更新
function gameLoop(deltaTime: number) {
Core.update(deltaTime);
requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);
```
## 核心特性
## Modules
- **实体查询** - 使用 Matcher API 进行高效的实体过滤
- **事件系统** - 类型安全的事件发布/订阅机制
- **性能优化** - SoA 存储优化,支持大规模实体处理
- **多线程支持** - Worker系统实现真正的并行计算充分利用多核CPU
- **多场景** - 支持 World/Scene 分层架构
- **时间管理** - 内置定时器和时间控制系统
ESEngine is organized into modular packages. Each feature has a runtime module and an optional editor extension.
## 平台支持
### Core
支持主流游戏引擎和 Web 平台:
| Package | Description |
|---------|-------------|
| `@esengine/ecs-framework` | Core ECS framework with entity management, component system, and queries |
| `@esengine/math` | Vector, matrix, and mathematical utilities |
| `@esengine/engine` | Rust/WASM 2D renderer |
| `@esengine/engine-core` | Engine module system and lifecycle management |
- **Cocos Creator** - 内置引擎集成支持,提供[专用调试插件](https://store.cocos.com/app/detail/7823)
- **Laya 引擎** - 完整的生命周期管理
- **原生 Web** - 浏览器环境直接运行
- **小游戏平台** - 微信、支付宝等小游戏
### Runtime Modules
| Package | Description |
|---------|-------------|
| `@esengine/sprite` | 2D sprite rendering and animation |
| `@esengine/tilemap` | Tile-based map rendering with animation support |
| `@esengine/physics-rapier2d` | 2D physics simulation powered by Rapier |
| `@esengine/behavior-tree` | Behavior tree AI system |
| `@esengine/blueprint` | Visual scripting runtime |
| `@esengine/camera` | Camera control and management |
| `@esengine/audio` | Audio playback |
| `@esengine/ui` | UI components |
| `@esengine/material-system` | Material and shader system |
| `@esengine/asset-system` | Asset loading and management |
## 示例项目
### Editor Extensions
- [Worker系统演示](https://esengine.github.io/ecs-framework/demos/worker-system/) - 多线程物理系统演示,展示高性能并行计算
- [割草机演示](https://github.com/esengine/lawn-mower-demo) - 完整的游戏示例
| Package | Description |
|---------|-------------|
| `@esengine/sprite-editor` | Sprite inspector and tools |
| `@esengine/tilemap-editor` | Visual tilemap editor with brush tools |
| `@esengine/physics-rapier2d-editor` | Physics collider visualization and editing |
| `@esengine/behavior-tree-editor` | Visual behavior tree editor |
| `@esengine/blueprint-editor` | Visual scripting editor |
| `@esengine/material-editor` | Material and shader editor |
| `@esengine/shader-editor` | Shader code editor |
## 文档
### Platform
- [快速入门](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 文档
| Package | Description |
|---------|-------------|
| `@esengine/platform-common` | Platform abstraction interfaces |
| `@esengine/platform-web` | Web browser runtime |
| `@esengine/platform-wechat` | WeChat Mini Game runtime |
## 生态系统
## Editor
- [路径寻找](https://github.com/esengine/ecs-astar) - A*、BFS、Dijkstra 算法
- [AI 系统](https://github.com/esengine/BehaviourTree-ai) - 行为树、效用 AI
ESEngine Editor is a cross-platform desktop application built with Tauri and React.
## 社区与支持
### Features
- [问题反馈](https://github.com/esengine/ecs-framework/issues) - Bug 报告和功能建议
- [QQ 交流群](https://jq.qq.com/?_wv=1027&k=29w1Nud6) - ecs游戏框架交流
- Scene hierarchy and entity management
- Component inspector with custom editors
- Asset browser with drag-and-drop support
- Tilemap editor with paint, fill, and selection tools
- Behavior tree visual editor
- Blueprint visual scripting
- Material and shader editing
- Built-in performance profiler
- Localization support (English, Chinese)
## 许可证
### Screenshot
[MIT](LICENSE) © 2025 ECS Framework
![ESEngine Editor](screenshots/main_screetshot.png)
## Supported Platforms
| Platform | Runtime | Editor |
|----------|---------|--------|
| Web Browser | Yes | - |
| Windows | - | Yes |
| macOS | - | Yes |
| WeChat Mini Game | In Progress | - |
| Playable Ads | Planned | - |
| Android | Planned | - |
| iOS | Planned | - |
| Windows Native | Planned | - |
| Other Platforms | Planned | - |
## Building from Source
### Prerequisites
- Node.js 18 or later
- pnpm 10 or later
- Rust toolchain (for WASM renderer)
- wasm-pack
### Setup
```bash
# Clone repository
git clone https://github.com/esengine/ecs-framework.git
cd ecs-framework
# Install dependencies
pnpm install
# Build all packages
pnpm build
# Build WASM renderer (optional)
pnpm build:wasm
```
### Running the Editor
```bash
cd packages/editor-app
pnpm tauri:dev
```
### Project Structure
```
ecs-framework/
├── packages/ Engine packages (runtime, editor, platform)
├── docs/ Documentation source
├── examples/ Example projects
├── scripts/ Build utilities
└── thirdparty/ Third-party dependencies
```
## Documentation
- [Getting Started](https://esengine.github.io/ecs-framework/guide/getting-started.html)
- [Architecture Guide](https://esengine.github.io/ecs-framework/guide/)
- [API Reference](https://esengine.github.io/ecs-framework/api/)
## Community
- [GitHub Issues](https://github.com/esengine/ecs-framework/issues) - Bug reports and feature requests
- [GitHub Discussions](https://github.com/esengine/ecs-framework/discussions) - Questions and ideas
## Contributing
Contributions are welcome. Please read the contributing guidelines before submitting a pull request.
1. Fork the repository
2. Create a feature branch
3. Make changes with tests
4. Submit a pull request
## License
ESEngine is licensed under the [MIT License](LICENSE).

246
README_CN.md Normal file
View File

@@ -0,0 +1,246 @@
# ESEngine
[English](./README.md) | **中文**
**[文档](https://esengine.github.io/ecs-framework/) | [API 参考](https://esengine.github.io/ecs-framework/api/) | [示例](./examples/)**
ESEngine 是一个跨平台 2D 游戏引擎,提供统一的开发界面。它包含完整的常用工具集,让开发者专注于游戏创作本身。
游戏可以导出到多个平台,包括 Web 浏览器、微信小游戏等小游戏平台。
## 免费开源
ESEngine 基于 MIT 协议完全免费开源。无附加条件,无版税。你的游戏完全属于你。
## 特性
- **数据驱动架构**:基于 ECS实体-组件-系统)模式构建,提供灵活高效的游戏逻辑
- **高性能渲染**Rust/WebAssembly 2D 渲染器,支持精灵批处理和 WebGL 2.0
- **可视化编辑器**:跨平台桌面编辑器,包含场景管理、资源浏览器和可视化工具
- **模块化设计**:按需使用,每个功能都是独立模块,可单独引入
- **多平台支持**:一套代码部署到 Web、微信小游戏等多个平台
## 获取引擎
### 通过 npm 安装
```bash
npm install @esengine/ecs-framework
```
### 从源码构建
详见 [从源码构建](#从源码构建) 章节。
### 编辑器下载
预编译的编辑器可在 [Releases](https://github.com/esengine/ecs-framework/releases) 页面下载,支持 Windows 和 macOS。
## 快速开始
```typescript
import {
Core, Scene, Entity, Component, EntitySystem,
Matcher, Time, ECSComponent, ECSSystem
} from '@esengine/ecs-framework';
@ECSComponent('Position')
class Position extends Component {
x = 0;
y = 0;
}
@ECSComponent('Velocity')
class Velocity extends Component {
dx = 0;
dy = 0;
}
@ECSSystem('Movement')
class MovementSystem extends EntitySystem {
constructor() {
super(Matcher.all(Position, Velocity));
}
protected process(entities: readonly Entity[]): void {
for (const entity of entities) {
const pos = entity.getComponent(Position);
const vel = entity.getComponent(Velocity);
pos.x += vel.dx * Time.deltaTime;
pos.y += vel.dy * Time.deltaTime;
}
}
}
Core.create();
const scene = new Scene();
scene.addSystem(new MovementSystem());
const player = scene.createEntity('Player');
player.addComponent(new Position());
player.addComponent(new Velocity());
Core.setScene(scene);
// 游戏循环
let lastTime = 0;
function gameLoop(currentTime: number) {
const deltaTime = (currentTime - lastTime) / 1000;
lastTime = currentTime;
Core.update(deltaTime);
requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);
```
## 模块
ESEngine 采用模块化组织。每个功能都有运行时模块和可选的编辑器扩展。
### 核心
| 包名 | 描述 |
|------|------|
| `@esengine/ecs-framework` | ECS 框架核心,包含实体管理、组件系统和查询 |
| `@esengine/math` | 向量、矩阵和数学工具 |
| `@esengine/engine` | Rust/WASM 2D 渲染器 |
| `@esengine/engine-core` | 引擎模块系统和生命周期管理 |
### 运行时模块
| 包名 | 描述 |
|------|------|
| `@esengine/sprite` | 2D 精灵渲染和动画 |
| `@esengine/tilemap` | Tilemap 渲染,支持动画 |
| `@esengine/physics-rapier2d` | 基于 Rapier 的 2D 物理模拟 |
| `@esengine/behavior-tree` | 行为树 AI 系统 |
| `@esengine/blueprint` | 可视化脚本运行时 |
| `@esengine/camera` | 相机控制和管理 |
| `@esengine/audio` | 音频播放 |
| `@esengine/ui` | UI 组件 |
| `@esengine/material-system` | 材质和着色器系统 |
| `@esengine/asset-system` | 资源加载和管理 |
### 编辑器扩展
| 包名 | 描述 |
|------|------|
| `@esengine/sprite-editor` | 精灵检视器和工具 |
| `@esengine/tilemap-editor` | 可视化 Tilemap 编辑器,支持笔刷工具 |
| `@esengine/physics-rapier2d-editor` | 物理碰撞体可视化和编辑 |
| `@esengine/behavior-tree-editor` | 可视化行为树编辑器 |
| `@esengine/blueprint-editor` | 可视化脚本编辑器 |
| `@esengine/material-editor` | 材质和着色器编辑器 |
| `@esengine/shader-editor` | 着色器代码编辑器 |
### 平台
| 包名 | 描述 |
|------|------|
| `@esengine/platform-common` | 平台抽象接口 |
| `@esengine/platform-web` | Web 浏览器运行时 |
| `@esengine/platform-wechat` | 微信小游戏运行时 |
## 编辑器
ESEngine 编辑器是基于 Tauri 和 React 构建的跨平台桌面应用。
### 功能
- 场景层级和实体管理
- 组件检视器,支持自定义编辑器
- 资源浏览器,支持拖放
- Tilemap 编辑器,支持绘制、填充、选择工具
- 行为树可视化编辑器
- 蓝图可视化脚本
- 材质和着色器编辑
- 内置性能分析器
- 多语言支持(英文、中文)
### 截图
![ESEngine Editor](screenshots/main_screetshot.png)
## 支持的平台
| 平台 | 运行时 | 编辑器 |
|------|--------|--------|
| Web 浏览器 | 支持 | - |
| Windows | - | 支持 |
| macOS | - | 支持 |
| 微信小游戏 | 开发中 | - |
| Playable 可玩广告 | 计划中 | - |
| Android | 计划中 | - |
| iOS | 计划中 | - |
| Windows 原生 | 计划中 | - |
| 其他平台 | 计划中 | - |
## 从源码构建
### 前置要求
- Node.js 18 或更高版本
- pnpm 10 或更高版本
- Rust 工具链(用于 WASM 渲染器)
- wasm-pack
### 安装
```bash
# 克隆仓库
git clone https://github.com/esengine/ecs-framework.git
cd ecs-framework
# 安装依赖
pnpm install
# 构建所有包
pnpm build
# 构建 WASM 渲染器(可选)
pnpm build:wasm
```
### 运行编辑器
```bash
cd packages/editor-app
pnpm tauri:dev
```
### 项目结构
```
ecs-framework/
├── packages/ 引擎包(运行时、编辑器、平台)
├── docs/ 文档源码
├── examples/ 示例项目
├── scripts/ 构建工具
└── thirdparty/ 第三方依赖
```
## 文档
- [快速入门](https://esengine.github.io/ecs-framework/guide/getting-started.html)
- [架构指南](https://esengine.github.io/ecs-framework/guide/)
- [API 参考](https://esengine.github.io/ecs-framework/api/)
## 社区
- [GitHub Issues](https://github.com/esengine/ecs-framework/issues) - Bug 反馈和功能建议
- [GitHub Discussions](https://github.com/esengine/ecs-framework/discussions) - 问题和想法
- [QQ 交流群](https://jq.qq.com/?_wv=1027&k=29w1Nud6) - 中文社区
## 贡献
欢迎贡献代码。提交 PR 前请阅读贡献指南。
1. Fork 仓库
2. 创建功能分支
3. 修改代码并测试
4. 提交 PR
## 许可证
ESEngine 基于 [MIT 协议](LICENSE) 开源。

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

View File

@@ -9,6 +9,184 @@ const corePackageJson = JSON.parse(
readFileSync(join(__dirname, '../../packages/core/package.json'), 'utf-8')
)
// Import i18n messages
import en from './i18n/en.json' with { type: 'json' }
import zh from './i18n/zh.json' with { type: 'json' }
// 创建侧边栏配置 | Create sidebar config
// prefix: 路径前缀,如 '' 或 '/en' | Path prefix like '' or '/en'
function createSidebar(t, prefix = '') {
return {
[`${prefix}/guide/`]: [
{
text: t.sidebar.gettingStarted,
items: [
{ text: t.sidebar.quickStart, link: `${prefix}/guide/getting-started` },
{ text: t.sidebar.guideOverview, link: `${prefix}/guide/` }
]
},
{
text: t.sidebar.coreConcepts,
collapsed: false,
items: [
{ text: t.sidebar.entity, link: `${prefix}/guide/entity` },
{ text: t.sidebar.hierarchy, link: `${prefix}/guide/hierarchy` },
{ text: t.sidebar.component, link: `${prefix}/guide/component` },
{ text: t.sidebar.entityQuery, link: `${prefix}/guide/entity-query` },
{
text: t.sidebar.system,
link: `${prefix}/guide/system`,
items: [
{ text: t.sidebar.workerSystem, link: `${prefix}/guide/worker-system` }
]
},
{
text: t.sidebar.scene,
link: `${prefix}/guide/scene`,
items: [
{ text: t.sidebar.sceneManager, link: `${prefix}/guide/scene-manager` },
{ text: t.sidebar.worldManager, link: `${prefix}/guide/world-manager` }
]
},
{
text: t.sidebar.behaviorTree,
link: `${prefix}/guide/behavior-tree/`,
items: [
{ text: t.sidebar.btGettingStarted, link: `${prefix}/guide/behavior-tree/getting-started` },
{ text: t.sidebar.btCoreConcepts, link: `${prefix}/guide/behavior-tree/core-concepts` },
{ text: t.sidebar.btEditorGuide, link: `${prefix}/guide/behavior-tree/editor-guide` },
{ text: t.sidebar.btEditorWorkflow, link: `${prefix}/guide/behavior-tree/editor-workflow` },
{ text: t.sidebar.btCustomActions, link: `${prefix}/guide/behavior-tree/custom-actions` },
{ text: t.sidebar.btCocosIntegration, link: `${prefix}/guide/behavior-tree/cocos-integration` },
{ text: t.sidebar.btLayaIntegration, link: `${prefix}/guide/behavior-tree/laya-integration` },
{ text: t.sidebar.btAdvancedUsage, link: `${prefix}/guide/behavior-tree/advanced-usage` },
{ text: t.sidebar.btBestPractices, link: `${prefix}/guide/behavior-tree/best-practices` }
]
},
{ text: t.sidebar.serialization, link: `${prefix}/guide/serialization` },
{ text: t.sidebar.eventSystem, link: `${prefix}/guide/event-system` },
{ text: t.sidebar.timeAndTimers, link: `${prefix}/guide/time-and-timers` },
{ text: t.sidebar.logging, link: `${prefix}/guide/logging` }
]
},
{
text: t.sidebar.advancedFeatures,
collapsed: false,
items: [
{ text: t.sidebar.serviceContainer, link: `${prefix}/guide/service-container` },
{ text: t.sidebar.pluginSystem, link: `${prefix}/guide/plugin-system` }
]
},
{
text: t.sidebar.platformAdapters,
link: `${prefix}/guide/platform-adapter`,
collapsed: false,
items: [
{ text: t.sidebar.browserAdapter, link: `${prefix}/guide/platform-adapter/browser` },
{ text: t.sidebar.wechatAdapter, link: `${prefix}/guide/platform-adapter/wechat-minigame` },
{ text: t.sidebar.nodejsAdapter, link: `${prefix}/guide/platform-adapter/nodejs` }
]
}
],
[`${prefix}/examples/`]: [
{
text: t.sidebar.examples,
items: [
{ text: t.sidebar.examplesOverview, link: `${prefix}/examples/` },
{ text: t.nav.workerDemo, link: `${prefix}/examples/worker-system-demo` }
]
}
],
[`${prefix}/api/`]: [
{
text: t.sidebar.apiReference,
items: [
{ text: t.sidebar.overview, link: `${prefix}/api/README` },
{
text: t.sidebar.coreClasses,
collapsed: false,
items: [
{ text: 'Core', link: `${prefix}/api/classes/Core` },
{ text: 'Scene', link: `${prefix}/api/classes/Scene` },
{ text: 'World', link: `${prefix}/api/classes/World` },
{ text: 'Entity', link: `${prefix}/api/classes/Entity` },
{ text: 'Component', link: `${prefix}/api/classes/Component` },
{ text: 'EntitySystem', link: `${prefix}/api/classes/EntitySystem` }
]
},
{
text: t.sidebar.systemClasses,
collapsed: true,
items: [
{ text: 'PassiveSystem', link: `${prefix}/api/classes/PassiveSystem` },
{ text: 'ProcessingSystem', link: `${prefix}/api/classes/ProcessingSystem` },
{ text: 'IntervalSystem', link: `${prefix}/api/classes/IntervalSystem` }
]
},
{
text: t.sidebar.utilities,
collapsed: true,
items: [
{ text: 'Matcher', link: `${prefix}/api/classes/Matcher` },
{ text: 'Time', link: `${prefix}/api/classes/Time` },
{ text: 'PerformanceMonitor', link: `${prefix}/api/classes/PerformanceMonitor` },
{ text: 'DebugManager', link: `${prefix}/api/classes/DebugManager` }
]
},
{
text: t.sidebar.interfaces,
collapsed: true,
items: [
{ text: 'IScene', link: `${prefix}/api/interfaces/IScene` },
{ text: 'IComponent', link: `${prefix}/api/interfaces/IComponent` },
{ text: 'ISystemBase', link: `${prefix}/api/interfaces/ISystemBase` },
{ text: 'ICoreConfig', link: `${prefix}/api/interfaces/ICoreConfig` }
]
},
{
text: t.sidebar.decorators,
collapsed: true,
items: [
{ text: '@ECSComponent', link: `${prefix}/api/functions/ECSComponent` },
{ text: '@ECSSystem', link: `${prefix}/api/functions/ECSSystem` }
]
},
{
text: t.sidebar.enums,
collapsed: true,
items: [
{ text: 'ECSEventType', link: `${prefix}/api/enumerations/ECSEventType` },
{ text: 'LogLevel', link: `${prefix}/api/enumerations/LogLevel` }
]
}
]
}
]
}
}
// 创建导航配置 | Create nav config
// prefix: 路径前缀,如 '' 或 '/en' | Path prefix like '' or '/en'
function createNav(t, prefix = '') {
return [
{ text: t.nav.home, link: `${prefix}/` },
{ text: t.nav.quickStart, link: `${prefix}/guide/getting-started` },
{ text: t.nav.guide, link: `${prefix}/guide/` },
{ text: t.nav.api, link: `${prefix}/api/README` },
{
text: t.nav.examples,
items: [
{ text: t.nav.workerDemo, link: `${prefix}/examples/worker-system-demo` },
{ text: t.nav.lawnMowerDemo, link: 'https://github.com/esengine/lawn-mower-demo' }
]
},
{
text: `v${corePackageJson.version}`,
link: 'https://github.com/esengine/ecs-framework/releases'
}
]
}
export default defineConfig({
vite: {
plugins: [
@@ -28,143 +206,49 @@ export default defineConfig({
}
}
},
title: 'ECS Framework',
description: '高性能TypeScript ECS框架 - 为游戏开发而生',
lang: 'zh-CN',
title: 'ESEngine',
appearance: 'force-dark',
locales: {
root: {
label: '简体中文',
lang: 'zh-CN',
description: '高性能 TypeScript ECS 框架 - 为游戏开发而生',
themeConfig: {
nav: createNav(zh, ''),
sidebar: createSidebar(zh, ''),
editLink: {
pattern: 'https://github.com/esengine/ecs-framework/edit/master/docs/:path',
text: zh.common.editOnGithub
},
outline: {
level: [2, 3],
label: zh.common.onThisPage
}
}
},
en: {
label: 'English',
lang: 'en',
link: '/en/',
description: 'High-performance TypeScript ECS Framework for Game Development',
themeConfig: {
nav: createNav(en, '/en'),
sidebar: createSidebar(en, '/en'),
editLink: {
pattern: 'https://github.com/esengine/ecs-framework/edit/master/docs/:path',
text: en.common.editOnGithub
},
outline: {
level: [2, 3],
label: en.common.onThisPage
}
}
}
},
themeConfig: {
nav: [
{ text: '首页', link: '/' },
{ text: '快速开始', link: '/guide/getting-started' },
{ text: '指南', link: '/guide/' },
{ text: 'API', link: '/api/README' },
{
text: '示例',
items: [
{ text: 'Worker系统演示', link: '/examples/worker-system-demo' },
{ text: '割草机演示', link: 'https://github.com/esengine/lawn-mower-demo' }
]
},
{
text: `v${corePackageJson.version}`,
link: 'https://github.com/esengine/ecs-framework/releases'
}
],
sidebar: {
'/guide/': [
{
text: '开始使用',
items: [
{ text: '快速开始', link: '/guide/getting-started' },
{ text: '指南概览', link: '/guide/' }
]
},
{
text: '核心概念',
collapsed: false,
items: [
{ text: '实体类 (Entity)', link: '/guide/entity' },
{ text: '组件系统 (Component)', link: '/guide/component' },
{
text: '系统架构 (System)',
link: '/guide/system',
items: [
{ text: 'Worker系统 (多线程)', link: '/guide/worker-system' }
]
},
{ text: '场景管理 (Scene)', link: '/guide/scene' },
{ text: '事件系统 (Event)', link: '/guide/event-system' },
{ text: '时间和定时器 (Time)', link: '/guide/time-and-timers' },
{ text: '日志系统 (Logger)', link: '/guide/logging' }
]
},
{
text: '平台适配器',
link: '/guide/platform-adapter',
collapsed: false,
items: [
{ text: '浏览器适配器', link: '/guide/platform-adapter/browser' },
{ text: '微信小游戏适配器', link: '/guide/platform-adapter/wechat-minigame' },
{ text: 'Node.js适配器', link: '/guide/platform-adapter/nodejs' }
]
}
],
'/examples/': [
{
text: '示例',
items: [
{ text: '示例概览', link: '/examples/' },
{ text: 'Worker系统演示', link: '/examples/worker-system-demo' }
]
}
],
'/api/': [
{
text: 'API 参考',
items: [
{ text: '概述', link: '/api/README' },
{
text: '核心类',
collapsed: false,
items: [
{ text: 'Core', link: '/api/classes/Core' },
{ text: 'Scene', link: '/api/classes/Scene' },
{ text: 'World', link: '/api/classes/World' },
{ text: 'Entity', link: '/api/classes/Entity' },
{ text: 'Component', link: '/api/classes/Component' },
{ text: 'EntitySystem', link: '/api/classes/EntitySystem' }
]
},
{
text: '系统类',
collapsed: true,
items: [
{ text: 'PassiveSystem', link: '/api/classes/PassiveSystem' },
{ text: 'ProcessingSystem', link: '/api/classes/ProcessingSystem' },
{ text: 'IntervalSystem', link: '/api/classes/IntervalSystem' }
]
},
{
text: '工具类',
collapsed: true,
items: [
{ text: 'Matcher', link: '/api/classes/Matcher' },
{ text: 'Time', link: '/api/classes/Time' },
{ text: 'PerformanceMonitor', link: '/api/classes/PerformanceMonitor' },
{ text: 'DebugManager', link: '/api/classes/DebugManager' }
]
},
{
text: '接口',
collapsed: true,
items: [
{ text: 'IScene', link: '/api/interfaces/IScene' },
{ text: 'IComponent', link: '/api/interfaces/IComponent' },
{ text: 'ISystemBase', link: '/api/interfaces/ISystemBase' },
{ text: 'ICoreConfig', link: '/api/interfaces/ICoreConfig' }
]
},
{
text: '装饰器',
collapsed: true,
items: [
{ text: '@ECSComponent', link: '/api/functions/ECSComponent' },
{ text: '@ECSSystem', link: '/api/functions/ECSSystem' }
]
},
{
text: '枚举',
collapsed: true,
items: [
{ text: 'ECSEventType', link: '/api/enumerations/ECSEventType' },
{ text: 'LogLevel', link: '/api/enumerations/LogLevel' }
]
}
]
}
]
},
siteTitle: 'ESEngine',
socialLinks: [
{ icon: 'github', link: 'https://github.com/esengine/ecs-framework' }
@@ -175,18 +259,8 @@ export default defineConfig({
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: '目录'
}
},
@@ -195,7 +269,7 @@ export default defineConfig({
['link', { rel: 'icon', href: '/favicon.ico' }]
],
base: '/ecs-framework/',
base: '/',
cleanUrls: true,
markdown: {
@@ -205,4 +279,4 @@ export default defineConfig({
dark: 'github-dark'
}
}
})
})

View File

@@ -0,0 +1,85 @@
{
"nav": {
"home": "Home",
"quickStart": "Quick Start",
"guide": "Guide",
"api": "API",
"examples": "Examples",
"workerDemo": "Worker System Demo",
"lawnMowerDemo": "Lawn Mower Demo"
},
"sidebar": {
"gettingStarted": "Getting Started",
"quickStart": "Quick Start",
"guideOverview": "Guide Overview",
"coreConcepts": "Core Concepts",
"entity": "Entity",
"hierarchy": "Hierarchy",
"component": "Component",
"entityQuery": "Entity Query",
"system": "System",
"workerSystem": "Worker System (Multithreading)",
"scene": "Scene",
"sceneManager": "SceneManager",
"worldManager": "WorldManager",
"behaviorTree": "Behavior Tree",
"btGettingStarted": "Getting Started",
"btCoreConcepts": "Core Concepts",
"btEditorGuide": "Editor Guide",
"btEditorWorkflow": "Editor Workflow",
"btCustomActions": "Custom Actions",
"btCocosIntegration": "Cocos Creator Integration",
"btLayaIntegration": "Laya Engine Integration",
"btAdvancedUsage": "Advanced Usage",
"btBestPractices": "Best Practices",
"serialization": "Serialization",
"eventSystem": "Event System",
"timeAndTimers": "Time and Timers",
"logging": "Logging",
"advancedFeatures": "Advanced Features",
"serviceContainer": "Service Container",
"pluginSystem": "Plugin System",
"platformAdapters": "Platform Adapters",
"browserAdapter": "Browser Adapter",
"wechatAdapter": "WeChat Mini Game Adapter",
"nodejsAdapter": "Node.js Adapter",
"examples": "Examples",
"examplesOverview": "Examples Overview",
"apiReference": "API Reference",
"overview": "Overview",
"coreClasses": "Core Classes",
"systemClasses": "System Classes",
"utilities": "Utilities",
"interfaces": "Interfaces",
"decorators": "Decorators",
"enums": "Enums"
},
"home": {
"title": "ESEngine - High-performance TypeScript ECS Framework",
"quickLinks": "Quick Links",
"viewDocs": "View Docs",
"getStarted": "Get Started",
"getStartedDesc": "From installation to your first ECS app, learn the core concepts in 5 minutes.",
"aiSystem": "AI System",
"behaviorTreeEditor": "Visual Behavior Tree Editor",
"behaviorTreeDesc": "Built-in AI behavior tree system with visual editing and real-time debugging.",
"coreFeatures": "Core Features",
"ecsArchitecture": "High-performance ECS Architecture",
"ecsArchitectureDesc": "Data-driven entity component system for large-scale entity processing with cache-friendly memory layout.",
"typeSupport": "Full Type Support",
"typeSupportDesc": "100% TypeScript with complete type definitions and compile-time checking for the best development experience.",
"visualBehaviorTree": "Visual Behavior Tree",
"visualBehaviorTreeDesc": "Built-in AI behavior tree system with visual editor, custom nodes, and real-time debugging.",
"multiPlatform": "Multi-Platform Support",
"multiPlatformDesc": "Support for browsers, Node.js, WeChat Mini Games, and seamless integration with major game engines.",
"modularDesign": "Modular Design",
"modularDesignDesc": "Core features packaged independently, import only what you need. Support for custom plugin extensions.",
"devTools": "Developer Tools",
"devToolsDesc": "Built-in performance monitoring, debugging tools, serialization system, and complete development toolchain.",
"learnMore": "Learn more →"
},
"common": {
"editOnGithub": "Edit this page on GitHub",
"onThisPage": "On this page"
}
}

View File

@@ -0,0 +1,21 @@
import en from './en.json'
import zh from './zh.json'
export const messages = { en, zh }
export type Locale = 'en' | 'zh'
export function getLocaleMessages(locale: Locale) {
return messages[locale] || messages.en
}
// Helper to get nested key value
export function t(messages: typeof en, key: string): string {
const keys = key.split('.')
let result: any = messages
for (const k of keys) {
result = result?.[k]
if (result === undefined) return key
}
return result
}

View File

@@ -0,0 +1,85 @@
{
"nav": {
"home": "首页",
"quickStart": "快速开始",
"guide": "指南",
"api": "API",
"examples": "示例",
"workerDemo": "Worker系统演示",
"lawnMowerDemo": "割草机演示"
},
"sidebar": {
"gettingStarted": "开始使用",
"quickStart": "快速开始",
"guideOverview": "指南概览",
"coreConcepts": "核心概念",
"entity": "实体类 (Entity)",
"hierarchy": "层级系统 (Hierarchy)",
"component": "组件系统 (Component)",
"entityQuery": "实体查询系统",
"system": "系统架构 (System)",
"workerSystem": "Worker系统 (多线程)",
"scene": "场景管理 (Scene)",
"sceneManager": "SceneManager",
"worldManager": "WorldManager",
"behaviorTree": "行为树系统 (Behavior Tree)",
"btGettingStarted": "快速开始",
"btCoreConcepts": "核心概念",
"btEditorGuide": "编辑器指南",
"btEditorWorkflow": "编辑器工作流",
"btCustomActions": "自定义动作组件",
"btCocosIntegration": "Cocos Creator集成",
"btLayaIntegration": "Laya引擎集成",
"btAdvancedUsage": "高级用法",
"btBestPractices": "最佳实践",
"serialization": "序列化系统 (Serialization)",
"eventSystem": "事件系统 (Event)",
"timeAndTimers": "时间和定时器 (Time)",
"logging": "日志系统 (Logger)",
"advancedFeatures": "高级特性",
"serviceContainer": "服务容器 (Service Container)",
"pluginSystem": "插件系统 (Plugin System)",
"platformAdapters": "平台适配器",
"browserAdapter": "浏览器适配器",
"wechatAdapter": "微信小游戏适配器",
"nodejsAdapter": "Node.js适配器",
"examples": "示例",
"examplesOverview": "示例概览",
"apiReference": "API 参考",
"overview": "概述",
"coreClasses": "核心类",
"systemClasses": "系统类",
"utilities": "工具类",
"interfaces": "接口",
"decorators": "装饰器",
"enums": "枚举"
},
"home": {
"title": "ESEngine - 高性能 TypeScript ECS 框架",
"quickLinks": "快速入口",
"viewDocs": "查看文档",
"getStarted": "快速开始",
"getStartedDesc": "从安装到创建第一个 ECS 应用,快速了解核心概念。",
"aiSystem": "AI 系统",
"behaviorTreeEditor": "行为树可视化编辑器",
"behaviorTreeDesc": "内置 AI 行为树系统,支持可视化编辑和实时调试。",
"coreFeatures": "核心特性",
"ecsArchitecture": "高性能 ECS 架构",
"ecsArchitectureDesc": "基于数据驱动的实体组件系统,支持大规模实体处理,缓存友好的内存布局。",
"typeSupport": "完整类型支持",
"typeSupportDesc": "100% TypeScript 编写,完整的类型定义和编译时检查,提供最佳的开发体验。",
"visualBehaviorTree": "可视化行为树",
"visualBehaviorTreeDesc": "内置 AI 行为树系统,提供可视化编辑器,支持自定义节点和实时调试。",
"multiPlatform": "多平台支持",
"multiPlatformDesc": "支持浏览器、Node.js、微信小游戏等多平台可与主流游戏引擎无缝集成。",
"modularDesign": "模块化设计",
"modularDesignDesc": "核心功能独立打包,按需引入。支持自定义插件扩展,灵活适配不同项目。",
"devTools": "开发者工具",
"devToolsDesc": "内置性能监控、调试工具、序列化系统等,提供完整的开发工具链。",
"learnMore": "了解更多 →"
},
"common": {
"editOnGithub": "在 GitHub 上编辑此页",
"onThisPage": "在这个页面上"
}
}

View File

@@ -0,0 +1,93 @@
<script setup>
defineProps({
title: String,
description: String,
icon: String,
link: String,
image: String
})
</script>
<template>
<a :href="link" class="feature-card">
<div class="card-image" v-if="image">
<img :src="image" :alt="title" />
</div>
<div class="card-body">
<div class="card-icon" v-if="icon && !image">{{ icon }}</div>
<h3 class="card-title">{{ title }}</h3>
<p class="card-description">{{ description }}</p>
</div>
</a>
</template>
<style scoped>
.feature-card {
display: flex;
flex-direction: column;
background: var(--es-bg-elevated, #252526);
border: 1px solid var(--es-border-default, #3e3e42);
border-radius: 4px;
overflow: hidden;
text-decoration: none;
transition: all 0.15s ease;
}
.feature-card:hover {
border-color: var(--es-primary, #007acc);
background: var(--es-bg-overlay, #2d2d2d);
}
.card-image {
width: 100%;
height: 160px;
overflow: hidden;
background: linear-gradient(135deg, #1e3a5f 0%, #1e1e1e 100%);
}
.card-image img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.feature-card:hover .card-image img {
transform: scale(1.05);
}
.card-body {
padding: 16px;
flex: 1;
display: flex;
flex-direction: column;
}
.card-icon {
font-size: 1.5rem;
margin-bottom: 12px;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: var(--es-bg-input, #3c3c3c);
border-radius: 4px;
}
.card-title {
font-size: 14px;
font-weight: 600;
color: var(--es-text-inverse, #ffffff);
margin: 0 0 8px 0;
line-height: 1.3;
}
.card-description {
font-size: 12px;
color: var(--es-text-secondary, #9d9d9d);
margin: 0;
line-height: 1.6;
flex: 1;
}
</style>

View File

@@ -0,0 +1,422 @@
<script setup>
import { onMounted, onUnmounted, ref } from 'vue'
const canvasRef = ref(null)
let animationId = null
let particles = []
let animationStartTime = null
let glowStartTime = null
// ESEngine 粒子颜色 - VS Code 风格配色(与编辑器统一)
const colors = ['#569CD6', '#4EC9B0', '#9CDCFE', '#C586C0', '#DCDCAA']
class Particle {
constructor(x, y, targetX, targetY) {
this.x = x
this.y = y
this.targetX = targetX
this.targetY = targetY
this.size = Math.random() * 2 + 1.5
this.alpha = Math.random() * 0.5 + 0.5
this.color = colors[Math.floor(Math.random() * colors.length)]
}
}
function createParticles(canvas, text, fontSize) {
const tempCanvas = document.createElement('canvas')
const tempCtx = tempCanvas.getContext('2d')
if (!tempCtx) return []
tempCtx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
const textMetrics = tempCtx.measureText(text)
const textWidth = textMetrics.width
const textHeight = fontSize
tempCanvas.width = textWidth + 40
tempCanvas.height = textHeight + 40
tempCtx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
tempCtx.textAlign = 'center'
tempCtx.textBaseline = 'middle'
tempCtx.fillStyle = '#ffffff'
tempCtx.fillText(text, tempCanvas.width / 2, tempCanvas.height / 2)
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height)
const pixels = imageData.data
const newParticles = []
const gap = 3
const width = canvas.width / (window.devicePixelRatio || 1)
const height = canvas.height / (window.devicePixelRatio || 1)
const offsetX = (width - tempCanvas.width) / 2
const offsetY = (height - tempCanvas.height) / 2
for (let y = 0; y < tempCanvas.height; y += gap) {
for (let x = 0; x < tempCanvas.width; x += gap) {
const index = (y * tempCanvas.width + x) * 4
const alpha = pixels[index + 3] || 0
if (alpha > 128) {
const angle = Math.random() * Math.PI * 2
const distance = Math.random() * Math.max(width, height)
newParticles.push(new Particle(
width / 2 + Math.cos(angle) * distance,
height / 2 + Math.sin(angle) * distance,
offsetX + x,
offsetY + y
))
}
}
}
return newParticles
}
function easeOutQuart(t) {
return 1 - Math.pow(1 - t, 4)
}
function easeOutCubic(t) {
return 1 - Math.pow(1 - t, 3)
}
function animate(canvas, ctx) {
const dpr = window.devicePixelRatio || 1
const width = canvas.width / dpr
const height = canvas.height / dpr
const currentTime = performance.now()
const duration = 2500
const glowDuration = 600
const elapsed = currentTime - animationStartTime
const progress = Math.min(elapsed / duration, 1)
const easedProgress = easeOutQuart(progress)
// 透明背景
ctx.clearRect(0, 0, width, height)
// 计算发光进度
let glowProgress = 0
if (progress >= 1) {
if (glowStartTime === null) {
glowStartTime = currentTime
}
glowProgress = Math.min((currentTime - glowStartTime) / glowDuration, 1)
glowProgress = easeOutCubic(glowProgress)
}
const text = 'ESEngine'
const fontSize = Math.min(width / 4, height / 3, 80)
const textY = height / 2
for (const particle of particles) {
const moveProgress = Math.min(easedProgress * 1.2, 1)
const currentX = particle.x + (particle.targetX - particle.x) * moveProgress
const currentY = particle.y + (particle.targetY - particle.y) * moveProgress
ctx.beginPath()
ctx.arc(currentX, currentY, particle.size, 0, Math.PI * 2)
ctx.fillStyle = particle.color
ctx.globalAlpha = particle.alpha * (1 - glowProgress * 0.3)
ctx.fill()
}
ctx.globalAlpha = 1
if (glowProgress > 0) {
ctx.save()
ctx.shadowColor = '#3b9eff'
ctx.shadowBlur = 30 * glowProgress
ctx.fillStyle = `rgba(255, 255, 255, ${glowProgress})`
ctx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillText(text, width / 2, textY)
ctx.restore()
}
if (glowProgress >= 1) {
const breathe = 0.8 + Math.sin(currentTime / 1000) * 0.2
ctx.save()
ctx.shadowColor = '#3b9eff'
ctx.shadowBlur = 20 * breathe
ctx.fillStyle = '#ffffff'
ctx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillText(text, width / 2, textY)
ctx.restore()
}
animationId = requestAnimationFrame(() => animate(canvas, ctx))
}
function initCanvas() {
const canvas = canvasRef.value
if (!canvas) return
const ctx = canvas.getContext('2d')
if (!ctx) return
const dpr = window.devicePixelRatio || 1
const container = canvas.parentElement
const width = container.offsetWidth
const height = container.offsetHeight
canvas.width = width * dpr
canvas.height = height * dpr
canvas.style.width = `${width}px`
canvas.style.height = `${height}px`
ctx.scale(dpr, dpr)
const text = 'ESEngine'
const fontSize = Math.min(width / 4, height / 3, 80)
particles = createParticles(canvas, text, fontSize)
animationStartTime = performance.now()
glowStartTime = null
if (animationId) {
cancelAnimationFrame(animationId)
}
animate(canvas, ctx)
}
onMounted(() => {
initCanvas()
window.addEventListener('resize', initCanvas)
})
onUnmounted(() => {
if (animationId) {
cancelAnimationFrame(animationId)
}
window.removeEventListener('resize', initCanvas)
})
</script>
<template>
<section class="hero-section">
<div class="hero-container">
<!-- 左侧文字区域 -->
<div class="hero-text">
<div class="hero-logo">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="16" cy="16" r="14" stroke="#9147ff" stroke-width="2"/>
<path d="M10 10h8v2h-6v3h5v2h-5v3h6v2h-8v-12z" fill="#9147ff"/>
</svg>
<span>ESENGINE</span>
</div>
<h1 class="hero-title">
我们构建框架<br/>
而你将创造游戏
</h1>
<p class="hero-description">
ESEngine 是一个高性能的 TypeScript ECS 框架为游戏开发者提供现代化的实体组件系统
无论是 2D 还是 3D 游戏都能帮助你快速构建可扩展的游戏架构
</p>
<div class="hero-actions">
<a href="/guide/getting-started" class="btn-primary">开始使用</a>
<a href="https://github.com/esengine/ecs-framework" class="btn-secondary" target="_blank">了解更多</a>
</div>
</div>
<!-- 右侧粒子动画区域 -->
<div class="hero-visual">
<div class="visual-container">
<canvas ref="canvasRef" class="particle-canvas"></canvas>
<div class="visual-label">
<span class="label-title">Entity Component System</span>
<span class="label-subtitle">High Performance Framework</span>
</div>
</div>
</div>
</div>
</section>
</template>
<style scoped>
.hero-section {
background: #0d0d0d;
padding: 80px 0;
min-height: calc(100vh - 64px);
display: flex;
align-items: center;
}
.hero-container {
max-width: 1400px;
margin: 0 auto;
padding: 0 48px;
display: grid;
grid-template-columns: 1fr 1.2fr;
gap: 64px;
align-items: center;
}
/* 左侧文字 */
.hero-text {
display: flex;
flex-direction: column;
gap: 24px;
}
.hero-logo {
display: flex;
align-items: center;
gap: 12px;
color: #ffffff;
font-size: 1rem;
font-weight: 700;
letter-spacing: 0.1em;
}
.hero-title {
font-size: 3rem;
font-weight: 700;
color: #ffffff;
line-height: 1.2;
margin: 0;
}
.hero-description {
font-size: 1.125rem;
color: #707070;
line-height: 1.7;
margin: 0;
max-width: 480px;
}
.hero-actions {
display: flex;
gap: 16px;
margin-top: 8px;
}
.btn-primary,
.btn-secondary {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 14px 28px;
border-radius: 4px;
font-weight: 600;
font-size: 0.9375rem;
text-decoration: none;
transition: all 0.2s ease;
}
.btn-primary {
background: #3b9eff;
color: #ffffff;
border: 1px solid #3b9eff;
border-radius: 6px;
}
.btn-primary:hover {
background: #5aadff;
border-color: #5aadff;
}
.btn-secondary {
background: #1a1a1a;
color: #a0a0a0;
border: 1px solid #2a2a2a;
border-radius: 6px;
}
.btn-secondary:hover {
background: #252525;
color: #ffffff;
}
.hero-visual {
display: flex;
justify-content: center;
}
.visual-container {
position: relative;
width: 100%;
max-width: 600px;
aspect-ratio: 4 / 3;
background: linear-gradient(135deg, #1a2a3a 0%, #1a1a1a 50%, #0d0d0d 100%);
border-radius: 12px;
border: 1px solid #2a2a2a;
overflow: hidden;
box-shadow: 0 20px 60px rgba(59, 158, 255, 0.1);
}
.particle-canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.visual-label {
position: absolute;
bottom: 24px;
left: 24px;
display: flex;
flex-direction: column;
gap: 4px;
}
.label-title {
font-size: 1.125rem;
font-weight: 600;
color: #ffffff;
}
.label-subtitle {
font-size: 0.875rem;
color: #737373;
}
/* 响应式 */
@media (max-width: 1024px) {
.hero-container {
grid-template-columns: 1fr;
gap: 48px;
padding: 0 24px;
}
.hero-section {
padding: 48px 0;
min-height: auto;
}
.hero-title {
font-size: 2.25rem;
}
.hero-description {
font-size: 1rem;
}
.visual-container {
max-width: 100%;
aspect-ratio: 16 / 9;
}
}
@media (max-width: 640px) {
.hero-title {
font-size: 1.75rem;
}
.hero-actions {
flex-direction: column;
}
.btn-primary,
.btn-secondary {
width: 100%;
justify-content: center;
}
}
</style>

View File

@@ -0,0 +1,422 @@
<script setup>
import { onMounted, onUnmounted, ref } from 'vue'
const canvasRef = ref(null)
let animationId = null
let particles = []
let animationStartTime = null
let glowStartTime = null
// ESEngine particle colors - VS Code style colors (unified with editor)
const colors = ['#569CD6', '#4EC9B0', '#9CDCFE', '#C586C0', '#DCDCAA']
class Particle {
constructor(x, y, targetX, targetY) {
this.x = x
this.y = y
this.targetX = targetX
this.targetY = targetY
this.size = Math.random() * 2 + 1.5
this.alpha = Math.random() * 0.5 + 0.5
this.color = colors[Math.floor(Math.random() * colors.length)]
}
}
function createParticles(canvas, text, fontSize) {
const tempCanvas = document.createElement('canvas')
const tempCtx = tempCanvas.getContext('2d')
if (!tempCtx) return []
tempCtx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
const textMetrics = tempCtx.measureText(text)
const textWidth = textMetrics.width
const textHeight = fontSize
tempCanvas.width = textWidth + 40
tempCanvas.height = textHeight + 40
tempCtx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
tempCtx.textAlign = 'center'
tempCtx.textBaseline = 'middle'
tempCtx.fillStyle = '#ffffff'
tempCtx.fillText(text, tempCanvas.width / 2, tempCanvas.height / 2)
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height)
const pixels = imageData.data
const newParticles = []
const gap = 3
const width = canvas.width / (window.devicePixelRatio || 1)
const height = canvas.height / (window.devicePixelRatio || 1)
const offsetX = (width - tempCanvas.width) / 2
const offsetY = (height - tempCanvas.height) / 2
for (let y = 0; y < tempCanvas.height; y += gap) {
for (let x = 0; x < tempCanvas.width; x += gap) {
const index = (y * tempCanvas.width + x) * 4
const alpha = pixels[index + 3] || 0
if (alpha > 128) {
const angle = Math.random() * Math.PI * 2
const distance = Math.random() * Math.max(width, height)
newParticles.push(new Particle(
width / 2 + Math.cos(angle) * distance,
height / 2 + Math.sin(angle) * distance,
offsetX + x,
offsetY + y
))
}
}
}
return newParticles
}
function easeOutQuart(t) {
return 1 - Math.pow(1 - t, 4)
}
function easeOutCubic(t) {
return 1 - Math.pow(1 - t, 3)
}
function animate(canvas, ctx) {
const dpr = window.devicePixelRatio || 1
const width = canvas.width / dpr
const height = canvas.height / dpr
const currentTime = performance.now()
const duration = 2500
const glowDuration = 600
const elapsed = currentTime - animationStartTime
const progress = Math.min(elapsed / duration, 1)
const easedProgress = easeOutQuart(progress)
// Transparent background
ctx.clearRect(0, 0, width, height)
// Calculate glow progress
let glowProgress = 0
if (progress >= 1) {
if (glowStartTime === null) {
glowStartTime = currentTime
}
glowProgress = Math.min((currentTime - glowStartTime) / glowDuration, 1)
glowProgress = easeOutCubic(glowProgress)
}
const text = 'ESEngine'
const fontSize = Math.min(width / 4, height / 3, 80)
const textY = height / 2
for (const particle of particles) {
const moveProgress = Math.min(easedProgress * 1.2, 1)
const currentX = particle.x + (particle.targetX - particle.x) * moveProgress
const currentY = particle.y + (particle.targetY - particle.y) * moveProgress
ctx.beginPath()
ctx.arc(currentX, currentY, particle.size, 0, Math.PI * 2)
ctx.fillStyle = particle.color
ctx.globalAlpha = particle.alpha * (1 - glowProgress * 0.3)
ctx.fill()
}
ctx.globalAlpha = 1
if (glowProgress > 0) {
ctx.save()
ctx.shadowColor = '#3b9eff'
ctx.shadowBlur = 30 * glowProgress
ctx.fillStyle = `rgba(255, 255, 255, ${glowProgress})`
ctx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillText(text, width / 2, textY)
ctx.restore()
}
if (glowProgress >= 1) {
const breathe = 0.8 + Math.sin(currentTime / 1000) * 0.2
ctx.save()
ctx.shadowColor = '#3b9eff'
ctx.shadowBlur = 20 * breathe
ctx.fillStyle = '#ffffff'
ctx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillText(text, width / 2, textY)
ctx.restore()
}
animationId = requestAnimationFrame(() => animate(canvas, ctx))
}
function initCanvas() {
const canvas = canvasRef.value
if (!canvas) return
const ctx = canvas.getContext('2d')
if (!ctx) return
const dpr = window.devicePixelRatio || 1
const container = canvas.parentElement
const width = container.offsetWidth
const height = container.offsetHeight
canvas.width = width * dpr
canvas.height = height * dpr
canvas.style.width = `${width}px`
canvas.style.height = `${height}px`
ctx.scale(dpr, dpr)
const text = 'ESEngine'
const fontSize = Math.min(width / 4, height / 3, 80)
particles = createParticles(canvas, text, fontSize)
animationStartTime = performance.now()
glowStartTime = null
if (animationId) {
cancelAnimationFrame(animationId)
}
animate(canvas, ctx)
}
onMounted(() => {
initCanvas()
window.addEventListener('resize', initCanvas)
})
onUnmounted(() => {
if (animationId) {
cancelAnimationFrame(animationId)
}
window.removeEventListener('resize', initCanvas)
})
</script>
<template>
<section class="hero-section">
<div class="hero-container">
<!-- Left text area -->
<div class="hero-text">
<div class="hero-logo">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="16" cy="16" r="14" stroke="#9147ff" stroke-width="2"/>
<path d="M10 10h8v2h-6v3h5v2h-5v3h6v2h-8v-12z" fill="#9147ff"/>
</svg>
<span>ESENGINE</span>
</div>
<h1 class="hero-title">
We build the framework.<br/>
You create the game.
</h1>
<p class="hero-description">
ESEngine is a high-performance TypeScript ECS framework for game developers.
Whether 2D or 3D games, it helps you build scalable game architecture quickly.
</p>
<div class="hero-actions">
<a href="/en/guide/getting-started" class="btn-primary">Get Started</a>
<a href="https://github.com/esengine/ecs-framework" class="btn-secondary" target="_blank">Learn More</a>
</div>
</div>
<!-- Right particle animation area -->
<div class="hero-visual">
<div class="visual-container">
<canvas ref="canvasRef" class="particle-canvas"></canvas>
<div class="visual-label">
<span class="label-title">Entity Component System</span>
<span class="label-subtitle">High Performance Framework</span>
</div>
</div>
</div>
</div>
</section>
</template>
<style scoped>
.hero-section {
background: #0d0d0d;
padding: 80px 0;
min-height: calc(100vh - 64px);
display: flex;
align-items: center;
}
.hero-container {
max-width: 1400px;
margin: 0 auto;
padding: 0 48px;
display: grid;
grid-template-columns: 1fr 1.2fr;
gap: 64px;
align-items: center;
}
/* Left text */
.hero-text {
display: flex;
flex-direction: column;
gap: 24px;
}
.hero-logo {
display: flex;
align-items: center;
gap: 12px;
color: #ffffff;
font-size: 1rem;
font-weight: 700;
letter-spacing: 0.1em;
}
.hero-title {
font-size: 3rem;
font-weight: 700;
color: #ffffff;
line-height: 1.2;
margin: 0;
}
.hero-description {
font-size: 1.125rem;
color: #707070;
line-height: 1.7;
margin: 0;
max-width: 480px;
}
.hero-actions {
display: flex;
gap: 16px;
margin-top: 8px;
}
.btn-primary,
.btn-secondary {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 14px 28px;
border-radius: 4px;
font-weight: 600;
font-size: 0.9375rem;
text-decoration: none;
transition: all 0.2s ease;
}
.btn-primary {
background: #3b9eff;
color: #ffffff;
border: 1px solid #3b9eff;
border-radius: 6px;
}
.btn-primary:hover {
background: #5aadff;
border-color: #5aadff;
}
.btn-secondary {
background: #1a1a1a;
color: #a0a0a0;
border: 1px solid #2a2a2a;
border-radius: 6px;
}
.btn-secondary:hover {
background: #252525;
color: #ffffff;
}
.hero-visual {
display: flex;
justify-content: center;
}
.visual-container {
position: relative;
width: 100%;
max-width: 600px;
aspect-ratio: 4 / 3;
background: linear-gradient(135deg, #1a2a3a 0%, #1a1a1a 50%, #0d0d0d 100%);
border-radius: 12px;
border: 1px solid #2a2a2a;
overflow: hidden;
box-shadow: 0 20px 60px rgba(59, 158, 255, 0.1);
}
.particle-canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.visual-label {
position: absolute;
bottom: 24px;
left: 24px;
display: flex;
flex-direction: column;
gap: 4px;
}
.label-title {
font-size: 1.125rem;
font-weight: 600;
color: #ffffff;
}
.label-subtitle {
font-size: 0.875rem;
color: #737373;
}
/* Responsive */
@media (max-width: 1024px) {
.hero-container {
grid-template-columns: 1fr;
gap: 48px;
padding: 0 24px;
}
.hero-section {
padding: 48px 0;
min-height: auto;
}
.hero-title {
font-size: 2.25rem;
}
.hero-description {
font-size: 1rem;
}
.visual-container {
max-width: 100%;
aspect-ratio: 16 / 9;
}
}
@media (max-width: 640px) {
.hero-title {
font-size: 1.75rem;
}
.hero-actions {
flex-direction: column;
}
.btn-primary,
.btn-secondary {
width: 100%;
justify-content: center;
}
}
</style>

View File

@@ -0,0 +1,594 @@
:root {
color-scheme: dark;
--vp-nav-height: 64px;
--es-bg-base: #1e1e1e;
--es-bg-elevated: #252526;
--es-bg-overlay: #2d2d2d;
--es-bg-input: #3c3c3c;
--es-bg-inset: #181818;
--es-bg-hover: #2a2d2e;
--es-bg-active: #37373d;
--es-bg-sidebar: #262626;
--es-bg-card: #2a2a2a;
--es-bg-header: #2d2d2d;
--es-text-primary: #cccccc;
--es-text-secondary: #9d9d9d;
--es-text-tertiary: #6a6a6a;
--es-text-inverse: #ffffff;
--es-text-muted: #aaaaaa;
--es-text-dim: #6a6a6a;
--es-font-xs: 11px;
--es-font-sm: 12px;
--es-font-base: 13px;
--es-font-md: 14px;
--es-font-lg: 16px;
--es-border-default: #3a3a3a;
--es-border-subtle: #1a1a1a;
--es-border-strong: #4a4a4a;
--es-primary: #3b82f6;
--es-primary-hover: #2563eb;
--es-success: #4ade80;
--es-warning: #f59e0b;
--es-error: #ef4444;
--es-info: #3b82f6;
--es-selected: #3d5a80;
--es-selected-hover: #4a6a90;
}
body {
background: var(--es-bg-base) !important;
}
html,
html.dark {
--vp-c-bg: var(--es-bg-base);
--vp-c-bg-soft: var(--es-bg-elevated);
--vp-c-bg-mute: var(--es-bg-overlay);
--vp-c-bg-alt: var(--es-bg-sidebar);
--vp-c-text-1: var(--es-text-primary);
--vp-c-text-2: var(--es-text-tertiary);
--vp-c-text-3: var(--es-text-muted);
--vp-c-divider: var(--es-border-default);
--vp-c-divider-light: var(--es-border-subtle);
}
html:not(.dark) {
--vp-c-bg: var(--es-bg-base) !important;
--vp-c-bg-soft: var(--es-bg-elevated) !important;
--vp-c-bg-mute: var(--es-bg-overlay) !important;
--vp-c-bg-alt: var(--es-bg-sidebar) !important;
--vp-c-text-1: var(--es-text-primary) !important;
--vp-c-text-2: var(--es-text-tertiary) !important;
--vp-c-text-3: var(--es-text-muted) !important;
}
.VPNav {
background: var(--es-bg-header) !important;
border-bottom: 1px solid var(--es-border-subtle) !important;
}
.VPNav .VPNavBar {
background: var(--es-bg-header) !important;
}
.VPNav .VPNavBar .wrapper {
background: var(--es-bg-header) !important;
}
.VPNav .VPNavBar::before,
.VPNav .VPNavBar::after {
display: none !important;
}
.VPNavBar {
background: var(--es-bg-header) !important;
}
.VPNavBar::before {
display: none !important;
}
.VPNavBarTitle .title {
color: var(--es-text-primary);
font-weight: 500;
font-size: var(--es-font-base);
}
.VPNavBarMenuLink {
color: var(--es-text-secondary) !important;
font-size: var(--es-font-sm) !important;
font-weight: 400 !important;
}
.VPNavBarMenuLink:hover {
color: var(--es-text-primary) !important;
}
.VPNavBarMenuLink.active {
color: var(--es-text-primary) !important;
}
.VPNavBarSearch .DocSearch-Button {
background: var(--es-bg-input) !important;
border: 1px solid var(--es-border-default) !important;
border-radius: 2px;
height: 26px;
}
.VPSidebar {
background: var(--es-bg-sidebar) !important;
border-right: 1px solid var(--es-border-subtle) !important;
}
.VPSidebarItem.level-0 > .item {
padding: 8px 0 4px 0;
}
.VPSidebarItem.level-0 > .item > .text {
font-weight: 600;
font-size: var(--es-font-xs);
color: var(--es-text-secondary);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.VPSidebarItem .link {
padding: 4px 8px;
margin: 1px 0;
border-radius: 2px;
color: var(--es-text-primary);
font-size: var(--es-font-sm);
transition: all 0.1s ease;
border-left: 2px solid transparent;
}
.VPSidebarItem .link:hover {
background: rgba(255, 255, 255, 0.03);
color: var(--es-text-inverse);
}
.VPSidebarItem.is-active > .item > .link {
background: var(--es-selected);
color: var(--es-text-inverse);
border-left: 2px solid var(--es-primary);
}
.VPSidebarItem.is-active > .item > .link:hover {
background: var(--es-selected-hover);
}
.VPSidebarItem.level-1 .link {
padding-left: 20px;
font-size: var(--es-font-sm);
}
.VPSidebarItem.level-2 .link {
padding-left: 32px;
font-size: var(--es-font-sm);
}
.VPSidebarItem .caret {
color: var(--es-text-secondary);
}
.VPSidebarItem .caret:hover {
color: var(--es-text-primary);
}
.VPContent {
background: var(--es-bg-card) !important;
padding-top: 0 !important;
}
.VPContent.has-sidebar {
background: var(--es-bg-card) !important;
}
/* 首页布局修复 | Home page layout fix */
.VPPage {
padding-top: 0 !important;
}
.Layout > .VPContent {
padding-top: var(--vp-nav-height) !important;
}
.VPDoc {
background: transparent !important;
}
.VPNavBar .content {
background: var(--es-bg-header) !important;
}
.VPNavBar .content-body {
background: var(--es-bg-header) !important;
}
.VPNavBar .divider {
display: none;
}
.VPLocalNav {
background: var(--es-bg-header) !important;
border-bottom: 1px solid var(--es-border-subtle) !important;
}
.VPNavScreenMenu {
background: var(--es-bg-base) !important;
}
.VPNavScreen {
background: var(--es-bg-base) !important;
}
.curtain {
display: none !important;
}
.VPNav .curtain,
.VPNavBar .curtain {
display: none !important;
}
[class*="curtain"] {
display: none !important;
}
.VPNav > div::before,
.VPNav > div::after {
display: none !important;
}
.vp-doc {
color: var(--es-text-primary);
}
.vp-doc h1 {
font-size: var(--es-font-lg);
font-weight: 600;
color: var(--es-text-inverse);
border-bottom: none;
padding-bottom: 0;
margin-bottom: 16px;
line-height: 1.3;
}
.vp-doc h2 {
font-size: var(--es-font-md);
font-weight: 600;
color: var(--es-text-inverse);
border-bottom: none;
padding-bottom: 0;
margin-top: 32px;
margin-bottom: 12px;
padding: 6px 12px;
background: var(--es-bg-header);
border-left: 3px solid var(--es-primary);
}
.vp-doc h3 {
font-size: var(--es-font-base);
font-weight: 600;
color: var(--es-text-primary);
margin-top: 20px;
margin-bottom: 8px;
}
.vp-doc p {
color: var(--es-text-primary);
line-height: 1.7;
font-size: var(--es-font-base);
margin: 12px 0;
}
.vp-doc ul,
.vp-doc ol {
padding-left: 20px;
margin: 12px 0;
}
.vp-doc li {
line-height: 1.7;
margin: 4px 0;
color: var(--es-text-primary);
font-size: var(--es-font-base);
}
.vp-doc li::marker {
color: var(--es-text-secondary);
}
.vp-doc strong {
color: var(--es-text-primary);
font-weight: 600;
}
.vp-doc a {
color: var(--es-primary);
text-decoration: none;
}
.vp-doc a:hover {
text-decoration: underline;
}
.VPDocAside {
padding-left: 16px;
border-left: 1px solid var(--es-border-subtle);
}
.VPDocAsideOutline {
padding: 0;
border: none !important;
}
.VPDocAsideOutline .content {
border: none !important;
padding-left: 0 !important;
}
.VPDocAsideOutline .outline-title {
font-size: var(--es-font-xs);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--es-text-secondary);
padding-bottom: 8px;
}
.VPDocAsideOutline .outline-link {
color: var(--es-text-secondary);
font-size: var(--es-font-xs);
padding: 4px 0;
line-height: 1.4;
display: block;
}
.VPDocAsideOutline .outline-link:hover {
color: var(--es-text-primary);
}
.VPDocAsideOutline .outline-link.active {
color: var(--es-primary);
}
.VPDocAsideOutline .outline-marker {
display: none;
}
div[class*='language-'] {
background: var(--es-bg-inset) !important;
border: 1px solid var(--es-border-default);
border-radius: 2px;
margin: 12px 0;
}
.vp-code-group .tabs {
background: var(--es-bg-header);
border-bottom: 1px solid var(--es-border-subtle);
}
.vp-doc :not(pre) > code {
background: var(--es-bg-input);
color: var(--es-primary);
padding: 2px 6px;
border-radius: 2px;
font-size: var(--es-font-xs);
}
.vp-doc table {
display: table;
width: 100%;
background: transparent;
border: none;
border-collapse: collapse;
margin: 16px 0;
font-size: var(--es-font-sm);
}
.vp-doc tr {
border-bottom: 1px solid var(--es-border-subtle);
background: transparent;
}
.vp-doc tr:last-child {
border-bottom: none;
}
.vp-doc tr:hover {
background: rgba(255, 255, 255, 0.02);
}
.vp-doc th {
background: var(--es-bg-header);
font-weight: 600;
font-size: var(--es-font-xs);
color: var(--es-text-secondary);
text-align: left;
padding: 8px 12px;
border-bottom: 1px solid var(--es-border-subtle);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.vp-doc td {
font-size: var(--es-font-sm);
color: var(--es-text-primary);
padding: 8px 12px;
vertical-align: top;
line-height: 1.5;
}
.vp-doc td:first-child {
font-weight: 500;
color: var(--es-text-primary);
min-width: 100px;
}
.vp-doc .warning,
.vp-doc .custom-block.warning {
background: rgba(245, 158, 11, 0.08);
border: none;
border-left: 3px solid var(--es-warning);
border-radius: 0 2px 2px 0;
padding: 10px 12px;
margin: 16px 0;
}
.vp-doc .warning .custom-block-title,
.vp-doc .custom-block.warning .custom-block-title {
color: var(--es-warning);
font-weight: 600;
font-size: var(--es-font-xs);
margin-bottom: 4px;
}
.vp-doc .warning p {
color: var(--es-text-primary);
margin: 0;
font-size: var(--es-font-xs);
}
.vp-doc .tip,
.vp-doc .custom-block.tip {
background: rgba(59, 130, 246, 0.08);
border: none;
border-left: 3px solid var(--es-primary);
border-radius: 0 2px 2px 0;
padding: 10px 12px;
margin: 16px 0;
}
.vp-doc .tip .custom-block-title,
.vp-doc .custom-block.tip .custom-block-title {
color: var(--es-primary);
font-weight: 600;
font-size: var(--es-font-xs);
margin-bottom: 4px;
}
.vp-doc .tip p {
color: var(--es-text-primary);
margin: 0;
font-size: var(--es-font-xs);
}
.vp-doc .info,
.vp-doc .custom-block.info {
background: rgba(74, 222, 128, 0.08);
border: none;
border-left: 3px solid var(--es-success);
border-radius: 0 2px 2px 0;
padding: 10px 12px;
margin: 16px 0;
}
.vp-doc .info .custom-block-title,
.vp-doc .custom-block.info .custom-block-title {
color: var(--es-success);
font-weight: 600;
font-size: var(--es-font-xs);
margin-bottom: 4px;
}
.vp-doc .danger,
.vp-doc .custom-block.danger {
background: rgba(239, 68, 68, 0.08);
border: none;
border-left: 3px solid var(--es-error);
border-radius: 0 2px 2px 0;
padding: 10px 12px;
margin: 16px 0;
}
.vp-doc .danger .custom-block-title,
.vp-doc .custom-block.danger .custom-block-title {
color: var(--es-error);
font-weight: 600;
font-size: var(--es-font-xs);
margin-bottom: 4px;
}
.vp-doc .card {
background: var(--es-bg-sidebar);
border: 1px solid var(--es-border-subtle);
border-radius: 4px;
padding: 12px;
margin: 16px 0;
}
.vp-doc .card-title {
font-size: var(--es-font-sm);
font-weight: 600;
color: var(--es-text-primary);
margin-bottom: 6px;
}
.vp-doc .card-description {
font-size: var(--es-font-xs);
color: var(--es-text-muted);
line-height: 1.5;
}
.vp-doc .tag {
display: inline-block;
padding: 2px 8px;
background: transparent;
border: 1px solid var(--es-border-default);
border-radius: 2px;
color: var(--es-text-secondary);
font-size: var(--es-font-xs);
margin-right: 4px;
margin-bottom: 4px;
}
.VPFooter {
background: var(--es-bg-sidebar) !important;
border-top: 1px solid var(--es-border-subtle) !important;
}
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--es-bg-card);
}
::-webkit-scrollbar-thumb {
background: var(--es-border-strong);
border-radius: 4px;
border: 2px solid var(--es-bg-card);
}
::-webkit-scrollbar-thumb:hover {
background: #5a5a5a;
}
::-webkit-scrollbar-corner {
background: transparent;
}
.home-container {
max-width: 1000px;
margin: 0 auto;
padding: 0 16px;
}
.home-section {
padding: 32px 0;
}
@media (max-width: 960px) {
.VPDoc .content {
padding: 16px !important;
}
}

View File

@@ -0,0 +1,14 @@
import DefaultTheme from 'vitepress/theme'
import ParticleHero from './components/ParticleHero.vue'
import ParticleHeroEn from './components/ParticleHeroEn.vue'
import FeatureCard from './components/FeatureCard.vue'
import './custom.css'
export default {
extends: DefaultTheme,
enhanceApp({ app }) {
app.component('ParticleHero', ParticleHero)
app.component('ParticleHeroEn', ParticleHeroEn)
app.component('FeatureCard', FeatureCard)
}
}

View File

@@ -0,0 +1,412 @@
# Quick Start
This guide will help you get started with ECS Framework, from installation to creating your first ECS application.
## Installation
### NPM Installation
```bash
# Using npm
npm install @esengine/ecs-framework
```
## Initialize Core
### Basic Initialization
The core of ECS Framework is the `Core` class, a singleton that manages the entire framework lifecycle.
```typescript
import { Core } from '@esengine/ecs-framework'
// Method 1: Using config object (recommended)
const core = Core.create({
debug: true, // Enable debug mode for detailed logs and performance monitoring
debugConfig: { // Optional: Advanced debug configuration
enabled: false, // Whether to enable WebSocket debug server
websocketUrl: 'ws://localhost:8080',
debugFrameRate: 30, // Debug data send frame rate
channels: {
entities: true,
systems: true,
performance: true,
components: true,
scenes: true
}
}
});
// Method 2: Simplified creation (backward compatible)
const core = Core.create(true); // Equivalent to { debug: true }
// Method 3: Production environment configuration
const core = Core.create({
debug: false // Disable debug in production
});
```
### Core Configuration Details
```typescript
interface ICoreConfig {
/** Enable debug mode - affects log level and performance monitoring */
debug?: boolean;
/** Advanced debug configuration - for dev tools integration */
debugConfig?: {
enabled: boolean; // Enable debug server
websocketUrl: string; // WebSocket server URL
autoReconnect?: boolean; // Auto reconnect
debugFrameRate?: 60 | 30 | 15; // Debug data send frame rate
channels: { // Data channel configuration
entities: boolean; // Entity data
systems: boolean; // System data
performance: boolean; // Performance data
components: boolean; // Component data
scenes: boolean; // Scene data
};
};
}
```
### Core Instance Management
Core uses singleton pattern, accessible via static property after creation:
```typescript
// Create instance
const core = Core.create(true);
// Get created instance
const instance = Core.Instance; // Returns current instance, null if not created
```
### Game Loop Integration
**Important**: Before creating entities and systems, you need to understand how to integrate ECS Framework into your game engine.
`Core.update(deltaTime)` is the framework heartbeat, must be called every frame. It handles:
- Updating the built-in Time class
- Updating all global managers (timers, object pools, etc.)
- Updating all entity systems in all scenes
- Processing entity creation and destruction
- Collecting performance data (in debug mode)
See engine integration examples: [Game Engine Integration](#game-engine-integration)
## Create Your First ECS Application
### 1. Define Components
Components are pure data containers that store entity state:
```typescript
import { Component, ECSComponent } from '@esengine/ecs-framework'
// Position component
@ECSComponent('Position')
class Position extends Component {
x: number = 0
y: number = 0
constructor(x: number = 0, y: number = 0) {
super()
this.x = x
this.y = y
}
}
// Velocity component
@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
}
}
// Sprite component
@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. Create Entity Systems
Systems contain game logic and process entities with specific components. ECS Framework provides Matcher-based entity filtering:
```typescript
import { EntitySystem, Matcher, Time, ECSSystem } from '@esengine/ecs-framework'
// Movement system - handles position and velocity
@ECSSystem('MovementSystem')
class MovementSystem extends EntitySystem {
constructor() {
// Use Matcher to define target entities: must have both Position and Velocity
super(Matcher.empty().all(Position, Velocity))
}
protected process(entities: readonly Entity[]): void {
// process method receives all matching entities
for (const entity of entities) {
const position = entity.getComponent(Position)!
const velocity = entity.getComponent(Velocity)!
// Update position (using framework's Time class)
position.x += velocity.dx * Time.deltaTime
position.y += velocity.dy * Time.deltaTime
// Boundary check example
if (position.x < 0) position.x = 0
if (position.y < 0) position.y = 0
}
}
}
// Render system - handles visible objects
@ECSSystem('RenderSystem')
class RenderSystem extends EntitySystem {
constructor() {
// Must have Position and Sprite, optional Velocity (for direction)
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) // May be null
// Flip sprite based on velocity direction (optional logic)
let flipX = false
if (velocity && velocity.dx < 0) {
flipX = true
}
// Render logic (pseudocode here)
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) {
// Actual render implementation depends on your game engine
const direction = flipX ? '<-' : '->'
console.log(`Render ${texture} at (${x.toFixed(1)}, ${y.toFixed(1)}) direction: ${direction}`)
}
}
```
### 3. Create Scene
Recommended to extend Scene class for custom scenes:
```typescript
import { Scene } from '@esengine/ecs-framework'
// Recommended: Extend Scene for custom scene
class GameScene extends Scene {
initialize(): void {
// Scene initialization logic
this.name = "MainScene";
// Add systems to scene
this.addSystem(new MovementSystem());
this.addSystem(new RenderSystem());
}
onStart(): void {
// Logic when scene starts running
console.log("Game scene started");
}
unload(): void {
// Cleanup logic when scene unloads
console.log("Game scene unloaded");
}
}
// Create and set scene
const gameScene = new GameScene();
Core.setScene(gameScene);
```
### 4. Create Entities
```typescript
// Create player entity
const player = gameScene.createEntity("Player");
player.addComponent(new Position(100, 100));
player.addComponent(new Velocity(50, 30)); // Move 50px/sec (x), 30px/sec (y)
player.addComponent(new Sprite("player.png", 64, 64));
```
## Scene Management
Core has built-in scene management, very simple to use:
```typescript
import { Core, Scene } from '@esengine/ecs-framework';
// Initialize Core
Core.create({ debug: true });
// Create and set scene
class GameScene extends Scene {
initialize(): void {
this.name = "GamePlay";
this.addSystem(new MovementSystem());
this.addSystem(new RenderSystem());
}
}
const gameScene = new GameScene();
Core.setScene(gameScene);
// Game loop (auto-updates scene)
function gameLoop(deltaTime: number) {
Core.update(deltaTime); // Auto-updates global services and scene
}
// Switch scenes
Core.loadScene(new MenuScene()); // Delayed switch (next frame)
Core.setScene(new GameScene()); // Immediate switch
// Access current scene
const currentScene = Core.scene;
// Using fluent API
const player = Core.ecsAPI?.createEntity('Player')
.addComponent(Position, 100, 100)
.addComponent(Velocity, 50, 0);
```
### Advanced: Using WorldManager for Multi-World
Only for complex server-side applications (MMO game servers, game room systems, etc.):
```typescript
import { Core, WorldManager } from '@esengine/ecs-framework';
// Initialize Core
Core.create({ debug: true });
// Get WorldManager from service container (Core auto-creates and registers it)
const worldManager = Core.services.resolve(WorldManager);
// Create multiple independent game worlds
const room1 = worldManager.createWorld('room_001');
const room2 = worldManager.createWorld('room_002');
// Create scenes in each world
const gameScene1 = room1.createScene('game', new GameScene());
const gameScene2 = room2.createScene('game', new GameScene());
// Activate scenes
room1.setSceneActive('game', true);
room2.setSceneActive('game', true);
// Game loop (need to manually update worlds)
function gameLoop(deltaTime: number) {
Core.update(deltaTime); // Update global services
worldManager.updateAll(); // Manually update all worlds
}
```
## Game Engine Integration
### Laya Engine Integration
```typescript
import { Stage } from "laya/display/Stage";
import { Laya } from "Laya";
import { Core } from '@esengine/ecs-framework';
// Initialize Laya
Laya.init(800, 600).then(() => {
// Initialize ECS
Core.create(true);
Core.setScene(new GameScene());
// Start game loop
Laya.timer.frameLoop(1, this, () => {
const deltaTime = Laya.timer.delta / 1000;
Core.update(deltaTime); // Auto-updates global services and scene
});
});
```
### Cocos Creator Integration
```typescript
import { Component, _decorator } from 'cc';
import { Core } from '@esengine/ecs-framework';
const { ccclass } = _decorator;
@ccclass('ECSGameManager')
export class ECSGameManager extends Component {
onLoad() {
// Initialize ECS
Core.create(true);
Core.setScene(new GameScene());
}
update(deltaTime: number) {
// Auto-updates global services and scene
Core.update(deltaTime);
}
onDestroy() {
// Cleanup resources
Core.destroy();
}
}
```
## Next Steps
You've successfully created your first ECS application! Next you can:
- Check the complete [API Documentation](/api/README)
- Explore more [practical examples](/examples/)
## FAQ
### Why isn't my system executing?
Ensure:
1. System is added to scene: `this.addSystem(system)` (in Scene's initialize method)
2. Scene is set: `Core.setScene(scene)`
3. Game loop is calling: `Core.update(deltaTime)`
### How to debug ECS applications?
Enable debug mode:
```typescript
Core.create({ debug: true })
// Get debug data
const debugData = Core.getDebugData()
console.log(debugData)
```

43
docs/en/guide/index.md Normal file
View File

@@ -0,0 +1,43 @@
# Guide
Welcome to the ECS Framework Guide. This guide covers the core concepts and usage of the framework.
## Core Concepts
### [Entity](/guide/entity)
Learn the basics of ECS architecture - how to use entities, lifecycle management, and best practices.
### [Component](/guide/component)
Learn how to create and use components for modular game feature design.
### [System](/guide/system)
Master system development to implement game logic processing.
### [Entity Query & Matcher](/guide/entity-query)
Learn to use Matcher for entity filtering and queries with `all`, `any`, `none`, `nothing` conditions.
### [Scene](/guide/scene)
Understand scene lifecycle, system management, and entity container features.
### [Event System](/guide/event-system)
Master the type-safe event system for component communication and system coordination.
### [Serialization](/guide/serialization)
Master serialization for scenes, entities, and components. Supports full and incremental serialization for game saves, network sync, and more.
### [Time and Timers](/guide/time-and-timers)
Learn time management and timer systems for precise game logic timing control.
### [Logging](/guide/logging)
Master the leveled logging system for debugging, monitoring, and error tracking.
### [Platform Adapter](/guide/platform-adapter)
Learn how to implement and register platform adapters for browsers, mini-games, Node.js, and more.
## Advanced Features
### [Service Container](/guide/service-container)
Master dependency injection and service management for loosely-coupled architecture.
### [Plugin System](/guide/plugin-system)
Learn how to develop and use plugins to extend framework functionality.

317
docs/en/index.md Normal file
View File

@@ -0,0 +1,317 @@
---
layout: page
title: ESEngine - High-performance TypeScript ECS Framework
---
<ParticleHeroEn />
<section class="news-section">
<div class="news-container">
<div class="news-header">
<h2 class="news-title">Quick Links</h2>
<a href="/en/guide/" class="news-more">View Docs</a>
</div>
<div class="news-grid">
<a href="/en/guide/getting-started" class="news-card">
<div class="news-card-image" style="background: linear-gradient(135deg, #1e3a5f 0%, #1e1e1e 100%);">
<div class="news-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24"><path fill="#4fc1ff" d="M12 3L1 9l4 2.18v6L12 21l7-3.82v-6l2-1.09V17h2V9zm6.82 6L12 12.72L5.18 9L12 5.28zM17 16l-5 2.72L7 16v-3.73L12 15l5-2.73z"/></svg>
</div>
<span class="news-badge">Quick Start</span>
</div>
<div class="news-card-content">
<h3>Get Started in 5 Minutes</h3>
<p>From installation to your first ECS app, learn the core concepts quickly.</p>
</div>
</a>
<a href="/en/guide/behavior-tree/" class="news-card">
<div class="news-card-image" style="background: linear-gradient(135deg, #1e3a5f 0%, #1e1e1e 100%);">
<div class="news-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24"><path fill="#4ec9b0" d="M12 2a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m3 20h-1v-7l-2-2l-2 2v7H9v-7.5l-2 2V22H6v-6l3-3l1-3.5c-.3.4-.6.7-1 1L6 9v1H4V8l5-3c.5-.3 1.1-.5 1.7-.5H11c.6 0 1.2.2 1.7.5l5 3v2h-2V9l-3 1.5c-.4-.3-.7-.6-1-1l1 3.5l3 3v6Z"/></svg>
</div>
<span class="news-badge">AI System</span>
</div>
<div class="news-card-content">
<h3>Visual Behavior Tree Editor</h3>
<p>Built-in AI behavior tree system with visual editing and real-time debugging.</p>
</div>
</a>
</div>
</div>
</section>
<section class="features-section">
<div class="features-container">
<h2 class="features-title">Core Features</h2>
<div class="features-grid">
<div class="feature-card">
<div class="feature-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#4fc1ff" d="M13 2.05v2.02c3.95.49 7 3.85 7 7.93c0 1.45-.39 2.79-1.06 3.95l1.59 1.09A9.94 9.94 0 0 0 22 12c0-5.18-3.95-9.45-9-9.95M12 19c-3.87 0-7-3.13-7-7c0-3.53 2.61-6.43 6-6.92V2.05c-5.06.5-9 4.76-9 9.95c0 5.52 4.47 10 9.99 10c3.31 0 6.24-1.61 8.06-4.09l-1.6-1.1A7.93 7.93 0 0 1 12 19"/><path fill="#4fc1ff" d="M12 6a6 6 0 0 0-6 6c0 3.31 2.69 6 6 6a6 6 0 0 0 0-12m0 10c-2.21 0-4-1.79-4-4s1.79-4 4-4s4 1.79 4 4s-1.79 4-4 4"/></svg>
</div>
<h3 class="feature-title">High-performance ECS Architecture</h3>
<p class="feature-desc">Data-driven entity component system for large-scale entity processing with cache-friendly memory layout.</p>
<a href="/en/guide/entity" class="feature-link">Learn more</a>
</div>
<div class="feature-card">
<div class="feature-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#569cd6" d="M3 3h18v18H3zm16.525 13.707c0-.795-.272-1.425-.816-1.89c-.544-.465-1.404-.804-2.58-1.016l-1.704-.296c-.616-.104-1.052-.26-1.308-.468c-.256-.21-.384-.468-.384-.776c0-.392.168-.7.504-.924c.336-.224.8-.336 1.392-.336c.56 0 1.008.124 1.344.372c.336.248.536.584.6 1.008h2.016c-.08-.96-.464-1.716-1.152-2.268c-.688-.552-1.6-.828-2.736-.828c-1.2 0-2.148.3-2.844.9c-.696.6-1.044 1.38-1.044 2.34c0 .76.252 1.368.756 1.824c.504.456 1.308.792 2.412.996l1.704.312c.624.12 1.068.28 1.332.48c.264.2.396.46.396.78c0 .424-.192.756-.576.996c-.384.24-.9.36-1.548.36c-.672 0-1.2-.14-1.584-.42c-.384-.28-.608-.668-.672-1.164H8.868c.048 1.016.46 1.808 1.236 2.376c.776.568 1.796.852 3.06.852c1.24 0 2.22-.292 2.94-.876c.72-.584 1.08-1.364 1.08-2.34z"/></svg>
</div>
<h3 class="feature-title">Full Type Support</h3>
<p class="feature-desc">100% TypeScript with complete type definitions and compile-time checking for the best development experience.</p>
<a href="/en/guide/component" class="feature-link">Learn more</a>
</div>
<div class="feature-card">
<div class="feature-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#4ec9b0" d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10s10-4.5 10-10S17.5 2 12 2m0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8s8 3.59 8 8s-3.59 8-8 8m-5-8l4-4v3h4v2h-4v3z"/></svg>
</div>
<h3 class="feature-title">Visual Behavior Tree</h3>
<p class="feature-desc">Built-in AI behavior tree system with visual editor, custom nodes, and real-time debugging.</p>
<a href="/en/guide/behavior-tree/" class="feature-link">Learn more</a>
</div>
<div class="feature-card">
<div class="feature-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#c586c0" d="M4 6h18V4H4c-1.1 0-2 .9-2 2v11H0v3h14v-3H4zm19 2h-6c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h6c.55 0 1-.45 1-1V9c0-.55-.45-1-1-1m-1 9h-4v-7h4z"/></svg>
</div>
<h3 class="feature-title">Multi-Platform Support</h3>
<p class="feature-desc">Support for browsers, Node.js, WeChat Mini Games, and seamless integration with major game engines.</p>
<a href="/en/guide/platform-adapter" class="feature-link">Learn more</a>
</div>
<div class="feature-card">
<div class="feature-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#dcdcaa" d="M4 3h6v2H4v14h6v2H4c-1.1 0-2-.9-2-2V5c0-1.1.9-2 2-2m9 0h6c1.1 0 2 .9 2 2v14c0 1.1-.9 2-2 2h-6v-2h6V5h-6zm-1 7h4v2h-4z"/></svg>
</div>
<h3 class="feature-title">Modular Design</h3>
<p class="feature-desc">Core features packaged independently, import only what you need. Support for custom plugin extensions.</p>
<a href="/en/guide/plugin-system" class="feature-link">Learn more</a>
</div>
<div class="feature-card">
<div class="feature-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#9cdcfe" d="M22.7 19l-9.1-9.1c.9-2.3.4-5-1.5-6.9c-2-2-5-2.4-7.4-1.3L9 6L6 9L1.6 4.7C.4 7.1.9 10.1 2.9 12.1c1.9 1.9 4.6 2.4 6.9 1.5l9.1 9.1c.4.4 1 .4 1.4 0l2.3-2.3c.5-.4.5-1.1.1-1.4"/></svg>
</div>
<h3 class="feature-title">Developer Tools</h3>
<p class="feature-desc">Built-in performance monitoring, debugging tools, serialization system, and complete development toolchain.</p>
<a href="/en/guide/logging" class="feature-link">Learn more</a>
</div>
</div>
</div>
</section>
<style scoped>
/* Home page specific styles */
.news-section {
background: #0d0d0d;
padding: 64px 0;
border-top: 1px solid #2a2a2a;
}
.news-container {
max-width: 1400px;
margin: 0 auto;
padding: 0 48px;
}
.news-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
}
.news-title {
font-size: 1.5rem;
font-weight: 700;
color: #ffffff;
margin: 0;
}
.news-more {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
background: #1a1a1a;
border: 1px solid #2a2a2a;
border-radius: 6px;
color: #a0a0a0;
font-size: 0.875rem;
font-weight: 500;
text-decoration: none;
transition: all 0.2s;
}
.news-more:hover {
background: #252525;
color: #ffffff;
}
.news-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24px;
}
.news-card {
display: flex;
background: #1f1f1f;
border: 1px solid #2a2a2a;
border-radius: 12px;
overflow: hidden;
text-decoration: none;
transition: all 0.2s;
}
.news-card:hover {
border-color: #3b9eff;
}
.news-card-image {
width: 200px;
min-height: 140px;
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 16px;
gap: 12px;
}
.news-icon {
opacity: 0.9;
}
.news-badge {
display: inline-block;
padding: 4px 12px;
background: transparent;
border: 1px solid #3a3a3a;
border-radius: 16px;
color: #a0a0a0;
font-size: 0.75rem;
font-weight: 500;
}
.news-card-content {
padding: 20px;
display: flex;
flex-direction: column;
justify-content: center;
}
.news-card-content h3 {
font-size: 1.125rem;
font-weight: 600;
color: #ffffff;
margin: 0 0 8px 0;
}
.news-card-content p {
font-size: 0.875rem;
color: #707070;
margin: 0;
line-height: 1.6;
}
.features-section {
background: #0d0d0d;
padding: 64px 0;
}
.features-container {
max-width: 1400px;
margin: 0 auto;
padding: 0 48px;
}
.features-title {
font-size: 1.5rem;
font-weight: 700;
color: #ffffff;
margin: 0 0 32px 0;
}
.features-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
.feature-card {
background: #1f1f1f;
border: 1px solid #2a2a2a;
border-radius: 12px;
padding: 24px;
transition: all 0.15s ease;
}
.feature-card:hover {
border-color: #3b9eff;
background: #252525;
}
.feature-icon {
width: 48px;
height: 48px;
background: #0d0d0d;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16px;
}
.feature-title {
font-size: 16px;
font-weight: 600;
color: #ffffff;
margin: 0 0 8px 0;
}
.feature-desc {
font-size: 14px;
color: #707070;
line-height: 1.7;
margin: 0 0 16px 0;
}
.feature-link {
font-size: 14px;
color: #3b9eff;
text-decoration: none;
font-weight: 500;
}
.feature-link:hover {
text-decoration: underline;
}
@media (max-width: 1024px) {
.news-container,
.features-container {
padding: 0 24px;
}
.news-grid {
grid-template-columns: 1fr;
}
.features-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.news-card {
flex-direction: column;
}
.news-card-image {
width: 100%;
min-height: 120px;
}
.features-grid {
grid-template-columns: 1fr;
}
}
</style>

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

View File

@@ -55,12 +55,28 @@ class Health extends Component {
}
```
### 组件装饰器
### @ECSComponent 装饰器
**必须使用 `@ECSComponent` 装饰器**,这确保了:
- 组件在代码混淆后仍能正确识别
- 提供稳定的类型名称用于序列化和调试
- 框架能正确管理组件注册
`@ECSComponent` 是组件类必须使用的装饰器,它为组件提供了类型标识和元数据管理。
#### 为什么必须使用
| 功能 | 说明 |
|------|------|
| **类型识别** | 提供稳定的类型名称,代码混淆后仍能正确识别 |
| **序列化支持** | 序列化/反序列化时使用该名称作为类型标识 |
| **组件注册** | 自动注册到 ComponentRegistry分配唯一的位掩码 |
| **调试支持** | 在调试工具和日志中显示可读的组件名称 |
#### 基本语法
```typescript
@ECSComponent(typeName: string)
```
- `typeName`: 组件的类型名称,建议使用与类名相同或相近的名称
#### 使用示例
```typescript
// ✅ 正确的用法
@@ -70,12 +86,63 @@ class Velocity extends Component {
dy: number = 0;
}
// ✅ 推荐:类型名与类名保持一致
@ECSComponent('PlayerController')
class PlayerController extends Component {
speed: number = 5;
}
// ❌ 错误的用法 - 没有装饰器
class BadComponent extends Component {
// 这样定义的组件可能在生产环境出现问题
// 这样定义的组件可能在生产环境出现问题
// 1. 代码压缩后类名变化,无法正确序列化
// 2. 组件未注册到框架,查询和匹配可能失效
}
```
#### 与 @Serializable 配合使用
当组件需要支持序列化时,`@ECSComponent``@Serializable` 需要一起使用:
```typescript
import { Component, ECSComponent, Serializable, Serialize } from '@esengine/ecs-framework';
@ECSComponent('Player')
@Serializable({ version: 1 })
class PlayerComponent extends Component {
@Serialize()
name: string = '';
@Serialize()
level: number = 1;
// 不使用 @Serialize() 的字段不会被序列化
private _cachedData: any = null;
}
```
> **注意**`@ECSComponent` 的 `typeName` 和 `@Serializable` 的 `typeId` 可以不同。如果 `@Serializable` 没有指定 `typeId`,则默认使用 `@ECSComponent` 的 `typeName`。
#### 组件类型名的唯一性
每个组件的类型名应该是唯一的:
```typescript
// ❌ 错误:两个组件使用相同的类型名
@ECSComponent('Health')
class HealthComponent extends Component { }
@ECSComponent('Health') // 冲突!
class EnemyHealthComponent extends Component { }
// ✅ 正确:使用不同的类型名
@ECSComponent('PlayerHealth')
class PlayerHealthComponent extends Component { }
@ECSComponent('EnemyHealth')
class EnemyHealthComponent extends Component { }
```
## 组件生命周期
组件提供了生命周期钩子,可以重写来执行特定的逻辑:
@@ -90,7 +157,7 @@ class ExampleComponent extends Component {
* 用于初始化资源、建立引用等
*/
onAddedToEntity(): void {
console.log(`组件 ${this.constructor.name} 添加实体 ${this.entity.name}`);
console.log(`组件 ${this.constructor.name} 添加实体ID: ${this.entityId}`);
this.resource = new SomeResource();
}
@@ -99,7 +166,7 @@ class ExampleComponent extends Component {
* 用于清理资源、断开引用等
*/
onRemovedFromEntity(): void {
console.log(`组件 ${this.constructor.name} 从实体 ${this.entity.name} 移除`);
console.log(`组件 ${this.constructor.name} 移除`);
if (this.resource) {
this.resource.cleanup();
this.resource = null;
@@ -108,30 +175,58 @@ class ExampleComponent extends Component {
}
```
## 访问实体
## 组件与实体的关系
组件可以通过 `this.entity` 访问其所属的实体:
组件存储了所属实体的ID (`entityId`)而不是直接引用实体对象。这是ECS数据导向设计的体现避免了循环引用。
在实际使用中,**应该在 System 中处理实体和组件的交互**,而不是在组件内部:
```typescript
@ECSComponent('Damage')
class Damage extends Component {
damage: number;
@ECSComponent('Health')
class Health extends Component {
current: number;
max: number;
constructor(damage: number) {
constructor(max: number = 100) {
super();
this.damage = damage;
this.max = max;
this.current = max;
}
// 在组件方法中访问实体和其他组件
applyDamage(): void {
const health = this.entity.getComponent(Health);
if (health) {
health.takeDamage(this.damage);
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;
// 如果生命值为0销毁实体
if (health.isDead()) {
this.entity.destroy();
entity.destroy();
}
// 应用伤害后移除 Damage 组件
entity.removeComponent(damage);
}
}
}
@@ -146,9 +241,27 @@ class Damage extends Component {
class ExampleComponent extends Component {
someData: string = "example";
showComponentInfo(): void {
console.log(`组件ID: ${this.id}`); // 唯一的组件ID
console.log(`所属实体: ${this.entity.name}`); // 所属实体引用
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}`);
}
}
}
```
@@ -245,7 +358,7 @@ class WeaponConfig extends Component {
### 1. 保持组件简单
```typescript
// 好的组件设计 - 单一职责
// 好的组件设计 - 单一职责
@ECSComponent('Position')
class Position extends Component {
x: number = 0;
@@ -258,7 +371,7 @@ class Velocity extends Component {
dy: number = 0;
}
// 避免的组件设计 - 职责过多
// 避免的组件设计 - 职责过多
@ECSComponent('GameObject')
class GameObject extends Component {
x: number;
@@ -330,16 +443,11 @@ class Inventory extends Component {
}
```
### 4. 避免在组件中存储实体引用
### 4. 引用其他实体
当组件需要关联其他实体时(如父子关系、跟随目标等),**推荐方式是存储实体ID**,然后在 System 中查找:
```typescript
// ❌ 避免:在组件中存储其他实体的引用
@ECSComponent('BadFollower')
class BadFollower extends Component {
target: Entity; // 直接引用可能导致内存泄漏
}
// ✅ 推荐存储实体ID通过场景查找
@ECSComponent('Follower')
class Follower extends Component {
targetId: number;
@@ -349,11 +457,269 @@ class Follower extends Component {
super();
this.targetId = targetId;
}
}
getTarget(): Entity | null {
return this.entity.scene?.findEntityById(this.targetId) || null;
// 在 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

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

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

View File

@@ -9,6 +9,12 @@
- 提供唯一标识ID
- 管理组件的生命周期
::: tip 关于父子层级关系
实体间的父子层级关系通过 `HierarchyComponent``HierarchySystem` 管理,而非 Entity 内置属性。这种设计遵循 ECS 组合原则 —— 只有需要层级关系的实体才添加此组件。
详见 [层级系统](./hierarchy.md) 文档。
:::
## 创建实体
**重要提示:实体必须通过场景创建,不支持手动创建!**
@@ -83,8 +89,8 @@ if (player.hasComponent(Position)) {
console.log("玩家有位置组件");
}
// 获取所有组件实例(直接访问 components 属性)
const allComponents = player.components; // Component[]
// 获取所有组件实例(只读属性)
const allComponents = player.components; // readonly Component[]
// 获取指定类型的所有组件(支持同类型多组件)
const allHealthComponents = player.getComponents(Health); // Health[]
@@ -285,4 +291,10 @@ entity.components.forEach(component => {
});
```
实体是 ECS 架构的核心概念之一,理解如何正确使用实体将帮助你构建高效、可维护的游戏代码。
实体是 ECS 架构的核心概念之一,理解如何正确使用实体将帮助你构建高效、可维护的游戏代码。
## 下一步
- 了解 [层级系统](./hierarchy.md) 建立实体间的父子关系
- 了解 [组件系统](./component.md) 为实体添加功能
- 了解 [场景管理](./scene.md) 组织和管理实体

View File

@@ -23,7 +23,6 @@ import { Core } from '@esengine/ecs-framework'
// 方式1使用配置对象推荐
const core = Core.create({
debug: true, // 启用调试模式,提供详细的日志和性能监控
enableEntitySystems: true, // 启用实体系统这是ECS的核心功能
debugConfig: { // 可选:高级调试配置
enabled: false, // 是否启用WebSocket调试服务器
websocketUrl: 'ws://localhost:8080',
@@ -39,12 +38,11 @@ const core = Core.create({
});
// 方式2简化创建向后兼容
const core = Core.create(true); // 等同于 { debug: true, enableEntitySystems: true }
const core = Core.create(true); // 等同于 { debug: true }
// 方式3生产环境配置
const core = Core.create({
debug: false, // 生产环境关闭调试
enableEntitySystems: true
debug: false // 生产环境关闭调试
});
```
@@ -55,9 +53,6 @@ interface ICoreConfig {
/** 是否启用调试模式 - 影响日志级别和性能监控 */
debug?: boolean;
/** 是否启用实体系统 - 核心ECS功能开关 */
enableEntitySystems?: boolean;
/** 高级调试配置 - 用于开发工具集成 */
debugConfig?: {
enabled: boolean; // 是否启用调试服务器
@@ -264,22 +259,17 @@ player.addComponent(new Velocity(50, 30)); // 每秒移动 50 像素x方向
player.addComponent(new Sprite("player.png", 64, 64));
```
## World 概念
## 场景管理
World 是 Scene 的容器,用于管理多个独立的游戏世界。这种设计特别适用于
- 多人游戏房间(每个房间一个 World
- 不同的游戏模式
- 独立的模拟环境
### 基本用法
Core 内置了场景管理功能,使用非常简单
```typescript
import { World, Scene } from '@esengine/ecs-framework'
import { Core, Scene } from '@esengine/ecs-framework';
// 创建游戏房间的World
const roomWorld = new World({ name: 'Room_001' });
// 初始化Core
Core.create({ debug: true });
// 在World中创建多个Scene
// 创建并设置场景
class GameScene extends Scene {
initialize(): void {
this.name = "GamePlay";
@@ -288,78 +278,106 @@ class GameScene extends Scene {
}
}
class UIScene extends Scene {
initialize(): void {
this.name = "UI";
// UI相关系统
}
const gameScene = new GameScene();
Core.setScene(gameScene);
// 游戏循环(自动更新场景)
function gameLoop(deltaTime: number) {
Core.update(deltaTime); // 自动更新全局服务和场景
}
// 添加Scene到World
const gameScene = roomWorld.createScene('game', new GameScene());
const uiScene = roomWorld.createScene('ui', new UIScene());
// 切换场景
Core.loadScene(new MenuScene()); // 延迟切换(下一帧)
Core.setScene(new GameScene()); // 立即切换
// 激活Scene
roomWorld.setSceneActive('game', true);
roomWorld.setSceneActive('ui', true);
// 访问当前场景
const currentScene = Core.scene;
// 启动World
roomWorld.start();
// 使用流式API
const player = Core.ecsAPI?.createEntity('Player')
.addComponent(Position, 100, 100)
.addComponent(Velocity, 50, 0);
```
### World 生命周期
### 高级:使用 WorldManager 管理多世界
World 提供了完整的生命周期管理
- `start()`: 启动 World 和所有全局系统
- `updateGlobalSystems()`: 更新全局系统(由 Core.update() 调用)
- `updateScenes()`: 更新所有激活的 Scene由 Core.update() 调用)
- `stop()`: 停止 World
- `destroy()`: 销毁 World 和所有资源
仅适用于复杂的服务器端应用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 { Stat } from "laya/utils/Stat"
import { Laya } from "Laya"
import { Stage } from "laya/display/Stage";
import { Laya } from "Laya";
import { Core } from '@esengine/ecs-framework';
// 初始化 Laya
Laya.init(800, 600).then(() => {
// 初始化 ECS
const core = Core.create(true)
// 设置场景...
Core.create(true);
Core.setScene(new GameScene());
// 启动游戏循环
Laya.timer.frameLoop(1, this, () => {
const deltaTime = Laya.timer.delta / 1000 // 转换为秒
Core.update(deltaTime)
})
})
const deltaTime = Laya.timer.delta / 1000;
Core.update(deltaTime); // 自动更新全局服务和场景
});
});
```
### Cocos Creator 集成
```typescript
import { Component, _decorator } from 'cc'
import { Component, _decorator } from 'cc';
import { Core } from '@esengine/ecs-framework';
const { ccclass } = _decorator
const { ccclass } = _decorator;
@ccclass('ECSGameManager')
export class ECSGameManager extends Component {
onLoad() {
// 初始化 ECS
const core = Core.create(true)
// 设置场景...
Core.create(true);
Core.setScene(new GameScene());
}
update(deltaTime: number) {
// 更新 ECS
Core.update(deltaTime)
// 自动更新全局服务和场景
Core.update(deltaTime);
}
onDestroy() {
// 清理资源
Core.destroy();
}
}
```
@@ -378,7 +396,7 @@ export class ECSGameManager extends Component {
确保:
1. 系统已添加到场景:`this.addSystem(system)` (在 Scene 的 initialize 方法中)
2. 场景已设置为当前场景`Core.setScene(scene)`
2. 场景已设置:`Core.setScene(scene)`
3. 游戏循环在调用:`Core.update(deltaTime)`
### 如何调试 ECS 应用?

437
docs/guide/hierarchy.md Normal file
View File

@@ -0,0 +1,437 @@
# 层级系统
在游戏开发中实体间的父子层级关系是常见需求。ECS Framework 采用组件化方式管理层级关系,通过 `HierarchyComponent``HierarchySystem` 实现,完全遵循 ECS 组合原则。
## 设计理念
### 为什么不在 Entity 中内置层级?
传统的游戏对象模型(如 Unity 的 GameObject将层级关系内置于实体中。ECS Framework 选择组件化方案的原因:
1. **ECS 组合原则**:层级是一种"功能",应该通过组件添加,而非所有实体都具备
2. **按需使用**:只有需要层级关系的实体才添加 `HierarchyComponent`
3. **数据与逻辑分离**`HierarchyComponent` 存储数据,`HierarchySystem` 处理逻辑
4. **序列化友好**:层级关系作为组件数据可以轻松序列化和反序列化
## 基本概念
### HierarchyComponent
存储层级关系数据的组件:
```typescript
import { HierarchyComponent } from '@esengine/ecs-framework';
// HierarchyComponent 的核心属性
interface HierarchyComponent {
parentId: number | null; // 父实体 IDnull 表示根实体
childIds: number[]; // 子实体 ID 列表
depth: number; // 在层级中的深度(由系统维护)
bActiveInHierarchy: boolean; // 在层级中是否激活(由系统维护)
}
```
### HierarchySystem
处理层级逻辑的系统,提供所有层级操作的 API
```typescript
import { HierarchySystem } from '@esengine/ecs-framework';
// 获取系统
const hierarchySystem = scene.getEntityProcessor(HierarchySystem);
```
## 快速开始
### 添加系统到场景
```typescript
import { Scene, HierarchySystem } from '@esengine/ecs-framework';
class GameScene extends Scene {
protected initialize(): void {
// 添加层级系统
this.addSystem(new HierarchySystem());
// 添加其他系统...
}
}
```
### 建立父子关系
```typescript
// 创建实体
const parent = scene.createEntity("Parent");
const child1 = scene.createEntity("Child1");
const child2 = scene.createEntity("Child2");
// 获取层级系统
const hierarchySystem = scene.getEntityProcessor(HierarchySystem);
// 设置父子关系(自动添加 HierarchyComponent
hierarchySystem.setParent(child1, parent);
hierarchySystem.setParent(child2, parent);
// 现在 parent 有两个子实体
```
### 查询层级
```typescript
// 获取父实体
const parentEntity = hierarchySystem.getParent(child1);
// 获取所有子实体
const children = hierarchySystem.getChildren(parent);
// 获取子实体数量
const count = hierarchySystem.getChildCount(parent);
// 检查是否有子实体
const hasKids = hierarchySystem.hasChildren(parent);
// 获取在层级中的深度
const depth = hierarchySystem.getDepth(child1); // 返回 1
```
## API 参考
### 父子关系操作
#### setParent
设置实体的父级:
```typescript
// 设置父级
hierarchySystem.setParent(child, parent);
// 移动到根级(无父级)
hierarchySystem.setParent(child, null);
```
#### insertChildAt
在指定位置插入子实体:
```typescript
// 在第一个位置插入
hierarchySystem.insertChildAt(parent, child, 0);
// 追加到末尾
hierarchySystem.insertChildAt(parent, child, -1);
```
#### removeChild
从父级移除子实体(子实体变为根级):
```typescript
const success = hierarchySystem.removeChild(parent, child);
```
#### removeAllChildren
移除所有子实体:
```typescript
hierarchySystem.removeAllChildren(parent);
```
### 层级查询
#### getParent / getChildren
```typescript
const parent = hierarchySystem.getParent(entity);
const children = hierarchySystem.getChildren(entity);
```
#### getRoot
获取实体的根节点:
```typescript
const root = hierarchySystem.getRoot(deepChild);
```
#### getRootEntities
获取所有根实体(没有父级的实体):
```typescript
const roots = hierarchySystem.getRootEntities();
```
#### isAncestorOf / isDescendantOf
检查祖先/后代关系:
```typescript
// grandparent -> parent -> child
const isAncestor = hierarchySystem.isAncestorOf(grandparent, child); // true
const isDescendant = hierarchySystem.isDescendantOf(child, grandparent); // true
```
### 层级遍历
#### findChild
根据名称查找子实体:
```typescript
// 直接子级中查找
const child = hierarchySystem.findChild(parent, "ChildName");
// 递归查找所有后代
const deepChild = hierarchySystem.findChild(parent, "DeepChild", true);
```
#### findChildrenByTag
根据标签查找子实体:
```typescript
// 查找直接子级
const tagged = hierarchySystem.findChildrenByTag(parent, TAG_ENEMY);
// 递归查找
const allTagged = hierarchySystem.findChildrenByTag(parent, TAG_ENEMY, true);
```
#### forEachChild
遍历子实体:
```typescript
// 遍历直接子级
hierarchySystem.forEachChild(parent, (child) => {
console.log(child.name);
});
// 递归遍历所有后代
hierarchySystem.forEachChild(parent, (child) => {
console.log(child.name);
}, true);
```
### 层级状态
#### isActiveInHierarchy
检查实体在层级中是否激活(考虑所有祖先的激活状态):
```typescript
// 如果 parent.active = false即使 child.active = true
// isActiveInHierarchy(child) 也会返回 false
const activeInHierarchy = hierarchySystem.isActiveInHierarchy(child);
```
#### getDepth
获取实体在层级中的深度(根实体深度为 0
```typescript
const depth = hierarchySystem.getDepth(entity);
```
### 扁平化层级(用于 UI 渲染)
```typescript
// 用于实现可展开/折叠的层级树视图
const expandedIds = new Set([parent.id]);
const flatNodes = hierarchySystem.flattenHierarchy(expandedIds);
// 返回 [{ entity, depth, bHasChildren, bIsExpanded }, ...]
```
## 完整示例
### 创建游戏角色层级
```typescript
import {
Scene,
HierarchySystem,
HierarchyComponent
} from '@esengine/ecs-framework';
class GameScene extends Scene {
private hierarchySystem!: HierarchySystem;
protected initialize(): void {
// 添加层级系统
this.hierarchySystem = new HierarchySystem();
this.addSystem(this.hierarchySystem);
// 创建角色层级
this.createPlayerHierarchy();
}
private createPlayerHierarchy(): void {
// 根实体
const player = this.createEntity("Player");
player.addComponent(new Transform(0, 0));
// 身体部件
const body = this.createEntity("Body");
body.addComponent(new Sprite("body.png"));
this.hierarchySystem.setParent(body, player);
// 武器(挂载在身体上)
const weapon = this.createEntity("Weapon");
weapon.addComponent(new Sprite("sword.png"));
this.hierarchySystem.setParent(weapon, body);
// 特效(挂载在武器上)
const effect = this.createEntity("WeaponEffect");
effect.addComponent(new ParticleEmitter());
this.hierarchySystem.setParent(effect, weapon);
// 查询层级信息
console.log(`Player 层级深度: ${this.hierarchySystem.getDepth(player)}`); // 0
console.log(`Weapon 层级深度: ${this.hierarchySystem.getDepth(weapon)}`); // 2
console.log(`Effect 层级深度: ${this.hierarchySystem.getDepth(effect)}`); // 3
}
public equipNewWeapon(weaponName: string): void {
const body = this.findEntity("Body");
const oldWeapon = this.hierarchySystem.findChild(body!, "Weapon");
if (oldWeapon) {
// 移除旧武器的所有子实体
this.hierarchySystem.removeAllChildren(oldWeapon);
oldWeapon.destroy();
}
// 创建新武器
const newWeapon = this.createEntity("Weapon");
newWeapon.addComponent(new Sprite(`${weaponName}.png`));
this.hierarchySystem.setParent(newWeapon, body!);
}
}
```
### 层级变换系统
结合 Transform 组件实现层级变换:
```typescript
import { EntitySystem, Matcher, HierarchySystem, HierarchyComponent } from '@esengine/ecs-framework';
class HierarchyTransformSystem extends EntitySystem {
private hierarchySystem!: HierarchySystem;
constructor() {
super(Matcher.empty().all(Transform, HierarchyComponent));
}
public onAddedToScene(): void {
// 获取层级系统引用
this.hierarchySystem = this.scene!.getEntityProcessor(HierarchySystem)!;
}
protected process(entities: readonly Entity[]): void {
// 按深度排序,确保父级先更新
const sorted = [...entities].sort((a, b) => {
return this.hierarchySystem.getDepth(a) - this.hierarchySystem.getDepth(b);
});
for (const entity of sorted) {
const transform = entity.getComponent(Transform)!;
const parent = this.hierarchySystem.getParent(entity);
if (parent) {
const parentTransform = parent.getComponent(Transform);
if (parentTransform) {
// 计算世界坐标
transform.worldX = parentTransform.worldX + transform.localX;
transform.worldY = parentTransform.worldY + transform.localY;
}
} else {
// 根实体,本地坐标即世界坐标
transform.worldX = transform.localX;
transform.worldY = transform.localY;
}
}
}
}
```
## 性能优化
### 缓存机制
`HierarchySystem` 内置了缓存机制:
- `depth``bActiveInHierarchy` 由系统自动维护
- 使用 `bCacheDirty` 标记优化更新
- 层级变化时自动标记所有子级缓存为脏
### 最佳实践
1. **避免深层嵌套**:系统限制最大深度为 32 层
2. **批量操作**:构建复杂层级时,尽量一次性设置好所有父子关系
3. **按需添加**:只有真正需要层级关系的实体才添加 `HierarchyComponent`
4. **缓存系统引用**:避免每次调用都获取 `HierarchySystem`
```typescript
// 好的做法
class MySystem extends EntitySystem {
private hierarchySystem!: HierarchySystem;
onAddedToScene() {
this.hierarchySystem = this.scene!.getEntityProcessor(HierarchySystem)!;
}
process() {
// 使用缓存的引用
const parent = this.hierarchySystem.getParent(entity);
}
}
// 避免的做法
process() {
// 每次都获取,性能较差
const system = this.scene!.getEntityProcessor(HierarchySystem);
}
```
## 迁移指南
如果你之前使用的是旧版 Entity 内置的层级 API请参考以下迁移指南
| 旧 API (已移除) | 新 API |
|----------------|--------|
| `entity.parent` | `hierarchySystem.getParent(entity)` |
| `entity.children` | `hierarchySystem.getChildren(entity)` |
| `entity.addChild(child)` | `hierarchySystem.setParent(child, entity)` |
| `entity.removeChild(child)` | `hierarchySystem.removeChild(entity, child)` |
| `entity.findChild(name)` | `hierarchySystem.findChild(entity, name)` |
| `entity.activeInHierarchy` | `hierarchySystem.isActiveInHierarchy(entity)` |
### 迁移示例
```typescript
// 旧代码
const parent = scene.createEntity("Parent");
const child = scene.createEntity("Child");
parent.addChild(child);
const found = parent.findChild("Child");
// 新代码
const hierarchySystem = scene.getEntityProcessor(HierarchySystem);
const parent = scene.createEntity("Parent");
const child = scene.createEntity("Child");
hierarchySystem.setParent(child, parent);
const found = hierarchySystem.findChild(parent, "Child");
```
## 下一步
- 了解 [实体类](./entity.md) 的其他功能
- 了解 [场景管理](./scene.md) 如何组织实体和系统
- 了解 [组件系统](./component.md) 如何定义和使用组件

View File

@@ -13,12 +13,18 @@
### [系统架构 (System)](./system.md)
掌握系统的编写方法,实现游戏逻辑的处理。
### [实体查询与 Matcher](./entity-query.md)
学习使用 Matcher 进行实体筛选和查询,掌握 `all``any``none``nothing` 等匹配条件。
### [场景管理 (Scene)](./scene.md)
了解场景的生命周期、系统管理和实体容器功能。
### [事件系统 (Event)](./event-system.md)
掌握类型安全的事件系统,实现组件间通信和系统协作。
### [序列化系统 (Serialization)](./serialization.md)
掌握场景、实体和组件的序列化方案,支持全量序列化和增量序列化,实现游戏存档、网络同步等功能。
### [时间和定时器 (Time)](./time-and-timers.md)
学习时间管理和定时器系统,实现游戏逻辑的精确时间控制。
@@ -26,4 +32,12 @@
掌握分级日志系统,用于调试、监控和错误追踪。
### [平台适配器 (Platform Adapter)](./platform-adapter.md)
了解如何为不同平台实现和注册平台适配器支持浏览器、小游戏、Node.js等环境。
了解如何为不同平台实现和注册平台适配器支持浏览器、小游戏、Node.js等环境。
## 高级特性
### [服务容器 (Service Container)](./service-container.md)
掌握依赖注入和服务管理,实现松耦合的架构设计。
### [插件系统 (Plugin System)](./plugin-system.md)
学习如何开发和使用插件,扩展框架功能,实现功能模块化。

View File

@@ -238,6 +238,50 @@ class HierarchicalLoggingExample {
}
```
### 集成第三方日志库
通过 `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
@@ -547,4 +591,4 @@ class LoggingConfiguration {
LoggingConfiguration.setupLogging();
```
日志系统是调试和监控应用的重要工具,正确使用日志系统能大大提高开发效率和问题排查能力。
日志系统是调试和监控应用的重要工具,正确使用日志系统能大大提高开发效率和问题排查能力。

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) 文档。

View File

@@ -11,6 +11,22 @@
- 事件系统支持
- 性能监控和调试信息
## 场景管理方式
ECS Framework 提供了两种场景管理方式:
1. **[SceneManager](./scene-manager.md)** - 适用于 95% 的游戏应用
- 单人游戏、简单多人游戏、移动游戏
- 轻量级,简单直观的 API
- 支持场景切换
2. **[WorldManager](./world-manager.md)** - 适用于高级多世界隔离场景
- MMO 游戏服务器、游戏房间系统
- 多 World 管理,每个 World 可包含多个场景
- 完全隔离的独立环境
本文档重点介绍 Scene 类本身的使用方法。关于场景管理器的详细信息,请查看对应的文档。
## 创建场景
### 继承 Scene 类
@@ -106,6 +122,13 @@ const scene = new ExampleScene();
// 场景的 initialize(), begin(), update(), end() 由框架自动调用
```
**生命周期方法**
1. `initialize()` - 场景初始化,设置系统和初始实体
2. `begin()` / `onStart()` - 场景开始运行
3. `update()` - 每帧更新(由场景管理器调用)
4. `end()` / `unload()` - 场景卸载,清理资源
## 实体管理
### 创建实体
@@ -247,15 +270,42 @@ class EventScene extends Scene {
}
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();
```
## 场景统计和调试
### 获取场景统计
@@ -287,110 +337,59 @@ class StatsScene extends Scene {
}
```
## 场景集成到框架
## 组件查询
场景可以通过两种方式运行
### 1. 简单的单场景应用
Scene 提供了强大的组件查询系统
```typescript
import { Core } from '@esengine/ecs-framework';
// 创建游戏场景
class GameScene extends Scene {
class QueryScene extends Scene {
protected initialize(): void {
this.name = "GameScene";
this.addSystem(new MovementSystem());
this.addSystem(new RenderSystem());
}
}
// 启动游戏
Core.create();
const gameScene = new GameScene();
Core.setScene(gameScene);
```
### 2. 复杂的多场景应用
```typescript
import { WorldManager } from '@esengine/ecs-framework';
// 获取WorldManager实例
const worldManager = WorldManager.getInstance();
// 创建World
const gameWorld = worldManager.createWorld('game', {
name: 'MainGame',
maxScenes: 5
});
// 在World中创建场景
const menuScene = gameWorld.createScene('menu', new MenuScene());
const gameScene = gameWorld.createScene('game', new GameScene());
// 激活场景
gameWorld.setSceneActive('menu', true);
```
## 多场景管理
在World中可以管理多个场景通过激活/停用来切换:
```typescript
class GameWorld extends World {
private menuScene: Scene;
private gameScene: Scene;
private gameOverScene: Scene;
public initialize(): void {
// 创建多个场景
this.menuScene = this.createScene('menu', new MenuScene());
this.gameScene = this.createScene('game', new GameScene());
this.gameOverScene = this.createScene('gameover', new GameOverScene());
// 设置初始场景
this.showMenu();
// 创建一些实体
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 showMenu(): void {
this.deactivateAllScenes();
this.setSceneActive('menu', true);
}
public queryEntities(): void {
// 通过 QuerySystem 查询
const entities = this.querySystem.query([Transform, Velocity]);
console.log(`找到 ${entities.length} 个有 Transform 和 Velocity 的实体`);
public startGame(): void {
this.deactivateAllScenes();
this.setSceneActive('game', true);
}
public showGameOver(): void {
this.deactivateAllScenes();
this.setSceneActive('gameover', true);
}
private deactivateAllScenes(): void {
this.setSceneActive('menu', false);
this.setSceneActive('game', false);
this.setSceneActive('gameover', false);
// 使用 ECS 流式 API如果通过 SceneManager
// const api = sceneManager.api;
// const entities = api?.find(Transform, Velocity);
}
}
```
## 与 World 的关系
## 性能监控
Scene 的运行架构层次
Scene 内置了性能监控功能
```typescript
// Core -> WorldManager -> World -> Scene -> EntitySystem -> Entity -> Component
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);
}
// 1. 简单应用Core直接管理单个Scene
Core.setScene(new GameScene());
// 2. 复杂应用WorldManager管理多个World每个World管理多个Scene
const worldManager = WorldManager.getInstance();
const world = worldManager.createWorld('gameWorld');
const scene = world.createScene('mainScene', new GameScene());
world.setSceneActive('mainScene', true);
// 获取性能报告
const report = this.performanceMonitor?.generateReport();
if (report) {
console.log('性能报告:', report);
}
}
}
```
## 最佳实践
@@ -398,7 +397,7 @@ world.setSceneActive('mainScene', true);
### 1. 场景职责分离
```typescript
// 好的场景设计 - 职责清晰
// 好的场景设计 - 职责清晰
class MenuScene extends Scene {
// 只处理菜单相关逻辑
}
@@ -411,7 +410,7 @@ class InventoryScene extends Scene {
// 只处理物品栏逻辑
}
// 避免的场景设计 - 职责混乱
// 避免的场景设计 - 职责混乱
class MegaScene extends Scene {
// 包含菜单、游戏、物品栏等所有逻辑
}
@@ -458,12 +457,25 @@ class ResourceScene extends Scene {
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;
}
}
```
@@ -504,7 +516,146 @@ class EventHandlingScene extends Scene {
private onPlayerInput(data: any): void {
// 处理玩家输入
}
public unload(): void {
// 清理事件监听
this.eventSystem.clear();
}
}
```
场景是 ECS 框架的核心容器,正确使用场景管理能让你的游戏架构更加清晰、模块化和易于维护。
### 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 框架的核心容器,正确使用场景管理能让你的游戏架构更加清晰、模块化和易于维护。

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

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

View File

@@ -0,0 +1,828 @@
# 服务容器
服务容器ServiceContainer是 ECS Framework 的依赖注入容器,负责管理框架中所有服务的注册、解析和生命周期。通过服务容器,你可以实现松耦合的架构设计,提高代码的可测试性和可维护性。
## 概述
### 什么是服务容器
服务容器是一个轻量级的依赖注入DI容器它提供了
- **服务注册**: 将服务类型注册到容器中
- **服务解析**: 从容器中获取服务实例
- **生命周期管理**: 自动管理服务实例的创建和销毁
- **依赖注入**: 自动解析服务之间的依赖关系
### 核心概念
#### 服务Service
服务是实现了 `IService` 接口的类,必须提供 `dispose()` 方法用于资源清理:
```typescript
import { IService } from '@esengine/ecs-framework';
class MyService implements IService {
constructor() {
// 初始化逻辑
}
dispose(): void {
// 清理资源
}
}
```
#### 服务标识符ServiceIdentifier
服务标识符用于在容器中唯一标识一个服务,支持两种类型:
- **类构造函数**: 直接使用服务类作为标识符,适用于具体实现类
- **Symbol**: 使用 Symbol 作为标识符,适用于接口抽象(推荐用于插件和跨包场景)
```typescript
// 方式1: 使用类作为标识符
Core.services.registerSingleton(DataService);
const data = Core.services.resolve(DataService);
// 方式2: 使用 Symbol 作为标识符(推荐用于接口)
const IFileSystem = Symbol.for('IFileSystem');
Core.services.registerInstance(IFileSystem, new TauriFileSystem());
const fs = Core.services.resolve<IFileSystem>(IFileSystem);
```
> **提示**: 使用 `Symbol.for()` 而非 `Symbol()` 可确保跨包/跨模块共享同一个标识符。详见[高级用法 - 接口与 Symbol 标识符模式](#接口与-symbol-标识符模式)。
#### 生命周期
服务容器支持两种生命周期:
- **Singleton单例**: 整个应用程序生命周期内只有一个实例,所有解析请求返回同一个实例
- **Transient瞬时**: 每次解析都创建新的实例
## 基础使用
### 访问服务容器
ECS Framework 提供了三级服务容器:
> **版本说明**World 服务容器功能在 v2.2.13+ 版本中可用
#### Core 级别服务容器
应用程序全局服务容器,可以通过 `Core.services` 访问:
```typescript
import { Core } from '@esengine/ecs-framework';
// 初始化Core
Core.create({ debug: true });
// 访问全局服务容器
const container = Core.services;
```
#### World 级别服务容器
每个 World 拥有独立的服务容器,用于管理 World 范围内的服务:
```typescript
import { World } from '@esengine/ecs-framework';
// 创建 World
const world = new World({ name: 'GameWorld' });
// 访问 World 级别的服务容器
const worldContainer = world.services;
// 注册 World 级别的服务
world.services.registerSingleton(RoomManager);
```
#### Scene 级别服务容器
每个 Scene 拥有独立的服务容器,用于管理 Scene 范围内的服务:
```typescript
// 访问 Scene 级别的服务容器
const sceneContainer = scene.services;
// 注册 Scene 级别的服务
scene.services.registerSingleton(PhysicsSystem);
```
#### 服务容器层级
```
Core.services (应用程序全局)
└─ World.services (World 级别)
└─ Scene.services (Scene 级别)
```
不同级别的服务容器是独立的,服务不会自动向上或向下查找。选择合适的容器级别:
- **Core.services**: 应用程序级别的全局服务(配置、插件管理器等)
- **World.services**: World 级别的服务(房间管理器、多人游戏状态等)
- **Scene.services**: Scene 级别的服务ECS 系统、场景特定逻辑等)
### 注册服务
#### 注册单例服务
单例服务在首次解析时创建,之后所有解析请求都返回同一个实例:
```typescript
class DataService implements IService {
private data: Map<string, any> = new Map();
getData(key: string) {
return this.data.get(key);
}
setData(key: string, value: any) {
this.data.set(key, value);
}
dispose(): void {
this.data.clear();
}
}
// 注册单例服务
Core.services.registerSingleton(DataService);
```
#### 注册瞬时服务
瞬时服务每次解析都创建新实例,适用于无状态或短生命周期的服务:
```typescript
class CommandService implements IService {
execute(command: string) {
console.log(`Executing: ${command}`);
}
dispose(): void {
// 清理资源
}
}
// 注册瞬时服务
Core.services.registerTransient(CommandService);
```
#### 注册服务实例
直接注册已创建的实例,自动视为单例:
```typescript
const config = new ConfigService();
config.load('./config.json');
// 注册实例
Core.services.registerInstance(ConfigService, config);
```
#### 使用工厂函数注册
工厂函数允许你在创建服务时执行自定义逻辑:
```typescript
Core.services.registerSingleton(LoggerService, (container) => {
const logger = new LoggerService();
logger.setLevel('debug');
return logger;
});
```
### 解析服务
#### resolve 方法
解析服务实例,如果服务未注册会抛出异常:
```typescript
// 解析服务
const dataService = Core.services.resolve(DataService);
dataService.setData('player', { name: 'Alice', score: 100 });
// 单例服务,多次解析返回同一个实例
const same = Core.services.resolve(DataService);
console.log(same === dataService); // true
```
#### tryResolve 方法
尝试解析服务,如果未注册返回 null 而不抛出异常:
```typescript
const optional = Core.services.tryResolve(OptionalService);
if (optional) {
optional.doSomething();
}
```
#### isRegistered 方法
检查服务是否已注册:
```typescript
if (Core.services.isRegistered(DataService)) {
const service = Core.services.resolve(DataService);
}
```
## 内置服务
Core 在初始化时自动注册了以下内置服务:
### TimerManager
定时器管理器,负责管理所有游戏定时器:
```typescript
const timerManager = Core.services.resolve(TimerManager);
// 创建定时器
timerManager.schedule(1.0, false, null, (timer) => {
console.log('1秒后执行');
});
```
### PerformanceMonitor
性能监控器,监控游戏性能并提供优化建议:
```typescript
const monitor = Core.services.resolve(PerformanceMonitor);
// 启用性能监控
monitor.enable();
// 获取性能数据
const fps = monitor.getFPS();
```
### SceneManager
场景管理器,管理单场景应用的场景生命周期:
```typescript
const sceneManager = Core.services.resolve(SceneManager);
// 设置当前场景
sceneManager.setScene(new GameScene());
// 获取当前场景
const currentScene = sceneManager.currentScene;
// 延迟切换场景
sceneManager.loadScene(new MenuScene());
// 更新场景
sceneManager.update();
```
### WorldManager
世界管理器,管理多个独立的 World 实例(高级用例):
```typescript
const worldManager = Core.services.resolve(WorldManager);
// 创建独立的游戏世界
const gameWorld = worldManager.createWorld('game_room_001', {
name: 'GameRoom',
maxScenes: 5
});
// 在World中创建场景
const scene = gameWorld.createScene('battle', new BattleScene());
gameWorld.setSceneActive('battle', true);
// 更新所有World
worldManager.updateAll();
```
**适用场景**:
- SceneManager: 适用于 95% 的游戏(单人游戏、简单多人游戏)
- WorldManager: 适用于 MMO 服务器、游戏房间系统等需要完全隔离的多世界应用
### PoolManager
对象池管理器,管理所有对象池:
```typescript
const poolManager = Core.services.resolve(PoolManager);
// 创建对象池
const bulletPool = poolManager.createPool('bullets', () => new Bullet(), 100);
```
### PluginManager
插件管理器,管理插件的安装和卸载:
```typescript
const pluginManager = Core.services.resolve(PluginManager);
// 获取所有已安装的插件
const plugins = pluginManager.getAllPlugins();
```
## 依赖注入
ECS Framework 提供了装饰器来简化依赖注入。
### @Injectable 装饰器
标记类为可注入的服务:
```typescript
import { Injectable, IService } from '@esengine/ecs-framework';
@Injectable()
class GameService implements IService {
constructor() {
console.log('GameService created');
}
dispose(): void {
console.log('GameService disposed');
}
}
```
### @InjectProperty 装饰器
通过属性装饰器注入依赖。注入时机是在构造函数执行后、`onInitialize()` 调用前完成:
```typescript
import { Injectable, InjectProperty, IService } from '@esengine/ecs-framework';
@Injectable()
class PlayerService implements IService {
@InjectProperty(DataService)
private data!: DataService;
@InjectProperty(GameService)
private game!: GameService;
dispose(): void {
// 清理资源
}
}
```
在 EntitySystem 中使用属性注入:
```typescript
@Injectable()
class CombatSystem extends EntitySystem {
@InjectProperty(TimeService)
private timeService!: TimeService;
@InjectProperty(AudioService)
private audio!: AudioService;
constructor() {
super(Matcher.all(Health, Attack));
}
onInitialize(): void {
// 此时属性已注入完成,可以安全使用
console.log('Delta time:', this.timeService.getDeltaTime());
}
processEntity(entity: Entity): void {
// 使用注入的服务
this.audio.playSound('attack');
}
}
```
> **注意**: 属性声明时使用 `!` 断言(如 `private data!: DataService`),表示该属性会在使用前被注入。
### 注册可注入服务
使用 `registerInjectable` 自动处理依赖注入:
```typescript
import { registerInjectable } from '@esengine/ecs-framework';
// 注册服务(会自动解析 @InjectProperty 依赖)
registerInjectable(Core.services, PlayerService);
// 解析时会自动注入属性依赖
const player = Core.services.resolve(PlayerService);
```
### @Updatable 装饰器
标记服务为可更新的,使其在每帧自动被调用:
```typescript
import { Injectable, Updatable, IService, IUpdatable } from '@esengine/ecs-framework';
@Injectable()
@Updatable() // 默认优先级为0
class PhysicsService implements IService, IUpdatable {
update(deltaTime?: number): void {
// 每帧更新物理模拟
}
dispose(): void {
// 清理资源
}
}
// 指定更新优先级(数值越小越先执行)
@Injectable()
@Updatable(10)
class RenderService implements IService, IUpdatable {
update(deltaTime?: number): void {
// 每帧渲染
}
dispose(): void {
// 清理资源
}
}
```
使用 `@Updatable` 装饰器的服务会被 Core 自动调用,无需手动管理:
```typescript
// Core.update() 会自动调用所有@Updatable服务的update方法
function gameLoop(deltaTime: number) {
Core.update(deltaTime); // 自动更新所有可更新服务
}
```
## 自定义服务
### 创建自定义服务
实现 `IService` 接口并注册到容器:
```typescript
import { IService } from '@esengine/ecs-framework';
class AudioService implements IService {
private sounds: Map<string, HTMLAudioElement> = new Map();
play(soundId: string) {
const sound = this.sounds.get(soundId);
if (sound) {
sound.play();
}
}
load(soundId: string, url: string) {
const audio = new Audio(url);
this.sounds.set(soundId, audio);
}
dispose(): void {
// 停止所有音效并清理
for (const sound of this.sounds.values()) {
sound.pause();
sound.src = '';
}
this.sounds.clear();
}
}
// 注册自定义服务
Core.services.registerSingleton(AudioService);
// 使用服务
const audio = Core.services.resolve(AudioService);
audio.load('jump', '/sounds/jump.mp3');
audio.play('jump');
```
### 服务间依赖
服务可以依赖其他服务:
```typescript
@Injectable()
class ConfigService implements IService {
private config: any = {};
get(key: string) {
return this.config[key];
}
dispose(): void {
this.config = {};
}
}
@Injectable()
class NetworkService implements IService {
constructor(
@Inject(ConfigService) private config: ConfigService
) {
// 使用配置服务
const apiUrl = this.config.get('apiUrl');
}
dispose(): void {
// 清理网络连接
}
}
// 注册服务(按依赖顺序)
registerInjectable(Core.services, ConfigService);
registerInjectable(Core.services, NetworkService);
```
## 高级用法
### 接口与 Symbol 标识符模式
在大型项目或需要跨平台适配的游戏中,推荐使用"接口 + Symbol.for 标识符"模式。这种模式实现了真正的依赖倒置,让代码依赖于抽象而非具体实现。
#### 为什么使用 Symbol.for
- **跨包共享**: `Symbol.for('key')` 在全局 Symbol 注册表中创建/获取 Symbol确保不同包中使用相同的标识符
- **接口解耦**: 消费者只依赖接口定义,不依赖具体实现类
- **可替换实现**: 可以在运行时注入不同的实现(如测试 Mock、不同平台适配
#### 定义接口和标识符
以音频服务为例游戏需要在不同平台Web、微信小游戏、原生App使用不同的音频实现
```typescript
// IAudioService.ts - 定义接口和标识符
export interface IAudioService {
dispose(): void;
playSound(id: string): void;
playMusic(id: string, loop?: boolean): void;
stopMusic(): void;
setVolume(volume: number): void;
preload(id: string, url: string): Promise<void>;
}
// 使用 Symbol.for 确保跨包共享同一个 Symbol
export const IAudioService = Symbol.for('IAudioService');
```
#### 实现接口
```typescript
// WebAudioService.ts - Web 平台实现
import { IAudioService } from './IAudioService';
export class WebAudioService implements IAudioService {
private audioContext: AudioContext;
private sounds: Map<string, AudioBuffer> = new Map();
constructor() {
this.audioContext = new AudioContext();
}
playSound(id: string): void {
const buffer = this.sounds.get(id);
if (buffer) {
const source = this.audioContext.createBufferSource();
source.buffer = buffer;
source.connect(this.audioContext.destination);
source.start();
}
}
async preload(id: string, url: string): Promise<void> {
const response = await fetch(url);
const arrayBuffer = await response.arrayBuffer();
const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
this.sounds.set(id, audioBuffer);
}
// ... 其他方法实现
dispose(): void {
this.audioContext.close();
this.sounds.clear();
}
}
```
```typescript
// WechatAudioService.ts - 微信小游戏平台实现
export class WechatAudioService implements IAudioService {
private innerAudioContexts: Map<string, WechatMinigame.InnerAudioContext> = new Map();
playSound(id: string): void {
const ctx = this.innerAudioContexts.get(id);
if (ctx) {
ctx.play();
}
}
async preload(id: string, url: string): Promise<void> {
const ctx = wx.createInnerAudioContext();
ctx.src = url;
this.innerAudioContexts.set(id, ctx);
}
// ... 其他方法实现
dispose(): void {
for (const ctx of this.innerAudioContexts.values()) {
ctx.destroy();
}
this.innerAudioContexts.clear();
}
}
```
#### 注册和使用
```typescript
import { IAudioService } from './IAudioService';
import { WebAudioService } from './WebAudioService';
import { WechatAudioService } from './WechatAudioService';
// 根据平台注册不同实现
if (typeof wx !== 'undefined') {
Core.services.registerInstance(IAudioService, new WechatAudioService());
} else {
Core.services.registerInstance(IAudioService, new WebAudioService());
}
// 业务代码中使用 - 不关心具体实现
const audio = Core.services.resolve<IAudioService>(IAudioService);
await audio.preload('explosion', '/sounds/explosion.mp3');
audio.playSound('explosion');
```
#### 跨模块使用
```typescript
// 在游戏系统中使用
import { IAudioService } from '@mygame/core';
class CombatSystem extends EntitySystem {
private audio: IAudioService;
initialize(): void {
// 获取音频服务,不需要知道具体实现
this.audio = this.scene.services.resolve<IAudioService>(IAudioService);
}
onEntityDeath(entity: Entity): void {
this.audio.playSound('death');
}
}
```
#### Symbol vs Symbol.for
```typescript
// Symbol() - 每次创建唯一的 Symbol
const sym1 = Symbol('test');
const sym2 = Symbol('test');
console.log(sym1 === sym2); // false - 不同的 Symbol
// Symbol.for() - 在全局注册表中共享
const sym3 = Symbol.for('test');
const sym4 = Symbol.for('test');
console.log(sym3 === sym4); // true - 同一个 Symbol
// 跨包场景
// package-a/index.ts
export const IMyService = Symbol.for('IMyService');
// package-b/index.ts (不同的包)
const IMyService = Symbol.for('IMyService');
// 与 package-a 中的是同一个 Symbol
```
### 循环依赖检测
服务容器会自动检测循环依赖:
```typescript
// A 依赖 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) - 在系统中使用服务

View File

@@ -157,8 +157,45 @@ const nameMatcher = Matcher.byName("Player"); // 匹配名称为 "Player" 的实
// 单组件匹配
const componentMatcher = Matcher.byComponent(Health); // 匹配拥有 Health 组件的实体
// 不匹配任何实体
const nothingMatcher = Matcher.nothing(); // 用于只需要生命周期回调的系统
```
### 空匹配器 vs Nothing 匹配器
```typescript
// empty() - 空条件,匹配所有实体
const emptyMatcher = Matcher.empty();
// nothing() - 不匹配任何实体,用于只需要生命周期方法的系统
const nothingMatcher = Matcher.nothing();
// 使用场景:只需要 onBegin/onEnd 生命周期的系统
@ECSSystem('FrameTimer')
class FrameTimerSystem extends EntitySystem {
constructor() {
super(Matcher.nothing()); // 不处理任何实体
}
protected onBegin(): void {
// 每帧开始时执行,例如:记录帧开始时间
console.log('帧开始');
}
protected process(entities: readonly Entity[]): void {
// 永远不会被调用,因为没有匹配的实体
}
protected onEnd(): void {
// 每帧结束时执行
console.log('帧结束');
}
}
```
> 💡 **提示**:更多关于 Matcher 和实体查询的详细用法,请参考 [实体查询系统](/guide/entity-query) 文档。
## 系统生命周期
系统提供了完整的生命周期回调:
@@ -354,14 +391,18 @@ class PerformanceSystem extends EntitySystem {
### 添加系统到场景
框架提供了两种方式添加系统:传入实例或传入类型(自动依赖注入)。
```typescript
// 在场景子类中添加系统
class GameScene extends Scene {
protected initialize(): void {
// 添加系统
// 方式1传入实例
this.addSystem(new MovementSystem());
this.addSystem(new RenderSystem());
this.addSystem(new PhysicsSystem());
// 方式2传入类型自动依赖注入
this.addEntityProcessor(PhysicsSystem);
// 设置系统更新顺序
const movementSystem = this.getSystem(MovementSystem);
@@ -372,6 +413,48 @@ class GameScene extends Scene {
}
```
### 系统依赖注入
系统实现了 `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
@@ -517,9 +600,28 @@ class GameSystem extends EntitySystem {
}
```
### 2. 使用装饰器
### 2. 使用 @ECSSystem 装饰器
**必须使用 `@ECSSystem` 装饰器**
`@ECSSystem` 是系统类必须使用的装饰器,它为系统提供类型标识和元数据管理。
#### 为什么必须使用
| 功能 | 说明 |
|------|------|
| **类型识别** | 提供稳定的系统名称,代码混淆后仍能正确识别 |
| **调试支持** | 在性能监控、日志和调试工具中显示可读的系统名称 |
| **系统管理** | 通过名称查找和管理系统 |
| **序列化支持** | 场景序列化时可以记录系统配置 |
#### 基本语法
```typescript
@ECSSystem(systemName: string)
```
- `systemName`: 系统的名称,建议使用描述性的名称
#### 使用示例
```typescript
// ✅ 正确的用法
@@ -528,12 +630,41 @@ class PhysicsSystem extends EntitySystem {
// 系统实现
}
// ✅ 推荐:使用描述性的名称
@ECSSystem('PlayerMovement')
class PlayerMovementSystem extends EntitySystem {
constructor() {
super(Matcher.all(Player, Position, Velocity));
}
}
// ❌ 错误的用法 - 没有装饰器
class BadSystem extends EntitySystem {
// 这样定义的系统可能在生产环境出现问题
// 这样定义的系统可能在生产环境出现问题
// 1. 代码压缩后类名变化,无法正确识别
// 2. 性能监控和调试工具显示不正确的名称
}
```
#### 系统名称的作用
```typescript
@ECSSystem('Combat')
class CombatSystem extends EntitySystem {
protected onInitialize(): void {
// 使用 systemName 属性访问系统名称
console.log(`系统 ${this.systemName} 已初始化`); // 输出: 系统 Combat 已初始化
}
}
// 通过名称查找系统
const combat = scene.getSystemByName('Combat');
// 性能监控中会显示系统名称
const perfData = combatSystem.getPerformanceData();
console.log(`${combatSystem.systemName} 执行时间: ${perfData?.executionTime}ms`);
```
### 3. 合理的更新顺序
```typescript

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,
// cleanupFrameInterval: 1800 // 间隔多少帧清理闲置 World
// }
```
### 手动清理
```typescript
// 手动触发清理
const cleanedCount = worldManager.cleanup();
console.log(`清理了 ${cleanedCount} 个 World`);
```
**清理条件**
- World 未激活
- 没有 Scene 或所有 Scene 都是空的
- 创建时间超过 10 分钟
## API 参考
### WorldManager API
| 方法 | 说明 |
|------|------|
| `createWorld(worldId, config?)` | 创建新 World |
| `removeWorld(worldId)` | 移除 World |
| `getWorld(worldId)` | 获取 World |
| `getAllWorlds()` | 获取所有 World |
| `getWorldIds()` | 获取所有 World ID |
| `setWorldActive(worldId, active)` | 设置 World 激活状态 |
| `isWorldActive(worldId)` | 检查 World 是否激活 |
| `getActiveWorlds()` | 获取所有活跃的 World |
| `updateAll()` | 更新所有活跃 World |
| `startAll()` | 启动所有 World |
| `stopAll()` | 停止所有 World |
| `findWorlds(predicate)` | 查找满足条件的 World |
| `findWorldByName(name)` | 根据名称查找 World |
| `getStats()` | 获取统计信息 |
| `getDetailedStatus()` | 获取详细状态信息 |
| `cleanup()` | 清理空 World |
| `destroy()` | 销毁 WorldManager |
### World API
| 方法 | 说明 |
|------|------|
| `createScene(sceneId, sceneInstance?)` | 创建并添加 Scene |
| `removeScene(sceneId)` | 移除 Scene |
| `getScene(sceneId)` | 获取 Scene |
| `getAllScenes()` | 获取所有 Scene |
| `getSceneIds()` | 获取所有 Scene ID |
| `setSceneActive(sceneId, active)` | 设置 Scene 激活状态 |
| `isSceneActive(sceneId)` | 检查 Scene 是否激活 |
| `getActiveSceneCount()` | 获取活跃 Scene 数量 |
| `addGlobalSystem(system)` | 添加全局系统 |
| `removeGlobalSystem(system)` | 移除全局系统 |
| `getGlobalSystem(type)` | 获取全局系统 |
| `start()` | 启动 World |
| `stop()` | 停止 World |
| `updateGlobalSystems()` | 更新全局系统 |
| `updateScenes()` | 更新所有激活 Scene |
| `destroy()` | 销毁 World |
| `getStatus()` | 获取 World 状态 |
| `getStats()` | 获取统计信息 |
### 属性
| 属性 | 说明 |
|------|------|
| `worldCount` | World 总数 |
| `activeWorldCount` | 活跃 World 数量 |
| `isRunning` | 是否正在运行 |
| `config` | 配置信息 |
## 完整示例
### MMO 游戏房间系统
```typescript
import { Core, WorldManager, Scene, World } from '@esengine/ecs-framework';
// 初始化
Core.create({ debug: true });
const worldManager = Core.services.resolve(WorldManager);
// 房间管理器
class RoomManager {
private worldManager: WorldManager;
private rooms: Map<string, World> = new Map();
constructor(worldManager: WorldManager) {
this.worldManager = worldManager;
}
// 创建游戏房间
public createRoom(roomId: string, maxPlayers: number): World {
const world = this.worldManager.createWorld(roomId, {
name: `Room_${roomId}`,
maxScenes: 3,
debug: true
});
// 创建房间场景
world.createScene('lobby', new LobbyScene());
world.createScene('game', new GameScene());
world.createScene('result', new ResultScene());
// 添加房间级别的系统
world.addGlobalSystem(new NetworkSystem(roomId));
world.addGlobalSystem(new RoomLogicSystem(maxPlayers));
// 激活 World 和初始场景
this.worldManager.setWorldActive(roomId, true);
world.setSceneActive('lobby', true);
this.rooms.set(roomId, world);
console.log(`房间 ${roomId} 已创建`);
return world;
}
// 玩家加入房间
public joinRoom(roomId: string, playerId: string): boolean {
const world = this.rooms.get(roomId);
if (!world) {
console.log(`房间 ${roomId} 不存在`);
return false;
}
// 在大厅场景中创建玩家实体
const lobbyScene = world.getScene('lobby');
if (lobbyScene) {
const player = lobbyScene.createEntity(`Player_${playerId}`);
// 添加玩家组件...
console.log(`玩家 ${playerId} 加入房间 ${roomId}`);
return true;
}
return false;
}
// 开始游戏
public startGame(roomId: string): void {
const world = this.rooms.get(roomId);
if (!world) return;
// 切换到游戏场景
world.setSceneActive('lobby', false);
world.setSceneActive('game', true);
console.log(`房间 ${roomId} 游戏开始`);
}
// 结束游戏
public endGame(roomId: string): void {
const world = this.rooms.get(roomId);
if (!world) return;
// 切换到结果场景
world.setSceneActive('game', false);
world.setSceneActive('result', true);
console.log(`房间 ${roomId} 游戏结束`);
}
// 关闭房间
public closeRoom(roomId: string): void {
this.worldManager.removeWorld(roomId);
this.rooms.delete(roomId);
console.log(`房间 ${roomId} 已关闭`);
}
// 获取房间列表
public getRoomList(): string[] {
return Array.from(this.rooms.keys());
}
// 获取房间统计
public getRoomStats(roomId: string) {
const world = this.rooms.get(roomId);
return world?.getStats();
}
}
// 使用房间管理器
const roomManager = new RoomManager(worldManager);
// 创建多个游戏房间
roomManager.createRoom('room_001', 4);
roomManager.createRoom('room_002', 4);
roomManager.createRoom('room_003', 2);
// 玩家加入
roomManager.joinRoom('room_001', 'player_1');
roomManager.joinRoom('room_001', 'player_2');
// 开始游戏
roomManager.startGame('room_001');
// 游戏循环
function gameLoop(deltaTime: number) {
Core.update(deltaTime);
worldManager.updateAll(); // 更新所有房间
}
// 定期清理空房间
setInterval(() => {
const stats = worldManager.getStats();
console.log(`当前房间数: ${stats.totalWorlds}`);
console.log(`活跃房间数: ${stats.activeWorlds}`);
worldManager.cleanup();
}, 60000); // 每分钟清理一次
```
## 最佳实践
### 1. 合理的 World 粒度
```typescript
// 推荐:每个独立环境一个 World
const room1 = worldManager.createWorld('room_1'); // 游戏房间1
const room2 = worldManager.createWorld('room_2'); // 游戏房间2
// 不推荐:过度使用 World
const world1 = worldManager.createWorld('ui'); // UI 不需要独立 World
const world2 = worldManager.createWorld('menu'); // 菜单不需要独立 World
```
### 2. 使用全局系统处理跨场景逻辑
```typescript
// 推荐World 级别的系统
class NetworkSystem implements IGlobalSystem {
update() {
// 网络处理不依赖场景
}
}
// 不推荐:在每个场景中重复创建
class GameScene extends Scene {
initialize() {
this.addSystem(new NetworkSystem()); // 不应该在场景级别
}
}
```
### 3. 及时清理不用的 World
```typescript
// 推荐:玩家离开时清理房间
function onPlayerLeave(roomId: string) {
const world = worldManager.getWorld(roomId);
if (world && world.sceneCount === 0) {
worldManager.removeWorld(roomId);
}
}
// 或使用自动清理
worldManager.cleanup();
```
### 4. 监控资源使用
```typescript
// 定期检查资源使用情况
setInterval(() => {
const stats = worldManager.getStats();
if (stats.totalWorlds > 100) {
console.warn('World 数量过多,考虑清理');
worldManager.cleanup();
}
if (stats.totalEntities > 10000) {
console.warn('实体数量过多,检查是否有泄漏');
}
}, 30000);
```
## 与 SceneManager 的对比
| 特性 | SceneManager | WorldManager |
|------|--------------|--------------|
| 适用场景 | 95% 的游戏应用 | 高级多世界隔离场景 |
| 复杂度 | 简单 | 复杂 |
| 场景数量 | 单场景(可切换) | 多 World每个 World 多场景 |
| 场景隔离 | 无(场景切换) | 完全隔离(每个 World 独立) |
| 性能开销 | 最小 | 较高 |
| 全局系统 | 无 | 支持World 级别) |
| 使用示例 | 单人游戏、移动游戏 | MMO 服务器、游戏房间系统 |
**何时使用 WorldManager**
- MMO 游戏服务器(每个房间一个 World
- 游戏大厅系统(每个游戏房间完全隔离)
- 需要运行多个完全独立的游戏实例
- 服务器端模拟多个游戏世界
**何时使用 SceneManager**
- 单人游戏
- 简单的多人游戏
- 移动游戏
- 场景之间需要切换但不需要同时运行
## 架构层次
WorldManager 在 ECS Framework 中的位置:
```
Core (全局服务)
└── WorldManager (世界管理)
├── World 1 (游戏房间1)
│ ├── GlobalSystem (全局系统)
│ ├── Scene 1 (场景1)
│ │ ├── EntitySystem
│ │ ├── Entity
│ │ └── Component
│ └── Scene 2 (场景2)
├── World 2 (游戏房间2)
│ ├── GlobalSystem
│ └── Scene 1
└── World 3 (游戏房间3)
```
WorldManager 为需要多世界隔离的高级应用提供了强大的管理能力。如果你的应用不需要多世界隔离,建议使用更简单的 [SceneManager](./scene-manager.md)。

View File

@@ -1,23 +1,317 @@
---
layout: home
layout: page
title: ESEngine - 高性能 TypeScript ECS 框架
---
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
<ParticleHero />
features:
- title: 高性能
details: 支持大规模实体处理
- title: 类型安全
details: 完整的TypeScript支持编译时类型检查
- title: 模块化设计
details: 核心功能独立打包,支持多平台
---
<section class="news-section">
<div class="news-container">
<div class="news-header">
<h2 class="news-title">快速入口</h2>
<a href="/guide/" class="news-more">查看文档</a>
</div>
<div class="news-grid">
<a href="/guide/getting-started" class="news-card">
<div class="news-card-image" style="background: linear-gradient(135deg, #1e3a5f 0%, #1e1e1e 100%);">
<div class="news-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24"><path fill="#4fc1ff" d="M12 3L1 9l4 2.18v6L12 21l7-3.82v-6l2-1.09V17h2V9zm6.82 6L12 12.72L5.18 9L12 5.28zM17 16l-5 2.72L7 16v-3.73L12 15l5-2.73z"/></svg>
</div>
<span class="news-badge">快速开始</span>
</div>
<div class="news-card-content">
<h3>5 分钟上手 ESEngine</h3>
<p>从安装到创建第一个 ECS 应用,快速了解核心概念。</p>
</div>
</a>
<a href="/guide/behavior-tree/" class="news-card">
<div class="news-card-image" style="background: linear-gradient(135deg, #1e3a5f 0%, #1e1e1e 100%);">
<div class="news-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24"><path fill="#4ec9b0" d="M12 2a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m3 20h-1v-7l-2-2l-2 2v7H9v-7.5l-2 2V22H6v-6l3-3l1-3.5c-.3.4-.6.7-1 1L6 9v1H4V8l5-3c.5-.3 1.1-.5 1.7-.5H11c.6 0 1.2.2 1.7.5l5 3v2h-2V9l-3 1.5c-.4-.3-.7-.6-1-1l1 3.5l3 3v6Z"/></svg>
</div>
<span class="news-badge">AI 系统</span>
</div>
<div class="news-card-content">
<h3>行为树可视化编辑器</h3>
<p>内置 AI 行为树系统,支持可视化编辑和实时调试。</p>
</div>
</a>
</div>
</div>
</section>
<section class="features-section">
<div class="features-container">
<h2 class="features-title">核心特性</h2>
<div class="features-grid">
<div class="feature-card">
<div class="feature-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#4fc1ff" d="M13 2.05v2.02c3.95.49 7 3.85 7 7.93c0 1.45-.39 2.79-1.06 3.95l1.59 1.09A9.94 9.94 0 0 0 22 12c0-5.18-3.95-9.45-9-9.95M12 19c-3.87 0-7-3.13-7-7c0-3.53 2.61-6.43 6-6.92V2.05c-5.06.5-9 4.76-9 9.95c0 5.52 4.47 10 9.99 10c3.31 0 6.24-1.61 8.06-4.09l-1.6-1.1A7.93 7.93 0 0 1 12 19"/><path fill="#4fc1ff" d="M12 6a6 6 0 0 0-6 6c0 3.31 2.69 6 6 6a6 6 0 0 0 0-12m0 10c-2.21 0-4-1.79-4-4s1.79-4 4-4s4 1.79 4 4s-1.79 4-4 4"/></svg>
</div>
<h3 class="feature-title">高性能 ECS 架构</h3>
<p class="feature-desc">基于数据驱动的实体组件系统,支持大规模实体处理,缓存友好的内存布局。</p>
<a href="/guide/entity" class="feature-link">了解更多 →</a>
</div>
<div class="feature-card">
<div class="feature-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#569cd6" d="M3 3h18v18H3zm16.525 13.707c0-.795-.272-1.425-.816-1.89c-.544-.465-1.404-.804-2.58-1.016l-1.704-.296c-.616-.104-1.052-.26-1.308-.468c-.256-.21-.384-.468-.384-.776c0-.392.168-.7.504-.924c.336-.224.8-.336 1.392-.336c.56 0 1.008.124 1.344.372c.336.248.536.584.6 1.008h2.016c-.08-.96-.464-1.716-1.152-2.268c-.688-.552-1.6-.828-2.736-.828c-1.2 0-2.148.3-2.844.9c-.696.6-1.044 1.38-1.044 2.34c0 .76.252 1.368.756 1.824c.504.456 1.308.792 2.412.996l1.704.312c.624.12 1.068.28 1.332.48c.264.2.396.46.396.78c0 .424-.192.756-.576.996c-.384.24-.9.36-1.548.36c-.672 0-1.2-.14-1.584-.42c-.384-.28-.608-.668-.672-1.164H8.868c.048 1.016.46 1.808 1.236 2.376c.776.568 1.796.852 3.06.852c1.24 0 2.22-.292 2.94-.876c.72-.584 1.08-1.364 1.08-2.34z"/></svg>
</div>
<h3 class="feature-title">完整类型支持</h3>
<p class="feature-desc">100% TypeScript 编写,完整的类型定义和编译时检查,提供最佳的开发体验。</p>
<a href="/guide/component" class="feature-link">了解更多 →</a>
</div>
<div class="feature-card">
<div class="feature-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#4ec9b0" d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10s10-4.5 10-10S17.5 2 12 2m0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8s8 3.59 8 8s-3.59 8-8 8m-5-8l4-4v3h4v2h-4v3z"/></svg>
</div>
<h3 class="feature-title">可视化行为树</h3>
<p class="feature-desc">内置 AI 行为树系统,提供可视化编辑器,支持自定义节点和实时调试。</p>
<a href="/guide/behavior-tree/" class="feature-link">了解更多 →</a>
</div>
<div class="feature-card">
<div class="feature-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#c586c0" d="M4 6h18V4H4c-1.1 0-2 .9-2 2v11H0v3h14v-3H4zm19 2h-6c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h6c.55 0 1-.45 1-1V9c0-.55-.45-1-1-1m-1 9h-4v-7h4z"/></svg>
</div>
<h3 class="feature-title">多平台支持</h3>
<p class="feature-desc">支持浏览器、Node.js、微信小游戏等多平台可与主流游戏引擎无缝集成。</p>
<a href="/guide/platform-adapter" class="feature-link">了解更多 →</a>
</div>
<div class="feature-card">
<div class="feature-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#dcdcaa" d="M4 3h6v2H4v14h6v2H4c-1.1 0-2-.9-2-2V5c0-1.1.9-2 2-2m9 0h6c1.1 0 2 .9 2 2v14c0 1.1-.9 2-2 2h-6v-2h6V5h-6zm-1 7h4v2h-4z"/></svg>
</div>
<h3 class="feature-title">模块化设计</h3>
<p class="feature-desc">核心功能独立打包,按需引入。支持自定义插件扩展,灵活适配不同项目。</p>
<a href="/guide/plugin-system" class="feature-link">了解更多 →</a>
</div>
<div class="feature-card">
<div class="feature-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#9cdcfe" d="M22.7 19l-9.1-9.1c.9-2.3.4-5-1.5-6.9c-2-2-5-2.4-7.4-1.3L9 6L6 9L1.6 4.7C.4 7.1.9 10.1 2.9 12.1c1.9 1.9 4.6 2.4 6.9 1.5l9.1 9.1c.4.4 1 .4 1.4 0l2.3-2.3c.5-.4.5-1.1.1-1.4"/></svg>
</div>
<h3 class="feature-title">开发者工具</h3>
<p class="feature-desc">内置性能监控、调试工具、序列化系统等,提供完整的开发工具链。</p>
<a href="/guide/logging" class="feature-link">了解更多 →</a>
</div>
</div>
</div>
</section>
<style scoped>
/* 首页专用样式 | Home page specific styles */
.news-section {
background: #0d0d0d;
padding: 64px 0;
border-top: 1px solid #2a2a2a;
}
.news-container {
max-width: 1400px;
margin: 0 auto;
padding: 0 48px;
}
.news-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
}
.news-title {
font-size: 1.5rem;
font-weight: 700;
color: #ffffff;
margin: 0;
}
.news-more {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
background: #1a1a1a;
border: 1px solid #2a2a2a;
border-radius: 6px;
color: #a0a0a0;
font-size: 0.875rem;
font-weight: 500;
text-decoration: none;
transition: all 0.2s;
}
.news-more:hover {
background: #252525;
color: #ffffff;
}
.news-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24px;
}
.news-card {
display: flex;
background: #1f1f1f;
border: 1px solid #2a2a2a;
border-radius: 12px;
overflow: hidden;
text-decoration: none;
transition: all 0.2s;
}
.news-card:hover {
border-color: #3b9eff;
}
.news-card-image {
width: 200px;
min-height: 140px;
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 16px;
gap: 12px;
}
.news-icon {
opacity: 0.9;
}
.news-badge {
display: inline-block;
padding: 4px 12px;
background: transparent;
border: 1px solid #3a3a3a;
border-radius: 16px;
color: #a0a0a0;
font-size: 0.75rem;
font-weight: 500;
}
.news-card-content {
padding: 20px;
display: flex;
flex-direction: column;
justify-content: center;
}
.news-card-content h3 {
font-size: 1.125rem;
font-weight: 600;
color: #ffffff;
margin: 0 0 8px 0;
}
.news-card-content p {
font-size: 0.875rem;
color: #707070;
margin: 0;
line-height: 1.6;
}
.features-section {
background: #0d0d0d;
padding: 64px 0;
}
.features-container {
max-width: 1400px;
margin: 0 auto;
padding: 0 48px;
}
.features-title {
font-size: 1.5rem;
font-weight: 700;
color: #ffffff;
margin: 0 0 32px 0;
}
.features-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
.feature-card {
background: #1f1f1f;
border: 1px solid #2a2a2a;
border-radius: 12px;
padding: 24px;
transition: all 0.15s ease;
}
.feature-card:hover {
border-color: #3b9eff;
background: #252525;
}
.feature-icon {
width: 48px;
height: 48px;
background: #0d0d0d;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16px;
}
.feature-title {
font-size: 16px;
font-weight: 600;
color: #ffffff;
margin: 0 0 8px 0;
}
.feature-desc {
font-size: 14px;
color: #707070;
line-height: 1.7;
margin: 0 0 16px 0;
}
.feature-link {
font-size: 14px;
color: #3b9eff;
text-decoration: none;
font-weight: 500;
}
.feature-link:hover {
text-decoration: underline;
}
@media (max-width: 1024px) {
.news-container,
.features-container {
padding: 0 24px;
}
.news-grid {
grid-template-columns: 1fr;
}
.features-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.news-card {
flex-direction: column;
}
.news-card-image {
width: 100%;
min-height: 120px;
}
.features-grid {
grid-template-columns: 1fr;
}
}
</style>

1
docs/public/CNAME Normal file
View File

@@ -0,0 +1 @@
esengine.cn

72
eslint.config.mjs Normal file
View File

@@ -0,0 +1,72 @@
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',
'**/wasm/**'
]
}
];

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>

View File

@@ -1,11 +1,11 @@
{
"name": "ecs-worker-system-demo",
"name": "ecs-core-demos",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ecs-worker-system-demo",
"name": "ecs-core-demos",
"version": "1.0.0",
"dependencies": {
"@esengine/ecs-framework": "file:../../packages/core"
@@ -17,8 +17,11 @@
},
"../../packages/core": {
"name": "@esengine/ecs-framework",
"version": "2.1.49",
"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",
@@ -29,6 +32,7 @@
"@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",
@@ -553,9 +557,9 @@
}
},
"node_modules/typescript": {
"version": "5.9.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"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": {

View File

@@ -1,7 +1,7 @@
{
"name": "ecs-worker-system-demo",
"name": "ecs-core-demos",
"version": "1.0.0",
"description": "ECS Framework Worker System Demo",
"description": "ECS Framework Core Demos - Multiple Interactive Examples",
"main": "src/main.ts",
"scripts": {
"dev": "vite",
@@ -15,4 +15,4 @@
"dependencies": {
"@esengine/ecs-framework": "file:../../packages/core"
}
}
}

352
examples/core-demos/pnpm-lock.yaml generated Normal file
View File

@@ -0,0 +1,352 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
'@esengine/ecs-framework':
specifier: file:../../packages/core
version: file:../../packages/core
devDependencies:
typescript:
specifier: ^5.0.0
version: 5.9.3
vite:
specifier: ^4.0.0
version: 4.5.14
packages:
'@esbuild/android-arm64@0.18.20':
resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==}
engines: {node: '>=12'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.18.20':
resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==}
engines: {node: '>=12'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.18.20':
resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==}
engines: {node: '>=12'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.18.20':
resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==}
engines: {node: '>=12'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.18.20':
resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==}
engines: {node: '>=12'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.18.20':
resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==}
engines: {node: '>=12'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.18.20':
resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==}
engines: {node: '>=12'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.18.20':
resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==}
engines: {node: '>=12'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.18.20':
resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==}
engines: {node: '>=12'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.18.20':
resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==}
engines: {node: '>=12'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.18.20':
resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==}
engines: {node: '>=12'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.18.20':
resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==}
engines: {node: '>=12'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.18.20':
resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==}
engines: {node: '>=12'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.18.20':
resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==}
engines: {node: '>=12'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.18.20':
resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==}
engines: {node: '>=12'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.18.20':
resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==}
engines: {node: '>=12'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-x64@0.18.20':
resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==}
engines: {node: '>=12'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-x64@0.18.20':
resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==}
engines: {node: '>=12'}
cpu: [x64]
os: [openbsd]
'@esbuild/sunos-x64@0.18.20':
resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==}
engines: {node: '>=12'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.18.20':
resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==}
engines: {node: '>=12'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.18.20':
resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==}
engines: {node: '>=12'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.18.20':
resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==}
engines: {node: '>=12'}
cpu: [x64]
os: [win32]
'@esengine/ecs-framework@file:../../packages/core':
resolution: {directory: ../../packages/core, type: directory}
esbuild@0.18.20:
resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==}
engines: {node: '>=12'}
hasBin: true
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
postcss@8.5.6:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
rollup@3.29.5:
resolution: {integrity: sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==}
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
hasBin: true
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
hasBin: true
vite@4.5.14:
resolution: {integrity: sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==}
engines: {node: ^14.18.0 || >=16.0.0}
hasBin: true
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
snapshots:
'@esbuild/android-arm64@0.18.20':
optional: true
'@esbuild/android-arm@0.18.20':
optional: true
'@esbuild/android-x64@0.18.20':
optional: true
'@esbuild/darwin-arm64@0.18.20':
optional: true
'@esbuild/darwin-x64@0.18.20':
optional: true
'@esbuild/freebsd-arm64@0.18.20':
optional: true
'@esbuild/freebsd-x64@0.18.20':
optional: true
'@esbuild/linux-arm64@0.18.20':
optional: true
'@esbuild/linux-arm@0.18.20':
optional: true
'@esbuild/linux-ia32@0.18.20':
optional: true
'@esbuild/linux-loong64@0.18.20':
optional: true
'@esbuild/linux-mips64el@0.18.20':
optional: true
'@esbuild/linux-ppc64@0.18.20':
optional: true
'@esbuild/linux-riscv64@0.18.20':
optional: true
'@esbuild/linux-s390x@0.18.20':
optional: true
'@esbuild/linux-x64@0.18.20':
optional: true
'@esbuild/netbsd-x64@0.18.20':
optional: true
'@esbuild/openbsd-x64@0.18.20':
optional: true
'@esbuild/sunos-x64@0.18.20':
optional: true
'@esbuild/win32-arm64@0.18.20':
optional: true
'@esbuild/win32-ia32@0.18.20':
optional: true
'@esbuild/win32-x64@0.18.20':
optional: true
'@esengine/ecs-framework@file:../../packages/core':
dependencies:
tslib: 2.8.1
esbuild@0.18.20:
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
fsevents@2.3.3:
optional: true
nanoid@3.3.11: {}
picocolors@1.1.1: {}
postcss@8.5.6:
dependencies:
nanoid: 3.3.11
picocolors: 1.1.1
source-map-js: 1.2.1
rollup@3.29.5:
optionalDependencies:
fsevents: 2.3.3
source-map-js@1.2.1: {}
tslib@2.8.1: {}
typescript@5.9.3: {}
vite@4.5.14:
dependencies:
esbuild: 0.18.20
postcss: 8.5.6
rollup: 3.29.5
optionalDependencies:
fsevents: 2.3.3

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

@@ -1,8 +1,9 @@
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
@@ -14,12 +15,7 @@
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
"emitDecoratorMetadata": true
},
"include": ["src/**/*"],
"references": [{ "path": "./tsconfig.node.json" }]
}
"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'
}
});

View File

@@ -1,177 +0,0 @@
<!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>
</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>
<script type="module" src="src/main.ts"></script>
</body>
</html>

View File

@@ -1,187 +0,0 @@
import { Scene } from '@esengine/ecs-framework';
import { PhysicsWorkerSystem, RenderSystem, LifetimeSystem } from './systems';
import { Position, Velocity, Physics, Renderable, Lifetime } from './components';
export class GameScene extends Scene {
private canvas: HTMLCanvasElement;
private physicsSystem!: PhysicsWorkerSystem;
private renderSystem!: RenderSystem;
private lifetimeSystem!: LifetimeSystem;
constructor(canvas: HTMLCanvasElement) {
super();
this.canvas = canvas;
}
override initialize(): void {
this.name = "WorkerDemoScene";
// 创建系统
this.physicsSystem = new PhysicsWorkerSystem(true); // 默认启用Worker
this.renderSystem = new RenderSystem(this.canvas);
this.lifetimeSystem = new LifetimeSystem();
// 设置系统执行顺序
this.physicsSystem.updateOrder = 1;
this.lifetimeSystem.updateOrder = 2;
this.renderSystem.updateOrder = 3;
// 添加系统到场景
this.addSystem(this.physicsSystem);
this.addSystem(this.lifetimeSystem);
this.addSystem(this.renderSystem);
}
override onStart(): void {
console.log("Worker演示场景已启动");
this.spawnInitialEntities();
}
override unload(): void {
console.log("Worker演示场景已卸载");
}
/**
* 生成初始实体
*/
public spawnInitialEntities(count: number = 1000): void {
this.clearAllEntities();
for (let i = 0; i < count; i++) {
this.createParticle();
}
}
/**
* 创建一个粒子实体
*/
public createParticle(): void {
const entity = this.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',
'#aaff44', '#44aaff', '#ff44aa', '#aa44ff', '#44ffaa', '#cccccc'
];
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)); // 5-15秒生命周期
}
/**
* 生成粒子爆发效果
*/
public spawnParticleExplosion(centerX: number, centerY: number, count: number = 50): void {
for (let i = 0; i < count; i++) {
const entity = this.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)); // 短生命周期
}
}
/**
* 清空所有实体
*/
public clearAllEntities(): void {
const entities = [...this.entities.buffer]; // 复制数组避免修改原数组
for (const entity of entities) {
entity.destroy();
}
}
/**
* 切换Worker启用状态
*/
public toggleWorker(): boolean {
const workerInfo = this.physicsSystem.getWorkerInfo();
const newWorkerEnabled = !workerInfo.enabled;
// 重新创建物理系统
this.removeSystem(this.physicsSystem);
this.physicsSystem = new PhysicsWorkerSystem(newWorkerEnabled);
this.physicsSystem.updateOrder = 1;
this.addSystem(this.physicsSystem);
return newWorkerEnabled;
}
/**
* 更新Worker配置
*/
public 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
});
}
}
/**
* 切换 SharedArrayBuffer 状态
*/
public toggleSharedArrayBuffer(): void {
this.physicsSystem.disableSharedArrayBuffer();
}
/**
* 获取物理系统状态
*/
public getPhysicsSystemStatus() {
return this.physicsSystem.getCurrentStatus();
}
/**
* 获取系统信息
*/
public getSystemInfo() {
return {
physics: this.physicsSystem.getWorkerInfo(),
entityCount: this.entities.count,
physicsConfig: this.physicsSystem.getPhysicsConfig()
};
}
}

View File

@@ -1,89 +0,0 @@
import { Component, ECSComponent } from '@esengine/ecs-framework';
// 位置组件
@ECSComponent('Position')
export 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('Velocity')
export 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;
}
scale(factor: number): void {
this.dx *= factor;
this.dy *= factor;
}
}
// 物理组件
@ECSComponent('Physics')
export 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('Renderable')
export 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('Lifetime')
export 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;
}
}

View File

@@ -1,376 +0,0 @@
import { Core, PlatformManager } from '@esengine/ecs-framework';
import { GameScene } from './GameScene';
import { BrowserAdapter } from './platform/BrowserAdapter';
// 性能监控
interface PerformanceStats {
fps: number;
frameTime: number;
physicsTime: number;
renderTime: number;
memoryUsage: number;
}
class WorkerDemo {
private gameScene: GameScene;
private canvas: HTMLCanvasElement;
private isRunning = false;
private lastTime = 0;
private frameCount = 0;
private fpsUpdateTime = 0;
private currentFPS = 0;
private lastWorkerStatusUpdate = 0;
// UI元素
private elements: { [key: string]: HTMLElement } = {};
constructor() {
// 注册浏览器适配器
const browserAdapter = new BrowserAdapter();
PlatformManager.getInstance().registerAdapter(browserAdapter);
// 获取canvas
this.canvas = document.getElementById('gameCanvas') as HTMLCanvasElement;
if (!this.canvas) {
throw new Error('Canvas element not found');
}
// 初始化UI元素引用
this.initializeUIElements();
// 初始化ECS Core
Core.create({
debug: true,
enableEntitySystems: true
});
// 创建游戏场景
this.gameScene = new GameScene(this.canvas);
// 设置场景
Core.setScene(this.gameScene);
// 绑定事件
this.bindEvents();
// 启动演示
this.start();
}
private initializeUIElements(): void {
const elementIds = [
'entityCount', 'entityCountValue', 'toggleWorker', 'toggleSAB',
'gravity', 'gravityValue', 'friction', 'frictionValue', 'spawnParticles',
'clearEntities', 'resetDemo', 'fps', 'entityCountStat', 'workerStatus', 'workerLoad',
'physicsTime', 'renderTime', 'frameTime', 'memoryUsage', 'sabStatus'
];
for (const id of elementIds) {
const element = document.getElementById(id);
if (element) {
this.elements[id] = element;
} else {
console.warn(`Element with id '${id}' not found`);
}
}
}
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.gameScene.spawnInitialEntities(count);
});
}
// Worker切换按钮
if (this.elements.toggleWorker) {
this.elements.toggleWorker.addEventListener('click', () => {
const workerEnabled = this.gameScene.toggleWorker();
this.elements.toggleWorker.textContent = workerEnabled ? '禁用 Worker' : '启用 Worker';
this.updateWorkerStatus();
});
}
// SharedArrayBuffer切换按钮
if (this.elements.toggleSAB) {
this.elements.toggleSAB.addEventListener('click', () => {
this.gameScene.toggleSharedArrayBuffer();
this.updateWorkerStatus();
});
}
// 重力滑块
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.gameScene.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.gameScene.updateWorkerConfig({ friction });
});
}
// 生成粒子按钮
if (this.elements.spawnParticles) {
this.elements.spawnParticles.addEventListener('click', () => {
const centerX = this.canvas.width / 2;
const centerY = this.canvas.height / 2;
this.gameScene.spawnParticleExplosion(centerX, centerY, 100);
});
}
// 清空实体按钮
if (this.elements.clearEntities) {
this.elements.clearEntities.addEventListener('click', () => {
this.gameScene.clearAllEntities();
});
}
// 重置演示按钮
if (this.elements.resetDemo) {
this.elements.resetDemo.addEventListener('click', () => {
this.resetDemo();
});
}
// Canvas点击事件 - 在点击位置生成粒子爆发
this.canvas.addEventListener('click', (event) => {
const rect = this.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
this.gameScene.spawnParticleExplosion(x, y, 30);
});
}
private start(): void {
this.isRunning = true;
this.lastTime = performance.now();
this.gameLoop();
console.log('Worker演示已启动');
}
private gameLoop = (): void => {
if (!this.isRunning) return;
const currentTime = performance.now();
const deltaTime = (currentTime - this.lastTime) / 1000; // 转换为秒
this.lastTime = currentTime;
// 更新ECS框架
const frameStartTime = performance.now();
Core.update(deltaTime);
const frameEndTime = performance.now();
// 更新性能统计
this.updatePerformanceStats({
fps: this.currentFPS,
frameTime: frameEndTime - frameStartTime,
physicsTime: (window as any).physicsExecutionTime || 0,
renderTime: (window as any).renderExecutionTime || 0,
memoryUsage: this.getMemoryUsage()
});
// 更新FPS计算
this.frameCount++;
if (currentTime - this.fpsUpdateTime >= 1000) {
this.currentFPS = this.frameCount;
this.frameCount = 0;
this.fpsUpdateTime = currentTime;
}
// 更新UI
this.updateUI();
// 继续循环
requestAnimationFrame(this.gameLoop);
};
private updatePerformanceStats(stats: PerformanceStats): void {
if (this.elements.fps) {
this.elements.fps.textContent = stats.fps.toString();
this.elements.fps.className = stats.fps >= 55 ? 'performance-high' :
stats.fps >= 30 ? 'performance-medium' : 'performance-low';
}
if (this.elements.frameTime) {
this.elements.frameTime.textContent = stats.frameTime.toFixed(2);
this.elements.frameTime.className = stats.frameTime <= 16 ? 'performance-high' :
stats.frameTime <= 33 ? 'performance-medium' : 'performance-low';
}
if (this.elements.physicsTime) {
this.elements.physicsTime.textContent = stats.physicsTime.toFixed(2);
this.elements.physicsTime.className = stats.physicsTime <= 8 ? 'performance-high' :
stats.physicsTime <= 16 ? 'performance-medium' : 'performance-low';
}
if (this.elements.renderTime) {
this.elements.renderTime.textContent = stats.renderTime.toFixed(2);
this.elements.renderTime.className = stats.renderTime <= 8 ? 'performance-high' :
stats.renderTime <= 16 ? 'performance-medium' : 'performance-low';
}
if (this.elements.memoryUsage) {
this.elements.memoryUsage.textContent = stats.memoryUsage.toFixed(1);
}
}
private updateUI(): void {
const currentTime = performance.now();
const systemInfo = this.gameScene.getSystemInfo();
// 更新实体数量(每帧更新)
if (this.elements.entityCountStat) {
this.elements.entityCountStat.textContent = systemInfo.entityCount.toString();
}
// 更新Worker状态每500ms更新一次即可
if (currentTime - this.lastWorkerStatusUpdate >= 500) {
this.updateWorkerStatus();
this.lastWorkerStatusUpdate = currentTime;
}
// 更新全局Worker信息供其他系统使用
(window as any).workerInfo = systemInfo.physics;
}
private updateWorkerStatus(): void {
const systemInfo = this.gameScene.getSystemInfo();
const workerInfo = systemInfo.physics;
const entityCount = systemInfo.entityCount;
const status = this.gameScene.getPhysicsSystemStatus();
if (this.elements.workerStatus) {
if (workerInfo.enabled) {
this.elements.workerStatus.textContent = `启用 (${workerInfo.workerCount} Workers)`;
this.elements.workerStatus.className = 'worker-enabled';
} else {
this.elements.workerStatus.textContent = '禁用';
this.elements.workerStatus.className = 'worker-disabled';
}
}
if (this.elements.workerLoad) {
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';
}
}
// 更新 SharedArrayBuffer 状态
if (this.elements.sabStatus) {
const modeNames = {
'shared-buffer': 'SharedArrayBuffer模式',
'single-worker': '单Worker模式',
'multi-worker': '多Worker模式',
'sync': '同步模式'
};
this.elements.sabStatus.textContent = modeNames[status.mode] || status.mode;
this.elements.sabStatus.className = status.mode === 'shared-buffer' ? 'worker-enabled' : 'worker-disabled';
}
// 更新 SharedArrayBuffer 按钮文本
if (this.elements.toggleSAB) {
if (status.sharedArrayBufferEnabled) {
this.elements.toggleSAB.textContent = '禁用 SharedArrayBuffer';
} else {
this.elements.toggleSAB.textContent = '启用 SharedArrayBuffer';
this.elements.toggleSAB.setAttribute('disabled', 'true'); // SAB 一旦禁用就无法重新启用
}
}
}
private getMemoryUsage(): number {
if ('memory' in performance) {
const memory = (performance as any).memory;
return memory.usedJSHeapSize / (1024 * 1024); // MB
}
return 0;
}
private resetDemo(): void {
// 重置所有控件到默认值
if (this.elements.entityCount) {
(this.elements.entityCount as HTMLInputElement).value = '1000';
this.elements.entityCountValue.textContent = '1000';
}
if (this.elements.gravity) {
(this.elements.gravity as HTMLInputElement).value = '100';
this.elements.gravityValue.textContent = '100';
}
if (this.elements.friction) {
(this.elements.friction as HTMLInputElement).value = '95';
this.elements.frictionValue.textContent = '95%';
}
// 确保Worker被启用
const workerInfo = this.gameScene.getSystemInfo().physics;
if (!workerInfo.enabled) {
this.gameScene.toggleWorker(); // 只有在禁用时才切换
}
if (this.elements.toggleWorker) {
this.elements.toggleWorker.textContent = '禁用 Worker';
}
// 重新生成实体
this.gameScene.spawnInitialEntities(1000);
// 重置配置
this.gameScene.updateWorkerConfig({
gravity: 100,
friction: 0.95
});
console.log('演示已重置');
}
public stop(): void {
this.isRunning = false;
}
}
// 启动演示
document.addEventListener('DOMContentLoaded', () => {
try {
new WorkerDemo();
} catch (error) {
console.error('启动演示失败:', error);
document.body.innerHTML = `
<div style="padding: 20px; color: red;">
<h1>启动失败</h1>
<p>错误: ${error}</p>
<p>请确保浏览器支持Web Workers和Canvas API</p>
</div>
`;
}
});

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