Compare commits

..

131 Commits

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

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

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

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

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

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

Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>
2025-10-16 14:45:32 +00:00
YHH
241acc9050 更新文档 2025-10-16 22:45:08 +08:00
YHH
8fa921930c Merge pull request #126 from esengine/issue-125-编辑器热更新
热更新配置
2025-10-16 22:31:26 +08:00
YHH
011e43811a 热更新配置 2025-10-16 22:26:50 +08:00
YHH
9f16debd75 新增rust编译缓存和ts构建缓存 2025-10-16 20:54:33 +08:00
YHH
92c56c439b 移除过时的工作流 2025-10-16 20:50:32 +08:00
YHH
7de6a5af0f v2.2.5 2025-10-16 20:37:16 +08:00
YHH
173a063781 Merge pull request #124 from esengine/issue-122-格式化的标准配置
所有源代码文件使用 LF (Unix 风格)
2025-10-16 20:29:21 +08:00
YHH
e04ac7c909 所有源代码文件使用 LF (Unix 风格)
prettier格式化
eslint代码质量检查
2025-10-16 20:24:45 +08:00
YHH
a6e49e1d47 修复publish-release中release_id错误问题 2025-10-16 20:07:56 +08:00
YHH
f0046c7dc2 新增icons作为编辑器图标 2025-10-16 19:52:17 +08:00
YHH
2a17c47c25 修复ts警告 2025-10-16 18:20:31 +08:00
YHH
8d741bf1b9 Merge pull request #123 from esengine/issue-119-插件化编辑器
Issue 119 插件化编辑器
2025-10-16 17:50:35 +08:00
YHH
c676006632 构建配置 2025-10-16 17:44:57 +08:00
YHH
5bcfd597b9 Merge remote-tracking branch 'remotes/origin/master' into issue-119-插件化编辑器 2025-10-16 17:37:24 +08:00
YHH
3cda3c2238 显示客户端链接的ip:port 2025-10-16 17:33:43 +08:00
YHH
43bdd7e43b 远程读取日志 2025-10-16 17:10:22 +08:00
YHH
1ec7892338 设置界面 2025-10-16 13:07:19 +08:00
YHH
6bcfd48a2f 清理调试日志 2025-10-16 12:21:18 +08:00
YHH
345ef70972 支持color类型 2025-10-16 12:00:17 +08:00
YHH
c876edca0c 调试实体和组件属性 2025-10-16 11:55:41 +08:00
YHH
fcf3def284 收集远端数据再profiler dockpanel上 2025-10-15 23:24:13 +08:00
YHH
6f1a2896dd 性能分析器及端口管理器 2025-10-15 22:30:49 +08:00
YHH
62381f4160 记录上一次操作的面板的size大小持久化 2025-10-15 20:26:40 +08:00
YHH
171805debf 禁用默认右键 2025-10-15 20:23:55 +08:00
YHH
619abcbfbc 插件管理器 2025-10-15 20:10:52 +08:00
YHH
03909924c2 app loading 2025-10-15 18:29:48 +08:00
YHH
f4ea077114 菜单栏 2025-10-15 18:24:13 +08:00
YHH
956ccf9195 2D/3D视口 2025-10-15 18:08:55 +08:00
YHH
e880925e3f 视口视图 2025-10-15 17:28:45 +08:00
YHH
0a860920ad 日志面板 2025-10-15 17:21:59 +08:00
YHH
fb7a1b1282 可动态识别属性 2025-10-15 17:15:05 +08:00
YHH
59970ef7c3 Merge pull request #121 from foxling/fix/parent-child-deserialization-bug
fix: 修复场景反序列化时子实体丢失的问题
2025-10-15 15:55:26 +08:00
LING YE
a7750c2894 fix: 修复场景反序列化时子实体丢失的问题
在场景反序列化过程中,子实体虽然保持了父子引用关系,
但未被添加到 Scene 的实体集合和查询系统中,导致查询时子实体"丢失"。
2025-10-15 15:48:54 +08:00
YHH
b69b81f63a 支持树形资源管理器 2025-10-15 10:08:15 +08:00
YHH
00fc6dfd67 Dock系统,支持Tab和拖放 2025-10-15 09:58:45 +08:00
YHH
82451e9fd3 可拖动调整大小的面板 2025-10-15 09:43:48 +08:00
YHH
d0fcc0e447 项目启动流程和资产浏览器功能 2025-10-15 09:34:44 +08:00
YHH
285279629e 优化加载脚本逻辑 2025-10-15 09:19:30 +08:00
YHH
cbfe09b5e9 组件发现和动态加载系统 2025-10-15 00:40:27 +08:00
YHH
b757c1d06c 项目打开功能 2025-10-15 00:23:19 +08:00
YHH
4550a6146a 组件属性编辑器 2025-10-15 00:15:12 +08:00
YHH
3224bb9696 国际化系统 2025-10-14 23:56:54 +08:00
YHH
3a5e73266e 组件注册与添加 2025-10-14 23:42:06 +08:00
YHH
1cf5641c4c 实体存储和管理服务 2025-10-14 23:31:09 +08:00
YHH
85dad41e60 Tauri 窗口,显示编辑器界面 2025-10-14 23:15:07 +08:00
YHH
bd839cf431 Tauri 编辑器应用框架 2025-10-14 22:53:26 +08:00
YHH
b20b2ae4ce 编辑器核心框架 2025-10-14 22:33:55 +08:00
YHH
cac6aedf78 调试插件 DebugPlugin 2025-10-14 22:12:35 +08:00
YHH
a572c80967 v2.2.4 2025-10-14 21:22:07 +08:00
YHH
a09e8261db Merge pull request #118 from esengine/issue-117-统一组件注册系统,移除ComponentTypeManager重复实现
移除了功能重复的ComponentTypeManager
2025-10-14 18:24:17 +08:00
YHH
62e8ebe926 移除了功能重复的ComponentTypeManager 2025-10-14 18:19:08 +08:00
YHH
96e0a9126f Merge pull request #116 from esengine/issue-94-响应式查询(Reactive_Query_System)/_Event-driven_Query
使用ComponentRegistry来确保与Entity.componentMask使用相同的bitIndex
2025-10-14 17:56:26 +08:00
YHH
4afb195814 Merge remote-tracking branch 'origin/master' into issue-94-响应式查询(Reactive_Query_System)/_Event-driven_Query 2025-10-14 17:51:59 +08:00
YHH
7da5366bca 修复querysystem循环依赖的问题 2025-10-14 17:50:32 +08:00
YHH
d979c38615 使用ComponentRegistry来确保与Entity.componentMask使用相同的bitIndex 2025-10-14 17:34:15 +08:00
YHH
7def06126b Merge pull request #115 from esengine/issue-114-reactive-query_文档
新增实体查询系统文档
2025-10-14 13:39:23 +08:00
YHH
9f600f88b0 新增实体查询系统文档 2025-10-14 13:33:12 +08:00
YHH
e3c4d5f0c0 Merge pull request #105 from esengine/issue-94-响应式查询(Reactive_Query_System)/_Event-driven_Query
响应式查询
2025-10-14 12:29:54 +08:00
YHH
b97f3a8431 移除了 EntitySystem.update() 中的冗余 invalidate() 调用 2025-10-14 12:08:08 +08:00
YHH
3b917a06af 修复响应式查询缓存失效和测试隔离问题 2025-10-14 11:48:04 +08:00
YHH
360106fb92 优化ReactiveQuery: 添加公共API、修复内存泄漏、提升通知性能 2025-10-13 23:55:43 +08:00
YHH
507ed5005f Merge remote-tracking branch 'origin/master' into issue-94-响应式查询(Reactive_Query_System)/_Event-driven_Query 2025-10-13 23:36:12 +08:00
YHH
8bea5d5e68 Merge remote-tracking branch 'origin/master' into issue-94-响应式查询(Reactive_Query_System)/_Event-driven_Query 2025-10-13 23:29:56 +08:00
YHH
99076adb72 Merge pull request #113 from esengine/issue-112-优化_DebugManager_使用_DI_避免依赖问题
使用DI避免依赖问题
2025-10-13 23:21:10 +08:00
YHH
d1a6230b23 新增调试配置服务用于DI 2025-10-13 23:17:10 +08:00
YHH
cfe3916934 使用DI避免依赖问题 2025-10-13 22:56:49 +08:00
YHH
d798995876 Merge pull request #111 from esengine/issue-110-_Inject_装饰器在_Cocos_Creator_环境中不执行
扩展 InjectableMetadata 接口支持属性注入,实现 @InjectProperty 装饰器
2025-10-12 23:44:22 +08:00
YHH
43e6b7bf88 扩展 InjectableMetadata 接口支持属性注入,实现 @InjectProperty 装饰器 2025-10-12 23:39:32 +08:00
YHH
942043f0b0 报告响应式查询的数量而不是传统缓存 2025-10-11 18:44:55 +08:00
YHH
23d81bca35 响应式查询 2025-10-11 18:31:20 +08:00
238 changed files with 42178 additions and 604 deletions

62
.all-contributorsrc Normal file
View File

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

36
.coderabbit.yaml Normal file
View File

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

29
.commitlintrc.json Normal file
View File

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

35
.editorconfig Normal file
View File

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

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

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

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

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

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

View File

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

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

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

90
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,90 @@
version: 2
updates:
# 核心包依赖
- package-ecosystem: "npm"
directory: "/packages/core"
schedule:
interval: "monthly" # 改为每月更新,减少干扰
open-pull-requests-limit: 3 # 减少同时打开的 PR 数量
labels:
- "dependencies"
- "core"
commit-message:
prefix: "chore(deps)"
include: "scope"
groups:
# 将开发依赖打包在一起
dev-dependencies:
dependency-type: "development"
# 将生产依赖的 patch 更新打包在一起
production-dependencies:
dependency-type: "production"
update-types:
- "patch"
- "minor"
# 忽略频繁更新但不重要的包
ignore:
- dependency-name: "@types/*"
update-types: ["version-update:semver-patch"]
# 编辑器应用依赖
- package-ecosystem: "npm"
directory: "/packages/editor-app"
schedule:
interval: "monthly"
open-pull-requests-limit: 3
labels:
- "dependencies"
- "editor"
commit-message:
prefix: "chore(deps)"
include: "scope"
groups:
dev-dependencies:
dependency-type: "development"
production-dependencies:
dependency-type: "production"
update-types:
- "patch"
- "minor"
ignore:
- dependency-name: "@types/*"
update-types: ["version-update:semver-patch"]
# 根目录依赖
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "monthly"
open-pull-requests-limit: 3
labels:
- "dependencies"
commit-message:
prefix: "chore(deps)"
groups:
dev-dependencies:
dependency-type: "development"
production-dependencies:
dependency-type: "production"
update-types:
- "patch"
- "minor"
ignore:
- dependency-name: "@types/*"
update-types: ["version-update:semver-patch"]
# GitHub Actions - 保持更新以获取安全修复
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
open-pull-requests-limit: 2
labels:
- "dependencies"
- "github-actions"
commit-message:
prefix: "chore(deps)"
groups:
github-actions:
patterns:
- "*"

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

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

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

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

View File

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

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

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

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

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

View File

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

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

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

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

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

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

@@ -0,0 +1,45 @@
name: Code Coverage
on:
push:
branches: [ master, main ]
pull_request:
branches: [ master, main ]
jobs:
coverage:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests with coverage
run: |
cd packages/core
npm run test:coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./packages/core/coverage/coverage-final.json
flags: core
name: core-coverage
fail_ci_if_error: true
verbose: true
- name: Upload coverage artifact
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: packages/core/coverage/

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

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

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

@@ -0,0 +1,31 @@
name: Commit Lint
on:
pull_request:
types: [opened, synchronize, reopened]
permissions:
contents: read
pull-requests: read
jobs:
commitlint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
cache: 'npm'
- name: Install commitlint
run: |
npm install --save-dev @commitlint/config-conventional @commitlint/cli
- name: Validate PR commits
run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose

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

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

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

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

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

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

