Compare commits

...

384 Commits

Author SHA1 Message Date
YHH
33ee0a04c6 升级tauri-action 2025-10-16 23:20:55 +08:00
YHH
d68f6922f8 Merge pull request #129 from esengine/release/editor-v1.0.1
chore(editor): Release v1.0.1
2025-10-16 23:09:26 +08:00
esengine
f8539d7958 chore(editor): bump version to 1.0.1 2025-10-16 15:08:22 +00:00
YHH
14dc911e0a Merge branch 'master' of https://github.com/esengine/ecs-framework 2025-10-16 22:55:09 +08:00
YHH
deccb6bf84 修复editor再ci上版本冲突问题 2025-10-16 22:54:58 +08:00
YHH
dacbfcae95 Merge pull request #127 from esengine/imgbot
[ImgBot] Optimize images
2025-10-16 22:49:51 +08:00
ImgBotApp
1b69ed17b7 [ImgBot] Optimize images
*Total -- 496.42kb -> 376.40kb (24.18%)

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

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

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

45
.eslintrc.json Normal file
View File

@@ -0,0 +1,45 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module",
"project": "./tsconfig.json"
},
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"semi": ["error", "always"],
"quotes": ["error", "single", { "avoidEscape": true }],
"indent": ["error", 4, { "SwitchCase": 1 }],
"no-trailing-spaces": "error",
"eol-last": ["error", "always"],
"comma-dangle": ["error", "none"],
"object-curly-spacing": ["error", "always"],
"array-bracket-spacing": ["error", "never"],
"arrow-parens": ["error", "always"],
"no-multiple-empty-lines": ["error", { "max": 2, "maxEOF": 1 }],
"no-console": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
"@typescript-eslint/no-non-null-assertion": "off"
},
"ignorePatterns": [
"node_modules/",
"dist/",
"bin/",
"build/",
"coverage/",
"thirdparty/",
"examples/lawn-mower-demo/",
"extensions/",
"*.min.js",
"*.d.ts"
]
}

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

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

@@ -0,0 +1,84 @@
name: CI
on:
push:
branches: [ master, main, develop ]
paths:
- 'packages/**'
- 'package.json'
- 'package-lock.json'
- 'tsconfig.json'
- 'jest.config.*'
- '.github/workflows/ci.yml'
pull_request:
branches: [ master, main, develop ]
paths:
- 'packages/**'
- 'package.json'
- 'package-lock.json'
- 'tsconfig.json'
- 'jest.config.*'
- '.github/workflows/ci.yml'
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build core package first
run: npm run build:core
- name: Run tests with coverage
run: npm run test:ci
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
file: ./coverage/lcov.info
flags: unittests
name: codecov-umbrella
fail_ci_if_error: false
build:
runs-on: ubuntu-latest
needs: test
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build project
run: npm run build
- name: Build npm package
run: npm run build:npm
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build-artifacts
path: |
bin/
dist/
retention-days: 7

67
.github/workflows/docs.yml vendored Normal file
View File

@@ -0,0 +1,67 @@
name: Deploy Documentation
on:
push:
branches: [master]
paths:
- 'docs/**'
- 'packages/**'
- 'typedoc.json'
- 'package.json'
- '.github/workflows/docs.yml'
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
cache: 'npm'
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Install dependencies
run: npm ci
- name: Build core package
run: npm run build:core
- name: Generate API documentation
run: npm run docs:api
- name: Build documentation
run: npm run docs:build
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: docs/.vitepress/dist
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
needs: build
runs-on: ubuntu-latest
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

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

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

91
.gitignore vendored
View File

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

33
.gitmodules vendored Normal file
View File

@@ -0,0 +1,33 @@
[submodule "thirdparty/BehaviourTree-ai"]
path = thirdparty/BehaviourTree-ai
url = https://github.com/esengine/BehaviourTree-ai.git
[submodule "thirdparty/admin-backend"]
path = thirdparty/admin-backend
url = https://github.com/esengine/admin-backend.git
[submodule "extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension"]
path = extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension
url = https://github.com/esengine/cocos-ecs-extension.git
[submodule "extensions/cocos/cocos-ecs/extensions/behaviour-tree"]
path = extensions/cocos/cocos-ecs/extensions/behaviour-tree
url = https://github.com/esengine/behaviour-tree.git
[submodule "extensions/cocos/cocos-ecs/extensions/cocos-terrain-gen"]
path = extensions/cocos/cocos-ecs/extensions/cocos-terrain-gen
url = https://github.com/esengine/cocos-terrain-gen.git
[submodule "extensions/cocos/cocos-ecs/extensions/mvvm-designer"]
path = extensions/cocos/cocos-ecs/extensions/mvvm-designer
url = https://github.com/esengine/mvvm-designer.git
[submodule "thirdparty/mvvm-ui-framework"]
path = thirdparty/mvvm-ui-framework
url = https://github.com/esengine/mvvm-ui-framework.git
[submodule "thirdparty/cocos-nexus"]
path = thirdparty/cocos-nexus
url = https://github.com/esengine/cocos-nexus.git
[submodule "extensions/cocos/cocos-ecs/extensions/utilityai_designer"]
path = extensions/cocos/cocos-ecs/extensions/utilityai_designer
url = https://github.com/esengine/utilityai_designer.git
[submodule "thirdparty/ecs-astar"]
path = thirdparty/ecs-astar
url = https://github.com/esengine/ecs-astar.git
[submodule "examples/lawn-mower-demo"]
path = examples/lawn-mower-demo
url = https://github.com/esengine/lawn-mower-demo.git

40
.npmignore Normal file
View File

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

2
.nxignore Normal file
View File

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

49
.prettierignore Normal file
View File

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

14
.prettierrc Normal file
View File

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

344
README.md
View File

@@ -1,295 +1,163 @@
# ECS Framework
[![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)
一个轻量级的 TypeScript ECSEntity-Component-System框架,专为游戏开发设计,适用于 Laya、Cocos 等游戏引擎
一个高性能的 TypeScript ECS (Entity-Component-System) 框架,专为现代游戏开发设计。
## 特性
## 特性
- 🚀 **轻量级 ECS 架构** - 基于实体组件系统,提供清晰的代码结构
- 📡 **事件系统** - 内置 Emitter 事件发射器,支持类型安全的事件管理
- **定时器系统** - 完整的定时器管理,支持延迟和重复任务
- 🔍 **查询系统** - 基于位掩码的高性能实体查询
- 🛠️ **性能监控** - 内置性能监控工具,帮助优化游戏性能
- 🎯 **对象池** - 内存管理优化,减少垃圾回收压力
- 📊 **数学库** - 完整的 2D 数学运算支持
- **高性能** - 针对大规模实体优化支持SoA存储和批量处理
- **多线程计算** - Worker系统支持真正的并行处理充分利用多核CPU性能
- **类型安全** - 完整的TypeScript支持编译时类型检查
- **现代架构** - 支持多World、多Scene的分层架构设计
- **开发友好** - 内置调试工具和性能监控
- **跨平台** - 支持Cocos Creator、Laya引擎和Web平台
## 📦 安装
## 安装
```bash
npm install @esengine/ecs-framework
```
## 🚀 快速开始
### 1. 初始化框架
## 快速开始
```typescript
import { Core, CoreEvents } from '@esengine/ecs-framework';
import { Core, Scene, Component, EntitySystem, ECSComponent, ECSSystem, Matcher, Time } from '@esengine/ecs-framework';
// 创建 Core 实例
const core = Core.create(true); // true 表示开启调试模式
// 在游戏循环中更新框架
function gameLoop() {
// 发送帧更新事件
Core.emitter.emit(CoreEvents.frameUpdated);
}
```
### 2. 创建场景
```typescript
import { Scene, Vector2, EntitySystem } from '@esengine/ecs-framework';
class GameScene extends Scene {
public initialize() {
// 创建玩家实体
const player = this.createEntity("Player");
// 设置位置
player.position = new Vector2(100, 100);
// 添加自定义组件
const movement = player.addComponent(new MovementComponent());
// 添加系统
this.addEntityProcessor(new MovementSystem());
}
public onStart() {
console.log("游戏场景已启动");
// 定义组件
@ECSComponent('Position')
class Position extends Component {
constructor(public x = 0, public y = 0) {
super();
}
}
// 设置当前场景
Core.scene = new GameScene();
```
### 3. 创建组件
```typescript
import { Component, Vector2, Time } from '@esengine/ecs-framework';
class MovementComponent extends Component {
public speed: number = 100;
public direction: Vector2 = Vector2.zero;
public update() {
if (this.direction.length > 0) {
const movement = this.direction.multiply(this.speed * Time.deltaTime);
this.entity.position = this.entity.position.add(movement);
}
@ECSComponent('Velocity')
class Velocity extends Component {
constructor(public dx = 0, public dy = 0) {
super();
}
}
```
### 4. 创建系统
```typescript
import { EntitySystem, Entity } from '@esengine/ecs-framework';
// 创建系统
@ECSSystem('Movement')
class MovementSystem extends EntitySystem {
protected process(entities: Entity[]) {
constructor() {
super(Matcher.all(Position, Velocity));
}
protected process(entities: readonly Entity[]): void {
for (const entity of entities) {
const movement = entity.getComponent(MovementComponent);
if (movement) {
movement.update();
}
const position = entity.getComponent(Position)!;
const velocity = entity.getComponent(Velocity)!;
position.x += velocity.dx * Time.deltaTime;
position.y += velocity.dy * Time.deltaTime;
}
}
}
```
## 📚 核心概念
// 创建场景并启动
class GameScene extends Scene {
protected initialize(): void {
this.addSystem(new MovementSystem());
### Entity实体
实体是游戏世界中的基本对象,包含位置、旋转、缩放等基本属性,可以添加组件来扩展功能。
```typescript
import { Vector2 } from '@esengine/ecs-framework';
const entity = scene.createEntity("MyEntity");
entity.position = new Vector2(100, 200);
entity.rotation = Math.PI / 4;
entity.scale = new Vector2(2, 2);
```
### Component组件
组件包含数据和行为,定义了实体的特性。
```typescript
import { Component } from '@esengine/ecs-framework';
class HealthComponent extends Component {
public maxHealth: number = 100;
public currentHealth: number = 100;
public takeDamage(damage: number) {
this.currentHealth = Math.max(0, this.currentHealth - damage);
if (this.currentHealth <= 0) {
this.entity.destroy();
}
}
}
```
### System系统
系统处理实体集合,实现游戏逻辑。
```typescript
import { EntitySystem, Entity } from '@esengine/ecs-framework';
class HealthSystem extends EntitySystem {
protected process(entities: Entity[]) {
for (const entity of entities) {
const health = entity.getComponent(HealthComponent);
if (health && health.currentHealth <= 0) {
entity.destroy();
}
}
}
}
```
## 🎮 高级功能
### 事件系统
```typescript
import { Core, CoreEvents } from '@esengine/ecs-framework';
// 监听事件
Core.emitter.addObserver(CoreEvents.frameUpdated, this.onFrameUpdate, this);
// 发射自定义事件
Core.emitter.emit("playerDied", { player: entity, score: 1000 });
// 移除监听
Core.emitter.removeObserver(CoreEvents.frameUpdated, this.onFrameUpdate);
```
### 定时器系统
```typescript
import { Core } from '@esengine/ecs-framework';
// 延迟执行
Core.schedule(2.0, false, this, (timer) => {
console.log("2秒后执行");
});
// 重复执行
Core.schedule(1.0, true, this, (timer) => {
console.log("每秒执行一次");
});
```
### 实体查询
```typescript
// 按名称查找
const player = scene.findEntity("Player");
// 按标签查找
const enemies = scene.findEntitiesByTag(1);
// 按ID查找
const entity = scene.findEntityById(123);
```
### 性能监控
```typescript
import { PerformanceMonitor } from '@esengine/ecs-framework';
// 获取性能数据
const monitor = PerformanceMonitor.instance;
console.log("平均FPS:", monitor.averageFPS);
console.log("内存使用:", monitor.memoryUsage);
```
## 🛠️ 开发工具
### 对象池
```typescript
// 创建对象池
class BulletPool extends es.Pool<Bullet> {
protected createObject(): Bullet {
return new Bullet();
const player = this.createEntity("Player");
player.addComponent(new Position(100, 100));
player.addComponent(new Velocity(50, 0));
}
}
const bulletPool = new BulletPool();
// 启动游戏
Core.create();
Core.setScene(new GameScene());
// 获取对象
const bullet = bulletPool.obtain();
// 释放对象
bulletPool.free(bullet);
// 游戏循环中更新
function gameLoop(deltaTime: number) {
Core.update(deltaTime);
}
```
### 实体调试
## 核心特性
```typescript
// 获取实体调试信息
const debugInfo = entity.getDebugInfo();
console.log("实体信息:", debugInfo);
- **实体查询** - 使用 Matcher API 进行高效的实体过滤
- **事件系统** - 类型安全的事件发布/订阅机制
- **性能优化** - SoA 存储优化,支持大规模实体处理
- **多线程支持** - Worker系统实现真正的并行计算充分利用多核CPU
- **多场景** - 支持 World/Scene 分层架构
- **时间管理** - 内置定时器和时间控制系统
// 获取场景统计
const stats = scene.getStats();
console.log("场景统计:", stats);
```
## 平台支持
## 📖 文档
支持主流游戏引擎和 Web 平台:
- [快速入门](docs/getting-started.md) - 从零开始学习框架使用
- [核心概念](docs/core-concepts.md) - 深入了解 ECS 架构和设计原理
- [查询系统使用指南](docs/query-system-usage.md) - 学习高性能查询系统的详细用法
- **Cocos Creator**
- **Laya 引擎**
- **原生 Web** - 浏览器环境直接运行
- **小游戏平台** - 微信、支付宝等小游戏
## 🔗 扩展库
## ECS Framework Editor
- [路径寻找库](https://github.com/esengine/ecs-astar) - A*、广度优先、Dijkstra、GOAP 算法
- [AI 系统](https://github.com/esengine/BehaviourTree-ai) - 行为树、效用 AI 系统
跨平台桌面编辑器,提供可视化开发和调试工具。
## 🤝 贡献
### 主要功能
欢迎提交 Issue 和 Pull Request
- **场景管理** - 可视化场景层级和实体管理
- **组件检视** - 实时查看和编辑实体组件
- **性能分析** - 内置 Profiler 监控系统性能
- **插件系统** - 可扩展的插件架构
- **远程调试** - 连接运行中的游戏进行实时调试
- **自动更新** - 支持热更新,自动获取最新版本
### 开发环境设置
### 下载
```bash
# 克隆项目
git clone https://github.com/esengine/ecs-framework.git
[![Latest Release](https://img.shields.io/github/v/release/esengine/ecs-framework?label=下载最新版本&style=for-the-badge)](https://github.com/esengine/ecs-framework/releases/latest)
# 进入源码目录
cd ecs-framework/source
支持 Windows、macOS (Intel & Apple Silicon)
# 安装依赖
npm install
### 截图
# 构建项目
npm run build
<img src="screenshots/main_screetshot.png" alt="ECS Framework Editor" width="800">
# 运行测试
npm test
```
<details>
<summary>查看更多截图</summary>
### 构建要求
**性能分析器**
<img src="screenshots/performance_profiler.png" alt="Performance Profiler" width="600">
- Node.js >= 14.0.0
- TypeScript >= 4.0.0
**插件管理**
<img src="screenshots/plugin_manager.png" alt="Plugin Manager" width="600">
## 📄 许可证
**设置界面**
<img src="screenshots/settings.png" alt="Settings" width="600">
本项目采用 [MIT](LICENSE) 许可证。
</details>
## 💬 交流群
## 示例项目
加入 QQ 群讨论:[ecs游戏框架交流](https://jq.qq.com/?_wv=1027&k=29w1Nud6)
- [Worker系统演示](https://esengine.github.io/ecs-framework/demos/worker-system/) - 多线程物理系统演示,展示高性能并行计算
- [割草机演示](https://github.com/esengine/lawn-mower-demo) - 完整的游戏示例
---
## 文档
**ECS Framework** - 让游戏开发更简单、更高效!
- [快速入门](https://esengine.github.io/ecs-framework/guide/getting-started.html) - 详细教程和平台集成
- [完整指南](https://esengine.github.io/ecs-framework/guide/) - ECS 概念和使用指南
- [API 参考](https://esengine.github.io/ecs-framework/api/) - 完整 API 文档
## 生态系统
- [路径寻找](https://github.com/esengine/ecs-astar) - A*、BFS、Dijkstra 算法
- [AI 系统](https://github.com/esengine/BehaviourTree-ai) - 行为树、效用 AI
## 社区与支持
- [问题反馈](https://github.com/esengine/ecs-framework/issues) - Bug 报告和功能建议
- [QQ 交流群](https://jq.qq.com/?_wv=1027&k=29w1Nud6) - ecs游戏框架交流
## 许可证
[MIT](LICENSE) © 2025 ECS Framework

View File

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

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

@@ -0,0 +1,225 @@
import { defineConfig } from 'vitepress'
import Icons from 'unplugin-icons/vite'
import { readFileSync } from 'fs'
import { join, dirname } from 'path'
import { fileURLToPath } from 'url'
const __dirname = dirname(fileURLToPath(import.meta.url))
const corePackageJson = JSON.parse(
readFileSync(join(__dirname, '../../packages/core/package.json'), 'utf-8')
)
export default defineConfig({
vite: {
plugins: [
Icons({
compiler: 'vue3',
autoInstall: true
})
],
server: {
fs: {
allow: ['..']
},
middlewareMode: false,
headers: {
'Cross-Origin-Embedder-Policy': 'require-corp',
'Cross-Origin-Opener-Policy': 'same-origin'
}
}
},
title: 'ECS Framework',
description: '高性能TypeScript ECS框架 - 为游戏开发而生',
lang: 'zh-CN',
themeConfig: {
nav: [
{ text: '首页', link: '/' },
{ text: '快速开始', link: '/guide/getting-started' },
{ text: '指南', link: '/guide/' },
{ text: 'API', link: '/api/README' },
{
text: '示例',
items: [
{ text: 'Worker系统演示', link: '/examples/worker-system-demo' },
{ text: '割草机演示', link: 'https://github.com/esengine/lawn-mower-demo' }
]
},
{
text: `v${corePackageJson.version}`,
link: 'https://github.com/esengine/ecs-framework/releases'
}
],
sidebar: {
'/guide/': [
{
text: '开始使用',
items: [
{ text: '快速开始', link: '/guide/getting-started' },
{ text: '指南概览', link: '/guide/' }
]
},
{
text: '核心概念',
collapsed: false,
items: [
{ text: '实体类 (Entity)', link: '/guide/entity' },
{ text: '组件系统 (Component)', link: '/guide/component' },
{ text: '实体查询系统', link: '/guide/entity-query' },
{
text: '系统架构 (System)',
link: '/guide/system',
items: [
{ text: 'Worker系统 (多线程)', link: '/guide/worker-system' }
]
},
{
text: '场景管理 (Scene)',
link: '/guide/scene',
items: [
{ text: 'SceneManager', link: '/guide/scene-manager' },
{ text: 'WorldManager', link: '/guide/world-manager' }
]
},
{ text: '序列化系统 (Serialization)', link: '/guide/serialization' },
{ text: '事件系统 (Event)', link: '/guide/event-system' },
{ text: '时间和定时器 (Time)', link: '/guide/time-and-timers' },
{ text: '日志系统 (Logger)', link: '/guide/logging' }
]
},
{
text: '高级特性',
collapsed: false,
items: [
{ text: '服务容器 (Service Container)', link: '/guide/service-container' },
{ text: '插件系统 (Plugin System)', link: '/guide/plugin-system' }
]
},
{
text: '平台适配器',
link: '/guide/platform-adapter',
collapsed: false,
items: [
{ text: '浏览器适配器', link: '/guide/platform-adapter/browser' },
{ text: '微信小游戏适配器', link: '/guide/platform-adapter/wechat-minigame' },
{ text: 'Node.js适配器', link: '/guide/platform-adapter/nodejs' }
]
}
],
'/examples/': [
{
text: '示例',
items: [
{ text: '示例概览', link: '/examples/' },
{ text: 'Worker系统演示', link: '/examples/worker-system-demo' }
]
}
],
'/api/': [
{
text: 'API 参考',
items: [
{ text: '概述', link: '/api/README' },
{
text: '核心类',
collapsed: false,
items: [
{ text: 'Core', link: '/api/classes/Core' },
{ text: 'Scene', link: '/api/classes/Scene' },
{ text: 'World', link: '/api/classes/World' },
{ text: 'Entity', link: '/api/classes/Entity' },
{ text: 'Component', link: '/api/classes/Component' },
{ text: 'EntitySystem', link: '/api/classes/EntitySystem' }
]
},
{
text: '系统类',
collapsed: true,
items: [
{ text: 'PassiveSystem', link: '/api/classes/PassiveSystem' },
{ text: 'ProcessingSystem', link: '/api/classes/ProcessingSystem' },
{ text: 'IntervalSystem', link: '/api/classes/IntervalSystem' }
]
},
{
text: '工具类',
collapsed: true,
items: [
{ text: 'Matcher', link: '/api/classes/Matcher' },
{ text: 'Time', link: '/api/classes/Time' },
{ text: 'PerformanceMonitor', link: '/api/classes/PerformanceMonitor' },
{ text: 'DebugManager', link: '/api/classes/DebugManager' }
]
},
{
text: '接口',
collapsed: true,
items: [
{ text: 'IScene', link: '/api/interfaces/IScene' },
{ text: 'IComponent', link: '/api/interfaces/IComponent' },
{ text: 'ISystemBase', link: '/api/interfaces/ISystemBase' },
{ text: 'ICoreConfig', link: '/api/interfaces/ICoreConfig' }
]
},
{
text: '装饰器',
collapsed: true,
items: [
{ text: '@ECSComponent', link: '/api/functions/ECSComponent' },
{ text: '@ECSSystem', link: '/api/functions/ECSSystem' }
]
},
{
text: '枚举',
collapsed: true,
items: [
{ text: 'ECSEventType', link: '/api/enumerations/ECSEventType' },
{ text: 'LogLevel', link: '/api/enumerations/LogLevel' }
]
}
]
}
]
},
socialLinks: [
{ icon: 'github', link: 'https://github.com/esengine/ecs-framework' }
],
footer: {
message: 'Released under the MIT License.',
copyright: 'Copyright © 2025 ECS Framework'
},
editLink: {
pattern: 'https://github.com/esengine/ecs-framework/edit/master/docs/:path',
text: '在 GitHub 上编辑此页'
},
search: {
provider: 'local'
},
outline: {
level: [2, 3],
label: '目录'
}
},
head: [
['meta', { name: 'theme-color', content: '#646cff' }],
['link', { rel: 'icon', href: '/favicon.ico' }]
],
base: '/ecs-framework/',
cleanUrls: true,
markdown: {
lineNumbers: true,
theme: {
light: 'github-light',
dark: 'github-dark'
}
}
})

View File

@@ -1,496 +0,0 @@
# 核心概念
ECS Framework 基于 Entity-Component-System 架构模式,这是一种高度模块化和可扩展的游戏开发架构。本文档将详细介绍框架的核心概念。
## ECS 架构概述
ECS 架构将传统的面向对象设计分解为三个核心部分:
- **Entity实体** - 游戏世界中的对象,包含基本属性如位置、旋转、缩放
- **Component组件** - 包含数据和行为的功能模块
- **System系统** - 处理实体集合的逻辑处理单元
## Core核心
Core 是框架的核心管理类,负责游戏的生命周期管理。
### 创建和配置
```typescript
import { Core } from './Core';
// 创建核心实例(调试模式)
const core = Core.create(true);
// 创建核心实例(发布模式)
const core = Core.create(false);
```
### 事件系统
```typescript
import { CoreEvents } from './ECS/CoreEvents';
// 监听核心事件
Core.emitter.addObserver(CoreEvents.frameUpdated, this.onUpdate, this);
// 发送帧更新事件
Core.emitter.emit(CoreEvents.frameUpdated);
// 发送自定义事件
Core.emitter.emit("customEvent", { data: "value" });
```
### 定时器系统
```typescript
// 延迟执行
Core.schedule(2.0, false, this, (timer) => {
console.log("2秒后执行");
});
// 重复执行
Core.schedule(1.0, true, this, (timer) => {
console.log("每秒执行一次");
});
```
## Scene场景
场景是游戏世界的容器,管理实体和系统的生命周期。
### 创建和使用场景
```typescript
import { Scene } from './ECS/Scene';
// 创建场景
const scene = new Scene();
scene.name = "GameScene";
// 设置为当前场景
Core.scene = scene;
// 场景生命周期
scene.begin(); // 开始场景
scene.update(); // 更新场景
scene.end(); // 结束场景
```
## Entity实体
实体是游戏世界中的基本对象,包含位置、旋转、缩放等基本属性。
### 实体的基本属性
```typescript
import { Vector2 } from './Math/Vector2';
const entity = scene.createEntity("MyEntity");
// 位置
entity.position = new Vector2(100, 200);
entity.position = entity.position.add(new Vector2(10, 0));
// 旋转(弧度)
entity.rotation = Math.PI / 4;
// 缩放
entity.scale = new Vector2(2, 2);
// 标签(用于分类)
entity.tag = 1;
// 启用状态
entity.enabled = true;
// 活跃状态
entity.active = true;
// 更新顺序
entity.updateOrder = 10;
```
### 实体层级关系
```typescript
// 添加子实体
const parent = scene.createEntity("Parent");
const child = scene.createEntity("Child");
parent.addChild(child);
// 获取父实体
const parentEntity = child.parent;
// 获取所有子实体
const children = parent.children;
// 查找子实体
const foundChild = parent.findChild("Child");
// 按标签查找子实体
const taggedChildren = parent.findChildrenByTag(1);
// 移除子实体
parent.removeChild(child);
// 移除所有子实体
parent.removeAllChildren();
```
### 实体生命周期
```typescript
// 检查实体是否被销毁
if (!entity.isDestroyed) {
// 实体仍然有效
}
// 销毁实体
entity.destroy();
// 获取实体调试信息
const debugInfo = entity.getDebugInfo();
console.log(debugInfo);
```
## Component组件
组件包含数据和行为,定义了实体的特性和能力。
### 创建组件
```typescript
import { Component } from './ECS/Component';
class HealthComponent extends Component {
public maxHealth: number = 100;
public currentHealth: number = 100;
public takeDamage(damage: number) {
this.currentHealth -= damage;
if (this.currentHealth <= 0) {
this.entity.destroy();
}
}
public heal(amount: number) {
this.currentHealth = Math.min(this.maxHealth, this.currentHealth + amount);
}
}
```
### 组件生命周期
```typescript
class MyComponent extends Component {
public onAddedToEntity() {
// 组件被添加到实体时调用
console.log("组件已添加到实体:", this.entity.name);
}
public onRemovedFromEntity() {
// 组件从实体移除时调用
console.log("组件已从实体移除");
}
public onEnabled() {
// 组件启用时调用
console.log("组件已启用");
}
public onDisabled() {
// 组件禁用时调用
console.log("组件已禁用");
}
public update() {
// 每帧更新(如果组件启用)
console.log("组件更新");
}
}
```
### 组件管理
```typescript
// 添加组件
const health = entity.addComponent(new HealthComponent());
// 创建并添加组件
const movement = entity.createComponent(MovementComponent, 200); // 传递构造参数
// 获取组件
const healthComp = entity.getComponent(HealthComponent);
// 检查组件是否存在
if (entity.hasComponent(HealthComponent)) {
// 处理逻辑
}
// 获取或创建组件
const weapon = entity.getOrCreateComponent(WeaponComponent);
// 获取多个同类型组件
const allHealthComps = entity.getComponents(HealthComponent);
// 移除组件
entity.removeComponent(healthComp);
// 按类型移除组件
entity.removeComponentByType(HealthComponent);
// 移除所有组件
entity.removeAllComponents();
```
## Scene场景
场景是实体和系统的容器,管理游戏世界的状态。
### 场景生命周期
```typescript
class GameScene extends es.Scene {
public initialize() {
// 场景初始化,创建实体和系统
this.setupEntities();
this.setupSystems();
}
public onStart() {
// 场景开始运行时调用
console.log("场景开始");
}
public unload() {
// 场景卸载时调用
console.log("场景卸载");
}
private setupEntities() {
const player = this.createEntity("Player");
player.addComponent(new PlayerComponent());
}
private setupSystems() {
this.addEntityProcessor(new MovementSystem());
}
}
```
### 实体管理
```typescript
// 创建实体
const entity = scene.createEntity("MyEntity");
// 添加现有实体
scene.addEntity(entity);
// 查找实体
const player = scene.findEntity("Player");
const entityById = scene.findEntityById(123);
const entitiesByTag = scene.findEntitiesByTag(1);
// 销毁所有实体
scene.destroyAllEntities();
// 获取场景统计信息
const stats = scene.getStats();
console.log("实体数量:", stats.entityCount);
console.log("系统数量:", stats.processorCount);
```
## System系统
系统处理实体集合,实现游戏的核心逻辑。
### EntitySystem
最常用的系统类型,处理实体集合:
```typescript
class MovementSystem extends es.EntitySystem {
protected process(entities: es.Entity[]) {
for (const entity of entities) {
const movement = entity.getComponent(MovementComponent);
if (movement) {
movement.update();
}
}
}
}
```
### ProcessingSystem
定期处理的系统:
```typescript
class HealthRegenerationSystem extends es.ProcessingSystem {
protected process(entities: es.Entity[]) {
for (const entity of entities) {
const health = entity.getComponent(HealthComponent);
if (health && health.currentHealth < health.maxHealth) {
health.currentHealth += 10 * es.Time.deltaTime;
}
}
}
}
```
### IntervalSystem
按时间间隔执行的系统:
```typescript
class SpawnSystem extends es.IntervalSystem {
constructor() {
super(3.0); // 每3秒执行一次
}
protected processSystem() {
// 生成敌人
const enemy = this.scene.createEntity("Enemy");
enemy.addComponent(new EnemyComponent());
}
}
```
### PassiveSystem
被动系统,不自动处理实体:
```typescript
class CollisionSystem extends es.PassiveSystem {
public checkCollisions() {
// 手动调用的碰撞检测逻辑
}
}
```
## Time时间
时间管理工具类,提供游戏时间相关功能:
```typescript
// 获取时间信息
console.log("帧时间:", es.Time.deltaTime);
console.log("总时间:", es.Time.totalTime);
console.log("帧数:", es.Time.frameCount);
console.log("时间缩放:", es.Time.timeScale);
// 设置时间缩放(慢动作效果)
es.Time.timeScale = 0.5;
// 检查时间间隔
if (es.Time.checkEvery(1.0, lastCheckTime)) {
// 每秒执行一次
}
```
## Vector2二维向量
二维向量类,提供数学运算:
```typescript
// 创建向量
const vec1 = new es.Vector2(10, 20);
const vec2 = es.Vector2.zero;
const vec3 = es.Vector2.one;
// 向量运算
const sum = vec1.add(vec2);
const diff = vec1.subtract(vec2);
const scaled = vec1.multiply(2);
const normalized = vec1.normalize();
// 向量属性
console.log("长度:", vec1.length);
console.log("长度平方:", vec1.lengthSquared);
// 静态方法
const distance = es.Vector2.distance(vec1, vec2);
const lerped = es.Vector2.lerp(vec1, vec2, 0.5);
const fromAngle = es.Vector2.fromAngle(Math.PI / 4);
```
## 性能监控
框架内置性能监控工具:
```typescript
// 获取性能监控实例
const monitor = es.PerformanceMonitor.instance;
// 查看性能数据
console.log("平均FPS:", monitor.averageFPS);
console.log("最小FPS:", monitor.minFPS);
console.log("最大FPS:", monitor.maxFPS);
console.log("内存使用:", monitor.memoryUsage);
// 重置性能数据
monitor.reset();
```
## 对象池
内存管理优化工具:
```typescript
// 创建对象池
class BulletPool extends es.Pool<Bullet> {
protected createObject(): Bullet {
return new Bullet();
}
}
const bulletPool = new BulletPool();
// 使用对象池
const bullet = bulletPool.obtain();
// 使用bullet...
bulletPool.free(bullet);
// 清空对象池
bulletPool.clear();
```
## 最佳实践
### 1. 实体设计
- 实体只包含基本属性,功能通过组件实现
- 合理使用实体层级关系
- 及时销毁不需要的实体
### 2. 组件设计
- 组件保持单一职责
- 使用生命周期方法进行初始化和清理
- 避免组件间直接依赖
### 3. 系统设计
- 系统专注于特定逻辑处理
- 合理设置系统更新顺序
- 使用被动系统处理特殊逻辑
### 4. 性能优化
- 使用对象池减少内存分配
- 监控性能数据
- 合理使用时间缩放
## 总结
ECS Framework 提供了完整的实体组件系统架构:
- **Core** 管理游戏生命周期和全局功能
- **Entity** 作为游戏对象的基础容器
- **Component** 实现具体的功能模块
- **System** 处理游戏逻辑
- **Scene** 管理游戏世界状态
通过合理使用这些核心概念,可以构建出结构清晰、易于维护的游戏代码。

23
docs/examples/index.md Normal file
View File

@@ -0,0 +1,23 @@
# 示例
这里展示了ECS Framework的各种使用示例通过实际的演示帮助您理解框架的功能和最佳实践。
## 🎮 互动演示
### [Worker系统演示](./worker-system-demo)
- **功能**: 展示Worker多线程物理计算和渲染优化
- **特性**: 1000+粒子实时物理模拟、碰撞检测、性能对比
- **技术点**: SharedArrayBuffer、Canvas 2D优化、实体生命周期管理
## 🔗 外部示例
### [割草机演示](https://github.com/esengine/lawn-mower-demo)
- **平台**: Cocos Creator 3.x
- **功能**: 完整的游戏演示项目
- **特性**: 展示ECS架构在实际游戏项目中的应用
## 📚 更多资源
- [快速开始指南](/guide/getting-started)
- [核心概念](/guide/)
- [API文档](/api/README)

View File

@@ -0,0 +1,13 @@
# Worker系统演示
这是一个展示ECS框架Worker系统功能的交互式演示。
## 在线演示
<div style="text-align: center; margin: 30px 0;">
<a href="/ecs-framework/demos/worker-system/index.html" target="_blank" style="display: inline-block; padding: 15px 30px; background: #4a9eff; color: white; text-decoration: none; border-radius: 8px; font-weight: bold; font-size: 16px; box-shadow: 0 4px 8px rgba(74, 158, 255, 0.3); transition: all 0.3s ease;">
🚀 打开Worker系统演示
</a>
</div>
> **注意**: 演示将在新窗口中打开展示完整的Worker系统功能包括实体管理、物理模拟和性能监控。

View File

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

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

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

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

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

288
docs/guide/entity.md Normal file
View File

@@ -0,0 +1,288 @@
# 实体类
在 ECS 架构中实体Entity是游戏世界中的基本对象。实体本身不包含游戏逻辑或数据它只是一个容器用来组合不同的组件来实现各种功能。
## 基本概念
实体是一个轻量级的对象,主要用于:
- 作为组件的容器
- 提供唯一标识ID
- 管理组件的生命周期
## 创建实体
**重要提示:实体必须通过场景创建,不支持手动创建!**
实体必须通过场景的 `createEntity()` 方法来创建,这样才能确保:
- 实体被正确添加到场景的实体管理系统中
- 实体被添加到查询系统中,供系统使用
- 实体获得正确的场景引用
- 触发相关的生命周期事件
```typescript
// 正确的方式:通过场景创建实体
const player = scene.createEntity("Player");
// ❌ 错误的方式:手动创建实体
// const entity = new Entity("MyEntity", 1); // 这样创建的实体系统无法管理
```
## 添加组件
实体通过添加组件来获得功能:
```typescript
import { Component, ECSComponent } from '@esengine/ecs-framework';
// 定义位置组件
@ECSComponent('Position')
class Position extends Component {
x: number = 0;
y: number = 0;
constructor(x: number = 0, y: number = 0) {
super();
this.x = x;
this.y = y;
}
}
// 定义健康组件
@ECSComponent('Health')
class Health extends Component {
current: number = 100;
max: number = 100;
constructor(max: number = 100) {
super();
this.max = max;
this.current = max;
}
}
// 给实体添加组件
const player = scene.createEntity("Player");
player.addComponent(new Position(100, 200));
player.addComponent(new Health(150));
```
## 获取组件
```typescript
// 获取组件(传入组件类,不是实例)
const position = player.getComponent(Position); // 返回 Position | null
const health = player.getComponent(Health); // 返回 Health | null
// 检查组件是否存在
if (position) {
console.log(`玩家位置: x=${position.x}, y=${position.y}`);
}
// 检查是否有某个组件
if (player.hasComponent(Position)) {
console.log("玩家有位置组件");
}
// 获取所有组件实例(只读属性)
const allComponents = player.components; // readonly Component[]
// 获取指定类型的所有组件(支持同类型多组件)
const allHealthComponents = player.getComponents(Health); // Health[]
// 获取或创建组件(如果不存在则自动创建)
const position = player.getOrCreateComponent(Position, 0, 0); // 传入构造参数
const health = player.getOrCreateComponent(Health, 100); // 如果存在则返回现有的,不存在则创建新的
```
## 移除组件
```typescript
// 方式1通过组件类型移除
const removedHealth = player.removeComponentByType(Health);
if (removedHealth) {
console.log("健康组件已被移除");
}
// 方式2通过组件实例移除
const healthComponent = player.getComponent(Health);
if (healthComponent) {
player.removeComponent(healthComponent);
}
// 批量移除多种组件类型
const removedComponents = player.removeComponentsByTypes([Position, Health]);
// 检查组件是否被移除
if (!player.hasComponent(Health)) {
console.log("健康组件已被移除");
}
```
## 实体查找
场景提供了多种方式来查找实体:
### 通过名称查找
```typescript
// 查找单个实体
const player = scene.findEntity("Player");
// 或使用别名方法
const player2 = scene.getEntityByName("Player");
if (player) {
console.log("找到玩家实体");
}
```
### 通过 ID 查找
```typescript
// 通过实体 ID 查找
const entity = scene.findEntityById(123);
```
### 通过标签查找
实体支持标签系统,用于快速分类和查找:
```typescript
// 设置标签
player.tag = 1; // 玩家标签
enemy.tag = 2; // 敌人标签
// 通过标签查找所有相关实体
const players = scene.findEntitiesByTag(1);
const enemies = scene.findEntitiesByTag(2);
// 或使用别名方法
const allPlayers = scene.getEntitiesByTag(1);
```
## 实体生命周期
```typescript
// 销毁实体
player.destroy();
// 检查实体是否已销毁
if (player.isDestroyed) {
console.log("实体已被销毁");
}
```
## 实体事件
实体的组件变化会触发事件:
```typescript
// 监听组件添加事件
scene.eventSystem.on('component:added', (data) => {
console.log('组件已添加:', data);
});
// 监听实体创建事件
scene.eventSystem.on('entity:created', (data) => {
console.log('实体已创建:', data.entityName);
});
```
## 性能优化
### 批量创建实体
框架提供了高性能的批量创建方法:
```typescript
// 批量创建 100 个子弹实体(高性能版本)
const bullets = scene.createEntities(100, "Bullet");
// 为每个子弹添加组件
bullets.forEach((bullet, index) => {
bullet.addComponent(new Position(Math.random() * 800, Math.random() * 600));
bullet.addComponent(new Velocity(Math.random() * 100 - 50, Math.random() * 100 - 50));
});
```
`createEntities()` 方法会:
- 批量分配实体 ID
- 批量添加到实体列表
- 优化查询系统更新
- 减少系统缓存清理次数
## 最佳实践
### 1. 合理的组件粒度
```typescript
// 好的做法:功能单一的组件
@ECSComponent('Position')
class Position extends Component {
x: number = 0;
y: number = 0;
}
@ECSComponent('Velocity')
class Velocity extends Component {
dx: number = 0;
dy: number = 0;
}
// 避免:功能过于复杂的组件
@ECSComponent('Player')
class Player extends Component {
// 避免在一个组件中包含太多不相关的属性
x: number;
y: number;
health: number;
inventory: Item[];
skills: Skill[];
}
```
### 2. 使用装饰器
始终使用 `@ECSComponent` 装饰器:
```typescript
@ECSComponent('Transform')
class Transform extends Component {
// 组件实现
}
```
### 3. 合理命名
```typescript
// 清晰的实体命名
const mainCharacter = scene.createEntity("MainCharacter");
const enemy1 = scene.createEntity("Goblin_001");
const collectible = scene.createEntity("HealthPotion");
```
### 4. 及时清理
```typescript
// 不再需要的实体应该及时销毁
if (enemy.getComponent(Health).current <= 0) {
enemy.destroy();
}
```
## 调试实体
框架提供了调试功能来帮助开发:
```typescript
// 获取实体调试信息
const debugInfo = entity.getDebugInfo();
console.log('实体信息:', debugInfo);
// 列出实体的所有组件
entity.components.forEach(component => {
console.log('组件:', component.constructor.name);
});
```
实体是 ECS 架构的核心概念之一,理解如何正确使用实体将帮助你构建高效、可维护的游戏代码。

551
docs/guide/event-system.md Normal file
View File

@@ -0,0 +1,551 @@
# 事件系统
ECS 框架内置了强大的类型安全事件系统,支持同步/异步事件、优先级、批处理等高级功能。事件系统是实现组件间通信、系统间协作的核心机制。
## 基本概念
事件系统提供了发布-订阅模式的实现,包含以下核心概念:
- **事件发布者**:发射事件的对象
- **事件监听者**:监听并处理特定事件的对象
- **事件类型**:字符串标识,用于区分不同类型的事件
- **事件数据**:事件携带的相关信息
## 事件系统架构
框架提供了两层事件系统:
1. **TypeSafeEventSystem** - 底层高性能事件系统
2. **EventBus** - 上层增强事件总线,提供更多便利功能
## 基本使用
### 在场景中使用事件系统
每个场景都有内置的事件系统:
```typescript
class GameScene extends Scene {
protected initialize(): void {
// 监听事件
this.eventSystem.on('player_died', this.onPlayerDied.bind(this));
this.eventSystem.on('enemy_spawned', this.onEnemySpawned.bind(this));
this.eventSystem.on('score_changed', this.onScoreChanged.bind(this));
}
private onPlayerDied(data: { player: Entity, cause: string }): void {
console.log(`玩家死亡,原因: ${data.cause}`);
// 处理玩家死亡逻辑
}
private onEnemySpawned(data: { enemy: Entity, position: { x: number, y: number } }): void {
console.log('敌人生成于:', data.position);
// 处理敌人生成逻辑
}
private onScoreChanged(data: { newScore: number, oldScore: number }): void {
console.log(`分数变化: ${data.oldScore} -> ${data.newScore}`);
// 更新UI显示
}
// 在系统中发射事件
someGameLogic(): void {
// 发射同步事件
this.eventSystem.emitSync('score_changed', {
newScore: 1000,
oldScore: 800
});
}
}
```
### 在系统中使用事件
系统可以方便地监听和发送事件:
```typescript
@ECSSystem('Combat')
class CombatSystem extends EntitySystem {
constructor() {
super(Matcher.all(Health, Combat));
}
protected onInitialize(): void {
// 使用系统提供的事件监听方法(自动清理)
this.addEventListener('player_attack', this.onPlayerAttack.bind(this));
this.addEventListener('enemy_death', this.onEnemyDeath.bind(this));
}
private onPlayerAttack(data: { damage: number, target: Entity }): void {
// 处理玩家攻击事件
const health = data.target.getComponent(Health);
if (health) {
health.current -= data.damage;
if (health.current <= 0) {
// 发送敌人死亡事件
this.scene?.eventSystem.emitSync('enemy_death', {
enemy: data.target,
killer: 'player'
});
}
}
}
private onEnemyDeath(data: { enemy: Entity, killer: string }): void {
// 处理敌人死亡
data.enemy.destroy();
// 发送经验奖励事件
this.scene?.eventSystem.emitSync('experience_gained', {
amount: 100,
source: 'enemy_kill'
});
}
protected process(entities: readonly Entity[]): void {
for (const entity of entities) {
const combat = entity.getComponent(Combat);
if (combat && combat.shouldAttack()) {
// 发射攻击事件
this.scene?.eventSystem.emitSync('player_attack', {
damage: combat.damage,
target: combat.target
});
}
}
}
}
```
## 高级功能
### 一次性监听器
```typescript
class GameScene extends Scene {
protected initialize(): void {
// 只监听一次的事件
this.eventSystem.once('game_start', this.onGameStart.bind(this));
// 或者使用配置对象
this.eventSystem.on('level_complete', this.onLevelComplete.bind(this), {
once: true // 只执行一次
});
}
private onGameStart(): void {
console.log('游戏开始!');
// 这个方法只会被调用一次
}
private onLevelComplete(): void {
console.log('关卡完成!');
// 这个方法也只会被调用一次
}
}
```
### 优先级控制
```typescript
class GameScene extends Scene {
protected initialize(): void {
// 高优先级监听器先执行
this.eventSystem.on('damage_dealt', this.onDamageDealt.bind(this), {
priority: 100 // 高优先级
});
// 普通优先级
this.eventSystem.on('damage_dealt', this.updateUI.bind(this), {
priority: 0 // 默认优先级
});
// 低优先级最后执行
this.eventSystem.on('damage_dealt', this.logDamage.bind(this), {
priority: -100 // 低优先级
});
}
private onDamageDealt(data: any): void {
// 最先执行 - 处理核心游戏逻辑
}
private updateUI(data: any): void {
// 中等优先级 - 更新界面
}
private logDamage(data: any): void {
// 最后执行 - 记录日志
}
}
```
### 异步事件处理
```typescript
class GameScene extends Scene {
protected initialize(): void {
// 监听异步事件
this.eventSystem.onAsync('save_game', this.onSaveGame.bind(this));
this.eventSystem.onAsync('load_data', this.onLoadData.bind(this));
}
private async onSaveGame(data: { saveSlot: number }): Promise<void> {
console.log(`开始保存游戏到槽位 ${data.saveSlot}`);
// 模拟异步保存操作
await this.saveGameData(data.saveSlot);
console.log('游戏保存完成');
}
private async onLoadData(data: { url: string }): Promise<void> {
try {
const response = await fetch(data.url);
const gameData = await response.json();
// 处理加载的数据
} catch (error) {
console.error('数据加载失败:', error);
}
}
private async saveGameData(slot: number): Promise<void> {
// 模拟保存操作
return new Promise(resolve => setTimeout(resolve, 1000));
}
// 发射异步事件
public async triggerSave(): Promise<void> {
// 使用 emit 而不是 emitSync 来触发异步监听器
await this.eventSystem.emit('save_game', { saveSlot: 1 });
console.log('所有异步保存操作完成');
}
}
```
### 事件统计和调试
```typescript
class GameScene extends Scene {
protected initialize(): void {
this.eventSystem.on('debug_event', this.onDebugEvent.bind(this));
}
private onDebugEvent(): void {
// 处理调试事件
}
public showEventStats(): void {
// 获取特定事件的统计信息
const stats = this.eventSystem.getStats('debug_event') as any;
if (stats) {
console.log('事件统计:');
console.log(`- 事件类型: ${stats.eventType}`);
console.log(`- 监听器数量: ${stats.listenerCount}`);
console.log(`- 触发次数: ${stats.triggerCount}`);
console.log(`- 平均执行时间: ${stats.averageExecutionTime.toFixed(2)}ms`);
}
// 获取所有事件统计
const allStats = this.eventSystem.getStats() as Map<string, any>;
console.log(`总共有 ${allStats.size} 种事件类型`);
}
public resetEventStats(): void {
// 重置特定事件的统计
this.eventSystem.resetStats('debug_event');
// 或重置所有统计
this.eventSystem.resetStats();
}
}
```
## 全局事件总线
对于跨场景的事件通信,可以使用全局事件总线:
```typescript
import { GlobalEventBus } from '@esengine/ecs-framework';
class GameManager {
private eventBus = GlobalEventBus.getInstance();
constructor() {
this.setupGlobalEvents();
}
private setupGlobalEvents(): void {
// 监听全局事件
this.eventBus.on('player_level_up', this.onPlayerLevelUp.bind(this));
this.eventBus.on('achievement_unlocked', this.onAchievementUnlocked.bind(this));
this.eventBus.onAsync('upload_score', this.onUploadScore.bind(this));
}
private onPlayerLevelUp(data: { level: number, experience: number }): void {
console.log(`玩家升级到 ${data.level} 级!`);
// 处理全局升级逻辑
}
private onAchievementUnlocked(data: { achievementId: string, name: string }): void {
console.log(`解锁成就: ${data.name}`);
// 显示成就通知
}
private async onUploadScore(data: { score: number, playerName: string }): Promise<void> {
// 异步上传分数到服务器
try {
await this.uploadToServer(data);
console.log('分数上传成功');
} catch (error) {
console.error('分数上传失败:', error);
}
}
public triggerGlobalEvent(): void {
// 发射全局事件
this.eventBus.emit('player_level_up', {
level: 10,
experience: 2500
});
}
private async uploadToServer(data: any): Promise<void> {
// 模拟服务器上传
return new Promise(resolve => setTimeout(resolve, 2000));
}
}
```
## 批处理事件
对于高频事件,可以使用批处理来提升性能:
```typescript
class MovementSystem extends EntitySystem {
protected onInitialize(): void {
// 设置位置更新事件的批处理
this.scene?.eventSystem.setBatchConfig('position_updated', {
batchSize: 50, // 批处理大小
delay: 16, // 延迟时间(毫秒)
enabled: true
});
// 监听批处理事件
this.addEventListener('position_updated:batch', this.onPositionBatch.bind(this));
}
private onPositionBatch(batchData: any): void {
console.log(`批处理位置更新,共 ${batchData.count} 个事件`);
// 批量处理所有位置更新
for (const event of batchData.events) {
this.updateMinimap(event.entityId, event.position);
}
}
protected process(entities: readonly Entity[]): void {
for (const entity of entities) {
const position = entity.getComponent(Position);
if (position && position.hasChanged) {
// 发射高频位置更新事件(会被批处理)
this.scene?.eventSystem.emitSync('position_updated', {
entityId: entity.id,
position: { x: position.x, y: position.y }
});
}
}
}
private updateMinimap(entityId: number, position: { x: number, y: number }): void {
// 更新小地图显示
}
public flushPositionUpdates(): void {
// 立即处理所有待处理的位置更新
this.scene?.eventSystem.flushBatch('position_updated');
}
}
```
## 预定义的 ECS 事件
框架提供了一些预定义的 ECS 生命周期事件:
```typescript
import { ECSEventType } from '@esengine/ecs-framework';
class ECSMonitor {
private eventBus = GlobalEventBus.getInstance();
constructor() {
this.setupECSEvents();
}
private setupECSEvents(): void {
// 监听实体生命周期事件
this.eventBus.onEntityCreated(this.onEntityCreated.bind(this));
this.eventBus.on(ECSEventType.ENTITY_DESTROYED, this.onEntityDestroyed.bind(this));
// 监听组件生命周期事件
this.eventBus.onComponentAdded(this.onComponentAdded.bind(this));
this.eventBus.on(ECSEventType.COMPONENT_REMOVED, this.onComponentRemoved.bind(this));
// 监听系统事件
this.eventBus.on(ECSEventType.SYSTEM_ADDED, this.onSystemAdded.bind(this));
this.eventBus.onSystemError(this.onSystemError.bind(this));
// 监听性能警告
this.eventBus.onPerformanceWarning(this.onPerformanceWarning.bind(this));
}
private onEntityCreated(data: any): void {
console.log(`实体创建: ${data.entityName} (ID: ${data.entity.id})`);
}
private onEntityDestroyed(data: any): void {
console.log(`实体销毁: ${data.entity.name} (ID: ${data.entity.id})`);
}
private onComponentAdded(data: any): void {
console.log(`组件添加: ${data.componentType} 到实体 ${data.entity.name}`);
}
private onComponentRemoved(data: any): void {
console.log(`组件移除: ${data.componentType} 从实体 ${data.entity.name}`);
}
private onSystemAdded(data: any): void {
console.log(`系统添加: ${data.systemName}`);
}
private onSystemError(data: any): void {
console.error(`系统错误: ${data.systemName}`, data.error);
}
private onPerformanceWarning(data: any): void {
console.warn(`性能警告: ${data.systemName} 执行时间过长 (${data.executionTime}ms)`);
}
}
```
## 最佳实践
### 1. 事件命名规范
```typescript
// ✅ 好的事件命名
this.eventSystem.emitSync('player:health_changed', data);
this.eventSystem.emitSync('enemy:spawned', data);
this.eventSystem.emitSync('ui:score_updated', data);
this.eventSystem.emitSync('game:paused', data);
// ❌ 避免的事件命名
this.eventSystem.emitSync('event1', data);
this.eventSystem.emitSync('update', data);
this.eventSystem.emitSync('change', data);
```
### 2. 类型安全的事件数据
```typescript
// 定义事件数据接口
interface PlayerHealthChangedEvent {
entityId: number;
oldHealth: number;
newHealth: number;
cause: 'damage' | 'healing';
}
interface EnemySpawnedEvent {
enemyType: string;
position: { x: number, y: number };
level: number;
}
// 使用类型安全的事件
class HealthSystem extends EntitySystem {
private onHealthChanged(data: PlayerHealthChangedEvent): void {
// TypeScript 会提供完整的类型检查
console.log(`生命值变化: ${data.oldHealth} -> ${data.newHealth}`);
}
}
```
### 3. 避免事件循环
```typescript
// ❌ 避免:可能导致无限循环
class BadEventHandler {
private onScoreChanged(data: any): void {
// 在处理分数变化时又触发分数变化事件
this.scene?.eventSystem.emitSync('score_changed', newData); // 危险!
}
}
// ✅ 正确:使用不同的事件类型或条件判断
class GoodEventHandler {
private isProcessingScore = false;
private onScoreChanged(data: any): void {
if (this.isProcessingScore) return; // 防止循环
this.isProcessingScore = true;
// 处理分数变化
this.updateUI(data);
this.isProcessingScore = false;
}
}
```
### 4. 及时清理事件监听器
```typescript
class TemporaryUI {
private listenerId: string;
constructor(scene: Scene) {
// 保存监听器ID用于后续清理
this.listenerId = scene.eventSystem.on('ui_update', this.onUpdate.bind(this));
}
private onUpdate(data: any): void {
// 处理UI更新
}
public destroy(): void {
// 清理事件监听器
if (this.listenerId) {
scene.eventSystem.off('ui_update', this.listenerId);
}
}
}
```
### 5. 性能考虑
```typescript
class OptimizedEventHandler {
protected onInitialize(): void {
// 对于高频事件,使用批处理
this.scene?.eventSystem.setBatchConfig('movement_update', {
batchSize: 100,
delay: 16,
enabled: true
});
// 对于低频但重要的事件,使用高优先级
this.addEventListener('game_over', this.onGameOver.bind(this), {
priority: 1000
});
// 对于一次性事件,使用 once
this.addEventListener('level_start', this.onLevelStart.bind(this), {
once: true
});
}
}
```
事件系统是 ECS 框架中实现松耦合架构的重要工具,正确使用事件系统能让你的游戏代码更加模块化、可维护和可扩展。

View File

@@ -0,0 +1,417 @@
# 快速开始
本指南将帮助你快速上手 ECS Framework从安装到创建第一个 ECS 应用。
## 安装
### NPM 安装
```bash
# 使用 npm
npm install @esengine/ecs-framework
```
## 初始化 Core
### 基础初始化
ECS Framework 的核心是 `Core` 类,它是一个单例模式,负责管理整个框架的生命周期。
```typescript
import { Core } from '@esengine/ecs-framework'
// 方式1使用配置对象推荐
const core = Core.create({
debug: true, // 启用调试模式,提供详细的日志和性能监控
enableEntitySystems: true, // 启用实体系统这是ECS的核心功能
debugConfig: { // 可选:高级调试配置
enabled: false, // 是否启用WebSocket调试服务器
websocketUrl: 'ws://localhost:8080',
debugFrameRate: 30, // 调试数据发送帧率
channels: {
entities: true,
systems: true,
performance: true,
components: true,
scenes: true
}
}
});
// 方式2简化创建向后兼容
const core = Core.create(true); // 等同于 { debug: true, enableEntitySystems: true }
// 方式3生产环境配置
const core = Core.create({
debug: false, // 生产环境关闭调试
enableEntitySystems: true
});
```
### Core 配置详解
```typescript
interface ICoreConfig {
/** 是否启用调试模式 - 影响日志级别和性能监控 */
debug?: boolean;
/** 是否启用实体系统 - 核心ECS功能开关 */
enableEntitySystems?: boolean;
/** 高级调试配置 - 用于开发工具集成 */
debugConfig?: {
enabled: boolean; // 是否启用调试服务器
websocketUrl: string; // WebSocket服务器地址
autoReconnect?: boolean; // 是否自动重连
debugFrameRate?: 60 | 30 | 15; // 调试数据发送帧率
channels: { // 数据通道配置
entities: boolean; // 实体数据
systems: boolean; // 系统数据
performance: boolean; // 性能数据
components: boolean; // 组件数据
scenes: boolean; // 场景数据
};
};
}
```
### Core 实例管理
Core 采用单例模式,创建后可以通过静态属性获取:
```typescript
// 创建实例
const core = Core.create(true);
// 获取已创建的实例
const instance = Core.Instance; // 获取当前实例,如果未创建则为 null
```
### 游戏循环集成
**重要**: 在创建实体和系统之前,你需要先了解如何将 ECS Framework 集成到你的游戏引擎中。
`Core.update(deltaTime)` 是整个框架的心跳,必须在游戏引擎的每一帧中调用。它负责:
- 更新框架内置的 Time 类
- 更新所有全局管理器(定时器、对象池等)
- 更新所有场景中的实体系统
- 处理实体的创建和销毁
- 收集性能数据(调试模式下)
各引擎集成示例请参考:[与游戏引擎集成](#与游戏引擎集成)
## 创建第一个 ECS 应用
### 1. 定义组件
组件是纯数据容器,用于存储实体的状态:
```typescript
import { Component, ECSComponent } from '@esengine/ecs-framework'
// 位置组件
@ECSComponent('Position')
class Position extends Component {
x: number = 0
y: number = 0
constructor(x: number = 0, y: number = 0) {
super()
this.x = x
this.y = y
}
}
// 速度组件
@ECSComponent('Velocity')
class Velocity extends Component {
dx: number = 0
dy: number = 0
constructor(dx: number = 0, dy: number = 0) {
super()
this.dx = dx
this.dy = dy
}
}
// 渲染组件
@ECSComponent('Sprite')
class Sprite extends Component {
texture: string = ''
width: number = 32
height: number = 32
constructor(texture: string, width: number = 32, height: number = 32) {
super()
this.texture = texture
this.width = width
this.height = height
}
}
```
### 2. 创建实体系统
系统包含游戏逻辑处理具有特定组件的实体。ECS Framework 提供了基于 Matcher 的实体过滤机制:
```typescript
import { EntitySystem, Matcher, Time, ECSSystem } from '@esengine/ecs-framework'
// 移动系统 - 处理位置和速度
@ECSSystem('MovementSystem')
class MovementSystem extends EntitySystem {
constructor() {
// 使用 Matcher 定义要处理的实体:必须同时拥有 Position 和 Velocity 组件
super(Matcher.empty().all(Position, Velocity))
}
protected process(entities: readonly Entity[]): void {
// process 方法接收所有匹配的实体
for (const entity of entities) {
const position = entity.getComponent(Position)!
const velocity = entity.getComponent(Velocity)!
// 更新位置使用框架的Time类
position.x += velocity.dx * Time.deltaTime
position.y += velocity.dy * Time.deltaTime
// 边界检查示例
if (position.x < 0) position.x = 0
if (position.y < 0) position.y = 0
}
}
}
// 渲染系统 - 处理可见对象
@ECSSystem('RenderSystem')
class RenderSystem extends EntitySystem {
constructor() {
// 必须有 Position 和 Sprite可选 Velocity用于方向判断
super(Matcher.empty().all(Position, Sprite).any(Velocity))
}
protected process(entities: readonly Entity[]): void {
for (const entity of entities) {
const position = entity.getComponent(Position)!
const sprite = entity.getComponent(Sprite)!
const velocity = entity.getComponent(Velocity) // 可能为 null
// 根据速度方向翻转精灵(可选逻辑)
let flipX = false
if (velocity && velocity.dx < 0) {
flipX = true
}
// 渲染逻辑(这里是伪代码)
this.drawSprite(sprite.texture, position.x, position.y, sprite.width, sprite.height, flipX)
}
}
private drawSprite(texture: string, x: number, y: number, width: number, height: number, flipX: boolean = false) {
// 实际的渲染实现将取决于你使用的游戏引擎
const direction = flipX ? '←' : '→'
console.log(`渲染 ${texture} 在位置 (${x.toFixed(1)}, ${y.toFixed(1)}) 方向: ${direction}`)
}
}
```
### 3. 创建场景
推荐继承 Scene 类来创建自定义场景:
```typescript
import { Scene } from '@esengine/ecs-framework'
// 推荐继承Scene创建自定义场景
class GameScene extends Scene {
initialize(): void {
// 场景初始化逻辑
this.name = "MainScene";
// 添加系统到场景
this.addSystem(new MovementSystem());
this.addSystem(new RenderSystem());
}
onStart(): void {
// 场景开始运行时的逻辑
console.log("游戏场景已启动");
}
unload(): void {
// 场景卸载时的清理逻辑
console.log("游戏场景已卸载");
}
}
// 创建并设置场景
const gameScene = new GameScene();
Core.setScene(gameScene);
```
### 4. 创建实体
```typescript
// 创建玩家实体
const player = gameScene.createEntity("Player");
player.addComponent(new Position(100, 100));
player.addComponent(new Velocity(50, 30)); // 每秒移动 50 像素x方向30 像素y方向
player.addComponent(new Sprite("player.png", 64, 64));
```
## 场景管理
Core 内置了场景管理功能,使用非常简单:
```typescript
import { Core, Scene } from '@esengine/ecs-framework';
// 初始化Core
Core.create({ debug: true });
// 创建并设置场景
class GameScene extends Scene {
initialize(): void {
this.name = "GamePlay";
this.addSystem(new MovementSystem());
this.addSystem(new RenderSystem());
}
}
const gameScene = new GameScene();
Core.setScene(gameScene);
// 游戏循环(自动更新场景)
function gameLoop(deltaTime: number) {
Core.update(deltaTime); // 自动更新全局服务和场景
}
// 切换场景
Core.loadScene(new MenuScene()); // 延迟切换(下一帧)
Core.setScene(new GameScene()); // 立即切换
// 访问当前场景
const currentScene = Core.scene;
// 使用流式API
const player = Core.ecsAPI?.createEntity('Player')
.addComponent(Position, 100, 100)
.addComponent(Velocity, 50, 0);
```
### 高级:使用 WorldManager 管理多世界
仅适用于复杂的服务器端应用MMO游戏服务器、游戏房间系统等
```typescript
import { Core, WorldManager } from '@esengine/ecs-framework';
// 初始化Core
Core.create({ debug: true });
// 从服务容器获取 WorldManagerCore 已自动创建并注册)
const worldManager = Core.services.resolve(WorldManager);
// 创建多个独立的游戏世界
const room1 = worldManager.createWorld('room_001');
const room2 = worldManager.createWorld('room_002');
// 在每个世界中创建场景
const gameScene1 = room1.createScene('game', new GameScene());
const gameScene2 = room2.createScene('game', new GameScene());
// 激活场景
room1.setSceneActive('game', true);
room2.setSceneActive('game', true);
// 游戏循环(需要手动更新世界)
function gameLoop(deltaTime: number) {
Core.update(deltaTime); // 更新全局服务
worldManager.updateAll(); // 手动更新所有世界
}
```
## 与游戏引擎集成
### Laya 引擎集成
```typescript
import { Stage } from "laya/display/Stage";
import { Laya } from "Laya";
import { Core } from '@esengine/ecs-framework';
// 初始化 Laya
Laya.init(800, 600).then(() => {
// 初始化 ECS
Core.create(true);
Core.setScene(new GameScene());
// 启动游戏循环
Laya.timer.frameLoop(1, this, () => {
const deltaTime = Laya.timer.delta / 1000;
Core.update(deltaTime); // 自动更新全局服务和场景
});
});
```
### Cocos Creator 集成
```typescript
import { Component, _decorator } from 'cc';
import { Core } from '@esengine/ecs-framework';
const { ccclass } = _decorator;
@ccclass('ECSGameManager')
export class ECSGameManager extends Component {
onLoad() {
// 初始化 ECS
Core.create(true);
Core.setScene(new GameScene());
}
update(deltaTime: number) {
// 自动更新全局服务和场景
Core.update(deltaTime);
}
onDestroy() {
// 清理资源
Core.destroy();
}
}
```
## 下一步
现在你已经成功创建了第一个 ECS 应用!接下来可以:
- 查看完整的 [API 文档](/api/README)
- 探索更多[实际应用示例](/examples/)
## 常见问题
### 为什么我的系统没有执行?
确保:
1. 系统已添加到场景:`this.addSystem(system)` (在 Scene 的 initialize 方法中)
2. 场景已设置:`Core.setScene(scene)`
3. 游戏循环在调用:`Core.update(deltaTime)`
### 如何调试 ECS 应用?
启用调试模式:
```typescript
Core.create({ debug: true })
// 获取调试数据
const debugData = Core.getDebugData()
console.log(debugData)
```

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

@@ -0,0 +1,40 @@
# 指南
欢迎使用 ECS Framework 指南。这里将详细介绍框架的各个核心概念和使用方法。
## 核心概念
### [实体类 (Entity)](./entity.md)
了解 ECS 架构的基础 - 实体类的使用方法、生命周期管理和最佳实践。
### [组件系统 (Component)](./component.md)
学习如何创建和使用组件,实现游戏功能的模块化设计。
### [系统架构 (System)](./system.md)
掌握系统的编写方法,实现游戏逻辑的处理。
### [场景管理 (Scene)](./scene.md)
了解场景的生命周期、系统管理和实体容器功能。
### [事件系统 (Event)](./event-system.md)
掌握类型安全的事件系统,实现组件间通信和系统协作。
### [序列化系统 (Serialization)](./serialization.md)
掌握场景、实体和组件的序列化方案,支持全量序列化和增量序列化,实现游戏存档、网络同步等功能。
### [时间和定时器 (Time)](./time-and-timers.md)
学习时间管理和定时器系统,实现游戏逻辑的精确时间控制。
### [日志系统 (Logger)](./logging.md)
掌握分级日志系统,用于调试、监控和错误追踪。
### [平台适配器 (Platform Adapter)](./platform-adapter.md)
了解如何为不同平台实现和注册平台适配器支持浏览器、小游戏、Node.js等环境。
## 高级特性
### [服务容器 (Service Container)](./service-container.md)
掌握依赖注入和服务管理,实现松耦合的架构设计。
### [插件系统 (Plugin System)](./plugin-system.md)
学习如何开发和使用插件,扩展框架功能,实现功能模块化。

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

@@ -0,0 +1,550 @@
# 日志系统
ECS 框架提供了功能强大的分级日志系统,支持多种日志级别、颜色输出、自定义前缀和灵活的配置选项。日志系统可以帮助开发者调试代码和监控应用运行状态。
## 基本概念
日志系统包含以下核心概念:
- **日志级别**Debug < Info < Warn < Error < Fatal < None
- **日志器**:具名的日志输出器,每个模块可以有自己的日志器
- **日志管理器**:全局管理所有日志器的单例
- **颜色配置**:支持控制台颜色输出
## 日志级别
```typescript
import { LogLevel } from '@esengine/ecs-framework';
// 日志级别从低到高
LogLevel.Debug // 0 - 调试信息
LogLevel.Info // 1 - 一般信息
LogLevel.Warn // 2 - 警告信息
LogLevel.Error // 3 - 错误信息
LogLevel.Fatal // 4 - 致命错误
LogLevel.None // 5 - 不输出任何日志
```
## 基本使用
### 使用默认日志器
```typescript
import { Logger } from '@esengine/ecs-framework';
class GameSystem extends EntitySystem {
protected process(entities: readonly Entity[]): void {
// 输出不同级别的日志
Logger.debug('处理实体数量:', entities.length);
Logger.info('系统正常运行');
Logger.warn('检测到性能问题');
Logger.error('处理过程中发生错误', new Error('示例错误'));
Logger.fatal('致命错误,系统即将停止');
}
}
```
### 创建命名日志器
```typescript
import { createLogger } from '@esengine/ecs-framework';
class MovementSystem extends EntitySystem {
private logger = createLogger('MovementSystem');
protected process(entities: readonly Entity[]): void {
this.logger.info(`处理 ${entities.length} 个移动实体`);
for (const entity of entities) {
const position = entity.getComponent(Position);
const velocity = entity.getComponent(Velocity);
if (position && velocity) {
position.x += velocity.dx * Time.deltaTime;
position.y += velocity.dy * Time.deltaTime;
this.logger.debug(`实体 ${entity.id} 移动到位置 (${position.x}, ${position.y})`);
}
}
}
protected onAdded(entity: Entity): void {
this.logger.info(`实体 ${entity.name} 加入移动系统`);
}
protected onRemoved(entity: Entity): void {
this.logger.warn(`实体 ${entity.name} 离开移动系统`);
}
}
```
### 系统内置日志器
框架的各个系统都有自己的日志器:
```typescript
// 框架内部使用示例
class Scene {
private static readonly _logger = createLogger('Scene');
public addSystem(system: EntitySystem): void {
Scene._logger.info(`添加系统: ${system.systemName}`);
// 系统添加逻辑
}
public removeSystem(system: EntitySystem): void {
Scene._logger.warn(`移除系统: ${system.systemName}`);
// 系统移除逻辑
}
}
```
## 日志配置
### 设置全局日志级别
```typescript
import { setGlobalLogLevel, LogLevel } from '@esengine/ecs-framework';
// 在开发环境显示所有日志
setGlobalLogLevel(LogLevel.Debug);
// 在生产环境只显示警告及以上级别
setGlobalLogLevel(LogLevel.Warn);
// 完全禁用日志输出
setGlobalLogLevel(LogLevel.None);
```
### 创建自定义配置的日志器
```typescript
import { ConsoleLogger, LogLevel } from '@esengine/ecs-framework';
class CustomLoggerExample {
private debugLogger: ConsoleLogger;
private productionLogger: ConsoleLogger;
constructor() {
// 开发环境日志器
this.debugLogger = new ConsoleLogger({
level: LogLevel.Debug,
enableTimestamp: true,
enableColors: true,
prefix: 'DEV'
});
// 生产环境日志器
this.productionLogger = new ConsoleLogger({
level: LogLevel.Error,
enableTimestamp: true,
enableColors: false,
prefix: 'PROD'
});
}
public logDevelopmentInfo(): void {
this.debugLogger.debug('这是调试信息');
this.debugLogger.info('开发环境信息');
}
public logProductionError(): void {
this.productionLogger.error('生产环境错误');
this.productionLogger.fatal('致命错误');
}
}
```
## 颜色配置
### 使用预定义颜色
```typescript
import { Colors, setLoggerColors } from '@esengine/ecs-framework';
// 自定义颜色方案
setLoggerColors({
debug: Colors.BRIGHT_BLACK,
info: Colors.BLUE,
warn: Colors.YELLOW,
error: Colors.RED,
fatal: Colors.BRIGHT_RED
});
```
### 完整颜色示例
```typescript
import { LoggerManager, Colors, LogLevel } from '@esengine/ecs-framework';
class ColorLoggerDemo {
private logger = createLogger('ColorDemo');
constructor() {
// 设置自定义颜色
const manager = LoggerManager.getInstance();
manager.setGlobalColors({
debug: Colors.CYAN,
info: Colors.GREEN,
warn: Colors.YELLOW,
error: Colors.RED,
fatal: `${Colors.BOLD}${Colors.BRIGHT_RED}`
});
}
public demonstrateColors(): void {
this.logger.debug('这是蓝绿色的调试信息');
this.logger.info('这是绿色的信息');
this.logger.warn('这是黄色的警告');
this.logger.error('这是红色的错误');
this.logger.fatal('这是加粗的亮红色致命错误');
}
public resetToDefaults(): void {
// 重置为默认颜色
LoggerManager.getInstance().resetColors();
}
}
```
## 高级功能
### 分层日志器
```typescript
import { LoggerManager } from '@esengine/ecs-framework';
class HierarchicalLoggingExample {
private systemLogger = createLogger('GameSystems');
private movementLogger: ILogger;
private renderLogger: ILogger;
constructor() {
const manager = LoggerManager.getInstance();
// 创建子日志器
this.movementLogger = manager.createChildLogger('GameSystems', 'Movement');
this.renderLogger = manager.createChildLogger('GameSystems', 'Render');
}
public demonstrateHierarchy(): void {
this.systemLogger.info('游戏系统启动');
// 子日志器会显示完整路径:[GameSystems.Movement]
this.movementLogger.debug('移动系统初始化');
// 子日志器会显示完整路径:[GameSystems.Render]
this.renderLogger.info('渲染系统启动');
}
}
```
### 自定义输出
```typescript
import { ConsoleLogger, LogLevel } from '@esengine/ecs-framework';
class CustomOutputLogger {
private fileLogger: ConsoleLogger;
private networkLogger: ConsoleLogger;
constructor() {
// 输出到文件的日志器(模拟)
this.fileLogger = new ConsoleLogger({
level: LogLevel.Info,
output: (level: LogLevel, message: string) => {
this.writeToFile(LogLevel[level], message);
}
});
// 发送到网络的日志器(模拟)
this.networkLogger = new ConsoleLogger({
level: LogLevel.Error,
output: (level: LogLevel, message: string) => {
this.sendToServer(LogLevel[level], message);
}
});
}
private writeToFile(level: string, message: string): void {
// 模拟文件写入
console.log(`[FILE] ${level}: ${message}`);
}
private sendToServer(level: string, message: string): void {
// 模拟网络发送
console.log(`[NETWORK] ${level}: ${message}`);
}
public logToFile(message: string): void {
this.fileLogger.info(message);
}
public logCriticalError(error: Error): void {
this.networkLogger.error('Critical error occurred', error);
}
}
```
## 实际应用示例
### 游戏系统日志
```typescript
class GameWithLogging {
private gameLogger = createLogger('Game');
private performanceLogger = createLogger('Performance');
private networkLogger = createLogger('Network');
constructor() {
// 在开发环境启用详细日志
if (process.env.NODE_ENV === 'development') {
setGlobalLogLevel(LogLevel.Debug);
} else {
setGlobalLogLevel(LogLevel.Warn);
}
}
public startGame(): void {
this.gameLogger.info('游戏开始启动');
try {
this.initializeSystems();
this.loadResources();
this.startGameLoop();
this.gameLogger.info('游戏启动成功');
} catch (error) {
this.gameLogger.fatal('游戏启动失败', error);
throw error;
}
}
private initializeSystems(): void {
this.gameLogger.debug('初始化游戏系统');
const systems = [
new MovementSystem(),
new RenderSystem(),
new PhysicsSystem()
];
for (const system of systems) {
const startTime = performance.now();
// 初始化系统
system.initialize();
const endTime = performance.now();
this.performanceLogger.debug(
`系统 ${system.systemName} 初始化耗时: ${(endTime - startTime).toFixed(2)}ms`
);
}
}
private loadResources(): void {
this.gameLogger.info('开始加载资源');
const resources = ['textures', 'sounds', 'data'];
for (const resource of resources) {
try {
this.loadResource(resource);
this.gameLogger.debug(`资源 ${resource} 加载成功`);
} catch (error) {
this.gameLogger.error(`资源 ${resource} 加载失败`, error);
}
}
}
private startGameLoop(): void {
this.gameLogger.info('启动游戏循环');
this.performanceLogger.debug('开始性能监控');
}
private loadResource(name: string): void {
// 模拟资源加载
if (Math.random() < 0.1) {
throw new Error(`Failed to load ${name}`);
}
}
public handleNetworkEvent(event: string, data: any): void {
this.networkLogger.info(`网络事件: ${event}`, data);
if (event === 'connection_lost') {
this.networkLogger.warn('网络连接丢失,尝试重连');
} else if (event === 'sync_error') {
this.networkLogger.error('数据同步错误', data);
}
}
}
```
### 错误追踪和调试
```typescript
class ErrorTrackingSystem extends EntitySystem {
private logger = createLogger('ErrorTracker');
private errorCounts = new Map<string, number>();
protected process(entities: readonly Entity[]): void {
for (const entity of entities) {
try {
this.processEntity(entity);
} catch (error) {
this.handleError(entity, error as Error);
}
}
}
private processEntity(entity: Entity): void {
// 模拟可能出错的实体处理
if (Math.random() < 0.01) { // 1% 概率出错
throw new Error(`Processing error for entity ${entity.id}`);
}
this.logger.debug(`成功处理实体 ${entity.id}`);
}
private handleError(entity: Entity, error: Error): void {
const errorKey = error.message;
const count = this.errorCounts.get(errorKey) || 0;
this.errorCounts.set(errorKey, count + 1);
this.logger.error(
`实体 ${entity.id} 处理失败 (第${count + 1}次): ${error.message}`,
{
entityId: entity.id,
entityName: entity.name,
componentCount: entity.components.length,
errorStack: error.stack
}
);
// 如果同一类型错误发生太多次,升级为警告
if (count >= 5) {
this.logger.warn(`错误 "${errorKey}" 已发生 ${count + 1} 次,可能需要关注`);
}
// 如果错误次数过多,升级为致命错误
if (count >= 20) {
this.logger.fatal(`错误 "${errorKey}" 发生次数过多,系统可能存在严重问题`);
}
}
public getErrorSummary(): void {
this.logger.info('=== 错误统计 ===');
for (const [error, count] of this.errorCounts) {
this.logger.info(`${error}: ${count}`);
}
}
}
```
## 最佳实践
### 1. 合理的日志级别选择
```typescript
class LoggingBestPractices {
private logger = createLogger('BestPractices');
public demonstrateLogLevels(): void {
// ✅ Debug - 详细的调试信息
this.logger.debug('变量值', { x: 10, y: 20 });
// ✅ Info - 重要的状态变化
this.logger.info('系统启动完成');
// ✅ Warn - 异常但不致命的情况
this.logger.warn('资源未找到,使用默认值');
// ✅ Error - 错误但程序可以继续
this.logger.error('保存失败,将重试', new Error('Network timeout'));
// ✅ Fatal - 致命错误,程序无法继续
this.logger.fatal('内存不足,程序即将退出');
}
}
```
### 2. 结构化日志数据
```typescript
class StructuredLogging {
private logger = createLogger('Structured');
public logWithStructuredData(): void {
// ✅ 提供结构化的上下文信息
this.logger.info('用户操作', {
userId: 12345,
action: 'move',
position: { x: 100, y: 200 },
timestamp: Date.now()
});
// ✅ 包含相关的错误上下文
this.logger.error('数据库查询失败', {
query: 'SELECT * FROM users',
parameters: { id: 123 },
connectionId: 'conn_456',
retryCount: 3
});
}
}
```
### 3. 避免日志性能问题
```typescript
class PerformanceConsciousLogging {
private logger = createLogger('Performance');
public efficientLogging(): void {
// ✅ 检查日志级别避免不必要的计算
if (this.logger.debug) {
const expensiveData = this.calculateExpensiveDebugInfo();
this.logger.debug('详细调试信息', expensiveData);
}
// ❌ 避免:总是计算昂贵的日志数据
// this.logger.debug('调试信息', this.calculateExpensiveDebugInfo());
}
private calculateExpensiveDebugInfo(): any {
// 模拟昂贵的计算
return { /* 复杂的调试数据 */ };
}
}
```
### 4. 日志配置管理
```typescript
class LoggingConfiguration {
public static setupLogging(): void {
// 根据环境配置日志级别
const isDevelopment = process.env.NODE_ENV === 'development';
const isProduction = process.env.NODE_ENV === 'production';
if (isDevelopment) {
setGlobalLogLevel(LogLevel.Debug);
setLoggerColors({
debug: Colors.CYAN,
info: Colors.GREEN,
warn: Colors.YELLOW,
error: Colors.RED,
fatal: Colors.BRIGHT_RED
});
} else if (isProduction) {
setGlobalLogLevel(LogLevel.Warn);
// 生产环境禁用颜色
LoggerManager.getInstance().resetColors();
}
}
}
// 在应用启动时配置日志
LoggingConfiguration.setupLogging();
```
日志系统是调试和监控应用的重要工具,正确使用日志系统能大大提高开发效率和问题排查能力。

View File

@@ -0,0 +1,289 @@
# 平台适配器
## 概述
ECS框架提供了平台适配器接口允许用户为不同的运行环境实现自定义的平台适配器。
**核心库只提供接口定义,平台适配器实现代码请从文档中复制使用。**
## 为什么不提供单独的适配器包?
1. **灵活性**: 不同项目对平台适配的需求可能不同,复制代码可以让用户根据需要自由修改
2. **减少依赖**: 避免引入不必要的依赖包,保持核心框架的精简
3. **定制化**: 用户可以根据具体的运行环境和需求进行定制
## 支持的平台
### 🌐 [浏览器适配器](./platform-adapter/browser.md)
支持所有现代浏览器环境,包括 Chrome、Firefox、Safari、Edge 等。
**特性支持**:
- ✅ Worker (Web Worker)
- ✅ SharedArrayBuffer (需要COOP/COEP)
- ✅ Transferable Objects
- ✅ Module Worker (现代浏览器)
**适用场景**: Web游戏、Web应用、PWA
---
### 📱 [微信小游戏适配器](./platform-adapter/wechat-minigame.md)
专为微信小游戏环境设计处理微信小游戏的特殊限制和API。
**特性支持**:
- ✅ Worker (最多1个需配置game.json)
- ❌ SharedArrayBuffer
- ❌ Transferable Objects
- ✅ 微信设备信息API
**适用场景**: 微信小游戏开发
---
### 🖥️ [Node.js适配器](./platform-adapter/nodejs.md)
为 Node.js 服务器环境提供支持,适用于游戏服务器和计算服务器。
**特性支持**:
- ✅ Worker Threads
- ✅ SharedArrayBuffer
- ✅ Transferable Objects
- ✅ 完整系统信息
**适用场景**: 游戏服务器、计算服务器、CLI工具
---
## 核心接口
### IPlatformAdapter
```typescript
export interface IPlatformAdapter {
readonly name: string;
readonly version?: string;
isWorkerSupported(): boolean;
isSharedArrayBufferSupported(): boolean;
getHardwareConcurrency(): number;
createWorker(script: string, options?: WorkerCreationOptions): PlatformWorker;
createSharedArrayBuffer(length: number): SharedArrayBuffer | null;
getHighResTimestamp(): number;
getPlatformConfig(): PlatformConfig;
getPlatformConfigAsync?(): Promise<PlatformConfig>;
}
```
### PlatformWorker 接口
```typescript
export interface PlatformWorker {
postMessage(message: any, transfer?: Transferable[]): void;
onMessage(handler: (event: { data: any }) => void): void;
onError(handler: (error: ErrorEvent) => void): void;
terminate(): void;
readonly state: 'running' | 'terminated';
}
```
## 使用方法
### 1. 选择合适的平台适配器
根据你的运行环境选择对应的适配器:
```typescript
import { PlatformManager } from '@esengine/ecs-framework';
// 浏览器环境
if (typeof window !== 'undefined') {
const { BrowserAdapter } = await import('./platform/BrowserAdapter');
PlatformManager.getInstance().registerAdapter(new BrowserAdapter());
}
// 微信小游戏环境
else if (typeof wx !== 'undefined') {
const { WeChatMiniGameAdapter } = await import('./platform/WeChatMiniGameAdapter');
PlatformManager.getInstance().registerAdapter(new WeChatMiniGameAdapter());
}
// Node.js环境
else if (typeof process !== 'undefined' && process.versions?.node) {
const { NodeAdapter } = await import('./platform/NodeAdapter');
PlatformManager.getInstance().registerAdapter(new NodeAdapter());
}
```
### 2. 检查适配器状态
```typescript
const manager = PlatformManager.getInstance();
// 检查是否已注册适配器
if (manager.hasAdapter()) {
const adapter = manager.getAdapter();
console.log('当前平台:', adapter.name);
console.log('平台版本:', adapter.version);
// 检查功能支持
console.log('Worker支持:', manager.supportsFeature('worker'));
console.log('SharedArrayBuffer支持:', manager.supportsFeature('shared-array-buffer'));
}
```
## 创建自定义适配器
如果现有的平台适配器不能满足你的需求,你可以创建自定义适配器:
### 1. 实现接口
```typescript
import type { IPlatformAdapter, PlatformWorker, WorkerCreationOptions, PlatformConfig } from '@esengine/ecs-framework';
export class CustomAdapter implements IPlatformAdapter {
public readonly name = 'custom';
public readonly version = '1.0.0';
public isWorkerSupported(): boolean {
// 实现你的 Worker 支持检查逻辑
return false;
}
public isSharedArrayBufferSupported(): boolean {
// 实现你的 SharedArrayBuffer 支持检查逻辑
return false;
}
public getHardwareConcurrency(): number {
// 返回你的平台的并发数
return 1;
}
public createWorker(script: string, options?: WorkerCreationOptions): PlatformWorker {
throw new Error('Worker not supported on this platform');
}
public createSharedArrayBuffer(length: number): SharedArrayBuffer | null {
return null;
}
public getHighResTimestamp(): number {
return Date.now();
}
public getPlatformConfig(): PlatformConfig {
return {
maxWorkerCount: 1,
supportsModuleWorker: false,
supportsTransferableObjects: false,
limitations: {
workerNotSupported: true
}
};
}
}
```
### 2. 注册自定义适配器
```typescript
import { PlatformManager } from '@esengine/ecs-framework';
import { CustomAdapter } from './CustomAdapter';
const customAdapter = new CustomAdapter();
PlatformManager.getInstance().registerAdapter(customAdapter);
```
## 最佳实践
### 1. 平台检测顺序
建议按照以下顺序检测和注册平台适配器:
```typescript
async function initializePlatform() {
const manager = PlatformManager.getInstance();
try {
// 1. 微信小游戏 (优先级最高,环境特征最明显)
if (typeof wx !== 'undefined' && wx.getSystemInfoSync) {
const { WeChatMiniGameAdapter } = await import('./platform/WeChatMiniGameAdapter');
manager.registerAdapter(new WeChatMiniGameAdapter());
return;
}
// 2. Node.js 环境
if (typeof process !== 'undefined' && process.versions?.node) {
const { NodeAdapter } = await import('./platform/NodeAdapter');
manager.registerAdapter(new NodeAdapter());
return;
}
// 3. 浏览器环境 (最后检测,覆盖面最广)
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
const { BrowserAdapter } = await import('./platform/BrowserAdapter');
manager.registerAdapter(new BrowserAdapter());
return;
}
// 4. 未知环境,使用默认适配器
console.warn('未识别的平台环境,使用默认适配器');
manager.registerAdapter(new CustomAdapter());
} catch (error) {
console.error('平台适配器初始化失败:', error);
throw error;
}
}
```
### 2. 功能降级处理
```typescript
function createWorkerSystem() {
const manager = PlatformManager.getInstance();
if (!manager.hasAdapter()) {
throw new Error('未注册平台适配器');
}
const config: WorkerSystemConfig = {
enableWorker: manager.supportsFeature('worker'),
workerCount: manager.supportsFeature('worker') ?
manager.getAdapter().getHardwareConcurrency() : 1,
useSharedArrayBuffer: manager.supportsFeature('shared-array-buffer')
};
// 如果不支持Worker自动降级到同步处理
if (!config.enableWorker) {
console.info('当前平台不支持Worker使用同步处理模式');
}
return new PhysicsSystem(config);
}
```
### 3. 错误处理
```typescript
try {
await initializePlatform();
// 验证适配器功能
const manager = PlatformManager.getInstance();
const adapter = manager.getAdapter();
console.log(`平台适配器初始化成功: ${adapter.name} v${adapter.version}`);
} catch (error) {
console.error('平台初始化失败:', error);
// 提供降级方案
const fallbackAdapter = new CustomAdapter();
PlatformManager.getInstance().registerAdapter(fallbackAdapter);
console.warn('使用降级适配器继续运行');
}
```

View File

@@ -0,0 +1,370 @@
# 浏览器适配器
## 概述
浏览器平台适配器为标准Web浏览器环境提供支持包括 Chrome、Firefox、Safari、Edge 等现代浏览器。
## 特性支持
-**Worker**: 支持 Web Worker 和 Module Worker
-**SharedArrayBuffer**: 支持需要COOP/COEP头部
-**Transferable Objects**: 完全支持
-**高精度时间**: 使用 `performance.now()`
-**基础信息**: 浏览器版本和基本配置
## 完整实现
```typescript
import type {
IPlatformAdapter,
PlatformWorker,
WorkerCreationOptions,
PlatformConfig
} from '@esengine/ecs-framework';
/**
* 浏览器平台适配器
* 支持标准Web浏览器环境
*/
export class BrowserAdapter implements IPlatformAdapter {
public readonly name = 'browser';
public readonly version: string;
constructor() {
this.version = this.getBrowserInfo();
}
/**
* 检查是否支持Worker
*/
public isWorkerSupported(): boolean {
return typeof Worker !== 'undefined';
}
/**
* 检查是否支持SharedArrayBuffer
*/
public isSharedArrayBufferSupported(): boolean {
return typeof SharedArrayBuffer !== 'undefined' && this.checkSharedArrayBufferEnabled();
}
/**
* 获取硬件并发数CPU核心数
*/
public getHardwareConcurrency(): number {
return navigator.hardwareConcurrency || 4;
}
/**
* 创建Worker
*/
public createWorker(script: string, options: WorkerCreationOptions = {}): PlatformWorker {
if (!this.isWorkerSupported()) {
throw new Error('浏览器不支持Worker');
}
try {
return new BrowserWorker(script, options);
} catch (error) {
throw new Error(`创建浏览器Worker失败: ${(error as Error).message}`);
}
}
/**
* 创建SharedArrayBuffer
*/
public createSharedArrayBuffer(length: number): SharedArrayBuffer | null {
if (!this.isSharedArrayBufferSupported()) {
return null;
}
try {
return new SharedArrayBuffer(length);
} catch (error) {
console.warn('SharedArrayBuffer创建失败:', error);
return null;
}
}
/**
* 获取高精度时间戳
*/
public getHighResTimestamp(): number {
return performance.now();
}
/**
* 获取平台配置
*/
public getPlatformConfig(): PlatformConfig {
return {
maxWorkerCount: this.getHardwareConcurrency(),
supportsModuleWorker: false,
supportsTransferableObjects: true,
maxSharedArrayBufferSize: 1024 * 1024 * 1024, // 1GB
workerScriptPrefix: '',
limitations: {
noEval: false,
requiresWorkerInit: false
}
};
}
/**
* 获取浏览器信息
*/
private getBrowserInfo(): string {
const userAgent = navigator.userAgent;
if (userAgent.includes('Chrome')) {
const match = userAgent.match(/Chrome\/([0-9.]+)/);
return match ? `Chrome ${match[1]}` : 'Chrome';
} else if (userAgent.includes('Firefox')) {
const match = userAgent.match(/Firefox\/([0-9.]+)/);
if (match) return `Firefox ${match[1]}`;
} else if (userAgent.includes('Safari')) {
const match = userAgent.match(/Version\/([0-9.]+)/);
if (match) return `Safari ${match[1]}`;
}
return 'Unknown Browser';
}
/**
* 检查SharedArrayBuffer是否真正可用
*/
private checkSharedArrayBufferEnabled(): boolean {
try {
new SharedArrayBuffer(8);
return true;
} catch {
return false;
}
}
}
/**
* 浏览器Worker封装
*/
class BrowserWorker implements PlatformWorker {
private _state: 'running' | 'terminated' = 'running';
private worker: Worker;
constructor(script: string, options: WorkerCreationOptions = {}) {
this.worker = this.createBrowserWorker(script, options);
}
public get state(): 'running' | 'terminated' {
return this._state;
}
public postMessage(message: any, transfer?: Transferable[]): void {
if (this._state === 'terminated') {
throw new Error('Worker已被终止');
}
try {
if (transfer && transfer.length > 0) {
this.worker.postMessage(message, transfer);
} else {
this.worker.postMessage(message);
}
} catch (error) {
throw new Error(`发送消息到Worker失败: ${(error as Error).message}`);
}
}
public onMessage(handler: (event: { data: any }) => void): void {
this.worker.onmessage = (event: MessageEvent) => {
handler({ data: event.data });
};
}
public onError(handler: (error: ErrorEvent) => void): void {
this.worker.onerror = handler;
}
public terminate(): void {
if (this._state === 'running') {
try {
this.worker.terminate();
this._state = 'terminated';
} catch (error) {
console.error('终止Worker失败:', error);
}
}
}
/**
* 创建浏览器Worker
*/
private createBrowserWorker(script: string, options: WorkerCreationOptions): Worker {
try {
// 创建Blob URL
const blob = new Blob([script], { type: 'application/javascript' });
const url = URL.createObjectURL(blob);
// 创建Worker
const worker = new Worker(url, {
type: options.type || 'classic',
credentials: options.credentials,
name: options.name
});
// 清理Blob URL延迟清理确保Worker已加载
setTimeout(() => {
URL.revokeObjectURL(url);
}, 1000);
return worker;
} catch (error) {
throw new Error(`无法创建浏览器Worker: ${(error as Error).message}`);
}
}
}
```
## 使用方法
### 1. 复制代码
将上述代码复制到你的项目中,例如 `src/platform/BrowserAdapter.ts`
### 2. 注册适配器
```typescript
import { PlatformManager } from '@esengine/ecs-framework';
import { BrowserAdapter } from './platform/BrowserAdapter';
// 创建并注册浏览器适配器
const browserAdapter = new BrowserAdapter();
PlatformManager.registerAdapter(browserAdapter);
// 框架会自动检测和使用合适的适配器
```
### 3. 使用 WorkerEntitySystem
浏览器适配器与 WorkerEntitySystem 配合使用,框架会自动处理 Worker 脚本的创建:
```typescript
import { WorkerEntitySystem, Matcher } from '@esengine/ecs-framework';
class PhysicsSystem extends WorkerEntitySystem {
constructor() {
super(Matcher.all(Transform, Velocity), {
enableWorker: true,
workerCount: navigator.hardwareConcurrency || 4,
useSharedArrayBuffer: true,
systemConfig: { gravity: 9.8 }
});
}
protected getDefaultEntityDataSize(): number {
return 6; // x, y, vx, vy, mass, radius
}
protected extractEntityData(entity: Entity): PhysicsData {
const transform = entity.getComponent(Transform);
const velocity = entity.getComponent(Velocity);
return {
x: transform.x,
y: transform.y,
vx: velocity.x,
vy: velocity.y,
mass: 1,
radius: 10
};
}
// 这个函数会被自动序列化并在Worker中执行
protected workerProcess(entities, deltaTime, config) {
return entities.map(entity => {
// 应用重力
entity.vy += config.gravity * deltaTime;
// 更新位置
entity.x += entity.vx * deltaTime;
entity.y += entity.vy * deltaTime;
return entity;
});
}
protected applyResult(entity: Entity, result: PhysicsData): void {
const transform = entity.getComponent(Transform);
const velocity = entity.getComponent(Velocity);
transform.x = result.x;
transform.y = result.y;
velocity.x = result.vx;
velocity.y = result.vy;
}
}
interface PhysicsData {
x: number;
y: number;
vx: number;
vy: number;
mass: number;
radius: number;
}
```
### 4. 验证适配器工作状态
```typescript
// 验证适配器是否正常工作
const adapter = new BrowserAdapter();
console.log('适配器名称:', adapter.name);
console.log('浏览器版本:', adapter.version);
console.log('Worker支持:', adapter.isWorkerSupported());
console.log('SharedArrayBuffer支持:', adapter.isSharedArrayBufferSupported());
console.log('CPU核心数:', adapter.getHardwareConcurrency());
```
## 重要注意事项
### SharedArrayBuffer 支持
SharedArrayBuffer 需要特殊的安全配置:
1. **HTTPS**: 必须在安全上下文中使用
2. **COOP/COEP 头部**: 需要设置正确的跨域隔离头部
```html
<!-- 在 HTML 中设置 -->
<meta http-equiv="Cross-Origin-Opener-Policy" content="same-origin">
<meta http-equiv="Cross-Origin-Embedder-Policy" content="require-corp">
```
或在服务器配置中设置:
```
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
```
### 浏览器兼容性
- **Worker**: 所有现代浏览器支持
- **Module Worker**: Chrome 80+, Firefox 114+
- **SharedArrayBuffer**: Chrome 68+, Firefox 79+需要COOP/COEP
- **Transferable Objects**: 所有现代浏览器支持
## 性能优化建议
1. **Worker 池**: 复用 Worker 实例,避免频繁创建和销毁
2. **数据传输**: 使用 Transferable Objects 减少数据拷贝
3. **SharedArrayBuffer**: 对于大量数据共享,使用 SharedArrayBuffer
4. **模块 Worker**: 在支持的浏览器中使用模块 Worker 来更好地组织代码
## 调试技巧
```typescript
// 检查浏览器支持情况
const adapter = new BrowserAdapter();
console.log('Worker支持:', adapter.isWorkerSupported());
console.log('SharedArrayBuffer支持:', adapter.isSharedArrayBufferSupported());
console.log('硬件并发数:', adapter.getHardwareConcurrency());
console.log('平台配置:', adapter.getPlatformConfig());
```

View File

@@ -0,0 +1,558 @@
# Node.js 适配器
## 概述
Node.js 平台适配器为 Node.js 服务器环境提供支持,适用于游戏服务器、计算服务器或其他需要 ECS 架构的服务器应用。
## 特性支持
-**Worker**: 支持(通过 `worker_threads` 模块)
-**SharedArrayBuffer**: 支持Node.js 16.17.0+
-**Transferable Objects**: 完全支持
-**高精度时间**: 使用 `process.hrtime.bigint()`
-**设备信息**: 完整的系统和进程信息
## 完整实现
```typescript
import { worker_threads, Worker, isMainThread, parentPort } from 'worker_threads';
import * as os from 'os';
import * as process from 'process';
import * as fs from 'fs';
import * as path from 'path';
import type {
IPlatformAdapter,
PlatformWorker,
WorkerCreationOptions,
PlatformConfig,
NodeDeviceInfo
} from '@esengine/ecs-framework';
/**
* Node.js 平台适配器
* 支持 Node.js 服务器环境
*/
export class NodeAdapter implements IPlatformAdapter {
public readonly name = 'nodejs';
public readonly version: string;
constructor() {
this.version = process.version;
}
/**
* 检查是否支持Worker
*/
public isWorkerSupported(): boolean {
try {
// 检查 worker_threads 模块是否可用
return typeof worker_threads !== 'undefined' && typeof Worker !== 'undefined';
} catch {
return false;
}
}
/**
* 检查是否支持SharedArrayBuffer
*/
public isSharedArrayBufferSupported(): boolean {
// Node.js 支持 SharedArrayBuffer
return typeof SharedArrayBuffer !== 'undefined';
}
/**
* 获取硬件并发数CPU核心数
*/
public getHardwareConcurrency(): number {
return os.cpus().length;
}
/**
* 创建Worker
*/
public createWorker(script: string, options: WorkerCreationOptions = {}): PlatformWorker {
if (!this.isWorkerSupported()) {
throw new Error('Node.js环境不支持Worker Threads');
}
try {
return new NodeWorker(script, options);
} catch (error) {
throw new Error(`创建Node.js Worker失败: ${(error as Error).message}`);
}
}
/**
* 创建SharedArrayBuffer
*/
public createSharedArrayBuffer(length: number): SharedArrayBuffer | null {
if (!this.isSharedArrayBufferSupported()) {
return null;
}
try {
return new SharedArrayBuffer(length);
} catch (error) {
console.warn('SharedArrayBuffer创建失败:', error);
return null;
}
}
/**
* 获取高精度时间戳(纳秒)
*/
public getHighResTimestamp(): number {
// 返回毫秒,与浏览器 performance.now() 保持一致
return Number(process.hrtime.bigint()) / 1000000;
}
/**
* 获取平台配置
*/
public getPlatformConfig(): PlatformConfig {
return {
maxWorkerCount: this.getHardwareConcurrency(),
supportsModuleWorker: true, // Node.js 支持 ES 模块
supportsTransferableObjects: true,
maxSharedArrayBufferSize: this.getMaxSharedArrayBufferSize(),
workerScriptPrefix: '',
limitations: {
noEval: false, // Node.js 支持 eval
requiresWorkerInit: false
},
extensions: {
platform: 'nodejs',
nodeVersion: process.version,
v8Version: process.versions.v8,
uvVersion: process.versions.uv,
zlibVersion: process.versions.zlib,
opensslVersion: process.versions.openssl,
architecture: process.arch,
endianness: os.endianness(),
pid: process.pid,
ppid: process.ppid
}
};
}
/**
* 获取Node.js设备信息
*/
public getDeviceInfo(): NodeDeviceInfo {
const cpus = os.cpus();
const networkInterfaces = os.networkInterfaces();
const userInfo = os.userInfo();
return {
// 系统信息
platform: os.platform(),
arch: os.arch(),
type: os.type(),
release: os.release(),
version: os.version(),
hostname: os.hostname(),
// CPU信息
cpus: cpus.map(cpu => ({
model: cpu.model,
speed: cpu.speed,
times: cpu.times
})),
// 内存信息
totalMemory: os.totalmem(),
freeMemory: os.freemem(),
usedMemory: os.totalmem() - os.freemem(),
// 负载信息
loadAverage: os.loadavg(),
// 网络接口
networkInterfaces: Object.fromEntries(
Object.entries(networkInterfaces).map(([name, interfaces]) => [
name,
(interfaces || []).map(iface => ({
address: iface.address,
netmask: iface.netmask,
family: iface.family as 'IPv4' | 'IPv6',
mac: iface.mac,
internal: iface.internal,
cidr: iface.cidr,
scopeid: iface.scopeid
}))
])
),
// 进程信息
process: {
pid: process.pid,
ppid: process.ppid,
version: process.version,
versions: process.versions,
uptime: process.uptime()
},
// 用户信息
userInfo: {
uid: userInfo.uid,
gid: userInfo.gid,
username: userInfo.username,
homedir: userInfo.homedir,
shell: userInfo.shell
}
};
}
/**
* 获取SharedArrayBuffer最大大小限制
*/
private getMaxSharedArrayBufferSize(): number {
const totalMemory = os.totalmem();
// 限制为系统总内存的50%
return Math.floor(totalMemory * 0.5);
}
}
/**
* Node.js Worker封装
*/
class NodeWorker implements PlatformWorker {
private _state: 'running' | 'terminated' = 'running';
private worker: Worker;
private isTemporaryFile: boolean = false;
private scriptPath: string;
constructor(script: string, options: WorkerCreationOptions = {}) {
try {
// 判断 script 是文件路径还是脚本内容
if (this.isFilePath(script)) {
// 直接使用文件路径
this.scriptPath = script;
this.isTemporaryFile = false;
} else {
// 将脚本内容写入临时文件
this.scriptPath = this.writeScriptToFile(script, options.name);
this.isTemporaryFile = true;
}
// 创建Worker
this.worker = new Worker(this.scriptPath, {
// Node.js Worker options
workerData: options.name ? { name: options.name } : undefined
});
} catch (error) {
throw new Error(`创建Node.js Worker失败: ${(error as Error).message}`);
}
}
/**
* 判断是否为文件路径
*/
private isFilePath(script: string): boolean {
// 检查是否看起来像文件路径
return (script.endsWith('.js') || script.endsWith('.mjs') || script.endsWith('.ts')) &&
!script.includes('\n') &&
!script.includes(';') &&
script.length < 500; // 文件路径通常不会太长
}
/**
* 将脚本内容写入临时文件
*/
private writeScriptToFile(script: string, name?: string): string {
const tmpDir = os.tmpdir();
const fileName = name ? `worker-${name}-${Date.now()}.js` : `worker-${Date.now()}.js`;
const filePath = path.join(tmpDir, fileName);
try {
fs.writeFileSync(filePath, script, 'utf8');
return filePath;
} catch (error) {
throw new Error(`写入Worker脚本文件失败: ${(error as Error).message}`);
}
}
public get state(): 'running' | 'terminated' {
return this._state;
}
public postMessage(message: any, transfer?: Transferable[]): void {
if (this._state === 'terminated') {
throw new Error('Worker已被终止');
}
try {
if (transfer && transfer.length > 0) {
// Node.js Worker 支持 Transferable Objects
this.worker.postMessage(message, transfer);
} else {
this.worker.postMessage(message);
}
} catch (error) {
throw new Error(`发送消息到Node.js Worker失败: ${(error as Error).message}`);
}
}
public onMessage(handler: (event: { data: any }) => void): void {
this.worker.on('message', (data: any) => {
handler({ data });
});
}
public onError(handler: (error: ErrorEvent) => void): void {
this.worker.on('error', (error: Error) => {
// 将 Error 转换为 ErrorEvent 格式
const errorEvent = {
message: error.message,
filename: '',
lineno: 0,
colno: 0,
error: error
} as ErrorEvent;
handler(errorEvent);
});
}
public terminate(): void {
if (this._state === 'running') {
try {
this.worker.terminate();
this._state = 'terminated';
// 清理临时脚本文件
this.cleanupScriptFile();
} catch (error) {
console.error('终止Node.js Worker失败:', error);
}
}
}
/**
* 清理临时脚本文件
*/
private cleanupScriptFile(): void {
// 只清理临时创建的文件,不清理用户提供的文件路径
if (this.scriptPath && this.isTemporaryFile) {
try {
fs.unlinkSync(this.scriptPath);
} catch (error) {
console.warn('清理Worker脚本文件失败:', error);
}
}
}
}
```
## 使用方法
### 1. 复制代码
将上述代码复制到你的项目中,例如 `src/platform/NodeAdapter.ts`
### 2. 注册适配器
```typescript
import { PlatformManager } from '@esengine/ecs-framework';
import { NodeAdapter } from './platform/NodeAdapter';
// 检查是否在Node.js环境
if (typeof process !== 'undefined' && process.versions && process.versions.node) {
const nodeAdapter = new NodeAdapter();
PlatformManager.getInstance().registerAdapter(nodeAdapter);
}
```
### 3. 使用 WorkerEntitySystem
Node.js 适配器与 WorkerEntitySystem 配合使用,框架会自动处理 Worker 脚本的创建:
```typescript
import { WorkerEntitySystem, Matcher } from '@esengine/ecs-framework';
import * as os from 'os';
class PhysicsSystem extends WorkerEntitySystem {
constructor() {
super(Matcher.all(Transform, Velocity), {
enableWorker: true,
workerCount: os.cpus().length, // 使用所有CPU核心
useSharedArrayBuffer: true,
systemConfig: { gravity: 9.8 }
});
}
protected getDefaultEntityDataSize(): number {
return 6; // x, y, vx, vy, mass, radius
}
protected extractEntityData(entity: Entity): PhysicsData {
const transform = entity.getComponent(Transform);
const velocity = entity.getComponent(Velocity);
return {
x: transform.x,
y: transform.y,
vx: velocity.x,
vy: velocity.y,
mass: 1,
radius: 10
};
}
// 这个函数会被自动序列化并在Worker中执行
protected workerProcess(entities, deltaTime, config) {
return entities.map(entity => {
// 应用重力
entity.vy += config.gravity * deltaTime;
// 更新位置
entity.x += entity.vx * deltaTime;
entity.y += entity.vy * deltaTime;
return entity;
});
}
protected applyResult(entity: Entity, result: PhysicsData): void {
const transform = entity.getComponent(Transform);
const velocity = entity.getComponent(Velocity);
transform.x = result.x;
transform.y = result.y;
velocity.x = result.vx;
velocity.y = result.vy;
}
}
interface PhysicsData {
x: number;
y: number;
vx: number;
vy: number;
mass: number;
radius: number;
}
```
### 4. 获取系统信息
```typescript
const manager = PlatformManager.getInstance();
if (manager.hasAdapter()) {
const adapter = manager.getAdapter();
const deviceInfo = adapter.getDeviceInfo();
console.log('Node.js版本:', deviceInfo.process?.version);
console.log('CPU核心数:', deviceInfo.cpus?.length);
console.log('总内存:', Math.round(deviceInfo.totalMemory! / 1024 / 1024), 'MB');
console.log('可用内存:', Math.round(deviceInfo.freeMemory! / 1024 / 1024), 'MB');
}
```
## 官方文档参考
Node.js Worker Threads 相关官方文档:
- [Worker Threads 官方文档](https://nodejs.org/api/worker_threads.html)
- [SharedArrayBuffer 支持](https://nodejs.org/api/globals.html#class-sharedarraybuffer)
- [OS 模块文档](https://nodejs.org/api/os.html)
- [Process 模块文档](https://nodejs.org/api/process.html)
## 重要注意事项
### Worker Threads 要求
- **Node.js版本**: 需要 Node.js 10.5.0+ (建议 12+)
- **模块类型**: 支持 CommonJS 和 ES 模块
- **线程限制**: 理论上无限制,但建议不超过 CPU 核心数的 2 倍
### 性能优化建议
#### 1. Worker 池管理
```typescript
class ServerPhysicsSystem extends WorkerEntitySystem {
constructor() {
const cpuCount = os.cpus().length;
super(Matcher.all(Transform, Velocity), {
enableWorker: true,
workerCount: Math.min(cpuCount * 2, 16), // 最多16个Worker
entitiesPerWorker: 1000, // 每个Worker处理1000个实体
useSharedArrayBuffer: true,
systemConfig: {
gravity: 9.8,
timeStep: 1/60
}
});
}
}
```
#### 2. 内存管理
```typescript
class MemoryMonitor {
public static checkMemoryUsage(): void {
const used = process.memoryUsage();
console.log('内存使用情况:');
console.log(` RSS: ${Math.round(used.rss / 1024 / 1024)} MB`);
console.log(` Heap Used: ${Math.round(used.heapUsed / 1024 / 1024)} MB`);
console.log(` Heap Total: ${Math.round(used.heapTotal / 1024 / 1024)} MB`);
console.log(` External: ${Math.round(used.external / 1024 / 1024)} MB`);
// 内存使用率过高时触发警告
if (used.heapUsed > used.heapTotal * 0.9) {
console.warn('内存使用率过高,建议优化或重启');
}
}
}
// 定期检查内存使用
setInterval(() => {
MemoryMonitor.checkMemoryUsage();
}, 30000); // 每30秒检查一次
```
#### 3. 服务器环境优化
```typescript
// 设置进程标题
process.title = 'ecs-game-server';
// 处理未捕获异常
process.on('uncaughtException', (error) => {
console.error('未捕获异常:', error);
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的Promise拒绝:', reason);
});
// 优雅关闭
process.on('SIGTERM', () => {
console.log('收到SIGTERM信号正在关闭服务器...');
// 清理资源
process.exit(0);
});
```
## 调试技巧
```typescript
// 检查Node.js环境支持情况
const adapter = new NodeAdapter();
console.log('Node.js版本:', adapter.version);
console.log('Worker支持:', adapter.isWorkerSupported());
console.log('SharedArrayBuffer支持:', adapter.isSharedArrayBufferSupported());
console.log('CPU核心数:', adapter.getHardwareConcurrency());
// 获取详细配置
const config = adapter.getPlatformConfig();
console.log('平台配置:', JSON.stringify(config, null, 2));
// 系统资源监控
const deviceInfo = adapter.getDeviceInfo();
console.log('系统负载:', deviceInfo.loadAverage);
console.log('网络接口:', Object.keys(deviceInfo.networkInterfaces!));
```

View File

@@ -0,0 +1,677 @@
# 微信小游戏适配器
## 概述
微信小游戏平台适配器专为微信小游戏环境设计处理微信小游戏的特殊限制和API。
## 特性支持
-**Worker**: 支持(通过 `wx.createWorker` 创建,需要配置 game.json
-**SharedArrayBuffer**: 不支持
-**Transferable Objects**: 不支持(只支持可序列化对象)
-**高精度时间**: 使用 `Date.now()``wx.getPerformance()`
-**设备信息**: 完整的微信小游戏设备信息
## 完整实现
```typescript
import type {
IPlatformAdapter,
PlatformWorker,
WorkerCreationOptions,
PlatformConfig,
WeChatDeviceInfo
} from '@esengine/ecs-framework';
/**
* 微信小游戏平台适配器
* 支持微信小游戏环境
*/
export class WeChatMiniGameAdapter implements IPlatformAdapter {
public readonly name = 'wechat-minigame';
public readonly version: string;
private systemInfo: any;
constructor() {
// 获取微信小游戏版本信息
this.systemInfo = this.getSystemInfo();
this.version = this.systemInfo.version || 'unknown';
}
/**
* 检查是否支持Worker
*/
public isWorkerSupported(): boolean {
// 微信小游戏支持Worker通过wx.createWorker创建
return typeof wx !== 'undefined' && typeof wx.createWorker === 'function';
}
/**
* 检查是否支持SharedArrayBuffer不支持
*/
public isSharedArrayBufferSupported(): boolean {
return false; // 微信小游戏不支持SharedArrayBuffer
}
/**
* 获取硬件并发数
*/
public getHardwareConcurrency(): number {
// 微信小游戏官方限制:最多只能创建 1 个 Worker
return 1;
}
/**
* 创建Worker
* @param script 脚本内容或文件路径
* @param options Worker创建选项
*/
public createWorker(script: string, options: WorkerCreationOptions = {}): PlatformWorker {
if (!this.isWorkerSupported()) {
throw new Error('微信小游戏不支持Worker');
}
try {
return new WeChatWorker(script, options);
} catch (error) {
throw new Error(`创建微信Worker失败: ${(error as Error).message}`);
}
}
/**
* 创建SharedArrayBuffer不支持
*/
public createSharedArrayBuffer(length: number): SharedArrayBuffer | null {
return null; // 微信小游戏不支持SharedArrayBuffer
}
/**
* 获取高精度时间戳
*/
public getHighResTimestamp(): number {
// 尝试使用微信的性能API否则使用Date.now()
if (typeof wx !== 'undefined' && wx.getPerformance) {
const performance = wx.getPerformance();
return performance.now();
}
return Date.now();
}
/**
* 获取平台配置
*/
public getPlatformConfig(): PlatformConfig {
return {
maxWorkerCount: 1, // 微信小游戏最多支持 1 个 Worker
supportsModuleWorker: false, // 不支持模块Worker
supportsTransferableObjects: this.checkTransferableObjectsSupport(),
maxSharedArrayBufferSize: 0,
workerScriptPrefix: '',
limitations: {
noEval: true, // 微信小游戏限制eval使用
requiresWorkerInit: false,
memoryLimit: this.getMemoryLimit(),
workerNotSupported: false,
workerLimitations: [
'最多只能创建 1 个 Worker',
'创建新Worker前必须先调用 Worker.terminate()',
'Worker脚本必须为项目内相对路径',
'需要在 game.json 中配置 workers 路径',
'使用 worker.onMessage() 而不是 self.onmessage',
'需要基础库 1.9.90 及以上版本'
]
},
extensions: {
platform: 'wechat-minigame',
systemInfo: this.systemInfo,
appId: this.systemInfo.host?.appId || 'unknown'
}
};
}
/**
* 获取微信小游戏设备信息
*/
public getDeviceInfo(): WeChatDeviceInfo {
return {
// 设备基础信息
brand: this.systemInfo.brand,
model: this.systemInfo.model,
platform: this.systemInfo.platform,
system: this.systemInfo.system,
benchmarkLevel: this.systemInfo.benchmarkLevel,
cpuType: this.systemInfo.cpuType,
memorySize: this.systemInfo.memorySize,
deviceAbi: this.systemInfo.deviceAbi,
abi: this.systemInfo.abi,
// 窗口信息
screenWidth: this.systemInfo.screenWidth,
screenHeight: this.systemInfo.screenHeight,
screenTop: this.systemInfo.screenTop,
windowWidth: this.systemInfo.windowWidth,
windowHeight: this.systemInfo.windowHeight,
pixelRatio: this.systemInfo.pixelRatio,
statusBarHeight: this.systemInfo.statusBarHeight,
safeArea: this.systemInfo.safeArea,
// 应用信息
version: this.systemInfo.version,
language: this.systemInfo.language,
theme: this.systemInfo.theme,
SDKVersion: this.systemInfo.SDKVersion,
enableDebug: this.systemInfo.enableDebug,
fontSizeSetting: this.systemInfo.fontSizeSetting,
host: this.systemInfo.host
};
}
/**
* 异步获取完整的平台配置
*/
public async getPlatformConfigAsync(): Promise<PlatformConfig> {
// 可以在这里添加异步获取设备性能信息的逻辑
const baseConfig = this.getPlatformConfig();
// 尝试获取设备性能信息
try {
const benchmarkLevel = await this.getBenchmarkLevel();
baseConfig.extensions = {
...baseConfig.extensions,
benchmarkLevel
};
} catch (error) {
console.warn('获取性能基准失败:', error);
}
return baseConfig;
}
/**
* 检查是否支持Transferable Objects
*/
private checkTransferableObjectsSupport(): boolean {
// 微信小游戏不支持 Transferable Objects
// 基础库 2.20.2 之前只支持可序列化的 key-value 对象
// 2.20.2 之后支持任意类型数据,但仍然不支持 Transferable Objects
return false;
}
/**
* 获取系统信息
*/
private getSystemInfo(): any {
if (typeof wx !== 'undefined' && wx.getSystemInfoSync) {
try {
return wx.getSystemInfoSync();
} catch (error) {
console.warn('获取微信系统信息失败:', error);
return {};
}
}
return {};
}
/**
* 获取内存限制
*/
private getMemoryLimit(): number {
// 微信小游戏通常有内存限制
const memorySize = this.systemInfo.memorySize;
if (memorySize) {
// 解析内存大小字符串(如 "4GB"
const match = memorySize.match(/(\d+)([GM]B)?/i);
if (match) {
const value = parseInt(match[1], 10);
const unit = match[2]?.toUpperCase();
if (unit === 'GB') {
return value * 1024 * 1024 * 1024;
} else if (unit === 'MB') {
return value * 1024 * 1024;
}
}
}
// 默认限制为512MB
return 512 * 1024 * 1024;
}
/**
* 异步获取设备性能基准
*/
private async getBenchmarkLevel(): Promise<number> {
return new Promise((resolve) => {
if (typeof wx !== 'undefined' && wx.getDeviceInfo) {
wx.getDeviceInfo({
success: (res: any) => {
resolve(res.benchmarkLevel || 0);
},
fail: () => {
resolve(0);
}
});
} else {
resolve(this.systemInfo.benchmarkLevel || 0);
}
});
}
}
/**
* 微信Worker封装
*/
class WeChatWorker implements PlatformWorker {
private _state: 'running' | 'terminated' = 'running';
private worker: any;
private scriptPath: string;
private isTemporaryFile: boolean = false;
constructor(script: string, options: WorkerCreationOptions = {}) {
if (typeof wx === 'undefined' || typeof wx.createWorker !== 'function') {
throw new Error('微信小游戏不支持Worker');
}
try {
// 判断 script 是文件路径还是脚本内容
if (this.isFilePath(script)) {
// 直接使用文件路径
this.scriptPath = script;
this.isTemporaryFile = false;
this.worker = wx.createWorker(this.scriptPath, {
useExperimentalWorker: true // 启用实验性Worker获得更好性能
});
} else {
// 微信小游戏不支持动态脚本内容,只能使用文件路径
// 将脚本内容写入文件系统
this.scriptPath = this.writeScriptToFile(script, options.name);
this.isTemporaryFile = true;
this.worker = wx.createWorker(this.scriptPath, {
useExperimentalWorker: true
});
}
} catch (error) {
throw new Error(`创建微信Worker失败: ${(error as Error).message}`);
}
}
/**
* 判断是否为文件路径
*/
private isFilePath(script: string): boolean {
// 简单判断:如果包含 .js 后缀且不包含换行符或分号,认为是文件路径
return script.endsWith('.js') &&
!script.includes('\n') &&
!script.includes(';') &&
script.length < 200; // 文件路径通常不会太长
}
/**
* 将脚本内容写入文件系统
* 注意微信小游戏不支持blob URL只能使用文件系统
*/
private writeScriptToFile(script: string, name?: string): string {
const fs = wx.getFileSystemManager();
const fileName = name ? `worker-${name}.js` : `worker-${Date.now()}.js`;
const filePath = `${wx.env.USER_DATA_PATH}/${fileName}`;
try {
fs.writeFileSync(filePath, script, 'utf8');
return filePath;
} catch (error) {
throw new Error(`写入Worker脚本文件失败: ${(error as Error).message}`);
}
}
public get state(): 'running' | 'terminated' {
return this._state;
}
public postMessage(message: any, transfer?: Transferable[]): void {
if (this._state === 'terminated') {
throw new Error('Worker已被终止');
}
try {
// 微信小游戏 Worker 只支持可序列化对象,忽略 transfer 参数
this.worker.postMessage(message);
} catch (error) {
throw new Error(`发送消息到微信Worker失败: ${(error as Error).message}`);
}
}
public onMessage(handler: (event: { data: any }) => void): void {
// 微信小游戏使用 onMessage 方法,不是 onmessage 属性
this.worker.onMessage((res: any) => {
handler({ data: res });
});
}
public onError(handler: (error: ErrorEvent) => void): void {
// 注意:微信小游戏 Worker 的错误处理可能与标准不同
if (this.worker.onError) {
this.worker.onError(handler);
}
}
public terminate(): void {
if (this._state === 'running') {
try {
this.worker.terminate();
this._state = 'terminated';
// 清理临时脚本文件
this.cleanupScriptFile();
} catch (error) {
console.error('终止微信Worker失败:', error);
}
}
}
/**
* 清理临时脚本文件
*/
private cleanupScriptFile(): void {
// 只清理临时创建的文件,不清理用户提供的文件路径
if (this.scriptPath && this.isTemporaryFile) {
try {
const fs = wx.getFileSystemManager();
fs.unlinkSync(this.scriptPath);
} catch (error) {
console.warn('清理Worker脚本文件失败:', error);
}
}
}
}
```
## 使用方法
### 1. 复制代码
将上述代码复制到你的项目中,例如 `src/platform/WeChatMiniGameAdapter.ts`
### 2. 注册适配器
```typescript
import { PlatformManager } from '@esengine/ecs-framework';
import { WeChatMiniGameAdapter } from './platform/WeChatMiniGameAdapter';
// 检查是否在微信小游戏环境
if (typeof wx !== 'undefined') {
const wechatAdapter = new WeChatMiniGameAdapter();
PlatformManager.getInstance().registerAdapter(wechatAdapter);
}
```
### 3. WorkerEntitySystem 使用方式
微信小游戏适配器与 WorkerEntitySystem 配合使用,自动处理 Worker 脚本创建:
#### 基本使用方式(推荐)
```typescript
import { WorkerEntitySystem, Matcher, Entity } from '@esengine/ecs-framework';
interface PhysicsData {
id: number;
x: number;
y: number;
vx: number;
vy: number;
mass: number;
}
class PhysicsSystem extends WorkerEntitySystem<PhysicsData> {
constructor() {
super(Matcher.all(Transform, Velocity), {
enableWorker: true,
workerCount: 1, // 微信小游戏限制只能创建1个Worker
systemConfig: { gravity: 100, friction: 0.95 }
});
}
protected getDefaultEntityDataSize(): number {
return 6; // id, x, y, vx, vy, mass
}
protected extractEntityData(entity: Entity): PhysicsData {
const transform = entity.getComponent(Transform);
const velocity = entity.getComponent(Velocity);
const physics = entity.getComponent(Physics);
return {
id: entity.id,
x: transform.x,
y: transform.y,
vx: velocity.x,
vy: velocity.y,
mass: physics.mass
};
}
// WorkerEntitySystem 会自动将此函数序列化并写入临时文件
protected workerProcess(entities: PhysicsData[], deltaTime: number, config: any): PhysicsData[] {
return entities.map(entity => {
// 应用重力
entity.vy += config.gravity * deltaTime;
// 更新位置
entity.x += entity.vx * deltaTime;
entity.y += entity.vy * deltaTime;
// 应用摩擦力
entity.vx *= config.friction;
entity.vy *= config.friction;
return entity;
});
}
protected applyResult(entity: Entity, result: PhysicsData): void {
const transform = entity.getComponent(Transform);
const velocity = entity.getComponent(Velocity);
transform.x = result.x;
transform.y = result.y;
velocity.x = result.vx;
velocity.y = result.vy;
}
}
```
#### 使用预先创建的 Worker 文件(可选)
如果你希望使用预先创建的 Worker 文件:
```typescript
// 1. 在 game.json 中配置 Worker 路径
/*
{
"workers": "workers"
}
*/
// 2. 创建 workers/physics.js 文件
// workers/physics.js 内容:
/*
// 微信小游戏 Worker 使用标准的 self.onmessage
self.onmessage = function(e) {
const { type, id, entities, deltaTime, systemConfig } = e.data;
if (entities) {
// 处理物理计算
const results = entities.map(entity => {
entity.vy += systemConfig.gravity * deltaTime;
entity.x += entity.vx * deltaTime;
entity.y += entity.vy * deltaTime;
return entity;
});
self.postMessage({ id, result: results });
}
};
*/
// 3. 通过平台适配器直接创建不推荐WorkerEntitySystem会自动处理
const adapter = PlatformManager.getInstance().getAdapter();
const worker = adapter.createWorker('workers/physics.js');
```
### 4. 获取设备信息
```typescript
const manager = PlatformManager.getInstance();
if (manager.hasAdapter()) {
const adapter = manager.getAdapter();
console.log('微信设备信息:', adapter.getDeviceInfo());
}
```
## 官方文档参考
在使用微信小游戏 Worker 之前,建议先阅读官方文档:
- [wx.createWorker API](https://developers.weixin.qq.com/minigame/dev/api/worker/wx.createWorker.html)
- [Worker.postMessage API](https://developers.weixin.qq.com/minigame/dev/api/worker/Worker.postMessage.html)
- [Worker.onMessage API](https://developers.weixin.qq.com/minigame/dev/api/worker/Worker.onMessage.html)
- [Worker.terminate API](https://developers.weixin.qq.com/minigame/dev/api/worker/Worker.terminate.html)
## 重要注意事项
### Worker 限制和配置
微信小游戏的 Worker 有以下限制:
- **数量限制**: 最多只能创建 1 个 Worker
- **版本要求**: 需要基础库 1.9.90 及以上版本
- **脚本支持**: 不支持 blob URL只能使用文件路径或写入文件系统
- **文件路径**: Worker 脚本路径必须为绝对路径,但不能以 "/" 开头
- **生命周期**: 创建新 Worker 前必须先调用 `Worker.terminate()` 终止当前 Worker
- **消息处理**: Worker 内使用标准的 `self.onmessage`,主线程使用 `worker.onMessage()`
- **实验性功能**: 支持 `useExperimentalWorker` 选项获得更好的 iOS 性能
#### Worker 配置(可选)
如果使用预先创建的 Worker 文件,需要在 `game.json` 中添加 workers 配置:
```json
{
"deviceOrientation": "portrait",
"showStatusBar": false,
"workers": "workers",
"subpackages": []
}
```
**注意**: 使用 WorkerEntitySystem 时无需此配置,框架会自动将脚本写入临时文件。
### 内存限制
微信小游戏有严格的内存限制:
- 通常限制在 256MB - 512MB
- 需要及时释放不用的资源
- 避免内存泄漏
### API 限制
- 不支持 `eval()` 函数
- 不支持 `Function` 构造器
- DOM API 受限
- 文件系统 API 受限
## 性能优化建议
### 1. 分帧处理
```typescript
class FramedProcessor {
private tasks: (() => void)[] = [];
private isProcessing = false;
public addTask(task: () => void): void {
this.tasks.push(task);
if (!this.isProcessing) {
this.processNextFrame();
}
}
private processNextFrame(): void {
this.isProcessing = true;
const startTime = Date.now();
const frameTime = 16; // 16ms per frame
while (this.tasks.length > 0 && Date.now() - startTime < frameTime) {
const task = this.tasks.shift();
if (task) task();
}
if (this.tasks.length > 0) {
setTimeout(() => this.processNextFrame(), 0);
} else {
this.isProcessing = false;
}
}
}
```
### 2. 内存管理
```typescript
class MemoryManager {
private static readonly MAX_MEMORY = 256 * 1024 * 1024; // 256MB
public static checkMemoryUsage(): void {
if (typeof wx !== 'undefined' && wx.getPerformance) {
const performance = wx.getPerformance();
const memoryInfo = performance.getEntries().find(
(entry: any) => entry.entryType === 'memory'
);
if (memoryInfo && memoryInfo.usedJSHeapSize > this.MAX_MEMORY * 0.8) {
console.warn('内存使用率过高,建议清理资源');
// 触发垃圾回收或资源清理
}
}
}
}
```
## 调试技巧
```typescript
// 检查微信小游戏环境
if (typeof wx !== 'undefined') {
const adapter = new WeChatMiniGameAdapter();
console.log('微信版本:', adapter.version);
console.log('设备信息:', adapter.getDeviceInfo());
console.log('平台配置:', adapter.getPlatformConfig());
// 检查功能支持
console.log('Worker支持:', adapter.isWorkerSupported());
console.log('SharedArrayBuffer支持:', adapter.isSharedArrayBufferSupported());
}
```
## 微信小游戏特殊API
```typescript
// 获取设备性能等级
if (typeof wx !== 'undefined' && wx.getDeviceInfo) {
wx.getDeviceInfo({
success: (res) => {
console.log('设备性能等级:', res.benchmarkLevel);
}
});
}
// 监听内存警告
if (typeof wx !== 'undefined' && wx.onMemoryWarning) {
wx.onMemoryWarning(() => {
console.warn('收到内存警告,开始清理资源');
// 清理不必要的资源
});
}
```

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

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

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

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

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

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

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

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

View File

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

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

@@ -0,0 +1,650 @@
# 系统架构
在 ECS 架构中系统System是处理业务逻辑的地方。系统负责对拥有特定组件组合的实体执行操作是 ECS 架构的逻辑处理单元。
## 基本概念
系统是继承自 `EntitySystem` 抽象基类的具体类,用于:
- 定义实体的处理逻辑(如移动、碰撞检测、渲染等)
- 根据组件组合筛选需要处理的实体
- 提供生命周期管理和性能监控
- 管理实体的添加、移除事件
## 系统类型
框架提供了几种不同类型的系统基类:
### EntitySystem - 基础系统
最基础的系统类,所有其他系统都继承自它:
```typescript
import { EntitySystem, ECSSystem, Matcher } from '@esengine/ecs-framework';
@ECSSystem('Movement')
class MovementSystem extends EntitySystem {
constructor() {
// 使用 Matcher 定义需要处理的实体条件
super(Matcher.all(Position, Velocity));
}
protected process(entities: readonly Entity[]): void {
for (const entity of entities) {
const position = entity.getComponent(Position);
const velocity = entity.getComponent(Velocity);
if (position && velocity) {
position.x += velocity.dx * Time.deltaTime;
position.y += velocity.dy * Time.deltaTime;
}
}
}
}
```
### ProcessingSystem - 处理系统
适用于不需要逐个处理实体的系统:
```typescript
@ECSSystem('Physics')
class PhysicsSystem extends ProcessingSystem {
constructor() {
super(); // 不需要指定 Matcher
}
public processSystem(): void {
// 执行物理世界步进
this.physicsWorld.step(Time.deltaTime);
}
}
```
### PassiveSystem - 被动系统
被动系统不进行主动处理,主要用于监听实体的添加和移除事件:
```typescript
@ECSSystem('EntityTracker')
class EntityTrackerSystem extends PassiveSystem {
constructor() {
super(Matcher.all(Health));
}
protected onAdded(entity: Entity): void {
console.log(`生命值实体被添加: ${entity.name}`);
}
protected onRemoved(entity: Entity): void {
console.log(`生命值实体被移除: ${entity.name}`);
}
}
```
### IntervalSystem - 间隔系统
按固定时间间隔执行的系统:
```typescript
@ECSSystem('AutoSave')
class AutoSaveSystem extends IntervalSystem {
constructor() {
// 每 5 秒执行一次
super(5.0, Matcher.all(SaveData));
}
protected process(entities: readonly Entity[]): void {
console.log('执行自动保存...');
// 保存游戏数据
this.saveGameData(entities);
}
private saveGameData(entities: readonly Entity[]): void {
// 保存逻辑
}
}
```
### WorkerEntitySystem - 多线程系统
基于Web Worker的多线程处理系统适用于计算密集型任务能够充分利用多核CPU性能。
Worker系统提供了真正的并行计算能力支持SharedArrayBuffer优化并具有自动降级支持。特别适合物理模拟、粒子系统、AI计算等场景。
**详细内容请参考:[Worker系统](/guide/worker-system)**
## 实体匹配器 (Matcher)
Matcher 用于定义系统需要处理哪些实体。它提供了灵活的条件组合:
### 基本匹配条件
```typescript
// 必须同时拥有 Position 和 Velocity 组件
const matcher1 = Matcher.all(Position, Velocity);
// 至少拥有 Health 或 Shield 组件之一
const matcher2 = Matcher.any(Health, Shield);
// 不能拥有 Dead 组件
const matcher3 = Matcher.none(Dead);
```
### 复合匹配条件
```typescript
// 复杂的组合条件
const complexMatcher = Matcher.all(Position, Velocity)
.any(Player, Enemy)
.none(Dead, Disabled);
@ECSSystem('Combat')
class CombatSystem extends EntitySystem {
constructor() {
super(complexMatcher);
}
}
```
### 特殊匹配条件
```typescript
// 按标签匹配
const tagMatcher = Matcher.byTag(1); // 匹配标签为 1 的实体
// 按名称匹配
const nameMatcher = Matcher.byName("Player"); // 匹配名称为 "Player" 的实体
// 单组件匹配
const componentMatcher = Matcher.byComponent(Health); // 匹配拥有 Health 组件的实体
```
## 系统生命周期
系统提供了完整的生命周期回调:
```typescript
@ECSSystem('Example')
class ExampleSystem extends EntitySystem {
protected onInitialize(): void {
console.log('系统初始化');
// 系统被添加到场景时调用,用于初始化资源
}
protected onBegin(): void {
// 每帧处理开始前调用
}
protected process(entities: readonly Entity[]): void {
// 主要的处理逻辑
for (const entity of entities) {
// 处理每个实体
}
}
protected lateProcess(entities: readonly Entity[]): void {
// 主处理之后的后期处理
}
protected onEnd(): void {
// 每帧处理结束后调用
}
protected onDestroy(): void {
console.log('系统销毁');
// 系统从场景移除时调用,用于清理资源
}
}
```
## 实体事件监听
系统可以监听实体的添加和移除事件:
```typescript
@ECSSystem('EnemyManager')
class EnemyManagerSystem extends EntitySystem {
private enemyCount = 0;
constructor() {
super(Matcher.all(Enemy, Health));
}
protected onAdded(entity: Entity): void {
this.enemyCount++;
console.log(`敌人加入战斗,当前敌人数量: ${this.enemyCount}`);
// 可以在这里为新敌人设置初始状态
const health = entity.getComponent(Health);
if (health) {
health.current = health.max;
}
}
protected onRemoved(entity: Entity): void {
this.enemyCount--;
console.log(`敌人被移除,剩余敌人数量: ${this.enemyCount}`);
// 检查是否所有敌人都被消灭
if (this.enemyCount === 0) {
this.scene?.eventSystem.emitSync('all_enemies_defeated');
}
}
}
```
## 系统属性和方法
### 重要属性
```typescript
@ECSSystem('Example')
class ExampleSystem extends EntitySystem {
showSystemInfo(): void {
console.log(`系统名称: ${this.systemName}`); // 系统名称
console.log(`更新顺序: ${this.updateOrder}`); // 更新时序
console.log(`是否启用: ${this.enabled}`); // 启用状态
console.log(`实体数量: ${this.entities.length}`); // 匹配的实体数量
console.log(`所属场景: ${this.scene?.name}`); // 所属场景
}
}
```
### 实体访问
```typescript
protected process(entities: readonly Entity[]): void {
// 方式1使用参数中的实体列表
for (const entity of entities) {
// 处理实体
}
// 方式2使用 this.entities 属性(与参数相同)
for (const entity of this.entities) {
// 处理实体
}
}
```
### 控制系统执行
```typescript
@ECSSystem('Conditional')
class ConditionalSystem extends EntitySystem {
private shouldProcess = true;
protected onCheckProcessing(): boolean {
// 返回 false 时跳过本次处理
return this.shouldProcess && this.entities.length > 0;
}
public pause(): void {
this.shouldProcess = false;
}
public resume(): void {
this.shouldProcess = true;
}
}
```
## 事件系统集成
系统可以方便地监听和发送事件:
```typescript
@ECSSystem('GameLogic')
class GameLogicSystem extends EntitySystem {
protected onInitialize(): void {
// 添加事件监听器(系统销毁时自动清理)
this.addEventListener('player_died', this.onPlayerDied.bind(this));
this.addEventListener('level_complete', this.onLevelComplete.bind(this));
}
private onPlayerDied(data: any): void {
console.log('玩家死亡,重新开始游戏');
// 处理玩家死亡逻辑
}
private onLevelComplete(data: any): void {
console.log('关卡完成,加载下一关');
// 处理关卡完成逻辑
}
protected process(entities: readonly Entity[]): void {
// 在处理过程中发送事件
for (const entity of entities) {
const health = entity.getComponent(Health);
if (health && health.current <= 0) {
this.scene?.eventSystem.emitSync('entity_died', { entity });
}
}
}
}
```
## 性能监控
系统内置了性能监控功能:
```typescript
@ECSSystem('Performance')
class PerformanceSystem extends EntitySystem {
protected onEnd(): void {
// 获取性能数据
const perfData = this.getPerformanceData();
if (perfData) {
console.log(`执行时间: ${perfData.executionTime.toFixed(2)}ms`);
}
// 获取性能统计
const stats = this.getPerformanceStats();
if (stats) {
console.log(`平均执行时间: ${stats.averageTime.toFixed(2)}ms`);
}
}
public resetPerformance(): void {
this.resetPerformanceData();
}
}
```
## 系统管理
### 添加系统到场景
框架提供了两种方式添加系统:传入实例或传入类型(自动依赖注入)。
```typescript
// 在场景子类中添加系统
class GameScene extends Scene {
protected initialize(): void {
// 方式1传入实例
this.addSystem(new MovementSystem());
this.addSystem(new RenderSystem());
// 方式2传入类型自动依赖注入
this.addEntityProcessor(PhysicsSystem);
// 设置系统更新顺序
const movementSystem = this.getSystem(MovementSystem);
if (movementSystem) {
movementSystem.updateOrder = 1;
}
}
}
```
### 系统依赖注入
系统实现了 `IService` 接口,支持通过依赖注入获取其他服务或系统:
```typescript
import { ECSSystem, Injectable, Inject } from '@esengine/ecs-framework';
@Injectable()
@ECSSystem('Physics')
class PhysicsSystem extends EntitySystem {
constructor(
@Inject(CollisionService) private collision: CollisionService
) {
super(Matcher.all(Transform, RigidBody));
}
protected process(entities: readonly Entity[]): void {
// 使用注入的服务
this.collision.detectCollisions(entities);
}
// 实现 IService 接口的 dispose 方法
public dispose(): void {
// 清理资源
}
}
// 使用时传入类型即可,框架会自动注入依赖
class GameScene extends Scene {
protected initialize(): void {
// 自动依赖注入
this.addEntityProcessor(PhysicsSystem);
}
}
```
注意事项:
- 使用 `@Injectable()` 装饰器标记需要依赖注入的系统
- 在构造函数参数中使用 `@Inject()` 装饰器声明依赖
- 系统必须实现 `dispose()` 方法IService 接口要求)
- 使用 `addEntityProcessor(类型)` 而不是 `addSystem(new 类型())` 来启用依赖注入
### 系统更新顺序
```typescript
@ECSSystem('Input')
class InputSystem extends EntitySystem {
constructor() {
super(Matcher.all(InputComponent));
this.updateOrder = -100; // 输入系统优先执行
}
}
@ECSSystem('Physics')
class PhysicsSystem extends EntitySystem {
constructor() {
super(Matcher.all(RigidBody));
this.updateOrder = 0; // 默认顺序
}
}
@ECSSystem('Render')
class RenderSystem extends EntitySystem {
constructor() {
super(Matcher.all(Sprite, Transform));
this.updateOrder = 100; // 渲染系统最后执行
}
}
```
## 复杂系统示例
### 碰撞检测系统
```typescript
@ECSSystem('Collision')
class CollisionSystem extends EntitySystem {
constructor() {
super(Matcher.all(Transform, Collider));
}
protected process(entities: readonly Entity[]): void {
// 简单的 n² 碰撞检测
for (let i = 0; i < entities.length; i++) {
for (let j = i + 1; j < entities.length; j++) {
this.checkCollision(entities[i], entities[j]);
}
}
}
private checkCollision(entityA: Entity, entityB: Entity): void {
const transformA = entityA.getComponent(Transform);
const transformB = entityB.getComponent(Transform);
const colliderA = entityA.getComponent(Collider);
const colliderB = entityB.getComponent(Collider);
if (this.isColliding(transformA, colliderA, transformB, colliderB)) {
// 发送碰撞事件
this.scene?.eventSystem.emitSync('collision', {
entityA,
entityB
});
}
}
private isColliding(transformA: Transform, colliderA: Collider,
transformB: Transform, colliderB: Collider): boolean {
// 碰撞检测逻辑
return false; // 简化示例
}
}
```
### 状态机系统
```typescript
@ECSSystem('StateMachine')
class StateMachineSystem extends EntitySystem {
constructor() {
super(Matcher.all(StateMachine));
}
protected process(entities: readonly Entity[]): void {
for (const entity of entities) {
const stateMachine = entity.getComponent(StateMachine);
if (stateMachine) {
stateMachine.updateTimer(Time.deltaTime);
this.updateState(entity, stateMachine);
}
}
}
private updateState(entity: Entity, stateMachine: StateMachine): void {
switch (stateMachine.currentState) {
case EntityState.Idle:
this.handleIdleState(entity, stateMachine);
break;
case EntityState.Moving:
this.handleMovingState(entity, stateMachine);
break;
case EntityState.Attacking:
this.handleAttackingState(entity, stateMachine);
break;
}
}
private handleIdleState(entity: Entity, stateMachine: StateMachine): void {
// 空闲状态逻辑
}
private handleMovingState(entity: Entity, stateMachine: StateMachine): void {
// 移动状态逻辑
}
private handleAttackingState(entity: Entity, stateMachine: StateMachine): void {
// 攻击状态逻辑
}
}
```
## 最佳实践
### 1. 系统单一职责
```typescript
// ✅ 好的系统设计 - 职责单一
@ECSSystem('Movement')
class MovementSystem extends EntitySystem {
constructor() {
super(Matcher.all(Position, Velocity));
}
}
@ECSSystem('Rendering')
class RenderingSystem extends EntitySystem {
constructor() {
super(Matcher.all(Sprite, Transform));
}
}
// ❌ 避免的系统设计 - 职责过多
@ECSSystem('GameSystem')
class GameSystem extends EntitySystem {
// 一个系统处理移动、渲染、音效等多种逻辑
}
```
### 2. 使用装饰器
**必须使用 `@ECSSystem` 装饰器**
```typescript
// ✅ 正确的用法
@ECSSystem('Physics')
class PhysicsSystem extends EntitySystem {
// 系统实现
}
// ❌ 错误的用法 - 没有装饰器
class BadSystem extends EntitySystem {
// 这样定义的系统可能在生产环境出现问题
}
```
### 3. 合理的更新顺序
```typescript
// 按逻辑顺序设置系统的更新时序
@ECSSystem('Input')
class InputSystem extends EntitySystem {
constructor() {
super();
this.updateOrder = -100; // 最先处理输入
}
}
@ECSSystem('Logic')
class GameLogicSystem extends EntitySystem {
constructor() {
super();
this.updateOrder = 0; // 处理游戏逻辑
}
}
@ECSSystem('Render')
class RenderSystem extends EntitySystem {
constructor() {
super();
this.updateOrder = 100; // 最后进行渲染
}
}
```
### 4. 避免在系统间直接引用
```typescript
// ❌ 避免:系统间直接引用
@ECSSystem('Bad')
class BadSystem extends EntitySystem {
private otherSystem: SomeOtherSystem; // 避免直接引用其他系统
}
// ✅ 推荐:通过事件系统通信
@ECSSystem('Good')
class GoodSystem extends EntitySystem {
protected process(entities: readonly Entity[]): void {
// 通过事件系统与其他系统通信
this.scene?.eventSystem.emitSync('data_updated', { entities });
}
}
```
### 5. 及时清理资源
```typescript
@ECSSystem('Resource')
class ResourceSystem extends EntitySystem {
private resources: Map<string, any> = new Map();
protected onDestroy(): void {
// 清理资源
for (const [key, resource] of this.resources) {
if (resource.dispose) {
resource.dispose();
}
}
this.resources.clear();
}
}
```
系统是 ECS 架构的逻辑处理核心,正确设计和使用系统能让你的游戏代码更加模块化、高效和易于维护。

View File

@@ -0,0 +1,553 @@
# 时间和定时器系统
ECS 框架提供了完整的时间管理和定时器系统,包括时间缩放、帧时间计算和灵活的定时器调度功能。
## Time 类
Time 类是框架的时间管理核心,提供了游戏时间相关的所有功能。
### 基本时间属性
```typescript
import { Time } from '@esengine/ecs-framework';
class GameSystem extends EntitySystem {
protected process(entities: readonly Entity[]): void {
// 获取帧时间(秒)
const deltaTime = Time.deltaTime;
// 获取未缩放的帧时间
const unscaledDelta = Time.unscaledDeltaTime;
// 获取游戏总时间
const totalTime = Time.totalTime;
// 获取当前帧数
const frameCount = Time.frameCount;
console.log(`${frameCount} 帧,帧时间: ${deltaTime}s总时间: ${totalTime}s`);
}
}
```
### 时间缩放
Time 类支持时间缩放功能,可以实现慢动作、快进等效果:
```typescript
class TimeControlSystem extends EntitySystem {
public enableSlowMotion(): void {
// 设置为慢动作50%速度)
Time.timeScale = 0.5;
console.log('慢动作模式启用');
}
public enableFastForward(): void {
// 设置为快进200%速度)
Time.timeScale = 2.0;
console.log('快进模式启用');
}
public pauseGame(): void {
// 暂停游戏(时间静止)
Time.timeScale = 0;
console.log('游戏暂停');
}
public resumeNormalSpeed(): void {
// 恢复正常速度
Time.timeScale = 1.0;
console.log('恢复正常速度');
}
protected process(entities: readonly Entity[]): void {
// deltaTime 会受到 timeScale 影响
const scaledDelta = Time.deltaTime; // 受时间缩放影响
const realDelta = Time.unscaledDeltaTime; // 不受时间缩放影响
for (const entity of entities) {
const movement = entity.getComponent(Movement);
if (movement) {
// 使用缩放时间进行游戏逻辑更新
movement.update(scaledDelta);
}
const ui = entity.getComponent(UIComponent);
if (ui) {
// UI 动画使用真实时间,不受游戏时间缩放影响
ui.update(realDelta);
}
}
}
}
```
### 时间检查工具
```typescript
class CooldownSystem extends EntitySystem {
private lastAttackTime = 0;
private lastSpawnTime = 0;
constructor() {
super(Matcher.all(Weapon));
}
protected process(entities: readonly Entity[]): void {
// 检查攻击冷却
if (Time.checkEvery(1.5, this.lastAttackTime)) {
this.performAttack();
this.lastAttackTime = Time.totalTime;
}
// 检查生成间隔
if (Time.checkEvery(3.0, this.lastSpawnTime)) {
this.spawnEnemy();
this.lastSpawnTime = Time.totalTime;
}
}
private performAttack(): void {
console.log('执行攻击!');
}
private spawnEnemy(): void {
console.log('生成敌人!');
}
}
```
## Core.schedule 定时器系统
Core 提供了强大的定时器调度功能,可以创建一次性或重复执行的定时器。
### 基本定时器使用
```typescript
import { Core } from '@esengine/ecs-framework';
class GameScene extends Scene {
protected initialize(): void {
// 创建一次性定时器
this.createOneTimeTimers();
// 创建重复定时器
this.createRepeatingTimers();
// 创建带上下文的定时器
this.createContextTimers();
}
private createOneTimeTimers(): void {
// 2秒后执行一次
Core.schedule(2.0, false, null, (timer) => {
console.log('2秒延迟执行');
});
// 5秒后显示提示
Core.schedule(5.0, false, this, (timer) => {
const scene = timer.getContext<GameScene>();
scene.showTip('游戏提示5秒已过');
});
}
private createRepeatingTimers(): void {
// 每秒重复执行
const heartbeatTimer = Core.schedule(1.0, true, null, (timer) => {
console.log(`游戏心跳 - 总时间: ${Time.totalTime.toFixed(1)}s`);
});
// 可以保存定时器引用用于后续控制
this.saveTimerReference(heartbeatTimer);
}
private createContextTimers(): void {
const gameData = { score: 0, level: 1 };
// 每2秒增加分数
Core.schedule(2.0, true, gameData, (timer) => {
const data = timer.getContext<typeof gameData>();
data.score += 10;
console.log(`分数增加!当前分数: ${data.score}`);
});
}
private saveTimerReference(timer: any): void {
// 可以稍后停止定时器
setTimeout(() => {
timer.stop();
console.log('定时器已停止');
}, 10000); // 10秒后停止
}
private showTip(message: string): void {
console.log('提示:', message);
}
}
```
### 定时器控制
```typescript
class TimerControlExample {
private attackTimer: any;
private spawnerTimer: any;
public startCombat(): void {
// 启动攻击定时器
this.attackTimer = Core.schedule(0.5, true, this, (timer) => {
const self = timer.getContext<TimerControlExample>();
self.performAttack();
});
// 启动敌人生成定时器
this.spawnerTimer = Core.schedule(3.0, true, null, (timer) => {
this.spawnEnemy();
});
}
public stopCombat(): void {
// 停止所有战斗相关定时器
if (this.attackTimer) {
this.attackTimer.stop();
console.log('攻击定时器已停止');
}
if (this.spawnerTimer) {
this.spawnerTimer.stop();
console.log('生成定时器已停止');
}
}
public resetAttackTimer(): void {
// 重置攻击定时器
if (this.attackTimer) {
this.attackTimer.reset();
console.log('攻击定时器已重置');
}
}
private performAttack(): void {
console.log('执行攻击');
}
private spawnEnemy(): void {
console.log('生成敌人');
}
}
```
### 复杂定时器场景
```typescript
class AdvancedTimerUsage {
private powerUpDuration = 0;
private powerUpActive = false;
public activatePowerUp(): void {
if (this.powerUpActive) {
console.log('能力提升已激活');
return;
}
this.powerUpActive = true;
this.powerUpDuration = 10; // 10秒持续时间
console.log('能力提升激活!');
// 每秒更新剩余时间
const countdownTimer = Core.schedule(1.0, true, this, (timer) => {
const self = timer.getContext<AdvancedTimerUsage>();
self.powerUpDuration--;
console.log(`能力提升剩余时间: ${self.powerUpDuration}`);
if (self.powerUpDuration <= 0) {
self.deactivatePowerUp();
timer.stop(); // 停止倒计时
}
});
// 能力提升结束定时器(备用)
Core.schedule(10.0, false, this, (timer) => {
const self = timer.getContext<AdvancedTimerUsage>();
if (self.powerUpActive) {
self.deactivatePowerUp();
}
});
}
private deactivatePowerUp(): void {
this.powerUpActive = false;
this.powerUpDuration = 0;
console.log('能力提升结束');
}
// 创建波次攻击定时器
public startWaveAttack(): void {
let waveCount = 0;
const maxWaves = 5;
const waveTimer = Core.schedule(2.0, true, { waveCount, maxWaves }, (timer) => {
const context = timer.getContext<{ waveCount: number, maxWaves: number }>();
context.waveCount++;
console.log(`${context.waveCount} 波攻击!`);
if (context.waveCount >= context.maxWaves) {
console.log('所有波次攻击完成');
timer.stop();
}
});
}
// 创建条件定时器
public startConditionalTimer(): void {
Core.schedule(0.1, true, this, (timer) => {
const self = timer.getContext<AdvancedTimerUsage>();
// 检查某个条件
if (self.shouldStopTimer()) {
console.log('条件满足,停止定时器');
timer.stop();
return;
}
// 继续执行定时器逻辑
self.performTimerAction();
});
}
private shouldStopTimer(): boolean {
// 检查停止条件
return Time.totalTime > 30; // 30秒后停止
}
private performTimerAction(): void {
console.log('执行定时器动作');
}
}
```
## 实际应用示例
### 技能冷却系统
```typescript
class SkillCooldownSystem extends EntitySystem {
constructor() {
super(Matcher.all(SkillComponent));
}
protected process(entities: readonly Entity[]): void {
for (const entity of entities) {
const skill = entity.getComponent(SkillComponent);
// 更新技能冷却
if (skill.isOnCooldown) {
skill.cooldownRemaining -= Time.deltaTime;
if (skill.cooldownRemaining <= 0) {
skill.cooldownRemaining = 0;
skill.isOnCooldown = false;
console.log(`技能 ${skill.name} 冷却完成`);
}
}
}
}
public useSkill(entity: Entity, skillName: string): boolean {
const skill = entity.getComponent(SkillComponent);
if (skill.isOnCooldown) {
console.log(`技能 ${skillName} 还在冷却中,剩余 ${skill.cooldownRemaining.toFixed(1)}`);
return false;
}
// 执行技能
this.executeSkill(entity, skill);
// 开始冷却
skill.isOnCooldown = true;
skill.cooldownRemaining = skill.cooldownDuration;
return true;
}
private executeSkill(entity: Entity, skill: SkillComponent): void {
console.log(`执行技能: ${skill.name}`);
// 技能效果逻辑
}
}
```
### 游戏状态定时器
```typescript
class GameStateManager {
private gamePhase = 'preparation';
private phaseTimer: any;
public startGame(): void {
this.startPreparationPhase();
}
private startPreparationPhase(): void {
this.gamePhase = 'preparation';
console.log('准备阶段开始 - 10秒准备时间');
this.phaseTimer = Core.schedule(10.0, false, this, (timer) => {
const self = timer.getContext<GameStateManager>();
self.startCombatPhase();
});
}
private startCombatPhase(): void {
this.gamePhase = 'combat';
console.log('战斗阶段开始 - 60秒战斗时间');
this.phaseTimer = Core.schedule(60.0, false, this, (timer) => {
const self = timer.getContext<GameStateManager>();
self.startResultPhase();
});
// 每10秒刷新一波敌人
Core.schedule(10.0, true, null, (timer) => {
if (this.gamePhase === 'combat') {
this.spawnEnemyWave();
} else {
timer.stop(); // 战斗阶段结束时停止刷新
}
});
}
private startResultPhase(): void {
this.gamePhase = 'result';
console.log('结算阶段开始 - 5秒结算时间');
this.phaseTimer = Core.schedule(5.0, false, this, (timer) => {
const self = timer.getContext<GameStateManager>();
self.endGame();
});
}
private endGame(): void {
console.log('游戏结束');
this.gamePhase = 'ended';
}
private spawnEnemyWave(): void {
console.log('刷新敌人波次');
}
public getCurrentPhase(): string {
return this.gamePhase;
}
}
```
## 最佳实践
### 1. 合理使用时间类型
```typescript
class MovementSystem extends EntitySystem {
protected process(entities: readonly Entity[]): void {
for (const entity of entities) {
const movement = entity.getComponent(Movement);
// ✅ 游戏逻辑使用缩放时间
movement.position.x += movement.velocity.x * Time.deltaTime;
// ✅ UI动画使用真实时间不受游戏暂停影响
const ui = entity.getComponent(UIAnimation);
if (ui) {
ui.update(Time.unscaledDeltaTime);
}
}
}
}
```
### 2. 定时器管理
```typescript
class TimerManager {
private timers: any[] = [];
public createManagedTimer(duration: number, repeats: boolean, callback: () => void): any {
const timer = Core.schedule(duration, repeats, null, callback);
this.timers.push(timer);
return timer;
}
public stopAllTimers(): void {
for (const timer of this.timers) {
timer.stop();
}
this.timers = [];
}
public cleanupCompletedTimers(): void {
this.timers = this.timers.filter(timer => !timer.isDone);
}
}
```
### 3. 避免过多的定时器
```typescript
// ❌ 避免:为每个实体创建定时器
class BadExample extends EntitySystem {
protected onAdded(entity: Entity): void {
Core.schedule(1.0, true, entity, (timer) => {
// 每个实体一个定时器,性能差
});
}
}
// ✅ 推荐:在系统中统一管理时间
class GoodExample extends EntitySystem {
private lastUpdateTime = 0;
protected process(entities: readonly Entity[]): void {
// 每秒执行一次逻辑
if (Time.checkEvery(1.0, this.lastUpdateTime)) {
this.processAllEntities(entities);
this.lastUpdateTime = Time.totalTime;
}
}
private processAllEntities(entities: readonly Entity[]): void {
// 批量处理所有实体
}
}
```
### 4. 定时器上下文使用
```typescript
interface TimerContext {
entityId: number;
duration: number;
onComplete: () => void;
}
class ContextualTimerExample {
public createEntityTimer(entityId: number, duration: number, onComplete: () => void): void {
const context: TimerContext = {
entityId,
duration,
onComplete
};
Core.schedule(duration, false, context, (timer) => {
const ctx = timer.getContext<TimerContext>();
console.log(`实体 ${ctx.entityId} 的定时器完成`);
ctx.onComplete();
});
}
}
```
时间和定时器系统是游戏开发中的重要工具,正确使用这些功能能让你的游戏逻辑更加精确和可控。

608
docs/guide/worker-system.md Normal file
View File

@@ -0,0 +1,608 @@
# Worker系统
Worker系统WorkerEntitySystem是ECS框架中基于Web Worker的多线程处理系统专为计算密集型任务设计能够充分利用多核CPU性能实现真正的并行计算。
## 核心特性
- **真正的并行计算**利用Web Worker在后台线程执行计算密集型任务
- **自动负载均衡**根据CPU核心数自动分配工作负载
- **SharedArrayBuffer优化**:零拷贝数据共享,提升大规模计算性能
- **降级支持**不支持Worker时自动回退到主线程处理
- **类型安全**完整的TypeScript支持和类型检查
## 基本用法
### 简单的物理系统示例
```typescript
interface PhysicsData {
id: number;
x: number;
y: number;
vx: number;
vy: number;
mass: number;
radius: number;
}
@ECSSystem('Physics')
class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsData> {
constructor() {
super(Matcher.all(Position, Velocity, Physics), {
enableWorker: true, // 启用Worker并行处理
workerCount: 8, // Worker数量系统会自动限制在硬件支持范围内
entitiesPerWorker: 100, // 每个Worker处理的实体数量
useSharedArrayBuffer: true, // 启用SharedArrayBuffer优化
entityDataSize: 7, // 每个实体数据大小
maxEntities: 10000, // 最大实体数量
systemConfig: { // 传递给Worker的配置
gravity: 100,
friction: 0.95
}
});
}
// 数据提取将Entity转换为可序列化的数据
protected extractEntityData(entity: Entity): PhysicsData {
const position = entity.getComponent(Position);
const velocity = entity.getComponent(Velocity);
const physics = entity.getComponent(Physics);
return {
id: entity.id,
x: position.x,
y: position.y,
vx: velocity.x,
vy: velocity.y,
mass: physics.mass,
radius: physics.radius
};
}
// Worker处理函数纯函数在Worker中执行
protected workerProcess(
entities: PhysicsData[],
deltaTime: number,
config: any
): PhysicsData[] {
return entities.map(entity => {
// 应用重力
entity.vy += config.gravity * deltaTime;
// 更新位置
entity.x += entity.vx * deltaTime;
entity.y += entity.vy * deltaTime;
// 应用摩擦力
entity.vx *= config.friction;
entity.vy *= config.friction;
return entity;
});
}
// 结果应用将Worker处理结果应用回Entity
protected applyResult(entity: Entity, result: PhysicsData): void {
const position = entity.getComponent(Position);
const velocity = entity.getComponent(Velocity);
position.x = result.x;
position.y = result.y;
velocity.x = result.vx;
velocity.y = result.vy;
}
// SharedArrayBuffer优化支持
protected getDefaultEntityDataSize(): number {
return 7; // id, x, y, vx, vy, mass, radius
}
protected writeEntityToBuffer(entityData: PhysicsData, offset: number): void {
if (!this.sharedFloatArray) return;
this.sharedFloatArray[offset + 0] = entityData.id;
this.sharedFloatArray[offset + 1] = entityData.x;
this.sharedFloatArray[offset + 2] = entityData.y;
this.sharedFloatArray[offset + 3] = entityData.vx;
this.sharedFloatArray[offset + 4] = entityData.vy;
this.sharedFloatArray[offset + 5] = entityData.mass;
this.sharedFloatArray[offset + 6] = entityData.radius;
}
protected readEntityFromBuffer(offset: number): PhysicsData | null {
if (!this.sharedFloatArray) return null;
return {
id: this.sharedFloatArray[offset + 0],
x: this.sharedFloatArray[offset + 1],
y: this.sharedFloatArray[offset + 2],
vx: this.sharedFloatArray[offset + 3],
vy: this.sharedFloatArray[offset + 4],
mass: this.sharedFloatArray[offset + 5],
radius: this.sharedFloatArray[offset + 6]
};
}
}
```
## 配置选项
Worker系统支持丰富的配置选项
```typescript
interface WorkerSystemConfig {
/** 是否启用Worker并行处理 */
enableWorker?: boolean;
/** Worker数量默认为CPU核心数自动限制在系统最大值内 */
workerCount?: number;
/** 每个Worker处理的实体数量用于控制负载分布 */
entitiesPerWorker?: number;
/** 系统配置数据会传递给Worker */
systemConfig?: any;
/** 是否使用SharedArrayBuffer优化 */
useSharedArrayBuffer?: boolean;
/** 每个实体在SharedArrayBuffer中占用的Float32数量 */
entityDataSize?: number;
/** 最大实体数量用于预分配SharedArrayBuffer */
maxEntities?: number;
}
```
### 配置建议
```typescript
constructor() {
super(matcher, {
// 根据任务复杂度决定是否启用
enableWorker: this.shouldUseWorker(),
// Worker数量系统会自动限制在硬件支持范围内
workerCount: 8, // 请求8个Worker实际数量受CPU核心数限制
// 每个Worker处理的实体数量可选
entitiesPerWorker: 200, // 精确控制负载分布
// 大量简单计算时启用SharedArrayBuffer
useSharedArrayBuffer: this.entityCount > 1000,
// 根据实际数据结构设置
entityDataSize: 8, // 确保与数据结构匹配
// 预估最大实体数量
maxEntities: 10000,
// 传递给Worker的全局配置
systemConfig: {
gravity: 9.8,
friction: 0.95,
worldBounds: { width: 1920, height: 1080 }
}
});
}
private shouldUseWorker(): boolean {
// 根据实体数量和计算复杂度决定
return this.expectedEntityCount > 100;
}
// 获取系统信息
getSystemInfo() {
const info = this.getWorkerInfo();
console.log(`Worker数量: ${info.workerCount}/${info.maxSystemWorkerCount}`);
console.log(`每Worker实体数: ${info.entitiesPerWorker || '自动分配'}`);
console.log(`当前模式: ${info.currentMode}`);
}
```
## 处理模式
Worker系统支持两种处理模式
### 1. 传统Worker模式
数据通过序列化在主线程和Worker间传递
```typescript
// 适用于:复杂计算逻辑,实体数量适中
constructor() {
super(matcher, {
enableWorker: true,
useSharedArrayBuffer: false, // 使用传统模式
workerCount: 2
});
}
protected workerProcess(entities: EntityData[], deltaTime: number): EntityData[] {
// 复杂的算法逻辑
return entities.map(entity => {
// AI决策、路径规划等复杂计算
return this.complexAILogic(entity, deltaTime);
});
}
```
### 2. SharedArrayBuffer模式
零拷贝数据共享,适合大量简单计算:
```typescript
// 适用于:大量实体的简单计算
constructor() {
super(matcher, {
enableWorker: true,
useSharedArrayBuffer: true, // 启用共享内存
entityDataSize: 6,
maxEntities: 10000
});
}
protected getSharedArrayBufferProcessFunction(): SharedArrayBufferProcessFunction {
return function(sharedFloatArray: Float32Array, startIndex: number, endIndex: number, deltaTime: number, config: any) {
const entitySize = 6;
for (let i = startIndex; i < endIndex; i++) {
const offset = i * entitySize;
// 读取数据
let x = sharedFloatArray[offset];
let y = sharedFloatArray[offset + 1];
let vx = sharedFloatArray[offset + 2];
let vy = sharedFloatArray[offset + 3];
// 物理计算
vy += config.gravity * deltaTime;
x += vx * deltaTime;
y += vy * deltaTime;
// 写回数据
sharedFloatArray[offset] = x;
sharedFloatArray[offset + 1] = y;
sharedFloatArray[offset + 2] = vx;
sharedFloatArray[offset + 3] = vy;
}
};
}
```
## 完整示例:粒子物理系统
一个包含碰撞检测的完整粒子物理系统:
```typescript
interface ParticleData {
id: number;
x: number;
y: number;
dx: number;
dy: number;
mass: number;
radius: number;
bounce: number;
friction: number;
}
@ECSSystem('ParticlePhysics')
class ParticlePhysicsWorkerSystem extends WorkerEntitySystem<ParticleData> {
constructor() {
super(Matcher.all(Position, Velocity, Physics, Renderable), {
enableWorker: true,
workerCount: 6, // 请求6个Worker自动限制在CPU核心数内
entitiesPerWorker: 150, // 每个Worker处理150个粒子
useSharedArrayBuffer: true,
entityDataSize: 9,
maxEntities: 5000,
systemConfig: {
gravity: 100,
canvasWidth: 800,
canvasHeight: 600,
groundFriction: 0.98
}
});
}
protected extractEntityData(entity: Entity): ParticleData {
const position = entity.getComponent(Position);
const velocity = entity.getComponent(Velocity);
const physics = entity.getComponent(Physics);
const renderable = entity.getComponent(Renderable);
return {
id: entity.id,
x: position.x,
y: position.y,
dx: velocity.dx,
dy: velocity.dy,
mass: physics.mass,
radius: renderable.size,
bounce: physics.bounce,
friction: physics.friction
};
}
protected workerProcess(
entities: ParticleData[],
deltaTime: number,
config: any
): ParticleData[] {
const result = entities.map(e => ({ ...e }));
// 基础物理更新
for (const particle of result) {
// 应用重力
particle.dy += config.gravity * deltaTime;
// 更新位置
particle.x += particle.dx * deltaTime;
particle.y += particle.dy * deltaTime;
// 边界碰撞
if (particle.x <= particle.radius) {
particle.x = particle.radius;
particle.dx = -particle.dx * particle.bounce;
} else if (particle.x >= config.canvasWidth - particle.radius) {
particle.x = config.canvasWidth - particle.radius;
particle.dx = -particle.dx * particle.bounce;
}
if (particle.y <= particle.radius) {
particle.y = particle.radius;
particle.dy = -particle.dy * particle.bounce;
} else if (particle.y >= config.canvasHeight - particle.radius) {
particle.y = config.canvasHeight - particle.radius;
particle.dy = -particle.dy * particle.bounce;
particle.dx *= config.groundFriction;
}
// 空气阻力
particle.dx *= particle.friction;
particle.dy *= particle.friction;
}
// 粒子间碰撞检测O(n²)算法)
for (let i = 0; i < result.length; i++) {
for (let j = i + 1; j < result.length; j++) {
const p1 = result[i];
const p2 = result[j];
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
const distance = Math.sqrt(dx * dx + dy * dy);
const minDistance = p1.radius + p2.radius;
if (distance < minDistance && distance > 0) {
// 分离粒子
const nx = dx / distance;
const ny = dy / distance;
const overlap = minDistance - distance;
p1.x -= nx * overlap * 0.5;
p1.y -= ny * overlap * 0.5;
p2.x += nx * overlap * 0.5;
p2.y += ny * overlap * 0.5;
// 弹性碰撞
const relativeVelocityX = p2.dx - p1.dx;
const relativeVelocityY = p2.dy - p1.dy;
const velocityAlongNormal = relativeVelocityX * nx + relativeVelocityY * ny;
if (velocityAlongNormal > 0) continue;
const restitution = (p1.bounce + p2.bounce) * 0.5;
const impulseScalar = -(1 + restitution) * velocityAlongNormal / (1/p1.mass + 1/p2.mass);
p1.dx -= impulseScalar * nx / p1.mass;
p1.dy -= impulseScalar * ny / p1.mass;
p2.dx += impulseScalar * nx / p2.mass;
p2.dy += impulseScalar * ny / p2.mass;
}
}
}
return result;
}
protected applyResult(entity: Entity, result: ParticleData): void {
if (!entity?.enabled) return;
const position = entity.getComponent(Position);
const velocity = entity.getComponent(Velocity);
if (position && velocity) {
position.set(result.x, result.y);
velocity.set(result.dx, result.dy);
}
}
protected getDefaultEntityDataSize(): number {
return 9;
}
protected writeEntityToBuffer(data: ParticleData, offset: number): void {
if (!this.sharedFloatArray) return;
this.sharedFloatArray[offset + 0] = data.id;
this.sharedFloatArray[offset + 1] = data.x;
this.sharedFloatArray[offset + 2] = data.y;
this.sharedFloatArray[offset + 3] = data.dx;
this.sharedFloatArray[offset + 4] = data.dy;
this.sharedFloatArray[offset + 5] = data.mass;
this.sharedFloatArray[offset + 6] = data.radius;
this.sharedFloatArray[offset + 7] = data.bounce;
this.sharedFloatArray[offset + 8] = data.friction;
}
protected readEntityFromBuffer(offset: number): ParticleData | null {
if (!this.sharedFloatArray) return null;
return {
id: this.sharedFloatArray[offset + 0],
x: this.sharedFloatArray[offset + 1],
y: this.sharedFloatArray[offset + 2],
dx: this.sharedFloatArray[offset + 3],
dy: this.sharedFloatArray[offset + 4],
mass: this.sharedFloatArray[offset + 5],
radius: this.sharedFloatArray[offset + 6],
bounce: this.sharedFloatArray[offset + 7],
friction: this.sharedFloatArray[offset + 8]
};
}
// 性能监控
public getPerformanceInfo(): {
enabled: boolean;
workerCount: number;
entitiesPerWorker?: number;
maxSystemWorkerCount: number;
entityCount: number;
isProcessing: boolean;
currentMode: string;
} {
const workerInfo = this.getWorkerInfo();
return {
...workerInfo,
entityCount: this.entities.length
};
}
}
```
## 适用场景
Worker系统特别适合以下场景
### 1. 物理模拟
- **重力系统**:大量实体的重力计算
- **碰撞检测**:复杂的碰撞算法
- **流体模拟**:粒子流体系统
- **布料模拟**:顶点物理计算
### 2. AI计算
- **路径寻找**A*、Dijkstra等算法
- **行为树**复杂的AI决策逻辑
- **群体智能**:鸟群、鱼群算法
- **神经网络**简单的AI推理
### 3. 数据处理
- **大量实体更新**:状态机、生命周期管理
- **统计计算**:游戏数据分析
- **图像处理**:纹理生成、效果计算
- **音频处理**:音效合成、频谱分析
## 最佳实践
### 1. Worker函数要求
```typescript
// ✅ 推荐Worker处理函数是纯函数
protected workerProcess(entities: PhysicsData[], deltaTime: number, config: any): PhysicsData[] {
// 只使用参数和标准JavaScript API
return entities.map(entity => {
// 纯计算逻辑,不依赖外部状态
entity.y += entity.velocity * deltaTime;
return entity;
});
}
// ❌ 避免在Worker函数中使用外部引用
protected workerProcess(entities: PhysicsData[], deltaTime: number): PhysicsData[] {
// this 和外部变量在Worker中不可用
return entities.map(entity => {
entity.y += this.someProperty; // ❌ 错误
return entity;
});
}
```
### 2. 数据设计
```typescript
// ✅ 推荐:合理的数据设计
interface SimplePhysicsData {
x: number;
y: number;
vx: number;
vy: number;
// 保持数据结构简单,便于序列化
}
// ❌ 避免:复杂的嵌套对象
interface ComplexData {
transform: {
position: { x: number; y: number };
rotation: { angle: number };
};
// 复杂嵌套结构增加序列化开销
}
```
### 3. Worker数量控制
```typescript
// ✅ 推荐灵活的Worker配置
constructor() {
super(matcher, {
// 直接指定需要的Worker数量系统会自动限制在硬件支持范围内
workerCount: 8, // 请求8个Worker
entitiesPerWorker: 100, // 每个Worker处理100个实体
enableWorker: this.shouldUseWorker(), // 条件启用
});
}
private shouldUseWorker(): boolean {
// 根据实体数量和复杂度决定是否使用Worker
return this.expectedEntityCount > 100;
}
// 获取实际使用的Worker信息
checkWorkerConfiguration() {
const info = this.getWorkerInfo();
console.log(`请求Worker数量: 8`);
console.log(`实际Worker数量: ${info.workerCount}`);
console.log(`系统最大支持: ${info.maxSystemWorkerCount}`);
console.log(`每Worker实体数: ${info.entitiesPerWorker || '自动分配'}`);
}
```
### 4. 性能监控
```typescript
// ✅ 推荐:性能监控
public getPerformanceMetrics(): WorkerPerformanceMetrics {
return {
...this.getWorkerInfo(),
entityCount: this.entities.length,
averageProcessTime: this.getAverageProcessTime(),
workerUtilization: this.getWorkerUtilization()
};
}
```
## 性能优化建议
### 1. 计算密集度评估
只对计算密集型任务使用Worker避免在简单计算上增加线程开销。
### 2. 数据传输优化
- 使用SharedArrayBuffer减少序列化开销
- 保持数据结构简单和扁平
- 避免频繁的大数据传输
### 3. 降级策略
始终提供主线程回退方案确保在不支持Worker的环境中正常运行。
### 4. 内存管理
及时清理Worker池和共享缓冲区避免内存泄漏。
### 5. 负载均衡
使用 `entitiesPerWorker` 参数精确控制负载分布避免某些Worker空闲而其他Worker过载。
## 在线演示
查看完整的Worker系统演示[Worker系统演示](https://esengine.github.io/ecs-framework/demos/worker-system/)
该演示展示了:
- 多线程物理计算
- 实时性能对比
- SharedArrayBuffer优化
- 大量实体的并行处理
Worker系统为ECS框架提供了强大的并行计算能力让你能够充分利用现代多核处理器的性能为复杂的游戏逻辑和计算密集型任务提供了高效的解决方案。

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

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

23
docs/index.md Normal file
View File

@@ -0,0 +1,23 @@
---
layout: home
hero:
name: "ECS Framework"
text: "高性能ECS框架"
tagline: "为Javascript游戏开发而设计"
actions:
- theme: brand
text: 快速开始
link: /guide/getting-started
- theme: alt
text: 查看示例
link: https://github.com/esengine/lawn-mower-demo
features:
- title: 高性能
details: 支持大规模实体处理
- title: 类型安全
details: 完整的TypeScript支持编译时类型检查
- title: 模块化设计
details: 核心功能独立打包,支持多平台
---

View File

@@ -0,0 +1,118 @@
/*! coi-serviceworker v0.1.7 - Guido Zuidhof and contributors, licensed under MIT */
let coepCredentialless = false;
if (typeof window === 'undefined') {
self.addEventListener("install", () => self.skipWaiting());
self.addEventListener("activate", (event) => event.waitUntil(self.clients.claim()));
self.addEventListener("message", (ev) => {
if (!ev.data) {
return;
} else if (ev.data.type === "deregister") {
self.registration
.unregister()
.then(() => {
return self.clients.matchAll();
})
.then(clients => {
clients.forEach((client) => client.navigate(client.url));
});
} else if (ev.data.type === "coepCredentialless") {
coepCredentialless = ev.data.value;
}
});
self.addEventListener("fetch", function (event) {
const r = event.request;
if (r.cache === "only-if-cached" && r.mode !== "same-origin") {
return;
}
const request = (coepCredentialless && r.mode === "no-cors")
? new Request(r, {
credentials: "omit",
})
: r;
event.respondWith(
fetch(request)
.then((response) => {
if (response.status === 0) {
return response;
}
const newHeaders = new Headers(response.headers);
newHeaders.set("Cross-Origin-Embedder-Policy",
coepCredentialless ? "credentialless" : "require-corp"
);
if (!coepCredentialless) {
newHeaders.set("Cross-Origin-Resource-Policy", "cross-origin");
}
newHeaders.set("Cross-Origin-Opener-Policy", "same-origin");
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: newHeaders,
});
})
.catch((e) => console.error(e))
);
});
} else {
(() => {
// You can customize the behavior of this script through a global `coi` variable.
const coi = {
shouldRegister: () => true,
shouldDeregister: () => false,
coepCredentialless: () => !(window.chrome || window.netscape),
doReload: () => window.location.reload(),
quiet: false,
...window.coi
};
const n = navigator;
if (n.serviceWorker && n.serviceWorker.controller) {
n.serviceWorker.controller.postMessage({
type: "coepCredentialless",
value: coi.coepCredentialless(),
});
if (coi.shouldDeregister()) {
n.serviceWorker.controller.postMessage({ type: "deregister" });
}
}
// If we're already coi: do nothing. Perhaps it's due to this script doing its job, or COOP/COEP are
// already set from the origin server. Also if the browser has no notion of crossOriginIsolated, just give up here.
if (window.crossOriginIsolated !== false || !coi.shouldRegister()) return;
if (!window.isSecureContext) {
!coi.quiet && console.log("COOP/COEP Service Worker not registered, a secure context is required.");
return;
}
// In some environments (e.g. Chrome incognito mode) this won't be available
if (n.serviceWorker) {
n.serviceWorker.register(window.document.currentScript.src).then(
(registration) => {
!coi.quiet && console.log("COOP/COEP Service Worker registered", registration.scope);
registration.addEventListener("updatefound", () => {
!coi.quiet && console.log("Reloading page to make use of updated COOP/COEP Service Worker.");
coi.doReload();
});
// If the registration is active, but it's not controlling the page
if (registration.active && !n.serviceWorker.controller) {
!coi.quiet && console.log("Reloading page to make use of COOP/COEP Service Worker.");
coi.doReload();
}
},
(err) => {
!coi.quiet && console.error("COOP/COEP Service Worker failed to register:", err);
}
);
}
})();
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,210 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ECS Framework Worker System Demo</title>
<style>
body {
margin: 0;
padding: 20px;
font-family: Arial, sans-serif;
background: #1a1a1a;
color: white;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
h1 {
text-align: center;
color: #4a9eff;
}
.demo-area {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.canvas-container {
flex: 1;
}
#gameCanvas {
border: 2px solid #4a9eff;
background: #000;
display: block;
}
.controls {
width: 300px;
background: #2a2a2a;
padding: 20px;
border-radius: 8px;
}
.control-group {
margin-bottom: 15px;
}
.control-group label {
display: block;
margin-bottom: 5px;
color: #ccc;
}
.control-group input, .control-group button {
width: 100%;
padding: 8px;
margin-bottom: 5px;
border: 1px solid #555;
background: #3a3a3a;
color: white;
border-radius: 4px;
}
.control-group button {
background: #4a9eff;
cursor: pointer;
transition: background 0.3s;
}
.control-group button:hover {
background: #3a8eef;
}
.control-group button:disabled {
background: #555;
cursor: not-allowed;
}
.stats {
background: #2a2a2a;
padding: 15px;
border-radius: 8px;
font-family: monospace;
font-size: 12px;
}
.stats h3 {
margin-top: 0;
color: #4a9eff;
}
.stat-line {
margin: 5px 0;
}
.worker-enabled {
color: #4eff4a;
}
.worker-disabled {
color: #ff4a4a;
}
.performance-high {
color: #4eff4a;
}
.performance-medium {
color: #ffff4a;
}
.performance-low {
color: #ff4a4a;
}
</style>
<script type="module" crossorigin src="/ecs-framework/demos/worker-system/assets/index-CrID--xK.js"></script>
</head>
<body>
<div class="container">
<h1>ECS Framework Worker System 演示</h1>
<div class="demo-area">
<div class="canvas-container">
<canvas id="gameCanvas" width="800" height="600"></canvas>
</div>
<div class="controls">
<div class="control-group">
<label>实体数量:</label>
<input type="range" id="entityCount" min="100" max="10000" value="1000" step="100">
<span id="entityCountValue">1000</span>
</div>
<div class="control-group">
<label>Worker 设置:</label>
<button id="toggleWorker">禁用 Worker</button>
<button id="toggleSAB">禁用 SharedArrayBuffer</button>
</div>
<div class="control-group">
<button id="spawnParticles">生成粒子系统</button>
<button id="clearEntities">清空所有实体</button>
<button id="resetDemo">重置演示</button>
</div>
<div class="control-group">
<label>物理参数:</label>
<input type="range" id="gravity" min="0" max="500" value="100" step="10">
<label>重力: <span id="gravityValue">100</span></label>
<input type="range" id="friction" min="0" max="100" value="95" step="5">
<label>摩擦力: <span id="frictionValue">95%</span></label>
</div>
</div>
</div>
<div class="stats">
<h3>性能统计</h3>
<div class="stat-line">FPS: <span id="fps">0</span></div>
<div class="stat-line">实体数量: <span id="entityCountStat">0</span></div>
<div class="stat-line">Worker状态: <span id="workerStatus" class="worker-disabled">未启用</span></div>
<div class="stat-line">Worker负载: <span id="workerLoad">N/A</span></div>
<div class="stat-line">运行模式: <span id="sabStatus" class="worker-disabled">同步模式</span></div>
<div class="stat-line">物理系统耗时: <span id="physicsTime">0</span>ms</div>
<div class="stat-line">渲染系统耗时: <span id="renderTime">0</span>ms</div>
<div class="stat-line">总帧时间: <span id="frameTime">0</span>ms</div>
<div class="stat-line">内存使用: <span id="memoryUsage">0</span>MB</div>
</div>
</div>
<!-- 使用 coi-serviceworker 启用 SharedArrayBuffer 支持 -->
<script src="/ecs-framework/coi-serviceworker.js"></script>
<script>
// Check SharedArrayBuffer support and display info
function checkSharedArrayBufferSupport() {
const hasSharedArrayBuffer = typeof SharedArrayBuffer !== 'undefined';
const isCrossOriginIsolated = self.crossOriginIsolated || false;
const isLocalhost = location.hostname === 'localhost' || location.hostname === '127.0.0.1';
const isGitHubPages = location.hostname === 'esengine.github.io';
console.log('=== SharedArrayBuffer 支持检测 ===');
console.log('SharedArrayBuffer 存在:', hasSharedArrayBuffer);
console.log('跨域隔离状态:', isCrossOriginIsolated);
console.log('是否本地环境:', isLocalhost);
console.log('是否 GitHub Pages:', isGitHubPages);
if (hasSharedArrayBuffer && isCrossOriginIsolated) {
console.log('✅ SharedArrayBuffer 功能已启用!');
console.log('系统将使用高性能的 SharedArrayBuffer 模式');
} else if (isGitHubPages) {
console.log(' 如果页面刷新,可能是 coi-serviceworker 正在设置跨域隔离');
console.log('刷新后 SharedArrayBuffer 应该可用');
}
return hasSharedArrayBuffer && isCrossOriginIsolated;
}
// Run check after page load
window.addEventListener('load', () => {
setTimeout(checkSharedArrayBufferSupport, 1000);
});
</script>
</body>
</html>

View File

@@ -1,325 +0,0 @@
# QuerySystem 使用指南
QuerySystem 是 ECS Framework 中的高性能实体查询系统,支持多级索引、智能缓存和类型安全的查询操作。
## 基本用法
### 1. 获取查询系统
```typescript
import { Scene } from './ECS/Scene';
import { Entity } from './ECS/Entity';
// 创建场景,查询系统会自动创建
const scene = new Scene();
const querySystem = scene.querySystem;
// 或者从Core获取当前场景的查询系统
import { Core } from './Core';
const currentQuerySystem = Core.scene?.querySystem;
```
### 2. 基本查询操作
```typescript
// 查询包含所有指定组件的实体
const result = querySystem.queryAll(PositionComponent, VelocityComponent);
console.log(`找到 ${result.count} 个实体`);
// 查询包含任意指定组件的实体
const anyResult = querySystem.queryAny(HealthComponent, ManaComponent);
// 查询不包含指定组件的实体
const noneResult = querySystem.queryNone(DeadComponent);
```
### 3. 类型安全查询
```typescript
// 类型安全的查询,返回实体和对应的组件
const typedResult = querySystem.queryAllTyped(PositionComponent, VelocityComponent);
for (let i = 0; i < typedResult.entities.length; i++) {
const entity = typedResult.entities[i];
const [position, velocity] = typedResult.components[i];
// position 和 velocity 都是类型安全的
}
// 查询单个组件类型
const healthEntities = querySystem.queryComponentTyped(HealthComponent);
healthEntities.forEach(({ entity, component }) => {
console.log(`实体 ${entity.name} 的生命值: ${component.value}`);
});
// 查询两个组件类型
const movableEntities = querySystem.queryTwoComponents(PositionComponent, VelocityComponent);
movableEntities.forEach(({ entity, component1: position, component2: velocity }) => {
// 更新位置
position.x += velocity.x;
position.y += velocity.y;
});
```
### 4. 使用查询构建器
```typescript
// 创建复杂查询
const query = querySystem.createQuery()
.withAll(PositionComponent, RenderComponent)
.without(HiddenComponent)
.withTag(1) // 特定标签
.orderByName()
.limit(10);
const result = query.execute();
// 链式操作
const visibleEnemies = querySystem.createQuery()
.withAll(EnemyComponent, PositionComponent)
.without(DeadComponent, HiddenComponent)
.filter(entity => entity.name.startsWith('Boss'))
.orderBy((a, b) => a.id - b.id);
// 迭代结果
visibleEnemies.forEach((entity, index) => {
console.log(`敌人 ${index}: ${entity.name}`);
});
```
### 5. 高级查询功能
```typescript
// 复合查询
const complexResult = querySystem.queryComplex(
{
type: QueryConditionType.ALL,
componentTypes: [PositionComponent, VelocityComponent],
mask: /* 位掩码 */
},
{
type: QueryConditionType.NONE,
componentTypes: [DeadComponent],
mask: /* 位掩码 */
}
);
// 批量查询
const batchResults = querySystem.batchQuery([
{ type: QueryConditionType.ALL, componentTypes: [HealthComponent], mask: /* 位掩码 */ },
{ type: QueryConditionType.ALL, componentTypes: [ManaComponent], mask: /* 位掩码 */ }
]);
// 并行查询
const parallelResults = await querySystem.parallelQuery([
{ type: QueryConditionType.ALL, componentTypes: [PositionComponent], mask: /* 位掩码 */ },
{ type: QueryConditionType.ALL, componentTypes: [VelocityComponent], mask: /* 位掩码 */ }
]);
```
## 场景级别的实体查询
除了使用QuerySystem您还可以直接使用Scene提供的便捷查询方法
### 基本场景查询
```typescript
// 按名称查找实体
const player = scene.findEntity("Player");
const playerAlt = scene.getEntityByName("Player"); // 别名方法
// 按ID查找实体
const entity = scene.findEntityById(123);
// 按标签查找实体
const enemies = scene.findEntitiesByTag(2);
const enemiesAlt = scene.getEntitiesByTag(2); // 别名方法
// 获取所有实体
const allEntities = scene.entities.buffer;
```
## 性能优化
### 1. 缓存管理
```typescript
// 设置缓存配置
querySystem.setCacheConfig(200, 2000); // 最大200个缓存项2秒超时
// 清空缓存
querySystem.clearCache();
// 预热常用查询
const commonQueries = [
{ type: QueryConditionType.ALL, componentTypes: [PositionComponent], mask: /* 位掩码 */ },
{ type: QueryConditionType.ALL, componentTypes: [VelocityComponent], mask: /* 位掩码 */ }
];
querySystem.warmUpCache(commonQueries);
```
### 2. 索引优化
```typescript
// 自动优化索引配置
querySystem.optimizeIndexes();
// 获取性能统计
const stats = querySystem.getStats();
console.log(`缓存命中率: ${(stats.hitRate * 100).toFixed(1)}%`);
console.log(`实体数量: ${stats.entityCount}`);
// 获取详细性能报告
const report = querySystem.getPerformanceReport();
console.log(report);
```
### 3. 查询监听和快照
```typescript
// 监听查询结果变更
const unwatch = querySystem.watchQuery(
{ type: QueryConditionType.ALL, componentTypes: [EnemyComponent], mask: /* 位掩码 */ },
(entities, changeType) => {
console.log(`敌人实体${changeType}: ${entities.length}`);
}
);
// 取消监听
unwatch();
// 创建查询快照
const snapshot1 = querySystem.createSnapshot(
{ type: QueryConditionType.ALL, componentTypes: [PlayerComponent], mask: /* 位掩码 */ }
);
// 稍后创建另一个快照
const snapshot2 = querySystem.createSnapshot(
{ type: QueryConditionType.ALL, componentTypes: [PlayerComponent], mask: /* 位掩码 */ }
);
// 比较快照
const diff = querySystem.compareSnapshots(snapshot1, snapshot2);
console.log(`新增: ${diff.added.length}, 移除: ${diff.removed.length}`);
```
## 实际使用示例
### 移动系统示例
```typescript
import { EntitySystem } from './ECS/Systems/EntitySystem';
class MovementSystem extends EntitySystem {
public update(): void {
// 查询所有可移动的实体
const movableEntities = this.scene.querySystem.queryTwoComponents(
PositionComponent,
VelocityComponent
);
movableEntities.forEach(({ entity, component1: position, component2: velocity }) => {
// 更新位置
position.x += velocity.x * Time.deltaTime;
position.y += velocity.y * Time.deltaTime;
// 边界检查
if (position.x < 0 || position.x > 800) {
velocity.x = -velocity.x;
}
if (position.y < 0 || position.y > 600) {
velocity.y = -velocity.y;
}
});
}
}
```
### 碰撞检测示例
```typescript
class CollisionSystem extends EntitySystem {
public update(): void {
// 获取所有具有碰撞器的实体
const collidableEntities = this.scene.querySystem.queryTwoComponents(
PositionComponent,
ColliderComponent
);
// 检测碰撞
for (let i = 0; i < collidableEntities.length; i++) {
for (let j = i + 1; j < collidableEntities.length; j++) {
const entityA = collidableEntities[i];
const entityB = collidableEntities[j];
if (this.checkCollision(entityA, entityB)) {
this.handleCollision(entityA.entity, entityB.entity);
}
}
}
}
private checkCollision(entityA: any, entityB: any): boolean {
// 简单的距离检测
const posA = entityA.component1;
const posB = entityB.component1;
const distance = Math.sqrt(
Math.pow(posA.x - posB.x, 2) + Math.pow(posA.y - posB.y, 2)
);
return distance < 50;
}
private handleCollision(entityA: Entity, entityB: Entity): void {
console.log(`碰撞检测: ${entityA.name}${entityB.name}`);
}
}
```
### 生命值管理示例
```typescript
class HealthSystem extends EntitySystem {
public update(): void {
// 查询所有具有生命值的实体
const healthEntities = this.scene.querySystem.queryComponentTyped(HealthComponent);
const deadEntities: Entity[] = [];
healthEntities.forEach(({ entity, component: health }) => {
// 检查死亡
if (health.currentHealth <= 0) {
deadEntities.push(entity);
}
});
// 移除死亡实体
deadEntities.forEach(entity => {
console.log(`实体 ${entity.name} 已死亡`);
entity.destroy();
});
}
}
```
## 最佳实践
### 1. 查询优化
- 尽量使用类型安全的查询方法
- 避免在每帧都创建新的查询
- 合理使用缓存机制
### 2. 性能监控
- 定期检查查询性能报告
- 监控缓存命中率
- 优化频繁使用的查询
### 3. 内存管理
- 及时清理不需要的查询监听器
- 合理设置缓存大小
- 避免创建过多的查询快照
### 4. 代码组织
- 将复杂查询封装到专门的方法中
- 使用查询构建器创建可读性更好的查询
- 在系统中合理组织查询逻辑

View File

@@ -0,0 +1,483 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ECS Framework Core Demos</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: #0f0f23;
color: #e0e0e0;
overflow: hidden;
height: 100vh;
}
.app-container {
display: flex;
height: 100vh;
}
/* 侧边栏 */
.sidebar {
width: 280px;
background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%);
border-right: 2px solid #0a3d62;
display: flex;
flex-direction: column;
box-shadow: 4px 0 20px rgba(0,0,0,0.5);
}
.sidebar-header {
padding: 30px 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
text-align: center;
}
.sidebar-header h1 {
font-size: 1.5em;
color: white;
margin-bottom: 8px;
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
.sidebar-header p {
font-size: 0.85em;
color: rgba(255,255,255,0.9);
font-weight: 300;
}
.demo-list {
flex: 1;
overflow-y: auto;
padding: 15px 0;
}
.demo-category {
margin-bottom: 10px;
}
.category-title {
padding: 12px 20px;
color: #8892b0;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
}
.demo-item {
padding: 14px 20px;
cursor: pointer;
transition: all 0.2s ease;
border-left: 3px solid transparent;
display: flex;
align-items: center;
gap: 12px;
}
.demo-item:hover {
background: rgba(102, 126, 234, 0.1);
border-left-color: #667eea;
}
.demo-item.active {
background: rgba(102, 126, 234, 0.2);
border-left-color: #667eea;
}
.demo-icon {
font-size: 20px;
width: 24px;
text-align: center;
}
.demo-info {
flex: 1;
}
.demo-name {
font-size: 14px;
font-weight: 500;
color: #ccd6f6;
margin-bottom: 2px;
}
.demo-item.active .demo-name {
color: #667eea;
font-weight: 600;
}
.demo-desc {
font-size: 11px;
color: #8892b0;
line-height: 1.3;
}
.sidebar-footer {
padding: 20px;
border-top: 1px solid rgba(255,255,255,0.1);
text-align: center;
}
.github-link {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
background: rgba(102, 126, 234, 0.2);
color: #667eea;
text-decoration: none;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
transition: all 0.2s;
}
.github-link:hover {
background: rgba(102, 126, 234, 0.3);
transform: translateY(-2px);
}
/* 主内容区 */
.main-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.content-header {
padding: 25px 40px;
background: #1a1a2e;
border-bottom: 2px solid #0a3d62;
}
.content-header h2 {
font-size: 2em;
color: #ccd6f6;
margin-bottom: 8px;
}
.content-header p {
color: #8892b0;
font-size: 14px;
line-height: 1.6;
}
.demo-canvas-container {
flex: 1;
position: relative;
overflow: hidden;
background: #0a0a15;
}
#demoCanvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
/* 控制面板 */
.control-panel {
position: absolute;
top: 20px;
right: 20px;
width: 320px;
background: rgba(26, 26, 46, 0.95);
border-radius: 12px;
border: 1px solid rgba(102, 126, 234, 0.3);
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
backdrop-filter: blur(10px);
max-height: calc(100% - 40px);
overflow-y: auto;
}
.control-panel-header {
padding: 15px 20px;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.2) 0%, rgba(118, 75, 162, 0.2) 100%);
border-bottom: 1px solid rgba(102, 126, 234, 0.3);
border-radius: 12px 12px 0 0;
}
.control-panel-header h3 {
color: #667eea;
font-size: 16px;
font-weight: 600;
}
.control-panel-content {
padding: 20px;
}
.control-section {
margin-bottom: 20px;
}
.control-section:last-child {
margin-bottom: 0;
}
.control-section h4 {
color: #8892b0;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 12px;
font-weight: 600;
}
.button-group {
display: flex;
flex-direction: column;
gap: 8px;
}
button {
padding: 10px 16px;
border: none;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
button:active {
transform: translateY(0);
}
button.secondary {
background: rgba(102, 126, 234, 0.2);
color: #667eea;
}
button.secondary:hover {
background: rgba(102, 126, 234, 0.3);
}
button.danger {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
button.success {
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
}
.input-group {
margin-bottom: 12px;
}
.input-group label {
display: block;
color: #8892b0;
font-size: 12px;
margin-bottom: 6px;
font-weight: 500;
}
.input-group input {
width: 100%;
padding: 8px 12px;
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 6px;
color: #ccd6f6;
font-size: 13px;
transition: all 0.2s;
}
.input-group input:focus {
outline: none;
border-color: #667eea;
background: rgba(255,255,255,0.08);
}
/* 统计信息 */
.stats-panel {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
margin-top: 15px;
}
.stat-item {
background: rgba(255,255,255,0.03);
padding: 12px;
border-radius: 6px;
border: 1px solid rgba(255,255,255,0.05);
}
.stat-label {
color: #8892b0;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 4px;
}
.stat-value {
color: #667eea;
font-size: 20px;
font-weight: 700;
}
/* Toast通知 */
.toast {
position: fixed;
bottom: 30px;
right: 30px;
background: rgba(26, 26, 46, 0.98);
border: 1px solid #667eea;
border-radius: 8px;
padding: 15px 20px;
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
transform: translateY(150%);
transition: transform 0.3s ease;
z-index: 1000;
min-width: 280px;
}
.toast.show {
transform: translateY(0);
}
.toast-content {
display: flex;
align-items: center;
gap: 12px;
}
.toast-icon {
font-size: 20px;
}
.toast-message {
color: #ccd6f6;
font-size: 14px;
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: rgba(255,255,255,0.05);
}
::-webkit-scrollbar-thumb {
background: rgba(102, 126, 234, 0.5);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(102, 126, 234, 0.7);
}
/* 加载动画 */
.loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
}
.loading-spinner {
width: 50px;
height: 50px;
border: 4px solid rgba(102, 126, 234, 0.2);
border-top-color: #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-text {
margin-top: 15px;
color: #8892b0;
font-size: 14px;
}
</style>
</head>
<body>
<div class="app-container">
<!-- 侧边栏 -->
<div class="sidebar">
<div class="sidebar-header">
<h1>🎮 ECS Core Demos</h1>
<p>交互式演示集合</p>
</div>
<div class="demo-list" id="demoList">
<!-- Demo列表将通过JS动态生成 -->
</div>
<div class="sidebar-footer">
<a href="https://github.com/esengine/ecs-framework" target="_blank" class="github-link">
<span></span>
<span>GitHub</span>
</a>
</div>
</div>
<!-- 主内容区 -->
<div class="main-content">
<div class="content-header">
<h2 id="demoTitle">选择一个演示开始</h2>
<p id="demoDescription">从左侧菜单选择一个演示查看效果</p>
</div>
<div class="demo-canvas-container">
<canvas id="demoCanvas"></canvas>
<!-- 控制面板 -->
<div class="control-panel" id="controlPanel" style="display: none;">
<div class="control-panel-header">
<h3>控制面板</h3>
</div>
<div class="control-panel-content" id="controlPanelContent">
<!-- 控制内容将由各个demo动态生成 -->
</div>
</div>
<!-- 加载动画 -->
<div class="loading" id="loading" style="display: none;">
<div class="loading-spinner"></div>
<div class="loading-text">加载中...</div>
</div>
</div>
</div>
</div>
<!-- Toast通知 -->
<div class="toast" id="toast">
<div class="toast-content">
<span class="toast-icon"></span>
<span class="toast-message" id="toastMessage"></span>
</div>
</div>
<script type="module" src="src/main.ts"></script>
</body>
</html>

630
examples/core-demos/package-lock.json generated Normal file
View File

@@ -0,0 +1,630 @@
{
"name": "ecs-core-demos",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ecs-core-demos",
"version": "1.0.0",
"dependencies": {
"@esengine/ecs-framework": "file:../../packages/core"
},
"devDependencies": {
"typescript": "^5.0.0",
"vite": "^4.0.0"
}
},
"../../packages/core": {
"name": "@esengine/ecs-framework",
"version": "2.1.51",
"license": "MIT",
"dependencies": {
"msgpack-lite": "^0.1.26"
},
"devDependencies": {
"@babel/core": "^7.28.3",
"@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1",
"@babel/plugin-transform-optional-chaining": "^7.27.1",
"@babel/preset-env": "^7.28.3",
"@rollup/plugin-babel": "^6.0.4",
"@rollup/plugin-commonjs": "^28.0.3",
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-terser": "^0.4.4",
"@types/jest": "^29.5.14",
"@types/msgpack-lite": "^0.1.11",
"@types/node": "^20.19.17",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"rimraf": "^5.0.0",
"rollup": "^4.42.0",
"rollup-plugin-dts": "^6.2.1",
"ts-jest": "^29.4.0",
"typescript": "^5.8.3"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
"integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
"integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
"integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
"integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
"integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
"integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
"integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
"integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
"integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
"integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
"integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
"integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
"integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
"integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
"integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
"integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
"integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
"integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
"integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
"integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
"integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
"integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esengine/ecs-framework": {
"resolved": "../../packages/core",
"link": true
},
"node_modules/esbuild": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
"integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/android-arm": "0.18.20",
"@esbuild/android-arm64": "0.18.20",
"@esbuild/android-x64": "0.18.20",
"@esbuild/darwin-arm64": "0.18.20",
"@esbuild/darwin-x64": "0.18.20",
"@esbuild/freebsd-arm64": "0.18.20",
"@esbuild/freebsd-x64": "0.18.20",
"@esbuild/linux-arm": "0.18.20",
"@esbuild/linux-arm64": "0.18.20",
"@esbuild/linux-ia32": "0.18.20",
"@esbuild/linux-loong64": "0.18.20",
"@esbuild/linux-mips64el": "0.18.20",
"@esbuild/linux-ppc64": "0.18.20",
"@esbuild/linux-riscv64": "0.18.20",
"@esbuild/linux-s390x": "0.18.20",
"@esbuild/linux-x64": "0.18.20",
"@esbuild/netbsd-x64": "0.18.20",
"@esbuild/openbsd-x64": "0.18.20",
"@esbuild/sunos-x64": "0.18.20",
"@esbuild/win32-arm64": "0.18.20",
"@esbuild/win32-ia32": "0.18.20",
"@esbuild/win32-x64": "0.18.20"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/rollup": {
"version": "3.29.5",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz",
"integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==",
"dev": true,
"license": "MIT",
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=14.18.0",
"npm": ">=8.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/vite": {
"version": "4.5.14",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.14.tgz",
"integrity": "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.18.10",
"postcss": "^8.4.27",
"rollup": "^3.27.1"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
},
"peerDependencies": {
"@types/node": ">= 14",
"less": "*",
"lightningcss": "^1.21.0",
"sass": "*",
"stylus": "*",
"sugarss": "*",
"terser": "^5.4.0"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"less": {
"optional": true
},
"lightningcss": {
"optional": true
},
"sass": {
"optional": true
},
"stylus": {
"optional": true
},
"sugarss": {
"optional": true
},
"terser": {
"optional": true
}
}
}
}
}

View File

@@ -0,0 +1,18 @@
{
"name": "ecs-core-demos",
"version": "1.0.0",
"description": "ECS Framework Core Demos - Multiple Interactive Examples",
"main": "src/main.ts",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"typescript": "^5.0.0",
"vite": "^4.0.0"
},
"dependencies": {
"@esengine/ecs-framework": "file:../../packages/core"
}
}

View File

@@ -0,0 +1,99 @@
import { Scene, Core } from '@esengine/ecs-framework';
export interface DemoInfo {
id: string;
name: string;
description: string;
category: string;
icon: string;
}
export abstract class DemoBase {
protected scene: Scene;
protected canvas: HTMLCanvasElement;
protected ctx: CanvasRenderingContext2D;
protected controlPanel: HTMLElement;
protected isRunning: boolean = false;
protected animationFrameId: number | null = null;
protected lastTime: number = 0;
constructor(canvas: HTMLCanvasElement, controlPanel: HTMLElement) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d')!;
this.controlPanel = controlPanel;
this.scene = new Scene({ name: this.getInfo().name });
// 设置canvas大小
this.resizeCanvas();
window.addEventListener('resize', () => this.resizeCanvas());
}
abstract getInfo(): DemoInfo;
abstract setup(): void;
abstract createControls(): void;
protected resizeCanvas() {
const rect = this.canvas.getBoundingClientRect();
this.canvas.width = rect.width;
this.canvas.height = rect.height;
}
public start() {
if (this.isRunning) return;
this.isRunning = true;
this.lastTime = performance.now();
// 设置当前场景到Core
Core.setScene(this.scene);
this.scene.begin();
this.loop();
}
public stop() {
this.isRunning = false;
if (this.animationFrameId !== null) {
cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = null;
}
}
public destroy() {
this.stop();
this.scene.end();
}
protected loop = () => {
if (!this.isRunning) return;
// 计算deltaTime
const currentTime = performance.now();
const deltaTime = (currentTime - this.lastTime) / 1000; // 转换为秒
this.lastTime = currentTime;
// 更新ECS框架
Core.update(deltaTime);
// 渲染
this.render();
// 继续循环
this.animationFrameId = requestAnimationFrame(this.loop);
}
protected abstract render(): void;
protected showToast(message: string, icon: string = '✅') {
const toast = document.getElementById('toast')!;
const toastMessage = document.getElementById('toastMessage')!;
const toastIcon = toast.querySelector('.toast-icon')!;
toastIcon.textContent = icon;
toastMessage.textContent = message;
toast.classList.add('show');
setTimeout(() => {
toast.classList.remove('show');
}, 3000);
}
}

View File

@@ -0,0 +1,468 @@
import { DemoBase, DemoInfo } from './DemoBase';
import {
Component,
ECSComponent,
Entity,
EntitySystem,
Matcher,
Serializable,
Serialize,
IncrementalSerializer
} from '@esengine/ecs-framework';
// ===== 组件定义 =====
@ECSComponent('IncDemo_Position')
@Serializable({ version: 1, typeId: 'IncDemo_Position' })
class PositionComponent extends Component {
@Serialize() x: number = 0;
@Serialize() y: number = 0;
constructor(x: number = 0, y: number = 0) {
super();
this.x = x;
this.y = y;
}
}
@ECSComponent('IncDemo_Velocity')
@Serializable({ version: 1, typeId: 'IncDemo_Velocity' })
class VelocityComponent extends Component {
@Serialize() vx: number = 0;
@Serialize() vy: number = 0;
constructor(vx: number = 0, vy: number = 0) {
super();
this.vx = vx;
this.vy = vy;
}
}
@ECSComponent('IncDemo_Renderable')
@Serializable({ version: 1, typeId: 'IncDemo_Renderable' })
class RenderableComponent extends Component {
@Serialize() color: string = '#ffffff';
@Serialize() radius: number = 10;
constructor(color: string = '#ffffff', radius: number = 10) {
super();
this.color = color;
this.radius = radius;
}
}
// ===== 系统定义 =====
class MovementSystem extends EntitySystem {
constructor() {
super(Matcher.all(PositionComponent, VelocityComponent));
}
protected override process(entities: readonly Entity[]): void {
for (const entity of entities) {
const [pos, vel] = this.getComponents(entity, PositionComponent, VelocityComponent);
pos.x += vel.vx;
pos.y += vel.vy;
if (pos.x < 0 || pos.x > 1200) vel.vx *= -1;
if (pos.y < 0 || pos.y > 600) vel.vy *= -1;
}
}
}
class RenderSystem extends EntitySystem {
private canvas: HTMLCanvasElement;
private ctx: CanvasRenderingContext2D;
constructor(canvas: HTMLCanvasElement) {
super(Matcher.all(PositionComponent, RenderableComponent));
this.canvas = canvas;
const ctx = canvas.getContext('2d');
if (!ctx) throw new Error('Failed to get canvas context');
this.ctx = ctx;
}
protected override process(entities: readonly Entity[]): void {
this.ctx.fillStyle = '#0a0a15';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
for (const entity of entities) {
const [pos, render] = this.getComponents(entity, PositionComponent, RenderableComponent);
this.ctx.fillStyle = render.color;
this.ctx.beginPath();
this.ctx.arc(pos.x, pos.y, render.radius, 0, Math.PI * 2);
this.ctx.fill();
this.ctx.fillStyle = 'white';
this.ctx.font = '10px Arial';
this.ctx.textAlign = 'center';
this.ctx.fillText(entity.name, pos.x, pos.y - render.radius - 5);
}
}
}
export class IncrementalSerializationDemo extends DemoBase {
private renderSystem!: RenderSystem;
private incrementalHistory: any[] = [];
private autoSnapshotInterval: number | null = null;
getInfo(): DemoInfo {
return {
id: 'incremental-serialization',
name: '增量序列化',
description: '演示增量序列化功能,只保存场景变更而非完整状态,适用于网络同步和回放系统',
category: '核心功能',
icon: '🔄'
};
}
setup() {
// 创建控制面板
this.createControls();
// 添加系统
this.renderSystem = new RenderSystem(this.canvas);
this.scene.addEntityProcessor(new MovementSystem());
this.scene.addEntityProcessor(this.renderSystem);
// 创建初始实体
this.createInitialEntities();
// 创建基础快照
this.scene.createIncrementalSnapshot();
this.addToHistory('Initial State');
}
private createInitialEntities() {
// 创建玩家
const player = this.scene.createEntity('Player');
player.addComponent(new PositionComponent(600, 300));
player.addComponent(new VelocityComponent(2, 1.5));
player.addComponent(new RenderableComponent('#4a9eff', 15));
// 设置场景数据
this.scene.sceneData.set('gameTime', 0);
this.scene.sceneData.set('score', 0);
}
private createRandomEntity() {
const entity = this.scene.createEntity(`Entity_${Date.now()}`);
entity.addComponent(new PositionComponent(
Math.random() * this.canvas.width,
Math.random() * this.canvas.height
));
entity.addComponent(new VelocityComponent(
(Math.random() - 0.5) * 3,
(Math.random() - 0.5) * 3
));
const colors = ['#ff6b6b', '#4ecdc4', '#ffe66d', '#a8dadc', '#f1faee'];
entity.addComponent(new RenderableComponent(
colors[Math.floor(Math.random() * colors.length)],
5 + Math.random() * 10
));
}
private addToHistory(label: string) {
const incremental = this.scene.serializeIncremental();
const stats = IncrementalSerializer.getIncrementalStats(incremental);
// 计算JSON和二进制格式的大小
const jsonSize = IncrementalSerializer.getIncrementalSize(incremental, 'json');
const binarySize = IncrementalSerializer.getIncrementalSize(incremental, 'binary');
this.incrementalHistory.push({
label,
incremental,
stats,
timestamp: Date.now(),
jsonSize,
binarySize
});
this.scene.updateIncrementalSnapshot();
this.updateHistoryPanel();
this.updateStats();
}
createControls() {
this.controlPanel.innerHTML = `
<div class="control-section">
<h4>实体控制</h4>
<div class="button-group">
<button id="addEntity" class="secondary">添加随机实体</button>
<button id="removeEntity" class="danger">删除最后一个实体</button>
<button id="modifyEntity" class="secondary">修改实体数据</button>
</div>
</div>
<div class="control-section">
<h4>增量快照</h4>
<div class="button-group">
<button id="captureSnapshot" class="success">捕获当前状态</button>
<button id="clearHistory" class="danger">清空历史</button>
</div>
<div style="margin-top: 10px;">
<label>
<input type="checkbox" id="autoSnapshot">
自动快照每2秒
</label>
</div>
</div>
<div class="control-section">
<h4>场景数据控制</h4>
<div class="input-group">
<label>游戏时间</label>
<input type="number" id="gameTime" value="0" step="1">
</div>
<div class="input-group">
<label>分数</label>
<input type="number" id="score" value="0" step="10">
</div>
<button id="updateSceneData" class="secondary">更新场景数据</button>
</div>
<div class="stats-panel">
<div class="stat-item">
<div class="stat-label">实体数量</div>
<div class="stat-value" id="entityCount">0</div>
</div>
<div class="stat-item">
<div class="stat-label">历史记录</div>
<div class="stat-value" id="historyCount">0</div>
</div>
<div class="stat-item">
<div class="stat-label">JSON大小</div>
<div class="stat-value" id="jsonSize">0B</div>
</div>
<div class="stat-item">
<div class="stat-label">二进制大小</div>
<div class="stat-value" id="binarySize">0B</div>
</div>
<div class="stat-item">
<div class="stat-label">压缩率</div>
<div class="stat-value" id="compressionRatio">0%</div>
</div>
<div class="stat-item">
<div class="stat-label">总变更数</div>
<div class="stat-value" id="totalChanges">0</div>
</div>
</div>
<div class="control-section">
<h4>增量历史 <small style="color: #8892b0;">(点击快照查看详情)</small></h4>
<div style="max-height: 300px; overflow-y: auto; background: rgba(0,0,0,0.3); padding: 10px; border-radius: 6px;" id="historyPanel">
暂无历史记录
</div>
</div>
<div class="control-section">
<h4>快照详情</h4>
<div style="max-height: 200px; overflow-y: auto; background: rgba(0,0,0,0.3); padding: 10px; border-radius: 6px; font-family: monospace; font-size: 11px; color: #8892b0;" id="snapshotDetails">
点击历史记录查看详情...
</div>
</div>
`;
this.bindEvents();
this.updateStats();
}
private bindEvents() {
document.getElementById('addEntity')!.addEventListener('click', () => {
this.createRandomEntity();
this.addToHistory('添加实体');
this.showToast('添加了一个随机实体');
});
document.getElementById('removeEntity')!.addEventListener('click', () => {
const entities = this.scene.entities.buffer;
if (entities.length > 1) {
const lastEntity = entities[entities.length - 1];
lastEntity.destroy();
this.addToHistory('删除实体');
this.showToast('删除了最后一个实体');
} else {
this.showToast('至少保留一个实体', '⚠️');
}
});
document.getElementById('modifyEntity')!.addEventListener('click', () => {
const entities = this.scene.entities.buffer;
if (entities.length > 0) {
const randomEntity = entities[Math.floor(Math.random() * entities.length)];
const pos = randomEntity.getComponent(PositionComponent);
if (pos) {
pos.x = Math.random() * this.canvas.width;
pos.y = Math.random() * this.canvas.height;
}
this.addToHistory('修改实体位置');
this.showToast(`修改了 ${randomEntity.name} 的位置`);
}
});
document.getElementById('captureSnapshot')!.addEventListener('click', () => {
this.addToHistory('手动快照');
this.showToast('已捕获当前状态', '📸');
});
document.getElementById('clearHistory')!.addEventListener('click', () => {
this.incrementalHistory = [];
this.scene.createIncrementalSnapshot();
this.addToHistory('清空后重新开始');
this.showToast('历史记录已清空');
});
document.getElementById('autoSnapshot')!.addEventListener('change', (e) => {
const checkbox = e.target as HTMLInputElement;
if (checkbox.checked) {
this.autoSnapshotInterval = window.setInterval(() => {
this.addToHistory('自动快照');
}, 2000);
this.showToast('自动快照已启用', '⏱️');
} else {
if (this.autoSnapshotInterval !== null) {
clearInterval(this.autoSnapshotInterval);
this.autoSnapshotInterval = null;
}
this.showToast('自动快照已禁用');
}
});
document.getElementById('updateSceneData')!.addEventListener('click', () => {
const gameTime = parseInt((document.getElementById('gameTime') as HTMLInputElement).value);
const score = parseInt((document.getElementById('score') as HTMLInputElement).value);
this.scene.sceneData.set('gameTime', gameTime);
this.scene.sceneData.set('score', score);
this.addToHistory('更新场景数据');
this.showToast('场景数据已更新');
});
}
private updateHistoryPanel() {
const panel = document.getElementById('historyPanel')!;
if (this.incrementalHistory.length === 0) {
panel.innerHTML = '暂无历史记录';
return;
}
panel.innerHTML = this.incrementalHistory.map((item, index) => {
const isLatest = index === this.incrementalHistory.length - 1;
const time = new Date(item.timestamp).toLocaleTimeString();
return `
<div class="history-item" data-index="${index}" style="
padding: 8px;
margin: 4px 0;
background: ${isLatest ? 'rgba(74, 158, 255, 0.2)' : 'rgba(136, 146, 176, 0.1)'};
border-left: 3px solid ${isLatest ? '#4a9eff' : '#8892b0'};
border-radius: 4px;
cursor: pointer;
transition: background 0.2s;
">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<strong>${item.label}</strong>
${isLatest ? '<span style="color: #4a9eff; margin-left: 8px;">●</span>' : ''}
</div>
<small style="color: #8892b0;">${time}</small>
</div>
<div style="font-size: 11px; color: #8892b0; margin-top: 4px;">
实体: +${item.stats.addedEntities} -${item.stats.removedEntities} ~${item.stats.updatedEntities} |
组件: +${item.stats.addedComponents} -${item.stats.removedComponents} ~${item.stats.updatedComponents}
</div>
<div style="font-size: 11px; color: #8892b0; margin-top: 2px;">
JSON: ${this.formatBytes(item.jsonSize)} |
Binary: ${this.formatBytes(item.binarySize)} |
<span style="color: #4ecdc4;">节省: ${((1 - item.binarySize / item.jsonSize) * 100).toFixed(1)}%</span>
</div>
</div>
`;
}).join('');
// 绑定点击事件
panel.querySelectorAll('.history-item').forEach(item => {
item.addEventListener('click', () => {
const index = parseInt(item.getAttribute('data-index')!);
this.showSnapshotDetails(index);
});
});
// 自动滚动到底部
panel.scrollTop = panel.scrollHeight;
}
private showSnapshotDetails(index: number) {
const item = this.incrementalHistory[index];
const detailsPanel = document.getElementById('snapshotDetails')!;
const compressionRatio = ((1 - item.binarySize / item.jsonSize) * 100).toFixed(1);
const details = {
版本: item.incremental.version,
基础版本: item.incremental.baseVersion,
时间戳: new Date(item.incremental.timestamp).toLocaleString(),
场景名称: item.incremental.sceneName,
: {
JSON大小: this.formatBytes(item.jsonSize),
二进制大小: this.formatBytes(item.binarySize),
: `${compressionRatio}%`,
节省字节: this.formatBytes(item.jsonSize - item.binarySize)
},
统计: item.stats,
实体变更: item.incremental.entityChanges.map((c: any) => ({
操作: c.operation,
实体ID: c.entityId,
实体名称: c.entityName
})),
组件变更: item.incremental.componentChanges.map((c: any) => ({
操作: c.operation,
实体ID: c.entityId,
组件类型: c.componentType
})),
场景数据变更: item.incremental.sceneDataChanges.map((c: any) => ({
: c.key,
: c.value,
已删除: c.deleted
}))
};
detailsPanel.textContent = JSON.stringify(details, null, 2);
}
private updateStats() {
document.getElementById('entityCount')!.textContent = this.scene.entities.count.toString();
document.getElementById('historyCount')!.textContent = this.incrementalHistory.length.toString();
if (this.incrementalHistory.length > 0) {
const lastItem = this.incrementalHistory[this.incrementalHistory.length - 1];
document.getElementById('jsonSize')!.textContent = this.formatBytes(lastItem.jsonSize);
document.getElementById('binarySize')!.textContent = this.formatBytes(lastItem.binarySize);
const compressionRatio = ((1 - lastItem.binarySize / lastItem.jsonSize) * 100).toFixed(1);
const ratioElement = document.getElementById('compressionRatio')!;
ratioElement.textContent = `${compressionRatio}%`;
ratioElement.style.color = parseFloat(compressionRatio) > 30 ? '#4ecdc4' : '#ffe66d';
document.getElementById('totalChanges')!.textContent = lastItem.stats.totalChanges.toString();
}
}
private formatBytes(bytes: number): string {
if (bytes < 1024) return `${bytes}B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
}
protected render() {
// RenderSystem会处理渲染
}
public destroy() {
if (this.autoSnapshotInterval !== null) {
clearInterval(this.autoSnapshotInterval);
}
super.destroy();
}
}

View File

@@ -0,0 +1,386 @@
import { DemoBase, DemoInfo } from './DemoBase';
import {
Component,
ECSComponent,
Entity,
EntitySystem,
Matcher,
Serializable,
Serialize,
SerializeAsMap
} from '@esengine/ecs-framework';
// ===== 组件定义 =====
@ECSComponent('SerDemo_Position')
@Serializable({ version: 1, typeId: 'SerDemo_Position' })
class PositionComponent extends Component {
@Serialize() x: number = 0;
@Serialize() y: number = 0;
constructor(x: number = 0, y: number = 0) {
super();
this.x = x;
this.y = y;
}
}
@ECSComponent('SerDemo_Velocity')
@Serializable({ version: 1, typeId: 'SerDemo_Velocity' })
class VelocityComponent extends Component {
@Serialize() vx: number = 0;
@Serialize() vy: number = 0;
constructor(vx: number = 0, vy: number = 0) {
super();
this.vx = vx;
this.vy = vy;
}
}
@ECSComponent('SerDemo_Renderable')
@Serializable({ version: 1, typeId: 'SerDemo_Renderable' })
class RenderableComponent extends Component {
@Serialize() color: string = '#ffffff';
@Serialize() radius: number = 10;
constructor(color: string = '#ffffff', radius: number = 10) {
super();
this.color = color;
this.radius = radius;
}
}
@ECSComponent('SerDemo_Player')
@Serializable({ version: 1, typeId: 'SerDemo_Player' })
class PlayerComponent extends Component {
@Serialize() name: string = 'Player';
@Serialize() level: number = 1;
@Serialize() health: number = 100;
@SerializeAsMap() inventory: Map<string, number> = new Map();
constructor(name: string = 'Player') {
super();
this.name = name;
}
}
// ===== 系统定义 =====
class MovementSystem extends EntitySystem {
constructor() {
super(Matcher.all(PositionComponent, VelocityComponent));
}
protected override process(entities: readonly Entity[]): void {
for (const entity of entities) {
const [pos, vel] = this.getComponents(entity, PositionComponent, VelocityComponent);
pos.x += vel.vx;
pos.y += vel.vy;
// 边界反弹
if (pos.x < 0 || pos.x > 1200) vel.vx *= -1;
if (pos.y < 0 || pos.y > 600) vel.vy *= -1;
}
}
}
class RenderSystem extends EntitySystem {
private canvas: HTMLCanvasElement;
private ctx: CanvasRenderingContext2D;
constructor(canvas: HTMLCanvasElement) {
super(Matcher.all(PositionComponent, RenderableComponent));
this.canvas = canvas;
this.ctx = canvas.getContext('2d')!;
}
protected override process(entities: readonly Entity[]): void {
// 清空画布
this.ctx.fillStyle = '#0a0a15';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
// 渲染所有实体
for (const entity of entities) {
const [pos, render] = this.getComponents(entity, PositionComponent, RenderableComponent);
this.ctx.fillStyle = render.color;
this.ctx.beginPath();
this.ctx.arc(pos.x, pos.y, render.radius, 0, Math.PI * 2);
this.ctx.fill();
// 如果是玩家,显示名字
const player = entity.getComponent(PlayerComponent);
if (player) {
this.ctx.fillStyle = 'white';
this.ctx.font = '12px Arial';
this.ctx.textAlign = 'center';
this.ctx.fillText(player.name, pos.x, pos.y - render.radius - 5);
}
}
}
}
export class SerializationDemo extends DemoBase {
private renderSystem!: RenderSystem;
private jsonData: string = '';
private binaryData: Buffer | null = null;
getInfo(): DemoInfo {
return {
id: 'serialization',
name: '场景序列化',
description: '演示场景的序列化和反序列化功能支持JSON和二进制格式',
category: '核心功能',
icon: '💾'
};
}
setup() {
// @ECSComponent装饰器会自动注册组件到ComponentRegistry
// ComponentRegistry会被序列化系统自动使用无需手动注册
// 添加系统
this.renderSystem = new RenderSystem(this.canvas);
this.scene.addEntityProcessor(new MovementSystem());
this.scene.addEntityProcessor(this.renderSystem);
// 创建初始实体
this.createInitialEntities();
// 创建控制面板
this.createControls();
}
private createInitialEntities() {
// 创建玩家
const player = this.scene.createEntity('Player');
player.addComponent(new PositionComponent(600, 300));
player.addComponent(new VelocityComponent(2, 1.5));
player.addComponent(new RenderableComponent('#4a9eff', 15));
const playerComp = new PlayerComponent('Hero');
playerComp.level = 5;
playerComp.health = 100;
playerComp.inventory.set('sword', 1);
playerComp.inventory.set('potion', 5);
player.addComponent(playerComp);
// 创建一些随机实体
for (let i = 0; i < 5; i++) {
this.createRandomEntity();
}
// 设置场景数据
this.scene.sceneData.set('weather', 'sunny');
this.scene.sceneData.set('gameTime', 12.5);
this.scene.sceneData.set('difficulty', 'normal');
}
private createRandomEntity() {
const entity = this.scene.createEntity(`Entity_${Date.now()}`);
entity.addComponent(new PositionComponent(
Math.random() * this.canvas.width,
Math.random() * this.canvas.height
));
entity.addComponent(new VelocityComponent(
(Math.random() - 0.5) * 3,
(Math.random() - 0.5) * 3
));
const colors = ['#ff6b6b', '#4ecdc4', '#ffe66d', '#a8dadc', '#f1faee'];
entity.addComponent(new RenderableComponent(
colors[Math.floor(Math.random() * colors.length)],
5 + Math.random() * 10
));
}
createControls() {
this.controlPanel.innerHTML = `
<div class="control-section">
<h4>实体控制</h4>
<div class="button-group">
<button id="addEntity" class="secondary">添加随机实体</button>
<button id="clearEntities" class="danger">清空所有实体</button>
</div>
</div>
<div class="control-section">
<h4>序列化操作</h4>
<div class="button-group">
<button id="serializeJSON">序列化为JSON</button>
<button id="serializeBinary" class="success">序列化为二进制</button>
<button id="deserialize" class="secondary">反序列化恢复</button>
</div>
</div>
<div class="control-section">
<h4>本地存储</h4>
<div class="button-group">
<button id="saveLocal" class="success">保存到LocalStorage</button>
<button id="loadLocal" class="secondary">从LocalStorage加载</button>
</div>
</div>
<div class="control-section">
<h4>场景数据</h4>
<div class="input-group">
<label>天气</label>
<input type="text" id="weather" value="sunny" placeholder="sunny/rainy/snowy">
</div>
<div class="input-group">
<label>游戏时间</label>
<input type="number" id="gameTime" value="12.5" step="0.1" min="0" max="24">
</div>
<button id="updateSceneData" class="secondary">更新场景数据</button>
</div>
<div class="stats-panel">
<div class="stat-item">
<div class="stat-label">实体数量</div>
<div class="stat-value" id="entityCount">0</div>
</div>
<div class="stat-item">
<div class="stat-label">JSON大小</div>
<div class="stat-value" id="jsonSize">0B</div>
</div>
<div class="stat-item">
<div class="stat-label">二进制大小</div>
<div class="stat-value" id="binarySize">0B</div>
</div>
<div class="stat-item">
<div class="stat-label">压缩率</div>
<div class="stat-value" id="compressionRatio">0%</div>
</div>
</div>
<div class="control-section">
<h4>序列化数据预览</h4>
<div style="max-height: 200px; overflow-y: auto; background: rgba(0,0,0,0.3); padding: 10px; border-radius: 6px; font-family: monospace; font-size: 11px; color: #8892b0; word-break: break-all;" id="dataPreview">
点击序列化按钮查看数据...
</div>
</div>
`;
// 绑定事件
this.bindEvents();
}
private bindEvents() {
document.getElementById('addEntity')!.addEventListener('click', () => {
this.createRandomEntity();
this.updateStats();
this.showToast('添加了一个随机实体');
});
document.getElementById('clearEntities')!.addEventListener('click', () => {
this.scene.destroyAllEntities();
this.createInitialEntities();
this.updateStats();
this.showToast('场景已重置');
});
document.getElementById('serializeJSON')!.addEventListener('click', () => {
this.jsonData = this.scene.serialize({ format: 'json', pretty: true }) as string;
this.updateDataPreview(this.jsonData, 'json');
this.updateStats();
this.showToast('已序列化为JSON格式');
});
document.getElementById('serializeBinary')!.addEventListener('click', () => {
this.binaryData = this.scene.serialize({ format: 'binary' }) as Buffer;
const base64 = this.binaryData.toString('base64');
this.updateDataPreview(`Binary Data (Base64):\n${base64.substring(0, 500)}...`, 'binary');
this.updateStats();
this.showToast('已序列化为二进制格式', '🔐');
});
document.getElementById('deserialize')!.addEventListener('click', () => {
const data = this.binaryData || this.jsonData;
if (!data) {
this.showToast('请先执行序列化操作', '⚠️');
return;
}
this.scene.deserialize(data, {
strategy: 'replace'
// componentRegistry会自动从ComponentRegistry获取无需手动传入
});
this.updateStats();
this.showToast('场景已恢复');
});
document.getElementById('saveLocal')!.addEventListener('click', () => {
const jsonData = this.scene.serialize({ format: 'json' }) as string;
localStorage.setItem('ecs_demo_scene', jsonData);
this.showToast('已保存到LocalStorage', '💾');
});
document.getElementById('loadLocal')!.addEventListener('click', () => {
const data = localStorage.getItem('ecs_demo_scene');
if (!data) {
this.showToast('LocalStorage中没有保存的场景', '⚠️');
return;
}
this.scene.deserialize(data, {
strategy: 'replace'
// componentRegistry会自动从ComponentRegistry获取无需手动传入
});
this.updateStats();
this.showToast('已从LocalStorage加载', '📂');
});
document.getElementById('updateSceneData')!.addEventListener('click', () => {
const weather = (document.getElementById('weather') as HTMLInputElement).value;
const gameTime = parseFloat((document.getElementById('gameTime') as HTMLInputElement).value);
this.scene.sceneData.set('weather', weather);
this.scene.sceneData.set('gameTime', gameTime);
this.showToast('场景数据已更新');
});
// 初始更新统计
this.updateStats();
}
private updateDataPreview(data: string, format: string) {
const preview = document.getElementById('dataPreview')!;
if (format === 'json') {
const truncated = data.length > 1000 ? data.substring(0, 1000) + '\n...(truncated)' : data;
preview.textContent = truncated;
} else {
preview.textContent = data;
}
}
private updateStats() {
const entityCount = this.scene.entities.count;
document.getElementById('entityCount')!.textContent = entityCount.toString();
// 计算JSON大小
if (this.jsonData) {
const jsonSize = new Blob([this.jsonData]).size;
document.getElementById('jsonSize')!.textContent = this.formatBytes(jsonSize);
}
// 计算二进制大小
if (this.binaryData) {
const binarySize = this.binaryData.length;
document.getElementById('binarySize')!.textContent = this.formatBytes(binarySize);
// 计算压缩率
if (this.jsonData) {
const jsonSize = new Blob([this.jsonData]).size;
const ratio = ((1 - binarySize / jsonSize) * 100).toFixed(1);
document.getElementById('compressionRatio')!.textContent = `${ratio}%`;
}
}
}
private formatBytes(bytes: number): string {
if (bytes < 1024) return `${bytes}B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
}
protected render() {
// RenderSystem会处理渲染
}
}

View File

@@ -0,0 +1,832 @@
import { DemoBase, DemoInfo } from './DemoBase';
import { Component, ECSComponent, WorkerEntitySystem, EntitySystem, Matcher, Entity, ECSSystem, PlatformManager, Time } from '@esengine/ecs-framework';
import { BrowserAdapter } from '../platform/BrowserAdapter';
// ============ 组件定义 ============
@ECSComponent('WorkerDemo_Position')
class Position extends Component {
x: number = 0;
y: number = 0;
constructor(x: number = 0, y: number = 0) {
super();
this.x = x;
this.y = y;
}
set(x: number, y: number): void {
this.x = x;
this.y = y;
}
}
@ECSComponent('WorkerDemo_Velocity')
class Velocity extends Component {
dx: number = 0;
dy: number = 0;
constructor(dx: number = 0, dy: number = 0) {
super();
this.dx = dx;
this.dy = dy;
}
set(dx: number, dy: number): void {
this.dx = dx;
this.dy = dy;
}
}
@ECSComponent('WorkerDemo_Physics')
class Physics extends Component {
mass: number = 1;
bounce: number = 0.8;
friction: number = 0.95;
constructor(mass: number = 1, bounce: number = 0.8, friction: number = 0.95) {
super();
this.mass = mass;
this.bounce = bounce;
this.friction = friction;
}
}
@ECSComponent('WorkerDemo_Renderable')
class Renderable extends Component {
color: string = '#ffffff';
size: number = 5;
shape: 'circle' | 'square' = 'circle';
constructor(color: string = '#ffffff', size: number = 5, shape: 'circle' | 'square' = 'circle') {
super();
this.color = color;
this.size = size;
this.shape = shape;
}
}
@ECSComponent('WorkerDemo_Lifetime')
class Lifetime extends Component {
maxAge: number = 5;
currentAge: number = 0;
constructor(maxAge: number = 5) {
super();
this.maxAge = maxAge;
this.currentAge = 0;
}
isDead(): boolean {
return this.currentAge >= this.maxAge;
}
}
// ============ 系统定义 ============
interface PhysicsEntityData {
id: number;
x: number;
y: number;
dx: number;
dy: number;
mass: number;
bounce: number;
friction: number;
radius: number;
}
interface PhysicsConfig {
gravity: number;
canvasWidth: number;
canvasHeight: number;
groundFriction: number;
}
@ECSSystem('PhysicsWorkerSystem')
class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsEntityData> {
private physicsConfig: PhysicsConfig;
constructor(enableWorker: boolean, canvasWidth: number, canvasHeight: number) {
const defaultConfig = {
gravity: 100,
canvasWidth,
canvasHeight,
groundFriction: 0.98
};
const isSharedArrayBufferAvailable = typeof SharedArrayBuffer !== 'undefined' && self.crossOriginIsolated;
super(
Matcher.empty().all(Position, Velocity, Physics),
{
enableWorker,
workerCount: isSharedArrayBufferAvailable ? (navigator.hardwareConcurrency || 2) : 1,
systemConfig: defaultConfig,
useSharedArrayBuffer: true
}
);
this.physicsConfig = defaultConfig;
}
protected extractEntityData(entity: Entity): PhysicsEntityData {
const position = entity.getComponent(Position)!;
const velocity = entity.getComponent(Velocity)!;
const physics = entity.getComponent(Physics)!;
const renderable = entity.getComponent(Renderable)!;
return {
id: entity.id,
x: position.x,
y: position.y,
dx: velocity.dx,
dy: velocity.dy,
mass: physics.mass,
bounce: physics.bounce,
friction: physics.friction,
radius: renderable.size
};
}
protected workerProcess(
entities: PhysicsEntityData[],
deltaTime: number,
systemConfig?: PhysicsConfig
): PhysicsEntityData[] {
const config = systemConfig || this.physicsConfig;
const result = entities.map(e => ({ ...e }));
for (let i = 0; i < result.length; i++) {
const entity = result[i];
entity.dy += config.gravity * deltaTime;
entity.x += entity.dx * deltaTime;
entity.y += entity.dy * deltaTime;
if (entity.x <= entity.radius) {
entity.x = entity.radius;
entity.dx = -entity.dx * entity.bounce;
} else if (entity.x >= config.canvasWidth - entity.radius) {
entity.x = config.canvasWidth - entity.radius;
entity.dx = -entity.dx * entity.bounce;
}
if (entity.y <= entity.radius) {
entity.y = entity.radius;
entity.dy = -entity.dy * entity.bounce;
} else if (entity.y >= config.canvasHeight - entity.radius) {
entity.y = config.canvasHeight - entity.radius;
entity.dy = -entity.dy * entity.bounce;
entity.dx *= config.groundFriction;
}
entity.dx *= entity.friction;
entity.dy *= entity.friction;
}
for (let i = 0; i < result.length; i++) {
for (let j = i + 1; j < result.length; j++) {
const ball1 = result[i];
const ball2 = result[j];
const dx = ball2.x - ball1.x;
const dy = ball2.y - ball1.y;
const distance = Math.sqrt(dx * dx + dy * dy);
const minDistance = ball1.radius + ball2.radius;
if (distance < minDistance && distance > 0) {
const nx = dx / distance;
const ny = dy / distance;
const overlap = minDistance - distance;
const separationX = nx * overlap * 0.5;
const separationY = ny * overlap * 0.5;
ball1.x -= separationX;
ball1.y -= separationY;
ball2.x += separationX;
ball2.y += separationY;
const relativeVelocityX = ball2.dx - ball1.dx;
const relativeVelocityY = ball2.dy - ball1.dy;
const velocityAlongNormal = relativeVelocityX * nx + relativeVelocityY * ny;
if (velocityAlongNormal > 0) continue;
const restitution = (ball1.bounce + ball2.bounce) * 0.5;
const impulseScalar = -(1 + restitution) * velocityAlongNormal / (1/ball1.mass + 1/ball2.mass);
const impulseX = impulseScalar * nx;
const impulseY = impulseScalar * ny;
ball1.dx -= impulseX / ball1.mass;
ball1.dy -= impulseY / ball1.mass;
ball2.dx += impulseX / ball2.mass;
ball2.dy += impulseY / ball2.mass;
const energyLoss = 0.98;
ball1.dx *= energyLoss;
ball1.dy *= energyLoss;
ball2.dx *= energyLoss;
ball2.dy *= energyLoss;
}
}
}
return result;
}
protected applyResult(entity: Entity, result: PhysicsEntityData): void {
if (!entity || !entity.enabled) return;
const position = entity.getComponent(Position);
const velocity = entity.getComponent(Velocity);
if (!position || !velocity) return;
position.set(result.x, result.y);
velocity.set(result.dx, result.dy);
}
public updatePhysicsConfig(newConfig: Partial<PhysicsConfig>): void {
Object.assign(this.physicsConfig, newConfig);
this.updateConfig({ systemConfig: this.physicsConfig });
}
public getPhysicsConfig(): PhysicsConfig {
return { ...this.physicsConfig };
}
protected getDefaultEntityDataSize(): number {
return 9;
}
protected writeEntityToBuffer(entityData: PhysicsEntityData, offset: number): void {
const sharedArray = (this as any).sharedFloatArray as Float32Array;
if (!sharedArray) return;
// 在第一个位置存储当前实体数量
const currentEntityCount = Math.floor(offset / 9) + 1;
sharedArray[0] = currentEntityCount;
// 数据从索引9开始存储第一个9个位置用作元数据区域
const dataOffset = offset + 9;
sharedArray[dataOffset + 0] = entityData.id;
sharedArray[dataOffset + 1] = entityData.x;
sharedArray[dataOffset + 2] = entityData.y;
sharedArray[dataOffset + 3] = entityData.dx;
sharedArray[dataOffset + 4] = entityData.dy;
sharedArray[dataOffset + 5] = entityData.mass;
sharedArray[dataOffset + 6] = entityData.bounce;
sharedArray[dataOffset + 7] = entityData.friction;
sharedArray[dataOffset + 8] = entityData.radius;
}
protected readEntityFromBuffer(offset: number): PhysicsEntityData | null {
const sharedArray = (this as any).sharedFloatArray as Float32Array;
if (!sharedArray) return null;
// 数据从索引9开始存储
const dataOffset = offset + 9;
return {
id: sharedArray[dataOffset + 0],
x: sharedArray[dataOffset + 1],
y: sharedArray[dataOffset + 2],
dx: sharedArray[dataOffset + 3],
dy: sharedArray[dataOffset + 4],
mass: sharedArray[dataOffset + 5],
bounce: sharedArray[dataOffset + 6],
friction: sharedArray[dataOffset + 7],
radius: sharedArray[dataOffset + 8]
};
}
protected getSharedArrayBufferProcessFunction(): any {
return function(sharedFloatArray: Float32Array, startIndex: number, endIndex: number, deltaTime: number, systemConfig?: any) {
const config = systemConfig || {
gravity: 100,
canvasWidth: 800,
canvasHeight: 600,
groundFriction: 0.98
};
const actualEntityCount = sharedFloatArray[0];
// 基础物理更新
for (let i = startIndex; i < endIndex && i < actualEntityCount; i++) {
const offset = i * 9 + 9;
const id = sharedFloatArray[offset + 0];
if (id === 0) continue;
let x = sharedFloatArray[offset + 1];
let y = sharedFloatArray[offset + 2];
let dx = sharedFloatArray[offset + 3];
let dy = sharedFloatArray[offset + 4];
const bounce = sharedFloatArray[offset + 6];
const friction = sharedFloatArray[offset + 7];
const radius = sharedFloatArray[offset + 8];
// 应用重力
dy += config.gravity * deltaTime;
// 更新位置
x += dx * deltaTime;
y += dy * deltaTime;
// 边界碰撞
if (x <= radius) {
x = radius;
dx = -dx * bounce;
} else if (x >= config.canvasWidth - radius) {
x = config.canvasWidth - radius;
dx = -dx * bounce;
}
if (y <= radius) {
y = radius;
dy = -dy * bounce;
} else if (y >= config.canvasHeight - radius) {
y = config.canvasHeight - radius;
dy = -dy * bounce;
dx *= config.groundFriction;
}
// 空气阻力
dx *= friction;
dy *= friction;
// 写回数据
sharedFloatArray[offset + 1] = x;
sharedFloatArray[offset + 2] = y;
sharedFloatArray[offset + 3] = dx;
sharedFloatArray[offset + 4] = dy;
}
// 碰撞检测
for (let i = startIndex; i < endIndex && i < actualEntityCount; i++) {
const offset1 = i * 9 + 9;
const id1 = sharedFloatArray[offset1 + 0];
if (id1 === 0) continue;
let x1 = sharedFloatArray[offset1 + 1];
let y1 = sharedFloatArray[offset1 + 2];
let dx1 = sharedFloatArray[offset1 + 3];
let dy1 = sharedFloatArray[offset1 + 4];
const mass1 = sharedFloatArray[offset1 + 5];
const bounce1 = sharedFloatArray[offset1 + 6];
const radius1 = sharedFloatArray[offset1 + 8];
for (let j = 0; j < actualEntityCount; j++) {
if (i === j) continue;
const offset2 = j * 9 + 9;
const id2 = sharedFloatArray[offset2 + 0];
if (id2 === 0) continue;
const x2 = sharedFloatArray[offset2 + 1];
const y2 = sharedFloatArray[offset2 + 2];
const dx2 = sharedFloatArray[offset2 + 3];
const dy2 = sharedFloatArray[offset2 + 4];
const mass2 = sharedFloatArray[offset2 + 5];
const bounce2 = sharedFloatArray[offset2 + 6];
const radius2 = sharedFloatArray[offset2 + 8];
if (isNaN(x2) || isNaN(y2) || isNaN(radius2) || radius2 <= 0) continue;
const deltaX = x2 - x1;
const deltaY = y2 - y1;
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
const minDistance = radius1 + radius2;
if (distance < minDistance && distance > 0) {
const nx = deltaX / distance;
const ny = deltaY / distance;
const overlap = minDistance - distance;
const separationX = nx * overlap * 0.5;
const separationY = ny * overlap * 0.5;
x1 -= separationX;
y1 -= separationY;
const relativeVelocityX = dx2 - dx1;
const relativeVelocityY = dy2 - dy1;
const velocityAlongNormal = relativeVelocityX * nx + relativeVelocityY * ny;
if (velocityAlongNormal > 0) continue;
const restitution = (bounce1 + bounce2) * 0.5;
const impulseScalar = -(1 + restitution) * velocityAlongNormal / (1/mass1 + 1/mass2);
const impulseX = impulseScalar * nx;
const impulseY = impulseScalar * ny;
dx1 -= impulseX / mass1;
dy1 -= impulseY / mass1;
const energyLoss = 0.98;
dx1 *= energyLoss;
dy1 *= energyLoss;
}
}
sharedFloatArray[offset1 + 1] = x1;
sharedFloatArray[offset1 + 2] = y1;
sharedFloatArray[offset1 + 3] = dx1;
sharedFloatArray[offset1 + 4] = dy1;
}
};
}
}
@ECSSystem('RenderSystem')
class RenderSystem extends EntitySystem {
private canvas: HTMLCanvasElement;
private ctx: CanvasRenderingContext2D;
constructor(canvas: HTMLCanvasElement) {
super(Matcher.all(Position, Renderable));
this.canvas = canvas;
this.ctx = canvas.getContext('2d')!;
}
protected override process(entities: readonly Entity[]): void {
this.ctx.fillStyle = '#000';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
for (const entity of entities) {
const position = this.requireComponent(entity, Position);
const renderable = this.requireComponent(entity, Renderable);
this.ctx.fillStyle = renderable.color;
this.ctx.beginPath();
this.ctx.arc(position.x, position.y, renderable.size, 0, Math.PI * 2);
this.ctx.fill();
}
}
}
@ECSSystem('LifetimeSystem')
class LifetimeSystem extends EntitySystem {
constructor() {
super(Matcher.all(Lifetime));
}
protected override process(entities: readonly Entity[]): void {
const deltaTime = Time.deltaTime;
for (const entity of entities) {
const lifetime = this.requireComponent(entity, Lifetime);
lifetime.currentAge += deltaTime;
if (lifetime.isDead()) {
entity.destroy();
}
}
}
}
// ============ Demo类 ============
export class WorkerSystemDemo extends DemoBase {
private physicsSystem!: PhysicsWorkerSystem;
private renderSystem!: RenderSystem;
private lifetimeSystem!: LifetimeSystem;
private currentFPS = 0;
private frameCount = 0;
private fpsUpdateTime = 0;
private elements: { [key: string]: HTMLElement } = {};
getInfo(): DemoInfo {
return {
id: 'worker-system',
name: 'Worker System',
description: '演示 ECS 框架中的多线程物理计算能力',
category: '核心功能',
icon: '⚙️'
};
}
setup(): void {
// 注册浏览器平台适配器
const browserAdapter = new BrowserAdapter();
PlatformManager.getInstance().registerAdapter(browserAdapter);
// 初始化系统
this.physicsSystem = new PhysicsWorkerSystem(true, this.canvas.width, this.canvas.height);
this.renderSystem = new RenderSystem(this.canvas);
this.lifetimeSystem = new LifetimeSystem();
this.physicsSystem.updateOrder = 1;
this.lifetimeSystem.updateOrder = 2;
this.renderSystem.updateOrder = 3;
this.scene.addSystem(this.physicsSystem);
this.scene.addSystem(this.lifetimeSystem);
this.scene.addSystem(this.renderSystem);
// 创建控制面板
this.createControls();
// 初始化UI元素引用
this.initializeUIElements();
this.bindEvents();
// 生成初始实体
this.spawnInitialEntities(1000);
}
createControls(): void {
this.controlPanel.innerHTML = `
<div style="background: #2a2a2a; padding: 20px; border-radius: 8px; height: 100%; overflow-y: auto;">
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; color: #ccc;">实体数量:</label>
<input type="range" id="entityCount" min="100" max="10000" value="1000" step="100"
style="width: 100%; margin-bottom: 5px;">
<span id="entityCountValue" style="color: #fff;">1000</span>
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; color: #ccc;">Worker 设置:</label>
<button id="toggleWorker" style="width: 100%; padding: 8px; margin-bottom: 5px;
background: #4a9eff; color: white; border: none; border-radius: 4px; cursor: pointer;">
禁用 Worker
</button>
</div>
<div style="margin-bottom: 15px;">
<button id="spawnParticles" style="width: 100%; padding: 8px; margin-bottom: 5px;
background: #4a9eff; color: white; border: none; border-radius: 4px; cursor: pointer;">
生成粒子爆炸
</button>
<button id="clearEntities" style="width: 100%; padding: 8px; margin-bottom: 5px;
background: #4a9eff; color: white; border: none; border-radius: 4px; cursor: pointer;">
清空所有实体
</button>
<button id="resetDemo" style="width: 100%; padding: 8px;
background: #4a9eff; color: white; border: none; border-radius: 4px; cursor: pointer;">
重置演示
</button>
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; color: #ccc;">物理参数:</label>
<input type="range" id="gravity" min="0" max="500" value="100" step="10"
style="width: 100%; margin-bottom: 5px;">
<label style="color: #ccc;">重力: <span id="gravityValue">100</span></label>
<input type="range" id="friction" min="0" max="100" value="95" step="5"
style="width: 100%; margin-top: 10px; margin-bottom: 5px;">
<label style="color: #ccc;">摩擦力: <span id="frictionValue">95%</span></label>
</div>
<div style="background: #1a1a1a; padding: 15px; border-radius: 8px; font-family: monospace; font-size: 12px;">
<h3 style="margin-top: 0; color: #4a9eff;">性能统计</h3>
<div style="margin: 5px 0; color: #ccc;">FPS: <span id="fps" style="color: #4eff4a;">0</span></div>
<div style="margin: 5px 0; color: #ccc;">实体数量: <span id="entityCountStat" style="color: #fff;">0</span></div>
<div style="margin: 5px 0; color: #ccc;">Worker状态: <span id="workerStatus" style="color: #ff4a4a;">未启用</span></div>
<div style="margin: 5px 0; color: #ccc;">Worker负载: <span id="workerLoad" style="color: #fff;">N/A</span></div>
</div>
</div>
`;
}
protected render(): void {
this.frameCount++;
const currentTime = performance.now();
if (currentTime - this.fpsUpdateTime >= 1000) {
this.currentFPS = this.frameCount;
this.frameCount = 0;
this.fpsUpdateTime = currentTime;
}
this.updateUI();
}
private initializeUIElements(): void {
const elementIds = [
'entityCount', 'entityCountValue', 'toggleWorker',
'gravity', 'gravityValue', 'friction', 'frictionValue', 'spawnParticles',
'clearEntities', 'resetDemo', 'fps', 'entityCountStat', 'workerStatus', 'workerLoad'
];
for (const id of elementIds) {
const element = document.getElementById(id);
if (element) {
this.elements[id] = element;
}
}
}
private bindEvents(): void {
if (this.elements.entityCount && this.elements.entityCountValue) {
const slider = this.elements.entityCount as HTMLInputElement;
slider.addEventListener('input', () => {
this.elements.entityCountValue.textContent = slider.value;
});
slider.addEventListener('change', () => {
const count = parseInt(slider.value);
this.spawnInitialEntities(count);
});
}
if (this.elements.toggleWorker) {
this.elements.toggleWorker.addEventListener('click', () => {
const workerEnabled = this.toggleWorker();
this.elements.toggleWorker.textContent = workerEnabled ? '禁用 Worker' : '启用 Worker';
});
}
if (this.elements.gravity && this.elements.gravityValue) {
const slider = this.elements.gravity as HTMLInputElement;
slider.addEventListener('input', () => {
this.elements.gravityValue.textContent = slider.value;
});
slider.addEventListener('change', () => {
const gravity = parseInt(slider.value);
this.updateWorkerConfig({ gravity });
});
}
if (this.elements.friction && this.elements.frictionValue) {
const slider = this.elements.friction as HTMLInputElement;
slider.addEventListener('input', () => {
const value = parseInt(slider.value);
this.elements.frictionValue.textContent = `${value}%`;
});
slider.addEventListener('change', () => {
const friction = parseInt(slider.value) / 100;
this.updateWorkerConfig({ friction });
});
}
if (this.elements.spawnParticles) {
this.elements.spawnParticles.addEventListener('click', () => {
const centerX = this.canvas.width / 2;
const centerY = this.canvas.height / 2;
this.spawnParticleExplosion(centerX, centerY, 100);
});
}
if (this.elements.clearEntities) {
this.elements.clearEntities.addEventListener('click', () => {
this.clearAllEntities();
});
}
if (this.elements.resetDemo) {
this.elements.resetDemo.addEventListener('click', () => {
(this.elements.entityCount as HTMLInputElement).value = '1000';
this.elements.entityCountValue.textContent = '1000';
(this.elements.gravity as HTMLInputElement).value = '100';
this.elements.gravityValue.textContent = '100';
(this.elements.friction as HTMLInputElement).value = '95';
this.elements.frictionValue.textContent = '95%';
this.spawnInitialEntities(1000);
this.updateWorkerConfig({ gravity: 100, friction: 0.95 });
});
}
this.canvas.addEventListener('click', (event) => {
const rect = this.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
this.spawnParticleExplosion(x, y, 30);
});
}
private updateUI(): void {
const workerInfo = this.physicsSystem.getWorkerInfo();
if (this.elements.fps) {
this.elements.fps.textContent = this.currentFPS.toString();
}
if (this.elements.entityCountStat) {
this.elements.entityCountStat.textContent = this.scene.entities.count.toString();
}
if (this.elements.workerStatus) {
if (workerInfo.enabled) {
this.elements.workerStatus.textContent = `启用 (${workerInfo.workerCount} Workers)`;
this.elements.workerStatus.style.color = '#4eff4a';
} else {
this.elements.workerStatus.textContent = '禁用';
this.elements.workerStatus.style.color = '#ff4a4a';
}
}
if (this.elements.workerLoad) {
const entityCount = this.scene.entities.count;
if (workerInfo.enabled && entityCount > 0) {
const entitiesPerWorker = Math.ceil(entityCount / workerInfo.workerCount);
this.elements.workerLoad.textContent = `${entitiesPerWorker}/Worker (共${workerInfo.workerCount}个)`;
} else {
this.elements.workerLoad.textContent = 'N/A';
}
}
}
private spawnInitialEntities(count: number = 1000): void {
this.clearAllEntities();
for (let i = 0; i < count; i++) {
this.createParticle();
}
}
private createParticle(): void {
const entity = this.scene.createEntity(`Particle_${Date.now()}_${Math.random()}`);
const x = Math.random() * (this.canvas.width - 20) + 10;
const y = Math.random() * (this.canvas.height - 20) + 10;
const dx = (Math.random() - 0.5) * 200;
const dy = (Math.random() - 0.5) * 200;
const mass = Math.random() * 3 + 2;
const bounce = 0.85 + Math.random() * 0.15;
const friction = 0.998 + Math.random() * 0.002;
const colors = [
'#ff4444', '#44ff44', '#4444ff', '#ffff44', '#ff44ff', '#44ffff', '#ffffff',
'#ff8844', '#88ff44', '#4488ff', '#ff4488', '#88ff88', '#8888ff', '#ffaa44'
];
const color = colors[Math.floor(Math.random() * colors.length)];
const size = Math.random() * 6 + 3;
entity.addComponent(new Position(x, y));
entity.addComponent(new Velocity(dx, dy));
entity.addComponent(new Physics(mass, bounce, friction));
entity.addComponent(new Renderable(color, size, 'circle'));
entity.addComponent(new Lifetime(5 + Math.random() * 10));
}
private spawnParticleExplosion(centerX: number, centerY: number, count: number = 50): void {
for (let i = 0; i < count; i++) {
const entity = this.scene.createEntity(`Explosion_${Date.now()}_${i}`);
const angle = (Math.PI * 2 * i) / count + (Math.random() - 0.5) * 0.5;
const distance = Math.random() * 30;
const x = centerX + Math.cos(angle) * distance;
const y = centerY + Math.sin(angle) * distance;
const speed = 100 + Math.random() * 150;
const dx = Math.cos(angle) * speed;
const dy = Math.sin(angle) * speed;
const mass = 0.5 + Math.random() * 1;
const bounce = 0.8 + Math.random() * 0.2;
const colors = ['#ffaa00', '#ff6600', '#ff0066', '#ff3300', '#ffff00'];
const color = colors[Math.floor(Math.random() * colors.length)];
const size = Math.random() * 4 + 2;
entity.addComponent(new Position(x, y));
entity.addComponent(new Velocity(dx, dy));
entity.addComponent(new Physics(mass, bounce, 0.999));
entity.addComponent(new Renderable(color, size, 'circle'));
entity.addComponent(new Lifetime(2 + Math.random() * 3));
}
}
private clearAllEntities(): void {
const entities = [...this.scene.entities.buffer];
for (const entity of entities) {
entity.destroy();
}
}
private toggleWorker(): boolean {
const workerInfo = this.physicsSystem.getWorkerInfo();
const newWorkerEnabled = !workerInfo.enabled;
// 保存当前物理配置
const currentConfig = this.physicsSystem.getPhysicsConfig();
this.scene.removeSystem(this.physicsSystem);
this.physicsSystem = new PhysicsWorkerSystem(newWorkerEnabled, this.canvas.width, this.canvas.height);
this.physicsSystem.updateOrder = 1;
// 恢复物理配置
this.physicsSystem.updatePhysicsConfig(currentConfig);
this.scene.addSystem(this.physicsSystem);
return newWorkerEnabled;
}
private updateWorkerConfig(config: { gravity?: number; friction?: number }): void {
if (config.gravity !== undefined || config.friction !== undefined) {
const physicsConfig = this.physicsSystem.getPhysicsConfig();
this.physicsSystem.updatePhysicsConfig({
gravity: config.gravity ?? physicsConfig.gravity,
groundFriction: config.friction ?? physicsConfig.groundFriction
});
}
}
}

View File

@@ -0,0 +1,13 @@
import { DemoBase } from './DemoBase';
import { SerializationDemo } from './SerializationDemo';
import { IncrementalSerializationDemo } from './IncrementalSerializationDemo';
import { WorkerSystemDemo } from './WorkerSystemDemo';
export { DemoBase, SerializationDemo, IncrementalSerializationDemo, WorkerSystemDemo };
// Demo注册表
export const DEMO_REGISTRY: typeof DemoBase[] = [
SerializationDemo,
IncrementalSerializationDemo,
WorkerSystemDemo
];

View File

@@ -0,0 +1,171 @@
import { DEMO_REGISTRY, DemoBase } from './demos';
import { Core } from '@esengine/ecs-framework';
class DemoManager {
private demos: Map<string, typeof DemoBase> = new Map();
private currentDemo: DemoBase | null = null;
private canvas: HTMLCanvasElement;
private controlPanel: HTMLElement;
constructor() {
// 初始化ECS Core
Core.create({
debug: true,
enableEntitySystems: true
});
this.canvas = document.getElementById('demoCanvas') as HTMLCanvasElement;
this.controlPanel = document.getElementById('controlPanel') as HTMLElement;
// 注册所有demos
for (const DemoClass of DEMO_REGISTRY) {
const tempInstance = new DemoClass(this.canvas, this.controlPanel);
const info = tempInstance.getInfo();
this.demos.set(info.id, DemoClass);
tempInstance.destroy();
}
// 渲染demo列表
this.renderDemoList();
// 自动加载第一个demo
const firstDemo = DEMO_REGISTRY[0];
if (firstDemo) {
const tempInstance = new firstDemo(this.canvas, this.controlPanel);
const info = tempInstance.getInfo();
tempInstance.destroy();
this.loadDemo(info.id);
}
}
private renderDemoList() {
const demoList = document.getElementById('demoList')!;
// 按分类组织demos
const categories = new Map<string, typeof DemoBase[]>();
for (const DemoClass of DEMO_REGISTRY) {
const tempInstance = new DemoClass(this.canvas, this.controlPanel);
const info = tempInstance.getInfo();
tempInstance.destroy();
if (!categories.has(info.category)) {
categories.set(info.category, []);
}
categories.get(info.category)!.push(DemoClass);
}
// 渲染分类和demos
let html = '';
for (const [category, demoClasses] of categories) {
html += `<div class="demo-category">`;
html += `<div class="category-title">${category}</div>`;
for (const DemoClass of demoClasses) {
const tempInstance = new DemoClass(this.canvas, this.controlPanel);
const info = tempInstance.getInfo();
tempInstance.destroy();
html += `
<div class="demo-item" data-demo-id="${info.id}">
<div class="demo-icon">${info.icon}</div>
<div class="demo-info">
<div class="demo-name">${info.name}</div>
<div class="demo-desc">${info.description}</div>
</div>
</div>
`;
}
html += `</div>`;
}
demoList.innerHTML = html;
// 绑定点击事件
demoList.querySelectorAll('.demo-item').forEach(item => {
item.addEventListener('click', () => {
const demoId = item.getAttribute('data-demo-id')!;
this.loadDemo(demoId);
});
});
}
private loadDemo(demoId: string) {
// 停止并销毁当前demo
if (this.currentDemo) {
this.currentDemo.destroy();
this.currentDemo = null;
}
// 显示加载动画
const loading = document.getElementById('loading')!;
loading.style.display = 'block';
// 延迟加载,给用户反馈
setTimeout(() => {
const DemoClass = this.demos.get(demoId);
if (!DemoClass) {
console.error(`Demo ${demoId} not found`);
loading.style.display = 'none';
return;
}
try {
// 创建新demo
this.currentDemo = new DemoClass(this.canvas, this.controlPanel);
const info = this.currentDemo.getInfo();
// 更新页面标题和描述
document.getElementById('demoTitle')!.textContent = info.name;
document.getElementById('demoDescription')!.textContent = info.description;
// 设置demo
this.currentDemo.setup();
// 显示控制面板
this.controlPanel.style.display = 'block';
// 启动demo
this.currentDemo.start();
// 更新菜单选中状态
document.querySelectorAll('.demo-item').forEach(item => {
item.classList.remove('active');
if (item.getAttribute('data-demo-id') === demoId) {
item.classList.add('active');
}
});
loading.style.display = 'none';
console.log(`✅ Demo "${info.name}" loaded successfully`);
} catch (error) {
console.error(`Failed to load demo ${demoId}:`, error);
loading.style.display = 'none';
this.showError('加载演示失败:' + (error as Error).message);
}
}, 300);
}
private showError(message: string) {
const toast = document.getElementById('toast')!;
const toastMessage = document.getElementById('toastMessage')!;
const toastIcon = toast.querySelector('.toast-icon')!;
toastIcon.textContent = '❌';
toastMessage.textContent = message;
toast.style.borderColor = '#f5576c';
toast.classList.add('show');
setTimeout(() => {
toast.classList.remove('show');
toast.style.borderColor = '#667eea';
}, 3000);
}
}
// 初始化
window.addEventListener('DOMContentLoaded', () => {
new DemoManager();
});

View File

@@ -0,0 +1,204 @@
import type {
IPlatformAdapter,
PlatformWorker,
WorkerCreationOptions,
PlatformConfig
} from '@esengine/ecs-framework';
/**
* 浏览器平台适配器
*/
export class BrowserAdapter implements IPlatformAdapter {
public readonly name = 'browser';
public readonly version: string;
constructor() {
this.version = this.getBrowserInfo();
}
/**
* 检查是否支持Worker
*/
public isWorkerSupported(): boolean {
return typeof Worker !== 'undefined';
}
/**
* 检查是否支持SharedArrayBuffer
*/
public isSharedArrayBufferSupported(): boolean {
return typeof SharedArrayBuffer !== 'undefined' && this.checkSharedArrayBufferEnabled();
}
/**
* 获取硬件并发数CPU核心数
*/
public getHardwareConcurrency(): number {
return navigator.hardwareConcurrency || 4;
}
/**
* 创建Worker
*/
public createWorker(script: string, options: WorkerCreationOptions = {}): PlatformWorker {
if (!this.isWorkerSupported()) {
throw new Error('浏览器不支持Worker');
}
try {
return new BrowserWorker(script, options);
} catch (error) {
throw new Error(`创建浏览器Worker失败: ${(error as Error).message}`);
}
}
/**
* 创建SharedArrayBuffer
*/
public createSharedArrayBuffer(length: number): SharedArrayBuffer | null {
if (!this.isSharedArrayBufferSupported()) {
return null;
}
try {
return new SharedArrayBuffer(length);
} catch (error) {
console.warn('SharedArrayBuffer创建失败:', error);
return null;
}
}
/**
* 获取高精度时间戳
*/
public getHighResTimestamp(): number {
return performance.now();
}
/**
* 获取平台配置
*/
public getPlatformConfig(): PlatformConfig {
return {
maxWorkerCount: this.getHardwareConcurrency(),
supportsModuleWorker: false,
supportsTransferableObjects: true,
maxSharedArrayBufferSize: 1024 * 1024 * 1024, // 1GB
workerScriptPrefix: '',
limitations: {
noEval: false,
requiresWorkerInit: false
}
};
}
/**
* 获取浏览器信息
*/
private getBrowserInfo(): string {
const userAgent = navigator.userAgent;
if (userAgent.includes('Chrome')) {
const match = userAgent.match(/Chrome\/([0-9.]+)/);
return match ? `Chrome ${match[1]}` : 'Chrome';
} else if (userAgent.includes('Firefox')) {
const match = userAgent.match(/Firefox\/([0-9.]+)/);
return match ? `Firefox ${match[1]}` : 'Firefox';
} else if (userAgent.includes('Safari')) {
const match = userAgent.match(/Version\/([0-9.]+)/);
return match ? `Safari ${match[1]}` : 'Safari';
}
return 'Unknown Browser';
}
/**
* 检查SharedArrayBuffer是否真正可用
*/
private checkSharedArrayBufferEnabled(): boolean {
try {
new SharedArrayBuffer(8);
return true;
} catch {
return false;
}
}
}
/**
* 浏览器Worker封装
*/
class BrowserWorker implements PlatformWorker {
private _state: 'running' | 'terminated' = 'running';
private worker: Worker;
constructor(script: string, options: WorkerCreationOptions = {}) {
this.worker = this.createBrowserWorker(script, options);
}
public get state(): 'running' | 'terminated' {
return this._state;
}
public postMessage(message: any, transfer?: Transferable[]): void {
if (this._state === 'terminated') {
throw new Error('Worker已被终止');
}
try {
if (transfer && transfer.length > 0) {
this.worker.postMessage(message, transfer);
} else {
this.worker.postMessage(message);
}
} catch (error) {
throw new Error(`发送消息到Worker失败: ${(error as Error).message}`);
}
}
public onMessage(handler: (event: { data: any }) => void): void {
this.worker.onmessage = (event: MessageEvent) => {
handler({ data: event.data });
};
}
public onError(handler: (error: ErrorEvent) => void): void {
this.worker.onerror = handler;
}
public terminate(): void {
if (this._state === 'running') {
try {
this.worker.terminate();
this._state = 'terminated';
} catch (error) {
console.error('终止Worker失败:', error);
}
}
}
/**
* 创建浏览器Worker
*/
private createBrowserWorker(script: string, options: WorkerCreationOptions): Worker {
try {
// 创建Blob URL
const blob = new Blob([script], { type: 'application/javascript' });
const url = URL.createObjectURL(blob);
// 创建Worker
const worker = new Worker(url, {
type: options.type || 'classic',
credentials: options.credentials,
name: options.name
});
// 清理Blob URL延迟清理确保Worker已加载
setTimeout(() => {
URL.revokeObjectURL(url);
}, 1000);
return worker;
} catch (error) {
throw new Error(`无法创建浏览器Worker: ${(error as Error).message}`);
}
}
}

View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"include": ["src"]
}

View File

@@ -0,0 +1,15 @@
import { defineConfig } from 'vite';
export default defineConfig({
server: {
port: 3003,
headers: {
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp'
}
},
build: {
target: 'es2020',
outDir: 'dist'
}
});

View File

@@ -0,0 +1,2 @@
[InternetShortcut]
URL=https://docs.cocos.com/creator/manual/en/scripting/setup.html#custom-script-template

24
extensions/cocos/cocos-ecs/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
#///////////////////////////
# Cocos Creator 3D Project
#///////////////////////////
library/
temp/
local/
build/
profiles/
native
#//////////////////////////
# NPM
#//////////////////////////
node_modules/
#//////////////////////////
# VSCode
#//////////////////////////
.vscode/
#//////////////////////////
# WebStorm
#//////////////////////////
.idea/

View File

@@ -0,0 +1,14 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "2a691dda-d56d-4a72-9fef-111a999415db",
"files": [],
"subMetas": {},
"userData": {
"isBundle": true,
"bundleConfigID": "default",
"bundleName": "resources",
"priority": 8
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "8c25761f-50d6-498b-a95f-d863bf1fbff1",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "3a66cbbc-6612-4408-838b-875d0bb2e9a3",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,317 @@
{
"nodes": [
{
"id": "node_15iffhg4p",
"type": "root",
"name": "根节点",
"description": "行为树的根节点,每棵树只能有一个根节点",
"children": [
"node_o6tsnrxyg"
]
},
{
"id": "node_o6tsnrxyg",
"type": "selector",
"name": "选择器",
"description": "按顺序执行子节点,任一成功则整体成功",
"properties": {
"abortType": "LowerPriority"
},
"children": [
"node_tljchzbno",
"node_txhx0hau5",
"node_r9kvcwv8u",
"node_520hedw22"
]
},
{
"id": "node_tljchzbno",
"type": "conditional-decorator",
"name": "休息条件装饰器",
"description": "基于条件执行子节点(拖拽条件节点到此装饰器来配置条件)",
"properties": {
"conditionType": "blackboardCompare",
"executeWhenTrue": true,
"abortType": "LowerPriority",
"shouldReevaluate": true,
"variableName": "{{isLowStamina}}",
"operator": "equal",
"compareValue": "true"
},
"children": [
"node_ulp8qx68h"
],
"condition": {
"type": "blackboard-value-comparison",
"properties": {}
}
},
{
"id": "node_txhx0hau5",
"type": "conditional-decorator",
"name": "存储条件装饰器",
"description": "基于条件执行子节点(拖拽条件节点到此装饰器来配置条件)",
"properties": {
"conditionType": "blackboardCompare",
"executeWhenTrue": true,
"abortType": "LowerPriority",
"shouldReevaluate": true,
"variableName": "{{hasOre}}",
"operator": "equal",
"compareValue": "true"
},
"children": [
"node_dhsz8rgl1"
],
"condition": {
"type": "blackboard-value-comparison",
"properties": {}
}
},
{
"id": "node_r9kvcwv8u",
"type": "conditional-decorator",
"name": "挖矿条件装饰器",
"description": "基于条件执行子节点(拖拽条件节点到此装饰器来配置条件)",
"properties": {
"conditionType": "blackboardCompare",
"executeWhenTrue": true,
"abortType": "LowerPriority",
"shouldReevaluate": true,
"variableName": "{{isLowStamina}}",
"operator": "equal",
"compareValue": "false"
},
"children": [
"node_zguxml6u7"
],
"condition": {
"type": "blackboard-value-comparison",
"properties": {}
}
},
{
"id": "node_ulp8qx68h",
"type": "sequence",
"name": "序列器",
"description": "按顺序执行子节点,任一失败则整体失败",
"properties": {
"abortType": "None"
},
"children": [
"node_0fgq85ovw",
"node_9v13vpqyr"
]
},
{
"id": "node_0fgq85ovw",
"type": "event-action",
"name": "回家休息",
"description": "执行已注册的事件处理函数(推荐)",
"properties": {
"eventName": "go-home-rest",
"parameters": "{}"
}
},
{
"id": "node_9v13vpqyr",
"type": "event-action",
"name": "恢复体力",
"description": "执行已注册的事件处理函数(推荐)",
"properties": {
"eventName": "recover-stamina",
"parameters": "{}"
}
},
{
"id": "node_ui4ja9mlj",
"type": "event-action",
"name": "前往仓库存储",
"description": "执行已注册的事件处理函数(推荐)",
"properties": {
"eventName": "store-ore",
"parameters": "{}"
}
},
{
"id": "node_969njccy2",
"type": "event-action",
"name": "挖掘金矿",
"description": "执行已注册的事件处理函数(推荐)",
"properties": {
"eventName": "mine-gold-ore",
"parameters": "{}"
}
},
{
"id": "node_520hedw22",
"type": "event-action",
"name": "默认待机",
"description": "执行已注册的事件处理函数(推荐)",
"properties": {
"eventName": "idle-behavior",
"parameters": "{}"
}
},
{
"id": "node_o5c7hv5wx",
"type": "set-blackboard-value",
"name": "设置黑板变量",
"description": "设置黑板变量的值",
"properties": {
"variableName": "{{hasOre}}",
"value": "false"
}
},
{
"id": "node_zf0sgkqev",
"type": "set-blackboard-value",
"name": "设置黑板变量",
"description": "设置黑板变量的值",
"properties": {
"variableName": "{{hasOre}}",
"value": "true"
}
},
{
"id": "node_dhsz8rgl1",
"type": "sequence",
"name": "序列器",
"description": "按顺序执行子节点,任一失败则整体失败",
"properties": {
"abortType": "None"
},
"children": [
"node_ui4ja9mlj",
"node_o5c7hv5wx"
]
},
{
"id": "node_zguxml6u7",
"type": "sequence",
"name": "序列器",
"description": "按顺序执行子节点,任一失败则整体失败",
"properties": {
"abortType": "None"
},
"children": [
"node_969njccy2",
"node_zf0sgkqev"
]
}
],
"blackboard": [
{
"name": "unitType",
"type": "string",
"value": "miner",
"description": "单位类型",
"group": "基础属性"
},
{
"name": "currentHealth",
"type": "number",
"value": 100,
"description": "当前生命值",
"group": "基础属性"
},
{
"name": "maxHealth",
"type": "number",
"value": 100,
"description": "最大生命值",
"group": "基础属性"
},
{
"name": "stamina",
"type": "number",
"value": 100,
"description": "当前体力值 - 挖矿会消耗体力",
"group": "体力系统"
},
{
"name": "maxStamina",
"type": "number",
"value": 100,
"description": "最大体力值",
"group": "体力系统"
},
{
"name": "staminaPercentage",
"type": "number",
"value": 1,
"description": "体力百分比",
"group": "体力系统"
},
{
"name": "isLowStamina",
"type": "boolean",
"value": false,
"description": "是否低体力 - 体力低于20%时为true",
"group": "体力系统"
},
{
"name": "isResting",
"type": "boolean",
"value": false,
"description": "是否正在休息",
"group": "体力系统"
},
{
"name": "homePosition",
"type": "vector3",
"value": {
"x": 0,
"y": 0,
"z": 0
},
"description": "家的位置 - 矿工休息的地方",
"group": "体力系统"
},
{
"name": "hasOre",
"type": "boolean",
"value": false,
"description": "是否携带矿石",
"group": "工作状态"
},
{
"name": "currentCommand",
"type": "string",
"value": "mine",
"description": "当前命令",
"group": "工作状态"
},
{
"name": "hasTarget",
"type": "boolean",
"value": false,
"description": "是否有目标",
"group": "工作状态"
},
{
"name": "targetPosition",
"type": "vector3",
"value": {
"x": 0,
"y": 0,
"z": 0
},
"description": "目标位置",
"group": "移动属性"
},
{
"name": "isMoving",
"type": "boolean",
"value": false,
"description": "是否正在移动",
"group": "移动属性"
}
],
"metadata": {
"name": "behavior-tree",
"created": "2025-06-25T14:06:55.596Z",
"version": "1.0",
"exportType": "clean"
}
}

View File

@@ -0,0 +1,11 @@
{
"ver": "2.0.1",
"importer": "json",
"imported": true,
"uuid": "598e1450-8c7a-46c7-9540-398f9809d627",
"files": [
".json"
],
"subMetas": {},
"userData": {}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,12 @@
{
"ver": "1.0.0",
"importer": "*",
"imported": true,
"uuid": "24c6e7e6-4ff0-4e7b-b470-9468bfa66b5d",
"files": [
".btree",
".json"
],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "2bf3ded8-4054-4d8f-a367-c76b21eaf538",
"files": [],
"subMetas": {},
"userData": {}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,13 @@
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "51a6e245-2983-4258-be9f-9e21378f7f9f",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "Panel_Node"
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "4fd895f7-6b0f-4357-aa3a-7c2e88ffac9a",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "829183be-61a1-4494-bf64-3df359c0e8e7",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "240e4a78-e55f-47a8-84de-39220bba1321",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,757 @@
[
{
"__type__": "cc.SceneAsset",
"_name": "behaviour-example-scene",
"_objFlags": 0,
"__editorExtras__": {},
"_native": "",
"scene": {
"__id__": 1
}
},
{
"__type__": "cc.Scene",
"_name": "behaviour-example-scene",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": null,
"_children": [
{
"__id__": 2
},
{
"__id__": 5
},
{
"__id__": 7
},
{
"__id__": 8
},
{
"__id__": 14
}
],
"_active": true,
"_components": [],
"_prefab": null,
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 1073741824,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"autoReleaseAssets": false,
"_globals": {
"__id__": 16
},
"_id": "ff354f0b-c2f5-4dea-8ffb-0152d175d11c"
},
{
"__type__": "cc.Node",
"_name": "Main Light",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 3
}
],
"_prefab": null,
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": -0.06397656665577071,
"y": -0.44608233363525845,
"z": -0.8239028751062036,
"w": -0.3436591377065261
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 1073741824,
"_euler": {
"__type__": "cc.Vec3",
"x": -117.894,
"y": -194.909,
"z": 38.562
},
"_id": "c0y6F5f+pAvI805TdmxIjx"
},
{
"__type__": "cc.DirectionalLight",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 2
},
"_enabled": true,
"__prefab": null,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 250,
"b": 240,
"a": 255
},
"_useColorTemperature": false,
"_colorTemperature": 6550,
"_staticSettings": {
"__id__": 4
},
"_visibility": -325058561,
"_illuminanceHDR": 65000,
"_illuminance": 65000,
"_illuminanceLDR": 1.6927083333333335,
"_shadowEnabled": false,
"_shadowPcf": 0,
"_shadowBias": 0.00001,
"_shadowNormalBias": 0,
"_shadowSaturation": 1,
"_shadowDistance": 50,
"_shadowInvisibleOcclusionRange": 200,
"_csmLevel": 4,
"_csmLayerLambda": 0.75,
"_csmOptimizationMode": 2,
"_csmAdvancedOptions": false,
"_csmLayersTransition": false,
"_csmTransitionRange": 0.05,
"_shadowFixedArea": false,
"_shadowNear": 0.1,
"_shadowFar": 10,
"_shadowOrthoSize": 5,
"_id": "597uMYCbhEtJQc0ffJlcgA"
},
{
"__type__": "cc.StaticLightSettings",
"_baked": false,
"_editorOnly": false,
"_castShadow": false
},
{
"__type__": "cc.Node",
"_name": "Main Camera",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 6
}
],
"_prefab": null,
"_lpos": {
"__type__": "cc.Vec3",
"x": -10,
"y": 10,
"z": 10
},
"_lrot": {
"__type__": "cc.Quat",
"x": -0.27781593346944056,
"y": -0.36497167621709875,
"z": -0.11507512748638377,
"w": 0.8811195706053617
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 1073741824,
"_euler": {
"__type__": "cc.Vec3",
"x": -35,
"y": -45,
"z": 0
},
"_id": "c9DMICJLFO5IeO07EPon7U"
},
{
"__type__": "cc.Camera",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 5
},
"_enabled": true,
"__prefab": null,
"_projection": 1,
"_priority": 0,
"_fov": 45,
"_fovAxis": 0,
"_orthoHeight": 10,
"_near": 1,
"_far": 1000,
"_color": {
"__type__": "cc.Color",
"r": 51,
"g": 51,
"b": 51,
"a": 255
},
"_depth": 1,
"_stencil": 0,
"_clearFlags": 14,
"_rect": {
"__type__": "cc.Rect",
"x": 0,
"y": 0,
"width": 1,
"height": 1
},
"_aperture": 19,
"_shutter": 7,
"_iso": 0,
"_screenScale": 1,
"_visibility": 1822425087,
"_targetTexture": null,
"_postProcess": null,
"_usePostProcess": false,
"_cameraType": -1,
"_trackingType": 0,
"_id": "7dWQTpwS5LrIHnc1zAPUtf"
},
{
"__type__": "cc.Node",
"_name": "GameWorld",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_components": [],
"_prefab": null,
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 1073741824,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": "8b9QorrGZIl64tVv0Z0vRQ"
},
{
"__type__": "cc.Node",
"_name": "Canvas",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [
{
"__id__": 9
}
],
"_active": true,
"_components": [
{
"__id__": 11
},
{
"__id__": 12
},
{
"__id__": 13
}
],
"_prefab": null,
"_lpos": {
"__type__": "cc.Vec3",
"x": 640,
"y": 360,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 1073741824,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": "4edRVPFLtIz5pR5edsryvx"
},
{
"__type__": "cc.Node",
"_name": "Camera",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 8
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 10
}
],
"_prefab": null,
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 1000
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 1073741824,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": "dfyZdh0bxJop4PyQrmHEP6"
},
{
"__type__": "cc.Camera",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 9
},
"_enabled": true,
"__prefab": null,
"_projection": 0,
"_priority": 1073741824,
"_fov": 45,
"_fovAxis": 0,
"_orthoHeight": 360,
"_near": 1,
"_far": 2000,
"_color": {
"__type__": "cc.Color",
"r": 0,
"g": 0,
"b": 0,
"a": 255
},
"_depth": 1,
"_stencil": 0,
"_clearFlags": 6,
"_rect": {
"__type__": "cc.Rect",
"x": 0,
"y": 0,
"width": 1,
"height": 1
},
"_aperture": 19,
"_shutter": 7,
"_iso": 0,
"_screenScale": 1,
"_visibility": 41943040,
"_targetTexture": null,
"_postProcess": null,
"_usePostProcess": false,
"_cameraType": -1,
"_trackingType": 0,
"_id": "48lLOhLY5Onqokj70aNP+E"
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 8
},
"_enabled": true,
"__prefab": null,
"_contentSize": {
"__type__": "cc.Size",
"width": 1280,
"height": 720
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": "c3qBrLTLNImoltQDlZ6coz"
},
{
"__type__": "cc.Canvas",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 8
},
"_enabled": true,
"__prefab": null,
"_cameraComponent": {
"__id__": 10
},
"_alignCanvasWithScreen": true,
"_id": "9d3SdE3ORAOZ6AG/imW6NO"
},
{
"__type__": "cc.Widget",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 8
},
"_enabled": true,
"__prefab": null,
"_alignFlags": 45,
"_target": null,
"_left": 0,
"_right": 0,
"_top": 0,
"_bottom": 0,
"_horizontalCenter": 0,
"_verticalCenter": 0,
"_isAbsLeft": true,
"_isAbsRight": true,
"_isAbsTop": true,
"_isAbsBottom": true,
"_isAbsHorizontalCenter": true,
"_isAbsVerticalCenter": true,
"_originalWidth": 0,
"_originalHeight": 0,
"_alignMode": 2,
"_lockFlags": 0,
"_id": "4a8iJypC1J8pMml467hQ6c"
},
{
"__type__": "cc.Node",
"_name": "RTSDemo",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 15
}
],
"_prefab": null,
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 1073741824,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": "89cmsd2gNNsq155xC7mob8"
},
{
"__type__": "c33869Km+9Bb7dw/OyRztvE",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 14
},
"_enabled": true,
"__prefab": null,
"minerCount": 1,
"goldMineCount": 3,
"_id": "86AIY7iYlMNqJsDC/+LIMU"
},
{
"__type__": "cc.SceneGlobals",
"ambient": {
"__id__": 17
},
"shadows": {
"__id__": 18
},
"_skybox": {
"__id__": 19
},
"fog": {
"__id__": 20
},
"octree": {
"__id__": 21
},
"skin": {
"__id__": 22
},
"lightProbeInfo": {
"__id__": 23
},
"postSettings": {
"__id__": 24
},
"bakedWithStationaryMainLight": false,
"bakedWithHighpLightmap": false
},
{
"__type__": "cc.AmbientInfo",
"_skyColorHDR": {
"__type__": "cc.Vec4",
"x": 0.2,
"y": 0.5,
"z": 0.8,
"w": 0.520833125
},
"_skyColor": {
"__type__": "cc.Vec4",
"x": 0.2,
"y": 0.5,
"z": 0.8,
"w": 0.520833125
},
"_skyIllumHDR": 20000,
"_skyIllum": 20000,
"_groundAlbedoHDR": {
"__type__": "cc.Vec4",
"x": 0.2,
"y": 0.2,
"z": 0.2,
"w": 1
},
"_groundAlbedo": {
"__type__": "cc.Vec4",
"x": 0.2,
"y": 0.2,
"z": 0.2,
"w": 1
},
"_skyColorLDR": {
"__type__": "cc.Vec4",
"x": 0.452588,
"y": 0.607642,
"z": 0.755699,
"w": 0
},
"_skyIllumLDR": 0.8,
"_groundAlbedoLDR": {
"__type__": "cc.Vec4",
"x": 0.618555,
"y": 0.577848,
"z": 0.544564,
"w": 0
}
},
{
"__type__": "cc.ShadowsInfo",
"_enabled": false,
"_type": 0,
"_normal": {
"__type__": "cc.Vec3",
"x": 0,
"y": 1,
"z": 0
},
"_distance": 0,
"_planeBias": 1,
"_shadowColor": {
"__type__": "cc.Color",
"r": 76,
"g": 76,
"b": 76,
"a": 255
},
"_maxReceived": 4,
"_size": {
"__type__": "cc.Vec2",
"x": 1024,
"y": 1024
}
},
{
"__type__": "cc.SkyboxInfo",
"_envLightingType": 0,
"_envmapHDR": {
"__uuid__": "d032ac98-05e1-4090-88bb-eb640dcb5fc1@b47c0",
"__expectedType__": "cc.TextureCube"
},
"_envmap": {
"__uuid__": "d032ac98-05e1-4090-88bb-eb640dcb5fc1@b47c0",
"__expectedType__": "cc.TextureCube"
},
"_envmapLDR": {
"__uuid__": "6f01cf7f-81bf-4a7e-bd5d-0afc19696480@b47c0",
"__expectedType__": "cc.TextureCube"
},
"_diffuseMapHDR": null,
"_diffuseMapLDR": null,
"_enabled": true,
"_useHDR": true,
"_editableMaterial": null,
"_reflectionHDR": null,
"_reflectionLDR": null,
"_rotationAngle": 0
},
{
"__type__": "cc.FogInfo",
"_type": 0,
"_fogColor": {
"__type__": "cc.Color",
"r": 200,
"g": 200,
"b": 200,
"a": 255
},
"_enabled": false,
"_fogDensity": 0.3,
"_fogStart": 0.5,
"_fogEnd": 300,
"_fogAtten": 5,
"_fogTop": 1.5,
"_fogRange": 1.2,
"_accurate": false
},
{
"__type__": "cc.OctreeInfo",
"_enabled": false,
"_minPos": {
"__type__": "cc.Vec3",
"x": -1024,
"y": -1024,
"z": -1024
},
"_maxPos": {
"__type__": "cc.Vec3",
"x": 1024,
"y": 1024,
"z": 1024
},
"_depth": 8
},
{
"__type__": "cc.SkinInfo",
"_enabled": true,
"_blurRadius": 0.01,
"_sssIntensity": 3
},
{
"__type__": "cc.LightProbeInfo",
"_giScale": 1,
"_giSamples": 1024,
"_bounces": 2,
"_reduceRinging": 0,
"_showProbe": true,
"_showWireframe": true,
"_showConvex": false,
"_data": null,
"_lightProbeSphereVolume": 1
},
{
"__type__": "cc.PostSettingsInfo",
"_toneMappingType": 0
}
]

View File

@@ -0,0 +1,11 @@
{
"ver": "1.1.50",
"importer": "scene",
"imported": true,
"uuid": "ff354f0b-c2f5-4dea-8ffb-0152d175d11c",
"files": [
".json"
],
"subMetas": {},
"userData": {}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
{
"ver": "1.1.50",
"importer": "scene",
"imported": true,
"uuid": "fcbf2917-6d43-4528-8829-7ee089594879",
"files": [
".json"
],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "1556cd72-9618-4f9f-b9e7-28152a33bde9",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,130 @@
import { _decorator, Component, Node, Vec3, Color } from 'cc';
import { SimplePrefabFactory } from './components/SimplePrefabFactory';
import { BehaviorTreeComponent } from './components/BehaviorTreeComponent';
import { StatusUIManager } from './components/StatusUIManager';
const { ccclass, property } = _decorator;
/**
* 矿工AI演示场景
*/
@ccclass('SimpleMinerDemo')
export class SimpleMinerDemo extends Component {
@property
minerCount: number = 1;
@property
goldMineCount: number = 3;
private miners: Node[] = [];
private goldMines: Node[] = [];
private warehouse: Node | null = null;
private ground: Node | null = null;
private totalOresCollected: number = 0;
private warehouseUI: any = null;
start() {
this.createWorld();
this.createWarehouse();
this.createGoldMines();
this.createMiners();
}
private createWorld() {
this.ground = SimplePrefabFactory.createGround(new Vec3(20, 0.2, 20));
this.node.addChild(this.ground);
this.ground.setWorldPosition(new Vec3(0, 0, 0));
}
private createWarehouse() {
this.warehouse = SimplePrefabFactory.createBuilding('Warehouse', new Vec3(2, 2, 2), Color.GRAY);
this.node.addChild(this.warehouse);
this.warehouse.setWorldPosition(new Vec3(0, 1, 0));
this.createWarehouseUI();
}
private createGoldMines() {
for (let i = 0; i < this.goldMineCount; i++) {
const angle = (i / this.goldMineCount) * Math.PI * 2;
const radius = 6 + Math.random() * 2;
const position = new Vec3(
Math.cos(angle) * radius,
0.8,
Math.sin(angle) * radius
);
const goldMine = SimplePrefabFactory.createResource(`GoldMine_${i + 1}`, Color.YELLOW);
this.node.addChild(goldMine);
goldMine.setWorldPosition(position);
goldMine.setScale(new Vec3(1.2, 1.2, 1.2));
this.goldMines.push(goldMine);
}
}
private createMiners() {
for (let i = 0; i < this.minerCount; i++) {
const angle = (i / this.minerCount) * Math.PI * 2;
const radius = 3;
const position = new Vec3(
Math.cos(angle) * radius,
1,
Math.sin(angle) * radius
);
const miner = SimplePrefabFactory.createUnit(`Miner_${i + 1}`, Color.BLUE);
this.node.addChild(miner);
miner.setWorldPosition(position);
const behaviorTree = miner.addComponent(BehaviorTreeComponent);
behaviorTree.behaviorTreeFile = 'miner-stamina-ai.bt';
behaviorTree.debugMode = true;
this.scheduleOnce(() => {
const blackboard = behaviorTree.getBlackboard();
if (blackboard) {
blackboard.setValue('homePosition', position.clone());
}
}, 0.5);
this.miners.push(miner);
}
}
public getAllGoldMines(): Node[] {
return this.goldMines.filter(mine => mine && mine.isValid);
}
public getWarehouse(): Node | null {
return this.warehouse;
}
public mineGoldOre(miner: Node): boolean {
this.totalOresCollected++;
this.updateWarehouseUI();
return true;
}
public getTotalOresCollected(): number {
return this.totalOresCollected;
}
private createWarehouseUI() {
if (!this.warehouse) return;
this.warehouseUI = StatusUIManager.createWarehouseUI(this.warehouse);
if (this.warehouseUI) {
this.updateWarehouseUI();
}
}
private updateWarehouseUI() {
if (this.warehouseUI && this.warehouseUI.warehouseCountLabel) {
this.warehouseUI.warehouseCountLabel.string = `🏭 总存储: ${this.totalOresCollected}`;
}
}
onDestroy() {
this.unscheduleAllCallbacks();
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "c3386f4a-9bef-416f-b770-fcec91cedbc4",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "d07d95ad-f180-4b6e-9d0a-7248e75ec795",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,518 @@
import { Node, resources, JsonAsset, Component, _decorator, Vec3, tween, instantiate, Prefab } from 'cc';
import { BehaviorTree, BehaviorTreeBuilder, Blackboard, TaskStatus, BehaviorTreeJSONConfig, EventRegistry, IBehaviorTreeContext, ActionResult } from '@esengine/ai';
import { MinerStatusUI } from './MinerStatusUI';
import { StatusUIManager } from './StatusUIManager';
const { ccclass, property } = _decorator;
@ccclass('BehaviorTreeComponent')
export class BehaviorTreeComponent extends Component {
@property
behaviorTreeFile: string = '';
@property
autoStart: boolean = true;
@property
debugMode: boolean = false;
@property
showStatusUI: boolean = true;
@property(Prefab)
statusUIPrefab: Prefab | null = null;
private behaviorTree: BehaviorTree<any> | null = null;
private statusUI: MinerStatusUI | null = null;
private blackboard: Blackboard | null = null;
private context: any = null;
private eventRegistry: EventRegistry | null = null;
private isLoaded: boolean = false;
private isRunning: boolean = false;
private actionStates: Map<string, {
isExecuting: boolean;
startTime: number;
duration: number;
}> = new Map();
start() {
if (this.autoStart && this.behaviorTreeFile) {
this.initialize();
}
if (this.showStatusUI) {
this.createStatusUI();
}
}
async initialize() {
if (!this.behaviorTreeFile) {
return;
}
try {
await this.loadBehaviorTree();
this.isLoaded = true;
this.isRunning = true;
} catch (error) {
// 静默处理
}
}
private async loadBehaviorTree(): Promise<void> {
return new Promise((resolve, reject) => {
let jsonPath = this.behaviorTreeFile;
resources.load(jsonPath, JsonAsset, (err, asset) => {
if (err) {
reject(err);
return;
}
try {
const treeData = asset.json as BehaviorTreeJSONConfig;
this.buildBehaviorTree(treeData);
resolve();
} catch (buildError) {
reject(buildError);
}
});
});
}
private buildBehaviorTree(treeData: BehaviorTreeJSONConfig) {
this.eventRegistry = new EventRegistry();
this.setupEventHandlers();
const baseContext = {
node: this.node,
component: this,
eventRegistry: this.eventRegistry
};
const result = BehaviorTreeBuilder.fromBehaviorTreeConfig(treeData, baseContext);
this.behaviorTree = result.tree;
this.blackboard = result.blackboard;
this.context = result.context;
this.initializeBlackboard();
}
private setupEventHandlers() {
if (!this.eventRegistry) return;
this.eventRegistry.registerAction('go-home-rest', (context, params) => {
return this.handleGoHomeRest(context, params);
});
this.eventRegistry.registerAction('recover-stamina', (context, params) => {
return this.handleRecoverStamina(context, params);
});
this.eventRegistry.registerAction('store-ore', (context, params) => {
return this.handleStoreOre(context, params);
});
this.eventRegistry.registerAction('mine-gold-ore', (context, params) => {
return this.handleMineGoldOre(context, params);
});
this.eventRegistry.registerAction('idle-behavior', (context, params) => {
return this.handleIdleBehavior(context, params);
});
}
private initializeBlackboard() {
if (!this.blackboard) return;
this.blackboard.setValue('stamina', 100);
this.blackboard.setValue('staminaPercentage', 1.0);
this.blackboard.setValue('isLowStamina', false);
this.blackboard.setValue('hasOre', false);
this.blackboard.setValue('isResting', false);
this.blackboard.setValue('homePosition', this.node.worldPosition);
}
private createStatusUI() {
if (!this.statusUIPrefab) {
this.createSimpleStatusUI();
return;
}
const uiNode = instantiate(this.statusUIPrefab);
const canvas = this.node.scene?.getChildByName('Canvas');
if (canvas) {
canvas.addChild(uiNode);
this.statusUI = uiNode.getComponent(MinerStatusUI);
if (this.statusUI) {
this.statusUI.setFollowTarget(this.node);
}
}
}
private createSimpleStatusUI() {
this.statusUI = StatusUIManager.createStatusUIForMiner(this.node);
}
private updateStatusUI() {
if (!this.statusUI || !this.blackboard) return;
const stamina = this.blackboard.getValue('stamina') || 0;
const maxStamina = this.blackboard.getValue('maxStamina') || 100;
const hasOre = this.blackboard.getValue('hasOre') || false;
const isResting = this.blackboard.getValue('isResting') || false;
// 更新体力
this.statusUI.updateStamina(stamina, maxStamina);
// 更新状态文本
let status = '';
if (isResting) {
status = '😴休息中';
} else if (hasOre) {
status = '🚚运输中';
} else {
status = '⛏️挖矿中';
}
this.statusUI.updateStatus(status);
// 获取仓库矿石总数
const gameManager = this.node.parent?.getComponent('SimpleMinerDemo');
const warehouseTotal = (gameManager as any)?.getTotalOresCollected() || 0;
// 更新矿石数量显示
this.statusUI.updateOreCount(hasOre, warehouseTotal);
// 更新动作进度
this.updateActionProgressUI();
}
private updateActionProgressUI() {
if (!this.statusUI) return;
let actionName = '';
let progress = 0;
// 检查当前正在执行的动作
for (const [key, state] of this.actionStates.entries()) {
if (state.isExecuting) {
const elapsed = Date.now() - state.startTime;
progress = Math.min(elapsed / state.duration, 1.0);
switch (key) {
case 'mine-gold-ore':
actionName = '⛏️ 挖掘中';
break;
case 'store-ore':
actionName = '📦 存储中';
break;
case 'recover-stamina':
actionName = '💤 恢复体力';
break;
default:
actionName = key;
}
break; // 只显示第一个正在执行的动作
}
}
// 如果没有正在执行的动作,清空进度显示
this.statusUI.updateActionProgress(actionName, progress);
}
// ==================== 行为树事件处理器 ====================
/**
* 清理动作状态 - 当动作被中止时调用
*/
private clearActionState(actionKey: string) {
if (this.actionStates.has(actionKey)) {
this.actionStates.delete(actionKey);
}
}
/**
* 回家休息 - 包含体力恢复逻辑
*/
private handleGoHomeRest(context: any, params: any): ActionResult {
const blackboard = this.blackboard;
if (!blackboard) return 'failure';
// 检查是否已经在家了
const homePos = blackboard.getValue('homePosition') || this.node.worldPosition;
const distance = Vec3.distance(this.node.worldPosition, homePos);
if (distance > 1.0) {
// 还没到家,继续移动
this.moveToPosition(homePos, 2.0);
return 'running';
} else {
this.clearActionState('mine-gold-ore');
this.clearActionState('store-ore');
blackboard.setValue('isResting', true);
const actionKey = 'go-home-rest';
const currentTime = Date.now();
// 初始化休息状态
if (!this.actionStates.has(actionKey)) {
this.actionStates.set(actionKey, {
isExecuting: true,
startTime: currentTime,
duration: 2000 // 2秒恢复一次
});
return 'running';
}
const actionState = this.actionStates.get(actionKey)!;
const elapsed = currentTime - actionState.startTime;
if (elapsed >= actionState.duration) {
const currentStamina = blackboard.getValue('stamina');
const newStamina = Math.min(100, currentStamina + 10);
blackboard.setValue('stamina', newStamina);
blackboard.setValue('staminaPercentage', newStamina / 100);
if (newStamina >= 80) {
blackboard.setValue('isResting', false);
blackboard.setValue('isLowStamina', false);
this.actionStates.delete(actionKey);
return 'success';
}
actionState.startTime = currentTime;
}
return 'running';
}
}
private handleRecoverStamina(context: any, params: any): ActionResult {
return 'success';
}
private handleMineGoldOre(context: any, params: any): ActionResult {
const blackboard = this.blackboard;
if (!blackboard) return 'failure';
const hasOre = blackboard.getValue('hasOre');
const isLowStamina = blackboard.getValue('isLowStamina');
const isResting = blackboard.getValue('isResting');
if (hasOre || isLowStamina || isResting) {
return 'failure';
}
const gameManager = this.node.parent?.getComponent('SimpleMinerDemo');
const goldMines = (gameManager as any)?.getAllGoldMines();
if (!goldMines?.length) return 'failure';
let nearestMine = goldMines[0];
let minDistance = Vec3.distance(this.node.worldPosition, nearestMine.worldPosition);
for (const mine of goldMines) {
const distance = Vec3.distance(this.node.worldPosition, mine.worldPosition);
if (distance < minDistance) {
minDistance = distance;
nearestMine = mine;
}
}
if (minDistance > 2.0) {
this.moveToPosition(nearestMine.worldPosition, 2.0);
return 'running';
} else {
const actionKey = 'mine-gold-ore';
const currentTime = Date.now();
if (!this.actionStates.has(actionKey)) {
this.actionStates.set(actionKey, {
isExecuting: true,
startTime: currentTime,
duration: 3000
});
return 'running';
}
const actionState = this.actionStates.get(actionKey)!;
const elapsed = currentTime - actionState.startTime;
if (elapsed >= actionState.duration) {
const currentStamina = blackboard.getValue('stamina');
const newStamina = Math.max(0, currentStamina - 15);
blackboard.setValue('stamina', newStamina);
blackboard.setValue('staminaPercentage', newStamina / 100);
blackboard.setValue('hasOre', true);
blackboard.setValue('isLowStamina', newStamina < 20);
this.actionStates.delete(actionKey);
return 'failure';
}
return 'running';
}
}
private handleStoreOre(context: any, params: any): ActionResult {
const blackboard = this.blackboard;
if (!blackboard) return 'failure';
const hasOre = blackboard.getValue('hasOre');
if (!hasOre) {
return 'failure';
}
const isLowStamina = blackboard.getValue('isLowStamina');
if (isLowStamina) {
return 'failure';
}
this.clearActionState('mine-gold-ore');
const gameManager = this.node.parent?.getComponent('SimpleMinerDemo');
const warehouse = (gameManager as any)?.getWarehouse();
if (!warehouse) return 'failure';
const distance = Vec3.distance(this.node.worldPosition, warehouse.worldPosition);
if (distance > 2.0) {
this.moveToPosition(warehouse.worldPosition, 2.0);
return 'running';
} else {
const actionKey = 'store-ore';
const currentTime = Date.now();
if (!this.actionStates.has(actionKey)) {
this.actionStates.set(actionKey, {
isExecuting: true,
startTime: currentTime,
duration: 1500
});
return 'running';
}
const actionState = this.actionStates.get(actionKey)!;
const elapsed = currentTime - actionState.startTime;
if (elapsed >= actionState.duration) {
blackboard.setValue('hasOre', false);
(gameManager as any).mineGoldOre(this.node);
this.actionStates.delete(actionKey);
return 'success';
}
return 'running';
}
}
private handleIdleBehavior(context: any, params: any): ActionResult {
return 'success';
}
private moveToPosition(targetPos: Vec3, duration: number) {
tween(this.node).stop();
tween(this.node).to(duration, { worldPosition: targetPos }).start();
}
update(deltaTime: number) {
if (this.behaviorTree && this.isRunning) {
this.behaviorTree.tick(deltaTime);
}
if (this.showStatusUI) {
this.updateStatusUI();
}
}
/**
* 获取黑板
*/
getBlackboard(): Blackboard | null {
return this.blackboard;
}
/**
* 获取行为树
*/
getBehaviorTree(): BehaviorTree<any> | null {
return this.behaviorTree;
}
/**
* 暂停行为树
*/
pause() {
this.isRunning = false;
if (this.debugMode) {
}
}
/**
* 恢复行为树
*/
resume() {
if (this.isLoaded) {
this.isRunning = true;
if (this.debugMode) {
}
}
}
/**
* 停止行为树
*/
stop() {
this.isRunning = false;
if (this.behaviorTree) {
this.behaviorTree.reset();
}
if (this.debugMode) {
}
}
/**
* 重新加载行为树
*/
async reload() {
this.stop();
await this.initialize();
}
/**
* 重置行为树状态
*/
reset() {
if (this.behaviorTree) {
this.behaviorTree.reset();
}
if (this.debugMode) {
}
}
onDestroy() {
this.stop();
if (this.eventRegistry) {
this.eventRegistry.clear();
}
// 清理UI
if (this.statusUI) {
this.statusUI.node.destroy();
this.statusUI = null;
}
this.behaviorTree = null;
this.blackboard = null;
this.context = null;
this.eventRegistry = null;
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "8efd182b-9891-4903-bef2-eb07b5184263",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,299 @@
import { _decorator, Component, resources, JsonAsset, Vec3 } from 'cc';
import { BehaviorTree, BehaviorTreeBuilder, Blackboard, BehaviorTreeJSONConfig, ExecutionContext, EventRegistry, ActionResult } from '@esengine/ai';
import { UnitController } from './UnitController';
import { RTSBehaviorHandler } from './RTSBehaviorHandler';
const { ccclass, property } = _decorator;
/**
* 游戏执行上下文接口
* 继承框架的ExecutionContext添加游戏特定的属性
*/
interface GameExecutionContext extends ExecutionContext {
unitController: UnitController;
gameObject: any;
eventRegistry?: EventRegistry;
// 确保继承索引签名
[key: string]: unknown;
}
/**
* 行为树管理器 - 使用@esengine/ai包管理行为树
*/
@ccclass('BehaviorTreeManager')
export class BehaviorTreeManager extends Component {
@property
debugMode: boolean = true;
@property
tickInterval: number = 0.1; // 行为树更新间隔(秒)- 10fps更新频率平衡性能和响应性
private behaviorTree: BehaviorTree<GameExecutionContext> | null = null;
private blackboard: Blackboard | null = null;
private context: GameExecutionContext | null = null;
private eventRegistry: EventRegistry | null = null;
private isLoaded: boolean = false;
private isRunning: boolean = false;
private lastTickTime: number = 0;
private unitController: UnitController | null = null;
private currentBehaviorTreeName: string = '';
private behaviorHandler: RTSBehaviorHandler | null = null;
/**
* 初始化行为树
*/
async initializeBehaviorTree(behaviorTreeName: string, unitController: UnitController) {
this.currentBehaviorTreeName = behaviorTreeName;
this.unitController = unitController;
// 获取RTSBehaviorHandler组件
this.behaviorHandler = this.getComponent(RTSBehaviorHandler);
if (!this.behaviorHandler) {
console.error(`BehaviorTreeManager: 未找到RTSBehaviorHandler组件 - ${this.node.name}`);
return;
}
try {
await this.loadBehaviorTree(behaviorTreeName);
this.setupBlackboard();
this.isLoaded = true;
this.isRunning = true;
} catch (error) {
console.error(`行为树初始化失败: ${behaviorTreeName}`, error);
}
}
/**
* 加载行为树文件
*/
private async loadBehaviorTree(behaviorTreeName: string): Promise<void> {
return new Promise((resolve, reject) => {
const jsonPath = `${behaviorTreeName}.bt`;
resources.load(jsonPath, JsonAsset, (err, asset) => {
if (err) {
console.error(`加载行为树文件失败: ${jsonPath}`, err);
reject(err);
return;
}
try {
const behaviorTreeData = asset.json as BehaviorTreeJSONConfig;
// 创建执行上下文
this.blackboard = new Blackboard();
this.eventRegistry = this.createEventRegistry();
this.context = {
blackboard: this.blackboard,
unitController: this.unitController!,
gameObject: this.node,
eventRegistry: this.eventRegistry
} as GameExecutionContext;
// 从JSON数据创建行为树
const buildResult = BehaviorTreeBuilder.fromBehaviorTreeConfig<GameExecutionContext>(behaviorTreeData, this.context);
this.behaviorTree = buildResult.tree;
this.blackboard = buildResult.blackboard;
resolve();
} catch (parseError) {
console.error(`创建行为树失败: ${jsonPath}`, parseError);
reject(parseError);
}
});
});
}
/**
* 创建事件注册表
*/
private createEventRegistry(): EventRegistry {
const registry = new EventRegistry();
// 注册体力系统矿工行为事件处理器
const eventHandlers = {
// 矿工体力系统核心行为
'mine-gold-ore': (context: any, params?: any) => this.callBehaviorHandler('onMineGoldOre', params),
'store-ore': (context: any, params?: any) => this.callBehaviorHandler('onStoreOre', params),
'go-home-rest': (context: any, params?: any) => this.callBehaviorHandler('onGoHomeRest', params),
'recover-stamina': (context: any, params?: any) => this.callBehaviorHandler('onRecoverStamina', params),
'idle-behavior': (context: any, params?: any) => this.callBehaviorHandler('onIdleBehavior', params)
};
// 将事件处理器注册到EventRegistry
Object.entries(eventHandlers).forEach(([eventName, handler]) => {
registry.registerAction(eventName, handler);
});
return registry;
}
/**
* 调用行为处理器的方法
*/
private callBehaviorHandler(methodName: string, params: any = {}): ActionResult {
if (!this.behaviorHandler) {
console.error(`BehaviorTreeManager: RTSBehaviorHandler未初始化 - ${this.node.name}`);
return 'failure';
}
try {
// 直接调用RTSBehaviorHandler的方法
const method = (this.behaviorHandler as any)[methodName];
if (typeof method === 'function') {
const result = method.call(this.behaviorHandler, params);
return result || 'success'; // 确保有返回值
} else {
console.error(`BehaviorTreeManager: 方法不存在: ${methodName}`);
return 'failure';
}
} catch (error) {
console.error(`BehaviorTreeManager: 调用方法失败: ${methodName}`, error);
return 'failure';
}
}
/**
* 设置黑板基础信息
*/
private setupBlackboard() {
if (!this.unitController || !this.blackboard) return;
// 设置矿工基础信息
this.blackboard.setValue('unitType', this.unitController.unitType);
this.blackboard.setValue('currentHealth', this.unitController.currentHealth);
this.blackboard.setValue('maxHealth', this.unitController.maxHealth);
this.blackboard.setValue('currentCommand', 'mine');
this.blackboard.setValue('hasOre', false);
this.blackboard.setValue('hasTarget', false);
this.blackboard.setValue('targetPosition', null);
this.blackboard.setValue('isMoving', false);
// 设置体力系统信息
this.blackboard.setValue('stamina', this.unitController.currentStamina);
this.blackboard.setValue('maxStamina', this.unitController.maxStamina);
this.blackboard.setValue('staminaPercentage', this.unitController.currentStamina / this.unitController.maxStamina);
this.blackboard.setValue('isLowStamina', this.unitController.currentStamina < this.unitController.maxStamina * 0.2);
this.blackboard.setValue('isResting', false);
this.blackboard.setValue('homePosition', this.unitController.homePosition);
}
/**
* 更新黑板值
*/
updateBlackboardValue(key: string, value: any) {
if (this.blackboard) {
this.blackboard.setValue(key, value);
}
}
/**
* 获取黑板值
*/
getBlackboardValue(key: string): any {
return this.blackboard?.getValue(key);
}
/**
* 获取黑板
*/
getBlackboard(): Blackboard | null {
return this.blackboard;
}
/**
* 获取行为树
*/
getBehaviorTree(): BehaviorTree<GameExecutionContext> | null {
return this.behaviorTree;
}
/**
* 更新行为树
*/
update(deltaTime: number) {
if (!this.isLoaded || !this.isRunning || !this.behaviorTree || !this.blackboard) return;
// 控制更新频率
this.lastTickTime += deltaTime;
if (this.lastTickTime < this.tickInterval) return;
this.lastTickTime = 0;
// 更新矿工状态信息
if (this.unitController) {
// 基础属性
this.blackboard.setValue('currentHealth', this.unitController.currentHealth);
this.blackboard.setValue('currentCommand', this.unitController.currentCommand);
this.blackboard.setValue('hasTarget', this.unitController.targetPosition && !this.unitController.targetPosition.equals(Vec3.ZERO));
this.blackboard.setValue('targetPosition', this.unitController.targetPosition);
this.blackboard.setValue('isMoving', this.unitController.targetPosition && !this.unitController.targetPosition.equals(Vec3.ZERO));
// 体力系统状态
this.blackboard.setValue('stamina', this.unitController.currentStamina);
this.blackboard.setValue('maxStamina', this.unitController.maxStamina);
this.blackboard.setValue('staminaPercentage', this.unitController.currentStamina / this.unitController.maxStamina);
this.blackboard.setValue('isLowStamina', this.unitController.currentStamina < this.unitController.maxStamina * 0.2);
this.blackboard.setValue('homePosition', this.unitController.homePosition);
}
// 执行行为树
try {
this.behaviorTree.tick(deltaTime);
} catch (error) {
console.error(`行为树执行错误: ${this.node.name}`, error);
}
}
/**
* 暂停行为树
*/
pause() {
this.isRunning = false;
}
/**
* 恢复行为树
*/
resume() {
if (this.isLoaded) {
this.isRunning = true;
}
}
/**
* 停止行为树
*/
stop() {
this.isRunning = false;
}
/**
* 重新加载行为树
*/
async reloadBehaviorTree() {
if (this.currentBehaviorTreeName && this.unitController) {
this.stop();
await this.initializeBehaviorTree(this.currentBehaviorTreeName, this.unitController);
}
}
/**
* 重置行为树
*/
reset() {
if (this.behaviorTree) {
this.behaviorTree.reset();
}
}
onDestroy() {
this.stop();
this.behaviorTree = null;
this.blackboard = null;
this.context = null;
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "891c88fc-282d-4791-a961-8d85244bfee7",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,130 @@
import { Component, _decorator, Label, ProgressBar, Node, UITransform, Canvas, find, Camera, Vec3, director, Color, Layers, Graphics } from 'cc';
const { ccclass, property } = _decorator;
/**
* 矿工状态UI组件
*/
@ccclass('MinerStatusUI')
export class MinerStatusUI extends Component {
nameLabel: Label | null = null;
statusLabel: Label | null = null;
staminaBar: ProgressBar | null = null;
actionProgressBar: ProgressBar | null = null;
actionLabel: Label | null = null;
oreCountLabel: Label | null = null;
warehouseCountLabel: Label | null = null;
@property
followTarget: Node | null = null;
@property
yOffset: number = 100;
private camera: Camera | null = null;
private canvas: Canvas | null = null;
start() {
this.node.layer = Layers.Enum.UI_2D;
this.camera = find('Main Camera')?.getComponent(Camera) || director.getScene()?.getComponentInChildren(Camera);
this.canvas = find('Canvas')?.getComponent(Canvas) || director.getScene()?.getComponentInChildren(Canvas);
if (this.nameLabel && this.followTarget) {
this.nameLabel.string = this.followTarget.name;
}
this.updateStamina(100, 100);
this.updateStatus('待机中');
this.updateActionProgress('', 0);
}
update() {
if (this.followTarget && this.camera && this.canvas) {
this.updateUIPosition();
}
}
private updateUIPosition() {
if (!this.followTarget || !this.camera || !this.canvas) return;
const targetWorldPos = this.followTarget.worldPosition.clone();
// 根据目标类型设置不同的Y偏移
if (this.followTarget.name.includes('Warehouse')) {
targetWorldPos.y += 3.0; // 仓库偏移更高
} else {
targetWorldPos.y += 2.0; // 矿工偏移
}
// 将世界坐标直接转换为UI坐标
const uiPos = new Vec3();
this.camera.convertToUINode(targetWorldPos, this.canvas.node, uiPos);
this.node.setPosition(uiPos);
}
setFollowTarget(target: Node) {
this.followTarget = target;
if (this.nameLabel) {
this.nameLabel.string = target.name;
}
}
updateStamina(current: number, max: number) {
if (this.staminaBar) {
this.staminaBar.progress = current / max;
}
if (this.staminaBar) {
const percentage = current / max;
const fillNode = this.staminaBar.node.getChildByName('Bar');
if (fillNode) {
const graphics = fillNode.getComponent(Graphics);
if (graphics) {
let color: Color;
if (percentage > 0.6) {
color = new Color(0, 255, 0, 255);
} else if (percentage > 0.3) {
color = new Color(255, 255, 0, 255);
} else {
color = new Color(255, 0, 0, 255);
}
graphics.clear();
graphics.fillColor = color;
graphics.rect(-75, -4, 150 * percentage, 8);
graphics.fill();
}
}
}
}
updateStatus(status: string) {
if (this.statusLabel) {
this.statusLabel.string = status;
}
}
updateActionProgress(actionName: string, progress: number) {
if (this.actionLabel) {
this.actionLabel.string = actionName;
}
if (this.actionProgressBar) {
this.actionProgressBar.progress = Math.max(0, Math.min(1, progress));
this.actionProgressBar.node.active = actionName !== '' && progress > 0;
}
}
setVisible(visible: boolean) {
this.node.active = visible;
}
updateOreCount(hasOre: boolean, warehouseTotal: number) {
if (this.oreCountLabel) {
this.oreCountLabel.string = hasOre ? '💎1' : '💎0';
}
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "5f877c25-5c26-49c6-bbb5-7ff36323e0a1",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,334 @@
import { _decorator, Component, Vec3, Node } from 'cc';
import { UnitController } from './UnitController';
const { ccclass } = _decorator;
/**
* 矿工体力系统行为处理器 - 处理挖矿、休息、存储的完整循环
* 展示体力驱动的工作-休息循环系统
*/
@ccclass('RTSBehaviorHandler')
export class RTSBehaviorHandler extends Component {
private unitController: UnitController | null = null;
private minerDemo: any = null; // MinerDemo组件引用
private lastActionTime: number = 0;
private actionCooldown: number = 0.5; // 动作冷却时间,避免频繁切换
private minerIndex: number = -1; // 矿工索引,用于找到对应的家
start() {
this.unitController = this.getComponent(UnitController);
// 获取场景中的MinerDemo组件
this.minerDemo = this.node.parent?.getComponent('MinerDemo');
if (!this.unitController) {
console.error('RTSBehaviorHandler: 未找到UnitController组件');
}
if (!this.minerDemo) {
console.error('RTSBehaviorHandler: 未找到MinerDemo组件');
}
// 从节点名称中提取矿工索引
const match = this.node.name.match(/Miner_(\d+)/);
if (match) {
this.minerIndex = parseInt(match[1]) - 1; // 转换为0基索引
}
this.lastActionTime = Date.now();
}
/**
* 检查动作冷却
*/
private isActionOnCooldown(): boolean {
return (Date.now() - this.lastActionTime) < (this.actionCooldown * 1000);
}
/**
* 更新动作时间
*/
private updateActionTime() {
this.lastActionTime = Date.now();
}
/**
* 挖掘金矿(永不枯竭)
* @param params 事件参数,包含黑板变量值
*/
onMineGoldOre(params: any = {}): string {
if (!this.unitController || !this.minerDemo) {
return 'failure';
}
// 检查体力是否充足
if (this.unitController.currentStamina < this.unitController.staminaCostPerMining) {
return 'failure';
}
// 检查是否已经携带矿石
const hasOre = this.unitController.getBlackboardValue('hasOre');
if (hasOre) {
return 'failure';
}
// 动作冷却检查
if (this.isActionOnCooldown()) {
return 'running';
}
// 获取所有金矿
const goldMines = this.minerDemo.getAllGoldMines();
if (goldMines.length === 0) {
return 'failure';
}
// 寻找最近的金矿
const currentPos = this.node.worldPosition;
let nearestMine: Node | null = null;
let minDistance = Infinity;
for (const mine of goldMines) {
if (!mine || !mine.isValid) continue;
const distance = Vec3.distance(currentPos, mine.worldPosition);
if (distance < minDistance) {
minDistance = distance;
nearestMine = mine;
}
}
if (!nearestMine) {
return 'failure';
}
// 检查是否已经到达金矿位置
if (minDistance < 2.0) {
// 检查是否正在移动
const isMoving = this.unitController.getBlackboardValue('isMoving');
if (isMoving) {
return 'running';
}
// 消耗体力
this.unitController.currentStamina = Math.max(0, this.unitController.currentStamina - this.unitController.staminaCostPerMining);
// 设置携带矿石状态
this.unitController.setBlackboardValue('hasOre', true);
// 通知演示管理器
this.minerDemo.mineGoldOre(this.node);
// 清除移动目标
this.unitController.clearTarget();
this.unitController.setBlackboardValue('isMoving', false);
this.updateActionTime();
return 'success';
} else {
// 设置移动目标
this.unitController.setTarget(nearestMine.worldPosition);
return 'running';
}
}
/**
* 前往仓库存储矿石
* @param params 事件参数,包含黑板变量值
*/
onStoreOre(params: any = {}): string {
if (!this.unitController || !this.minerDemo) {
return 'failure';
}
// 检查是否携带矿石
const hasOre = this.unitController.getBlackboardValue('hasOre');
if (!hasOre) {
return 'failure';
}
// 动作冷却检查
if (this.isActionOnCooldown()) {
return 'running';
}
const warehouse = this.minerDemo.getWarehouse();
if (!warehouse || !warehouse.isValid) {
return 'failure';
}
// 计算到仓库的距离
const currentPos = this.node.worldPosition;
const warehousePos = warehouse.worldPosition;
const distance = Vec3.distance(currentPos, warehousePos);
// 检查是否已经到达仓库
if (distance < 2.5) {
// 检查是否正在移动
const isMoving = this.unitController.getBlackboardValue('isMoving');
if (isMoving) {
return 'running';
}
// 清除携带矿石状态
this.unitController.setBlackboardValue('hasOre', false);
// 清除移动目标
this.unitController.clearTarget();
this.unitController.setBlackboardValue('isMoving', false);
this.updateActionTime();
return 'success';
} else {
// 设置移动目标
this.unitController.setTarget(warehousePos);
return 'running';
}
}
/**
* 回家休息
* @param params 事件参数,包含黑板变量值
*/
onGoHomeRest(params: any = {}): string {
if (!this.unitController || !this.minerDemo) {
return 'failure';
}
// 动作冷却检查
if (this.isActionOnCooldown()) {
return 'running';
}
// 获取矿工的家
const home = this.minerDemo.getMinerHome(this.minerIndex);
if (!home || !home.isValid) {
return 'failure';
}
// 计算到家的距离
const currentPos = this.node.worldPosition;
const homePos = home.worldPosition;
const distance = Vec3.distance(currentPos, homePos);
// 检查是否已经到达家
if (distance < 2.0) {
// 检查是否正在移动
const isMoving = this.unitController.getBlackboardValue('isMoving');
if (isMoving) {
return 'running';
}
// 设置休息状态
this.unitController.setBlackboardValue('isResting', true);
// 清除移动目标
this.unitController.clearTarget();
this.unitController.setBlackboardValue('isMoving', false);
this.updateActionTime();
return 'success';
} else {
// 设置移动目标
this.unitController.setTarget(homePos);
return 'running';
}
}
/**
* 恢复体力
* @param params 事件参数,包含黑板变量值
*/
onRecoverStamina(params: any = {}): string {
if (!this.unitController) {
return 'failure';
}
// 检查是否在家中
const isResting = this.unitController.getBlackboardValue('isResting');
if (!isResting) {
return 'failure';
}
// 恢复体力
const oldStamina = this.unitController.currentStamina;
this.unitController.currentStamina = Math.min(this.unitController.maxStamina,
this.unitController.currentStamina + this.unitController.staminaRecoveryRate * 0.1); // 每次恢复2点体力
const isFullyRested = this.unitController.currentStamina >= this.unitController.maxStamina;
if (isFullyRested) {
// 清除休息状态
this.unitController.setBlackboardValue('isResting', false);
// 通知演示管理器
this.minerDemo.completeRestCycle();
this.updateActionTime();
return 'success';
} else {
// 体力还在恢复中
return 'running';
}
}
/**
* 待机行为
* @param params 事件参数,包含黑板变量值
*/
onIdleBehavior(params: any = {}): string {
if (!this.unitController) {
return 'failure';
}
// 清除移动目标,确保停止移动
this.unitController.clearTarget();
this.unitController.setBlackboardValue('isMoving', false);
return 'success';
}
/**
* 获取矿工状态摘要
*/
getMinerStatus(): string {
if (!this.unitController) return 'Unknown';
const hasOre = this.unitController.getBlackboardValue('hasOre');
const isMoving = this.unitController.getBlackboardValue('isMoving');
const isResting = this.unitController.getBlackboardValue('isResting');
const stamina = this.unitController.currentStamina;
const maxStamina = this.unitController.maxStamina;
let status = '';
if (isResting) {
status = '😴休息中';
} else if (hasOre) {
status = isMoving ? '🚚运输中' : '📦携带矿石';
} else {
status = isMoving ? '🚶移动中' : '⛏️挖矿';
}
return `${status} (体力:${stamina.toFixed(0)}/${maxStamina})`;
}
/**
* 调试信息
*/
getDebugInfo(): any {
if (!this.unitController) return {};
return {
name: this.node.name,
hasOre: this.unitController.getBlackboardValue('hasOre'),
isMoving: this.unitController.getBlackboardValue('isMoving'),
isResting: this.unitController.getBlackboardValue('isResting'),
stamina: this.unitController.currentStamina,
maxStamina: this.unitController.maxStamina,
staminaPercentage: this.unitController.currentStamina / this.unitController.maxStamina,
isLowStamina: this.unitController.currentStamina < this.unitController.maxStamina * 0.2,
status: this.getMinerStatus()
};
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "739ff9ee-42d5-4542-bb5b-3e7611c729e2",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,126 @@
import { _decorator, Component, Node, Vec3, MeshRenderer, BoxCollider, RigidBody, Material, Color, primitives, utils } from 'cc';
const { ccclass, property } = _decorator;
/**
* 简单预制体工厂
*/
@ccclass('SimplePrefabFactory')
export class SimplePrefabFactory extends Component {
/**
* 创建单位节点
*/
static createUnit(name: string, color: Color = Color.WHITE): Node {
const unit = new Node(name);
// 添加网格渲染器
const meshRenderer = unit.addComponent(MeshRenderer);
// 创建立方体网格
const mesh = utils.createMesh(primitives.box({ width: 1, height: 1, length: 1 }));
meshRenderer.mesh = mesh;
// 创建材质
const material = new Material();
material.initialize({ effectName: 'builtin-unlit' });
material.setProperty('mainColor', color);
meshRenderer.material = material;
// 添加碰撞器
const collider = unit.addComponent(BoxCollider);
collider.size = new Vec3(1, 1, 1);
// 添加刚体
const rigidBody = unit.addComponent(RigidBody);
rigidBody.type = RigidBody.Type.KINEMATIC;
return unit;
}
/**
* 创建建筑节点
*/
static createBuilding(name: string, size: Vec3 = new Vec3(2, 2, 2), color: Color = Color.GRAY): Node {
const building = new Node(name);
// 添加网格渲染器
const meshRenderer = building.addComponent(MeshRenderer);
// 创建立方体网格
const mesh = utils.createMesh(primitives.box({
width: size.x,
height: size.y,
length: size.z
}));
meshRenderer.mesh = mesh;
// 创建材质
const material = new Material();
material.initialize({ effectName: 'builtin-unlit' });
material.setProperty('mainColor', color);
meshRenderer.material = material;
// 添加碰撞器
const collider = building.addComponent(BoxCollider);
collider.size = size;
return building;
}
/**
* 创建资源节点
*/
static createResource(name: string, color: Color = Color.YELLOW): Node {
const resource = new Node(name);
// 添加网格渲染器
const meshRenderer = resource.addComponent(MeshRenderer);
// 创建球体网格
const mesh = utils.createMesh(primitives.sphere(0.5));
meshRenderer.mesh = mesh;
// 创建材质
const material = new Material();
material.initialize({ effectName: 'builtin-unlit' });
material.setProperty('mainColor', color);
meshRenderer.material = material;
// 添加碰撞器
const collider = resource.addComponent(BoxCollider);
collider.size = new Vec3(1, 1, 1);
return resource;
}
/**
* 创建地面节点
*/
static createGround(size: Vec3 = new Vec3(50, 0.1, 50), color: Color = new Color(100, 150, 100, 255)): Node {
const ground = new Node('Ground');
// 添加网格渲染器
const meshRenderer = ground.addComponent(MeshRenderer);
// 创建平面网格
const mesh = utils.createMesh(primitives.box({
width: size.x,
height: size.y,
length: size.z
}));
meshRenderer.mesh = mesh;
// 创建材质
const material = new Material();
material.initialize({ effectName: 'builtin-unlit' });
material.setProperty('mainColor', color);
meshRenderer.material = material;
// 添加碰撞器
const collider = ground.addComponent(BoxCollider);
collider.size = size;
return ground;
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "ac45cfc7-cf47-4315-bdf0-ba002b45b4b6",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,282 @@
import { Component, _decorator, Node, Label, ProgressBar, UITransform, Widget, Canvas, find, director, Color, Sprite, Layers, Graphics } from 'cc';
import { MinerStatusUI } from './MinerStatusUI';
const { ccclass, property } = _decorator;
/**
* 状态UI管理器
* 负责创建和管理游戏对象的状态显示界面
*/
@ccclass('StatusUIManager')
export class StatusUIManager extends Component {
/**
* 为矿工创建状态显示UI
*/
static createStatusUIForMiner(miner: Node): MinerStatusUI | null {
const canvas = find('Canvas') || director.getScene()?.getChildByName('Canvas');
if (!canvas) {
return null;
}
const uiRoot = new Node(`${miner.name}_StatusUI`);
canvas.addChild(uiRoot);
const uiTransform = uiRoot.addComponent(UITransform);
uiTransform.setContentSize(200, 100);
const borderNode = new Node('Border');
uiRoot.addChild(borderNode);
const borderTransform = borderNode.addComponent(UITransform);
borderTransform.setContentSize(202, 102);
const borderGraphics = borderNode.addComponent(Graphics);
borderGraphics.fillColor = new Color(100, 100, 100, 120);
borderGraphics.rect(-101, -51, 202, 102);
borderGraphics.fill();
const borderWidget = borderNode.addComponent(Widget);
borderWidget.isAlignTop = true;
borderWidget.isAlignBottom = true;
borderWidget.isAlignLeft = true;
borderWidget.isAlignRight = true;
borderWidget.top = -1;
borderWidget.bottom = -1;
borderWidget.left = -1;
borderWidget.right = -1;
borderWidget.updateAlignment();
const backgroundNode = new Node('Background');
uiRoot.addChild(backgroundNode);
const backgroundTransform = backgroundNode.addComponent(UITransform);
backgroundTransform.setContentSize(200, 100);
const backgroundGraphics = backgroundNode.addComponent(Graphics);
backgroundGraphics.fillColor = new Color(0, 0, 0, 100);
backgroundGraphics.rect(-100, -50, 200, 100);
backgroundGraphics.fill();
const statusUI = uiRoot.addComponent(MinerStatusUI);
statusUI.setFollowTarget(miner);
const nameNode = new Node('NameLabel');
uiRoot.addChild(nameNode);
const nameTransform = nameNode.addComponent(UITransform);
nameTransform.setContentSize(200, 25);
const nameLabel = nameNode.addComponent(Label);
nameLabel.string = miner.name;
nameLabel.fontSize = 16;
nameLabel.color = new Color(255, 255, 255, 255);
const nameWidget = nameNode.addComponent(Widget);
nameWidget.isAlignTop = true;
nameWidget.top = 0;
nameWidget.isAlignHorizontalCenter = true;
nameWidget.updateAlignment();
// 创建状态标签
const statusNode = new Node('StatusLabel');
uiRoot.addChild(statusNode);
const statusTransform = statusNode.addComponent(UITransform);
statusTransform.setContentSize(200, 20);
const statusLabel = statusNode.addComponent(Label);
statusLabel.string = '待机中';
statusLabel.fontSize = 14;
statusLabel.color = new Color(200, 200, 200, 255);
// 设置状态标签位置
const statusWidget = statusNode.addComponent(Widget);
statusWidget.isAlignTop = true;
statusWidget.top = 25;
statusWidget.isAlignHorizontalCenter = true;
statusWidget.updateAlignment();
// 创建体力进度条
const staminaBarNode = new Node('StaminaBar');
uiRoot.addChild(staminaBarNode);
const staminaBarTransform = staminaBarNode.addComponent(UITransform);
staminaBarTransform.setContentSize(150, 8);
const staminaBar = staminaBarNode.addComponent(ProgressBar);
staminaBar.progress = 1.0;
// 创建体力进度条背景
const staminaBgNode = new Node('Background');
staminaBarNode.addChild(staminaBgNode);
const staminaBgTransform = staminaBgNode.addComponent(UITransform);
staminaBgTransform.setContentSize(150, 8);
const staminaBgGraphics = staminaBgNode.addComponent(Graphics);
staminaBgGraphics.fillColor = new Color(50, 50, 50, 255);
staminaBgGraphics.rect(-75, -4, 150, 8);
staminaBgGraphics.fill();
// 创建体力进度条填充
const staminaFillNode = new Node('Bar');
staminaBarNode.addChild(staminaFillNode);
const staminaFillTransform = staminaFillNode.addComponent(UITransform);
staminaFillTransform.setContentSize(150, 8);
const staminaFillGraphics = staminaFillNode.addComponent(Graphics);
staminaFillGraphics.fillColor = new Color(0, 255, 0, 255);
staminaFillGraphics.rect(-75, -4, 150, 8);
staminaFillGraphics.fill();
// 设置体力进度条位置
const staminaWidget = staminaBarNode.addComponent(Widget);
staminaWidget.isAlignTop = true;
staminaWidget.top = 45;
staminaWidget.isAlignHorizontalCenter = true;
staminaWidget.updateAlignment();
// 创建动作进度条
const actionBarNode = new Node('ActionProgressBar');
uiRoot.addChild(actionBarNode);
const actionBarTransform = actionBarNode.addComponent(UITransform);
actionBarTransform.setContentSize(150, 6);
const actionBar = actionBarNode.addComponent(ProgressBar);
actionBar.progress = 0;
actionBarNode.active = false; // 初始隐藏
// 创建动作进度条背景
const actionBgNode = new Node('Background');
actionBarNode.addChild(actionBgNode);
const actionBgTransform = actionBgNode.addComponent(UITransform);
actionBgTransform.setContentSize(150, 6);
const actionBgGraphics = actionBgNode.addComponent(Graphics);
actionBgGraphics.fillColor = new Color(50, 50, 50, 255);
actionBgGraphics.rect(-75, -3, 150, 6);
actionBgGraphics.fill();
// 创建动作进度条填充
const actionFillNode = new Node('Bar');
actionBarNode.addChild(actionFillNode);
const actionFillTransform = actionFillNode.addComponent(UITransform);
actionFillTransform.setContentSize(150, 6);
const actionFillGraphics = actionFillNode.addComponent(Graphics);
actionFillGraphics.fillColor = new Color(255, 255, 0, 255);
actionFillGraphics.rect(-75, -3, 150, 6);
actionFillGraphics.fill();
// 设置动作进度条位置
const actionWidget = actionBarNode.addComponent(Widget);
actionWidget.isAlignTop = true;
actionWidget.top = 55;
actionWidget.isAlignHorizontalCenter = true;
actionWidget.updateAlignment();
// 创建动作标签
const actionLabelNode = new Node('ActionLabel');
uiRoot.addChild(actionLabelNode);
const actionLabelTransform = actionLabelNode.addComponent(UITransform);
actionLabelTransform.setContentSize(200, 15);
const actionLabel = actionLabelNode.addComponent(Label);
actionLabel.string = '';
actionLabel.fontSize = 12;
actionLabel.color = new Color(255, 255, 0, 255);
// 设置动作标签位置
const actionLabelWidget = actionLabelNode.addComponent(Widget);
actionLabelWidget.isAlignTop = true;
actionLabelWidget.top = 65;
actionLabelWidget.isAlignHorizontalCenter = true;
actionLabelWidget.updateAlignment();
// 创建矿石数量标签
const oreCountNode = new Node('OreCountLabel');
uiRoot.addChild(oreCountNode);
const oreCountTransform = oreCountNode.addComponent(UITransform);
oreCountTransform.setContentSize(100, 15);
const oreCountLabel = oreCountNode.addComponent(Label);
oreCountLabel.string = '💎0';
oreCountLabel.fontSize = 12;
oreCountLabel.color = new Color(255, 215, 0, 255); // 金色
// 设置矿石数量标签位置(居中显示)
const oreCountWidget = oreCountNode.addComponent(Widget);
oreCountWidget.isAlignTop = true;
oreCountWidget.top = 80;
oreCountWidget.isAlignHorizontalCenter = true;
oreCountWidget.updateAlignment();
statusUI.nameLabel = nameLabel;
statusUI.statusLabel = statusLabel;
statusUI.staminaBar = staminaBar;
statusUI.actionProgressBar = actionBar;
statusUI.actionLabel = actionLabel;
statusUI.oreCountLabel = oreCountLabel;
statusUI.warehouseCountLabel = null;
StatusUIManager.setNodeLayerRecursively(uiRoot, Layers.Enum.UI_2D);
return statusUI;
}
/**
* 递归设置节点及其子节点的层级
*/
private static setNodeLayerRecursively(node: Node, layer: number) {
node.layer = layer;
for (const child of node.children) {
StatusUIManager.setNodeLayerRecursively(child, layer);
}
}
/**
* 从矿工名字中提取索引号
*/
private static extractMinerIndex(minerName: string): number {
const match = minerName.match(/Miner_(\d+)/);
if (match) {
return parseInt(match[1]) - 1;
}
return 0;
}
/**
* 为仓库创建存储量显示UI
*/
static createWarehouseUI(warehouse: Node): MinerStatusUI | null {
const canvas = find('Canvas') || director.getScene()?.getChildByName('Canvas');
if (!canvas) {
return null;
}
const uiRoot = new Node(`${warehouse.name}_StorageUI`);
canvas.addChild(uiRoot);
const uiTransform = uiRoot.addComponent(UITransform);
uiTransform.setContentSize(120, 40);
const backgroundNode = new Node('Background');
uiRoot.addChild(backgroundNode);
const backgroundTransform = backgroundNode.addComponent(UITransform);
backgroundTransform.setContentSize(120, 40);
const backgroundGraphics = backgroundNode.addComponent(Graphics);
backgroundGraphics.fillColor = new Color(0, 0, 0, 120);
backgroundGraphics.rect(-60, -20, 120, 40);
backgroundGraphics.fill();
const storageNode = new Node('StorageLabel');
uiRoot.addChild(storageNode);
const storageTransform = storageNode.addComponent(UITransform);
storageTransform.setContentSize(120, 30);
const storageLabel = storageNode.addComponent(Label);
storageLabel.string = '🏭 总存储: 0';
storageLabel.fontSize = 14;
storageLabel.color = new Color(255, 255, 255, 255);
const storageWidget = storageNode.addComponent(Widget);
storageWidget.isAlignHorizontalCenter = true;
storageWidget.isAlignVerticalCenter = true;
storageWidget.updateAlignment();
const statusUI = uiRoot.addComponent(MinerStatusUI);
statusUI.setFollowTarget(warehouse);
statusUI.nameLabel = null;
statusUI.statusLabel = null;
statusUI.staminaBar = null;
statusUI.actionProgressBar = null;
statusUI.actionLabel = null;
statusUI.oreCountLabel = null;
statusUI.warehouseCountLabel = storageLabel;
StatusUIManager.setNodeLayerRecursively(uiRoot, Layers.Enum.UI_2D);
return statusUI;
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "7478e794-dd80-4661-9421-8e147d33c51e",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,353 @@
import { _decorator, Component, Node, Vec3, MeshRenderer, Color, tween } from 'cc';
import { BehaviorTreeManager } from './BehaviorTreeManager';
import { RTSBehaviorHandler } from './RTSBehaviorHandler';
const { ccclass, property } = _decorator;
/**
* 单位配置接口
*/
export interface UnitConfig {
unitType: string;
behaviorTreeName: string;
maxHealth: number;
moveSpeed: number;
attackRange: number;
attackDamage: number;
color: string;
}
/**
* 单位控制器
*/
@ccclass('UnitController')
export class UnitController extends Component {
@property
showDebugInfo: boolean = true;
// 单位属性
public unitType: string = '';
public maxHealth: number = 100;
public currentHealth: number = 100;
public moveSpeed: number = 1.5;
public attackRange: number = 2;
public attackDamage: number = 25;
public isSelected: boolean = false;
public currentCommand: string = 'idle';
public targetPosition: Vec3 = Vec3.ZERO.clone();
public targetNode: Node | null = null;
public lastAttackTime: number = 0;
public attackCooldown: number = 1.5;
public color: string = 'white';
// 体力系统属性
public maxStamina: number = 100;
public currentStamina: number = 100;
public homePosition: Vec3 = Vec3.ZERO.clone();
public staminaRecoveryRate: number = 20; // 每秒恢复的体力
public staminaCostPerMining: number = 15; // 每次挖矿消耗的体力
// 移动状态管理
private isMoving: boolean = false;
private moveStartTime: number = 0;
private lastTargetUpdateTime: number = 0;
private behaviorTreeManager: BehaviorTreeManager | null = null;
private behaviorHandler: Component | null = null;
private meshRenderer: MeshRenderer | null = null;
onLoad() {
this.meshRenderer = this.getComponent(MeshRenderer);
// 创建行为树管理器
this.behaviorTreeManager = this.addComponent(BehaviorTreeManager);
// 添加RTS行为处理器
try {
// 添加RTSBehaviorHandler组件
this.behaviorHandler = this.addComponent(RTSBehaviorHandler);
} catch (error) {
console.warn('RTSBehaviorHandler组件添加失败', error);
}
}
/**
* 设置单位配置
*/
setup(config: UnitConfig) {
this.unitType = config.unitType;
this.maxHealth = config.maxHealth;
this.currentHealth = config.maxHealth;
this.moveSpeed = config.moveSpeed;
this.attackRange = config.attackRange;
this.attackDamage = config.attackDamage;
this.color = config.color;
// 设置材质颜色
this.setUnitColor(config.color);
// 设置节点名称显示单位类型
this.node.name = `${config.unitType.toUpperCase()}_${this.node.name}`;
// 初始化行为树
if (this.behaviorTreeManager) {
this.behaviorTreeManager.initializeBehaviorTree(config.behaviorTreeName, this);
}
}
/**
* 设置单位颜色
*/
private setUnitColor(colorName: string) {
if (!this.meshRenderer || !this.meshRenderer.material) return;
const colorMap: { [key: string]: Color } = {
'red': Color.RED,
'green': Color.GREEN,
'blue': Color.BLUE,
'yellow': Color.YELLOW,
'white': Color.WHITE,
'cyan': Color.CYAN,
'magenta': Color.MAGENTA
};
const color = colorMap[colorName] || Color.WHITE;
this.meshRenderer.material.setProperty('mainColor', color);
}
/**
* 设置选择状态
*/
setSelected(selected: boolean) {
this.isSelected = selected;
// 视觉效果
if (selected) {
this.showSelectionEffect();
} else {
this.hideSelectionEffect();
}
// 更新行为树黑板
if (this.behaviorTreeManager) {
this.behaviorTreeManager.updateBlackboardValue('isSelected', selected);
}
}
/**
* 显示选择效果
*/
private showSelectionEffect() {
// 添加选择圈效果
tween(this.node)
.to(0.3, { scale: new Vec3(1.1, 1.1, 1.1) })
.to(0.3, { scale: Vec3.ONE })
.union()
.repeatForever()
.start();
}
/**
* 隐藏选择效果
*/
private hideSelectionEffect() {
// 停止所有缩放动画
tween(this.node).stop();
this.node.setScale(Vec3.ONE);
}
/**
* 发布命令
*/
issueCommand(command: string, target?: Vec3 | Node) {
this.currentCommand = command;
// 设置目标
if (target instanceof Vec3) {
this.targetPosition = target.clone();
this.targetNode = null;
} else if (target instanceof Node) {
this.targetPosition = target.worldPosition.clone();
this.targetNode = target;
}
// 更新行为树黑板
if (this.behaviorTreeManager) {
this.behaviorTreeManager.updateBlackboardValue('currentCommand', command);
this.behaviorTreeManager.updateBlackboardValue('hasTarget', target !== undefined);
this.behaviorTreeManager.updateBlackboardValue('targetPosition', this.targetPosition);
if (target instanceof Node) {
this.behaviorTreeManager.updateBlackboardValue('targetType',
target.name.includes('Resource') ? 'resource' :
target.name.includes('Building') ? 'building' : 'unit');
}
}
}
/**
* 设置黑板变量值
*/
setBlackboardValue(key: string, value: any) {
if (this.behaviorTreeManager) {
this.behaviorTreeManager.updateBlackboardValue(key, value);
}
}
/**
* 获取黑板变量值
*/
getBlackboardValue(key: string): any {
return this.behaviorTreeManager?.getBlackboardValue(key);
}
/**
* 设置移动目标
*/
setTarget(position: Vec3) {
this.targetPosition = position.clone();
this.isMoving = true;
this.moveStartTime = Date.now();
}
/**
* 清除移动目标
*/
clearTarget() {
this.targetPosition = Vec3.ZERO.clone();
this.isMoving = false;
}
/**
* 受到伤害
*/
takeDamage(damage: number) {
this.currentHealth = Math.max(0, this.currentHealth - damage);
// 更新行为树黑板
if (this.behaviorTreeManager) {
this.behaviorTreeManager.updateBlackboardValue('currentHealth', this.currentHealth);
this.behaviorTreeManager.updateBlackboardValue('healthPercentage', this.currentHealth / this.maxHealth);
this.behaviorTreeManager.updateBlackboardValue('isLowHealth', this.currentHealth < this.maxHealth * 0.3);
}
// 视觉效果
this.showDamageEffect();
if (this.currentHealth <= 0) {
this.die();
}
}
/**
* 显示受伤效果
*/
private showDamageEffect() {
if (!this.meshRenderer || !this.meshRenderer.material) return;
// 闪红效果
const originalColor = this.meshRenderer.material.getProperty('mainColor') as Color;
this.meshRenderer.material.setProperty('mainColor', Color.RED);
this.scheduleOnce(() => {
if (this.meshRenderer && this.meshRenderer.material) {
this.meshRenderer.material.setProperty('mainColor', originalColor);
}
}, 0.2);
}
/**
* 单位死亡
*/
private die() {
console.log(`单位 ${this.node.name} 死亡`);
// 播放死亡动画后销毁节点
tween(this.node)
.to(0.5, { scale: Vec3.ZERO })
.call(() => {
this.node.destroy();
})
.start();
}
/**
* 移动到目标位置只在水平面移动不改变Y轴
*/
moveToTarget(targetPos: Vec3, speed?: number, deltaTime?: number): boolean {
const currentPos = this.node.worldPosition;
const distance = Vec3.distance(currentPos, targetPos);
if (distance < 0.5) {
this.isMoving = false;
return true;
}
const actualSpeed = speed || this.moveSpeed;
const actualDeltaTime = deltaTime || 0.016;
const direction = new Vec3();
Vec3.subtract(direction, targetPos, currentPos);
direction.normalize();
const moveDistance = actualSpeed * actualDeltaTime;
const newPosition = new Vec3();
Vec3.scaleAndAdd(newPosition, currentPos, direction, moveDistance);
this.node.setWorldPosition(newPosition);
this.isMoving = true;
return false;
}
/**
* 攻击目标
*/
attackTarget(): boolean {
const currentTime = Date.now();
if (currentTime - this.lastAttackTime < this.attackCooldown * 1000) {
return false;
}
if (this.targetNode && this.targetNode.isValid) {
const distance = Vec3.distance(this.node.worldPosition, this.targetNode.worldPosition);
if (distance <= this.attackRange) {
this.lastAttackTime = currentTime;
return true;
}
}
return false;
}
update(deltaTime: number) {
if (this.behaviorTreeManager) {
this.behaviorTreeManager.update(deltaTime);
}
if (this.isMoving && !this.targetPosition.equals(Vec3.ZERO)) {
const reached = this.moveToTarget(this.targetPosition, this.moveSpeed, deltaTime);
if (reached) {
this.clearTarget();
}
}
// 调试信息显示
if (this.showDebugInfo) {
this.updateDebugInfo();
}
}
/**
* 更新调试信息
*/
private updateDebugInfo() {
// 可以在这里添加调试信息的显示逻辑
// 比如在单位上方显示状态文本等
}
onDestroy() {
// 停止所有动画
tween(this.node).stop();
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "4ac64480-2d09-4de6-a22c-add022790676",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "a1f43720-46e1-4d07-b56a-c9307e45726c",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,103 @@
import { Core } from '@esengine/ecs-framework';
import { Component, _decorator } from 'cc';
import { GameScene } from './scenes/GameScene';
const { ccclass, property } = _decorator;
/**
* ECS管理器 - Cocos Creator组件
* 将此组件添加到场景中的任意节点上即可启动ECS框架
*
* 使用说明:
* 1. 在Cocos Creator场景中创建一个空节点
* 2. 将此ECSManager组件添加到该节点
* 3. 运行场景即可自动启动ECS框架
*/
@ccclass('ECSManager')
export class ECSManager extends Component {
@property({
tooltip: '是否启用调试模式(建议开发阶段开启)'
})
public debugMode: boolean = true;
private isInitialized: boolean = false;
/**
* 组件启动时初始化ECS
*/
start() {
this.initializeECS();
}
/**
* 初始化ECS框架
*/
private initializeECS(): void {
if (this.isInitialized) return;
// ECS框架初始化开始
try {
// 1. 创建Core实例启用调试功能
if (this.debugMode) {
Core.create({
debug: true,
enableEntitySystems: true,
debugConfig: {
enabled: true,
websocketUrl: 'ws://localhost:8080',
autoReconnect: true,
debugFrameRate: 30,
channels: {
entities: true,
systems: true,
performance: true,
components: true,
scenes: true
}
}
});
console.log('✅ ECS调试模式已启用');
} else {
Core.create({
debug: false,
enableEntitySystems: true
});
console.log(' ECS调试模式已禁用');
}
// 2. 创建游戏场景
const gameScene = new GameScene();
// 3. 设置为当前场景会自动调用scene.begin()
Core.setScene(gameScene);
this.isInitialized = true;
// ECS框架初始化完成
} catch (error) {
console.error('ECS框架初始化失败:', error);
}
}
/**
* 每帧更新ECS框架
*/
update(deltaTime: number) {
if (this.isInitialized) {
// 更新ECS核心系统
Core.update(deltaTime);
}
}
/**
* 组件销毁时清理ECS
*/
onDestroy() {
if (this.isInitialized) {
// ECS框架清理
this.isInitialized = false;
}
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "b89656f0-6320-4b6d-81cd-447bf811230c",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,153 @@
# ECS框架启动模板
欢迎使用ECS框架这是一个最基础的启动模板帮助您快速开始ECS项目开发。
## 📁 项目结构
```
ecs/
├── components/ # 组件目录(请在此添加您的组件)
├── systems/ # 系统目录(请在此添加您的系统)
├── scenes/ # 场景目录
│ └── GameScene.ts # 主游戏场景
├── ECSManager.ts # ECS管理器组件
└── README.md # 本文档
```
## 🚀 快速开始
### 1. 启动ECS框架
ECS框架已经配置完成您只需要
1. 在Cocos Creator中打开您的场景
2. 创建一个空节点(例如命名为"ECSManager"
3.`ECSManager` 组件添加到该节点
4. 运行场景ECS框架将自动启动
### 2. 查看控制台输出
如果一切正常,您将在控制台看到:
```
🎮 正在初始化ECS框架...
🔧 ECS调试模式已启用可在Cocos Creator扩展面板中查看调试信息
🎯 游戏场景已创建
✅ ECS框架初始化成功
🚀 游戏场景已启动
```
### 3. 使用调试面板
ECS框架已启用调试功能您可以
1. 在Cocos Creator编辑器菜单中选择 "扩展" → "ECS Framework" → "调试面板"
2. 调试面板将显示实时的ECS运行状态
- 实体数量和状态
- 系统执行信息
- 性能监控数据
- 组件统计信息
**注意**:调试功能会消耗一定性能,正式发布时建议关闭调试模式。
## 📚 下一步开发
### 创建您的第一个组件
`components/` 目录下创建组件:
```typescript
// components/PositionComponent.ts
import { Component } from '@esengine/ecs-framework';
import { Vec3 } from 'cc';
export class PositionComponent extends Component {
public position: Vec3 = new Vec3();
constructor(x: number = 0, y: number = 0, z: number = 0) {
super();
this.position.set(x, y, z);
}
}
```
### 创建您的第一个系统
`systems/` 目录下创建系统:
```typescript
// systems/MovementSystem.ts
import { EntitySystem, Entity, Matcher } from '@esengine/ecs-framework';
import { PositionComponent } from '../components/PositionComponent';
export class MovementSystem extends EntitySystem {
constructor() {
super(Matcher.empty().all(PositionComponent));
}
protected process(entities: Entity[]): void {
for (const entity of entities) {
const position = entity.getComponent(PositionComponent);
if (position) {
// TODO: 在这里编写移动逻辑
console.log(`实体 ${entity.name} 位置: ${position.position}`);
}
}
}
}
```
### 在场景中注册系统
`scenes/GameScene.ts``initialize()` 方法中添加:
```typescript
import { MovementSystem } from '../systems/MovementSystem';
public initialize(): void {
super.initialize();
this.name = "MainGameScene";
// 添加系统
this.addEntityProcessor(new MovementSystem());
// 创建测试实体
const testEntity = this.createEntity("TestEntity");
testEntity.addComponent(new PositionComponent(0, 0, 0));
}
```
## 🔗 学习资源
- [ECS框架完整文档](https://github.com/esengine/ecs-framework)
- [ECS概念详解](https://github.com/esengine/ecs-framework/blob/master/docs/concepts-explained.md)
- [新手教程](https://github.com/esengine/ecs-framework/blob/master/docs/beginner-tutorials.md)
- [组件设计指南](https://github.com/esengine/ecs-framework/blob/master/docs/component-design-guide.md)
- [系统开发指南](https://github.com/esengine/ecs-framework/blob/master/docs/system-guide.md)
## 💡 开发提示
1. **组件只存储数据**:避免在组件中编写复杂逻辑
2. **系统处理逻辑**:所有业务逻辑应该在系统中实现
3. **使用Matcher过滤实体**系统通过Matcher指定需要处理的实体类型
4. **性能优化**:大量实体时考虑使用位掩码查询和组件索引
## ❓ 常见问题
### Q: 如何创建实体?
A: 在场景中使用 `this.createEntity("实体名称")`
### Q: 如何给实体添加组件?
A: 使用 `entity.addComponent(new YourComponent())`
### Q: 如何获取实体的组件?
A: 使用 `entity.getComponent(YourComponent)`
### Q: 如何删除实体?
A: 使用 `entity.destroy()``this.destroyEntity(entity)`
---
🎮 **开始您的ECS开发之旅吧**
如有问题请查阅官方文档或提交Issue。

View File

@@ -0,0 +1,11 @@
{
"ver": "1.0.1",
"importer": "text",
"imported": true,
"uuid": "ca94b460-6c6a-4f72-9ec1-ab5fcd2e0e0a",
"files": [
".json"
],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "3c7bd2b3-6781-482c-be41-21f3dde0e2ab",
"files": [],
"subMetas": {},
"userData": {}
}

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