Compare commits
169 Commits
v2.1.52
...
issue-189-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ea55303dc | ||
|
|
c458a5e036 | ||
|
|
c511725d1f | ||
|
|
3876d9b92b | ||
|
|
f863c48ab0 | ||
|
|
10096795a1 | ||
|
|
8b146c8d5f | ||
|
|
1208c4ffeb | ||
|
|
ec5de97973 | ||
|
|
0daa92cfb7 | ||
|
|
130f466026 | ||
|
|
f93de87940 | ||
|
|
367d97e9bb | ||
|
|
77701f214c | ||
|
|
b5b64f8c41 | ||
|
|
ab04ad30f1 | ||
|
|
330d9a6fdb | ||
|
|
e762343142 | ||
|
|
fce9e3d4d6 | ||
|
|
0e5855ee4e | ||
|
|
ba61737bc7 | ||
|
|
bd7ea1f713 | ||
|
|
1abd20edf5 | ||
|
|
496513c641 | ||
|
|
848b637f45 | ||
|
|
39049601d4 | ||
|
|
e31cdd17d3 | ||
|
|
2a3f2d49b8 | ||
|
|
ca452889d7 | ||
|
|
2df501ec07 | ||
|
|
6b1e6c6fdc | ||
|
|
570e970e1c | ||
|
|
f4e3505d52 | ||
|
|
9af2b9859a | ||
|
|
d99d314621 | ||
|
|
c5b8b18e33 | ||
|
|
7280265a64 | ||
|
|
35fa0ef884 | ||
|
|
9c778cb71b | ||
|
|
4a401744c1 | ||
|
|
5f6b2d4d40 | ||
|
|
bf25218af2 | ||
|
|
1f7f9d9f84 | ||
|
|
2be53a04e4 | ||
|
|
7f56ebc786 | ||
|
|
a801e4f50e | ||
|
|
a9f9ad9b94 | ||
|
|
3cf1dab5b9 | ||
|
|
63165bbbfc | ||
|
|
61caad2bef | ||
|
|
b826bbc4c7 | ||
|
|
2ce7dad8d8 | ||
|
|
dff400bf22 | ||
|
|
27ce902344 | ||
|
|
33ee0a04c6 | ||
|
|
d68f6922f8 | ||
|
|
f8539d7958 | ||
|
|
14dc911e0a | ||
|
|
deccb6bf84 | ||
|
|
dacbfcae95 | ||
|
|
1b69ed17b7 | ||
|
|
241acc9050 | ||
|
|
8fa921930c | ||
|
|
011e43811a | ||
|
|
9f16debd75 | ||
|
|
92c56c439b | ||
|
|
7de6a5af0f | ||
|
|
173a063781 | ||
|
|
e04ac7c909 | ||
|
|
a6e49e1d47 | ||
|
|
f0046c7dc2 | ||
|
|
2a17c47c25 | ||
|
|
8d741bf1b9 | ||
|
|
c676006632 | ||
|
|
5bcfd597b9 | ||
|
|
3cda3c2238 | ||
|
|
43bdd7e43b | ||
|
|
1ec7892338 | ||
|
|
6bcfd48a2f | ||
|
|
345ef70972 | ||
|
|
c876edca0c | ||
|
|
fcf3def284 | ||
|
|
6f1a2896dd | ||
|
|
62381f4160 | ||
|
|
171805debf | ||
|
|
619abcbfbc | ||
|
|
03909924c2 | ||
|
|
f4ea077114 | ||
|
|
956ccf9195 | ||
|
|
e880925e3f | ||
|
|
0a860920ad | ||
|
|
fb7a1b1282 | ||
|
|
59970ef7c3 | ||
|
|
a7750c2894 | ||
|
|
b69b81f63a | ||
|
|
00fc6dfd67 | ||
|
|
82451e9fd3 | ||
|
|
d0fcc0e447 | ||
|
|
285279629e | ||
|
|
cbfe09b5e9 | ||
|
|
b757c1d06c | ||
|
|
4550a6146a | ||
|
|
3224bb9696 | ||
|
|
3a5e73266e | ||
|
|
1cf5641c4c | ||
|
|
85dad41e60 | ||
|
|
bd839cf431 | ||
|
|
b20b2ae4ce | ||
|
|
cac6aedf78 | ||
|
|
a572c80967 | ||
|
|
a09e8261db | ||
|
|
62e8ebe926 | ||
|
|
96e0a9126f | ||
|
|
4afb195814 | ||
|
|
7da5366bca | ||
|
|
d979c38615 | ||
|
|
7def06126b | ||
|
|
9f600f88b0 | ||
|
|
e3c4d5f0c0 | ||
|
|
b97f3a8431 | ||
|
|
3b917a06af | ||
|
|
360106fb92 | ||
|
|
507ed5005f | ||
|
|
8bea5d5e68 | ||
|
|
99076adb72 | ||
|
|
d1a6230b23 | ||
|
|
cfe3916934 | ||
|
|
d798995876 | ||
|
|
43e6b7bf88 | ||
|
|
9253686de1 | ||
|
|
7e7eae2d1a | ||
|
|
1924d979d6 | ||
|
|
ed84394301 | ||
|
|
bb99cf5389 | ||
|
|
2d0700f441 | ||
|
|
e3ead8a695 | ||
|
|
942043f0b0 | ||
|
|
23d81bca35 | ||
|
|
701f538e57 | ||
|
|
bb3017ffc2 | ||
|
|
f2b9c5cc5a | ||
|
|
532a52acfc | ||
|
|
c19b5ae9a7 | ||
|
|
5f507532ed | ||
|
|
6e48f22540 | ||
|
|
66aa9f4f20 | ||
|
|
62f895efe0 | ||
|
|
4a060e1ce3 | ||
|
|
a0177c9163 | ||
|
|
f45af34614 | ||
|
|
14a8d755f0 | ||
|
|
b67ab80c75 | ||
|
|
ae71af856b | ||
|
|
279c1d9bc9 | ||
|
|
9068a109b0 | ||
|
|
7850fc610c | ||
|
|
536871d09b | ||
|
|
1af2cf5f99 | ||
|
|
b13132b259 | ||
|
|
a1a6970ea4 | ||
|
|
41bbe23404 | ||
|
|
1d2a3e283e | ||
|
|
62d7521384 | ||
|
|
bf14b59a28 | ||
|
|
0a0f64510f | ||
|
|
9445c735c3 | ||
|
|
7339e7ecec | ||
|
|
79f7c89e23 | ||
|
|
e724e5a1ba |
62
.all-contributorsrc
Normal file
62
.all-contributorsrc
Normal 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": "[](#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
36
.coderabbit.yaml
Normal 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
29
.commitlintrc.json
Normal 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
35
.editorconfig
Normal 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
45
.eslintrc.json
Normal 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
44
.gitattributes
vendored
Normal 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
130
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal 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
17
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal 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
|
||||
90
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
90
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal 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
64
.github/ISSUE_TEMPLATE/question.yml
vendored
Normal 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
90
.github/dependabot.yml
vendored
Normal 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
32
.github/labeler.yml
vendored
Normal 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
95
.github/labels.yml
vendored
Normal 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: '依赖更新'
|
||||
73
.github/workflows/ai-batch-analyze-issues.yml
vendored
Normal file
73
.github/workflows/ai-batch-analyze-issues.yml
vendored
Normal 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
61
.github/workflows/ai-helper-tip.yml
vendored
Normal 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
85
.github/workflows/ai-issue-helper.yml
vendored
Normal 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
|
||||
});
|
||||
56
.github/workflows/ai-issue-moderator.yml
vendored
Normal file
56
.github/workflows/ai-issue-moderator.yml
vendored
Normal 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
160
.github/workflows/batch-label-issues.yml
vendored
Normal 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
146
.github/workflows/cleanup-dependabot.yml
vendored
Normal 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
45
.github/workflows/codecov.yml
vendored
Normal 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
41
.github/workflows/codeql.yml
vendored
Normal 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
31
.github/workflows/commitlint.yml
vendored
Normal 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
23
.github/workflows/issue-labeler.yml
vendored
Normal 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
28
.github/workflows/issue-translator.yml
vendored
Normal 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
151
.github/workflows/release-editor.yml
vendored
Normal 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
68
.github/workflows/release.yml
vendored
Normal 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
43
.github/workflows/size-limit.yml
vendored
Normal 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
60
.github/workflows/stale.yml
vendored
Normal 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
58
.github/workflows/welcome.yml
vendored
Normal 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
9
.gitignore
vendored
@@ -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
49
.prettierignore
Normal 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
14
.prettierrc
Normal 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
58
.releaserc.json
Normal 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
25
.size-limit.json
Normal 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
133
CONTRIBUTING.md
Normal 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
214
LICENSE
@@ -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
155
README.md
@@ -1,12 +1,53 @@
|
||||
# ECS Framework
|
||||
|
||||
[](https://github.com/esengine/ecs-framework/actions)
|
||||
[](https://codecov.io/gh/esengine/ecs-framework)
|
||||
[](https://badge.fury.io/js/%40esengine%2Fecs-framework)
|
||||
[](https://www.npmjs.com/package/@esengine/ecs-framework)
|
||||
[](https://bundlephobia.com/package/@esengine/ecs-framework)
|
||||
[](https://www.typescriptlang.org/)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](#contributors)
|
||||
[](https://github.com/esengine/ecs-framework/stargazers)
|
||||
[](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">
|
||||
|
||||
[](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">
|
||||
|
||||
[](https://www.npmjs.com/package/@esengine/ecs-framework)
|
||||
|
||||
[](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 监控系统性能
|
||||
- **插件系统** - 可扩展的插件架构
|
||||
- **远程调试** - 连接运行中的游戏进行实时调试
|
||||
- **自动更新** - 支持热更新,自动获取最新版本
|
||||
|
||||
### 下载
|
||||
|
||||
[](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">
|
||||
|
||||
[](https://github.com/sponsors/esengine)
|
||||
[](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
|
||||
|
||||
@@ -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',
|
||||
@@ -73,13 +74,28 @@ export default defineConfig({
|
||||
{ text: 'Worker系统 (多线程)', link: '/guide/worker-system' }
|
||||
]
|
||||
},
|
||||
{ text: '场景管理 (Scene)', link: '/guide/scene' },
|
||||
{
|
||||
text: '场景管理 (Scene)',
|
||||
link: '/guide/scene',
|
||||
items: [
|
||||
{ text: 'SceneManager', link: '/guide/scene-manager' },
|
||||
{ text: 'WorldManager', link: '/guide/world-manager' }
|
||||
]
|
||||
},
|
||||
{ text: '序列化系统 (Serialization)', link: '/guide/serialization' },
|
||||
{ text: '事件系统 (Event)', link: '/guide/event-system' },
|
||||
{ text: '时间和定时器 (Time)', link: '/guide/time-and-timers' },
|
||||
{ text: '日志系统 (Logger)', link: '/guide/logging' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '高级特性',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: '服务容器 (Service Container)', link: '/guide/service-container' },
|
||||
{ text: '插件系统 (Plugin System)', link: '/guide/plugin-system' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '平台适配器',
|
||||
link: '/guide/platform-adapter',
|
||||
|
||||
@@ -63,14 +63,14 @@ class Health extends Component {
|
||||
- 框架能正确管理组件注册
|
||||
|
||||
```typescript
|
||||
// ✅ 正确的用法
|
||||
// 正确的用法
|
||||
@ECSComponent('Velocity')
|
||||
class Velocity extends Component {
|
||||
dx: number = 0;
|
||||
dy: number = 0;
|
||||
}
|
||||
|
||||
// ❌ 错误的用法 - 没有装饰器
|
||||
// 错误的用法 - 没有装饰器
|
||||
class BadComponent extends Component {
|
||||
// 这样定义的组件可能在生产环境出现问题
|
||||
}
|
||||
@@ -90,7 +90,7 @@ class ExampleComponent extends Component {
|
||||
* 用于初始化资源、建立引用等
|
||||
*/
|
||||
onAddedToEntity(): void {
|
||||
console.log(`组件 ${this.constructor.name} 被添加到实体 ${this.entity.name}`);
|
||||
console.log(`组件 ${this.constructor.name} 已添加,实体ID: ${this.entityId}`);
|
||||
this.resource = new SomeResource();
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ class ExampleComponent extends Component {
|
||||
* 用于清理资源、断开引用等
|
||||
*/
|
||||
onRemovedFromEntity(): void {
|
||||
console.log(`组件 ${this.constructor.name} 从实体 ${this.entity.name} 移除`);
|
||||
console.log(`组件 ${this.constructor.name} 已移除`);
|
||||
if (this.resource) {
|
||||
this.resource.cleanup();
|
||||
this.resource = null;
|
||||
@@ -108,30 +108,58 @@ class ExampleComponent extends Component {
|
||||
}
|
||||
```
|
||||
|
||||
## 访问实体
|
||||
## 组件与实体的关系
|
||||
|
||||
组件可以通过 `this.entity` 访问其所属的实体:
|
||||
组件存储了所属实体的ID (`entityId`),而不是直接引用实体对象。这是ECS数据导向设计的体现,避免了循环引用。
|
||||
|
||||
在实际使用中,**应该在 System 中处理实体和组件的交互**,而不是在组件内部:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Damage')
|
||||
class Damage extends Component {
|
||||
damage: number;
|
||||
@ECSComponent('Health')
|
||||
class Health extends Component {
|
||||
current: number;
|
||||
max: number;
|
||||
|
||||
constructor(damage: number) {
|
||||
constructor(max: number = 100) {
|
||||
super();
|
||||
this.damage = damage;
|
||||
this.max = max;
|
||||
this.current = max;
|
||||
}
|
||||
|
||||
// 在组件方法中访问实体和其他组件
|
||||
applyDamage(): void {
|
||||
const health = this.entity.getComponent(Health);
|
||||
if (health) {
|
||||
health.takeDamage(this.damage);
|
||||
isDead(): boolean {
|
||||
return this.current <= 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('Damage')
|
||||
class Damage extends Component {
|
||||
value: number;
|
||||
|
||||
constructor(value: number) {
|
||||
super();
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
// 推荐:在 System 中处理逻辑
|
||||
class DamageSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(new Matcher().all(Health, Damage));
|
||||
}
|
||||
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health)!;
|
||||
const damage = entity.getComponent(Damage)!;
|
||||
|
||||
health.current -= damage.value;
|
||||
|
||||
// 如果生命值为0,销毁实体
|
||||
if (health.isDead()) {
|
||||
this.entity.destroy();
|
||||
entity.destroy();
|
||||
}
|
||||
|
||||
// 应用伤害后移除 Damage 组件
|
||||
entity.removeComponent(damage);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,9 +174,27 @@ class Damage extends Component {
|
||||
class ExampleComponent extends Component {
|
||||
someData: string = "example";
|
||||
|
||||
showComponentInfo(): void {
|
||||
console.log(`组件ID: ${this.id}`); // 唯一的组件ID
|
||||
console.log(`所属实体: ${this.entity.name}`); // 所属实体引用
|
||||
onAddedToEntity(): void {
|
||||
console.log(`组件ID: ${this.id}`); // 唯一的组件ID
|
||||
console.log(`所属实体ID: ${this.entityId}`); // 所属实体的ID
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
如果需要访问实体对象,应该在 System 中进行:
|
||||
|
||||
```typescript
|
||||
class ExampleSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(new Matcher().all(ExampleComponent));
|
||||
}
|
||||
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const comp = entity.getComponent(ExampleComponent)!;
|
||||
console.log(`实体名称: ${entity.name}`);
|
||||
console.log(`组件数据: ${comp.someData}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -245,7 +291,7 @@ class WeaponConfig extends Component {
|
||||
### 1. 保持组件简单
|
||||
|
||||
```typescript
|
||||
// ✅ 好的组件设计 - 单一职责
|
||||
// 好的组件设计 - 单一职责
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
x: number = 0;
|
||||
@@ -258,7 +304,7 @@ class Velocity extends Component {
|
||||
dy: number = 0;
|
||||
}
|
||||
|
||||
// ❌ 避免的组件设计 - 职责过多
|
||||
// 避免的组件设计 - 职责过多
|
||||
@ECSComponent('GameObject')
|
||||
class GameObject extends Component {
|
||||
x: number;
|
||||
@@ -330,16 +376,11 @@ class Inventory extends Component {
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 避免在组件中存储实体引用
|
||||
### 4. 引用其他实体
|
||||
|
||||
当组件需要关联其他实体时(如父子关系、跟随目标等),**推荐方式是存储实体ID**,然后在 System 中查找:
|
||||
|
||||
```typescript
|
||||
// ❌ 避免:在组件中存储其他实体的引用
|
||||
@ECSComponent('BadFollower')
|
||||
class BadFollower extends Component {
|
||||
target: Entity; // 直接引用可能导致内存泄漏
|
||||
}
|
||||
|
||||
// ✅ 推荐:存储实体ID,通过场景查找
|
||||
@ECSComponent('Follower')
|
||||
class Follower extends Component {
|
||||
targetId: number;
|
||||
@@ -349,11 +390,269 @@ class Follower extends Component {
|
||||
super();
|
||||
this.targetId = targetId;
|
||||
}
|
||||
}
|
||||
|
||||
getTarget(): Entity | null {
|
||||
return this.entity.scene?.findEntityById(this.targetId) || null;
|
||||
// 在 System 中查找目标实体并处理逻辑
|
||||
class FollowerSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(new Matcher().all(Follower, Position));
|
||||
}
|
||||
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const follower = entity.getComponent(Follower)!;
|
||||
const position = entity.getComponent(Position)!;
|
||||
|
||||
// 通过场景查找目标实体
|
||||
const target = entity.scene?.findEntityById(follower.targetId);
|
||||
if (target) {
|
||||
const targetPos = target.getComponent(Position);
|
||||
if (targetPos) {
|
||||
// 跟随逻辑
|
||||
const dx = targetPos.x - position.x;
|
||||
const dy = targetPos.y - position.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance > follower.followDistance) {
|
||||
// 移动靠近目标
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这种方式的优势:
|
||||
- 组件保持简单,只存储基本数据类型
|
||||
- 符合数据导向设计
|
||||
- 在 System 中统一处理查找和逻辑
|
||||
- 易于理解和维护
|
||||
|
||||
**避免在组件中直接存储实体引用**:
|
||||
|
||||
```typescript
|
||||
// 错误示范:直接存储实体引用
|
||||
@ECSComponent('BadFollower')
|
||||
class BadFollower extends Component {
|
||||
target: Entity; // 实体销毁后仍持有引用,可能导致内存泄漏
|
||||
}
|
||||
```
|
||||
|
||||
## 高级特性
|
||||
|
||||
### EntityRef 装饰器 - 自动引用追踪
|
||||
|
||||
框架提供了 `@EntityRef` 装饰器用于**特殊场景**下安全地存储实体引用。这是一个高级特性,一般情况下推荐使用存储ID的方式。
|
||||
|
||||
#### 什么时候需要 EntityRef?
|
||||
|
||||
在以下场景中,`@EntityRef` 可以简化代码:
|
||||
|
||||
1. **父子关系**: 需要在组件中直接访问父实体或子实体
|
||||
2. **复杂关联**: 实体之间有多个引用关系
|
||||
3. **频繁访问**: 需要在多处访问引用的实体,使用ID查找会有性能开销
|
||||
|
||||
#### 核心特性
|
||||
|
||||
`@EntityRef` 装饰器通过 **ReferenceTracker** 自动追踪引用关系:
|
||||
|
||||
- 当被引用的实体销毁时,所有指向它的 `@EntityRef` 属性自动设为 `null`
|
||||
- 防止跨场景引用(会输出警告并拒绝设置)
|
||||
- 防止引用已销毁的实体(会输出警告并设为 `null`)
|
||||
- 使用 WeakRef 避免内存泄漏(自动GC支持)
|
||||
- 组件移除时自动清理引用注册
|
||||
|
||||
#### 基本用法
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent, EntityRef, Entity } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSComponent('Parent')
|
||||
class ParentComponent extends Component {
|
||||
@EntityRef()
|
||||
parent: Entity | null = null;
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
const scene = new Scene();
|
||||
const parent = scene.createEntity('Parent');
|
||||
const child = scene.createEntity('Child');
|
||||
|
||||
const comp = child.addComponent(new ParentComponent());
|
||||
comp.parent = parent;
|
||||
|
||||
console.log(comp.parent); // Entity { name: 'Parent' }
|
||||
|
||||
// 当 parent 被销毁时,comp.parent 自动变为 null
|
||||
parent.destroy();
|
||||
console.log(comp.parent); // null
|
||||
```
|
||||
|
||||
#### 多个引用属性
|
||||
|
||||
一个组件可以有多个 `@EntityRef` 属性:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Combat')
|
||||
class CombatComponent extends Component {
|
||||
@EntityRef()
|
||||
target: Entity | null = null;
|
||||
|
||||
@EntityRef()
|
||||
ally: Entity | null = null;
|
||||
|
||||
@EntityRef()
|
||||
lastAttacker: Entity | null = null;
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
const player = scene.createEntity('Player');
|
||||
const enemy = scene.createEntity('Enemy');
|
||||
const npc = scene.createEntity('NPC');
|
||||
|
||||
const combat = player.addComponent(new CombatComponent());
|
||||
combat.target = enemy;
|
||||
combat.ally = npc;
|
||||
|
||||
// enemy 销毁后,只有 target 变为 null,ally 仍然有效
|
||||
enemy.destroy();
|
||||
console.log(combat.target); // null
|
||||
console.log(combat.ally); // Entity { name: 'NPC' }
|
||||
```
|
||||
|
||||
#### 安全检查
|
||||
|
||||
`@EntityRef` 提供了多重安全检查:
|
||||
|
||||
```typescript
|
||||
const scene1 = new Scene();
|
||||
const scene2 = new Scene();
|
||||
|
||||
const entity1 = scene1.createEntity('Entity1');
|
||||
const entity2 = scene2.createEntity('Entity2');
|
||||
|
||||
const comp = entity1.addComponent(new ParentComponent());
|
||||
|
||||
// 跨场景引用会失败
|
||||
comp.parent = entity2; // 输出错误日志,comp.parent 为 null
|
||||
console.log(comp.parent); // null
|
||||
|
||||
// 引用已销毁的实体会失败
|
||||
const entity3 = scene1.createEntity('Entity3');
|
||||
entity3.destroy();
|
||||
comp.parent = entity3; // 输出警告日志,comp.parent 为 null
|
||||
console.log(comp.parent); // null
|
||||
```
|
||||
|
||||
#### 实现原理
|
||||
|
||||
`@EntityRef` 使用以下机制实现自动引用追踪:
|
||||
|
||||
1. **ReferenceTracker**: Scene 持有一个引用追踪器,记录所有实体引用关系
|
||||
2. **WeakRef**: 使用弱引用存储组件,避免循环引用导致内存泄漏
|
||||
3. **属性拦截**: 通过 `Object.defineProperty` 拦截 getter/setter
|
||||
4. **自动清理**: 实体销毁时,ReferenceTracker 遍历所有引用并设为 null
|
||||
|
||||
```typescript
|
||||
// 简化的实现原理
|
||||
class ReferenceTracker {
|
||||
// entityId -> 引用该实体的所有组件记录
|
||||
private _references: Map<number, Set<{ component: WeakRef<Component>, propertyKey: string }>>;
|
||||
|
||||
// 实体销毁时调用
|
||||
clearReferencesTo(entityId: number): void {
|
||||
const records = this._references.get(entityId);
|
||||
if (records) {
|
||||
for (const record of records) {
|
||||
const component = record.component.deref();
|
||||
if (component) {
|
||||
// 将组件的引用属性设为 null
|
||||
(component as any)[record.propertyKey] = null;
|
||||
}
|
||||
}
|
||||
this._references.delete(entityId);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 性能考虑
|
||||
|
||||
`@EntityRef` 会带来一些性能开销:
|
||||
|
||||
- **写入开销**: 每次设置引用时需要更新 ReferenceTracker
|
||||
- **内存开销**: ReferenceTracker 需要维护引用映射表
|
||||
- **销毁开销**: 实体销毁时需要遍历所有引用并清理
|
||||
|
||||
对于大多数场景,这些开销是可以接受的。但如果有**大量实体和频繁的引用变更**,存储ID可能更高效。
|
||||
|
||||
#### 最佳实践
|
||||
|
||||
```typescript
|
||||
// 推荐:适合使用 @EntityRef 的场景 - 父子关系
|
||||
@ECSComponent('Transform')
|
||||
class Transform extends Component {
|
||||
@EntityRef()
|
||||
parent: Entity | null = null;
|
||||
|
||||
position: { x: number, y: number } = { x: 0, y: 0 };
|
||||
|
||||
// 可以直接访问父实体的组件
|
||||
getWorldPosition(): { x: number, y: number } {
|
||||
if (!this.parent) {
|
||||
return { ...this.position };
|
||||
}
|
||||
|
||||
const parentTransform = this.parent.getComponent(Transform);
|
||||
if (parentTransform) {
|
||||
const parentPos = parentTransform.getWorldPosition();
|
||||
return {
|
||||
x: parentPos.x + this.position.x,
|
||||
y: parentPos.y + this.position.y
|
||||
};
|
||||
}
|
||||
|
||||
return { ...this.position };
|
||||
}
|
||||
}
|
||||
|
||||
// 不推荐:不适合使用 @EntityRef 的场景 - 大量动态目标
|
||||
@ECSComponent('AITarget')
|
||||
class AITarget extends Component {
|
||||
@EntityRef()
|
||||
target: Entity | null = null; // 如果目标频繁变化,用ID更好
|
||||
|
||||
updateCooldown: number = 0;
|
||||
}
|
||||
|
||||
// 推荐:这种场景用ID更好
|
||||
@ECSComponent('AITarget')
|
||||
class AITargetBetter extends Component {
|
||||
targetId: number | null = null; // 存储ID
|
||||
updateCooldown: number = 0;
|
||||
}
|
||||
```
|
||||
|
||||
#### 调试支持
|
||||
|
||||
ReferenceTracker 提供了调试接口:
|
||||
|
||||
```typescript
|
||||
// 查看某个实体被哪些组件引用
|
||||
const references = scene.referenceTracker.getReferencesTo(entity.id);
|
||||
console.log(`实体 ${entity.name} 被 ${references.length} 个组件引用`);
|
||||
|
||||
// 获取完整的调试信息
|
||||
const debugInfo = scene.referenceTracker.getDebugInfo();
|
||||
console.log(debugInfo);
|
||||
```
|
||||
|
||||
#### 总结
|
||||
|
||||
- **推荐做法**: 大部分情况使用存储ID + System查找的方式
|
||||
- **EntityRef 适用场景**: 父子关系、复杂关联、组件内需要直接访问引用实体的场景
|
||||
- **核心优势**: 自动清理、防止悬空引用、代码更简洁
|
||||
- **注意事项**: 有性能开销,不适合大量动态引用的场景
|
||||
|
||||
组件是 ECS 架构的数据载体,正确设计组件能让你的游戏代码更模块化、可维护和高性能。
|
||||
501
docs/guide/entity-query.md
Normal file
501
docs/guide/entity-query.md
Normal 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 参考
|
||||
@@ -264,22 +264,17 @@ player.addComponent(new Velocity(50, 30)); // 每秒移动 50 像素(x方向
|
||||
player.addComponent(new Sprite("player.png", 64, 64));
|
||||
```
|
||||
|
||||
## World 概念
|
||||
## 场景管理
|
||||
|
||||
World 是 Scene 的容器,用于管理多个独立的游戏世界。这种设计特别适用于:
|
||||
- 多人游戏房间(每个房间一个 World)
|
||||
- 不同的游戏模式
|
||||
- 独立的模拟环境
|
||||
|
||||
### 基本用法
|
||||
Core 内置了场景管理功能,使用非常简单:
|
||||
|
||||
```typescript
|
||||
import { World, Scene } from '@esengine/ecs-framework'
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// 创建游戏房间的World
|
||||
const roomWorld = new World({ name: 'Room_001' });
|
||||
// 初始化Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 在World中创建多个Scene
|
||||
// 创建并设置场景
|
||||
class GameScene extends Scene {
|
||||
initialize(): void {
|
||||
this.name = "GamePlay";
|
||||
@@ -288,78 +283,106 @@ class GameScene extends Scene {
|
||||
}
|
||||
}
|
||||
|
||||
class UIScene extends Scene {
|
||||
initialize(): void {
|
||||
this.name = "UI";
|
||||
// UI相关系统
|
||||
}
|
||||
const gameScene = new GameScene();
|
||||
Core.setScene(gameScene);
|
||||
|
||||
// 游戏循环(自动更新场景)
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // 自动更新全局服务和场景
|
||||
}
|
||||
|
||||
// 添加Scene到World
|
||||
const gameScene = roomWorld.createScene('game', new GameScene());
|
||||
const uiScene = roomWorld.createScene('ui', new UIScene());
|
||||
// 切换场景
|
||||
Core.loadScene(new MenuScene()); // 延迟切换(下一帧)
|
||||
Core.setScene(new GameScene()); // 立即切换
|
||||
|
||||
// 激活Scene
|
||||
roomWorld.setSceneActive('game', true);
|
||||
roomWorld.setSceneActive('ui', true);
|
||||
// 访问当前场景
|
||||
const currentScene = Core.scene;
|
||||
|
||||
// 启动World
|
||||
roomWorld.start();
|
||||
// 使用流式API
|
||||
const player = Core.ecsAPI?.createEntity('Player')
|
||||
.addComponent(Position, 100, 100)
|
||||
.addComponent(Velocity, 50, 0);
|
||||
```
|
||||
|
||||
### World 生命周期
|
||||
### 高级:使用 WorldManager 管理多世界
|
||||
|
||||
World 提供了完整的生命周期管理:
|
||||
- `start()`: 启动 World 和所有全局系统
|
||||
- `updateGlobalSystems()`: 更新全局系统(由 Core.update() 调用)
|
||||
- `updateScenes()`: 更新所有激活的 Scene(由 Core.update() 调用)
|
||||
- `stop()`: 停止 World
|
||||
- `destroy()`: 销毁 World 和所有资源
|
||||
仅适用于复杂的服务器端应用(MMO游戏服务器、游戏房间系统等):
|
||||
|
||||
```typescript
|
||||
import { Core, WorldManager } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始化Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 从服务容器获取 WorldManager(Core 已自动创建并注册)
|
||||
const worldManager = Core.services.resolve(WorldManager);
|
||||
|
||||
// 创建多个独立的游戏世界
|
||||
const room1 = worldManager.createWorld('room_001');
|
||||
const room2 = worldManager.createWorld('room_002');
|
||||
|
||||
// 在每个世界中创建场景
|
||||
const gameScene1 = room1.createScene('game', new GameScene());
|
||||
const gameScene2 = room2.createScene('game', new GameScene());
|
||||
|
||||
// 激活场景
|
||||
room1.setSceneActive('game', true);
|
||||
room2.setSceneActive('game', true);
|
||||
|
||||
// 游戏循环(需要手动更新世界)
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // 更新全局服务
|
||||
worldManager.updateAll(); // 手动更新所有世界
|
||||
}
|
||||
```
|
||||
|
||||
## 与游戏引擎集成
|
||||
|
||||
### Laya 引擎集成
|
||||
|
||||
```typescript
|
||||
import { Stage } from "laya/display/Stage"
|
||||
import { Stat } from "laya/utils/Stat"
|
||||
import { Laya } from "Laya"
|
||||
import { Stage } from "laya/display/Stage";
|
||||
import { Laya } from "Laya";
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始化 Laya
|
||||
Laya.init(800, 600).then(() => {
|
||||
// 初始化 ECS
|
||||
const core = Core.create(true)
|
||||
|
||||
// 设置场景...
|
||||
Core.create(true);
|
||||
Core.setScene(new GameScene());
|
||||
|
||||
// 启动游戏循环
|
||||
Laya.timer.frameLoop(1, this, () => {
|
||||
const deltaTime = Laya.timer.delta / 1000 // 转换为秒
|
||||
Core.update(deltaTime)
|
||||
})
|
||||
})
|
||||
const deltaTime = Laya.timer.delta / 1000;
|
||||
Core.update(deltaTime); // 自动更新全局服务和场景
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Cocos Creator 集成
|
||||
|
||||
```typescript
|
||||
import { Component, _decorator } from 'cc'
|
||||
import { Component, _decorator } from 'cc';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
const { ccclass } = _decorator
|
||||
const { ccclass } = _decorator;
|
||||
|
||||
@ccclass('ECSGameManager')
|
||||
export class ECSGameManager extends Component {
|
||||
|
||||
onLoad() {
|
||||
// 初始化 ECS
|
||||
const core = Core.create(true)
|
||||
|
||||
// 设置场景...
|
||||
Core.create(true);
|
||||
Core.setScene(new GameScene());
|
||||
}
|
||||
|
||||
update(deltaTime: number) {
|
||||
// 更新 ECS
|
||||
Core.update(deltaTime)
|
||||
// 自动更新全局服务和场景
|
||||
Core.update(deltaTime);
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
// 清理资源
|
||||
Core.destroy();
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -378,7 +401,7 @@ export class ECSGameManager extends Component {
|
||||
|
||||
确保:
|
||||
1. 系统已添加到场景:`this.addSystem(system)` (在 Scene 的 initialize 方法中)
|
||||
2. 场景已设置为当前场景:`Core.setScene(scene)`
|
||||
2. 场景已设置:`Core.setScene(scene)`
|
||||
3. 游戏循环在调用:`Core.update(deltaTime)`
|
||||
|
||||
### 如何调试 ECS 应用?
|
||||
|
||||
@@ -29,4 +29,12 @@
|
||||
掌握分级日志系统,用于调试、监控和错误追踪。
|
||||
|
||||
### [平台适配器 (Platform Adapter)](./platform-adapter.md)
|
||||
了解如何为不同平台实现和注册平台适配器,支持浏览器、小游戏、Node.js等环境。
|
||||
了解如何为不同平台实现和注册平台适配器,支持浏览器、小游戏、Node.js等环境。
|
||||
|
||||
## 高级特性
|
||||
|
||||
### [服务容器 (Service Container)](./service-container.md)
|
||||
掌握依赖注入和服务管理,实现松耦合的架构设计。
|
||||
|
||||
### [插件系统 (Plugin System)](./plugin-system.md)
|
||||
学习如何开发和使用插件,扩展框架功能,实现功能模块化。
|
||||
643
docs/guide/plugin-system.md
Normal file
643
docs/guide/plugin-system.md
Normal file
@@ -0,0 +1,643 @@
|
||||
# 插件系统
|
||||
|
||||
插件系统允许你以模块化的方式扩展 ECS Framework 的功能。通过插件,你可以封装特定功能(如网络同步、物理引擎、调试工具等),并在多个项目中复用。
|
||||
|
||||
## 概述
|
||||
|
||||
### 什么是插件
|
||||
|
||||
插件是实现了 `IPlugin` 接口的类,可以在运行时动态安装到框架中。插件可以:
|
||||
|
||||
- 注册自定义服务到服务容器
|
||||
- 添加系统到场景
|
||||
- 注册自定义组件
|
||||
- 扩展框架功能
|
||||
|
||||
### 插件的优势
|
||||
|
||||
- **模块化**: 将功能封装为独立模块,提高代码可维护性
|
||||
- **可复用**: 同一个插件可以在多个项目中使用
|
||||
- **解耦**: 核心框架与扩展功能分离
|
||||
- **热插拔**: 运行时动态安装和卸载插件
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 创建第一个插件
|
||||
|
||||
创建一个简单的调试插件:
|
||||
|
||||
```typescript
|
||||
import { IPlugin, Core, ServiceContainer } from '@esengine/ecs-framework';
|
||||
|
||||
class DebugPlugin implements IPlugin {
|
||||
readonly name = 'debug-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
console.log('Debug plugin installed');
|
||||
|
||||
// 可以在这里注册服务、添加系统等
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
console.log('Debug plugin uninstalled');
|
||||
// 清理资源
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 安装插件
|
||||
|
||||
使用 `Core.installPlugin()` 安装插件:
|
||||
|
||||
```typescript
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始化Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 安装插件
|
||||
await Core.installPlugin(new DebugPlugin());
|
||||
|
||||
// 检查插件是否已安装
|
||||
if (Core.isPluginInstalled('debug-plugin')) {
|
||||
console.log('Debug plugin is running');
|
||||
}
|
||||
```
|
||||
|
||||
### 卸载插件
|
||||
|
||||
```typescript
|
||||
// 卸载插件
|
||||
await Core.uninstallPlugin('debug-plugin');
|
||||
```
|
||||
|
||||
### 获取插件实例
|
||||
|
||||
```typescript
|
||||
// 获取已安装的插件
|
||||
const plugin = Core.getPlugin('debug-plugin');
|
||||
if (plugin) {
|
||||
console.log(`Plugin version: ${plugin.version}`);
|
||||
}
|
||||
```
|
||||
|
||||
## 插件开发
|
||||
|
||||
### IPlugin 接口
|
||||
|
||||
所有插件必须实现 `IPlugin` 接口:
|
||||
|
||||
```typescript
|
||||
export interface IPlugin {
|
||||
// 插件唯一名称
|
||||
readonly name: string;
|
||||
|
||||
// 插件版本(建议遵循semver规范)
|
||||
readonly version: string;
|
||||
|
||||
// 依赖的其他插件(可选)
|
||||
readonly dependencies?: readonly string[];
|
||||
|
||||
// 安装插件时调用
|
||||
install(core: Core, services: ServiceContainer): void | Promise<void>;
|
||||
|
||||
// 卸载插件时调用
|
||||
uninstall(): void | Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
### 插件生命周期
|
||||
|
||||
#### install 方法
|
||||
|
||||
在插件安装时调用,用于初始化插件:
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// 1. 注册服务
|
||||
services.registerSingleton(MyService);
|
||||
|
||||
// 2. 访问当前场景
|
||||
const scene = core.scene;
|
||||
if (scene) {
|
||||
// 3. 添加系统
|
||||
scene.addSystem(new MySystem());
|
||||
}
|
||||
|
||||
// 4. 其他初始化逻辑
|
||||
console.log('Plugin initialized');
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 清理逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### uninstall 方法
|
||||
|
||||
在插件卸载时调用,用于清理资源:
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-plugin';
|
||||
readonly version = '1.0.0';
|
||||
private myService?: MyService;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
this.myService = new MyService();
|
||||
services.registerInstance(MyService, this.myService);
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 清理服务
|
||||
if (this.myService) {
|
||||
this.myService.dispose();
|
||||
this.myService = undefined;
|
||||
}
|
||||
|
||||
// 移除事件监听器
|
||||
// 释放其他资源
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 异步插件
|
||||
|
||||
插件的 `install` 和 `uninstall` 方法都支持异步:
|
||||
|
||||
```typescript
|
||||
class AsyncPlugin implements IPlugin {
|
||||
readonly name = 'async-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
async install(core: Core, services: ServiceContainer): Promise<void> {
|
||||
// 异步加载资源
|
||||
const config = await fetch('/plugin-config.json').then(r => r.json());
|
||||
|
||||
// 使用加载的配置初始化服务
|
||||
const service = new MyService(config);
|
||||
services.registerInstance(MyService, service);
|
||||
}
|
||||
|
||||
async uninstall(): Promise<void> {
|
||||
// 异步清理
|
||||
await this.saveState();
|
||||
}
|
||||
|
||||
private async saveState() {
|
||||
// 保存插件状态
|
||||
}
|
||||
}
|
||||
|
||||
// 使用
|
||||
await Core.installPlugin(new AsyncPlugin());
|
||||
```
|
||||
|
||||
### 注册服务
|
||||
|
||||
插件可以向服务容器注册自己的服务:
|
||||
|
||||
```typescript
|
||||
import { IService } from '@esengine/ecs-framework';
|
||||
|
||||
class NetworkService implements IService {
|
||||
connect(url: string) {
|
||||
console.log(`Connecting to ${url}`);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
console.log('Network service disposed');
|
||||
}
|
||||
}
|
||||
|
||||
class NetworkPlugin implements IPlugin {
|
||||
readonly name = 'network-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// 注册网络服务
|
||||
services.registerSingleton(NetworkService);
|
||||
|
||||
// 解析并使用服务
|
||||
const network = services.resolve(NetworkService);
|
||||
network.connect('ws://localhost:8080');
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 服务容器会自动调用服务的dispose方法
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 添加系统
|
||||
|
||||
插件可以向场景添加自定义系统:
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, Matcher } from '@esengine/ecs-framework';
|
||||
|
||||
class PhysicsSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(PhysicsBody));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 物理模拟逻辑
|
||||
}
|
||||
}
|
||||
|
||||
class PhysicsPlugin implements IPlugin {
|
||||
readonly name = 'physics-plugin';
|
||||
readonly version = '1.0.0';
|
||||
private physicsSystem?: PhysicsSystem;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
const scene = core.scene;
|
||||
if (scene) {
|
||||
this.physicsSystem = new PhysicsSystem();
|
||||
scene.addSystem(this.physicsSystem);
|
||||
}
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 移除系统
|
||||
if (this.physicsSystem) {
|
||||
const scene = Core.scene;
|
||||
if (scene) {
|
||||
scene.removeSystem(this.physicsSystem);
|
||||
}
|
||||
this.physicsSystem = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 依赖管理
|
||||
|
||||
### 声明依赖
|
||||
|
||||
插件可以声明对其他插件的依赖:
|
||||
|
||||
```typescript
|
||||
class AdvancedPhysicsPlugin implements IPlugin {
|
||||
readonly name = 'advanced-physics';
|
||||
readonly version = '2.0.0';
|
||||
|
||||
// 声明依赖基础物理插件
|
||||
readonly dependencies = ['physics-plugin'] as const;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// 可以安全地使用physics-plugin提供的服务
|
||||
const physicsService = services.resolve(PhysicsService);
|
||||
// ...
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 清理
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 依赖检查
|
||||
|
||||
框架会自动检查依赖关系,如果依赖未满足会抛出错误:
|
||||
|
||||
```typescript
|
||||
// 错误:physics-plugin 未安装
|
||||
try {
|
||||
await Core.installPlugin(new AdvancedPhysicsPlugin());
|
||||
} catch (error) {
|
||||
console.error(error); // Plugin advanced-physics has unmet dependencies: physics-plugin
|
||||
}
|
||||
|
||||
// 正确:先安装依赖
|
||||
await Core.installPlugin(new PhysicsPlugin());
|
||||
await Core.installPlugin(new AdvancedPhysicsPlugin());
|
||||
```
|
||||
|
||||
### 卸载顺序
|
||||
|
||||
框架会检查依赖关系,防止卸载被其他插件依赖的插件:
|
||||
|
||||
```typescript
|
||||
await Core.installPlugin(new PhysicsPlugin());
|
||||
await Core.installPlugin(new AdvancedPhysicsPlugin());
|
||||
|
||||
// 错误:physics-plugin 被 advanced-physics 依赖
|
||||
try {
|
||||
await Core.uninstallPlugin('physics-plugin');
|
||||
} catch (error) {
|
||||
console.error(error); // Cannot uninstall plugin physics-plugin: it is required by advanced-physics
|
||||
}
|
||||
|
||||
// 正确:先卸载依赖它的插件
|
||||
await Core.uninstallPlugin('advanced-physics');
|
||||
await Core.uninstallPlugin('physics-plugin');
|
||||
```
|
||||
|
||||
## 插件管理
|
||||
|
||||
### 通过 Core 管理
|
||||
|
||||
Core 类提供了便捷的插件管理方法:
|
||||
|
||||
```typescript
|
||||
// 安装插件
|
||||
await Core.installPlugin(myPlugin);
|
||||
|
||||
// 卸载插件
|
||||
await Core.uninstallPlugin('plugin-name');
|
||||
|
||||
// 检查插件是否已安装
|
||||
if (Core.isPluginInstalled('plugin-name')) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// 获取插件实例
|
||||
const plugin = Core.getPlugin('plugin-name');
|
||||
```
|
||||
|
||||
### 通过 PluginManager 管理
|
||||
|
||||
也可以直接使用 PluginManager 服务:
|
||||
|
||||
```typescript
|
||||
const pluginManager = Core.services.resolve(PluginManager);
|
||||
|
||||
// 获取所有插件
|
||||
const allPlugins = pluginManager.getAllPlugins();
|
||||
console.log(`Total plugins: ${allPlugins.length}`);
|
||||
|
||||
// 获取插件元数据
|
||||
const metadata = pluginManager.getMetadata('my-plugin');
|
||||
if (metadata) {
|
||||
console.log(`State: ${metadata.state}`);
|
||||
console.log(`Installed at: ${new Date(metadata.installedAt!)}`);
|
||||
}
|
||||
|
||||
// 获取所有插件元数据
|
||||
const allMetadata = pluginManager.getAllMetadata();
|
||||
for (const meta of allMetadata) {
|
||||
console.log(`${meta.name} v${meta.version} - ${meta.state}`);
|
||||
}
|
||||
```
|
||||
|
||||
## 实用插件示例
|
||||
|
||||
### 网络同步插件
|
||||
|
||||
```typescript
|
||||
import { IPlugin, IService, Core, ServiceContainer } from '@esengine/ecs-framework';
|
||||
|
||||
class NetworkSyncService implements IService {
|
||||
private ws?: WebSocket;
|
||||
|
||||
connect(url: string) {
|
||||
this.ws = new WebSocket(url);
|
||||
this.ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
this.handleMessage(data);
|
||||
};
|
||||
}
|
||||
|
||||
private handleMessage(data: any) {
|
||||
// 处理网络消息
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
this.ws = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NetworkSyncPlugin implements IPlugin {
|
||||
readonly name = 'network-sync';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// 注册网络服务
|
||||
services.registerSingleton(NetworkSyncService);
|
||||
|
||||
// 自动连接
|
||||
const network = services.resolve(NetworkSyncService);
|
||||
network.connect('ws://localhost:8080');
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 服务会自动dispose
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 性能分析插件
|
||||
|
||||
```typescript
|
||||
class PerformanceAnalysisPlugin implements IPlugin {
|
||||
readonly name = 'performance-analysis';
|
||||
readonly version = '1.0.0';
|
||||
private frameCount = 0;
|
||||
private totalTime = 0;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
const monitor = services.resolve(PerformanceMonitor);
|
||||
monitor.enable();
|
||||
|
||||
// 定期输出性能报告
|
||||
const timer = services.resolve(TimerManager);
|
||||
timer.schedule(5.0, true, null, () => {
|
||||
this.printReport(monitor);
|
||||
});
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 清理
|
||||
}
|
||||
|
||||
private printReport(monitor: PerformanceMonitor) {
|
||||
console.log('=== Performance Report ===');
|
||||
console.log(`FPS: ${monitor.getFPS()}`);
|
||||
console.log(`Memory: ${monitor.getMemoryUsage()} MB`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 命名规范
|
||||
|
||||
- 插件名称使用小写字母和连字符:`my-awesome-plugin`
|
||||
- 版本号遵循语义化版本规范:`1.0.0`
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-awesome-plugin'; // 好
|
||||
readonly version = '1.0.0'; // 好
|
||||
}
|
||||
```
|
||||
|
||||
### 清理资源
|
||||
|
||||
始终在 `uninstall` 中清理插件创建的所有资源:
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-plugin';
|
||||
readonly version = '1.0.0';
|
||||
private timerId?: number;
|
||||
private listener?: () => void;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// 添加定时器
|
||||
this.timerId = setInterval(() => {
|
||||
// ...
|
||||
}, 1000);
|
||||
|
||||
// 添加事件监听
|
||||
this.listener = () => {};
|
||||
window.addEventListener('resize', this.listener);
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 清理定时器
|
||||
if (this.timerId) {
|
||||
clearInterval(this.timerId);
|
||||
this.timerId = undefined;
|
||||
}
|
||||
|
||||
// 移除事件监听
|
||||
if (this.listener) {
|
||||
window.removeEventListener('resize', this.listener);
|
||||
this.listener = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 错误处理
|
||||
|
||||
在插件中妥善处理错误,避免影响整个应用:
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
async install(core: Core, services: ServiceContainer): Promise<void> {
|
||||
try {
|
||||
// 可能失败的操作
|
||||
await this.loadConfig();
|
||||
} catch (error) {
|
||||
console.error('Failed to load plugin config:', error);
|
||||
throw error; // 重新抛出,让框架知道安装失败
|
||||
}
|
||||
}
|
||||
|
||||
async uninstall(): Promise<void> {
|
||||
try {
|
||||
await this.cleanup();
|
||||
} catch (error) {
|
||||
console.error('Failed to cleanup plugin:', error);
|
||||
// 即使清理失败也不应该阻止卸载
|
||||
}
|
||||
}
|
||||
|
||||
private async loadConfig() {
|
||||
// 加载配置
|
||||
}
|
||||
|
||||
private async cleanup() {
|
||||
// 清理
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 配置化
|
||||
|
||||
允许用户配置插件行为:
|
||||
|
||||
```typescript
|
||||
interface NetworkPluginConfig {
|
||||
serverUrl: string;
|
||||
autoReconnect: boolean;
|
||||
timeout: number;
|
||||
}
|
||||
|
||||
class NetworkPlugin implements IPlugin {
|
||||
readonly name = 'network-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
constructor(private config: NetworkPluginConfig) {}
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
const network = new NetworkService(this.config);
|
||||
services.registerInstance(NetworkService, network);
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 清理
|
||||
}
|
||||
}
|
||||
|
||||
// 使用
|
||||
const plugin = new NetworkPlugin({
|
||||
serverUrl: 'ws://localhost:8080',
|
||||
autoReconnect: true,
|
||||
timeout: 5000
|
||||
});
|
||||
|
||||
await Core.installPlugin(plugin);
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 插件安装失败
|
||||
|
||||
**问题**: 插件安装时抛出错误
|
||||
|
||||
**原因**:
|
||||
- 依赖未满足
|
||||
- install 方法中有异常
|
||||
- 服务注册冲突
|
||||
|
||||
**解决**:
|
||||
1. 检查依赖是否已安装
|
||||
2. 查看错误日志
|
||||
3. 确保服务名称不冲突
|
||||
|
||||
### 插件卸载后仍有副作用
|
||||
|
||||
**问题**: 卸载插件后,插件的功能仍在运行
|
||||
|
||||
**原因**: uninstall 方法中未正确清理资源
|
||||
|
||||
**解决**: 确保在 uninstall 中清理:
|
||||
- 定时器
|
||||
- 事件监听器
|
||||
- WebSocket连接
|
||||
- 系统引用
|
||||
|
||||
### 何时使用插件
|
||||
|
||||
**适合使用插件**:
|
||||
- 可选功能(调试工具、性能分析)
|
||||
- 第三方集成(网络库、物理引擎)
|
||||
- 跨项目复用的功能模块
|
||||
|
||||
**不适合使用插件**:
|
||||
- 核心游戏逻辑
|
||||
- 简单的工具类
|
||||
- 项目特定的功能
|
||||
|
||||
## 相关链接
|
||||
|
||||
- [服务容器](./service-container.md) - 在插件中使用服务容器
|
||||
- [系统架构](./system.md) - 在插件中添加系统
|
||||
- [快速开始](./getting-started.md) - Core 初始化和基础使用
|
||||
675
docs/guide/scene-manager.md
Normal file
675
docs/guide/scene-manager.md
Normal file
@@ -0,0 +1,675 @@
|
||||
# SceneManager
|
||||
|
||||
SceneManager 是 ECS Framework 提供的轻量级场景管理器,适用于 95% 的游戏应用。它提供简单直观的 API,支持场景切换和延迟加载。
|
||||
|
||||
## 适用场景
|
||||
|
||||
SceneManager 适合以下场景:
|
||||
- 单人游戏
|
||||
- 简单多人游戏
|
||||
- 移动游戏
|
||||
- 需要场景切换的游戏(菜单、游戏、暂停等)
|
||||
- 不需要多 World 隔离的项目
|
||||
|
||||
## 特点
|
||||
|
||||
- 轻量级,零额外开销
|
||||
- 简单直观的 API
|
||||
- 支持延迟场景切换(避免在当前帧中途切换)
|
||||
- 自动管理 ECS 流式 API
|
||||
- 自动处理场景生命周期
|
||||
- 集成在 Core 中,自动更新
|
||||
|
||||
## 基本使用
|
||||
|
||||
### 推荐方式:使用 Core 的静态方法
|
||||
|
||||
这是最简单和推荐的方式,适合大多数应用:
|
||||
|
||||
```typescript
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// 1. 初始化 Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 2. 创建并设置场景
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "GameScene";
|
||||
|
||||
// 添加系统
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
|
||||
// 创建初始实体
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Transform(400, 300));
|
||||
player.addComponent(new Health(100));
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log("游戏场景已启动");
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 设置场景
|
||||
Core.setScene(new GameScene());
|
||||
|
||||
// 4. 游戏循环(Core.update 会自动更新场景)
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // 自动更新所有服务和场景
|
||||
}
|
||||
|
||||
// Laya 引擎集成
|
||||
Laya.timer.frameLoop(1, this, () => {
|
||||
const deltaTime = Laya.timer.delta / 1000;
|
||||
Core.update(deltaTime);
|
||||
});
|
||||
|
||||
// Cocos Creator 集成
|
||||
update(deltaTime: number) {
|
||||
Core.update(deltaTime);
|
||||
}
|
||||
```
|
||||
|
||||
### 高级方式:直接使用 SceneManager
|
||||
|
||||
如果需要更多控制,可以直接使用 SceneManager:
|
||||
|
||||
```typescript
|
||||
import { Core, SceneManager, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始化 Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 获取 SceneManager(Core 已自动创建并注册)
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
|
||||
// 设置场景
|
||||
const gameScene = new GameScene();
|
||||
sceneManager.setScene(gameScene);
|
||||
|
||||
// 游戏循环(仍然使用 Core.update)
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // Core会自动调用sceneManager.update()
|
||||
}
|
||||
```
|
||||
|
||||
**重要**:无论使用哪种方式,游戏循环中都应该只调用 `Core.update()`,它会自动更新 SceneManager 和场景。不需要手动调用 `sceneManager.update()`。
|
||||
|
||||
## 场景切换
|
||||
|
||||
### 立即切换
|
||||
|
||||
使用 `Core.setScene()` 或 `sceneManager.setScene()` 立即切换场景:
|
||||
|
||||
```typescript
|
||||
// 方式1:使用 Core(推荐)
|
||||
Core.setScene(new MenuScene());
|
||||
|
||||
// 方式2:使用 SceneManager
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
sceneManager.setScene(new MenuScene());
|
||||
```
|
||||
|
||||
### 延迟切换
|
||||
|
||||
使用 `Core.loadScene()` 或 `sceneManager.loadScene()` 延迟切换场景,场景会在下一帧切换:
|
||||
|
||||
```typescript
|
||||
// 方式1:使用 Core(推荐)
|
||||
Core.loadScene(new GameOverScene());
|
||||
|
||||
// 方式2:使用 SceneManager
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
sceneManager.loadScene(new GameOverScene());
|
||||
```
|
||||
|
||||
在 System 中切换场景时,应该使用延迟切换:
|
||||
|
||||
```typescript
|
||||
class GameOverSystem extends EntitySystem {
|
||||
process(entities: readonly Entity[]): void {
|
||||
const player = entities.find(e => e.name === 'Player');
|
||||
const health = player?.getComponent(Health);
|
||||
|
||||
if (health && health.value <= 0) {
|
||||
// 延迟切换到游戏结束场景(下一帧生效)
|
||||
Core.loadScene(new GameOverScene());
|
||||
// 当前帧继续执行,不会中断当前系统的处理
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 完整的场景切换示例
|
||||
|
||||
```typescript
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始化
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 菜单场景
|
||||
class MenuScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "MenuScene";
|
||||
|
||||
// 监听开始游戏事件
|
||||
this.eventSystem.on('start_game', () => {
|
||||
Core.loadScene(new GameScene());
|
||||
});
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log("显示菜单界面");
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
console.log("菜单场景卸载");
|
||||
}
|
||||
}
|
||||
|
||||
// 游戏场景
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "GameScene";
|
||||
|
||||
// 创建游戏实体
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Transform(400, 300));
|
||||
player.addComponent(new Health(100));
|
||||
|
||||
// 监听游戏结束事件
|
||||
this.eventSystem.on('game_over', () => {
|
||||
Core.loadScene(new GameOverScene());
|
||||
});
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log("游戏开始");
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
console.log("游戏场景卸载");
|
||||
}
|
||||
}
|
||||
|
||||
// 游戏结束场景
|
||||
class GameOverScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "GameOverScene";
|
||||
|
||||
// 监听返回菜单事件
|
||||
this.eventSystem.on('back_to_menu', () => {
|
||||
Core.loadScene(new MenuScene());
|
||||
});
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log("显示游戏结束界面");
|
||||
}
|
||||
}
|
||||
|
||||
// 开始游戏
|
||||
Core.setScene(new MenuScene());
|
||||
|
||||
// 游戏循环
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // 自动更新场景
|
||||
}
|
||||
```
|
||||
|
||||
## API 参考
|
||||
|
||||
### Core 静态方法(推荐)
|
||||
|
||||
#### Core.setScene()
|
||||
|
||||
立即切换场景。
|
||||
|
||||
```typescript
|
||||
public static setScene<T extends IScene>(scene: T): T
|
||||
```
|
||||
|
||||
**参数**:
|
||||
- `scene` - 要设置的场景实例
|
||||
|
||||
**返回**:
|
||||
- 返回设置的场景实例
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
const gameScene = Core.setScene(new GameScene());
|
||||
console.log(gameScene.name);
|
||||
```
|
||||
|
||||
#### Core.loadScene()
|
||||
|
||||
延迟加载场景(下一帧切换)。
|
||||
|
||||
```typescript
|
||||
public static loadScene<T extends IScene>(scene: T): void
|
||||
```
|
||||
|
||||
**参数**:
|
||||
- `scene` - 要加载的场景实例
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
Core.loadScene(new GameOverScene());
|
||||
```
|
||||
|
||||
#### Core.scene
|
||||
|
||||
获取当前活跃的场景。
|
||||
|
||||
```typescript
|
||||
public static get scene(): IScene | null
|
||||
```
|
||||
|
||||
**返回**:
|
||||
- 当前场景实例,如果没有场景则返回 null
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
const currentScene = Core.scene;
|
||||
if (currentScene) {
|
||||
console.log(`当前场景: ${currentScene.name}`);
|
||||
}
|
||||
```
|
||||
|
||||
#### Core.ecsAPI
|
||||
|
||||
获取 ECS 流式 API。
|
||||
|
||||
```typescript
|
||||
public static get ecsAPI(): ECSFluentAPI | null
|
||||
```
|
||||
|
||||
**返回**:
|
||||
- ECS API 实例,如果当前没有场景则返回 null
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
const api = Core.ecsAPI;
|
||||
if (api) {
|
||||
// 查询实体
|
||||
const enemies = api.find(Enemy, Transform);
|
||||
|
||||
// 发射事件
|
||||
api.emit('game:start', { level: 1 });
|
||||
}
|
||||
```
|
||||
|
||||
### SceneManager 方法(高级)
|
||||
|
||||
如果需要直接使用 SceneManager,可以通过服务容器获取:
|
||||
|
||||
```typescript
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
```
|
||||
|
||||
#### setScene()
|
||||
|
||||
立即切换场景。
|
||||
|
||||
```typescript
|
||||
public setScene<T extends IScene>(scene: T): T
|
||||
```
|
||||
|
||||
#### loadScene()
|
||||
|
||||
延迟加载场景。
|
||||
|
||||
```typescript
|
||||
public loadScene<T extends IScene>(scene: T): void
|
||||
```
|
||||
|
||||
#### currentScene
|
||||
|
||||
获取当前场景。
|
||||
|
||||
```typescript
|
||||
public get currentScene(): IScene | null
|
||||
```
|
||||
|
||||
#### api
|
||||
|
||||
获取 ECS 流式 API。
|
||||
|
||||
```typescript
|
||||
public get api(): ECSFluentAPI | null
|
||||
```
|
||||
|
||||
#### hasScene
|
||||
|
||||
检查是否有活跃场景。
|
||||
|
||||
```typescript
|
||||
public get hasScene(): boolean
|
||||
```
|
||||
|
||||
#### hasPendingScene
|
||||
|
||||
检查是否有待切换的场景。
|
||||
|
||||
```typescript
|
||||
public get hasPendingScene(): boolean
|
||||
```
|
||||
|
||||
## 使用 ECS 流式 API
|
||||
|
||||
通过 `Core.ecsAPI` 可以方便地访问场景的 ECS 功能:
|
||||
|
||||
```typescript
|
||||
const api = Core.ecsAPI;
|
||||
if (!api) {
|
||||
console.error('没有活跃场景');
|
||||
return;
|
||||
}
|
||||
|
||||
// 查询实体
|
||||
const players = api.find(Player, Transform);
|
||||
const enemies = api.find(Enemy, Health, Transform);
|
||||
|
||||
// 发射事件
|
||||
api.emit('player:scored', { points: 100 });
|
||||
|
||||
// 监听事件
|
||||
api.on('enemy:died', (data) => {
|
||||
console.log('敌人死亡:', data);
|
||||
});
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 使用 Core 的静态方法
|
||||
|
||||
```typescript
|
||||
// 推荐:使用 Core 的静态方法
|
||||
Core.setScene(new GameScene());
|
||||
Core.loadScene(new MenuScene());
|
||||
const currentScene = Core.scene;
|
||||
|
||||
// 不推荐:除非有特殊需求,否则不需要直接使用 SceneManager
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
sceneManager.setScene(new GameScene());
|
||||
```
|
||||
|
||||
### 2. 只调用 Core.update()
|
||||
|
||||
```typescript
|
||||
// 正确:只调用 Core.update()
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // 自动更新所有服务和场景
|
||||
}
|
||||
|
||||
// 错误:不要手动调用 sceneManager.update()
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime);
|
||||
sceneManager.update(); // 重复更新,会导致问题!
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 使用延迟切换避免问题
|
||||
|
||||
在 System 中切换场景时,应该使用 `loadScene()` 而不是 `setScene()`:
|
||||
|
||||
```typescript
|
||||
// 推荐:延迟切换
|
||||
class HealthSystem extends EntitySystem {
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health);
|
||||
if (health.value <= 0) {
|
||||
Core.loadScene(new GameOverScene());
|
||||
// 当前帧继续处理其他实体
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 不推荐:立即切换可能导致问题
|
||||
class HealthSystem extends EntitySystem {
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health);
|
||||
if (health.value <= 0) {
|
||||
Core.setScene(new GameOverScene());
|
||||
// 场景立即切换,当前帧的其他实体可能无法正常处理
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 场景职责分离
|
||||
|
||||
每个场景应该只负责一个特定的游戏状态:
|
||||
|
||||
```typescript
|
||||
// 好的设计 - 职责清晰
|
||||
class MenuScene extends Scene {
|
||||
// 只处理菜单相关逻辑
|
||||
}
|
||||
|
||||
class GameScene extends Scene {
|
||||
// 只处理游戏玩法逻辑
|
||||
}
|
||||
|
||||
class PauseScene extends Scene {
|
||||
// 只处理暂停界面逻辑
|
||||
}
|
||||
|
||||
// 避免的设计 - 职责混乱
|
||||
class MegaScene extends Scene {
|
||||
// 包含菜单、游戏、暂停等所有逻辑
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 资源管理
|
||||
|
||||
在场景的 `unload()` 方法中清理资源:
|
||||
|
||||
```typescript
|
||||
class GameScene extends Scene {
|
||||
private textures: Map<string, any> = new Map();
|
||||
private sounds: Map<string, any> = new Map();
|
||||
|
||||
protected initialize(): void {
|
||||
this.loadResources();
|
||||
}
|
||||
|
||||
private loadResources(): void {
|
||||
this.textures.set('player', loadTexture('player.png'));
|
||||
this.sounds.set('bgm', loadSound('bgm.mp3'));
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// 清理资源
|
||||
this.textures.clear();
|
||||
this.sounds.clear();
|
||||
console.log('场景资源已清理');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. 事件驱动的场景切换
|
||||
|
||||
使用事件系统来触发场景切换,保持代码解耦:
|
||||
|
||||
```typescript
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 监听场景切换事件
|
||||
this.eventSystem.on('goto:menu', () => {
|
||||
Core.loadScene(new MenuScene());
|
||||
});
|
||||
|
||||
this.eventSystem.on('goto:gameover', (data) => {
|
||||
Core.loadScene(new GameOverScene());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 在 System 中触发事件
|
||||
class GameLogicSystem extends EntitySystem {
|
||||
process(entities: readonly Entity[]): void {
|
||||
if (levelComplete) {
|
||||
this.scene.eventSystem.emitSync('goto:gameover', {
|
||||
score: 1000,
|
||||
level: 5
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 架构层次
|
||||
|
||||
SceneManager 在 ECS Framework 中的位置:
|
||||
|
||||
```
|
||||
Core (全局服务)
|
||||
└── SceneManager (场景管理,自动更新)
|
||||
└── Scene (当前场景)
|
||||
├── EntitySystem (系统)
|
||||
├── Entity (实体)
|
||||
└── Component (组件)
|
||||
```
|
||||
|
||||
## 与 WorldManager 的对比
|
||||
|
||||
| 特性 | SceneManager | WorldManager |
|
||||
|------|--------------|--------------|
|
||||
| 适用场景 | 95% 的游戏应用 | 高级多世界隔离场景 |
|
||||
| 复杂度 | 简单 | 复杂 |
|
||||
| 场景数量 | 单场景(可切换) | 多 World,每个 World 多场景 |
|
||||
| 性能开销 | 最小 | 较高 |
|
||||
| 使用方式 | `Core.setScene()` | `worldManager.createWorld()` |
|
||||
|
||||
**何时使用 SceneManager**:
|
||||
- 单人游戏
|
||||
- 简单的多人游戏
|
||||
- 移动游戏
|
||||
- 场景之间需要切换但不需要同时运行
|
||||
|
||||
**何时使用 WorldManager**:
|
||||
- MMO 游戏服务器(每个房间一个 World)
|
||||
- 游戏大厅系统(每个游戏房间完全隔离)
|
||||
- 需要运行多个完全独立的游戏实例
|
||||
|
||||
## 完整示例
|
||||
|
||||
```typescript
|
||||
import { Core, Scene, EntitySystem, Entity, Matcher } from '@esengine/ecs-framework';
|
||||
|
||||
// 定义组件
|
||||
class Transform {
|
||||
constructor(public x: number, public y: number) {}
|
||||
}
|
||||
|
||||
class Velocity {
|
||||
constructor(public vx: number, public vy: number) {}
|
||||
}
|
||||
|
||||
class Health {
|
||||
constructor(public value: number) {}
|
||||
}
|
||||
|
||||
// 定义系统
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Transform, Velocity));
|
||||
}
|
||||
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const transform = entity.getComponent(Transform);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
if (transform && velocity) {
|
||||
transform.x += velocity.vx;
|
||||
transform.y += velocity.vy;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 定义场景
|
||||
class MenuScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "MenuScene";
|
||||
console.log("菜单场景初始化");
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log("菜单场景启动");
|
||||
}
|
||||
}
|
||||
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "GameScene";
|
||||
|
||||
// 添加系统
|
||||
this.addSystem(new MovementSystem());
|
||||
|
||||
// 创建玩家
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Transform(400, 300));
|
||||
player.addComponent(new Velocity(0, 0));
|
||||
player.addComponent(new Health(100));
|
||||
|
||||
// 创建敌人
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const enemy = this.createEntity(`Enemy_${i}`);
|
||||
enemy.addComponent(new Transform(
|
||||
Math.random() * 800,
|
||||
Math.random() * 600
|
||||
));
|
||||
enemy.addComponent(new Velocity(
|
||||
Math.random() * 100 - 50,
|
||||
Math.random() * 100 - 50
|
||||
));
|
||||
enemy.addComponent(new Health(50));
|
||||
}
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log('游戏场景启动');
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
console.log('游戏场景卸载');
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 设置初始场景
|
||||
Core.setScene(new MenuScene());
|
||||
|
||||
// 游戏循环
|
||||
let lastTime = 0;
|
||||
function gameLoop(currentTime: number) {
|
||||
const deltaTime = (currentTime - lastTime) / 1000;
|
||||
lastTime = currentTime;
|
||||
|
||||
// 只需要调用 Core.update,它会自动更新场景
|
||||
Core.update(deltaTime);
|
||||
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
requestAnimationFrame(gameLoop);
|
||||
|
||||
// 切换到游戏场景
|
||||
setTimeout(() => {
|
||||
Core.loadScene(new GameScene());
|
||||
}, 3000);
|
||||
```
|
||||
|
||||
SceneManager 为大多数游戏提供了简单而强大的场景管理能力。通过 Core 的静态方法,你可以轻松地管理场景切换。如果你需要更高级的多世界隔离功能,请参考 [WorldManager](./world-manager.md) 文档。
|
||||
@@ -11,6 +11,22 @@
|
||||
- 事件系统支持
|
||||
- 性能监控和调试信息
|
||||
|
||||
## 场景管理方式
|
||||
|
||||
ECS Framework 提供了两种场景管理方式:
|
||||
|
||||
1. **[SceneManager](./scene-manager.md)** - 适用于 95% 的游戏应用
|
||||
- 单人游戏、简单多人游戏、移动游戏
|
||||
- 轻量级,简单直观的 API
|
||||
- 支持场景切换
|
||||
|
||||
2. **[WorldManager](./world-manager.md)** - 适用于高级多世界隔离场景
|
||||
- MMO 游戏服务器、游戏房间系统
|
||||
- 多 World 管理,每个 World 可包含多个场景
|
||||
- 完全隔离的独立环境
|
||||
|
||||
本文档重点介绍 Scene 类本身的使用方法。关于场景管理器的详细信息,请查看对应的文档。
|
||||
|
||||
## 创建场景
|
||||
|
||||
### 继承 Scene 类
|
||||
@@ -106,6 +122,13 @@ const scene = new ExampleScene();
|
||||
// 场景的 initialize(), begin(), update(), end() 由框架自动调用
|
||||
```
|
||||
|
||||
**生命周期方法**:
|
||||
|
||||
1. `initialize()` - 场景初始化,设置系统和初始实体
|
||||
2. `begin()` / `onStart()` - 场景开始运行
|
||||
3. `update()` - 每帧更新(由场景管理器调用)
|
||||
4. `end()` / `unload()` - 场景卸载,清理资源
|
||||
|
||||
## 实体管理
|
||||
|
||||
### 创建实体
|
||||
@@ -247,15 +270,42 @@ class EventScene extends Scene {
|
||||
}
|
||||
|
||||
public triggerGameEvent(): void {
|
||||
// 发送事件
|
||||
// 发送事件(同步)
|
||||
this.eventSystem.emitSync('custom_event', {
|
||||
message: "这是自定义事件",
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
// 发送事件(异步)
|
||||
this.eventSystem.emit('async_event', {
|
||||
data: "异步事件数据"
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 事件系统 API
|
||||
|
||||
```typescript
|
||||
// 监听事件
|
||||
this.eventSystem.on('event_name', callback);
|
||||
|
||||
// 监听一次(自动取消订阅)
|
||||
this.eventSystem.once('event_name', callback);
|
||||
|
||||
// 取消监听
|
||||
this.eventSystem.off('event_name', callback);
|
||||
|
||||
// 同步发送事件
|
||||
this.eventSystem.emitSync('event_name', data);
|
||||
|
||||
// 异步发送事件
|
||||
this.eventSystem.emit('event_name', data);
|
||||
|
||||
// 清除所有事件监听
|
||||
this.eventSystem.clear();
|
||||
```
|
||||
|
||||
## 场景统计和调试
|
||||
|
||||
### 获取场景统计
|
||||
@@ -287,110 +337,59 @@ class StatsScene extends Scene {
|
||||
}
|
||||
```
|
||||
|
||||
## 场景集成到框架
|
||||
## 组件查询
|
||||
|
||||
场景可以通过两种方式运行:
|
||||
|
||||
### 1. 简单的单场景应用
|
||||
Scene 提供了强大的组件查询系统:
|
||||
|
||||
```typescript
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
// 创建游戏场景
|
||||
class GameScene extends Scene {
|
||||
class QueryScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "GameScene";
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
}
|
||||
}
|
||||
|
||||
// 启动游戏
|
||||
Core.create();
|
||||
const gameScene = new GameScene();
|
||||
Core.setScene(gameScene);
|
||||
```
|
||||
|
||||
### 2. 复杂的多场景应用
|
||||
|
||||
```typescript
|
||||
import { WorldManager } from '@esengine/ecs-framework';
|
||||
|
||||
// 获取WorldManager实例
|
||||
const worldManager = WorldManager.getInstance();
|
||||
|
||||
// 创建World
|
||||
const gameWorld = worldManager.createWorld('game', {
|
||||
name: 'MainGame',
|
||||
maxScenes: 5
|
||||
});
|
||||
|
||||
// 在World中创建场景
|
||||
const menuScene = gameWorld.createScene('menu', new MenuScene());
|
||||
const gameScene = gameWorld.createScene('game', new GameScene());
|
||||
|
||||
// 激活场景
|
||||
gameWorld.setSceneActive('menu', true);
|
||||
```
|
||||
|
||||
## 多场景管理
|
||||
|
||||
在World中可以管理多个场景,通过激活/停用来切换:
|
||||
|
||||
```typescript
|
||||
class GameWorld extends World {
|
||||
private menuScene: Scene;
|
||||
private gameScene: Scene;
|
||||
private gameOverScene: Scene;
|
||||
|
||||
public initialize(): void {
|
||||
// 创建多个场景
|
||||
this.menuScene = this.createScene('menu', new MenuScene());
|
||||
this.gameScene = this.createScene('game', new GameScene());
|
||||
this.gameOverScene = this.createScene('gameover', new GameOverScene());
|
||||
|
||||
// 设置初始场景
|
||||
this.showMenu();
|
||||
// 创建一些实体
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const entity = this.createEntity(`Entity_${i}`);
|
||||
entity.addComponent(new Transform(i * 10, 0));
|
||||
entity.addComponent(new Velocity(1, 0));
|
||||
if (i % 2 === 0) {
|
||||
entity.addComponent(new Renderer());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public showMenu(): void {
|
||||
this.deactivateAllScenes();
|
||||
this.setSceneActive('menu', true);
|
||||
}
|
||||
public queryEntities(): void {
|
||||
// 通过 QuerySystem 查询
|
||||
const entities = this.querySystem.query([Transform, Velocity]);
|
||||
console.log(`找到 ${entities.length} 个有 Transform 和 Velocity 的实体`);
|
||||
|
||||
public startGame(): void {
|
||||
this.deactivateAllScenes();
|
||||
this.setSceneActive('game', true);
|
||||
}
|
||||
|
||||
public showGameOver(): void {
|
||||
this.deactivateAllScenes();
|
||||
this.setSceneActive('gameover', true);
|
||||
}
|
||||
|
||||
private deactivateAllScenes(): void {
|
||||
this.setSceneActive('menu', false);
|
||||
this.setSceneActive('game', false);
|
||||
this.setSceneActive('gameover', false);
|
||||
// 使用 ECS 流式 API(如果通过 SceneManager)
|
||||
// const api = sceneManager.api;
|
||||
// const entities = api?.find(Transform, Velocity);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 与 World 的关系
|
||||
## 性能监控
|
||||
|
||||
Scene 的运行架构层次:
|
||||
Scene 内置了性能监控功能:
|
||||
|
||||
```typescript
|
||||
// Core -> WorldManager -> World -> Scene -> EntitySystem -> Entity -> Component
|
||||
class PerformanceScene extends Scene {
|
||||
public showPerformance(): void {
|
||||
// 获取性能数据
|
||||
const perfData = this.performanceMonitor?.getPerformanceData();
|
||||
if (perfData) {
|
||||
console.log('FPS:', perfData.fps);
|
||||
console.log('帧时间:', perfData.frameTime);
|
||||
console.log('实体更新时间:', perfData.entityUpdateTime);
|
||||
console.log('系统更新时间:', perfData.systemUpdateTime);
|
||||
}
|
||||
|
||||
// 1. 简单应用:Core直接管理单个Scene
|
||||
Core.setScene(new GameScene());
|
||||
|
||||
// 2. 复杂应用:WorldManager管理多个World,每个World管理多个Scene
|
||||
const worldManager = WorldManager.getInstance();
|
||||
const world = worldManager.createWorld('gameWorld');
|
||||
const scene = world.createScene('mainScene', new GameScene());
|
||||
world.setSceneActive('mainScene', true);
|
||||
// 获取性能报告
|
||||
const report = this.performanceMonitor?.generateReport();
|
||||
if (report) {
|
||||
console.log('性能报告:', report);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
@@ -398,7 +397,7 @@ world.setSceneActive('mainScene', true);
|
||||
### 1. 场景职责分离
|
||||
|
||||
```typescript
|
||||
// ✅ 好的场景设计 - 职责清晰
|
||||
// 好的场景设计 - 职责清晰
|
||||
class MenuScene extends Scene {
|
||||
// 只处理菜单相关逻辑
|
||||
}
|
||||
@@ -411,7 +410,7 @@ class InventoryScene extends Scene {
|
||||
// 只处理物品栏逻辑
|
||||
}
|
||||
|
||||
// ❌ 避免的场景设计 - 职责混乱
|
||||
// 避免的场景设计 - 职责混乱
|
||||
class MegaScene extends Scene {
|
||||
// 包含菜单、游戏、物品栏等所有逻辑
|
||||
}
|
||||
@@ -458,12 +457,25 @@ class ResourceScene extends Scene {
|
||||
|
||||
private loadResources(): void {
|
||||
// 加载场景所需资源
|
||||
this.textures.set('player', this.loadTexture('player.png'));
|
||||
this.sounds.set('bgm', this.loadSound('bgm.mp3'));
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// 清理资源
|
||||
this.textures.clear();
|
||||
this.sounds.clear();
|
||||
console.log('场景资源已清理');
|
||||
}
|
||||
|
||||
private loadTexture(path: string): any {
|
||||
// 加载纹理
|
||||
return null;
|
||||
}
|
||||
|
||||
private loadSound(path: string): any {
|
||||
// 加载音效
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -504,7 +516,146 @@ class EventHandlingScene extends Scene {
|
||||
private onPlayerInput(data: any): void {
|
||||
// 处理玩家输入
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// 清理事件监听
|
||||
this.eventSystem.clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
场景是 ECS 框架的核心容器,正确使用场景管理能让你的游戏架构更加清晰、模块化和易于维护。
|
||||
### 5. 初始化顺序
|
||||
|
||||
```typescript
|
||||
class ProperInitScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 1. 首先设置场景配置
|
||||
this.name = "GameScene";
|
||||
|
||||
// 2. 然后添加系统(按依赖顺序)
|
||||
this.addSystem(new InputSystem());
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new PhysicsSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
|
||||
// 3. 最后创建实体
|
||||
this.createEntities();
|
||||
|
||||
// 4. 设置事件监听
|
||||
this.setupEvents();
|
||||
}
|
||||
|
||||
private createEntities(): void {
|
||||
// 创建实体
|
||||
}
|
||||
|
||||
private setupEvents(): void {
|
||||
// 设置事件监听
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 完整示例
|
||||
|
||||
```typescript
|
||||
import { Scene, EntitySystem, Entity, Matcher } from '@esengine/ecs-framework';
|
||||
|
||||
// 定义组件
|
||||
class Transform {
|
||||
constructor(public x: number, public y: number) {}
|
||||
}
|
||||
|
||||
class Velocity {
|
||||
constructor(public vx: number, public vy: number) {}
|
||||
}
|
||||
|
||||
class Health {
|
||||
constructor(public value: number) {}
|
||||
}
|
||||
|
||||
// 定义系统
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Transform, Velocity));
|
||||
}
|
||||
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const transform = entity.getComponent(Transform);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
if (transform && velocity) {
|
||||
transform.x += velocity.vx;
|
||||
transform.y += velocity.vy;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 定义场景
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "GameScene";
|
||||
|
||||
// 添加系统
|
||||
this.addSystem(new MovementSystem());
|
||||
|
||||
// 创建玩家
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Transform(400, 300));
|
||||
player.addComponent(new Velocity(0, 0));
|
||||
player.addComponent(new Health(100));
|
||||
|
||||
// 创建敌人
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const enemy = this.createEntity(`Enemy_${i}`);
|
||||
enemy.addComponent(new Transform(
|
||||
Math.random() * 800,
|
||||
Math.random() * 600
|
||||
));
|
||||
enemy.addComponent(new Velocity(
|
||||
Math.random() * 100 - 50,
|
||||
Math.random() * 100 - 50
|
||||
));
|
||||
enemy.addComponent(new Health(50));
|
||||
}
|
||||
|
||||
// 设置事件监听
|
||||
this.eventSystem.on('player_died', () => {
|
||||
console.log('玩家死亡!');
|
||||
});
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log('游戏场景启动');
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
console.log('游戏场景卸载');
|
||||
this.eventSystem.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// 使用场景
|
||||
// 方式1:通过 SceneManager(推荐)
|
||||
import { Core, SceneManager } from '@esengine/ecs-framework';
|
||||
|
||||
Core.create({ debug: true });
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
sceneManager.setScene(new GameScene());
|
||||
|
||||
// 方式2:通过 WorldManager(高级用例)
|
||||
import { WorldManager } from '@esengine/ecs-framework';
|
||||
|
||||
const worldManager = Core.services.resolve(WorldManager);
|
||||
const world = worldManager.createWorld('game');
|
||||
world.createScene('main', new GameScene());
|
||||
world.setSceneActive('main', true);
|
||||
```
|
||||
|
||||
## 下一步
|
||||
|
||||
- 了解 [SceneManager](./scene-manager.md) - 适用于大多数游戏的简单场景管理
|
||||
- 了解 [WorldManager](./world-manager.md) - 适用于需要多世界隔离的高级场景
|
||||
|
||||
场景是 ECS 框架的核心容器,正确使用场景管理能让你的游戏架构更加清晰、模块化和易于维护。
|
||||
|
||||
@@ -14,6 +14,18 @@
|
||||
- **JSON格式**:人类可读,便于调试和编辑
|
||||
- **Binary格式**:使用MessagePack,体积更小,性能更高
|
||||
|
||||
> **📢 v2.2.2 重要变更**
|
||||
>
|
||||
> 从 v2.2.2 开始,二进制序列化格式返回 `Uint8Array` 而非 Node.js 的 `Buffer`,以确保浏览器兼容性:
|
||||
> - `serialize({ format: 'binary' })` 返回 `string | Uint8Array`(原为 `string | Buffer`)
|
||||
> - `deserialize(data)` 接收 `string | Uint8Array`(原为 `string | Buffer`)
|
||||
> - `applyIncremental(data)` 接收 `IncrementalSnapshot | string | Uint8Array`(原为包含 `Buffer`)
|
||||
>
|
||||
> **迁移影响**:
|
||||
> - ✅ **运行时兼容**:Node.js 的 `Buffer` 继承自 `Uint8Array`,现有代码可直接运行
|
||||
> - ⚠️ **类型检查**:如果你的 TypeScript 代码中显式使用了 `Buffer` 类型,需要改为 `Uint8Array`
|
||||
> - ✅ **浏览器支持**:`Uint8Array` 是标准 JavaScript 类型,所有现代浏览器都支持
|
||||
|
||||
## 全量序列化
|
||||
|
||||
### 基础用法
|
||||
@@ -63,6 +75,7 @@ const binaryData = scene.serialize({
|
||||
});
|
||||
|
||||
// 保存为文件(Node.js环境)
|
||||
// 注意:binaryData 是 Uint8Array 类型,Node.js 的 fs 可以直接写入
|
||||
fs.writeFileSync('save.bin', binaryData);
|
||||
```
|
||||
|
||||
@@ -285,7 +298,7 @@ otherScene.applyIncremental(incremental);
|
||||
const jsonData = IncrementalSerializer.serializeIncremental(incremental, { format: 'json' });
|
||||
otherScene.applyIncremental(jsonData);
|
||||
|
||||
// 从二进制Buffer应用
|
||||
// 从二进制Uint8Array应用
|
||||
const binaryData = IncrementalSerializer.serializeIncremental(incremental, { format: 'binary' });
|
||||
otherScene.applyIncremental(binaryData);
|
||||
```
|
||||
@@ -552,9 +565,9 @@ class NetworkSync {
|
||||
}
|
||||
|
||||
private receiveIncremental(data: ArrayBuffer): void {
|
||||
// 直接应用二进制数据
|
||||
const buffer = Buffer.from(data);
|
||||
this.scene.applyIncremental(buffer);
|
||||
// 直接应用二进制数据(ArrayBuffer 转 Uint8Array)
|
||||
const uint8Array = new Uint8Array(data);
|
||||
this.scene.applyIncremental(uint8Array);
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -790,7 +803,7 @@ class LargeDataComponent extends Component {
|
||||
|
||||
- [`Scene.createIncrementalSnapshot()`](/api/classes/Scene#createincrementalsnapshot) - 创建基础快照
|
||||
- [`Scene.serializeIncremental()`](/api/classes/Scene#serializeincremental) - 获取增量变更
|
||||
- [`Scene.applyIncremental()`](/api/classes/Scene#applyincremental) - 应用增量变更(支持IncrementalSnapshot对象、JSON字符串或二进制Buffer)
|
||||
- [`Scene.applyIncremental()`](/api/classes/Scene#applyincremental) - 应用增量变更(支持IncrementalSnapshot对象、JSON字符串或二进制Uint8Array)
|
||||
- [`Scene.updateIncrementalSnapshot()`](/api/classes/Scene#updateincrementalsnapshot) - 更新快照基准
|
||||
- [`Scene.clearIncrementalSnapshot()`](/api/classes/Scene#clearincrementalsnapshot) - 清除快照
|
||||
- [`Scene.hasIncrementalSnapshot()`](/api/classes/Scene#hasincrementalsnapshot) - 检查是否有快照
|
||||
|
||||
589
docs/guide/service-container.md
Normal file
589
docs/guide/service-container.md
Normal file
@@ -0,0 +1,589 @@
|
||||
# 服务容器
|
||||
|
||||
服务容器(ServiceContainer)是 ECS Framework 的依赖注入容器,负责管理框架中所有服务的注册、解析和生命周期。通过服务容器,你可以实现松耦合的架构设计,提高代码的可测试性和可维护性。
|
||||
|
||||
## 概述
|
||||
|
||||
### 什么是服务容器
|
||||
|
||||
服务容器是一个轻量级的依赖注入(DI)容器,它提供了:
|
||||
|
||||
- **服务注册**: 将服务类型注册到容器中
|
||||
- **服务解析**: 从容器中获取服务实例
|
||||
- **生命周期管理**: 自动管理服务实例的创建和销毁
|
||||
- **依赖注入**: 自动解析服务之间的依赖关系
|
||||
|
||||
### 核心概念
|
||||
|
||||
#### 服务(Service)
|
||||
|
||||
服务是实现了 `IService` 接口的类,必须提供 `dispose()` 方法用于资源清理:
|
||||
|
||||
```typescript
|
||||
import { IService } from '@esengine/ecs-framework';
|
||||
|
||||
class MyService implements IService {
|
||||
constructor() {
|
||||
// 初始化逻辑
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
// 清理资源
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 生命周期
|
||||
|
||||
服务容器支持两种生命周期:
|
||||
|
||||
- **Singleton(单例)**: 整个应用程序生命周期内只有一个实例,所有解析请求返回同一个实例
|
||||
- **Transient(瞬时)**: 每次解析都创建新的实例
|
||||
|
||||
## 基础使用
|
||||
|
||||
### 访问服务容器
|
||||
|
||||
Core 类内置了服务容器,可以通过 `Core.services` 访问:
|
||||
|
||||
```typescript
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始化Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 访问服务容器
|
||||
const container = Core.services;
|
||||
```
|
||||
|
||||
### 注册服务
|
||||
|
||||
#### 注册单例服务
|
||||
|
||||
单例服务在首次解析时创建,之后所有解析请求都返回同一个实例:
|
||||
|
||||
```typescript
|
||||
class DataService implements IService {
|
||||
private data: Map<string, any> = new Map();
|
||||
|
||||
getData(key: string) {
|
||||
return this.data.get(key);
|
||||
}
|
||||
|
||||
setData(key: string, value: any) {
|
||||
this.data.set(key, value);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.data.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// 注册单例服务
|
||||
Core.services.registerSingleton(DataService);
|
||||
```
|
||||
|
||||
#### 注册瞬时服务
|
||||
|
||||
瞬时服务每次解析都创建新实例,适用于无状态或短生命周期的服务:
|
||||
|
||||
```typescript
|
||||
class CommandService implements IService {
|
||||
execute(command: string) {
|
||||
console.log(`Executing: ${command}`);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
// 清理资源
|
||||
}
|
||||
}
|
||||
|
||||
// 注册瞬时服务
|
||||
Core.services.registerTransient(CommandService);
|
||||
```
|
||||
|
||||
#### 注册服务实例
|
||||
|
||||
直接注册已创建的实例,自动视为单例:
|
||||
|
||||
```typescript
|
||||
const config = new ConfigService();
|
||||
config.load('./config.json');
|
||||
|
||||
// 注册实例
|
||||
Core.services.registerInstance(ConfigService, config);
|
||||
```
|
||||
|
||||
#### 使用工厂函数注册
|
||||
|
||||
工厂函数允许你在创建服务时执行自定义逻辑:
|
||||
|
||||
```typescript
|
||||
Core.services.registerSingleton(LoggerService, (container) => {
|
||||
const logger = new LoggerService();
|
||||
logger.setLevel('debug');
|
||||
return logger;
|
||||
});
|
||||
```
|
||||
|
||||
### 解析服务
|
||||
|
||||
#### resolve 方法
|
||||
|
||||
解析服务实例,如果服务未注册会抛出异常:
|
||||
|
||||
```typescript
|
||||
// 解析服务
|
||||
const dataService = Core.services.resolve(DataService);
|
||||
dataService.setData('player', { name: 'Alice', score: 100 });
|
||||
|
||||
// 单例服务,多次解析返回同一个实例
|
||||
const same = Core.services.resolve(DataService);
|
||||
console.log(same === dataService); // true
|
||||
```
|
||||
|
||||
#### tryResolve 方法
|
||||
|
||||
尝试解析服务,如果未注册返回 null 而不抛出异常:
|
||||
|
||||
```typescript
|
||||
const optional = Core.services.tryResolve(OptionalService);
|
||||
if (optional) {
|
||||
optional.doSomething();
|
||||
}
|
||||
```
|
||||
|
||||
#### isRegistered 方法
|
||||
|
||||
检查服务是否已注册:
|
||||
|
||||
```typescript
|
||||
if (Core.services.isRegistered(DataService)) {
|
||||
const service = Core.services.resolve(DataService);
|
||||
}
|
||||
```
|
||||
|
||||
## 内置服务
|
||||
|
||||
Core 在初始化时自动注册了以下内置服务:
|
||||
|
||||
### TimerManager
|
||||
|
||||
定时器管理器,负责管理所有游戏定时器:
|
||||
|
||||
```typescript
|
||||
const timerManager = Core.services.resolve(TimerManager);
|
||||
|
||||
// 创建定时器
|
||||
timerManager.schedule(1.0, false, null, (timer) => {
|
||||
console.log('1秒后执行');
|
||||
});
|
||||
```
|
||||
|
||||
### PerformanceMonitor
|
||||
|
||||
性能监控器,监控游戏性能并提供优化建议:
|
||||
|
||||
```typescript
|
||||
const monitor = Core.services.resolve(PerformanceMonitor);
|
||||
|
||||
// 启用性能监控
|
||||
monitor.enable();
|
||||
|
||||
// 获取性能数据
|
||||
const fps = monitor.getFPS();
|
||||
```
|
||||
|
||||
### SceneManager
|
||||
|
||||
场景管理器,管理单场景应用的场景生命周期:
|
||||
|
||||
```typescript
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
|
||||
// 设置当前场景
|
||||
sceneManager.setScene(new GameScene());
|
||||
|
||||
// 获取当前场景
|
||||
const currentScene = sceneManager.currentScene;
|
||||
|
||||
// 延迟切换场景
|
||||
sceneManager.loadScene(new MenuScene());
|
||||
|
||||
// 更新场景
|
||||
sceneManager.update();
|
||||
```
|
||||
|
||||
### WorldManager
|
||||
|
||||
世界管理器,管理多个独立的 World 实例(高级用例):
|
||||
|
||||
```typescript
|
||||
const worldManager = Core.services.resolve(WorldManager);
|
||||
|
||||
// 创建独立的游戏世界
|
||||
const gameWorld = worldManager.createWorld('game_room_001', {
|
||||
name: 'GameRoom',
|
||||
maxScenes: 5
|
||||
});
|
||||
|
||||
// 在World中创建场景
|
||||
const scene = gameWorld.createScene('battle', new BattleScene());
|
||||
gameWorld.setSceneActive('battle', true);
|
||||
|
||||
// 更新所有World
|
||||
worldManager.updateAll();
|
||||
```
|
||||
|
||||
**适用场景**:
|
||||
- SceneManager: 适用于 95% 的游戏(单人游戏、简单多人游戏)
|
||||
- WorldManager: 适用于 MMO 服务器、游戏房间系统等需要完全隔离的多世界应用
|
||||
|
||||
### PoolManager
|
||||
|
||||
对象池管理器,管理所有对象池:
|
||||
|
||||
```typescript
|
||||
const poolManager = Core.services.resolve(PoolManager);
|
||||
|
||||
// 创建对象池
|
||||
const bulletPool = poolManager.createPool('bullets', () => new Bullet(), 100);
|
||||
```
|
||||
|
||||
### PluginManager
|
||||
|
||||
插件管理器,管理插件的安装和卸载:
|
||||
|
||||
```typescript
|
||||
const pluginManager = Core.services.resolve(PluginManager);
|
||||
|
||||
// 获取所有已安装的插件
|
||||
const plugins = pluginManager.getAllPlugins();
|
||||
```
|
||||
|
||||
## 依赖注入
|
||||
|
||||
ECS Framework 提供了装饰器来简化依赖注入。
|
||||
|
||||
### @Injectable 装饰器
|
||||
|
||||
标记类为可注入的服务:
|
||||
|
||||
```typescript
|
||||
import { Injectable, IService } from '@esengine/ecs-framework';
|
||||
|
||||
@Injectable()
|
||||
class GameService implements IService {
|
||||
constructor() {
|
||||
console.log('GameService created');
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
console.log('GameService disposed');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### @Inject 装饰器
|
||||
|
||||
在构造函数中注入依赖:
|
||||
|
||||
```typescript
|
||||
import { Injectable, Inject, IService } from '@esengine/ecs-framework';
|
||||
|
||||
@Injectable()
|
||||
class PlayerService implements IService {
|
||||
constructor(
|
||||
@Inject(DataService) private data: DataService,
|
||||
@Inject(GameService) private game: GameService
|
||||
) {
|
||||
// data 和 game 会自动从容器中解析
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
// 清理资源
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 注册可注入服务
|
||||
|
||||
使用 `registerInjectable` 自动处理依赖注入:
|
||||
|
||||
```typescript
|
||||
import { registerInjectable } from '@esengine/ecs-framework';
|
||||
|
||||
// 注册服务(会自动解析@Inject依赖)
|
||||
registerInjectable(Core.services, PlayerService);
|
||||
|
||||
// 解析时会自动注入依赖
|
||||
const player = Core.services.resolve(PlayerService);
|
||||
```
|
||||
|
||||
### @Updatable 装饰器
|
||||
|
||||
标记服务为可更新的,使其在每帧自动被调用:
|
||||
|
||||
```typescript
|
||||
import { Injectable, Updatable, IService, IUpdatable } from '@esengine/ecs-framework';
|
||||
|
||||
@Injectable()
|
||||
@Updatable() // 默认优先级为0
|
||||
class PhysicsService implements IService, IUpdatable {
|
||||
update(deltaTime?: number): void {
|
||||
// 每帧更新物理模拟
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
// 清理资源
|
||||
}
|
||||
}
|
||||
|
||||
// 指定更新优先级(数值越小越先执行)
|
||||
@Injectable()
|
||||
@Updatable(10)
|
||||
class RenderService implements IService, IUpdatable {
|
||||
update(deltaTime?: number): void {
|
||||
// 每帧渲染
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
// 清理资源
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
使用 `@Updatable` 装饰器的服务会被 Core 自动调用,无需手动管理:
|
||||
|
||||
```typescript
|
||||
// Core.update() 会自动调用所有@Updatable服务的update方法
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // 自动更新所有可更新服务
|
||||
}
|
||||
```
|
||||
|
||||
## 自定义服务
|
||||
|
||||
### 创建自定义服务
|
||||
|
||||
实现 `IService` 接口并注册到容器:
|
||||
|
||||
```typescript
|
||||
import { IService } from '@esengine/ecs-framework';
|
||||
|
||||
class AudioService implements IService {
|
||||
private sounds: Map<string, HTMLAudioElement> = new Map();
|
||||
|
||||
play(soundId: string) {
|
||||
const sound = this.sounds.get(soundId);
|
||||
if (sound) {
|
||||
sound.play();
|
||||
}
|
||||
}
|
||||
|
||||
load(soundId: string, url: string) {
|
||||
const audio = new Audio(url);
|
||||
this.sounds.set(soundId, audio);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
// 停止所有音效并清理
|
||||
for (const sound of this.sounds.values()) {
|
||||
sound.pause();
|
||||
sound.src = '';
|
||||
}
|
||||
this.sounds.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// 注册自定义服务
|
||||
Core.services.registerSingleton(AudioService);
|
||||
|
||||
// 使用服务
|
||||
const audio = Core.services.resolve(AudioService);
|
||||
audio.load('jump', '/sounds/jump.mp3');
|
||||
audio.play('jump');
|
||||
```
|
||||
|
||||
### 服务间依赖
|
||||
|
||||
服务可以依赖其他服务:
|
||||
|
||||
```typescript
|
||||
@Injectable()
|
||||
class ConfigService implements IService {
|
||||
private config: any = {};
|
||||
|
||||
get(key: string) {
|
||||
return this.config[key];
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.config = {};
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
class NetworkService implements IService {
|
||||
constructor(
|
||||
@Inject(ConfigService) private config: ConfigService
|
||||
) {
|
||||
// 使用配置服务
|
||||
const apiUrl = this.config.get('apiUrl');
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
// 清理网络连接
|
||||
}
|
||||
}
|
||||
|
||||
// 注册服务(按依赖顺序)
|
||||
registerInjectable(Core.services, ConfigService);
|
||||
registerInjectable(Core.services, NetworkService);
|
||||
```
|
||||
|
||||
## 高级用法
|
||||
|
||||
### 服务替换(测试)
|
||||
|
||||
在测试中替换真实服务为模拟服务:
|
||||
|
||||
```typescript
|
||||
// 测试代码
|
||||
class MockDataService implements IService {
|
||||
getData(key: string) {
|
||||
return 'mock data';
|
||||
}
|
||||
|
||||
dispose(): void {}
|
||||
}
|
||||
|
||||
// 注册模拟服务(用于测试)
|
||||
Core.services.registerInstance(DataService, new MockDataService());
|
||||
```
|
||||
|
||||
### 循环依赖检测
|
||||
|
||||
服务容器会自动检测循环依赖:
|
||||
|
||||
```typescript
|
||||
// A 依赖 B,B 依赖 A
|
||||
@Injectable()
|
||||
class ServiceA implements IService {
|
||||
constructor(@Inject(ServiceB) b: ServiceB) {}
|
||||
dispose(): void {}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
class ServiceB implements IService {
|
||||
constructor(@Inject(ServiceA) a: ServiceA) {}
|
||||
dispose(): void {}
|
||||
}
|
||||
|
||||
// 解析时会抛出错误: Circular dependency detected: ServiceA -> ServiceB -> ServiceA
|
||||
```
|
||||
|
||||
### 获取所有服务
|
||||
|
||||
```typescript
|
||||
// 获取所有已注册的服务类型
|
||||
const types = Core.services.getRegisteredServices();
|
||||
|
||||
// 获取所有已实例化的服务实例
|
||||
const instances = Core.services.getAll();
|
||||
```
|
||||
|
||||
### 服务清理
|
||||
|
||||
```typescript
|
||||
// 注销单个服务
|
||||
Core.services.unregister(MyService);
|
||||
|
||||
// 清空所有服务(会调用每个服务的dispose方法)
|
||||
Core.services.clear();
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 服务命名
|
||||
|
||||
服务类名应该以 `Service` 或 `Manager` 结尾,清晰表达其职责:
|
||||
|
||||
```typescript
|
||||
class PlayerService implements IService {}
|
||||
class AudioManager implements IService {}
|
||||
class NetworkService implements IService {}
|
||||
```
|
||||
|
||||
### 资源清理
|
||||
|
||||
始终在 `dispose()` 方法中清理资源:
|
||||
|
||||
```typescript
|
||||
class ResourceService implements IService {
|
||||
private resources: Map<string, Resource> = new Map();
|
||||
|
||||
dispose(): void {
|
||||
// 释放所有资源
|
||||
for (const resource of this.resources.values()) {
|
||||
resource.release();
|
||||
}
|
||||
this.resources.clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 避免过度使用
|
||||
|
||||
不要把所有类都注册为服务,服务应该是:
|
||||
|
||||
- 全局单例或需要共享状态
|
||||
- 需要在多处使用
|
||||
- 生命周期需要管理
|
||||
- 需要依赖注入
|
||||
|
||||
对于简单的工具类或数据类,直接创建实例即可。
|
||||
|
||||
### 依赖方向
|
||||
|
||||
保持清晰的依赖方向,避免循环依赖:
|
||||
|
||||
```
|
||||
高层服务 -> 中层服务 -> 底层服务
|
||||
GameLogic -> DataService -> ConfigService
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 服务未注册错误
|
||||
|
||||
**问题**: `Error: Service MyService is not registered`
|
||||
|
||||
**解决**:
|
||||
```typescript
|
||||
// 确保服务已注册
|
||||
Core.services.registerSingleton(MyService);
|
||||
|
||||
// 或者使用tryResolve
|
||||
const service = Core.services.tryResolve(MyService);
|
||||
if (!service) {
|
||||
console.log('Service not found');
|
||||
}
|
||||
```
|
||||
|
||||
### 循环依赖错误
|
||||
|
||||
**问题**: `Circular dependency detected`
|
||||
|
||||
**解决**: 重新设计服务依赖关系,引入中间服务或使用事件系统解耦。
|
||||
|
||||
### 何时使用单例 vs 瞬时
|
||||
|
||||
- **单例**: 管理器类、配置、缓存、状态管理
|
||||
- **瞬时**: 命令对象、请求处理器、临时任务
|
||||
|
||||
## 相关链接
|
||||
|
||||
- [插件系统](./plugin-system.md) - 使用服务容器注册插件服务
|
||||
- [快速开始](./getting-started.md) - Core 初始化和基础使用
|
||||
- [系统架构](./system.md) - 在系统中使用服务
|
||||
@@ -354,14 +354,18 @@ class PerformanceSystem extends EntitySystem {
|
||||
|
||||
### 添加系统到场景
|
||||
|
||||
框架提供了两种方式添加系统:传入实例或传入类型(自动依赖注入)。
|
||||
|
||||
```typescript
|
||||
// 在场景子类中添加系统
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 添加系统
|
||||
// 方式1:传入实例
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
this.addSystem(new PhysicsSystem());
|
||||
|
||||
// 方式2:传入类型(自动依赖注入)
|
||||
this.addEntityProcessor(PhysicsSystem);
|
||||
|
||||
// 设置系统更新顺序
|
||||
const movementSystem = this.getSystem(MovementSystem);
|
||||
@@ -372,6 +376,48 @@ class GameScene extends Scene {
|
||||
}
|
||||
```
|
||||
|
||||
### 系统依赖注入
|
||||
|
||||
系统实现了 `IService` 接口,支持通过依赖注入获取其他服务或系统:
|
||||
|
||||
```typescript
|
||||
import { ECSSystem, Injectable, Inject } from '@esengine/ecs-framework';
|
||||
|
||||
@Injectable()
|
||||
@ECSSystem('Physics')
|
||||
class PhysicsSystem extends EntitySystem {
|
||||
constructor(
|
||||
@Inject(CollisionService) private collision: CollisionService
|
||||
) {
|
||||
super(Matcher.all(Transform, RigidBody));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 使用注入的服务
|
||||
this.collision.detectCollisions(entities);
|
||||
}
|
||||
|
||||
// 实现 IService 接口的 dispose 方法
|
||||
public dispose(): void {
|
||||
// 清理资源
|
||||
}
|
||||
}
|
||||
|
||||
// 使用时传入类型即可,框架会自动注入依赖
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 自动依赖注入
|
||||
this.addEntityProcessor(PhysicsSystem);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
注意事项:
|
||||
- 使用 `@Injectable()` 装饰器标记需要依赖注入的系统
|
||||
- 在构造函数参数中使用 `@Inject()` 装饰器声明依赖
|
||||
- 系统必须实现 `dispose()` 方法(IService 接口要求)
|
||||
- 使用 `addEntityProcessor(类型)` 而不是 `addSystem(new 类型())` 来启用依赖注入
|
||||
|
||||
### 系统更新顺序
|
||||
|
||||
```typescript
|
||||
|
||||
761
docs/guide/world-manager.md
Normal file
761
docs/guide/world-manager.md
Normal file
@@ -0,0 +1,761 @@
|
||||
# WorldManager
|
||||
|
||||
WorldManager 是 ECS Framework 提供的高级世界管理器,用于管理多个完全隔离的游戏世界(World)。每个 World 都是独立的 ECS 环境,可以包含多个场景。
|
||||
|
||||
## 适用场景
|
||||
|
||||
WorldManager 适合以下高级场景:
|
||||
- MMO 游戏服务器的多房间管理
|
||||
- 游戏大厅系统(每个游戏房间完全隔离)
|
||||
- 服务器端的多游戏实例
|
||||
- 需要完全隔离的多个游戏环境
|
||||
- 需要同时运行多个独立世界的应用
|
||||
|
||||
## 特点
|
||||
|
||||
- 多 World 管理,每个 World 完全独立
|
||||
- 每个 World 可以包含多个 Scene
|
||||
- 支持 World 的激活/停用
|
||||
- 自动清理空 World
|
||||
- World 级别的全局系统
|
||||
- 批量操作和查询
|
||||
|
||||
## 基本使用
|
||||
|
||||
### 初始化
|
||||
|
||||
WorldManager 是 Core 的内置服务,通过服务容器获取:
|
||||
|
||||
```typescript
|
||||
import { Core, WorldManager } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始化 Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 从服务容器获取 WorldManager(Core 已自动创建并注册)
|
||||
const worldManager = Core.services.resolve(WorldManager);
|
||||
```
|
||||
|
||||
### 创建 World
|
||||
|
||||
```typescript
|
||||
// 创建游戏房间 World
|
||||
const room1 = worldManager.createWorld('room_001', {
|
||||
name: 'GameRoom_001',
|
||||
maxScenes: 5,
|
||||
debug: true
|
||||
});
|
||||
|
||||
// 激活 World
|
||||
worldManager.setWorldActive('room_001', true);
|
||||
|
||||
// 创建更多房间
|
||||
const room2 = worldManager.createWorld('room_002', {
|
||||
name: 'GameRoom_002',
|
||||
maxScenes: 5
|
||||
});
|
||||
|
||||
worldManager.setWorldActive('room_002', true);
|
||||
```
|
||||
|
||||
### 游戏循环
|
||||
|
||||
在游戏循环中更新所有活跃的 World:
|
||||
|
||||
```typescript
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // 更新全局服务
|
||||
worldManager.updateAll(); // 更新所有活跃的 World
|
||||
}
|
||||
|
||||
// 启动游戏循环
|
||||
let lastTime = 0;
|
||||
setInterval(() => {
|
||||
const currentTime = Date.now();
|
||||
const deltaTime = (currentTime - lastTime) / 1000;
|
||||
lastTime = currentTime;
|
||||
|
||||
gameLoop(deltaTime);
|
||||
}, 16); // 60 FPS
|
||||
```
|
||||
|
||||
## World 管理
|
||||
|
||||
### 创建 World
|
||||
|
||||
```typescript
|
||||
// 基本创建
|
||||
const world = worldManager.createWorld('worldId');
|
||||
|
||||
// 带配置创建
|
||||
const world = worldManager.createWorld('worldId', {
|
||||
name: 'MyWorld',
|
||||
maxScenes: 10,
|
||||
autoCleanup: true,
|
||||
debug: true
|
||||
});
|
||||
```
|
||||
|
||||
**配置选项(IWorldConfig)**:
|
||||
- `name?: string` - World 名称
|
||||
- `maxScenes?: number` - 最大场景数量限制(默认 10)
|
||||
- `autoCleanup?: boolean` - 是否自动清理空场景(默认 true)
|
||||
- `debug?: boolean` - 是否启用调试模式(默认 false)
|
||||
|
||||
### 获取 World
|
||||
|
||||
```typescript
|
||||
// 通过 ID 获取
|
||||
const world = worldManager.getWorld('room_001');
|
||||
if (world) {
|
||||
console.log(`World: ${world.name}`);
|
||||
}
|
||||
|
||||
// 获取所有 World
|
||||
const allWorlds = worldManager.getAllWorlds();
|
||||
console.log(`共有 ${allWorlds.length} 个 World`);
|
||||
|
||||
// 获取所有 World ID
|
||||
const worldIds = worldManager.getWorldIds();
|
||||
console.log('World 列表:', worldIds);
|
||||
|
||||
// 通过名称查找
|
||||
const world = worldManager.findWorldByName('GameRoom_001');
|
||||
```
|
||||
|
||||
### 激活和停用 World
|
||||
|
||||
```typescript
|
||||
// 激活 World(开始运行和更新)
|
||||
worldManager.setWorldActive('room_001', true);
|
||||
|
||||
// 停用 World(停止更新但保留数据)
|
||||
worldManager.setWorldActive('room_001', false);
|
||||
|
||||
// 检查 World 是否激活
|
||||
if (worldManager.isWorldActive('room_001')) {
|
||||
console.log('房间正在运行');
|
||||
}
|
||||
|
||||
// 获取所有活跃的 World
|
||||
const activeWorlds = worldManager.getActiveWorlds();
|
||||
console.log(`当前有 ${activeWorlds.length} 个活跃 World`);
|
||||
```
|
||||
|
||||
### 移除 World
|
||||
|
||||
```typescript
|
||||
// 移除 World(会自动停用并销毁)
|
||||
const removed = worldManager.removeWorld('room_001');
|
||||
if (removed) {
|
||||
console.log('World 已移除');
|
||||
}
|
||||
```
|
||||
|
||||
## World 中的场景管理
|
||||
|
||||
每个 World 可以包含多个 Scene 并独立管理它们的生命周期。
|
||||
|
||||
### 创建场景
|
||||
|
||||
```typescript
|
||||
const world = worldManager.getWorld('room_001');
|
||||
if (!world) return;
|
||||
|
||||
// 创建场景
|
||||
const mainScene = world.createScene('main', new MainScene());
|
||||
const uiScene = world.createScene('ui', new UIScene());
|
||||
const hudScene = world.createScene('hud', new HUDScene());
|
||||
|
||||
// 激活场景
|
||||
world.setSceneActive('main', true);
|
||||
world.setSceneActive('ui', true);
|
||||
world.setSceneActive('hud', false);
|
||||
```
|
||||
|
||||
### 查询场景
|
||||
|
||||
```typescript
|
||||
// 获取特定场景
|
||||
const mainScene = world.getScene<MainScene>('main');
|
||||
if (mainScene) {
|
||||
console.log(`场景名称: ${mainScene.name}`);
|
||||
}
|
||||
|
||||
// 获取所有场景
|
||||
const allScenes = world.getAllScenes();
|
||||
console.log(`World 中共有 ${allScenes.length} 个场景`);
|
||||
|
||||
// 获取所有场景 ID
|
||||
const sceneIds = world.getSceneIds();
|
||||
console.log('场景列表:', sceneIds);
|
||||
|
||||
// 获取活跃场景数量
|
||||
const activeCount = world.getActiveSceneCount();
|
||||
console.log(`当前有 ${activeCount} 个活跃场景`);
|
||||
|
||||
// 检查场景是否激活
|
||||
if (world.isSceneActive('main')) {
|
||||
console.log('主场景正在运行');
|
||||
}
|
||||
```
|
||||
|
||||
### 场景切换
|
||||
|
||||
World 支持多场景同时运行,也支持场景切换:
|
||||
|
||||
```typescript
|
||||
class GameWorld {
|
||||
private world: World;
|
||||
|
||||
constructor(worldManager: WorldManager) {
|
||||
this.world = worldManager.createWorld('game', {
|
||||
name: 'GameWorld',
|
||||
maxScenes: 5
|
||||
});
|
||||
|
||||
// 创建所有场景
|
||||
this.world.createScene('menu', new MenuScene());
|
||||
this.world.createScene('game', new GameScene());
|
||||
this.world.createScene('pause', new PauseScene());
|
||||
this.world.createScene('gameover', new GameOverScene());
|
||||
|
||||
// 激活 World
|
||||
worldManager.setWorldActive('game', true);
|
||||
}
|
||||
|
||||
public showMenu(): void {
|
||||
this.deactivateAllScenes();
|
||||
this.world.setSceneActive('menu', true);
|
||||
}
|
||||
|
||||
public startGame(): void {
|
||||
this.deactivateAllScenes();
|
||||
this.world.setSceneActive('game', true);
|
||||
}
|
||||
|
||||
public pauseGame(): void {
|
||||
// 游戏场景继续存在但停止更新
|
||||
this.world.setSceneActive('game', false);
|
||||
// 显示暂停界面
|
||||
this.world.setSceneActive('pause', true);
|
||||
}
|
||||
|
||||
public resumeGame(): void {
|
||||
this.world.setSceneActive('pause', false);
|
||||
this.world.setSceneActive('game', true);
|
||||
}
|
||||
|
||||
public showGameOver(): void {
|
||||
this.deactivateAllScenes();
|
||||
this.world.setSceneActive('gameover', true);
|
||||
}
|
||||
|
||||
private deactivateAllScenes(): void {
|
||||
const sceneIds = this.world.getSceneIds();
|
||||
sceneIds.forEach(id => this.world.setSceneActive(id, false));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 移除场景
|
||||
|
||||
```typescript
|
||||
// 移除不再需要的场景
|
||||
const removed = world.removeScene('oldScene');
|
||||
if (removed) {
|
||||
console.log('场景已移除');
|
||||
}
|
||||
|
||||
// 场景会自动调用 end() 方法进行清理
|
||||
```
|
||||
|
||||
## 全局系统
|
||||
|
||||
World 支持全局系统,这些系统在 World 级别运行,不依赖特定 Scene。
|
||||
|
||||
### 定义全局系统
|
||||
|
||||
```typescript
|
||||
import { IGlobalSystem } from '@esengine/ecs-framework';
|
||||
|
||||
// 网络系统(World 级别)
|
||||
class NetworkSystem implements IGlobalSystem {
|
||||
readonly name = 'NetworkSystem';
|
||||
|
||||
private connectionId: string;
|
||||
|
||||
constructor(connectionId: string) {
|
||||
this.connectionId = connectionId;
|
||||
}
|
||||
|
||||
initialize(): void {
|
||||
console.log(`网络系统初始化: ${this.connectionId}`);
|
||||
// 建立网络连接
|
||||
}
|
||||
|
||||
update(deltaTime?: number): void {
|
||||
// 处理网络消息,不依赖任何 Scene
|
||||
// 接收和发送网络包
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
console.log(`网络系统销毁: ${this.connectionId}`);
|
||||
// 关闭网络连接
|
||||
}
|
||||
}
|
||||
|
||||
// 物理系统(World 级别)
|
||||
class PhysicsSystem implements IGlobalSystem {
|
||||
readonly name = 'PhysicsSystem';
|
||||
|
||||
initialize(): void {
|
||||
console.log('物理系统初始化');
|
||||
}
|
||||
|
||||
update(deltaTime?: number): void {
|
||||
// 物理模拟,作用于 World 中所有场景
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
console.log('物理系统销毁');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用全局系统
|
||||
|
||||
```typescript
|
||||
const world = worldManager.getWorld('room_001');
|
||||
if (!world) return;
|
||||
|
||||
// 添加全局系统
|
||||
const networkSystem = world.addGlobalSystem(new NetworkSystem('conn_001'));
|
||||
const physicsSystem = world.addGlobalSystem(new PhysicsSystem());
|
||||
|
||||
// 获取全局系统
|
||||
const network = world.getGlobalSystem(NetworkSystem);
|
||||
if (network) {
|
||||
console.log('找到网络系统');
|
||||
}
|
||||
|
||||
// 移除全局系统
|
||||
world.removeGlobalSystem(networkSystem);
|
||||
```
|
||||
|
||||
## 批量操作
|
||||
|
||||
### 更新所有 World
|
||||
|
||||
```typescript
|
||||
// 更新所有活跃的 World(应该在游戏循环中调用)
|
||||
worldManager.updateAll();
|
||||
|
||||
// 这会自动更新每个 World 的:
|
||||
// 1. 全局系统
|
||||
// 2. 所有活跃场景
|
||||
```
|
||||
|
||||
### 启动和停止
|
||||
|
||||
```typescript
|
||||
// 启动所有 World
|
||||
worldManager.startAll();
|
||||
|
||||
// 停止所有 World
|
||||
worldManager.stopAll();
|
||||
|
||||
// 检查是否正在运行
|
||||
if (worldManager.isRunning) {
|
||||
console.log('WorldManager 正在运行');
|
||||
}
|
||||
```
|
||||
|
||||
### 查找 World
|
||||
|
||||
```typescript
|
||||
// 使用条件查找
|
||||
const emptyWorlds = worldManager.findWorlds(world => {
|
||||
return world.sceneCount === 0;
|
||||
});
|
||||
|
||||
// 查找活跃的 World
|
||||
const activeWorlds = worldManager.findWorlds(world => {
|
||||
return world.isActive;
|
||||
});
|
||||
|
||||
// 查找特定名称的 World
|
||||
const world = worldManager.findWorldByName('GameRoom_001');
|
||||
```
|
||||
|
||||
## 统计和监控
|
||||
|
||||
### 获取统计信息
|
||||
|
||||
```typescript
|
||||
const stats = worldManager.getStats();
|
||||
|
||||
console.log(`总 World 数: ${stats.totalWorlds}`);
|
||||
console.log(`活跃 World 数: ${stats.activeWorlds}`);
|
||||
console.log(`总场景数: ${stats.totalScenes}`);
|
||||
console.log(`总实体数: ${stats.totalEntities}`);
|
||||
console.log(`总系统数: ${stats.totalSystems}`);
|
||||
|
||||
// 查看每个 World 的详细信息
|
||||
stats.worlds.forEach(worldInfo => {
|
||||
console.log(`World: ${worldInfo.name}`);
|
||||
console.log(` 场景数: ${worldInfo.sceneCount}`);
|
||||
console.log(` 是否活跃: ${worldInfo.isActive}`);
|
||||
});
|
||||
```
|
||||
|
||||
### 获取详细状态
|
||||
|
||||
```typescript
|
||||
const status = worldManager.getDetailedStatus();
|
||||
|
||||
// 包含所有 World 的详细状态
|
||||
status.worlds.forEach(worldStatus => {
|
||||
console.log(`World ID: ${worldStatus.id}`);
|
||||
console.log(`状态:`, worldStatus.status);
|
||||
});
|
||||
```
|
||||
|
||||
## 自动清理
|
||||
|
||||
WorldManager 支持自动清理空的 World。
|
||||
|
||||
### 配置清理
|
||||
|
||||
```typescript
|
||||
// 创建带清理配置的 WorldManager
|
||||
const worldManager = Core.services.resolve(WorldManager);
|
||||
|
||||
// WorldManager 的配置在 Core 中设置:
|
||||
// {
|
||||
// maxWorlds: 50,
|
||||
// autoCleanup: true,
|
||||
// cleanupInterval: 30000 // 30 秒
|
||||
// }
|
||||
```
|
||||
|
||||
### 手动清理
|
||||
|
||||
```typescript
|
||||
// 手动触发清理
|
||||
const cleanedCount = worldManager.cleanup();
|
||||
console.log(`清理了 ${cleanedCount} 个 World`);
|
||||
```
|
||||
|
||||
**清理条件**:
|
||||
- World 未激活
|
||||
- 没有 Scene 或所有 Scene 都是空的
|
||||
- 创建时间超过 10 分钟
|
||||
|
||||
## API 参考
|
||||
|
||||
### WorldManager API
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `createWorld(worldId, config?)` | 创建新 World |
|
||||
| `removeWorld(worldId)` | 移除 World |
|
||||
| `getWorld(worldId)` | 获取 World |
|
||||
| `getAllWorlds()` | 获取所有 World |
|
||||
| `getWorldIds()` | 获取所有 World ID |
|
||||
| `setWorldActive(worldId, active)` | 设置 World 激活状态 |
|
||||
| `isWorldActive(worldId)` | 检查 World 是否激活 |
|
||||
| `getActiveWorlds()` | 获取所有活跃的 World |
|
||||
| `updateAll()` | 更新所有活跃 World |
|
||||
| `startAll()` | 启动所有 World |
|
||||
| `stopAll()` | 停止所有 World |
|
||||
| `findWorlds(predicate)` | 查找满足条件的 World |
|
||||
| `findWorldByName(name)` | 根据名称查找 World |
|
||||
| `getStats()` | 获取统计信息 |
|
||||
| `getDetailedStatus()` | 获取详细状态信息 |
|
||||
| `cleanup()` | 清理空 World |
|
||||
| `destroy()` | 销毁 WorldManager |
|
||||
|
||||
### World API
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `createScene(sceneId, sceneInstance?)` | 创建并添加 Scene |
|
||||
| `removeScene(sceneId)` | 移除 Scene |
|
||||
| `getScene(sceneId)` | 获取 Scene |
|
||||
| `getAllScenes()` | 获取所有 Scene |
|
||||
| `getSceneIds()` | 获取所有 Scene ID |
|
||||
| `setSceneActive(sceneId, active)` | 设置 Scene 激活状态 |
|
||||
| `isSceneActive(sceneId)` | 检查 Scene 是否激活 |
|
||||
| `getActiveSceneCount()` | 获取活跃 Scene 数量 |
|
||||
| `addGlobalSystem(system)` | 添加全局系统 |
|
||||
| `removeGlobalSystem(system)` | 移除全局系统 |
|
||||
| `getGlobalSystem(type)` | 获取全局系统 |
|
||||
| `start()` | 启动 World |
|
||||
| `stop()` | 停止 World |
|
||||
| `updateGlobalSystems()` | 更新全局系统 |
|
||||
| `updateScenes()` | 更新所有激活 Scene |
|
||||
| `destroy()` | 销毁 World |
|
||||
| `getStatus()` | 获取 World 状态 |
|
||||
| `getStats()` | 获取统计信息 |
|
||||
|
||||
### 属性
|
||||
|
||||
| 属性 | 说明 |
|
||||
|------|------|
|
||||
| `worldCount` | World 总数 |
|
||||
| `activeWorldCount` | 活跃 World 数量 |
|
||||
| `isRunning` | 是否正在运行 |
|
||||
| `config` | 配置信息 |
|
||||
|
||||
## 完整示例
|
||||
|
||||
### MMO 游戏房间系统
|
||||
|
||||
```typescript
|
||||
import { Core, WorldManager, Scene, World } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始化
|
||||
Core.create({ debug: true });
|
||||
const worldManager = Core.services.resolve(WorldManager);
|
||||
|
||||
// 房间管理器
|
||||
class RoomManager {
|
||||
private worldManager: WorldManager;
|
||||
private rooms: Map<string, World> = new Map();
|
||||
|
||||
constructor(worldManager: WorldManager) {
|
||||
this.worldManager = worldManager;
|
||||
}
|
||||
|
||||
// 创建游戏房间
|
||||
public createRoom(roomId: string, maxPlayers: number): World {
|
||||
const world = this.worldManager.createWorld(roomId, {
|
||||
name: `Room_${roomId}`,
|
||||
maxScenes: 3,
|
||||
debug: true
|
||||
});
|
||||
|
||||
// 创建房间场景
|
||||
world.createScene('lobby', new LobbyScene());
|
||||
world.createScene('game', new GameScene());
|
||||
world.createScene('result', new ResultScene());
|
||||
|
||||
// 添加房间级别的系统
|
||||
world.addGlobalSystem(new NetworkSystem(roomId));
|
||||
world.addGlobalSystem(new RoomLogicSystem(maxPlayers));
|
||||
|
||||
// 激活 World 和初始场景
|
||||
this.worldManager.setWorldActive(roomId, true);
|
||||
world.setSceneActive('lobby', true);
|
||||
|
||||
this.rooms.set(roomId, world);
|
||||
console.log(`房间 ${roomId} 已创建`);
|
||||
|
||||
return world;
|
||||
}
|
||||
|
||||
// 玩家加入房间
|
||||
public joinRoom(roomId: string, playerId: string): boolean {
|
||||
const world = this.rooms.get(roomId);
|
||||
if (!world) {
|
||||
console.log(`房间 ${roomId} 不存在`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 在大厅场景中创建玩家实体
|
||||
const lobbyScene = world.getScene('lobby');
|
||||
if (lobbyScene) {
|
||||
const player = lobbyScene.createEntity(`Player_${playerId}`);
|
||||
// 添加玩家组件...
|
||||
console.log(`玩家 ${playerId} 加入房间 ${roomId}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// 开始游戏
|
||||
public startGame(roomId: string): void {
|
||||
const world = this.rooms.get(roomId);
|
||||
if (!world) return;
|
||||
|
||||
// 切换到游戏场景
|
||||
world.setSceneActive('lobby', false);
|
||||
world.setSceneActive('game', true);
|
||||
|
||||
console.log(`房间 ${roomId} 游戏开始`);
|
||||
}
|
||||
|
||||
// 结束游戏
|
||||
public endGame(roomId: string): void {
|
||||
const world = this.rooms.get(roomId);
|
||||
if (!world) return;
|
||||
|
||||
// 切换到结果场景
|
||||
world.setSceneActive('game', false);
|
||||
world.setSceneActive('result', true);
|
||||
|
||||
console.log(`房间 ${roomId} 游戏结束`);
|
||||
}
|
||||
|
||||
// 关闭房间
|
||||
public closeRoom(roomId: string): void {
|
||||
this.worldManager.removeWorld(roomId);
|
||||
this.rooms.delete(roomId);
|
||||
console.log(`房间 ${roomId} 已关闭`);
|
||||
}
|
||||
|
||||
// 获取房间列表
|
||||
public getRoomList(): string[] {
|
||||
return Array.from(this.rooms.keys());
|
||||
}
|
||||
|
||||
// 获取房间统计
|
||||
public getRoomStats(roomId: string) {
|
||||
const world = this.rooms.get(roomId);
|
||||
return world?.getStats();
|
||||
}
|
||||
}
|
||||
|
||||
// 使用房间管理器
|
||||
const roomManager = new RoomManager(worldManager);
|
||||
|
||||
// 创建多个游戏房间
|
||||
roomManager.createRoom('room_001', 4);
|
||||
roomManager.createRoom('room_002', 4);
|
||||
roomManager.createRoom('room_003', 2);
|
||||
|
||||
// 玩家加入
|
||||
roomManager.joinRoom('room_001', 'player_1');
|
||||
roomManager.joinRoom('room_001', 'player_2');
|
||||
|
||||
// 开始游戏
|
||||
roomManager.startGame('room_001');
|
||||
|
||||
// 游戏循环
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime);
|
||||
worldManager.updateAll(); // 更新所有房间
|
||||
}
|
||||
|
||||
// 定期清理空房间
|
||||
setInterval(() => {
|
||||
const stats = worldManager.getStats();
|
||||
console.log(`当前房间数: ${stats.totalWorlds}`);
|
||||
console.log(`活跃房间数: ${stats.activeWorlds}`);
|
||||
|
||||
worldManager.cleanup();
|
||||
}, 60000); // 每分钟清理一次
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 合理的 World 粒度
|
||||
|
||||
```typescript
|
||||
// 推荐:每个独立环境一个 World
|
||||
const room1 = worldManager.createWorld('room_1'); // 游戏房间1
|
||||
const room2 = worldManager.createWorld('room_2'); // 游戏房间2
|
||||
|
||||
// 不推荐:过度使用 World
|
||||
const world1 = worldManager.createWorld('ui'); // UI 不需要独立 World
|
||||
const world2 = worldManager.createWorld('menu'); // 菜单不需要独立 World
|
||||
```
|
||||
|
||||
### 2. 使用全局系统处理跨场景逻辑
|
||||
|
||||
```typescript
|
||||
// 推荐:World 级别的系统
|
||||
class NetworkSystem implements IGlobalSystem {
|
||||
update() {
|
||||
// 网络处理不依赖场景
|
||||
}
|
||||
}
|
||||
|
||||
// 不推荐:在每个场景中重复创建
|
||||
class GameScene extends Scene {
|
||||
initialize() {
|
||||
this.addSystem(new NetworkSystem()); // 不应该在场景级别
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 及时清理不用的 World
|
||||
|
||||
```typescript
|
||||
// 推荐:玩家离开时清理房间
|
||||
function onPlayerLeave(roomId: string) {
|
||||
const world = worldManager.getWorld(roomId);
|
||||
if (world && world.sceneCount === 0) {
|
||||
worldManager.removeWorld(roomId);
|
||||
}
|
||||
}
|
||||
|
||||
// 或使用自动清理
|
||||
worldManager.cleanup();
|
||||
```
|
||||
|
||||
### 4. 监控资源使用
|
||||
|
||||
```typescript
|
||||
// 定期检查资源使用情况
|
||||
setInterval(() => {
|
||||
const stats = worldManager.getStats();
|
||||
|
||||
if (stats.totalWorlds > 100) {
|
||||
console.warn('World 数量过多,考虑清理');
|
||||
worldManager.cleanup();
|
||||
}
|
||||
|
||||
if (stats.totalEntities > 10000) {
|
||||
console.warn('实体数量过多,检查是否有泄漏');
|
||||
}
|
||||
}, 30000);
|
||||
```
|
||||
|
||||
## 与 SceneManager 的对比
|
||||
|
||||
| 特性 | SceneManager | WorldManager |
|
||||
|------|--------------|--------------|
|
||||
| 适用场景 | 95% 的游戏应用 | 高级多世界隔离场景 |
|
||||
| 复杂度 | 简单 | 复杂 |
|
||||
| 场景数量 | 单场景(可切换) | 多 World,每个 World 多场景 |
|
||||
| 场景隔离 | 无(场景切换) | 完全隔离(每个 World 独立) |
|
||||
| 性能开销 | 最小 | 较高 |
|
||||
| 全局系统 | 无 | 支持(World 级别) |
|
||||
| 使用示例 | 单人游戏、移动游戏 | MMO 服务器、游戏房间系统 |
|
||||
|
||||
**何时使用 WorldManager**:
|
||||
- MMO 游戏服务器(每个房间一个 World)
|
||||
- 游戏大厅系统(每个游戏房间完全隔离)
|
||||
- 需要运行多个完全独立的游戏实例
|
||||
- 服务器端模拟多个游戏世界
|
||||
|
||||
**何时使用 SceneManager**:
|
||||
- 单人游戏
|
||||
- 简单的多人游戏
|
||||
- 移动游戏
|
||||
- 场景之间需要切换但不需要同时运行
|
||||
|
||||
## 架构层次
|
||||
|
||||
WorldManager 在 ECS Framework 中的位置:
|
||||
|
||||
```
|
||||
Core (全局服务)
|
||||
└── WorldManager (世界管理)
|
||||
├── World 1 (游戏房间1)
|
||||
│ ├── GlobalSystem (全局系统)
|
||||
│ ├── Scene 1 (场景1)
|
||||
│ │ ├── EntitySystem
|
||||
│ │ ├── Entity
|
||||
│ │ └── Component
|
||||
│ └── Scene 2 (场景2)
|
||||
├── World 2 (游戏房间2)
|
||||
│ ├── GlobalSystem
|
||||
│ └── Scene 1
|
||||
└── World 3 (游戏房间3)
|
||||
```
|
||||
|
||||
WorldManager 为需要多世界隔离的高级应用提供了强大的管理能力。如果你的应用不需要多世界隔离,建议使用更简单的 [SceneManager](./scene-manager.md)。
|
||||
@@ -2,7 +2,9 @@ import { DemoBase, DemoInfo } from './DemoBase';
|
||||
import {
|
||||
Component,
|
||||
ECSComponent,
|
||||
Entity,
|
||||
EntitySystem,
|
||||
Matcher,
|
||||
Serializable,
|
||||
Serialize,
|
||||
IncrementalSerializer
|
||||
@@ -47,19 +49,19 @@ class RenderableComponent extends Component {
|
||||
|
||||
// ===== 系统定义 =====
|
||||
class MovementSystem extends EntitySystem {
|
||||
update() {
|
||||
if (!this.scene) return;
|
||||
const entities = this.scene.entities.buffer;
|
||||
for (const entity of entities) {
|
||||
const pos = entity.getComponent(PositionComponent);
|
||||
const vel = entity.getComponent(VelocityComponent);
|
||||
if (pos && vel) {
|
||||
pos.x += vel.vx;
|
||||
pos.y += vel.vy;
|
||||
constructor() {
|
||||
super(Matcher.all(PositionComponent, VelocityComponent));
|
||||
}
|
||||
|
||||
if (pos.x < 0 || pos.x > 1200) vel.vx *= -1;
|
||||
if (pos.y < 0 || pos.y > 600) vel.vy *= -1;
|
||||
}
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const [pos, vel] = this.getComponents(entity, PositionComponent, VelocityComponent);
|
||||
|
||||
pos.x += vel.vx;
|
||||
pos.y += vel.vy;
|
||||
|
||||
if (pos.x < 0 || pos.x > 1200) vel.vx *= -1;
|
||||
if (pos.y < 0 || pos.y > 600) vel.vy *= -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,34 +71,29 @@ class RenderSystem extends EntitySystem {
|
||||
private ctx: CanvasRenderingContext2D;
|
||||
|
||||
constructor(canvas: HTMLCanvasElement) {
|
||||
super();
|
||||
super(Matcher.all(PositionComponent, RenderableComponent));
|
||||
this.canvas = canvas;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) throw new Error('Failed to get canvas context');
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
update() {
|
||||
if (!this.scene) return;
|
||||
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
this.ctx.fillStyle = '#0a0a15';
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
const entities = this.scene.entities.buffer;
|
||||
for (const entity of entities) {
|
||||
const pos = entity.getComponent(PositionComponent);
|
||||
const render = entity.getComponent(RenderableComponent);
|
||||
if (pos && render) {
|
||||
this.ctx.fillStyle = render.color;
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(pos.x, pos.y, render.radius, 0, Math.PI * 2);
|
||||
this.ctx.fill();
|
||||
const [pos, render] = this.getComponents(entity, PositionComponent, RenderableComponent);
|
||||
|
||||
this.ctx.fillStyle = 'white';
|
||||
this.ctx.font = '10px Arial';
|
||||
this.ctx.textAlign = 'center';
|
||||
this.ctx.fillText(entity.name, pos.x, pos.y - render.radius - 5);
|
||||
}
|
||||
this.ctx.fillStyle = render.color;
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(pos.x, pos.y, render.radius, 0, Math.PI * 2);
|
||||
this.ctx.fill();
|
||||
|
||||
this.ctx.fillStyle = 'white';
|
||||
this.ctx.font = '10px Arial';
|
||||
this.ctx.textAlign = 'center';
|
||||
this.ctx.fillText(entity.name, pos.x, pos.y - render.radius - 5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
ECSComponent,
|
||||
Entity,
|
||||
EntitySystem,
|
||||
Matcher,
|
||||
Serializable,
|
||||
Serialize,
|
||||
SerializeAsMap
|
||||
@@ -61,19 +62,20 @@ class PlayerComponent extends Component {
|
||||
|
||||
// ===== 系统定义 =====
|
||||
class MovementSystem extends EntitySystem {
|
||||
update() {
|
||||
const entities = this.scene.entities.buffer;
|
||||
for (const entity of entities) {
|
||||
const pos = entity.getComponent(PositionComponent);
|
||||
const vel = entity.getComponent(VelocityComponent);
|
||||
if (pos && vel) {
|
||||
pos.x += vel.vx;
|
||||
pos.y += vel.vy;
|
||||
constructor() {
|
||||
super(Matcher.all(PositionComponent, VelocityComponent));
|
||||
}
|
||||
|
||||
// 边界反弹
|
||||
if (pos.x < 0 || pos.x > 1200) vel.vx *= -1;
|
||||
if (pos.y < 0 || pos.y > 600) vel.vy *= -1;
|
||||
}
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const [pos, vel] = this.getComponents(entity, PositionComponent, VelocityComponent);
|
||||
|
||||
pos.x += vel.vx;
|
||||
pos.y += vel.vy;
|
||||
|
||||
// 边界反弹
|
||||
if (pos.x < 0 || pos.x > 1200) vel.vx *= -1;
|
||||
if (pos.y < 0 || pos.y > 600) vel.vy *= -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,35 +85,32 @@ class RenderSystem extends EntitySystem {
|
||||
private ctx: CanvasRenderingContext2D;
|
||||
|
||||
constructor(canvas: HTMLCanvasElement) {
|
||||
super();
|
||||
super(Matcher.all(PositionComponent, RenderableComponent));
|
||||
this.canvas = canvas;
|
||||
this.ctx = canvas.getContext('2d')!;
|
||||
}
|
||||
|
||||
update() {
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
// 清空画布
|
||||
this.ctx.fillStyle = '#0a0a15';
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
// 渲染所有实体
|
||||
const entities = this.scene.entities.buffer;
|
||||
for (const entity of entities) {
|
||||
const pos = entity.getComponent(PositionComponent);
|
||||
const render = entity.getComponent(RenderableComponent);
|
||||
if (pos && render) {
|
||||
this.ctx.fillStyle = render.color;
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(pos.x, pos.y, render.radius, 0, Math.PI * 2);
|
||||
this.ctx.fill();
|
||||
const [pos, render] = this.getComponents(entity, PositionComponent, RenderableComponent);
|
||||
|
||||
// 如果是玩家,显示名字
|
||||
const player = entity.getComponent(PlayerComponent);
|
||||
if (player) {
|
||||
this.ctx.fillStyle = 'white';
|
||||
this.ctx.font = '12px Arial';
|
||||
this.ctx.textAlign = 'center';
|
||||
this.ctx.fillText(player.name, pos.x, pos.y - render.radius - 5);
|
||||
}
|
||||
this.ctx.fillStyle = render.color;
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(pos.x, pos.y, render.radius, 0, Math.PI * 2);
|
||||
this.ctx.fill();
|
||||
|
||||
// 如果是玩家,显示名字
|
||||
const player = entity.getComponent(PlayerComponent);
|
||||
if (player) {
|
||||
this.ctx.fillStyle = 'white';
|
||||
this.ctx.font = '12px Arial';
|
||||
this.ctx.textAlign = 'center';
|
||||
this.ctx.fillText(player.name, pos.x, pos.y - render.radius - 5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -447,20 +447,18 @@ class RenderSystem extends EntitySystem {
|
||||
private ctx: CanvasRenderingContext2D;
|
||||
|
||||
constructor(canvas: HTMLCanvasElement) {
|
||||
super(Matcher.empty().all(Position, Renderable));
|
||||
super(Matcher.all(Position, Renderable));
|
||||
this.canvas = canvas;
|
||||
this.ctx = canvas.getContext('2d')!;
|
||||
}
|
||||
|
||||
protected override process(entities: Entity[]): void {
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
this.ctx.fillStyle = '#000';
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
for (const entity of entities) {
|
||||
const position = entity.getComponent(Position);
|
||||
const renderable = entity.getComponent(Renderable);
|
||||
|
||||
if (!position || !renderable) continue;
|
||||
const position = this.requireComponent(entity, Position);
|
||||
const renderable = this.requireComponent(entity, Renderable);
|
||||
|
||||
this.ctx.fillStyle = renderable.color;
|
||||
this.ctx.beginPath();
|
||||
@@ -473,15 +471,14 @@ class RenderSystem extends EntitySystem {
|
||||
@ECSSystem('LifetimeSystem')
|
||||
class LifetimeSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(Lifetime));
|
||||
super(Matcher.all(Lifetime));
|
||||
}
|
||||
|
||||
protected override process(entities: Entity[]): void {
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
const deltaTime = Time.deltaTime;
|
||||
|
||||
for (const entity of entities) {
|
||||
const lifetime = entity.getComponent(Lifetime);
|
||||
if (!lifetime) continue;
|
||||
const lifetime = this.requireComponent(entity, Lifetime);
|
||||
|
||||
lifetime.currentAge += deltaTime;
|
||||
if (lifetime.isDead()) {
|
||||
|
||||
10995
package-lock.json
generated
10995
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
28
package.json
28
package.json
@@ -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",
|
||||
|
||||
68
packages/core/.releaserc.json
Normal file
68
packages/core/.releaserc.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@esengine/ecs-framework",
|
||||
"version": "2.1.52",
|
||||
"version": "2.2.8",
|
||||
"description": "用于Laya、Cocos Creator等JavaScript游戏引擎的高性能ECS框架",
|
||||
"main": "bin/index.js",
|
||||
"types": "bin/index.d.ts",
|
||||
@@ -58,7 +58,6 @@
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/msgpack-lite": "^0.1.11",
|
||||
"@types/node": "^20.19.17",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
@@ -78,6 +77,7 @@
|
||||
"directory": "packages/core"
|
||||
},
|
||||
"dependencies": {
|
||||
"msgpack-lite": "^0.1.26"
|
||||
"@msgpack/msgpack": "^3.0.0",
|
||||
"tslib": "^2.8.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,13 @@ module.exports = [
|
||||
})
|
||||
],
|
||||
external,
|
||||
onwarn(warning, warn) {
|
||||
// 忽略 msgpack-lite 的循环依赖警告
|
||||
if (warning.code === 'CIRCULAR_DEPENDENCY' && warning.ids && warning.ids.some(id => id.includes('msgpack-lite'))) {
|
||||
return;
|
||||
}
|
||||
warn(warning);
|
||||
},
|
||||
treeshake: {
|
||||
moduleSideEffects: false,
|
||||
propertyReadSideEffects: false,
|
||||
@@ -78,6 +85,12 @@ module.exports = [
|
||||
})
|
||||
],
|
||||
external,
|
||||
onwarn(warning, warn) {
|
||||
if (warning.code === 'CIRCULAR_DEPENDENCY' && warning.ids && warning.ids.some(id => id.includes('msgpack-lite'))) {
|
||||
return;
|
||||
}
|
||||
warn(warning);
|
||||
},
|
||||
treeshake: {
|
||||
moduleSideEffects: false
|
||||
}
|
||||
@@ -103,6 +116,12 @@ module.exports = [
|
||||
})
|
||||
],
|
||||
external: [],
|
||||
onwarn(warning, warn) {
|
||||
if (warning.code === 'CIRCULAR_DEPENDENCY' && warning.ids && warning.ids.some(id => id.includes('msgpack-lite'))) {
|
||||
return;
|
||||
}
|
||||
warn(warning);
|
||||
},
|
||||
treeshake: {
|
||||
moduleSideEffects: false
|
||||
}
|
||||
@@ -157,6 +176,12 @@ module.exports = [
|
||||
})
|
||||
],
|
||||
external: [],
|
||||
onwarn(warning, warn) {
|
||||
if (warning.code === 'CIRCULAR_DEPENDENCY' && warning.ids && warning.ids.some(id => id.includes('msgpack-lite'))) {
|
||||
return;
|
||||
}
|
||||
warn(warning);
|
||||
},
|
||||
treeshake: {
|
||||
moduleSideEffects: false
|
||||
}
|
||||
|
||||
@@ -1,149 +1,152 @@
|
||||
import { GlobalManager } from './Utils/GlobalManager';
|
||||
import { TimerManager } from './Utils/Timers/TimerManager';
|
||||
import { ITimer } from './Utils/Timers/ITimer';
|
||||
import { Timer } from './Utils/Timers/Timer';
|
||||
import { Time } from './Utils/Time';
|
||||
import { PerformanceMonitor } from './Utils/PerformanceMonitor';
|
||||
import { PoolManager } from './Utils/Pool/PoolManager';
|
||||
import { ECSFluentAPI, createECSAPI } from './ECS/Core/FluentAPI';
|
||||
import { IScene } from './ECS/IScene';
|
||||
import { WorldManager, IWorldManagerConfig } from './ECS/WorldManager';
|
||||
import { DebugManager } from './Utils/Debug';
|
||||
import { ICoreConfig, IECSDebugConfig } from './Types';
|
||||
import { ICoreConfig, IECSDebugConfig, IUpdatable, isUpdatable } from './Types';
|
||||
import { createLogger } from './Utils/Logger';
|
||||
import { SceneManager } from './ECS/SceneManager';
|
||||
import { IScene } from './ECS/IScene';
|
||||
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';
|
||||
|
||||
/**
|
||||
* 游戏引擎核心类
|
||||
*
|
||||
* 负责管理游戏的生命周期、场景切换、全局管理器和定时器系统。
|
||||
* 提供统一的游戏循环管理。
|
||||
*
|
||||
*
|
||||
* 职责:
|
||||
* - 提供全局服务(Timer、Performance、Pool等)
|
||||
* - 管理场景生命周期(内置SceneManager)
|
||||
* - 管理全局管理器的生命周期
|
||||
* - 提供统一的游戏循环更新入口
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 创建核心实例
|
||||
* const core = Core.create(true);
|
||||
*
|
||||
* // 设置场景
|
||||
* Core.scene = new MyScene();
|
||||
*
|
||||
* // 在游戏循环中更新(Laya引擎示例)
|
||||
* Laya.timer.frameLoop(1, this, () => {
|
||||
* const deltaTime = Laya.timer.delta / 1000;
|
||||
* // 初始化并设置场景
|
||||
* Core.create({ debug: true });
|
||||
* Core.setScene(new GameScene());
|
||||
*
|
||||
* // 游戏循环(自动更新全局服务和场景)
|
||||
* function gameLoop(deltaTime: number) {
|
||||
* Core.update(deltaTime);
|
||||
* });
|
||||
*
|
||||
* // 调度定时器
|
||||
* }
|
||||
*
|
||||
* // 使用定时器
|
||||
* Core.schedule(1.0, false, null, (timer) => {
|
||||
* Core._logger.info("1秒后执行");
|
||||
* console.log("1秒后执行");
|
||||
* });
|
||||
*
|
||||
* // 切换场景
|
||||
* Core.loadScene(new MenuScene()); // 延迟切换
|
||||
* Core.setScene(new GameScene()); // 立即切换
|
||||
*
|
||||
* // 获取当前场景
|
||||
* const currentScene = Core.scene;
|
||||
* ```
|
||||
*/
|
||||
export class Core {
|
||||
/**
|
||||
* 游戏暂停状态
|
||||
*
|
||||
*
|
||||
* 当设置为true时,游戏循环将暂停执行。
|
||||
*/
|
||||
public static paused = false;
|
||||
|
||||
/**
|
||||
* 默认World ID
|
||||
*
|
||||
* 用于单Scene模式的默认World标识
|
||||
*/
|
||||
private static readonly DEFAULT_WORLD_ID = '__default__';
|
||||
|
||||
/**
|
||||
* 默认Scene ID
|
||||
*
|
||||
* 用于单Scene模式的默认Scene标识
|
||||
*/
|
||||
private static readonly DEFAULT_SCENE_ID = '__main__';
|
||||
|
||||
/**
|
||||
* 全局核心实例
|
||||
*
|
||||
* 可能为null表示Core尚未初始化或已被销毁
|
||||
*/
|
||||
private static _instance: Core;
|
||||
private static _instance: Core | null = null;
|
||||
|
||||
/**
|
||||
* Core专用日志器
|
||||
*/
|
||||
private static _logger = createLogger('Core');
|
||||
|
||||
|
||||
/**
|
||||
* 实体系统启用状态
|
||||
*
|
||||
*
|
||||
* 控制是否启用ECS实体系统功能。
|
||||
*/
|
||||
public static entitySystemsEnabled: boolean;
|
||||
|
||||
|
||||
/**
|
||||
* 调试模式标志
|
||||
*
|
||||
*
|
||||
* 在调试模式下会启用额外的性能监控和错误检查。
|
||||
*/
|
||||
public readonly debug: boolean;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 全局管理器集合
|
||||
*
|
||||
* 存储所有注册的全局管理器实例。
|
||||
* 服务容器
|
||||
*
|
||||
* 管理所有服务的注册、解析和生命周期。
|
||||
*/
|
||||
public _globalManagers: GlobalManager[] = [];
|
||||
|
||||
private _serviceContainer: ServiceContainer;
|
||||
|
||||
/**
|
||||
* 定时器管理器
|
||||
*
|
||||
*
|
||||
* 负责管理所有的游戏定时器。
|
||||
*/
|
||||
public _timerManager: TimerManager;
|
||||
|
||||
/**
|
||||
* 性能监控器
|
||||
*
|
||||
*
|
||||
* 监控游戏性能并提供优化建议。
|
||||
*/
|
||||
public _performanceMonitor: PerformanceMonitor;
|
||||
|
||||
/**
|
||||
* 对象池管理器
|
||||
*
|
||||
*
|
||||
* 管理所有对象池的生命周期。
|
||||
*/
|
||||
public _poolManager: PoolManager;
|
||||
|
||||
/**
|
||||
* ECS流式API
|
||||
*
|
||||
* 提供便捷的ECS操作接口。
|
||||
*/
|
||||
public _ecsAPI?: ECSFluentAPI;
|
||||
|
||||
|
||||
/**
|
||||
* 调试管理器
|
||||
*
|
||||
*
|
||||
* 负责收集和发送调试数据。
|
||||
*/
|
||||
public _debugManager?: DebugManager;
|
||||
|
||||
/**
|
||||
* World管理器
|
||||
*
|
||||
* 管理多个World实例,支持多房间/多世界架构。
|
||||
* 场景管理器
|
||||
*
|
||||
* 管理当前场景的生命周期。
|
||||
*/
|
||||
public _worldManager?: WorldManager;
|
||||
private _sceneManager: SceneManager;
|
||||
|
||||
/**
|
||||
* World管理器
|
||||
*
|
||||
* 管理多个独立的World实例(可选)。
|
||||
*/
|
||||
private _worldManager: WorldManager;
|
||||
|
||||
/**
|
||||
* 插件管理器
|
||||
*
|
||||
* 管理所有插件的生命周期。
|
||||
*/
|
||||
private _pluginManager: PluginManager;
|
||||
|
||||
/**
|
||||
* Core配置
|
||||
*/
|
||||
private _config: ICoreConfig;
|
||||
|
||||
|
||||
/**
|
||||
* 创建核心实例
|
||||
*
|
||||
*
|
||||
* @param config - Core配置对象
|
||||
*/
|
||||
private constructor(config: ICoreConfig = {}) {
|
||||
@@ -156,37 +159,68 @@ export class Core {
|
||||
...config
|
||||
};
|
||||
|
||||
// 初始化服务容器
|
||||
this._serviceContainer = new ServiceContainer();
|
||||
|
||||
// 初始化管理器
|
||||
// 初始化定时器管理器
|
||||
this._timerManager = new TimerManager();
|
||||
Core.registerGlobalManager(this._timerManager);
|
||||
this._serviceContainer.registerInstance(TimerManager, this._timerManager);
|
||||
|
||||
// 初始化性能监控器
|
||||
this._performanceMonitor = PerformanceMonitor.instance;
|
||||
|
||||
this._performanceMonitor = new PerformanceMonitor();
|
||||
this._serviceContainer.registerInstance(PerformanceMonitor, this._performanceMonitor);
|
||||
|
||||
// 在调试模式下启用性能监控
|
||||
if (this._config.debug) {
|
||||
this._performanceMonitor.enable();
|
||||
}
|
||||
|
||||
// 初始化对象池管理器
|
||||
this._poolManager = PoolManager.getInstance();
|
||||
|
||||
this._poolManager = new PoolManager();
|
||||
this._serviceContainer.registerInstance(PoolManager, this._poolManager);
|
||||
|
||||
// 初始化场景管理器
|
||||
this._sceneManager = new SceneManager(this._performanceMonitor);
|
||||
this._serviceContainer.registerInstance(SceneManager, this._sceneManager);
|
||||
|
||||
// 设置场景切换回调,通知调试管理器
|
||||
this._sceneManager.setSceneChangedCallback(() => {
|
||||
if (this._debugManager) {
|
||||
this._debugManager.onSceneChanged();
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化World管理器
|
||||
this._worldManager = new WorldManager(this._config.worldManagerConfig);
|
||||
this._serviceContainer.registerInstance(WorldManager, this._worldManager);
|
||||
|
||||
// 初始化插件管理器
|
||||
this._pluginManager = new PluginManager();
|
||||
this._pluginManager.initialize(this, this._serviceContainer);
|
||||
this._serviceContainer.registerInstance(PluginManager, this._pluginManager);
|
||||
|
||||
Core.entitySystemsEnabled = this._config.enableEntitySystems ?? true;
|
||||
this.debug = this._config.debug ?? true;
|
||||
|
||||
// 初始化调试管理器
|
||||
if (this._config.debugConfig?.enabled) {
|
||||
this._debugManager = new DebugManager(this, this._config.debugConfig);
|
||||
}
|
||||
const configService = new DebugConfigService();
|
||||
configService.setConfig(this._config.debugConfig);
|
||||
this._serviceContainer.registerInstance(DebugConfigService, configService);
|
||||
|
||||
this._serviceContainer.registerSingleton(DebugManager, (c) =>
|
||||
createInstance(DebugManager, c)
|
||||
);
|
||||
|
||||
this._debugManager = this._serviceContainer.resolve(DebugManager);
|
||||
}
|
||||
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取核心实例
|
||||
*
|
||||
*
|
||||
* @returns 全局核心实例
|
||||
*/
|
||||
public static get Instance() {
|
||||
@@ -194,104 +228,196 @@ export class Core {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前活动的场景(属性访问器)
|
||||
*
|
||||
* @returns 当前场景实例,如果没有则返回null
|
||||
* 获取服务容器
|
||||
*
|
||||
* 用于注册和解析自定义服务。
|
||||
*
|
||||
* @returns 服务容器实例
|
||||
* @throws 如果Core实例未创建
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 注册自定义服务
|
||||
* Core.services.registerSingleton(MyService);
|
||||
*
|
||||
* // 解析服务
|
||||
* const myService = Core.services.resolve(MyService);
|
||||
* ```
|
||||
*/
|
||||
public static get scene(): IScene | null {
|
||||
return this.getScene();
|
||||
public static get services(): ServiceContainer {
|
||||
if (!this._instance) {
|
||||
throw new Error('Core实例未创建,请先调用Core.create()');
|
||||
}
|
||||
return this._instance._serviceContainer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前活动的场景(方法调用)
|
||||
*
|
||||
* @returns 当前场景实例,如果没有则返回null
|
||||
* 获取World管理器
|
||||
*
|
||||
* 用于管理多个独立的World实例(高级用户)。
|
||||
*
|
||||
* @returns WorldManager实例
|
||||
* @throws 如果Core实例未创建
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 创建多个游戏房间
|
||||
* const wm = Core.worldManager;
|
||||
* const room1 = wm.createWorld('room_001');
|
||||
* room1.createScene('game', new GameScene());
|
||||
* room1.start();
|
||||
* ```
|
||||
*/
|
||||
public static getScene<T extends IScene>(): T | null {
|
||||
public static get worldManager(): WorldManager {
|
||||
if (!this._instance) {
|
||||
return null;
|
||||
throw new Error('Core实例未创建,请先调用Core.create()');
|
||||
}
|
||||
|
||||
// 确保默认World存在
|
||||
this._instance.ensureDefaultWorld();
|
||||
|
||||
const defaultWorld = this._instance._worldManager!.getWorld(this.DEFAULT_WORLD_ID);
|
||||
return defaultWorld?.getScene(this.DEFAULT_SCENE_ID) as T || null;
|
||||
return this._instance._worldManager;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 设置当前场景
|
||||
*
|
||||
* @param scene - 要设置的场景实例
|
||||
* @returns 设置的场景实例,便于链式调用
|
||||
*/
|
||||
public static setScene<T extends IScene>(scene: T): T {
|
||||
if (!this._instance) {
|
||||
throw new Error("Core实例未创建,请先调用Core.create()");
|
||||
}
|
||||
|
||||
// 确保默认World存在
|
||||
this._instance.ensureDefaultWorld();
|
||||
|
||||
const defaultWorld = this._instance._worldManager!.getWorld(this.DEFAULT_WORLD_ID)!;
|
||||
|
||||
// 移除旧的主Scene(如果存在)
|
||||
if (defaultWorld.getScene(this.DEFAULT_SCENE_ID)) {
|
||||
defaultWorld.removeScene(this.DEFAULT_SCENE_ID);
|
||||
}
|
||||
|
||||
// 添加新Scene到默认World
|
||||
defaultWorld.createScene(this.DEFAULT_SCENE_ID, scene);
|
||||
defaultWorld.setSceneActive(this.DEFAULT_SCENE_ID, true);
|
||||
|
||||
// 触发场景切换回调
|
||||
this._instance.onSceneChanged();
|
||||
|
||||
return scene;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 创建Core实例
|
||||
*
|
||||
*
|
||||
* 如果实例已存在,则返回现有实例。
|
||||
*
|
||||
*
|
||||
* @param config - Core配置,也可以直接传入boolean表示debug模式(向后兼容)
|
||||
* @returns Core实例
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 方式1:使用配置对象
|
||||
* Core.create({
|
||||
* debug: true,
|
||||
* enableEntitySystems: true,
|
||||
* debugConfig: {
|
||||
* enabled: true,
|
||||
* websocketUrl: 'ws://localhost:9229'
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* // 方式2:简单模式(向后兼容)
|
||||
* Core.create(true); // debug = true
|
||||
* ```
|
||||
*/
|
||||
public static create(config: ICoreConfig | boolean = true): Core {
|
||||
if (this._instance == null) {
|
||||
// 向后兼容:如果传入boolean,转换为配置对象
|
||||
const coreConfig: ICoreConfig = typeof config === 'boolean'
|
||||
const coreConfig: ICoreConfig = typeof config === 'boolean'
|
||||
? { debug: config, enableEntitySystems: true }
|
||||
: config;
|
||||
this._instance = new Core(coreConfig);
|
||||
} else {
|
||||
this._logger.warn('Core实例已创建,返回现有实例');
|
||||
}
|
||||
return this._instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新游戏逻辑
|
||||
*
|
||||
* 此方法应该在游戏引擎的更新循环中调用。
|
||||
*
|
||||
* @param deltaTime - 外部引擎提供的帧时间间隔(秒)
|
||||
*
|
||||
* 设置当前场景
|
||||
*
|
||||
* @param scene - 要设置的场景
|
||||
* @returns 设置的场景实例
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Laya引擎
|
||||
* Core.create({ debug: true });
|
||||
*
|
||||
* // 创建并设置场景
|
||||
* const gameScene = new GameScene();
|
||||
* Core.setScene(gameScene);
|
||||
* ```
|
||||
*/
|
||||
public static setScene<T extends IScene>(scene: T): T {
|
||||
if (!this._instance) {
|
||||
Core._logger.warn("Core实例未创建,请先调用Core.create()");
|
||||
throw new Error("Core实例未创建");
|
||||
}
|
||||
|
||||
return this._instance._sceneManager.setScene(scene);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前场景
|
||||
*
|
||||
* @returns 当前场景,如果没有场景则返回null
|
||||
*/
|
||||
public static get scene(): IScene | null {
|
||||
if (!this._instance) {
|
||||
return null;
|
||||
}
|
||||
return this._instance._sceneManager.currentScene;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取ECS流式API
|
||||
*
|
||||
* @returns ECS API实例,如果当前没有场景则返回null
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 使用流式API创建实体
|
||||
* const player = Core.ecsAPI?.createEntity('Player')
|
||||
* .addComponent(Position, 100, 100)
|
||||
* .addComponent(Velocity, 50, 0);
|
||||
*
|
||||
* // 查询实体
|
||||
* const enemies = Core.ecsAPI?.query(Enemy, Transform);
|
||||
*
|
||||
* // 发射事件
|
||||
* Core.ecsAPI?.emit('game:start', { level: 1 });
|
||||
* ```
|
||||
*/
|
||||
public static get ecsAPI() {
|
||||
if (!this._instance) {
|
||||
return null;
|
||||
}
|
||||
return this._instance._sceneManager.api;
|
||||
}
|
||||
|
||||
/**
|
||||
* 延迟加载场景(下一帧切换)
|
||||
*
|
||||
* @param scene - 要加载的场景
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 延迟切换场景(在下一帧生效)
|
||||
* Core.loadScene(new MenuScene());
|
||||
* ```
|
||||
*/
|
||||
public static loadScene<T extends IScene>(scene: T): void {
|
||||
if (!this._instance) {
|
||||
Core._logger.warn("Core实例未创建,请先调用Core.create()");
|
||||
return;
|
||||
}
|
||||
|
||||
this._instance._sceneManager.loadScene(scene);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新游戏逻辑
|
||||
*
|
||||
* 此方法应该在游戏引擎的更新循环中调用。
|
||||
* 会自动更新全局服务和当前场景。
|
||||
*
|
||||
* @param deltaTime - 外部引擎提供的帧时间间隔(秒)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 初始化
|
||||
* Core.create({ debug: true });
|
||||
* Core.setScene(new GameScene());
|
||||
*
|
||||
* // Laya引擎集成
|
||||
* Laya.timer.frameLoop(1, this, () => {
|
||||
* const deltaTime = Laya.timer.delta / 1000;
|
||||
* Core.update(deltaTime);
|
||||
* Core.update(deltaTime); // 自动更新全局服务和场景
|
||||
* });
|
||||
*
|
||||
* // Cocos Creator
|
||||
*
|
||||
* // Cocos Creator集成
|
||||
* update(deltaTime: number) {
|
||||
* Core.update(deltaTime);
|
||||
* Core.update(deltaTime); // 自动更新全局服务和场景
|
||||
* }
|
||||
*
|
||||
|
||||
* ```
|
||||
*/
|
||||
public static update(deltaTime: number): void {
|
||||
@@ -299,78 +425,49 @@ export class Core {
|
||||
Core._logger.warn("Core实例未创建,请先调用Core.create()");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
this._instance.updateInternal(deltaTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册全局管理器
|
||||
*
|
||||
* 将管理器添加到全局管理器列表中,并启用它。
|
||||
*
|
||||
* @param manager - 要注册的全局管理器
|
||||
*/
|
||||
public static registerGlobalManager(manager: GlobalManager) {
|
||||
this._instance._globalManagers.push(manager);
|
||||
manager.enabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销全局管理器
|
||||
*
|
||||
* 从全局管理器列表中移除管理器,并禁用它。
|
||||
*
|
||||
* @param manager - 要注销的全局管理器
|
||||
*/
|
||||
public static unregisterGlobalManager(manager: GlobalManager) {
|
||||
this._instance._globalManagers.splice(this._instance._globalManagers.indexOf(manager), 1);
|
||||
manager.enabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定类型的全局管理器
|
||||
*
|
||||
* @param type - 管理器类型构造函数
|
||||
* @returns 管理器实例,如果未找到则返回null
|
||||
*/
|
||||
public static getGlobalManager<T extends GlobalManager>(type: new (...args: unknown[]) => T): T | null {
|
||||
for (const manager of this._instance._globalManagers) {
|
||||
if (manager instanceof type)
|
||||
return manager as T;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调度定时器
|
||||
*
|
||||
*
|
||||
* 创建一个定时器,在指定时间后执行回调函数。
|
||||
*
|
||||
*
|
||||
* @param timeInSeconds - 延迟时间(秒)
|
||||
* @param repeats - 是否重复执行,默认为false
|
||||
* @param context - 回调函数的上下文,默认为null
|
||||
* @param onTime - 定时器触发时的回调函数
|
||||
* @returns 创建的定时器实例
|
||||
* @throws 如果Core实例未创建或onTime回调未提供
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 一次性定时器
|
||||
* Core.schedule(1.0, false, null, (timer) => {
|
||||
* console.log("1秒后执行一次");
|
||||
* });
|
||||
*
|
||||
* // 重复定时器
|
||||
* Core.schedule(0.5, true, null, (timer) => {
|
||||
* console.log("每0.5秒执行一次");
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
public static schedule<TContext = unknown>(timeInSeconds: number, repeats: boolean = false, context?: TContext, onTime?: (timer: ITimer<TContext>) => void): Timer<TContext> {
|
||||
if (!this._instance) {
|
||||
throw new Error('Core实例未创建,请先调用Core.create()');
|
||||
}
|
||||
if (!onTime) {
|
||||
throw new Error('onTime callback is required');
|
||||
}
|
||||
return this._instance._timerManager.schedule(timeInSeconds, repeats, context as TContext, onTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取ECS流式API
|
||||
*
|
||||
* @returns ECS API实例,如果未初始化则返回null
|
||||
*/
|
||||
public static get ecsAPI(): ECSFluentAPI | null {
|
||||
return this._instance?._ecsAPI || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用调试功能
|
||||
*
|
||||
*
|
||||
* @param config 调试配置
|
||||
*/
|
||||
public static enableDebug(config: IECSDebugConfig): void {
|
||||
@@ -382,7 +479,15 @@ export class Core {
|
||||
if (this._instance._debugManager) {
|
||||
this._instance._debugManager.updateConfig(config);
|
||||
} else {
|
||||
this._instance._debugManager = new DebugManager(this._instance, 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._debugManager = this._instance._serviceContainer.resolve(DebugManager);
|
||||
}
|
||||
|
||||
// 更新Core配置
|
||||
@@ -408,7 +513,7 @@ export class Core {
|
||||
|
||||
/**
|
||||
* 获取调试数据
|
||||
*
|
||||
*
|
||||
* @returns 当前调试数据,如果调试未启用则返回null
|
||||
*/
|
||||
public static getDebugData(): unknown {
|
||||
@@ -421,118 +526,114 @@ export class Core {
|
||||
|
||||
/**
|
||||
* 检查调试是否启用
|
||||
*
|
||||
*
|
||||
* @returns 调试状态
|
||||
*/
|
||||
public static get isDebugEnabled(): boolean {
|
||||
return this._instance?._config.debugConfig?.enabled || false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取WorldManager实例
|
||||
* 安装插件
|
||||
*
|
||||
* @param config 可选的WorldManager配置,用于覆盖默认配置
|
||||
* @returns WorldManager实例,如果未初始化则自动创建
|
||||
* @param plugin - 插件实例
|
||||
* @throws 如果Core实例未创建或插件安装失败
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* Core.create({ debug: true });
|
||||
*
|
||||
* // 安装插件
|
||||
* await Core.installPlugin(new MyPlugin());
|
||||
* ```
|
||||
*/
|
||||
public static getWorldManager(config?: Partial<IWorldManagerConfig>): WorldManager {
|
||||
public static async installPlugin(plugin: IPlugin): Promise<void> {
|
||||
if (!this._instance) {
|
||||
throw new Error("Core实例未创建,请先调用Core.create()");
|
||||
throw new Error('Core实例未创建,请先调用Core.create()');
|
||||
}
|
||||
|
||||
if (!this._instance._worldManager) {
|
||||
// 多World模式的配置(用户主动获取WorldManager)
|
||||
const defaultConfig = {
|
||||
maxWorlds: 50,
|
||||
autoCleanup: true,
|
||||
cleanupInterval: 60000,
|
||||
debug: this._instance._config.debug
|
||||
};
|
||||
|
||||
this._instance._worldManager = WorldManager.getInstance({
|
||||
...defaultConfig,
|
||||
...config // 用户传入的配置会覆盖默认配置
|
||||
});
|
||||
}
|
||||
|
||||
return this._instance._worldManager;
|
||||
await this._instance._pluginManager.install(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用World管理
|
||||
* 卸载插件
|
||||
*
|
||||
* 显式启用World功能,用于多房间/多世界架构
|
||||
* @param name - 插件名称
|
||||
* @throws 如果Core实例未创建或插件卸载失败
|
||||
*
|
||||
* @param config 可选的WorldManager配置,用于覆盖默认配置
|
||||
* @example
|
||||
* ```typescript
|
||||
* await Core.uninstallPlugin('my-plugin');
|
||||
* ```
|
||||
*/
|
||||
public static enableWorldManager(config?: Partial<IWorldManagerConfig>): WorldManager {
|
||||
return this.getWorldManager(config);
|
||||
public static async uninstallPlugin(name: string): Promise<void> {
|
||||
if (!this._instance) {
|
||||
throw new Error('Core实例未创建,请先调用Core.create()');
|
||||
}
|
||||
|
||||
await this._instance._pluginManager.uninstall(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保默认World存在
|
||||
*
|
||||
* 内部方法,用于懒初始化默认World
|
||||
* 获取插件实例
|
||||
*
|
||||
* @param name - 插件名称
|
||||
* @returns 插件实例,如果未安装则返回undefined
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const myPlugin = Core.getPlugin('my-plugin');
|
||||
* if (myPlugin) {
|
||||
* console.log(myPlugin.version);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
private ensureDefaultWorld(): void {
|
||||
if (!this._worldManager) {
|
||||
this._worldManager = WorldManager.getInstance({
|
||||
maxWorlds: 1, // 单场景用户只需要1个World
|
||||
autoCleanup: false, // 单场景不需要自动清理
|
||||
cleanupInterval: 0, // 禁用清理定时器
|
||||
debug: this._config.debug
|
||||
});
|
||||
public static getPlugin(name: string): IPlugin | undefined {
|
||||
if (!this._instance) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// 检查默认World是否存在
|
||||
if (!this._worldManager.getWorld(Core.DEFAULT_WORLD_ID)) {
|
||||
this._worldManager.createWorld(Core.DEFAULT_WORLD_ID, {
|
||||
name: 'DefaultWorld',
|
||||
maxScenes: 1,
|
||||
autoCleanup: false
|
||||
});
|
||||
this._worldManager.setWorldActive(Core.DEFAULT_WORLD_ID, true);
|
||||
}
|
||||
return this._instance._pluginManager.getPlugin(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景切换回调
|
||||
*
|
||||
* 在场景切换时调用,用于重置时间系统等。
|
||||
* 检查插件是否已安装
|
||||
*
|
||||
* @param name - 插件名称
|
||||
* @returns 是否已安装
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* if (Core.isPluginInstalled('my-plugin')) {
|
||||
* console.log('Plugin is installed');
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
public onSceneChanged() {
|
||||
Time.sceneChanged();
|
||||
|
||||
// 获取当前Scene(从默认World)
|
||||
const currentScene = Core.getScene();
|
||||
|
||||
// 初始化ECS API(如果场景支持)
|
||||
if (currentScene && currentScene.querySystem && currentScene.eventSystem) {
|
||||
this._ecsAPI = createECSAPI(currentScene, currentScene.querySystem, currentScene.eventSystem);
|
||||
public static isPluginInstalled(name: string): boolean {
|
||||
if (!this._instance) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 延迟调试管理器通知,避免在场景初始化过程中干扰属性
|
||||
if (this._debugManager) {
|
||||
queueMicrotask(() => {
|
||||
this._debugManager?.onSceneChanged();
|
||||
});
|
||||
}
|
||||
return this._instance._pluginManager.isInstalled(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化核心系统
|
||||
*
|
||||
*
|
||||
* 执行核心系统的初始化逻辑。
|
||||
*/
|
||||
protected initialize() {
|
||||
// 核心系统初始化
|
||||
Core._logger.info('Core initialized', {
|
||||
debug: this.debug,
|
||||
entitySystemsEnabled: Core.entitySystemsEnabled,
|
||||
debugEnabled: this._config.debugConfig?.enabled || false
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 内部更新方法
|
||||
*
|
||||
*
|
||||
* @param deltaTime - 帧时间间隔(秒)
|
||||
*/
|
||||
private updateInternal(deltaTime: number): void {
|
||||
@@ -549,44 +650,43 @@ export class Core {
|
||||
this._performanceMonitor.updateFPS(Time.deltaTime);
|
||||
}
|
||||
|
||||
// 更新全局管理器
|
||||
const managersStartTime = this._performanceMonitor.startMonitoring('GlobalManagers.update');
|
||||
for (const globalManager of this._globalManagers) {
|
||||
if (globalManager.enabled)
|
||||
globalManager.update();
|
||||
}
|
||||
this._performanceMonitor.endMonitoring('GlobalManagers.update', managersStartTime, this._globalManagers.length);
|
||||
// 更新所有可更新的服务
|
||||
const servicesStartTime = this._performanceMonitor.startMonitoring('Services.update');
|
||||
this._serviceContainer.updateAll(deltaTime);
|
||||
this._performanceMonitor.endMonitoring('Services.update', servicesStartTime, this._serviceContainer.getUpdatableCount());
|
||||
|
||||
// 更新对象池管理器
|
||||
this._poolManager.update();
|
||||
|
||||
// 更新所有World
|
||||
if (this._worldManager) {
|
||||
const worldsStartTime = this._performanceMonitor.startMonitoring('Worlds.update');
|
||||
const activeWorlds = this._worldManager.getActiveWorlds();
|
||||
let totalWorldEntities = 0;
|
||||
// 更新默认场景(通过 SceneManager)
|
||||
this._sceneManager.update();
|
||||
|
||||
for (const world of activeWorlds) {
|
||||
// 更新World的全局System
|
||||
world.updateGlobalSystems();
|
||||
|
||||
// 更新World中的所有Scene
|
||||
world.updateScenes();
|
||||
|
||||
// 统计实体数量(用于性能监控)
|
||||
const worldStats = world.getStats();
|
||||
totalWorldEntities += worldStats.totalEntities;
|
||||
}
|
||||
|
||||
this._performanceMonitor.endMonitoring('Worlds.update', worldsStartTime, totalWorldEntities);
|
||||
}
|
||||
|
||||
// 更新调试管理器(基于FPS的数据发送)
|
||||
if (this._debugManager) {
|
||||
this._debugManager.onFrameUpdate(deltaTime);
|
||||
}
|
||||
// 更新额外的 WorldManager
|
||||
this._worldManager.updateAll();
|
||||
|
||||
// 结束性能监控
|
||||
this._performanceMonitor.endMonitoring('Core.update', frameStartTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁Core实例
|
||||
*
|
||||
* 清理所有资源,通常在应用程序关闭时调用。
|
||||
*/
|
||||
public static destroy(): void {
|
||||
if (!this._instance) return;
|
||||
|
||||
// 停止调试管理器
|
||||
if (this._instance._debugManager) {
|
||||
this._instance._debugManager.stop();
|
||||
}
|
||||
|
||||
// 清理所有服务
|
||||
this._instance._serviceContainer.clear();
|
||||
|
||||
Core._logger.info('Core destroyed');
|
||||
|
||||
// 清空实例引用,允许重新创建Core实例
|
||||
this._instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
364
packages/core/src/Core/DI/Decorators.ts
Normal file
364
packages/core/src/Core/DI/Decorators.ts
Normal file
@@ -0,0 +1,364 @@
|
||||
/**
|
||||
* 依赖注入装饰器
|
||||
*
|
||||
* 提供 @Injectable、@Inject 和 @Updatable 装饰器,用于标记可注入的类和依赖注入点
|
||||
*/
|
||||
|
||||
import type { ServiceContainer } from '../ServiceContainer';
|
||||
import type { IService, ServiceType } from '../ServiceContainer';
|
||||
|
||||
/**
|
||||
* 依赖注入元数据键
|
||||
*/
|
||||
const INJECTABLE_METADATA_KEY = Symbol('injectable');
|
||||
const INJECT_METADATA_KEY = Symbol('inject');
|
||||
const INJECT_PARAMS_METADATA_KEY = Symbol('inject:params');
|
||||
const UPDATABLE_METADATA_KEY = Symbol('updatable');
|
||||
|
||||
/**
|
||||
* 依赖注入元数据存储
|
||||
*/
|
||||
const injectableMetadata = new WeakMap<any, InjectableMetadata>();
|
||||
const injectMetadata = new WeakMap<any, Map<number, ServiceType<any> | string | symbol>>();
|
||||
const updatableMetadata = new WeakMap<any, UpdatableMetadata>();
|
||||
|
||||
/**
|
||||
* 可注入元数据接口
|
||||
*/
|
||||
export interface InjectableMetadata {
|
||||
/**
|
||||
* 是否可注入
|
||||
*/
|
||||
injectable: boolean;
|
||||
|
||||
/**
|
||||
* 依赖列表
|
||||
*/
|
||||
dependencies: Array<ServiceType<any> | string | symbol>;
|
||||
|
||||
/**
|
||||
* 属性注入映射
|
||||
* key: 属性名, value: 服务类型
|
||||
*/
|
||||
properties?: Map<string | symbol, ServiceType<any>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 可更新元数据接口
|
||||
*/
|
||||
export interface UpdatableMetadata {
|
||||
/**
|
||||
* 是否可更新
|
||||
*/
|
||||
updatable: boolean;
|
||||
|
||||
/**
|
||||
* 更新优先级(数值越小越先执行,默认0)
|
||||
*/
|
||||
priority: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Injectable() 装饰器
|
||||
*
|
||||
* 标记类为可注入的服务,使其可以通过ServiceContainer进行依赖注入
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @Injectable()
|
||||
* class TimeService implements IService {
|
||||
* constructor() {}
|
||||
* dispose() {}
|
||||
* }
|
||||
*
|
||||
* @Injectable()
|
||||
* class PhysicsSystem extends EntitySystem {
|
||||
* constructor(
|
||||
* @Inject(TimeService) private timeService: TimeService
|
||||
* ) {
|
||||
* super();
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function Injectable(): ClassDecorator {
|
||||
return function <T extends Function>(target: T): T {
|
||||
const existing = injectableMetadata.get(target);
|
||||
|
||||
injectableMetadata.set(target, {
|
||||
injectable: true,
|
||||
dependencies: [],
|
||||
properties: existing?.properties
|
||||
});
|
||||
|
||||
return target;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @Updatable() 装饰器
|
||||
*
|
||||
* 标记服务类为可更新的,使其在每帧自动被ServiceContainer调用update方法。
|
||||
* 使用此装饰器的类必须实现IUpdatable接口(包含update方法)。
|
||||
*
|
||||
* @param priority - 更新优先级(数值越小越先执行,默认0)
|
||||
* @throws 如果类没有实现update方法,将在运行时抛出错误
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @Injectable()
|
||||
* @Updatable()
|
||||
* class TimerManager implements IService, IUpdatable {
|
||||
* update(deltaTime?: number) {
|
||||
* // 每帧更新逻辑
|
||||
* }
|
||||
* dispose() {}
|
||||
* }
|
||||
*
|
||||
* // 指定优先级
|
||||
* @Injectable()
|
||||
* @Updatable(10)
|
||||
* class PhysicsManager implements IService, IUpdatable {
|
||||
* update() { }
|
||||
* dispose() {}
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function Updatable(priority: number = 0): ClassDecorator {
|
||||
return function <T extends Function>(target: T): T {
|
||||
// 验证类原型上是否有update方法
|
||||
const prototype = (target as any).prototype;
|
||||
if (!prototype || typeof prototype.update !== 'function') {
|
||||
throw new Error(
|
||||
`@Updatable() decorator requires class ${target.name} to implement IUpdatable interface with update() method. ` +
|
||||
`Please add 'implements IUpdatable' and define update(deltaTime?: number): void method.`
|
||||
);
|
||||
}
|
||||
|
||||
// 标记为可更新
|
||||
updatableMetadata.set(target, {
|
||||
updatable: true,
|
||||
priority
|
||||
});
|
||||
|
||||
return target;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @Inject() 装饰器
|
||||
*
|
||||
* 标记构造函数参数需要注入的服务类型
|
||||
*
|
||||
* @param serviceType 服务类型标识符
|
||||
*/
|
||||
export function Inject(serviceType: ServiceType<any> | string | symbol): ParameterDecorator {
|
||||
return function (target: Object, propertyKey: string | symbol | undefined, parameterIndex: number) {
|
||||
// 获取或创建注入元数据
|
||||
let params = injectMetadata.get(target);
|
||||
if (!params) {
|
||||
params = new Map();
|
||||
injectMetadata.set(target, params);
|
||||
}
|
||||
|
||||
// 记录参数索引和服务类型的映射
|
||||
params.set(parameterIndex, serviceType);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @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);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查类是否标记为可注入
|
||||
*
|
||||
* @param target 目标类
|
||||
* @returns 是否可注入
|
||||
*/
|
||||
export function isInjectable(target: any): boolean {
|
||||
const metadata = injectableMetadata.get(target);
|
||||
return metadata?.injectable ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取类的依赖注入元数据
|
||||
*
|
||||
* @param target 目标类
|
||||
* @returns 依赖注入元数据
|
||||
*/
|
||||
export function getInjectableMetadata(target: any): InjectableMetadata | undefined {
|
||||
return injectableMetadata.get(target);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取构造函数参数的注入元数据
|
||||
*
|
||||
* @param target 目标类
|
||||
* @returns 参数索引到服务类型的映射
|
||||
*/
|
||||
export function getInjectMetadata(target: any): Map<number, ServiceType<any> | string | symbol> {
|
||||
return injectMetadata.get(target) || new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建实例并自动注入依赖
|
||||
*
|
||||
* @param constructor 构造函数
|
||||
* @param container 服务容器
|
||||
* @returns 创建的实例
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const instance = createInstance(MySystem, container);
|
||||
* ```
|
||||
*/
|
||||
export function createInstance<T>(
|
||||
constructor: new (...args: any[]) => T,
|
||||
container: ServiceContainer
|
||||
): T {
|
||||
// 获取参数注入元数据
|
||||
const injectParams = getInjectMetadata(constructor);
|
||||
|
||||
// 解析依赖
|
||||
const dependencies: any[] = [];
|
||||
|
||||
// 获取构造函数参数数量
|
||||
const paramCount = constructor.length;
|
||||
|
||||
for (let i = 0; i < paramCount; i++) {
|
||||
const serviceType = injectParams.get(i);
|
||||
|
||||
if (serviceType) {
|
||||
// 如果有显式的@Inject标记,使用标记的类型
|
||||
if (typeof serviceType === 'string' || typeof serviceType === 'symbol') {
|
||||
// 字符串或Symbol类型的服务标识
|
||||
throw new Error(
|
||||
`String and Symbol service identifiers are not yet supported in constructor injection. ` +
|
||||
`Please use class types for ${constructor.name} parameter ${i}`
|
||||
);
|
||||
} else {
|
||||
// 类类型
|
||||
dependencies.push(container.resolve(serviceType as ServiceType<any>));
|
||||
}
|
||||
} else {
|
||||
// 没有@Inject标记,传入undefined
|
||||
dependencies.push(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建实例
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查类是否标记为可更新
|
||||
*
|
||||
* @param target 目标类
|
||||
* @returns 是否可更新
|
||||
*/
|
||||
export function isUpdatable(target: any): boolean {
|
||||
const metadata = updatableMetadata.get(target);
|
||||
return metadata?.updatable ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取类的可更新元数据
|
||||
*
|
||||
* @param target 目标类
|
||||
* @returns 可更新元数据
|
||||
*/
|
||||
export function getUpdatableMetadata(target: any): UpdatableMetadata | undefined {
|
||||
return updatableMetadata.get(target);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册可注入的服务到容器
|
||||
*
|
||||
* 自动检测@Injectable装饰器并注册服务
|
||||
*
|
||||
* @param container 服务容器
|
||||
* @param serviceType 服务类型
|
||||
* @param singleton 是否注册为单例(默认true)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @Injectable()
|
||||
* class MyService implements IService {
|
||||
* dispose() {}
|
||||
* }
|
||||
*
|
||||
* // 自动注册
|
||||
* registerInjectable(Core.services, MyService);
|
||||
* ```
|
||||
*/
|
||||
export function registerInjectable<T extends IService>(
|
||||
container: ServiceContainer,
|
||||
serviceType: ServiceType<T>,
|
||||
singleton: boolean = true
|
||||
): void {
|
||||
if (!isInjectable(serviceType)) {
|
||||
throw new Error(
|
||||
`${serviceType.name} is not marked as @Injectable(). ` +
|
||||
`Please add @Injectable() decorator to the class.`
|
||||
);
|
||||
}
|
||||
|
||||
// 创建工厂函数,使用createInstance自动解析依赖
|
||||
const factory = (c: ServiceContainer) => createInstance(serviceType, c);
|
||||
|
||||
// 注册到容器
|
||||
if (singleton) {
|
||||
container.registerSingleton(serviceType, factory);
|
||||
} else {
|
||||
container.registerTransient(serviceType, factory);
|
||||
}
|
||||
}
|
||||
22
packages/core/src/Core/DI/index.ts
Normal file
22
packages/core/src/Core/DI/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 依赖注入模块
|
||||
*
|
||||
* 提供装饰器和工具函数,用于实现依赖注入模式
|
||||
*/
|
||||
|
||||
export {
|
||||
Injectable,
|
||||
Inject,
|
||||
InjectProperty,
|
||||
Updatable,
|
||||
isInjectable,
|
||||
getInjectableMetadata,
|
||||
getInjectMetadata,
|
||||
isUpdatable,
|
||||
getUpdatableMetadata,
|
||||
createInstance,
|
||||
injectProperties,
|
||||
registerInjectable
|
||||
} from './Decorators';
|
||||
|
||||
export type { InjectableMetadata, UpdatableMetadata } from './Decorators';
|
||||
124
packages/core/src/Core/Plugin.ts
Normal file
124
packages/core/src/Core/Plugin.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import type { Core } from '../Core';
|
||||
import type { ServiceContainer } from './ServiceContainer';
|
||||
|
||||
/**
|
||||
* 插件状态
|
||||
*/
|
||||
export enum PluginState {
|
||||
/**
|
||||
* 未安装
|
||||
*/
|
||||
NotInstalled = 'not_installed',
|
||||
|
||||
/**
|
||||
* 已安装
|
||||
*/
|
||||
Installed = 'installed',
|
||||
|
||||
/**
|
||||
* 安装失败
|
||||
*/
|
||||
Failed = 'failed'
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件接口
|
||||
*
|
||||
* 所有插件都必须实现此接口。
|
||||
* 插件提供了一种扩展框架功能的标准方式。
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* class MyPlugin implements IPlugin {
|
||||
* readonly name = 'my-plugin';
|
||||
* readonly version = '1.0.0';
|
||||
* readonly dependencies = ['other-plugin'];
|
||||
*
|
||||
* async install(core: Core, services: ServiceContainer) {
|
||||
* // 注册服务
|
||||
* services.registerSingleton(MyService);
|
||||
*
|
||||
* // 添加系统
|
||||
* const world = core.getWorld();
|
||||
* if (world) {
|
||||
* world.addSystem(new MySystem());
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* async uninstall() {
|
||||
* // 清理资源
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export interface IPlugin {
|
||||
/**
|
||||
* 插件唯一名称
|
||||
*
|
||||
* 用于依赖解析和插件管理。
|
||||
*/
|
||||
readonly name: string;
|
||||
|
||||
/**
|
||||
* 插件版本
|
||||
*
|
||||
* 遵循语义化版本规范 (semver)。
|
||||
*/
|
||||
readonly version: string;
|
||||
|
||||
/**
|
||||
* 依赖的其他插件名称列表
|
||||
*
|
||||
* 这些插件必须在当前插件之前安装。
|
||||
*/
|
||||
readonly dependencies?: readonly string[];
|
||||
|
||||
/**
|
||||
* 安装插件
|
||||
*
|
||||
* 在此方法中初始化插件,注册服务、系统等。
|
||||
* 可以是同步或异步的。
|
||||
*
|
||||
* @param core - Core实例,用于访问World等
|
||||
* @param services - 服务容器,用于注册服务
|
||||
*/
|
||||
install(core: Core, services: ServiceContainer): void | Promise<void>;
|
||||
|
||||
/**
|
||||
* 卸载插件
|
||||
*
|
||||
* 清理插件占用的资源。
|
||||
* 可以是同步或异步的。
|
||||
*/
|
||||
uninstall(): void | Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件元数据
|
||||
*/
|
||||
export interface IPluginMetadata {
|
||||
/**
|
||||
* 插件名称
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* 插件版本
|
||||
*/
|
||||
version: string;
|
||||
|
||||
/**
|
||||
* 插件状态
|
||||
*/
|
||||
state: PluginState;
|
||||
|
||||
/**
|
||||
* 安装时间戳
|
||||
*/
|
||||
installedAt?: number;
|
||||
|
||||
/**
|
||||
* 错误信息(如果安装失败)
|
||||
*/
|
||||
error?: string;
|
||||
}
|
||||
266
packages/core/src/Core/PluginManager.ts
Normal file
266
packages/core/src/Core/PluginManager.ts
Normal file
@@ -0,0 +1,266 @@
|
||||
import { IPlugin, IPluginMetadata, PluginState } from './Plugin';
|
||||
import type { IService } from './ServiceContainer';
|
||||
import type { Core } from '../Core';
|
||||
import type { ServiceContainer } from './ServiceContainer';
|
||||
import { createLogger } from '../Utils/Logger';
|
||||
|
||||
const logger = createLogger('PluginManager');
|
||||
|
||||
/**
|
||||
* 插件管理器
|
||||
*
|
||||
* 负责插件的注册、安装、卸载和生命周期管理。
|
||||
* 支持依赖检查和异步加载。
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const core = Core.create();
|
||||
* const pluginManager = core.getService(PluginManager);
|
||||
*
|
||||
* // 注册插件
|
||||
* await pluginManager.install(new MyPlugin());
|
||||
*
|
||||
* // 查询插件
|
||||
* const plugin = pluginManager.getPlugin('my-plugin');
|
||||
*
|
||||
* // 卸载插件
|
||||
* await pluginManager.uninstall('my-plugin');
|
||||
* ```
|
||||
*/
|
||||
export class PluginManager implements IService {
|
||||
/**
|
||||
* 已安装的插件
|
||||
*/
|
||||
private _plugins: Map<string, IPlugin> = new Map();
|
||||
|
||||
/**
|
||||
* 插件元数据
|
||||
*/
|
||||
private _metadata: Map<string, IPluginMetadata> = new Map();
|
||||
|
||||
/**
|
||||
* Core实例引用
|
||||
*/
|
||||
private _core: Core | null = null;
|
||||
|
||||
/**
|
||||
* 服务容器引用
|
||||
*/
|
||||
private _services: ServiceContainer | null = null;
|
||||
|
||||
/**
|
||||
* 初始化插件管理器
|
||||
*
|
||||
* @param core - Core实例
|
||||
* @param services - 服务容器
|
||||
*/
|
||||
public initialize(core: Core, services: ServiceContainer): void {
|
||||
this._core = core;
|
||||
this._services = services;
|
||||
logger.info('PluginManager initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* 安装插件
|
||||
*
|
||||
* 会自动检查依赖并按正确顺序安装。
|
||||
*
|
||||
* @param plugin - 插件实例
|
||||
* @throws 如果依赖检查失败或安装失败
|
||||
*/
|
||||
public async install(plugin: IPlugin): Promise<void> {
|
||||
if (!this._core || !this._services) {
|
||||
throw new Error('PluginManager not initialized. Call initialize() first.');
|
||||
}
|
||||
|
||||
// 检查是否已安装
|
||||
if (this._plugins.has(plugin.name)) {
|
||||
logger.warn(`Plugin ${plugin.name} is already installed`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查依赖
|
||||
if (plugin.dependencies && plugin.dependencies.length > 0) {
|
||||
this._checkDependencies(plugin);
|
||||
}
|
||||
|
||||
// 创建元数据
|
||||
const metadata: IPluginMetadata = {
|
||||
name: plugin.name,
|
||||
version: plugin.version,
|
||||
state: PluginState.NotInstalled,
|
||||
installedAt: Date.now()
|
||||
};
|
||||
|
||||
this._metadata.set(plugin.name, metadata);
|
||||
|
||||
try {
|
||||
// 调用插件的安装方法
|
||||
logger.info(`Installing plugin: ${plugin.name} v${plugin.version}`);
|
||||
await plugin.install(this._core, this._services);
|
||||
|
||||
// 标记为已安装
|
||||
this._plugins.set(plugin.name, plugin);
|
||||
metadata.state = PluginState.Installed;
|
||||
|
||||
logger.info(`Plugin ${plugin.name} installed successfully`);
|
||||
} catch (error) {
|
||||
// 安装失败
|
||||
metadata.state = PluginState.Failed;
|
||||
metadata.error = error instanceof Error ? error.message : String(error);
|
||||
|
||||
logger.error(`Failed to install plugin ${plugin.name}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 卸载插件
|
||||
*
|
||||
* @param name - 插件名称
|
||||
* @throws 如果插件未安装或卸载失败
|
||||
*/
|
||||
public async uninstall(name: string): Promise<void> {
|
||||
const plugin = this._plugins.get(name);
|
||||
if (!plugin) {
|
||||
throw new Error(`Plugin ${name} is not installed`);
|
||||
}
|
||||
|
||||
// 检查是否有其他插件依赖此插件
|
||||
this._checkDependents(name);
|
||||
|
||||
try {
|
||||
logger.info(`Uninstalling plugin: ${name}`);
|
||||
await plugin.uninstall();
|
||||
|
||||
// 从注册表中移除
|
||||
this._plugins.delete(name);
|
||||
this._metadata.delete(name);
|
||||
|
||||
logger.info(`Plugin ${name} uninstalled successfully`);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to uninstall plugin ${name}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取插件实例
|
||||
*
|
||||
* @param name - 插件名称
|
||||
* @returns 插件实例,如果未安装则返回undefined
|
||||
*/
|
||||
public getPlugin(name: string): IPlugin | undefined {
|
||||
return this._plugins.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取插件元数据
|
||||
*
|
||||
* @param name - 插件名称
|
||||
* @returns 插件元数据,如果未安装则返回undefined
|
||||
*/
|
||||
public getMetadata(name: string): IPluginMetadata | undefined {
|
||||
return this._metadata.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有已安装的插件
|
||||
*
|
||||
* @returns 插件列表
|
||||
*/
|
||||
public getAllPlugins(): IPlugin[] {
|
||||
return Array.from(this._plugins.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有插件元数据
|
||||
*
|
||||
* @returns 元数据列表
|
||||
*/
|
||||
public getAllMetadata(): IPluginMetadata[] {
|
||||
return Array.from(this._metadata.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查插件是否已安装
|
||||
*
|
||||
* @param name - 插件名称
|
||||
* @returns 是否已安装
|
||||
*/
|
||||
public isInstalled(name: string): boolean {
|
||||
return this._plugins.has(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查插件依赖
|
||||
*
|
||||
* @param plugin - 插件实例
|
||||
* @throws 如果依赖未满足
|
||||
*/
|
||||
private _checkDependencies(plugin: IPlugin): void {
|
||||
if (!plugin.dependencies) {
|
||||
return;
|
||||
}
|
||||
|
||||
const missingDeps: string[] = [];
|
||||
|
||||
for (const dep of plugin.dependencies) {
|
||||
if (!this._plugins.has(dep)) {
|
||||
missingDeps.push(dep);
|
||||
}
|
||||
}
|
||||
|
||||
if (missingDeps.length > 0) {
|
||||
throw new Error(
|
||||
`Plugin ${plugin.name} has unmet dependencies: ${missingDeps.join(', ')}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有其他插件依赖指定插件
|
||||
*
|
||||
* @param name - 插件名称
|
||||
* @throws 如果有其他插件依赖此插件
|
||||
*/
|
||||
private _checkDependents(name: string): void {
|
||||
const dependents: string[] = [];
|
||||
|
||||
for (const plugin of this._plugins.values()) {
|
||||
if (plugin.dependencies && plugin.dependencies.includes(name)) {
|
||||
dependents.push(plugin.name);
|
||||
}
|
||||
}
|
||||
|
||||
if (dependents.length > 0) {
|
||||
throw new Error(
|
||||
`Cannot uninstall plugin ${name}: it is required by ${dependents.join(', ')}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源
|
||||
*/
|
||||
public dispose(): void {
|
||||
// 卸载所有插件(逆序,先卸载依赖项)
|
||||
const plugins = Array.from(this._plugins.values()).reverse();
|
||||
|
||||
for (const plugin of plugins) {
|
||||
try {
|
||||
logger.info(`Disposing plugin: ${plugin.name}`);
|
||||
plugin.uninstall();
|
||||
} catch (error) {
|
||||
logger.error(`Error disposing plugin ${plugin.name}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
this._plugins.clear();
|
||||
this._metadata.clear();
|
||||
this._core = null;
|
||||
this._services = null;
|
||||
|
||||
logger.info('PluginManager disposed');
|
||||
}
|
||||
}
|
||||
423
packages/core/src/Core/ServiceContainer.ts
Normal file
423
packages/core/src/Core/ServiceContainer.ts
Normal file
@@ -0,0 +1,423 @@
|
||||
import { createLogger } from '../Utils/Logger';
|
||||
import { isUpdatable as checkUpdatable, getUpdatableMetadata } from './DI';
|
||||
|
||||
const logger = createLogger('ServiceContainer');
|
||||
|
||||
/**
|
||||
* 服务基础接口
|
||||
* 所有通过 ServiceContainer 管理的服务都应该实现此接口
|
||||
*/
|
||||
export interface IService {
|
||||
/**
|
||||
* 释放服务占用的资源
|
||||
* 当服务被注销或容器被清空时调用
|
||||
*/
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务类型
|
||||
*
|
||||
* 支持任意构造函数签名,以便与依赖注入装饰器配合使用
|
||||
*/
|
||||
export type ServiceType<T extends IService> = new (...args: any[]) => T;
|
||||
|
||||
/**
|
||||
* 服务生命周期
|
||||
*/
|
||||
export enum ServiceLifetime {
|
||||
/**
|
||||
* 单例模式 - 整个应用生命周期内只有一个实例
|
||||
*/
|
||||
Singleton = 'singleton',
|
||||
|
||||
/**
|
||||
* 瞬时模式 - 每次请求都创建新实例
|
||||
*/
|
||||
Transient = 'transient'
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务注册信息
|
||||
*/
|
||||
interface ServiceRegistration<T extends IService> {
|
||||
/**
|
||||
* 服务类型
|
||||
*/
|
||||
type: ServiceType<T>;
|
||||
|
||||
/**
|
||||
* 服务实例(单例模式)
|
||||
*/
|
||||
instance?: T;
|
||||
|
||||
/**
|
||||
* 工厂函数
|
||||
*/
|
||||
factory?: (container: ServiceContainer) => T;
|
||||
|
||||
/**
|
||||
* 生命周期
|
||||
*/
|
||||
lifetime: ServiceLifetime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务容器
|
||||
*
|
||||
* 负责管理所有服务的注册、解析和生命周期。
|
||||
* 支持依赖注入和服务定位模式。
|
||||
*
|
||||
* 特点:
|
||||
* - 单例和瞬时两种生命周期
|
||||
* - 支持工厂函数注册
|
||||
* - 支持实例注册
|
||||
* - 类型安全的服务解析
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const container = new ServiceContainer();
|
||||
*
|
||||
* // 注册单例服务
|
||||
* container.registerSingleton(TimerManager);
|
||||
*
|
||||
* // 注册工厂函数
|
||||
* container.registerSingleton(Logger, (c) => createLogger('App'));
|
||||
*
|
||||
* // 注册实例
|
||||
* container.registerInstance(Config, new Config());
|
||||
*
|
||||
* // 解析服务
|
||||
* const timer = container.resolve(TimerManager);
|
||||
* ```
|
||||
*/
|
||||
export class ServiceContainer {
|
||||
/**
|
||||
* 服务注册表
|
||||
*/
|
||||
private _services: Map<ServiceType<IService>, ServiceRegistration<IService>> = new Map();
|
||||
|
||||
/**
|
||||
* 正在解析的服务栈(用于循环依赖检测)
|
||||
*/
|
||||
private _resolving: Set<ServiceType<IService>> = new Set();
|
||||
|
||||
/**
|
||||
* 可更新的服务列表
|
||||
*
|
||||
* 自动收集所有使用@Updatable装饰器标记的服务,供Core统一更新
|
||||
* 按优先级排序(数值越小越先执行)
|
||||
*/
|
||||
private _updatableServices: Array<{ instance: any; priority: number }> = [];
|
||||
|
||||
/**
|
||||
* 注册单例服务
|
||||
*
|
||||
* @param type - 服务类型
|
||||
* @param factory - 可选的工厂函数
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 直接注册类型
|
||||
* container.registerSingleton(TimerManager);
|
||||
*
|
||||
* // 使用工厂函数
|
||||
* container.registerSingleton(Logger, (c) => {
|
||||
* return createLogger('App');
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
public registerSingleton<T extends IService>(
|
||||
type: ServiceType<T>,
|
||||
factory?: (container: ServiceContainer) => T
|
||||
): void {
|
||||
if (this._services.has(type as ServiceType<IService>)) {
|
||||
logger.warn(`Service ${type.name} is already registered`);
|
||||
return;
|
||||
}
|
||||
|
||||
this._services.set(type as ServiceType<IService>, {
|
||||
type: type as ServiceType<IService>,
|
||||
factory: factory as ((container: ServiceContainer) => IService) | undefined,
|
||||
lifetime: ServiceLifetime.Singleton
|
||||
});
|
||||
|
||||
logger.debug(`Registered singleton service: ${type.name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册瞬时服务
|
||||
*
|
||||
* 每次解析都会创建新实例。
|
||||
*
|
||||
* @param type - 服务类型
|
||||
* @param factory - 可选的工厂函数
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 每次解析都创建新实例
|
||||
* container.registerTransient(Command);
|
||||
* ```
|
||||
*/
|
||||
public registerTransient<T extends IService>(
|
||||
type: ServiceType<T>,
|
||||
factory?: (container: ServiceContainer) => T
|
||||
): void {
|
||||
if (this._services.has(type as ServiceType<IService>)) {
|
||||
logger.warn(`Service ${type.name} is already registered`);
|
||||
return;
|
||||
}
|
||||
|
||||
this._services.set(type as ServiceType<IService>, {
|
||||
type: type as ServiceType<IService>,
|
||||
factory: factory as ((container: ServiceContainer) => IService) | undefined,
|
||||
lifetime: ServiceLifetime.Transient
|
||||
});
|
||||
|
||||
logger.debug(`Registered transient service: ${type.name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册服务实例
|
||||
*
|
||||
* 直接注册已创建的实例,自动视为单例。
|
||||
*
|
||||
* @param type - 服务类型(构造函数,仅用作标识)
|
||||
* @param instance - 服务实例
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const config = new Config();
|
||||
* container.registerInstance(Config, config);
|
||||
* ```
|
||||
*/
|
||||
public registerInstance<T extends IService>(type: new (...args: any[]) => T, instance: T): void {
|
||||
if (this._services.has(type as ServiceType<IService>)) {
|
||||
logger.warn(`Service ${type.name} is already registered`);
|
||||
return;
|
||||
}
|
||||
|
||||
this._services.set(type as ServiceType<IService>, {
|
||||
type: type as ServiceType<IService>,
|
||||
instance: instance as IService,
|
||||
lifetime: ServiceLifetime.Singleton
|
||||
});
|
||||
|
||||
// 如果使用了@Updatable装饰器,添加到可更新列表
|
||||
if (checkUpdatable(type)) {
|
||||
const metadata = getUpdatableMetadata(type);
|
||||
const priority = metadata?.priority ?? 0;
|
||||
this._updatableServices.push({ instance, priority });
|
||||
|
||||
// 按优先级排序(数值越小越先执行)
|
||||
this._updatableServices.sort((a, b) => a.priority - b.priority);
|
||||
|
||||
logger.debug(`Service ${type.name} is updatable (priority: ${priority}), added to update list`);
|
||||
}
|
||||
|
||||
logger.debug(`Registered service instance: ${type.name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析服务
|
||||
*
|
||||
* @param type - 服务类型
|
||||
* @returns 服务实例
|
||||
* @throws 如果服务未注册或存在循环依赖
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const timer = container.resolve(TimerManager);
|
||||
* ```
|
||||
*/
|
||||
public resolve<T extends IService>(type: ServiceType<T>): T {
|
||||
const registration = this._services.get(type as ServiceType<IService>);
|
||||
|
||||
if (!registration) {
|
||||
throw new Error(`Service ${type.name} is not registered`);
|
||||
}
|
||||
|
||||
// 检测循环依赖
|
||||
if (this._resolving.has(type as ServiceType<IService>)) {
|
||||
const chain = Array.from(this._resolving).map(t => t.name).join(' -> ');
|
||||
throw new Error(`Circular dependency detected: ${chain} -> ${type.name}`);
|
||||
}
|
||||
|
||||
// 如果是单例且已经有实例,直接返回
|
||||
if (registration.lifetime === ServiceLifetime.Singleton && registration.instance) {
|
||||
return registration.instance as T;
|
||||
}
|
||||
|
||||
// 添加到解析栈
|
||||
this._resolving.add(type as ServiceType<IService>);
|
||||
|
||||
try {
|
||||
// 创建实例
|
||||
let instance: IService;
|
||||
|
||||
if (registration.factory) {
|
||||
// 使用工厂函数
|
||||
instance = registration.factory(this);
|
||||
} else {
|
||||
// 直接构造
|
||||
instance = new (registration.type)();
|
||||
}
|
||||
|
||||
// 如果是单例,缓存实例
|
||||
if (registration.lifetime === ServiceLifetime.Singleton) {
|
||||
registration.instance = instance;
|
||||
|
||||
// 如果使用了@Updatable装饰器,添加到可更新列表
|
||||
if (checkUpdatable(registration.type)) {
|
||||
const metadata = getUpdatableMetadata(registration.type);
|
||||
const priority = metadata?.priority ?? 0;
|
||||
this._updatableServices.push({ instance, priority });
|
||||
|
||||
// 按优先级排序(数值越小越先执行)
|
||||
this._updatableServices.sort((a, b) => a.priority - b.priority);
|
||||
|
||||
logger.debug(`Service ${type.name} is updatable (priority: ${priority}), added to update list`);
|
||||
}
|
||||
}
|
||||
|
||||
return instance as T;
|
||||
} finally {
|
||||
// 从解析栈移除
|
||||
this._resolving.delete(type as ServiceType<IService>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试解析服务
|
||||
*
|
||||
* 如果服务未注册,返回null而不是抛出异常。
|
||||
*
|
||||
* @param type - 服务类型
|
||||
* @returns 服务实例或null
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const timer = container.tryResolve(TimerManager);
|
||||
* if (timer) {
|
||||
* timer.schedule(...);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
public tryResolve<T extends IService>(type: ServiceType<T>): T | null {
|
||||
try {
|
||||
return this.resolve(type);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查服务是否已注册
|
||||
*
|
||||
* @param type - 服务类型
|
||||
* @returns 是否已注册
|
||||
*/
|
||||
public isRegistered<T extends IService>(type: ServiceType<T>): boolean {
|
||||
return this._services.has(type as ServiceType<IService>);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销服务
|
||||
*
|
||||
* @param type - 服务类型
|
||||
* @returns 是否成功注销
|
||||
*/
|
||||
public unregister<T extends IService>(type: ServiceType<T>): boolean {
|
||||
const registration = this._services.get(type as ServiceType<IService>);
|
||||
if (!registration) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果有单例实例,调用 dispose
|
||||
if (registration.instance) {
|
||||
// 从可更新列表中移除
|
||||
const index = this._updatableServices.findIndex(item => item.instance === registration.instance);
|
||||
if (index !== -1) {
|
||||
this._updatableServices.splice(index, 1);
|
||||
}
|
||||
|
||||
registration.instance.dispose();
|
||||
}
|
||||
|
||||
this._services.delete(type as ServiceType<IService>);
|
||||
logger.debug(`Unregistered service: ${type.name}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有服务
|
||||
*/
|
||||
public clear(): void {
|
||||
// 清理所有单例实例
|
||||
for (const [, registration] of this._services) {
|
||||
if (registration.instance) {
|
||||
registration.instance.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
this._services.clear();
|
||||
this._updatableServices = [];
|
||||
logger.debug('Cleared all services');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有已注册的服务类型
|
||||
*
|
||||
* @returns 服务类型数组
|
||||
*/
|
||||
public getRegisteredServices(): ServiceType<IService>[] {
|
||||
return Array.from(this._services.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新所有使用@Updatable装饰器标记的服务
|
||||
*
|
||||
* 此方法会按优先级顺序遍历所有可更新的服务并调用它们的update方法。
|
||||
* 所有服务在注册时已经由@Updatable装饰器验证过必须实现IUpdatable接口。
|
||||
* 通常在Core的主更新循环中调用。
|
||||
*
|
||||
* @param deltaTime - 可选的帧时间间隔(秒)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 在Core的update方法中
|
||||
* this._serviceContainer.updateAll(deltaTime);
|
||||
* ```
|
||||
*/
|
||||
public updateAll(deltaTime?: number): void {
|
||||
for (const { instance } of this._updatableServices) {
|
||||
instance.update(deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有可更新的服务数量
|
||||
*
|
||||
* @returns 可更新服务的数量
|
||||
*/
|
||||
public getUpdatableCount(): number {
|
||||
return this._updatableServices.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有已实例化的服务实例
|
||||
*
|
||||
* @returns 所有服务实例的数组
|
||||
*/
|
||||
public getAll(): IService[] {
|
||||
const instances: IService[] = [];
|
||||
|
||||
for (const descriptor of this._services.values()) {
|
||||
if (descriptor.instance) {
|
||||
instances.push(descriptor.instance);
|
||||
}
|
||||
}
|
||||
|
||||
return instances;
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,13 @@ export abstract class Component implements IComponent {
|
||||
*/
|
||||
public readonly id: number;
|
||||
|
||||
/**
|
||||
* 所属实体ID
|
||||
*
|
||||
* 存储实体ID而非引用,避免循环引用,符合ECS数据导向设计。
|
||||
*/
|
||||
public entityId: number | null = null;
|
||||
|
||||
/**
|
||||
* 创建组件实例
|
||||
*
|
||||
@@ -63,8 +70,7 @@ export abstract class Component implements IComponent {
|
||||
* 这是一个生命周期钩子,用于组件的初始化逻辑。
|
||||
* 虽然保留此方法,但建议将复杂的初始化逻辑放在 System 中处理。
|
||||
*/
|
||||
public onAddedToEntity(): void {
|
||||
}
|
||||
public onAddedToEntity(): void {}
|
||||
|
||||
/**
|
||||
* 组件从实体移除时的回调
|
||||
@@ -75,7 +81,5 @@ export abstract class Component implements IComponent {
|
||||
* 这是一个生命周期钩子,用于组件的清理逻辑。
|
||||
* 虽然保留此方法,但建议将复杂的清理逻辑放在 System 中处理。
|
||||
*/
|
||||
public onRemovedFromEntity(): void {
|
||||
}
|
||||
|
||||
}
|
||||
public onRemovedFromEntity(): void {}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
/**
|
||||
@@ -65,8 +65,6 @@ export class ArchetypeSystem {
|
||||
|
||||
archetype.entities.add(entity);
|
||||
this._entityToArchetype.set(entity, archetype);
|
||||
|
||||
this.updateComponentIndexes(archetype, componentTypes, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,12 +117,6 @@ export class ArchetypeSystem {
|
||||
newArchetype.entities.add(entity);
|
||||
this._entityToArchetype.set(entity, newArchetype);
|
||||
|
||||
// 更新组件索引
|
||||
if (currentArchetype) {
|
||||
this.updateComponentIndexes(currentArchetype, currentArchetype.componentTypes, false);
|
||||
}
|
||||
this.updateComponentIndexes(newArchetype, newComponentTypes, true);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -140,19 +132,52 @@ export class ArchetypeSystem {
|
||||
let totalEntities = 0;
|
||||
|
||||
if (operation === 'AND') {
|
||||
// 生成查询的 BitMask
|
||||
const queryMask = this.generateArchetypeId(componentTypes);
|
||||
|
||||
// 使用 BitMask 位运算快速判断原型是否包含所有指定组件
|
||||
for (const archetype of this._allArchetypes) {
|
||||
if (BitMask64Utils.hasAll(archetype.id, queryMask)) {
|
||||
if (componentTypes.length === 0) {
|
||||
for (const archetype of this._allArchetypes) {
|
||||
matchingArchetypes.push(archetype);
|
||||
totalEntities += archetype.entities.size;
|
||||
}
|
||||
return { archetypes: matchingArchetypes, totalEntities };
|
||||
}
|
||||
|
||||
if (componentTypes.length === 1) {
|
||||
const archetypes = this._componentToArchetypes.get(componentTypes[0]);
|
||||
if (archetypes) {
|
||||
for (const archetype of archetypes) {
|
||||
matchingArchetypes.push(archetype);
|
||||
totalEntities += archetype.entities.size;
|
||||
}
|
||||
}
|
||||
return { archetypes: matchingArchetypes, totalEntities };
|
||||
}
|
||||
|
||||
let smallestSet: Set<Archetype> | undefined;
|
||||
let smallestSize = Infinity;
|
||||
|
||||
for (const componentType of componentTypes) {
|
||||
const archetypes = this._componentToArchetypes.get(componentType);
|
||||
if (!archetypes || archetypes.size === 0) {
|
||||
return { archetypes: [], totalEntities: 0 };
|
||||
}
|
||||
if (archetypes.size < smallestSize) {
|
||||
smallestSize = archetypes.size;
|
||||
smallestSet = archetypes;
|
||||
}
|
||||
}
|
||||
|
||||
const queryMask = this.generateArchetypeId(componentTypes);
|
||||
|
||||
if (smallestSet) {
|
||||
for (const archetype of smallestSet) {
|
||||
if (BitMask64Utils.hasAll(archetype.id, queryMask)) {
|
||||
matchingArchetypes.push(archetype);
|
||||
totalEntities += archetype.entities.size;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const foundArchetypes = new Set<Archetype>();
|
||||
|
||||
|
||||
for (const componentType of componentTypes) {
|
||||
const archetypes = this._componentToArchetypes.get(componentType);
|
||||
if (archetypes) {
|
||||
@@ -161,13 +186,13 @@ export class ArchetypeSystem {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for (const archetype of foundArchetypes) {
|
||||
matchingArchetypes.push(archetype);
|
||||
totalEntities += archetype.entities.size;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
archetypes: matchingArchetypes,
|
||||
totalEntities
|
||||
@@ -241,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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -252,40 +285,26 @@ export class ArchetypeSystem {
|
||||
*/
|
||||
private createArchetype(componentTypes: ComponentType[]): Archetype {
|
||||
const id = this.generateArchetypeId(componentTypes);
|
||||
|
||||
|
||||
const archetype: Archetype = {
|
||||
id,
|
||||
componentTypes: [...componentTypes],
|
||||
entities: new Set<Entity>()
|
||||
};
|
||||
// 存储原型ID - 原型
|
||||
this._archetypes.set(id,archetype);
|
||||
// 更新数组
|
||||
this.updateAllArchetypeArrays();
|
||||
return archetype;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新组件索引
|
||||
*/
|
||||
private updateComponentIndexes(archetype: Archetype, componentTypes: ComponentType[], add: boolean): void {
|
||||
|
||||
for (const componentType of componentTypes) {
|
||||
let archetypes = this._componentToArchetypes.get(componentType);
|
||||
if (!archetypes) {
|
||||
archetypes = new Set();
|
||||
this._componentToArchetypes.set(componentType, archetypes);
|
||||
}
|
||||
|
||||
if (add) {
|
||||
archetypes.add(archetype);
|
||||
} else {
|
||||
archetypes.delete(archetype);
|
||||
if (archetypes.size === 0) {
|
||||
this._componentToArchetypes.delete(componentType);
|
||||
}
|
||||
}
|
||||
archetypes.add(archetype);
|
||||
}
|
||||
|
||||
return archetype;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -8,24 +8,41 @@ export class ComponentPool<T extends Component> {
|
||||
private createFn: () => T;
|
||||
private resetFn?: (component: T) => void;
|
||||
private maxSize: number;
|
||||
private minSize: number;
|
||||
private growthFactor: number;
|
||||
|
||||
private stats = {
|
||||
totalCreated: 0,
|
||||
totalAcquired: 0,
|
||||
totalReleased: 0
|
||||
};
|
||||
|
||||
constructor(
|
||||
createFn: () => T,
|
||||
resetFn?: (component: T) => void,
|
||||
maxSize: number = 1000
|
||||
maxSize: number = 1000,
|
||||
minSize: number = 10,
|
||||
growthFactor: number = 1.5
|
||||
) {
|
||||
this.createFn = createFn;
|
||||
this.resetFn = resetFn;
|
||||
this.maxSize = maxSize;
|
||||
this.minSize = Math.max(1, minSize);
|
||||
this.growthFactor = Math.max(1.1, growthFactor);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一个组件实例
|
||||
*/
|
||||
acquire(): T {
|
||||
this.stats.totalAcquired++;
|
||||
|
||||
if (this.pool.length > 0) {
|
||||
return this.pool.pop()!;
|
||||
}
|
||||
|
||||
this.stats.totalCreated++;
|
||||
|
||||
return this.createFn();
|
||||
}
|
||||
|
||||
@@ -33,20 +50,41 @@ export class ComponentPool<T extends Component> {
|
||||
* 释放一个组件实例回池中
|
||||
*/
|
||||
release(component: T): void {
|
||||
if (this.pool.length < this.maxSize) {
|
||||
if (this.resetFn) {
|
||||
this.resetFn(component);
|
||||
}
|
||||
this.pool.push(component);
|
||||
this.stats.totalReleased++;
|
||||
|
||||
if (this.pool.length >= this.maxSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.resetFn) {
|
||||
this.resetFn(component);
|
||||
}
|
||||
|
||||
this.pool.push(component);
|
||||
}
|
||||
|
||||
/**
|
||||
* 预填充对象池
|
||||
*/
|
||||
prewarm(count: number): void {
|
||||
for (let i = 0; i < count && this.pool.length < this.maxSize; i++) {
|
||||
this.pool.push(this.createFn());
|
||||
const targetCount = Math.min(count, this.maxSize);
|
||||
|
||||
for (let i = this.pool.length; i < targetCount; i++) {
|
||||
const component = this.createFn();
|
||||
if (this.resetFn) {
|
||||
this.resetFn(component);
|
||||
}
|
||||
this.pool.push(component);
|
||||
this.stats.totalCreated++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动收缩池大小
|
||||
*/
|
||||
shrink(): void {
|
||||
while (this.pool.length > this.minSize) {
|
||||
this.pool.pop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +108,35 @@ export class ComponentPool<T extends Component> {
|
||||
getMaxSize(): number {
|
||||
return this.maxSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取统计信息
|
||||
*/
|
||||
getStats() {
|
||||
const hitRate = this.stats.totalAcquired === 0
|
||||
? 0
|
||||
: (this.stats.totalAcquired - this.stats.totalCreated) / this.stats.totalAcquired;
|
||||
|
||||
return {
|
||||
totalCreated: this.stats.totalCreated,
|
||||
totalAcquired: this.stats.totalAcquired,
|
||||
totalReleased: this.stats.totalReleased,
|
||||
hitRate: hitRate,
|
||||
currentSize: this.pool.length,
|
||||
maxSize: this.maxSize,
|
||||
minSize: this.minSize,
|
||||
utilizationRate: this.pool.length / this.maxSize
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件使用追踪
|
||||
*/
|
||||
interface ComponentUsageTracker {
|
||||
createCount: number;
|
||||
releaseCount: number;
|
||||
lastAccessTime: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,6 +145,10 @@ export class ComponentPool<T extends Component> {
|
||||
export class ComponentPoolManager {
|
||||
private static instance: ComponentPoolManager;
|
||||
private pools = new Map<string, ComponentPool<any>>();
|
||||
private usageTracker = new Map<string, ComponentUsageTracker>();
|
||||
|
||||
private autoCleanupInterval = 60000;
|
||||
private lastCleanupTime = 0;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
@@ -95,9 +166,16 @@ export class ComponentPoolManager {
|
||||
componentName: string,
|
||||
createFn: () => T,
|
||||
resetFn?: (component: T) => void,
|
||||
maxSize?: number
|
||||
maxSize?: number,
|
||||
minSize?: number
|
||||
): void {
|
||||
this.pools.set(componentName, new ComponentPool(createFn, resetFn, maxSize));
|
||||
this.pools.set(componentName, new ComponentPool(createFn, resetFn, maxSize, minSize));
|
||||
|
||||
this.usageTracker.set(componentName, {
|
||||
createCount: 0,
|
||||
releaseCount: 0,
|
||||
lastAccessTime: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,6 +183,9 @@ export class ComponentPoolManager {
|
||||
*/
|
||||
acquireComponent<T extends Component>(componentName: string): T | null {
|
||||
const pool = this.pools.get(componentName);
|
||||
|
||||
this.trackUsage(componentName, 'create');
|
||||
|
||||
return pool ? pool.acquire() : null;
|
||||
}
|
||||
|
||||
@@ -113,11 +194,71 @@ export class ComponentPoolManager {
|
||||
*/
|
||||
releaseComponent<T extends Component>(componentName: string, component: T): void {
|
||||
const pool = this.pools.get(componentName);
|
||||
|
||||
this.trackUsage(componentName, 'release');
|
||||
|
||||
if (pool) {
|
||||
pool.release(component);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 追踪使用情况
|
||||
*/
|
||||
private trackUsage(componentName: string, action: 'create' | 'release'): void {
|
||||
let tracker = this.usageTracker.get(componentName);
|
||||
|
||||
if (!tracker) {
|
||||
tracker = {
|
||||
createCount: 0,
|
||||
releaseCount: 0,
|
||||
lastAccessTime: Date.now()
|
||||
};
|
||||
this.usageTracker.set(componentName, tracker);
|
||||
}
|
||||
|
||||
if (action === 'create') {
|
||||
tracker.createCount++;
|
||||
} else {
|
||||
tracker.releaseCount++;
|
||||
}
|
||||
|
||||
tracker.lastAccessTime = Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动清理(定期调用)
|
||||
*/
|
||||
public update(): void {
|
||||
const now = Date.now();
|
||||
|
||||
if (now - this.lastCleanupTime < this.autoCleanupInterval) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [name, tracker] of this.usageTracker.entries()) {
|
||||
const inactive = now - tracker.lastAccessTime > 120000;
|
||||
|
||||
if (inactive) {
|
||||
const pool = this.pools.get(name);
|
||||
if (pool) {
|
||||
pool.shrink();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.lastCleanupTime = now;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取热点组件列表
|
||||
*/
|
||||
public getHotComponents(threshold: number = 100): string[] {
|
||||
return Array.from(this.usageTracker.entries())
|
||||
.filter(([_, tracker]) => tracker.createCount > threshold)
|
||||
.map(([name]) => name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 预热所有池
|
||||
*/
|
||||
@@ -137,10 +278,28 @@ export class ComponentPoolManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置管理器,移除所有注册的池
|
||||
* 重置管理器
|
||||
*/
|
||||
reset(): void {
|
||||
this.pools.clear();
|
||||
this.usageTracker.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取全局统计信息
|
||||
*/
|
||||
getGlobalStats() {
|
||||
const stats: any[] = [];
|
||||
|
||||
for (const [name, pool] of this.pools.entries()) {
|
||||
stats.push({
|
||||
componentName: name,
|
||||
poolStats: pool.getStats(),
|
||||
usage: this.usageTracker.get(name)
|
||||
});
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,7 +317,7 @@ export class ComponentPoolManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取池利用率信息(用于调试)
|
||||
* 获取池利用率信息
|
||||
*/
|
||||
getPoolUtilization(): Map<string, { used: number; total: number; utilization: number }> {
|
||||
const utilization = new Map();
|
||||
@@ -167,7 +326,7 @@ export class ComponentPoolManager {
|
||||
const maxSize = pool.getMaxSize();
|
||||
const used = maxSize - available;
|
||||
const utilRate = maxSize > 0 ? (used / maxSize * 100) : 0;
|
||||
|
||||
|
||||
utilization.set(name, {
|
||||
used: used,
|
||||
total: maxSize,
|
||||
@@ -183,11 +342,11 @@ export class ComponentPoolManager {
|
||||
getComponentUtilization(componentName: string): number {
|
||||
const pool = this.pools.get(componentName);
|
||||
if (!pool) return 0;
|
||||
|
||||
|
||||
const available = pool.getAvailableCount();
|
||||
const maxSize = pool.getMaxSize();
|
||||
const used = maxSize - available;
|
||||
|
||||
|
||||
return maxSize > 0 ? (used / maxSize * 100) : 0;
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,9 @@ export class EntityBuilder {
|
||||
constructor(scene: IScene, storageManager: ComponentStorageManager) {
|
||||
this.scene = scene;
|
||||
this.storageManager = storageManager;
|
||||
this.entity = new Entity("", scene.identifierPool.checkOut());
|
||||
const id = scene.identifierPool.checkOut();
|
||||
this.entity = new Entity("", id);
|
||||
this.entity.scene = this.scene as any;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 };
|
||||
|
||||
/**
|
||||
* 实体索引结构
|
||||
@@ -77,19 +47,16 @@ export class QuerySystem {
|
||||
private entities: Entity[] = [];
|
||||
private entityIndex: EntityIndex;
|
||||
|
||||
// 版本号,用于缓存失效
|
||||
private _version = 0;
|
||||
|
||||
// 查询缓存系统
|
||||
private queryCache = new Map<string, QueryCacheEntry>();
|
||||
private cacheMaxSize = 1000;
|
||||
private cacheTimeout = 5000; // 5秒缓存过期
|
||||
private cacheTimeout = 5000;
|
||||
|
||||
private componentMaskCache = new Map<string, BitMask64Data>();
|
||||
|
||||
private archetypeSystem: ArchetypeSystem;
|
||||
|
||||
// 性能统计
|
||||
private queryStats = {
|
||||
totalQueries: 0,
|
||||
cacheHits: 0,
|
||||
@@ -99,6 +66,9 @@ export class QuerySystem {
|
||||
dirtyChecks: 0
|
||||
};
|
||||
|
||||
private resultArrayPool: Entity[][] = [];
|
||||
private poolMaxSize = 50;
|
||||
|
||||
constructor() {
|
||||
this.entityIndex = {
|
||||
byTag: new Map(),
|
||||
@@ -108,19 +78,32 @@ export class QuerySystem {
|
||||
this.archetypeSystem = new ArchetypeSystem();
|
||||
}
|
||||
|
||||
private acquireResultArray(): Entity[] {
|
||||
if (this.resultArrayPool.length > 0) {
|
||||
return this.resultArrayPool.pop()!;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
private releaseResultArray(array: Entity[]): void {
|
||||
if (this.resultArrayPool.length < this.poolMaxSize) {
|
||||
array.length = 0;
|
||||
this.resultArrayPool.push(array);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置实体列表并重建索引
|
||||
*
|
||||
*
|
||||
* 当实体集合发生大规模变化时调用此方法。
|
||||
* 系统将重新构建所有索引以确保查询性能。
|
||||
*
|
||||
*
|
||||
* @param entities 新的实体列表
|
||||
*/
|
||||
public setEntities(entities: Entity[]): void {
|
||||
this.entities = entities;
|
||||
this.clearQueryCache();
|
||||
this.clearReactiveQueries();
|
||||
this.rebuildIndexes();
|
||||
}
|
||||
|
||||
@@ -140,12 +123,14 @@ export class QuerySystem {
|
||||
|
||||
this.archetypeSystem.addEntity(entity);
|
||||
|
||||
// 通知响应式查询
|
||||
this.notifyReactiveQueriesEntityAdded(entity);
|
||||
|
||||
// 只有在非延迟模式下才立即清理缓存
|
||||
if (!deferCacheClear) {
|
||||
this.clearQueryCache();
|
||||
}
|
||||
|
||||
|
||||
// 更新版本号
|
||||
this._version++;
|
||||
}
|
||||
@@ -223,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++;
|
||||
}
|
||||
}
|
||||
@@ -258,6 +253,9 @@ export class QuerySystem {
|
||||
// 重新添加实体到索引(基于新的组件状态)
|
||||
this.addEntityToIndexes(entity);
|
||||
|
||||
// 通知响应式查询
|
||||
this.notifyReactiveQueriesEntityChanged(entity);
|
||||
|
||||
// 清理查询缓存,因为实体组件状态已改变
|
||||
this.clearQueryCache();
|
||||
|
||||
@@ -345,13 +343,13 @@ export class QuerySystem {
|
||||
|
||||
/**
|
||||
* 查询包含所有指定组件的实体
|
||||
*
|
||||
*
|
||||
* 返回同时包含所有指定组件类型的实体列表。
|
||||
* 系统会自动选择最高效的查询策略,包括索引查找和缓存机制。
|
||||
*
|
||||
* 内部使用响应式查询作为智能缓存,自动跟踪实体变化,性能更优。
|
||||
*
|
||||
* @param componentTypes 要查询的组件类型列表
|
||||
* @returns 查询结果,包含匹配的实体和性能信息
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 查询同时具有位置和速度组件的实体
|
||||
@@ -363,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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -424,13 +404,13 @@ export class QuerySystem {
|
||||
|
||||
/**
|
||||
* 查询包含任意指定组件的实体
|
||||
*
|
||||
*
|
||||
* 返回包含任意一个指定组件类型的实体列表。
|
||||
* 使用集合合并算法确保高效的查询性能。
|
||||
*
|
||||
* 内部使用响应式查询作为智能缓存,自动跟踪实体变化,性能更优。
|
||||
*
|
||||
* @param componentTypes 要查询的组件类型列表
|
||||
* @returns 查询结果,包含匹配的实体和性能信息
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 查询具有武器或护甲组件的实体
|
||||
@@ -442,49 +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: 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
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询不包含任何指定组件的实体
|
||||
*
|
||||
*
|
||||
* 返回不包含任何指定组件类型的实体列表。
|
||||
* 适用于排除特定类型实体的查询场景。
|
||||
*
|
||||
* 内部使用响应式查询作为智能缓存,自动跟踪实体变化,性能更优。
|
||||
*
|
||||
* @param componentTypes 要排除的组件类型列表
|
||||
* @returns 查询结果,包含匹配的实体和性能信息
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 查询不具有AI和玩家控制组件的实体(如静态物体)
|
||||
@@ -496,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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -744,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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 高效的缓存键生成
|
||||
*/
|
||||
@@ -767,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();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -779,6 +844,7 @@ export class QuerySystem {
|
||||
*
|
||||
* 根据组件类型列表生成对应的位掩码。
|
||||
* 使用缓存避免重复计算。
|
||||
* 注意:必须使用ComponentRegistry来确保与Entity.componentMask使用相同的bitIndex
|
||||
*
|
||||
* @param componentTypes 组件类型列表
|
||||
* @returns 生成的位掩码
|
||||
@@ -795,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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -867,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%'
|
||||
}
|
||||
@@ -876,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
36
packages/core/src/ECS/Core/QueryTypes.ts
Normal file
36
packages/core/src/ECS/Core/QueryTypes.ts
Normal 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;
|
||||
}
|
||||
475
packages/core/src/ECS/Core/ReactiveQuery.ts
Normal file
475
packages/core/src/ECS/Core/ReactiveQuery.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
345
packages/core/src/ECS/Core/ReferenceTracker.ts
Normal file
345
packages/core/src/ECS/Core/ReferenceTracker.ts
Normal file
@@ -0,0 +1,345 @@
|
||||
import { Component } from '../Component';
|
||||
import type { Entity } from '../Entity';
|
||||
import type { IScene } from '../IScene';
|
||||
import { createLogger } from '../../Utils/Logger';
|
||||
|
||||
const logger = createLogger('ReferenceTracker');
|
||||
|
||||
/**
|
||||
* WeakRef 接口定义
|
||||
*
|
||||
* 用于 ES2020 环境下的类型定义
|
||||
*/
|
||||
interface IWeakRef<T extends object> {
|
||||
deref(): T | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* WeakRef Polyfill for ES2020 compatibility
|
||||
*
|
||||
* 为了兼容 Cocos Creator、Laya、微信小游戏等目标平台(仅支持 ES2020),
|
||||
* 提供 WeakRef 的 Polyfill 实现。
|
||||
*
|
||||
* - 现代浏览器:自动使用原生 WeakRef (自动 GC)
|
||||
* - 旧环境:使用 Polyfill (无自动 GC,但 Scene 销毁时会手动清理)
|
||||
*/
|
||||
class WeakRefPolyfill<T extends object> implements IWeakRef<T> {
|
||||
private _target: T;
|
||||
|
||||
constructor(target: T) {
|
||||
this._target = target;
|
||||
}
|
||||
|
||||
deref(): T | undefined {
|
||||
return this._target;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WeakRef 构造函数类型
|
||||
*/
|
||||
interface IWeakRefConstructor {
|
||||
new <T extends object>(target: T): IWeakRef<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* WeakRef 实现
|
||||
*
|
||||
* 优先使用原生 WeakRef,不支持时降级到 Polyfill
|
||||
*/
|
||||
const WeakRefImpl: IWeakRefConstructor = (
|
||||
(typeof globalThis !== 'undefined' && (globalThis as any).WeakRef) ||
|
||||
(typeof global !== 'undefined' && (global as any).WeakRef) ||
|
||||
(typeof window !== 'undefined' && (window as any).WeakRef) ||
|
||||
WeakRefPolyfill
|
||||
) as IWeakRefConstructor;
|
||||
|
||||
/**
|
||||
* Entity引用记录
|
||||
*/
|
||||
export interface EntityRefRecord {
|
||||
component: IWeakRef<Component>;
|
||||
propertyKey: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局EntityID到Scene的映射
|
||||
*
|
||||
* 使用全局Map记录每个Entity ID对应的Scene,用于装饰器通过Component.entityId查找Scene。
|
||||
*/
|
||||
const globalEntitySceneMap = new Map<number, IWeakRef<IScene>>();
|
||||
|
||||
/**
|
||||
* 通过Entity ID获取Scene
|
||||
*
|
||||
* @param entityId Entity ID
|
||||
* @returns Scene实例,如果不存在则返回null
|
||||
*/
|
||||
export function getSceneByEntityId(entityId: number): IScene | null {
|
||||
const sceneRef = globalEntitySceneMap.get(entityId);
|
||||
return sceneRef?.deref() || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Entity引用追踪器
|
||||
*
|
||||
* 追踪Component中对Entity的引用,当Entity被销毁时自动清理所有引用。
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const tracker = new ReferenceTracker();
|
||||
* tracker.registerReference(targetEntity, component, 'parent');
|
||||
* targetEntity.destroy(); // 自动将 component.parent 设为 null
|
||||
* ```
|
||||
*/
|
||||
export class ReferenceTracker {
|
||||
/**
|
||||
* Entity ID -> 引用该Entity的所有组件记录
|
||||
*/
|
||||
private _references: Map<number, Set<EntityRefRecord>> = new Map();
|
||||
|
||||
/**
|
||||
* 当前Scene的引用
|
||||
*/
|
||||
private _scene: IWeakRef<IScene> | null = null;
|
||||
|
||||
/**
|
||||
* 注册Entity引用
|
||||
*
|
||||
* @param entity 被引用的Entity
|
||||
* @param component 持有引用的Component
|
||||
* @param propertyKey Component中存储引用的属性名
|
||||
*/
|
||||
public registerReference(entity: Entity, component: Component, propertyKey: string): void {
|
||||
const entityId = entity.id;
|
||||
|
||||
let records = this._references.get(entityId);
|
||||
if (!records) {
|
||||
records = new Set();
|
||||
this._references.set(entityId, records);
|
||||
}
|
||||
|
||||
const existingRecord = this._findRecord(records, component, propertyKey);
|
||||
if (existingRecord) {
|
||||
return;
|
||||
}
|
||||
|
||||
records.add({
|
||||
component: new WeakRefImpl(component),
|
||||
propertyKey
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销Entity引用
|
||||
*
|
||||
* @param entity 被引用的Entity
|
||||
* @param component 持有引用的Component
|
||||
* @param propertyKey Component中存储引用的属性名
|
||||
*/
|
||||
public unregisterReference(entity: Entity, component: Component, propertyKey: string): void {
|
||||
const entityId = entity.id;
|
||||
const records = this._references.get(entityId);
|
||||
|
||||
if (!records) {
|
||||
return;
|
||||
}
|
||||
|
||||
const record = this._findRecord(records, component, propertyKey);
|
||||
if (record) {
|
||||
records.delete(record);
|
||||
|
||||
if (records.size === 0) {
|
||||
this._references.delete(entityId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理所有指向指定Entity的引用
|
||||
*
|
||||
* 将所有引用该Entity的Component属性设为null。
|
||||
*
|
||||
* @param entityId 被销毁的Entity ID
|
||||
*/
|
||||
public clearReferencesTo(entityId: number): void {
|
||||
const records = this._references.get(entityId);
|
||||
|
||||
if (!records) {
|
||||
return;
|
||||
}
|
||||
|
||||
const validRecords: EntityRefRecord[] = [];
|
||||
|
||||
for (const record of records) {
|
||||
const component = record.component.deref();
|
||||
|
||||
if (component) {
|
||||
validRecords.push(record);
|
||||
}
|
||||
}
|
||||
|
||||
for (const record of validRecords) {
|
||||
const component = record.component.deref();
|
||||
if (component) {
|
||||
(component as any)[record.propertyKey] = null;
|
||||
}
|
||||
}
|
||||
|
||||
this._references.delete(entityId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理Component的所有引用注册
|
||||
*
|
||||
* 当Component被移除时调用,清理该Component注册的所有引用。
|
||||
*
|
||||
* @param component 被移除的Component
|
||||
*/
|
||||
public clearComponentReferences(component: Component): void {
|
||||
for (const [entityId, records] of this._references.entries()) {
|
||||
const toDelete: EntityRefRecord[] = [];
|
||||
|
||||
for (const record of records) {
|
||||
const comp = record.component.deref();
|
||||
if (!comp || comp === component) {
|
||||
toDelete.push(record);
|
||||
}
|
||||
}
|
||||
|
||||
for (const record of toDelete) {
|
||||
records.delete(record);
|
||||
}
|
||||
|
||||
if (records.size === 0) {
|
||||
this._references.delete(entityId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指向指定Entity的所有引用记录
|
||||
*
|
||||
* @param entityId Entity ID
|
||||
* @returns 引用记录数组(仅包含有效引用)
|
||||
*/
|
||||
public getReferencesTo(entityId: number): EntityRefRecord[] {
|
||||
const records = this._references.get(entityId);
|
||||
if (!records) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const validRecords: EntityRefRecord[] = [];
|
||||
|
||||
for (const record of records) {
|
||||
const component = record.component.deref();
|
||||
if (component) {
|
||||
validRecords.push(record);
|
||||
}
|
||||
}
|
||||
|
||||
return validRecords;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理所有失效的WeakRef引用
|
||||
*
|
||||
* 遍历所有记录,移除已被GC回收的Component引用。
|
||||
*/
|
||||
public cleanup(): void {
|
||||
const entitiesToDelete: number[] = [];
|
||||
|
||||
for (const [entityId, records] of this._references.entries()) {
|
||||
const toDelete: EntityRefRecord[] = [];
|
||||
|
||||
for (const record of records) {
|
||||
if (!record.component.deref()) {
|
||||
toDelete.push(record);
|
||||
}
|
||||
}
|
||||
|
||||
for (const record of toDelete) {
|
||||
records.delete(record);
|
||||
}
|
||||
|
||||
if (records.size === 0) {
|
||||
entitiesToDelete.push(entityId);
|
||||
}
|
||||
}
|
||||
|
||||
for (const entityId of entitiesToDelete) {
|
||||
this._references.delete(entityId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置Scene引用
|
||||
*
|
||||
* @param scene Scene实例
|
||||
*/
|
||||
public setScene(scene: IScene): void {
|
||||
this._scene = new WeakRefImpl(scene);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册Entity到Scene的映射
|
||||
*
|
||||
* @param entityId Entity ID
|
||||
* @param scene Scene实例
|
||||
*/
|
||||
public registerEntityScene(entityId: number, scene: IScene): void {
|
||||
globalEntitySceneMap.set(entityId, new WeakRefImpl(scene));
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销Entity到Scene的映射
|
||||
*
|
||||
* @param entityId Entity ID
|
||||
*/
|
||||
public unregisterEntityScene(entityId: number): void {
|
||||
globalEntitySceneMap.delete(entityId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取调试信息
|
||||
*/
|
||||
public getDebugInfo(): object {
|
||||
const info: Record<string, any> = {};
|
||||
|
||||
for (const [entityId, records] of this._references.entries()) {
|
||||
const validRecords = [];
|
||||
for (const record of records) {
|
||||
const component = record.component.deref();
|
||||
if (component) {
|
||||
validRecords.push({
|
||||
componentId: component.id,
|
||||
propertyKey: record.propertyKey
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (validRecords.length > 0) {
|
||||
info[`entity_${entityId}`] = validRecords;
|
||||
}
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找指定的引用记录
|
||||
*/
|
||||
private _findRecord(
|
||||
records: Set<EntityRefRecord>,
|
||||
component: Component,
|
||||
propertyKey: string
|
||||
): EntityRefRecord | undefined {
|
||||
for (const record of records) {
|
||||
const comp = record.component.deref();
|
||||
if (comp === component && record.propertyKey === propertyKey) {
|
||||
return record;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
147
packages/core/src/ECS/Decorators/EntityRefDecorator.ts
Normal file
147
packages/core/src/ECS/Decorators/EntityRefDecorator.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import type { Entity } from '../Entity';
|
||||
import type { Component } from '../Component';
|
||||
import { getSceneByEntityId } from '../Core/ReferenceTracker';
|
||||
import { createLogger } from '../../Utils/Logger';
|
||||
|
||||
const logger = createLogger('EntityRefDecorator');
|
||||
|
||||
/**
|
||||
* EntityRef元数据的Symbol键
|
||||
*/
|
||||
export const ENTITY_REF_METADATA = Symbol('EntityRefMetadata');
|
||||
|
||||
/**
|
||||
* EntityRef值存储的Symbol键
|
||||
*/
|
||||
const ENTITY_REF_VALUES = Symbol('EntityRefValues');
|
||||
|
||||
/**
|
||||
* EntityRef元数据
|
||||
*/
|
||||
export interface EntityRefMetadata {
|
||||
properties: Set<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取或创建组件的EntityRef值存储Map
|
||||
*/
|
||||
function getValueMap(component: Component): Map<string, Entity | null> {
|
||||
let map = (component as any)[ENTITY_REF_VALUES];
|
||||
if (!map) {
|
||||
map = new Map<string, Entity | null>();
|
||||
(component as any)[ENTITY_REF_VALUES] = map;
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Entity引用装饰器
|
||||
*
|
||||
* 标记Component属性为Entity引用,自动追踪引用关系。
|
||||
* 当被引用的Entity销毁时,该属性会自动设为null。
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* class ParentComponent extends Component {
|
||||
* @EntityRef() parent: Entity | null = null;
|
||||
* }
|
||||
*
|
||||
* const parent = scene.createEntity('Parent');
|
||||
* const child = scene.createEntity('Child');
|
||||
* const comp = child.addComponent(new ParentComponent());
|
||||
*
|
||||
* comp.parent = parent;
|
||||
* parent.destroy(); // comp.parent 自动变为 null
|
||||
* ```
|
||||
*/
|
||||
export function EntityRef(): PropertyDecorator {
|
||||
return function (target: any, propertyKey: string | symbol) {
|
||||
const constructor = target.constructor;
|
||||
|
||||
let metadata: EntityRefMetadata = constructor[ENTITY_REF_METADATA];
|
||||
if (!metadata) {
|
||||
metadata = {
|
||||
properties: new Set()
|
||||
};
|
||||
constructor[ENTITY_REF_METADATA] = metadata;
|
||||
}
|
||||
|
||||
const propKeyString = typeof propertyKey === 'symbol' ? propertyKey.toString() : propertyKey;
|
||||
metadata.properties.add(propKeyString);
|
||||
|
||||
Object.defineProperty(target, propertyKey, {
|
||||
get: function (this: Component) {
|
||||
const valueMap = getValueMap(this);
|
||||
return valueMap.get(propKeyString) || null;
|
||||
},
|
||||
set: function (this: Component, newValue: Entity | null) {
|
||||
const valueMap = getValueMap(this);
|
||||
const oldValue = valueMap.get(propKeyString) || null;
|
||||
|
||||
if (oldValue === newValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scene = this.entityId !== null ? getSceneByEntityId(this.entityId) : null;
|
||||
|
||||
if (!scene || !scene.referenceTracker) {
|
||||
valueMap.set(propKeyString, newValue);
|
||||
return;
|
||||
}
|
||||
|
||||
const tracker = scene.referenceTracker;
|
||||
|
||||
if (oldValue) {
|
||||
tracker.unregisterReference(oldValue, this, propKeyString);
|
||||
}
|
||||
|
||||
if (newValue) {
|
||||
if (newValue.scene !== scene) {
|
||||
logger.error(`Cannot reference Entity from different Scene. Entity: ${newValue.name}, Scene: ${newValue.scene?.name || 'null'}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (newValue.isDestroyed) {
|
||||
logger.warn(`Cannot reference destroyed Entity: ${newValue.name}`);
|
||||
valueMap.set(propKeyString, null);
|
||||
return;
|
||||
}
|
||||
|
||||
tracker.registerReference(newValue, this, propKeyString);
|
||||
}
|
||||
|
||||
valueMap.set(propKeyString, newValue);
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Component的EntityRef元数据
|
||||
*
|
||||
* @param component Component实例或Component类
|
||||
* @returns EntityRef元数据,如果不存在则返回null
|
||||
*/
|
||||
export function getEntityRefMetadata(component: any): EntityRefMetadata | null {
|
||||
if (!component) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const constructor = typeof component === 'function'
|
||||
? component
|
||||
: component.constructor;
|
||||
|
||||
return constructor[ENTITY_REF_METADATA] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查Component是否有EntityRef属性
|
||||
*
|
||||
* @param component Component实例或Component类
|
||||
* @returns 如果有EntityRef属性返回true
|
||||
*/
|
||||
export function hasEntityRef(component: any): boolean {
|
||||
return getEntityRefMetadata(component) !== null;
|
||||
}
|
||||
@@ -39,34 +39,72 @@ export function ECSComponent(typeName: string) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* System元数据配置
|
||||
*/
|
||||
export interface SystemMetadata {
|
||||
/**
|
||||
* 更新顺序(数值越小越先执行,默认0)
|
||||
*/
|
||||
updateOrder?: number;
|
||||
|
||||
/**
|
||||
* 是否默认启用(默认true)
|
||||
*/
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统类型装饰器
|
||||
* 用于为系统类指定固定的类型名称,避免在代码混淆后失效
|
||||
*
|
||||
*
|
||||
* @param typeName 系统类型名称
|
||||
* @param metadata 系统元数据配置
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 基本使用
|
||||
* @ECSSystem('Movement')
|
||||
* class MovementSystem extends EntitySystem {
|
||||
* protected process(entities: Entity[]): void {
|
||||
* // 系统逻辑
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* // 配置更新顺序
|
||||
* @Injectable()
|
||||
* @ECSSystem('Physics', { updateOrder: 10 })
|
||||
* class PhysicsSystem extends EntitySystem {
|
||||
* constructor(@Inject(CollisionSystem) private collision: CollisionSystem) {
|
||||
* super(Matcher.empty().all(Transform, RigidBody));
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function ECSSystem(typeName: string) {
|
||||
export function ECSSystem(typeName: string, metadata?: SystemMetadata) {
|
||||
return function <T extends new (...args: any[]) => EntitySystem>(target: T): T {
|
||||
if (!typeName || typeof typeName !== 'string') {
|
||||
throw new Error('ECSSystem装饰器必须提供有效的类型名称');
|
||||
}
|
||||
|
||||
|
||||
// 在构造函数上存储类型名称
|
||||
(target as any)[SYSTEM_TYPE_NAME] = typeName;
|
||||
|
||||
|
||||
// 存储元数据
|
||||
if (metadata) {
|
||||
(target as any).__systemMetadata__ = metadata;
|
||||
}
|
||||
|
||||
return target;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取System的元数据
|
||||
*/
|
||||
export function getSystemMetadata(systemType: new (...args: any[]) => EntitySystem): SystemMetadata | undefined {
|
||||
return (systemType as any).__systemMetadata__;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取组件类型的名称,优先使用装饰器指定的名称
|
||||
*
|
||||
|
||||
@@ -5,6 +5,18 @@ export {
|
||||
getSystemTypeName,
|
||||
getComponentInstanceTypeName,
|
||||
getSystemInstanceTypeName,
|
||||
getSystemMetadata,
|
||||
COMPONENT_TYPE_NAME,
|
||||
SYSTEM_TYPE_NAME
|
||||
} from './TypeDecorators';
|
||||
} from './TypeDecorators';
|
||||
|
||||
export type { SystemMetadata } from './TypeDecorators';
|
||||
|
||||
export {
|
||||
EntityRef,
|
||||
getEntityRefMetadata,
|
||||
hasEntityRef,
|
||||
ENTITY_REF_METADATA
|
||||
} from './EntityRefDecorator';
|
||||
|
||||
export type { EntityRefMetadata } from './EntityRefDecorator';
|
||||
@@ -145,12 +145,6 @@ export class Entity {
|
||||
*/
|
||||
private _componentCache: Component[] | null = null;
|
||||
|
||||
/**
|
||||
* 本地组件存储(用于没有 Scene 的 Entity)
|
||||
* 当 Entity 添加到 Scene 时,组件会迁移到 Scene 的 componentStorageManager
|
||||
*/
|
||||
private _localComponents: Map<ComponentType, Component> = new Map();
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
@@ -186,28 +180,23 @@ export class Entity {
|
||||
*/
|
||||
private _rebuildComponentCache(): void {
|
||||
const components: Component[] = [];
|
||||
const mask = this._componentMask;
|
||||
|
||||
if (!this.scene?.componentStorageManager) {
|
||||
this._componentCache = components;
|
||||
return;
|
||||
}
|
||||
|
||||
const mask = this._componentMask;
|
||||
const maxBitIndex = ComponentRegistry.getRegisteredCount();
|
||||
|
||||
for (let bitIndex = 0; bitIndex < maxBitIndex; bitIndex++) {
|
||||
if (BitMask64Utils.getBit(mask, bitIndex)) {
|
||||
const componentType = ComponentRegistry.getTypeByBitIndex(bitIndex);
|
||||
if (componentType) {
|
||||
let component: Component | null = null;
|
||||
|
||||
// 优先从 Scene 存储获取
|
||||
if (this.scene?.componentStorageManager) {
|
||||
component = this.scene.componentStorageManager.getComponent(
|
||||
this.id,
|
||||
componentType
|
||||
);
|
||||
}
|
||||
|
||||
// Fallback 到本地存储
|
||||
if (!component) {
|
||||
component = this._localComponents.get(componentType) || null;
|
||||
}
|
||||
const component = this.scene.componentStorageManager.getComponent(
|
||||
this.id,
|
||||
componentType
|
||||
);
|
||||
|
||||
if (component) {
|
||||
components.push(component);
|
||||
@@ -378,9 +367,6 @@ export class Entity {
|
||||
ComponentRegistry.register(componentType);
|
||||
}
|
||||
|
||||
// 存储到本地 Map
|
||||
this._localComponents.set(componentType, component);
|
||||
|
||||
// 更新位掩码
|
||||
const componentMask = ComponentRegistry.getBitMask(componentType);
|
||||
BitMask64Utils.orInPlace(this._componentMask, componentMask);
|
||||
@@ -406,19 +392,29 @@ export class Entity {
|
||||
*/
|
||||
public addComponent<T extends Component>(component: T): T {
|
||||
const componentType = component.constructor as ComponentType<T>;
|
||||
|
||||
|
||||
if (!this.scene) {
|
||||
throw new Error(`Entity must be added to Scene before adding components. Use scene.createEntity() instead of new Entity()`);
|
||||
}
|
||||
|
||||
if (!this.scene.componentStorageManager) {
|
||||
throw new Error(`Scene does not have componentStorageManager`);
|
||||
}
|
||||
|
||||
if (this.hasComponent(componentType)) {
|
||||
throw new Error(`Entity ${this.name} already has component ${getComponentTypeName(componentType)}`);
|
||||
}
|
||||
|
||||
this.addComponentInternal(component);
|
||||
|
||||
if (this.scene && this.scene.componentStorageManager) {
|
||||
this.scene.componentStorageManager.addComponent(this.id, component);
|
||||
}
|
||||
|
||||
this.scene.componentStorageManager.addComponent(this.id, component);
|
||||
|
||||
component.entityId = this.id;
|
||||
if (this.scene.referenceTracker) {
|
||||
this.scene.referenceTracker.registerEntityScene(this.id, this.scene);
|
||||
}
|
||||
component.onAddedToEntity();
|
||||
|
||||
|
||||
if (Entity.eventBus) {
|
||||
Entity.eventBus.emitComponentAdded({
|
||||
timestamp: Date.now(),
|
||||
@@ -430,7 +426,7 @@ export class Entity {
|
||||
component: component
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 通知所有相关的QuerySystem组件已变动
|
||||
Entity.notifyQuerySystems(this);
|
||||
@@ -459,16 +455,13 @@ export class Entity {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 优先从 Scene 存储获取
|
||||
if (this.scene?.componentStorageManager) {
|
||||
const component = this.scene.componentStorageManager.getComponent(this.id, type);
|
||||
if (component) {
|
||||
return component as T;
|
||||
}
|
||||
// 从Scene存储获取
|
||||
if (!this.scene?.componentStorageManager) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Fallback 到本地存储
|
||||
return (this._localComponents.get(type) as T) || null;
|
||||
const component = this.scene.componentStorageManager.getComponent(this.id, type);
|
||||
return component as T | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -538,24 +531,27 @@ export class Entity {
|
||||
|
||||
const bitIndex = ComponentRegistry.getBitIndex(componentType);
|
||||
|
||||
// 从本地存储移除
|
||||
this._localComponents.delete(componentType);
|
||||
|
||||
// 更新位掩码
|
||||
BitMask64Utils.clearBit(this._componentMask, bitIndex);
|
||||
|
||||
// 使缓存失效
|
||||
this._componentCache = null;
|
||||
|
||||
// 从 Scene 存储移除
|
||||
if (this.scene && this.scene.componentStorageManager) {
|
||||
// 从Scene存储移除
|
||||
if (this.scene?.componentStorageManager) {
|
||||
this.scene.componentStorageManager.removeComponent(this.id, componentType);
|
||||
}
|
||||
|
||||
if (this.scene?.referenceTracker) {
|
||||
this.scene.referenceTracker.clearComponentReferences(component);
|
||||
}
|
||||
|
||||
if (component.onRemovedFromEntity) {
|
||||
component.onRemovedFromEntity();
|
||||
}
|
||||
|
||||
component.entityId = null;
|
||||
|
||||
if (Entity.eventBus) {
|
||||
Entity.eventBus.emitComponentRemoved({
|
||||
timestamp: Date.now(),
|
||||
@@ -593,9 +589,6 @@ export class Entity {
|
||||
public removeAllComponents(): void {
|
||||
const componentsToRemove = [...this.components];
|
||||
|
||||
// 清除本地存储
|
||||
this._localComponents.clear();
|
||||
|
||||
// 清除位掩码
|
||||
BitMask64Utils.clear(this._componentMask);
|
||||
|
||||
@@ -605,7 +598,7 @@ export class Entity {
|
||||
for (const component of componentsToRemove) {
|
||||
const componentType = component.constructor as ComponentType;
|
||||
|
||||
if (this.scene && this.scene.componentStorageManager) {
|
||||
if (this.scene?.componentStorageManager) {
|
||||
this.scene.componentStorageManager.removeComponent(this.id, componentType);
|
||||
}
|
||||
|
||||
@@ -874,8 +867,8 @@ export class Entity {
|
||||
|
||||
/**
|
||||
* 销毁实体
|
||||
*
|
||||
* 移除所有组件、子实体并标记为已销毁。
|
||||
*
|
||||
* 移除所有组件、子实体并标记为已销毁
|
||||
*/
|
||||
public destroy(): void {
|
||||
if (this._isDestroyed) {
|
||||
@@ -883,29 +876,71 @@ export class Entity {
|
||||
}
|
||||
|
||||
this._isDestroyed = true;
|
||||
|
||||
|
||||
if (this.scene && this.scene.referenceTracker) {
|
||||
this.scene.referenceTracker.clearReferencesTo(this.id);
|
||||
this.scene.referenceTracker.unregisterEntityScene(this.id);
|
||||
}
|
||||
|
||||
const childrenToDestroy = [...this._children];
|
||||
for (const child of childrenToDestroy) {
|
||||
child.destroy();
|
||||
}
|
||||
|
||||
|
||||
if (this._parent) {
|
||||
this._parent.removeChild(this);
|
||||
}
|
||||
|
||||
|
||||
this.removeAllComponents();
|
||||
|
||||
|
||||
if (this.scene) {
|
||||
if (this.scene.querySystem) {
|
||||
this.scene.querySystem.removeEntity(this);
|
||||
}
|
||||
|
||||
|
||||
if (this.scene.entities) {
|
||||
this.scene.entities.remove(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量销毁所有子实体
|
||||
*/
|
||||
public destroyAllChildren(): void {
|
||||
if (this._children.length === 0) return;
|
||||
|
||||
const scene = this.scene;
|
||||
const toDestroy: Entity[] = [];
|
||||
|
||||
const collectChildren = (entity: Entity) => {
|
||||
for (const child of entity._children) {
|
||||
toDestroy.push(child);
|
||||
collectChildren(child);
|
||||
}
|
||||
};
|
||||
collectChildren(this);
|
||||
|
||||
for (const entity of toDestroy) {
|
||||
entity._isDestroyed = true;
|
||||
}
|
||||
|
||||
for (const entity of toDestroy) {
|
||||
entity.removeAllComponents();
|
||||
}
|
||||
|
||||
if (scene) {
|
||||
for (const entity of toDestroy) {
|
||||
scene.entities.remove(entity);
|
||||
scene.querySystem.removeEntity(entity);
|
||||
}
|
||||
|
||||
scene.clearSystemEntityCaches();
|
||||
}
|
||||
|
||||
this._children.length = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 比较实体
|
||||
*
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { Entity } from './Entity';
|
||||
import { EntityList } from './Utils/EntityList';
|
||||
import { EntityProcessorList } from './Utils/EntityProcessorList';
|
||||
import { IdentifierPool } from './Utils/IdentifierPool';
|
||||
import { EntitySystem } from './Systems/EntitySystem';
|
||||
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';
|
||||
|
||||
/**
|
||||
* 场景接口定义
|
||||
@@ -41,12 +45,7 @@ export interface IScene {
|
||||
* 场景中的实体集合
|
||||
*/
|
||||
readonly entities: EntityList;
|
||||
|
||||
/**
|
||||
* 实体系统处理器集合
|
||||
*/
|
||||
readonly entityProcessors: EntityProcessorList;
|
||||
|
||||
|
||||
/**
|
||||
* 标识符池
|
||||
*/
|
||||
@@ -67,6 +66,18 @@ export interface IScene {
|
||||
*/
|
||||
readonly eventSystem: TypeSafeEventSystem;
|
||||
|
||||
/**
|
||||
* 引用追踪器
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
import { Entity } from './Entity';
|
||||
import { EntityList } from './Utils/EntityList';
|
||||
import { EntityProcessorList } from './Utils/EntityProcessorList';
|
||||
import { IdentifierPool } from './Utils/IdentifierPool';
|
||||
import { EntitySystem } from './Systems/EntitySystem';
|
||||
import { ComponentStorageManager, ComponentRegistry } from './Core/ComponentStorage';
|
||||
import { QuerySystem } from './Core/QuerySystem';
|
||||
import { TypeSafeEventSystem } from './Core/EventSystem';
|
||||
import { EventBus } from './Core/EventBus';
|
||||
import { ReferenceTracker } from './Core/ReferenceTracker';
|
||||
import { IScene, ISceneConfig } from './IScene';
|
||||
import { getComponentInstanceTypeName, getSystemInstanceTypeName } from './Decorators';
|
||||
import { getComponentInstanceTypeName, getSystemInstanceTypeName, getSystemMetadata } from "./Decorators";
|
||||
import { TypedQueryBuilder } from './Core/Query/TypedQuery';
|
||||
import { SceneSerializer, SceneSerializationOptions, SceneDeserializationOptions } from './Serialization/SceneSerializer';
|
||||
import { IncrementalSerializer, IncrementalSnapshot, IncrementalSerializationOptions } from './Serialization/IncrementalSerializer';
|
||||
import { ComponentPoolManager } from './Core/ComponentPool';
|
||||
import { PerformanceMonitor } from '../Utils/PerformanceMonitor';
|
||||
import { ServiceContainer, type ServiceType } from '../Core/ServiceContainer';
|
||||
import { createInstance, isInjectable, injectProperties } from '../Core/DI';
|
||||
import { isUpdatable, getUpdatableMetadata } from '../Core/DI/Decorators';
|
||||
import { createLogger } from '../Utils/Logger';
|
||||
|
||||
/**
|
||||
* 游戏场景默认实现类
|
||||
*
|
||||
*
|
||||
* 实现IScene接口,提供场景的基础功能。
|
||||
* 推荐使用组合而非继承的方式来构建自定义场景。
|
||||
*/
|
||||
@@ -41,12 +47,6 @@ export class Scene implements IScene {
|
||||
*/
|
||||
public readonly entities: EntityList;
|
||||
|
||||
/**
|
||||
* 实体系统处理器集合
|
||||
*
|
||||
* 管理场景内所有实体系统的执行。
|
||||
*/
|
||||
public readonly entityProcessors: EntityProcessorList;
|
||||
|
||||
/**
|
||||
* 实体ID池
|
||||
@@ -71,36 +71,117 @@ export class Scene implements IScene {
|
||||
|
||||
/**
|
||||
* 事件系统
|
||||
*
|
||||
*
|
||||
* 类型安全的事件系统。
|
||||
*/
|
||||
public readonly eventSystem: TypeSafeEventSystem;
|
||||
|
||||
|
||||
/**
|
||||
* 引用追踪器
|
||||
*
|
||||
* 追踪Component中对Entity的引用,当Entity销毁时自动清理引用。
|
||||
*/
|
||||
public readonly referenceTracker: ReferenceTracker;
|
||||
|
||||
/**
|
||||
* 服务容器
|
||||
*
|
||||
* 场景级别的依赖注入容器,用于管理EntitySystem和其他服务的生命周期。
|
||||
* 每个Scene拥有独立的服务容器,实现场景间的隔离。
|
||||
*/
|
||||
private readonly _services: ServiceContainer;
|
||||
|
||||
/**
|
||||
* 日志记录器
|
||||
*/
|
||||
private readonly logger: ReturnType<typeof createLogger>;
|
||||
|
||||
/**
|
||||
* 性能监控器缓存
|
||||
*
|
||||
* 用于监控场景和系统的性能。从 ServiceContainer 获取。
|
||||
*/
|
||||
private _performanceMonitor: PerformanceMonitor | null = null;
|
||||
|
||||
/**
|
||||
* 场景是否已开始运行
|
||||
*/
|
||||
private _didSceneBegin: boolean = false;
|
||||
|
||||
/**
|
||||
* 获取系统列表(兼容性属性)
|
||||
* 获取场景中所有已注册的EntitySystem
|
||||
*
|
||||
* 按updateOrder排序。
|
||||
*
|
||||
* @returns 系统列表
|
||||
*/
|
||||
public get systems(): EntitySystem[] {
|
||||
return this.entityProcessors.processors;
|
||||
// 从ServiceContainer获取所有EntitySystem实例
|
||||
const services = this._services.getAll();
|
||||
const systems: EntitySystem[] = [];
|
||||
|
||||
for (const service of services) {
|
||||
if (service instanceof EntitySystem) {
|
||||
systems.push(service);
|
||||
}
|
||||
}
|
||||
|
||||
// 按updateOrder排序
|
||||
systems.sort((a, b) => a.updateOrder - b.updateOrder);
|
||||
|
||||
return systems;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过类型获取System实例
|
||||
*
|
||||
* @param systemType System类型
|
||||
* @returns System实例,如果未找到则返回null
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const physics = scene.getSystem(PhysicsSystem);
|
||||
* if (physics) {
|
||||
* physics.doSomething();
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
public getSystem<T extends EntitySystem>(systemType: ServiceType<T>): T | null {
|
||||
return this._services.tryResolve(systemType) as T | null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取场景的服务容器
|
||||
*
|
||||
* 用于注册和解析场景级别的服务(如EntitySystem)。
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 注册服务
|
||||
* scene.services.registerSingleton(PhysicsSystem);
|
||||
*
|
||||
* // 解析服务
|
||||
* const physics = scene.services.resolve(PhysicsSystem);
|
||||
* ```
|
||||
*/
|
||||
public get services(): ServiceContainer {
|
||||
return this._services;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建场景实例
|
||||
*/
|
||||
constructor(config?: ISceneConfig) {
|
||||
this.entities = new EntityList(this);
|
||||
this.entityProcessors = new EntityProcessorList();
|
||||
this.identifierPool = new IdentifierPool();
|
||||
this.componentStorageManager = new ComponentStorageManager();
|
||||
this.querySystem = new QuerySystem();
|
||||
this.eventSystem = new TypeSafeEventSystem();
|
||||
this.referenceTracker = new ReferenceTracker();
|
||||
this._services = new ServiceContainer();
|
||||
this.logger = createLogger('Scene');
|
||||
|
||||
// 应用配置
|
||||
if (config?.name) {
|
||||
this.name = config.name;
|
||||
}
|
||||
@@ -108,7 +189,7 @@ export class Scene implements IScene {
|
||||
if (!Entity.eventBus) {
|
||||
Entity.eventBus = new EventBus(false);
|
||||
}
|
||||
|
||||
|
||||
if (Entity.eventBus) {
|
||||
Entity.eventBus.onComponentAdded((data: unknown) => {
|
||||
this.eventSystem.emitSync('component:added', data);
|
||||
@@ -116,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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化场景
|
||||
*
|
||||
@@ -146,10 +240,6 @@ export class Scene implements IScene {
|
||||
* 这个方法会启动场景。它将启动实体处理器等,并调用onStart方法。
|
||||
*/
|
||||
public begin() {
|
||||
// 启动实体处理器
|
||||
if (this.entityProcessors != null)
|
||||
this.entityProcessors.begin();
|
||||
|
||||
// 标记场景已开始运行并调用onStart方法
|
||||
this._didSceneBegin = true;
|
||||
this.onStart();
|
||||
@@ -173,9 +263,8 @@ export class Scene implements IScene {
|
||||
// 清空组件存储
|
||||
this.componentStorageManager.clear();
|
||||
|
||||
// 结束实体处理器
|
||||
if (this.entityProcessors)
|
||||
this.entityProcessors.end();
|
||||
// 清空服务容器(会调用所有服务的dispose方法,包括所有EntitySystem)
|
||||
this._services.clear();
|
||||
|
||||
// 调用卸载方法
|
||||
this.unload();
|
||||
@@ -185,16 +274,32 @@ export class Scene implements IScene {
|
||||
* 更新场景
|
||||
*/
|
||||
public update() {
|
||||
// 更新实体列表(处理延迟操作)
|
||||
ComponentPoolManager.getInstance().update();
|
||||
|
||||
this.entities.updateLists();
|
||||
|
||||
// 更新实体处理器
|
||||
if (this.entityProcessors != null)
|
||||
this.entityProcessors.update();
|
||||
// 更新所有EntitySystem
|
||||
const systems = this.systems;
|
||||
for (const system of systems) {
|
||||
if (system.enabled) {
|
||||
try {
|
||||
system.update();
|
||||
} catch (error) {
|
||||
this.logger.error(`Error in system ${system.constructor.name}.update():`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新实体处理器后处理
|
||||
if (this.entityProcessors != null)
|
||||
this.entityProcessors.lateUpdate();
|
||||
// LateUpdate
|
||||
for (const system of systems) {
|
||||
if (system.enabled) {
|
||||
try {
|
||||
system.lateUpdate();
|
||||
} catch (error) {
|
||||
this.logger.error(`Error in system ${system.constructor.name}.lateUpdate():`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -214,7 +319,7 @@ export class Scene implements IScene {
|
||||
* 当实体或组件发生变化时调用
|
||||
*/
|
||||
public clearSystemEntityCaches(): void {
|
||||
for (const system of this.entityProcessors.processors) {
|
||||
for (const system of this.systems) {
|
||||
system.clearEntityCache();
|
||||
}
|
||||
}
|
||||
@@ -273,13 +378,35 @@ export class Scene implements IScene {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 批量销毁实体
|
||||
*/
|
||||
public destroyEntities(entities: Entity[]): void {
|
||||
if (entities.length === 0) return;
|
||||
|
||||
for (const entity of entities) {
|
||||
entity._isDestroyed = true;
|
||||
}
|
||||
|
||||
for (const entity of entities) {
|
||||
entity.removeAllComponents();
|
||||
}
|
||||
|
||||
for (const entity of entities) {
|
||||
this.entities.remove(entity);
|
||||
this.querySystem.removeEntity(entity);
|
||||
}
|
||||
|
||||
this.querySystem.clearCache();
|
||||
this.clearSystemEntityCaches();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从场景中删除所有实体
|
||||
*/
|
||||
public destroyAllEntities() {
|
||||
this.entities.removeAllEntities();
|
||||
|
||||
// 清理查询系统中的实体引用和缓存
|
||||
|
||||
this.querySystem.setEntities([]);
|
||||
}
|
||||
|
||||
@@ -395,18 +522,136 @@ export class Scene implements IScene {
|
||||
|
||||
/**
|
||||
* 在场景中添加一个EntitySystem处理器
|
||||
* @param processor 处理器
|
||||
*
|
||||
* 支持两种使用方式:
|
||||
* 1. 传入类型(推荐):自动使用DI创建实例,支持@Injectable和@Inject装饰器
|
||||
* 2. 传入实例:直接使用提供的实例
|
||||
*
|
||||
* @param systemTypeOrInstance 系统类型或系统实例
|
||||
* @returns 添加的处理器实例
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 方式1:传入类型,自动DI(推荐)
|
||||
* @Injectable()
|
||||
* class PhysicsSystem extends EntitySystem {
|
||||
* constructor(@Inject(CollisionSystem) private collision: CollisionSystem) {
|
||||
* super(Matcher.empty().all(Transform));
|
||||
* }
|
||||
* }
|
||||
* scene.addEntityProcessor(PhysicsSystem);
|
||||
*
|
||||
* // 方式2:传入实例
|
||||
* const system = new MySystem();
|
||||
* scene.addEntityProcessor(system);
|
||||
* ```
|
||||
*/
|
||||
public addEntityProcessor(processor: EntitySystem) {
|
||||
if (this.entityProcessors.processors.includes(processor)) {
|
||||
return processor;
|
||||
public addEntityProcessor<T extends EntitySystem>(
|
||||
systemTypeOrInstance: ServiceType<T> | T
|
||||
): T {
|
||||
let system: T;
|
||||
let constructor: any;
|
||||
|
||||
if (typeof systemTypeOrInstance === 'function') {
|
||||
constructor = systemTypeOrInstance;
|
||||
|
||||
if (this._services.isRegistered(constructor)) {
|
||||
const existingSystem = this._services.resolve(constructor) as T;
|
||||
this.logger.debug(`System ${constructor.name} already registered, returning existing instance`);
|
||||
return existingSystem;
|
||||
}
|
||||
|
||||
if (isInjectable(constructor)) {
|
||||
system = createInstance(constructor, this._services) as T;
|
||||
} else {
|
||||
system = new (constructor as any)() as T;
|
||||
}
|
||||
} else {
|
||||
system = systemTypeOrInstance;
|
||||
constructor = system.constructor;
|
||||
|
||||
if (this._services.isRegistered(constructor)) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
processor.scene = this;
|
||||
this.entityProcessors.add(processor);
|
||||
processor.initialize();
|
||||
processor.setUpdateOrder(this.entityProcessors.count - 1);
|
||||
return processor;
|
||||
|
||||
system.scene = this;
|
||||
|
||||
system.setPerformanceMonitor(this.performanceMonitor);
|
||||
|
||||
const metadata = getSystemMetadata(constructor);
|
||||
if (metadata?.updateOrder !== undefined) {
|
||||
system.setUpdateOrder(metadata.updateOrder);
|
||||
}
|
||||
if (metadata?.enabled !== undefined) {
|
||||
system.enabled = metadata.enabled;
|
||||
}
|
||||
|
||||
this._services.registerInstance(constructor, system);
|
||||
|
||||
injectProperties(system, this._services);
|
||||
|
||||
system.initialize();
|
||||
|
||||
this.logger.debug(`System ${constructor.name} registered and initialized`);
|
||||
|
||||
return system;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量注册EntitySystem到场景(使用DI)
|
||||
*
|
||||
* 自动按照依赖顺序注册多个System。
|
||||
* 所有System必须使用@Injectable装饰器标记。
|
||||
*
|
||||
* @param systemTypes System类型数组
|
||||
* @returns 注册的System实例数组
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @Injectable()
|
||||
* @ECSSystem('Collision', { updateOrder: 5 })
|
||||
* class CollisionSystem extends EntitySystem implements IService {
|
||||
* constructor() { super(Matcher.empty().all(Collider)); }
|
||||
* dispose() {}
|
||||
* }
|
||||
*
|
||||
* @Injectable()
|
||||
* @ECSSystem('Physics', { updateOrder: 10 })
|
||||
* class PhysicsSystem extends EntitySystem implements IService {
|
||||
* constructor(@Inject(CollisionSystem) private collision: CollisionSystem) {
|
||||
* super(Matcher.empty().all(Transform, RigidBody));
|
||||
* }
|
||||
* dispose() {}
|
||||
* }
|
||||
*
|
||||
* // 批量注册(自动解析依赖顺序)
|
||||
* scene.registerSystems([
|
||||
* CollisionSystem,
|
||||
* PhysicsSystem, // 自动注入CollisionSystem
|
||||
* RenderSystem
|
||||
* ]);
|
||||
* ```
|
||||
*/
|
||||
public registerSystems(systemTypes: Array<ServiceType<EntitySystem>>): EntitySystem[] {
|
||||
const registeredSystems: EntitySystem[] = [];
|
||||
|
||||
for (const systemType of systemTypes) {
|
||||
const system = this.addEntityProcessor(systemType);
|
||||
registeredSystems.push(system);
|
||||
}
|
||||
|
||||
return registeredSystems;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -421,8 +666,13 @@ export class Scene implements IScene {
|
||||
* 从场景中删除EntitySystem处理器
|
||||
* @param processor 要删除的处理器
|
||||
*/
|
||||
public removeEntityProcessor(processor: EntitySystem) {
|
||||
this.entityProcessors.remove(processor);
|
||||
public removeEntityProcessor(processor: EntitySystem): void {
|
||||
const constructor = processor.constructor as any;
|
||||
|
||||
// 从ServiceContainer移除
|
||||
this._services.unregister(constructor);
|
||||
|
||||
// 重置System状态
|
||||
processor.reset();
|
||||
}
|
||||
|
||||
@@ -436,10 +686,24 @@ export class Scene implements IScene {
|
||||
|
||||
/**
|
||||
* 获取指定类型的EntitySystem处理器
|
||||
*
|
||||
* @deprecated 推荐使用依赖注入代替此方法。使用 `scene.services.resolve(SystemType)` 或在System构造函数中使用 `@Inject(SystemType)` 装饰器。
|
||||
*
|
||||
* @param type 处理器类型
|
||||
* @returns 处理器实例,如果未找到则返回null
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @Injectable()
|
||||
* class MySystem extends EntitySystem {
|
||||
* constructor(@Inject(PhysicsSystem) private physics: PhysicsSystem) {
|
||||
* super();
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
public getEntityProcessor<T extends EntitySystem>(type: new (...args: unknown[]) => T): T | null {
|
||||
return this.entityProcessors.getProcessor(type);
|
||||
return this._services.tryResolve(type as any) as T | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -452,7 +716,7 @@ export class Scene implements IScene {
|
||||
} {
|
||||
return {
|
||||
entityCount: this.entities.count,
|
||||
processorCount: this.entityProcessors.count,
|
||||
processorCount: this.systems.length,
|
||||
componentStorageStats: this.componentStorageManager.getAllStats()
|
||||
};
|
||||
}
|
||||
@@ -479,10 +743,11 @@ export class Scene implements IScene {
|
||||
}>;
|
||||
componentStats: Map<string, any>;
|
||||
} {
|
||||
const systems = this.systems;
|
||||
return {
|
||||
name: this.name || this.constructor.name,
|
||||
entityCount: this.entities.count,
|
||||
processorCount: this.entityProcessors.count,
|
||||
processorCount: systems.length,
|
||||
isRunning: this._didSceneBegin,
|
||||
entities: this.entities.buffer.map(entity => ({
|
||||
name: entity.name,
|
||||
@@ -490,7 +755,7 @@ export class Scene implements IScene {
|
||||
componentCount: entity.components.length,
|
||||
componentTypes: entity.components.map(c => getComponentInstanceTypeName(c))
|
||||
})),
|
||||
processors: this.entityProcessors.processors.map(processor => ({
|
||||
processors: systems.map(processor => ({
|
||||
name: getSystemInstanceTypeName(processor),
|
||||
updateOrder: processor.updateOrder,
|
||||
entityCount: (processor as any)._entities?.length || 0
|
||||
@@ -502,10 +767,10 @@ export class Scene implements IScene {
|
||||
/**
|
||||
* 序列化场景
|
||||
*
|
||||
* 将场景及其所有实体、组件序列化为JSON字符串或二进制Buffer
|
||||
* 将场景及其所有实体、组件序列化为JSON字符串或二进制Uint8Array
|
||||
*
|
||||
* @param options 序列化选项
|
||||
* @returns 序列化后的数据(JSON字符串或二进制Buffer)
|
||||
* @returns 序列化后的数据(JSON字符串或二进制Uint8Array)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
@@ -521,7 +786,7 @@ export class Scene implements IScene {
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
public serialize(options?: SceneSerializationOptions): string | Buffer {
|
||||
public serialize(options?: SceneSerializationOptions): string | Uint8Array {
|
||||
return SceneSerializer.serialize(this, options);
|
||||
}
|
||||
|
||||
@@ -530,7 +795,7 @@ export class Scene implements IScene {
|
||||
*
|
||||
* 从序列化数据恢复场景状态
|
||||
*
|
||||
* @param saveData 序列化的数据(JSON字符串或二进制Buffer)
|
||||
* @param saveData 序列化的数据(JSON字符串或二进制Uint8Array)
|
||||
* @param options 反序列化选项
|
||||
*
|
||||
* @example
|
||||
@@ -546,7 +811,7 @@ export class Scene implements IScene {
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
public deserialize(saveData: string | Buffer, options?: SceneDeserializationOptions): void {
|
||||
public deserialize(saveData: string | Uint8Array, options?: SceneDeserializationOptions): void {
|
||||
SceneSerializer.deserialize(this, saveData, options);
|
||||
}
|
||||
|
||||
@@ -618,7 +883,7 @@ export class Scene implements IScene {
|
||||
/**
|
||||
* 应用增量变更到场景
|
||||
*
|
||||
* @param incremental 增量快照数据(IncrementalSnapshot对象、JSON字符串或二进制Buffer)
|
||||
* @param incremental 增量快照数据(IncrementalSnapshot对象、JSON字符串或二进制Uint8Array)
|
||||
* @param componentRegistry 组件类型注册表(可选,默认使用全局注册表)
|
||||
*
|
||||
* @example
|
||||
@@ -630,18 +895,21 @@ export class Scene implements IScene {
|
||||
* const jsonData = IncrementalSerializer.serializeIncremental(snapshot, { format: 'json' });
|
||||
* scene.applyIncremental(jsonData);
|
||||
*
|
||||
* // 从二进制Buffer应用
|
||||
* // 从二进制Uint8Array应用
|
||||
* const binaryData = IncrementalSerializer.serializeIncremental(snapshot, { format: 'binary' });
|
||||
* scene.applyIncremental(binaryData);
|
||||
* ```
|
||||
*/
|
||||
public applyIncremental(
|
||||
incremental: IncrementalSnapshot | string | Buffer,
|
||||
incremental: IncrementalSnapshot | string | Uint8Array,
|
||||
componentRegistry?: Map<string, any>
|
||||
): void {
|
||||
const snapshot = (typeof incremental === 'string' || Buffer.isBuffer(incremental))
|
||||
? IncrementalSerializer.deserializeIncremental(incremental)
|
||||
: incremental;
|
||||
const isSerializedData = typeof incremental === 'string' ||
|
||||
incremental instanceof Uint8Array;
|
||||
|
||||
const snapshot = isSerializedData
|
||||
? IncrementalSerializer.deserializeIncremental(incremental as string | Uint8Array)
|
||||
: incremental as IncrementalSnapshot;
|
||||
|
||||
const registry = componentRegistry || ComponentRegistry.getAllComponentNames() as Map<string, any>;
|
||||
|
||||
|
||||
274
packages/core/src/ECS/SceneManager.ts
Normal file
274
packages/core/src/ECS/SceneManager.ts
Normal file
@@ -0,0 +1,274 @@
|
||||
import { IScene } from './IScene';
|
||||
import { ECSFluentAPI, createECSAPI } from './Core/FluentAPI';
|
||||
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';
|
||||
|
||||
/**
|
||||
* 单场景管理器
|
||||
*
|
||||
* 适用场景:
|
||||
* - 单人游戏
|
||||
* - 简单场景切换
|
||||
* - 不需要多World隔离的项目
|
||||
*
|
||||
* 特点:
|
||||
* - 轻量级,零额外开销
|
||||
* - 简单直观的API
|
||||
* - 支持延迟场景切换
|
||||
* - 自动管理ECS API
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 初始化Core
|
||||
* Core.create({ debug: true });
|
||||
*
|
||||
* // 创建场景管理器
|
||||
* const sceneManager = new SceneManager();
|
||||
*
|
||||
* // 设置场景
|
||||
* class GameScene extends Scene {
|
||||
* initialize() {
|
||||
* const player = this.createEntity('Player');
|
||||
* player.addComponent(new Transform(100, 100));
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* sceneManager.setScene(new GameScene());
|
||||
*
|
||||
* // 游戏循环
|
||||
* function gameLoop(deltaTime: number) {
|
||||
* Core.update(deltaTime); // 更新全局服务
|
||||
* sceneManager.update(); // 更新场景
|
||||
* }
|
||||
*
|
||||
* // 延迟切换场景(下一帧生效)
|
||||
* sceneManager.loadScene(new MenuScene());
|
||||
* ```
|
||||
*/
|
||||
export class SceneManager implements IService {
|
||||
/**
|
||||
* 内部默认World
|
||||
*/
|
||||
private _defaultWorld: World;
|
||||
|
||||
/**
|
||||
* 待切换的下一个场景(延迟切换用)
|
||||
*/
|
||||
private _nextScene: IScene | null = null;
|
||||
|
||||
/**
|
||||
* ECS流式API
|
||||
*/
|
||||
private _ecsAPI: ECSFluentAPI | null = null;
|
||||
|
||||
/**
|
||||
* 日志器
|
||||
*/
|
||||
private _logger = createLogger('SceneManager');
|
||||
|
||||
/**
|
||||
* 场景切换回调函数
|
||||
*/
|
||||
private _onSceneChangedCallback?: () => void;
|
||||
|
||||
/**
|
||||
* 性能监控器(从 Core 注入)
|
||||
*/
|
||||
private _performanceMonitor: PerformanceMonitor | null = null;
|
||||
|
||||
/**
|
||||
* 默认场景ID
|
||||
*/
|
||||
private static readonly DEFAULT_SCENE_ID = '__main__';
|
||||
|
||||
constructor(performanceMonitor?: PerformanceMonitor) {
|
||||
this._defaultWorld = new World({ name: '__default__' });
|
||||
this._defaultWorld.start();
|
||||
this._performanceMonitor = performanceMonitor || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置场景切换回调
|
||||
*
|
||||
* @param callback 场景切换时的回调函数
|
||||
* @internal
|
||||
*/
|
||||
public setSceneChangedCallback(callback: () => void): void {
|
||||
this._onSceneChangedCallback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前场景(立即切换)
|
||||
*
|
||||
* 会自动处理旧场景的结束和新场景的初始化。
|
||||
*
|
||||
* @param scene - 要设置的场景实例
|
||||
* @returns 返回设置的场景实例,便于链式调用
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const gameScene = sceneManager.setScene(new GameScene());
|
||||
* console.log(gameScene.name); // 可以立即使用返回的场景
|
||||
* ```
|
||||
*/
|
||||
public setScene<T extends IScene>(scene: T): T {
|
||||
// 移除旧场景
|
||||
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);
|
||||
|
||||
// 重建ECS API
|
||||
if (scene.querySystem && scene.eventSystem) {
|
||||
this._ecsAPI = createECSAPI(scene, scene.querySystem, scene.eventSystem);
|
||||
} else {
|
||||
this._ecsAPI = null;
|
||||
}
|
||||
|
||||
// 触发场景切换回调
|
||||
Time.sceneChanged();
|
||||
|
||||
// 通知调试管理器(通过回调)
|
||||
if (this._onSceneChangedCallback) {
|
||||
this._onSceneChangedCallback();
|
||||
}
|
||||
|
||||
this._logger.info(`Scene changed to: ${scene.name}`);
|
||||
|
||||
return scene;
|
||||
}
|
||||
|
||||
/**
|
||||
* 延迟加载场景(下一帧切换)
|
||||
*
|
||||
* 场景不会立即切换,而是在下一次调用 update() 时切换。
|
||||
* 这对于避免在当前帧的中途切换场景很有用。
|
||||
*
|
||||
* @param scene - 要加载的场景实例
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 在某个System中触发场景切换
|
||||
* class GameOverSystem extends EntitySystem {
|
||||
* process(entities: readonly Entity[]) {
|
||||
* if (playerHealth <= 0) {
|
||||
* sceneManager.loadScene(new GameOverScene());
|
||||
* // 当前帧继续执行,场景将在下一帧切换
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
public loadScene<T extends IScene>(scene: T): void {
|
||||
this._nextScene = scene;
|
||||
this._logger.info(`Scheduled scene load: ${scene.name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前活跃的场景
|
||||
*
|
||||
* @returns 当前场景实例,如果没有场景则返回null
|
||||
*/
|
||||
public get currentScene(): IScene | null {
|
||||
return this._defaultWorld.getScene(SceneManager.DEFAULT_SCENE_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取ECS流式API
|
||||
*
|
||||
* 提供便捷的实体查询、事件发射等功能。
|
||||
*
|
||||
* @returns ECS API实例,如果当前没有场景则返回null
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const api = sceneManager.api;
|
||||
* if (api) {
|
||||
* // 查询所有敌人
|
||||
* const enemies = api.find(Enemy, Transform);
|
||||
*
|
||||
* // 发射事件
|
||||
* api.emit('game:start', { level: 1 });
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
public get api(): ECSFluentAPI | null {
|
||||
return this._ecsAPI;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新场景
|
||||
*
|
||||
* 应该在每帧的游戏循环中调用。
|
||||
* 会自动处理延迟场景切换。
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* function gameLoop(deltaTime: number) {
|
||||
* Core.update(deltaTime);
|
||||
* sceneManager.update(); // 每帧调用
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
public update(): void {
|
||||
// 处理延迟场景切换
|
||||
if (this._nextScene) {
|
||||
this.setScene(this._nextScene);
|
||||
this._nextScene = null;
|
||||
}
|
||||
|
||||
// 通过 World 统一更新
|
||||
this._defaultWorld.updateGlobalSystems();
|
||||
this._defaultWorld.updateScenes();
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁场景管理器
|
||||
*
|
||||
* 会自动结束当前场景并清理所有资源。
|
||||
* 通常在应用程序关闭时调用。
|
||||
*/
|
||||
public destroy(): void {
|
||||
this._logger.info('SceneManager destroying');
|
||||
|
||||
this._defaultWorld.destroy();
|
||||
this._nextScene = null;
|
||||
this._ecsAPI = null;
|
||||
|
||||
this._logger.info('SceneManager destroyed');
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有活跃场景
|
||||
*
|
||||
* @returns 如果有活跃场景返回true,否则返回false
|
||||
*/
|
||||
public get hasScene(): boolean {
|
||||
return this._defaultWorld.getScene(SceneManager.DEFAULT_SCENE_ID) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有待切换的场景
|
||||
*
|
||||
* @returns 如果有待切换场景返回true,否则返回false
|
||||
*/
|
||||
public get hasPendingScene(): boolean {
|
||||
return this._nextScene !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源(IService接口)
|
||||
*/
|
||||
public dispose(): void {
|
||||
this.destroy();
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { Entity } from '../Entity';
|
||||
import { Component } from '../Component';
|
||||
import { ComponentType } from '../Core/ComponentStorage';
|
||||
import { ComponentSerializer, SerializedComponent } from './ComponentSerializer';
|
||||
import { IScene } from '../IScene';
|
||||
|
||||
/**
|
||||
* 序列化后的实体数据
|
||||
@@ -108,18 +109,25 @@ export class EntitySerializer {
|
||||
* @param componentRegistry 组件类型注册表
|
||||
* @param idGenerator 实体ID生成器(用于生成新ID或保持原ID)
|
||||
* @param preserveIds 是否保持原始ID(默认false)
|
||||
* @param scene 目标场景(可选,用于设置entity.scene以支持添加组件)
|
||||
* @returns 反序列化后的实体
|
||||
*/
|
||||
public static deserialize(
|
||||
serializedEntity: SerializedEntity,
|
||||
componentRegistry: Map<string, ComponentType>,
|
||||
idGenerator: () => number,
|
||||
preserveIds: boolean = false
|
||||
preserveIds: boolean = false,
|
||||
scene?: IScene
|
||||
): Entity {
|
||||
// 创建实体(使用原始ID或新生成的ID)
|
||||
const entityId = preserveIds ? serializedEntity.id : idGenerator();
|
||||
const entity = new Entity(serializedEntity.name, entityId);
|
||||
|
||||
// 如果提供了scene,先设置entity.scene以支持添加组件
|
||||
if (scene) {
|
||||
entity.scene = scene;
|
||||
}
|
||||
|
||||
// 恢复实体属性
|
||||
entity.tag = serializedEntity.tag;
|
||||
entity.active = serializedEntity.active;
|
||||
@@ -142,7 +150,8 @@ export class EntitySerializer {
|
||||
childData,
|
||||
componentRegistry,
|
||||
idGenerator,
|
||||
preserveIds
|
||||
preserveIds,
|
||||
scene
|
||||
);
|
||||
entity.addChild(childEntity);
|
||||
}
|
||||
@@ -181,13 +190,15 @@ export class EntitySerializer {
|
||||
* @param componentRegistry 组件类型注册表
|
||||
* @param idGenerator 实体ID生成器
|
||||
* @param preserveIds 是否保持原始ID
|
||||
* @param scene 目标场景(可选,用于设置entity.scene以支持添加组件)
|
||||
* @returns 反序列化后的实体数组
|
||||
*/
|
||||
public static deserializeEntities(
|
||||
serializedEntities: SerializedEntity[],
|
||||
componentRegistry: Map<string, ComponentType>,
|
||||
idGenerator: () => number,
|
||||
preserveIds: boolean = false
|
||||
preserveIds: boolean = false,
|
||||
scene?: IScene
|
||||
): Entity[] {
|
||||
const result: Entity[] = [];
|
||||
|
||||
@@ -196,7 +207,8 @@ export class EntitySerializer {
|
||||
serialized,
|
||||
componentRegistry,
|
||||
idGenerator,
|
||||
preserveIds
|
||||
preserveIds,
|
||||
scene
|
||||
);
|
||||
result.push(entity);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Component } from '../Component';
|
||||
import { ComponentSerializer, SerializedComponent } from './ComponentSerializer';
|
||||
import { SerializedEntity } from './EntitySerializer';
|
||||
import { ComponentType } from '../Core/ComponentStorage';
|
||||
import * as msgpack from 'msgpack-lite';
|
||||
import { encode, decode } from '@msgpack/msgpack';
|
||||
|
||||
/**
|
||||
* 变更操作类型
|
||||
@@ -609,7 +609,7 @@ export class IncrementalSerializer {
|
||||
*
|
||||
* @param incremental 增量快照
|
||||
* @param options 序列化选项
|
||||
* @returns 序列化后的数据(JSON字符串或二进制Buffer)
|
||||
* @returns 序列化后的数据(JSON字符串或二进制Uint8Array)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
@@ -631,7 +631,7 @@ export class IncrementalSerializer {
|
||||
public static serializeIncremental(
|
||||
incremental: IncrementalSnapshot,
|
||||
options?: { format?: IncrementalSerializationFormat; pretty?: boolean }
|
||||
): string | Buffer {
|
||||
): string | Uint8Array {
|
||||
const opts = {
|
||||
format: 'json' as IncrementalSerializationFormat,
|
||||
pretty: false,
|
||||
@@ -639,7 +639,7 @@ export class IncrementalSerializer {
|
||||
};
|
||||
|
||||
if (opts.format === 'binary') {
|
||||
return msgpack.encode(incremental);
|
||||
return encode(incremental);
|
||||
} else {
|
||||
return opts.pretty
|
||||
? JSON.stringify(incremental, null, 2)
|
||||
@@ -650,7 +650,7 @@ export class IncrementalSerializer {
|
||||
/**
|
||||
* 反序列化增量快照
|
||||
*
|
||||
* @param data 序列化的数据(JSON字符串或二进制Buffer)
|
||||
* @param data 序列化的数据(JSON字符串或二进制Uint8Array)
|
||||
* @returns 增量快照
|
||||
*
|
||||
* @example
|
||||
@@ -662,13 +662,13 @@ export class IncrementalSerializer {
|
||||
* const snapshot = IncrementalSerializer.deserializeIncremental(buffer);
|
||||
* ```
|
||||
*/
|
||||
public static deserializeIncremental(data: string | Buffer): IncrementalSnapshot {
|
||||
public static deserializeIncremental(data: string | Uint8Array): IncrementalSnapshot {
|
||||
if (typeof data === 'string') {
|
||||
// JSON格式
|
||||
return JSON.parse(data);
|
||||
} else {
|
||||
// 二进制格式(MessagePack)
|
||||
return msgpack.decode(data);
|
||||
return decode(data) as IncrementalSnapshot;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -687,7 +687,15 @@ export class IncrementalSerializer {
|
||||
|
||||
if (typeof data === 'string') {
|
||||
// JSON格式:计算UTF-8编码后的字节数
|
||||
return Buffer.byteLength(data, 'utf8');
|
||||
// 使用 Blob 来计算浏览器和 Node.js 环境兼容的字节数
|
||||
if (typeof Blob !== 'undefined') {
|
||||
return new Blob([data]).size;
|
||||
} else if (typeof Buffer !== 'undefined') {
|
||||
return Buffer.byteLength(data, 'utf8');
|
||||
} else {
|
||||
// 回退方案:粗略估算(不精确,但可用)
|
||||
return new TextEncoder().encode(data).length;
|
||||
}
|
||||
} else {
|
||||
// 二进制格式:直接返回Buffer长度
|
||||
return data.length;
|
||||
|
||||
@@ -11,7 +11,7 @@ import { ComponentType, ComponentRegistry } from '../Core/ComponentStorage';
|
||||
import { EntitySerializer, SerializedEntity } from './EntitySerializer';
|
||||
import { getComponentTypeName } from '../Decorators';
|
||||
import { getSerializationMetadata } from './SerializationDecorators';
|
||||
import * as msgpack from 'msgpack-lite';
|
||||
import { encode, decode } from '@msgpack/msgpack';
|
||||
|
||||
/**
|
||||
* 场景序列化格式
|
||||
@@ -154,9 +154,9 @@ export class SceneSerializer {
|
||||
*
|
||||
* @param scene 要序列化的场景
|
||||
* @param options 序列化选项
|
||||
* @returns 序列化后的数据(JSON字符串或二进制Buffer)
|
||||
* @returns 序列化后的数据(JSON字符串或二进制Uint8Array)
|
||||
*/
|
||||
public static serialize(scene: IScene, options?: SceneSerializationOptions): string | Buffer {
|
||||
public static serialize(scene: IScene, options?: SceneSerializationOptions): string | Uint8Array {
|
||||
const opts: SceneSerializationOptions = {
|
||||
systems: false,
|
||||
format: 'json',
|
||||
@@ -207,7 +207,7 @@ export class SceneSerializer {
|
||||
: JSON.stringify(serializedScene);
|
||||
} else {
|
||||
// 二进制格式(使用 MessagePack)
|
||||
return msgpack.encode(serializedScene);
|
||||
return encode(serializedScene);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,12 +215,12 @@ export class SceneSerializer {
|
||||
* 反序列化场景
|
||||
*
|
||||
* @param scene 目标场景
|
||||
* @param saveData 序列化的数据(JSON字符串或二进制Buffer)
|
||||
* @param saveData 序列化的数据(JSON字符串或二进制Uint8Array)
|
||||
* @param options 反序列化选项
|
||||
*/
|
||||
public static deserialize(
|
||||
scene: IScene,
|
||||
saveData: string | Buffer,
|
||||
saveData: string | Uint8Array,
|
||||
options?: SceneDeserializationOptions
|
||||
): void {
|
||||
const opts: SceneDeserializationOptions = {
|
||||
@@ -237,7 +237,7 @@ export class SceneSerializer {
|
||||
serializedScene = JSON.parse(saveData);
|
||||
} else {
|
||||
// 二进制格式(MessagePack)
|
||||
serializedScene = msgpack.decode(saveData);
|
||||
serializedScene = decode(saveData) as SerializedScene;
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to parse save data: ${error}`);
|
||||
@@ -269,20 +269,44 @@ export class SceneSerializer {
|
||||
serializedScene.entities,
|
||||
componentRegistry,
|
||||
idGenerator,
|
||||
opts.preserveIds || false
|
||||
opts.preserveIds || false,
|
||||
scene
|
||||
);
|
||||
|
||||
// 将实体添加到场景
|
||||
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); // 递归处理子实体的子实体
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化场景自定义数据
|
||||
*
|
||||
|
||||
@@ -8,6 +8,7 @@ import { getSystemInstanceTypeName } from '../Decorators';
|
||||
import { createLogger } from '../../Utils/Logger';
|
||||
import type { EventListenerConfig, TypeSafeEventSystem, EventHandler } from '../Core/EventSystem';
|
||||
import type { ComponentConstructor, ComponentInstance } from '../../Types/TypeHelpers';
|
||||
import type { IService } from '../../Core/ServiceContainer';
|
||||
|
||||
/**
|
||||
* 事件监听器记录
|
||||
@@ -35,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 {
|
||||
@@ -50,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 {
|
||||
@@ -65,10 +66,10 @@ interface EventListenerRecord {
|
||||
*/
|
||||
export abstract class EntitySystem<
|
||||
TComponents extends readonly ComponentConstructor[] = []
|
||||
> implements ISystemBase {
|
||||
> implements ISystemBase, IService {
|
||||
private _updateOrder: number;
|
||||
private _enabled: boolean;
|
||||
private _performanceMonitor: PerformanceMonitor;
|
||||
private _performanceMonitor: PerformanceMonitor | null;
|
||||
private _systemName: string;
|
||||
private _initialized: boolean;
|
||||
private _matcher: Matcher;
|
||||
@@ -148,7 +149,7 @@ export abstract class EntitySystem<
|
||||
constructor(matcher?: Matcher) {
|
||||
this._updateOrder = 0;
|
||||
this._enabled = true;
|
||||
this._performanceMonitor = PerformanceMonitor.instance;
|
||||
this._performanceMonitor = null;
|
||||
this._systemName = getSystemInstanceTypeName(this);
|
||||
this._initialized = false;
|
||||
this._matcher = matcher || Matcher.empty();
|
||||
@@ -192,6 +193,23 @@ export abstract class EntitySystem<
|
||||
this._scene = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置性能监控器
|
||||
*/
|
||||
public setPerformanceMonitor(monitor: PerformanceMonitor): void {
|
||||
this._performanceMonitor = monitor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取性能监控器
|
||||
*/
|
||||
private getPerformanceMonitor(): PerformanceMonitor {
|
||||
if (!this._performanceMonitor) {
|
||||
throw new Error(`${this._systemName}: PerformanceMonitor未注入,请确保在Core.create()之后再添加System到Scene`);
|
||||
}
|
||||
return this._performanceMonitor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取实体匹配器
|
||||
*/
|
||||
@@ -205,9 +223,6 @@ export abstract class EntitySystem<
|
||||
*/
|
||||
public setUpdateOrder(order: number): void {
|
||||
this._updateOrder = order;
|
||||
if (this.scene && this.scene.entityProcessors) {
|
||||
this.scene.entityProcessors.setDirty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -533,18 +548,20 @@ export abstract class EntitySystem<
|
||||
return;
|
||||
}
|
||||
|
||||
const startTime = this._performanceMonitor.startMonitoring(this._systemName);
|
||||
const monitor = this.getPerformanceMonitor();
|
||||
const startTime = monitor.startMonitoring(this._systemName);
|
||||
let entityCount = 0;
|
||||
|
||||
try {
|
||||
this.onBegin();
|
||||
// 查询实体并存储到帧缓存中
|
||||
// 响应式查询会自动维护最新的实体列表,updateEntityTracking会在检测到变化时invalidate
|
||||
this._entityCache.frame = this.queryEntities();
|
||||
entityCount = this._entityCache.frame.length;
|
||||
|
||||
this.process(this._entityCache.frame);
|
||||
} finally {
|
||||
this._performanceMonitor.endMonitoring(this._systemName, startTime, entityCount);
|
||||
monitor.endMonitoring(this._systemName, startTime, entityCount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -556,7 +573,8 @@ export abstract class EntitySystem<
|
||||
return;
|
||||
}
|
||||
|
||||
const startTime = this._performanceMonitor.startMonitoring(`${this._systemName}_Late`);
|
||||
const monitor = this.getPerformanceMonitor();
|
||||
const startTime = monitor.startMonitoring(`${this._systemName}_Late`);
|
||||
let entityCount = 0;
|
||||
|
||||
try {
|
||||
@@ -566,7 +584,7 @@ export abstract class EntitySystem<
|
||||
this.lateProcess(entities);
|
||||
this.onEnd();
|
||||
} finally {
|
||||
this._performanceMonitor.endMonitoring(`${this._systemName}_Late`, startTime, entityCount);
|
||||
monitor.endMonitoring(`${this._systemName}_Late`, startTime, entityCount);
|
||||
// 清理帧缓存
|
||||
this._entityCache.clearFrame();
|
||||
}
|
||||
@@ -626,27 +644,27 @@ export abstract class EntitySystem<
|
||||
|
||||
/**
|
||||
* 获取系统的性能数据
|
||||
*
|
||||
*
|
||||
* @returns 性能数据或undefined
|
||||
*/
|
||||
public getPerformanceData() {
|
||||
return this._performanceMonitor.getSystemData(this._systemName);
|
||||
return this.getPerformanceMonitor().getSystemData(this._systemName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统的性能统计
|
||||
*
|
||||
*
|
||||
* @returns 性能统计或undefined
|
||||
*/
|
||||
public getPerformanceStats() {
|
||||
return this._performanceMonitor.getSystemStats(this._systemName);
|
||||
return this.getPerformanceMonitor().getSystemStats(this._systemName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置系统的性能数据
|
||||
*/
|
||||
public resetPerformanceData(): void {
|
||||
this._performanceMonitor.resetSystem(this._systemName);
|
||||
this.getPerformanceMonitor().resetSystem(this._systemName);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -706,15 +724,43 @@ export abstract class EntitySystem<
|
||||
|
||||
/**
|
||||
* 当实体从系统中移除时调用
|
||||
*
|
||||
*
|
||||
* 子类可以重写此方法来处理实体移除事件。
|
||||
*
|
||||
*
|
||||
* @param entity 被移除的实体
|
||||
*/
|
||||
protected onRemoved(entity: Entity): void {
|
||||
// 子类可以重写此方法
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放系统资源
|
||||
*
|
||||
* 实现IService接口要求的dispose方法。
|
||||
* 当系统从Scene中移除或Scene销毁时调用。
|
||||
*
|
||||
* 默认行为:
|
||||
* - 移除所有事件监听器
|
||||
* - 清空所有缓存
|
||||
* - 重置初始化状态
|
||||
*
|
||||
* 子类可以重写此方法来清理自定义资源,但应该调用super.dispose()。
|
||||
*/
|
||||
public dispose(): void {
|
||||
// 移除所有事件监听器
|
||||
this.cleanupManualEventListeners();
|
||||
|
||||
// 清空所有缓存
|
||||
this._entityCache.clearAll();
|
||||
this._entityIdMap = null;
|
||||
|
||||
// 重置状态
|
||||
this._initialized = false;
|
||||
this._scene = null;
|
||||
|
||||
this.logger.debug(`System ${this._systemName} disposed`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加事件监听器
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -183,6 +183,17 @@ export class World {
|
||||
return Array.from(this._scenes.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除所有Scene
|
||||
*/
|
||||
public removeAllScenes(): void {
|
||||
const sceneIds = Array.from(this._scenes.keys());
|
||||
for (const sceneId of sceneIds) {
|
||||
this.removeScene(sceneId);
|
||||
}
|
||||
logger.info(`从World '${this.name}' 中移除所有Scene`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置Scene激活状态
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { World, IWorldConfig } from './World';
|
||||
import { createLogger } from '../Utils/Logger';
|
||||
import type { IService } from '../Core/ServiceContainer';
|
||||
|
||||
const logger = createLogger('WorldManager');
|
||||
|
||||
@@ -30,40 +31,45 @@ export interface IWorldManagerConfig {
|
||||
|
||||
/**
|
||||
* World管理器 - 管理所有World实例
|
||||
*
|
||||
* WorldManager是全局单例,负责管理所有World的生命周期。
|
||||
*
|
||||
* WorldManager负责管理多个独立的World实例。
|
||||
* 每个World都是独立的ECS环境,可以包含多个Scene。
|
||||
*
|
||||
* 设计理念:
|
||||
* - Core负责单Scene的传统ECS管理
|
||||
* - World负责多Scene的管理和协调
|
||||
* - WorldManager负责多World的全局管理
|
||||
*
|
||||
*
|
||||
* 适用场景:
|
||||
* - MMO游戏的多房间管理
|
||||
* - 服务器端的多游戏实例
|
||||
* - 需要完全隔离的多个游戏环境
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 获取全局WorldManager
|
||||
* const worldManager = WorldManager.getInstance();
|
||||
*
|
||||
* // 创建WorldManager实例
|
||||
* const worldManager = new WorldManager({
|
||||
* maxWorlds: 100,
|
||||
* autoCleanup: true
|
||||
* });
|
||||
*
|
||||
* // 创建游戏房间World
|
||||
* const roomWorld = worldManager.createWorld('room_001', {
|
||||
* const room1 = worldManager.createWorld('room_001', {
|
||||
* name: 'GameRoom_001',
|
||||
* maxScenes: 5
|
||||
* });
|
||||
*
|
||||
* // 在游戏循环中更新所有World
|
||||
* worldManager.updateAll(deltaTime);
|
||||
* room1.setActive(true);
|
||||
*
|
||||
* // 游戏循环
|
||||
* function gameLoop(deltaTime: number) {
|
||||
* Core.update(deltaTime);
|
||||
* worldManager.updateAll(); // 更新所有活跃World
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export class WorldManager {
|
||||
private static _instance: WorldManager | null = null;
|
||||
|
||||
export class WorldManager implements IService {
|
||||
private readonly _config: IWorldManagerConfig;
|
||||
private readonly _worlds: Map<string, World> = new Map();
|
||||
private readonly _activeWorlds: Set<string> = new Set();
|
||||
private _cleanupTimer: NodeJS.Timeout | null = null;
|
||||
private _cleanupTimer: ReturnType<typeof setInterval> | null = null;
|
||||
private _isRunning: boolean = false;
|
||||
|
||||
private constructor(config: IWorldManagerConfig = {}) {
|
||||
public constructor(config: IWorldManagerConfig = {}) {
|
||||
this._config = {
|
||||
maxWorlds: 50,
|
||||
autoCleanup: true,
|
||||
@@ -72,6 +78,9 @@ export class WorldManager {
|
||||
...config
|
||||
};
|
||||
|
||||
// 默认启动运行状态
|
||||
this._isRunning = true;
|
||||
|
||||
logger.info('WorldManager已初始化', {
|
||||
maxWorlds: this._config.maxWorlds,
|
||||
autoCleanup: this._config.autoCleanup,
|
||||
@@ -81,26 +90,6 @@ export class WorldManager {
|
||||
this.startCleanupTimer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取WorldManager单例实例
|
||||
*/
|
||||
public static getInstance(config?: IWorldManagerConfig): WorldManager {
|
||||
if (!this._instance) {
|
||||
this._instance = new WorldManager(config);
|
||||
}
|
||||
return this._instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置WorldManager实例(主要用于测试)
|
||||
*/
|
||||
public static reset(): void {
|
||||
if (this._instance) {
|
||||
this._instance.destroy();
|
||||
this._instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== World管理 =====
|
||||
|
||||
/**
|
||||
@@ -205,9 +194,37 @@ export class WorldManager {
|
||||
|
||||
// ===== 批量操作 =====
|
||||
|
||||
/**
|
||||
* 更新所有活跃的World
|
||||
*
|
||||
* 应该在每帧的游戏循环中调用。
|
||||
* 会自动更新所有活跃World的全局系统和场景。
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* function gameLoop(deltaTime: number) {
|
||||
* Core.update(deltaTime); // 更新全局服务
|
||||
* worldManager.updateAll(); // 更新所有World
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
public updateAll(): void {
|
||||
if (!this._isRunning) return;
|
||||
|
||||
for (const worldId of this._activeWorlds) {
|
||||
const world = this._worlds.get(worldId);
|
||||
if (world && world.isActive) {
|
||||
// 更新World的全局System
|
||||
world.updateGlobalSystems();
|
||||
|
||||
// 更新World中的所有Scene
|
||||
world.updateScenes();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有激活的World
|
||||
* 注意:此方法供Core.update()使用
|
||||
*/
|
||||
public getActiveWorlds(): World[] {
|
||||
const activeWorlds: World[] = [];
|
||||
@@ -371,6 +388,14 @@ export class WorldManager {
|
||||
logger.info('WorldManager已销毁');
|
||||
}
|
||||
|
||||
/**
|
||||
* 实现 IService 接口的 dispose 方法
|
||||
* 调用 destroy 方法进行清理
|
||||
*/
|
||||
public dispose(): void {
|
||||
this.destroy();
|
||||
}
|
||||
|
||||
// ===== 私有方法 =====
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,10 +6,15 @@ export * from './Utils';
|
||||
export * from './Decorators';
|
||||
export { Scene } from './Scene';
|
||||
export { IScene, ISceneFactory, ISceneConfig } from './IScene';
|
||||
export { SceneManager } from './SceneManager';
|
||||
export { World, IWorldConfig } from './World';
|
||||
export { WorldManager, IWorldManagerConfig } from './WorldManager';
|
||||
export * from './Core/Events';
|
||||
export * from './Core/Query';
|
||||
export * from './Core/Storage';
|
||||
export * from './Core/StorageDecorators';
|
||||
export * from './Serialization';
|
||||
export * from './Serialization';
|
||||
export { ReferenceTracker, getSceneByEntityId } from './Core/ReferenceTracker';
|
||||
export type { EntityRefRecord } from './Core/ReferenceTracker';
|
||||
export { ReactiveQuery, ReactiveQueryChangeType } from './Core/ReactiveQuery';
|
||||
export type { ReactiveQueryChange, ReactiveQueryListener, ReactiveQueryConfig } from './Core/ReactiveQuery';
|
||||
359
packages/core/src/Plugins/DebugPlugin.ts
Normal file
359
packages/core/src/Plugins/DebugPlugin.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
1
packages/core/src/Plugins/index.ts
Normal file
1
packages/core/src/Plugins/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './DebugPlugin';
|
||||
20
packages/core/src/Types/IUpdatable.ts
Normal file
20
packages/core/src/Types/IUpdatable.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* 可更新接口
|
||||
*
|
||||
* 实现此接口的服务将在每帧被Core自动调用update方法
|
||||
*/
|
||||
export interface IUpdatable {
|
||||
/**
|
||||
* 每帧更新方法
|
||||
*
|
||||
* @param deltaTime - 帧时间间隔(秒),可选参数
|
||||
*/
|
||||
update(deltaTime?: number): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查对象是否实现了IUpdatable接口
|
||||
*/
|
||||
export function isUpdatable(obj: any): obj is IUpdatable {
|
||||
return obj && typeof obj.update === 'function';
|
||||
}
|
||||
@@ -2,8 +2,11 @@
|
||||
* 框架核心类型定义
|
||||
*/
|
||||
|
||||
import type { IWorldManagerConfig } from '../ECS';
|
||||
|
||||
// 导出TypeScript类型增强工具
|
||||
export * from './TypeHelpers';
|
||||
export * from './IUpdatable';
|
||||
|
||||
/**
|
||||
* 组件接口
|
||||
@@ -15,7 +18,7 @@ export interface IComponent {
|
||||
/** 组件唯一标识符 */
|
||||
readonly id: number;
|
||||
/** 组件所属的实体ID */
|
||||
entityId?: string | number;
|
||||
entityId: number | null;
|
||||
|
||||
/** 组件添加到实体时的回调 */
|
||||
onAddedToEntity(): void;
|
||||
@@ -263,6 +266,8 @@ export interface ICoreConfig {
|
||||
enableEntitySystems?: boolean;
|
||||
/** 调试配置 */
|
||||
debugConfig?: IECSDebugConfig;
|
||||
/** WorldManager配置 */
|
||||
worldManagerConfig?: IWorldManagerConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
45
packages/core/src/Utils/Debug/DebugConfigService.ts
Normal file
45
packages/core/src/Utils/Debug/DebugConfigService.ts
Normal 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 {
|
||||
// 清理资源
|
||||
}
|
||||
}
|
||||
@@ -9,13 +9,21 @@ import { Component } from '../../ECS/Component';
|
||||
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';
|
||||
|
||||
/**
|
||||
* 调试管理器
|
||||
*
|
||||
* 整合所有调试数据收集器,负责收集和发送调试数据
|
||||
*/
|
||||
export class DebugManager {
|
||||
@Injectable()
|
||||
@Updatable()
|
||||
export class DebugManager implements IService, IUpdatable {
|
||||
private config: IECSDebugConfig;
|
||||
private webSocketManager: WebSocketManager;
|
||||
private entityCollector: EntityDataCollector;
|
||||
@@ -23,25 +31,29 @@ export class DebugManager {
|
||||
private performanceCollector: PerformanceDataCollector;
|
||||
private componentCollector: ComponentDataCollector;
|
||||
private sceneCollector: SceneDataCollector;
|
||||
private sceneProvider: () => any;
|
||||
private performanceMonitorProvider: () => any;
|
||||
private sceneManager: SceneManager;
|
||||
private performanceMonitor: PerformanceMonitor;
|
||||
|
||||
private frameCounter: number = 0;
|
||||
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 core Core实例
|
||||
* @param config 调试配置
|
||||
*/
|
||||
constructor(core: any, config: IECSDebugConfig) {
|
||||
this.config = config;
|
||||
|
||||
// 设置提供器函数
|
||||
this.sceneProvider = () => (core as any).scene || (core.constructor as any).scene;
|
||||
this.performanceMonitorProvider = () => core._performanceMonitor;
|
||||
constructor(
|
||||
@Inject(SceneManager) sceneManager: SceneManager,
|
||||
@Inject(PerformanceMonitor) performanceMonitor: PerformanceMonitor,
|
||||
@Inject(DebugConfigService) configService: DebugConfigService
|
||||
) {
|
||||
this.config = configService.getConfig();
|
||||
this.sceneManager = sceneManager;
|
||||
this.performanceMonitor = performanceMonitor;
|
||||
|
||||
// 初始化数据收集器
|
||||
this.entityCollector = new EntityDataCollector();
|
||||
@@ -52,17 +64,20 @@ export class DebugManager {
|
||||
|
||||
// 初始化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();
|
||||
}
|
||||
|
||||
@@ -86,6 +101,118 @@ export class DebugManager {
|
||||
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) {
|
||||
// 静默失败,避免递归日志
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新配置
|
||||
*/
|
||||
@@ -108,16 +235,12 @@ export class DebugManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 帧更新回调
|
||||
*/
|
||||
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;
|
||||
@@ -205,7 +328,8 @@ export class DebugManager {
|
||||
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',
|
||||
@@ -237,7 +361,8 @@ export class DebugManager {
|
||||
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',
|
||||
@@ -260,7 +385,8 @@ export class DebugManager {
|
||||
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',
|
||||
@@ -292,7 +418,8 @@ export class DebugManager {
|
||||
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',
|
||||
@@ -338,7 +465,7 @@ export class DebugManager {
|
||||
|
||||
// 收集其他内存统计
|
||||
const baseMemoryInfo = this.collectBaseMemoryInfo();
|
||||
const scene = this.sceneProvider();
|
||||
const scene = this.sceneManager.currentScene;
|
||||
|
||||
// 使用专门的内存计算方法收集实体数据
|
||||
const entityData = this.entityCollector.collectEntityDataWithMemory(scene);
|
||||
@@ -452,7 +579,7 @@ export class DebugManager {
|
||||
/**
|
||||
* 收集组件内存统计(仅用于内存快照)
|
||||
*/
|
||||
private collectComponentMemoryStats(entityList: { buffer: Array<{ id: number; name?: string; destroyed?: boolean; components?: Component[] }> }): {
|
||||
private collectComponentMemoryStats(entityList: { buffer: Array<{ id: number; name?: string; destroyed?: boolean; components?: readonly Component[] }> }): {
|
||||
totalMemory: number;
|
||||
componentTypes: number;
|
||||
totalInstances: number;
|
||||
@@ -546,7 +673,7 @@ export class DebugManager {
|
||||
updateOrder: number;
|
||||
}>;
|
||||
} {
|
||||
const scene = this.sceneProvider();
|
||||
const scene = this.sceneManager.currentScene;
|
||||
let totalSystemMemory = 0;
|
||||
const systemBreakdown: Array<{
|
||||
name: string;
|
||||
@@ -556,11 +683,11 @@ export class DebugManager {
|
||||
}> = [];
|
||||
|
||||
try {
|
||||
const entityProcessors = scene?.entityProcessors;
|
||||
if (entityProcessors && entityProcessors.processors) {
|
||||
const systems = scene?.systems;
|
||||
if (systems) {
|
||||
const systemTypeMemoryCache = new Map<string, number>();
|
||||
|
||||
for (const system of entityProcessors.processors) {
|
||||
for (const system of systems) {
|
||||
const systemTypeName = getSystemInstanceTypeName(system);
|
||||
|
||||
let systemMemory: number;
|
||||
@@ -720,16 +847,15 @@ export class DebugManager {
|
||||
error?: string;
|
||||
} {
|
||||
try {
|
||||
const performanceMonitor = this.performanceMonitorProvider();
|
||||
if (!performanceMonitor) {
|
||||
if (!this.performanceMonitor) {
|
||||
return { enabled: false };
|
||||
}
|
||||
|
||||
const stats = performanceMonitor.getAllSystemStats();
|
||||
const warnings = performanceMonitor.getPerformanceWarnings();
|
||||
const stats = this.performanceMonitor.getAllSystemStats();
|
||||
const warnings = this.performanceMonitor.getPerformanceWarnings();
|
||||
|
||||
return {
|
||||
enabled: (performanceMonitor as { enabled?: boolean }).enabled ?? false,
|
||||
enabled: (this.performanceMonitor as { enabled?: boolean }).enabled ?? false,
|
||||
systemCount: stats.size,
|
||||
warnings: warnings.slice(0, 10), // 最多10个警告
|
||||
topSystems: Array.from(stats.entries()).map((entry) => {
|
||||
@@ -753,7 +879,7 @@ export class DebugManager {
|
||||
*/
|
||||
public getDebugData(): IECSDebugData {
|
||||
const currentTime = Date.now();
|
||||
const scene = this.sceneProvider();
|
||||
const scene = this.sceneManager.currentScene;
|
||||
|
||||
const debugData: IECSDebugData = {
|
||||
timestamp: currentTime,
|
||||
@@ -769,13 +895,11 @@ export class DebugManager {
|
||||
}
|
||||
|
||||
if (this.config.channels.systems) {
|
||||
const performanceMonitor = this.performanceMonitorProvider();
|
||||
debugData.systems = this.systemCollector.collectSystemData(performanceMonitor, scene);
|
||||
debugData.systems = this.systemCollector.collectSystemData(this.performanceMonitor, scene);
|
||||
}
|
||||
|
||||
if (this.config.channels.performance) {
|
||||
const performanceMonitor = this.performanceMonitorProvider();
|
||||
debugData.performance = this.performanceCollector.collectPerformanceData(performanceMonitor);
|
||||
debugData.performance = this.performanceCollector.collectPerformanceData(this.performanceMonitor);
|
||||
}
|
||||
|
||||
if (this.config.channels.components) {
|
||||
@@ -821,4 +945,18 @@ export class DebugManager {
|
||||
// console.error('[ECS Debug] 发送调试数据失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -9,7 +9,7 @@ export class WebSocketManager {
|
||||
private reconnectInterval: number = 2000;
|
||||
private url: string;
|
||||
private autoReconnect: boolean;
|
||||
private reconnectTimer?: NodeJS.Timeout;
|
||||
private reconnectTimer?: ReturnType<typeof setTimeout>;
|
||||
private onOpen?: (event: Event) => void;
|
||||
private onClose?: (event: CloseEvent) => void;
|
||||
private onError?: (error: Event | any) => void;
|
||||
|
||||
@@ -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';
|
||||
@@ -99,20 +99,20 @@ export interface PerformanceThresholds {
|
||||
};
|
||||
}
|
||||
|
||||
import type { IService } from '../Core/ServiceContainer';
|
||||
|
||||
/**
|
||||
* 高性能监控器
|
||||
* 用于监控ECS系统的性能表现,提供详细的分析和优化建议
|
||||
*/
|
||||
export class PerformanceMonitor {
|
||||
private static _instance: PerformanceMonitor;
|
||||
|
||||
export class PerformanceMonitor implements IService {
|
||||
private _systemData = new Map<string, PerformanceData>();
|
||||
private _systemStats = new Map<string, PerformanceStats>();
|
||||
private _warnings: PerformanceWarning[] = [];
|
||||
private _isEnabled = false;
|
||||
private _maxRecentSamples = 60; // 保留最近60帧的数据
|
||||
private _maxWarnings = 100; // 最大警告数量
|
||||
|
||||
|
||||
// 性能阈值配置
|
||||
private _thresholds: PerformanceThresholds = {
|
||||
executionTime: { warning: 16.67, critical: 33.33 }, // 60fps和30fps对应的帧时间
|
||||
@@ -139,18 +139,8 @@ export class PerformanceMonitor {
|
||||
private _gcCount = 0;
|
||||
private _lastGcCheck = 0;
|
||||
private _gcCheckInterval = 1000;
|
||||
|
||||
/**
|
||||
* 获取单例实例
|
||||
*/
|
||||
public static get instance(): PerformanceMonitor {
|
||||
if (!PerformanceMonitor._instance) {
|
||||
PerformanceMonitor._instance = new PerformanceMonitor();
|
||||
}
|
||||
return PerformanceMonitor._instance;
|
||||
}
|
||||
|
||||
private constructor() {}
|
||||
constructor() {}
|
||||
|
||||
/**
|
||||
* 启用性能监控
|
||||
@@ -392,7 +382,7 @@ export class PerformanceMonitor {
|
||||
*/
|
||||
public setMaxRecentSamples(maxSamples: number): void {
|
||||
this._maxRecentSamples = maxSamples;
|
||||
|
||||
|
||||
// 裁剪现有数据
|
||||
for (const stats of this._systemStats.values()) {
|
||||
while (stats.recentTimes.length > maxSamples) {
|
||||
@@ -400,4 +390,16 @@ export class PerformanceMonitor {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源
|
||||
*/
|
||||
public dispose(): void {
|
||||
this._systemData.clear();
|
||||
this._systemStats.clear();
|
||||
this._warnings = [];
|
||||
this._fpsHistory = [];
|
||||
this._memoryHistory = [];
|
||||
this._isEnabled = false;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,18 @@
|
||||
import { IPoolable, PoolStats } from './IPoolable';
|
||||
import { Pool } from './Pool';
|
||||
import type { IService } from '../../Core/ServiceContainer';
|
||||
|
||||
/**
|
||||
* 池管理器
|
||||
* 统一管理所有对象池
|
||||
*/
|
||||
export class PoolManager {
|
||||
private static instance: PoolManager;
|
||||
export class PoolManager implements IService {
|
||||
private pools = new Map<string, Pool<any>>();
|
||||
private autoCompactInterval = 60000; // 60秒
|
||||
private lastCompactTime = 0;
|
||||
|
||||
public static getInstance(): PoolManager {
|
||||
if (!PoolManager.instance) {
|
||||
PoolManager.instance = new PoolManager();
|
||||
}
|
||||
return PoolManager.instance;
|
||||
constructor() {
|
||||
// 普通构造函数,不再使用单例模式
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -228,4 +225,12 @@ export class PoolManager {
|
||||
this.pools.clear();
|
||||
this.lastCompactTime = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源
|
||||
* 实现 IService 接口
|
||||
*/
|
||||
public dispose(): void {
|
||||
this.reset();
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,19 @@
|
||||
import { GlobalManager } from '../GlobalManager';
|
||||
import { Timer } from './Timer';
|
||||
import { ITimer } from './ITimer';
|
||||
import type { IService } from '../../Core/ServiceContainer';
|
||||
import type { IUpdatable } from '../../Types/IUpdatable';
|
||||
import { Updatable } from '../../Core/DI';
|
||||
|
||||
/**
|
||||
* 定时器管理器
|
||||
*
|
||||
* 允许动作的延迟和重复执行
|
||||
*/
|
||||
export class TimerManager extends GlobalManager {
|
||||
@Updatable()
|
||||
export class TimerManager implements IService, IUpdatable {
|
||||
public _timers: Array<Timer<unknown>> = [];
|
||||
|
||||
public override update() {
|
||||
public update() {
|
||||
for (let i = this._timers.length - 1; i >= 0; i --){
|
||||
if (this._timers[i].tick()){
|
||||
this._timers[i].unload();
|
||||
@@ -31,4 +36,14 @@ export class TimerManager extends GlobalManager {
|
||||
|
||||
return timer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源
|
||||
*/
|
||||
public dispose(): void {
|
||||
for (const timer of this._timers) {
|
||||
timer.unload();
|
||||
}
|
||||
this._timers = [];
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,28 @@
|
||||
|
||||
// 核心模块
|
||||
export { Core } from './Core';
|
||||
export { ServiceContainer, ServiceLifetime } from './Core/ServiceContainer';
|
||||
export type { IService, ServiceType } from './Core/ServiceContainer';
|
||||
|
||||
// 插件系统
|
||||
export { PluginManager } from './Core/PluginManager';
|
||||
export { PluginState } from './Core/Plugin';
|
||||
export type { IPlugin, IPluginMetadata } from './Core/Plugin';
|
||||
|
||||
// 内置插件
|
||||
export * from './Plugins';
|
||||
|
||||
// 依赖注入
|
||||
export {
|
||||
Injectable,
|
||||
Inject,
|
||||
Updatable,
|
||||
registerInjectable,
|
||||
createInstance,
|
||||
isUpdatable,
|
||||
getUpdatableMetadata
|
||||
} from './Core/DI';
|
||||
export type { InjectableMetadata, UpdatableMetadata } from './Core/DI';
|
||||
|
||||
// 核心管理器
|
||||
export { Emitter, FuncPack } from './Utils/Emitter';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user