@@ -0,0 +1,68 @@
name: Release
on:
workflow_dispatch:
inputs:
dry_run:
description: 'Dry run (仅预览,不实际发布)'
type: boolean
default: false
required: false
permissions:
contents: write
issues: write
pull-requests: write
id-token: write
jobs:
release-core:
name: Release Core Package
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
registry-url: 'https://registry.npmjs.org'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: |
cd packages/core
npm run test:ci
- name: Build package
run: |
cd packages/core
npm run build:npm
- name: Release (Dry Run)
if: ${{ github.event.inputs.dry_run == 'true' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
cd packages/core
npx semantic-release --dry-run
- name: Release
if: ${{ github.event.inputs.dry_run != 'true' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
cd packages/core
npx semantic-release

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

@@ -0,0 +1,43 @@
name: Size Limit
on:
pull_request:
branches:
- master
- main
paths:
- 'packages/core/src/**'
- 'packages/core/package.json'
- '.size-limit.json'
permissions:
contents: read
pull-requests: write
issues: write
jobs:
size:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build core package
run: |
cd packages/core
npm run build:npm
- name: Check bundle size
uses: andresz1/size-limit-action@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
skip_step: install

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

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

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

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

9
.gitignore vendored
View File

@@ -69,3 +69,12 @@ docs/.vitepress/dist/
/demo/.idea/
/demo/.vscode/
/demo_wxgame/
# Tauri 构建产物
**/src-tauri/target/
**/src-tauri/WixTools/
**/src-tauri/gen/
# Tauri 捆绑输出
**/src-tauri/target/release/bundle/
**/src-tauri/target/debug/bundle/

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

58
.releaserc.json Normal file
View File

@@ -0,0 +1,58 @@
{
"branches": ["master", "main"],
"plugins": [
[
"@semantic-release/commit-analyzer",
{
"preset": "angular",
"releaseRules": [
{ "type": "feat", "release": "minor" },
{ "type": "fix", "release": "patch" },
{ "type": "perf", "release": "patch" },
{ "type": "revert", "release": "patch" },
{ "type": "docs", "release": false },
{ "type": "chore", "release": false },
{ "type": "refactor", "release": "patch" },
{ "type": "test", "release": false },
{ "type": "build", "release": false },
{ "type": "ci", "release": false }
]
}
],
[
"@semantic-release/release-notes-generator",
{
"preset": "angular",
"writerOpts": {
"commitsSort": ["subject", "scope"]
}
}
],
[
"@semantic-release/changelog",
{
"changelogFile": "CHANGELOG.md",
"changelogTitle": "# Changelog\n\nAll notable changes to this project will be documented in this file."
}
],
[
"@semantic-release/npm",
{
"npmPublish": false
}
],
[
"@semantic-release/git",
{
"assets": ["CHANGELOG.md", "package.json", "package-lock.json"],
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}
],
[
"@semantic-release/github",
{
"assets": []
}
]
]
}

25
.size-limit.json Normal file
View File

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

133
CONTRIBUTING.md Normal file
View File

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

214
LICENSE
View File

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

155
README.md
View File

@@ -1,12 +1,53 @@
# ECS Framework
[![CI](https://github.com/esengine/ecs-framework/workflows/CI/badge.svg)](https://github.com/esengine/ecs-framework/actions)
[![codecov](https://codecov.io/gh/esengine/ecs-framework/graph/badge.svg)](https://codecov.io/gh/esengine/ecs-framework)
[![npm version](https://badge.fury.io/js/%40esengine%2Fecs-framework.svg)](https://badge.fury.io/js/%40esengine%2Fecs-framework)
[![npm downloads](https://img.shields.io/npm/dm/@esengine/ecs-framework.svg)](https://www.npmjs.com/package/@esengine/ecs-framework)
[![Bundle Size](https://img.shields.io/bundlephobia/minzip/@esengine/ecs-framework)](https://bundlephobia.com/package/@esengine/ecs-framework)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-3178C6?style=flat&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![All Contributors](https://img.shields.io/badge/all_contributors-0-orange.svg?style=flat-square)](#contributors)
[![GitHub stars](https://img.shields.io/github/stars/esengine/ecs-framework?style=social)](https://github.com/esengine/ecs-framework/stargazers)
[![DeepWiki](https://img.shields.io/badge/_AI_文档-DeepWiki-6366f1?style=flat&logo=gitbook&logoColor=white)](https://deepwiki.com/esengine/ecs-framework)
一个高性能的 TypeScript ECS (Entity-Component-System) 框架,专为现代游戏开发而设计。
<div align="center">
<p>一个高性能的 TypeScript ECS (Entity-Component-System) 框架,专为现代游戏开发而设计。</p>
<p>A high-performance TypeScript ECS (Entity-Component-System) framework designed for modern game development.</p>
</div>
---
## 📊 项目统计 / Project Stats
<div align="center">
[![Star History Chart](https://api.star-history.com/svg?repos=esengine/ecs-framework&type=Date)](https://star-history.com/#esengine/ecs-framework&Date)
</div>
<div align="center">
<a href="https://github.com/esengine/ecs-framework/graphs/contributors">
<img src="https://contrib.rocks/image?repo=esengine/ecs-framework" />
</a>
</div>
### 📈 下载趋势 / Download Trends
<div align="center">
[![NPM Downloads](https://img.shields.io/npm/dt/@esengine/ecs-framework?label=Total%20Downloads&style=for-the-badge&color=blue)](https://www.npmjs.com/package/@esengine/ecs-framework)
[![NPM Trends](https://img.shields.io/npm/dm/@esengine/ecs-framework?label=Monthly%20Downloads&style=for-the-badge&color=success)](https://npmtrends.com/@esengine/ecs-framework)
</div>
---
## 特性
@@ -91,15 +132,75 @@ function gameLoop(deltaTime: number) {
- **多场景** - 支持 World/Scene 分层架构
- **时间管理** - 内置定时器和时间控制系统
## 🏗️ 架构设计 / Architecture
```mermaid
graph TB
A[Core 核心] --> B[World 世界]
B --> C[Scene 场景]
C --> D[EntityManager 实体管理器]
C --> E[SystemManager 系统管理器]
D --> F[Entity 实体]
F --> G[Component 组件]
E --> H[EntitySystem 实体系统]
E --> I[WorkerSystem 工作线程系统]
style A fill:#e1f5ff
style B fill:#fff3e0
style C fill:#f3e5f5
style D fill:#e8f5e9
style E fill:#fff9c4
style F fill:#ffebee
style G fill:#e0f2f1
style H fill:#fce4ec
style I fill:#f1f8e9
```
## 平台支持
支持主流游戏引擎和 Web 平台:
- **Cocos Creator** - 内置引擎集成支持,提供[专用调试插件](https://store.cocos.com/app/detail/7823)
- **Laya 引擎** - 完整的生命周期管理
- **Cocos Creator**
- **Laya 引擎**
- **原生 Web** - 浏览器环境直接运行
- **小游戏平台** - 微信、支付宝等小游戏
## ECS Framework Editor
跨平台桌面编辑器,提供可视化开发和调试工具。
### 主要功能
- **场景管理** - 可视化场景层级和实体管理
- **组件检视** - 实时查看和编辑实体组件
- **性能分析** - 内置 Profiler 监控系统性能
- **插件系统** - 可扩展的插件架构
- **远程调试** - 连接运行中的游戏进行实时调试
- **自动更新** - 支持热更新,自动获取最新版本
### 下载
[![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)
支持 Windows、macOS (Intel & Apple Silicon)
### 截图
<img src="screenshots/main_screetshot.png" alt="ECS Framework Editor" width="800">
<details>
<summary>查看更多截图</summary>
**性能分析器**
<img src="screenshots/performance_profiler.png" alt="Performance Profiler" width="600">
**插件管理**
<img src="screenshots/plugin_manager.png" alt="Plugin Manager" width="600">
**设置界面**
<img src="screenshots/settings.png" alt="Settings" width="600">
</details>
## 示例项目
@@ -108,6 +209,7 @@ function gameLoop(deltaTime: number) {
## 文档
- [📚 AI智能文档](https://deepwiki.com/esengine/ecs-framework) - AI助手随时解答你的问题
- [快速入门](https://esengine.github.io/ecs-framework/guide/getting-started.html) - 详细教程和平台集成
- [完整指南](https://esengine.github.io/ecs-framework/guide/) - ECS 概念和使用指南
- [API 参考](https://esengine.github.io/ecs-framework/api/) - 完整 API 文档
@@ -117,11 +219,56 @@ function gameLoop(deltaTime: number) {
- [路径寻找](https://github.com/esengine/ecs-astar) - A*、BFS、Dijkstra 算法
- [AI 系统](https://github.com/esengine/BehaviourTree-ai) - 行为树、效用 AI
## 💪 支持项目 / Support the Project
如果这个项目对你有帮助,请考虑:
If this project helps you, please consider:
<div align="center">
[![GitHub Sponsors](https://img.shields.io/badge/Sponsor-GitHub%20Sponsors-ea4aaa?style=for-the-badge&logo=github)](https://github.com/sponsors/esengine)
[![Star on GitHub](https://img.shields.io/badge/⭐_Star-on_GitHub-yellow?style=for-the-badge&logo=github)](https://github.com/esengine/ecs-framework)
</div>
- ⭐ 给项目点个 Star
- 🐛 报告 Bug 或提出新功能
- 📝 改进文档
- 💖 成为赞助者
## 社区与支持
- [问题反馈](https://github.com/esengine/ecs-framework/issues) - Bug 报告和功能建议
- [讨论区](https://github.com/esengine/ecs-framework/discussions) - 提问、分享想法
- [QQ 交流群](https://jq.qq.com/?_wv=1027&k=29w1Nud6) - ecs游戏框架交流
## 贡献者 / Contributors
感谢所有为这个项目做出贡献的人!
Thanks goes to these wonderful people:
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tbody>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/esengine"><img src="https://avatars.githubusercontent.com/esengine?s=100" width="100px;" alt="esengine"/><br /><sub><b>esengine</b></sub></a><br /><a href="#maintenance-esengine" title="Maintenance">🚧</a> <a href="https://github.com/esengine/ecs-framework/commits?author=esengine" title="Code">💻</a> <a href="#design-esengine" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/foxling"><img src="https://avatars.githubusercontent.com/foxling?s=100" width="100px;" alt="LING YE"/><br /><sub><b>LING YE</b></sub></a><br /><a href="https://github.com/esengine/ecs-framework/commits?author=foxling" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MirageTank"><img src="https://avatars.githubusercontent.com/MirageTank?s=100" width="100px;" alt="MirageTank"/><br /><sub><b>MirageTank</b></sub></a><br /><a href="https://github.com/esengine/ecs-framework/commits?author=MirageTank" title="Code">💻</a></td>
</tr>
</tbody>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
本项目遵循 [all-contributors](https://github.com/all-contributors/all-contributors) 规范。欢迎任何形式的贡献!
## 许可证
[MIT](LICENSE) © 2025 ECS Framework
[MIT](LICENSE) © 2025 ECS Framework

View File

@@ -66,6 +66,7 @@ export default defineConfig({
items: [
{ text: '实体类 (Entity)', link: '/guide/entity' },
{ text: '组件系统 (Component)', link: '/guide/component' },
{ text: '实体查询系统', link: '/guide/entity-query' },
{
text: '系统架构 (System)',
link: '/guide/system',

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 参考

10944
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -49,30 +49,56 @@
"publish:network-server:patch": "cd packages/network-server && npm run publish:patch",
"publish": "lerna publish",
"version": "lerna version",
"release": "semantic-release",
"release:core": "cd packages/core && semantic-release",
"contributors:add": "all-contributors add",
"contributors:generate": "all-contributors generate",
"contributors:check": "all-contributors check",
"docs:dev": "vitepress dev docs",
"docs:build": "npm run docs:api && vitepress build docs",
"docs:preview": "vitepress preview docs",
"docs:api": "typedoc",
"docs:api:watch": "typedoc --watch",
"update:worker-demo": "npm run build:core && cd examples/worker-system-demo && npm run build && cd ../.. && npm run copy:worker-demo",
"copy:worker-demo": "node scripts/update-worker-demo.js"
"copy:worker-demo": "node scripts/update-worker-demo.js",
"format": "prettier --write \"packages/**/src/**/*.{ts,tsx,js,jsx}\"",
"format:check": "prettier --check \"packages/**/src/**/*.{ts,tsx,js,jsx}\"",
"lint": "eslint \"packages/**/src/**/*.{ts,tsx,js,jsx}\"",
"lint:fix": "eslint \"packages/**/src/**/*.{ts,tsx,js,jsx}\" --fix"
},
"author": "yhh",
"license": "MIT",
"devDependencies": {
"@commitlint/cli": "^18.6.0",
"@commitlint/config-conventional": "^18.6.0",
"@iconify/json": "^2.2.388",
"@rollup/plugin-commonjs": "^28.0.3",
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-terser": "^0.4.4",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/commit-analyzer": "^11.1.0",
"@semantic-release/git": "^10.0.1",
"@semantic-release/github": "^9.2.6",
"@semantic-release/npm": "^11.0.2",
"@semantic-release/release-notes-generator": "^12.1.0",
"@size-limit/preset-small-lib": "^11.0.2",
"@types/jest": "^29.5.14",
"@types/node": "^20.19.0",
"@typescript-eslint/eslint-plugin": "^8.46.1",
"@typescript-eslint/parser": "^8.46.1",
"all-contributors-cli": "^6.26.1",
"eslint": "^9.37.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"lerna": "^8.1.8",
"prettier": "^3.6.2",
"rimraf": "^5.0.0",
"rollup": "^4.42.0",
"rollup-plugin-dts": "^6.2.1",
"semantic-release": "^23.0.0",
"semantic-release-monorepo": "^8.0.2",
"semver": "^7.6.3",
"size-limit": "^11.0.2",
"ts-jest": "^29.4.0",
"typedoc": "^0.28.13",
"typedoc-plugin-markdown": "^4.9.0",

View File

@@ -0,0 +1,68 @@
{
"branches": ["master", "main"],
"tagFormat": "core-v${version}",
"plugins": [
[
"@semantic-release/commit-analyzer",
{
"preset": "angular",
"releaseRules": [
{ "type": "feat", "release": "minor" },
{ "type": "fix", "release": "patch" },
{ "type": "perf", "release": "patch" },
{ "type": "revert", "release": "patch" },
{ "type": "docs", "release": false },
{ "type": "chore", "release": false },
{ "type": "refactor", "release": "patch" },
{ "type": "test", "release": false },
{ "type": "build", "release": false },
{ "type": "ci", "release": false }
],
"parserOpts": {
"noteKeywords": ["BREAKING CHANGE", "BREAKING CHANGES"]
}
}
],
[
"@semantic-release/release-notes-generator",
{
"preset": "angular",
"writerOpts": {
"commitsSort": ["subject", "scope"]
}
}
],
[
"@semantic-release/changelog",
{
"changelogFile": "CHANGELOG.md",
"changelogTitle": "# @esengine/ecs-framework Changelog\n\nAll notable changes to the core package will be documented in this file."
}
],
[
"@semantic-release/npm",
{
"pkgRoot": "dist",
"tarballDir": "release"
}
],
[
"@semantic-release/git",
{
"assets": ["CHANGELOG.md", "package.json", "package-lock.json"],
"message": "chore(core): release v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}
],
[
"@semantic-release/github",
{
"assets": [
{
"path": "release/*.tgz",
"label": "npm package"
}
]
}
]
]
}

View File

@@ -1,6 +1,6 @@
{
"name": "@esengine/ecs-framework",
"version": "2.2.3",
"version": "2.2.8",
"description": "用于Laya、Cocos Creator等JavaScript游戏引擎的高性能ECS框架",
"main": "bin/index.js",
"types": "bin/index.d.ts",

View File

@@ -13,6 +13,8 @@ import { ServiceContainer } from './Core/ServiceContainer';
import { PluginManager } from './Core/PluginManager';
import { IPlugin } from './Core/Plugin';
import { WorldManager } from './ECS/WorldManager';
import { DebugConfigService } from './Utils/Debug/DebugConfigService';
import { createInstance } from './Core/DI/Decorators';
/**
* 游戏引擎核心类
@@ -178,7 +180,7 @@ export class Core {
this._serviceContainer.registerInstance(PoolManager, this._poolManager);
// 初始化场景管理器
this._sceneManager = new SceneManager();
this._sceneManager = new SceneManager(this._performanceMonitor);
this._serviceContainer.registerInstance(SceneManager, this._sceneManager);
// 设置场景切换回调,通知调试管理器
@@ -189,7 +191,7 @@ export class Core {
});
// 初始化World管理器
this._worldManager = new WorldManager();
this._worldManager = new WorldManager(this._config.worldManagerConfig);
this._serviceContainer.registerInstance(WorldManager, this._worldManager);
// 初始化插件管理器
@@ -202,14 +204,15 @@ export class Core {
// 初始化调试管理器
if (this._config.debugConfig?.enabled) {
// 使用DI容器创建DebugManager前两个参数从容器解析config手动传入
const config = this._config.debugConfig;
this._debugManager = new DebugManager(
this._serviceContainer.resolve(SceneManager),
this._serviceContainer.resolve(PerformanceMonitor),
config
const configService = new DebugConfigService();
configService.setConfig(this._config.debugConfig);
this._serviceContainer.registerInstance(DebugConfigService, configService);
this._serviceContainer.registerSingleton(DebugManager, (c) =>
createInstance(DebugManager, c)
);
this._serviceContainer.registerInstance(DebugManager, this._debugManager);
this._debugManager = this._serviceContainer.resolve(DebugManager);
}
this.initialize();
@@ -476,13 +479,15 @@ export class Core {
if (this._instance._debugManager) {
this._instance._debugManager.updateConfig(config);
} else {
// 使用DI容器创建DebugManager
this._instance._debugManager = new DebugManager(
this._instance._serviceContainer.resolve(SceneManager),
this._instance._serviceContainer.resolve(PerformanceMonitor),
config
const configService = new DebugConfigService();
configService.setConfig(config);
this._instance._serviceContainer.registerInstance(DebugConfigService, configService);
this._instance._serviceContainer.registerSingleton(DebugManager, (c) =>
createInstance(DebugManager, c)
);
this._instance._serviceContainer.registerInstance(DebugManager, this._instance._debugManager);
this._instance._debugManager = this._instance._serviceContainer.resolve(DebugManager);
}
// 更新Core配置
@@ -659,11 +664,6 @@ export class Core {
// 更新额外的 WorldManager
this._worldManager.updateAll();
// 更新调试管理器基于FPS的数据发送
if (this._debugManager) {
this._debugManager.onFrameUpdate(deltaTime);
}
// 结束性能监控
this._performanceMonitor.endMonitoring('Core.update', frameStartTime);
}

View File

@@ -35,6 +35,12 @@ export interface InjectableMetadata {
* 依赖列表
*/
dependencies: Array<ServiceType<any> | string | symbol>;
/**
* 属性注入映射
* key: 属性名, value: 服务类型
*/
properties?: Map<string | symbol, ServiceType<any>>;
}
/**
@@ -77,10 +83,12 @@ export interface UpdatableMetadata {
*/
export function Injectable(): ClassDecorator {
return function <T extends Function>(target: T): T {
// 标记为可注入
const existing = injectableMetadata.get(target);
injectableMetadata.set(target, {
injectable: true,
dependencies: []
dependencies: [],
properties: existing?.properties
});
return target;
@@ -142,20 +150,7 @@ export function Updatable(priority: number = 0): ClassDecorator {
*
* 标记构造函数参数需要注入的服务类型
*
* @param serviceType 服务类型标识符类、字符串或Symbol
*
* @example
* ```typescript
* @Injectable()
* class MySystem extends EntitySystem {
* constructor(
* @Inject(TimeService) private timeService: TimeService,
* @Inject(PhysicsService) private physics: PhysicsService
* ) {
* super();
* }
* }
* ```
* @param serviceType 服务类型标识符
*/
export function Inject(serviceType: ServiceType<any> | string | symbol): ParameterDecorator {
return function (target: Object, propertyKey: string | symbol | undefined, parameterIndex: number) {
@@ -171,6 +166,35 @@ export function Inject(serviceType: ServiceType<any> | string | symbol): Paramet
};
}
/**
* @InjectProperty() 装饰器
*
* 通过属性装饰器注入依赖
*
* 注入时机在构造函数执行后、onInitialize() 调用前完成
*
* @param serviceType 服务类型
*/
export function InjectProperty(serviceType: ServiceType<any>): PropertyDecorator {
return function (target: any, propertyKey: string | symbol) {
let metadata = injectableMetadata.get(target.constructor);
if (!metadata) {
metadata = {
injectable: true,
dependencies: []
};
injectableMetadata.set(target.constructor, metadata);
}
if (!metadata.properties) {
metadata.properties = new Map();
}
metadata.properties.set(propertyKey, serviceType);
};
}
/**
* 检查类是否标记为可注入
*
@@ -252,6 +276,29 @@ export function createInstance<T>(
return new constructor(...dependencies);
}
/**
* 为实例注入属性依赖
*
* @param instance 目标实例
* @param container 服务容器
*/
export function injectProperties<T>(instance: T, container: ServiceContainer): void {
const constructor = (instance as any).constructor;
const metadata = getInjectableMetadata(constructor);
if (!metadata?.properties || metadata.properties.size === 0) {
return;
}
for (const [propertyKey, serviceType] of metadata.properties) {
const service = container.resolve(serviceType);
if (service !== null) {
(instance as any)[propertyKey] = service;
}
}
}
/**
* 检查类是否标记为可更新
*

View File

@@ -7,6 +7,7 @@
export {
Injectable,
Inject,
InjectProperty,
Updatable,
isInjectable,
getInjectableMetadata,
@@ -14,6 +15,7 @@ export {
isUpdatable,
getUpdatableMetadata,
createInstance,
injectProperties,
registerInjectable
} from './Decorators';

View File

@@ -70,8 +70,7 @@ export abstract class Component implements IComponent {
* 这是一个生命周期钩子,用于组件的初始化逻辑。
* 虽然保留此方法,但建议将复杂的初始化逻辑放在 System 中处理。
*/
public onAddedToEntity(): void {
}
public onAddedToEntity(): void {}
/**
* 组件从实体移除时的回调
@@ -82,7 +81,5 @@ export abstract class Component implements IComponent {
* 这是一个生命周期钩子,用于组件的清理逻辑。
* 虽然保留此方法,但建议将复杂的清理逻辑放在 System 中处理。
*/
public onRemovedFromEntity(): void {
}
}
public onRemovedFromEntity(): void {}
}

View File

@@ -1,6 +1,6 @@
import { Entity } from '../Entity';
import { ComponentType } from './ComponentStorage';
import { BitMask64Data, BitMask64Utils, ComponentTypeManager } from "../Utils";
import { ComponentType, ComponentRegistry } from './ComponentStorage';
import { BitMask64Data, BitMask64Utils } from "../Utils";
import { BitMaskHashMap } from "../Utils/BitMaskHashMap";
/**
@@ -266,10 +266,18 @@ export class ArchetypeSystem {
/**
* 生成原型ID
* 使用ComponentRegistry确保与Entity.componentMask使用相同的bitIndex
*/
private generateArchetypeId(componentTypes: ComponentType[]): ArchetypeId {
let entityBits = ComponentTypeManager.instance.getEntityBits(componentTypes);
return entityBits.getValue();
let mask = BitMask64Utils.clone(BitMask64Utils.ZERO);
for (const type of componentTypes) {
if (!ComponentRegistry.isRegistered(type)) {
ComponentRegistry.register(type);
}
const bitMask = ComponentRegistry.getBitMask(type);
BitMask64Utils.orInPlace(mask, bitMask);
}
return mask;
}
/**

View File

@@ -5,40 +5,10 @@ import { BitMask64Utils, BitMask64Data } from '../Utils/BigIntCompatibility';
import { createLogger } from '../../Utils/Logger';
import { getComponentTypeName } from '../Decorators';
import { Archetype, ArchetypeSystem } from './ArchetypeSystem';
import { ComponentTypeManager } from "../Utils";
import { ReactiveQuery, ReactiveQueryConfig } from './ReactiveQuery';
import { QueryCondition, QueryConditionType, QueryResult } from './QueryTypes';
/**
* 查询条件类型
*/
export enum QueryConditionType {
/** 必须包含所有指定组件 */
ALL = 'all',
/** 必须包含任意一个指定组件 */
ANY = 'any',
/** 不能包含任何指定组件 */
NONE = 'none'
}
/**
* 查询条件接口
*/
export interface QueryCondition {
type: QueryConditionType;
componentTypes: ComponentType[];
mask: BitMask64Data;
}
/**
* 实体查询结果接口
*/
export interface QueryResult {
entities: readonly Entity[];
count: number;
/** 查询执行时间(毫秒) */
executionTime: number;
/** 是否来自缓存 */
fromCache: boolean;
}
export { QueryCondition, QueryConditionType, QueryResult };
/**
* 实体索引结构
@@ -124,15 +94,16 @@ export class QuerySystem {
/**
* 设置实体列表并重建索引
*
*
* 当实体集合发生大规模变化时调用此方法。
* 系统将重新构建所有索引以确保查询性能。
*
*
* @param entities 新的实体列表
*/
public setEntities(entities: Entity[]): void {
this.entities = entities;
this.clearQueryCache();
this.clearReactiveQueries();
this.rebuildIndexes();
}
@@ -152,12 +123,14 @@ export class QuerySystem {
this.archetypeSystem.addEntity(entity);
// 通知响应式查询
this.notifyReactiveQueriesEntityAdded(entity);
// 只有在非延迟模式下才立即清理缓存
if (!deferCacheClear) {
this.clearQueryCache();
}
// 更新版本号
this._version++;
}
@@ -235,14 +208,24 @@ export class QuerySystem {
public removeEntity(entity: Entity): void {
const index = this.entities.indexOf(entity);
if (index !== -1) {
const componentTypes: ComponentType[] = [];
for (const component of entity.components) {
componentTypes.push(component.constructor as ComponentType);
}
this.entities.splice(index, 1);
this.removeEntityFromIndexes(entity);
this.archetypeSystem.removeEntity(entity);
if (componentTypes.length > 0) {
this.notifyReactiveQueriesEntityRemoved(entity, componentTypes);
} else {
this.notifyReactiveQueriesEntityRemovedFallback(entity);
}
this.clearQueryCache();
// 更新版本号
this._version++;
}
}
@@ -270,6 +253,9 @@ export class QuerySystem {
// 重新添加实体到索引(基于新的组件状态)
this.addEntityToIndexes(entity);
// 通知响应式查询
this.notifyReactiveQueriesEntityChanged(entity);
// 清理查询缓存,因为实体组件状态已改变
this.clearQueryCache();
@@ -357,13 +343,13 @@ export class QuerySystem {
/**
* 查询包含所有指定组件的实体
*
*
* 返回同时包含所有指定组件类型的实体列表。
* 系统会自动选择最高效的查询策略,包括索引查找和缓存机制
*
* 内部使用响应式查询作为智能缓存,自动跟踪实体变化,性能更优
*
* @param componentTypes 要查询的组件类型列表
* @returns 查询结果,包含匹配的实体和性能信息
*
*
* @example
* ```typescript
* // 查询同时具有位置和速度组件的实体
@@ -375,38 +361,20 @@ export class QuerySystem {
const startTime = performance.now();
this.queryStats.totalQueries++;
// 生成缓存
const cacheKey = this.generateCacheKey('all', componentTypes);
// 使用内部响应式查询作为智能缓存
const reactiveQuery = this.getOrCreateReactiveQuery(QueryConditionType.ALL, componentTypes);
// 检查缓存
const cached = this.getFromCache(cacheKey);
if (cached) {
this.queryStats.cacheHits++;
return {
entities: cached,
count: cached.length,
executionTime: performance.now() - startTime,
fromCache: true
};
}
// 从响应式查询获取结果(永远是最新的)
const entities = reactiveQuery.getEntities();
this.queryStats.archetypeHits++;
const archetypeResult = this.archetypeSystem.queryArchetypes(componentTypes, 'AND');
const entities: Entity[] = [];
for (const archetype of archetypeResult.archetypes) {
for (const entity of archetype.entities) {
entities.push(entity);
}
}
this.addToCache(cacheKey, entities);
// 统计为缓存命中(响应式查询本质上是永不过期的智能缓存)
this.queryStats.cacheHits++;
return {
entities,
count: entities.length,
executionTime: performance.now() - startTime,
fromCache: false
fromCache: true
};
}
@@ -436,13 +404,13 @@ export class QuerySystem {
/**
* 查询包含任意指定组件的实体
*
*
* 返回包含任意一个指定组件类型的实体列表。
* 使用集合合并算法确保高效的查询性能
*
* 内部使用响应式查询作为智能缓存,自动跟踪实体变化,性能更优
*
* @param componentTypes 要查询的组件类型列表
* @returns 查询结果,包含匹配的实体和性能信息
*
*
* @example
* ```typescript
* // 查询具有武器或护甲组件的实体
@@ -454,52 +422,32 @@ export class QuerySystem {
const startTime = performance.now();
this.queryStats.totalQueries++;
const cacheKey = this.generateCacheKey('any', componentTypes);
// 使用内部响应式查询作为智能缓存
const reactiveQuery = this.getOrCreateReactiveQuery(QueryConditionType.ANY, componentTypes);
// 检查缓存
const cached = this.getFromCache(cacheKey);
if (cached) {
this.queryStats.cacheHits++;
return {
entities: cached,
count: cached.length,
executionTime: performance.now() - startTime,
fromCache: true
};
}
// 从响应式查询获取结果(永远是最新的)
const entities = reactiveQuery.getEntities();
this.queryStats.archetypeHits++;
const archetypeResult = this.archetypeSystem.queryArchetypes(componentTypes, 'OR');
const entities = this.acquireResultArray();
for (const archetype of archetypeResult.archetypes) {
for (const entity of archetype.entities) {
entities.push(entity);
}
}
const frozenEntities = [...entities];
this.releaseResultArray(entities);
this.addToCache(cacheKey, frozenEntities);
// 统计为缓存命中(响应式查询本质上是永不过期的智能缓存)
this.queryStats.cacheHits++;
return {
entities: frozenEntities,
count: frozenEntities.length,
entities,
count: entities.length,
executionTime: performance.now() - startTime,
fromCache: false
fromCache: true
};
}
/**
* 查询不包含任何指定组件的实体
*
*
* 返回不包含任何指定组件类型的实体列表。
* 适用于排除特定类型实体的查询场景
*
* 内部使用响应式查询作为智能缓存,自动跟踪实体变化,性能更优
*
* @param componentTypes 要排除的组件类型列表
* @returns 查询结果,包含匹配的实体和性能信息
*
*
* @example
* ```typescript
* // 查询不具有AI和玩家控制组件的实体如静态物体
@@ -511,32 +459,20 @@ export class QuerySystem {
const startTime = performance.now();
this.queryStats.totalQueries++;
const cacheKey = this.generateCacheKey('none', componentTypes);
// 使用内部响应式查询作为智能缓存
const reactiveQuery = this.getOrCreateReactiveQuery(QueryConditionType.NONE, componentTypes);
// 检查缓存
const cached = this.getFromCache(cacheKey);
if (cached) {
this.queryStats.cacheHits++;
return {
entities: cached,
count: cached.length,
executionTime: performance.now() - startTime,
fromCache: true
};
}
// 从响应式查询获取结果(永远是最新的)
const entities = reactiveQuery.getEntities();
const mask = this.createComponentMask(componentTypes);
const entities = this.entities.filter(entity =>
BitMask64Utils.hasNone(entity.componentMask, mask)
);
this.addToCache(cacheKey, entities);
// 统计为缓存命中(响应式查询本质上是永不过期的智能缓存)
this.queryStats.cacheHits++;
return {
entities,
count: entities.length,
executionTime: performance.now() - startTime,
fromCache: false
fromCache: true
};
}
@@ -759,6 +695,20 @@ export class QuerySystem {
this.componentMaskCache.clear();
}
/**
* 清除所有响应式查询
*
* 销毁所有响应式查询实例并清理索引
* 通常在setEntities时调用以确保缓存一致性
*/
private clearReactiveQueries(): void {
for (const query of this._reactiveQueries.values()) {
query.dispose();
}
this._reactiveQueries.clear();
this._reactiveQueriesByComponent.clear();
}
/**
* 高效的缓存键生成
*/
@@ -782,11 +732,111 @@ export class QuerySystem {
/**
* 清理查询缓存
*
*
* 用于外部调用清理缓存,通常在批量操作后使用。
* 注意:此方法也会清理响应式查询缓存
*/
public clearCache(): void {
this.clearQueryCache();
this.clearReactiveQueries();
}
/**
* 创建响应式查询
*
* 响应式查询会自动跟踪实体/组件的变化,并通过事件通知订阅者。
* 适合需要实时响应实体变化的场景(如UI更新、AI系统等)。
*
* @param componentTypes 查询的组件类型列表
* @param config 可选的查询配置
* @returns 响应式查询实例
*
* @example
* ```typescript
* const query = querySystem.createReactiveQuery([Position, Velocity], {
* enableBatchMode: true,
* batchDelay: 16
* });
*
* query.subscribe((change) => {
* if (change.type === ReactiveQueryChangeType.ADDED) {
* console.log('新实体:', change.entity);
* }
* });
* ```
*/
public createReactiveQuery(
componentTypes: ComponentType[],
config?: ReactiveQueryConfig
): ReactiveQuery {
if (!componentTypes || componentTypes.length === 0) {
throw new Error('组件类型列表不能为空');
}
const mask = this.createComponentMask(componentTypes);
const condition: QueryCondition = {
type: QueryConditionType.ALL,
componentTypes,
mask
};
const query = new ReactiveQuery(condition, config);
const initialEntities = this.executeTraditionalQuery(
QueryConditionType.ALL,
componentTypes
);
query.initializeWith(initialEntities);
const cacheKey = this.generateCacheKey('all', componentTypes);
this._reactiveQueries.set(cacheKey, query);
for (const type of componentTypes) {
let queries = this._reactiveQueriesByComponent.get(type);
if (!queries) {
queries = new Set();
this._reactiveQueriesByComponent.set(type, queries);
}
queries.add(query);
}
return query;
}
/**
* 销毁响应式查询
*
* 清理查询占用的资源,包括监听器和实体引用。
* 销毁后的查询不应再被使用。
*
* @param query 要销毁的响应式查询
*
* @example
* ```typescript
* const query = querySystem.createReactiveQuery([Position, Velocity]);
* // ... 使用查询
* querySystem.destroyReactiveQuery(query);
* ```
*/
public destroyReactiveQuery(query: ReactiveQuery): void {
if (!query) {
return;
}
const cacheKey = query.id;
this._reactiveQueries.delete(cacheKey);
for (const type of query.condition.componentTypes) {
const queries = this._reactiveQueriesByComponent.get(type);
if (queries) {
queries.delete(query);
if (queries.size === 0) {
this._reactiveQueriesByComponent.delete(type);
}
}
}
query.dispose();
}
/**
@@ -794,6 +844,7 @@ export class QuerySystem {
*
* 根据组件类型列表生成对应的位掩码。
* 使用缓存避免重复计算。
* 注意:必须使用ComponentRegistry来确保与Entity.componentMask使用相同的bitIndex
*
* @param componentTypes 组件类型列表
* @returns 生成的位掩码
@@ -810,10 +861,20 @@ export class QuerySystem {
return cached;
}
let mask = ComponentTypeManager.instance.getEntityBits(componentTypes);
// 使用ComponentRegistry而不是ComponentTypeManager,确保bitIndex一致
let mask = BitMask64Utils.clone(BitMask64Utils.ZERO);
for (const type of componentTypes) {
// 确保组件已注册
if (!ComponentRegistry.isRegistered(type)) {
ComponentRegistry.register(type);
}
const bitMask = ComponentRegistry.getBitMask(type);
BitMask64Utils.orInPlace(mask, bitMask);
}
// 缓存结果
this.componentMaskCache.set(cacheKey, mask.getValue());
return mask.getValue();
this.componentMaskCache.set(cacheKey, mask);
return mask;
}
/**
@@ -882,7 +943,7 @@ export class QuerySystem {
}))
},
cacheStats: {
size: this.queryCache.size,
size: this._reactiveQueries.size,
hitRate: this.queryStats.totalQueries > 0 ?
(this.queryStats.cacheHits / this.queryStats.totalQueries * 100).toFixed(2) + '%' : '0%'
}
@@ -891,12 +952,230 @@ export class QuerySystem {
/**
* 获取实体所属的原型信息
*
*
* @param entity 要查询的实体
*/
public getEntityArchetype(entity: Entity): Archetype | undefined {
return this.archetypeSystem.getEntityArchetype(entity);
}
// ============================================================
// 响应式查询支持(内部智能缓存)
// ============================================================
/**
* 响应式查询集合(内部使用,作为智能缓存)
* 传统查询API(queryAll/queryAny/queryNone)内部自动使用响应式查询优化性能
*/
private _reactiveQueries: Map<string, ReactiveQuery> = new Map();
/**
* 按组件类型索引的响应式查询
* 用于快速定位哪些查询关心某个组件类型
*/
private _reactiveQueriesByComponent: Map<ComponentType, Set<ReactiveQuery>> = new Map();
/**
* 获取或创建内部响应式查询(作为智能缓存)
*
* @param queryType 查询类型
* @param componentTypes 组件类型列表
* @returns 响应式查询实例
*/
private getOrCreateReactiveQuery(
queryType: QueryConditionType,
componentTypes: ComponentType[]
): ReactiveQuery {
// 生成缓存键(与传统缓存键格式一致)
const cacheKey = this.generateCacheKey(queryType, componentTypes);
// 检查是否已存在响应式查询
let reactiveQuery = this._reactiveQueries.get(cacheKey);
if (!reactiveQuery) {
// 创建查询条件
const mask = this.createComponentMask(componentTypes);
const condition: QueryCondition = {
type: queryType,
componentTypes,
mask
};
// 创建响应式查询(禁用批量模式,保持实时性)
reactiveQuery = new ReactiveQuery(condition, {
enableBatchMode: false,
debug: false
});
// 初始化查询结果(使用传统方式获取初始数据)
const initialEntities = this.executeTraditionalQuery(queryType, componentTypes);
reactiveQuery.initializeWith(initialEntities);
// 注册响应式查询
this._reactiveQueries.set(cacheKey, reactiveQuery);
// 为每个组件类型注册索引
for (const type of componentTypes) {
let queries = this._reactiveQueriesByComponent.get(type);
if (!queries) {
queries = new Set();
this._reactiveQueriesByComponent.set(type, queries);
}
queries.add(reactiveQuery);
}
this._logger.debug(`创建内部响应式查询缓存: ${cacheKey}`);
}
return reactiveQuery;
}
/**
* 执行传统查询(内部使用,用于响应式查询初始化)
*
* @param queryType 查询类型
* @param componentTypes 组件类型列表
* @returns 匹配的实体列表
*/
private executeTraditionalQuery(
queryType: QueryConditionType,
componentTypes: ComponentType[]
): Entity[] {
switch (queryType) {
case QueryConditionType.ALL: {
const archetypeResult = this.archetypeSystem.queryArchetypes(componentTypes, 'AND');
const entities: Entity[] = [];
for (const archetype of archetypeResult.archetypes) {
for (const entity of archetype.entities) {
entities.push(entity);
}
}
return entities;
}
case QueryConditionType.ANY: {
const archetypeResult = this.archetypeSystem.queryArchetypes(componentTypes, 'OR');
const entities: Entity[] = [];
for (const archetype of archetypeResult.archetypes) {
for (const entity of archetype.entities) {
entities.push(entity);
}
}
return entities;
}
case QueryConditionType.NONE: {
const mask = this.createComponentMask(componentTypes);
return this.entities.filter(entity =>
BitMask64Utils.hasNone(entity.componentMask, mask)
);
}
default:
return [];
}
}
/**
* 通知所有响应式查询实体已添加
*
* 使用组件类型索引,只通知关心该实体组件的查询
*
* @param entity 添加的实体
*/
private notifyReactiveQueriesEntityAdded(entity: Entity): void {
if (this._reactiveQueries.size === 0) return;
const notified = new Set<ReactiveQuery>();
for (const component of entity.components) {
const componentType = component.constructor as ComponentType;
const queries = this._reactiveQueriesByComponent.get(componentType);
if (queries) {
for (const query of queries) {
if (!notified.has(query)) {
query.notifyEntityAdded(entity);
notified.add(query);
}
}
}
}
}
/**
* 通知响应式查询实体已移除
*
* 使用组件类型索引,只通知关心该实体组件的查询
*
* @param entity 移除的实体
* @param componentTypes 实体移除前的组件类型列表
*/
private notifyReactiveQueriesEntityRemoved(entity: Entity, componentTypes: ComponentType[]): void {
if (this._reactiveQueries.size === 0) return;
const notified = new Set<ReactiveQuery>();
for (const componentType of componentTypes) {
const queries = this._reactiveQueriesByComponent.get(componentType);
if (queries) {
for (const query of queries) {
if (!notified.has(query)) {
query.notifyEntityRemoved(entity);
notified.add(query);
}
}
}
}
}
/**
* 通知响应式查询实体已移除(后备方案)
*
* 当实体已经清空组件时使用,通知所有查询
*
* @param entity 移除的实体
*/
private notifyReactiveQueriesEntityRemovedFallback(entity: Entity): void {
if (this._reactiveQueries.size === 0) return;
for (const query of this._reactiveQueries.values()) {
query.notifyEntityRemoved(entity);
}
}
/**
* 通知响应式查询实体已变化
*
* 使用混合策略:
* 1. 首先通知关心实体当前组件的查询
* 2. 然后通知所有其他查询(包括那些可能因为组件移除而不再匹配的查询)
*
* @param entity 变化的实体
*/
private notifyReactiveQueriesEntityChanged(entity: Entity): void {
if (this._reactiveQueries.size === 0) {
return;
}
const notified = new Set<ReactiveQuery>();
for (const component of entity.components) {
const componentType = component.constructor as ComponentType;
const queries = this._reactiveQueriesByComponent.get(componentType);
if (queries) {
for (const query of queries) {
if (!notified.has(query)) {
query.notifyEntityChanged(entity);
notified.add(query);
}
}
}
}
for (const query of this._reactiveQueries.values()) {
if (!notified.has(query)) {
query.notifyEntityChanged(entity);
}
}
}
}
/**

View File

@@ -0,0 +1,36 @@
import { ComponentType } from './ComponentStorage';
import { BitMask64Data } from '../Utils/BigIntCompatibility';
import { Entity } from '../Entity';
/**
* 查询条件类型
*/
export enum QueryConditionType {
/** 必须包含所有指定组件 */
ALL = 'all',
/** 必须包含任意一个指定组件 */
ANY = 'any',
/** 不能包含任何指定组件 */
NONE = 'none'
}
/**
* 查询条件接口
*/
export interface QueryCondition {
type: QueryConditionType;
componentTypes: ComponentType[];
mask: BitMask64Data;
}
/**
* 实体查询结果接口
*/
export interface QueryResult {
entities: readonly Entity[];
count: number;
/** 查询执行时间(毫秒) */
executionTime: number;
/** 是否来自缓存 */
fromCache: boolean;
}

View File

@@ -0,0 +1,475 @@
import { Entity } from '../Entity';
import { QueryCondition, QueryConditionType } from './QueryTypes';
import { BitMask64Utils } from '../Utils/BigIntCompatibility';
import { createLogger } from '../../Utils/Logger';
const logger = createLogger('ReactiveQuery');
/**
* 响应式查询变化类型
*/
export enum ReactiveQueryChangeType {
/** 实体添加到查询结果 */
ADDED = 'added',
/** 实体从查询结果移除 */
REMOVED = 'removed',
/** 查询结果批量更新 */
BATCH_UPDATE = 'batch_update'
}
/**
* 响应式查询变化事件
*/
export interface ReactiveQueryChange {
/** 变化类型 */
type: ReactiveQueryChangeType;
/** 变化的实体 */
entity?: Entity;
/** 批量变化的实体 */
entities?: readonly Entity[];
/** 新增的实体列表(仅batch_update时有效) */
added?: readonly Entity[];
/** 移除的实体列表(仅batch_update时有效) */
removed?: readonly Entity[];
}
/**
* 响应式查询监听器
*/
export type ReactiveQueryListener = (change: ReactiveQueryChange) => void;
/**
* 响应式查询配置
*/
export interface ReactiveQueryConfig {
/** 是否启用批量模式(减少通知频率) */
enableBatchMode?: boolean;
/** 批量模式的延迟时间(毫秒) */
batchDelay?: number;
/** 调试模式 */
debug?: boolean;
}
/**
* 响应式查询类
*
* 提供基于事件驱动的实体查询机制,只在实体/组件真正变化时触发通知。
*
* 核心特性:
* - Event-driven: 基于事件的增量更新
* - 精确通知: 只通知真正匹配的变化
* - 性能优化: 避免每帧重复查询
*
* @example
* ```typescript
* // 创建响应式查询
* const query = new ReactiveQuery(querySystem, {
* type: QueryConditionType.ALL,
* componentTypes: [Position, Velocity],
* mask: createMask([Position, Velocity])
* });
*
* // 订阅变化
* query.subscribe((change) => {
* if (change.type === ReactiveQueryChangeType.ADDED) {
* console.log('新实体:', change.entity);
* }
* });
*
* // 获取当前结果
* const entities = query.getEntities();
* ```
*/
export class ReactiveQuery {
/** 当前查询结果 */
private _entities: Entity[] = [];
/** 实体ID集合,用于快速查找 */
private _entityIdSet: Set<number> = new Set();
/** 查询条件 */
private readonly _condition: QueryCondition;
/** 监听器列表 */
private _listeners: ReactiveQueryListener[] = [];
/** 配置 */
private readonly _config: ReactiveQueryConfig;
/** 批量变化缓存 */
private _batchChanges: {
added: Entity[];
removed: Entity[];
timer: ReturnType<typeof setTimeout> | null;
};
/** 查询ID(用于调试) */
private readonly _id: string;
/** 是否已激活 */
private _active: boolean = true;
constructor(condition: QueryCondition, config: ReactiveQueryConfig = {}) {
this._condition = condition;
this._config = {
enableBatchMode: config.enableBatchMode ?? true,
batchDelay: config.batchDelay ?? 16, // 默认一帧
debug: config.debug ?? false
};
this._id = this.generateQueryId();
this._batchChanges = {
added: [],
removed: [],
timer: null
};
if (this._config.debug) {
logger.debug(`创建ReactiveQuery: ${this._id}`);
}
}
/**
* 生成查询ID
*/
private generateQueryId(): string {
const typeStr = this._condition.type;
const componentsStr = this._condition.componentTypes
.map(t => t.name)
.sort()
.join(',');
return `${typeStr}:${componentsStr}`;
}
/**
* 订阅查询变化
*
* @param listener 监听器函数
* @returns 取消订阅的函数
*/
public subscribe(listener: ReactiveQueryListener): () => void {
if (!this._active) {
throw new Error(`Cannot subscribe to disposed ReactiveQuery ${this._id}`);
}
if (typeof listener !== 'function') {
throw new TypeError('Listener must be a function');
}
this._listeners.push(listener);
if (this._config.debug) {
logger.debug(`订阅ReactiveQuery: ${this._id}, 监听器数量: ${this._listeners.length}`);
}
return () => {
const index = this._listeners.indexOf(listener);
if (index !== -1) {
this._listeners.splice(index, 1);
}
};
}
/**
* 取消所有订阅
*/
public unsubscribeAll(): void {
this._listeners.length = 0;
}
/**
* 获取当前查询结果
*/
public getEntities(): readonly Entity[] {
return this._entities;
}
/**
* 获取查询结果数量
*/
public get count(): number {
return this._entities.length;
}
/**
* 检查实体是否匹配查询条件
*
* @param entity 要检查的实体
* @returns 是否匹配
*/
public matches(entity: Entity): boolean {
const entityMask = entity.componentMask;
switch (this._condition.type) {
case QueryConditionType.ALL:
return BitMask64Utils.hasAll(entityMask, this._condition.mask);
case QueryConditionType.ANY:
return BitMask64Utils.hasAny(entityMask, this._condition.mask);
case QueryConditionType.NONE:
return BitMask64Utils.hasNone(entityMask, this._condition.mask);
default:
return false;
}
}
/**
* 通知实体添加
*
* 当Scene中添加实体时调用
*
* @param entity 添加的实体
*/
public notifyEntityAdded(entity: Entity): void {
if (!this._active) return;
// 检查实体是否匹配查询条件
if (!this.matches(entity)) {
return;
}
// 检查是否已存在
if (this._entityIdSet.has(entity.id)) {
return;
}
// 添加到结果集
this._entities.push(entity);
this._entityIdSet.add(entity.id);
// 通知监听器
if (this._config.enableBatchMode) {
this.addToBatch('added', entity);
} else {
this.notifyListeners({
type: ReactiveQueryChangeType.ADDED,
entity
});
}
if (this._config.debug) {
logger.debug(`ReactiveQuery ${this._id}: 实体添加 ${entity.name}(${entity.id})`);
}
}
/**
* 通知实体移除
*
* 当Scene中移除实体时调用
*
* @param entity 移除的实体
*/
public notifyEntityRemoved(entity: Entity): void {
if (!this._active) return;
// 检查是否在结果集中
if (!this._entityIdSet.has(entity.id)) {
return;
}
// 从结果集移除
const index = this._entities.indexOf(entity);
if (index !== -1) {
this._entities.splice(index, 1);
}
this._entityIdSet.delete(entity.id);
// 通知监听器
if (this._config.enableBatchMode) {
this.addToBatch('removed', entity);
} else {
this.notifyListeners({
type: ReactiveQueryChangeType.REMOVED,
entity
});
}
if (this._config.debug) {
logger.debug(`ReactiveQuery ${this._id}: 实体移除 ${entity.name}(${entity.id})`);
}
}
/**
* 通知实体组件变化
*
* 当实体的组件发生变化时调用
*
* @param entity 变化的实体
*/
public notifyEntityChanged(entity: Entity): void {
if (!this._active) return;
const wasMatching = this._entityIdSet.has(entity.id);
const isMatching = this.matches(entity);
if (wasMatching && !isMatching) {
// 实体不再匹配,从结果集移除
this.notifyEntityRemoved(entity);
} else if (!wasMatching && isMatching) {
// 实体现在匹配,添加到结果集
this.notifyEntityAdded(entity);
}
}
/**
* 批量初始化查询结果
*
* @param entities 初始实体列表
*/
public initializeWith(entities: readonly Entity[]): void {
// 清空现有结果
this._entities.length = 0;
this._entityIdSet.clear();
// 筛选匹配的实体
for (const entity of entities) {
if (this.matches(entity)) {
this._entities.push(entity);
this._entityIdSet.add(entity.id);
}
}
if (this._config.debug) {
logger.debug(`ReactiveQuery ${this._id}: 初始化 ${this._entities.length} 个实体`);
}
}
/**
* 添加到批量变化缓存
*/
private addToBatch(type: 'added' | 'removed', entity: Entity): void {
if (type === 'added') {
this._batchChanges.added.push(entity);
} else {
this._batchChanges.removed.push(entity);
}
// 启动批量通知定时器
if (this._batchChanges.timer === null) {
this._batchChanges.timer = setTimeout(() => {
this.flushBatchChanges();
}, this._config.batchDelay);
}
}
/**
* 刷新批量变化
*/
private flushBatchChanges(): void {
if (this._batchChanges.added.length === 0 && this._batchChanges.removed.length === 0) {
this._batchChanges.timer = null;
return;
}
const added = [...this._batchChanges.added];
const removed = [...this._batchChanges.removed];
// 清空缓存
this._batchChanges.added.length = 0;
this._batchChanges.removed.length = 0;
this._batchChanges.timer = null;
// 通知监听器
this.notifyListeners({
type: ReactiveQueryChangeType.BATCH_UPDATE,
added,
removed,
entities: this._entities
});
if (this._config.debug) {
logger.debug(`ReactiveQuery ${this._id}: 批量更新 +${added.length} -${removed.length}`);
}
}
/**
* 通知所有监听器
*/
private notifyListeners(change: ReactiveQueryChange): void {
const listeners = [...this._listeners];
for (const listener of listeners) {
try {
listener(change);
} catch (error) {
logger.error(`ReactiveQuery ${this._id}: 监听器执行出错`, error);
}
}
}
/**
* 暂停响应式查询
*
* 暂停后不再响应实体变化,但可以继续获取当前结果
*/
public pause(): void {
this._active = false;
// 清空批量变化缓存
if (this._batchChanges.timer !== null) {
clearTimeout(this._batchChanges.timer);
this._batchChanges.timer = null;
}
this._batchChanges.added.length = 0;
this._batchChanges.removed.length = 0;
}
/**
* 恢复响应式查询
*/
public resume(): void {
this._active = true;
}
/**
* 销毁响应式查询
*
* 释放所有资源,清空监听器和结果集
*/
public dispose(): void {
if (this._batchChanges.timer !== null) {
clearTimeout(this._batchChanges.timer);
this._batchChanges.timer = null;
}
this._batchChanges.added.length = 0;
this._batchChanges.removed.length = 0;
this._active = false;
this.unsubscribeAll();
this._entities.length = 0;
this._entityIdSet.clear();
if (this._config.debug) {
logger.debug(`ReactiveQuery ${this._id}: 已销毁`);
}
}
/**
* 获取查询条件
*/
public get condition(): QueryCondition {
return this._condition;
}
/**
* 获取查询ID
*/
public get id(): string {
return this._id;
}
/**
* 检查是否激活
*/
public get active(): boolean {
return this._active;
}
/**
* 获取监听器数量
*/
public get listenerCount(): number {
return this._listeners.length;
}
}

View File

@@ -75,7 +75,7 @@ export interface SystemMetadata {
* @ECSSystem('Physics', { updateOrder: 10 })
* class PhysicsSystem extends EntitySystem {
* constructor(@Inject(CollisionSystem) private collision: CollisionSystem) {
* super(Matcher.of(Transform, RigidBody));
* super(Matcher.empty().all(Transform, RigidBody));
* }
* }
* ```

View File

@@ -6,6 +6,10 @@ import { ComponentStorageManager } from './Core/ComponentStorage';
import { QuerySystem } from './Core/QuerySystem';
import { TypeSafeEventSystem } from './Core/EventSystem';
import type { ReferenceTracker } from './Core/ReferenceTracker';
import type { ServiceContainer, ServiceType } from '../Core/ServiceContainer';
import type { TypedQueryBuilder } from './Core/Query/TypedQuery';
import type { SceneSerializationOptions, SceneDeserializationOptions } from './Serialization/SceneSerializer';
import type { IncrementalSnapshot, IncrementalSerializationOptions } from './Serialization/IncrementalSerializer';
/**
* 场景接口定义
@@ -67,6 +71,13 @@ export interface IScene {
*/
readonly referenceTracker: ReferenceTracker;
/**
* 服务容器
*
* 场景级别的依赖注入容器,用于管理服务的生命周期。
*/
readonly services: ServiceContainer;
/**
* 获取系统列表
*/
@@ -151,6 +162,140 @@ export interface IScene {
* 获取实体处理器
*/
getEntityProcessor<T extends EntitySystem>(type: new (...args: any[]) => T): T | null;
/**
* 根据ID查找实体
*/
findEntityById(id: number): Entity | null;
/**
* 根据名称查找实体
*/
getEntityByName(name: string): Entity | null;
/**
* 根据标签查找实体
*/
getEntitiesByTag(tag: number): Entity[];
/**
* 批量销毁实体
*/
destroyEntities(entities: Entity[]): void;
/**
* 查询拥有所有指定组件的实体
*/
queryAll(...componentTypes: any[]): { entities: readonly Entity[] };
/**
* 查询拥有任意一个指定组件的实体
*/
queryAny(...componentTypes: any[]): { entities: readonly Entity[] };
/**
* 查询不包含指定组件的实体
*/
queryNone(...componentTypes: any[]): { entities: readonly Entity[] };
/**
* 创建类型安全的查询构建器
*/
query(): TypedQueryBuilder;
/**
* 通过类型获取System实例
*/
getSystem<T extends EntitySystem>(systemType: ServiceType<T>): T | null;
/**
* 批量注册EntitySystem到场景
*/
registerSystems(systemTypes: Array<ServiceType<EntitySystem>>): EntitySystem[];
/**
* 添加系统到场景
*/
addSystem(system: EntitySystem): EntitySystem;
/**
* 从场景中删除系统
*/
removeSystem(system: EntitySystem): void;
/**
* 获取场景统计信息
*/
getStats(): {
entityCount: number;
processorCount: number;
componentStorageStats: Map<string, any>;
};
/**
* 获取场景的调试信息
*/
getDebugInfo(): {
name: string;
entityCount: number;
processorCount: number;
isRunning: boolean;
entities: Array<{
name: string;
id: number;
componentCount: number;
componentTypes: string[];
}>;
processors: Array<{
name: string;
updateOrder: number;
entityCount: number;
}>;
componentStats: Map<string, any>;
};
/**
* 序列化场景
*/
serialize(options?: SceneSerializationOptions): string | Uint8Array;
/**
* 反序列化场景
*/
deserialize(saveData: string | Uint8Array, options?: SceneDeserializationOptions): void;
/**
* 创建增量序列化的基础快照
*/
createIncrementalSnapshot(options?: IncrementalSerializationOptions): void;
/**
* 增量序列化场景
*/
serializeIncremental(options?: IncrementalSerializationOptions): IncrementalSnapshot;
/**
* 应用增量变更到场景
*/
applyIncremental(
incremental: IncrementalSnapshot | string | Uint8Array,
componentRegistry?: Map<string, any>
): void;
/**
* 更新增量快照基准
*/
updateIncrementalSnapshot(options?: IncrementalSerializationOptions): void;
/**
* 清除增量快照
*/
clearIncrementalSnapshot(): void;
/**
* 检查是否有增量快照
*/
hasIncrementalSnapshot(): boolean;
}
/**
@@ -171,12 +316,4 @@ export interface ISceneConfig {
* 场景名称
*/
name?: string;
/**
* 性能监控器实例(可选)
*
* 如果不提供Scene会自动从Core.services获取全局PerformanceMonitor。
* 提供此参数可以实现场景级别的独立性能监控。
*/
performanceMonitor?: any;
}

View File

@@ -15,13 +15,13 @@ import { IncrementalSerializer, IncrementalSnapshot, IncrementalSerializationOpt
import { ComponentPoolManager } from './Core/ComponentPool';
import { PerformanceMonitor } from '../Utils/PerformanceMonitor';
import { ServiceContainer, type ServiceType } from '../Core/ServiceContainer';
import { createInstance, isInjectable } from '../Core/DI';
import { createInstance, isInjectable, injectProperties } from '../Core/DI';
import { isUpdatable, getUpdatableMetadata } from '../Core/DI/Decorators';
import { createLogger } from '../Utils/Logger';
/**
* 游戏场景默认实现类
*
*
* 实现IScene接口提供场景的基础功能。
* 推荐使用组合而非继承的方式来构建自定义场景。
*/
@@ -97,11 +97,11 @@ export class Scene implements IScene {
private readonly logger: ReturnType<typeof createLogger>;
/**
* 性能监控器
* 性能监控器缓存
*
* 用于监控场景和系统的性能。可以在构造函数中注入如果不提供则从Core获取。
* 用于监控场景和系统的性能。从 ServiceContainer 获取。
*/
private readonly _performanceMonitor: PerformanceMonitor;
private _performanceMonitor: PerformanceMonitor | null = null;
/**
* 场景是否已开始运行
@@ -182,10 +182,6 @@ export class Scene implements IScene {
this._services = new ServiceContainer();
this.logger = createLogger('Scene');
// 从配置获取 PerformanceMonitor如果未提供则创建一个新实例
// Scene 应该是独立的,不依赖于 Core通过构造函数参数明确依赖关系
this._performanceMonitor = config?.performanceMonitor || new PerformanceMonitor();
if (config?.name) {
this.name = config.name;
}
@@ -201,6 +197,19 @@ export class Scene implements IScene {
}
}
/**
* 获取性能监控器
*
* 从 ServiceContainer 获取,如果未注册则创建默认实例(向后兼容)
*/
private get performanceMonitor(): PerformanceMonitor {
if (!this._performanceMonitor) {
this._performanceMonitor = this._services.tryResolve(PerformanceMonitor)
?? new PerformanceMonitor();
}
return this._performanceMonitor;
}
/**
* 初始化场景
*
@@ -527,7 +536,7 @@ export class Scene implements IScene {
* @Injectable()
* class PhysicsSystem extends EntitySystem {
* constructor(@Inject(CollisionSystem) private collision: CollisionSystem) {
* super(Matcher.of(Transform));
* super(Matcher.empty().all(Transform));
* }
* }
* scene.addEntityProcessor(PhysicsSystem);
@@ -547,7 +556,9 @@ export class Scene implements IScene {
constructor = systemTypeOrInstance;
if (this._services.isRegistered(constructor)) {
return this._services.resolve(constructor) as T;
const existingSystem = this._services.resolve(constructor) as T;
this.logger.debug(`System ${constructor.name} already registered, returning existing instance`);
return existingSystem;
}
if (isInjectable(constructor)) {
@@ -560,13 +571,23 @@ export class Scene implements IScene {
constructor = system.constructor;
if (this._services.isRegistered(constructor)) {
return system;
const existingSystem = this._services.resolve(constructor);
if (existingSystem === system) {
this.logger.debug(`System ${constructor.name} instance already registered, returning it`);
return system;
} else {
this.logger.warn(
`Attempting to register a different instance of ${constructor.name}, ` +
`but type is already registered. Returning existing instance.`
);
return existingSystem as T;
}
}
}
system.scene = this;
system.setPerformanceMonitor(this._performanceMonitor);
system.setPerformanceMonitor(this.performanceMonitor);
const metadata = getSystemMetadata(constructor);
if (metadata?.updateOrder !== undefined) {
@@ -578,8 +599,12 @@ export class Scene implements IScene {
this._services.registerInstance(constructor, system);
injectProperties(system, this._services);
system.initialize();
this.logger.debug(`System ${constructor.name} registered and initialized`);
return system;
}
@@ -597,7 +622,7 @@ export class Scene implements IScene {
* @Injectable()
* @ECSSystem('Collision', { updateOrder: 5 })
* class CollisionSystem extends EntitySystem implements IService {
* constructor() { super(Matcher.of(Collider)); }
* constructor() { super(Matcher.empty().all(Collider)); }
* dispose() {}
* }
*
@@ -605,7 +630,7 @@ export class Scene implements IScene {
* @ECSSystem('Physics', { updateOrder: 10 })
* class PhysicsSystem extends EntitySystem implements IService {
* constructor(@Inject(CollisionSystem) private collision: CollisionSystem) {
* super(Matcher.of(Transform, RigidBody));
* super(Matcher.empty().all(Transform, RigidBody));
* }
* dispose() {}
* }

View File

@@ -4,6 +4,7 @@ import { Time } from '../Utils/Time';
import { createLogger } from '../Utils/Logger';
import type { IService } from '../Core/ServiceContainer';
import { World } from './World';
import { PerformanceMonitor } from '../Utils/PerformanceMonitor';
/**
* 单场景管理器
@@ -73,14 +74,20 @@ export class SceneManager implements IService {
*/
private _onSceneChangedCallback?: () => void;
/**
* 性能监控器(从 Core 注入)
*/
private _performanceMonitor: PerformanceMonitor | null = null;
/**
* 默认场景ID
*/
private static readonly DEFAULT_SCENE_ID = '__main__';
constructor() {
constructor(performanceMonitor?: PerformanceMonitor) {
this._defaultWorld = new World({ name: '__default__' });
this._defaultWorld.start();
this._performanceMonitor = performanceMonitor || null;
}
/**
@@ -111,6 +118,11 @@ export class SceneManager implements IService {
// 移除旧场景
this._defaultWorld.removeAllScenes();
// 注册全局 PerformanceMonitor 到 Scene 的 ServiceContainer
if (this._performanceMonitor) {
scene.services.registerInstance(PerformanceMonitor, this._performanceMonitor);
}
// 通过 World 创建新场景
this._defaultWorld.createScene(SceneManager.DEFAULT_SCENE_ID, scene);
this._defaultWorld.setSceneActive(SceneManager.DEFAULT_SCENE_ID, true);

View File

@@ -275,15 +275,38 @@ export class SceneSerializer {
// 将实体添加到场景
for (const entity of entities) {
scene.addEntity(entity);
scene.addEntity(entity, true);
this.addChildrenRecursively(entity, scene);
}
// 统一清理缓存(批量操作完成后)
scene.querySystem.clearCache();
scene.clearSystemEntityCaches();
// 反序列化场景自定义数据
if (serializedScene.sceneData) {
this.deserializeSceneData(serializedScene.sceneData, scene.sceneData);
}
}
/**
* 递归添加实体的所有子实体到场景
*
* 修复反序列化时子实体丢失的问题:
* EntitySerializer.deserialize会提前设置子实体的scene引用
* 导致Entity.addChild的条件判断(!child.scene)跳过scene.addEntity调用。
* 因此需要在SceneSerializer中统一递归添加所有子实体。
*
* @param entity 父实体
* @param scene 目标场景
*/
private static addChildrenRecursively(entity: Entity, scene: IScene): void {
for (const child of entity.children) {
scene.addEntity(child, true); // 延迟缓存清理
this.addChildrenRecursively(child, scene); // 递归处理子实体的子实体
}
}
/**
* 序列化场景自定义数据
*

View File

@@ -36,7 +36,7 @@ interface EventListenerRecord {
* // 传统方式
* class MovementSystem extends EntitySystem {
* constructor() {
* super(Matcher.of(Transform, Velocity));
* super(Matcher.empty().all(Transform, Velocity));
* }
*
* protected process(entities: readonly Entity[]): void {
@@ -51,7 +51,7 @@ interface EventListenerRecord {
* // 类型安全方式
* class MovementSystem extends EntitySystem<[typeof Transform, typeof Velocity]> {
* constructor() {
* super(Matcher.of(Transform, Velocity));
* super(Matcher.empty().all(Transform, Velocity));
* }
*
* protected process(entities: readonly Entity[]): void {
@@ -555,6 +555,7 @@ export abstract class EntitySystem<
try {
this.onBegin();
// 查询实体并存储到帧缓存中
// 响应式查询会自动维护最新的实体列表updateEntityTracking会在检测到变化时invalidate
this._entityCache.frame = this.queryEntities();
entityCount = this._entityCache.frame.length;

View File

@@ -1,101 +0,0 @@
import { Bits } from './Bits';
import { getComponentTypeName } from '../Decorators';
import { ComponentType } from "../../Types";
/**
* 组件类型管理器
* 负责管理组件类型的注册和ID分配
* 支持无限数量的组件类型(通过自动扩展 BitMask
*/
export class ComponentTypeManager {
private static _instance: ComponentTypeManager;
private _componentTypes = new Map<Function, number>();
private _typeNames = new Map<number, string>();
private _nextTypeId = 0;
/**
* 获取单例实例
*/
public static get instance(): ComponentTypeManager {
if (!ComponentTypeManager._instance) {
ComponentTypeManager._instance = new ComponentTypeManager();
}
return ComponentTypeManager._instance;
}
private constructor() {}
/**
* 获取组件类型的ID
* @param componentType 组件类型构造函数
* @returns 组件类型ID
*/
public getTypeId(componentType: ComponentType): number {
let typeId = this._componentTypes.get(componentType);
if (typeId === undefined) {
typeId = this._nextTypeId++;
this._componentTypes.set(componentType, typeId);
this._typeNames.set(typeId, getComponentTypeName(componentType));
}
return typeId;
}
/**
* 获取组件类型名称
* @param typeId 组件类型ID
* @returns 组件类型名称
*/
public getTypeName(typeId: number): string {
return this._typeNames.get(typeId) || 'Unknown';
}
/**
* 创建包含指定组件类型的Bits对象
* @param componentTypes 组件类型构造函数数组
* @returns Bits对象
*/
public createBits(...componentTypes: ComponentType[]): Bits {
const bits = new Bits();
for (const componentType of componentTypes) {
const typeId = this.getTypeId(componentType);
bits.set(typeId);
}
return bits;
}
/**
* 获取实体的组件位掩码
* @param components 组件数组
* @returns Bits对象
*/
public getEntityBits(components: ComponentType[]): Bits {
const bits = new Bits();
for (const component of components) {
const typeId = this.getTypeId(component);
bits.set(typeId);
}
return bits;
}
/**
* 重置管理器(主要用于测试)
*/
public reset(): void {
this._componentTypes.clear();
this._typeNames.clear();
this._nextTypeId = 0;
}
/**
* 获取已注册的组件类型数量
*/
public get registeredTypeCount(): number {
return this._componentTypes.size;
}
}

View File

@@ -4,7 +4,6 @@ export { EntityProcessorList } from './EntityProcessorList';
export { IdentifierPool } from './IdentifierPool';
export { Matcher } from './Matcher';
export { Bits } from './Bits';
export { ComponentTypeManager } from './ComponentTypeManager';
export { BitMask64Utils, BitMask64Data } from './BigIntCompatibility';
export { SparseSet } from './SparseSet';
export { ComponentSparseSet } from './ComponentSparseSet';

View File

@@ -15,4 +15,6 @@ export * from './Core/Storage';
export * from './Core/StorageDecorators';
export * from './Serialization';
export { ReferenceTracker, getSceneByEntityId } from './Core/ReferenceTracker';
export type { EntityRefRecord } from './Core/ReferenceTracker';
export type { EntityRefRecord } from './Core/ReferenceTracker';
export { ReactiveQuery, ReactiveQueryChangeType } from './Core/ReactiveQuery';
export type { ReactiveQueryChange, ReactiveQueryListener, ReactiveQueryConfig } from './Core/ReactiveQuery';

View File

@@ -0,0 +1,359 @@
import type { Core } from '../Core';
import type { ServiceContainer } from '../Core/ServiceContainer';
import { IPlugin } from '../Core/Plugin';
import { createLogger } from '../Utils/Logger';
import type { Scene } from '../ECS/Scene';
import type { IScene } from '../ECS/IScene';
import type { Entity } from '../ECS/Entity';
import type { Component } from '../ECS/Component';
import type { EntitySystem } from '../ECS/Systems/EntitySystem';
import { WorldManager } from '../ECS/WorldManager';
import { Injectable, Inject } from '../Core/DI/Decorators';
import type { IService } from '../Core/ServiceContainer';
import type { PerformanceData } from '../Utils/PerformanceMonitor';
const logger = createLogger('DebugPlugin');
/**
* ECS 调试插件统计信息
*/
export interface ECSDebugStats {
scenes: SceneDebugInfo[];
totalEntities: number;
totalSystems: number;
timestamp: number;
}
/**
* 场景调试信息
*/
export interface SceneDebugInfo {
name: string;
entityCount: number;
systems: SystemDebugInfo[];
entities: EntityDebugInfo[];
}
/**
* 系统调试信息
*/
export interface SystemDebugInfo {
name: string;
enabled: boolean;
updateOrder: number;
entityCount: number;
performance?: {
avgExecutionTime: number;
maxExecutionTime: number;
totalCalls: number;
};
}
/**
* 实体调试信息
*/
export interface EntityDebugInfo {
id: number;
name: string;
enabled: boolean;
tag: number;
componentCount: number;
components: ComponentDebugInfo[];
}
/**
* 组件调试信息
*/
export interface ComponentDebugInfo {
type: string;
data: any;
}
/**
* ECS 调试插件
*
* 提供运行时调试功能:
* - 实时查看实体和组件信息
* - System 执行统计
* - 性能监控
* - 实体查询
*
* @example
* ```typescript
* const core = Core.create();
* const debugPlugin = new DebugPlugin({ autoStart: true, updateInterval: 1000 });
* await core.pluginManager.install(debugPlugin);
*
* // 获取调试信息
* const stats = debugPlugin.getStats();
* console.log('Total entities:', stats.totalEntities);
*
* // 查询实体
* const entities = debugPlugin.queryEntities({ tag: 1 });
* ```
*/
@Injectable()
export class DebugPlugin implements IPlugin, IService {
readonly name = '@esengine/debug-plugin';
readonly version = '1.0.0';
private worldManager: WorldManager | null = null;
private updateInterval: number;
private updateTimer: any = null;
private autoStart: boolean;
/**
* 创建调试插件实例
*
* @param options - 配置选项
*/
constructor(options?: { autoStart?: boolean; updateInterval?: number }) {
this.autoStart = options?.autoStart ?? false;
this.updateInterval = options?.updateInterval ?? 1000;
}
/**
* 安装插件
*/
async install(core: Core, services: ServiceContainer): Promise<void> {
this.worldManager = services.resolve(WorldManager);
logger.info('ECS Debug Plugin installed');
if (this.autoStart) {
this.start();
}
}
/**
* 卸载插件
*/
async uninstall(): Promise<void> {
this.stop();
this.worldManager = null;
logger.info('ECS Debug Plugin uninstalled');
}
/**
* 实现 IService 接口
*/
public dispose(): void {
this.stop();
this.worldManager = null;
}
/**
* 启动调试监控
*/
public start(): void {
if (this.updateTimer) {
logger.warn('Debug monitoring already started');
return;
}
logger.info('Starting debug monitoring');
this.updateTimer = setInterval(() => {
this.logStats();
}, this.updateInterval);
}
/**
* 停止调试监控
*/
public stop(): void {
if (this.updateTimer) {
clearInterval(this.updateTimer);
this.updateTimer = null;
logger.info('Debug monitoring stopped');
}
}
/**
* 获取当前 ECS 统计信息
*/
public getStats(): ECSDebugStats {
if (!this.worldManager) {
throw new Error('Plugin not installed');
}
const scenes: SceneDebugInfo[] = [];
let totalEntities = 0;
let totalSystems = 0;
const worlds = this.worldManager.getAllWorlds();
for (const world of worlds) {
for (const scene of world.getAllScenes()) {
const sceneInfo = this.getSceneInfo(scene);
scenes.push(sceneInfo);
totalEntities += sceneInfo.entityCount;
totalSystems += sceneInfo.systems.length;
}
}
return {
scenes,
totalEntities,
totalSystems,
timestamp: Date.now()
};
}
/**
* 获取场景调试信息
*/
public getSceneInfo(scene: IScene): SceneDebugInfo {
const entities = scene.entities.buffer;
const systems = scene.systems;
return {
name: scene.name,
entityCount: entities.length,
systems: systems.map(sys => this.getSystemInfo(sys)),
entities: entities.map(entity => this.getEntityInfo(entity))
};
}
/**
* 获取系统调试信息
*/
private getSystemInfo(system: EntitySystem): SystemDebugInfo {
const perfStats = system.getPerformanceStats();
return {
name: system.constructor.name,
enabled: system.enabled,
updateOrder: system.updateOrder,
entityCount: system.entities.length,
performance: perfStats ? {
avgExecutionTime: perfStats.averageTime,
maxExecutionTime: perfStats.maxTime,
totalCalls: perfStats.executionCount
} : undefined
};
}
/**
* 获取实体调试信息
*/
public getEntityInfo(entity: Entity): EntityDebugInfo {
const components = entity.components;
return {
id: entity.id,
name: entity.name,
enabled: entity.enabled,
tag: entity.tag,
componentCount: components.length,
components: components.map(comp => this.getComponentInfo(comp))
};
}
/**
* 获取组件调试信息
*/
private getComponentInfo(component: any): ComponentDebugInfo {
const type = component.constructor.name;
const data: any = {};
for (const key of Object.keys(component)) {
if (!key.startsWith('_')) {
const value = component[key];
if (typeof value !== 'function') {
data[key] = value;
}
}
}
return { type, data };
}
/**
* 查询实体
*
* @param filter - 查询过滤器
*/
public queryEntities(filter: {
sceneId?: string;
tag?: number;
name?: string;
hasComponent?: string;
}): EntityDebugInfo[] {
if (!this.worldManager) {
throw new Error('Plugin not installed');
}
const results: EntityDebugInfo[] = [];
const worlds = this.worldManager.getAllWorlds();
for (const world of worlds) {
for (const scene of world.getAllScenes()) {
if (filter.sceneId && scene.name !== filter.sceneId) {
continue;
}
for (const entity of scene.entities.buffer) {
if (filter.tag !== undefined && entity.tag !== filter.tag) {
continue;
}
if (filter.name && !entity.name.includes(filter.name)) {
continue;
}
if (filter.hasComponent) {
const hasComp = entity.components.some(
c => c.constructor.name === filter.hasComponent
);
if (!hasComp) {
continue;
}
}
results.push(this.getEntityInfo(entity));
}
}
}
return results;
}
/**
* 打印统计信息到日志
*/
private logStats(): void {
const stats = this.getStats();
logger.info('=== ECS Debug Stats ===');
logger.info(`Total Entities: ${stats.totalEntities}`);
logger.info(`Total Systems: ${stats.totalSystems}`);
logger.info(`Scenes: ${stats.scenes.length}`);
for (const scene of stats.scenes) {
logger.info(`\n[Scene: ${scene.name}]`);
logger.info(` Entities: ${scene.entityCount}`);
logger.info(` Systems: ${scene.systems.length}`);
for (const system of scene.systems) {
const perfStr = system.performance
? ` | Avg: ${system.performance.avgExecutionTime.toFixed(2)}ms, Max: ${system.performance.maxExecutionTime.toFixed(2)}ms`
: '';
logger.info(
` - ${system.name} (${system.enabled ? 'enabled' : 'disabled'}) | Entities: ${system.entityCount}${perfStr}`
);
}
}
logger.info('========================\n');
}
/**
* 导出调试数据为 JSON
*/
public exportJSON(): string {
const stats = this.getStats();
return JSON.stringify(stats, null, 2);
}
}

View File

@@ -0,0 +1 @@
export * from './DebugPlugin';

View File

@@ -2,6 +2,8 @@
* 框架核心类型定义
*/
import type { IWorldManagerConfig } from '../ECS';
// 导出TypeScript类型增强工具
export * from './TypeHelpers';
export * from './IUpdatable';
@@ -264,6 +266,8 @@ export interface ICoreConfig {
enableEntitySystems?: boolean;
/** 调试配置 */
debugConfig?: IECSDebugConfig;
/** WorldManager配置 */
worldManagerConfig?: IWorldManagerConfig;
}
/**

View File

@@ -0,0 +1,45 @@
import { IECSDebugConfig } from '../../Types';
import { Injectable } from '../../Core/DI/Decorators';
import type { IService } from '../../Core/ServiceContainer';
/**
* 调试配置服务
*
* 管理调试系统的配置信息
*/
@Injectable()
export class DebugConfigService implements IService {
private _config: IECSDebugConfig;
constructor() {
this._config = {
enabled: false,
websocketUrl: '',
debugFrameRate: 30,
autoReconnect: true,
channels: {
entities: true,
systems: true,
performance: true,
components: true,
scenes: true
}
};
}
public setConfig(config: IECSDebugConfig): void {
this._config = config;
}
public getConfig(): IECSDebugConfig {
return this._config;
}
public isEnabled(): boolean {
return this._config.enabled;
}
dispose(): void {
// 清理资源
}
}

View File

@@ -10,17 +10,20 @@ import { ComponentPoolManager } from '../../ECS/Core/ComponentPool';
import { Pool } from '../../Utils/Pool';
import { getComponentInstanceTypeName, getSystemInstanceTypeName } from '../../ECS/Decorators';
import type { IService } from '../../Core/ServiceContainer';
import type { IUpdatable } from '../../Types/IUpdatable';
import { SceneManager } from '../../ECS/SceneManager';
import { PerformanceMonitor } from '../PerformanceMonitor';
import { Injectable, Inject, Updatable } from '../../Core/DI/Decorators';
import { DebugConfigService } from './DebugConfigService';
/**
* 调试管理器
*
* 整合所有调试数据收集器,负责收集和发送调试数据
*
* 通过构造函数接收SceneManager和PerformanceMonitor避免直接依赖Core实例
*/
export class DebugManager implements IService {
@Injectable()
@Updatable()
export class DebugManager implements IService, IUpdatable {
private config: IECSDebugConfig;
private webSocketManager: WebSocketManager;
private entityCollector: EntityDataCollector;
@@ -35,19 +38,20 @@ export class DebugManager implements IService {
private lastSendTime: number = 0;
private sendInterval: number;
private isRunning: boolean = false;
private originalConsole = {
log: console.log.bind(console),
debug: console.debug.bind(console),
info: console.info.bind(console),
warn: console.warn.bind(console),
error: console.error.bind(console)
};
/**
* 构造调试管理器
* @param sceneManager 场景管理器
* @param performanceMonitor 性能监控器
* @param config 调试配置
*/
constructor(
sceneManager: SceneManager,
performanceMonitor: PerformanceMonitor,
config: IECSDebugConfig
@Inject(SceneManager) sceneManager: SceneManager,
@Inject(PerformanceMonitor) performanceMonitor: PerformanceMonitor,
@Inject(DebugConfigService) configService: DebugConfigService
) {
this.config = config;
this.config = configService.getConfig();
this.sceneManager = sceneManager;
this.performanceMonitor = performanceMonitor;
@@ -60,17 +64,20 @@ export class DebugManager implements IService {
// 初始化WebSocket管理器
this.webSocketManager = new WebSocketManager(
config.websocketUrl,
config.autoReconnect !== false
this.config.websocketUrl,
this.config.autoReconnect !== false
);
// 设置消息处理回调
this.webSocketManager.setMessageHandler(this.handleMessage.bind(this));
// 计算发送间隔(基于帧率)
const debugFrameRate = config.debugFrameRate || 30;
const debugFrameRate = this.config.debugFrameRate || 30;
this.sendInterval = 1000 / debugFrameRate;
// 拦截 console 日志
this.interceptConsole();
this.start();
}
@@ -94,6 +101,118 @@ export class DebugManager implements IService {
this.webSocketManager.disconnect();
}
/**
* 拦截 console 日志并转发到编辑器
*/
private interceptConsole(): void {
console.log = (...args: unknown[]) => {
this.sendLog('info', this.formatLogMessage(args));
this.originalConsole.log(...args);
};
console.debug = (...args: unknown[]) => {
this.sendLog('debug', this.formatLogMessage(args));
this.originalConsole.debug(...args);
};
console.info = (...args: unknown[]) => {
this.sendLog('info', this.formatLogMessage(args));
this.originalConsole.info(...args);
};
console.warn = (...args: unknown[]) => {
this.sendLog('warn', this.formatLogMessage(args));
this.originalConsole.warn(...args);
};
console.error = (...args: unknown[]) => {
this.sendLog('error', this.formatLogMessage(args));
this.originalConsole.error(...args);
};
}
/**
* 格式化日志消息
*/
private formatLogMessage(args: unknown[]): string {
return args.map(arg => {
if (typeof arg === 'string') return arg;
if (arg instanceof Error) return `${arg.name}: ${arg.message}`;
if (arg === null) return 'null';
if (arg === undefined) return 'undefined';
if (typeof arg === 'object') {
try {
return this.safeStringify(arg, 6);
} catch {
return Object.prototype.toString.call(arg);
}
}
return String(arg);
}).join(' ');
}
/**
* 安全的 JSON 序列化,支持循环引用和深度限制
*/
private safeStringify(obj: any, maxDepth: number = 6): string {
const seen = new WeakSet();
const stringify = (value: any, depth: number): any => {
if (value === null) return null;
if (value === undefined) return undefined;
if (typeof value !== 'object') return value;
if (depth >= maxDepth) {
return '[Max Depth Reached]';
}
if (seen.has(value)) {
return '[Circular]';
}
seen.add(value);
if (Array.isArray(value)) {
const result = value.map(item => stringify(item, depth + 1));
seen.delete(value);
return result;
}
const result: any = {};
for (const key in value) {
if (Object.prototype.hasOwnProperty.call(value, key)) {
result[key] = stringify(value[key], depth + 1);
}
}
seen.delete(value);
return result;
};
return JSON.stringify(stringify(obj, 0));
}
/**
* 发送日志到编辑器
*/
private sendLog(level: string, message: string): void {
if (!this.webSocketManager.getConnectionStatus()) {
return;
}
try {
this.webSocketManager.send({
type: 'log',
data: {
level,
message,
timestamp: new Date().toISOString()
}
});
} catch (error) {
// 静默失败,避免递归日志
}
}
/**
* 更新配置
*/
@@ -116,16 +235,12 @@ export class DebugManager implements IService {
}
}
/**
* 帧更新回调
*/
public onFrameUpdate(deltaTime: number): void {
public update(deltaTime?: number): void {
if (!this.isRunning || !this.config.enabled) return;
this.frameCounter++;
const currentTime = Date.now();
// 基于配置的帧率发送数据
if (currentTime - this.lastSendTime >= this.sendInterval) {
this.sendDebugData();
this.lastSendTime = currentTime;
@@ -213,7 +328,8 @@ export class DebugManager implements IService {
return;
}
const expandedData = this.entityCollector.expandLazyObject(entityId, componentIndex, propertyPath);
const scene = this.sceneManager.currentScene;
const expandedData = this.entityCollector.expandLazyObject(entityId, componentIndex, propertyPath, scene);
this.webSocketManager.send({
type: 'expand_lazy_object_response',
@@ -245,7 +361,8 @@ export class DebugManager implements IService {
return;
}
const properties = this.entityCollector.getComponentProperties(entityId, componentIndex);
const scene = this.sceneManager.currentScene;
const properties = this.entityCollector.getComponentProperties(entityId, componentIndex, scene);
this.webSocketManager.send({
type: 'get_component_properties_response',
@@ -268,7 +385,8 @@ export class DebugManager implements IService {
try {
const { requestId } = message;
const rawEntityList = this.entityCollector.getRawEntityList();
const scene = this.sceneManager.currentScene;
const rawEntityList = this.entityCollector.getRawEntityList(scene);
this.webSocketManager.send({
type: 'get_raw_entity_list_response',
@@ -300,7 +418,8 @@ export class DebugManager implements IService {
return;
}
const entityDetails = this.entityCollector.getEntityDetails(entityId);
const scene = this.sceneManager.currentScene;
const entityDetails = this.entityCollector.getEntityDetails(entityId, scene);
this.webSocketManager.send({
type: 'get_entity_details_response',
@@ -832,5 +951,12 @@ export class DebugManager implements IService {
*/
public dispose(): void {
this.stop();
// 恢复原始 console 方法
console.log = this.originalConsole.log;
console.debug = this.originalConsole.debug;
console.info = this.originalConsole.info;
console.warn = this.originalConsole.warn;
console.error = this.originalConsole.error;
}
}

View File

@@ -1,7 +1,6 @@
import { IEntityDebugData } from '../../Types';
import { Entity } from '../../ECS/Entity';
import { Component } from '../../ECS/Component';
import { ComponentTypeManager } from '../../ECS/Utils/ComponentTypeManager';
import { getComponentInstanceTypeName, getSystemInstanceTypeName } from '../../ECS/Decorators';
import { IScene } from '../../ECS/IScene';
@@ -264,8 +263,7 @@ export class EntityDataCollector {
componentCount: entity.components?.length || 0,
memory: 0
}))
.sort((a: any, b: any) => b.componentCount - a.componentCount)
.slice(0, 10);
.sort((a: any, b: any) => b.componentCount - a.componentCount);
}
@@ -304,7 +302,7 @@ export class EntityDataCollector {
});
if (archetype.entities) {
archetype.entities.slice(0, 5).forEach((entity: any) => {
archetype.entities.forEach((entity: any) => {
topEntities.push({
id: entity.id.toString(),
name: entity.name || `Entity_${entity.id}`,
@@ -353,7 +351,7 @@ export class EntityDataCollector {
});
if (archetype.entities) {
archetype.entities.slice(0, 5).forEach((entity: any) => {
archetype.entities.forEach((entity: any) => {
topEntities.push({
id: entity.id.toString(),
name: entity.name || `Entity_${entity.id}`,
@@ -722,20 +720,7 @@ export class EntityDataCollector {
properties: Record<string, any>;
}> {
return components.map((component: Component) => {
let typeName = getComponentInstanceTypeName(component);
if (!typeName || typeName === 'Object' || typeName === 'Function') {
try {
const typeManager = ComponentTypeManager.instance;
const componentType = component.constructor as any;
const typeId = typeManager.getTypeId(componentType);
typeName = typeManager.getTypeName(typeId);
} catch (error) {
typeName = 'UnknownComponent';
}
}
// 提取实际的组件属性
const typeName = getComponentInstanceTypeName(component);
const properties: Record<string, any> = {};
try {

View File

@@ -4,4 +4,5 @@ export { PerformanceDataCollector } from './PerformanceDataCollector';
export { ComponentDataCollector } from './ComponentDataCollector';
export { SceneDataCollector } from './SceneDataCollector';
export { WebSocketManager } from './WebSocketManager';
export { DebugManager } from './DebugManager';
export { DebugManager } from './DebugManager';
export { DebugConfigService } from './DebugConfigService';

View File

@@ -13,6 +13,9 @@ export { PluginManager } from './Core/PluginManager';
export { PluginState } from './Core/Plugin';
export type { IPlugin, IPluginMetadata } from './Core/Plugin';
// 内置插件
export * from './Plugins';
// 依赖注入
export {
Injectable,

View File

@@ -0,0 +1,668 @@
import { Core } from '../src/Core';
import { Scene } from '../src/ECS/Scene';
import { DebugManager } from '../src/Utils/Debug/DebugManager';
import { DebugConfigService } from '../src/Utils/Debug/DebugConfigService';
import { IECSDebugConfig } from '../src/Types';
import { createLogger } from '../src/Utils/Logger';
const logger = createLogger('DebugManagerTest');
class TestScene extends Scene {
public initializeCalled = false;
public override initialize(): void {
this.initializeCalled = true;
}
public override begin(): void {
}
public override end(): void {
}
}
describe('DebugManager DI Architecture Tests', () => {
let originalConsoleWarn: typeof console.warn;
let originalConsoleError: typeof console.error;
beforeEach(() => {
(Core as any)._instance = null;
originalConsoleWarn = console.warn;
originalConsoleError = console.error;
console.warn = jest.fn();
console.error = jest.fn();
});
afterEach(() => {
console.warn = originalConsoleWarn;
console.error = originalConsoleError;
if (Core.Instance) {
Core.destroy();
}
});
describe('DebugConfigService - Configuration Service', () => {
test('should create with default configuration', () => {
const configService = new DebugConfigService();
const config = configService.getConfig();
expect(config).toBeDefined();
expect(config.enabled).toBe(false);
expect(config.websocketUrl).toBe('');
expect(config.debugFrameRate).toBe(30);
expect(config.autoReconnect).toBe(true);
expect(config.channels).toBeDefined();
});
test('should set and get configuration', () => {
const configService = new DebugConfigService();
const newConfig: IECSDebugConfig = {
enabled: true,
websocketUrl: 'ws://localhost:9229',
debugFrameRate: 60,
autoReconnect: false,
channels: {
entities: true,
systems: true,
performance: true,
components: true,
scenes: true
}
};
configService.setConfig(newConfig);
const retrievedConfig = configService.getConfig();
expect(retrievedConfig).toEqual(newConfig);
expect(retrievedConfig.enabled).toBe(true);
expect(retrievedConfig.websocketUrl).toBe('ws://localhost:9229');
expect(retrievedConfig.debugFrameRate).toBe(60);
});
test('should return correct enabled status', () => {
const configService = new DebugConfigService();
expect(configService.isEnabled()).toBe(false);
configService.setConfig({
enabled: true,
websocketUrl: 'ws://localhost:9229',
debugFrameRate: 30,
autoReconnect: true,
channels: {
entities: true,
systems: true,
performance: true,
components: true,
scenes: true
}
});
expect(configService.isEnabled()).toBe(true);
});
test('should implement dispose method', () => {
const configService = new DebugConfigService();
expect(() => configService.dispose()).not.toThrow();
});
});
describe('DebugManager - DI Initialization', () => {
test('should initialize DebugManager through DI when debug config is enabled', () => {
const debugConfig: IECSDebugConfig = {
enabled: true,
websocketUrl: 'ws://localhost:9229',
debugFrameRate: 30,
autoReconnect: true,
channels: {
entities: true,
systems: true,
performance: true,
components: true,
scenes: true
}
};
const core = Core.create({
debug: true,
debugConfig: debugConfig
});
expect((core as any)._debugManager).toBeDefined();
expect((core as any)._debugManager).toBeInstanceOf(DebugManager);
});
test('should not create DebugManager when debug config is disabled', () => {
const core = Core.create({
debug: true,
debugConfig: {
enabled: false,
websocketUrl: '',
debugFrameRate: 30,
autoReconnect: true,
channels: {
entities: true,
systems: true,
performance: true,
components: true,
scenes: true
}
}
});
expect((core as any)._debugManager).toBeUndefined();
});
test('should not create DebugManager when no debug config provided', () => {
const core = Core.create({ debug: true });
expect((core as any)._debugManager).toBeUndefined();
});
test('should register DebugConfigService in ServiceContainer', () => {
const debugConfig: IECSDebugConfig = {
enabled: true,
websocketUrl: 'ws://localhost:9229',
debugFrameRate: 30,
autoReconnect: true,
channels: {
entities: true,
systems: true,
performance: true,
components: true,
scenes: true
}
};
Core.create({
debug: true,
debugConfig: debugConfig
});
const configService = Core.services.resolve(DebugConfigService);
expect(configService).toBeDefined();
expect(configService).toBeInstanceOf(DebugConfigService);
});
test('should inject all dependencies correctly', () => {
const debugConfig: IECSDebugConfig = {
enabled: true,
websocketUrl: 'ws://localhost:9229',
debugFrameRate: 30,
autoReconnect: true,
channels: {
entities: true,
systems: true,
performance: true,
components: true,
scenes: true
}
};
const core = Core.create({
debug: true,
debugConfig: debugConfig
});
const debugManager = (core as any)._debugManager as DebugManager;
expect(debugManager).toBeDefined();
const sceneManager = (debugManager as any).sceneManager;
const performanceMonitor = (debugManager as any).performanceMonitor;
const config = (debugManager as any).config;
expect(sceneManager).toBeDefined();
expect(performanceMonitor).toBeDefined();
expect(config).toBeDefined();
expect(config.enabled).toBe(true);
expect(config.websocketUrl).toBe('ws://localhost:9229');
});
});
describe('Core.enableDebug - Runtime Activation', () => {
test('should enable debug at runtime', () => {
const core = Core.create({ debug: true });
expect(Core.isDebugEnabled).toBe(false);
const debugConfig: IECSDebugConfig = {
enabled: true,
websocketUrl: 'ws://localhost:9229',
debugFrameRate: 30,
autoReconnect: true,
channels: {
entities: true,
systems: true,
performance: true,
components: true,
scenes: true
}
};
Core.enableDebug(debugConfig);
expect(Core.isDebugEnabled).toBe(true);
expect((core as any)._debugManager).toBeDefined();
});
test('should create DebugConfigService when enabling debug at runtime', () => {
Core.create({ debug: true });
const debugConfig: IECSDebugConfig = {
enabled: true,
websocketUrl: 'ws://localhost:9229',
debugFrameRate: 30,
autoReconnect: true,
channels: {
entities: true,
systems: true,
performance: true,
components: true,
scenes: true
}
};
Core.enableDebug(debugConfig);
const configService = Core.services.resolve(DebugConfigService);
expect(configService).toBeDefined();
expect(configService.getConfig()).toEqual(debugConfig);
});
test('should update existing DebugManager config when already enabled', () => {
const initialConfig: IECSDebugConfig = {
enabled: true,
websocketUrl: 'ws://localhost:9229',
debugFrameRate: 30,
autoReconnect: true,
channels: {
entities: true,
systems: true,
performance: true,
components: true,
scenes: true
}
};
Core.create({ debug: true, debugConfig: initialConfig });
const updatedConfig: IECSDebugConfig = {
enabled: true,
websocketUrl: 'ws://localhost:8080',
debugFrameRate: 60,
autoReconnect: false,
channels: {
entities: false,
systems: true,
performance: true,
components: false,
scenes: true
}
};
Core.enableDebug(updatedConfig);
expect(Core.isDebugEnabled).toBe(true);
const debugManager = (Core.Instance as any)._debugManager;
expect(debugManager).toBeDefined();
});
test('should show warning when enabling debug without Core instance', () => {
const debugConfig: IECSDebugConfig = {
enabled: true,
websocketUrl: 'ws://localhost:9229',
debugFrameRate: 30,
autoReconnect: true,
channels: {
entities: true,
systems: true,
performance: true,
components: true,
scenes: true
}
};
Core.enableDebug(debugConfig);
expect(console.warn).toHaveBeenCalledWith(
expect.stringContaining('Core实例未创建请先调用Core.create()')
);
});
});
describe('Core.disableDebug', () => {
test('should disable debug functionality', () => {
const debugConfig: IECSDebugConfig = {
enabled: true,
websocketUrl: 'ws://localhost:9229',
debugFrameRate: 30,
autoReconnect: true,
channels: {
entities: true,
systems: true,
performance: true,
components: true,
scenes: true
}
};
Core.create({ debug: true, debugConfig: debugConfig });
expect(Core.isDebugEnabled).toBe(true);
Core.disableDebug();
expect(Core.isDebugEnabled).toBe(false);
expect((Core.Instance as any)._debugManager).toBeUndefined();
});
test('should handle disabling when Core instance does not exist', () => {
expect(() => Core.disableDebug()).not.toThrow();
});
test('should handle disabling when debug was never enabled', () => {
Core.create({ debug: true });
expect(() => Core.disableDebug()).not.toThrow();
});
});
describe('DebugManager - Auto Update Integration', () => {
test('should be registered as updatable service', () => {
const debugConfig: IECSDebugConfig = {
enabled: true,
websocketUrl: 'ws://localhost:9229',
debugFrameRate: 30,
autoReconnect: true,
channels: {
entities: true,
systems: true,
performance: true,
components: true,
scenes: true
}
};
Core.create({ debug: true, debugConfig: debugConfig });
const serviceContainer = (Core.Instance as any)._serviceContainer;
const updatableCount = serviceContainer.getUpdatableCount();
expect(updatableCount).toBeGreaterThan(0);
});
test('should be updated during Core.update cycle', () => {
const debugConfig: IECSDebugConfig = {
enabled: true,
websocketUrl: 'ws://localhost:9229',
debugFrameRate: 30,
autoReconnect: true,
channels: {
entities: true,
systems: true,
performance: true,
components: true,
scenes: true
}
};
const core = Core.create({ debug: true, debugConfig: debugConfig });
const debugManager = (core as any)._debugManager as DebugManager;
const updateSpy = jest.spyOn(debugManager, 'update');
Core.update(0.016);
expect(updateSpy).toHaveBeenCalledWith(0.016);
});
});
describe('DebugManager - Scene Integration', () => {
test('should respond to scene changes', () => {
const debugConfig: IECSDebugConfig = {
enabled: true,
websocketUrl: 'ws://localhost:9229',
debugFrameRate: 30,
autoReconnect: true,
channels: {
entities: true,
systems: true,
performance: true,
components: true,
scenes: true
}
};
const core = Core.create({ debug: true, debugConfig: debugConfig });
const debugManager = (core as any)._debugManager as DebugManager;
const sceneChangeSpy = jest.spyOn(debugManager, 'onSceneChanged');
const testScene = new TestScene();
Core.setScene(testScene);
expect(sceneChangeSpy).toHaveBeenCalled();
});
test('should collect debug data from current scene', () => {
const debugConfig: IECSDebugConfig = {
enabled: true,
websocketUrl: 'ws://localhost:9229',
debugFrameRate: 30,
autoReconnect: true,
channels: {
entities: true,
systems: true,
performance: true,
components: true,
scenes: true
}
};
Core.create({ debug: true, debugConfig: debugConfig });
const testScene = new TestScene();
Core.setScene(testScene);
const debugData = Core.getDebugData();
expect(debugData).toBeDefined();
expect(debugData).toHaveProperty('timestamp');
expect(debugData).toHaveProperty('frameworkVersion');
expect(debugData).toHaveProperty('currentScene');
});
});
describe('DebugManager - Configuration Management', () => {
test('should use correct debug frame rate', () => {
const debugConfig: IECSDebugConfig = {
enabled: true,
websocketUrl: 'ws://localhost:9229',
debugFrameRate: 60,
autoReconnect: true,
channels: {
entities: true,
systems: true,
performance: true,
components: true,
scenes: true
}
};
const core = Core.create({ debug: true, debugConfig: debugConfig });
const debugManager = (core as any)._debugManager as DebugManager;
const sendInterval = (debugManager as any).sendInterval;
expect(sendInterval).toBe(1000 / 60);
});
test('should handle channel configuration correctly', () => {
const debugConfig: IECSDebugConfig = {
enabled: true,
websocketUrl: 'ws://localhost:9229',
debugFrameRate: 30,
autoReconnect: true,
channels: {
entities: true,
systems: false,
performance: false,
components: true,
scenes: false
}
};
Core.create({ debug: true, debugConfig: debugConfig });
const testScene = new TestScene();
Core.setScene(testScene);
const debugData = Core.getDebugData() as any;
expect(debugData.entities).toBeDefined();
expect(debugData.components).toBeDefined();
expect(debugData.systems).toBeUndefined();
expect(debugData.performance).toBeUndefined();
expect(debugData.scenes).toBeUndefined();
});
});
describe('DebugManager - Lifecycle', () => {
test('should start automatically on creation', () => {
const debugConfig: IECSDebugConfig = {
enabled: true,
websocketUrl: 'ws://localhost:9229',
debugFrameRate: 30,
autoReconnect: true,
channels: {
entities: true,
systems: true,
performance: true,
components: true,
scenes: true
}
};
const core = Core.create({ debug: true, debugConfig: debugConfig });
const debugManager = (core as any)._debugManager as DebugManager;
const isRunning = (debugManager as any).isRunning;
expect(isRunning).toBe(true);
});
test('should stop when disabling debug', () => {
const debugConfig: IECSDebugConfig = {
enabled: true,
websocketUrl: 'ws://localhost:9229',
debugFrameRate: 30,
autoReconnect: true,
channels: {
entities: true,
systems: true,
performance: true,
components: true,
scenes: true
}
};
Core.create({ debug: true, debugConfig: debugConfig });
const debugManager = (Core.Instance as any)._debugManager as DebugManager;
const stopSpy = jest.spyOn(debugManager, 'stop');
Core.disableDebug();
expect(stopSpy).toHaveBeenCalled();
});
test('should dispose properly on Core.destroy', () => {
const debugConfig: IECSDebugConfig = {
enabled: true,
websocketUrl: 'ws://localhost:9229',
debugFrameRate: 30,
autoReconnect: true,
channels: {
entities: true,
systems: true,
performance: true,
components: true,
scenes: true
}
};
Core.create({ debug: true, debugConfig: debugConfig });
const debugManager = (Core.Instance as any)._debugManager as DebugManager;
const stopSpy = jest.spyOn(debugManager, 'stop');
Core.destroy();
expect(stopSpy).toHaveBeenCalled();
});
});
describe('DebugManager - Pure DI Architecture Validation', () => {
test('should resolve all dependencies through ServiceContainer', () => {
const debugConfig: IECSDebugConfig = {
enabled: true,
websocketUrl: 'ws://localhost:9229',
debugFrameRate: 30,
autoReconnect: true,
channels: {
entities: true,
systems: true,
performance: true,
components: true,
scenes: true
}
};
Core.create({ debug: true, debugConfig: debugConfig });
const debugConfigService = Core.services.resolve(DebugConfigService);
expect(debugConfigService).toBeDefined();
const config = debugConfigService.getConfig();
expect(config).toEqual(debugConfig);
});
test('should not have mixed DI patterns', () => {
const debugConfig: IECSDebugConfig = {
enabled: true,
websocketUrl: 'ws://localhost:9229',
debugFrameRate: 30,
autoReconnect: true,
channels: {
entities: true,
systems: true,
performance: true,
components: true,
scenes: true
}
};
const core = Core.create({ debug: true, debugConfig: debugConfig });
const debugManager = (core as any)._debugManager as DebugManager;
const sceneManager = (debugManager as any).sceneManager;
const performanceMonitor = (debugManager as any).performanceMonitor;
const config = (debugManager as any).config;
expect(sceneManager).toBeDefined();
expect(performanceMonitor).toBeDefined();
expect(config).toBeDefined();
expect(config.enabled).toBe(true);
});
test('should use factory pattern for registration', () => {
const debugConfig: IECSDebugConfig = {
enabled: true,
websocketUrl: 'ws://localhost:9229',
debugFrameRate: 30,
autoReconnect: true,
channels: {
entities: true,
systems: true,
performance: true,
components: true,
scenes: true
}
};
Core.create({ debug: true, debugConfig: debugConfig });
const debugManager1 = (Core.Instance as any)._debugManager;
const debugManager2 = Core.services.resolve(DebugManager);
expect(debugManager1).toBe(debugManager2);
});
});
});

View File

@@ -0,0 +1,94 @@
import { Scene } from '../../../src/ECS/Scene';
import { Entity } from '../../../src/ECS/Entity';
import { Component } from '../../../src/ECS/Component';
import { EntitySystem } from '../../../src/ECS/Systems/EntitySystem';
import { Matcher } from '../../../src/ECS/Utils/Matcher';
import { ComponentRegistry } from '../../../src/ECS/Core/ComponentStorage';
// 简单的测试组件
class HealthComponent extends Component {
public health: number;
constructor(...args: unknown[]) {
super();
const [health = 100] = args as [number?];
this.health = health;
}
}
// 简单的测试系统
class HealthSystem extends EntitySystem {
public onAddedEntities: Entity[] = [];
constructor() {
super(Matcher.empty().all(HealthComponent));
}
protected override onAdded(entity: Entity): void {
console.log('[HealthSystem] onAdded called:', { id: entity.id, name: entity.name });
this.onAddedEntities.push(entity);
}
}
describe('MinimalSystemInit - 最小化系统初始化测试', () => {
let scene: Scene;
beforeEach(() => {
ComponentRegistry.reset();
scene = new Scene();
});
afterEach(() => {
if (scene) {
scene.end();
}
});
test('先创建实体和组件,再添加系统 - 应该触发onAdded', () => {
console.log('\\n=== Test 1: 先创建实体再添加系统 ===');
// 1. 创建实体并添加组件
const entity = scene.createEntity('TestEntity');
entity.addComponent(new HealthComponent(100));
console.log('[Test] Entity created with HealthComponent');
console.log('[Test] ComponentRegistry registered types:', ComponentRegistry.getRegisteredCount());
// 2. 验证QuerySystem能查询到实体
const queryResult = scene.querySystem.queryAll(HealthComponent);
console.log('[Test] QuerySystem result:', { count: queryResult.count });
// 3. 添加系统
const system = new HealthSystem();
console.log('[Test] Adding system to scene...');
scene.addEntityProcessor(system);
console.log('[Test] System added, onAddedEntities.length =', system.onAddedEntities.length);
// 4. 验证
expect(system.onAddedEntities).toHaveLength(1);
});
test('先添加系统,再创建实体和组件 - 应该在update时触发onAdded', () => {
console.log('\\n=== Test 2: 先添加系统再创建实体 ===');
// 1. 先添加系统
const system = new HealthSystem();
scene.addEntityProcessor(system);
console.log('[Test] System added, onAddedEntities.length =', system.onAddedEntities.length);
// 2. 创建实体并添加组件
const entity = scene.createEntity('TestEntity');
entity.addComponent(new HealthComponent(100));
console.log('[Test] Entity created with HealthComponent');
// 3. 调用update触发系统查询
console.log('[Test] Calling scene.update()...');
scene.update();
console.log('[Test] After update, onAddedEntities.length =', system.onAddedEntities.length);
// 4. 验证
expect(system.onAddedEntities).toHaveLength(1);
});
});

View File

@@ -0,0 +1,160 @@
import { Scene } from '../../../src/ECS/Scene';
import { Entity } from '../../../src/ECS/Entity';
import { Component } from '../../../src/ECS/Component';
import { EntitySystem } from '../../../src/ECS/Systems/EntitySystem';
import { Matcher } from '../../../src/ECS/Utils/Matcher';
import { ComponentRegistry } from '../../../src/ECS/Core/ComponentStorage';
// 测试组件
class PositionComponent extends Component {
public x: number;
public y: number;
constructor(...args: unknown[]) {
super();
const [x = 0, y = 0] = args as [number?, number?];
this.x = x;
this.y = y;
}
}
class VelocityComponent extends Component {
public vx: number;
public vy: number;
constructor(...args: unknown[]) {
super();
const [vx = 0, vy = 0] = args as [number?, number?];
this.vx = vx;
this.vy = vy;
}
}
class HealthComponent extends Component {
public health: number;
constructor(...args: unknown[]) {
super();
const [health = 100] = args as [number?];
this.health = health;
}
}
// 测试系统
class MovementSystem extends EntitySystem {
public onAddedEntities: Entity[] = [];
constructor() {
super(Matcher.empty().all(PositionComponent, VelocityComponent));
}
protected override onAdded(entity: Entity): void {
console.log('[MovementSystem] onAdded:', { id: entity.id, name: entity.name });
this.onAddedEntities.push(entity);
}
}
class HealthSystem extends EntitySystem {
public onAddedEntities: Entity[] = [];
constructor() {
super(Matcher.empty().all(HealthComponent));
}
protected override onAdded(entity: Entity): void {
console.log('[HealthSystem] onAdded:', { id: entity.id, name: entity.name });
this.onAddedEntities.push(entity);
}
}
describe('MultiSystemInit - 多系统初始化测试', () => {
let scene: Scene;
beforeEach(() => {
ComponentRegistry.reset();
scene = new Scene();
});
afterEach(() => {
if (scene) {
scene.end();
}
});
test('多个系统同时响应同一实体 - 复现失败场景', () => {
console.log('\\n=== Test: 多个系统同时响应同一实体 ===');
// 1. 创建实体并添加所有组件
const entity = scene.createEntity('Entity');
entity.addComponent(new PositionComponent(0, 0));
entity.addComponent(new VelocityComponent(1, 1));
entity.addComponent(new HealthComponent(100));
console.log('[Test] Entity created with Position, Velocity, Health');
console.log('[Test] ComponentRegistry registered types:', ComponentRegistry.getRegisteredCount());
// 2. 验证QuerySystem能查询到实体
const movementQuery = scene.querySystem.queryAll(PositionComponent, VelocityComponent);
const healthQuery = scene.querySystem.queryAll(HealthComponent);
console.log('[Test] MovementQuery result:', { count: movementQuery.count });
console.log('[Test] HealthQuery result:', { count: healthQuery.count });
// 3. 添加两个系统
console.log('[Test] Adding MovementSystem...');
const movementSystem = new MovementSystem();
scene.addEntityProcessor(movementSystem);
console.log('[Test] MovementSystem added, onAddedEntities.length =', movementSystem.onAddedEntities.length);
console.log('[Test] Adding HealthSystem...');
const healthSystem = new HealthSystem();
scene.addEntityProcessor(healthSystem);
console.log('[Test] HealthSystem added, onAddedEntities.length =', healthSystem.onAddedEntities.length);
// 4. 验证
console.log('[Test] Final check:');
console.log(' MovementSystem.onAddedEntities.length =', movementSystem.onAddedEntities.length);
console.log(' HealthSystem.onAddedEntities.length =', healthSystem.onAddedEntities.length);
expect(movementSystem.onAddedEntities).toHaveLength(1);
expect(healthSystem.onAddedEntities).toHaveLength(1);
});
test('不同系统匹配不同实体 - 复现失败场景', () => {
console.log('\\n=== Test: 不同系统匹配不同实体 ===');
// 1. 创建两个实体
const movingEntity = scene.createEntity('Moving');
movingEntity.addComponent(new PositionComponent(0, 0));
movingEntity.addComponent(new VelocityComponent(1, 1));
const healthEntity = scene.createEntity('Health');
healthEntity.addComponent(new HealthComponent(100));
console.log('[Test] Two entities created');
// 2. 验证QuerySystem
const movementQuery = scene.querySystem.queryAll(PositionComponent, VelocityComponent);
const healthQuery = scene.querySystem.queryAll(HealthComponent);
console.log('[Test] MovementQuery result:', { count: movementQuery.count });
console.log('[Test] HealthQuery result:', { count: healthQuery.count });
// 3. 添加系统
console.log('[Test] Adding systems...');
const movementSystem = new MovementSystem();
const healthSystem = new HealthSystem();
scene.addEntityProcessor(movementSystem);
scene.addEntityProcessor(healthSystem);
console.log('[Test] Systems added');
console.log(' MovementSystem.onAddedEntities.length =', movementSystem.onAddedEntities.length);
console.log(' HealthSystem.onAddedEntities.length =', healthSystem.onAddedEntities.length);
// 4. 验证
expect(movementSystem.onAddedEntities).toHaveLength(1);
expect(movementSystem.onAddedEntities[0]).toBe(movingEntity);
expect(healthSystem.onAddedEntities).toHaveLength(1);
expect(healthSystem.onAddedEntities[0]).toBe(healthEntity);
});
});

View File

@@ -449,22 +449,24 @@ describe('QuerySystem - 查询系统测试', () => {
expect(parseFloat(stats.cacheStats.hitRate)).toBeLessThanOrEqual(100);
});
test('缓存命中率应该在重复查询时提高', () => {
test('缓存命中率应该在重复查询时保持高命中率', () => {
entities[0].addComponent(new PositionComponent(10, 20));
entities[1].addComponent(new PositionComponent(30, 40));
// 第一次查询(缓存未命中)
// 第一次查询(创建响应式查询缓存)
querySystem.queryAll(PositionComponent);
let stats = querySystem.getStats();
const initialHitRate = parseFloat(stats.cacheStats.hitRate);
// 重复查询应该命中缓存
// 重复查询(应该持续命中响应式查询缓存)
for (let i = 0; i < 10; i++) {
querySystem.queryAll(PositionComponent);
}
stats = querySystem.getStats();
expect(parseFloat(stats.cacheStats.hitRate)).toBeGreaterThan(initialHitRate);
// 响应式查询永远100%命中,这是期望的优化效果
expect(parseFloat(stats.cacheStats.hitRate)).toBeGreaterThanOrEqual(initialHitRate);
expect(parseFloat(stats.cacheStats.hitRate)).toBe(100);
});
test('应该能够清理查询缓存', () => {

View File

@@ -3,7 +3,6 @@ import { Entity } from '../../../src/ECS/Entity';
import { Component } from '../../../src/ECS/Component';
import { EntitySystem } from '../../../src/ECS/Systems/EntitySystem';
import { Matcher } from '../../../src/ECS/Utils/Matcher';
import { ComponentTypeManager } from '../../../src/ECS/Utils/ComponentTypeManager';
/**
* System初始化测试套件
@@ -205,11 +204,16 @@ describe('SystemInitialization - 系统初始化测试', () => {
let scene: Scene;
beforeEach(() => {
ComponentTypeManager.instance.reset();
scene = new Scene();
scene.name = 'InitializationTestScene';
});
afterEach(() => {
if (scene) {
scene.end();
}
});
describe('初始化时序', () => {
test('先添加实体再添加系统 - 系统应该正确初始化', () => {
const player = scene.createEntity('Player');

View File

@@ -3,7 +3,7 @@ import { EntitySystem } from '../../src/ECS/Systems/EntitySystem';
import { Entity } from '../../src/ECS/Entity';
import { Component } from '../../src/ECS/Component';
import { Matcher } from '../../src/ECS/Utils/Matcher';
import { Injectable, Inject } from '../../src/Core/DI';
import { Injectable, Inject, InjectProperty } from '../../src/Core/DI';
import { Core } from '../../src/Core';
import type { IService } from '../../src/Core/ServiceContainer';
import { ECSSystem } from '../../src/ECS/Decorators';
@@ -423,4 +423,170 @@ describe('EntitySystem - 依赖注入测试', () => {
expect(transform.y).toBeCloseTo(0.8, 1);
});
});
describe('属性注入 @InjectProperty', () => {
test('应该支持单个属性注入', () => {
@Injectable()
@ECSSystem('Config')
class GameConfig extends EntitySystem implements IService {
public bulletDamage = 10;
constructor() {
super(Matcher.empty());
}
override dispose() {}
}
@Injectable()
@ECSSystem('Combat')
class CombatSystem extends EntitySystem implements IService {
@InjectProperty(GameConfig)
gameConfig!: GameConfig;
constructor() {
super(Matcher.empty().all(Health));
}
protected override onInitialize(): void {
expect(this.gameConfig).toBeInstanceOf(GameConfig);
expect(this.gameConfig.bulletDamage).toBe(10);
}
override dispose() {}
}
scene.addEntityProcessor(GameConfig);
scene.addEntityProcessor(CombatSystem);
});
test('应该支持多个属性注入', () => {
@Injectable()
@ECSSystem('Time')
class TimeService extends EntitySystem implements IService {
public deltaTime = 0.016;
constructor() {
super(Matcher.empty());
}
override dispose() {}
}
@Injectable()
@ECSSystem('Collision')
class CollisionSystem extends EntitySystem implements IService {
public checkCount = 0;
constructor() {
super(Matcher.empty());
}
override dispose() {}
}
@Injectable()
@ECSSystem('Physics')
class PhysicsSystem extends EntitySystem implements IService {
@InjectProperty(TimeService)
time!: TimeService;
@InjectProperty(CollisionSystem)
collision!: CollisionSystem;
constructor() {
super(Matcher.empty());
}
protected override onInitialize(): void {
expect(this.time).toBeInstanceOf(TimeService);
expect(this.collision).toBeInstanceOf(CollisionSystem);
expect(this.time.deltaTime).toBe(0.016);
}
override dispose() {}
}
scene.registerSystems([TimeService, CollisionSystem, PhysicsSystem]);
});
test('属性注入应该在onInitialize之前完成', () => {
@Injectable()
@ECSSystem('Service')
class TestService extends EntitySystem implements IService {
public value = 42;
constructor() {
super(Matcher.empty());
}
override dispose() {}
}
@Injectable()
@ECSSystem('Consumer')
class ConsumerSystem extends EntitySystem implements IService {
@InjectProperty(TestService)
service!: TestService;
private initializeValue = 0;
constructor() {
super(Matcher.empty());
}
protected override onInitialize(): void {
this.initializeValue = this.service.value;
}
public getInitializeValue(): number {
return this.initializeValue;
}
override dispose() {}
}
scene.addEntityProcessor(TestService);
const consumer = scene.addEntityProcessor(ConsumerSystem);
expect(consumer.getInitializeValue()).toBe(42);
});
test('属性注入可以与构造函数注入混合使用', () => {
@Injectable()
@ECSSystem('A')
class ServiceA extends EntitySystem implements IService {
public valueA = 'A';
constructor() {
super(Matcher.empty());
}
override dispose() {}
}
@Injectable()
@ECSSystem('B')
class ServiceB extends EntitySystem implements IService {
public valueB = 'B';
constructor() {
super(Matcher.empty());
}
override dispose() {}
}
@Injectable()
@ECSSystem('Mixed')
class MixedSystem extends EntitySystem implements IService {
@InjectProperty(ServiceB)
serviceB!: ServiceB;
constructor(@Inject(ServiceA) public serviceA: ServiceA) {
super(Matcher.empty());
}
protected override onInitialize(): void {
expect(this.serviceA).toBeInstanceOf(ServiceA);
expect(this.serviceB).toBeInstanceOf(ServiceB);
expect(this.serviceA.valueA).toBe('A');
expect(this.serviceB.valueB).toBe('B');
}
override dispose() {}
}
scene.registerSystems([ServiceA, ServiceB, MixedSystem]);
});
});
});

View File

@@ -589,25 +589,10 @@ describe('Scene - 场景管理系统测试', () => {
});
});
describe('依赖注入优化', () => {
test('应该支持注入自定义PerformanceMonitor', () => {
const mockPerfMonitor = {
startMeasure: jest.fn(),
endMeasure: jest.fn(),
recordSystemData: jest.fn(),
recordEntityCount: jest.fn(),
recordComponentCount: jest.fn(),
update: jest.fn(),
getSystemData: jest.fn(),
getSystemStats: jest.fn(),
resetSystem: jest.fn(),
reset: jest.fn(),
dispose: jest.fn()
};
describe('性能监控', () => {
test('Scene应该自动创建PerformanceMonitor', () => {
const customScene = new Scene({
name: 'CustomScene',
performanceMonitor: mockPerfMonitor as any
name: 'CustomScene'
});
class TestSystem extends EntitySystem {
@@ -619,13 +604,14 @@ describe('Scene - 场景管理系统测试', () => {
const system = new TestSystem();
customScene.addEntityProcessor(system);
expect(mockPerfMonitor).toBeDefined();
expect(customScene).toBeDefined();
customScene.end();
});
test('未提供PerformanceMonitor时应该从Core获取', () => {
const defaultScene = new Scene({ name: 'DefaultScene' });
test('每个Scene应该有独立的PerformanceMonitor', () => {
const scene1 = new Scene({ name: 'Scene1' });
const scene2 = new Scene({ name: 'Scene2' });
class TestSystem extends EntitySystem {
constructor() {
@@ -633,12 +619,14 @@ describe('Scene - 场景管理系统测试', () => {
}
}
const system = new TestSystem();
defaultScene.addEntityProcessor(system);
scene1.addEntityProcessor(new TestSystem());
scene2.addEntityProcessor(new TestSystem());
expect(defaultScene).toBeDefined();
expect(scene1).toBeDefined();
expect(scene2).toBeDefined();
defaultScene.end();
scene1.end();
scene2.end();
});
});
});

View File

@@ -0,0 +1,117 @@
/**
* 父子实体序列化和反序列化测试
*
* 测试场景序列化和反序列化时父子实体关系的正确性
*/
import { Scene } from '../../../src';
describe('父子实体序列化测试', () => {
let scene: Scene;
beforeEach(() => {
scene = new Scene({ name: 'TestScene' });
});
afterEach(() => {
scene.end();
});
test('应该正确反序列化父子实体层次结构', () => {
// 创建父实体
const parent = scene.createEntity('parent');
parent.tag = 100;
// 创建2个子实体
const child1 = scene.createEntity('child1');
child1.tag = 200;
parent.addChild(child1);
const child2 = scene.createEntity('child2');
child2.tag = 200;
parent.addChild(child2);
// 创建1个顶层实体对照组
const topLevel = scene.createEntity('topLevel');
topLevel.tag = 200;
// 验证序列化前的状态
expect(scene.querySystem.queryAll().entities.length).toBe(4);
expect(scene.findEntitiesByTag(100).length).toBe(1);
expect(scene.findEntitiesByTag(200).length).toBe(3);
// 序列化
const serialized = scene.serialize({ format: 'json' });
// 创建新场景并反序列化
const scene2 = new Scene({ name: 'LoadTestScene' });
scene2.deserialize(serialized as string, {
strategy: 'replace',
preserveIds: true,
});
// 验证所有实体都被正确恢复
const allEntities = scene2.querySystem.queryAll().entities;
expect(allEntities.length).toBe(4);
expect(scene2.findEntitiesByTag(100).length).toBe(1);
expect(scene2.findEntitiesByTag(200).length).toBe(3);
// 验证父子关系正确恢复
const restoredParent = scene2.findEntity('parent');
expect(restoredParent).not.toBeNull();
expect(restoredParent!.children.length).toBe(2);
const restoredChild1 = scene2.findEntity('child1');
const restoredChild2 = scene2.findEntity('child2');
expect(restoredChild1).not.toBeNull();
expect(restoredChild2).not.toBeNull();
expect(restoredChild1!.parent).toBe(restoredParent);
expect(restoredChild2!.parent).toBe(restoredParent);
scene2.end();
});
test('应该正确反序列化多层级实体层次结构', () => {
// 创建多层级实体结构grandparent -> parent -> child
const grandparent = scene.createEntity('grandparent');
grandparent.tag = 1;
const parent = scene.createEntity('parent');
parent.tag = 2;
grandparent.addChild(parent);
const child = scene.createEntity('child');
child.tag = 3;
parent.addChild(child);
expect(scene.querySystem.queryAll().entities.length).toBe(3);
// 序列化
const serialized = scene.serialize({ format: 'json' });
// 创建新场景并反序列化
const scene2 = new Scene({ name: 'LoadTestScene' });
scene2.deserialize(serialized as string, {
strategy: 'replace',
preserveIds: true,
});
// 验证多层级结构正确恢复
expect(scene2.querySystem.queryAll().entities.length).toBe(3);
const restoredGrandparent = scene2.findEntity('grandparent');
const restoredParent = scene2.findEntity('parent');
const restoredChild = scene2.findEntity('child');
expect(restoredGrandparent).not.toBeNull();
expect(restoredParent).not.toBeNull();
expect(restoredChild).not.toBeNull();
expect(restoredParent!.parent).toBe(restoredGrandparent);
expect(restoredChild!.parent).toBe(restoredParent);
expect(restoredGrandparent!.children.length).toBe(1);
expect(restoredParent!.children.length).toBe(1);
scene2.end();
});
});

View File

@@ -0,0 +1,352 @@
import { Core } from '../../src/Core';
import { World } from '../../src/ECS/World';
import { Scene } from '../../src/ECS/Scene';
import { Component } from '../../src/ECS/Component';
import { Matcher } from '../../src/ECS/Utils/Matcher';
import { DebugPlugin } from '../../src/Plugins/DebugPlugin';
import { Injectable } from '../../src/Core/DI';
import { ECSSystem } from '../../src/ECS/Decorators';
import { EntitySystem } from '../../src/ECS/Systems/EntitySystem';
class HealthComponent extends Component {
public health: number = 100;
public maxHealth: number = 100;
}
class PositionComponent extends Component {
public x: number = 0;
public y: number = 0;
}
@Injectable()
@ECSSystem('TestSystem', { updateOrder: 10 })
class TestSystem extends EntitySystem {
constructor() {
super(Matcher.empty().all(PositionComponent));
}
protected override process(entities: readonly import('../../src/ECS/Entity').Entity[]): void {
// 模拟处理逻辑
}
}
describe('DebugPlugin', () => {
let core: Core;
let world: World;
let scene: Scene;
let debugPlugin: DebugPlugin;
beforeEach(() => {
core = Core.create({ debug: false });
world = Core.worldManager.createWorld('test-world', { name: 'test-world' });
scene = world.createScene('test-scene');
world.setSceneActive('test-scene', true);
world.start();
debugPlugin = new DebugPlugin({ autoStart: false, updateInterval: 1000 });
});
afterEach(() => {
debugPlugin.stop();
Core.destroy();
});
describe('基本功能', () => {
it('应该能够安装插件', async () => {
await Core.installPlugin(debugPlugin);
expect(Core.isPluginInstalled('@esengine/debug-plugin')).toBe(true);
});
it('应该能够卸载插件', async () => {
await Core.installPlugin(debugPlugin);
await Core.uninstallPlugin('@esengine/debug-plugin');
expect(Core.isPluginInstalled('@esengine/debug-plugin')).toBe(false);
});
it('应该能够获取插件信息', async () => {
await Core.installPlugin(debugPlugin);
const plugin = Core.getPlugin('@esengine/debug-plugin');
expect(plugin).toBeDefined();
expect(plugin?.name).toBe('@esengine/debug-plugin');
expect(plugin?.version).toBe('1.0.0');
});
});
describe('统计信息', () => {
beforeEach(async () => {
await Core.installPlugin(debugPlugin);
});
it('应该能够获取 ECS 统计信息', () => {
const entity1 = scene.createEntity('Entity1');
entity1.addComponent(new PositionComponent());
const entity2 = scene.createEntity('Entity2');
entity2.addComponent(new HealthComponent());
const stats = debugPlugin.getStats();
expect(stats).toBeDefined();
expect(stats.totalEntities).toBe(2);
expect(stats.scenes.length).toBe(1);
expect(stats.scenes[0].name).toBe('test-scene');
expect(stats.scenes[0].entityCount).toBe(2);
});
it('应该能够获取场景信息', () => {
const entity = scene.createEntity('TestEntity');
entity.addComponent(new PositionComponent());
entity.addComponent(new HealthComponent());
scene.registerSystems([TestSystem]);
const sceneInfo = debugPlugin.getSceneInfo(scene);
expect(sceneInfo.name).toBe('test-scene');
expect(sceneInfo.entityCount).toBe(1);
expect(sceneInfo.systems.length).toBeGreaterThan(0);
expect(sceneInfo.entities.length).toBe(1);
});
it('应该能够获取实体详细信息', () => {
const entity = scene.createEntity('PlayerEntity');
entity.tag = 1;
entity.addComponent(new PositionComponent());
entity.addComponent(new HealthComponent());
const entityInfo = debugPlugin.getEntityInfo(entity);
expect(entityInfo.name).toBe('PlayerEntity');
expect(entityInfo.tag).toBe(1);
expect(entityInfo.enabled).toBe(true);
expect(entityInfo.componentCount).toBe(2);
expect(entityInfo.components.length).toBe(2);
const componentTypes = entityInfo.components.map(c => c.type);
expect(componentTypes).toContain('PositionComponent');
expect(componentTypes).toContain('HealthComponent');
});
it('应该能够获取组件数据', () => {
const entity = scene.createEntity('TestEntity');
const position = new PositionComponent();
position.x = 100;
position.y = 200;
entity.addComponent(position);
const entityInfo = debugPlugin.getEntityInfo(entity);
const positionInfo = entityInfo.components.find(c => c.type === 'PositionComponent');
expect(positionInfo).toBeDefined();
expect(positionInfo?.data.x).toBe(100);
expect(positionInfo?.data.y).toBe(200);
});
});
describe('实体查询', () => {
beforeEach(async () => {
await Core.installPlugin(debugPlugin);
const entity1 = scene.createEntity('Player');
entity1.tag = 1;
entity1.addComponent(new PositionComponent());
entity1.addComponent(new HealthComponent());
const entity2 = scene.createEntity('Enemy');
entity2.tag = 2;
entity2.addComponent(new PositionComponent());
const entity3 = scene.createEntity('Item');
entity3.tag = 3;
entity3.addComponent(new HealthComponent());
});
it('应该能够按 tag 查询实体', () => {
const results = debugPlugin.queryEntities({ tag: 1 });
expect(results.length).toBe(1);
expect(results[0].name).toBe('Player');
expect(results[0].tag).toBe(1);
});
it('应该能够按名称查询实体', () => {
const results = debugPlugin.queryEntities({ name: 'Player' });
expect(results.length).toBe(1);
expect(results[0].name).toBe('Player');
});
it('应该能够按组件查询实体', () => {
const results = debugPlugin.queryEntities({ hasComponent: 'PositionComponent' });
expect(results.length).toBe(2);
expect(results.map(r => r.name)).toContain('Player');
expect(results.map(r => r.name)).toContain('Enemy');
});
it('应该能够组合多个过滤条件', () => {
const results = debugPlugin.queryEntities({
tag: 1,
hasComponent: 'HealthComponent'
});
expect(results.length).toBe(1);
expect(results[0].name).toBe('Player');
});
it('应该在没有匹配时返回空数组', () => {
const results = debugPlugin.queryEntities({ tag: 999 });
expect(results.length).toBe(0);
});
});
describe('监控功能', () => {
beforeEach(async () => {
await Core.installPlugin(debugPlugin);
});
it('应该能够启动监控', () => {
debugPlugin.start();
expect(debugPlugin['updateTimer']).not.toBeNull();
});
it('应该能够停止监控', () => {
debugPlugin.start();
debugPlugin.stop();
expect(debugPlugin['updateTimer']).toBeNull();
});
it('应该防止重复启动', () => {
debugPlugin.start();
const timer1 = debugPlugin['updateTimer'];
debugPlugin.start();
const timer2 = debugPlugin['updateTimer'];
expect(timer1).toBe(timer2);
debugPlugin.stop();
});
it('应该支持自动启动', async () => {
await Core.uninstallPlugin('@esengine/debug-plugin');
const autoPlugin = new DebugPlugin({ autoStart: true, updateInterval: 100 });
await Core.installPlugin(autoPlugin);
expect(autoPlugin['updateTimer']).not.toBeNull();
autoPlugin.stop();
});
});
describe('数据导出', () => {
beforeEach(async () => {
await Core.installPlugin(debugPlugin);
});
it('应该能够导出 JSON 格式数据', () => {
const entity = scene.createEntity('TestEntity');
entity.addComponent(new PositionComponent());
const json = debugPlugin.exportJSON();
expect(json).toBeDefined();
expect(typeof json).toBe('string');
const data = JSON.parse(json);
expect(data.totalEntities).toBe(1);
expect(data.scenes).toBeDefined();
expect(data.timestamp).toBeDefined();
});
it('导出的 JSON 应该包含完整的实体信息', () => {
const entity = scene.createEntity('ComplexEntity');
const position = new PositionComponent();
position.x = 50;
position.y = 75;
entity.addComponent(position);
const json = debugPlugin.exportJSON();
const data = JSON.parse(json);
const entityData = data.scenes[0].entities[0];
expect(entityData.name).toBe('ComplexEntity');
expect(entityData.components[0].data.x).toBe(50);
expect(entityData.components[0].data.y).toBe(75);
});
});
describe('性能监控', () => {
beforeEach(async () => {
await Core.installPlugin(debugPlugin);
scene.registerSystems([TestSystem]);
});
it('应该能够获取 System 性能数据', () => {
scene.createEntity('E1').addComponent(new PositionComponent());
scene.createEntity('E2').addComponent(new PositionComponent());
scene.update();
scene.update();
scene.update();
const sceneInfo = debugPlugin.getSceneInfo(scene);
const systemInfo = sceneInfo.systems.find(s => s.name === 'TestSystem');
expect(systemInfo).toBeDefined();
if (systemInfo?.performance) {
expect(systemInfo.performance.totalCalls).toBeGreaterThan(0);
expect(systemInfo.performance.avgExecutionTime).toBeGreaterThanOrEqual(0);
}
});
it('应该记录 System 的实体数量', () => {
scene.createEntity('E1').addComponent(new PositionComponent());
scene.createEntity('E2').addComponent(new PositionComponent());
scene.createEntity('E3').addComponent(new HealthComponent());
const sceneInfo = debugPlugin.getSceneInfo(scene);
const systemInfo = sceneInfo.systems.find(s => s.name === 'TestSystem');
expect(systemInfo).toBeDefined();
expect(systemInfo?.entityCount).toBe(2);
});
});
describe('错误处理', () => {
it('应该在未安装时抛出错误', () => {
expect(() => {
debugPlugin.getStats();
}).toThrow('Plugin not installed');
});
it('应该在未安装时查询实体抛出错误', () => {
expect(() => {
debugPlugin.queryEntities({ tag: 1 });
}).toThrow('Plugin not installed');
});
it('应该处理空场景', async () => {
await Core.installPlugin(debugPlugin);
const stats = debugPlugin.getStats();
expect(stats.totalEntities).toBe(0);
expect(stats.totalSystems).toBe(0);
});
it('应该处理没有 World 的情况', async () => {
Core.destroy();
Core.create({ debug: false });
const tempPlugin = new DebugPlugin();
await Core.installPlugin(tempPlugin);
const stats = tempPlugin.getStats();
expect(stats.totalEntities).toBe(0);
expect(stats.scenes.length).toBe(0);
});
});
});

View File

@@ -0,0 +1,109 @@
import { describe, it, expect, beforeEach } from '@jest/globals';
import { Scene, Entity, Component, EntitySystem, Matcher, ECSComponent } from '../src';
@ECSComponent('TestTransform')
class TestTransform extends Component {
constructor(public x: number = 0, public y: number = 0) {
super();
}
}
@ECSComponent('TestRenderable')
class TestRenderable extends Component {
constructor(public sprite: string = 'default') {
super();
}
}
class TestRenderSystem extends EntitySystem {
public entitiesFound: Entity[] = [];
constructor() {
super(Matcher.all(TestTransform, TestRenderable));
}
protected override process(entities: readonly Entity[]): void {
this.entitiesFound = Array.from(entities);
console.log(`TestRenderSystem.process: 找到 ${entities.length} 个实体`);
}
protected override onAdded(entity: Entity): void {
console.log(`TestRenderSystem.onAdded: 实体 ${entity.name}(${entity.id}) 被添加`);
}
}
describe('响应式查询调试', () => {
let scene: Scene;
let system: TestRenderSystem;
beforeEach(() => {
scene = new Scene();
system = new TestRenderSystem();
scene.addEntityProcessor(system);
scene.begin();
});
it('应该在实体添加组件后能被System发现', () => {
console.log('\n=== 测试开始 ===');
// 1. 创建实体(此时没有组件)
console.log('\n步骤1: 创建实体');
const entity = scene.createEntity('TestEntity');
console.log(`实体已创建: ${entity.name}(${entity.id})`);
console.log(`QuerySystem中的实体数量: ${scene.querySystem.getAllEntities().length}`);
// 2. 添加组件
console.log('\n步骤2: 添加 TestTransform 组件');
entity.addComponent(new TestTransform(100, 200));
console.log(`实体组件数量: ${entity.components.length}`);
console.log('\n步骤3: 添加 TestRenderable 组件');
entity.addComponent(new TestRenderable('test-sprite'));
console.log(`实体组件数量: ${entity.components.length}`);
// 3. 触发系统更新
console.log('\n步骤4: 更新Scene');
scene.update();
// 4. 检查System是否找到了实体
console.log(`\nSystem找到的实体数量: ${system.entitiesFound.length}`);
if (system.entitiesFound.length > 0) {
console.log(`找到的实体: ${system.entitiesFound.map(e => `${e.name}(${e.id})`).join(', ')}`);
}
// 5. 直接查询QuerySystem
console.log('\n步骤5: 直接查询QuerySystem');
const queryResult = scene.querySystem.queryAll(TestTransform, TestRenderable);
console.log(`QuerySystem.queryAll 返回: ${queryResult.entities.length} 个实体`);
console.log('\n=== 测试结束 ===\n');
expect(system.entitiesFound.length).toBe(1);
expect(system.entitiesFound[0]).toBe(entity);
expect(queryResult.entities.length).toBe(1);
expect(queryResult.entities[0]).toBe(entity);
});
it('应该测试响应式查询的内部状态', () => {
console.log('\n=== 响应式查询内部状态测试 ===');
// 创建实体并添加组件
const entity = scene.createEntity('TestEntity');
entity.addComponent(new TestTransform(100, 200));
entity.addComponent(new TestRenderable('test-sprite'));
// 获取QuerySystem的内部状态
const querySystem = scene.querySystem as any;
console.log(`\n响应式查询数量: ${querySystem._reactiveQueries.size}`);
console.log(`组件索引数量: ${querySystem._reactiveQueriesByComponent.size}`);
// 检查响应式查询
for (const [key, query] of querySystem._reactiveQueries) {
console.log(`\n查询: ${key}`);
console.log(` 实体数量: ${(query as any)._entities.length}`);
console.log(` 实体ID集合: ${Array.from((query as any)._entityIdSet).join(', ')}`);
}
console.log('\n=== 测试结束 ===\n');
});
});

View File

@@ -0,0 +1,89 @@
import { describe, it, expect } from '@jest/globals';
import { Scene, Entity, Component, EntitySystem, Matcher, ECSComponent } from '../src';
@ECSComponent('TestTransform')
class TestTransform extends Component {
constructor(public x: number = 0, public y: number = 0) {
super();
}
}
@ECSComponent('TestRenderable')
class TestRenderable extends Component {
constructor(public sprite: string = 'default') {
super();
}
}
class TestRenderSystem extends EntitySystem {
public entitiesFound: Entity[] = [];
constructor() {
super(Matcher.all(TestTransform, TestRenderable));
}
protected override process(entities: readonly Entity[]): void {
this.entitiesFound = Array.from(entities);
console.log(`TestRenderSystem.process: 找到 ${entities.length} 个实体`);
}
protected override onAdded(entity: Entity): void {
console.log(`TestRenderSystem.onAdded: 实体 ${entity.name}(${entity.id}) 被添加`);
}
}
class TestGameScene extends Scene {
private renderSystem: TestRenderSystem | null = null;
public override initialize(): void {
super.initialize();
console.log('\n=== Scene.initialize() 开始 ===');
// 1. 先添加System这是GameScene的做法
console.log('步骤1: 添加RenderSystem');
this.renderSystem = new TestRenderSystem();
this.addEntityProcessor(this.renderSystem);
// 2. 然后创建实体这是GameScene的做法
console.log('\n步骤2: 创建实体');
const entity = this.createEntity('Player');
console.log(`实体已创建: ${entity.name}(${entity.id})`);
console.log('\n步骤3: 添加组件');
entity.addComponent(new TestTransform(100, 200));
entity.addComponent(new TestRenderable('player-sprite'));
console.log(`实体组件数量: ${entity.components.length}`);
console.log('=== Scene.initialize() 结束 ===\n');
}
public getRenderSystem(): TestRenderSystem | null {
return this.renderSystem;
}
}
describe('响应式查询时序测试模拟GameScene', () => {
it('应该在Scene.initialize()中先添加System再创建实体时正常工作', () => {
console.log('\n\n========== 测试开始 ==========');
const scene = new TestGameScene();
console.log('\n调用scene.initialize()');
scene.initialize();
console.log('\n调用Scene.begin()');
scene.begin();
console.log('\n第一次Scene.update()');
scene.update();
const renderSystem = scene.getRenderSystem();
expect(renderSystem).not.toBeNull();
console.log(`\nRenderSystem找到的实体数量: ${renderSystem!.entitiesFound.length}`);
expect(renderSystem!.entitiesFound.length).toBe(1);
console.log('========== 测试结束 ==========\n\n');
});
});

View File

@@ -60,15 +60,15 @@ afterEach(() => {
const { Core } = require('../src/Core');
const { WorldManager } = require('../src/ECS/WorldManager');
// 重置 Core 和 WorldManager 单例
// 销毁 Core 和 WorldManager 单例
if (Core._instance) {
Core.reset();
Core.destroy();
}
if (WorldManager._instance) {
if (WorldManager._instance.destroy) {
WorldManager._instance.destroy();
}
WorldManager.reset();
WorldManager._instance = null;
}
} catch (error) {
// 忽略清理错误

View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ECS Framework Editor</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,37 @@
{
"name": "@esengine/editor-app",
"version": "1.0.3",
"description": "ECS Framework Editor Application - Cross-platform desktop editor",
"type": "module",
"private": true,
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"tauri": "tauri",
"tauri:dev": "tauri dev",
"tauri:build": "tauri build",
"version": "node scripts/sync-version.js && git add src-tauri/tauri.conf.json"
},
"dependencies": {
"@esengine/ecs-framework": "file:../core",
"@esengine/editor-core": "file:../editor-core",
"@tauri-apps/api": "^2.2.0",
"@tauri-apps/plugin-dialog": "^2.4.0",
"@tauri-apps/plugin-shell": "^2.0.0",
"json5": "^2.2.3",
"lucide-react": "^0.545.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@tauri-apps/cli": "^2.2.0",
"@tauri-apps/plugin-updater": "^2.9.0",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.4",
"sharp": "^0.34.4",
"typescript": "^5.8.3",
"vite": "^6.0.7"
}
}

View File

@@ -0,0 +1,33 @@
#!/usr/bin/env node
/**
* 同步 package.json 和 tauri.conf.json 的版本号
* 在 npm version 命令执行后自动运行
*/
import { readFileSync, writeFileSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// 读取 package.json
const packageJsonPath = join(__dirname, '../package.json');
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
const newVersion = packageJson.version;
// 读取 tauri.conf.json
const tauriConfigPath = join(__dirname, '../src-tauri/tauri.conf.json');
const tauriConfig = JSON.parse(readFileSync(tauriConfigPath, 'utf8'));
// 更新 tauri.conf.json 的版本号
const oldVersion = tauriConfig.version;
tauriConfig.version = newVersion;
// 写回文件(保持格式)
writeFileSync(tauriConfigPath, JSON.stringify(tauriConfig, null, 2) + '\n', 'utf8');
console.log(`✓ Version synced: ${oldVersion}${newVersion}`);
console.log(` - package.json: ${newVersion}`);
console.log(` - tauri.conf.json: ${newVersion}`);

5787
packages/editor-app/src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,36 @@
[package]
name = "ecs-editor"
version = "1.0.0"
description = "ECS Framework Editor - Cross-platform desktop editor"
authors = ["yhh"]
edition = "2021"
[lib]
name = "ecs_editor_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2.0", features = [] }
[dependencies]
tauri = { version = "2.0", features = ["protocol-asset"] }
tauri-plugin-shell = "2.0"
tauri-plugin-dialog = "2.0"
tauri-plugin-updater = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
glob = "0.3"
tokio = { version = "1", features = ["full"] }
tokio-tungstenite = "0.21"
futures-util = "0.3"
chrono = "0.4"
[profile.dev]
incremental = true
[profile.release]
codegen-units = 1
lto = true
opt-level = "s"
panic = "abort"
strip = true

View File

@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 915 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 888 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

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