Compare commits
412 Commits
v2.1.15
...
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 | ||
|
|
fdaa94a61d | ||
|
|
6af0074c36 | ||
|
|
97a69fed09 | ||
|
|
959879440d | ||
|
|
fd1bbb0e00 | ||
|
|
072e68cf43 | ||
|
|
610232e6b0 | ||
|
|
69c46f32eb | ||
|
|
06b3f92007 | ||
|
|
c631290049 | ||
|
|
f41c1a3ca3 | ||
|
|
bd6ba84087 | ||
|
|
1512409eb3 | ||
|
|
bcb5feeb1c | ||
|
|
da8b7cf601 | ||
|
|
316527c459 | ||
|
|
da70818b22 | ||
|
|
5ea3b72b2b | ||
|
|
632864b361 | ||
|
|
952247def0 | ||
|
|
51debede52 | ||
|
|
ce7b731bcf | ||
|
|
86e2dc8fdb | ||
|
|
78047134c2 | ||
|
|
125a1686ab | ||
|
|
d542ac48b8 | ||
|
|
1ac0227c90 | ||
|
|
a5e70bcd99 | ||
|
|
38763de7f6 | ||
|
|
db73b077c5 | ||
|
|
0969d09da1 | ||
|
|
a07108a431 | ||
|
|
6693b56ab8 | ||
|
|
a7349bd360 | ||
|
|
e92c0040b5 | ||
|
|
f448fa48c4 | ||
|
|
aa33cad4fa | ||
|
|
d0cb7d5359 | ||
|
|
90153b98fe | ||
|
|
8c4e8d523e | ||
|
|
90ad4b3ec4 | ||
|
|
62bc6b547e | ||
|
|
be11060674 | ||
|
|
d62bf9f7f9 | ||
|
|
61fcd52c65 | ||
|
|
2947ddeb64 | ||
|
|
d9b752c180 | ||
|
|
b82891caee | ||
|
|
05f04ef37e | ||
|
|
66dc9780b9 | ||
|
|
d48b22c656 | ||
|
|
727b1864eb | ||
|
|
de3bfd7551 | ||
|
|
dedb91379f | ||
|
|
1dfcd008aa | ||
|
|
cf2dc91af6 | ||
|
|
a66f80a766 | ||
|
|
f4e49c316e | ||
|
|
d1cd72bbb2 | ||
|
|
6178851def | ||
|
|
945f772c30 | ||
|
|
b546c9c712 | ||
|
|
413ce93b31 | ||
|
|
cffe32911d | ||
|
|
4f651eb42e | ||
|
|
6da1585b6b | ||
|
|
b988e81a1b | ||
|
|
1a1c1087d2 | ||
|
|
1a1549230f | ||
|
|
64ea53eba1 | ||
|
|
5e052a7e7d | ||
|
|
cf9ea495d0 | ||
|
|
9603c6423b | ||
|
|
457eef585e | ||
|
|
1ade449c4d | ||
|
|
aa9d73a810 | ||
|
|
cc266a7ba9 | ||
|
|
d8ea324018 | ||
|
|
60566e8d78 | ||
|
|
306d2994dc | ||
|
|
e6a8791fc3 | ||
|
|
6cbbc06998 | ||
|
|
0b4244fd8e | ||
|
|
367ddfbf8a | ||
|
|
168e028098 | ||
|
|
042ded37d2 | ||
|
|
4137eb2bce | ||
|
|
20a3f03e12 | ||
|
|
7792710694 | ||
|
|
dbddbbdfb8 | ||
|
|
4869f5741e | ||
|
|
bda547dd2e | ||
|
|
ef80b03a44 | ||
|
|
6e511ae949 | ||
|
|
94541d0abb | ||
|
|
586a0e5d14 | ||
|
|
814842dbaf | ||
|
|
70a993573f | ||
|
|
21659cbb13 | ||
|
|
a44251cc55 | ||
|
|
69616bbddc | ||
|
|
0a1d7ac083 | ||
|
|
364bc4cdab | ||
|
|
2504eb24e1 | ||
|
|
bdbef0bd0d | ||
|
|
e4e38ee4e6 | ||
|
|
021e892e33 | ||
|
|
c27d5022fd | ||
|
|
6730a5d625 | ||
|
|
32092f992d | ||
|
|
a5f0c8f6b5 | ||
|
|
85cd93e51a | ||
|
|
0b7e623748 | ||
|
|
62f250b43c | ||
|
|
25136349ff | ||
|
|
baeb047e27 | ||
|
|
56dd18b983 | ||
|
|
86cb70a94f | ||
|
|
9f76d37a82 | ||
|
|
a026ed9428 | ||
|
|
c178e2fbcc | ||
|
|
b88bb1dc87 | ||
|
|
3069e28224 | ||
|
|
d69b3af99b | ||
|
|
7398b7c6d0 | ||
|
|
5d57904d22 | ||
|
|
7daf352a25 | ||
|
|
6a49f6a534 | ||
|
|
5bce08683a | ||
|
|
edc60fc3d8 | ||
|
|
1361fd8a90 | ||
|
|
d539bb3dd9 | ||
|
|
3b9ae4f384 | ||
|
|
2783448de5 | ||
|
|
6e21ff08d5 | ||
|
|
e56278e4a6 | ||
|
|
fc9bf816dd | ||
|
|
854fd7df3a | ||
|
|
87dd564a12 | ||
|
|
2d389308ea | ||
|
|
ea8523be35 | ||
|
|
4479f0fab0 | ||
|
|
7a000318a6 | ||
|
|
9a08ae74b6 | ||
|
|
f3d2950df3 | ||
|
|
8cfba4a166 | ||
|
|
51e6bba2a7 | ||
|
|
ccbfa78070 | ||
|
|
69655f1936 | ||
|
|
6ea366cfed | ||
|
|
b7d17fb16d | ||
|
|
f3dc8c6344 | ||
|
|
69ec545854 | ||
|
|
65386ff731 | ||
|
|
01fa33e122 | ||
|
|
0411aa9aef | ||
|
|
4a5c890121 | ||
|
|
4c11fdc176 | ||
|
|
d99e7a45ea | ||
|
|
52528ff1b7 | ||
|
|
4a9317f3f4 | ||
|
|
9450dd5869 | ||
|
|
d5471e4828 | ||
|
|
2f71785add | ||
|
|
608f5030b2 | ||
|
|
dd8f3714ed | ||
|
|
abec2b3648 | ||
|
|
ea06a9f07d | ||
|
|
9f54759cc5 | ||
|
|
55dd5f9ed0 | ||
|
|
05455421fb | ||
|
|
af61067f08 | ||
|
|
19cda88248 | ||
|
|
0edb2738a1 | ||
|
|
e1bc364525 | ||
|
|
2925ee380d | ||
|
|
731edf5872 | ||
|
|
7b85039b17 | ||
|
|
2bc45fa574 | ||
|
|
d2b4455205 | ||
|
|
bce4a26197 | ||
|
|
1da5040d60 | ||
|
|
afd33e053b | ||
|
|
171d03c006 | ||
|
|
34d5237aaa | ||
|
|
037c3d6a05 | ||
|
|
5596ba634e | ||
|
|
a5f69065f4 | ||
|
|
969ef249ea | ||
|
|
a37183851f | ||
|
|
4cf3e1a769 | ||
|
|
354e5a2761 | ||
|
|
c9fd8cc2a7 | ||
|
|
bb19f752a1 | ||
|
|
6bd9c1055c | ||
|
|
dff77097c6 | ||
|
|
b4dc1c5661 | ||
|
|
992338d924 | ||
|
|
f88a402b0c | ||
|
|
5938d36149 | ||
|
|
78577db3f9 | ||
|
|
0b4a6b77e2 | ||
|
|
01084a8897 | ||
|
|
0f18a1979e | ||
|
|
68a615bc7b | ||
|
|
add1068c1a | ||
|
|
7a40df9965 | ||
|
|
3e6a1aa59a | ||
|
|
d3fe79cf39 | ||
|
|
48fa547c8f | ||
|
|
80e2f7df71 | ||
|
|
0107f1f58a | ||
|
|
d29c9a96f4 | ||
|
|
37d75c3281 | ||
|
|
666ded7b89 | ||
|
|
73a882f75e | ||
|
|
310f5f2349 | ||
|
|
8c86d6b696 | ||
|
|
82cd163adc | ||
|
|
802ee25621 | ||
|
|
f48ebb65ba | ||
|
|
aaa2a8ed2c | ||
|
|
5a06f5420b | ||
|
|
343f5a44f2 | ||
|
|
92125aee3a | ||
|
|
96f651b7ca | ||
|
|
06ea01e928 | ||
|
|
577f1e429a | ||
|
|
7808f64fe5 | ||
|
|
e6789e49e4 | ||
|
|
797619aece | ||
|
|
1b5363611d | ||
|
|
103f773286 | ||
|
|
d9ef0b587e | ||
|
|
d5b98256f0 | ||
|
|
efcceaa898 | ||
|
|
e4aad11965 | ||
|
|
47207fad52 | ||
|
|
202bf82896 | ||
|
|
0e3274a743 | ||
|
|
b06174926d | ||
|
|
abb23a3c02 | ||
|
|
0c8f232282 |
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"
|
||||
84
.github/workflows/ci.yml
vendored
Normal file
84
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master, main, develop ]
|
||||
paths:
|
||||
- 'packages/**'
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
- 'tsconfig.json'
|
||||
- 'jest.config.*'
|
||||
- '.github/workflows/ci.yml'
|
||||
pull_request:
|
||||
branches: [ master, main, develop ]
|
||||
paths:
|
||||
- 'packages/**'
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
- 'tsconfig.json'
|
||||
- 'jest.config.*'
|
||||
- '.github/workflows/ci.yml'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build core package first
|
||||
run: npm run build:core
|
||||
|
||||
- name: Run tests with coverage
|
||||
run: npm run test:ci
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
file: ./coverage/lcov.info
|
||||
flags: unittests
|
||||
name: codecov-umbrella
|
||||
fail_ci_if_error: false
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build project
|
||||
run: npm run build
|
||||
|
||||
- name: Build npm package
|
||||
run: npm run build:npm
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-artifacts
|
||||
path: |
|
||||
bin/
|
||||
dist/
|
||||
retention-days: 7
|
||||
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
|
||||
67
.github/workflows/docs.yml
vendored
Normal file
67
.github/workflows/docs.yml
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
name: Deploy Documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- 'packages/**'
|
||||
- 'typedoc.json'
|
||||
- 'package.json'
|
||||
- '.github/workflows/docs.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: pages
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build core package
|
||||
run: npm run build:core
|
||||
|
||||
- name: Generate API documentation
|
||||
run: npm run docs:api
|
||||
|
||||
- name: Build documentation
|
||||
run: npm run docs:build
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: docs/.vitepress/dist
|
||||
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
23
.github/workflows/issue-labeler.yml
vendored
Normal file
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).
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -55,6 +55,8 @@ pnpm-lock.yaml
|
||||
# 文档生成
|
||||
docs/api/
|
||||
docs/build/
|
||||
docs/.vitepress/cache/
|
||||
docs/.vitepress/dist/
|
||||
|
||||
# 备份文件
|
||||
*.bak
|
||||
@@ -67,3 +69,12 @@ docs/build/
|
||||
/demo/.idea/
|
||||
/demo/.vscode/
|
||||
/demo_wxgame/
|
||||
|
||||
# Tauri 构建产物
|
||||
**/src-tauri/target/
|
||||
**/src-tauri/WixTools/
|
||||
**/src-tauri/gen/
|
||||
|
||||
# Tauri 捆绑输出
|
||||
**/src-tauri/target/release/bundle/
|
||||
**/src-tauri/target/debug/bundle/
|
||||
|
||||
33
.gitmodules
vendored
Normal file
33
.gitmodules
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
[submodule "thirdparty/BehaviourTree-ai"]
|
||||
path = thirdparty/BehaviourTree-ai
|
||||
url = https://github.com/esengine/BehaviourTree-ai.git
|
||||
[submodule "thirdparty/admin-backend"]
|
||||
path = thirdparty/admin-backend
|
||||
url = https://github.com/esengine/admin-backend.git
|
||||
[submodule "extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension"]
|
||||
path = extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension
|
||||
url = https://github.com/esengine/cocos-ecs-extension.git
|
||||
[submodule "extensions/cocos/cocos-ecs/extensions/behaviour-tree"]
|
||||
path = extensions/cocos/cocos-ecs/extensions/behaviour-tree
|
||||
url = https://github.com/esengine/behaviour-tree.git
|
||||
[submodule "extensions/cocos/cocos-ecs/extensions/cocos-terrain-gen"]
|
||||
path = extensions/cocos/cocos-ecs/extensions/cocos-terrain-gen
|
||||
url = https://github.com/esengine/cocos-terrain-gen.git
|
||||
[submodule "extensions/cocos/cocos-ecs/extensions/mvvm-designer"]
|
||||
path = extensions/cocos/cocos-ecs/extensions/mvvm-designer
|
||||
url = https://github.com/esengine/mvvm-designer.git
|
||||
[submodule "thirdparty/mvvm-ui-framework"]
|
||||
path = thirdparty/mvvm-ui-framework
|
||||
url = https://github.com/esengine/mvvm-ui-framework.git
|
||||
[submodule "thirdparty/cocos-nexus"]
|
||||
path = thirdparty/cocos-nexus
|
||||
url = https://github.com/esengine/cocos-nexus.git
|
||||
[submodule "extensions/cocos/cocos-ecs/extensions/utilityai_designer"]
|
||||
path = extensions/cocos/cocos-ecs/extensions/utilityai_designer
|
||||
url = https://github.com/esengine/utilityai_designer.git
|
||||
[submodule "thirdparty/ecs-astar"]
|
||||
path = thirdparty/ecs-astar
|
||||
url = https://github.com/esengine/ecs-astar.git
|
||||
[submodule "examples/lawn-mower-demo"]
|
||||
path = examples/lawn-mower-demo
|
||||
url = https://github.com/esengine/lawn-mower-demo.git
|
||||
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.
|
||||
|
||||
466
README.md
466
README.md
@@ -1,326 +1,274 @@
|
||||
# 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>
|
||||
|
||||
- 🏗️ **现代化 ECS 架构** - 完整的实体组件系统,提供清晰的代码结构
|
||||
- 📡 **类型安全事件系统** - 增强的事件总线,支持异步事件、优先级、批处理和装饰器
|
||||
- ⏰ **定时器管理系统** - 完整的定时器管理,支持延迟和重复任务
|
||||
- 🔍 **智能查询系统** - 支持复杂的实体查询,流式API设计
|
||||
- ⚡ **高性能优化** - 组件索引、Archetype系统、脏标记机制三重优化
|
||||
- 🛠️ **开发者友好** - 完整的TypeScript支持,丰富的调试工具
|
||||
- 📦 **轻量级设计** - 最小化依赖,适用于各种游戏引擎
|
||||
<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>
|
||||
|
||||
---
|
||||
|
||||
## 特性
|
||||
|
||||
- **高性能** - 针对大规模实体优化,支持SoA存储和批量处理
|
||||
- **多线程计算** - Worker系统支持真正的并行处理,充分利用多核CPU性能
|
||||
- **类型安全** - 完整的TypeScript支持,编译时类型检查
|
||||
- **现代架构** - 支持多World、多Scene的分层架构设计
|
||||
- **开发友好** - 内置调试工具和性能监控
|
||||
- **跨平台** - 支持Cocos Creator、Laya引擎和Web平台
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
npm install @esengine/ecs-framework
|
||||
```
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 基础设置
|
||||
## 快速开始
|
||||
|
||||
```typescript
|
||||
import { Core, CoreEvents, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// 创建 Core 实例
|
||||
const core = Core.create(true); // 开启调试模式
|
||||
|
||||
// 创建场景
|
||||
class GameScene extends Scene {
|
||||
public initialize() {
|
||||
// 场景初始化逻辑
|
||||
}
|
||||
}
|
||||
|
||||
// 在游戏循环中更新框架
|
||||
function gameLoop() {
|
||||
Core.emitter.emit(CoreEvents.frameUpdated);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 创建实体和组件
|
||||
|
||||
```typescript
|
||||
import { Component, Entity } from '@esengine/ecs-framework';
|
||||
import { Core, Scene, Component, EntitySystem, ECSComponent, ECSSystem, Matcher, Time } from '@esengine/ecs-framework';
|
||||
|
||||
// 定义组件
|
||||
class PositionComponent extends Component {
|
||||
constructor(public x: number = 0, public y: number = 0) {
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
constructor(public x = 0, public y = 0) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
class VelocityComponent extends Component {
|
||||
constructor(public dx: number = 0, public dy: number = 0) {
|
||||
@ECSComponent('Velocity')
|
||||
class Velocity extends Component {
|
||||
constructor(public dx = 0, public dy = 0) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
// 创建实体
|
||||
const entity = scene.createEntity("Player");
|
||||
entity.addComponent(new PositionComponent(100, 100));
|
||||
entity.addComponent(new VelocityComponent(10, 0));
|
||||
```
|
||||
|
||||
### 3. 创建处理系统
|
||||
|
||||
```typescript
|
||||
import { EntitySystem } from '@esengine/ecs-framework';
|
||||
|
||||
// 创建系统
|
||||
@ECSSystem('Movement')
|
||||
class MovementSystem extends EntitySystem {
|
||||
public process(entities: Entity[]) {
|
||||
constructor() {
|
||||
super(Matcher.all(Position, Velocity));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const position = entity.getComponent(PositionComponent);
|
||||
const velocity = entity.getComponent(VelocityComponent);
|
||||
|
||||
if (position && velocity) {
|
||||
position.x += velocity.dx;
|
||||
position.y += velocity.dy;
|
||||
}
|
||||
const position = entity.getComponent(Position)!;
|
||||
const velocity = entity.getComponent(Velocity)!;
|
||||
|
||||
position.x += velocity.dx * Time.deltaTime;
|
||||
position.y += velocity.dy * Time.deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加系统到场景
|
||||
scene.addEntityProcessor(new MovementSystem());
|
||||
```
|
||||
// 创建场景并启动
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.addSystem(new MovementSystem());
|
||||
|
||||
### 4. 实体查询和管理
|
||||
|
||||
```typescript
|
||||
import { EntityManager } from '@esengine/ecs-framework';
|
||||
|
||||
// 使用EntityManager进行高级查询
|
||||
const entityManager = new EntityManager();
|
||||
|
||||
// 查询具有特定组件的实体
|
||||
const movingEntities = entityManager
|
||||
.query()
|
||||
.withAll(PositionComponent, VelocityComponent)
|
||||
.execute();
|
||||
|
||||
// 查询带标签的实体
|
||||
const enemies = entityManager.getEntitiesByTag(1);
|
||||
|
||||
// 批量创建实体
|
||||
const bullets = entityManager.createEntities(100, "bullet");
|
||||
```
|
||||
|
||||
### 5. 事件系统
|
||||
|
||||
```typescript
|
||||
import { EventBus, ECSEventType, EventHandler } from '@esengine/ecs-framework';
|
||||
|
||||
// 获取事件总线
|
||||
const eventBus = entityManager.eventBus;
|
||||
|
||||
// 监听实体创建事件
|
||||
eventBus.onEntityCreated((data) => {
|
||||
console.log(`Entity created: ${data.entityName}`);
|
||||
});
|
||||
|
||||
// 监听组件添加事件
|
||||
eventBus.onComponentAdded((data) => {
|
||||
console.log(`Component ${data.componentType} added to entity ${data.entityId}`);
|
||||
});
|
||||
|
||||
// 使用装饰器自动注册事件监听器
|
||||
class GameManager {
|
||||
@EventHandler(ECSEventType.ENTITY_DESTROYED)
|
||||
onEntityDestroyed(data) {
|
||||
console.log('Entity destroyed:', data.entityName);
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Position(100, 100));
|
||||
player.addComponent(new Velocity(50, 0));
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义事件
|
||||
eventBus.emit('player:levelup', { playerId: 123, newLevel: 5 });
|
||||
```
|
||||
// 启动游戏
|
||||
Core.create();
|
||||
Core.setScene(new GameScene());
|
||||
|
||||
## 🆚 框架对比
|
||||
|
||||
与其他 TypeScript ECS 框架相比,我们的优势:
|
||||
|
||||
| 特性 | @esengine/ecs-framework | bitecs | ecsy | Miniplex |
|
||||
|------|-------------------------|-------|------|----------|
|
||||
| **TypeScript 支持** | ✅ 原生支持 | ✅ 完整支持 | ⚠️ 部分支持 | ✅ 原生支持 |
|
||||
| **事件系统** | ✅ 类型安全+装饰器 | ❌ 无内置事件系统 | ⚠️ 基础事件 | ✅ 响应式事件 |
|
||||
| **查询系统** | ✅ 智能查询+流式API | ✅ 高性能 | ✅ 基础查询 | ✅ 响应式查询 |
|
||||
| **性能优化** | ✅ 多层优化系统 | ✅ 高性能优化 | ⚠️ 基础优化 | ✅ React集成优化 |
|
||||
| **实体管理器** | ✅ 统一管理接口 | ❌ 无统一接口 | ✅ 基础管理 | ✅ 响应式管理 |
|
||||
| **组件索引** | ✅ 哈希+位图索引 | ✅ 原生支持 | ❌ 无索引系统 | ✅ 自动索引 |
|
||||
| **Archetype系统** | ✅ 内置支持 | ✅ 内置支持 | ❌ 无Archetype | ❌ 无Archetype |
|
||||
| **脏标记系统** | ✅ 细粒度追踪 | ⚠️ 基础支持 | ❌ 无脏标记 | ✅ React级追踪 |
|
||||
| **批量操作** | ✅ 全面的批量API | ✅ 批量支持 | ⚠️ 有限支持 | ⚠️ 有限支持 |
|
||||
| **游戏引擎集成** | ✅ 通用设计 | ✅ 通用设计 | ✅ 通用设计 | ⚠️ 主要针对React |
|
||||
| **学习曲线** | 🟢 中等 | 🟡 较陡峭 | 🟢 简单 | 🟡 需要React知识 |
|
||||
| **社区生态** | 🟡 成长中 | 🟢 活跃 | 🟡 稳定 | 🟡 小众但精品 |
|
||||
|
||||
### 为什么选择我们?
|
||||
|
||||
**相比 bitecs**:
|
||||
- 更友好的 TypeScript API,无需手动管理内存
|
||||
- 完整的实体管理器,开发体验更佳
|
||||
- 内置类型安全事件系统,bitecs需要自己实现
|
||||
- 多种索引系统可选,适应不同场景
|
||||
|
||||
**相比 ecsy**:
|
||||
- 现代化的性能优化系统(组件索引、Archetype、脏标记)
|
||||
- 更完整的 TypeScript 类型定义
|
||||
- 增强的事件系统,支持装饰器和异步事件
|
||||
- 活跃的维护和功能更新
|
||||
|
||||
**相比 Miniplex**:
|
||||
- 不依赖 React 生态,可用于任何游戏引擎
|
||||
- 专门针对游戏开发优化
|
||||
- 更轻量级的核心设计
|
||||
- 传统事件模式,更适合游戏开发习惯
|
||||
|
||||
## 📚 核心概念
|
||||
|
||||
### Entity(实体)
|
||||
实体是游戏世界中的基本对象,可以挂载组件和运行系统。
|
||||
|
||||
```typescript
|
||||
// 创建实体
|
||||
const entity = scene.createEntity("Player");
|
||||
|
||||
// 设置实体属性
|
||||
entity.tag = 1;
|
||||
entity.updateOrder = 0;
|
||||
entity.enabled = true;
|
||||
|
||||
// 批量创建实体
|
||||
const entities = scene.createEntities(100, "Enemy");
|
||||
```
|
||||
|
||||
### Component(组件)
|
||||
组件存储数据,定义实体的属性和状态。
|
||||
|
||||
```typescript
|
||||
import { Component } from '@esengine/ecs-framework';
|
||||
|
||||
class HealthComponent extends Component {
|
||||
public maxHealth: number = 100;
|
||||
public currentHealth: number = 100;
|
||||
|
||||
public takeDamage(damage: number) {
|
||||
this.currentHealth = Math.max(0, this.currentHealth - damage);
|
||||
if (this.currentHealth <= 0) {
|
||||
this.entity.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加组件到实体
|
||||
entity.addComponent(new HealthComponent());
|
||||
```
|
||||
|
||||
### System(系统)
|
||||
系统处理具有特定组件的实体集合,实现游戏逻辑。
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, Entity } from '@esengine/ecs-framework';
|
||||
|
||||
class HealthSystem extends EntitySystem {
|
||||
protected process(entities: Entity[]) {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(HealthComponent);
|
||||
if (health && health.currentHealth <= 0) {
|
||||
// 处理实体死亡逻辑
|
||||
entity.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
// 游戏循环中更新
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime);
|
||||
}
|
||||
```
|
||||
|
||||
## 🧪 测试
|
||||
## 核心特性
|
||||
|
||||
```bash
|
||||
# 运行所有测试
|
||||
npm run test
|
||||
- **实体查询** - 使用 Matcher API 进行高效的实体过滤
|
||||
- **事件系统** - 类型安全的事件发布/订阅机制
|
||||
- **性能优化** - SoA 存储优化,支持大规模实体处理
|
||||
- **多线程支持** - Worker系统实现真正的并行计算,充分利用多核CPU
|
||||
- **多场景** - 支持 World/Scene 分层架构
|
||||
- **时间管理** - 内置定时器和时间控制系统
|
||||
|
||||
# 性能基准测试
|
||||
npm run benchmark
|
||||
## 🏗️ 架构设计 / Architecture
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
A[Core 核心] --> B[World 世界]
|
||||
B --> C[Scene 场景]
|
||||
C --> D[EntityManager 实体管理器]
|
||||
C --> E[SystemManager 系统管理器]
|
||||
D --> F[Entity 实体]
|
||||
F --> G[Component 组件]
|
||||
E --> H[EntitySystem 实体系统]
|
||||
E --> I[WorkerSystem 工作线程系统]
|
||||
|
||||
style A fill:#e1f5ff
|
||||
style B fill:#fff3e0
|
||||
style C fill:#f3e5f5
|
||||
style D fill:#e8f5e9
|
||||
style E fill:#fff9c4
|
||||
style F fill:#ffebee
|
||||
style G fill:#e0f2f1
|
||||
style H fill:#fce4ec
|
||||
style I fill:#f1f8e9
|
||||
```
|
||||
|
||||
## 📖 文档
|
||||
## 平台支持
|
||||
|
||||
- [快速入门](docs/getting-started.md) - 从零开始学习框架使用
|
||||
- [EntityManager 使用指南](docs/entity-manager-example.md) - 详细了解实体管理器的高级功能
|
||||
- [事件系统使用指南](docs/event-system-example.md) - 学习类型安全事件系统的完整用法
|
||||
- [性能优化指南](docs/performance-optimization.md) - 深入了解三大性能优化系统
|
||||
- [核心概念](docs/core-concepts.md) - 深入了解 ECS 架构和设计原理
|
||||
- [查询系统使用指南](docs/query-system-usage.md) - 学习高性能查询系统的详细用法
|
||||
支持主流游戏引擎和 Web 平台:
|
||||
|
||||
## 🔗 扩展库
|
||||
- **Cocos Creator**
|
||||
- **Laya 引擎**
|
||||
- **原生 Web** - 浏览器环境直接运行
|
||||
- **小游戏平台** - 微信、支付宝等小游戏
|
||||
|
||||
- [路径寻找库](https://github.com/esengine/ecs-astar) - A*、广度优先、Dijkstra、GOAP 算法
|
||||
- [AI 系统](https://github.com/esengine/BehaviourTree-ai) - 行为树、效用 AI 系统
|
||||
## ECS Framework Editor
|
||||
|
||||
## 🤝 贡献
|
||||
跨平台桌面编辑器,提供可视化开发和调试工具。
|
||||
|
||||
欢迎提交 Issue 和 Pull Request!
|
||||
### 主要功能
|
||||
|
||||
### 开发环境设置
|
||||
- **场景管理** - 可视化场景层级和实体管理
|
||||
- **组件检视** - 实时查看和编辑实体组件
|
||||
- **性能分析** - 内置 Profiler 监控系统性能
|
||||
- **插件系统** - 可扩展的插件架构
|
||||
- **远程调试** - 连接运行中的游戏进行实时调试
|
||||
- **自动更新** - 支持热更新,自动获取最新版本
|
||||
|
||||
```bash
|
||||
# 克隆项目
|
||||
git clone https://github.com/esengine/ecs-framework.git
|
||||
cd ecs-framework
|
||||
### 下载
|
||||
|
||||
# 运行基准测试
|
||||
node benchmark.js
|
||||
[](https://github.com/esengine/ecs-framework/releases/latest)
|
||||
|
||||
# 开发构建 (在source目录)
|
||||
cd source && npm install && npm run build
|
||||
```
|
||||
支持 Windows、macOS (Intel & Apple Silicon)
|
||||
|
||||
### 构建要求
|
||||
### 截图
|
||||
|
||||
- Node.js >= 14.0.0
|
||||
- TypeScript >= 4.0.0
|
||||
<img src="screenshots/main_screetshot.png" alt="ECS Framework Editor" width="800">
|
||||
|
||||
## 📄 许可证
|
||||
<details>
|
||||
<summary>查看更多截图</summary>
|
||||
|
||||
本项目采用 [MIT](LICENSE) 许可证。
|
||||
**性能分析器**
|
||||
<img src="screenshots/performance_profiler.png" alt="Performance Profiler" width="600">
|
||||
|
||||
## 💬 交流群
|
||||
**插件管理**
|
||||
<img src="screenshots/plugin_manager.png" alt="Plugin Manager" width="600">
|
||||
|
||||
加入 QQ 群讨论:[ecs游戏框架交流](https://jq.qq.com/?_wv=1027&k=29w1Nud6)
|
||||
**设置界面**
|
||||
<img src="screenshots/settings.png" alt="Settings" width="600">
|
||||
|
||||
### 🚀 核心性能指标
|
||||
</details>
|
||||
|
||||
```bash
|
||||
实体创建: 640,000+ 个/秒
|
||||
组件查询: O(1) 复杂度(使用索引)
|
||||
内存优化: 30-50% 减少分配
|
||||
批量操作: 显著提升处理效率
|
||||
```
|
||||
## 示例项目
|
||||
|
||||
### 🎯 性能优化技术
|
||||
- [Worker系统演示](https://esengine.github.io/ecs-framework/demos/worker-system/) - 多线程物理系统演示,展示高性能并行计算
|
||||
- [割草机演示](https://github.com/esengine/lawn-mower-demo) - 完整的游戏示例
|
||||
|
||||
- **组件索引系统**: 哈希和位图双重索引,支持 O(1) 查询
|
||||
- **Archetype 系统**: 按组件组合分组,减少查询开销
|
||||
- **脏标记机制**: 细粒度变更追踪,避免不必要的计算
|
||||
- **批量操作 API**: 减少函数调用开销,提升大规模操作效率
|
||||
- **智能缓存**: 查询结果缓存和延迟清理机制
|
||||
## 文档
|
||||
|
||||
### 🔧 性能建议
|
||||
- [📚 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 文档
|
||||
|
||||
1. **大规模场景**: 使用批量API和组件索引
|
||||
2. **频繁查询**: 启用Archetype系统进行快速筛选
|
||||
3. **实时游戏**: 利用脏标记减少无效更新
|
||||
4. **移动端**: 建议实体数量控制在20,000以内
|
||||
## 生态系统
|
||||
|
||||
运行 `npm run benchmark` 查看在您的环境中的具体性能表现。
|
||||
- [路径寻找](https://github.com/esengine/ecs-astar) - A*、BFS、Dijkstra 算法
|
||||
- [AI 系统](https://github.com/esengine/BehaviourTree-ai) - 行为树、效用 AI
|
||||
|
||||
---
|
||||
## 💪 支持项目 / Support the Project
|
||||
|
||||
**ECS Framework** - 让游戏开发更简单、更高效!
|
||||
如果这个项目对你有帮助,请考虑:
|
||||
|
||||
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
|
||||
|
||||
225
docs/.vitepress/config.mjs
Normal file
225
docs/.vitepress/config.mjs
Normal file
@@ -0,0 +1,225 @@
|
||||
import { defineConfig } from 'vitepress'
|
||||
import Icons from 'unplugin-icons/vite'
|
||||
import { readFileSync } from 'fs'
|
||||
import { join, dirname } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const corePackageJson = JSON.parse(
|
||||
readFileSync(join(__dirname, '../../packages/core/package.json'), 'utf-8')
|
||||
)
|
||||
|
||||
export default defineConfig({
|
||||
vite: {
|
||||
plugins: [
|
||||
Icons({
|
||||
compiler: 'vue3',
|
||||
autoInstall: true
|
||||
})
|
||||
],
|
||||
server: {
|
||||
fs: {
|
||||
allow: ['..']
|
||||
},
|
||||
middlewareMode: false,
|
||||
headers: {
|
||||
'Cross-Origin-Embedder-Policy': 'require-corp',
|
||||
'Cross-Origin-Opener-Policy': 'same-origin'
|
||||
}
|
||||
}
|
||||
},
|
||||
title: 'ECS Framework',
|
||||
description: '高性能TypeScript ECS框架 - 为游戏开发而生',
|
||||
lang: 'zh-CN',
|
||||
|
||||
themeConfig: {
|
||||
nav: [
|
||||
{ text: '首页', link: '/' },
|
||||
{ text: '快速开始', link: '/guide/getting-started' },
|
||||
{ text: '指南', link: '/guide/' },
|
||||
{ text: 'API', link: '/api/README' },
|
||||
{
|
||||
text: '示例',
|
||||
items: [
|
||||
{ text: 'Worker系统演示', link: '/examples/worker-system-demo' },
|
||||
{ text: '割草机演示', link: 'https://github.com/esengine/lawn-mower-demo' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: `v${corePackageJson.version}`,
|
||||
link: 'https://github.com/esengine/ecs-framework/releases'
|
||||
}
|
||||
],
|
||||
|
||||
sidebar: {
|
||||
'/guide/': [
|
||||
{
|
||||
text: '开始使用',
|
||||
items: [
|
||||
{ text: '快速开始', link: '/guide/getting-started' },
|
||||
{ text: '指南概览', link: '/guide/' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '核心概念',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: '实体类 (Entity)', link: '/guide/entity' },
|
||||
{ text: '组件系统 (Component)', link: '/guide/component' },
|
||||
{ text: '实体查询系统', link: '/guide/entity-query' },
|
||||
{
|
||||
text: '系统架构 (System)',
|
||||
link: '/guide/system',
|
||||
items: [
|
||||
{ text: 'Worker系统 (多线程)', link: '/guide/worker-system' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '场景管理 (Scene)',
|
||||
link: '/guide/scene',
|
||||
items: [
|
||||
{ text: 'SceneManager', link: '/guide/scene-manager' },
|
||||
{ text: 'WorldManager', link: '/guide/world-manager' }
|
||||
]
|
||||
},
|
||||
{ text: '序列化系统 (Serialization)', link: '/guide/serialization' },
|
||||
{ text: '事件系统 (Event)', link: '/guide/event-system' },
|
||||
{ text: '时间和定时器 (Time)', link: '/guide/time-and-timers' },
|
||||
{ text: '日志系统 (Logger)', link: '/guide/logging' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '高级特性',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: '服务容器 (Service Container)', link: '/guide/service-container' },
|
||||
{ text: '插件系统 (Plugin System)', link: '/guide/plugin-system' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '平台适配器',
|
||||
link: '/guide/platform-adapter',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: '浏览器适配器', link: '/guide/platform-adapter/browser' },
|
||||
{ text: '微信小游戏适配器', link: '/guide/platform-adapter/wechat-minigame' },
|
||||
{ text: 'Node.js适配器', link: '/guide/platform-adapter/nodejs' }
|
||||
]
|
||||
}
|
||||
],
|
||||
'/examples/': [
|
||||
{
|
||||
text: '示例',
|
||||
items: [
|
||||
{ text: '示例概览', link: '/examples/' },
|
||||
{ text: 'Worker系统演示', link: '/examples/worker-system-demo' }
|
||||
]
|
||||
}
|
||||
],
|
||||
'/api/': [
|
||||
{
|
||||
text: 'API 参考',
|
||||
items: [
|
||||
{ text: '概述', link: '/api/README' },
|
||||
{
|
||||
text: '核心类',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: 'Core', link: '/api/classes/Core' },
|
||||
{ text: 'Scene', link: '/api/classes/Scene' },
|
||||
{ text: 'World', link: '/api/classes/World' },
|
||||
{ text: 'Entity', link: '/api/classes/Entity' },
|
||||
{ text: 'Component', link: '/api/classes/Component' },
|
||||
{ text: 'EntitySystem', link: '/api/classes/EntitySystem' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '系统类',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'PassiveSystem', link: '/api/classes/PassiveSystem' },
|
||||
{ text: 'ProcessingSystem', link: '/api/classes/ProcessingSystem' },
|
||||
{ text: 'IntervalSystem', link: '/api/classes/IntervalSystem' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '工具类',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Matcher', link: '/api/classes/Matcher' },
|
||||
{ text: 'Time', link: '/api/classes/Time' },
|
||||
{ text: 'PerformanceMonitor', link: '/api/classes/PerformanceMonitor' },
|
||||
{ text: 'DebugManager', link: '/api/classes/DebugManager' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '接口',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'IScene', link: '/api/interfaces/IScene' },
|
||||
{ text: 'IComponent', link: '/api/interfaces/IComponent' },
|
||||
{ text: 'ISystemBase', link: '/api/interfaces/ISystemBase' },
|
||||
{ text: 'ICoreConfig', link: '/api/interfaces/ICoreConfig' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '装饰器',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: '@ECSComponent', link: '/api/functions/ECSComponent' },
|
||||
{ text: '@ECSSystem', link: '/api/functions/ECSSystem' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '枚举',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'ECSEventType', link: '/api/enumerations/ECSEventType' },
|
||||
{ text: 'LogLevel', link: '/api/enumerations/LogLevel' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
socialLinks: [
|
||||
{ icon: 'github', link: 'https://github.com/esengine/ecs-framework' }
|
||||
],
|
||||
|
||||
footer: {
|
||||
message: 'Released under the MIT License.',
|
||||
copyright: 'Copyright © 2025 ECS Framework'
|
||||
},
|
||||
|
||||
editLink: {
|
||||
pattern: 'https://github.com/esengine/ecs-framework/edit/master/docs/:path',
|
||||
text: '在 GitHub 上编辑此页'
|
||||
},
|
||||
|
||||
search: {
|
||||
provider: 'local'
|
||||
},
|
||||
|
||||
outline: {
|
||||
level: [2, 3],
|
||||
label: '目录'
|
||||
}
|
||||
},
|
||||
|
||||
head: [
|
||||
['meta', { name: 'theme-color', content: '#646cff' }],
|
||||
['link', { rel: 'icon', href: '/favicon.ico' }]
|
||||
],
|
||||
|
||||
base: '/ecs-framework/',
|
||||
cleanUrls: true,
|
||||
|
||||
markdown: {
|
||||
lineNumbers: true,
|
||||
theme: {
|
||||
light: 'github-light',
|
||||
dark: 'github-dark'
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,622 +0,0 @@
|
||||
# 核心概念
|
||||
|
||||
ECS Framework 基于 Entity-Component-System 架构模式,这是一种高度模块化和可扩展的游戏开发架构。本文档将详细介绍框架的核心概念。
|
||||
|
||||
## ECS 架构概述
|
||||
|
||||
ECS 架构将传统的面向对象设计分解为三个核心部分:
|
||||
|
||||
- **Entity(实体)** - 游戏世界中的对象,包含基本属性如位置、旋转、缩放
|
||||
- **Component(组件)** - 包含数据和行为的功能模块
|
||||
- **System(系统)** - 处理实体集合的逻辑处理单元
|
||||
|
||||
## Core(核心)
|
||||
|
||||
Core 是框架的核心管理类,负责游戏的生命周期管理。
|
||||
|
||||
### 创建和配置
|
||||
|
||||
```typescript
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
// 创建核心实例(调试模式)
|
||||
const core = Core.create(true);
|
||||
|
||||
// 创建核心实例(发布模式)
|
||||
const core = Core.create(false);
|
||||
```
|
||||
|
||||
### 事件系统
|
||||
|
||||
```typescript
|
||||
import { CoreEvents } from '@esengine/ecs-framework';
|
||||
|
||||
// 监听核心事件
|
||||
Core.emitter.addObserver(CoreEvents.frameUpdated, this.onUpdate, this);
|
||||
|
||||
// 发送帧更新事件
|
||||
Core.emitter.emit(CoreEvents.frameUpdated);
|
||||
|
||||
// 发送自定义事件
|
||||
Core.emitter.emit("customEvent", { data: "value" });
|
||||
```
|
||||
|
||||
### 定时器系统
|
||||
|
||||
```typescript
|
||||
// 延迟执行
|
||||
Core.schedule(2.0, false, this, (timer) => {
|
||||
console.log("2秒后执行");
|
||||
});
|
||||
|
||||
// 重复执行
|
||||
Core.schedule(1.0, true, this, (timer) => {
|
||||
console.log("每秒执行一次");
|
||||
});
|
||||
```
|
||||
|
||||
## Scene(场景)
|
||||
|
||||
场景是游戏世界的容器,管理实体和系统的生命周期。
|
||||
|
||||
### 创建和使用场景
|
||||
|
||||
```typescript
|
||||
import { Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// 创建场景
|
||||
const scene = new Scene();
|
||||
scene.name = "GameScene";
|
||||
|
||||
// 设置为当前场景
|
||||
Core.scene = scene;
|
||||
|
||||
// 场景生命周期
|
||||
scene.begin(); // 开始场景
|
||||
scene.update(); // 更新场景
|
||||
scene.end(); // 结束场景
|
||||
```
|
||||
|
||||
### 批量实体管理
|
||||
|
||||
```typescript
|
||||
// 批量创建实体 - 高性能
|
||||
const entities = scene.createEntities(1000, "Enemy");
|
||||
|
||||
// 批量添加实体(延迟缓存清理)
|
||||
entities.forEach(entity => {
|
||||
scene.addEntity(entity, false); // 延迟清理
|
||||
});
|
||||
scene.querySystem.clearCache(); // 手动清理缓存
|
||||
|
||||
// 获取性能统计
|
||||
const stats = scene.getPerformanceStats();
|
||||
console.log(`实体数量: ${stats.entityCount}`);
|
||||
```
|
||||
|
||||
## Entity(实体)
|
||||
|
||||
实体是游戏世界中的基本对象,包含位置、旋转、缩放等基本属性。
|
||||
|
||||
### 实体的基本属性
|
||||
|
||||
```typescript
|
||||
import { Vector2 } from '@esengine/ecs-framework';
|
||||
|
||||
const entity = scene.createEntity("MyEntity");
|
||||
|
||||
// 位置
|
||||
entity.position = new Vector2(100, 200);
|
||||
entity.position = entity.position.add(new Vector2(10, 0));
|
||||
|
||||
// 旋转(弧度)
|
||||
entity.rotation = Math.PI / 4;
|
||||
|
||||
// 缩放
|
||||
entity.scale = new Vector2(2, 2);
|
||||
|
||||
// 标签(用于分类)
|
||||
entity.tag = 1;
|
||||
|
||||
// 启用状态
|
||||
entity.enabled = true;
|
||||
|
||||
// 活跃状态
|
||||
entity.active = true;
|
||||
|
||||
// 更新顺序
|
||||
entity.updateOrder = 10;
|
||||
```
|
||||
|
||||
### 实体层级关系
|
||||
|
||||
```typescript
|
||||
// 添加子实体
|
||||
const parent = scene.createEntity("Parent");
|
||||
const child = scene.createEntity("Child");
|
||||
parent.addChild(child);
|
||||
|
||||
// 获取父实体
|
||||
const parentEntity = child.parent;
|
||||
|
||||
// 获取所有子实体
|
||||
const children = parent.children;
|
||||
|
||||
// 查找子实体
|
||||
const foundChild = parent.findChild("Child");
|
||||
|
||||
// 按标签查找子实体
|
||||
const taggedChildren = parent.findChildrenByTag(1);
|
||||
|
||||
// 移除子实体
|
||||
parent.removeChild(child);
|
||||
|
||||
// 移除所有子实体
|
||||
parent.removeAllChildren();
|
||||
```
|
||||
|
||||
### 实体生命周期
|
||||
|
||||
```typescript
|
||||
// 检查实体是否被销毁
|
||||
if (!entity.isDestroyed) {
|
||||
// 实体仍然有效
|
||||
}
|
||||
|
||||
// 销毁实体
|
||||
entity.destroy();
|
||||
|
||||
// 获取实体调试信息
|
||||
const debugInfo = entity.getDebugInfo();
|
||||
console.log(debugInfo);
|
||||
```
|
||||
|
||||
## Component(组件)
|
||||
|
||||
组件包含数据和行为,定义了实体的特性和能力。
|
||||
|
||||
### 创建组件
|
||||
|
||||
```typescript
|
||||
import { Component } from '@esengine/ecs-framework';
|
||||
|
||||
class HealthComponent extends Component {
|
||||
public maxHealth: number = 100;
|
||||
public currentHealth: number = 100;
|
||||
|
||||
public takeDamage(damage: number) {
|
||||
this.currentHealth -= damage;
|
||||
if (this.currentHealth <= 0) {
|
||||
this.entity.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
public heal(amount: number) {
|
||||
this.currentHealth = Math.min(this.maxHealth, this.currentHealth + amount);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 组件生命周期
|
||||
|
||||
```typescript
|
||||
class MyComponent extends Component {
|
||||
public onAddedToEntity() {
|
||||
// 组件被添加到实体时调用
|
||||
console.log("组件已添加到实体:", this.entity.name);
|
||||
}
|
||||
|
||||
public onRemovedFromEntity() {
|
||||
// 组件从实体移除时调用
|
||||
console.log("组件已从实体移除");
|
||||
}
|
||||
|
||||
public onEnabled() {
|
||||
// 组件启用时调用
|
||||
console.log("组件已启用");
|
||||
}
|
||||
|
||||
public onDisabled() {
|
||||
// 组件禁用时调用
|
||||
console.log("组件已禁用");
|
||||
}
|
||||
|
||||
public update() {
|
||||
// 每帧更新(如果组件启用)
|
||||
console.log("组件更新");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 组件管理
|
||||
|
||||
```typescript
|
||||
// 添加组件
|
||||
const health = entity.addComponent(new HealthComponent());
|
||||
|
||||
// 创建并添加组件
|
||||
const movement = entity.createComponent(MovementComponent, 200); // 传递构造参数
|
||||
|
||||
// 获取组件
|
||||
const healthComp = entity.getComponent(HealthComponent);
|
||||
|
||||
// 检查组件是否存在
|
||||
if (entity.hasComponent(HealthComponent)) {
|
||||
// 处理逻辑
|
||||
}
|
||||
|
||||
// 获取或创建组件
|
||||
const weapon = entity.getOrCreateComponent(WeaponComponent);
|
||||
|
||||
// 获取多个同类型组件
|
||||
const allHealthComps = entity.getComponents(HealthComponent);
|
||||
|
||||
// 移除组件
|
||||
entity.removeComponent(healthComp);
|
||||
|
||||
// 按类型移除组件
|
||||
entity.removeComponentByType(HealthComponent);
|
||||
|
||||
// 移除所有组件
|
||||
entity.removeAllComponents();
|
||||
```
|
||||
|
||||
### 组件对象池优化
|
||||
|
||||
```typescript
|
||||
import { Component, ComponentPoolManager } from '@esengine/ecs-framework';
|
||||
|
||||
class BulletComponent extends Component {
|
||||
public damage: number = 10;
|
||||
public speed: number = 300;
|
||||
|
||||
// 对象池重置方法
|
||||
public reset() {
|
||||
this.damage = 10;
|
||||
this.speed = 300;
|
||||
}
|
||||
}
|
||||
|
||||
// 注册组件池
|
||||
ComponentPoolManager.getInstance().registerPool(BulletComponent, 1000);
|
||||
|
||||
// 使用对象池获取组件
|
||||
const bullet = ComponentPoolManager.getInstance().getComponent(BulletComponent);
|
||||
entity.addComponent(bullet);
|
||||
|
||||
// 释放组件回对象池
|
||||
ComponentPoolManager.getInstance().releaseComponent(bullet);
|
||||
|
||||
// 预热组件池
|
||||
ComponentPoolManager.getInstance().preWarmPools({
|
||||
BulletComponent: 1000,
|
||||
EffectComponent: 500
|
||||
});
|
||||
|
||||
// 获取池统计
|
||||
const stats = ComponentPoolManager.getInstance().getPoolStats();
|
||||
console.log('组件池统计:', stats);
|
||||
```
|
||||
|
||||
## Scene(场景)
|
||||
|
||||
场景是实体和系统的容器,管理游戏世界的状态。
|
||||
|
||||
### 场景生命周期
|
||||
|
||||
```typescript
|
||||
class GameScene extends es.Scene {
|
||||
public initialize() {
|
||||
// 场景初始化,创建实体和系统
|
||||
this.setupEntities();
|
||||
this.setupSystems();
|
||||
}
|
||||
|
||||
public onStart() {
|
||||
// 场景开始运行时调用
|
||||
console.log("场景开始");
|
||||
}
|
||||
|
||||
public unload() {
|
||||
// 场景卸载时调用
|
||||
console.log("场景卸载");
|
||||
}
|
||||
|
||||
private setupEntities() {
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new PlayerComponent());
|
||||
}
|
||||
|
||||
private setupSystems() {
|
||||
this.addEntityProcessor(new MovementSystem());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 实体管理
|
||||
|
||||
```typescript
|
||||
// 创建实体
|
||||
const entity = scene.createEntity("MyEntity");
|
||||
|
||||
// 添加现有实体
|
||||
scene.addEntity(entity);
|
||||
|
||||
// 查找实体
|
||||
const player = scene.findEntity("Player");
|
||||
const entityById = scene.findEntityById(123);
|
||||
const entitiesByTag = scene.findEntitiesByTag(1);
|
||||
|
||||
// 销毁所有实体
|
||||
scene.destroyAllEntities();
|
||||
|
||||
// 获取场景统计信息
|
||||
const stats = scene.getStats();
|
||||
console.log("实体数量:", stats.entityCount);
|
||||
console.log("系统数量:", stats.processorCount);
|
||||
```
|
||||
|
||||
## System(系统)
|
||||
|
||||
系统处理实体集合,实现游戏的核心逻辑。
|
||||
|
||||
### EntitySystem
|
||||
|
||||
最常用的系统类型,处理实体集合:
|
||||
|
||||
```typescript
|
||||
class MovementSystem extends es.EntitySystem {
|
||||
protected process(entities: es.Entity[]) {
|
||||
for (const entity of entities) {
|
||||
const movement = entity.getComponent(MovementComponent);
|
||||
if (movement) {
|
||||
movement.update();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ProcessingSystem
|
||||
|
||||
定期处理的系统:
|
||||
|
||||
```typescript
|
||||
class HealthRegenerationSystem extends es.ProcessingSystem {
|
||||
protected process(entities: es.Entity[]) {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(HealthComponent);
|
||||
if (health && health.currentHealth < health.maxHealth) {
|
||||
health.currentHealth += 10 * es.Time.deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### IntervalSystem
|
||||
|
||||
按时间间隔执行的系统:
|
||||
|
||||
```typescript
|
||||
class SpawnSystem extends es.IntervalSystem {
|
||||
constructor() {
|
||||
super(3.0); // 每3秒执行一次
|
||||
}
|
||||
|
||||
protected processSystem() {
|
||||
// 生成敌人
|
||||
const enemy = this.scene.createEntity("Enemy");
|
||||
enemy.addComponent(new EnemyComponent());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### PassiveSystem
|
||||
|
||||
被动系统,不自动处理实体:
|
||||
|
||||
```typescript
|
||||
class CollisionSystem extends es.PassiveSystem {
|
||||
public checkCollisions() {
|
||||
// 手动调用的碰撞检测逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Time(时间)
|
||||
|
||||
时间管理工具类,提供游戏时间相关功能:
|
||||
|
||||
```typescript
|
||||
// 获取时间信息
|
||||
console.log("帧时间:", es.Time.deltaTime);
|
||||
console.log("总时间:", es.Time.totalTime);
|
||||
console.log("帧数:", es.Time.frameCount);
|
||||
console.log("时间缩放:", es.Time.timeScale);
|
||||
|
||||
// 设置时间缩放(慢动作效果)
|
||||
es.Time.timeScale = 0.5;
|
||||
|
||||
// 检查时间间隔
|
||||
if (es.Time.checkEvery(1.0, lastCheckTime)) {
|
||||
// 每秒执行一次
|
||||
}
|
||||
```
|
||||
|
||||
## Vector2(二维向量)
|
||||
|
||||
二维向量类,提供数学运算:
|
||||
|
||||
```typescript
|
||||
// 创建向量
|
||||
const vec1 = new es.Vector2(10, 20);
|
||||
const vec2 = es.Vector2.zero;
|
||||
const vec3 = es.Vector2.one;
|
||||
|
||||
// 向量运算
|
||||
const sum = vec1.add(vec2);
|
||||
const diff = vec1.subtract(vec2);
|
||||
const scaled = vec1.multiply(2);
|
||||
const normalized = vec1.normalize();
|
||||
|
||||
// 向量属性
|
||||
console.log("长度:", vec1.length);
|
||||
console.log("长度平方:", vec1.lengthSquared);
|
||||
|
||||
// 静态方法
|
||||
const distance = es.Vector2.distance(vec1, vec2);
|
||||
const lerped = es.Vector2.lerp(vec1, vec2, 0.5);
|
||||
const fromAngle = es.Vector2.fromAngle(Math.PI / 4);
|
||||
```
|
||||
|
||||
## 性能监控
|
||||
|
||||
框架内置性能监控工具:
|
||||
|
||||
```typescript
|
||||
// 获取性能监控实例
|
||||
const monitor = es.PerformanceMonitor.instance;
|
||||
|
||||
// 查看性能数据
|
||||
console.log("平均FPS:", monitor.averageFPS);
|
||||
console.log("最小FPS:", monitor.minFPS);
|
||||
console.log("最大FPS:", monitor.maxFPS);
|
||||
console.log("内存使用:", monitor.memoryUsage);
|
||||
|
||||
// 重置性能数据
|
||||
monitor.reset();
|
||||
```
|
||||
|
||||
## 对象池
|
||||
|
||||
内存管理优化工具:
|
||||
|
||||
```typescript
|
||||
// 创建对象池
|
||||
class BulletPool extends es.Pool<Bullet> {
|
||||
protected createObject(): Bullet {
|
||||
return new Bullet();
|
||||
}
|
||||
}
|
||||
|
||||
const bulletPool = new BulletPool();
|
||||
|
||||
// 使用对象池
|
||||
const bullet = bulletPool.obtain();
|
||||
// 使用bullet...
|
||||
bulletPool.free(bullet);
|
||||
|
||||
// 清空对象池
|
||||
bulletPool.clear();
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 实体设计
|
||||
|
||||
- 实体只包含基本属性,功能通过组件实现
|
||||
- 合理使用实体层级关系
|
||||
- 及时销毁不需要的实体
|
||||
|
||||
### 2. 组件设计
|
||||
|
||||
- 组件保持单一职责
|
||||
- 使用生命周期方法进行初始化和清理
|
||||
- 避免组件间直接依赖
|
||||
|
||||
### 3. 系统设计
|
||||
|
||||
- 系统专注于特定逻辑处理
|
||||
- 合理设置系统更新顺序
|
||||
- 使用被动系统处理特殊逻辑
|
||||
|
||||
### 4. 性能优化
|
||||
|
||||
- 使用对象池减少内存分配
|
||||
- 监控性能数据
|
||||
- 合理使用时间缩放
|
||||
|
||||
## 高级性能优化功能
|
||||
|
||||
### 位掩码优化器
|
||||
|
||||
位掩码优化器可以预计算和缓存常用的组件掩码,提升查询性能。
|
||||
|
||||
```typescript
|
||||
import { BitMaskOptimizer } from '@esengine/ecs-framework';
|
||||
|
||||
const optimizer = BitMaskOptimizer.getInstance();
|
||||
|
||||
// 注册组件类型
|
||||
optimizer.registerComponentType(PositionComponent);
|
||||
optimizer.registerComponentType(VelocityComponent);
|
||||
optimizer.registerComponentType(RenderComponent);
|
||||
|
||||
// 预计算常用掩码组合
|
||||
optimizer.precomputeCommonMasks();
|
||||
|
||||
// 获取优化的掩码
|
||||
const positionMask = optimizer.getComponentMask(PositionComponent);
|
||||
const movementMask = optimizer.getCombinedMask([PositionComponent, VelocityComponent]);
|
||||
|
||||
// 掩码操作
|
||||
const hasBothComponents = optimizer.hasAllComponents(entityMask, movementMask);
|
||||
const hasAnyComponent = optimizer.hasAnyComponent(entityMask, movementMask);
|
||||
|
||||
// 获取掩码分析
|
||||
const analysis = optimizer.analyzeMask(entityMask);
|
||||
console.log('掩码包含的组件类型:', analysis.componentTypes);
|
||||
```
|
||||
|
||||
### 延迟索引更新器
|
||||
|
||||
批量更新索引可以显著提升大规模实体操作的性能。
|
||||
|
||||
```typescript
|
||||
import { IndexUpdateBatcher } from '@esengine/ecs-framework';
|
||||
|
||||
const batcher = new IndexUpdateBatcher((updates) => {
|
||||
// 处理批量更新
|
||||
console.log(`批量处理 ${updates.length} 个索引更新`);
|
||||
});
|
||||
|
||||
// 配置批量大小和延迟
|
||||
batcher.configure(100, 16); // 批量大小100,延迟16ms
|
||||
|
||||
// 添加更新任务
|
||||
batcher.addUpdate("add", entity, componentMask);
|
||||
batcher.addUpdate("remove", entity, componentMask);
|
||||
|
||||
// 强制刷新
|
||||
batcher.flush();
|
||||
```
|
||||
|
||||
### 批量操作API
|
||||
|
||||
```typescript
|
||||
// 批量创建实体 - 最高性能
|
||||
const entities = scene.createEntities(10000, "Bullets");
|
||||
|
||||
// 延迟缓存清理
|
||||
entities.forEach(entity => {
|
||||
scene.addEntity(entity, false); // 延迟清理
|
||||
});
|
||||
scene.querySystem.clearCache(); // 手动清理
|
||||
|
||||
// 批量查询优化
|
||||
const movingEntities = scene.getEntitiesWithComponents([PositionComponent, VelocityComponent]);
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
ECS Framework 提供了完整的实体组件系统架构:
|
||||
|
||||
- **Core** 管理游戏生命周期和全局功能
|
||||
- **Entity** 作为游戏对象的基础容器
|
||||
- **Component** 实现具体的功能模块,支持对象池优化
|
||||
- **System** 处理游戏逻辑
|
||||
- **Scene** 管理游戏世界状态,支持批量操作
|
||||
- **高级优化** 位掩码优化器、组件对象池、批量操作等
|
||||
|
||||
通过合理使用这些核心概念和优化功能,可以构建出高性能、结构清晰、易于维护的游戏代码。
|
||||
@@ -1,482 +0,0 @@
|
||||
# 实体使用指南
|
||||
|
||||
本指南详细介绍 ECS Framework 中实体(Entity)的所有功能和使用方法。
|
||||
|
||||
## 实体概述
|
||||
|
||||
实体(Entity)是 ECS 架构中的核心概念之一,它作为组件的容器存在。实体本身不包含游戏逻辑,所有功能都通过添加不同的组件来实现。
|
||||
|
||||
### 实体的特点
|
||||
|
||||
- **轻量级容器**:实体只是组件的载体,不包含具体的游戏逻辑
|
||||
- **唯一标识**:每个实体都有唯一的ID和名称
|
||||
- **层次结构**:支持父子关系,可以构建复杂的实体层次
|
||||
- **高性能查询**:基于位掩码的组件查询系统
|
||||
- **生命周期管理**:完整的创建、更新、销毁流程
|
||||
|
||||
## 创建实体
|
||||
|
||||
### 基本创建方式
|
||||
|
||||
```typescript
|
||||
import { Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// 通过场景创建实体
|
||||
const scene = new Scene();
|
||||
const entity = scene.createEntity("Player");
|
||||
|
||||
console.log(entity.name); // "Player"
|
||||
console.log(entity.id); // 唯一的数字ID
|
||||
```
|
||||
|
||||
### 批量创建实体(推荐)
|
||||
|
||||
```typescript
|
||||
import { Scene } from '@esengine/ecs-framework';
|
||||
|
||||
const scene = new Scene();
|
||||
|
||||
// 批量创建1000个实体 - 高性能
|
||||
const entities = scene.createEntities(1000, "Enemy");
|
||||
|
||||
// 批量配置
|
||||
entities.forEach((entity, index) => {
|
||||
entity.tag = 2; // 敌人标签
|
||||
// 添加组件...
|
||||
});
|
||||
```
|
||||
|
||||
### 使用流式API创建
|
||||
|
||||
```typescript
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
// 使用ECS流式API
|
||||
const entity = Core.ecsAPI
|
||||
?.entity("Enemy")
|
||||
.withComponent(new PositionComponent(100, 200))
|
||||
.withComponent(new HealthComponent(50))
|
||||
.withTag(2)
|
||||
.build();
|
||||
```
|
||||
|
||||
## 实体属性
|
||||
|
||||
### 基本属性
|
||||
|
||||
```typescript
|
||||
// 实体名称 - 用于调试和标识
|
||||
entity.name = "Player";
|
||||
|
||||
// 实体ID - 只读,场景内唯一
|
||||
console.log(entity.id); // 例如: 1
|
||||
|
||||
// 标签 - 用于分类和快速查询
|
||||
entity.tag = 1; // 玩家标签
|
||||
entity.tag = 2; // 敌人标签
|
||||
|
||||
// 更新顺序 - 控制实体在系统中的处理优先级
|
||||
entity.updateOrder = 0; // 数值越小优先级越高
|
||||
```
|
||||
|
||||
### 状态控制
|
||||
|
||||
```typescript
|
||||
// 启用状态 - 控制实体是否参与更新和处理
|
||||
entity.enabled = true; // 启用实体
|
||||
entity.enabled = false; // 禁用实体
|
||||
|
||||
// 激活状态 - 控制实体及其子实体的活跃状态
|
||||
entity.active = true; // 激活实体
|
||||
entity.active = false; // 停用实体
|
||||
|
||||
// 检查层次结构中的激活状态
|
||||
if (entity.activeInHierarchy) {
|
||||
// 实体在整个层次结构中都是激活的
|
||||
}
|
||||
|
||||
// 检查销毁状态
|
||||
if (entity.isDestroyed) {
|
||||
// 实体已被销毁
|
||||
}
|
||||
```
|
||||
|
||||
### 更新间隔
|
||||
|
||||
```typescript
|
||||
// 控制实体更新频率
|
||||
entity.updateInterval = 1; // 每帧更新
|
||||
entity.updateInterval = 2; // 每2帧更新一次
|
||||
entity.updateInterval = 5; // 每5帧更新一次
|
||||
```
|
||||
|
||||
## 组件管理
|
||||
|
||||
### 添加组件
|
||||
|
||||
```typescript
|
||||
// 创建并添加组件
|
||||
const healthComponent = entity.addComponent(new HealthComponent(100));
|
||||
|
||||
// 使用工厂方法创建组件
|
||||
const positionComponent = entity.createComponent(PositionComponent, 100, 200);
|
||||
|
||||
// 批量添加组件
|
||||
const components = entity.addComponents([
|
||||
new PositionComponent(0, 0),
|
||||
new VelocityComponent(50, 0),
|
||||
new HealthComponent(100)
|
||||
]);
|
||||
```
|
||||
|
||||
### 获取组件
|
||||
|
||||
```typescript
|
||||
// 获取单个组件
|
||||
const health = entity.getComponent(HealthComponent);
|
||||
if (health) {
|
||||
console.log(`当前生命值: ${health.currentHealth}`);
|
||||
}
|
||||
|
||||
// 获取或创建组件(如果不存在则创建)
|
||||
const position = entity.getOrCreateComponent(PositionComponent, 0, 0);
|
||||
|
||||
// 获取多个同类型组件(如果组件可以重复添加)
|
||||
const allHealthComponents = entity.getComponents(HealthComponent);
|
||||
```
|
||||
|
||||
### 检查组件
|
||||
|
||||
```typescript
|
||||
// 检查是否拥有指定组件
|
||||
if (entity.hasComponent(HealthComponent)) {
|
||||
// 实体拥有生命值组件
|
||||
}
|
||||
|
||||
// 检查组件掩码(高性能)
|
||||
const mask = entity.componentMask;
|
||||
console.log(`组件掩码: ${mask.toString(2)}`);
|
||||
```
|
||||
|
||||
### 移除组件
|
||||
|
||||
```typescript
|
||||
// 移除指定组件实例
|
||||
const healthComponent = entity.getComponent(HealthComponent);
|
||||
if (healthComponent) {
|
||||
entity.removeComponent(healthComponent);
|
||||
}
|
||||
|
||||
// 按类型移除组件
|
||||
const removedHealth = entity.removeComponentByType(HealthComponent);
|
||||
|
||||
// 批量移除组件
|
||||
const removedComponents = entity.removeComponentsByTypes([
|
||||
HealthComponent,
|
||||
VelocityComponent
|
||||
]);
|
||||
|
||||
// 移除所有组件
|
||||
entity.removeAllComponents();
|
||||
```
|
||||
|
||||
## 层次结构管理
|
||||
|
||||
### 父子关系
|
||||
|
||||
```typescript
|
||||
// 创建父子实体
|
||||
const player = scene.createEntity("Player");
|
||||
const weapon = scene.createEntity("Weapon");
|
||||
const shield = scene.createEntity("Shield");
|
||||
|
||||
// 添加子实体
|
||||
player.addChild(weapon);
|
||||
player.addChild(shield);
|
||||
|
||||
// 获取父实体
|
||||
console.log(weapon.parent === player); // true
|
||||
|
||||
// 获取所有子实体
|
||||
const children = player.children;
|
||||
console.log(children.length); // 2
|
||||
|
||||
// 获取子实体数量
|
||||
console.log(player.childCount); // 2
|
||||
```
|
||||
|
||||
### 查找子实体
|
||||
|
||||
```typescript
|
||||
// 按名称查找子实体
|
||||
const weapon = player.findChild("Weapon");
|
||||
|
||||
// 递归查找子实体
|
||||
const deepChild = player.findChild("DeepChild", true);
|
||||
|
||||
// 按标签查找子实体
|
||||
const enemies = player.findChildrenByTag(2); // 查找所有敌人标签的子实体
|
||||
|
||||
// 递归按标签查找
|
||||
const allEnemies = player.findChildrenByTag(2, true);
|
||||
```
|
||||
|
||||
### 层次结构操作
|
||||
|
||||
```typescript
|
||||
// 移除子实体
|
||||
const removed = player.removeChild(weapon);
|
||||
|
||||
// 移除所有子实体
|
||||
player.removeAllChildren();
|
||||
|
||||
// 获取根实体
|
||||
const root = weapon.getRoot();
|
||||
|
||||
// 检查祖先关系
|
||||
if (player.isAncestorOf(weapon)) {
|
||||
// player 是 weapon 的祖先
|
||||
}
|
||||
|
||||
// 检查后代关系
|
||||
if (weapon.isDescendantOf(player)) {
|
||||
// weapon 是 player 的后代
|
||||
}
|
||||
|
||||
// 获取实体在层次结构中的深度
|
||||
const depth = weapon.getDepth(); // 从根实体开始计算的深度
|
||||
```
|
||||
|
||||
### 遍历子实体
|
||||
|
||||
```typescript
|
||||
// 遍历直接子实体
|
||||
player.forEachChild((child, index) => {
|
||||
console.log(`子实体 ${index}: ${child.name}`);
|
||||
});
|
||||
|
||||
// 递归遍历所有子实体
|
||||
player.forEachChild((child, index) => {
|
||||
console.log(`子实体 ${index}: ${child.name} (深度: ${child.getDepth()})`);
|
||||
}, true);
|
||||
```
|
||||
|
||||
## 实体生命周期
|
||||
|
||||
### 更新循环
|
||||
|
||||
```typescript
|
||||
// 手动更新实体(通常由场景自动调用)
|
||||
entity.update();
|
||||
|
||||
// 实体会自动调用所有组件的update方法
|
||||
class MyComponent extends Component {
|
||||
public update(): void {
|
||||
// 组件更新逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 销毁实体
|
||||
|
||||
```typescript
|
||||
// 销毁实体
|
||||
entity.destroy();
|
||||
|
||||
// 检查是否已销毁
|
||||
if (entity.isDestroyed) {
|
||||
console.log("实体已被销毁");
|
||||
}
|
||||
|
||||
// 销毁实体时会自动:
|
||||
// 1. 移除所有组件
|
||||
// 2. 从父实体中移除
|
||||
// 3. 销毁所有子实体
|
||||
// 4. 从场景中移除
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 组件缓存
|
||||
|
||||
```typescript
|
||||
// 预热组件缓存(提高后续访问性能)
|
||||
entity.warmUpComponentCache();
|
||||
|
||||
// 清理组件缓存
|
||||
entity.cleanupComponentCache();
|
||||
|
||||
// 获取缓存统计信息
|
||||
const cacheStats = entity.getComponentCacheStats();
|
||||
console.log(`缓存命中率: ${cacheStats.cacheStats.hitRate}`);
|
||||
console.log(`组件访问统计:`, cacheStats.accessStats);
|
||||
```
|
||||
|
||||
### 批量操作
|
||||
|
||||
```typescript
|
||||
// 批量添加组件(比单个添加更高效)
|
||||
const components = entity.addComponents([
|
||||
new PositionComponent(0, 0),
|
||||
new VelocityComponent(50, 0),
|
||||
new HealthComponent(100)
|
||||
]);
|
||||
|
||||
// 批量移除组件
|
||||
const removed = entity.removeComponentsByTypes([
|
||||
HealthComponent,
|
||||
VelocityComponent
|
||||
]);
|
||||
```
|
||||
|
||||
## 调试和监控
|
||||
|
||||
### 调试信息
|
||||
|
||||
```typescript
|
||||
// 获取详细的调试信息
|
||||
const debugInfo = entity.getDebugInfo();
|
||||
console.log("实体调试信息:", debugInfo);
|
||||
|
||||
// 调试信息包含:
|
||||
// - 基本属性(名称、ID、状态等)
|
||||
// - 组件信息(数量、类型、掩码等)
|
||||
// - 层次结构信息(父子关系、深度等)
|
||||
// - 性能统计(缓存命中率、访问统计等)
|
||||
```
|
||||
|
||||
### 实体比较
|
||||
|
||||
```typescript
|
||||
// 比较两个实体的优先级
|
||||
const result = entity1.compareTo(entity2);
|
||||
if (result < 0) {
|
||||
// entity1 优先级更高
|
||||
} else if (result > 0) {
|
||||
// entity2 优先级更高
|
||||
} else {
|
||||
// 优先级相同
|
||||
}
|
||||
|
||||
// 实体的字符串表示
|
||||
console.log(entity.toString()); // "Entity[Player:1]"
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 合理使用标签
|
||||
|
||||
```typescript
|
||||
// 定义标签常量
|
||||
const Tags = {
|
||||
PLAYER: 1,
|
||||
ENEMY: 2,
|
||||
PROJECTILE: 3,
|
||||
PICKUP: 4
|
||||
} as const;
|
||||
|
||||
// 使用标签进行分类
|
||||
player.tag = Tags.PLAYER;
|
||||
enemy.tag = Tags.ENEMY;
|
||||
```
|
||||
|
||||
### 2. 优化更新顺序
|
||||
|
||||
```typescript
|
||||
// 设置合理的更新顺序
|
||||
player.updateOrder = 0; // 玩家最先更新
|
||||
enemy.updateOrder = 1; // 敌人其次
|
||||
projectile.updateOrder = 2; // 投射物最后
|
||||
```
|
||||
|
||||
### 3. 合理使用层次结构
|
||||
|
||||
```typescript
|
||||
// 创建复合实体
|
||||
const tank = scene.createEntity("Tank");
|
||||
const turret = scene.createEntity("Turret");
|
||||
const barrel = scene.createEntity("Barrel");
|
||||
|
||||
// 建立层次关系
|
||||
tank.addChild(turret);
|
||||
turret.addChild(barrel);
|
||||
|
||||
// 这样可以通过控制父实体来影响整个层次结构
|
||||
tank.active = false; // 整个坦克都会被停用
|
||||
```
|
||||
|
||||
### 4. 组件缓存优化
|
||||
|
||||
```typescript
|
||||
// 对于频繁访问的组件,预热缓存
|
||||
entity.warmUpComponentCache();
|
||||
|
||||
// 定期清理不常用的缓存
|
||||
setInterval(() => {
|
||||
entity.cleanupComponentCache();
|
||||
}, 5000);
|
||||
```
|
||||
|
||||
### 5. 避免内存泄漏
|
||||
|
||||
```typescript
|
||||
// 确保正确销毁实体
|
||||
if (entity.isDestroyed) {
|
||||
return; // 避免操作已销毁的实体
|
||||
}
|
||||
|
||||
// 在适当的时候销毁不需要的实体
|
||||
if (enemy.getComponent(HealthComponent)?.isDead()) {
|
||||
enemy.destroy();
|
||||
}
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 实体可以在不同场景间移动吗?
|
||||
|
||||
A: 不可以。实体与场景紧密绑定,如果需要在场景间传递数据,应该序列化实体的组件数据,然后在新场景中重新创建。
|
||||
|
||||
### Q: 如何实现实体的位置、旋转、缩放?
|
||||
|
||||
A: 框架本身不提供这些属性,需要通过组件来实现:
|
||||
|
||||
```typescript
|
||||
class TransformComponent extends Component {
|
||||
public position = { x: 0, y: 0 };
|
||||
public rotation = 0;
|
||||
public scale = { x: 1, y: 1 };
|
||||
}
|
||||
|
||||
const transform = entity.addComponent(new TransformComponent());
|
||||
transform.position.x = 100;
|
||||
transform.rotation = Math.PI / 4;
|
||||
```
|
||||
|
||||
### Q: 实体的更新顺序如何影响性能?
|
||||
|
||||
A: 更新顺序主要影响游戏逻辑的执行顺序,对性能影响较小。但合理的更新顺序可以避免一些逻辑问题,比如确保输入处理在移动之前执行。
|
||||
|
||||
### Q: 如何处理大量实体的性能问题?
|
||||
|
||||
A:
|
||||
1. 使用对象池重用实体
|
||||
2. 合理使用组件缓存
|
||||
3. 避免不必要的组件查询
|
||||
4. 使用批量操作
|
||||
5. 定期清理销毁的实体
|
||||
|
||||
```typescript
|
||||
// 使用对象池
|
||||
class EntityPool extends Pool<Entity> {
|
||||
protected createObject(): Entity {
|
||||
return scene.createEntity("PooledEntity");
|
||||
}
|
||||
|
||||
protected resetObject(entity: Entity): void {
|
||||
entity.removeAllComponents();
|
||||
entity.active = true;
|
||||
entity.enabled = true;
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,421 +0,0 @@
|
||||
# EntityManager 使用指南
|
||||
|
||||
EntityManager 是 ECS Framework 的核心管理系统,提供统一的实体管理、高性能查询和自动优化功能。
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 创建实体管理器
|
||||
|
||||
```typescript
|
||||
import { EntityManager, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// 通常在游戏管理器中创建
|
||||
const scene = new Scene();
|
||||
const entityManager = new EntityManager(scene);
|
||||
```
|
||||
|
||||
### 基础实体操作
|
||||
|
||||
```typescript
|
||||
// 创建单个实体
|
||||
const player = entityManager.createEntity("Player");
|
||||
player.addComponent(new PositionComponent(100, 100));
|
||||
player.addComponent(new HealthComponent(100));
|
||||
player.tag = "player";
|
||||
|
||||
// 批量创建实体
|
||||
const enemies = entityManager.createEntities(50, "Enemy");
|
||||
enemies.forEach((enemy, index) => {
|
||||
enemy.addComponent(new PositionComponent(
|
||||
Math.random() * 800,
|
||||
Math.random() * 600
|
||||
));
|
||||
enemy.addComponent(new HealthComponent(30));
|
||||
enemy.tag = "enemy";
|
||||
});
|
||||
|
||||
// 销毁实体
|
||||
entityManager.destroyEntity(player);
|
||||
```
|
||||
|
||||
## 高性能查询系统
|
||||
|
||||
EntityManager 提供多种查询方式,自动选择最优的查询策略。
|
||||
|
||||
### 基础查询
|
||||
|
||||
```typescript
|
||||
// 通过ID查询(O(1))
|
||||
const entity = entityManager.getEntity(123);
|
||||
|
||||
// 通过名称查询(O(1) 哈希查找)
|
||||
const player = entityManager.getEntityByName("Player");
|
||||
|
||||
// 通过标签查询(O(1) 索引查找)
|
||||
const enemies = entityManager.getEntitiesByTag("enemy");
|
||||
|
||||
// 组件查询(使用O(1)组件索引)
|
||||
const healthEntities = entityManager.getEntitiesWithComponent(HealthComponent);
|
||||
|
||||
// 多组件查询(使用Archetype优化)
|
||||
const movingEntities = entityManager.getEntitiesWithComponents([
|
||||
PositionComponent,
|
||||
VelocityComponent
|
||||
]);
|
||||
```
|
||||
|
||||
### 流式查询API
|
||||
|
||||
EntityManager 提供强大的流式查询构建器:
|
||||
|
||||
```typescript
|
||||
// 基础查询构建
|
||||
const results = entityManager
|
||||
.query()
|
||||
.withAll([PositionComponent, HealthComponent]) // 必须包含这些组件
|
||||
.withoutTag("dead") // 不能有死亡标签
|
||||
.active(true) // 必须激活
|
||||
.execute();
|
||||
|
||||
// 复杂条件查询
|
||||
const livingEnemies = entityManager
|
||||
.query()
|
||||
.withAll([PositionComponent, HealthComponent])
|
||||
.withTag("enemy")
|
||||
.withoutTag("dead")
|
||||
.where(entity => {
|
||||
const health = entity.getComponent(HealthComponent);
|
||||
return health && health.currentHealth > 0;
|
||||
})
|
||||
.execute();
|
||||
|
||||
// 查询变体
|
||||
const firstEnemy = entityManager
|
||||
.query()
|
||||
.withTag("enemy")
|
||||
.first(); // 获取第一个匹配
|
||||
|
||||
const enemyCount = entityManager
|
||||
.query()
|
||||
.withTag("enemy")
|
||||
.count(); // 获取数量
|
||||
|
||||
// 直接处理查询结果
|
||||
entityManager
|
||||
.query()
|
||||
.withAll([HealthComponent])
|
||||
.forEach(entity => {
|
||||
const health = entity.getComponent(HealthComponent);
|
||||
if (health.currentHealth <= 0) {
|
||||
entity.addTag("dead");
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 高级查询选项
|
||||
|
||||
```typescript
|
||||
// 组合条件查询
|
||||
const combatUnits = entityManager
|
||||
.query()
|
||||
.withAll([PositionComponent, HealthComponent]) // AND条件
|
||||
.withAny([WeaponComponent, MagicComponent]) // OR条件
|
||||
.without([DeadComponent]) // NOT条件
|
||||
.withTag("combatant")
|
||||
.withoutTag("peaceful")
|
||||
.active(true)
|
||||
.enabled(true)
|
||||
.execute();
|
||||
|
||||
// 使用自定义过滤器
|
||||
const nearbyEnemies = entityManager
|
||||
.query()
|
||||
.withAll([PositionComponent])
|
||||
.withTag("enemy")
|
||||
.where(entity => {
|
||||
const pos = entity.getComponent(PositionComponent);
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(pos.x - playerPos.x, 2) +
|
||||
Math.pow(pos.y - playerPos.y, 2)
|
||||
);
|
||||
return distance < 100; // 距离玩家100像素内
|
||||
})
|
||||
.execute();
|
||||
```
|
||||
|
||||
## 批量操作
|
||||
|
||||
EntityManager 提供高效的批量操作方法:
|
||||
|
||||
```typescript
|
||||
// 遍历所有实体
|
||||
entityManager.forEachEntity(entity => {
|
||||
// 处理每个实体
|
||||
if (entity.position.x < 0) {
|
||||
entity.position.x = 0;
|
||||
}
|
||||
});
|
||||
|
||||
// 遍历特定组件的实体
|
||||
entityManager.forEachEntityWithComponent(HealthComponent, (entity, health) => {
|
||||
if (health.currentHealth <= 0) {
|
||||
entity.addTag("dead");
|
||||
entity.enabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
// 批量创建并配置实体
|
||||
const bullets = entityManager.createEntities(100, "Bullet", (bullet, index) => {
|
||||
bullet.addComponent(new PositionComponent(
|
||||
100 + index * 10,
|
||||
100
|
||||
));
|
||||
bullet.addComponent(new VelocityComponent(0, -200));
|
||||
bullet.tag = "projectile";
|
||||
});
|
||||
```
|
||||
|
||||
## 性能优化系统
|
||||
|
||||
EntityManager 内置了三个性能优化系统:
|
||||
|
||||
### 1. 组件索引系统
|
||||
|
||||
自动为组件查询提供O(1)性能:
|
||||
|
||||
```typescript
|
||||
// 获取组件索引统计
|
||||
const componentIndex = entityManager.getComponentIndex();
|
||||
const stats = componentIndex.getPerformanceStats();
|
||||
|
||||
console.log('组件索引统计:', {
|
||||
totalQueries: stats.totalQueries,
|
||||
indexHits: stats.indexHits,
|
||||
hitRate: (stats.indexHits / stats.totalQueries * 100).toFixed(2) + '%'
|
||||
});
|
||||
|
||||
// 手动优化(通常自动进行)
|
||||
componentIndex.optimize();
|
||||
```
|
||||
|
||||
### 2. Archetype系统
|
||||
|
||||
按组件组合分组实体,优化批量查询:
|
||||
|
||||
```typescript
|
||||
// 获取Archetype统计
|
||||
const archetypeSystem = entityManager.getArchetypeSystem();
|
||||
const archetypeStats = archetypeSystem.getStatistics();
|
||||
|
||||
console.log('Archetype统计:', {
|
||||
totalArchetypes: archetypeStats.totalArchetypes,
|
||||
totalEntities: archetypeStats.totalEntities,
|
||||
queryCacheSize: archetypeStats.queryCacheSize
|
||||
});
|
||||
|
||||
// 查看所有原型
|
||||
console.log('当前原型:', archetypeSystem.getAllArchetypes());
|
||||
```
|
||||
|
||||
### 3. 脏标记系统
|
||||
|
||||
追踪实体变更,避免不必要的更新:
|
||||
|
||||
```typescript
|
||||
// 获取脏标记统计
|
||||
const dirtyTracking = entityManager.getDirtyTrackingSystem();
|
||||
const dirtyStats = dirtyTracking.getPerformanceStats();
|
||||
|
||||
console.log('脏标记统计:', {
|
||||
totalMarks: dirtyStats.totalMarks,
|
||||
batchesProcessed: dirtyStats.batchesProcessed,
|
||||
listenersNotified: dirtyStats.listenersNotified
|
||||
});
|
||||
|
||||
// 手动处理脏标记
|
||||
dirtyTracking.processDirtyMarks();
|
||||
```
|
||||
|
||||
## 实体管理器统计
|
||||
|
||||
获取EntityManager的综合性能数据:
|
||||
|
||||
```typescript
|
||||
const stats = entityManager.getStatistics();
|
||||
|
||||
console.log('EntityManager统计:', {
|
||||
// 基础统计
|
||||
entityCount: stats.entityCount,
|
||||
activeEntityCount: stats.activeEntityCount,
|
||||
|
||||
// 查询统计
|
||||
totalQueries: stats.totalQueries,
|
||||
indexHits: stats.indexHits,
|
||||
archetypeHits: stats.archetypeHits,
|
||||
|
||||
// 性能指标
|
||||
averageQueryTime: stats.averageQueryTime,
|
||||
hitRate: (stats.indexHits / stats.totalQueries * 100).toFixed(2) + '%'
|
||||
});
|
||||
```
|
||||
|
||||
## 系统优化和清理
|
||||
|
||||
```typescript
|
||||
// 手动触发优化
|
||||
entityManager.optimize();
|
||||
|
||||
// 内存清理
|
||||
entityManager.cleanup();
|
||||
|
||||
// 压缩数据结构
|
||||
entityManager.compact();
|
||||
|
||||
// 获取内存使用情况
|
||||
const memoryStats = entityManager.getMemoryUsage();
|
||||
console.log('内存使用:', {
|
||||
entityIndexSize: memoryStats.entityIndex,
|
||||
componentIndexSize: memoryStats.componentIndex,
|
||||
archetypeSize: memoryStats.archetype
|
||||
});
|
||||
```
|
||||
|
||||
## 实际使用案例
|
||||
|
||||
### 游戏系统集成
|
||||
|
||||
```typescript
|
||||
class MovementSystem extends EntitySystem {
|
||||
private entityManager: EntityManager;
|
||||
|
||||
constructor(scene: Scene) {
|
||||
super();
|
||||
this.entityManager = new EntityManager(scene);
|
||||
}
|
||||
|
||||
protected process(entities: Entity[]): void {
|
||||
// 使用高效查询获取移动实体
|
||||
const movingEntities = this.entityManager
|
||||
.query()
|
||||
.withAll([PositionComponent, VelocityComponent])
|
||||
.active(true)
|
||||
.execute();
|
||||
|
||||
// 批量处理
|
||||
movingEntities.forEach(entity => {
|
||||
const position = entity.getComponent(PositionComponent);
|
||||
const velocity = entity.getComponent(VelocityComponent);
|
||||
|
||||
position.x += velocity.dx * Time.deltaTime;
|
||||
position.y += velocity.dy * Time.deltaTime;
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 复杂查询示例
|
||||
|
||||
```typescript
|
||||
// 战斗系统:查找攻击范围内的敌人
|
||||
class CombatSystem {
|
||||
private entityManager: EntityManager;
|
||||
|
||||
findTargetsInRange(attacker: Entity, range: number): Entity[] {
|
||||
const attackerPos = attacker.getComponent(PositionComponent);
|
||||
if (!attackerPos) return [];
|
||||
|
||||
return this.entityManager
|
||||
.query()
|
||||
.withAll([PositionComponent, HealthComponent])
|
||||
.withTag("enemy")
|
||||
.withoutTag("dead")
|
||||
.where(entity => {
|
||||
const pos = entity.getComponent(PositionComponent);
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(pos.x - attackerPos.x, 2) +
|
||||
Math.pow(pos.y - attackerPos.y, 2)
|
||||
);
|
||||
return distance <= range;
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
// 优化版本:使用空间分区(如果实现了的话)
|
||||
findTargetsInRangeOptimized(attacker: Entity, range: number): Entity[] {
|
||||
// 首先通过空间查询缩小范围
|
||||
const nearbyEntities = this.spatialIndex.queryRange(
|
||||
attackerPos.x - range,
|
||||
attackerPos.y - range,
|
||||
attackerPos.x + range,
|
||||
attackerPos.y + range
|
||||
);
|
||||
|
||||
// 然后使用EntityManager进行精确过滤
|
||||
return this.entityManager
|
||||
.query()
|
||||
.withAll([HealthComponent])
|
||||
.withTag("enemy")
|
||||
.withoutTag("dead")
|
||||
.where(entity => nearbyEntities.includes(entity))
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 性能建议
|
||||
|
||||
### 查询优化
|
||||
|
||||
1. **利用索引**: 优先使用组件查询和标签查询,它们具有O(1)性能
|
||||
2. **减少自定义过滤**: `where()`条件虽然灵活,但会降低性能
|
||||
3. **缓存查询结果**: 对于不经常变化的查询结果,考虑缓存
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:使用索引查询
|
||||
const enemies = entityManager.getEntitiesByTag("enemy");
|
||||
|
||||
// ⚠️ 谨慎:自定义过滤
|
||||
const enemies = entityManager
|
||||
.query()
|
||||
.where(entity => entity.name.includes("Enemy"))
|
||||
.execute();
|
||||
```
|
||||
|
||||
### 批量操作优化
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:批量创建
|
||||
const bullets = entityManager.createEntities(100, "Bullet");
|
||||
|
||||
// ❌ 避免:循环单独创建
|
||||
for (let i = 0; i < 100; i++) {
|
||||
entityManager.createEntity("Bullet");
|
||||
}
|
||||
```
|
||||
|
||||
### 内存管理
|
||||
|
||||
```typescript
|
||||
// 定期清理
|
||||
setInterval(() => {
|
||||
entityManager.cleanup();
|
||||
}, 30000); // 每30秒清理一次
|
||||
|
||||
// 监控性能
|
||||
const stats = entityManager.getStatistics();
|
||||
if (stats.indexHits / stats.totalQueries < 0.8) {
|
||||
console.warn('查询命中率较低,考虑优化查询策略');
|
||||
}
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
EntityManager 提供了:
|
||||
|
||||
- **统一接口**: 所有实体操作通过一个管理器完成
|
||||
- **自动优化**: 内置三个性能优化系统
|
||||
- **灵活查询**: 从简单的ID查找到复杂的条件查询
|
||||
- **性能监控**: 完整的统计和诊断信息
|
||||
- **批量操作**: 高效的批量处理能力
|
||||
|
||||
通过合理使用EntityManager,您可以构建高性能、可维护的ECS游戏系统。
|
||||
@@ -1,496 +0,0 @@
|
||||
# ECS事件系统使用指南
|
||||
|
||||
本文档介绍如何使用ECS框架的增强事件系统,包括类型安全的事件发布订阅、预定义的ECS事件类型和高级功能。
|
||||
|
||||
## 目录
|
||||
|
||||
1. [基础用法](#基础用法)
|
||||
2. [预定义ECS事件](#预定义ecs事件)
|
||||
3. [事件装饰器](#事件装饰器)
|
||||
4. [高级功能](#高级功能)
|
||||
5. [性能优化](#性能优化)
|
||||
6. [最佳实践](#最佳实践)
|
||||
|
||||
## 基础用法
|
||||
|
||||
### 创建事件总线
|
||||
|
||||
```typescript
|
||||
import { EventBus, GlobalEventBus } from './ECS';
|
||||
|
||||
// 方式1:创建独立的事件总线
|
||||
const eventBus = new EventBus(true); // true启用调试模式
|
||||
|
||||
// 方式2:使用全局事件总线
|
||||
const globalEventBus = GlobalEventBus.getInstance(true);
|
||||
```
|
||||
|
||||
### 基本事件发布订阅
|
||||
|
||||
```typescript
|
||||
// 定义事件数据类型
|
||||
interface PlayerDiedEvent {
|
||||
playerId: number;
|
||||
cause: string;
|
||||
position: { x: number; y: number };
|
||||
}
|
||||
|
||||
// 监听事件
|
||||
const listenerId = eventBus.on<PlayerDiedEvent>('player:died', (data) => {
|
||||
console.log(`Player ${data.playerId} died at (${data.position.x}, ${data.position.y})`);
|
||||
console.log(`Cause: ${data.cause}`);
|
||||
});
|
||||
|
||||
// 发射事件
|
||||
eventBus.emit('player:died', {
|
||||
playerId: 123,
|
||||
cause: 'enemy_attack',
|
||||
position: { x: 100, y: 200 }
|
||||
});
|
||||
|
||||
// 移除监听器
|
||||
eventBus.off('player:died', listenerId);
|
||||
```
|
||||
|
||||
### 一次性事件监听
|
||||
|
||||
```typescript
|
||||
// 只监听一次
|
||||
eventBus.once<PlayerDiedEvent>('player:died', (data) => {
|
||||
console.log('This will only be called once');
|
||||
});
|
||||
```
|
||||
|
||||
### 异步事件处理
|
||||
|
||||
```typescript
|
||||
// 异步事件监听
|
||||
eventBus.onAsync<PlayerDiedEvent>('player:died', async (data) => {
|
||||
await savePlayerDeathToDatabase(data);
|
||||
await updateLeaderboard(data.playerId);
|
||||
});
|
||||
|
||||
// 异步事件发射
|
||||
await eventBus.emitAsync('player:died', playerData);
|
||||
```
|
||||
|
||||
## 预定义ECS事件
|
||||
|
||||
框架提供了完整的ECS事件类型定义,支持实体、组件、系统等核心概念的事件。
|
||||
|
||||
### 实体事件
|
||||
|
||||
```typescript
|
||||
import { ECSEventType, IEntityEventData } from './ECS';
|
||||
|
||||
// 监听实体创建事件
|
||||
eventBus.onEntityCreated((data: IEntityEventData) => {
|
||||
console.log(`Entity created: ${data.entityName} (ID: ${data.entityId})`);
|
||||
});
|
||||
|
||||
// 监听实体销毁事件
|
||||
eventBus.on<IEntityEventData>(ECSEventType.ENTITY_DESTROYED, (data) => {
|
||||
console.log(`Entity destroyed: ${data.entityName}`);
|
||||
});
|
||||
|
||||
// 手动发射实体事件
|
||||
eventBus.emitEntityCreated({
|
||||
timestamp: Date.now(),
|
||||
source: 'GameManager',
|
||||
entityId: 123,
|
||||
entityName: 'Player',
|
||||
entityTag: 'player'
|
||||
});
|
||||
```
|
||||
|
||||
### 组件事件
|
||||
|
||||
```typescript
|
||||
import { IComponentEventData } from './ECS';
|
||||
|
||||
// 监听组件添加事件
|
||||
eventBus.onComponentAdded((data: IComponentEventData) => {
|
||||
console.log(`Component ${data.componentType} added to entity ${data.entityId}`);
|
||||
});
|
||||
|
||||
// 监听组件移除事件
|
||||
eventBus.on<IComponentEventData>(ECSEventType.COMPONENT_REMOVED, (data) => {
|
||||
console.log(`Component ${data.componentType} removed from entity ${data.entityId}`);
|
||||
});
|
||||
```
|
||||
|
||||
### 系统事件
|
||||
|
||||
```typescript
|
||||
import { ISystemEventData } from './ECS';
|
||||
|
||||
// 监听系统错误
|
||||
eventBus.onSystemError((data: ISystemEventData) => {
|
||||
console.error(`System error in ${data.systemName}: ${data.systemType}`);
|
||||
});
|
||||
|
||||
// 监听系统处理开始/结束
|
||||
eventBus.on<ISystemEventData>(ECSEventType.SYSTEM_PROCESSING_START, (data) => {
|
||||
console.log(`System ${data.systemName} started processing`);
|
||||
});
|
||||
```
|
||||
|
||||
### 性能事件
|
||||
|
||||
```typescript
|
||||
import { IPerformanceEventData } from './ECS';
|
||||
|
||||
// 监听性能警告
|
||||
eventBus.onPerformanceWarning((data: IPerformanceEventData) => {
|
||||
console.warn(`Performance warning: ${data.operation} took ${data.executionTime}ms`);
|
||||
});
|
||||
|
||||
// 监听内存使用过高
|
||||
eventBus.on<IPerformanceEventData>(ECSEventType.MEMORY_USAGE_HIGH, (data) => {
|
||||
console.warn(`High memory usage: ${data.memoryUsage}MB`);
|
||||
});
|
||||
```
|
||||
|
||||
## 事件装饰器
|
||||
|
||||
使用装饰器可以自动注册事件监听器,简化代码编写。
|
||||
|
||||
### 基础装饰器
|
||||
|
||||
```typescript
|
||||
import { EventHandler, AsyncEventHandler, EventPriority } from './ECS';
|
||||
|
||||
class GameManager {
|
||||
@EventHandler(ECSEventType.ENTITY_CREATED, { priority: EventPriority.HIGH })
|
||||
onEntityCreated(data: IEntityEventData) {
|
||||
console.log(`New entity: ${data.entityName}`);
|
||||
}
|
||||
|
||||
@AsyncEventHandler(ECSEventType.ENTITY_DESTROYED)
|
||||
async onEntityDestroyed(data: IEntityEventData) {
|
||||
await this.cleanupEntityResources(data.entityId);
|
||||
}
|
||||
|
||||
@EventHandler('custom:game:event', { once: true })
|
||||
onGameStart(data: any) {
|
||||
console.log('Game started!');
|
||||
}
|
||||
|
||||
// 需要手动调用初始化方法
|
||||
constructor() {
|
||||
// 如果类有initEventListeners方法,会自动注册装饰器定义的监听器
|
||||
if (this.initEventListeners) {
|
||||
this.initEventListeners();
|
||||
}
|
||||
}
|
||||
|
||||
private async cleanupEntityResources(entityId: number) {
|
||||
// 清理实体相关资源
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 优先级和配置
|
||||
|
||||
```typescript
|
||||
class SystemManager {
|
||||
@EventHandler(ECSEventType.SYSTEM_ERROR, {
|
||||
priority: EventPriority.CRITICAL,
|
||||
context: this
|
||||
})
|
||||
handleSystemError(data: ISystemEventData) {
|
||||
this.logError(data);
|
||||
this.restartSystem(data.systemName);
|
||||
}
|
||||
|
||||
@AsyncEventHandler(ECSEventType.PERFORMANCE_WARNING, {
|
||||
priority: EventPriority.LOW,
|
||||
async: true
|
||||
})
|
||||
async handlePerformanceWarning(data: IPerformanceEventData) {
|
||||
await this.optimizePerformance(data);
|
||||
}
|
||||
|
||||
private logError(data: ISystemEventData) {
|
||||
// 错误日志记录
|
||||
}
|
||||
|
||||
private restartSystem(systemName: string) {
|
||||
// 重启系统
|
||||
}
|
||||
|
||||
private async optimizePerformance(data: IPerformanceEventData) {
|
||||
// 性能优化逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 高级功能
|
||||
|
||||
### 事件批处理
|
||||
|
||||
```typescript
|
||||
// 设置批处理配置
|
||||
eventBus.setBatchConfig('entity:update', 100, 16); // 批量100个,延迟16ms
|
||||
|
||||
// 发射事件(会被批处理)
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
eventBus.emit('entity:update', { entityId: i, data: 'update' });
|
||||
}
|
||||
|
||||
// 手动刷新批处理队列
|
||||
eventBus.flushBatch('entity:update');
|
||||
```
|
||||
|
||||
### 事件统计和监控
|
||||
|
||||
```typescript
|
||||
// 获取单个事件统计
|
||||
const stats = eventBus.getStats('entity:created');
|
||||
console.log(`Event triggered ${stats.triggerCount} times`);
|
||||
console.log(`Average execution time: ${stats.averageExecutionTime}ms`);
|
||||
|
||||
// 获取所有事件统计
|
||||
const allStats = eventBus.getStats();
|
||||
if (allStats instanceof Map) {
|
||||
allStats.forEach((stat, eventType) => {
|
||||
console.log(`${eventType}: ${stat.triggerCount} triggers`);
|
||||
});
|
||||
}
|
||||
|
||||
// 重置统计
|
||||
eventBus.resetStats('entity:created');
|
||||
```
|
||||
|
||||
### 事件类型验证
|
||||
|
||||
```typescript
|
||||
import { EventTypeValidator } from './ECS';
|
||||
|
||||
// 检查事件类型是否有效
|
||||
if (EventTypeValidator.isValid('entity:created')) {
|
||||
eventBus.emit('entity:created', data);
|
||||
}
|
||||
|
||||
// 添加自定义事件类型
|
||||
EventTypeValidator.addCustomType('game:custom:event');
|
||||
|
||||
// 获取所有有效事件类型
|
||||
const validTypes = EventTypeValidator.getAllValidTypes();
|
||||
console.log('Valid event types:', validTypes);
|
||||
```
|
||||
|
||||
### 调试和日志
|
||||
|
||||
```typescript
|
||||
// 启用调试模式
|
||||
eventBus.setDebugMode(true);
|
||||
|
||||
// 设置最大监听器数量
|
||||
eventBus.setMaxListeners(50);
|
||||
|
||||
// 检查是否有监听器
|
||||
if (eventBus.hasListeners('entity:created')) {
|
||||
console.log('Has listeners for entity:created');
|
||||
}
|
||||
|
||||
// 获取监听器数量
|
||||
const count = eventBus.getListenerCount('entity:created');
|
||||
console.log(`${count} listeners for entity:created`);
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 事件过滤和条件监听
|
||||
|
||||
```typescript
|
||||
// 使用条件过滤减少不必要的事件处理
|
||||
eventBus.on<IEntityEventData>(ECSEventType.ENTITY_CREATED, (data) => {
|
||||
// 只处理玩家实体
|
||||
if (data.entityTag === 'player') {
|
||||
handlePlayerCreated(data);
|
||||
}
|
||||
});
|
||||
|
||||
// 更好的方式:使用具体的事件类型
|
||||
eventBus.on<IEntityEventData>('entity:player:created', handlePlayerCreated);
|
||||
```
|
||||
|
||||
### 内存管理
|
||||
|
||||
```typescript
|
||||
class EventManager {
|
||||
private listeners: string[] = [];
|
||||
|
||||
public setupListeners() {
|
||||
// 保存监听器ID以便清理
|
||||
this.listeners.push(
|
||||
eventBus.on('event1', this.handler1.bind(this)),
|
||||
eventBus.on('event2', this.handler2.bind(this))
|
||||
);
|
||||
}
|
||||
|
||||
public cleanup() {
|
||||
// 清理所有监听器
|
||||
this.listeners.forEach(id => {
|
||||
eventBus.off('event1', id);
|
||||
eventBus.off('event2', id);
|
||||
});
|
||||
this.listeners.length = 0;
|
||||
}
|
||||
|
||||
private handler1(data: any) { /* ... */ }
|
||||
private handler2(data: any) { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
### 异步事件优化
|
||||
|
||||
```typescript
|
||||
// 使用Promise.all并行处理多个异步事件
|
||||
const promises = [
|
||||
eventBus.emitAsync('save:player', playerData),
|
||||
eventBus.emitAsync('update:leaderboard', scoreData),
|
||||
eventBus.emitAsync('notify:friends', notificationData)
|
||||
];
|
||||
|
||||
await Promise.all(promises);
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 事件命名规范
|
||||
|
||||
```typescript
|
||||
// 推荐的事件命名格式:模块:对象:动作
|
||||
const EVENT_NAMES = {
|
||||
// 实体相关
|
||||
ENTITY_PLAYER_CREATED: 'entity:player:created',
|
||||
ENTITY_ENEMY_DESTROYED: 'entity:enemy:destroyed',
|
||||
|
||||
// 游戏逻辑相关
|
||||
GAME_LEVEL_COMPLETED: 'game:level:completed',
|
||||
GAME_SCORE_UPDATED: 'game:score:updated',
|
||||
|
||||
// UI相关
|
||||
UI_MENU_OPENED: 'ui:menu:opened',
|
||||
UI_BUTTON_CLICKED: 'ui:button:clicked'
|
||||
};
|
||||
```
|
||||
|
||||
### 2. 类型安全
|
||||
|
||||
```typescript
|
||||
// 定义强类型的事件数据接口
|
||||
interface GameEvents {
|
||||
'player:levelup': { playerId: number; newLevel: number; experience: number };
|
||||
'inventory:item:added': { itemId: string; quantity: number; playerId: number };
|
||||
'combat:damage:dealt': { attackerId: number; targetId: number; damage: number };
|
||||
}
|
||||
|
||||
// 创建类型安全的事件发射器
|
||||
class TypedEventBus {
|
||||
private eventBus = new EventBus();
|
||||
|
||||
emit<K extends keyof GameEvents>(eventType: K, data: GameEvents[K]) {
|
||||
this.eventBus.emit(eventType, data);
|
||||
}
|
||||
|
||||
on<K extends keyof GameEvents>(
|
||||
eventType: K,
|
||||
handler: (data: GameEvents[K]) => void
|
||||
) {
|
||||
return this.eventBus.on(eventType, handler);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 错误处理
|
||||
|
||||
```typescript
|
||||
// 在事件处理器中添加错误处理
|
||||
eventBus.on<IEntityEventData>(ECSEventType.ENTITY_CREATED, (data) => {
|
||||
try {
|
||||
processEntityCreation(data);
|
||||
} catch (error) {
|
||||
console.error('Error processing entity creation:', error);
|
||||
// 发射错误事件
|
||||
eventBus.emit(ECSEventType.ERROR_OCCURRED, {
|
||||
timestamp: Date.now(),
|
||||
source: 'EntityCreationHandler',
|
||||
error: error.message,
|
||||
context: data
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 4. 模块化事件管理
|
||||
|
||||
```typescript
|
||||
// 为不同模块创建专门的事件管理器
|
||||
class PlayerEventManager {
|
||||
constructor(private eventBus: EventBus) {
|
||||
this.setupListeners();
|
||||
}
|
||||
|
||||
private setupListeners() {
|
||||
this.eventBus.onEntityCreated(this.onPlayerCreated.bind(this));
|
||||
this.eventBus.on('player:levelup', this.onPlayerLevelUp.bind(this));
|
||||
this.eventBus.on('player:died', this.onPlayerDied.bind(this));
|
||||
}
|
||||
|
||||
private onPlayerCreated(data: IEntityEventData) {
|
||||
if (data.entityTag === 'player') {
|
||||
// 处理玩家创建逻辑
|
||||
}
|
||||
}
|
||||
|
||||
private onPlayerLevelUp(data: any) {
|
||||
// 处理玩家升级逻辑
|
||||
}
|
||||
|
||||
private onPlayerDied(data: any) {
|
||||
// 处理玩家死亡逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 与EntityManager集成
|
||||
|
||||
```typescript
|
||||
import { EntityManager } from './ECS';
|
||||
|
||||
// EntityManager会自动设置事件总线
|
||||
const entityManager = new EntityManager();
|
||||
|
||||
// 获取事件总线实例
|
||||
const eventBus = entityManager.eventBus;
|
||||
|
||||
// 监听自动发射的ECS事件
|
||||
eventBus.onEntityCreated((data) => {
|
||||
console.log('Entity created automatically:', data);
|
||||
});
|
||||
|
||||
eventBus.onComponentAdded((data) => {
|
||||
console.log('Component added automatically:', data);
|
||||
});
|
||||
|
||||
// 创建实体时会自动发射事件
|
||||
const entity = entityManager.createEntity('Player');
|
||||
|
||||
// 添加组件时会自动发射事件
|
||||
entity.addComponent(new HealthComponent(100));
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
ECS框架的事件系统提供了:
|
||||
|
||||
- **类型安全**:完整的TypeScript类型支持
|
||||
- **高性能**:批处理、缓存和优化机制
|
||||
- **易用性**:装饰器、预定义事件类型
|
||||
- **可扩展**:自定义事件类型和验证
|
||||
- **调试友好**:详细的统计信息和调试模式
|
||||
|
||||
通过合理使用事件系统,可以实现松耦合的模块化架构,提高代码的可维护性和扩展性。
|
||||
23
docs/examples/index.md
Normal file
23
docs/examples/index.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# 示例
|
||||
|
||||
这里展示了ECS Framework的各种使用示例,通过实际的演示帮助您理解框架的功能和最佳实践。
|
||||
|
||||
## 🎮 互动演示
|
||||
|
||||
### [Worker系统演示](./worker-system-demo)
|
||||
- **功能**: 展示Worker多线程物理计算和渲染优化
|
||||
- **特性**: 1000+粒子实时物理模拟、碰撞检测、性能对比
|
||||
- **技术点**: SharedArrayBuffer、Canvas 2D优化、实体生命周期管理
|
||||
|
||||
## 🔗 外部示例
|
||||
|
||||
### [割草机演示](https://github.com/esengine/lawn-mower-demo)
|
||||
- **平台**: Cocos Creator 3.x
|
||||
- **功能**: 完整的游戏演示项目
|
||||
- **特性**: 展示ECS架构在实际游戏项目中的应用
|
||||
|
||||
## 📚 更多资源
|
||||
|
||||
- [快速开始指南](/guide/getting-started)
|
||||
- [核心概念](/guide/)
|
||||
- [API文档](/api/README)
|
||||
13
docs/examples/worker-system-demo.md
Normal file
13
docs/examples/worker-system-demo.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Worker系统演示
|
||||
|
||||
这是一个展示ECS框架Worker系统功能的交互式演示。
|
||||
|
||||
## 在线演示
|
||||
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="/ecs-framework/demos/worker-system/index.html" target="_blank" style="display: inline-block; padding: 15px 30px; background: #4a9eff; color: white; text-decoration: none; border-radius: 8px; font-weight: bold; font-size: 16px; box-shadow: 0 4px 8px rgba(74, 158, 255, 0.3); transition: all 0.3s ease;">
|
||||
🚀 打开Worker系统演示
|
||||
</a>
|
||||
</div>
|
||||
|
||||
> **注意**: 演示将在新窗口中打开,展示完整的Worker系统功能,包括实体管理、物理模拟和性能监控。
|
||||
@@ -1,549 +0,0 @@
|
||||
# 快速入门
|
||||
|
||||
本指南将帮助您快速上手 ECS Framework,这是一个专业级的实体组件系统框架,采用现代化架构设计,专为高性能游戏开发打造。
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
ecs-framework/
|
||||
├── source/
|
||||
│ ├── src/ # 源代码
|
||||
│ │ ├── ECS/ # ECS核心系统
|
||||
│ │ ├── Types/ # 类型定义
|
||||
│ │ ├── Utils/ # 工具类
|
||||
│ │ └── Testing/ # 测试文件
|
||||
│ ├── scripts/ # 构建脚本
|
||||
│ └── tsconfig.json # TypeScript配置
|
||||
└── docs/ # 文档
|
||||
```
|
||||
|
||||
## 安装和使用
|
||||
|
||||
### NPM 安装
|
||||
|
||||
```bash
|
||||
npm install @esengine/ecs-framework
|
||||
```
|
||||
|
||||
### 从源码构建
|
||||
|
||||
```bash
|
||||
# 克隆项目
|
||||
git clone https://github.com/esengine/ecs-framework.git
|
||||
|
||||
# 进入源码目录
|
||||
cd ecs-framework/source
|
||||
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# 编译TypeScript
|
||||
npm run build
|
||||
```
|
||||
|
||||
## 基础设置
|
||||
|
||||
### 1. 导入框架
|
||||
|
||||
```typescript
|
||||
// 导入核心类
|
||||
import {
|
||||
Core,
|
||||
Entity,
|
||||
Component,
|
||||
Scene,
|
||||
EntitySystem,
|
||||
EntityManager,
|
||||
ComponentIndexManager,
|
||||
ArchetypeSystem,
|
||||
DirtyTrackingSystem
|
||||
} from '@esengine/ecs-framework';
|
||||
```
|
||||
|
||||
### 2. 创建基础管理器
|
||||
|
||||
```typescript
|
||||
class GameManager {
|
||||
private core: Core;
|
||||
private scene: Scene;
|
||||
private entityManager: EntityManager;
|
||||
|
||||
constructor() {
|
||||
// 创建核心实例
|
||||
this.core = Core.create(true);
|
||||
|
||||
// 创建场景
|
||||
this.scene = new Scene();
|
||||
this.scene.name = "GameScene";
|
||||
|
||||
// 设置当前场景
|
||||
Core.scene = this.scene;
|
||||
|
||||
// 初始化实体管理器
|
||||
this.entityManager = new EntityManager(this.scene);
|
||||
|
||||
// 初始化性能优化
|
||||
this.setupPerformanceOptimizations();
|
||||
}
|
||||
|
||||
private setupPerformanceOptimizations() {
|
||||
// 启用组件索引(自动优化查询性能)
|
||||
// EntityManager内部已自动启用
|
||||
|
||||
// 可选:手动配置优化系统
|
||||
const componentIndex = this.entityManager.getComponentIndex();
|
||||
const archetypeSystem = this.entityManager.getArchetypeSystem();
|
||||
const dirtyTracking = this.entityManager.getDirtyTrackingSystem();
|
||||
|
||||
// 优化系统会自动工作,通常无需手动配置
|
||||
}
|
||||
|
||||
public update(deltaTime: number): void {
|
||||
// 更新场景
|
||||
this.scene.update();
|
||||
|
||||
// 处理系统逻辑
|
||||
this.updateSystems(deltaTime);
|
||||
}
|
||||
|
||||
private updateSystems(deltaTime: number): void {
|
||||
// 在这里添加您的系统更新逻辑
|
||||
}
|
||||
|
||||
// 提供实体管理器访问
|
||||
public getEntityManager(): EntityManager {
|
||||
return this.entityManager;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 游戏循环
|
||||
|
||||
```typescript
|
||||
const gameManager = new GameManager();
|
||||
let lastTime = performance.now();
|
||||
|
||||
function gameLoop() {
|
||||
const currentTime = performance.now();
|
||||
const deltaTime = (currentTime - lastTime) / 1000; // 转换为秒
|
||||
lastTime = currentTime;
|
||||
|
||||
gameManager.update(deltaTime);
|
||||
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
// 启动游戏循环
|
||||
gameLoop();
|
||||
```
|
||||
|
||||
## 创建实体和组件
|
||||
|
||||
### 1. 定义组件
|
||||
|
||||
```typescript
|
||||
import { Component, ComponentPoolManager } from '@esengine/ecs-framework';
|
||||
|
||||
// 位置组件
|
||||
class PositionComponent extends Component {
|
||||
public x: number = 0;
|
||||
public y: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
// 对象池重置方法
|
||||
public reset() {
|
||||
this.x = 0;
|
||||
this.y = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 速度组件
|
||||
class VelocityComponent extends Component {
|
||||
public x: number = 0;
|
||||
public y: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this.x = 0;
|
||||
this.y = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 生命值组件
|
||||
class HealthComponent extends Component {
|
||||
public maxHealth: number = 100;
|
||||
public currentHealth: number = 100;
|
||||
|
||||
constructor(maxHealth: number = 100) {
|
||||
super();
|
||||
this.maxHealth = maxHealth;
|
||||
this.currentHealth = maxHealth;
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this.maxHealth = 100;
|
||||
this.currentHealth = 100;
|
||||
}
|
||||
|
||||
public takeDamage(damage: number): void {
|
||||
this.currentHealth = Math.max(0, this.currentHealth - damage);
|
||||
}
|
||||
|
||||
public heal(amount: number): void {
|
||||
this.currentHealth = Math.min(this.maxHealth, this.currentHealth + amount);
|
||||
}
|
||||
|
||||
public isDead(): boolean {
|
||||
return this.currentHealth <= 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 简单的组件定义
|
||||
// 注:框架会自动优化组件的存储和查询
|
||||
```
|
||||
|
||||
## 使用 EntityManager
|
||||
|
||||
EntityManager 是框架的核心功能,提供统一的实体管理和高性能查询接口。
|
||||
|
||||
### 1. 基础用法
|
||||
|
||||
```typescript
|
||||
// 获取EntityManager实例(在GameManager中已创建)
|
||||
const entityManager = gameManager.getEntityManager();
|
||||
|
||||
// 创建单个实体
|
||||
const player = entityManager.createEntity("Player");
|
||||
player.addComponent(new PositionComponent(100, 100));
|
||||
player.addComponent(new VelocityComponent(50, 0));
|
||||
|
||||
// 批量创建实体
|
||||
const enemies = entityManager.createEntities(50, "Enemy");
|
||||
enemies.forEach((enemy, index) => {
|
||||
enemy.addComponent(new PositionComponent(
|
||||
Math.random() * 800,
|
||||
Math.random() * 600
|
||||
));
|
||||
enemy.addComponent(new HealthComponent(30));
|
||||
enemy.tag = "enemy";
|
||||
});
|
||||
```
|
||||
|
||||
### 2. 高性能查询
|
||||
|
||||
```typescript
|
||||
// 流式查询API - 支持复杂查询条件
|
||||
const movingEntities = entityManager
|
||||
.query()
|
||||
.withAll([PositionComponent, VelocityComponent])
|
||||
.withoutTag("dead")
|
||||
.active(true)
|
||||
.execute();
|
||||
|
||||
// 快速组件查询(使用O(1)索引)
|
||||
const healthEntities = entityManager.getEntitiesWithComponent(HealthComponent);
|
||||
|
||||
// 标签查询
|
||||
const allEnemies = entityManager.getEntitiesByTag("enemy");
|
||||
|
||||
// 名称查询
|
||||
const specificEnemy = entityManager.getEntityByName("BossEnemy");
|
||||
|
||||
// 复合查询
|
||||
const livingEnemies = entityManager
|
||||
.query()
|
||||
.withAll([PositionComponent, HealthComponent])
|
||||
.withTag("enemy")
|
||||
.withoutTag("dead")
|
||||
.where(entity => {
|
||||
const health = entity.getComponent(HealthComponent);
|
||||
return health && health.currentHealth > 0;
|
||||
})
|
||||
.execute();
|
||||
```
|
||||
|
||||
### 3. 批量操作
|
||||
|
||||
```typescript
|
||||
// 批量处理实体
|
||||
entityManager.forEachEntity(entity => {
|
||||
// 处理所有实体
|
||||
if (entity.tag === "bullet" && entity.position.y < 0) {
|
||||
entity.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
// 批量处理特定组件的实体
|
||||
entityManager.forEachEntityWithComponent(HealthComponent, (entity, health) => {
|
||||
if (health.currentHealth <= 0) {
|
||||
entity.addTag("dead");
|
||||
entity.enabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
// 获取统计信息
|
||||
const stats = entityManager.getStatistics();
|
||||
console.log(`总实体数: ${stats.entityCount}`);
|
||||
console.log(`索引命中率: ${stats.indexHits}/${stats.totalQueries}`);
|
||||
```
|
||||
|
||||
### 4. 性能优化功能
|
||||
|
||||
```typescript
|
||||
// 获取性能优化系统
|
||||
const componentIndex = entityManager.getComponentIndex();
|
||||
const archetypeSystem = entityManager.getArchetypeSystem();
|
||||
const dirtyTracking = entityManager.getDirtyTrackingSystem();
|
||||
|
||||
// 查看性能统计
|
||||
console.log('组件索引统计:', componentIndex.getPerformanceStats());
|
||||
console.log('Archetype统计:', archetypeSystem.getStatistics());
|
||||
console.log('脏标记统计:', dirtyTracking.getPerformanceStats());
|
||||
|
||||
// 手动优化(通常自动进行)
|
||||
entityManager.optimize();
|
||||
|
||||
// 内存清理
|
||||
entityManager.cleanup();
|
||||
```
|
||||
|
||||
## 创建系统
|
||||
|
||||
系统处理具有特定组件的实体集合,实现游戏逻辑。
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, Entity } from '@esengine/ecs-framework';
|
||||
|
||||
class MovementSystem extends EntitySystem {
|
||||
protected process(entities: Entity[]): void {
|
||||
// 使用EntityManager进行高效查询
|
||||
const entityManager = new EntityManager(this.scene);
|
||||
const movingEntities = entityManager
|
||||
.query()
|
||||
.withAll([PositionComponent, VelocityComponent])
|
||||
.execute();
|
||||
|
||||
movingEntities.forEach(entity => {
|
||||
const position = entity.getComponent(PositionComponent);
|
||||
const velocity = entity.getComponent(VelocityComponent);
|
||||
|
||||
if (position && velocity) {
|
||||
// 更新位置
|
||||
position.x += velocity.x * 0.016; // 假设60FPS
|
||||
position.y += velocity.y * 0.016;
|
||||
|
||||
// 边界检查
|
||||
if (position.x < 0 || position.x > 800) {
|
||||
velocity.x = -velocity.x;
|
||||
}
|
||||
if (position.y < 0 || position.y > 600) {
|
||||
velocity.y = -velocity.y;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class HealthSystem extends EntitySystem {
|
||||
protected process(entities: Entity[]): void {
|
||||
const entityManager = new EntityManager(this.scene);
|
||||
|
||||
// 查找所有有生命值的实体
|
||||
entityManager.forEachEntityWithComponent(HealthComponent, (entity, health) => {
|
||||
if (health.isDead()) {
|
||||
entity.destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 添加系统到场景
|
||||
gameManager.scene.addEntityProcessor(new MovementSystem());
|
||||
gameManager.scene.addEntityProcessor(new HealthSystem());
|
||||
```
|
||||
|
||||
## 高级功能
|
||||
|
||||
### 事件系统
|
||||
|
||||
```typescript
|
||||
import { Core, CoreEvents } from '@esengine/ecs-framework';
|
||||
|
||||
// 监听框架事件
|
||||
Core.emitter.addObserver(CoreEvents.frameUpdated, this.onFrameUpdate, this);
|
||||
|
||||
// 发射自定义事件
|
||||
Core.emitter.emit("playerDied", { player: entity, score: 1000 });
|
||||
|
||||
// 移除监听
|
||||
Core.emitter.removeObserver(CoreEvents.frameUpdated, this.onFrameUpdate);
|
||||
```
|
||||
|
||||
### 性能监控
|
||||
|
||||
```typescript
|
||||
// 获取EntityManager性能统计
|
||||
const stats = entityManager.getStatistics();
|
||||
console.log(`总实体数: ${stats.entityCount}`);
|
||||
console.log(`索引命中率: ${stats.indexHits}/${stats.totalQueries}`);
|
||||
|
||||
// 获取各优化系统的统计
|
||||
console.log('组件索引:', entityManager.getComponentIndex().getPerformanceStats());
|
||||
console.log('Archetype:', entityManager.getArchetypeSystem().getStatistics());
|
||||
console.log('脏标记:', entityManager.getDirtyTrackingSystem().getPerformanceStats());
|
||||
```
|
||||
|
||||
## 简单示例
|
||||
|
||||
以下是一个完整的示例,展示了框架的主要功能:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
Core,
|
||||
Entity,
|
||||
Component,
|
||||
Scene,
|
||||
EntitySystem,
|
||||
EntityManager
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
// 游戏管理器
|
||||
class SimpleGame {
|
||||
private core: Core;
|
||||
private scene: Scene;
|
||||
private entityManager: EntityManager;
|
||||
|
||||
constructor() {
|
||||
this.core = Core.create(true);
|
||||
this.scene = new Scene();
|
||||
this.scene.name = "GameScene";
|
||||
Core.scene = this.scene;
|
||||
|
||||
this.entityManager = new EntityManager(this.scene);
|
||||
this.setupSystems();
|
||||
}
|
||||
|
||||
private setupSystems(): void {
|
||||
this.scene.addEntityProcessor(new MovementSystem());
|
||||
this.scene.addEntityProcessor(new HealthSystem());
|
||||
}
|
||||
|
||||
public start(): void {
|
||||
// 创建游戏实体
|
||||
this.createPlayer();
|
||||
this.createEnemies(50);
|
||||
|
||||
// 启动游戏循环
|
||||
this.gameLoop();
|
||||
}
|
||||
|
||||
private createPlayer(): Entity {
|
||||
const player = this.entityManager.createEntity("Player");
|
||||
player.addComponent(new PositionComponent(400, 300));
|
||||
player.addComponent(new VelocityComponent(0, 0));
|
||||
player.addComponent(new HealthComponent(100));
|
||||
player.tag = "player";
|
||||
return player;
|
||||
}
|
||||
|
||||
private createEnemies(count: number): Entity[] {
|
||||
const enemies = this.entityManager.createEntities(count, "Enemy");
|
||||
|
||||
enemies.forEach((enemy, index) => {
|
||||
enemy.addComponent(new PositionComponent(
|
||||
Math.random() * 800,
|
||||
Math.random() * 600
|
||||
));
|
||||
enemy.addComponent(new VelocityComponent(
|
||||
(Math.random() - 0.5) * 100,
|
||||
(Math.random() - 0.5) * 100
|
||||
));
|
||||
enemy.addComponent(new HealthComponent(50));
|
||||
enemy.tag = "enemy";
|
||||
});
|
||||
|
||||
return enemies;
|
||||
}
|
||||
|
||||
private gameLoop(): void {
|
||||
const update = () => {
|
||||
// 更新场景
|
||||
this.scene.update();
|
||||
requestAnimationFrame(update);
|
||||
};
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
// 启动游戏
|
||||
const game = new SimpleGame();
|
||||
game.start();
|
||||
```
|
||||
|
||||
## 性能优化建议
|
||||
|
||||
### 1. 大规模实体处理
|
||||
- 使用 `EntityManager.createEntities()` 批量创建实体
|
||||
- 利用组件索引系统进行高效查询
|
||||
- 启用Archetype系统减少查询遍历
|
||||
|
||||
### 2. 查询优化
|
||||
- 使用 `EntityManager.query()` 流式API构建复杂查询
|
||||
- 缓存频繁查询的结果
|
||||
- 利用脏标记系统避免不必要的更新
|
||||
|
||||
### 3. 性能监控
|
||||
- 定期检查 `EntityManager.getStatistics()` 获取性能数据
|
||||
- 监控组件索引命中率
|
||||
- 使用框架提供的性能统计功能
|
||||
|
||||
## 下一步
|
||||
|
||||
现在您已经掌握了 ECS Framework 的基础用法,可以继续学习:
|
||||
|
||||
- [EntityManager 使用指南](entity-manager-example.md) - 详细了解实体管理器的高级功能
|
||||
- [性能优化指南](performance-optimization.md) - 深入了解三大性能优化系统
|
||||
- [核心概念](core-concepts.md) - 深入了解 ECS 架构和设计原理
|
||||
- [查询系统使用指南](query-system-usage.md) - 学习高性能查询系统的详细用法
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 如何在不同游戏引擎中集成?
|
||||
|
||||
A: ECS Framework 是引擎无关的,您只需要:
|
||||
1. 通过npm安装框架 `npm install @esengine/ecs-framework`
|
||||
2. 在游戏引擎的主循环中调用 `scene.update()`
|
||||
3. 根据需要集成渲染、输入等引擎特定功能
|
||||
|
||||
### Q: 如何处理输入?
|
||||
|
||||
A: 框架本身不提供输入处理,建议:
|
||||
1. 创建一个输入组件来存储输入状态
|
||||
2. 在游戏引擎的输入回调中更新输入组件
|
||||
3. 创建输入处理系统来响应输入状态
|
||||
|
||||
### Q: 如何优化大规模实体性能?
|
||||
|
||||
A: 关键优化策略:
|
||||
1. 使用 `EntityManager` 的高级查询功能
|
||||
2. 启用组件索引系统进行快速查询
|
||||
3. 利用Archetype系统减少查询遍历
|
||||
4. 使用脏标记系统避免不必要的更新
|
||||
|
||||
### Q: EntityManager 有什么优势?
|
||||
|
||||
A: EntityManager 提供了:
|
||||
- O(1) 复杂度的组件查询(使用索引)
|
||||
- 流式API的复杂查询构建
|
||||
- 自动的性能优化系统集成
|
||||
- 统一的实体管理接口
|
||||
658
docs/guide/component.md
Normal file
658
docs/guide/component.md
Normal file
@@ -0,0 +1,658 @@
|
||||
# 组件系统
|
||||
|
||||
在 ECS 架构中,组件(Component)是数据和行为的载体。组件定义了实体具有的属性和功能,是 ECS 架构的核心构建块。
|
||||
|
||||
## 基本概念
|
||||
|
||||
组件是继承自 `Component` 抽象基类的具体类,用于:
|
||||
- 存储实体的数据(如位置、速度、健康值等)
|
||||
- 定义与数据相关的行为方法
|
||||
- 提供生命周期回调钩子
|
||||
- 支持序列化和调试
|
||||
|
||||
## 创建组件
|
||||
|
||||
### 基础组件定义
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
x: number = 0;
|
||||
y: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('Health')
|
||||
class Health extends Component {
|
||||
current: number;
|
||||
max: number;
|
||||
|
||||
constructor(max: number = 100) {
|
||||
super();
|
||||
this.max = max;
|
||||
this.current = max;
|
||||
}
|
||||
|
||||
// 组件可以包含行为方法
|
||||
takeDamage(damage: number): void {
|
||||
this.current = Math.max(0, this.current - damage);
|
||||
}
|
||||
|
||||
heal(amount: number): void {
|
||||
this.current = Math.min(this.max, this.current + amount);
|
||||
}
|
||||
|
||||
isDead(): boolean {
|
||||
return this.current <= 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 组件装饰器
|
||||
|
||||
**必须使用 `@ECSComponent` 装饰器**,这确保了:
|
||||
- 组件在代码混淆后仍能正确识别
|
||||
- 提供稳定的类型名称用于序列化和调试
|
||||
- 框架能正确管理组件注册
|
||||
|
||||
```typescript
|
||||
// 正确的用法
|
||||
@ECSComponent('Velocity')
|
||||
class Velocity extends Component {
|
||||
dx: number = 0;
|
||||
dy: number = 0;
|
||||
}
|
||||
|
||||
// 错误的用法 - 没有装饰器
|
||||
class BadComponent extends Component {
|
||||
// 这样定义的组件可能在生产环境出现问题
|
||||
}
|
||||
```
|
||||
|
||||
## 组件生命周期
|
||||
|
||||
组件提供了生命周期钩子,可以重写来执行特定的逻辑:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('ExampleComponent')
|
||||
class ExampleComponent extends Component {
|
||||
private resource: SomeResource | null = null;
|
||||
|
||||
/**
|
||||
* 组件被添加到实体时调用
|
||||
* 用于初始化资源、建立引用等
|
||||
*/
|
||||
onAddedToEntity(): void {
|
||||
console.log(`组件 ${this.constructor.name} 已添加,实体ID: ${this.entityId}`);
|
||||
this.resource = new SomeResource();
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件从实体移除时调用
|
||||
* 用于清理资源、断开引用等
|
||||
*/
|
||||
onRemovedFromEntity(): void {
|
||||
console.log(`组件 ${this.constructor.name} 已移除`);
|
||||
if (this.resource) {
|
||||
this.resource.cleanup();
|
||||
this.resource = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 组件与实体的关系
|
||||
|
||||
组件存储了所属实体的ID (`entityId`),而不是直接引用实体对象。这是ECS数据导向设计的体现,避免了循环引用。
|
||||
|
||||
在实际使用中,**应该在 System 中处理实体和组件的交互**,而不是在组件内部:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Health')
|
||||
class Health extends Component {
|
||||
current: number;
|
||||
max: number;
|
||||
|
||||
constructor(max: number = 100) {
|
||||
super();
|
||||
this.max = max;
|
||||
this.current = max;
|
||||
}
|
||||
|
||||
isDead(): boolean {
|
||||
return this.current <= 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('Damage')
|
||||
class Damage extends Component {
|
||||
value: number;
|
||||
|
||||
constructor(value: number) {
|
||||
super();
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
// 推荐:在 System 中处理逻辑
|
||||
class DamageSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(new Matcher().all(Health, Damage));
|
||||
}
|
||||
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health)!;
|
||||
const damage = entity.getComponent(Damage)!;
|
||||
|
||||
health.current -= damage.value;
|
||||
|
||||
if (health.isDead()) {
|
||||
entity.destroy();
|
||||
}
|
||||
|
||||
// 应用伤害后移除 Damage 组件
|
||||
entity.removeComponent(damage);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 组件属性
|
||||
|
||||
每个组件都有一些内置属性:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('ExampleComponent')
|
||||
class ExampleComponent extends Component {
|
||||
someData: string = "example";
|
||||
|
||||
onAddedToEntity(): void {
|
||||
console.log(`组件ID: ${this.id}`); // 唯一的组件ID
|
||||
console.log(`所属实体ID: ${this.entityId}`); // 所属实体的ID
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
如果需要访问实体对象,应该在 System 中进行:
|
||||
|
||||
```typescript
|
||||
class ExampleSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(new Matcher().all(ExampleComponent));
|
||||
}
|
||||
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const comp = entity.getComponent(ExampleComponent)!;
|
||||
console.log(`实体名称: ${entity.name}`);
|
||||
console.log(`组件数据: ${comp.someData}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 复杂组件示例
|
||||
|
||||
### 状态机组件
|
||||
|
||||
```typescript
|
||||
enum EntityState {
|
||||
Idle,
|
||||
Moving,
|
||||
Attacking,
|
||||
Dead
|
||||
}
|
||||
|
||||
@ECSComponent('StateMachine')
|
||||
class StateMachine extends Component {
|
||||
private _currentState: EntityState = EntityState.Idle;
|
||||
private _previousState: EntityState = EntityState.Idle;
|
||||
private _stateTimer: number = 0;
|
||||
|
||||
get currentState(): EntityState {
|
||||
return this._currentState;
|
||||
}
|
||||
|
||||
get previousState(): EntityState {
|
||||
return this._previousState;
|
||||
}
|
||||
|
||||
get stateTimer(): number {
|
||||
return this._stateTimer;
|
||||
}
|
||||
|
||||
changeState(newState: EntityState): void {
|
||||
if (this._currentState !== newState) {
|
||||
this._previousState = this._currentState;
|
||||
this._currentState = newState;
|
||||
this._stateTimer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
updateTimer(deltaTime: number): void {
|
||||
this._stateTimer += deltaTime;
|
||||
}
|
||||
|
||||
isInState(state: EntityState): boolean {
|
||||
return this._currentState === state;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 配置数据组件
|
||||
|
||||
```typescript
|
||||
interface WeaponData {
|
||||
damage: number;
|
||||
range: number;
|
||||
fireRate: number;
|
||||
ammo: number;
|
||||
}
|
||||
|
||||
@ECSComponent('WeaponConfig')
|
||||
class WeaponConfig extends Component {
|
||||
data: WeaponData;
|
||||
|
||||
constructor(weaponData: WeaponData) {
|
||||
super();
|
||||
this.data = { ...weaponData }; // 深拷贝避免共享引用
|
||||
}
|
||||
|
||||
// 提供便捷的访问方法
|
||||
getDamage(): number {
|
||||
return this.data.damage;
|
||||
}
|
||||
|
||||
canFire(): boolean {
|
||||
return this.data.ammo > 0;
|
||||
}
|
||||
|
||||
consumeAmmo(): boolean {
|
||||
if (this.data.ammo > 0) {
|
||||
this.data.ammo--;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 保持组件简单
|
||||
|
||||
```typescript
|
||||
// 好的组件设计 - 单一职责
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
x: number = 0;
|
||||
y: number = 0;
|
||||
}
|
||||
|
||||
@ECSComponent('Velocity')
|
||||
class Velocity extends Component {
|
||||
dx: number = 0;
|
||||
dy: number = 0;
|
||||
}
|
||||
|
||||
// 避免的组件设计 - 职责过多
|
||||
@ECSComponent('GameObject')
|
||||
class GameObject extends Component {
|
||||
x: number;
|
||||
y: number;
|
||||
dx: number;
|
||||
dy: number;
|
||||
health: number;
|
||||
damage: number;
|
||||
sprite: string;
|
||||
// 太多不相关的属性
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 使用构造函数初始化
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Transform')
|
||||
class Transform extends Component {
|
||||
x: number;
|
||||
y: number;
|
||||
rotation: number;
|
||||
scale: number;
|
||||
|
||||
constructor(x = 0, y = 0, rotation = 0, scale = 1) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.rotation = rotation;
|
||||
this.scale = scale;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 明确的类型定义
|
||||
|
||||
```typescript
|
||||
interface InventoryItem {
|
||||
id: string;
|
||||
name: string;
|
||||
quantity: number;
|
||||
type: 'weapon' | 'consumable' | 'misc';
|
||||
}
|
||||
|
||||
@ECSComponent('Inventory')
|
||||
class Inventory extends Component {
|
||||
items: InventoryItem[] = [];
|
||||
maxSlots: number;
|
||||
|
||||
constructor(maxSlots: number = 20) {
|
||||
super();
|
||||
this.maxSlots = maxSlots;
|
||||
}
|
||||
|
||||
addItem(item: InventoryItem): boolean {
|
||||
if (this.items.length < this.maxSlots) {
|
||||
this.items.push(item);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
removeItem(itemId: string): InventoryItem | null {
|
||||
const index = this.items.findIndex(item => item.id === itemId);
|
||||
if (index !== -1) {
|
||||
return this.items.splice(index, 1)[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 引用其他实体
|
||||
|
||||
当组件需要关联其他实体时(如父子关系、跟随目标等),**推荐方式是存储实体ID**,然后在 System 中查找:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Follower')
|
||||
class Follower extends Component {
|
||||
targetId: number;
|
||||
followDistance: number = 50;
|
||||
|
||||
constructor(targetId: number) {
|
||||
super();
|
||||
this.targetId = targetId;
|
||||
}
|
||||
}
|
||||
|
||||
// 在 System 中查找目标实体并处理逻辑
|
||||
class FollowerSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(new Matcher().all(Follower, Position));
|
||||
}
|
||||
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const follower = entity.getComponent(Follower)!;
|
||||
const position = entity.getComponent(Position)!;
|
||||
|
||||
// 通过场景查找目标实体
|
||||
const target = entity.scene?.findEntityById(follower.targetId);
|
||||
if (target) {
|
||||
const targetPos = target.getComponent(Position);
|
||||
if (targetPos) {
|
||||
// 跟随逻辑
|
||||
const dx = targetPos.x - position.x;
|
||||
const dy = targetPos.y - position.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance > follower.followDistance) {
|
||||
// 移动靠近目标
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这种方式的优势:
|
||||
- 组件保持简单,只存储基本数据类型
|
||||
- 符合数据导向设计
|
||||
- 在 System 中统一处理查找和逻辑
|
||||
- 易于理解和维护
|
||||
|
||||
**避免在组件中直接存储实体引用**:
|
||||
|
||||
```typescript
|
||||
// 错误示范:直接存储实体引用
|
||||
@ECSComponent('BadFollower')
|
||||
class BadFollower extends Component {
|
||||
target: Entity; // 实体销毁后仍持有引用,可能导致内存泄漏
|
||||
}
|
||||
```
|
||||
|
||||
## 高级特性
|
||||
|
||||
### EntityRef 装饰器 - 自动引用追踪
|
||||
|
||||
框架提供了 `@EntityRef` 装饰器用于**特殊场景**下安全地存储实体引用。这是一个高级特性,一般情况下推荐使用存储ID的方式。
|
||||
|
||||
#### 什么时候需要 EntityRef?
|
||||
|
||||
在以下场景中,`@EntityRef` 可以简化代码:
|
||||
|
||||
1. **父子关系**: 需要在组件中直接访问父实体或子实体
|
||||
2. **复杂关联**: 实体之间有多个引用关系
|
||||
3. **频繁访问**: 需要在多处访问引用的实体,使用ID查找会有性能开销
|
||||
|
||||
#### 核心特性
|
||||
|
||||
`@EntityRef` 装饰器通过 **ReferenceTracker** 自动追踪引用关系:
|
||||
|
||||
- 当被引用的实体销毁时,所有指向它的 `@EntityRef` 属性自动设为 `null`
|
||||
- 防止跨场景引用(会输出警告并拒绝设置)
|
||||
- 防止引用已销毁的实体(会输出警告并设为 `null`)
|
||||
- 使用 WeakRef 避免内存泄漏(自动GC支持)
|
||||
- 组件移除时自动清理引用注册
|
||||
|
||||
#### 基本用法
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent, EntityRef, Entity } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSComponent('Parent')
|
||||
class ParentComponent extends Component {
|
||||
@EntityRef()
|
||||
parent: Entity | null = null;
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
const scene = new Scene();
|
||||
const parent = scene.createEntity('Parent');
|
||||
const child = scene.createEntity('Child');
|
||||
|
||||
const comp = child.addComponent(new ParentComponent());
|
||||
comp.parent = parent;
|
||||
|
||||
console.log(comp.parent); // Entity { name: 'Parent' }
|
||||
|
||||
// 当 parent 被销毁时,comp.parent 自动变为 null
|
||||
parent.destroy();
|
||||
console.log(comp.parent); // null
|
||||
```
|
||||
|
||||
#### 多个引用属性
|
||||
|
||||
一个组件可以有多个 `@EntityRef` 属性:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Combat')
|
||||
class CombatComponent extends Component {
|
||||
@EntityRef()
|
||||
target: Entity | null = null;
|
||||
|
||||
@EntityRef()
|
||||
ally: Entity | null = null;
|
||||
|
||||
@EntityRef()
|
||||
lastAttacker: Entity | null = null;
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
const player = scene.createEntity('Player');
|
||||
const enemy = scene.createEntity('Enemy');
|
||||
const npc = scene.createEntity('NPC');
|
||||
|
||||
const combat = player.addComponent(new CombatComponent());
|
||||
combat.target = enemy;
|
||||
combat.ally = npc;
|
||||
|
||||
// enemy 销毁后,只有 target 变为 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 参考
|
||||
288
docs/guide/entity.md
Normal file
288
docs/guide/entity.md
Normal file
@@ -0,0 +1,288 @@
|
||||
# 实体类
|
||||
|
||||
在 ECS 架构中,实体(Entity)是游戏世界中的基本对象。实体本身不包含游戏逻辑或数据,它只是一个容器,用来组合不同的组件来实现各种功能。
|
||||
|
||||
## 基本概念
|
||||
|
||||
实体是一个轻量级的对象,主要用于:
|
||||
- 作为组件的容器
|
||||
- 提供唯一标识(ID)
|
||||
- 管理组件的生命周期
|
||||
|
||||
## 创建实体
|
||||
|
||||
**重要提示:实体必须通过场景创建,不支持手动创建!**
|
||||
|
||||
实体必须通过场景的 `createEntity()` 方法来创建,这样才能确保:
|
||||
- 实体被正确添加到场景的实体管理系统中
|
||||
- 实体被添加到查询系统中,供系统使用
|
||||
- 实体获得正确的场景引用
|
||||
- 触发相关的生命周期事件
|
||||
|
||||
```typescript
|
||||
// 正确的方式:通过场景创建实体
|
||||
const player = scene.createEntity("Player");
|
||||
|
||||
// ❌ 错误的方式:手动创建实体
|
||||
// const entity = new Entity("MyEntity", 1); // 这样创建的实体系统无法管理
|
||||
```
|
||||
|
||||
## 添加组件
|
||||
|
||||
实体通过添加组件来获得功能:
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent } from '@esengine/ecs-framework';
|
||||
|
||||
// 定义位置组件
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
x: number = 0;
|
||||
y: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
|
||||
// 定义健康组件
|
||||
@ECSComponent('Health')
|
||||
class Health extends Component {
|
||||
current: number = 100;
|
||||
max: number = 100;
|
||||
|
||||
constructor(max: number = 100) {
|
||||
super();
|
||||
this.max = max;
|
||||
this.current = max;
|
||||
}
|
||||
}
|
||||
|
||||
// 给实体添加组件
|
||||
const player = scene.createEntity("Player");
|
||||
player.addComponent(new Position(100, 200));
|
||||
player.addComponent(new Health(150));
|
||||
```
|
||||
|
||||
## 获取组件
|
||||
|
||||
```typescript
|
||||
// 获取组件(传入组件类,不是实例)
|
||||
const position = player.getComponent(Position); // 返回 Position | null
|
||||
const health = player.getComponent(Health); // 返回 Health | null
|
||||
|
||||
// 检查组件是否存在
|
||||
if (position) {
|
||||
console.log(`玩家位置: x=${position.x}, y=${position.y}`);
|
||||
}
|
||||
|
||||
// 检查是否有某个组件
|
||||
if (player.hasComponent(Position)) {
|
||||
console.log("玩家有位置组件");
|
||||
}
|
||||
|
||||
// 获取所有组件实例(只读属性)
|
||||
const allComponents = player.components; // readonly Component[]
|
||||
|
||||
// 获取指定类型的所有组件(支持同类型多组件)
|
||||
const allHealthComponents = player.getComponents(Health); // Health[]
|
||||
|
||||
// 获取或创建组件(如果不存在则自动创建)
|
||||
const position = player.getOrCreateComponent(Position, 0, 0); // 传入构造参数
|
||||
const health = player.getOrCreateComponent(Health, 100); // 如果存在则返回现有的,不存在则创建新的
|
||||
```
|
||||
|
||||
## 移除组件
|
||||
|
||||
```typescript
|
||||
// 方式1:通过组件类型移除
|
||||
const removedHealth = player.removeComponentByType(Health);
|
||||
if (removedHealth) {
|
||||
console.log("健康组件已被移除");
|
||||
}
|
||||
|
||||
// 方式2:通过组件实例移除
|
||||
const healthComponent = player.getComponent(Health);
|
||||
if (healthComponent) {
|
||||
player.removeComponent(healthComponent);
|
||||
}
|
||||
|
||||
// 批量移除多种组件类型
|
||||
const removedComponents = player.removeComponentsByTypes([Position, Health]);
|
||||
|
||||
// 检查组件是否被移除
|
||||
if (!player.hasComponent(Health)) {
|
||||
console.log("健康组件已被移除");
|
||||
}
|
||||
```
|
||||
|
||||
## 实体查找
|
||||
|
||||
场景提供了多种方式来查找实体:
|
||||
|
||||
### 通过名称查找
|
||||
|
||||
```typescript
|
||||
// 查找单个实体
|
||||
const player = scene.findEntity("Player");
|
||||
// 或使用别名方法
|
||||
const player2 = scene.getEntityByName("Player");
|
||||
|
||||
if (player) {
|
||||
console.log("找到玩家实体");
|
||||
}
|
||||
```
|
||||
|
||||
### 通过 ID 查找
|
||||
|
||||
```typescript
|
||||
// 通过实体 ID 查找
|
||||
const entity = scene.findEntityById(123);
|
||||
```
|
||||
|
||||
### 通过标签查找
|
||||
|
||||
实体支持标签系统,用于快速分类和查找:
|
||||
|
||||
```typescript
|
||||
// 设置标签
|
||||
player.tag = 1; // 玩家标签
|
||||
enemy.tag = 2; // 敌人标签
|
||||
|
||||
// 通过标签查找所有相关实体
|
||||
const players = scene.findEntitiesByTag(1);
|
||||
const enemies = scene.findEntitiesByTag(2);
|
||||
// 或使用别名方法
|
||||
const allPlayers = scene.getEntitiesByTag(1);
|
||||
```
|
||||
|
||||
|
||||
## 实体生命周期
|
||||
|
||||
```typescript
|
||||
// 销毁实体
|
||||
player.destroy();
|
||||
|
||||
// 检查实体是否已销毁
|
||||
if (player.isDestroyed) {
|
||||
console.log("实体已被销毁");
|
||||
}
|
||||
```
|
||||
|
||||
## 实体事件
|
||||
|
||||
实体的组件变化会触发事件:
|
||||
|
||||
```typescript
|
||||
// 监听组件添加事件
|
||||
scene.eventSystem.on('component:added', (data) => {
|
||||
console.log('组件已添加:', data);
|
||||
});
|
||||
|
||||
// 监听实体创建事件
|
||||
scene.eventSystem.on('entity:created', (data) => {
|
||||
console.log('实体已创建:', data.entityName);
|
||||
});
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
|
||||
### 批量创建实体
|
||||
|
||||
框架提供了高性能的批量创建方法:
|
||||
|
||||
```typescript
|
||||
// 批量创建 100 个子弹实体(高性能版本)
|
||||
const bullets = scene.createEntities(100, "Bullet");
|
||||
|
||||
// 为每个子弹添加组件
|
||||
bullets.forEach((bullet, index) => {
|
||||
bullet.addComponent(new Position(Math.random() * 800, Math.random() * 600));
|
||||
bullet.addComponent(new Velocity(Math.random() * 100 - 50, Math.random() * 100 - 50));
|
||||
});
|
||||
```
|
||||
|
||||
`createEntities()` 方法会:
|
||||
- 批量分配实体 ID
|
||||
- 批量添加到实体列表
|
||||
- 优化查询系统更新
|
||||
- 减少系统缓存清理次数
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 合理的组件粒度
|
||||
|
||||
```typescript
|
||||
// 好的做法:功能单一的组件
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
x: number = 0;
|
||||
y: number = 0;
|
||||
}
|
||||
|
||||
@ECSComponent('Velocity')
|
||||
class Velocity extends Component {
|
||||
dx: number = 0;
|
||||
dy: number = 0;
|
||||
}
|
||||
|
||||
// 避免:功能过于复杂的组件
|
||||
@ECSComponent('Player')
|
||||
class Player extends Component {
|
||||
// 避免在一个组件中包含太多不相关的属性
|
||||
x: number;
|
||||
y: number;
|
||||
health: number;
|
||||
inventory: Item[];
|
||||
skills: Skill[];
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 使用装饰器
|
||||
|
||||
始终使用 `@ECSComponent` 装饰器:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Transform')
|
||||
class Transform extends Component {
|
||||
// 组件实现
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 合理命名
|
||||
|
||||
```typescript
|
||||
// 清晰的实体命名
|
||||
const mainCharacter = scene.createEntity("MainCharacter");
|
||||
const enemy1 = scene.createEntity("Goblin_001");
|
||||
const collectible = scene.createEntity("HealthPotion");
|
||||
```
|
||||
|
||||
### 4. 及时清理
|
||||
|
||||
```typescript
|
||||
// 不再需要的实体应该及时销毁
|
||||
if (enemy.getComponent(Health).current <= 0) {
|
||||
enemy.destroy();
|
||||
}
|
||||
```
|
||||
|
||||
## 调试实体
|
||||
|
||||
框架提供了调试功能来帮助开发:
|
||||
|
||||
```typescript
|
||||
// 获取实体调试信息
|
||||
const debugInfo = entity.getDebugInfo();
|
||||
console.log('实体信息:', debugInfo);
|
||||
|
||||
// 列出实体的所有组件
|
||||
entity.components.forEach(component => {
|
||||
console.log('组件:', component.constructor.name);
|
||||
});
|
||||
```
|
||||
|
||||
实体是 ECS 架构的核心概念之一,理解如何正确使用实体将帮助你构建高效、可维护的游戏代码。
|
||||
551
docs/guide/event-system.md
Normal file
551
docs/guide/event-system.md
Normal file
@@ -0,0 +1,551 @@
|
||||
# 事件系统
|
||||
|
||||
ECS 框架内置了强大的类型安全事件系统,支持同步/异步事件、优先级、批处理等高级功能。事件系统是实现组件间通信、系统间协作的核心机制。
|
||||
|
||||
## 基本概念
|
||||
|
||||
事件系统提供了发布-订阅模式的实现,包含以下核心概念:
|
||||
- **事件发布者**:发射事件的对象
|
||||
- **事件监听者**:监听并处理特定事件的对象
|
||||
- **事件类型**:字符串标识,用于区分不同类型的事件
|
||||
- **事件数据**:事件携带的相关信息
|
||||
|
||||
## 事件系统架构
|
||||
|
||||
框架提供了两层事件系统:
|
||||
|
||||
1. **TypeSafeEventSystem** - 底层高性能事件系统
|
||||
2. **EventBus** - 上层增强事件总线,提供更多便利功能
|
||||
|
||||
## 基本使用
|
||||
|
||||
### 在场景中使用事件系统
|
||||
|
||||
每个场景都有内置的事件系统:
|
||||
|
||||
```typescript
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 监听事件
|
||||
this.eventSystem.on('player_died', this.onPlayerDied.bind(this));
|
||||
this.eventSystem.on('enemy_spawned', this.onEnemySpawned.bind(this));
|
||||
this.eventSystem.on('score_changed', this.onScoreChanged.bind(this));
|
||||
}
|
||||
|
||||
private onPlayerDied(data: { player: Entity, cause: string }): void {
|
||||
console.log(`玩家死亡,原因: ${data.cause}`);
|
||||
// 处理玩家死亡逻辑
|
||||
}
|
||||
|
||||
private onEnemySpawned(data: { enemy: Entity, position: { x: number, y: number } }): void {
|
||||
console.log('敌人生成于:', data.position);
|
||||
// 处理敌人生成逻辑
|
||||
}
|
||||
|
||||
private onScoreChanged(data: { newScore: number, oldScore: number }): void {
|
||||
console.log(`分数变化: ${data.oldScore} -> ${data.newScore}`);
|
||||
// 更新UI显示
|
||||
}
|
||||
|
||||
// 在系统中发射事件
|
||||
someGameLogic(): void {
|
||||
// 发射同步事件
|
||||
this.eventSystem.emitSync('score_changed', {
|
||||
newScore: 1000,
|
||||
oldScore: 800
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 在系统中使用事件
|
||||
|
||||
系统可以方便地监听和发送事件:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Combat')
|
||||
class CombatSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Health, Combat));
|
||||
}
|
||||
|
||||
protected onInitialize(): void {
|
||||
// 使用系统提供的事件监听方法(自动清理)
|
||||
this.addEventListener('player_attack', this.onPlayerAttack.bind(this));
|
||||
this.addEventListener('enemy_death', this.onEnemyDeath.bind(this));
|
||||
}
|
||||
|
||||
private onPlayerAttack(data: { damage: number, target: Entity }): void {
|
||||
// 处理玩家攻击事件
|
||||
const health = data.target.getComponent(Health);
|
||||
if (health) {
|
||||
health.current -= data.damage;
|
||||
|
||||
if (health.current <= 0) {
|
||||
// 发送敌人死亡事件
|
||||
this.scene?.eventSystem.emitSync('enemy_death', {
|
||||
enemy: data.target,
|
||||
killer: 'player'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private onEnemyDeath(data: { enemy: Entity, killer: string }): void {
|
||||
// 处理敌人死亡
|
||||
data.enemy.destroy();
|
||||
|
||||
// 发送经验奖励事件
|
||||
this.scene?.eventSystem.emitSync('experience_gained', {
|
||||
amount: 100,
|
||||
source: 'enemy_kill'
|
||||
});
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const combat = entity.getComponent(Combat);
|
||||
if (combat && combat.shouldAttack()) {
|
||||
// 发射攻击事件
|
||||
this.scene?.eventSystem.emitSync('player_attack', {
|
||||
damage: combat.damage,
|
||||
target: combat.target
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 高级功能
|
||||
|
||||
### 一次性监听器
|
||||
|
||||
```typescript
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 只监听一次的事件
|
||||
this.eventSystem.once('game_start', this.onGameStart.bind(this));
|
||||
|
||||
// 或者使用配置对象
|
||||
this.eventSystem.on('level_complete', this.onLevelComplete.bind(this), {
|
||||
once: true // 只执行一次
|
||||
});
|
||||
}
|
||||
|
||||
private onGameStart(): void {
|
||||
console.log('游戏开始!');
|
||||
// 这个方法只会被调用一次
|
||||
}
|
||||
|
||||
private onLevelComplete(): void {
|
||||
console.log('关卡完成!');
|
||||
// 这个方法也只会被调用一次
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 优先级控制
|
||||
|
||||
```typescript
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 高优先级监听器先执行
|
||||
this.eventSystem.on('damage_dealt', this.onDamageDealt.bind(this), {
|
||||
priority: 100 // 高优先级
|
||||
});
|
||||
|
||||
// 普通优先级
|
||||
this.eventSystem.on('damage_dealt', this.updateUI.bind(this), {
|
||||
priority: 0 // 默认优先级
|
||||
});
|
||||
|
||||
// 低优先级最后执行
|
||||
this.eventSystem.on('damage_dealt', this.logDamage.bind(this), {
|
||||
priority: -100 // 低优先级
|
||||
});
|
||||
}
|
||||
|
||||
private onDamageDealt(data: any): void {
|
||||
// 最先执行 - 处理核心游戏逻辑
|
||||
}
|
||||
|
||||
private updateUI(data: any): void {
|
||||
// 中等优先级 - 更新界面
|
||||
}
|
||||
|
||||
private logDamage(data: any): void {
|
||||
// 最后执行 - 记录日志
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 异步事件处理
|
||||
|
||||
```typescript
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 监听异步事件
|
||||
this.eventSystem.onAsync('save_game', this.onSaveGame.bind(this));
|
||||
this.eventSystem.onAsync('load_data', this.onLoadData.bind(this));
|
||||
}
|
||||
|
||||
private async onSaveGame(data: { saveSlot: number }): Promise<void> {
|
||||
console.log(`开始保存游戏到槽位 ${data.saveSlot}`);
|
||||
|
||||
// 模拟异步保存操作
|
||||
await this.saveGameData(data.saveSlot);
|
||||
|
||||
console.log('游戏保存完成');
|
||||
}
|
||||
|
||||
private async onLoadData(data: { url: string }): Promise<void> {
|
||||
try {
|
||||
const response = await fetch(data.url);
|
||||
const gameData = await response.json();
|
||||
// 处理加载的数据
|
||||
} catch (error) {
|
||||
console.error('数据加载失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private async saveGameData(slot: number): Promise<void> {
|
||||
// 模拟保存操作
|
||||
return new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
|
||||
// 发射异步事件
|
||||
public async triggerSave(): Promise<void> {
|
||||
// 使用 emit 而不是 emitSync 来触发异步监听器
|
||||
await this.eventSystem.emit('save_game', { saveSlot: 1 });
|
||||
console.log('所有异步保存操作完成');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 事件统计和调试
|
||||
|
||||
```typescript
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.eventSystem.on('debug_event', this.onDebugEvent.bind(this));
|
||||
}
|
||||
|
||||
private onDebugEvent(): void {
|
||||
// 处理调试事件
|
||||
}
|
||||
|
||||
public showEventStats(): void {
|
||||
// 获取特定事件的统计信息
|
||||
const stats = this.eventSystem.getStats('debug_event') as any;
|
||||
if (stats) {
|
||||
console.log('事件统计:');
|
||||
console.log(`- 事件类型: ${stats.eventType}`);
|
||||
console.log(`- 监听器数量: ${stats.listenerCount}`);
|
||||
console.log(`- 触发次数: ${stats.triggerCount}`);
|
||||
console.log(`- 平均执行时间: ${stats.averageExecutionTime.toFixed(2)}ms`);
|
||||
}
|
||||
|
||||
// 获取所有事件统计
|
||||
const allStats = this.eventSystem.getStats() as Map<string, any>;
|
||||
console.log(`总共有 ${allStats.size} 种事件类型`);
|
||||
}
|
||||
|
||||
public resetEventStats(): void {
|
||||
// 重置特定事件的统计
|
||||
this.eventSystem.resetStats('debug_event');
|
||||
|
||||
// 或重置所有统计
|
||||
this.eventSystem.resetStats();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 全局事件总线
|
||||
|
||||
对于跨场景的事件通信,可以使用全局事件总线:
|
||||
|
||||
```typescript
|
||||
import { GlobalEventBus } from '@esengine/ecs-framework';
|
||||
|
||||
class GameManager {
|
||||
private eventBus = GlobalEventBus.getInstance();
|
||||
|
||||
constructor() {
|
||||
this.setupGlobalEvents();
|
||||
}
|
||||
|
||||
private setupGlobalEvents(): void {
|
||||
// 监听全局事件
|
||||
this.eventBus.on('player_level_up', this.onPlayerLevelUp.bind(this));
|
||||
this.eventBus.on('achievement_unlocked', this.onAchievementUnlocked.bind(this));
|
||||
this.eventBus.onAsync('upload_score', this.onUploadScore.bind(this));
|
||||
}
|
||||
|
||||
private onPlayerLevelUp(data: { level: number, experience: number }): void {
|
||||
console.log(`玩家升级到 ${data.level} 级!`);
|
||||
// 处理全局升级逻辑
|
||||
}
|
||||
|
||||
private onAchievementUnlocked(data: { achievementId: string, name: string }): void {
|
||||
console.log(`解锁成就: ${data.name}`);
|
||||
// 显示成就通知
|
||||
}
|
||||
|
||||
private async onUploadScore(data: { score: number, playerName: string }): Promise<void> {
|
||||
// 异步上传分数到服务器
|
||||
try {
|
||||
await this.uploadToServer(data);
|
||||
console.log('分数上传成功');
|
||||
} catch (error) {
|
||||
console.error('分数上传失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
public triggerGlobalEvent(): void {
|
||||
// 发射全局事件
|
||||
this.eventBus.emit('player_level_up', {
|
||||
level: 10,
|
||||
experience: 2500
|
||||
});
|
||||
}
|
||||
|
||||
private async uploadToServer(data: any): Promise<void> {
|
||||
// 模拟服务器上传
|
||||
return new Promise(resolve => setTimeout(resolve, 2000));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 批处理事件
|
||||
|
||||
对于高频事件,可以使用批处理来提升性能:
|
||||
|
||||
```typescript
|
||||
class MovementSystem extends EntitySystem {
|
||||
protected onInitialize(): void {
|
||||
// 设置位置更新事件的批处理
|
||||
this.scene?.eventSystem.setBatchConfig('position_updated', {
|
||||
batchSize: 50, // 批处理大小
|
||||
delay: 16, // 延迟时间(毫秒)
|
||||
enabled: true
|
||||
});
|
||||
|
||||
// 监听批处理事件
|
||||
this.addEventListener('position_updated:batch', this.onPositionBatch.bind(this));
|
||||
}
|
||||
|
||||
private onPositionBatch(batchData: any): void {
|
||||
console.log(`批处理位置更新,共 ${batchData.count} 个事件`);
|
||||
|
||||
// 批量处理所有位置更新
|
||||
for (const event of batchData.events) {
|
||||
this.updateMinimap(event.entityId, event.position);
|
||||
}
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const position = entity.getComponent(Position);
|
||||
if (position && position.hasChanged) {
|
||||
// 发射高频位置更新事件(会被批处理)
|
||||
this.scene?.eventSystem.emitSync('position_updated', {
|
||||
entityId: entity.id,
|
||||
position: { x: position.x, y: position.y }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateMinimap(entityId: number, position: { x: number, y: number }): void {
|
||||
// 更新小地图显示
|
||||
}
|
||||
|
||||
public flushPositionUpdates(): void {
|
||||
// 立即处理所有待处理的位置更新
|
||||
this.scene?.eventSystem.flushBatch('position_updated');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 预定义的 ECS 事件
|
||||
|
||||
框架提供了一些预定义的 ECS 生命周期事件:
|
||||
|
||||
```typescript
|
||||
import { ECSEventType } from '@esengine/ecs-framework';
|
||||
|
||||
class ECSMonitor {
|
||||
private eventBus = GlobalEventBus.getInstance();
|
||||
|
||||
constructor() {
|
||||
this.setupECSEvents();
|
||||
}
|
||||
|
||||
private setupECSEvents(): void {
|
||||
// 监听实体生命周期事件
|
||||
this.eventBus.onEntityCreated(this.onEntityCreated.bind(this));
|
||||
this.eventBus.on(ECSEventType.ENTITY_DESTROYED, this.onEntityDestroyed.bind(this));
|
||||
|
||||
// 监听组件生命周期事件
|
||||
this.eventBus.onComponentAdded(this.onComponentAdded.bind(this));
|
||||
this.eventBus.on(ECSEventType.COMPONENT_REMOVED, this.onComponentRemoved.bind(this));
|
||||
|
||||
// 监听系统事件
|
||||
this.eventBus.on(ECSEventType.SYSTEM_ADDED, this.onSystemAdded.bind(this));
|
||||
this.eventBus.onSystemError(this.onSystemError.bind(this));
|
||||
|
||||
// 监听性能警告
|
||||
this.eventBus.onPerformanceWarning(this.onPerformanceWarning.bind(this));
|
||||
}
|
||||
|
||||
private onEntityCreated(data: any): void {
|
||||
console.log(`实体创建: ${data.entityName} (ID: ${data.entity.id})`);
|
||||
}
|
||||
|
||||
private onEntityDestroyed(data: any): void {
|
||||
console.log(`实体销毁: ${data.entity.name} (ID: ${data.entity.id})`);
|
||||
}
|
||||
|
||||
private onComponentAdded(data: any): void {
|
||||
console.log(`组件添加: ${data.componentType} 到实体 ${data.entity.name}`);
|
||||
}
|
||||
|
||||
private onComponentRemoved(data: any): void {
|
||||
console.log(`组件移除: ${data.componentType} 从实体 ${data.entity.name}`);
|
||||
}
|
||||
|
||||
private onSystemAdded(data: any): void {
|
||||
console.log(`系统添加: ${data.systemName}`);
|
||||
}
|
||||
|
||||
private onSystemError(data: any): void {
|
||||
console.error(`系统错误: ${data.systemName}`, data.error);
|
||||
}
|
||||
|
||||
private onPerformanceWarning(data: any): void {
|
||||
console.warn(`性能警告: ${data.systemName} 执行时间过长 (${data.executionTime}ms)`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 事件命名规范
|
||||
|
||||
```typescript
|
||||
// ✅ 好的事件命名
|
||||
this.eventSystem.emitSync('player:health_changed', data);
|
||||
this.eventSystem.emitSync('enemy:spawned', data);
|
||||
this.eventSystem.emitSync('ui:score_updated', data);
|
||||
this.eventSystem.emitSync('game:paused', data);
|
||||
|
||||
// ❌ 避免的事件命名
|
||||
this.eventSystem.emitSync('event1', data);
|
||||
this.eventSystem.emitSync('update', data);
|
||||
this.eventSystem.emitSync('change', data);
|
||||
```
|
||||
|
||||
### 2. 类型安全的事件数据
|
||||
|
||||
```typescript
|
||||
// 定义事件数据接口
|
||||
interface PlayerHealthChangedEvent {
|
||||
entityId: number;
|
||||
oldHealth: number;
|
||||
newHealth: number;
|
||||
cause: 'damage' | 'healing';
|
||||
}
|
||||
|
||||
interface EnemySpawnedEvent {
|
||||
enemyType: string;
|
||||
position: { x: number, y: number };
|
||||
level: number;
|
||||
}
|
||||
|
||||
// 使用类型安全的事件
|
||||
class HealthSystem extends EntitySystem {
|
||||
private onHealthChanged(data: PlayerHealthChangedEvent): void {
|
||||
// TypeScript 会提供完整的类型检查
|
||||
console.log(`生命值变化: ${data.oldHealth} -> ${data.newHealth}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 避免事件循环
|
||||
|
||||
```typescript
|
||||
// ❌ 避免:可能导致无限循环
|
||||
class BadEventHandler {
|
||||
private onScoreChanged(data: any): void {
|
||||
// 在处理分数变化时又触发分数变化事件
|
||||
this.scene?.eventSystem.emitSync('score_changed', newData); // 危险!
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 正确:使用不同的事件类型或条件判断
|
||||
class GoodEventHandler {
|
||||
private isProcessingScore = false;
|
||||
|
||||
private onScoreChanged(data: any): void {
|
||||
if (this.isProcessingScore) return; // 防止循环
|
||||
|
||||
this.isProcessingScore = true;
|
||||
// 处理分数变化
|
||||
this.updateUI(data);
|
||||
this.isProcessingScore = false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 及时清理事件监听器
|
||||
|
||||
```typescript
|
||||
class TemporaryUI {
|
||||
private listenerId: string;
|
||||
|
||||
constructor(scene: Scene) {
|
||||
// 保存监听器ID用于后续清理
|
||||
this.listenerId = scene.eventSystem.on('ui_update', this.onUpdate.bind(this));
|
||||
}
|
||||
|
||||
private onUpdate(data: any): void {
|
||||
// 处理UI更新
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
// 清理事件监听器
|
||||
if (this.listenerId) {
|
||||
scene.eventSystem.off('ui_update', this.listenerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 性能考虑
|
||||
|
||||
```typescript
|
||||
class OptimizedEventHandler {
|
||||
protected onInitialize(): void {
|
||||
// 对于高频事件,使用批处理
|
||||
this.scene?.eventSystem.setBatchConfig('movement_update', {
|
||||
batchSize: 100,
|
||||
delay: 16,
|
||||
enabled: true
|
||||
});
|
||||
|
||||
// 对于低频但重要的事件,使用高优先级
|
||||
this.addEventListener('game_over', this.onGameOver.bind(this), {
|
||||
priority: 1000
|
||||
});
|
||||
|
||||
// 对于一次性事件,使用 once
|
||||
this.addEventListener('level_start', this.onLevelStart.bind(this), {
|
||||
once: true
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
事件系统是 ECS 框架中实现松耦合架构的重要工具,正确使用事件系统能让你的游戏代码更加模块化、可维护和可扩展。
|
||||
417
docs/guide/getting-started.md
Normal file
417
docs/guide/getting-started.md
Normal file
@@ -0,0 +1,417 @@
|
||||
# 快速开始
|
||||
|
||||
本指南将帮助你快速上手 ECS Framework,从安装到创建第一个 ECS 应用。
|
||||
|
||||
## 安装
|
||||
|
||||
### NPM 安装
|
||||
|
||||
```bash
|
||||
# 使用 npm
|
||||
npm install @esengine/ecs-framework
|
||||
```
|
||||
|
||||
## 初始化 Core
|
||||
|
||||
### 基础初始化
|
||||
|
||||
ECS Framework 的核心是 `Core` 类,它是一个单例模式,负责管理整个框架的生命周期。
|
||||
|
||||
```typescript
|
||||
import { Core } from '@esengine/ecs-framework'
|
||||
|
||||
// 方式1:使用配置对象(推荐)
|
||||
const core = Core.create({
|
||||
debug: true, // 启用调试模式,提供详细的日志和性能监控
|
||||
enableEntitySystems: true, // 启用实体系统,这是ECS的核心功能
|
||||
debugConfig: { // 可选:高级调试配置
|
||||
enabled: false, // 是否启用WebSocket调试服务器
|
||||
websocketUrl: 'ws://localhost:8080',
|
||||
debugFrameRate: 30, // 调试数据发送帧率
|
||||
channels: {
|
||||
entities: true,
|
||||
systems: true,
|
||||
performance: true,
|
||||
components: true,
|
||||
scenes: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 方式2:简化创建(向后兼容)
|
||||
const core = Core.create(true); // 等同于 { debug: true, enableEntitySystems: true }
|
||||
|
||||
// 方式3:生产环境配置
|
||||
const core = Core.create({
|
||||
debug: false, // 生产环境关闭调试
|
||||
enableEntitySystems: true
|
||||
});
|
||||
```
|
||||
|
||||
### Core 配置详解
|
||||
|
||||
```typescript
|
||||
interface ICoreConfig {
|
||||
/** 是否启用调试模式 - 影响日志级别和性能监控 */
|
||||
debug?: boolean;
|
||||
|
||||
/** 是否启用实体系统 - 核心ECS功能开关 */
|
||||
enableEntitySystems?: boolean;
|
||||
|
||||
/** 高级调试配置 - 用于开发工具集成 */
|
||||
debugConfig?: {
|
||||
enabled: boolean; // 是否启用调试服务器
|
||||
websocketUrl: string; // WebSocket服务器地址
|
||||
autoReconnect?: boolean; // 是否自动重连
|
||||
debugFrameRate?: 60 | 30 | 15; // 调试数据发送帧率
|
||||
channels: { // 数据通道配置
|
||||
entities: boolean; // 实体数据
|
||||
systems: boolean; // 系统数据
|
||||
performance: boolean; // 性能数据
|
||||
components: boolean; // 组件数据
|
||||
scenes: boolean; // 场景数据
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Core 实例管理
|
||||
|
||||
Core 采用单例模式,创建后可以通过静态属性获取:
|
||||
|
||||
```typescript
|
||||
// 创建实例
|
||||
const core = Core.create(true);
|
||||
|
||||
// 获取已创建的实例
|
||||
const instance = Core.Instance; // 获取当前实例,如果未创建则为 null
|
||||
```
|
||||
|
||||
### 游戏循环集成
|
||||
|
||||
**重要**: 在创建实体和系统之前,你需要先了解如何将 ECS Framework 集成到你的游戏引擎中。
|
||||
|
||||
`Core.update(deltaTime)` 是整个框架的心跳,必须在游戏引擎的每一帧中调用。它负责:
|
||||
- 更新框架内置的 Time 类
|
||||
- 更新所有全局管理器(定时器、对象池等)
|
||||
- 更新所有场景中的实体系统
|
||||
- 处理实体的创建和销毁
|
||||
- 收集性能数据(调试模式下)
|
||||
|
||||
各引擎集成示例请参考:[与游戏引擎集成](#与游戏引擎集成)
|
||||
|
||||
## 创建第一个 ECS 应用
|
||||
|
||||
### 1. 定义组件
|
||||
|
||||
组件是纯数据容器,用于存储实体的状态:
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent } from '@esengine/ecs-framework'
|
||||
|
||||
// 位置组件
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
x: number = 0
|
||||
y: number = 0
|
||||
|
||||
constructor(x: number = 0, y: number = 0) {
|
||||
super()
|
||||
this.x = x
|
||||
this.y = y
|
||||
}
|
||||
}
|
||||
|
||||
// 速度组件
|
||||
@ECSComponent('Velocity')
|
||||
class Velocity extends Component {
|
||||
dx: number = 0
|
||||
dy: number = 0
|
||||
|
||||
constructor(dx: number = 0, dy: number = 0) {
|
||||
super()
|
||||
this.dx = dx
|
||||
this.dy = dy
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染组件
|
||||
@ECSComponent('Sprite')
|
||||
class Sprite extends Component {
|
||||
texture: string = ''
|
||||
width: number = 32
|
||||
height: number = 32
|
||||
|
||||
constructor(texture: string, width: number = 32, height: number = 32) {
|
||||
super()
|
||||
this.texture = texture
|
||||
this.width = width
|
||||
this.height = height
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 创建实体系统
|
||||
|
||||
系统包含游戏逻辑,处理具有特定组件的实体。ECS Framework 提供了基于 Matcher 的实体过滤机制:
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, Matcher, Time, ECSSystem } from '@esengine/ecs-framework'
|
||||
|
||||
// 移动系统 - 处理位置和速度
|
||||
@ECSSystem('MovementSystem')
|
||||
class MovementSystem extends EntitySystem {
|
||||
|
||||
constructor() {
|
||||
// 使用 Matcher 定义要处理的实体:必须同时拥有 Position 和 Velocity 组件
|
||||
super(Matcher.empty().all(Position, Velocity))
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// process 方法接收所有匹配的实体
|
||||
for (const entity of entities) {
|
||||
const position = entity.getComponent(Position)!
|
||||
const velocity = entity.getComponent(Velocity)!
|
||||
|
||||
// 更新位置(使用框架的Time类)
|
||||
position.x += velocity.dx * Time.deltaTime
|
||||
position.y += velocity.dy * Time.deltaTime
|
||||
|
||||
// 边界检查示例
|
||||
if (position.x < 0) position.x = 0
|
||||
if (position.y < 0) position.y = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染系统 - 处理可见对象
|
||||
@ECSSystem('RenderSystem')
|
||||
class RenderSystem extends EntitySystem {
|
||||
|
||||
constructor() {
|
||||
// 必须有 Position 和 Sprite,可选 Velocity(用于方向判断)
|
||||
super(Matcher.empty().all(Position, Sprite).any(Velocity))
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const position = entity.getComponent(Position)!
|
||||
const sprite = entity.getComponent(Sprite)!
|
||||
const velocity = entity.getComponent(Velocity) // 可能为 null
|
||||
|
||||
// 根据速度方向翻转精灵(可选逻辑)
|
||||
let flipX = false
|
||||
if (velocity && velocity.dx < 0) {
|
||||
flipX = true
|
||||
}
|
||||
|
||||
// 渲染逻辑(这里是伪代码)
|
||||
this.drawSprite(sprite.texture, position.x, position.y, sprite.width, sprite.height, flipX)
|
||||
}
|
||||
}
|
||||
|
||||
private drawSprite(texture: string, x: number, y: number, width: number, height: number, flipX: boolean = false) {
|
||||
// 实际的渲染实现将取决于你使用的游戏引擎
|
||||
const direction = flipX ? '←' : '→'
|
||||
console.log(`渲染 ${texture} 在位置 (${x.toFixed(1)}, ${y.toFixed(1)}) 方向: ${direction}`)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### 3. 创建场景
|
||||
|
||||
推荐继承 Scene 类来创建自定义场景:
|
||||
|
||||
```typescript
|
||||
import { Scene } from '@esengine/ecs-framework'
|
||||
|
||||
// 推荐:继承Scene创建自定义场景
|
||||
class GameScene extends Scene {
|
||||
|
||||
initialize(): void {
|
||||
// 场景初始化逻辑
|
||||
this.name = "MainScene";
|
||||
|
||||
// 添加系统到场景
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
}
|
||||
|
||||
onStart(): void {
|
||||
// 场景开始运行时的逻辑
|
||||
console.log("游戏场景已启动");
|
||||
}
|
||||
|
||||
unload(): void {
|
||||
// 场景卸载时的清理逻辑
|
||||
console.log("游戏场景已卸载");
|
||||
}
|
||||
}
|
||||
|
||||
// 创建并设置场景
|
||||
const gameScene = new GameScene();
|
||||
Core.setScene(gameScene);
|
||||
```
|
||||
|
||||
### 4. 创建实体
|
||||
|
||||
```typescript
|
||||
// 创建玩家实体
|
||||
const player = gameScene.createEntity("Player");
|
||||
player.addComponent(new Position(100, 100));
|
||||
player.addComponent(new Velocity(50, 30)); // 每秒移动 50 像素(x方向),30 像素(y方向)
|
||||
player.addComponent(new Sprite("player.png", 64, 64));
|
||||
```
|
||||
|
||||
## 场景管理
|
||||
|
||||
Core 内置了场景管理功能,使用非常简单:
|
||||
|
||||
```typescript
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始化Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 创建并设置场景
|
||||
class GameScene extends Scene {
|
||||
initialize(): void {
|
||||
this.name = "GamePlay";
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
}
|
||||
}
|
||||
|
||||
const gameScene = new GameScene();
|
||||
Core.setScene(gameScene);
|
||||
|
||||
// 游戏循环(自动更新场景)
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // 自动更新全局服务和场景
|
||||
}
|
||||
|
||||
// 切换场景
|
||||
Core.loadScene(new MenuScene()); // 延迟切换(下一帧)
|
||||
Core.setScene(new GameScene()); // 立即切换
|
||||
|
||||
// 访问当前场景
|
||||
const currentScene = Core.scene;
|
||||
|
||||
// 使用流式API
|
||||
const player = Core.ecsAPI?.createEntity('Player')
|
||||
.addComponent(Position, 100, 100)
|
||||
.addComponent(Velocity, 50, 0);
|
||||
```
|
||||
|
||||
### 高级:使用 WorldManager 管理多世界
|
||||
|
||||
仅适用于复杂的服务器端应用(MMO游戏服务器、游戏房间系统等):
|
||||
|
||||
```typescript
|
||||
import { Core, WorldManager } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始化Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 从服务容器获取 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 { Laya } from "Laya";
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始化 Laya
|
||||
Laya.init(800, 600).then(() => {
|
||||
// 初始化 ECS
|
||||
Core.create(true);
|
||||
Core.setScene(new GameScene());
|
||||
|
||||
// 启动游戏循环
|
||||
Laya.timer.frameLoop(1, this, () => {
|
||||
const deltaTime = Laya.timer.delta / 1000;
|
||||
Core.update(deltaTime); // 自动更新全局服务和场景
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Cocos Creator 集成
|
||||
|
||||
```typescript
|
||||
import { Component, _decorator } from 'cc';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
const { ccclass } = _decorator;
|
||||
|
||||
@ccclass('ECSGameManager')
|
||||
export class ECSGameManager extends Component {
|
||||
onLoad() {
|
||||
// 初始化 ECS
|
||||
Core.create(true);
|
||||
Core.setScene(new GameScene());
|
||||
}
|
||||
|
||||
update(deltaTime: number) {
|
||||
// 自动更新全局服务和场景
|
||||
Core.update(deltaTime);
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
// 清理资源
|
||||
Core.destroy();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 下一步
|
||||
|
||||
现在你已经成功创建了第一个 ECS 应用!接下来可以:
|
||||
|
||||
- 查看完整的 [API 文档](/api/README)
|
||||
- 探索更多[实际应用示例](/examples/)
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 为什么我的系统没有执行?
|
||||
|
||||
确保:
|
||||
1. 系统已添加到场景:`this.addSystem(system)` (在 Scene 的 initialize 方法中)
|
||||
2. 场景已设置:`Core.setScene(scene)`
|
||||
3. 游戏循环在调用:`Core.update(deltaTime)`
|
||||
|
||||
### 如何调试 ECS 应用?
|
||||
|
||||
启用调试模式:
|
||||
|
||||
```typescript
|
||||
Core.create({ debug: true })
|
||||
|
||||
// 获取调试数据
|
||||
const debugData = Core.getDebugData()
|
||||
console.log(debugData)
|
||||
```
|
||||
40
docs/guide/index.md
Normal file
40
docs/guide/index.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# 指南
|
||||
|
||||
欢迎使用 ECS Framework 指南。这里将详细介绍框架的各个核心概念和使用方法。
|
||||
|
||||
## 核心概念
|
||||
|
||||
### [实体类 (Entity)](./entity.md)
|
||||
了解 ECS 架构的基础 - 实体类的使用方法、生命周期管理和最佳实践。
|
||||
|
||||
### [组件系统 (Component)](./component.md)
|
||||
学习如何创建和使用组件,实现游戏功能的模块化设计。
|
||||
|
||||
### [系统架构 (System)](./system.md)
|
||||
掌握系统的编写方法,实现游戏逻辑的处理。
|
||||
|
||||
### [场景管理 (Scene)](./scene.md)
|
||||
了解场景的生命周期、系统管理和实体容器功能。
|
||||
|
||||
### [事件系统 (Event)](./event-system.md)
|
||||
掌握类型安全的事件系统,实现组件间通信和系统协作。
|
||||
|
||||
### [序列化系统 (Serialization)](./serialization.md)
|
||||
掌握场景、实体和组件的序列化方案,支持全量序列化和增量序列化,实现游戏存档、网络同步等功能。
|
||||
|
||||
### [时间和定时器 (Time)](./time-and-timers.md)
|
||||
学习时间管理和定时器系统,实现游戏逻辑的精确时间控制。
|
||||
|
||||
### [日志系统 (Logger)](./logging.md)
|
||||
掌握分级日志系统,用于调试、监控和错误追踪。
|
||||
|
||||
### [平台适配器 (Platform Adapter)](./platform-adapter.md)
|
||||
了解如何为不同平台实现和注册平台适配器,支持浏览器、小游戏、Node.js等环境。
|
||||
|
||||
## 高级特性
|
||||
|
||||
### [服务容器 (Service Container)](./service-container.md)
|
||||
掌握依赖注入和服务管理,实现松耦合的架构设计。
|
||||
|
||||
### [插件系统 (Plugin System)](./plugin-system.md)
|
||||
学习如何开发和使用插件,扩展框架功能,实现功能模块化。
|
||||
550
docs/guide/logging.md
Normal file
550
docs/guide/logging.md
Normal file
@@ -0,0 +1,550 @@
|
||||
# 日志系统
|
||||
|
||||
ECS 框架提供了功能强大的分级日志系统,支持多种日志级别、颜色输出、自定义前缀和灵活的配置选项。日志系统可以帮助开发者调试代码和监控应用运行状态。
|
||||
|
||||
## 基本概念
|
||||
|
||||
日志系统包含以下核心概念:
|
||||
- **日志级别**:Debug < Info < Warn < Error < Fatal < None
|
||||
- **日志器**:具名的日志输出器,每个模块可以有自己的日志器
|
||||
- **日志管理器**:全局管理所有日志器的单例
|
||||
- **颜色配置**:支持控制台颜色输出
|
||||
|
||||
## 日志级别
|
||||
|
||||
```typescript
|
||||
import { LogLevel } from '@esengine/ecs-framework';
|
||||
|
||||
// 日志级别从低到高
|
||||
LogLevel.Debug // 0 - 调试信息
|
||||
LogLevel.Info // 1 - 一般信息
|
||||
LogLevel.Warn // 2 - 警告信息
|
||||
LogLevel.Error // 3 - 错误信息
|
||||
LogLevel.Fatal // 4 - 致命错误
|
||||
LogLevel.None // 5 - 不输出任何日志
|
||||
```
|
||||
|
||||
## 基本使用
|
||||
|
||||
### 使用默认日志器
|
||||
|
||||
```typescript
|
||||
import { Logger } from '@esengine/ecs-framework';
|
||||
|
||||
class GameSystem extends EntitySystem {
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 输出不同级别的日志
|
||||
Logger.debug('处理实体数量:', entities.length);
|
||||
Logger.info('系统正常运行');
|
||||
Logger.warn('检测到性能问题');
|
||||
Logger.error('处理过程中发生错误', new Error('示例错误'));
|
||||
Logger.fatal('致命错误,系统即将停止');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 创建命名日志器
|
||||
|
||||
```typescript
|
||||
import { createLogger } from '@esengine/ecs-framework';
|
||||
|
||||
class MovementSystem extends EntitySystem {
|
||||
private logger = createLogger('MovementSystem');
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
this.logger.info(`处理 ${entities.length} 个移动实体`);
|
||||
|
||||
for (const entity of entities) {
|
||||
const position = entity.getComponent(Position);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
if (position && velocity) {
|
||||
position.x += velocity.dx * Time.deltaTime;
|
||||
position.y += velocity.dy * Time.deltaTime;
|
||||
|
||||
this.logger.debug(`实体 ${entity.id} 移动到位置 (${position.x}, ${position.y})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected onAdded(entity: Entity): void {
|
||||
this.logger.info(`实体 ${entity.name} 加入移动系统`);
|
||||
}
|
||||
|
||||
protected onRemoved(entity: Entity): void {
|
||||
this.logger.warn(`实体 ${entity.name} 离开移动系统`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 系统内置日志器
|
||||
|
||||
框架的各个系统都有自己的日志器:
|
||||
|
||||
```typescript
|
||||
// 框架内部使用示例
|
||||
class Scene {
|
||||
private static readonly _logger = createLogger('Scene');
|
||||
|
||||
public addSystem(system: EntitySystem): void {
|
||||
Scene._logger.info(`添加系统: ${system.systemName}`);
|
||||
// 系统添加逻辑
|
||||
}
|
||||
|
||||
public removeSystem(system: EntitySystem): void {
|
||||
Scene._logger.warn(`移除系统: ${system.systemName}`);
|
||||
// 系统移除逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 日志配置
|
||||
|
||||
### 设置全局日志级别
|
||||
|
||||
```typescript
|
||||
import { setGlobalLogLevel, LogLevel } from '@esengine/ecs-framework';
|
||||
|
||||
// 在开发环境显示所有日志
|
||||
setGlobalLogLevel(LogLevel.Debug);
|
||||
|
||||
// 在生产环境只显示警告及以上级别
|
||||
setGlobalLogLevel(LogLevel.Warn);
|
||||
|
||||
// 完全禁用日志输出
|
||||
setGlobalLogLevel(LogLevel.None);
|
||||
```
|
||||
|
||||
### 创建自定义配置的日志器
|
||||
|
||||
```typescript
|
||||
import { ConsoleLogger, LogLevel } from '@esengine/ecs-framework';
|
||||
|
||||
class CustomLoggerExample {
|
||||
private debugLogger: ConsoleLogger;
|
||||
private productionLogger: ConsoleLogger;
|
||||
|
||||
constructor() {
|
||||
// 开发环境日志器
|
||||
this.debugLogger = new ConsoleLogger({
|
||||
level: LogLevel.Debug,
|
||||
enableTimestamp: true,
|
||||
enableColors: true,
|
||||
prefix: 'DEV'
|
||||
});
|
||||
|
||||
// 生产环境日志器
|
||||
this.productionLogger = new ConsoleLogger({
|
||||
level: LogLevel.Error,
|
||||
enableTimestamp: true,
|
||||
enableColors: false,
|
||||
prefix: 'PROD'
|
||||
});
|
||||
}
|
||||
|
||||
public logDevelopmentInfo(): void {
|
||||
this.debugLogger.debug('这是调试信息');
|
||||
this.debugLogger.info('开发环境信息');
|
||||
}
|
||||
|
||||
public logProductionError(): void {
|
||||
this.productionLogger.error('生产环境错误');
|
||||
this.productionLogger.fatal('致命错误');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 颜色配置
|
||||
|
||||
### 使用预定义颜色
|
||||
|
||||
```typescript
|
||||
import { Colors, setLoggerColors } from '@esengine/ecs-framework';
|
||||
|
||||
// 自定义颜色方案
|
||||
setLoggerColors({
|
||||
debug: Colors.BRIGHT_BLACK,
|
||||
info: Colors.BLUE,
|
||||
warn: Colors.YELLOW,
|
||||
error: Colors.RED,
|
||||
fatal: Colors.BRIGHT_RED
|
||||
});
|
||||
```
|
||||
|
||||
### 完整颜色示例
|
||||
|
||||
```typescript
|
||||
import { LoggerManager, Colors, LogLevel } from '@esengine/ecs-framework';
|
||||
|
||||
class ColorLoggerDemo {
|
||||
private logger = createLogger('ColorDemo');
|
||||
|
||||
constructor() {
|
||||
// 设置自定义颜色
|
||||
const manager = LoggerManager.getInstance();
|
||||
manager.setGlobalColors({
|
||||
debug: Colors.CYAN,
|
||||
info: Colors.GREEN,
|
||||
warn: Colors.YELLOW,
|
||||
error: Colors.RED,
|
||||
fatal: `${Colors.BOLD}${Colors.BRIGHT_RED}`
|
||||
});
|
||||
}
|
||||
|
||||
public demonstrateColors(): void {
|
||||
this.logger.debug('这是蓝绿色的调试信息');
|
||||
this.logger.info('这是绿色的信息');
|
||||
this.logger.warn('这是黄色的警告');
|
||||
this.logger.error('这是红色的错误');
|
||||
this.logger.fatal('这是加粗的亮红色致命错误');
|
||||
}
|
||||
|
||||
public resetToDefaults(): void {
|
||||
// 重置为默认颜色
|
||||
LoggerManager.getInstance().resetColors();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 高级功能
|
||||
|
||||
### 分层日志器
|
||||
|
||||
```typescript
|
||||
import { LoggerManager } from '@esengine/ecs-framework';
|
||||
|
||||
class HierarchicalLoggingExample {
|
||||
private systemLogger = createLogger('GameSystems');
|
||||
private movementLogger: ILogger;
|
||||
private renderLogger: ILogger;
|
||||
|
||||
constructor() {
|
||||
const manager = LoggerManager.getInstance();
|
||||
|
||||
// 创建子日志器
|
||||
this.movementLogger = manager.createChildLogger('GameSystems', 'Movement');
|
||||
this.renderLogger = manager.createChildLogger('GameSystems', 'Render');
|
||||
}
|
||||
|
||||
public demonstrateHierarchy(): void {
|
||||
this.systemLogger.info('游戏系统启动');
|
||||
|
||||
// 子日志器会显示完整路径:[GameSystems.Movement]
|
||||
this.movementLogger.debug('移动系统初始化');
|
||||
|
||||
// 子日志器会显示完整路径:[GameSystems.Render]
|
||||
this.renderLogger.info('渲染系统启动');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 自定义输出
|
||||
|
||||
```typescript
|
||||
import { ConsoleLogger, LogLevel } from '@esengine/ecs-framework';
|
||||
|
||||
class CustomOutputLogger {
|
||||
private fileLogger: ConsoleLogger;
|
||||
private networkLogger: ConsoleLogger;
|
||||
|
||||
constructor() {
|
||||
// 输出到文件的日志器(模拟)
|
||||
this.fileLogger = new ConsoleLogger({
|
||||
level: LogLevel.Info,
|
||||
output: (level: LogLevel, message: string) => {
|
||||
this.writeToFile(LogLevel[level], message);
|
||||
}
|
||||
});
|
||||
|
||||
// 发送到网络的日志器(模拟)
|
||||
this.networkLogger = new ConsoleLogger({
|
||||
level: LogLevel.Error,
|
||||
output: (level: LogLevel, message: string) => {
|
||||
this.sendToServer(LogLevel[level], message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private writeToFile(level: string, message: string): void {
|
||||
// 模拟文件写入
|
||||
console.log(`[FILE] ${level}: ${message}`);
|
||||
}
|
||||
|
||||
private sendToServer(level: string, message: string): void {
|
||||
// 模拟网络发送
|
||||
console.log(`[NETWORK] ${level}: ${message}`);
|
||||
}
|
||||
|
||||
public logToFile(message: string): void {
|
||||
this.fileLogger.info(message);
|
||||
}
|
||||
|
||||
public logCriticalError(error: Error): void {
|
||||
this.networkLogger.error('Critical error occurred', error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 实际应用示例
|
||||
|
||||
### 游戏系统日志
|
||||
|
||||
```typescript
|
||||
class GameWithLogging {
|
||||
private gameLogger = createLogger('Game');
|
||||
private performanceLogger = createLogger('Performance');
|
||||
private networkLogger = createLogger('Network');
|
||||
|
||||
constructor() {
|
||||
// 在开发环境启用详细日志
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
setGlobalLogLevel(LogLevel.Debug);
|
||||
} else {
|
||||
setGlobalLogLevel(LogLevel.Warn);
|
||||
}
|
||||
}
|
||||
|
||||
public startGame(): void {
|
||||
this.gameLogger.info('游戏开始启动');
|
||||
|
||||
try {
|
||||
this.initializeSystems();
|
||||
this.loadResources();
|
||||
this.startGameLoop();
|
||||
|
||||
this.gameLogger.info('游戏启动成功');
|
||||
} catch (error) {
|
||||
this.gameLogger.fatal('游戏启动失败', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private initializeSystems(): void {
|
||||
this.gameLogger.debug('初始化游戏系统');
|
||||
|
||||
const systems = [
|
||||
new MovementSystem(),
|
||||
new RenderSystem(),
|
||||
new PhysicsSystem()
|
||||
];
|
||||
|
||||
for (const system of systems) {
|
||||
const startTime = performance.now();
|
||||
|
||||
// 初始化系统
|
||||
system.initialize();
|
||||
|
||||
const endTime = performance.now();
|
||||
this.performanceLogger.debug(
|
||||
`系统 ${system.systemName} 初始化耗时: ${(endTime - startTime).toFixed(2)}ms`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private loadResources(): void {
|
||||
this.gameLogger.info('开始加载资源');
|
||||
|
||||
const resources = ['textures', 'sounds', 'data'];
|
||||
for (const resource of resources) {
|
||||
try {
|
||||
this.loadResource(resource);
|
||||
this.gameLogger.debug(`资源 ${resource} 加载成功`);
|
||||
} catch (error) {
|
||||
this.gameLogger.error(`资源 ${resource} 加载失败`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private startGameLoop(): void {
|
||||
this.gameLogger.info('启动游戏循环');
|
||||
this.performanceLogger.debug('开始性能监控');
|
||||
}
|
||||
|
||||
private loadResource(name: string): void {
|
||||
// 模拟资源加载
|
||||
if (Math.random() < 0.1) {
|
||||
throw new Error(`Failed to load ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
public handleNetworkEvent(event: string, data: any): void {
|
||||
this.networkLogger.info(`网络事件: ${event}`, data);
|
||||
|
||||
if (event === 'connection_lost') {
|
||||
this.networkLogger.warn('网络连接丢失,尝试重连');
|
||||
} else if (event === 'sync_error') {
|
||||
this.networkLogger.error('数据同步错误', data);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 错误追踪和调试
|
||||
|
||||
```typescript
|
||||
class ErrorTrackingSystem extends EntitySystem {
|
||||
private logger = createLogger('ErrorTracker');
|
||||
private errorCounts = new Map<string, number>();
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
try {
|
||||
this.processEntity(entity);
|
||||
} catch (error) {
|
||||
this.handleError(entity, error as Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private processEntity(entity: Entity): void {
|
||||
// 模拟可能出错的实体处理
|
||||
if (Math.random() < 0.01) { // 1% 概率出错
|
||||
throw new Error(`Processing error for entity ${entity.id}`);
|
||||
}
|
||||
|
||||
this.logger.debug(`成功处理实体 ${entity.id}`);
|
||||
}
|
||||
|
||||
private handleError(entity: Entity, error: Error): void {
|
||||
const errorKey = error.message;
|
||||
const count = this.errorCounts.get(errorKey) || 0;
|
||||
this.errorCounts.set(errorKey, count + 1);
|
||||
|
||||
this.logger.error(
|
||||
`实体 ${entity.id} 处理失败 (第${count + 1}次): ${error.message}`,
|
||||
{
|
||||
entityId: entity.id,
|
||||
entityName: entity.name,
|
||||
componentCount: entity.components.length,
|
||||
errorStack: error.stack
|
||||
}
|
||||
);
|
||||
|
||||
// 如果同一类型错误发生太多次,升级为警告
|
||||
if (count >= 5) {
|
||||
this.logger.warn(`错误 "${errorKey}" 已发生 ${count + 1} 次,可能需要关注`);
|
||||
}
|
||||
|
||||
// 如果错误次数过多,升级为致命错误
|
||||
if (count >= 20) {
|
||||
this.logger.fatal(`错误 "${errorKey}" 发生次数过多,系统可能存在严重问题`);
|
||||
}
|
||||
}
|
||||
|
||||
public getErrorSummary(): void {
|
||||
this.logger.info('=== 错误统计 ===');
|
||||
for (const [error, count] of this.errorCounts) {
|
||||
this.logger.info(`${error}: ${count} 次`);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 合理的日志级别选择
|
||||
|
||||
```typescript
|
||||
class LoggingBestPractices {
|
||||
private logger = createLogger('BestPractices');
|
||||
|
||||
public demonstrateLogLevels(): void {
|
||||
// ✅ Debug - 详细的调试信息
|
||||
this.logger.debug('变量值', { x: 10, y: 20 });
|
||||
|
||||
// ✅ Info - 重要的状态变化
|
||||
this.logger.info('系统启动完成');
|
||||
|
||||
// ✅ Warn - 异常但不致命的情况
|
||||
this.logger.warn('资源未找到,使用默认值');
|
||||
|
||||
// ✅ Error - 错误但程序可以继续
|
||||
this.logger.error('保存失败,将重试', new Error('Network timeout'));
|
||||
|
||||
// ✅ Fatal - 致命错误,程序无法继续
|
||||
this.logger.fatal('内存不足,程序即将退出');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 结构化日志数据
|
||||
|
||||
```typescript
|
||||
class StructuredLogging {
|
||||
private logger = createLogger('Structured');
|
||||
|
||||
public logWithStructuredData(): void {
|
||||
// ✅ 提供结构化的上下文信息
|
||||
this.logger.info('用户操作', {
|
||||
userId: 12345,
|
||||
action: 'move',
|
||||
position: { x: 100, y: 200 },
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
// ✅ 包含相关的错误上下文
|
||||
this.logger.error('数据库查询失败', {
|
||||
query: 'SELECT * FROM users',
|
||||
parameters: { id: 123 },
|
||||
connectionId: 'conn_456',
|
||||
retryCount: 3
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 避免日志性能问题
|
||||
|
||||
```typescript
|
||||
class PerformanceConsciousLogging {
|
||||
private logger = createLogger('Performance');
|
||||
|
||||
public efficientLogging(): void {
|
||||
// ✅ 检查日志级别避免不必要的计算
|
||||
if (this.logger.debug) {
|
||||
const expensiveData = this.calculateExpensiveDebugInfo();
|
||||
this.logger.debug('详细调试信息', expensiveData);
|
||||
}
|
||||
|
||||
// ❌ 避免:总是计算昂贵的日志数据
|
||||
// this.logger.debug('调试信息', this.calculateExpensiveDebugInfo());
|
||||
}
|
||||
|
||||
private calculateExpensiveDebugInfo(): any {
|
||||
// 模拟昂贵的计算
|
||||
return { /* 复杂的调试数据 */ };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 日志配置管理
|
||||
|
||||
```typescript
|
||||
class LoggingConfiguration {
|
||||
public static setupLogging(): void {
|
||||
// 根据环境配置日志级别
|
||||
const isDevelopment = process.env.NODE_ENV === 'development';
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
if (isDevelopment) {
|
||||
setGlobalLogLevel(LogLevel.Debug);
|
||||
setLoggerColors({
|
||||
debug: Colors.CYAN,
|
||||
info: Colors.GREEN,
|
||||
warn: Colors.YELLOW,
|
||||
error: Colors.RED,
|
||||
fatal: Colors.BRIGHT_RED
|
||||
});
|
||||
} else if (isProduction) {
|
||||
setGlobalLogLevel(LogLevel.Warn);
|
||||
// 生产环境禁用颜色
|
||||
LoggerManager.getInstance().resetColors();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 在应用启动时配置日志
|
||||
LoggingConfiguration.setupLogging();
|
||||
```
|
||||
|
||||
日志系统是调试和监控应用的重要工具,正确使用日志系统能大大提高开发效率和问题排查能力。
|
||||
289
docs/guide/platform-adapter.md
Normal file
289
docs/guide/platform-adapter.md
Normal file
@@ -0,0 +1,289 @@
|
||||
# 平台适配器
|
||||
|
||||
## 概述
|
||||
|
||||
ECS框架提供了平台适配器接口,允许用户为不同的运行环境实现自定义的平台适配器。
|
||||
|
||||
**核心库只提供接口定义,平台适配器实现代码请从文档中复制使用。**
|
||||
|
||||
## 为什么不提供单独的适配器包?
|
||||
|
||||
1. **灵活性**: 不同项目对平台适配的需求可能不同,复制代码可以让用户根据需要自由修改
|
||||
2. **减少依赖**: 避免引入不必要的依赖包,保持核心框架的精简
|
||||
3. **定制化**: 用户可以根据具体的运行环境和需求进行定制
|
||||
|
||||
## 支持的平台
|
||||
|
||||
### 🌐 [浏览器适配器](./platform-adapter/browser.md)
|
||||
|
||||
支持所有现代浏览器环境,包括 Chrome、Firefox、Safari、Edge 等。
|
||||
|
||||
**特性支持**:
|
||||
- ✅ Worker (Web Worker)
|
||||
- ✅ SharedArrayBuffer (需要COOP/COEP)
|
||||
- ✅ Transferable Objects
|
||||
- ✅ Module Worker (现代浏览器)
|
||||
|
||||
**适用场景**: Web游戏、Web应用、PWA
|
||||
|
||||
---
|
||||
|
||||
### 📱 [微信小游戏适配器](./platform-adapter/wechat-minigame.md)
|
||||
|
||||
专为微信小游戏环境设计,处理微信小游戏的特殊限制和API。
|
||||
|
||||
**特性支持**:
|
||||
- ✅ Worker (最多1个,需配置game.json)
|
||||
- ❌ SharedArrayBuffer
|
||||
- ❌ Transferable Objects
|
||||
- ✅ 微信设备信息API
|
||||
|
||||
**适用场景**: 微信小游戏开发
|
||||
|
||||
---
|
||||
|
||||
### 🖥️ [Node.js适配器](./platform-adapter/nodejs.md)
|
||||
|
||||
为 Node.js 服务器环境提供支持,适用于游戏服务器和计算服务器。
|
||||
|
||||
**特性支持**:
|
||||
- ✅ Worker Threads
|
||||
- ✅ SharedArrayBuffer
|
||||
- ✅ Transferable Objects
|
||||
- ✅ 完整系统信息
|
||||
|
||||
**适用场景**: 游戏服务器、计算服务器、CLI工具
|
||||
|
||||
---
|
||||
|
||||
## 核心接口
|
||||
|
||||
### IPlatformAdapter
|
||||
|
||||
```typescript
|
||||
export interface IPlatformAdapter {
|
||||
readonly name: string;
|
||||
readonly version?: string;
|
||||
|
||||
isWorkerSupported(): boolean;
|
||||
isSharedArrayBufferSupported(): boolean;
|
||||
getHardwareConcurrency(): number;
|
||||
createWorker(script: string, options?: WorkerCreationOptions): PlatformWorker;
|
||||
createSharedArrayBuffer(length: number): SharedArrayBuffer | null;
|
||||
getHighResTimestamp(): number;
|
||||
getPlatformConfig(): PlatformConfig;
|
||||
getPlatformConfigAsync?(): Promise<PlatformConfig>;
|
||||
}
|
||||
```
|
||||
|
||||
### PlatformWorker 接口
|
||||
|
||||
```typescript
|
||||
export interface PlatformWorker {
|
||||
postMessage(message: any, transfer?: Transferable[]): void;
|
||||
onMessage(handler: (event: { data: any }) => void): void;
|
||||
onError(handler: (error: ErrorEvent) => void): void;
|
||||
terminate(): void;
|
||||
readonly state: 'running' | 'terminated';
|
||||
}
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 选择合适的平台适配器
|
||||
|
||||
根据你的运行环境选择对应的适配器:
|
||||
|
||||
```typescript
|
||||
import { PlatformManager } from '@esengine/ecs-framework';
|
||||
|
||||
// 浏览器环境
|
||||
if (typeof window !== 'undefined') {
|
||||
const { BrowserAdapter } = await import('./platform/BrowserAdapter');
|
||||
PlatformManager.getInstance().registerAdapter(new BrowserAdapter());
|
||||
}
|
||||
|
||||
// 微信小游戏环境
|
||||
else if (typeof wx !== 'undefined') {
|
||||
const { WeChatMiniGameAdapter } = await import('./platform/WeChatMiniGameAdapter');
|
||||
PlatformManager.getInstance().registerAdapter(new WeChatMiniGameAdapter());
|
||||
}
|
||||
|
||||
// Node.js环境
|
||||
else if (typeof process !== 'undefined' && process.versions?.node) {
|
||||
const { NodeAdapter } = await import('./platform/NodeAdapter');
|
||||
PlatformManager.getInstance().registerAdapter(new NodeAdapter());
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 检查适配器状态
|
||||
|
||||
```typescript
|
||||
const manager = PlatformManager.getInstance();
|
||||
|
||||
// 检查是否已注册适配器
|
||||
if (manager.hasAdapter()) {
|
||||
const adapter = manager.getAdapter();
|
||||
console.log('当前平台:', adapter.name);
|
||||
console.log('平台版本:', adapter.version);
|
||||
|
||||
// 检查功能支持
|
||||
console.log('Worker支持:', manager.supportsFeature('worker'));
|
||||
console.log('SharedArrayBuffer支持:', manager.supportsFeature('shared-array-buffer'));
|
||||
}
|
||||
```
|
||||
|
||||
## 创建自定义适配器
|
||||
|
||||
如果现有的平台适配器不能满足你的需求,你可以创建自定义适配器:
|
||||
|
||||
### 1. 实现接口
|
||||
|
||||
```typescript
|
||||
import type { IPlatformAdapter, PlatformWorker, WorkerCreationOptions, PlatformConfig } from '@esengine/ecs-framework';
|
||||
|
||||
export class CustomAdapter implements IPlatformAdapter {
|
||||
public readonly name = 'custom';
|
||||
public readonly version = '1.0.0';
|
||||
|
||||
public isWorkerSupported(): boolean {
|
||||
// 实现你的 Worker 支持检查逻辑
|
||||
return false;
|
||||
}
|
||||
|
||||
public isSharedArrayBufferSupported(): boolean {
|
||||
// 实现你的 SharedArrayBuffer 支持检查逻辑
|
||||
return false;
|
||||
}
|
||||
|
||||
public getHardwareConcurrency(): number {
|
||||
// 返回你的平台的并发数
|
||||
return 1;
|
||||
}
|
||||
|
||||
public createWorker(script: string, options?: WorkerCreationOptions): PlatformWorker {
|
||||
throw new Error('Worker not supported on this platform');
|
||||
}
|
||||
|
||||
public createSharedArrayBuffer(length: number): SharedArrayBuffer | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
public getHighResTimestamp(): number {
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
public getPlatformConfig(): PlatformConfig {
|
||||
return {
|
||||
maxWorkerCount: 1,
|
||||
supportsModuleWorker: false,
|
||||
supportsTransferableObjects: false,
|
||||
limitations: {
|
||||
workerNotSupported: true
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 注册自定义适配器
|
||||
|
||||
```typescript
|
||||
import { PlatformManager } from '@esengine/ecs-framework';
|
||||
import { CustomAdapter } from './CustomAdapter';
|
||||
|
||||
const customAdapter = new CustomAdapter();
|
||||
PlatformManager.getInstance().registerAdapter(customAdapter);
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 平台检测顺序
|
||||
|
||||
建议按照以下顺序检测和注册平台适配器:
|
||||
|
||||
```typescript
|
||||
async function initializePlatform() {
|
||||
const manager = PlatformManager.getInstance();
|
||||
|
||||
try {
|
||||
// 1. 微信小游戏 (优先级最高,环境特征最明显)
|
||||
if (typeof wx !== 'undefined' && wx.getSystemInfoSync) {
|
||||
const { WeChatMiniGameAdapter } = await import('./platform/WeChatMiniGameAdapter');
|
||||
manager.registerAdapter(new WeChatMiniGameAdapter());
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Node.js 环境
|
||||
if (typeof process !== 'undefined' && process.versions?.node) {
|
||||
const { NodeAdapter } = await import('./platform/NodeAdapter');
|
||||
manager.registerAdapter(new NodeAdapter());
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 浏览器环境 (最后检测,覆盖面最广)
|
||||
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
|
||||
const { BrowserAdapter } = await import('./platform/BrowserAdapter');
|
||||
manager.registerAdapter(new BrowserAdapter());
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. 未知环境,使用默认适配器
|
||||
console.warn('未识别的平台环境,使用默认适配器');
|
||||
manager.registerAdapter(new CustomAdapter());
|
||||
|
||||
} catch (error) {
|
||||
console.error('平台适配器初始化失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 功能降级处理
|
||||
|
||||
```typescript
|
||||
function createWorkerSystem() {
|
||||
const manager = PlatformManager.getInstance();
|
||||
|
||||
if (!manager.hasAdapter()) {
|
||||
throw new Error('未注册平台适配器');
|
||||
}
|
||||
|
||||
const config: WorkerSystemConfig = {
|
||||
enableWorker: manager.supportsFeature('worker'),
|
||||
workerCount: manager.supportsFeature('worker') ?
|
||||
manager.getAdapter().getHardwareConcurrency() : 1,
|
||||
useSharedArrayBuffer: manager.supportsFeature('shared-array-buffer')
|
||||
};
|
||||
|
||||
// 如果不支持Worker,自动降级到同步处理
|
||||
if (!config.enableWorker) {
|
||||
console.info('当前平台不支持Worker,使用同步处理模式');
|
||||
}
|
||||
|
||||
return new PhysicsSystem(config);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 错误处理
|
||||
|
||||
```typescript
|
||||
try {
|
||||
await initializePlatform();
|
||||
|
||||
// 验证适配器功能
|
||||
const manager = PlatformManager.getInstance();
|
||||
const adapter = manager.getAdapter();
|
||||
|
||||
console.log(`平台适配器初始化成功: ${adapter.name} v${adapter.version}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('平台初始化失败:', error);
|
||||
|
||||
// 提供降级方案
|
||||
const fallbackAdapter = new CustomAdapter();
|
||||
PlatformManager.getInstance().registerAdapter(fallbackAdapter);
|
||||
|
||||
console.warn('使用降级适配器继续运行');
|
||||
}
|
||||
```
|
||||
370
docs/guide/platform-adapter/browser.md
Normal file
370
docs/guide/platform-adapter/browser.md
Normal file
@@ -0,0 +1,370 @@
|
||||
# 浏览器适配器
|
||||
|
||||
## 概述
|
||||
|
||||
浏览器平台适配器为标准Web浏览器环境提供支持,包括 Chrome、Firefox、Safari、Edge 等现代浏览器。
|
||||
|
||||
## 特性支持
|
||||
|
||||
- ✅ **Worker**: 支持 Web Worker 和 Module Worker
|
||||
- ✅ **SharedArrayBuffer**: 支持(需要COOP/COEP头部)
|
||||
- ✅ **Transferable Objects**: 完全支持
|
||||
- ✅ **高精度时间**: 使用 `performance.now()`
|
||||
- ✅ **基础信息**: 浏览器版本和基本配置
|
||||
|
||||
## 完整实现
|
||||
|
||||
```typescript
|
||||
import type {
|
||||
IPlatformAdapter,
|
||||
PlatformWorker,
|
||||
WorkerCreationOptions,
|
||||
PlatformConfig
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* 浏览器平台适配器
|
||||
* 支持标准Web浏览器环境
|
||||
*/
|
||||
export class BrowserAdapter implements IPlatformAdapter {
|
||||
public readonly name = 'browser';
|
||||
public readonly version: string;
|
||||
|
||||
constructor() {
|
||||
this.version = this.getBrowserInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否支持Worker
|
||||
*/
|
||||
public isWorkerSupported(): boolean {
|
||||
return typeof Worker !== 'undefined';
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否支持SharedArrayBuffer
|
||||
*/
|
||||
public isSharedArrayBufferSupported(): boolean {
|
||||
return typeof SharedArrayBuffer !== 'undefined' && this.checkSharedArrayBufferEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取硬件并发数(CPU核心数)
|
||||
*/
|
||||
public getHardwareConcurrency(): number {
|
||||
return navigator.hardwareConcurrency || 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建Worker
|
||||
*/
|
||||
public createWorker(script: string, options: WorkerCreationOptions = {}): PlatformWorker {
|
||||
if (!this.isWorkerSupported()) {
|
||||
throw new Error('浏览器不支持Worker');
|
||||
}
|
||||
|
||||
try {
|
||||
return new BrowserWorker(script, options);
|
||||
} catch (error) {
|
||||
throw new Error(`创建浏览器Worker失败: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建SharedArrayBuffer
|
||||
*/
|
||||
public createSharedArrayBuffer(length: number): SharedArrayBuffer | null {
|
||||
if (!this.isSharedArrayBufferSupported()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return new SharedArrayBuffer(length);
|
||||
} catch (error) {
|
||||
console.warn('SharedArrayBuffer创建失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取高精度时间戳
|
||||
*/
|
||||
public getHighResTimestamp(): number {
|
||||
return performance.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取平台配置
|
||||
*/
|
||||
public getPlatformConfig(): PlatformConfig {
|
||||
return {
|
||||
maxWorkerCount: this.getHardwareConcurrency(),
|
||||
supportsModuleWorker: false,
|
||||
supportsTransferableObjects: true,
|
||||
maxSharedArrayBufferSize: 1024 * 1024 * 1024, // 1GB
|
||||
workerScriptPrefix: '',
|
||||
limitations: {
|
||||
noEval: false,
|
||||
requiresWorkerInit: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取浏览器信息
|
||||
*/
|
||||
private getBrowserInfo(): string {
|
||||
const userAgent = navigator.userAgent;
|
||||
if (userAgent.includes('Chrome')) {
|
||||
const match = userAgent.match(/Chrome\/([0-9.]+)/);
|
||||
return match ? `Chrome ${match[1]}` : 'Chrome';
|
||||
} else if (userAgent.includes('Firefox')) {
|
||||
const match = userAgent.match(/Firefox\/([0-9.]+)/);
|
||||
if (match) return `Firefox ${match[1]}`;
|
||||
} else if (userAgent.includes('Safari')) {
|
||||
const match = userAgent.match(/Version\/([0-9.]+)/);
|
||||
if (match) return `Safari ${match[1]}`;
|
||||
}
|
||||
return 'Unknown Browser';
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查SharedArrayBuffer是否真正可用
|
||||
*/
|
||||
private checkSharedArrayBufferEnabled(): boolean {
|
||||
try {
|
||||
new SharedArrayBuffer(8);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 浏览器Worker封装
|
||||
*/
|
||||
class BrowserWorker implements PlatformWorker {
|
||||
private _state: 'running' | 'terminated' = 'running';
|
||||
private worker: Worker;
|
||||
|
||||
constructor(script: string, options: WorkerCreationOptions = {}) {
|
||||
this.worker = this.createBrowserWorker(script, options);
|
||||
}
|
||||
|
||||
public get state(): 'running' | 'terminated' {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
public postMessage(message: any, transfer?: Transferable[]): void {
|
||||
if (this._state === 'terminated') {
|
||||
throw new Error('Worker已被终止');
|
||||
}
|
||||
|
||||
try {
|
||||
if (transfer && transfer.length > 0) {
|
||||
this.worker.postMessage(message, transfer);
|
||||
} else {
|
||||
this.worker.postMessage(message);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`发送消息到Worker失败: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public onMessage(handler: (event: { data: any }) => void): void {
|
||||
this.worker.onmessage = (event: MessageEvent) => {
|
||||
handler({ data: event.data });
|
||||
};
|
||||
}
|
||||
|
||||
public onError(handler: (error: ErrorEvent) => void): void {
|
||||
this.worker.onerror = handler;
|
||||
}
|
||||
|
||||
public terminate(): void {
|
||||
if (this._state === 'running') {
|
||||
try {
|
||||
this.worker.terminate();
|
||||
this._state = 'terminated';
|
||||
} catch (error) {
|
||||
console.error('终止Worker失败:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建浏览器Worker
|
||||
*/
|
||||
private createBrowserWorker(script: string, options: WorkerCreationOptions): Worker {
|
||||
try {
|
||||
// 创建Blob URL
|
||||
const blob = new Blob([script], { type: 'application/javascript' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
// 创建Worker
|
||||
const worker = new Worker(url, {
|
||||
type: options.type || 'classic',
|
||||
credentials: options.credentials,
|
||||
name: options.name
|
||||
});
|
||||
|
||||
// 清理Blob URL(延迟清理,确保Worker已加载)
|
||||
setTimeout(() => {
|
||||
URL.revokeObjectURL(url);
|
||||
}, 1000);
|
||||
|
||||
return worker;
|
||||
} catch (error) {
|
||||
throw new Error(`无法创建浏览器Worker: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 复制代码
|
||||
|
||||
将上述代码复制到你的项目中,例如 `src/platform/BrowserAdapter.ts`。
|
||||
|
||||
### 2. 注册适配器
|
||||
|
||||
```typescript
|
||||
import { PlatformManager } from '@esengine/ecs-framework';
|
||||
import { BrowserAdapter } from './platform/BrowserAdapter';
|
||||
|
||||
// 创建并注册浏览器适配器
|
||||
const browserAdapter = new BrowserAdapter();
|
||||
PlatformManager.registerAdapter(browserAdapter);
|
||||
|
||||
// 框架会自动检测和使用合适的适配器
|
||||
```
|
||||
|
||||
### 3. 使用 WorkerEntitySystem
|
||||
|
||||
浏览器适配器与 WorkerEntitySystem 配合使用,框架会自动处理 Worker 脚本的创建:
|
||||
|
||||
```typescript
|
||||
import { WorkerEntitySystem, Matcher } from '@esengine/ecs-framework';
|
||||
|
||||
class PhysicsSystem extends WorkerEntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Transform, Velocity), {
|
||||
enableWorker: true,
|
||||
workerCount: navigator.hardwareConcurrency || 4,
|
||||
useSharedArrayBuffer: true,
|
||||
systemConfig: { gravity: 9.8 }
|
||||
});
|
||||
}
|
||||
|
||||
protected getDefaultEntityDataSize(): number {
|
||||
return 6; // x, y, vx, vy, mass, radius
|
||||
}
|
||||
|
||||
protected extractEntityData(entity: Entity): PhysicsData {
|
||||
const transform = entity.getComponent(Transform);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
return {
|
||||
x: transform.x,
|
||||
y: transform.y,
|
||||
vx: velocity.x,
|
||||
vy: velocity.y,
|
||||
mass: 1,
|
||||
radius: 10
|
||||
};
|
||||
}
|
||||
|
||||
// 这个函数会被自动序列化并在Worker中执行
|
||||
protected workerProcess(entities, deltaTime, config) {
|
||||
return entities.map(entity => {
|
||||
// 应用重力
|
||||
entity.vy += config.gravity * deltaTime;
|
||||
|
||||
// 更新位置
|
||||
entity.x += entity.vx * deltaTime;
|
||||
entity.y += entity.vy * deltaTime;
|
||||
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
protected applyResult(entity: Entity, result: PhysicsData): void {
|
||||
const transform = entity.getComponent(Transform);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
transform.x = result.x;
|
||||
transform.y = result.y;
|
||||
velocity.x = result.vx;
|
||||
velocity.y = result.vy;
|
||||
}
|
||||
}
|
||||
|
||||
interface PhysicsData {
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
mass: number;
|
||||
radius: number;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 验证适配器工作状态
|
||||
|
||||
```typescript
|
||||
// 验证适配器是否正常工作
|
||||
const adapter = new BrowserAdapter();
|
||||
console.log('适配器名称:', adapter.name);
|
||||
console.log('浏览器版本:', adapter.version);
|
||||
console.log('Worker支持:', adapter.isWorkerSupported());
|
||||
console.log('SharedArrayBuffer支持:', adapter.isSharedArrayBufferSupported());
|
||||
console.log('CPU核心数:', adapter.getHardwareConcurrency());
|
||||
```
|
||||
|
||||
## 重要注意事项
|
||||
|
||||
### SharedArrayBuffer 支持
|
||||
|
||||
SharedArrayBuffer 需要特殊的安全配置:
|
||||
|
||||
1. **HTTPS**: 必须在安全上下文中使用
|
||||
2. **COOP/COEP 头部**: 需要设置正确的跨域隔离头部
|
||||
|
||||
```html
|
||||
<!-- 在 HTML 中设置 -->
|
||||
<meta http-equiv="Cross-Origin-Opener-Policy" content="same-origin">
|
||||
<meta http-equiv="Cross-Origin-Embedder-Policy" content="require-corp">
|
||||
```
|
||||
|
||||
或在服务器配置中设置:
|
||||
|
||||
```
|
||||
Cross-Origin-Opener-Policy: same-origin
|
||||
Cross-Origin-Embedder-Policy: require-corp
|
||||
```
|
||||
|
||||
### 浏览器兼容性
|
||||
|
||||
- **Worker**: 所有现代浏览器支持
|
||||
- **Module Worker**: Chrome 80+, Firefox 114+
|
||||
- **SharedArrayBuffer**: Chrome 68+, Firefox 79+(需要COOP/COEP)
|
||||
- **Transferable Objects**: 所有现代浏览器支持
|
||||
|
||||
## 性能优化建议
|
||||
|
||||
1. **Worker 池**: 复用 Worker 实例,避免频繁创建和销毁
|
||||
2. **数据传输**: 使用 Transferable Objects 减少数据拷贝
|
||||
3. **SharedArrayBuffer**: 对于大量数据共享,使用 SharedArrayBuffer
|
||||
4. **模块 Worker**: 在支持的浏览器中使用模块 Worker 来更好地组织代码
|
||||
|
||||
## 调试技巧
|
||||
|
||||
```typescript
|
||||
// 检查浏览器支持情况
|
||||
const adapter = new BrowserAdapter();
|
||||
console.log('Worker支持:', adapter.isWorkerSupported());
|
||||
console.log('SharedArrayBuffer支持:', adapter.isSharedArrayBufferSupported());
|
||||
console.log('硬件并发数:', adapter.getHardwareConcurrency());
|
||||
console.log('平台配置:', adapter.getPlatformConfig());
|
||||
```
|
||||
558
docs/guide/platform-adapter/nodejs.md
Normal file
558
docs/guide/platform-adapter/nodejs.md
Normal file
@@ -0,0 +1,558 @@
|
||||
# Node.js 适配器
|
||||
|
||||
## 概述
|
||||
|
||||
Node.js 平台适配器为 Node.js 服务器环境提供支持,适用于游戏服务器、计算服务器或其他需要 ECS 架构的服务器应用。
|
||||
|
||||
## 特性支持
|
||||
|
||||
- ✅ **Worker**: 支持(通过 `worker_threads` 模块)
|
||||
- ❌ **SharedArrayBuffer**: 支持(Node.js 16.17.0+)
|
||||
- ✅ **Transferable Objects**: 完全支持
|
||||
- ✅ **高精度时间**: 使用 `process.hrtime.bigint()`
|
||||
- ✅ **设备信息**: 完整的系统和进程信息
|
||||
|
||||
## 完整实现
|
||||
|
||||
```typescript
|
||||
import { worker_threads, Worker, isMainThread, parentPort } from 'worker_threads';
|
||||
import * as os from 'os';
|
||||
import * as process from 'process';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import type {
|
||||
IPlatformAdapter,
|
||||
PlatformWorker,
|
||||
WorkerCreationOptions,
|
||||
PlatformConfig,
|
||||
NodeDeviceInfo
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* Node.js 平台适配器
|
||||
* 支持 Node.js 服务器环境
|
||||
*/
|
||||
export class NodeAdapter implements IPlatformAdapter {
|
||||
public readonly name = 'nodejs';
|
||||
public readonly version: string;
|
||||
|
||||
constructor() {
|
||||
this.version = process.version;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否支持Worker
|
||||
*/
|
||||
public isWorkerSupported(): boolean {
|
||||
try {
|
||||
// 检查 worker_threads 模块是否可用
|
||||
return typeof worker_threads !== 'undefined' && typeof Worker !== 'undefined';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否支持SharedArrayBuffer
|
||||
*/
|
||||
public isSharedArrayBufferSupported(): boolean {
|
||||
// Node.js 支持 SharedArrayBuffer
|
||||
return typeof SharedArrayBuffer !== 'undefined';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取硬件并发数(CPU核心数)
|
||||
*/
|
||||
public getHardwareConcurrency(): number {
|
||||
return os.cpus().length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建Worker
|
||||
*/
|
||||
public createWorker(script: string, options: WorkerCreationOptions = {}): PlatformWorker {
|
||||
if (!this.isWorkerSupported()) {
|
||||
throw new Error('Node.js环境不支持Worker Threads');
|
||||
}
|
||||
|
||||
try {
|
||||
return new NodeWorker(script, options);
|
||||
} catch (error) {
|
||||
throw new Error(`创建Node.js Worker失败: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建SharedArrayBuffer
|
||||
*/
|
||||
public createSharedArrayBuffer(length: number): SharedArrayBuffer | null {
|
||||
if (!this.isSharedArrayBufferSupported()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return new SharedArrayBuffer(length);
|
||||
} catch (error) {
|
||||
console.warn('SharedArrayBuffer创建失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取高精度时间戳(纳秒)
|
||||
*/
|
||||
public getHighResTimestamp(): number {
|
||||
// 返回毫秒,与浏览器 performance.now() 保持一致
|
||||
return Number(process.hrtime.bigint()) / 1000000;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取平台配置
|
||||
*/
|
||||
public getPlatformConfig(): PlatformConfig {
|
||||
return {
|
||||
maxWorkerCount: this.getHardwareConcurrency(),
|
||||
supportsModuleWorker: true, // Node.js 支持 ES 模块
|
||||
supportsTransferableObjects: true,
|
||||
maxSharedArrayBufferSize: this.getMaxSharedArrayBufferSize(),
|
||||
workerScriptPrefix: '',
|
||||
limitations: {
|
||||
noEval: false, // Node.js 支持 eval
|
||||
requiresWorkerInit: false
|
||||
},
|
||||
extensions: {
|
||||
platform: 'nodejs',
|
||||
nodeVersion: process.version,
|
||||
v8Version: process.versions.v8,
|
||||
uvVersion: process.versions.uv,
|
||||
zlibVersion: process.versions.zlib,
|
||||
opensslVersion: process.versions.openssl,
|
||||
architecture: process.arch,
|
||||
endianness: os.endianness(),
|
||||
pid: process.pid,
|
||||
ppid: process.ppid
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Node.js设备信息
|
||||
*/
|
||||
public getDeviceInfo(): NodeDeviceInfo {
|
||||
const cpus = os.cpus();
|
||||
const networkInterfaces = os.networkInterfaces();
|
||||
const userInfo = os.userInfo();
|
||||
|
||||
return {
|
||||
// 系统信息
|
||||
platform: os.platform(),
|
||||
arch: os.arch(),
|
||||
type: os.type(),
|
||||
release: os.release(),
|
||||
version: os.version(),
|
||||
hostname: os.hostname(),
|
||||
|
||||
// CPU信息
|
||||
cpus: cpus.map(cpu => ({
|
||||
model: cpu.model,
|
||||
speed: cpu.speed,
|
||||
times: cpu.times
|
||||
})),
|
||||
|
||||
// 内存信息
|
||||
totalMemory: os.totalmem(),
|
||||
freeMemory: os.freemem(),
|
||||
usedMemory: os.totalmem() - os.freemem(),
|
||||
|
||||
// 负载信息
|
||||
loadAverage: os.loadavg(),
|
||||
|
||||
// 网络接口
|
||||
networkInterfaces: Object.fromEntries(
|
||||
Object.entries(networkInterfaces).map(([name, interfaces]) => [
|
||||
name,
|
||||
(interfaces || []).map(iface => ({
|
||||
address: iface.address,
|
||||
netmask: iface.netmask,
|
||||
family: iface.family as 'IPv4' | 'IPv6',
|
||||
mac: iface.mac,
|
||||
internal: iface.internal,
|
||||
cidr: iface.cidr,
|
||||
scopeid: iface.scopeid
|
||||
}))
|
||||
])
|
||||
),
|
||||
|
||||
// 进程信息
|
||||
process: {
|
||||
pid: process.pid,
|
||||
ppid: process.ppid,
|
||||
version: process.version,
|
||||
versions: process.versions,
|
||||
uptime: process.uptime()
|
||||
},
|
||||
|
||||
// 用户信息
|
||||
userInfo: {
|
||||
uid: userInfo.uid,
|
||||
gid: userInfo.gid,
|
||||
username: userInfo.username,
|
||||
homedir: userInfo.homedir,
|
||||
shell: userInfo.shell
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取SharedArrayBuffer最大大小限制
|
||||
*/
|
||||
private getMaxSharedArrayBufferSize(): number {
|
||||
const totalMemory = os.totalmem();
|
||||
// 限制为系统总内存的50%
|
||||
return Math.floor(totalMemory * 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Node.js Worker封装
|
||||
*/
|
||||
class NodeWorker implements PlatformWorker {
|
||||
private _state: 'running' | 'terminated' = 'running';
|
||||
private worker: Worker;
|
||||
private isTemporaryFile: boolean = false;
|
||||
private scriptPath: string;
|
||||
|
||||
constructor(script: string, options: WorkerCreationOptions = {}) {
|
||||
try {
|
||||
// 判断 script 是文件路径还是脚本内容
|
||||
if (this.isFilePath(script)) {
|
||||
// 直接使用文件路径
|
||||
this.scriptPath = script;
|
||||
this.isTemporaryFile = false;
|
||||
} else {
|
||||
// 将脚本内容写入临时文件
|
||||
this.scriptPath = this.writeScriptToFile(script, options.name);
|
||||
this.isTemporaryFile = true;
|
||||
}
|
||||
|
||||
// 创建Worker
|
||||
this.worker = new Worker(this.scriptPath, {
|
||||
// Node.js Worker options
|
||||
workerData: options.name ? { name: options.name } : undefined
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(`创建Node.js Worker失败: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为文件路径
|
||||
*/
|
||||
private isFilePath(script: string): boolean {
|
||||
// 检查是否看起来像文件路径
|
||||
return (script.endsWith('.js') || script.endsWith('.mjs') || script.endsWith('.ts')) &&
|
||||
!script.includes('\n') &&
|
||||
!script.includes(';') &&
|
||||
script.length < 500; // 文件路径通常不会太长
|
||||
}
|
||||
|
||||
/**
|
||||
* 将脚本内容写入临时文件
|
||||
*/
|
||||
private writeScriptToFile(script: string, name?: string): string {
|
||||
const tmpDir = os.tmpdir();
|
||||
const fileName = name ? `worker-${name}-${Date.now()}.js` : `worker-${Date.now()}.js`;
|
||||
const filePath = path.join(tmpDir, fileName);
|
||||
|
||||
try {
|
||||
fs.writeFileSync(filePath, script, 'utf8');
|
||||
return filePath;
|
||||
} catch (error) {
|
||||
throw new Error(`写入Worker脚本文件失败: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public get state(): 'running' | 'terminated' {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
public postMessage(message: any, transfer?: Transferable[]): void {
|
||||
if (this._state === 'terminated') {
|
||||
throw new Error('Worker已被终止');
|
||||
}
|
||||
|
||||
try {
|
||||
if (transfer && transfer.length > 0) {
|
||||
// Node.js Worker 支持 Transferable Objects
|
||||
this.worker.postMessage(message, transfer);
|
||||
} else {
|
||||
this.worker.postMessage(message);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`发送消息到Node.js Worker失败: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public onMessage(handler: (event: { data: any }) => void): void {
|
||||
this.worker.on('message', (data: any) => {
|
||||
handler({ data });
|
||||
});
|
||||
}
|
||||
|
||||
public onError(handler: (error: ErrorEvent) => void): void {
|
||||
this.worker.on('error', (error: Error) => {
|
||||
// 将 Error 转换为 ErrorEvent 格式
|
||||
const errorEvent = {
|
||||
message: error.message,
|
||||
filename: '',
|
||||
lineno: 0,
|
||||
colno: 0,
|
||||
error: error
|
||||
} as ErrorEvent;
|
||||
handler(errorEvent);
|
||||
});
|
||||
}
|
||||
|
||||
public terminate(): void {
|
||||
if (this._state === 'running') {
|
||||
try {
|
||||
this.worker.terminate();
|
||||
this._state = 'terminated';
|
||||
|
||||
// 清理临时脚本文件
|
||||
this.cleanupScriptFile();
|
||||
} catch (error) {
|
||||
console.error('终止Node.js Worker失败:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理临时脚本文件
|
||||
*/
|
||||
private cleanupScriptFile(): void {
|
||||
// 只清理临时创建的文件,不清理用户提供的文件路径
|
||||
if (this.scriptPath && this.isTemporaryFile) {
|
||||
try {
|
||||
fs.unlinkSync(this.scriptPath);
|
||||
} catch (error) {
|
||||
console.warn('清理Worker脚本文件失败:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 复制代码
|
||||
|
||||
将上述代码复制到你的项目中,例如 `src/platform/NodeAdapter.ts`。
|
||||
|
||||
### 2. 注册适配器
|
||||
|
||||
```typescript
|
||||
import { PlatformManager } from '@esengine/ecs-framework';
|
||||
import { NodeAdapter } from './platform/NodeAdapter';
|
||||
|
||||
// 检查是否在Node.js环境
|
||||
if (typeof process !== 'undefined' && process.versions && process.versions.node) {
|
||||
const nodeAdapter = new NodeAdapter();
|
||||
PlatformManager.getInstance().registerAdapter(nodeAdapter);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 使用 WorkerEntitySystem
|
||||
|
||||
Node.js 适配器与 WorkerEntitySystem 配合使用,框架会自动处理 Worker 脚本的创建:
|
||||
|
||||
```typescript
|
||||
import { WorkerEntitySystem, Matcher } from '@esengine/ecs-framework';
|
||||
import * as os from 'os';
|
||||
|
||||
class PhysicsSystem extends WorkerEntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Transform, Velocity), {
|
||||
enableWorker: true,
|
||||
workerCount: os.cpus().length, // 使用所有CPU核心
|
||||
useSharedArrayBuffer: true,
|
||||
systemConfig: { gravity: 9.8 }
|
||||
});
|
||||
}
|
||||
|
||||
protected getDefaultEntityDataSize(): number {
|
||||
return 6; // x, y, vx, vy, mass, radius
|
||||
}
|
||||
|
||||
protected extractEntityData(entity: Entity): PhysicsData {
|
||||
const transform = entity.getComponent(Transform);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
return {
|
||||
x: transform.x,
|
||||
y: transform.y,
|
||||
vx: velocity.x,
|
||||
vy: velocity.y,
|
||||
mass: 1,
|
||||
radius: 10
|
||||
};
|
||||
}
|
||||
|
||||
// 这个函数会被自动序列化并在Worker中执行
|
||||
protected workerProcess(entities, deltaTime, config) {
|
||||
return entities.map(entity => {
|
||||
// 应用重力
|
||||
entity.vy += config.gravity * deltaTime;
|
||||
|
||||
// 更新位置
|
||||
entity.x += entity.vx * deltaTime;
|
||||
entity.y += entity.vy * deltaTime;
|
||||
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
protected applyResult(entity: Entity, result: PhysicsData): void {
|
||||
const transform = entity.getComponent(Transform);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
transform.x = result.x;
|
||||
transform.y = result.y;
|
||||
velocity.x = result.vx;
|
||||
velocity.y = result.vy;
|
||||
}
|
||||
}
|
||||
|
||||
interface PhysicsData {
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
mass: number;
|
||||
radius: number;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 获取系统信息
|
||||
|
||||
```typescript
|
||||
const manager = PlatformManager.getInstance();
|
||||
if (manager.hasAdapter()) {
|
||||
const adapter = manager.getAdapter();
|
||||
const deviceInfo = adapter.getDeviceInfo();
|
||||
|
||||
console.log('Node.js版本:', deviceInfo.process?.version);
|
||||
console.log('CPU核心数:', deviceInfo.cpus?.length);
|
||||
console.log('总内存:', Math.round(deviceInfo.totalMemory! / 1024 / 1024), 'MB');
|
||||
console.log('可用内存:', Math.round(deviceInfo.freeMemory! / 1024 / 1024), 'MB');
|
||||
}
|
||||
```
|
||||
|
||||
## 官方文档参考
|
||||
|
||||
Node.js Worker Threads 相关官方文档:
|
||||
|
||||
- [Worker Threads 官方文档](https://nodejs.org/api/worker_threads.html)
|
||||
- [SharedArrayBuffer 支持](https://nodejs.org/api/globals.html#class-sharedarraybuffer)
|
||||
- [OS 模块文档](https://nodejs.org/api/os.html)
|
||||
- [Process 模块文档](https://nodejs.org/api/process.html)
|
||||
|
||||
## 重要注意事项
|
||||
|
||||
### Worker Threads 要求
|
||||
|
||||
- **Node.js版本**: 需要 Node.js 10.5.0+ (建议 12+)
|
||||
- **模块类型**: 支持 CommonJS 和 ES 模块
|
||||
- **线程限制**: 理论上无限制,但建议不超过 CPU 核心数的 2 倍
|
||||
|
||||
### 性能优化建议
|
||||
|
||||
#### 1. Worker 池管理
|
||||
|
||||
```typescript
|
||||
class ServerPhysicsSystem extends WorkerEntitySystem {
|
||||
constructor() {
|
||||
const cpuCount = os.cpus().length;
|
||||
super(Matcher.all(Transform, Velocity), {
|
||||
enableWorker: true,
|
||||
workerCount: Math.min(cpuCount * 2, 16), // 最多16个Worker
|
||||
entitiesPerWorker: 1000, // 每个Worker处理1000个实体
|
||||
useSharedArrayBuffer: true,
|
||||
systemConfig: {
|
||||
gravity: 9.8,
|
||||
timeStep: 1/60
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 内存管理
|
||||
|
||||
```typescript
|
||||
class MemoryMonitor {
|
||||
public static checkMemoryUsage(): void {
|
||||
const used = process.memoryUsage();
|
||||
|
||||
console.log('内存使用情况:');
|
||||
console.log(` RSS: ${Math.round(used.rss / 1024 / 1024)} MB`);
|
||||
console.log(` Heap Used: ${Math.round(used.heapUsed / 1024 / 1024)} MB`);
|
||||
console.log(` Heap Total: ${Math.round(used.heapTotal / 1024 / 1024)} MB`);
|
||||
console.log(` External: ${Math.round(used.external / 1024 / 1024)} MB`);
|
||||
|
||||
// 内存使用率过高时触发警告
|
||||
if (used.heapUsed > used.heapTotal * 0.9) {
|
||||
console.warn('内存使用率过高,建议优化或重启');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 定期检查内存使用
|
||||
setInterval(() => {
|
||||
MemoryMonitor.checkMemoryUsage();
|
||||
}, 30000); // 每30秒检查一次
|
||||
```
|
||||
|
||||
#### 3. 服务器环境优化
|
||||
|
||||
```typescript
|
||||
// 设置进程标题
|
||||
process.title = 'ecs-game-server';
|
||||
|
||||
// 处理未捕获异常
|
||||
process.on('uncaughtException', (error) => {
|
||||
console.error('未捕获异常:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.error('未处理的Promise拒绝:', reason);
|
||||
});
|
||||
|
||||
// 优雅关闭
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('收到SIGTERM信号,正在关闭服务器...');
|
||||
// 清理资源
|
||||
process.exit(0);
|
||||
});
|
||||
```
|
||||
|
||||
## 调试技巧
|
||||
|
||||
```typescript
|
||||
// 检查Node.js环境支持情况
|
||||
const adapter = new NodeAdapter();
|
||||
console.log('Node.js版本:', adapter.version);
|
||||
console.log('Worker支持:', adapter.isWorkerSupported());
|
||||
console.log('SharedArrayBuffer支持:', adapter.isSharedArrayBufferSupported());
|
||||
console.log('CPU核心数:', adapter.getHardwareConcurrency());
|
||||
|
||||
// 获取详细配置
|
||||
const config = adapter.getPlatformConfig();
|
||||
console.log('平台配置:', JSON.stringify(config, null, 2));
|
||||
|
||||
// 系统资源监控
|
||||
const deviceInfo = adapter.getDeviceInfo();
|
||||
console.log('系统负载:', deviceInfo.loadAverage);
|
||||
console.log('网络接口:', Object.keys(deviceInfo.networkInterfaces!));
|
||||
```
|
||||
677
docs/guide/platform-adapter/wechat-minigame.md
Normal file
677
docs/guide/platform-adapter/wechat-minigame.md
Normal file
@@ -0,0 +1,677 @@
|
||||
# 微信小游戏适配器
|
||||
|
||||
## 概述
|
||||
|
||||
微信小游戏平台适配器专为微信小游戏环境设计,处理微信小游戏的特殊限制和API。
|
||||
|
||||
## 特性支持
|
||||
|
||||
- ✅ **Worker**: 支持(通过 `wx.createWorker` 创建,需要配置 game.json)
|
||||
- ❌ **SharedArrayBuffer**: 不支持
|
||||
- ❌ **Transferable Objects**: 不支持(只支持可序列化对象)
|
||||
- ✅ **高精度时间**: 使用 `Date.now()` 或 `wx.getPerformance()`
|
||||
- ✅ **设备信息**: 完整的微信小游戏设备信息
|
||||
|
||||
## 完整实现
|
||||
|
||||
```typescript
|
||||
import type {
|
||||
IPlatformAdapter,
|
||||
PlatformWorker,
|
||||
WorkerCreationOptions,
|
||||
PlatformConfig,
|
||||
WeChatDeviceInfo
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* 微信小游戏平台适配器
|
||||
* 支持微信小游戏环境
|
||||
*/
|
||||
export class WeChatMiniGameAdapter implements IPlatformAdapter {
|
||||
public readonly name = 'wechat-minigame';
|
||||
public readonly version: string;
|
||||
private systemInfo: any;
|
||||
|
||||
constructor() {
|
||||
// 获取微信小游戏版本信息
|
||||
this.systemInfo = this.getSystemInfo();
|
||||
this.version = this.systemInfo.version || 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否支持Worker
|
||||
*/
|
||||
public isWorkerSupported(): boolean {
|
||||
// 微信小游戏支持Worker,通过wx.createWorker创建
|
||||
return typeof wx !== 'undefined' && typeof wx.createWorker === 'function';
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否支持SharedArrayBuffer(不支持)
|
||||
*/
|
||||
public isSharedArrayBufferSupported(): boolean {
|
||||
return false; // 微信小游戏不支持SharedArrayBuffer
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取硬件并发数
|
||||
*/
|
||||
public getHardwareConcurrency(): number {
|
||||
// 微信小游戏官方限制:最多只能创建 1 个 Worker
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建Worker
|
||||
* @param script 脚本内容或文件路径
|
||||
* @param options Worker创建选项
|
||||
*/
|
||||
public createWorker(script: string, options: WorkerCreationOptions = {}): PlatformWorker {
|
||||
if (!this.isWorkerSupported()) {
|
||||
throw new Error('微信小游戏不支持Worker');
|
||||
}
|
||||
|
||||
try {
|
||||
return new WeChatWorker(script, options);
|
||||
} catch (error) {
|
||||
throw new Error(`创建微信Worker失败: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建SharedArrayBuffer(不支持)
|
||||
*/
|
||||
public createSharedArrayBuffer(length: number): SharedArrayBuffer | null {
|
||||
return null; // 微信小游戏不支持SharedArrayBuffer
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取高精度时间戳
|
||||
*/
|
||||
public getHighResTimestamp(): number {
|
||||
// 尝试使用微信的性能API,否则使用Date.now()
|
||||
if (typeof wx !== 'undefined' && wx.getPerformance) {
|
||||
const performance = wx.getPerformance();
|
||||
return performance.now();
|
||||
}
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取平台配置
|
||||
*/
|
||||
public getPlatformConfig(): PlatformConfig {
|
||||
return {
|
||||
maxWorkerCount: 1, // 微信小游戏最多支持 1 个 Worker
|
||||
supportsModuleWorker: false, // 不支持模块Worker
|
||||
supportsTransferableObjects: this.checkTransferableObjectsSupport(),
|
||||
maxSharedArrayBufferSize: 0,
|
||||
workerScriptPrefix: '',
|
||||
limitations: {
|
||||
noEval: true, // 微信小游戏限制eval使用
|
||||
requiresWorkerInit: false,
|
||||
memoryLimit: this.getMemoryLimit(),
|
||||
workerNotSupported: false,
|
||||
workerLimitations: [
|
||||
'最多只能创建 1 个 Worker',
|
||||
'创建新Worker前必须先调用 Worker.terminate()',
|
||||
'Worker脚本必须为项目内相对路径',
|
||||
'需要在 game.json 中配置 workers 路径',
|
||||
'使用 worker.onMessage() 而不是 self.onmessage',
|
||||
'需要基础库 1.9.90 及以上版本'
|
||||
]
|
||||
},
|
||||
extensions: {
|
||||
platform: 'wechat-minigame',
|
||||
systemInfo: this.systemInfo,
|
||||
appId: this.systemInfo.host?.appId || 'unknown'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取微信小游戏设备信息
|
||||
*/
|
||||
public getDeviceInfo(): WeChatDeviceInfo {
|
||||
return {
|
||||
// 设备基础信息
|
||||
brand: this.systemInfo.brand,
|
||||
model: this.systemInfo.model,
|
||||
platform: this.systemInfo.platform,
|
||||
system: this.systemInfo.system,
|
||||
benchmarkLevel: this.systemInfo.benchmarkLevel,
|
||||
cpuType: this.systemInfo.cpuType,
|
||||
memorySize: this.systemInfo.memorySize,
|
||||
deviceAbi: this.systemInfo.deviceAbi,
|
||||
abi: this.systemInfo.abi,
|
||||
|
||||
// 窗口信息
|
||||
screenWidth: this.systemInfo.screenWidth,
|
||||
screenHeight: this.systemInfo.screenHeight,
|
||||
screenTop: this.systemInfo.screenTop,
|
||||
windowWidth: this.systemInfo.windowWidth,
|
||||
windowHeight: this.systemInfo.windowHeight,
|
||||
pixelRatio: this.systemInfo.pixelRatio,
|
||||
statusBarHeight: this.systemInfo.statusBarHeight,
|
||||
safeArea: this.systemInfo.safeArea,
|
||||
|
||||
// 应用信息
|
||||
version: this.systemInfo.version,
|
||||
language: this.systemInfo.language,
|
||||
theme: this.systemInfo.theme,
|
||||
SDKVersion: this.systemInfo.SDKVersion,
|
||||
enableDebug: this.systemInfo.enableDebug,
|
||||
fontSizeSetting: this.systemInfo.fontSizeSetting,
|
||||
host: this.systemInfo.host
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步获取完整的平台配置
|
||||
*/
|
||||
public async getPlatformConfigAsync(): Promise<PlatformConfig> {
|
||||
// 可以在这里添加异步获取设备性能信息的逻辑
|
||||
const baseConfig = this.getPlatformConfig();
|
||||
|
||||
// 尝试获取设备性能信息
|
||||
try {
|
||||
const benchmarkLevel = await this.getBenchmarkLevel();
|
||||
baseConfig.extensions = {
|
||||
...baseConfig.extensions,
|
||||
benchmarkLevel
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn('获取性能基准失败:', error);
|
||||
}
|
||||
|
||||
return baseConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否支持Transferable Objects
|
||||
*/
|
||||
private checkTransferableObjectsSupport(): boolean {
|
||||
// 微信小游戏不支持 Transferable Objects
|
||||
// 基础库 2.20.2 之前只支持可序列化的 key-value 对象
|
||||
// 2.20.2 之后支持任意类型数据,但仍然不支持 Transferable Objects
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统信息
|
||||
*/
|
||||
private getSystemInfo(): any {
|
||||
if (typeof wx !== 'undefined' && wx.getSystemInfoSync) {
|
||||
try {
|
||||
return wx.getSystemInfoSync();
|
||||
} catch (error) {
|
||||
console.warn('获取微信系统信息失败:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取内存限制
|
||||
*/
|
||||
private getMemoryLimit(): number {
|
||||
// 微信小游戏通常有内存限制
|
||||
const memorySize = this.systemInfo.memorySize;
|
||||
if (memorySize) {
|
||||
// 解析内存大小字符串(如 "4GB")
|
||||
const match = memorySize.match(/(\d+)([GM]B)?/i);
|
||||
if (match) {
|
||||
const value = parseInt(match[1], 10);
|
||||
const unit = match[2]?.toUpperCase();
|
||||
|
||||
if (unit === 'GB') {
|
||||
return value * 1024 * 1024 * 1024;
|
||||
} else if (unit === 'MB') {
|
||||
return value * 1024 * 1024;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 默认限制为512MB
|
||||
return 512 * 1024 * 1024;
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步获取设备性能基准
|
||||
*/
|
||||
private async getBenchmarkLevel(): Promise<number> {
|
||||
return new Promise((resolve) => {
|
||||
if (typeof wx !== 'undefined' && wx.getDeviceInfo) {
|
||||
wx.getDeviceInfo({
|
||||
success: (res: any) => {
|
||||
resolve(res.benchmarkLevel || 0);
|
||||
},
|
||||
fail: () => {
|
||||
resolve(0);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resolve(this.systemInfo.benchmarkLevel || 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信Worker封装
|
||||
*/
|
||||
class WeChatWorker implements PlatformWorker {
|
||||
private _state: 'running' | 'terminated' = 'running';
|
||||
private worker: any;
|
||||
private scriptPath: string;
|
||||
private isTemporaryFile: boolean = false;
|
||||
|
||||
constructor(script: string, options: WorkerCreationOptions = {}) {
|
||||
if (typeof wx === 'undefined' || typeof wx.createWorker !== 'function') {
|
||||
throw new Error('微信小游戏不支持Worker');
|
||||
}
|
||||
|
||||
try {
|
||||
// 判断 script 是文件路径还是脚本内容
|
||||
if (this.isFilePath(script)) {
|
||||
// 直接使用文件路径
|
||||
this.scriptPath = script;
|
||||
this.isTemporaryFile = false;
|
||||
this.worker = wx.createWorker(this.scriptPath, {
|
||||
useExperimentalWorker: true // 启用实验性Worker获得更好性能
|
||||
});
|
||||
} else {
|
||||
// 微信小游戏不支持动态脚本内容,只能使用文件路径
|
||||
// 将脚本内容写入文件系统
|
||||
this.scriptPath = this.writeScriptToFile(script, options.name);
|
||||
this.isTemporaryFile = true;
|
||||
this.worker = wx.createWorker(this.scriptPath, {
|
||||
useExperimentalWorker: true
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`创建微信Worker失败: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为文件路径
|
||||
*/
|
||||
private isFilePath(script: string): boolean {
|
||||
// 简单判断:如果包含 .js 后缀且不包含换行符或分号,认为是文件路径
|
||||
return script.endsWith('.js') &&
|
||||
!script.includes('\n') &&
|
||||
!script.includes(';') &&
|
||||
script.length < 200; // 文件路径通常不会太长
|
||||
}
|
||||
|
||||
/**
|
||||
* 将脚本内容写入文件系统
|
||||
* 注意:微信小游戏不支持blob URL,只能使用文件系统
|
||||
*/
|
||||
private writeScriptToFile(script: string, name?: string): string {
|
||||
const fs = wx.getFileSystemManager();
|
||||
const fileName = name ? `worker-${name}.js` : `worker-${Date.now()}.js`;
|
||||
const filePath = `${wx.env.USER_DATA_PATH}/${fileName}`;
|
||||
|
||||
try {
|
||||
fs.writeFileSync(filePath, script, 'utf8');
|
||||
return filePath;
|
||||
} catch (error) {
|
||||
throw new Error(`写入Worker脚本文件失败: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public get state(): 'running' | 'terminated' {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
public postMessage(message: any, transfer?: Transferable[]): void {
|
||||
if (this._state === 'terminated') {
|
||||
throw new Error('Worker已被终止');
|
||||
}
|
||||
|
||||
try {
|
||||
// 微信小游戏 Worker 只支持可序列化对象,忽略 transfer 参数
|
||||
this.worker.postMessage(message);
|
||||
} catch (error) {
|
||||
throw new Error(`发送消息到微信Worker失败: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public onMessage(handler: (event: { data: any }) => void): void {
|
||||
// 微信小游戏使用 onMessage 方法,不是 onmessage 属性
|
||||
this.worker.onMessage((res: any) => {
|
||||
handler({ data: res });
|
||||
});
|
||||
}
|
||||
|
||||
public onError(handler: (error: ErrorEvent) => void): void {
|
||||
// 注意:微信小游戏 Worker 的错误处理可能与标准不同
|
||||
if (this.worker.onError) {
|
||||
this.worker.onError(handler);
|
||||
}
|
||||
}
|
||||
|
||||
public terminate(): void {
|
||||
if (this._state === 'running') {
|
||||
try {
|
||||
this.worker.terminate();
|
||||
this._state = 'terminated';
|
||||
|
||||
// 清理临时脚本文件
|
||||
this.cleanupScriptFile();
|
||||
} catch (error) {
|
||||
console.error('终止微信Worker失败:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理临时脚本文件
|
||||
*/
|
||||
private cleanupScriptFile(): void {
|
||||
// 只清理临时创建的文件,不清理用户提供的文件路径
|
||||
if (this.scriptPath && this.isTemporaryFile) {
|
||||
try {
|
||||
const fs = wx.getFileSystemManager();
|
||||
fs.unlinkSync(this.scriptPath);
|
||||
} catch (error) {
|
||||
console.warn('清理Worker脚本文件失败:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 复制代码
|
||||
|
||||
将上述代码复制到你的项目中,例如 `src/platform/WeChatMiniGameAdapter.ts`。
|
||||
|
||||
### 2. 注册适配器
|
||||
|
||||
```typescript
|
||||
import { PlatformManager } from '@esengine/ecs-framework';
|
||||
import { WeChatMiniGameAdapter } from './platform/WeChatMiniGameAdapter';
|
||||
|
||||
// 检查是否在微信小游戏环境
|
||||
if (typeof wx !== 'undefined') {
|
||||
const wechatAdapter = new WeChatMiniGameAdapter();
|
||||
PlatformManager.getInstance().registerAdapter(wechatAdapter);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. WorkerEntitySystem 使用方式
|
||||
|
||||
微信小游戏适配器与 WorkerEntitySystem 配合使用,自动处理 Worker 脚本创建:
|
||||
|
||||
#### 基本使用方式(推荐)
|
||||
|
||||
```typescript
|
||||
import { WorkerEntitySystem, Matcher, Entity } from '@esengine/ecs-framework';
|
||||
|
||||
interface PhysicsData {
|
||||
id: number;
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
mass: number;
|
||||
}
|
||||
|
||||
class PhysicsSystem extends WorkerEntitySystem<PhysicsData> {
|
||||
constructor() {
|
||||
super(Matcher.all(Transform, Velocity), {
|
||||
enableWorker: true,
|
||||
workerCount: 1, // 微信小游戏限制只能创建1个Worker
|
||||
systemConfig: { gravity: 100, friction: 0.95 }
|
||||
});
|
||||
}
|
||||
|
||||
protected getDefaultEntityDataSize(): number {
|
||||
return 6; // id, x, y, vx, vy, mass
|
||||
}
|
||||
|
||||
protected extractEntityData(entity: Entity): PhysicsData {
|
||||
const transform = entity.getComponent(Transform);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
const physics = entity.getComponent(Physics);
|
||||
|
||||
return {
|
||||
id: entity.id,
|
||||
x: transform.x,
|
||||
y: transform.y,
|
||||
vx: velocity.x,
|
||||
vy: velocity.y,
|
||||
mass: physics.mass
|
||||
};
|
||||
}
|
||||
|
||||
// WorkerEntitySystem 会自动将此函数序列化并写入临时文件
|
||||
protected workerProcess(entities: PhysicsData[], deltaTime: number, config: any): PhysicsData[] {
|
||||
return entities.map(entity => {
|
||||
// 应用重力
|
||||
entity.vy += config.gravity * deltaTime;
|
||||
|
||||
// 更新位置
|
||||
entity.x += entity.vx * deltaTime;
|
||||
entity.y += entity.vy * deltaTime;
|
||||
|
||||
// 应用摩擦力
|
||||
entity.vx *= config.friction;
|
||||
entity.vy *= config.friction;
|
||||
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
protected applyResult(entity: Entity, result: PhysicsData): void {
|
||||
const transform = entity.getComponent(Transform);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
transform.x = result.x;
|
||||
transform.y = result.y;
|
||||
velocity.x = result.vx;
|
||||
velocity.y = result.vy;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 使用预先创建的 Worker 文件(可选)
|
||||
|
||||
如果你希望使用预先创建的 Worker 文件:
|
||||
|
||||
```typescript
|
||||
// 1. 在 game.json 中配置 Worker 路径
|
||||
/*
|
||||
{
|
||||
"workers": "workers"
|
||||
}
|
||||
*/
|
||||
|
||||
// 2. 创建 workers/physics.js 文件
|
||||
// workers/physics.js 内容:
|
||||
/*
|
||||
// 微信小游戏 Worker 使用标准的 self.onmessage
|
||||
self.onmessage = function(e) {
|
||||
const { type, id, entities, deltaTime, systemConfig } = e.data;
|
||||
|
||||
if (entities) {
|
||||
// 处理物理计算
|
||||
const results = entities.map(entity => {
|
||||
entity.vy += systemConfig.gravity * deltaTime;
|
||||
entity.x += entity.vx * deltaTime;
|
||||
entity.y += entity.vy * deltaTime;
|
||||
return entity;
|
||||
});
|
||||
|
||||
self.postMessage({ id, result: results });
|
||||
}
|
||||
};
|
||||
*/
|
||||
|
||||
// 3. 通过平台适配器直接创建(不推荐,WorkerEntitySystem会自动处理)
|
||||
const adapter = PlatformManager.getInstance().getAdapter();
|
||||
const worker = adapter.createWorker('workers/physics.js');
|
||||
```
|
||||
|
||||
### 4. 获取设备信息
|
||||
|
||||
```typescript
|
||||
const manager = PlatformManager.getInstance();
|
||||
if (manager.hasAdapter()) {
|
||||
const adapter = manager.getAdapter();
|
||||
console.log('微信设备信息:', adapter.getDeviceInfo());
|
||||
}
|
||||
```
|
||||
|
||||
## 官方文档参考
|
||||
|
||||
在使用微信小游戏 Worker 之前,建议先阅读官方文档:
|
||||
|
||||
- [wx.createWorker API](https://developers.weixin.qq.com/minigame/dev/api/worker/wx.createWorker.html)
|
||||
- [Worker.postMessage API](https://developers.weixin.qq.com/minigame/dev/api/worker/Worker.postMessage.html)
|
||||
- [Worker.onMessage API](https://developers.weixin.qq.com/minigame/dev/api/worker/Worker.onMessage.html)
|
||||
- [Worker.terminate API](https://developers.weixin.qq.com/minigame/dev/api/worker/Worker.terminate.html)
|
||||
|
||||
## 重要注意事项
|
||||
|
||||
### Worker 限制和配置
|
||||
|
||||
微信小游戏的 Worker 有以下限制:
|
||||
|
||||
- **数量限制**: 最多只能创建 1 个 Worker
|
||||
- **版本要求**: 需要基础库 1.9.90 及以上版本
|
||||
- **脚本支持**: 不支持 blob URL,只能使用文件路径或写入文件系统
|
||||
- **文件路径**: Worker 脚本路径必须为绝对路径,但不能以 "/" 开头
|
||||
- **生命周期**: 创建新 Worker 前必须先调用 `Worker.terminate()` 终止当前 Worker
|
||||
- **消息处理**: Worker 内使用标准的 `self.onmessage`,主线程使用 `worker.onMessage()`
|
||||
- **实验性功能**: 支持 `useExperimentalWorker` 选项获得更好的 iOS 性能
|
||||
|
||||
#### Worker 配置(可选)
|
||||
|
||||
如果使用预先创建的 Worker 文件,需要在 `game.json` 中添加 workers 配置:
|
||||
|
||||
```json
|
||||
{
|
||||
"deviceOrientation": "portrait",
|
||||
"showStatusBar": false,
|
||||
"workers": "workers",
|
||||
"subpackages": []
|
||||
}
|
||||
```
|
||||
|
||||
**注意**: 使用 WorkerEntitySystem 时无需此配置,框架会自动将脚本写入临时文件。
|
||||
|
||||
### 内存限制
|
||||
|
||||
微信小游戏有严格的内存限制:
|
||||
|
||||
- 通常限制在 256MB - 512MB
|
||||
- 需要及时释放不用的资源
|
||||
- 避免内存泄漏
|
||||
|
||||
### API 限制
|
||||
|
||||
- 不支持 `eval()` 函数
|
||||
- 不支持 `Function` 构造器
|
||||
- DOM API 受限
|
||||
- 文件系统 API 受限
|
||||
|
||||
## 性能优化建议
|
||||
|
||||
### 1. 分帧处理
|
||||
|
||||
```typescript
|
||||
class FramedProcessor {
|
||||
private tasks: (() => void)[] = [];
|
||||
private isProcessing = false;
|
||||
|
||||
public addTask(task: () => void): void {
|
||||
this.tasks.push(task);
|
||||
if (!this.isProcessing) {
|
||||
this.processNextFrame();
|
||||
}
|
||||
}
|
||||
|
||||
private processNextFrame(): void {
|
||||
this.isProcessing = true;
|
||||
const startTime = Date.now();
|
||||
const frameTime = 16; // 16ms per frame
|
||||
|
||||
while (this.tasks.length > 0 && Date.now() - startTime < frameTime) {
|
||||
const task = this.tasks.shift();
|
||||
if (task) task();
|
||||
}
|
||||
|
||||
if (this.tasks.length > 0) {
|
||||
setTimeout(() => this.processNextFrame(), 0);
|
||||
} else {
|
||||
this.isProcessing = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 内存管理
|
||||
|
||||
```typescript
|
||||
class MemoryManager {
|
||||
private static readonly MAX_MEMORY = 256 * 1024 * 1024; // 256MB
|
||||
|
||||
public static checkMemoryUsage(): void {
|
||||
if (typeof wx !== 'undefined' && wx.getPerformance) {
|
||||
const performance = wx.getPerformance();
|
||||
const memoryInfo = performance.getEntries().find(
|
||||
(entry: any) => entry.entryType === 'memory'
|
||||
);
|
||||
|
||||
if (memoryInfo && memoryInfo.usedJSHeapSize > this.MAX_MEMORY * 0.8) {
|
||||
console.warn('内存使用率过高,建议清理资源');
|
||||
// 触发垃圾回收或资源清理
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 调试技巧
|
||||
|
||||
```typescript
|
||||
// 检查微信小游戏环境
|
||||
if (typeof wx !== 'undefined') {
|
||||
const adapter = new WeChatMiniGameAdapter();
|
||||
|
||||
console.log('微信版本:', adapter.version);
|
||||
console.log('设备信息:', adapter.getDeviceInfo());
|
||||
console.log('平台配置:', adapter.getPlatformConfig());
|
||||
|
||||
// 检查功能支持
|
||||
console.log('Worker支持:', adapter.isWorkerSupported());
|
||||
console.log('SharedArrayBuffer支持:', adapter.isSharedArrayBufferSupported());
|
||||
}
|
||||
```
|
||||
|
||||
## 微信小游戏特殊API
|
||||
|
||||
```typescript
|
||||
// 获取设备性能等级
|
||||
if (typeof wx !== 'undefined' && wx.getDeviceInfo) {
|
||||
wx.getDeviceInfo({
|
||||
success: (res) => {
|
||||
console.log('设备性能等级:', res.benchmarkLevel);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 监听内存警告
|
||||
if (typeof wx !== 'undefined' && wx.onMemoryWarning) {
|
||||
wx.onMemoryWarning(() => {
|
||||
console.warn('收到内存警告,开始清理资源');
|
||||
// 清理不必要的资源
|
||||
});
|
||||
}
|
||||
```
|
||||
643
docs/guide/plugin-system.md
Normal file
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) 文档。
|
||||
661
docs/guide/scene.md
Normal file
661
docs/guide/scene.md
Normal file
@@ -0,0 +1,661 @@
|
||||
# 场景管理
|
||||
|
||||
在 ECS 架构中,场景(Scene)是游戏世界的容器,负责管理实体、系统和组件的生命周期。场景提供了完整的 ECS 运行环境。
|
||||
|
||||
## 基本概念
|
||||
|
||||
场景是 ECS 框架的核心容器,提供:
|
||||
- 实体的创建、管理和销毁
|
||||
- 系统的注册和执行调度
|
||||
- 组件的存储和查询
|
||||
- 事件系统支持
|
||||
- 性能监控和调试信息
|
||||
|
||||
## 场景管理方式
|
||||
|
||||
ECS Framework 提供了两种场景管理方式:
|
||||
|
||||
1. **[SceneManager](./scene-manager.md)** - 适用于 95% 的游戏应用
|
||||
- 单人游戏、简单多人游戏、移动游戏
|
||||
- 轻量级,简单直观的 API
|
||||
- 支持场景切换
|
||||
|
||||
2. **[WorldManager](./world-manager.md)** - 适用于高级多世界隔离场景
|
||||
- MMO 游戏服务器、游戏房间系统
|
||||
- 多 World 管理,每个 World 可包含多个场景
|
||||
- 完全隔离的独立环境
|
||||
|
||||
本文档重点介绍 Scene 类本身的使用方法。关于场景管理器的详细信息,请查看对应的文档。
|
||||
|
||||
## 创建场景
|
||||
|
||||
### 继承 Scene 类
|
||||
|
||||
**推荐做法:继承 Scene 类来创建自定义场景**
|
||||
|
||||
```typescript
|
||||
import { Scene, EntitySystem } from '@esengine/ecs-framework';
|
||||
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 设置场景名称
|
||||
this.name = "GameScene";
|
||||
|
||||
// 添加系统
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
this.addSystem(new PhysicsSystem());
|
||||
|
||||
// 创建初始实体
|
||||
this.createInitialEntities();
|
||||
}
|
||||
|
||||
private createInitialEntities(): void {
|
||||
// 创建玩家
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Position(400, 300));
|
||||
player.addComponent(new Health(100));
|
||||
player.addComponent(new PlayerController());
|
||||
|
||||
// 创建敌人
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const enemy = this.createEntity(`Enemy_${i}`);
|
||||
enemy.addComponent(new Position(Math.random() * 800, Math.random() * 600));
|
||||
enemy.addComponent(new Health(50));
|
||||
enemy.addComponent(new EnemyAI());
|
||||
}
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log("游戏场景已启动");
|
||||
// 场景启动时的逻辑
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
console.log("游戏场景已卸载");
|
||||
// 场景卸载时的清理逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用场景配置
|
||||
|
||||
```typescript
|
||||
import { ISceneConfig } from '@esengine/ecs-framework';
|
||||
|
||||
const config: ISceneConfig = {
|
||||
name: "MainGame",
|
||||
enableEntityDirectUpdate: false
|
||||
};
|
||||
|
||||
class ConfiguredScene extends Scene {
|
||||
constructor() {
|
||||
super(config);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 场景生命周期
|
||||
|
||||
场景提供了完整的生命周期管理:
|
||||
|
||||
```typescript
|
||||
class ExampleScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 场景初始化:设置系统和初始实体
|
||||
console.log("场景初始化");
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
// 场景开始运行:游戏逻辑开始执行
|
||||
console.log("场景开始运行");
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// 场景卸载:清理资源
|
||||
console.log("场景卸载");
|
||||
}
|
||||
}
|
||||
|
||||
// 使用场景(由框架自动管理生命周期)
|
||||
const scene = new ExampleScene();
|
||||
// 场景的 initialize(), begin(), update(), end() 由框架自动调用
|
||||
```
|
||||
|
||||
**生命周期方法**:
|
||||
|
||||
1. `initialize()` - 场景初始化,设置系统和初始实体
|
||||
2. `begin()` / `onStart()` - 场景开始运行
|
||||
3. `update()` - 每帧更新(由场景管理器调用)
|
||||
4. `end()` / `unload()` - 场景卸载,清理资源
|
||||
|
||||
## 实体管理
|
||||
|
||||
### 创建实体
|
||||
|
||||
```typescript
|
||||
class EntityScene extends Scene {
|
||||
createGameEntities(): void {
|
||||
// 创建单个实体
|
||||
const player = this.createEntity("Player");
|
||||
|
||||
// 批量创建实体(高性能)
|
||||
const bullets = this.createEntities(100, "Bullet");
|
||||
|
||||
// 为批量创建的实体添加组件
|
||||
bullets.forEach((bullet, index) => {
|
||||
bullet.addComponent(new Position(index * 10, 100));
|
||||
bullet.addComponent(new Velocity(Math.random() * 200 - 100, -300));
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 查找实体
|
||||
|
||||
```typescript
|
||||
class SearchScene extends Scene {
|
||||
findEntities(): void {
|
||||
// 按名称查找
|
||||
const player = this.findEntity("Player");
|
||||
const player2 = this.getEntityByName("Player"); // 别名方法
|
||||
|
||||
// 按 ID 查找
|
||||
const entity = this.findEntityById(123);
|
||||
|
||||
// 按标签查找
|
||||
const enemies = this.findEntitiesByTag(2);
|
||||
const enemies2 = this.getEntitiesByTag(2); // 别名方法
|
||||
|
||||
if (player) {
|
||||
console.log(`找到玩家: ${player.name}`);
|
||||
}
|
||||
|
||||
console.log(`找到 ${enemies.length} 个敌人`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 销毁实体
|
||||
|
||||
```typescript
|
||||
class DestroyScene extends Scene {
|
||||
cleanupEntities(): void {
|
||||
// 销毁所有实体
|
||||
this.destroyAllEntities();
|
||||
|
||||
// 单个实体的销毁通过实体本身
|
||||
const enemy = this.findEntity("Enemy_1");
|
||||
if (enemy) {
|
||||
enemy.destroy(); // 实体会自动从场景中移除
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 系统管理
|
||||
|
||||
### 添加和移除系统
|
||||
|
||||
```typescript
|
||||
class SystemScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 添加系统
|
||||
const movementSystem = new MovementSystem();
|
||||
this.addSystem(movementSystem);
|
||||
|
||||
// 设置系统更新顺序
|
||||
movementSystem.updateOrder = 1;
|
||||
|
||||
// 添加更多系统
|
||||
this.addSystem(new PhysicsSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
}
|
||||
|
||||
public removeUnnecessarySystems(): void {
|
||||
// 获取系统
|
||||
const physicsSystem = this.getEntityProcessor(PhysicsSystem);
|
||||
|
||||
// 移除系统
|
||||
if (physicsSystem) {
|
||||
this.removeSystem(physicsSystem);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 系统访问
|
||||
|
||||
```typescript
|
||||
class SystemAccessScene extends Scene {
|
||||
public pausePhysics(): void {
|
||||
const physicsSystem = this.getEntityProcessor(PhysicsSystem);
|
||||
if (physicsSystem) {
|
||||
physicsSystem.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
public getAllSystems(): EntitySystem[] {
|
||||
return this.systems; // 获取所有系统
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 事件系统
|
||||
|
||||
场景内置了类型安全的事件系统:
|
||||
|
||||
```typescript
|
||||
class EventScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 监听事件
|
||||
this.eventSystem.on('player_died', this.onPlayerDied.bind(this));
|
||||
this.eventSystem.on('enemy_spawned', this.onEnemySpawned.bind(this));
|
||||
this.eventSystem.on('level_complete', this.onLevelComplete.bind(this));
|
||||
}
|
||||
|
||||
private onPlayerDied(data: any): void {
|
||||
console.log('玩家死亡事件');
|
||||
// 处理玩家死亡
|
||||
}
|
||||
|
||||
private onEnemySpawned(data: any): void {
|
||||
console.log('敌人生成事件');
|
||||
// 处理敌人生成
|
||||
}
|
||||
|
||||
private onLevelComplete(data: any): void {
|
||||
console.log('关卡完成事件');
|
||||
// 处理关卡完成
|
||||
}
|
||||
|
||||
public triggerGameEvent(): void {
|
||||
// 发送事件(同步)
|
||||
this.eventSystem.emitSync('custom_event', {
|
||||
message: "这是自定义事件",
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
// 发送事件(异步)
|
||||
this.eventSystem.emit('async_event', {
|
||||
data: "异步事件数据"
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 事件系统 API
|
||||
|
||||
```typescript
|
||||
// 监听事件
|
||||
this.eventSystem.on('event_name', callback);
|
||||
|
||||
// 监听一次(自动取消订阅)
|
||||
this.eventSystem.once('event_name', callback);
|
||||
|
||||
// 取消监听
|
||||
this.eventSystem.off('event_name', callback);
|
||||
|
||||
// 同步发送事件
|
||||
this.eventSystem.emitSync('event_name', data);
|
||||
|
||||
// 异步发送事件
|
||||
this.eventSystem.emit('event_name', data);
|
||||
|
||||
// 清除所有事件监听
|
||||
this.eventSystem.clear();
|
||||
```
|
||||
|
||||
## 场景统计和调试
|
||||
|
||||
### 获取场景统计
|
||||
|
||||
```typescript
|
||||
class StatsScene extends Scene {
|
||||
public showStats(): void {
|
||||
const stats = this.getStats();
|
||||
console.log(`实体数量: ${stats.entityCount}`);
|
||||
console.log(`系统数量: ${stats.processorCount}`);
|
||||
console.log('组件存储统计:', stats.componentStorageStats);
|
||||
}
|
||||
|
||||
public showDebugInfo(): void {
|
||||
const debugInfo = this.getDebugInfo();
|
||||
console.log('场景调试信息:', debugInfo);
|
||||
|
||||
// 显示所有实体信息
|
||||
debugInfo.entities.forEach(entity => {
|
||||
console.log(`实体 ${entity.name}(${entity.id}): ${entity.componentCount} 个组件`);
|
||||
console.log('组件类型:', entity.componentTypes);
|
||||
});
|
||||
|
||||
// 显示所有系统信息
|
||||
debugInfo.processors.forEach(processor => {
|
||||
console.log(`系统 ${processor.name}: 处理 ${processor.entityCount} 个实体`);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 组件查询
|
||||
|
||||
Scene 提供了强大的组件查询系统:
|
||||
|
||||
```typescript
|
||||
class QueryScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 创建一些实体
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const entity = this.createEntity(`Entity_${i}`);
|
||||
entity.addComponent(new Transform(i * 10, 0));
|
||||
entity.addComponent(new Velocity(1, 0));
|
||||
if (i % 2 === 0) {
|
||||
entity.addComponent(new Renderer());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public queryEntities(): void {
|
||||
// 通过 QuerySystem 查询
|
||||
const entities = this.querySystem.query([Transform, Velocity]);
|
||||
console.log(`找到 ${entities.length} 个有 Transform 和 Velocity 的实体`);
|
||||
|
||||
// 使用 ECS 流式 API(如果通过 SceneManager)
|
||||
// const api = sceneManager.api;
|
||||
// const entities = api?.find(Transform, Velocity);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 性能监控
|
||||
|
||||
Scene 内置了性能监控功能:
|
||||
|
||||
```typescript
|
||||
class PerformanceScene extends Scene {
|
||||
public showPerformance(): void {
|
||||
// 获取性能数据
|
||||
const perfData = this.performanceMonitor?.getPerformanceData();
|
||||
if (perfData) {
|
||||
console.log('FPS:', perfData.fps);
|
||||
console.log('帧时间:', perfData.frameTime);
|
||||
console.log('实体更新时间:', perfData.entityUpdateTime);
|
||||
console.log('系统更新时间:', perfData.systemUpdateTime);
|
||||
}
|
||||
|
||||
// 获取性能报告
|
||||
const report = this.performanceMonitor?.generateReport();
|
||||
if (report) {
|
||||
console.log('性能报告:', report);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 场景职责分离
|
||||
|
||||
```typescript
|
||||
// 好的场景设计 - 职责清晰
|
||||
class MenuScene extends Scene {
|
||||
// 只处理菜单相关逻辑
|
||||
}
|
||||
|
||||
class GameScene extends Scene {
|
||||
// 只处理游戏玩法逻辑
|
||||
}
|
||||
|
||||
class InventoryScene extends Scene {
|
||||
// 只处理物品栏逻辑
|
||||
}
|
||||
|
||||
// 避免的场景设计 - 职责混乱
|
||||
class MegaScene extends Scene {
|
||||
// 包含菜单、游戏、物品栏等所有逻辑
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 合理的系统组织
|
||||
|
||||
```typescript
|
||||
class OrganizedScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 按功能和依赖关系添加系统
|
||||
this.addInputSystems();
|
||||
this.addLogicSystems();
|
||||
this.addRenderSystems();
|
||||
}
|
||||
|
||||
private addInputSystems(): void {
|
||||
this.addSystem(new InputSystem());
|
||||
}
|
||||
|
||||
private addLogicSystems(): void {
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new PhysicsSystem());
|
||||
this.addSystem(new CollisionSystem());
|
||||
}
|
||||
|
||||
private addRenderSystems(): void {
|
||||
this.addSystem(new RenderSystem());
|
||||
this.addSystem(new UISystem());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 资源管理
|
||||
|
||||
```typescript
|
||||
class ResourceScene extends Scene {
|
||||
private textures: Map<string, any> = new Map();
|
||||
private sounds: Map<string, any> = new Map();
|
||||
|
||||
protected initialize(): void {
|
||||
this.loadResources();
|
||||
}
|
||||
|
||||
private loadResources(): void {
|
||||
// 加载场景所需资源
|
||||
this.textures.set('player', this.loadTexture('player.png'));
|
||||
this.sounds.set('bgm', this.loadSound('bgm.mp3'));
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// 清理资源
|
||||
this.textures.clear();
|
||||
this.sounds.clear();
|
||||
console.log('场景资源已清理');
|
||||
}
|
||||
|
||||
private loadTexture(path: string): any {
|
||||
// 加载纹理
|
||||
return null;
|
||||
}
|
||||
|
||||
private loadSound(path: string): any {
|
||||
// 加载音效
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 事件处理规范
|
||||
|
||||
```typescript
|
||||
class EventHandlingScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 集中管理事件监听
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
private setupEventListeners(): void {
|
||||
this.eventSystem.on('game_pause', this.onGamePause.bind(this));
|
||||
this.eventSystem.on('game_resume', this.onGameResume.bind(this));
|
||||
this.eventSystem.on('player_input', this.onPlayerInput.bind(this));
|
||||
}
|
||||
|
||||
private onGamePause(): void {
|
||||
// 暂停游戏逻辑
|
||||
this.systems.forEach(system => {
|
||||
if (system instanceof GameLogicSystem) {
|
||||
system.enabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private onGameResume(): void {
|
||||
// 恢复游戏逻辑
|
||||
this.systems.forEach(system => {
|
||||
if (system instanceof GameLogicSystem) {
|
||||
system.enabled = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private onPlayerInput(data: any): void {
|
||||
// 处理玩家输入
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// 清理事件监听
|
||||
this.eventSystem.clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 初始化顺序
|
||||
|
||||
```typescript
|
||||
class ProperInitScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 1. 首先设置场景配置
|
||||
this.name = "GameScene";
|
||||
|
||||
// 2. 然后添加系统(按依赖顺序)
|
||||
this.addSystem(new InputSystem());
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new PhysicsSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
|
||||
// 3. 最后创建实体
|
||||
this.createEntities();
|
||||
|
||||
// 4. 设置事件监听
|
||||
this.setupEvents();
|
||||
}
|
||||
|
||||
private createEntities(): void {
|
||||
// 创建实体
|
||||
}
|
||||
|
||||
private setupEvents(): void {
|
||||
// 设置事件监听
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 完整示例
|
||||
|
||||
```typescript
|
||||
import { Scene, EntitySystem, Entity, Matcher } from '@esengine/ecs-framework';
|
||||
|
||||
// 定义组件
|
||||
class Transform {
|
||||
constructor(public x: number, public y: number) {}
|
||||
}
|
||||
|
||||
class Velocity {
|
||||
constructor(public vx: number, public vy: number) {}
|
||||
}
|
||||
|
||||
class Health {
|
||||
constructor(public value: number) {}
|
||||
}
|
||||
|
||||
// 定义系统
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Transform, Velocity));
|
||||
}
|
||||
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const transform = entity.getComponent(Transform);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
if (transform && velocity) {
|
||||
transform.x += velocity.vx;
|
||||
transform.y += velocity.vy;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 定义场景
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "GameScene";
|
||||
|
||||
// 添加系统
|
||||
this.addSystem(new MovementSystem());
|
||||
|
||||
// 创建玩家
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Transform(400, 300));
|
||||
player.addComponent(new Velocity(0, 0));
|
||||
player.addComponent(new Health(100));
|
||||
|
||||
// 创建敌人
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const enemy = this.createEntity(`Enemy_${i}`);
|
||||
enemy.addComponent(new Transform(
|
||||
Math.random() * 800,
|
||||
Math.random() * 600
|
||||
));
|
||||
enemy.addComponent(new Velocity(
|
||||
Math.random() * 100 - 50,
|
||||
Math.random() * 100 - 50
|
||||
));
|
||||
enemy.addComponent(new Health(50));
|
||||
}
|
||||
|
||||
// 设置事件监听
|
||||
this.eventSystem.on('player_died', () => {
|
||||
console.log('玩家死亡!');
|
||||
});
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log('游戏场景启动');
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
console.log('游戏场景卸载');
|
||||
this.eventSystem.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// 使用场景
|
||||
// 方式1:通过 SceneManager(推荐)
|
||||
import { Core, SceneManager } from '@esengine/ecs-framework';
|
||||
|
||||
Core.create({ debug: true });
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
sceneManager.setScene(new GameScene());
|
||||
|
||||
// 方式2:通过 WorldManager(高级用例)
|
||||
import { WorldManager } from '@esengine/ecs-framework';
|
||||
|
||||
const worldManager = Core.services.resolve(WorldManager);
|
||||
const world = worldManager.createWorld('game');
|
||||
world.createScene('main', new GameScene());
|
||||
world.setSceneActive('main', true);
|
||||
```
|
||||
|
||||
## 下一步
|
||||
|
||||
- 了解 [SceneManager](./scene-manager.md) - 适用于大多数游戏的简单场景管理
|
||||
- 了解 [WorldManager](./world-manager.md) - 适用于需要多世界隔离的高级场景
|
||||
|
||||
场景是 ECS 框架的核心容器,正确使用场景管理能让你的游戏架构更加清晰、模块化和易于维护。
|
||||
823
docs/guide/serialization.md
Normal file
823
docs/guide/serialization.md
Normal file
@@ -0,0 +1,823 @@
|
||||
# 序列化系统
|
||||
|
||||
序列化系统提供了完整的场景、实体和组件数据持久化方案,支持全量序列化和增量序列化两种模式,适用于游戏存档、网络同步、场景编辑器、时间回溯等场景。
|
||||
|
||||
## 基本概念
|
||||
|
||||
序列化系统分为两个层次:
|
||||
|
||||
- **全量序列化**:序列化完整的场景状态,包括所有实体、组件和场景数据
|
||||
- **增量序列化**:只序列化相对于基础快照的变更部分,大幅减少数据量
|
||||
|
||||
### 支持的数据格式
|
||||
|
||||
- **JSON格式**:人类可读,便于调试和编辑
|
||||
- **Binary格式**:使用MessagePack,体积更小,性能更高
|
||||
|
||||
> **📢 v2.2.2 重要变更**
|
||||
>
|
||||
> 从 v2.2.2 开始,二进制序列化格式返回 `Uint8Array` 而非 Node.js 的 `Buffer`,以确保浏览器兼容性:
|
||||
> - `serialize({ format: 'binary' })` 返回 `string | Uint8Array`(原为 `string | Buffer`)
|
||||
> - `deserialize(data)` 接收 `string | Uint8Array`(原为 `string | Buffer`)
|
||||
> - `applyIncremental(data)` 接收 `IncrementalSnapshot | string | Uint8Array`(原为包含 `Buffer`)
|
||||
>
|
||||
> **迁移影响**:
|
||||
> - ✅ **运行时兼容**:Node.js 的 `Buffer` 继承自 `Uint8Array`,现有代码可直接运行
|
||||
> - ⚠️ **类型检查**:如果你的 TypeScript 代码中显式使用了 `Buffer` 类型,需要改为 `Uint8Array`
|
||||
> - ✅ **浏览器支持**:`Uint8Array` 是标准 JavaScript 类型,所有现代浏览器都支持
|
||||
|
||||
## 全量序列化
|
||||
|
||||
### 基础用法
|
||||
|
||||
#### 1. 标记可序列化组件
|
||||
|
||||
使用 `@Serializable` 和 `@Serialize` 装饰器标记需要序列化的组件和字段:
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent, Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSComponent('Player')
|
||||
@Serializable({ version: 1 })
|
||||
class PlayerComponent extends Component {
|
||||
@Serialize()
|
||||
public name: string = '';
|
||||
|
||||
@Serialize()
|
||||
public level: number = 1;
|
||||
|
||||
@Serialize()
|
||||
public experience: number = 0;
|
||||
|
||||
@Serialize()
|
||||
public position: { x: number; y: number } = { x: 0, y: 0 };
|
||||
|
||||
// 不使用 @Serialize() 的字段不会被序列化
|
||||
private tempData: any = null;
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 序列化场景
|
||||
|
||||
```typescript
|
||||
// JSON格式序列化
|
||||
const jsonData = scene.serialize({
|
||||
format: 'json',
|
||||
pretty: true // 美化输出
|
||||
});
|
||||
|
||||
// 保存到本地存储
|
||||
localStorage.setItem('gameSave', jsonData);
|
||||
|
||||
// Binary格式序列化(更小的体积)
|
||||
const binaryData = scene.serialize({
|
||||
format: 'binary'
|
||||
});
|
||||
|
||||
// 保存为文件(Node.js环境)
|
||||
// 注意:binaryData 是 Uint8Array 类型,Node.js 的 fs 可以直接写入
|
||||
fs.writeFileSync('save.bin', binaryData);
|
||||
```
|
||||
|
||||
#### 3. 反序列化场景
|
||||
|
||||
```typescript
|
||||
// 从JSON恢复
|
||||
const saveData = localStorage.getItem('gameSave');
|
||||
if (saveData) {
|
||||
scene.deserialize(saveData, {
|
||||
strategy: 'replace' // 替换当前场景内容
|
||||
});
|
||||
}
|
||||
|
||||
// 从Binary恢复
|
||||
const binaryData = fs.readFileSync('save.bin');
|
||||
scene.deserialize(binaryData, {
|
||||
strategy: 'merge' // 合并到现有场景
|
||||
});
|
||||
```
|
||||
|
||||
### 序列化选项
|
||||
|
||||
#### SerializationOptions
|
||||
|
||||
```typescript
|
||||
interface SceneSerializationOptions {
|
||||
// 指定要序列化的组件类型(可选)
|
||||
components?: ComponentType[];
|
||||
|
||||
// 序列化格式:'json' 或 'binary'
|
||||
format?: 'json' | 'binary';
|
||||
|
||||
// JSON美化输出
|
||||
pretty?: boolean;
|
||||
|
||||
// 包含元数据
|
||||
includeMetadata?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
示例:
|
||||
|
||||
```typescript
|
||||
// 只序列化特定组件类型
|
||||
const saveData = scene.serialize({
|
||||
format: 'json',
|
||||
components: [PlayerComponent, InventoryComponent],
|
||||
pretty: true,
|
||||
includeMetadata: true
|
||||
});
|
||||
```
|
||||
|
||||
#### DeserializationOptions
|
||||
|
||||
```typescript
|
||||
interface SceneDeserializationOptions {
|
||||
// 反序列化策略
|
||||
strategy?: 'merge' | 'replace';
|
||||
|
||||
// 组件类型注册表(可选,默认使用全局注册表)
|
||||
componentRegistry?: Map<string, ComponentType>;
|
||||
}
|
||||
```
|
||||
|
||||
### 高级装饰器
|
||||
|
||||
#### 字段序列化选项
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Advanced')
|
||||
@Serializable({ version: 1 })
|
||||
class AdvancedComponent extends Component {
|
||||
// 使用别名
|
||||
@Serialize({ alias: 'playerName' })
|
||||
public name: string = '';
|
||||
|
||||
// 自定义序列化器
|
||||
@Serialize({
|
||||
serializer: (value: Date) => value.toISOString(),
|
||||
deserializer: (value: string) => new Date(value)
|
||||
})
|
||||
public createdAt: Date = new Date();
|
||||
|
||||
// 忽略序列化
|
||||
@IgnoreSerialization()
|
||||
public cachedData: any = null;
|
||||
}
|
||||
```
|
||||
|
||||
#### 集合类型序列化
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Collections')
|
||||
@Serializable({ version: 1 })
|
||||
class CollectionsComponent extends Component {
|
||||
// Map序列化
|
||||
@SerializeAsMap()
|
||||
public inventory: Map<string, number> = new Map();
|
||||
|
||||
// Set序列化
|
||||
@SerializeAsSet()
|
||||
public acquiredSkills: Set<string> = new Set();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.inventory.set('gold', 100);
|
||||
this.inventory.set('silver', 50);
|
||||
this.acquiredSkills.add('attack');
|
||||
this.acquiredSkills.add('defense');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 场景自定义数据
|
||||
|
||||
除了实体和组件,还可以序列化场景级别的配置数据:
|
||||
|
||||
```typescript
|
||||
// 设置场景数据
|
||||
scene.sceneData.set('weather', 'rainy');
|
||||
scene.sceneData.set('difficulty', 'hard');
|
||||
scene.sceneData.set('checkpoint', { x: 100, y: 200 });
|
||||
|
||||
// 序列化时会自动包含场景数据
|
||||
const saveData = scene.serialize({ format: 'json' });
|
||||
|
||||
// 反序列化后场景数据会恢复
|
||||
scene.deserialize(saveData);
|
||||
console.log(scene.sceneData.get('weather')); // 'rainy'
|
||||
```
|
||||
|
||||
## 增量序列化
|
||||
|
||||
增量序列化只保存场景的变更部分,适用于网络同步、撤销/重做、时间回溯等需要频繁保存状态的场景。
|
||||
|
||||
### 基础用法
|
||||
|
||||
#### 1. 创建基础快照
|
||||
|
||||
```typescript
|
||||
// 在需要开始记录变更前创建基础快照
|
||||
scene.createIncrementalSnapshot();
|
||||
```
|
||||
|
||||
#### 2. 修改场景
|
||||
|
||||
```typescript
|
||||
// 添加实体
|
||||
const enemy = scene.createEntity('Enemy');
|
||||
enemy.addComponent(new PositionComponent(100, 200));
|
||||
enemy.addComponent(new HealthComponent(50));
|
||||
|
||||
// 修改组件
|
||||
const player = scene.findEntity('Player');
|
||||
const pos = player.getComponent(PositionComponent);
|
||||
pos.x = 300;
|
||||
pos.y = 400;
|
||||
|
||||
// 删除组件
|
||||
player.removeComponentByType(BuffComponent);
|
||||
|
||||
// 删除实体
|
||||
const oldEntity = scene.findEntity('ToDelete');
|
||||
oldEntity.destroy();
|
||||
|
||||
// 修改场景数据
|
||||
scene.sceneData.set('score', 1000);
|
||||
```
|
||||
|
||||
#### 3. 获取增量变更
|
||||
|
||||
```typescript
|
||||
// 获取相对于基础快照的所有变更
|
||||
const incremental = scene.serializeIncremental();
|
||||
|
||||
// 查看变更统计
|
||||
const stats = IncrementalSerializer.getIncrementalStats(incremental);
|
||||
console.log('总变更数:', stats.totalChanges);
|
||||
console.log('新增实体:', stats.addedEntities);
|
||||
console.log('删除实体:', stats.removedEntities);
|
||||
console.log('新增组件:', stats.addedComponents);
|
||||
console.log('更新组件:', stats.updatedComponents);
|
||||
```
|
||||
|
||||
#### 4. 序列化增量数据
|
||||
|
||||
```typescript
|
||||
// JSON格式(默认)
|
||||
const jsonData = IncrementalSerializer.serializeIncremental(incremental, {
|
||||
format: 'json'
|
||||
});
|
||||
|
||||
// 二进制格式(更小的体积,更高性能)
|
||||
const binaryData = IncrementalSerializer.serializeIncremental(incremental, {
|
||||
format: 'binary'
|
||||
});
|
||||
|
||||
// 美化JSON输出(便于调试)
|
||||
const prettyJson = IncrementalSerializer.serializeIncremental(incremental, {
|
||||
format: 'json',
|
||||
pretty: true
|
||||
});
|
||||
|
||||
// 发送或保存
|
||||
socket.send(binaryData); // 网络传输使用二进制
|
||||
localStorage.setItem('changes', jsonData); // 本地存储可用JSON
|
||||
```
|
||||
|
||||
#### 5. 应用增量变更
|
||||
|
||||
```typescript
|
||||
// 在另一个场景应用变更
|
||||
const otherScene = new Scene();
|
||||
|
||||
// 直接应用增量对象
|
||||
otherScene.applyIncremental(incremental);
|
||||
|
||||
// 从JSON字符串应用
|
||||
const jsonData = IncrementalSerializer.serializeIncremental(incremental, { format: 'json' });
|
||||
otherScene.applyIncremental(jsonData);
|
||||
|
||||
// 从二进制Uint8Array应用
|
||||
const binaryData = IncrementalSerializer.serializeIncremental(incremental, { format: 'binary' });
|
||||
otherScene.applyIncremental(binaryData);
|
||||
```
|
||||
|
||||
### 增量快照管理
|
||||
|
||||
#### 更新快照基准
|
||||
|
||||
在应用增量变更后,可以更新快照基准:
|
||||
|
||||
```typescript
|
||||
// 创建初始快照
|
||||
scene.createIncrementalSnapshot();
|
||||
|
||||
// 第一次修改
|
||||
entity.addComponent(new VelocityComponent(5, 0));
|
||||
const incremental1 = scene.serializeIncremental();
|
||||
|
||||
// 更新基准(将当前状态设为新的基准)
|
||||
scene.updateIncrementalSnapshot();
|
||||
|
||||
// 第二次修改(增量将基于更新后的基准)
|
||||
entity.getComponent(VelocityComponent).dx = 10;
|
||||
const incremental2 = scene.serializeIncremental();
|
||||
```
|
||||
|
||||
#### 清除快照
|
||||
|
||||
```typescript
|
||||
// 释放快照占用的内存
|
||||
scene.clearIncrementalSnapshot();
|
||||
|
||||
// 检查是否有快照
|
||||
if (scene.hasIncrementalSnapshot()) {
|
||||
console.log('存在增量快照');
|
||||
}
|
||||
```
|
||||
|
||||
### 增量序列化选项
|
||||
|
||||
```typescript
|
||||
interface IncrementalSerializationOptions {
|
||||
// 是否进行组件数据的深度对比
|
||||
// 默认true,设为false可提升性能但可能漏掉组件内部字段变更
|
||||
deepComponentComparison?: boolean;
|
||||
|
||||
// 是否跟踪场景数据变更
|
||||
// 默认true
|
||||
trackSceneData?: boolean;
|
||||
|
||||
// 是否压缩快照(使用JSON序列化)
|
||||
// 默认false
|
||||
compressSnapshot?: boolean;
|
||||
|
||||
// 序列化格式
|
||||
// 'json': JSON格式(可读性好,方便调试)
|
||||
// 'binary': MessagePack二进制格式(体积小,性能高)
|
||||
// 默认 'json'
|
||||
format?: 'json' | 'binary';
|
||||
|
||||
// 是否美化JSON输出(仅在format='json'时有效)
|
||||
// 默认false
|
||||
pretty?: boolean;
|
||||
}
|
||||
|
||||
// 使用选项
|
||||
scene.createIncrementalSnapshot({
|
||||
deepComponentComparison: true,
|
||||
trackSceneData: true
|
||||
});
|
||||
```
|
||||
|
||||
### 增量数据结构
|
||||
|
||||
增量快照包含以下变更类型:
|
||||
|
||||
```typescript
|
||||
interface IncrementalSnapshot {
|
||||
version: number; // 快照版本号
|
||||
timestamp: number; // 时间戳
|
||||
sceneName: string; // 场景名称
|
||||
baseVersion: number; // 基础版本号
|
||||
entityChanges: EntityChange[]; // 实体变更
|
||||
componentChanges: ComponentChange[]; // 组件变更
|
||||
sceneDataChanges: SceneDataChange[]; // 场景数据变更
|
||||
}
|
||||
|
||||
// 变更操作类型
|
||||
enum ChangeOperation {
|
||||
EntityAdded = 'entity_added',
|
||||
EntityRemoved = 'entity_removed',
|
||||
EntityUpdated = 'entity_updated',
|
||||
ComponentAdded = 'component_added',
|
||||
ComponentRemoved = 'component_removed',
|
||||
ComponentUpdated = 'component_updated',
|
||||
SceneDataUpdated = 'scene_data_updated'
|
||||
}
|
||||
```
|
||||
|
||||
## 版本迁移
|
||||
|
||||
当组件结构发生变化时,版本迁移系统可以自动升级旧版本的存档数据。
|
||||
|
||||
### 注册迁移函数
|
||||
|
||||
```typescript
|
||||
import { VersionMigrationManager } from '@esengine/ecs-framework';
|
||||
|
||||
// 假设 PlayerComponent v1 有 hp 字段
|
||||
// v2 改为 health 和 maxHealth 字段
|
||||
|
||||
// 注册从版本1到版本2的迁移
|
||||
VersionMigrationManager.registerComponentMigration(
|
||||
'Player',
|
||||
1, // 从版本
|
||||
2, // 到版本
|
||||
(data) => {
|
||||
// 迁移逻辑
|
||||
const newData = {
|
||||
...data,
|
||||
health: data.hp,
|
||||
maxHealth: data.hp,
|
||||
};
|
||||
delete newData.hp;
|
||||
return newData;
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### 使用迁移构建器
|
||||
|
||||
```typescript
|
||||
import { MigrationBuilder } from '@esengine/ecs-framework';
|
||||
|
||||
new MigrationBuilder()
|
||||
.forComponent('Player')
|
||||
.fromVersionToVersion(2, 3)
|
||||
.migrate((data) => {
|
||||
// 从版本2迁移到版本3
|
||||
data.experience = data.exp || 0;
|
||||
delete data.exp;
|
||||
return data;
|
||||
});
|
||||
```
|
||||
|
||||
### 场景级迁移
|
||||
|
||||
```typescript
|
||||
// 注册场景级迁移
|
||||
VersionMigrationManager.registerSceneMigration(
|
||||
1, // 从版本
|
||||
2, // 到版本
|
||||
(scene) => {
|
||||
// 迁移场景结构
|
||||
scene.metadata = {
|
||||
...scene.metadata,
|
||||
migratedFrom: 1
|
||||
};
|
||||
return scene;
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### 检查迁移路径
|
||||
|
||||
```typescript
|
||||
// 检查是否可以迁移
|
||||
const canMigrate = VersionMigrationManager.canMigrateComponent(
|
||||
'Player',
|
||||
1, // 从版本
|
||||
3 // 到版本
|
||||
);
|
||||
|
||||
if (canMigrate) {
|
||||
// 可以安全迁移
|
||||
scene.deserialize(oldSaveData);
|
||||
}
|
||||
|
||||
// 获取迁移路径
|
||||
const path = VersionMigrationManager.getComponentMigrationPath('Player');
|
||||
console.log('可用迁移版本:', path); // [1, 2, 3]
|
||||
```
|
||||
|
||||
## 使用场景
|
||||
|
||||
### 游戏存档系统
|
||||
|
||||
```typescript
|
||||
class SaveSystem {
|
||||
private static SAVE_KEY = 'game_save';
|
||||
|
||||
// 保存游戏
|
||||
public static saveGame(scene: Scene): void {
|
||||
const saveData = scene.serialize({
|
||||
format: 'json',
|
||||
pretty: false
|
||||
});
|
||||
|
||||
localStorage.setItem(this.SAVE_KEY, saveData);
|
||||
console.log('游戏已保存');
|
||||
}
|
||||
|
||||
// 加载游戏
|
||||
public static loadGame(scene: Scene): boolean {
|
||||
const saveData = localStorage.getItem(this.SAVE_KEY);
|
||||
if (saveData) {
|
||||
scene.deserialize(saveData, {
|
||||
strategy: 'replace'
|
||||
});
|
||||
console.log('游戏已加载');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否有存档
|
||||
public static hasSave(): boolean {
|
||||
return localStorage.getItem(this.SAVE_KEY) !== null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 网络同步
|
||||
|
||||
```typescript
|
||||
class NetworkSync {
|
||||
private baseSnapshot?: any;
|
||||
private syncInterval: number = 100; // 100ms同步一次
|
||||
|
||||
constructor(private scene: Scene, private socket: WebSocket) {
|
||||
this.setupSync();
|
||||
}
|
||||
|
||||
private setupSync(): void {
|
||||
// 创建基础快照
|
||||
this.scene.createIncrementalSnapshot();
|
||||
|
||||
// 定期发送增量
|
||||
setInterval(() => {
|
||||
this.sendIncremental();
|
||||
}, this.syncInterval);
|
||||
|
||||
// 接收远程增量
|
||||
this.socket.onmessage = (event) => {
|
||||
this.receiveIncremental(event.data);
|
||||
};
|
||||
}
|
||||
|
||||
private sendIncremental(): void {
|
||||
const incremental = this.scene.serializeIncremental();
|
||||
const stats = IncrementalSerializer.getIncrementalStats(incremental);
|
||||
|
||||
// 只在有变更时发送
|
||||
if (stats.totalChanges > 0) {
|
||||
// 使用二进制格式减少网络传输量
|
||||
const binaryData = IncrementalSerializer.serializeIncremental(incremental, {
|
||||
format: 'binary'
|
||||
});
|
||||
this.socket.send(binaryData);
|
||||
|
||||
// 更新基准
|
||||
this.scene.updateIncrementalSnapshot();
|
||||
}
|
||||
}
|
||||
|
||||
private receiveIncremental(data: ArrayBuffer): void {
|
||||
// 直接应用二进制数据(ArrayBuffer 转 Uint8Array)
|
||||
const uint8Array = new Uint8Array(data);
|
||||
this.scene.applyIncremental(uint8Array);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 撤销/重做系统
|
||||
|
||||
```typescript
|
||||
class UndoRedoSystem {
|
||||
private history: IncrementalSnapshot[] = [];
|
||||
private currentIndex: number = -1;
|
||||
private maxHistory: number = 50;
|
||||
|
||||
constructor(private scene: Scene) {
|
||||
// 创建初始快照
|
||||
this.scene.createIncrementalSnapshot();
|
||||
this.saveState('Initial');
|
||||
}
|
||||
|
||||
// 保存当前状态
|
||||
public saveState(label: string): void {
|
||||
const incremental = this.scene.serializeIncremental();
|
||||
|
||||
// 删除当前位置之后的历史
|
||||
this.history = this.history.slice(0, this.currentIndex + 1);
|
||||
|
||||
// 添加新状态
|
||||
this.history.push(incremental);
|
||||
this.currentIndex++;
|
||||
|
||||
// 限制历史记录数量
|
||||
if (this.history.length > this.maxHistory) {
|
||||
this.history.shift();
|
||||
this.currentIndex--;
|
||||
}
|
||||
|
||||
// 更新快照基准
|
||||
this.scene.updateIncrementalSnapshot();
|
||||
}
|
||||
|
||||
// 撤销
|
||||
public undo(): boolean {
|
||||
if (this.currentIndex > 0) {
|
||||
this.currentIndex--;
|
||||
const incremental = this.history[this.currentIndex];
|
||||
this.scene.applyIncremental(incremental);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 重做
|
||||
public redo(): boolean {
|
||||
if (this.currentIndex < this.history.length - 1) {
|
||||
this.currentIndex++;
|
||||
const incremental = this.history[this.currentIndex];
|
||||
this.scene.applyIncremental(incremental);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public canUndo(): boolean {
|
||||
return this.currentIndex > 0;
|
||||
}
|
||||
|
||||
public canRedo(): boolean {
|
||||
return this.currentIndex < this.history.length - 1;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 关卡编辑器
|
||||
|
||||
```typescript
|
||||
class LevelEditor {
|
||||
// 导出关卡
|
||||
public exportLevel(scene: Scene, filename: string): void {
|
||||
const levelData = scene.serialize({
|
||||
format: 'json',
|
||||
pretty: true,
|
||||
includeMetadata: true
|
||||
});
|
||||
|
||||
// 浏览器环境
|
||||
const blob = new Blob([levelData], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// 导入关卡
|
||||
public importLevel(scene: Scene, fileContent: string): void {
|
||||
scene.deserialize(fileContent, {
|
||||
strategy: 'replace'
|
||||
});
|
||||
}
|
||||
|
||||
// 验证关卡数据
|
||||
public validateLevel(saveData: string): boolean {
|
||||
const validation = SceneSerializer.validate(saveData);
|
||||
if (!validation.valid) {
|
||||
console.error('关卡数据无效:', validation.errors);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 获取关卡信息(不完全反序列化)
|
||||
public getLevelInfo(saveData: string): any {
|
||||
const info = SceneSerializer.getInfo(saveData);
|
||||
return info;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 性能优化建议
|
||||
|
||||
### 1. 选择合适的格式
|
||||
|
||||
- **开发阶段**:使用JSON格式,便于调试和查看
|
||||
- **生产环境**:使用Binary格式,减少30-50%的数据大小
|
||||
|
||||
### 2. 按需序列化
|
||||
|
||||
```typescript
|
||||
// 只序列化需要持久化的组件
|
||||
const saveData = scene.serialize({
|
||||
format: 'binary',
|
||||
components: [PlayerComponent, InventoryComponent, QuestComponent]
|
||||
});
|
||||
```
|
||||
|
||||
### 3. 增量序列化优化
|
||||
|
||||
```typescript
|
||||
// 对于高频同步,关闭深度对比以提升性能
|
||||
scene.createIncrementalSnapshot({
|
||||
deepComponentComparison: false // 只检测组件的添加/删除
|
||||
});
|
||||
```
|
||||
|
||||
### 4. 批量操作
|
||||
|
||||
```typescript
|
||||
// 批量修改后再序列化
|
||||
scene.entities.buffer.forEach(entity => {
|
||||
// 批量修改
|
||||
});
|
||||
|
||||
// 一次性序列化所有变更
|
||||
const incremental = scene.serializeIncremental();
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 明确序列化字段
|
||||
|
||||
```typescript
|
||||
// 明确标记需要序列化的字段
|
||||
@ECSComponent('Player')
|
||||
@Serializable({ version: 1 })
|
||||
class PlayerComponent extends Component {
|
||||
@Serialize()
|
||||
public name: string = '';
|
||||
|
||||
@Serialize()
|
||||
public level: number = 1;
|
||||
|
||||
// 运行时数据不序列化
|
||||
private _cachedSprite: any = null;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 使用版本控制
|
||||
|
||||
```typescript
|
||||
// 为组件指定版本
|
||||
@Serializable({ version: 2 })
|
||||
class PlayerComponent extends Component {
|
||||
// 版本2的字段
|
||||
}
|
||||
|
||||
// 注册迁移函数确保兼容性
|
||||
VersionMigrationManager.registerComponentMigration('Player', 1, 2, migrateV1ToV2);
|
||||
```
|
||||
|
||||
### 3. 避免循环引用
|
||||
|
||||
```typescript
|
||||
// 不要在组件中直接引用其他实体
|
||||
@ECSComponent('Follower')
|
||||
@Serializable({ version: 1 })
|
||||
class FollowerComponent extends Component {
|
||||
// 存储实体ID而不是实体引用
|
||||
@Serialize()
|
||||
public targetId: number = 0;
|
||||
|
||||
// 通过场景查找目标实体
|
||||
public getTarget(scene: Scene): Entity | null {
|
||||
return scene.entities.findEntityById(this.targetId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 压缩大数据
|
||||
|
||||
```typescript
|
||||
// 对于大型数据结构,使用自定义序列化
|
||||
@ECSComponent('LargeData')
|
||||
@Serializable({ version: 1 })
|
||||
class LargeDataComponent extends Component {
|
||||
@Serialize({
|
||||
serializer: (data: LargeObject) => compressData(data),
|
||||
deserializer: (data: CompressedData) => decompressData(data)
|
||||
})
|
||||
public data: LargeObject;
|
||||
}
|
||||
```
|
||||
|
||||
## API参考
|
||||
|
||||
### 全量序列化API
|
||||
|
||||
- [`Scene.serialize()`](/api/classes/Scene#serialize) - 序列化场景
|
||||
- [`Scene.deserialize()`](/api/classes/Scene#deserialize) - 反序列化场景
|
||||
- [`SceneSerializer`](/api/classes/SceneSerializer) - 场景序列化器
|
||||
- [`ComponentSerializer`](/api/classes/ComponentSerializer) - 组件序列化器
|
||||
|
||||
### 增量序列化API
|
||||
|
||||
- [`Scene.createIncrementalSnapshot()`](/api/classes/Scene#createincrementalsnapshot) - 创建基础快照
|
||||
- [`Scene.serializeIncremental()`](/api/classes/Scene#serializeincremental) - 获取增量变更
|
||||
- [`Scene.applyIncremental()`](/api/classes/Scene#applyincremental) - 应用增量变更(支持IncrementalSnapshot对象、JSON字符串或二进制Uint8Array)
|
||||
- [`Scene.updateIncrementalSnapshot()`](/api/classes/Scene#updateincrementalsnapshot) - 更新快照基准
|
||||
- [`Scene.clearIncrementalSnapshot()`](/api/classes/Scene#clearincrementalsnapshot) - 清除快照
|
||||
- [`Scene.hasIncrementalSnapshot()`](/api/classes/Scene#hasincrementalsnapshot) - 检查是否有快照
|
||||
- [`IncrementalSerializer`](/api/classes/IncrementalSerializer) - 增量序列化器
|
||||
- [`IncrementalSnapshot`](/api/interfaces/IncrementalSnapshot) - 增量快照接口
|
||||
- [`IncrementalSerializationOptions`](/api/interfaces/IncrementalSerializationOptions) - 增量序列化选项
|
||||
- [`IncrementalSerializationFormat`](/api/type-aliases/IncrementalSerializationFormat) - 序列化格式类型
|
||||
|
||||
### 版本迁移API
|
||||
|
||||
- [`VersionMigrationManager`](/api/classes/VersionMigrationManager) - 版本迁移管理器
|
||||
- `VersionMigrationManager.registerComponentMigration()` - 注册组件迁移
|
||||
- `VersionMigrationManager.registerSceneMigration()` - 注册场景迁移
|
||||
- `VersionMigrationManager.canMigrateComponent()` - 检查是否可以迁移
|
||||
- `VersionMigrationManager.getComponentMigrationPath()` - 获取迁移路径
|
||||
|
||||
序列化系统是构建完整游戏的重要基础设施,合理使用可以实现强大的功能,如存档系统、网络同步、关卡编辑器等。
|
||||
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) - 在系统中使用服务
|
||||
650
docs/guide/system.md
Normal file
650
docs/guide/system.md
Normal file
@@ -0,0 +1,650 @@
|
||||
# 系统架构
|
||||
|
||||
在 ECS 架构中,系统(System)是处理业务逻辑的地方。系统负责对拥有特定组件组合的实体执行操作,是 ECS 架构的逻辑处理单元。
|
||||
|
||||
## 基本概念
|
||||
|
||||
系统是继承自 `EntitySystem` 抽象基类的具体类,用于:
|
||||
- 定义实体的处理逻辑(如移动、碰撞检测、渲染等)
|
||||
- 根据组件组合筛选需要处理的实体
|
||||
- 提供生命周期管理和性能监控
|
||||
- 管理实体的添加、移除事件
|
||||
|
||||
## 系统类型
|
||||
|
||||
框架提供了几种不同类型的系统基类:
|
||||
|
||||
### EntitySystem - 基础系统
|
||||
|
||||
最基础的系统类,所有其他系统都继承自它:
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, ECSSystem, Matcher } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSSystem('Movement')
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// 使用 Matcher 定义需要处理的实体条件
|
||||
super(Matcher.all(Position, Velocity));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const position = entity.getComponent(Position);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
if (position && velocity) {
|
||||
position.x += velocity.dx * Time.deltaTime;
|
||||
position.y += velocity.dy * Time.deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ProcessingSystem - 处理系统
|
||||
|
||||
适用于不需要逐个处理实体的系统:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Physics')
|
||||
class PhysicsSystem extends ProcessingSystem {
|
||||
constructor() {
|
||||
super(); // 不需要指定 Matcher
|
||||
}
|
||||
|
||||
public processSystem(): void {
|
||||
// 执行物理世界步进
|
||||
this.physicsWorld.step(Time.deltaTime);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### PassiveSystem - 被动系统
|
||||
|
||||
被动系统不进行主动处理,主要用于监听实体的添加和移除事件:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('EntityTracker')
|
||||
class EntityTrackerSystem extends PassiveSystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Health));
|
||||
}
|
||||
|
||||
protected onAdded(entity: Entity): void {
|
||||
console.log(`生命值实体被添加: ${entity.name}`);
|
||||
}
|
||||
|
||||
protected onRemoved(entity: Entity): void {
|
||||
console.log(`生命值实体被移除: ${entity.name}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### IntervalSystem - 间隔系统
|
||||
|
||||
按固定时间间隔执行的系统:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('AutoSave')
|
||||
class AutoSaveSystem extends IntervalSystem {
|
||||
constructor() {
|
||||
// 每 5 秒执行一次
|
||||
super(5.0, Matcher.all(SaveData));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
console.log('执行自动保存...');
|
||||
// 保存游戏数据
|
||||
this.saveGameData(entities);
|
||||
}
|
||||
|
||||
private saveGameData(entities: readonly Entity[]): void {
|
||||
// 保存逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### WorkerEntitySystem - 多线程系统
|
||||
|
||||
基于Web Worker的多线程处理系统,适用于计算密集型任务,能够充分利用多核CPU性能。
|
||||
|
||||
Worker系统提供了真正的并行计算能力,支持SharedArrayBuffer优化,并具有自动降级支持。特别适合物理模拟、粒子系统、AI计算等场景。
|
||||
|
||||
**详细内容请参考:[Worker系统](/guide/worker-system)**
|
||||
|
||||
## 实体匹配器 (Matcher)
|
||||
|
||||
Matcher 用于定义系统需要处理哪些实体。它提供了灵活的条件组合:
|
||||
|
||||
### 基本匹配条件
|
||||
|
||||
```typescript
|
||||
// 必须同时拥有 Position 和 Velocity 组件
|
||||
const matcher1 = Matcher.all(Position, Velocity);
|
||||
|
||||
// 至少拥有 Health 或 Shield 组件之一
|
||||
const matcher2 = Matcher.any(Health, Shield);
|
||||
|
||||
// 不能拥有 Dead 组件
|
||||
const matcher3 = Matcher.none(Dead);
|
||||
```
|
||||
|
||||
### 复合匹配条件
|
||||
|
||||
```typescript
|
||||
// 复杂的组合条件
|
||||
const complexMatcher = Matcher.all(Position, Velocity)
|
||||
.any(Player, Enemy)
|
||||
.none(Dead, Disabled);
|
||||
|
||||
@ECSSystem('Combat')
|
||||
class CombatSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(complexMatcher);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 特殊匹配条件
|
||||
|
||||
```typescript
|
||||
// 按标签匹配
|
||||
const tagMatcher = Matcher.byTag(1); // 匹配标签为 1 的实体
|
||||
|
||||
// 按名称匹配
|
||||
const nameMatcher = Matcher.byName("Player"); // 匹配名称为 "Player" 的实体
|
||||
|
||||
// 单组件匹配
|
||||
const componentMatcher = Matcher.byComponent(Health); // 匹配拥有 Health 组件的实体
|
||||
```
|
||||
|
||||
## 系统生命周期
|
||||
|
||||
系统提供了完整的生命周期回调:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Example')
|
||||
class ExampleSystem extends EntitySystem {
|
||||
protected onInitialize(): void {
|
||||
console.log('系统初始化');
|
||||
// 系统被添加到场景时调用,用于初始化资源
|
||||
}
|
||||
|
||||
protected onBegin(): void {
|
||||
// 每帧处理开始前调用
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 主要的处理逻辑
|
||||
for (const entity of entities) {
|
||||
// 处理每个实体
|
||||
}
|
||||
}
|
||||
|
||||
protected lateProcess(entities: readonly Entity[]): void {
|
||||
// 主处理之后的后期处理
|
||||
}
|
||||
|
||||
protected onEnd(): void {
|
||||
// 每帧处理结束后调用
|
||||
}
|
||||
|
||||
protected onDestroy(): void {
|
||||
console.log('系统销毁');
|
||||
// 系统从场景移除时调用,用于清理资源
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 实体事件监听
|
||||
|
||||
系统可以监听实体的添加和移除事件:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('EnemyManager')
|
||||
class EnemyManagerSystem extends EntitySystem {
|
||||
private enemyCount = 0;
|
||||
|
||||
constructor() {
|
||||
super(Matcher.all(Enemy, Health));
|
||||
}
|
||||
|
||||
protected onAdded(entity: Entity): void {
|
||||
this.enemyCount++;
|
||||
console.log(`敌人加入战斗,当前敌人数量: ${this.enemyCount}`);
|
||||
|
||||
// 可以在这里为新敌人设置初始状态
|
||||
const health = entity.getComponent(Health);
|
||||
if (health) {
|
||||
health.current = health.max;
|
||||
}
|
||||
}
|
||||
|
||||
protected onRemoved(entity: Entity): void {
|
||||
this.enemyCount--;
|
||||
console.log(`敌人被移除,剩余敌人数量: ${this.enemyCount}`);
|
||||
|
||||
// 检查是否所有敌人都被消灭
|
||||
if (this.enemyCount === 0) {
|
||||
this.scene?.eventSystem.emitSync('all_enemies_defeated');
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 系统属性和方法
|
||||
|
||||
### 重要属性
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Example')
|
||||
class ExampleSystem extends EntitySystem {
|
||||
showSystemInfo(): void {
|
||||
console.log(`系统名称: ${this.systemName}`); // 系统名称
|
||||
console.log(`更新顺序: ${this.updateOrder}`); // 更新时序
|
||||
console.log(`是否启用: ${this.enabled}`); // 启用状态
|
||||
console.log(`实体数量: ${this.entities.length}`); // 匹配的实体数量
|
||||
console.log(`所属场景: ${this.scene?.name}`); // 所属场景
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 实体访问
|
||||
|
||||
```typescript
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 方式1:使用参数中的实体列表
|
||||
for (const entity of entities) {
|
||||
// 处理实体
|
||||
}
|
||||
|
||||
// 方式2:使用 this.entities 属性(与参数相同)
|
||||
for (const entity of this.entities) {
|
||||
// 处理实体
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 控制系统执行
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Conditional')
|
||||
class ConditionalSystem extends EntitySystem {
|
||||
private shouldProcess = true;
|
||||
|
||||
protected onCheckProcessing(): boolean {
|
||||
// 返回 false 时跳过本次处理
|
||||
return this.shouldProcess && this.entities.length > 0;
|
||||
}
|
||||
|
||||
public pause(): void {
|
||||
this.shouldProcess = false;
|
||||
}
|
||||
|
||||
public resume(): void {
|
||||
this.shouldProcess = true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 事件系统集成
|
||||
|
||||
系统可以方便地监听和发送事件:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('GameLogic')
|
||||
class GameLogicSystem extends EntitySystem {
|
||||
protected onInitialize(): void {
|
||||
// 添加事件监听器(系统销毁时自动清理)
|
||||
this.addEventListener('player_died', this.onPlayerDied.bind(this));
|
||||
this.addEventListener('level_complete', this.onLevelComplete.bind(this));
|
||||
}
|
||||
|
||||
private onPlayerDied(data: any): void {
|
||||
console.log('玩家死亡,重新开始游戏');
|
||||
// 处理玩家死亡逻辑
|
||||
}
|
||||
|
||||
private onLevelComplete(data: any): void {
|
||||
console.log('关卡完成,加载下一关');
|
||||
// 处理关卡完成逻辑
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 在处理过程中发送事件
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health);
|
||||
if (health && health.current <= 0) {
|
||||
this.scene?.eventSystem.emitSync('entity_died', { entity });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 性能监控
|
||||
|
||||
系统内置了性能监控功能:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Performance')
|
||||
class PerformanceSystem extends EntitySystem {
|
||||
protected onEnd(): void {
|
||||
// 获取性能数据
|
||||
const perfData = this.getPerformanceData();
|
||||
if (perfData) {
|
||||
console.log(`执行时间: ${perfData.executionTime.toFixed(2)}ms`);
|
||||
}
|
||||
|
||||
// 获取性能统计
|
||||
const stats = this.getPerformanceStats();
|
||||
if (stats) {
|
||||
console.log(`平均执行时间: ${stats.averageTime.toFixed(2)}ms`);
|
||||
}
|
||||
}
|
||||
|
||||
public resetPerformance(): void {
|
||||
this.resetPerformanceData();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 系统管理
|
||||
|
||||
### 添加系统到场景
|
||||
|
||||
框架提供了两种方式添加系统:传入实例或传入类型(自动依赖注入)。
|
||||
|
||||
```typescript
|
||||
// 在场景子类中添加系统
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 方式1:传入实例
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
|
||||
// 方式2:传入类型(自动依赖注入)
|
||||
this.addEntityProcessor(PhysicsSystem);
|
||||
|
||||
// 设置系统更新顺序
|
||||
const movementSystem = this.getSystem(MovementSystem);
|
||||
if (movementSystem) {
|
||||
movementSystem.updateOrder = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 系统依赖注入
|
||||
|
||||
系统实现了 `IService` 接口,支持通过依赖注入获取其他服务或系统:
|
||||
|
||||
```typescript
|
||||
import { ECSSystem, Injectable, Inject } from '@esengine/ecs-framework';
|
||||
|
||||
@Injectable()
|
||||
@ECSSystem('Physics')
|
||||
class PhysicsSystem extends EntitySystem {
|
||||
constructor(
|
||||
@Inject(CollisionService) private collision: CollisionService
|
||||
) {
|
||||
super(Matcher.all(Transform, RigidBody));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 使用注入的服务
|
||||
this.collision.detectCollisions(entities);
|
||||
}
|
||||
|
||||
// 实现 IService 接口的 dispose 方法
|
||||
public dispose(): void {
|
||||
// 清理资源
|
||||
}
|
||||
}
|
||||
|
||||
// 使用时传入类型即可,框架会自动注入依赖
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 自动依赖注入
|
||||
this.addEntityProcessor(PhysicsSystem);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
注意事项:
|
||||
- 使用 `@Injectable()` 装饰器标记需要依赖注入的系统
|
||||
- 在构造函数参数中使用 `@Inject()` 装饰器声明依赖
|
||||
- 系统必须实现 `dispose()` 方法(IService 接口要求)
|
||||
- 使用 `addEntityProcessor(类型)` 而不是 `addSystem(new 类型())` 来启用依赖注入
|
||||
|
||||
### 系统更新顺序
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Input')
|
||||
class InputSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(InputComponent));
|
||||
this.updateOrder = -100; // 输入系统优先执行
|
||||
}
|
||||
}
|
||||
|
||||
@ECSSystem('Physics')
|
||||
class PhysicsSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(RigidBody));
|
||||
this.updateOrder = 0; // 默认顺序
|
||||
}
|
||||
}
|
||||
|
||||
@ECSSystem('Render')
|
||||
class RenderSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Sprite, Transform));
|
||||
this.updateOrder = 100; // 渲染系统最后执行
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 复杂系统示例
|
||||
|
||||
### 碰撞检测系统
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Collision')
|
||||
class CollisionSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Transform, Collider));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 简单的 n² 碰撞检测
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
for (let j = i + 1; j < entities.length; j++) {
|
||||
this.checkCollision(entities[i], entities[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private checkCollision(entityA: Entity, entityB: Entity): void {
|
||||
const transformA = entityA.getComponent(Transform);
|
||||
const transformB = entityB.getComponent(Transform);
|
||||
const colliderA = entityA.getComponent(Collider);
|
||||
const colliderB = entityB.getComponent(Collider);
|
||||
|
||||
if (this.isColliding(transformA, colliderA, transformB, colliderB)) {
|
||||
// 发送碰撞事件
|
||||
this.scene?.eventSystem.emitSync('collision', {
|
||||
entityA,
|
||||
entityB
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private isColliding(transformA: Transform, colliderA: Collider,
|
||||
transformB: Transform, colliderB: Collider): boolean {
|
||||
// 碰撞检测逻辑
|
||||
return false; // 简化示例
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 状态机系统
|
||||
|
||||
```typescript
|
||||
@ECSSystem('StateMachine')
|
||||
class StateMachineSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(StateMachine));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const stateMachine = entity.getComponent(StateMachine);
|
||||
if (stateMachine) {
|
||||
stateMachine.updateTimer(Time.deltaTime);
|
||||
this.updateState(entity, stateMachine);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateState(entity: Entity, stateMachine: StateMachine): void {
|
||||
switch (stateMachine.currentState) {
|
||||
case EntityState.Idle:
|
||||
this.handleIdleState(entity, stateMachine);
|
||||
break;
|
||||
case EntityState.Moving:
|
||||
this.handleMovingState(entity, stateMachine);
|
||||
break;
|
||||
case EntityState.Attacking:
|
||||
this.handleAttackingState(entity, stateMachine);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private handleIdleState(entity: Entity, stateMachine: StateMachine): void {
|
||||
// 空闲状态逻辑
|
||||
}
|
||||
|
||||
private handleMovingState(entity: Entity, stateMachine: StateMachine): void {
|
||||
// 移动状态逻辑
|
||||
}
|
||||
|
||||
private handleAttackingState(entity: Entity, stateMachine: StateMachine): void {
|
||||
// 攻击状态逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 系统单一职责
|
||||
|
||||
```typescript
|
||||
// ✅ 好的系统设计 - 职责单一
|
||||
@ECSSystem('Movement')
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Position, Velocity));
|
||||
}
|
||||
}
|
||||
|
||||
@ECSSystem('Rendering')
|
||||
class RenderingSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Sprite, Transform));
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ 避免的系统设计 - 职责过多
|
||||
@ECSSystem('GameSystem')
|
||||
class GameSystem extends EntitySystem {
|
||||
// 一个系统处理移动、渲染、音效等多种逻辑
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 使用装饰器
|
||||
|
||||
**必须使用 `@ECSSystem` 装饰器**:
|
||||
|
||||
```typescript
|
||||
// ✅ 正确的用法
|
||||
@ECSSystem('Physics')
|
||||
class PhysicsSystem extends EntitySystem {
|
||||
// 系统实现
|
||||
}
|
||||
|
||||
// ❌ 错误的用法 - 没有装饰器
|
||||
class BadSystem extends EntitySystem {
|
||||
// 这样定义的系统可能在生产环境出现问题
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 合理的更新顺序
|
||||
|
||||
```typescript
|
||||
// 按逻辑顺序设置系统的更新时序
|
||||
@ECSSystem('Input')
|
||||
class InputSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super();
|
||||
this.updateOrder = -100; // 最先处理输入
|
||||
}
|
||||
}
|
||||
|
||||
@ECSSystem('Logic')
|
||||
class GameLogicSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super();
|
||||
this.updateOrder = 0; // 处理游戏逻辑
|
||||
}
|
||||
}
|
||||
|
||||
@ECSSystem('Render')
|
||||
class RenderSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super();
|
||||
this.updateOrder = 100; // 最后进行渲染
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 避免在系统间直接引用
|
||||
|
||||
```typescript
|
||||
// ❌ 避免:系统间直接引用
|
||||
@ECSSystem('Bad')
|
||||
class BadSystem extends EntitySystem {
|
||||
private otherSystem: SomeOtherSystem; // 避免直接引用其他系统
|
||||
}
|
||||
|
||||
// ✅ 推荐:通过事件系统通信
|
||||
@ECSSystem('Good')
|
||||
class GoodSystem extends EntitySystem {
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 通过事件系统与其他系统通信
|
||||
this.scene?.eventSystem.emitSync('data_updated', { entities });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 及时清理资源
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Resource')
|
||||
class ResourceSystem extends EntitySystem {
|
||||
private resources: Map<string, any> = new Map();
|
||||
|
||||
protected onDestroy(): void {
|
||||
// 清理资源
|
||||
for (const [key, resource] of this.resources) {
|
||||
if (resource.dispose) {
|
||||
resource.dispose();
|
||||
}
|
||||
}
|
||||
this.resources.clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
系统是 ECS 架构的逻辑处理核心,正确设计和使用系统能让你的游戏代码更加模块化、高效和易于维护。
|
||||
553
docs/guide/time-and-timers.md
Normal file
553
docs/guide/time-and-timers.md
Normal file
@@ -0,0 +1,553 @@
|
||||
# 时间和定时器系统
|
||||
|
||||
ECS 框架提供了完整的时间管理和定时器系统,包括时间缩放、帧时间计算和灵活的定时器调度功能。
|
||||
|
||||
## Time 类
|
||||
|
||||
Time 类是框架的时间管理核心,提供了游戏时间相关的所有功能。
|
||||
|
||||
### 基本时间属性
|
||||
|
||||
```typescript
|
||||
import { Time } from '@esengine/ecs-framework';
|
||||
|
||||
class GameSystem extends EntitySystem {
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 获取帧时间(秒)
|
||||
const deltaTime = Time.deltaTime;
|
||||
|
||||
// 获取未缩放的帧时间
|
||||
const unscaledDelta = Time.unscaledDeltaTime;
|
||||
|
||||
// 获取游戏总时间
|
||||
const totalTime = Time.totalTime;
|
||||
|
||||
// 获取当前帧数
|
||||
const frameCount = Time.frameCount;
|
||||
|
||||
console.log(`第 ${frameCount} 帧,帧时间: ${deltaTime}s,总时间: ${totalTime}s`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 时间缩放
|
||||
|
||||
Time 类支持时间缩放功能,可以实现慢动作、快进等效果:
|
||||
|
||||
```typescript
|
||||
class TimeControlSystem extends EntitySystem {
|
||||
public enableSlowMotion(): void {
|
||||
// 设置为慢动作(50%速度)
|
||||
Time.timeScale = 0.5;
|
||||
console.log('慢动作模式启用');
|
||||
}
|
||||
|
||||
public enableFastForward(): void {
|
||||
// 设置为快进(200%速度)
|
||||
Time.timeScale = 2.0;
|
||||
console.log('快进模式启用');
|
||||
}
|
||||
|
||||
public pauseGame(): void {
|
||||
// 暂停游戏(时间静止)
|
||||
Time.timeScale = 0;
|
||||
console.log('游戏暂停');
|
||||
}
|
||||
|
||||
public resumeNormalSpeed(): void {
|
||||
// 恢复正常速度
|
||||
Time.timeScale = 1.0;
|
||||
console.log('恢复正常速度');
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// deltaTime 会受到 timeScale 影响
|
||||
const scaledDelta = Time.deltaTime; // 受时间缩放影响
|
||||
const realDelta = Time.unscaledDeltaTime; // 不受时间缩放影响
|
||||
|
||||
for (const entity of entities) {
|
||||
const movement = entity.getComponent(Movement);
|
||||
if (movement) {
|
||||
// 使用缩放时间进行游戏逻辑更新
|
||||
movement.update(scaledDelta);
|
||||
}
|
||||
|
||||
const ui = entity.getComponent(UIComponent);
|
||||
if (ui) {
|
||||
// UI 动画使用真实时间,不受游戏时间缩放影响
|
||||
ui.update(realDelta);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 时间检查工具
|
||||
|
||||
```typescript
|
||||
class CooldownSystem extends EntitySystem {
|
||||
private lastAttackTime = 0;
|
||||
private lastSpawnTime = 0;
|
||||
|
||||
constructor() {
|
||||
super(Matcher.all(Weapon));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 检查攻击冷却
|
||||
if (Time.checkEvery(1.5, this.lastAttackTime)) {
|
||||
this.performAttack();
|
||||
this.lastAttackTime = Time.totalTime;
|
||||
}
|
||||
|
||||
// 检查生成间隔
|
||||
if (Time.checkEvery(3.0, this.lastSpawnTime)) {
|
||||
this.spawnEnemy();
|
||||
this.lastSpawnTime = Time.totalTime;
|
||||
}
|
||||
}
|
||||
|
||||
private performAttack(): void {
|
||||
console.log('执行攻击!');
|
||||
}
|
||||
|
||||
private spawnEnemy(): void {
|
||||
console.log('生成敌人!');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Core.schedule 定时器系统
|
||||
|
||||
Core 提供了强大的定时器调度功能,可以创建一次性或重复执行的定时器。
|
||||
|
||||
### 基本定时器使用
|
||||
|
||||
```typescript
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 创建一次性定时器
|
||||
this.createOneTimeTimers();
|
||||
|
||||
// 创建重复定时器
|
||||
this.createRepeatingTimers();
|
||||
|
||||
// 创建带上下文的定时器
|
||||
this.createContextTimers();
|
||||
}
|
||||
|
||||
private createOneTimeTimers(): void {
|
||||
// 2秒后执行一次
|
||||
Core.schedule(2.0, false, null, (timer) => {
|
||||
console.log('2秒延迟执行');
|
||||
});
|
||||
|
||||
// 5秒后显示提示
|
||||
Core.schedule(5.0, false, this, (timer) => {
|
||||
const scene = timer.getContext<GameScene>();
|
||||
scene.showTip('游戏提示:5秒已过!');
|
||||
});
|
||||
}
|
||||
|
||||
private createRepeatingTimers(): void {
|
||||
// 每秒重复执行
|
||||
const heartbeatTimer = Core.schedule(1.0, true, null, (timer) => {
|
||||
console.log(`游戏心跳 - 总时间: ${Time.totalTime.toFixed(1)}s`);
|
||||
});
|
||||
|
||||
// 可以保存定时器引用用于后续控制
|
||||
this.saveTimerReference(heartbeatTimer);
|
||||
}
|
||||
|
||||
private createContextTimers(): void {
|
||||
const gameData = { score: 0, level: 1 };
|
||||
|
||||
// 每2秒增加分数
|
||||
Core.schedule(2.0, true, gameData, (timer) => {
|
||||
const data = timer.getContext<typeof gameData>();
|
||||
data.score += 10;
|
||||
console.log(`分数增加!当前分数: ${data.score}`);
|
||||
});
|
||||
}
|
||||
|
||||
private saveTimerReference(timer: any): void {
|
||||
// 可以稍后停止定时器
|
||||
setTimeout(() => {
|
||||
timer.stop();
|
||||
console.log('定时器已停止');
|
||||
}, 10000); // 10秒后停止
|
||||
}
|
||||
|
||||
private showTip(message: string): void {
|
||||
console.log('提示:', message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 定时器控制
|
||||
|
||||
```typescript
|
||||
class TimerControlExample {
|
||||
private attackTimer: any;
|
||||
private spawnerTimer: any;
|
||||
|
||||
public startCombat(): void {
|
||||
// 启动攻击定时器
|
||||
this.attackTimer = Core.schedule(0.5, true, this, (timer) => {
|
||||
const self = timer.getContext<TimerControlExample>();
|
||||
self.performAttack();
|
||||
});
|
||||
|
||||
// 启动敌人生成定时器
|
||||
this.spawnerTimer = Core.schedule(3.0, true, null, (timer) => {
|
||||
this.spawnEnemy();
|
||||
});
|
||||
}
|
||||
|
||||
public stopCombat(): void {
|
||||
// 停止所有战斗相关定时器
|
||||
if (this.attackTimer) {
|
||||
this.attackTimer.stop();
|
||||
console.log('攻击定时器已停止');
|
||||
}
|
||||
|
||||
if (this.spawnerTimer) {
|
||||
this.spawnerTimer.stop();
|
||||
console.log('生成定时器已停止');
|
||||
}
|
||||
}
|
||||
|
||||
public resetAttackTimer(): void {
|
||||
// 重置攻击定时器
|
||||
if (this.attackTimer) {
|
||||
this.attackTimer.reset();
|
||||
console.log('攻击定时器已重置');
|
||||
}
|
||||
}
|
||||
|
||||
private performAttack(): void {
|
||||
console.log('执行攻击');
|
||||
}
|
||||
|
||||
private spawnEnemy(): void {
|
||||
console.log('生成敌人');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 复杂定时器场景
|
||||
|
||||
```typescript
|
||||
class AdvancedTimerUsage {
|
||||
private powerUpDuration = 0;
|
||||
private powerUpActive = false;
|
||||
|
||||
public activatePowerUp(): void {
|
||||
if (this.powerUpActive) {
|
||||
console.log('能力提升已激活');
|
||||
return;
|
||||
}
|
||||
|
||||
this.powerUpActive = true;
|
||||
this.powerUpDuration = 10; // 10秒持续时间
|
||||
|
||||
console.log('能力提升激活!');
|
||||
|
||||
// 每秒更新剩余时间
|
||||
const countdownTimer = Core.schedule(1.0, true, this, (timer) => {
|
||||
const self = timer.getContext<AdvancedTimerUsage>();
|
||||
self.powerUpDuration--;
|
||||
|
||||
console.log(`能力提升剩余时间: ${self.powerUpDuration}秒`);
|
||||
|
||||
if (self.powerUpDuration <= 0) {
|
||||
self.deactivatePowerUp();
|
||||
timer.stop(); // 停止倒计时
|
||||
}
|
||||
});
|
||||
|
||||
// 能力提升结束定时器(备用)
|
||||
Core.schedule(10.0, false, this, (timer) => {
|
||||
const self = timer.getContext<AdvancedTimerUsage>();
|
||||
if (self.powerUpActive) {
|
||||
self.deactivatePowerUp();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private deactivatePowerUp(): void {
|
||||
this.powerUpActive = false;
|
||||
this.powerUpDuration = 0;
|
||||
console.log('能力提升结束');
|
||||
}
|
||||
|
||||
// 创建波次攻击定时器
|
||||
public startWaveAttack(): void {
|
||||
let waveCount = 0;
|
||||
const maxWaves = 5;
|
||||
|
||||
const waveTimer = Core.schedule(2.0, true, { waveCount, maxWaves }, (timer) => {
|
||||
const context = timer.getContext<{ waveCount: number, maxWaves: number }>();
|
||||
context.waveCount++;
|
||||
|
||||
console.log(`第 ${context.waveCount} 波攻击!`);
|
||||
|
||||
if (context.waveCount >= context.maxWaves) {
|
||||
console.log('所有波次攻击完成');
|
||||
timer.stop();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 创建条件定时器
|
||||
public startConditionalTimer(): void {
|
||||
Core.schedule(0.1, true, this, (timer) => {
|
||||
const self = timer.getContext<AdvancedTimerUsage>();
|
||||
|
||||
// 检查某个条件
|
||||
if (self.shouldStopTimer()) {
|
||||
console.log('条件满足,停止定时器');
|
||||
timer.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
// 继续执行定时器逻辑
|
||||
self.performTimerAction();
|
||||
});
|
||||
}
|
||||
|
||||
private shouldStopTimer(): boolean {
|
||||
// 检查停止条件
|
||||
return Time.totalTime > 30; // 30秒后停止
|
||||
}
|
||||
|
||||
private performTimerAction(): void {
|
||||
console.log('执行定时器动作');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 实际应用示例
|
||||
|
||||
### 技能冷却系统
|
||||
|
||||
```typescript
|
||||
class SkillCooldownSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(SkillComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const skill = entity.getComponent(SkillComponent);
|
||||
|
||||
// 更新技能冷却
|
||||
if (skill.isOnCooldown) {
|
||||
skill.cooldownRemaining -= Time.deltaTime;
|
||||
|
||||
if (skill.cooldownRemaining <= 0) {
|
||||
skill.cooldownRemaining = 0;
|
||||
skill.isOnCooldown = false;
|
||||
console.log(`技能 ${skill.name} 冷却完成`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public useSkill(entity: Entity, skillName: string): boolean {
|
||||
const skill = entity.getComponent(SkillComponent);
|
||||
|
||||
if (skill.isOnCooldown) {
|
||||
console.log(`技能 ${skillName} 还在冷却中,剩余 ${skill.cooldownRemaining.toFixed(1)}秒`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 执行技能
|
||||
this.executeSkill(entity, skill);
|
||||
|
||||
// 开始冷却
|
||||
skill.isOnCooldown = true;
|
||||
skill.cooldownRemaining = skill.cooldownDuration;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private executeSkill(entity: Entity, skill: SkillComponent): void {
|
||||
console.log(`执行技能: ${skill.name}`);
|
||||
// 技能效果逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 游戏状态定时器
|
||||
|
||||
```typescript
|
||||
class GameStateManager {
|
||||
private gamePhase = 'preparation';
|
||||
private phaseTimer: any;
|
||||
|
||||
public startGame(): void {
|
||||
this.startPreparationPhase();
|
||||
}
|
||||
|
||||
private startPreparationPhase(): void {
|
||||
this.gamePhase = 'preparation';
|
||||
console.log('准备阶段开始 - 10秒准备时间');
|
||||
|
||||
this.phaseTimer = Core.schedule(10.0, false, this, (timer) => {
|
||||
const self = timer.getContext<GameStateManager>();
|
||||
self.startCombatPhase();
|
||||
});
|
||||
}
|
||||
|
||||
private startCombatPhase(): void {
|
||||
this.gamePhase = 'combat';
|
||||
console.log('战斗阶段开始 - 60秒战斗时间');
|
||||
|
||||
this.phaseTimer = Core.schedule(60.0, false, this, (timer) => {
|
||||
const self = timer.getContext<GameStateManager>();
|
||||
self.startResultPhase();
|
||||
});
|
||||
|
||||
// 每10秒刷新一波敌人
|
||||
Core.schedule(10.0, true, null, (timer) => {
|
||||
if (this.gamePhase === 'combat') {
|
||||
this.spawnEnemyWave();
|
||||
} else {
|
||||
timer.stop(); // 战斗阶段结束时停止刷新
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private startResultPhase(): void {
|
||||
this.gamePhase = 'result';
|
||||
console.log('结算阶段开始 - 5秒结算时间');
|
||||
|
||||
this.phaseTimer = Core.schedule(5.0, false, this, (timer) => {
|
||||
const self = timer.getContext<GameStateManager>();
|
||||
self.endGame();
|
||||
});
|
||||
}
|
||||
|
||||
private endGame(): void {
|
||||
console.log('游戏结束');
|
||||
this.gamePhase = 'ended';
|
||||
}
|
||||
|
||||
private spawnEnemyWave(): void {
|
||||
console.log('刷新敌人波次');
|
||||
}
|
||||
|
||||
public getCurrentPhase(): string {
|
||||
return this.gamePhase;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 合理使用时间类型
|
||||
|
||||
```typescript
|
||||
class MovementSystem extends EntitySystem {
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const movement = entity.getComponent(Movement);
|
||||
|
||||
// ✅ 游戏逻辑使用缩放时间
|
||||
movement.position.x += movement.velocity.x * Time.deltaTime;
|
||||
|
||||
// ✅ UI动画使用真实时间(不受游戏暂停影响)
|
||||
const ui = entity.getComponent(UIAnimation);
|
||||
if (ui) {
|
||||
ui.update(Time.unscaledDeltaTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 定时器管理
|
||||
|
||||
```typescript
|
||||
class TimerManager {
|
||||
private timers: any[] = [];
|
||||
|
||||
public createManagedTimer(duration: number, repeats: boolean, callback: () => void): any {
|
||||
const timer = Core.schedule(duration, repeats, null, callback);
|
||||
this.timers.push(timer);
|
||||
return timer;
|
||||
}
|
||||
|
||||
public stopAllTimers(): void {
|
||||
for (const timer of this.timers) {
|
||||
timer.stop();
|
||||
}
|
||||
this.timers = [];
|
||||
}
|
||||
|
||||
public cleanupCompletedTimers(): void {
|
||||
this.timers = this.timers.filter(timer => !timer.isDone);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 避免过多的定时器
|
||||
|
||||
```typescript
|
||||
// ❌ 避免:为每个实体创建定时器
|
||||
class BadExample extends EntitySystem {
|
||||
protected onAdded(entity: Entity): void {
|
||||
Core.schedule(1.0, true, entity, (timer) => {
|
||||
// 每个实体一个定时器,性能差
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 推荐:在系统中统一管理时间
|
||||
class GoodExample extends EntitySystem {
|
||||
private lastUpdateTime = 0;
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 每秒执行一次逻辑
|
||||
if (Time.checkEvery(1.0, this.lastUpdateTime)) {
|
||||
this.processAllEntities(entities);
|
||||
this.lastUpdateTime = Time.totalTime;
|
||||
}
|
||||
}
|
||||
|
||||
private processAllEntities(entities: readonly Entity[]): void {
|
||||
// 批量处理所有实体
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 定时器上下文使用
|
||||
|
||||
```typescript
|
||||
interface TimerContext {
|
||||
entityId: number;
|
||||
duration: number;
|
||||
onComplete: () => void;
|
||||
}
|
||||
|
||||
class ContextualTimerExample {
|
||||
public createEntityTimer(entityId: number, duration: number, onComplete: () => void): void {
|
||||
const context: TimerContext = {
|
||||
entityId,
|
||||
duration,
|
||||
onComplete
|
||||
};
|
||||
|
||||
Core.schedule(duration, false, context, (timer) => {
|
||||
const ctx = timer.getContext<TimerContext>();
|
||||
console.log(`实体 ${ctx.entityId} 的定时器完成`);
|
||||
ctx.onComplete();
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
时间和定时器系统是游戏开发中的重要工具,正确使用这些功能能让你的游戏逻辑更加精确和可控。
|
||||
608
docs/guide/worker-system.md
Normal file
608
docs/guide/worker-system.md
Normal file
@@ -0,0 +1,608 @@
|
||||
# Worker系统
|
||||
|
||||
Worker系统(WorkerEntitySystem)是ECS框架中基于Web Worker的多线程处理系统,专为计算密集型任务设计,能够充分利用多核CPU性能,实现真正的并行计算。
|
||||
|
||||
## 核心特性
|
||||
|
||||
- **真正的并行计算**:利用Web Worker在后台线程执行计算密集型任务
|
||||
- **自动负载均衡**:根据CPU核心数自动分配工作负载
|
||||
- **SharedArrayBuffer优化**:零拷贝数据共享,提升大规模计算性能
|
||||
- **降级支持**:不支持Worker时自动回退到主线程处理
|
||||
- **类型安全**:完整的TypeScript支持和类型检查
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 简单的物理系统示例
|
||||
|
||||
```typescript
|
||||
interface PhysicsData {
|
||||
id: number;
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
mass: number;
|
||||
radius: number;
|
||||
}
|
||||
|
||||
@ECSSystem('Physics')
|
||||
class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsData> {
|
||||
constructor() {
|
||||
super(Matcher.all(Position, Velocity, Physics), {
|
||||
enableWorker: true, // 启用Worker并行处理
|
||||
workerCount: 8, // Worker数量,系统会自动限制在硬件支持范围内
|
||||
entitiesPerWorker: 100, // 每个Worker处理的实体数量
|
||||
useSharedArrayBuffer: true, // 启用SharedArrayBuffer优化
|
||||
entityDataSize: 7, // 每个实体数据大小
|
||||
maxEntities: 10000, // 最大实体数量
|
||||
systemConfig: { // 传递给Worker的配置
|
||||
gravity: 100,
|
||||
friction: 0.95
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 数据提取:将Entity转换为可序列化的数据
|
||||
protected extractEntityData(entity: Entity): PhysicsData {
|
||||
const position = entity.getComponent(Position);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
const physics = entity.getComponent(Physics);
|
||||
|
||||
return {
|
||||
id: entity.id,
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
vx: velocity.x,
|
||||
vy: velocity.y,
|
||||
mass: physics.mass,
|
||||
radius: physics.radius
|
||||
};
|
||||
}
|
||||
|
||||
// Worker处理函数:纯函数,在Worker中执行
|
||||
protected workerProcess(
|
||||
entities: PhysicsData[],
|
||||
deltaTime: number,
|
||||
config: any
|
||||
): PhysicsData[] {
|
||||
return entities.map(entity => {
|
||||
// 应用重力
|
||||
entity.vy += config.gravity * deltaTime;
|
||||
|
||||
// 更新位置
|
||||
entity.x += entity.vx * deltaTime;
|
||||
entity.y += entity.vy * deltaTime;
|
||||
|
||||
// 应用摩擦力
|
||||
entity.vx *= config.friction;
|
||||
entity.vy *= config.friction;
|
||||
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
// 结果应用:将Worker处理结果应用回Entity
|
||||
protected applyResult(entity: Entity, result: PhysicsData): void {
|
||||
const position = entity.getComponent(Position);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
position.x = result.x;
|
||||
position.y = result.y;
|
||||
velocity.x = result.vx;
|
||||
velocity.y = result.vy;
|
||||
}
|
||||
|
||||
// SharedArrayBuffer优化支持
|
||||
protected getDefaultEntityDataSize(): number {
|
||||
return 7; // id, x, y, vx, vy, mass, radius
|
||||
}
|
||||
|
||||
protected writeEntityToBuffer(entityData: PhysicsData, offset: number): void {
|
||||
if (!this.sharedFloatArray) return;
|
||||
|
||||
this.sharedFloatArray[offset + 0] = entityData.id;
|
||||
this.sharedFloatArray[offset + 1] = entityData.x;
|
||||
this.sharedFloatArray[offset + 2] = entityData.y;
|
||||
this.sharedFloatArray[offset + 3] = entityData.vx;
|
||||
this.sharedFloatArray[offset + 4] = entityData.vy;
|
||||
this.sharedFloatArray[offset + 5] = entityData.mass;
|
||||
this.sharedFloatArray[offset + 6] = entityData.radius;
|
||||
}
|
||||
|
||||
protected readEntityFromBuffer(offset: number): PhysicsData | null {
|
||||
if (!this.sharedFloatArray) return null;
|
||||
|
||||
return {
|
||||
id: this.sharedFloatArray[offset + 0],
|
||||
x: this.sharedFloatArray[offset + 1],
|
||||
y: this.sharedFloatArray[offset + 2],
|
||||
vx: this.sharedFloatArray[offset + 3],
|
||||
vy: this.sharedFloatArray[offset + 4],
|
||||
mass: this.sharedFloatArray[offset + 5],
|
||||
radius: this.sharedFloatArray[offset + 6]
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 配置选项
|
||||
|
||||
Worker系统支持丰富的配置选项:
|
||||
|
||||
```typescript
|
||||
interface WorkerSystemConfig {
|
||||
/** 是否启用Worker并行处理 */
|
||||
enableWorker?: boolean;
|
||||
/** Worker数量,默认为CPU核心数,自动限制在系统最大值内 */
|
||||
workerCount?: number;
|
||||
/** 每个Worker处理的实体数量,用于控制负载分布 */
|
||||
entitiesPerWorker?: number;
|
||||
/** 系统配置数据,会传递给Worker */
|
||||
systemConfig?: any;
|
||||
/** 是否使用SharedArrayBuffer优化 */
|
||||
useSharedArrayBuffer?: boolean;
|
||||
/** 每个实体在SharedArrayBuffer中占用的Float32数量 */
|
||||
entityDataSize?: number;
|
||||
/** 最大实体数量(用于预分配SharedArrayBuffer) */
|
||||
maxEntities?: number;
|
||||
}
|
||||
```
|
||||
|
||||
### 配置建议
|
||||
|
||||
```typescript
|
||||
constructor() {
|
||||
super(matcher, {
|
||||
// 根据任务复杂度决定是否启用
|
||||
enableWorker: this.shouldUseWorker(),
|
||||
|
||||
// Worker数量:系统会自动限制在硬件支持范围内
|
||||
workerCount: 8, // 请求8个Worker,实际数量受CPU核心数限制
|
||||
|
||||
// 每个Worker处理的实体数量(可选)
|
||||
entitiesPerWorker: 200, // 精确控制负载分布
|
||||
|
||||
// 大量简单计算时启用SharedArrayBuffer
|
||||
useSharedArrayBuffer: this.entityCount > 1000,
|
||||
|
||||
// 根据实际数据结构设置
|
||||
entityDataSize: 8, // 确保与数据结构匹配
|
||||
|
||||
// 预估最大实体数量
|
||||
maxEntities: 10000,
|
||||
|
||||
// 传递给Worker的全局配置
|
||||
systemConfig: {
|
||||
gravity: 9.8,
|
||||
friction: 0.95,
|
||||
worldBounds: { width: 1920, height: 1080 }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private shouldUseWorker(): boolean {
|
||||
// 根据实体数量和计算复杂度决定
|
||||
return this.expectedEntityCount > 100;
|
||||
}
|
||||
|
||||
// 获取系统信息
|
||||
getSystemInfo() {
|
||||
const info = this.getWorkerInfo();
|
||||
console.log(`Worker数量: ${info.workerCount}/${info.maxSystemWorkerCount}`);
|
||||
console.log(`每Worker实体数: ${info.entitiesPerWorker || '自动分配'}`);
|
||||
console.log(`当前模式: ${info.currentMode}`);
|
||||
}
|
||||
```
|
||||
|
||||
## 处理模式
|
||||
|
||||
Worker系统支持两种处理模式:
|
||||
|
||||
### 1. 传统Worker模式
|
||||
|
||||
数据通过序列化在主线程和Worker间传递:
|
||||
|
||||
```typescript
|
||||
// 适用于:复杂计算逻辑,实体数量适中
|
||||
constructor() {
|
||||
super(matcher, {
|
||||
enableWorker: true,
|
||||
useSharedArrayBuffer: false, // 使用传统模式
|
||||
workerCount: 2
|
||||
});
|
||||
}
|
||||
|
||||
protected workerProcess(entities: EntityData[], deltaTime: number): EntityData[] {
|
||||
// 复杂的算法逻辑
|
||||
return entities.map(entity => {
|
||||
// AI决策、路径规划等复杂计算
|
||||
return this.complexAILogic(entity, deltaTime);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 2. SharedArrayBuffer模式
|
||||
|
||||
零拷贝数据共享,适合大量简单计算:
|
||||
|
||||
```typescript
|
||||
// 适用于:大量实体的简单计算
|
||||
constructor() {
|
||||
super(matcher, {
|
||||
enableWorker: true,
|
||||
useSharedArrayBuffer: true, // 启用共享内存
|
||||
entityDataSize: 6,
|
||||
maxEntities: 10000
|
||||
});
|
||||
}
|
||||
|
||||
protected getSharedArrayBufferProcessFunction(): SharedArrayBufferProcessFunction {
|
||||
return function(sharedFloatArray: Float32Array, startIndex: number, endIndex: number, deltaTime: number, config: any) {
|
||||
const entitySize = 6;
|
||||
for (let i = startIndex; i < endIndex; i++) {
|
||||
const offset = i * entitySize;
|
||||
|
||||
// 读取数据
|
||||
let x = sharedFloatArray[offset];
|
||||
let y = sharedFloatArray[offset + 1];
|
||||
let vx = sharedFloatArray[offset + 2];
|
||||
let vy = sharedFloatArray[offset + 3];
|
||||
|
||||
// 物理计算
|
||||
vy += config.gravity * deltaTime;
|
||||
x += vx * deltaTime;
|
||||
y += vy * deltaTime;
|
||||
|
||||
// 写回数据
|
||||
sharedFloatArray[offset] = x;
|
||||
sharedFloatArray[offset + 1] = y;
|
||||
sharedFloatArray[offset + 2] = vx;
|
||||
sharedFloatArray[offset + 3] = vy;
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## 完整示例:粒子物理系统
|
||||
|
||||
一个包含碰撞检测的完整粒子物理系统:
|
||||
|
||||
```typescript
|
||||
interface ParticleData {
|
||||
id: number;
|
||||
x: number;
|
||||
y: number;
|
||||
dx: number;
|
||||
dy: number;
|
||||
mass: number;
|
||||
radius: number;
|
||||
bounce: number;
|
||||
friction: number;
|
||||
}
|
||||
|
||||
@ECSSystem('ParticlePhysics')
|
||||
class ParticlePhysicsWorkerSystem extends WorkerEntitySystem<ParticleData> {
|
||||
constructor() {
|
||||
super(Matcher.all(Position, Velocity, Physics, Renderable), {
|
||||
enableWorker: true,
|
||||
workerCount: 6, // 请求6个Worker,自动限制在CPU核心数内
|
||||
entitiesPerWorker: 150, // 每个Worker处理150个粒子
|
||||
useSharedArrayBuffer: true,
|
||||
entityDataSize: 9,
|
||||
maxEntities: 5000,
|
||||
systemConfig: {
|
||||
gravity: 100,
|
||||
canvasWidth: 800,
|
||||
canvasHeight: 600,
|
||||
groundFriction: 0.98
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected extractEntityData(entity: Entity): ParticleData {
|
||||
const position = entity.getComponent(Position);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
const physics = entity.getComponent(Physics);
|
||||
const renderable = entity.getComponent(Renderable);
|
||||
|
||||
return {
|
||||
id: entity.id,
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
dx: velocity.dx,
|
||||
dy: velocity.dy,
|
||||
mass: physics.mass,
|
||||
radius: renderable.size,
|
||||
bounce: physics.bounce,
|
||||
friction: physics.friction
|
||||
};
|
||||
}
|
||||
|
||||
protected workerProcess(
|
||||
entities: ParticleData[],
|
||||
deltaTime: number,
|
||||
config: any
|
||||
): ParticleData[] {
|
||||
const result = entities.map(e => ({ ...e }));
|
||||
|
||||
// 基础物理更新
|
||||
for (const particle of result) {
|
||||
// 应用重力
|
||||
particle.dy += config.gravity * deltaTime;
|
||||
|
||||
// 更新位置
|
||||
particle.x += particle.dx * deltaTime;
|
||||
particle.y += particle.dy * deltaTime;
|
||||
|
||||
// 边界碰撞
|
||||
if (particle.x <= particle.radius) {
|
||||
particle.x = particle.radius;
|
||||
particle.dx = -particle.dx * particle.bounce;
|
||||
} else if (particle.x >= config.canvasWidth - particle.radius) {
|
||||
particle.x = config.canvasWidth - particle.radius;
|
||||
particle.dx = -particle.dx * particle.bounce;
|
||||
}
|
||||
|
||||
if (particle.y <= particle.radius) {
|
||||
particle.y = particle.radius;
|
||||
particle.dy = -particle.dy * particle.bounce;
|
||||
} else if (particle.y >= config.canvasHeight - particle.radius) {
|
||||
particle.y = config.canvasHeight - particle.radius;
|
||||
particle.dy = -particle.dy * particle.bounce;
|
||||
particle.dx *= config.groundFriction;
|
||||
}
|
||||
|
||||
// 空气阻力
|
||||
particle.dx *= particle.friction;
|
||||
particle.dy *= particle.friction;
|
||||
}
|
||||
|
||||
// 粒子间碰撞检测(O(n²)算法)
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
for (let j = i + 1; j < result.length; j++) {
|
||||
const p1 = result[i];
|
||||
const p2 = result[j];
|
||||
|
||||
const dx = p2.x - p1.x;
|
||||
const dy = p2.y - p1.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
const minDistance = p1.radius + p2.radius;
|
||||
|
||||
if (distance < minDistance && distance > 0) {
|
||||
// 分离粒子
|
||||
const nx = dx / distance;
|
||||
const ny = dy / distance;
|
||||
const overlap = minDistance - distance;
|
||||
|
||||
p1.x -= nx * overlap * 0.5;
|
||||
p1.y -= ny * overlap * 0.5;
|
||||
p2.x += nx * overlap * 0.5;
|
||||
p2.y += ny * overlap * 0.5;
|
||||
|
||||
// 弹性碰撞
|
||||
const relativeVelocityX = p2.dx - p1.dx;
|
||||
const relativeVelocityY = p2.dy - p1.dy;
|
||||
const velocityAlongNormal = relativeVelocityX * nx + relativeVelocityY * ny;
|
||||
|
||||
if (velocityAlongNormal > 0) continue;
|
||||
|
||||
const restitution = (p1.bounce + p2.bounce) * 0.5;
|
||||
const impulseScalar = -(1 + restitution) * velocityAlongNormal / (1/p1.mass + 1/p2.mass);
|
||||
|
||||
p1.dx -= impulseScalar * nx / p1.mass;
|
||||
p1.dy -= impulseScalar * ny / p1.mass;
|
||||
p2.dx += impulseScalar * nx / p2.mass;
|
||||
p2.dy += impulseScalar * ny / p2.mass;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected applyResult(entity: Entity, result: ParticleData): void {
|
||||
if (!entity?.enabled) return;
|
||||
|
||||
const position = entity.getComponent(Position);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
if (position && velocity) {
|
||||
position.set(result.x, result.y);
|
||||
velocity.set(result.dx, result.dy);
|
||||
}
|
||||
}
|
||||
|
||||
protected getDefaultEntityDataSize(): number {
|
||||
return 9;
|
||||
}
|
||||
|
||||
protected writeEntityToBuffer(data: ParticleData, offset: number): void {
|
||||
if (!this.sharedFloatArray) return;
|
||||
|
||||
this.sharedFloatArray[offset + 0] = data.id;
|
||||
this.sharedFloatArray[offset + 1] = data.x;
|
||||
this.sharedFloatArray[offset + 2] = data.y;
|
||||
this.sharedFloatArray[offset + 3] = data.dx;
|
||||
this.sharedFloatArray[offset + 4] = data.dy;
|
||||
this.sharedFloatArray[offset + 5] = data.mass;
|
||||
this.sharedFloatArray[offset + 6] = data.radius;
|
||||
this.sharedFloatArray[offset + 7] = data.bounce;
|
||||
this.sharedFloatArray[offset + 8] = data.friction;
|
||||
}
|
||||
|
||||
protected readEntityFromBuffer(offset: number): ParticleData | null {
|
||||
if (!this.sharedFloatArray) return null;
|
||||
|
||||
return {
|
||||
id: this.sharedFloatArray[offset + 0],
|
||||
x: this.sharedFloatArray[offset + 1],
|
||||
y: this.sharedFloatArray[offset + 2],
|
||||
dx: this.sharedFloatArray[offset + 3],
|
||||
dy: this.sharedFloatArray[offset + 4],
|
||||
mass: this.sharedFloatArray[offset + 5],
|
||||
radius: this.sharedFloatArray[offset + 6],
|
||||
bounce: this.sharedFloatArray[offset + 7],
|
||||
friction: this.sharedFloatArray[offset + 8]
|
||||
};
|
||||
}
|
||||
|
||||
// 性能监控
|
||||
public getPerformanceInfo(): {
|
||||
enabled: boolean;
|
||||
workerCount: number;
|
||||
entitiesPerWorker?: number;
|
||||
maxSystemWorkerCount: number;
|
||||
entityCount: number;
|
||||
isProcessing: boolean;
|
||||
currentMode: string;
|
||||
} {
|
||||
const workerInfo = this.getWorkerInfo();
|
||||
return {
|
||||
...workerInfo,
|
||||
entityCount: this.entities.length
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 适用场景
|
||||
|
||||
Worker系统特别适合以下场景:
|
||||
|
||||
### 1. 物理模拟
|
||||
- **重力系统**:大量实体的重力计算
|
||||
- **碰撞检测**:复杂的碰撞算法
|
||||
- **流体模拟**:粒子流体系统
|
||||
- **布料模拟**:顶点物理计算
|
||||
|
||||
### 2. AI计算
|
||||
- **路径寻找**:A*、Dijkstra等算法
|
||||
- **行为树**:复杂的AI决策逻辑
|
||||
- **群体智能**:鸟群、鱼群算法
|
||||
- **神经网络**:简单的AI推理
|
||||
|
||||
### 3. 数据处理
|
||||
- **大量实体更新**:状态机、生命周期管理
|
||||
- **统计计算**:游戏数据分析
|
||||
- **图像处理**:纹理生成、效果计算
|
||||
- **音频处理**:音效合成、频谱分析
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. Worker函数要求
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:Worker处理函数是纯函数
|
||||
protected workerProcess(entities: PhysicsData[], deltaTime: number, config: any): PhysicsData[] {
|
||||
// 只使用参数和标准JavaScript API
|
||||
return entities.map(entity => {
|
||||
// 纯计算逻辑,不依赖外部状态
|
||||
entity.y += entity.velocity * deltaTime;
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
// ❌ 避免:在Worker函数中使用外部引用
|
||||
protected workerProcess(entities: PhysicsData[], deltaTime: number): PhysicsData[] {
|
||||
// this 和外部变量在Worker中不可用
|
||||
return entities.map(entity => {
|
||||
entity.y += this.someProperty; // ❌ 错误
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 数据设计
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:合理的数据设计
|
||||
interface SimplePhysicsData {
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
// 保持数据结构简单,便于序列化
|
||||
}
|
||||
|
||||
// ❌ 避免:复杂的嵌套对象
|
||||
interface ComplexData {
|
||||
transform: {
|
||||
position: { x: number; y: number };
|
||||
rotation: { angle: number };
|
||||
};
|
||||
// 复杂嵌套结构增加序列化开销
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Worker数量控制
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:灵活的Worker配置
|
||||
constructor() {
|
||||
super(matcher, {
|
||||
// 直接指定需要的Worker数量,系统会自动限制在硬件支持范围内
|
||||
workerCount: 8, // 请求8个Worker
|
||||
entitiesPerWorker: 100, // 每个Worker处理100个实体
|
||||
enableWorker: this.shouldUseWorker(), // 条件启用
|
||||
});
|
||||
}
|
||||
|
||||
private shouldUseWorker(): boolean {
|
||||
// 根据实体数量和复杂度决定是否使用Worker
|
||||
return this.expectedEntityCount > 100;
|
||||
}
|
||||
|
||||
// 获取实际使用的Worker信息
|
||||
checkWorkerConfiguration() {
|
||||
const info = this.getWorkerInfo();
|
||||
console.log(`请求Worker数量: 8`);
|
||||
console.log(`实际Worker数量: ${info.workerCount}`);
|
||||
console.log(`系统最大支持: ${info.maxSystemWorkerCount}`);
|
||||
console.log(`每Worker实体数: ${info.entitiesPerWorker || '自动分配'}`);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 性能监控
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:性能监控
|
||||
public getPerformanceMetrics(): WorkerPerformanceMetrics {
|
||||
return {
|
||||
...this.getWorkerInfo(),
|
||||
entityCount: this.entities.length,
|
||||
averageProcessTime: this.getAverageProcessTime(),
|
||||
workerUtilization: this.getWorkerUtilization()
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## 性能优化建议
|
||||
|
||||
### 1. 计算密集度评估
|
||||
只对计算密集型任务使用Worker,避免在简单计算上增加线程开销。
|
||||
|
||||
### 2. 数据传输优化
|
||||
- 使用SharedArrayBuffer减少序列化开销
|
||||
- 保持数据结构简单和扁平
|
||||
- 避免频繁的大数据传输
|
||||
|
||||
### 3. 降级策略
|
||||
始终提供主线程回退方案,确保在不支持Worker的环境中正常运行。
|
||||
|
||||
### 4. 内存管理
|
||||
及时清理Worker池和共享缓冲区,避免内存泄漏。
|
||||
|
||||
### 5. 负载均衡
|
||||
使用 `entitiesPerWorker` 参数精确控制负载分布,避免某些Worker空闲而其他Worker过载。
|
||||
|
||||
## 在线演示
|
||||
|
||||
查看完整的Worker系统演示:[Worker系统演示](https://esengine.github.io/ecs-framework/demos/worker-system/)
|
||||
|
||||
该演示展示了:
|
||||
- 多线程物理计算
|
||||
- 实时性能对比
|
||||
- SharedArrayBuffer优化
|
||||
- 大量实体的并行处理
|
||||
|
||||
Worker系统为ECS框架提供了强大的并行计算能力,让你能够充分利用现代多核处理器的性能,为复杂的游戏逻辑和计算密集型任务提供了高效的解决方案。
|
||||
761
docs/guide/world-manager.md
Normal file
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)。
|
||||
23
docs/index.md
Normal file
23
docs/index.md
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
layout: home
|
||||
|
||||
hero:
|
||||
name: "ECS Framework"
|
||||
text: "高性能ECS框架"
|
||||
tagline: "为Javascript游戏开发而设计"
|
||||
actions:
|
||||
- theme: brand
|
||||
text: 快速开始
|
||||
link: /guide/getting-started
|
||||
- theme: alt
|
||||
text: 查看示例
|
||||
link: https://github.com/esengine/lawn-mower-demo
|
||||
|
||||
features:
|
||||
- title: 高性能
|
||||
details: 支持大规模实体处理
|
||||
- title: 类型安全
|
||||
details: 完整的TypeScript支持,编译时类型检查
|
||||
- title: 模块化设计
|
||||
details: 核心功能独立打包,支持多平台
|
||||
---
|
||||
@@ -1,497 +0,0 @@
|
||||
# 性能优化指南
|
||||
|
||||
ECS Framework 提供了多层性能优化系统,确保在各种规模的游戏中都能提供卓越的性能表现。
|
||||
|
||||
## 性能优化架构
|
||||
|
||||
### 三大核心优化系统
|
||||
|
||||
1. **组件索引系统 (ComponentIndex)** - 提供 O(1) 组件查询性能
|
||||
2. **Archetype系统** - 按组件组合分组实体,减少查询遍历
|
||||
3. **脏标记系统 (DirtyTracking)** - 细粒度变更追踪,避免不必要更新
|
||||
|
||||
这三个系统协同工作,为不同场景提供最优的性能表现。
|
||||
|
||||
## 性能基准
|
||||
|
||||
### 核心操作性能
|
||||
|
||||
```
|
||||
实体创建: 640,000+ 个/秒
|
||||
组件查询: O(1) 复杂度(使用索引)
|
||||
内存优化: 30-50% 减少分配
|
||||
批量操作: 显著提升处理效率
|
||||
```
|
||||
|
||||
### 查询性能对比
|
||||
|
||||
| 查询类型 | 传统方式 | 使用索引 | 性能提升 |
|
||||
|----------|----------|----------|----------|
|
||||
| 单组件查询 | O(n) | O(1) | 1000x+ |
|
||||
| 多组件查询 | O(n*m) | O(k) | 100x+ |
|
||||
| 标签查询 | O(n) | O(1) | 1000x+ |
|
||||
| 复合查询 | O(n*m*k) | O(min(k1,k2)) | 500x+ |
|
||||
|
||||
*n=实体数量, m=组件种类, k=匹配实体数量*
|
||||
|
||||
## 组件索引系统
|
||||
|
||||
### 索引类型选择
|
||||
|
||||
框架提供两种索引实现:
|
||||
|
||||
#### 哈希索引 (HashComponentIndex)
|
||||
- **适用场景**: 通用查询,平衡的读写性能
|
||||
- **优势**: O(1) 查询,较低内存开销
|
||||
- **缺点**: 哈希冲突时性能下降
|
||||
|
||||
```typescript
|
||||
// 自动选择最优索引类型
|
||||
const componentIndex = entityManager.getComponentIndex();
|
||||
|
||||
// 手动配置哈希索引
|
||||
componentIndex.setIndexType(HealthComponent, 'hash');
|
||||
```
|
||||
|
||||
#### 位图索引 (BitmapComponentIndex)
|
||||
- **适用场景**: 大规模实体,频繁的组合查询
|
||||
- **优势**: 超快的 AND/OR 操作,空间压缩
|
||||
- **缺点**: 更新成本较高,内存开销随实体数量增长
|
||||
|
||||
```typescript
|
||||
// 配置位图索引用于大规模查询
|
||||
componentIndex.setIndexType(PositionComponent, 'bitmap');
|
||||
```
|
||||
|
||||
### 智能索引管理
|
||||
|
||||
ComponentIndexManager 会根据使用模式自动优化:
|
||||
|
||||
```typescript
|
||||
// 获取索引性能统计
|
||||
const stats = componentIndex.getPerformanceStats();
|
||||
console.log('索引性能:', {
|
||||
queriesPerSecond: stats.queriesPerSecond,
|
||||
hitRate: stats.hitRate,
|
||||
indexType: stats.recommendedType
|
||||
});
|
||||
|
||||
// 自动优化索引类型
|
||||
componentIndex.optimize(); // 根据使用模式切换索引类型
|
||||
```
|
||||
|
||||
## Archetype系统优化
|
||||
|
||||
### 原型分组策略
|
||||
|
||||
Archetype系统将实体按组件组合分组,实现快速批量操作:
|
||||
|
||||
```typescript
|
||||
// 获取Archetype统计
|
||||
const archetypeSystem = entityManager.getArchetypeSystem();
|
||||
const stats = archetypeSystem.getStatistics();
|
||||
|
||||
console.log('Archetype优化:', {
|
||||
totalArchetypes: stats.totalArchetypes, // 原型数量
|
||||
avgEntitiesPerArchetype: stats.averageEntitiesPerArchetype,
|
||||
queryCacheHits: stats.queryCacheHits // 缓存命中次数
|
||||
});
|
||||
```
|
||||
|
||||
### 查询缓存机制
|
||||
|
||||
```typescript
|
||||
// 启用查询缓存(默认开启)
|
||||
archetypeSystem.enableQueryCache(true);
|
||||
|
||||
// 缓存大小限制(避免内存泄漏)
|
||||
archetypeSystem.setMaxCacheSize(1000);
|
||||
|
||||
// 清理过期缓存
|
||||
archetypeSystem.cleanCache();
|
||||
```
|
||||
|
||||
### 最佳实践
|
||||
|
||||
1. **组件设计**: 避免创建过多单独的原型
|
||||
2. **批量操作**: 利用原型批量处理相同组件组合的实体
|
||||
3. **缓存管理**: 定期清理查询缓存
|
||||
|
||||
```typescript
|
||||
// ✅ 好的设计:复用组件组合
|
||||
class MovementSystem extends EntitySystem {
|
||||
process() {
|
||||
// 一次查询处理所有移动实体
|
||||
const movingEntities = this.entityManager
|
||||
.query()
|
||||
.withAll([PositionComponent, VelocityComponent])
|
||||
.execute(); // 利用Archetype快速获取
|
||||
|
||||
// 批量处理
|
||||
movingEntities.forEach(entity => {
|
||||
// 更新逻辑
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ 避免:频繁查询不同组合
|
||||
class BadSystem extends EntitySystem {
|
||||
process() {
|
||||
// 多次小查询,无法充分利用Archetype
|
||||
const players = this.queryPlayers();
|
||||
const enemies = this.queryEnemies();
|
||||
const bullets = this.queryBullets();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 脏标记系统优化
|
||||
|
||||
### 脏标记类型
|
||||
|
||||
系统提供细粒度的脏标记追踪:
|
||||
|
||||
```typescript
|
||||
enum DirtyType {
|
||||
COMPONENT_ADDED, // 组件添加
|
||||
COMPONENT_REMOVED, // 组件移除
|
||||
COMPONENT_MODIFIED, // 组件修改
|
||||
ENTITY_ENABLED, // 实体启用
|
||||
ENTITY_DISABLED, // 实体禁用
|
||||
TAG_ADDED, // 标签添加
|
||||
TAG_REMOVED // 标签移除
|
||||
}
|
||||
```
|
||||
|
||||
### 批量处理配置
|
||||
|
||||
```typescript
|
||||
const dirtyTracking = entityManager.getDirtyTrackingSystem();
|
||||
|
||||
// 配置批量处理参数
|
||||
dirtyTracking.configure({
|
||||
batchSize: 100, // 每批处理100个脏标记
|
||||
timeSliceMs: 16, // 每帧最多处理16ms
|
||||
processingInterval: 1 // 每帧处理一次
|
||||
});
|
||||
|
||||
// 监听脏标记事件
|
||||
dirtyTracking.addListener(DirtyType.COMPONENT_MODIFIED, (entity, component) => {
|
||||
// 响应组件修改
|
||||
this.invalidateRenderCache(entity);
|
||||
}, { priority: 10 });
|
||||
```
|
||||
|
||||
### 性能监控
|
||||
|
||||
```typescript
|
||||
const dirtyStats = dirtyTracking.getPerformanceStats();
|
||||
console.log('脏标记性能:', {
|
||||
totalMarks: dirtyStats.totalMarks,
|
||||
batchesProcessed: dirtyStats.batchesProcessed,
|
||||
averageBatchTime: dirtyStats.averageBatchTime,
|
||||
queueSize: dirtyStats.currentQueueSize
|
||||
});
|
||||
```
|
||||
|
||||
## 查询优化策略
|
||||
|
||||
### 查询层次选择
|
||||
|
||||
根据查询复杂度选择最优方法:
|
||||
|
||||
```typescript
|
||||
// 1. 简单查询:直接使用索引
|
||||
const healthEntities = entityManager.getEntitiesWithComponent(HealthComponent);
|
||||
|
||||
// 2. 双组件查询:使用Archetype
|
||||
const movingEntities = entityManager.getEntitiesWithComponents([
|
||||
PositionComponent,
|
||||
VelocityComponent
|
||||
]);
|
||||
|
||||
// 3. 复杂查询:组合使用
|
||||
const combatants = entityManager
|
||||
.query()
|
||||
.withAll([PositionComponent, HealthComponent]) // Archetype预筛选
|
||||
.withTag("combat") // 索引过滤
|
||||
.where(entity => { // 自定义精确过滤
|
||||
const health = entity.getComponent(HealthComponent);
|
||||
return health.currentHealth > health.maxHealth * 0.3;
|
||||
})
|
||||
.execute();
|
||||
```
|
||||
|
||||
### 查询缓存策略
|
||||
|
||||
```typescript
|
||||
class CombatSystem extends EntitySystem {
|
||||
private cachedEnemies: Entity[] = [];
|
||||
private lastEnemyCacheUpdate = 0;
|
||||
|
||||
process() {
|
||||
const currentTime = performance.now();
|
||||
|
||||
// 每200ms更新一次敌人缓存
|
||||
if (currentTime - this.lastEnemyCacheUpdate > 200) {
|
||||
this.cachedEnemies = this.entityManager
|
||||
.getEntitiesByTag("enemy");
|
||||
this.lastEnemyCacheUpdate = currentTime;
|
||||
}
|
||||
|
||||
// 使用缓存的结果
|
||||
this.processCombat(this.cachedEnemies);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 内存优化
|
||||
|
||||
### 内存使用监控
|
||||
|
||||
```typescript
|
||||
// 获取各系统内存使用情况
|
||||
const memoryStats = entityManager.getMemoryUsage();
|
||||
console.log('内存使用情况:', {
|
||||
entityIndex: memoryStats.entityIndex, // 实体索引
|
||||
componentIndex: memoryStats.componentIndex, // 组件索引
|
||||
archetype: memoryStats.archetype, // 原型系统
|
||||
dirtyTracking: memoryStats.dirtyTracking, // 脏标记
|
||||
total: memoryStats.total
|
||||
});
|
||||
```
|
||||
|
||||
### 内存清理策略
|
||||
|
||||
```typescript
|
||||
// 定期内存清理
|
||||
setInterval(() => {
|
||||
entityManager.cleanup(); // 清理无效引用
|
||||
entityManager.compact(); // 压缩数据结构
|
||||
}, 30000); // 每30秒清理一次
|
||||
|
||||
// 游戏场景切换时的深度清理
|
||||
function switchScene() {
|
||||
entityManager.destroyAllEntities();
|
||||
entityManager.cleanup();
|
||||
entityManager.compact();
|
||||
|
||||
// 重置优化系统
|
||||
entityManager.getComponentIndex().reset();
|
||||
entityManager.getArchetypeSystem().clearCache();
|
||||
entityManager.getDirtyTrackingSystem().clear();
|
||||
}
|
||||
```
|
||||
|
||||
## 实战优化案例
|
||||
|
||||
### 大规模射击游戏优化
|
||||
|
||||
```typescript
|
||||
class BulletSystem extends EntitySystem {
|
||||
private bulletPool: Entity[] = [];
|
||||
private maxBullets = 1000;
|
||||
|
||||
constructor(entityManager: EntityManager) {
|
||||
super();
|
||||
this.prewarmBulletPool();
|
||||
}
|
||||
|
||||
private prewarmBulletPool() {
|
||||
// 预创建子弹池
|
||||
this.bulletPool = this.entityManager.createEntities(
|
||||
this.maxBullets,
|
||||
"Bullet"
|
||||
);
|
||||
|
||||
// 初始化为非激活状态
|
||||
this.bulletPool.forEach(bullet => {
|
||||
bullet.enabled = false;
|
||||
bullet.addComponent(new PositionComponent());
|
||||
bullet.addComponent(new VelocityComponent());
|
||||
bullet.addComponent(new BulletComponent());
|
||||
});
|
||||
}
|
||||
|
||||
public spawnBullet(x: number, y: number, vx: number, vy: number): Entity | null {
|
||||
// 从池中获取非激活子弹(使用索引快速查询)
|
||||
const availableBullet = this.entityManager
|
||||
.query()
|
||||
.withAll([BulletComponent])
|
||||
.active(false)
|
||||
.first();
|
||||
|
||||
if (availableBullet) {
|
||||
// 重用现有子弹
|
||||
const pos = availableBullet.getComponent(PositionComponent);
|
||||
const vel = availableBullet.getComponent(VelocityComponent);
|
||||
|
||||
pos.x = x; pos.y = y;
|
||||
vel.x = vx; vel.y = vy;
|
||||
availableBullet.enabled = true;
|
||||
|
||||
return availableBullet;
|
||||
}
|
||||
|
||||
return null; // 池已满
|
||||
}
|
||||
|
||||
process() {
|
||||
// 批量处理所有激活的子弹
|
||||
this.entityManager.forEachEntityWithComponent(
|
||||
BulletComponent,
|
||||
(entity, bullet) => {
|
||||
if (!entity.enabled) return;
|
||||
|
||||
// 更新位置
|
||||
const pos = entity.getComponent(PositionComponent);
|
||||
const vel = entity.getComponent(VelocityComponent);
|
||||
|
||||
pos.x += vel.x * Time.deltaTime;
|
||||
pos.y += vel.y * Time.deltaTime;
|
||||
|
||||
// 边界检查,回收到池中
|
||||
if (pos.x < 0 || pos.x > 800 || pos.y < 0 || pos.y > 600) {
|
||||
entity.enabled = false; // 回收而不是销毁
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### AI系统性能优化
|
||||
|
||||
```typescript
|
||||
class AISystem extends EntitySystem {
|
||||
private spatialGrid: SpatialGrid;
|
||||
private updateFrequency = 60; // 60Hz更新频率
|
||||
private lastUpdate = 0;
|
||||
|
||||
process() {
|
||||
const currentTime = performance.now();
|
||||
|
||||
// 控制更新频率
|
||||
if (currentTime - this.lastUpdate < 1000 / this.updateFrequency) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用空间分区优化邻居查询
|
||||
const aiEntities = this.entityManager
|
||||
.query()
|
||||
.withAll([PositionComponent, AIComponent])
|
||||
.active(true)
|
||||
.execute();
|
||||
|
||||
// 分批处理AI实体
|
||||
const batchSize = 50;
|
||||
for (let i = 0; i < aiEntities.length; i += batchSize) {
|
||||
const batch = aiEntities.slice(i, i + batchSize);
|
||||
this.processBatch(batch);
|
||||
|
||||
// 时间片控制,避免单帧卡顿
|
||||
if (performance.now() - currentTime > 10) { // 10ms时间片
|
||||
break; // 下一帧继续处理
|
||||
}
|
||||
}
|
||||
|
||||
this.lastUpdate = currentTime;
|
||||
}
|
||||
|
||||
private processBatch(entities: Entity[]) {
|
||||
entities.forEach(entity => {
|
||||
const pos = entity.getComponent(PositionComponent);
|
||||
const ai = entity.getComponent(AIComponent);
|
||||
|
||||
// 空间查询优化邻居搜索
|
||||
const neighbors = this.spatialGrid.queryRadius(pos.x, pos.y, ai.sightRange);
|
||||
|
||||
// AI决策逻辑
|
||||
ai.update(neighbors);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 性能监控工具
|
||||
|
||||
### 实时性能仪表板
|
||||
|
||||
```typescript
|
||||
class PerformanceDashboard {
|
||||
private stats: any = {};
|
||||
private updateInterval = 1000; // 1秒更新一次
|
||||
|
||||
constructor(private entityManager: EntityManager) {
|
||||
setInterval(() => this.updateStats(), this.updateInterval);
|
||||
}
|
||||
|
||||
private updateStats() {
|
||||
this.stats = {
|
||||
// 基础统计
|
||||
entities: this.entityManager.getStatistics(),
|
||||
|
||||
// 组件索引
|
||||
componentIndex: this.entityManager.getComponentIndex().getPerformanceStats(),
|
||||
|
||||
// Archetype系统
|
||||
archetype: this.entityManager.getArchetypeSystem().getStatistics(),
|
||||
|
||||
// 脏标记系统
|
||||
dirtyTracking: this.entityManager.getDirtyTrackingSystem().getPerformanceStats(),
|
||||
|
||||
// 内存使用
|
||||
memory: this.entityManager.getMemoryUsage(),
|
||||
|
||||
// 计算性能指标
|
||||
performance: this.calculatePerformanceMetrics()
|
||||
};
|
||||
|
||||
this.displayStats();
|
||||
}
|
||||
|
||||
private calculatePerformanceMetrics() {
|
||||
const componentStats = this.stats.componentIndex;
|
||||
const archetypeStats = this.stats.archetype;
|
||||
|
||||
return {
|
||||
queryHitRate: componentStats.hitRate,
|
||||
archetypeEfficiency: archetypeStats.averageEntitiesPerArchetype,
|
||||
memoryEfficiency: this.stats.memory.compressionRatio,
|
||||
overallPerformance: this.calculateOverallScore()
|
||||
};
|
||||
}
|
||||
|
||||
private displayStats() {
|
||||
console.log('=== ECS性能仪表板 ===');
|
||||
console.log('查询命中率:', this.stats.performance.queryHitRate.toFixed(2) + '%');
|
||||
console.log('内存使用:', (this.stats.memory.total / 1024 / 1024).toFixed(2) + 'MB');
|
||||
console.log('整体性能评分:', this.stats.performance.overallPerformance.toFixed(1) + '/10');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 优化检查清单
|
||||
|
||||
### 开发阶段
|
||||
|
||||
- [ ] 使用EntityManager而不是直接操作Scene
|
||||
- [ ] 优先使用组件查询和标签查询
|
||||
- [ ] 设计合理的组件组合,避免过度碎片化
|
||||
- [ ] 实现对象池机制减少频繁创建/销毁
|
||||
|
||||
### 运行时优化
|
||||
|
||||
- [ ] 监控查询命中率,保持在80%以上
|
||||
- [ ] 控制Archetype数量,避免过度分散
|
||||
- [ ] 配置适当的脏标记批量处理参数
|
||||
- [ ] 定期进行内存清理和数据压缩
|
||||
|
||||
### 性能监控
|
||||
|
||||
- [ ] 定期检查性能统计数据
|
||||
- [ ] 监控内存使用趋势
|
||||
- [ ] 设置性能预警阈值
|
||||
- [ ] 在不同设备上进行性能测试
|
||||
|
||||
通过系统性地应用这些优化策略,您可以构建出在各种规模下都能提供卓越性能的ECS游戏系统。
|
||||
@@ -1,306 +0,0 @@
|
||||
# ECS框架性能基准
|
||||
|
||||
本文档展示了ECS框架的真实性能数据和瓶颈分析。
|
||||
|
||||
## 🚀 快速测试
|
||||
|
||||
```bash
|
||||
# 快速性能基准测试
|
||||
npm run benchmark
|
||||
|
||||
# 完整性能测试
|
||||
npm run test:performance
|
||||
|
||||
# 单元测试
|
||||
npm run test:unit
|
||||
```
|
||||
|
||||
## 📊 性能基准数据
|
||||
|
||||
> 测试环境: Node.js, Windows 10, 现代桌面CPU
|
||||
|
||||
### 1. 实体创建性能
|
||||
|
||||
| 实体数量 | 创建时间 | 创建速度 | 每个实体耗时 | 性能等级 |
|
||||
|---------|---------|---------|-------------|---------|
|
||||
| 1,000 | 1.56ms | 640,697个/秒 | 0.0016ms | 🚀 极致 |
|
||||
| 5,000 | 19.47ms | 256,805个/秒 | 0.0039ms | 🚀 极致 |
|
||||
| 10,000 | 39.94ms | 250,345个/秒 | 0.0040ms | 🚀 极致 |
|
||||
| 50,000 | 258.17ms | 193,673个/秒 | 0.0052ms | ✅ 优秀 |
|
||||
| 100,000 | 463.04ms | 215,963个/秒 | 0.0046ms | ✅ 优秀 |
|
||||
| 500,000 | 3,087ms | 161,990个/秒 | 0.0062ms | ✅ 优秀 |
|
||||
|
||||
**结论**: 🚀 实体创建性能达到极致水平,大规模创建50万实体仅需3秒
|
||||
|
||||
### 2. 性能瓶颈分析 (500,000个实体)
|
||||
|
||||
**当前瓶颈分布**:
|
||||
```
|
||||
实体创建: 46.3% (1,429ms)
|
||||
组件添加: 53.5% (1,651ms) ← 主要瓶颈
|
||||
标签分配: 0.2% (7ms)
|
||||
```
|
||||
|
||||
**特征**: 框架实现了均衡的性能分布,各部分开销相对合理
|
||||
|
||||
### 3. 组件添加性能详细分析
|
||||
|
||||
| 组件类型 | 添加速度 | 平均耗时 | 性能等级 |
|
||||
|---------|---------|---------|---------|
|
||||
| PositionComponent | 596,929组件/秒 | 0.0017ms | 🚀 极致 |
|
||||
| VelocityComponent | 1,186,770组件/秒 | 0.0008ms | 🚀 极致 |
|
||||
| HealthComponent | 841,982组件/秒 | 0.0012ms | 🚀 极致 |
|
||||
| RenderComponent | 763,351组件/秒 | 0.0013ms | 🚀 极致 |
|
||||
| AIComponent | 185,964组件/秒 | 0.0054ms | ✅ 优秀 |
|
||||
|
||||
### 4. 优化技术性能影响
|
||||
|
||||
| 优化技术 | 性能提升 | 内存影响 | 适用场景 |
|
||||
|---------|---------|---------|---------|
|
||||
| 组件对象池 | 30-50% | 减少分配 | 频繁创建/销毁 |
|
||||
| 位掩码优化器 | 20-40% | 缓存开销 | 大量查询操作 |
|
||||
| 批量操作 | 显著提升 | 无明显影响 | 大规模实体创建 |
|
||||
| 延迟索引更新 | 60-80% | 临时内存增加 | 批量实体操作 |
|
||||
| 索引去重优化 | 避免O(n) | 轻微内存增加 | 防止重复实体 |
|
||||
|
||||
### 5. 查询系统性能
|
||||
|
||||
#### 5.1 基础查询性能
|
||||
| 查询类型 | 查询速度 | 每次查询耗时 | 性能等级 |
|
||||
|---------|---------|-------------|---------|
|
||||
| 单组件查询 | 12,178次/秒 | 0.082ms | ✅ 优秀 |
|
||||
| 多组件查询 | 9,439次/秒 | 0.106ms | ✅ 优秀 |
|
||||
| 复合查询 | 7,407次/秒 | 0.135ms | ✅ 良好 |
|
||||
|
||||
#### 5.2 缓存查询性能
|
||||
| 缓存状态 | 访问速度 | 性能特征 |
|
||||
|---------|---------|---------|
|
||||
| 缓存命中 | 零延迟 | 🚀 即时响应 |
|
||||
| 缓存未命中 | 标准查询 | ✅ 自动构建 |
|
||||
| 缓存清理 | 批量延迟 | 🔧 优化策略 |
|
||||
|
||||
### 6. 新功能性能基准
|
||||
|
||||
#### 6.1 组件对象池性能
|
||||
```
|
||||
📊 对象池 vs 直接创建 (10,000次操作)
|
||||
对象池获取: 1.65ms (6,060,606次/秒)
|
||||
直接创建: 1.51ms (6,622,516次/秒)
|
||||
|
||||
⚠️ 小规模测试中对象池可能略慢,但在大规模应用中:
|
||||
- 减少30-50%的内存分配
|
||||
- 避免垃圾回收压力
|
||||
- 提升长期运行稳定性
|
||||
```
|
||||
|
||||
#### 6.2 位掩码优化器性能
|
||||
```
|
||||
🔥 位掩码操作性能 (100,000次操作)
|
||||
单个掩码创建: 20.00ms (5,000,000次/秒)
|
||||
组合掩码创建: 53.69ms (1,862,285次/秒)
|
||||
缓存掩码访问: <1ms (近零延迟)
|
||||
```
|
||||
|
||||
## 🎯 性能扩展性分析
|
||||
|
||||
### 实体创建扩展性
|
||||
```
|
||||
📈 创建速度趋势分析
|
||||
1K-10K实体: 250,000-640,000 实体/秒 (优秀)
|
||||
10K-100K实体: 200,000-250,000 实体/秒 (良好)
|
||||
100K-500K实体: 160,000-220,000 实体/秒 (稳定)
|
||||
|
||||
结论: 性能随规模稳定下降,无突然性能悬崖
|
||||
```
|
||||
|
||||
### 内存使用效率
|
||||
| 实体数量 | 内存使用 | 每实体内存 | 内存效率 |
|
||||
|---------|---------|-----------|---------|
|
||||
| 1,000 | 3.5MB | 3.5KB | 🚀 极致 |
|
||||
| 5,000 | 7.1MB | 1.4KB | 🚀 极致 |
|
||||
| 10,000 | 20.8MB | 2.1KB | ✅ 优秀 |
|
||||
| 50,000 | ~100MB | ~2KB | ✅ 优秀 |
|
||||
|
||||
## 💡 性能优化建议
|
||||
|
||||
### 1. 实体创建最佳实践
|
||||
|
||||
**✅ 推荐做法**:
|
||||
```typescript
|
||||
// 使用批量创建API
|
||||
const entities = scene.createEntities(10000, "Enemies");
|
||||
|
||||
// 延迟缓存清理
|
||||
entities.forEach(entity => {
|
||||
scene.addEntity(entity, false); // 延迟清理
|
||||
});
|
||||
scene.querySystem.clearCache(); // 手动清理
|
||||
```
|
||||
|
||||
**❌ 避免做法**:
|
||||
```typescript
|
||||
// 避免循环单个创建
|
||||
for (let i = 0; i < 10000; i++) {
|
||||
scene.createEntity("Enemy" + i); // 每次触发缓存清理
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 组件池优化策略
|
||||
|
||||
**预热策略**:
|
||||
```typescript
|
||||
// 预热常用组件池
|
||||
ComponentPoolManager.getInstance().preWarmPools({
|
||||
BulletComponent: 2000, // 子弹大量创建
|
||||
EffectComponent: 1000, // 特效频繁使用
|
||||
PickupComponent: 500 // 道具适量缓存
|
||||
});
|
||||
```
|
||||
|
||||
**使用模式**:
|
||||
```typescript
|
||||
// 高效的组件复用
|
||||
const bullet = ComponentPoolManager.getInstance().getComponent(BulletComponent);
|
||||
bullet.reset(); // 重置状态
|
||||
entity.addComponent(bullet);
|
||||
|
||||
// 销毁时释放到池
|
||||
ComponentPoolManager.getInstance().releaseComponent(bullet);
|
||||
```
|
||||
|
||||
### 3. 查询优化策略
|
||||
|
||||
**缓存策略**:
|
||||
```typescript
|
||||
// 缓存频繁查询结果
|
||||
class MovementSystem extends EntitySystem {
|
||||
private cachedMovingEntities: Entity[];
|
||||
private lastCacheFrame: number = 0;
|
||||
|
||||
protected process(entities: Entity[]) {
|
||||
// 每5帧更新一次缓存
|
||||
if (Time.frameCount - this.lastCacheFrame > 5) {
|
||||
this.cachedMovingEntities = scene.getEntitiesWithComponents([Position, Velocity]);
|
||||
this.lastCacheFrame = Time.frameCount;
|
||||
}
|
||||
|
||||
// 使用缓存结果
|
||||
this.processMovement(this.cachedMovingEntities);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 不同规模应用建议
|
||||
|
||||
#### 小型游戏 (< 5,000实体)
|
||||
- ✅ 可以随意使用所有功能
|
||||
- ✅ 不需要特殊优化
|
||||
- ✅ 专注于游戏逻辑开发
|
||||
|
||||
#### 中型游戏 (5,000-50,000实体)
|
||||
- ✅ 使用批量操作API
|
||||
- ✅ 启用组件对象池
|
||||
- ⚠️ 注意查询频率
|
||||
|
||||
#### 大型游戏 (50,000+实体)
|
||||
- 🚀 必须使用批量操作
|
||||
- 🚀 必须启用对象池
|
||||
- 🚀 必须缓存查询结果
|
||||
- 🚀 考虑分区处理
|
||||
|
||||
## 🌍 平台性能对比
|
||||
|
||||
### Windows 桌面端 (测试平台)
|
||||
- **实体创建**: 640,697实体/秒
|
||||
- **组件操作**: 596,929组件/秒
|
||||
- **推荐实体数**: ≤ 200,000
|
||||
|
||||
### 预估其他平台性能
|
||||
|
||||
| 平台类型 | 预估性能比例 | 推荐实体数 | 特殊注意 |
|
||||
|---------|-------------|-----------|---------|
|
||||
| macOS桌面 | 90-100% | ≤ 180,000 | 内存管理优秀 |
|
||||
| Linux桌面 | 95-105% | ≤ 200,000 | 性能最优 |
|
||||
| Chrome浏览器 | 60-80% | ≤ 100,000 | V8引擎优化 |
|
||||
| Firefox浏览器 | 50-70% | ≤ 80,000 | SpiderMonkey限制 |
|
||||
| Safari浏览器 | 55-75% | ≤ 90,000 | JavaScriptCore |
|
||||
| Node.js服务器 | 100-110% | ≤ 500,000 | 服务器级性能 |
|
||||
| Android Chrome | 30-50% | ≤ 30,000 | 移动端限制 |
|
||||
| iOS Safari | 40-60% | ≤ 40,000 | iOS优化较好 |
|
||||
|
||||
## 🔬 测试环境详情
|
||||
|
||||
### 硬件环境
|
||||
- **操作系统**: Windows 10 (Build 26100)
|
||||
- **处理器**: 现代桌面CPU
|
||||
- **内存**: 充足RAM
|
||||
- **存储**: SSD高速存储
|
||||
|
||||
### 软件环境
|
||||
- **Node.js**: v16+
|
||||
- **TypeScript**: v5.8.3
|
||||
- **ECS框架版本**: v2.0.6
|
||||
- **测试工具**: 内置基准测试套件
|
||||
|
||||
### 测试方法
|
||||
- **实体配置**: 位置、速度、生命值、渲染、AI组件随机分配
|
||||
- **测试迭代**: 多次测试取平均值
|
||||
- **内存监控**: 实时内存使用情况
|
||||
- **性能指标**: performance.now()高精度计时
|
||||
|
||||
## 📋 性能测试清单
|
||||
|
||||
### 运行完整性能测试
|
||||
|
||||
```bash
|
||||
# 1. 快速基准测试 (2-3分钟)
|
||||
npm run benchmark
|
||||
|
||||
# 2. 完整性能测试 (10-15分钟)
|
||||
npm run test:performance
|
||||
|
||||
# 3. 单元测试验证 (30秒)
|
||||
npm run test:unit
|
||||
|
||||
# 4. 所有测试 (15-20分钟)
|
||||
npm run test
|
||||
```
|
||||
|
||||
### 自定义性能测试
|
||||
|
||||
```typescript
|
||||
import { runEntityCreationBenchmark } from '@esengine/ecs-framework/Testing/Performance/benchmark';
|
||||
|
||||
// 自定义规模测试
|
||||
await runEntityCreationBenchmark([1000, 5000, 10000]);
|
||||
|
||||
// 组件性能测试
|
||||
await runComponentPerformanceTest();
|
||||
|
||||
// 查询性能测试
|
||||
await runQueryPerformanceTest();
|
||||
```
|
||||
|
||||
## 🏆 性能总结
|
||||
|
||||
### 🎯 核心能力
|
||||
1. **实体创建速度**: 最高64万实体/秒
|
||||
2. **大规模处理**: 50万实体仅需3秒创建
|
||||
3. **均衡性能**: 各组件开销分布合理
|
||||
4. **扩展性**: 性能随规模线性下降,无突然悬崖
|
||||
|
||||
### 🔧 技术特点
|
||||
1. **批量操作架构** - 大幅减少单次操作开销
|
||||
2. **智能缓存策略** - 延迟清理机制
|
||||
3. **索引系统优化** - 避免O(n)操作
|
||||
4. **内存管理优化** - 对象池和位掩码缓存
|
||||
|
||||
### 🌟 实际应用价值
|
||||
- **小型游戏**: 性能过剩,专注玩法
|
||||
- **中型游戏**: 性能充足,适度优化
|
||||
- **大型游戏**: 需要优化策略,但完全可行
|
||||
- **服务器端**: 可处理大规模实体管理
|
||||
|
||||
---
|
||||
|
||||
**结论**: ECS框架达到了产品级性能标准,能够满足从休闲小游戏到复杂RTS游戏的各种需求。框架层面的性能已经充分优化,为开发者提供了坚实的性能基础。
|
||||
118
docs/public/coi-serviceworker.js
Normal file
118
docs/public/coi-serviceworker.js
Normal file
@@ -0,0 +1,118 @@
|
||||
/*! coi-serviceworker v0.1.7 - Guido Zuidhof and contributors, licensed under MIT */
|
||||
let coepCredentialless = false;
|
||||
if (typeof window === 'undefined') {
|
||||
self.addEventListener("install", () => self.skipWaiting());
|
||||
self.addEventListener("activate", (event) => event.waitUntil(self.clients.claim()));
|
||||
|
||||
self.addEventListener("message", (ev) => {
|
||||
if (!ev.data) {
|
||||
return;
|
||||
} else if (ev.data.type === "deregister") {
|
||||
self.registration
|
||||
.unregister()
|
||||
.then(() => {
|
||||
return self.clients.matchAll();
|
||||
})
|
||||
.then(clients => {
|
||||
clients.forEach((client) => client.navigate(client.url));
|
||||
});
|
||||
} else if (ev.data.type === "coepCredentialless") {
|
||||
coepCredentialless = ev.data.value;
|
||||
}
|
||||
});
|
||||
|
||||
self.addEventListener("fetch", function (event) {
|
||||
const r = event.request;
|
||||
if (r.cache === "only-if-cached" && r.mode !== "same-origin") {
|
||||
return;
|
||||
}
|
||||
|
||||
const request = (coepCredentialless && r.mode === "no-cors")
|
||||
? new Request(r, {
|
||||
credentials: "omit",
|
||||
})
|
||||
: r;
|
||||
event.respondWith(
|
||||
fetch(request)
|
||||
.then((response) => {
|
||||
if (response.status === 0) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const newHeaders = new Headers(response.headers);
|
||||
newHeaders.set("Cross-Origin-Embedder-Policy",
|
||||
coepCredentialless ? "credentialless" : "require-corp"
|
||||
);
|
||||
if (!coepCredentialless) {
|
||||
newHeaders.set("Cross-Origin-Resource-Policy", "cross-origin");
|
||||
}
|
||||
newHeaders.set("Cross-Origin-Opener-Policy", "same-origin");
|
||||
|
||||
return new Response(response.body, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: newHeaders,
|
||||
});
|
||||
})
|
||||
.catch((e) => console.error(e))
|
||||
);
|
||||
});
|
||||
|
||||
} else {
|
||||
(() => {
|
||||
// You can customize the behavior of this script through a global `coi` variable.
|
||||
const coi = {
|
||||
shouldRegister: () => true,
|
||||
shouldDeregister: () => false,
|
||||
coepCredentialless: () => !(window.chrome || window.netscape),
|
||||
doReload: () => window.location.reload(),
|
||||
quiet: false,
|
||||
...window.coi
|
||||
};
|
||||
|
||||
const n = navigator;
|
||||
|
||||
if (n.serviceWorker && n.serviceWorker.controller) {
|
||||
n.serviceWorker.controller.postMessage({
|
||||
type: "coepCredentialless",
|
||||
value: coi.coepCredentialless(),
|
||||
});
|
||||
|
||||
if (coi.shouldDeregister()) {
|
||||
n.serviceWorker.controller.postMessage({ type: "deregister" });
|
||||
}
|
||||
}
|
||||
|
||||
// If we're already coi: do nothing. Perhaps it's due to this script doing its job, or COOP/COEP are
|
||||
// already set from the origin server. Also if the browser has no notion of crossOriginIsolated, just give up here.
|
||||
if (window.crossOriginIsolated !== false || !coi.shouldRegister()) return;
|
||||
|
||||
if (!window.isSecureContext) {
|
||||
!coi.quiet && console.log("COOP/COEP Service Worker not registered, a secure context is required.");
|
||||
return;
|
||||
}
|
||||
|
||||
// In some environments (e.g. Chrome incognito mode) this won't be available
|
||||
if (n.serviceWorker) {
|
||||
n.serviceWorker.register(window.document.currentScript.src).then(
|
||||
(registration) => {
|
||||
!coi.quiet && console.log("COOP/COEP Service Worker registered", registration.scope);
|
||||
|
||||
registration.addEventListener("updatefound", () => {
|
||||
!coi.quiet && console.log("Reloading page to make use of updated COOP/COEP Service Worker.");
|
||||
coi.doReload();
|
||||
});
|
||||
|
||||
// If the registration is active, but it's not controlling the page
|
||||
if (registration.active && !n.serviceWorker.controller) {
|
||||
!coi.quiet && console.log("Reloading page to make use of COOP/COEP Service Worker.");
|
||||
coi.doReload();
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
!coi.quiet && console.error("COOP/COEP Service Worker failed to register:", err);
|
||||
}
|
||||
);
|
||||
}
|
||||
})();
|
||||
}
|
||||
12849
docs/public/demos/worker-system/assets/index-CrID--xK.js
Normal file
12849
docs/public/demos/worker-system/assets/index-CrID--xK.js
Normal file
File diff suppressed because it is too large
Load Diff
210
docs/public/demos/worker-system/index.html
Normal file
210
docs/public/demos/worker-system/index.html
Normal file
@@ -0,0 +1,210 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ECS Framework Worker System Demo</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
font-family: Arial, sans-serif;
|
||||
background: #1a1a1a;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #4a9eff;
|
||||
}
|
||||
|
||||
.demo-area {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.canvas-container {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#gameCanvas {
|
||||
border: 2px solid #4a9eff;
|
||||
background: #000;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.controls {
|
||||
width: 300px;
|
||||
background: #2a2a2a;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.control-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.control-group input, .control-group button {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
margin-bottom: 5px;
|
||||
border: 1px solid #555;
|
||||
background: #3a3a3a;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.control-group button {
|
||||
background: #4a9eff;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.control-group button:hover {
|
||||
background: #3a8eef;
|
||||
}
|
||||
|
||||
.control-group button:disabled {
|
||||
background: #555;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.stats {
|
||||
background: #2a2a2a;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.stats h3 {
|
||||
margin-top: 0;
|
||||
color: #4a9eff;
|
||||
}
|
||||
|
||||
.stat-line {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.worker-enabled {
|
||||
color: #4eff4a;
|
||||
}
|
||||
|
||||
.worker-disabled {
|
||||
color: #ff4a4a;
|
||||
}
|
||||
|
||||
.performance-high {
|
||||
color: #4eff4a;
|
||||
}
|
||||
|
||||
.performance-medium {
|
||||
color: #ffff4a;
|
||||
}
|
||||
|
||||
.performance-low {
|
||||
color: #ff4a4a;
|
||||
}
|
||||
</style>
|
||||
<script type="module" crossorigin src="/ecs-framework/demos/worker-system/assets/index-CrID--xK.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>ECS Framework Worker System 演示</h1>
|
||||
|
||||
<div class="demo-area">
|
||||
<div class="canvas-container">
|
||||
<canvas id="gameCanvas" width="800" height="600"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<div class="control-group">
|
||||
<label>实体数量:</label>
|
||||
<input type="range" id="entityCount" min="100" max="10000" value="1000" step="100">
|
||||
<span id="entityCountValue">1000</span>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Worker 设置:</label>
|
||||
<button id="toggleWorker">禁用 Worker</button>
|
||||
<button id="toggleSAB">禁用 SharedArrayBuffer</button>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<button id="spawnParticles">生成粒子系统</button>
|
||||
<button id="clearEntities">清空所有实体</button>
|
||||
<button id="resetDemo">重置演示</button>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>物理参数:</label>
|
||||
<input type="range" id="gravity" min="0" max="500" value="100" step="10">
|
||||
<label>重力: <span id="gravityValue">100</span></label>
|
||||
|
||||
<input type="range" id="friction" min="0" max="100" value="95" step="5">
|
||||
<label>摩擦力: <span id="frictionValue">95%</span></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<h3>性能统计</h3>
|
||||
<div class="stat-line">FPS: <span id="fps">0</span></div>
|
||||
<div class="stat-line">实体数量: <span id="entityCountStat">0</span></div>
|
||||
<div class="stat-line">Worker状态: <span id="workerStatus" class="worker-disabled">未启用</span></div>
|
||||
<div class="stat-line">Worker负载: <span id="workerLoad">N/A</span></div>
|
||||
<div class="stat-line">运行模式: <span id="sabStatus" class="worker-disabled">同步模式</span></div>
|
||||
<div class="stat-line">物理系统耗时: <span id="physicsTime">0</span>ms</div>
|
||||
<div class="stat-line">渲染系统耗时: <span id="renderTime">0</span>ms</div>
|
||||
<div class="stat-line">总帧时间: <span id="frameTime">0</span>ms</div>
|
||||
<div class="stat-line">内存使用: <span id="memoryUsage">0</span>MB</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 使用 coi-serviceworker 启用 SharedArrayBuffer 支持 -->
|
||||
<script src="/ecs-framework/coi-serviceworker.js"></script>
|
||||
|
||||
<script>
|
||||
// Check SharedArrayBuffer support and display info
|
||||
function checkSharedArrayBufferSupport() {
|
||||
const hasSharedArrayBuffer = typeof SharedArrayBuffer !== 'undefined';
|
||||
const isCrossOriginIsolated = self.crossOriginIsolated || false;
|
||||
const isLocalhost = location.hostname === 'localhost' || location.hostname === '127.0.0.1';
|
||||
const isGitHubPages = location.hostname === 'esengine.github.io';
|
||||
|
||||
console.log('=== SharedArrayBuffer 支持检测 ===');
|
||||
console.log('SharedArrayBuffer 存在:', hasSharedArrayBuffer);
|
||||
console.log('跨域隔离状态:', isCrossOriginIsolated);
|
||||
console.log('是否本地环境:', isLocalhost);
|
||||
console.log('是否 GitHub Pages:', isGitHubPages);
|
||||
|
||||
if (hasSharedArrayBuffer && isCrossOriginIsolated) {
|
||||
console.log('✅ SharedArrayBuffer 功能已启用!');
|
||||
console.log('系统将使用高性能的 SharedArrayBuffer 模式');
|
||||
} else if (isGitHubPages) {
|
||||
console.log('ℹ️ 如果页面刷新,可能是 coi-serviceworker 正在设置跨域隔离');
|
||||
console.log('刷新后 SharedArrayBuffer 应该可用');
|
||||
}
|
||||
|
||||
return hasSharedArrayBuffer && isCrossOriginIsolated;
|
||||
}
|
||||
|
||||
// Run check after page load
|
||||
window.addEventListener('load', () => {
|
||||
setTimeout(checkSharedArrayBufferSupport, 1000);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,324 +0,0 @@
|
||||
# QuerySystem 使用指南
|
||||
|
||||
QuerySystem 是 ECS Framework 中的高性能实体查询系统,支持多级索引、智能缓存和类型安全的查询操作。
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 1. 获取查询系统
|
||||
|
||||
```typescript
|
||||
import { Scene, Entity } from '@esengine/ecs-framework';
|
||||
|
||||
// 创建场景,查询系统会自动创建
|
||||
const scene = new Scene();
|
||||
const querySystem = scene.querySystem;
|
||||
|
||||
// 或者从Core获取当前场景的查询系统
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
const currentQuerySystem = Core.scene?.querySystem;
|
||||
```
|
||||
|
||||
### 2. 基本查询操作
|
||||
|
||||
```typescript
|
||||
// 查询包含所有指定组件的实体
|
||||
const result = querySystem.queryAll(PositionComponent, VelocityComponent);
|
||||
console.log(`找到 ${result.count} 个实体`);
|
||||
|
||||
// 查询包含任意指定组件的实体
|
||||
const anyResult = querySystem.queryAny(HealthComponent, ManaComponent);
|
||||
|
||||
// 查询不包含指定组件的实体
|
||||
const noneResult = querySystem.queryNone(DeadComponent);
|
||||
```
|
||||
|
||||
### 3. 类型安全查询
|
||||
|
||||
```typescript
|
||||
// 类型安全的查询,返回实体和对应的组件
|
||||
const typedResult = querySystem.queryAllTyped(PositionComponent, VelocityComponent);
|
||||
for (let i = 0; i < typedResult.entities.length; i++) {
|
||||
const entity = typedResult.entities[i];
|
||||
const [position, velocity] = typedResult.components[i];
|
||||
// position 和 velocity 都是类型安全的
|
||||
}
|
||||
|
||||
// 查询单个组件类型
|
||||
const healthEntities = querySystem.queryComponentTyped(HealthComponent);
|
||||
healthEntities.forEach(({ entity, component }) => {
|
||||
console.log(`实体 ${entity.name} 的生命值: ${component.value}`);
|
||||
});
|
||||
|
||||
// 查询两个组件类型
|
||||
const movableEntities = querySystem.queryTwoComponents(PositionComponent, VelocityComponent);
|
||||
movableEntities.forEach(({ entity, component1: position, component2: velocity }) => {
|
||||
// 更新位置
|
||||
position.x += velocity.x;
|
||||
position.y += velocity.y;
|
||||
});
|
||||
```
|
||||
|
||||
### 4. 使用查询构建器
|
||||
|
||||
```typescript
|
||||
// 创建复杂查询
|
||||
const query = querySystem.createQuery()
|
||||
.withAll(PositionComponent, RenderComponent)
|
||||
.without(HiddenComponent)
|
||||
.withTag(1) // 特定标签
|
||||
.orderByName()
|
||||
.limit(10);
|
||||
|
||||
const result = query.execute();
|
||||
|
||||
// 链式操作
|
||||
const visibleEnemies = querySystem.createQuery()
|
||||
.withAll(EnemyComponent, PositionComponent)
|
||||
.without(DeadComponent, HiddenComponent)
|
||||
.filter(entity => entity.name.startsWith('Boss'))
|
||||
.orderBy((a, b) => a.id - b.id);
|
||||
|
||||
// 迭代结果
|
||||
visibleEnemies.forEach((entity, index) => {
|
||||
console.log(`敌人 ${index}: ${entity.name}`);
|
||||
});
|
||||
```
|
||||
|
||||
### 5. 高级查询功能
|
||||
|
||||
```typescript
|
||||
// 复合查询
|
||||
const complexResult = querySystem.queryComplex(
|
||||
{
|
||||
type: QueryConditionType.ALL,
|
||||
componentTypes: [PositionComponent, VelocityComponent],
|
||||
mask: /* 位掩码 */
|
||||
},
|
||||
{
|
||||
type: QueryConditionType.NONE,
|
||||
componentTypes: [DeadComponent],
|
||||
mask: /* 位掩码 */
|
||||
}
|
||||
);
|
||||
|
||||
// 批量查询
|
||||
const batchResults = querySystem.batchQuery([
|
||||
{ type: QueryConditionType.ALL, componentTypes: [HealthComponent], mask: /* 位掩码 */ },
|
||||
{ type: QueryConditionType.ALL, componentTypes: [ManaComponent], mask: /* 位掩码 */ }
|
||||
]);
|
||||
|
||||
// 并行查询
|
||||
const parallelResults = await querySystem.parallelQuery([
|
||||
{ type: QueryConditionType.ALL, componentTypes: [PositionComponent], mask: /* 位掩码 */ },
|
||||
{ type: QueryConditionType.ALL, componentTypes: [VelocityComponent], mask: /* 位掩码 */ }
|
||||
]);
|
||||
```
|
||||
|
||||
## 场景级别的实体查询
|
||||
|
||||
除了使用QuerySystem,您还可以直接使用Scene提供的便捷查询方法:
|
||||
|
||||
### 基本场景查询
|
||||
|
||||
```typescript
|
||||
// 按名称查找实体
|
||||
const player = scene.findEntity("Player");
|
||||
const playerAlt = scene.getEntityByName("Player"); // 别名方法
|
||||
|
||||
// 按ID查找实体
|
||||
const entity = scene.findEntityById(123);
|
||||
|
||||
// 按标签查找实体
|
||||
const enemies = scene.findEntitiesByTag(2);
|
||||
const enemiesAlt = scene.getEntitiesByTag(2); // 别名方法
|
||||
|
||||
// 获取所有实体
|
||||
const allEntities = scene.entities.buffer;
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 1. 缓存管理
|
||||
|
||||
```typescript
|
||||
// 设置缓存配置
|
||||
querySystem.setCacheConfig(200, 2000); // 最大200个缓存项,2秒超时
|
||||
|
||||
// 清空缓存
|
||||
querySystem.clearCache();
|
||||
|
||||
// 预热常用查询
|
||||
const commonQueries = [
|
||||
{ type: QueryConditionType.ALL, componentTypes: [PositionComponent], mask: /* 位掩码 */ },
|
||||
{ type: QueryConditionType.ALL, componentTypes: [VelocityComponent], mask: /* 位掩码 */ }
|
||||
];
|
||||
querySystem.warmUpCache(commonQueries);
|
||||
```
|
||||
|
||||
### 2. 索引优化
|
||||
|
||||
```typescript
|
||||
// 自动优化索引配置
|
||||
querySystem.optimizeIndexes();
|
||||
|
||||
// 获取性能统计
|
||||
const stats = querySystem.getStats();
|
||||
console.log(`缓存命中率: ${(stats.hitRate * 100).toFixed(1)}%`);
|
||||
console.log(`实体数量: ${stats.entityCount}`);
|
||||
|
||||
// 获取详细性能报告
|
||||
const report = querySystem.getPerformanceReport();
|
||||
console.log(report);
|
||||
```
|
||||
|
||||
### 3. 查询监听和快照
|
||||
|
||||
```typescript
|
||||
// 监听查询结果变更
|
||||
const unwatch = querySystem.watchQuery(
|
||||
{ type: QueryConditionType.ALL, componentTypes: [EnemyComponent], mask: /* 位掩码 */ },
|
||||
(entities, changeType) => {
|
||||
console.log(`敌人实体${changeType}: ${entities.length}个`);
|
||||
}
|
||||
);
|
||||
|
||||
// 取消监听
|
||||
unwatch();
|
||||
|
||||
// 创建查询快照
|
||||
const snapshot1 = querySystem.createSnapshot(
|
||||
{ type: QueryConditionType.ALL, componentTypes: [PlayerComponent], mask: /* 位掩码 */ }
|
||||
);
|
||||
|
||||
// 稍后创建另一个快照
|
||||
const snapshot2 = querySystem.createSnapshot(
|
||||
{ type: QueryConditionType.ALL, componentTypes: [PlayerComponent], mask: /* 位掩码 */ }
|
||||
);
|
||||
|
||||
// 比较快照
|
||||
const diff = querySystem.compareSnapshots(snapshot1, snapshot2);
|
||||
console.log(`新增: ${diff.added.length}, 移除: ${diff.removed.length}`);
|
||||
```
|
||||
|
||||
## 实际使用示例
|
||||
|
||||
### 移动系统示例
|
||||
|
||||
```typescript
|
||||
import { EntitySystem } from '@esengine/ecs-framework';
|
||||
|
||||
class MovementSystem extends EntitySystem {
|
||||
public update(): void {
|
||||
// 查询所有可移动的实体
|
||||
const movableEntities = this.scene.querySystem.queryTwoComponents(
|
||||
PositionComponent,
|
||||
VelocityComponent
|
||||
);
|
||||
|
||||
movableEntities.forEach(({ entity, component1: position, component2: velocity }) => {
|
||||
// 更新位置
|
||||
position.x += velocity.x * Time.deltaTime;
|
||||
position.y += velocity.y * Time.deltaTime;
|
||||
|
||||
// 边界检查
|
||||
if (position.x < 0 || position.x > 800) {
|
||||
velocity.x = -velocity.x;
|
||||
}
|
||||
if (position.y < 0 || position.y > 600) {
|
||||
velocity.y = -velocity.y;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 碰撞检测示例
|
||||
|
||||
```typescript
|
||||
class CollisionSystem extends EntitySystem {
|
||||
public update(): void {
|
||||
// 获取所有具有碰撞器的实体
|
||||
const collidableEntities = this.scene.querySystem.queryTwoComponents(
|
||||
PositionComponent,
|
||||
ColliderComponent
|
||||
);
|
||||
|
||||
// 检测碰撞
|
||||
for (let i = 0; i < collidableEntities.length; i++) {
|
||||
for (let j = i + 1; j < collidableEntities.length; j++) {
|
||||
const entityA = collidableEntities[i];
|
||||
const entityB = collidableEntities[j];
|
||||
|
||||
if (this.checkCollision(entityA, entityB)) {
|
||||
this.handleCollision(entityA.entity, entityB.entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private checkCollision(entityA: any, entityB: any): boolean {
|
||||
// 简单的距离检测
|
||||
const posA = entityA.component1;
|
||||
const posB = entityB.component1;
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(posA.x - posB.x, 2) + Math.pow(posA.y - posB.y, 2)
|
||||
);
|
||||
return distance < 50;
|
||||
}
|
||||
|
||||
private handleCollision(entityA: Entity, entityB: Entity): void {
|
||||
console.log(`碰撞检测: ${entityA.name} 与 ${entityB.name}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 生命值管理示例
|
||||
|
||||
```typescript
|
||||
class HealthSystem extends EntitySystem {
|
||||
public update(): void {
|
||||
// 查询所有具有生命值的实体
|
||||
const healthEntities = this.scene.querySystem.queryComponentTyped(HealthComponent);
|
||||
const deadEntities: Entity[] = [];
|
||||
|
||||
healthEntities.forEach(({ entity, component: health }) => {
|
||||
// 检查死亡
|
||||
if (health.currentHealth <= 0) {
|
||||
deadEntities.push(entity);
|
||||
}
|
||||
});
|
||||
|
||||
// 移除死亡实体
|
||||
deadEntities.forEach(entity => {
|
||||
console.log(`实体 ${entity.name} 已死亡`);
|
||||
entity.destroy();
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 查询优化
|
||||
|
||||
- 尽量使用类型安全的查询方法
|
||||
- 避免在每帧都创建新的查询
|
||||
- 合理使用缓存机制
|
||||
|
||||
### 2. 性能监控
|
||||
|
||||
- 定期检查查询性能报告
|
||||
- 监控缓存命中率
|
||||
- 优化频繁使用的查询
|
||||
|
||||
### 3. 内存管理
|
||||
|
||||
- 及时清理不需要的查询监听器
|
||||
- 合理设置缓存大小
|
||||
- 避免创建过多的查询快照
|
||||
|
||||
### 4. 代码组织
|
||||
|
||||
- 将复杂查询封装到专门的方法中
|
||||
- 使用查询构建器创建可读性更好的查询
|
||||
- 在系统中合理组织查询逻辑
|
||||
483
examples/core-demos/index.html
Normal file
483
examples/core-demos/index.html
Normal file
@@ -0,0 +1,483 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ECS Framework Core Demos</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
background: #0f0f23;
|
||||
color: #e0e0e0;
|
||||
overflow: hidden;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* 侧边栏 */
|
||||
.sidebar {
|
||||
width: 280px;
|
||||
background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%);
|
||||
border-right: 2px solid #0a3d62;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 4px 0 20px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 30px 20px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sidebar-header h1 {
|
||||
font-size: 1.5em;
|
||||
color: white;
|
||||
margin-bottom: 8px;
|
||||
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.sidebar-header p {
|
||||
font-size: 0.85em;
|
||||
color: rgba(255,255,255,0.9);
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.demo-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
.demo-category {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.category-title {
|
||||
padding: 12px 20px;
|
||||
color: #8892b0;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.demo-item {
|
||||
padding: 14px 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border-left: 3px solid transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.demo-item:hover {
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
border-left-color: #667eea;
|
||||
}
|
||||
|
||||
.demo-item.active {
|
||||
background: rgba(102, 126, 234, 0.2);
|
||||
border-left-color: #667eea;
|
||||
}
|
||||
|
||||
.demo-icon {
|
||||
font-size: 20px;
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.demo-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.demo-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #ccd6f6;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.demo-item.active .demo-name {
|
||||
color: #667eea;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.demo-desc {
|
||||
font-size: 11px;
|
||||
color: #8892b0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: 20px;
|
||||
border-top: 1px solid rgba(255,255,255,0.1);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.github-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 20px;
|
||||
background: rgba(102, 126, 234, 0.2);
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.github-link:hover {
|
||||
background: rgba(102, 126, 234, 0.3);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* 主内容区 */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content-header {
|
||||
padding: 25px 40px;
|
||||
background: #1a1a2e;
|
||||
border-bottom: 2px solid #0a3d62;
|
||||
}
|
||||
|
||||
.content-header h2 {
|
||||
font-size: 2em;
|
||||
color: #ccd6f6;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.content-header p {
|
||||
color: #8892b0;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.demo-canvas-container {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: #0a0a15;
|
||||
}
|
||||
|
||||
#demoCanvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 控制面板 */
|
||||
.control-panel {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
width: 320px;
|
||||
background: rgba(26, 26, 46, 0.95);
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(102, 126, 234, 0.3);
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
|
||||
backdrop-filter: blur(10px);
|
||||
max-height: calc(100% - 40px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.control-panel-header {
|
||||
padding: 15px 20px;
|
||||
background: linear-gradient(135deg, rgba(102, 126, 234, 0.2) 0%, rgba(118, 75, 162, 0.2) 100%);
|
||||
border-bottom: 1px solid rgba(102, 126, 234, 0.3);
|
||||
border-radius: 12px 12px 0 0;
|
||||
}
|
||||
|
||||
.control-panel-header h3 {
|
||||
color: #667eea;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.control-panel-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.control-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.control-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.control-section h4 {
|
||||
color: #8892b0;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px 16px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
button.secondary {
|
||||
background: rgba(102, 126, 234, 0.2);
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
button.secondary:hover {
|
||||
background: rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
button.danger {
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
}
|
||||
|
||||
button.success {
|
||||
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.input-group label {
|
||||
display: block;
|
||||
color: #8892b0;
|
||||
font-size: 12px;
|
||||
margin-bottom: 6px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.input-group input {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
background: rgba(255,255,255,0.05);
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
border-radius: 6px;
|
||||
color: #ccd6f6;
|
||||
font-size: 13px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.input-group input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
background: rgba(255,255,255,0.08);
|
||||
}
|
||||
|
||||
/* 统计信息 */
|
||||
.stats-panel {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
background: rgba(255,255,255,0.03);
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(255,255,255,0.05);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #8892b0;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
color: #667eea;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Toast通知 */
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 30px;
|
||||
right: 30px;
|
||||
background: rgba(26, 26, 46, 0.98);
|
||||
border: 1px solid #667eea;
|
||||
border-radius: 8px;
|
||||
padding: 15px 20px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
|
||||
transform: translateY(150%);
|
||||
transition: transform 0.3s ease;
|
||||
z-index: 1000;
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
.toast.show {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.toast-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.toast-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.toast-message {
|
||||
color: #ccd6f6;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(255,255,255,0.05);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(102, 126, 234, 0.5);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(102, 126, 234, 0.7);
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
.loading {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 4px solid rgba(102, 126, 234, 0.2);
|
||||
border-top-color: #667eea;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
margin-top: 15px;
|
||||
color: #8892b0;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-container">
|
||||
<!-- 侧边栏 -->
|
||||
<div class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h1>🎮 ECS Core Demos</h1>
|
||||
<p>交互式演示集合</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-list" id="demoList">
|
||||
<!-- Demo列表将通过JS动态生成 -->
|
||||
</div>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<a href="https://github.com/esengine/ecs-framework" target="_blank" class="github-link">
|
||||
<span>⭐</span>
|
||||
<span>GitHub</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<div class="main-content">
|
||||
<div class="content-header">
|
||||
<h2 id="demoTitle">选择一个演示开始</h2>
|
||||
<p id="demoDescription">从左侧菜单选择一个演示查看效果</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-canvas-container">
|
||||
<canvas id="demoCanvas"></canvas>
|
||||
|
||||
<!-- 控制面板 -->
|
||||
<div class="control-panel" id="controlPanel" style="display: none;">
|
||||
<div class="control-panel-header">
|
||||
<h3>控制面板</h3>
|
||||
</div>
|
||||
<div class="control-panel-content" id="controlPanelContent">
|
||||
<!-- 控制内容将由各个demo动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载动画 -->
|
||||
<div class="loading" id="loading" style="display: none;">
|
||||
<div class="loading-spinner"></div>
|
||||
<div class="loading-text">加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast通知 -->
|
||||
<div class="toast" id="toast">
|
||||
<div class="toast-content">
|
||||
<span class="toast-icon">✅</span>
|
||||
<span class="toast-message" id="toastMessage"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
630
examples/core-demos/package-lock.json
generated
Normal file
630
examples/core-demos/package-lock.json
generated
Normal file
@@ -0,0 +1,630 @@
|
||||
{
|
||||
"name": "ecs-core-demos",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ecs-core-demos",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@esengine/ecs-framework": "file:../../packages/core"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"../../packages/core": {
|
||||
"name": "@esengine/ecs-framework",
|
||||
"version": "2.1.51",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"msgpack-lite": "^0.1.26"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.28.3",
|
||||
"@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1",
|
||||
"@babel/plugin-transform-optional-chaining": "^7.27.1",
|
||||
"@babel/preset-env": "^7.28.3",
|
||||
"@rollup/plugin-babel": "^6.0.4",
|
||||
"@rollup/plugin-commonjs": "^28.0.3",
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/msgpack-lite": "^0.1.11",
|
||||
"@types/node": "^20.19.17",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"rimraf": "^5.0.0",
|
||||
"rollup": "^4.42.0",
|
||||
"rollup-plugin-dts": "^6.2.1",
|
||||
"ts-jest": "^29.4.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
|
||||
"integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
|
||||
"integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
|
||||
"integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
|
||||
"integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
|
||||
"integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
|
||||
"integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
|
||||
"integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
|
||||
"integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
|
||||
"integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
|
||||
"integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
|
||||
"integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
|
||||
"integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
|
||||
"integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
|
||||
"integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
|
||||
"integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
|
||||
"integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
|
||||
"integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
|
||||
"integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
|
||||
"integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
|
||||
"integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
|
||||
"integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
|
||||
"integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esengine/ecs-framework": {
|
||||
"resolved": "../../packages/core",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
|
||||
"integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/android-arm": "0.18.20",
|
||||
"@esbuild/android-arm64": "0.18.20",
|
||||
"@esbuild/android-x64": "0.18.20",
|
||||
"@esbuild/darwin-arm64": "0.18.20",
|
||||
"@esbuild/darwin-x64": "0.18.20",
|
||||
"@esbuild/freebsd-arm64": "0.18.20",
|
||||
"@esbuild/freebsd-x64": "0.18.20",
|
||||
"@esbuild/linux-arm": "0.18.20",
|
||||
"@esbuild/linux-arm64": "0.18.20",
|
||||
"@esbuild/linux-ia32": "0.18.20",
|
||||
"@esbuild/linux-loong64": "0.18.20",
|
||||
"@esbuild/linux-mips64el": "0.18.20",
|
||||
"@esbuild/linux-ppc64": "0.18.20",
|
||||
"@esbuild/linux-riscv64": "0.18.20",
|
||||
"@esbuild/linux-s390x": "0.18.20",
|
||||
"@esbuild/linux-x64": "0.18.20",
|
||||
"@esbuild/netbsd-x64": "0.18.20",
|
||||
"@esbuild/openbsd-x64": "0.18.20",
|
||||
"@esbuild/sunos-x64": "0.18.20",
|
||||
"@esbuild/win32-arm64": "0.18.20",
|
||||
"@esbuild/win32-ia32": "0.18.20",
|
||||
"@esbuild/win32-x64": "0.18.20"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "3.29.5",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz",
|
||||
"integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.18.0",
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "4.5.14",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.14.tgz",
|
||||
"integrity": "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.18.10",
|
||||
"postcss": "^8.4.27",
|
||||
"rollup": "^3.27.1"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">= 14",
|
||||
"less": "*",
|
||||
"lightningcss": "^1.21.0",
|
||||
"sass": "*",
|
||||
"stylus": "*",
|
||||
"sugarss": "*",
|
||||
"terser": "^5.4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"less": {
|
||||
"optional": true
|
||||
},
|
||||
"lightningcss": {
|
||||
"optional": true
|
||||
},
|
||||
"sass": {
|
||||
"optional": true
|
||||
},
|
||||
"stylus": {
|
||||
"optional": true
|
||||
},
|
||||
"sugarss": {
|
||||
"optional": true
|
||||
},
|
||||
"terser": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
examples/core-demos/package.json
Normal file
18
examples/core-demos/package.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "ecs-core-demos",
|
||||
"version": "1.0.0",
|
||||
"description": "ECS Framework Core Demos - Multiple Interactive Examples",
|
||||
"main": "src/main.ts",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@esengine/ecs-framework": "file:../../packages/core"
|
||||
}
|
||||
}
|
||||
99
examples/core-demos/src/demos/DemoBase.ts
Normal file
99
examples/core-demos/src/demos/DemoBase.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { Scene, Core } from '@esengine/ecs-framework';
|
||||
|
||||
export interface DemoInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
category: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export abstract class DemoBase {
|
||||
protected scene: Scene;
|
||||
protected canvas: HTMLCanvasElement;
|
||||
protected ctx: CanvasRenderingContext2D;
|
||||
protected controlPanel: HTMLElement;
|
||||
protected isRunning: boolean = false;
|
||||
protected animationFrameId: number | null = null;
|
||||
protected lastTime: number = 0;
|
||||
|
||||
constructor(canvas: HTMLCanvasElement, controlPanel: HTMLElement) {
|
||||
this.canvas = canvas;
|
||||
this.ctx = canvas.getContext('2d')!;
|
||||
this.controlPanel = controlPanel;
|
||||
this.scene = new Scene({ name: this.getInfo().name });
|
||||
|
||||
// 设置canvas大小
|
||||
this.resizeCanvas();
|
||||
window.addEventListener('resize', () => this.resizeCanvas());
|
||||
}
|
||||
|
||||
abstract getInfo(): DemoInfo;
|
||||
abstract setup(): void;
|
||||
abstract createControls(): void;
|
||||
|
||||
protected resizeCanvas() {
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
this.canvas.width = rect.width;
|
||||
this.canvas.height = rect.height;
|
||||
}
|
||||
|
||||
public start() {
|
||||
if (this.isRunning) return;
|
||||
this.isRunning = true;
|
||||
this.lastTime = performance.now();
|
||||
|
||||
// 设置当前场景到Core
|
||||
Core.setScene(this.scene);
|
||||
|
||||
this.scene.begin();
|
||||
this.loop();
|
||||
}
|
||||
|
||||
public stop() {
|
||||
this.isRunning = false;
|
||||
if (this.animationFrameId !== null) {
|
||||
cancelAnimationFrame(this.animationFrameId);
|
||||
this.animationFrameId = null;
|
||||
}
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.stop();
|
||||
this.scene.end();
|
||||
}
|
||||
|
||||
protected loop = () => {
|
||||
if (!this.isRunning) return;
|
||||
|
||||
// 计算deltaTime
|
||||
const currentTime = performance.now();
|
||||
const deltaTime = (currentTime - this.lastTime) / 1000; // 转换为秒
|
||||
this.lastTime = currentTime;
|
||||
|
||||
// 更新ECS框架
|
||||
Core.update(deltaTime);
|
||||
|
||||
// 渲染
|
||||
this.render();
|
||||
|
||||
// 继续循环
|
||||
this.animationFrameId = requestAnimationFrame(this.loop);
|
||||
}
|
||||
|
||||
protected abstract render(): void;
|
||||
|
||||
protected showToast(message: string, icon: string = '✅') {
|
||||
const toast = document.getElementById('toast')!;
|
||||
const toastMessage = document.getElementById('toastMessage')!;
|
||||
const toastIcon = toast.querySelector('.toast-icon')!;
|
||||
|
||||
toastIcon.textContent = icon;
|
||||
toastMessage.textContent = message;
|
||||
|
||||
toast.classList.add('show');
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('show');
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
468
examples/core-demos/src/demos/IncrementalSerializationDemo.ts
Normal file
468
examples/core-demos/src/demos/IncrementalSerializationDemo.ts
Normal file
@@ -0,0 +1,468 @@
|
||||
import { DemoBase, DemoInfo } from './DemoBase';
|
||||
import {
|
||||
Component,
|
||||
ECSComponent,
|
||||
Entity,
|
||||
EntitySystem,
|
||||
Matcher,
|
||||
Serializable,
|
||||
Serialize,
|
||||
IncrementalSerializer
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
// ===== 组件定义 =====
|
||||
@ECSComponent('IncDemo_Position')
|
||||
@Serializable({ version: 1, typeId: 'IncDemo_Position' })
|
||||
class PositionComponent extends Component {
|
||||
@Serialize() x: number = 0;
|
||||
@Serialize() y: number = 0;
|
||||
constructor(x: number = 0, y: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('IncDemo_Velocity')
|
||||
@Serializable({ version: 1, typeId: 'IncDemo_Velocity' })
|
||||
class VelocityComponent extends Component {
|
||||
@Serialize() vx: number = 0;
|
||||
@Serialize() vy: number = 0;
|
||||
constructor(vx: number = 0, vy: number = 0) {
|
||||
super();
|
||||
this.vx = vx;
|
||||
this.vy = vy;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('IncDemo_Renderable')
|
||||
@Serializable({ version: 1, typeId: 'IncDemo_Renderable' })
|
||||
class RenderableComponent extends Component {
|
||||
@Serialize() color: string = '#ffffff';
|
||||
@Serialize() radius: number = 10;
|
||||
constructor(color: string = '#ffffff', radius: number = 10) {
|
||||
super();
|
||||
this.color = color;
|
||||
this.radius = radius;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 系统定义 =====
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(PositionComponent, VelocityComponent));
|
||||
}
|
||||
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const [pos, vel] = this.getComponents(entity, PositionComponent, VelocityComponent);
|
||||
|
||||
pos.x += vel.vx;
|
||||
pos.y += vel.vy;
|
||||
|
||||
if (pos.x < 0 || pos.x > 1200) vel.vx *= -1;
|
||||
if (pos.y < 0 || pos.y > 600) vel.vy *= -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RenderSystem extends EntitySystem {
|
||||
private canvas: HTMLCanvasElement;
|
||||
private ctx: CanvasRenderingContext2D;
|
||||
|
||||
constructor(canvas: HTMLCanvasElement) {
|
||||
super(Matcher.all(PositionComponent, RenderableComponent));
|
||||
this.canvas = canvas;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) throw new Error('Failed to get canvas context');
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
this.ctx.fillStyle = '#0a0a15';
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
for (const entity of entities) {
|
||||
const [pos, render] = this.getComponents(entity, PositionComponent, RenderableComponent);
|
||||
|
||||
this.ctx.fillStyle = render.color;
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(pos.x, pos.y, render.radius, 0, Math.PI * 2);
|
||||
this.ctx.fill();
|
||||
|
||||
this.ctx.fillStyle = 'white';
|
||||
this.ctx.font = '10px Arial';
|
||||
this.ctx.textAlign = 'center';
|
||||
this.ctx.fillText(entity.name, pos.x, pos.y - render.radius - 5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class IncrementalSerializationDemo extends DemoBase {
|
||||
private renderSystem!: RenderSystem;
|
||||
private incrementalHistory: any[] = [];
|
||||
private autoSnapshotInterval: number | null = null;
|
||||
|
||||
getInfo(): DemoInfo {
|
||||
return {
|
||||
id: 'incremental-serialization',
|
||||
name: '增量序列化',
|
||||
description: '演示增量序列化功能,只保存场景变更而非完整状态,适用于网络同步和回放系统',
|
||||
category: '核心功能',
|
||||
icon: '🔄'
|
||||
};
|
||||
}
|
||||
|
||||
setup() {
|
||||
// 创建控制面板
|
||||
this.createControls();
|
||||
|
||||
// 添加系统
|
||||
this.renderSystem = new RenderSystem(this.canvas);
|
||||
this.scene.addEntityProcessor(new MovementSystem());
|
||||
this.scene.addEntityProcessor(this.renderSystem);
|
||||
|
||||
// 创建初始实体
|
||||
this.createInitialEntities();
|
||||
|
||||
// 创建基础快照
|
||||
this.scene.createIncrementalSnapshot();
|
||||
this.addToHistory('Initial State');
|
||||
}
|
||||
|
||||
private createInitialEntities() {
|
||||
// 创建玩家
|
||||
const player = this.scene.createEntity('Player');
|
||||
player.addComponent(new PositionComponent(600, 300));
|
||||
player.addComponent(new VelocityComponent(2, 1.5));
|
||||
player.addComponent(new RenderableComponent('#4a9eff', 15));
|
||||
|
||||
// 设置场景数据
|
||||
this.scene.sceneData.set('gameTime', 0);
|
||||
this.scene.sceneData.set('score', 0);
|
||||
}
|
||||
|
||||
private createRandomEntity() {
|
||||
const entity = this.scene.createEntity(`Entity_${Date.now()}`);
|
||||
entity.addComponent(new PositionComponent(
|
||||
Math.random() * this.canvas.width,
|
||||
Math.random() * this.canvas.height
|
||||
));
|
||||
entity.addComponent(new VelocityComponent(
|
||||
(Math.random() - 0.5) * 3,
|
||||
(Math.random() - 0.5) * 3
|
||||
));
|
||||
const colors = ['#ff6b6b', '#4ecdc4', '#ffe66d', '#a8dadc', '#f1faee'];
|
||||
entity.addComponent(new RenderableComponent(
|
||||
colors[Math.floor(Math.random() * colors.length)],
|
||||
5 + Math.random() * 10
|
||||
));
|
||||
}
|
||||
|
||||
private addToHistory(label: string) {
|
||||
const incremental = this.scene.serializeIncremental();
|
||||
const stats = IncrementalSerializer.getIncrementalStats(incremental);
|
||||
|
||||
// 计算JSON和二进制格式的大小
|
||||
const jsonSize = IncrementalSerializer.getIncrementalSize(incremental, 'json');
|
||||
const binarySize = IncrementalSerializer.getIncrementalSize(incremental, 'binary');
|
||||
|
||||
this.incrementalHistory.push({
|
||||
label,
|
||||
incremental,
|
||||
stats,
|
||||
timestamp: Date.now(),
|
||||
jsonSize,
|
||||
binarySize
|
||||
});
|
||||
|
||||
this.scene.updateIncrementalSnapshot();
|
||||
this.updateHistoryPanel();
|
||||
this.updateStats();
|
||||
}
|
||||
|
||||
createControls() {
|
||||
this.controlPanel.innerHTML = `
|
||||
<div class="control-section">
|
||||
<h4>实体控制</h4>
|
||||
<div class="button-group">
|
||||
<button id="addEntity" class="secondary">添加随机实体</button>
|
||||
<button id="removeEntity" class="danger">删除最后一个实体</button>
|
||||
<button id="modifyEntity" class="secondary">修改实体数据</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-section">
|
||||
<h4>增量快照</h4>
|
||||
<div class="button-group">
|
||||
<button id="captureSnapshot" class="success">捕获当前状态</button>
|
||||
<button id="clearHistory" class="danger">清空历史</button>
|
||||
</div>
|
||||
<div style="margin-top: 10px;">
|
||||
<label>
|
||||
<input type="checkbox" id="autoSnapshot">
|
||||
自动快照(每2秒)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-section">
|
||||
<h4>场景数据控制</h4>
|
||||
<div class="input-group">
|
||||
<label>游戏时间</label>
|
||||
<input type="number" id="gameTime" value="0" step="1">
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label>分数</label>
|
||||
<input type="number" id="score" value="0" step="10">
|
||||
</div>
|
||||
<button id="updateSceneData" class="secondary">更新场景数据</button>
|
||||
</div>
|
||||
|
||||
<div class="stats-panel">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">实体数量</div>
|
||||
<div class="stat-value" id="entityCount">0</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">历史记录</div>
|
||||
<div class="stat-value" id="historyCount">0</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">JSON大小</div>
|
||||
<div class="stat-value" id="jsonSize">0B</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">二进制大小</div>
|
||||
<div class="stat-value" id="binarySize">0B</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">压缩率</div>
|
||||
<div class="stat-value" id="compressionRatio">0%</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">总变更数</div>
|
||||
<div class="stat-value" id="totalChanges">0</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-section">
|
||||
<h4>增量历史 <small style="color: #8892b0;">(点击快照查看详情)</small></h4>
|
||||
<div style="max-height: 300px; overflow-y: auto; background: rgba(0,0,0,0.3); padding: 10px; border-radius: 6px;" id="historyPanel">
|
||||
暂无历史记录
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-section">
|
||||
<h4>快照详情</h4>
|
||||
<div style="max-height: 200px; overflow-y: auto; background: rgba(0,0,0,0.3); padding: 10px; border-radius: 6px; font-family: monospace; font-size: 11px; color: #8892b0;" id="snapshotDetails">
|
||||
点击历史记录查看详情...
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.bindEvents();
|
||||
this.updateStats();
|
||||
}
|
||||
|
||||
private bindEvents() {
|
||||
document.getElementById('addEntity')!.addEventListener('click', () => {
|
||||
this.createRandomEntity();
|
||||
this.addToHistory('添加实体');
|
||||
this.showToast('添加了一个随机实体');
|
||||
});
|
||||
|
||||
document.getElementById('removeEntity')!.addEventListener('click', () => {
|
||||
const entities = this.scene.entities.buffer;
|
||||
if (entities.length > 1) {
|
||||
const lastEntity = entities[entities.length - 1];
|
||||
lastEntity.destroy();
|
||||
this.addToHistory('删除实体');
|
||||
this.showToast('删除了最后一个实体');
|
||||
} else {
|
||||
this.showToast('至少保留一个实体', '⚠️');
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('modifyEntity')!.addEventListener('click', () => {
|
||||
const entities = this.scene.entities.buffer;
|
||||
if (entities.length > 0) {
|
||||
const randomEntity = entities[Math.floor(Math.random() * entities.length)];
|
||||
const pos = randomEntity.getComponent(PositionComponent);
|
||||
if (pos) {
|
||||
pos.x = Math.random() * this.canvas.width;
|
||||
pos.y = Math.random() * this.canvas.height;
|
||||
}
|
||||
this.addToHistory('修改实体位置');
|
||||
this.showToast(`修改了 ${randomEntity.name} 的位置`);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('captureSnapshot')!.addEventListener('click', () => {
|
||||
this.addToHistory('手动快照');
|
||||
this.showToast('已捕获当前状态', '📸');
|
||||
});
|
||||
|
||||
document.getElementById('clearHistory')!.addEventListener('click', () => {
|
||||
this.incrementalHistory = [];
|
||||
this.scene.createIncrementalSnapshot();
|
||||
this.addToHistory('清空后重新开始');
|
||||
this.showToast('历史记录已清空');
|
||||
});
|
||||
|
||||
document.getElementById('autoSnapshot')!.addEventListener('change', (e) => {
|
||||
const checkbox = e.target as HTMLInputElement;
|
||||
if (checkbox.checked) {
|
||||
this.autoSnapshotInterval = window.setInterval(() => {
|
||||
this.addToHistory('自动快照');
|
||||
}, 2000);
|
||||
this.showToast('自动快照已启用', '⏱️');
|
||||
} else {
|
||||
if (this.autoSnapshotInterval !== null) {
|
||||
clearInterval(this.autoSnapshotInterval);
|
||||
this.autoSnapshotInterval = null;
|
||||
}
|
||||
this.showToast('自动快照已禁用');
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('updateSceneData')!.addEventListener('click', () => {
|
||||
const gameTime = parseInt((document.getElementById('gameTime') as HTMLInputElement).value);
|
||||
const score = parseInt((document.getElementById('score') as HTMLInputElement).value);
|
||||
|
||||
this.scene.sceneData.set('gameTime', gameTime);
|
||||
this.scene.sceneData.set('score', score);
|
||||
|
||||
this.addToHistory('更新场景数据');
|
||||
this.showToast('场景数据已更新');
|
||||
});
|
||||
}
|
||||
|
||||
private updateHistoryPanel() {
|
||||
const panel = document.getElementById('historyPanel')!;
|
||||
|
||||
if (this.incrementalHistory.length === 0) {
|
||||
panel.innerHTML = '暂无历史记录';
|
||||
return;
|
||||
}
|
||||
|
||||
panel.innerHTML = this.incrementalHistory.map((item, index) => {
|
||||
const isLatest = index === this.incrementalHistory.length - 1;
|
||||
const time = new Date(item.timestamp).toLocaleTimeString();
|
||||
|
||||
return `
|
||||
<div class="history-item" data-index="${index}" style="
|
||||
padding: 8px;
|
||||
margin: 4px 0;
|
||||
background: ${isLatest ? 'rgba(74, 158, 255, 0.2)' : 'rgba(136, 146, 176, 0.1)'};
|
||||
border-left: 3px solid ${isLatest ? '#4a9eff' : '#8892b0'};
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<div>
|
||||
<strong>${item.label}</strong>
|
||||
${isLatest ? '<span style="color: #4a9eff; margin-left: 8px;">●</span>' : ''}
|
||||
</div>
|
||||
<small style="color: #8892b0;">${time}</small>
|
||||
</div>
|
||||
<div style="font-size: 11px; color: #8892b0; margin-top: 4px;">
|
||||
实体: +${item.stats.addedEntities} -${item.stats.removedEntities} ~${item.stats.updatedEntities} |
|
||||
组件: +${item.stats.addedComponents} -${item.stats.removedComponents} ~${item.stats.updatedComponents}
|
||||
</div>
|
||||
<div style="font-size: 11px; color: #8892b0; margin-top: 2px;">
|
||||
JSON: ${this.formatBytes(item.jsonSize)} |
|
||||
Binary: ${this.formatBytes(item.binarySize)} |
|
||||
<span style="color: #4ecdc4;">节省: ${((1 - item.binarySize / item.jsonSize) * 100).toFixed(1)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// 绑定点击事件
|
||||
panel.querySelectorAll('.history-item').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
const index = parseInt(item.getAttribute('data-index')!);
|
||||
this.showSnapshotDetails(index);
|
||||
});
|
||||
});
|
||||
|
||||
// 自动滚动到底部
|
||||
panel.scrollTop = panel.scrollHeight;
|
||||
}
|
||||
|
||||
private showSnapshotDetails(index: number) {
|
||||
const item = this.incrementalHistory[index];
|
||||
const detailsPanel = document.getElementById('snapshotDetails')!;
|
||||
|
||||
const compressionRatio = ((1 - item.binarySize / item.jsonSize) * 100).toFixed(1);
|
||||
|
||||
const details = {
|
||||
版本: item.incremental.version,
|
||||
基础版本: item.incremental.baseVersion,
|
||||
时间戳: new Date(item.incremental.timestamp).toLocaleString(),
|
||||
场景名称: item.incremental.sceneName,
|
||||
格式对比: {
|
||||
JSON大小: this.formatBytes(item.jsonSize),
|
||||
二进制大小: this.formatBytes(item.binarySize),
|
||||
压缩率: `${compressionRatio}%`,
|
||||
节省字节: this.formatBytes(item.jsonSize - item.binarySize)
|
||||
},
|
||||
统计: item.stats,
|
||||
实体变更: item.incremental.entityChanges.map((c: any) => ({
|
||||
操作: c.operation,
|
||||
实体ID: c.entityId,
|
||||
实体名称: c.entityName
|
||||
})),
|
||||
组件变更: item.incremental.componentChanges.map((c: any) => ({
|
||||
操作: c.operation,
|
||||
实体ID: c.entityId,
|
||||
组件类型: c.componentType
|
||||
})),
|
||||
场景数据变更: item.incremental.sceneDataChanges.map((c: any) => ({
|
||||
键: c.key,
|
||||
值: c.value,
|
||||
已删除: c.deleted
|
||||
}))
|
||||
};
|
||||
|
||||
detailsPanel.textContent = JSON.stringify(details, null, 2);
|
||||
}
|
||||
|
||||
private updateStats() {
|
||||
document.getElementById('entityCount')!.textContent = this.scene.entities.count.toString();
|
||||
document.getElementById('historyCount')!.textContent = this.incrementalHistory.length.toString();
|
||||
|
||||
if (this.incrementalHistory.length > 0) {
|
||||
const lastItem = this.incrementalHistory[this.incrementalHistory.length - 1];
|
||||
|
||||
document.getElementById('jsonSize')!.textContent = this.formatBytes(lastItem.jsonSize);
|
||||
document.getElementById('binarySize')!.textContent = this.formatBytes(lastItem.binarySize);
|
||||
|
||||
const compressionRatio = ((1 - lastItem.binarySize / lastItem.jsonSize) * 100).toFixed(1);
|
||||
const ratioElement = document.getElementById('compressionRatio')!;
|
||||
ratioElement.textContent = `${compressionRatio}%`;
|
||||
ratioElement.style.color = parseFloat(compressionRatio) > 30 ? '#4ecdc4' : '#ffe66d';
|
||||
|
||||
document.getElementById('totalChanges')!.textContent = lastItem.stats.totalChanges.toString();
|
||||
}
|
||||
}
|
||||
|
||||
private formatBytes(bytes: number): string {
|
||||
if (bytes < 1024) return `${bytes}B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
// RenderSystem会处理渲染
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
if (this.autoSnapshotInterval !== null) {
|
||||
clearInterval(this.autoSnapshotInterval);
|
||||
}
|
||||
super.destroy();
|
||||
}
|
||||
}
|
||||
386
examples/core-demos/src/demos/SerializationDemo.ts
Normal file
386
examples/core-demos/src/demos/SerializationDemo.ts
Normal file
@@ -0,0 +1,386 @@
|
||||
import { DemoBase, DemoInfo } from './DemoBase';
|
||||
import {
|
||||
Component,
|
||||
ECSComponent,
|
||||
Entity,
|
||||
EntitySystem,
|
||||
Matcher,
|
||||
Serializable,
|
||||
Serialize,
|
||||
SerializeAsMap
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
// ===== 组件定义 =====
|
||||
@ECSComponent('SerDemo_Position')
|
||||
@Serializable({ version: 1, typeId: 'SerDemo_Position' })
|
||||
class PositionComponent extends Component {
|
||||
@Serialize() x: number = 0;
|
||||
@Serialize() y: number = 0;
|
||||
constructor(x: number = 0, y: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('SerDemo_Velocity')
|
||||
@Serializable({ version: 1, typeId: 'SerDemo_Velocity' })
|
||||
class VelocityComponent extends Component {
|
||||
@Serialize() vx: number = 0;
|
||||
@Serialize() vy: number = 0;
|
||||
constructor(vx: number = 0, vy: number = 0) {
|
||||
super();
|
||||
this.vx = vx;
|
||||
this.vy = vy;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('SerDemo_Renderable')
|
||||
@Serializable({ version: 1, typeId: 'SerDemo_Renderable' })
|
||||
class RenderableComponent extends Component {
|
||||
@Serialize() color: string = '#ffffff';
|
||||
@Serialize() radius: number = 10;
|
||||
constructor(color: string = '#ffffff', radius: number = 10) {
|
||||
super();
|
||||
this.color = color;
|
||||
this.radius = radius;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('SerDemo_Player')
|
||||
@Serializable({ version: 1, typeId: 'SerDemo_Player' })
|
||||
class PlayerComponent extends Component {
|
||||
@Serialize() name: string = 'Player';
|
||||
@Serialize() level: number = 1;
|
||||
@Serialize() health: number = 100;
|
||||
@SerializeAsMap() inventory: Map<string, number> = new Map();
|
||||
constructor(name: string = 'Player') {
|
||||
super();
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 系统定义 =====
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(PositionComponent, VelocityComponent));
|
||||
}
|
||||
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const [pos, vel] = this.getComponents(entity, PositionComponent, VelocityComponent);
|
||||
|
||||
pos.x += vel.vx;
|
||||
pos.y += vel.vy;
|
||||
|
||||
// 边界反弹
|
||||
if (pos.x < 0 || pos.x > 1200) vel.vx *= -1;
|
||||
if (pos.y < 0 || pos.y > 600) vel.vy *= -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RenderSystem extends EntitySystem {
|
||||
private canvas: HTMLCanvasElement;
|
||||
private ctx: CanvasRenderingContext2D;
|
||||
|
||||
constructor(canvas: HTMLCanvasElement) {
|
||||
super(Matcher.all(PositionComponent, RenderableComponent));
|
||||
this.canvas = canvas;
|
||||
this.ctx = canvas.getContext('2d')!;
|
||||
}
|
||||
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
// 清空画布
|
||||
this.ctx.fillStyle = '#0a0a15';
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
// 渲染所有实体
|
||||
for (const entity of entities) {
|
||||
const [pos, render] = this.getComponents(entity, PositionComponent, RenderableComponent);
|
||||
|
||||
this.ctx.fillStyle = render.color;
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(pos.x, pos.y, render.radius, 0, Math.PI * 2);
|
||||
this.ctx.fill();
|
||||
|
||||
// 如果是玩家,显示名字
|
||||
const player = entity.getComponent(PlayerComponent);
|
||||
if (player) {
|
||||
this.ctx.fillStyle = 'white';
|
||||
this.ctx.font = '12px Arial';
|
||||
this.ctx.textAlign = 'center';
|
||||
this.ctx.fillText(player.name, pos.x, pos.y - render.radius - 5);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class SerializationDemo extends DemoBase {
|
||||
private renderSystem!: RenderSystem;
|
||||
private jsonData: string = '';
|
||||
private binaryData: Buffer | null = null;
|
||||
|
||||
getInfo(): DemoInfo {
|
||||
return {
|
||||
id: 'serialization',
|
||||
name: '场景序列化',
|
||||
description: '演示场景的序列化和反序列化功能,支持JSON和二进制格式',
|
||||
category: '核心功能',
|
||||
icon: '💾'
|
||||
};
|
||||
}
|
||||
|
||||
setup() {
|
||||
// @ECSComponent装饰器会自动注册组件到ComponentRegistry
|
||||
// ComponentRegistry会被序列化系统自动使用,无需手动注册
|
||||
|
||||
// 添加系统
|
||||
this.renderSystem = new RenderSystem(this.canvas);
|
||||
this.scene.addEntityProcessor(new MovementSystem());
|
||||
this.scene.addEntityProcessor(this.renderSystem);
|
||||
|
||||
// 创建初始实体
|
||||
this.createInitialEntities();
|
||||
|
||||
// 创建控制面板
|
||||
this.createControls();
|
||||
}
|
||||
|
||||
private createInitialEntities() {
|
||||
// 创建玩家
|
||||
const player = this.scene.createEntity('Player');
|
||||
player.addComponent(new PositionComponent(600, 300));
|
||||
player.addComponent(new VelocityComponent(2, 1.5));
|
||||
player.addComponent(new RenderableComponent('#4a9eff', 15));
|
||||
const playerComp = new PlayerComponent('Hero');
|
||||
playerComp.level = 5;
|
||||
playerComp.health = 100;
|
||||
playerComp.inventory.set('sword', 1);
|
||||
playerComp.inventory.set('potion', 5);
|
||||
player.addComponent(playerComp);
|
||||
|
||||
// 创建一些随机实体
|
||||
for (let i = 0; i < 5; i++) {
|
||||
this.createRandomEntity();
|
||||
}
|
||||
|
||||
// 设置场景数据
|
||||
this.scene.sceneData.set('weather', 'sunny');
|
||||
this.scene.sceneData.set('gameTime', 12.5);
|
||||
this.scene.sceneData.set('difficulty', 'normal');
|
||||
}
|
||||
|
||||
private createRandomEntity() {
|
||||
const entity = this.scene.createEntity(`Entity_${Date.now()}`);
|
||||
entity.addComponent(new PositionComponent(
|
||||
Math.random() * this.canvas.width,
|
||||
Math.random() * this.canvas.height
|
||||
));
|
||||
entity.addComponent(new VelocityComponent(
|
||||
(Math.random() - 0.5) * 3,
|
||||
(Math.random() - 0.5) * 3
|
||||
));
|
||||
const colors = ['#ff6b6b', '#4ecdc4', '#ffe66d', '#a8dadc', '#f1faee'];
|
||||
entity.addComponent(new RenderableComponent(
|
||||
colors[Math.floor(Math.random() * colors.length)],
|
||||
5 + Math.random() * 10
|
||||
));
|
||||
}
|
||||
|
||||
createControls() {
|
||||
this.controlPanel.innerHTML = `
|
||||
<div class="control-section">
|
||||
<h4>实体控制</h4>
|
||||
<div class="button-group">
|
||||
<button id="addEntity" class="secondary">添加随机实体</button>
|
||||
<button id="clearEntities" class="danger">清空所有实体</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-section">
|
||||
<h4>序列化操作</h4>
|
||||
<div class="button-group">
|
||||
<button id="serializeJSON">序列化为JSON</button>
|
||||
<button id="serializeBinary" class="success">序列化为二进制</button>
|
||||
<button id="deserialize" class="secondary">反序列化恢复</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-section">
|
||||
<h4>本地存储</h4>
|
||||
<div class="button-group">
|
||||
<button id="saveLocal" class="success">保存到LocalStorage</button>
|
||||
<button id="loadLocal" class="secondary">从LocalStorage加载</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-section">
|
||||
<h4>场景数据</h4>
|
||||
<div class="input-group">
|
||||
<label>天气</label>
|
||||
<input type="text" id="weather" value="sunny" placeholder="sunny/rainy/snowy">
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label>游戏时间</label>
|
||||
<input type="number" id="gameTime" value="12.5" step="0.1" min="0" max="24">
|
||||
</div>
|
||||
<button id="updateSceneData" class="secondary">更新场景数据</button>
|
||||
</div>
|
||||
|
||||
<div class="stats-panel">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">实体数量</div>
|
||||
<div class="stat-value" id="entityCount">0</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">JSON大小</div>
|
||||
<div class="stat-value" id="jsonSize">0B</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">二进制大小</div>
|
||||
<div class="stat-value" id="binarySize">0B</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">压缩率</div>
|
||||
<div class="stat-value" id="compressionRatio">0%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-section">
|
||||
<h4>序列化数据预览</h4>
|
||||
<div style="max-height: 200px; overflow-y: auto; background: rgba(0,0,0,0.3); padding: 10px; border-radius: 6px; font-family: monospace; font-size: 11px; color: #8892b0; word-break: break-all;" id="dataPreview">
|
||||
点击序列化按钮查看数据...
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 绑定事件
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
private bindEvents() {
|
||||
document.getElementById('addEntity')!.addEventListener('click', () => {
|
||||
this.createRandomEntity();
|
||||
this.updateStats();
|
||||
this.showToast('添加了一个随机实体');
|
||||
});
|
||||
|
||||
document.getElementById('clearEntities')!.addEventListener('click', () => {
|
||||
this.scene.destroyAllEntities();
|
||||
this.createInitialEntities();
|
||||
this.updateStats();
|
||||
this.showToast('场景已重置');
|
||||
});
|
||||
|
||||
document.getElementById('serializeJSON')!.addEventListener('click', () => {
|
||||
this.jsonData = this.scene.serialize({ format: 'json', pretty: true }) as string;
|
||||
this.updateDataPreview(this.jsonData, 'json');
|
||||
this.updateStats();
|
||||
this.showToast('已序列化为JSON格式');
|
||||
});
|
||||
|
||||
document.getElementById('serializeBinary')!.addEventListener('click', () => {
|
||||
this.binaryData = this.scene.serialize({ format: 'binary' }) as Buffer;
|
||||
const base64 = this.binaryData.toString('base64');
|
||||
this.updateDataPreview(`Binary Data (Base64):\n${base64.substring(0, 500)}...`, 'binary');
|
||||
this.updateStats();
|
||||
this.showToast('已序列化为二进制格式', '🔐');
|
||||
});
|
||||
|
||||
document.getElementById('deserialize')!.addEventListener('click', () => {
|
||||
const data = this.binaryData || this.jsonData;
|
||||
if (!data) {
|
||||
this.showToast('请先执行序列化操作', '⚠️');
|
||||
return;
|
||||
}
|
||||
|
||||
this.scene.deserialize(data, {
|
||||
strategy: 'replace'
|
||||
// componentRegistry会自动从ComponentRegistry获取,无需手动传入
|
||||
});
|
||||
|
||||
this.updateStats();
|
||||
this.showToast('场景已恢复');
|
||||
});
|
||||
|
||||
document.getElementById('saveLocal')!.addEventListener('click', () => {
|
||||
const jsonData = this.scene.serialize({ format: 'json' }) as string;
|
||||
localStorage.setItem('ecs_demo_scene', jsonData);
|
||||
this.showToast('已保存到LocalStorage', '💾');
|
||||
});
|
||||
|
||||
document.getElementById('loadLocal')!.addEventListener('click', () => {
|
||||
const data = localStorage.getItem('ecs_demo_scene');
|
||||
if (!data) {
|
||||
this.showToast('LocalStorage中没有保存的场景', '⚠️');
|
||||
return;
|
||||
}
|
||||
|
||||
this.scene.deserialize(data, {
|
||||
strategy: 'replace'
|
||||
// componentRegistry会自动从ComponentRegistry获取,无需手动传入
|
||||
});
|
||||
|
||||
this.updateStats();
|
||||
this.showToast('已从LocalStorage加载', '📂');
|
||||
});
|
||||
|
||||
document.getElementById('updateSceneData')!.addEventListener('click', () => {
|
||||
const weather = (document.getElementById('weather') as HTMLInputElement).value;
|
||||
const gameTime = parseFloat((document.getElementById('gameTime') as HTMLInputElement).value);
|
||||
|
||||
this.scene.sceneData.set('weather', weather);
|
||||
this.scene.sceneData.set('gameTime', gameTime);
|
||||
|
||||
this.showToast('场景数据已更新');
|
||||
});
|
||||
|
||||
// 初始更新统计
|
||||
this.updateStats();
|
||||
}
|
||||
|
||||
private updateDataPreview(data: string, format: string) {
|
||||
const preview = document.getElementById('dataPreview')!;
|
||||
if (format === 'json') {
|
||||
const truncated = data.length > 1000 ? data.substring(0, 1000) + '\n...(truncated)' : data;
|
||||
preview.textContent = truncated;
|
||||
} else {
|
||||
preview.textContent = data;
|
||||
}
|
||||
}
|
||||
|
||||
private updateStats() {
|
||||
const entityCount = this.scene.entities.count;
|
||||
document.getElementById('entityCount')!.textContent = entityCount.toString();
|
||||
|
||||
// 计算JSON大小
|
||||
if (this.jsonData) {
|
||||
const jsonSize = new Blob([this.jsonData]).size;
|
||||
document.getElementById('jsonSize')!.textContent = this.formatBytes(jsonSize);
|
||||
}
|
||||
|
||||
// 计算二进制大小
|
||||
if (this.binaryData) {
|
||||
const binarySize = this.binaryData.length;
|
||||
document.getElementById('binarySize')!.textContent = this.formatBytes(binarySize);
|
||||
|
||||
// 计算压缩率
|
||||
if (this.jsonData) {
|
||||
const jsonSize = new Blob([this.jsonData]).size;
|
||||
const ratio = ((1 - binarySize / jsonSize) * 100).toFixed(1);
|
||||
document.getElementById('compressionRatio')!.textContent = `${ratio}%`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private formatBytes(bytes: number): string {
|
||||
if (bytes < 1024) return `${bytes}B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
// RenderSystem会处理渲染
|
||||
}
|
||||
}
|
||||
832
examples/core-demos/src/demos/WorkerSystemDemo.ts
Normal file
832
examples/core-demos/src/demos/WorkerSystemDemo.ts
Normal file
@@ -0,0 +1,832 @@
|
||||
import { DemoBase, DemoInfo } from './DemoBase';
|
||||
import { Component, ECSComponent, WorkerEntitySystem, EntitySystem, Matcher, Entity, ECSSystem, PlatformManager, Time } from '@esengine/ecs-framework';
|
||||
import { BrowserAdapter } from '../platform/BrowserAdapter';
|
||||
|
||||
// ============ 组件定义 ============
|
||||
|
||||
@ECSComponent('WorkerDemo_Position')
|
||||
class Position extends Component {
|
||||
x: number = 0;
|
||||
y: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
set(x: number, y: number): void {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('WorkerDemo_Velocity')
|
||||
class Velocity extends Component {
|
||||
dx: number = 0;
|
||||
dy: number = 0;
|
||||
|
||||
constructor(dx: number = 0, dy: number = 0) {
|
||||
super();
|
||||
this.dx = dx;
|
||||
this.dy = dy;
|
||||
}
|
||||
|
||||
set(dx: number, dy: number): void {
|
||||
this.dx = dx;
|
||||
this.dy = dy;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('WorkerDemo_Physics')
|
||||
class Physics extends Component {
|
||||
mass: number = 1;
|
||||
bounce: number = 0.8;
|
||||
friction: number = 0.95;
|
||||
|
||||
constructor(mass: number = 1, bounce: number = 0.8, friction: number = 0.95) {
|
||||
super();
|
||||
this.mass = mass;
|
||||
this.bounce = bounce;
|
||||
this.friction = friction;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('WorkerDemo_Renderable')
|
||||
class Renderable extends Component {
|
||||
color: string = '#ffffff';
|
||||
size: number = 5;
|
||||
shape: 'circle' | 'square' = 'circle';
|
||||
|
||||
constructor(color: string = '#ffffff', size: number = 5, shape: 'circle' | 'square' = 'circle') {
|
||||
super();
|
||||
this.color = color;
|
||||
this.size = size;
|
||||
this.shape = shape;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('WorkerDemo_Lifetime')
|
||||
class Lifetime extends Component {
|
||||
maxAge: number = 5;
|
||||
currentAge: number = 0;
|
||||
|
||||
constructor(maxAge: number = 5) {
|
||||
super();
|
||||
this.maxAge = maxAge;
|
||||
this.currentAge = 0;
|
||||
}
|
||||
|
||||
isDead(): boolean {
|
||||
return this.currentAge >= this.maxAge;
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 系统定义 ============
|
||||
|
||||
interface PhysicsEntityData {
|
||||
id: number;
|
||||
x: number;
|
||||
y: number;
|
||||
dx: number;
|
||||
dy: number;
|
||||
mass: number;
|
||||
bounce: number;
|
||||
friction: number;
|
||||
radius: number;
|
||||
}
|
||||
|
||||
interface PhysicsConfig {
|
||||
gravity: number;
|
||||
canvasWidth: number;
|
||||
canvasHeight: number;
|
||||
groundFriction: number;
|
||||
}
|
||||
|
||||
@ECSSystem('PhysicsWorkerSystem')
|
||||
class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsEntityData> {
|
||||
private physicsConfig: PhysicsConfig;
|
||||
|
||||
constructor(enableWorker: boolean, canvasWidth: number, canvasHeight: number) {
|
||||
const defaultConfig = {
|
||||
gravity: 100,
|
||||
canvasWidth,
|
||||
canvasHeight,
|
||||
groundFriction: 0.98
|
||||
};
|
||||
|
||||
const isSharedArrayBufferAvailable = typeof SharedArrayBuffer !== 'undefined' && self.crossOriginIsolated;
|
||||
|
||||
super(
|
||||
Matcher.empty().all(Position, Velocity, Physics),
|
||||
{
|
||||
enableWorker,
|
||||
workerCount: isSharedArrayBufferAvailable ? (navigator.hardwareConcurrency || 2) : 1,
|
||||
systemConfig: defaultConfig,
|
||||
useSharedArrayBuffer: true
|
||||
}
|
||||
);
|
||||
|
||||
this.physicsConfig = defaultConfig;
|
||||
}
|
||||
|
||||
protected extractEntityData(entity: Entity): PhysicsEntityData {
|
||||
const position = entity.getComponent(Position)!;
|
||||
const velocity = entity.getComponent(Velocity)!;
|
||||
const physics = entity.getComponent(Physics)!;
|
||||
const renderable = entity.getComponent(Renderable)!;
|
||||
|
||||
return {
|
||||
id: entity.id,
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
dx: velocity.dx,
|
||||
dy: velocity.dy,
|
||||
mass: physics.mass,
|
||||
bounce: physics.bounce,
|
||||
friction: physics.friction,
|
||||
radius: renderable.size
|
||||
};
|
||||
}
|
||||
|
||||
protected workerProcess(
|
||||
entities: PhysicsEntityData[],
|
||||
deltaTime: number,
|
||||
systemConfig?: PhysicsConfig
|
||||
): PhysicsEntityData[] {
|
||||
const config = systemConfig || this.physicsConfig;
|
||||
const result = entities.map(e => ({ ...e }));
|
||||
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
const entity = result[i];
|
||||
|
||||
entity.dy += config.gravity * deltaTime;
|
||||
entity.x += entity.dx * deltaTime;
|
||||
entity.y += entity.dy * deltaTime;
|
||||
|
||||
if (entity.x <= entity.radius) {
|
||||
entity.x = entity.radius;
|
||||
entity.dx = -entity.dx * entity.bounce;
|
||||
} else if (entity.x >= config.canvasWidth - entity.radius) {
|
||||
entity.x = config.canvasWidth - entity.radius;
|
||||
entity.dx = -entity.dx * entity.bounce;
|
||||
}
|
||||
|
||||
if (entity.y <= entity.radius) {
|
||||
entity.y = entity.radius;
|
||||
entity.dy = -entity.dy * entity.bounce;
|
||||
} else if (entity.y >= config.canvasHeight - entity.radius) {
|
||||
entity.y = config.canvasHeight - entity.radius;
|
||||
entity.dy = -entity.dy * entity.bounce;
|
||||
entity.dx *= config.groundFriction;
|
||||
}
|
||||
|
||||
entity.dx *= entity.friction;
|
||||
entity.dy *= entity.friction;
|
||||
}
|
||||
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
for (let j = i + 1; j < result.length; j++) {
|
||||
const ball1 = result[i];
|
||||
const ball2 = result[j];
|
||||
|
||||
const dx = ball2.x - ball1.x;
|
||||
const dy = ball2.y - ball1.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
const minDistance = ball1.radius + ball2.radius;
|
||||
|
||||
if (distance < minDistance && distance > 0) {
|
||||
const nx = dx / distance;
|
||||
const ny = dy / distance;
|
||||
|
||||
const overlap = minDistance - distance;
|
||||
const separationX = nx * overlap * 0.5;
|
||||
const separationY = ny * overlap * 0.5;
|
||||
|
||||
ball1.x -= separationX;
|
||||
ball1.y -= separationY;
|
||||
ball2.x += separationX;
|
||||
ball2.y += separationY;
|
||||
|
||||
const relativeVelocityX = ball2.dx - ball1.dx;
|
||||
const relativeVelocityY = ball2.dy - ball1.dy;
|
||||
const velocityAlongNormal = relativeVelocityX * nx + relativeVelocityY * ny;
|
||||
|
||||
if (velocityAlongNormal > 0) continue;
|
||||
|
||||
const restitution = (ball1.bounce + ball2.bounce) * 0.5;
|
||||
const impulseScalar = -(1 + restitution) * velocityAlongNormal / (1/ball1.mass + 1/ball2.mass);
|
||||
|
||||
const impulseX = impulseScalar * nx;
|
||||
const impulseY = impulseScalar * ny;
|
||||
|
||||
ball1.dx -= impulseX / ball1.mass;
|
||||
ball1.dy -= impulseY / ball1.mass;
|
||||
ball2.dx += impulseX / ball2.mass;
|
||||
ball2.dy += impulseY / ball2.mass;
|
||||
|
||||
const energyLoss = 0.98;
|
||||
ball1.dx *= energyLoss;
|
||||
ball1.dy *= energyLoss;
|
||||
ball2.dx *= energyLoss;
|
||||
ball2.dy *= energyLoss;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected applyResult(entity: Entity, result: PhysicsEntityData): void {
|
||||
if (!entity || !entity.enabled) return;
|
||||
|
||||
const position = entity.getComponent(Position);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
if (!position || !velocity) return;
|
||||
|
||||
position.set(result.x, result.y);
|
||||
velocity.set(result.dx, result.dy);
|
||||
}
|
||||
|
||||
public updatePhysicsConfig(newConfig: Partial<PhysicsConfig>): void {
|
||||
Object.assign(this.physicsConfig, newConfig);
|
||||
this.updateConfig({ systemConfig: this.physicsConfig });
|
||||
}
|
||||
|
||||
public getPhysicsConfig(): PhysicsConfig {
|
||||
return { ...this.physicsConfig };
|
||||
}
|
||||
|
||||
protected getDefaultEntityDataSize(): number {
|
||||
return 9;
|
||||
}
|
||||
|
||||
protected writeEntityToBuffer(entityData: PhysicsEntityData, offset: number): void {
|
||||
const sharedArray = (this as any).sharedFloatArray as Float32Array;
|
||||
if (!sharedArray) return;
|
||||
|
||||
// 在第一个位置存储当前实体数量
|
||||
const currentEntityCount = Math.floor(offset / 9) + 1;
|
||||
sharedArray[0] = currentEntityCount;
|
||||
|
||||
// 数据从索引9开始存储(第一个9个位置用作元数据区域)
|
||||
const dataOffset = offset + 9;
|
||||
sharedArray[dataOffset + 0] = entityData.id;
|
||||
sharedArray[dataOffset + 1] = entityData.x;
|
||||
sharedArray[dataOffset + 2] = entityData.y;
|
||||
sharedArray[dataOffset + 3] = entityData.dx;
|
||||
sharedArray[dataOffset + 4] = entityData.dy;
|
||||
sharedArray[dataOffset + 5] = entityData.mass;
|
||||
sharedArray[dataOffset + 6] = entityData.bounce;
|
||||
sharedArray[dataOffset + 7] = entityData.friction;
|
||||
sharedArray[dataOffset + 8] = entityData.radius;
|
||||
}
|
||||
|
||||
protected readEntityFromBuffer(offset: number): PhysicsEntityData | null {
|
||||
const sharedArray = (this as any).sharedFloatArray as Float32Array;
|
||||
if (!sharedArray) return null;
|
||||
|
||||
// 数据从索引9开始存储
|
||||
const dataOffset = offset + 9;
|
||||
return {
|
||||
id: sharedArray[dataOffset + 0],
|
||||
x: sharedArray[dataOffset + 1],
|
||||
y: sharedArray[dataOffset + 2],
|
||||
dx: sharedArray[dataOffset + 3],
|
||||
dy: sharedArray[dataOffset + 4],
|
||||
mass: sharedArray[dataOffset + 5],
|
||||
bounce: sharedArray[dataOffset + 6],
|
||||
friction: sharedArray[dataOffset + 7],
|
||||
radius: sharedArray[dataOffset + 8]
|
||||
};
|
||||
}
|
||||
|
||||
protected getSharedArrayBufferProcessFunction(): any {
|
||||
return function(sharedFloatArray: Float32Array, startIndex: number, endIndex: number, deltaTime: number, systemConfig?: any) {
|
||||
const config = systemConfig || {
|
||||
gravity: 100,
|
||||
canvasWidth: 800,
|
||||
canvasHeight: 600,
|
||||
groundFriction: 0.98
|
||||
};
|
||||
|
||||
const actualEntityCount = sharedFloatArray[0];
|
||||
|
||||
// 基础物理更新
|
||||
for (let i = startIndex; i < endIndex && i < actualEntityCount; i++) {
|
||||
const offset = i * 9 + 9;
|
||||
|
||||
const id = sharedFloatArray[offset + 0];
|
||||
if (id === 0) continue;
|
||||
|
||||
let x = sharedFloatArray[offset + 1];
|
||||
let y = sharedFloatArray[offset + 2];
|
||||
let dx = sharedFloatArray[offset + 3];
|
||||
let dy = sharedFloatArray[offset + 4];
|
||||
const bounce = sharedFloatArray[offset + 6];
|
||||
const friction = sharedFloatArray[offset + 7];
|
||||
const radius = sharedFloatArray[offset + 8];
|
||||
|
||||
// 应用重力
|
||||
dy += config.gravity * deltaTime;
|
||||
|
||||
// 更新位置
|
||||
x += dx * deltaTime;
|
||||
y += dy * deltaTime;
|
||||
|
||||
// 边界碰撞
|
||||
if (x <= radius) {
|
||||
x = radius;
|
||||
dx = -dx * bounce;
|
||||
} else if (x >= config.canvasWidth - radius) {
|
||||
x = config.canvasWidth - radius;
|
||||
dx = -dx * bounce;
|
||||
}
|
||||
|
||||
if (y <= radius) {
|
||||
y = radius;
|
||||
dy = -dy * bounce;
|
||||
} else if (y >= config.canvasHeight - radius) {
|
||||
y = config.canvasHeight - radius;
|
||||
dy = -dy * bounce;
|
||||
dx *= config.groundFriction;
|
||||
}
|
||||
|
||||
// 空气阻力
|
||||
dx *= friction;
|
||||
dy *= friction;
|
||||
|
||||
// 写回数据
|
||||
sharedFloatArray[offset + 1] = x;
|
||||
sharedFloatArray[offset + 2] = y;
|
||||
sharedFloatArray[offset + 3] = dx;
|
||||
sharedFloatArray[offset + 4] = dy;
|
||||
}
|
||||
|
||||
// 碰撞检测
|
||||
for (let i = startIndex; i < endIndex && i < actualEntityCount; i++) {
|
||||
const offset1 = i * 9 + 9;
|
||||
const id1 = sharedFloatArray[offset1 + 0];
|
||||
if (id1 === 0) continue;
|
||||
|
||||
let x1 = sharedFloatArray[offset1 + 1];
|
||||
let y1 = sharedFloatArray[offset1 + 2];
|
||||
let dx1 = sharedFloatArray[offset1 + 3];
|
||||
let dy1 = sharedFloatArray[offset1 + 4];
|
||||
const mass1 = sharedFloatArray[offset1 + 5];
|
||||
const bounce1 = sharedFloatArray[offset1 + 6];
|
||||
const radius1 = sharedFloatArray[offset1 + 8];
|
||||
|
||||
for (let j = 0; j < actualEntityCount; j++) {
|
||||
if (i === j) continue;
|
||||
|
||||
const offset2 = j * 9 + 9;
|
||||
const id2 = sharedFloatArray[offset2 + 0];
|
||||
if (id2 === 0) continue;
|
||||
|
||||
const x2 = sharedFloatArray[offset2 + 1];
|
||||
const y2 = sharedFloatArray[offset2 + 2];
|
||||
const dx2 = sharedFloatArray[offset2 + 3];
|
||||
const dy2 = sharedFloatArray[offset2 + 4];
|
||||
const mass2 = sharedFloatArray[offset2 + 5];
|
||||
const bounce2 = sharedFloatArray[offset2 + 6];
|
||||
const radius2 = sharedFloatArray[offset2 + 8];
|
||||
|
||||
if (isNaN(x2) || isNaN(y2) || isNaN(radius2) || radius2 <= 0) continue;
|
||||
|
||||
const deltaX = x2 - x1;
|
||||
const deltaY = y2 - y1;
|
||||
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
||||
const minDistance = radius1 + radius2;
|
||||
|
||||
if (distance < minDistance && distance > 0) {
|
||||
const nx = deltaX / distance;
|
||||
const ny = deltaY / distance;
|
||||
|
||||
const overlap = minDistance - distance;
|
||||
const separationX = nx * overlap * 0.5;
|
||||
const separationY = ny * overlap * 0.5;
|
||||
|
||||
x1 -= separationX;
|
||||
y1 -= separationY;
|
||||
|
||||
const relativeVelocityX = dx2 - dx1;
|
||||
const relativeVelocityY = dy2 - dy1;
|
||||
const velocityAlongNormal = relativeVelocityX * nx + relativeVelocityY * ny;
|
||||
|
||||
if (velocityAlongNormal > 0) continue;
|
||||
|
||||
const restitution = (bounce1 + bounce2) * 0.5;
|
||||
const impulseScalar = -(1 + restitution) * velocityAlongNormal / (1/mass1 + 1/mass2);
|
||||
|
||||
const impulseX = impulseScalar * nx;
|
||||
const impulseY = impulseScalar * ny;
|
||||
|
||||
dx1 -= impulseX / mass1;
|
||||
dy1 -= impulseY / mass1;
|
||||
|
||||
const energyLoss = 0.98;
|
||||
dx1 *= energyLoss;
|
||||
dy1 *= energyLoss;
|
||||
}
|
||||
}
|
||||
|
||||
sharedFloatArray[offset1 + 1] = x1;
|
||||
sharedFloatArray[offset1 + 2] = y1;
|
||||
sharedFloatArray[offset1 + 3] = dx1;
|
||||
sharedFloatArray[offset1 + 4] = dy1;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ECSSystem('RenderSystem')
|
||||
class RenderSystem extends EntitySystem {
|
||||
private canvas: HTMLCanvasElement;
|
||||
private ctx: CanvasRenderingContext2D;
|
||||
|
||||
constructor(canvas: HTMLCanvasElement) {
|
||||
super(Matcher.all(Position, Renderable));
|
||||
this.canvas = canvas;
|
||||
this.ctx = canvas.getContext('2d')!;
|
||||
}
|
||||
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
this.ctx.fillStyle = '#000';
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
for (const entity of entities) {
|
||||
const position = this.requireComponent(entity, Position);
|
||||
const renderable = this.requireComponent(entity, Renderable);
|
||||
|
||||
this.ctx.fillStyle = renderable.color;
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(position.x, position.y, renderable.size, 0, Math.PI * 2);
|
||||
this.ctx.fill();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ECSSystem('LifetimeSystem')
|
||||
class LifetimeSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Lifetime));
|
||||
}
|
||||
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
const deltaTime = Time.deltaTime;
|
||||
|
||||
for (const entity of entities) {
|
||||
const lifetime = this.requireComponent(entity, Lifetime);
|
||||
|
||||
lifetime.currentAge += deltaTime;
|
||||
if (lifetime.isDead()) {
|
||||
entity.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Demo类 ============
|
||||
|
||||
export class WorkerSystemDemo extends DemoBase {
|
||||
private physicsSystem!: PhysicsWorkerSystem;
|
||||
private renderSystem!: RenderSystem;
|
||||
private lifetimeSystem!: LifetimeSystem;
|
||||
private currentFPS = 0;
|
||||
private frameCount = 0;
|
||||
private fpsUpdateTime = 0;
|
||||
private elements: { [key: string]: HTMLElement } = {};
|
||||
|
||||
getInfo(): DemoInfo {
|
||||
return {
|
||||
id: 'worker-system',
|
||||
name: 'Worker System',
|
||||
description: '演示 ECS 框架中的多线程物理计算能力',
|
||||
category: '核心功能',
|
||||
icon: '⚙️'
|
||||
};
|
||||
}
|
||||
|
||||
setup(): void {
|
||||
// 注册浏览器平台适配器
|
||||
const browserAdapter = new BrowserAdapter();
|
||||
PlatformManager.getInstance().registerAdapter(browserAdapter);
|
||||
|
||||
// 初始化系统
|
||||
this.physicsSystem = new PhysicsWorkerSystem(true, this.canvas.width, this.canvas.height);
|
||||
this.renderSystem = new RenderSystem(this.canvas);
|
||||
this.lifetimeSystem = new LifetimeSystem();
|
||||
|
||||
this.physicsSystem.updateOrder = 1;
|
||||
this.lifetimeSystem.updateOrder = 2;
|
||||
this.renderSystem.updateOrder = 3;
|
||||
|
||||
this.scene.addSystem(this.physicsSystem);
|
||||
this.scene.addSystem(this.lifetimeSystem);
|
||||
this.scene.addSystem(this.renderSystem);
|
||||
|
||||
// 创建控制面板
|
||||
this.createControls();
|
||||
|
||||
// 初始化UI元素引用
|
||||
this.initializeUIElements();
|
||||
this.bindEvents();
|
||||
|
||||
// 生成初始实体
|
||||
this.spawnInitialEntities(1000);
|
||||
}
|
||||
|
||||
createControls(): void {
|
||||
this.controlPanel.innerHTML = `
|
||||
<div style="background: #2a2a2a; padding: 20px; border-radius: 8px; height: 100%; overflow-y: auto;">
|
||||
<div style="margin-bottom: 15px;">
|
||||
<label style="display: block; margin-bottom: 5px; color: #ccc;">实体数量:</label>
|
||||
<input type="range" id="entityCount" min="100" max="10000" value="1000" step="100"
|
||||
style="width: 100%; margin-bottom: 5px;">
|
||||
<span id="entityCountValue" style="color: #fff;">1000</span>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 15px;">
|
||||
<label style="display: block; margin-bottom: 5px; color: #ccc;">Worker 设置:</label>
|
||||
<button id="toggleWorker" style="width: 100%; padding: 8px; margin-bottom: 5px;
|
||||
background: #4a9eff; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
||||
禁用 Worker
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 15px;">
|
||||
<button id="spawnParticles" style="width: 100%; padding: 8px; margin-bottom: 5px;
|
||||
background: #4a9eff; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
||||
生成粒子爆炸
|
||||
</button>
|
||||
<button id="clearEntities" style="width: 100%; padding: 8px; margin-bottom: 5px;
|
||||
background: #4a9eff; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
||||
清空所有实体
|
||||
</button>
|
||||
<button id="resetDemo" style="width: 100%; padding: 8px;
|
||||
background: #4a9eff; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
||||
重置演示
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 15px;">
|
||||
<label style="display: block; margin-bottom: 5px; color: #ccc;">物理参数:</label>
|
||||
<input type="range" id="gravity" min="0" max="500" value="100" step="10"
|
||||
style="width: 100%; margin-bottom: 5px;">
|
||||
<label style="color: #ccc;">重力: <span id="gravityValue">100</span></label>
|
||||
|
||||
<input type="range" id="friction" min="0" max="100" value="95" step="5"
|
||||
style="width: 100%; margin-top: 10px; margin-bottom: 5px;">
|
||||
<label style="color: #ccc;">摩擦力: <span id="frictionValue">95%</span></label>
|
||||
</div>
|
||||
|
||||
<div style="background: #1a1a1a; padding: 15px; border-radius: 8px; font-family: monospace; font-size: 12px;">
|
||||
<h3 style="margin-top: 0; color: #4a9eff;">性能统计</h3>
|
||||
<div style="margin: 5px 0; color: #ccc;">FPS: <span id="fps" style="color: #4eff4a;">0</span></div>
|
||||
<div style="margin: 5px 0; color: #ccc;">实体数量: <span id="entityCountStat" style="color: #fff;">0</span></div>
|
||||
<div style="margin: 5px 0; color: #ccc;">Worker状态: <span id="workerStatus" style="color: #ff4a4a;">未启用</span></div>
|
||||
<div style="margin: 5px 0; color: #ccc;">Worker负载: <span id="workerLoad" style="color: #fff;">N/A</span></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected render(): void {
|
||||
this.frameCount++;
|
||||
const currentTime = performance.now();
|
||||
|
||||
if (currentTime - this.fpsUpdateTime >= 1000) {
|
||||
this.currentFPS = this.frameCount;
|
||||
this.frameCount = 0;
|
||||
this.fpsUpdateTime = currentTime;
|
||||
}
|
||||
|
||||
this.updateUI();
|
||||
}
|
||||
|
||||
private initializeUIElements(): void {
|
||||
const elementIds = [
|
||||
'entityCount', 'entityCountValue', 'toggleWorker',
|
||||
'gravity', 'gravityValue', 'friction', 'frictionValue', 'spawnParticles',
|
||||
'clearEntities', 'resetDemo', 'fps', 'entityCountStat', 'workerStatus', 'workerLoad'
|
||||
];
|
||||
|
||||
for (const id of elementIds) {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
this.elements[id] = element;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bindEvents(): void {
|
||||
if (this.elements.entityCount && this.elements.entityCountValue) {
|
||||
const slider = this.elements.entityCount as HTMLInputElement;
|
||||
slider.addEventListener('input', () => {
|
||||
this.elements.entityCountValue.textContent = slider.value;
|
||||
});
|
||||
|
||||
slider.addEventListener('change', () => {
|
||||
const count = parseInt(slider.value);
|
||||
this.spawnInitialEntities(count);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.elements.toggleWorker) {
|
||||
this.elements.toggleWorker.addEventListener('click', () => {
|
||||
const workerEnabled = this.toggleWorker();
|
||||
this.elements.toggleWorker.textContent = workerEnabled ? '禁用 Worker' : '启用 Worker';
|
||||
});
|
||||
}
|
||||
|
||||
if (this.elements.gravity && this.elements.gravityValue) {
|
||||
const slider = this.elements.gravity as HTMLInputElement;
|
||||
slider.addEventListener('input', () => {
|
||||
this.elements.gravityValue.textContent = slider.value;
|
||||
});
|
||||
|
||||
slider.addEventListener('change', () => {
|
||||
const gravity = parseInt(slider.value);
|
||||
this.updateWorkerConfig({ gravity });
|
||||
});
|
||||
}
|
||||
|
||||
if (this.elements.friction && this.elements.frictionValue) {
|
||||
const slider = this.elements.friction as HTMLInputElement;
|
||||
slider.addEventListener('input', () => {
|
||||
const value = parseInt(slider.value);
|
||||
this.elements.frictionValue.textContent = `${value}%`;
|
||||
});
|
||||
|
||||
slider.addEventListener('change', () => {
|
||||
const friction = parseInt(slider.value) / 100;
|
||||
this.updateWorkerConfig({ friction });
|
||||
});
|
||||
}
|
||||
|
||||
if (this.elements.spawnParticles) {
|
||||
this.elements.spawnParticles.addEventListener('click', () => {
|
||||
const centerX = this.canvas.width / 2;
|
||||
const centerY = this.canvas.height / 2;
|
||||
this.spawnParticleExplosion(centerX, centerY, 100);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.elements.clearEntities) {
|
||||
this.elements.clearEntities.addEventListener('click', () => {
|
||||
this.clearAllEntities();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.elements.resetDemo) {
|
||||
this.elements.resetDemo.addEventListener('click', () => {
|
||||
(this.elements.entityCount as HTMLInputElement).value = '1000';
|
||||
this.elements.entityCountValue.textContent = '1000';
|
||||
(this.elements.gravity as HTMLInputElement).value = '100';
|
||||
this.elements.gravityValue.textContent = '100';
|
||||
(this.elements.friction as HTMLInputElement).value = '95';
|
||||
this.elements.frictionValue.textContent = '95%';
|
||||
|
||||
this.spawnInitialEntities(1000);
|
||||
this.updateWorkerConfig({ gravity: 100, friction: 0.95 });
|
||||
});
|
||||
}
|
||||
|
||||
this.canvas.addEventListener('click', (event) => {
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
const x = event.clientX - rect.left;
|
||||
const y = event.clientY - rect.top;
|
||||
this.spawnParticleExplosion(x, y, 30);
|
||||
});
|
||||
}
|
||||
|
||||
private updateUI(): void {
|
||||
const workerInfo = this.physicsSystem.getWorkerInfo();
|
||||
|
||||
if (this.elements.fps) {
|
||||
this.elements.fps.textContent = this.currentFPS.toString();
|
||||
}
|
||||
|
||||
if (this.elements.entityCountStat) {
|
||||
this.elements.entityCountStat.textContent = this.scene.entities.count.toString();
|
||||
}
|
||||
|
||||
if (this.elements.workerStatus) {
|
||||
if (workerInfo.enabled) {
|
||||
this.elements.workerStatus.textContent = `启用 (${workerInfo.workerCount} Workers)`;
|
||||
this.elements.workerStatus.style.color = '#4eff4a';
|
||||
} else {
|
||||
this.elements.workerStatus.textContent = '禁用';
|
||||
this.elements.workerStatus.style.color = '#ff4a4a';
|
||||
}
|
||||
}
|
||||
|
||||
if (this.elements.workerLoad) {
|
||||
const entityCount = this.scene.entities.count;
|
||||
if (workerInfo.enabled && entityCount > 0) {
|
||||
const entitiesPerWorker = Math.ceil(entityCount / workerInfo.workerCount);
|
||||
this.elements.workerLoad.textContent = `${entitiesPerWorker}/Worker (共${workerInfo.workerCount}个)`;
|
||||
} else {
|
||||
this.elements.workerLoad.textContent = 'N/A';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private spawnInitialEntities(count: number = 1000): void {
|
||||
this.clearAllEntities();
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
this.createParticle();
|
||||
}
|
||||
}
|
||||
|
||||
private createParticle(): void {
|
||||
const entity = this.scene.createEntity(`Particle_${Date.now()}_${Math.random()}`);
|
||||
|
||||
const x = Math.random() * (this.canvas.width - 20) + 10;
|
||||
const y = Math.random() * (this.canvas.height - 20) + 10;
|
||||
const dx = (Math.random() - 0.5) * 200;
|
||||
const dy = (Math.random() - 0.5) * 200;
|
||||
const mass = Math.random() * 3 + 2;
|
||||
const bounce = 0.85 + Math.random() * 0.15;
|
||||
const friction = 0.998 + Math.random() * 0.002;
|
||||
|
||||
const colors = [
|
||||
'#ff4444', '#44ff44', '#4444ff', '#ffff44', '#ff44ff', '#44ffff', '#ffffff',
|
||||
'#ff8844', '#88ff44', '#4488ff', '#ff4488', '#88ff88', '#8888ff', '#ffaa44'
|
||||
];
|
||||
const color = colors[Math.floor(Math.random() * colors.length)];
|
||||
const size = Math.random() * 6 + 3;
|
||||
|
||||
entity.addComponent(new Position(x, y));
|
||||
entity.addComponent(new Velocity(dx, dy));
|
||||
entity.addComponent(new Physics(mass, bounce, friction));
|
||||
entity.addComponent(new Renderable(color, size, 'circle'));
|
||||
entity.addComponent(new Lifetime(5 + Math.random() * 10));
|
||||
}
|
||||
|
||||
private spawnParticleExplosion(centerX: number, centerY: number, count: number = 50): void {
|
||||
for (let i = 0; i < count; i++) {
|
||||
const entity = this.scene.createEntity(`Explosion_${Date.now()}_${i}`);
|
||||
|
||||
const angle = (Math.PI * 2 * i) / count + (Math.random() - 0.5) * 0.5;
|
||||
const distance = Math.random() * 30;
|
||||
const x = centerX + Math.cos(angle) * distance;
|
||||
const y = centerY + Math.sin(angle) * distance;
|
||||
|
||||
const speed = 100 + Math.random() * 150;
|
||||
const dx = Math.cos(angle) * speed;
|
||||
const dy = Math.sin(angle) * speed;
|
||||
const mass = 0.5 + Math.random() * 1;
|
||||
const bounce = 0.8 + Math.random() * 0.2;
|
||||
|
||||
const colors = ['#ffaa00', '#ff6600', '#ff0066', '#ff3300', '#ffff00'];
|
||||
const color = colors[Math.floor(Math.random() * colors.length)];
|
||||
const size = Math.random() * 4 + 2;
|
||||
|
||||
entity.addComponent(new Position(x, y));
|
||||
entity.addComponent(new Velocity(dx, dy));
|
||||
entity.addComponent(new Physics(mass, bounce, 0.999));
|
||||
entity.addComponent(new Renderable(color, size, 'circle'));
|
||||
entity.addComponent(new Lifetime(2 + Math.random() * 3));
|
||||
}
|
||||
}
|
||||
|
||||
private clearAllEntities(): void {
|
||||
const entities = [...this.scene.entities.buffer];
|
||||
for (const entity of entities) {
|
||||
entity.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
private toggleWorker(): boolean {
|
||||
const workerInfo = this.physicsSystem.getWorkerInfo();
|
||||
const newWorkerEnabled = !workerInfo.enabled;
|
||||
|
||||
// 保存当前物理配置
|
||||
const currentConfig = this.physicsSystem.getPhysicsConfig();
|
||||
|
||||
this.scene.removeSystem(this.physicsSystem);
|
||||
this.physicsSystem = new PhysicsWorkerSystem(newWorkerEnabled, this.canvas.width, this.canvas.height);
|
||||
this.physicsSystem.updateOrder = 1;
|
||||
|
||||
// 恢复物理配置
|
||||
this.physicsSystem.updatePhysicsConfig(currentConfig);
|
||||
|
||||
this.scene.addSystem(this.physicsSystem);
|
||||
|
||||
return newWorkerEnabled;
|
||||
}
|
||||
|
||||
private updateWorkerConfig(config: { gravity?: number; friction?: number }): void {
|
||||
if (config.gravity !== undefined || config.friction !== undefined) {
|
||||
const physicsConfig = this.physicsSystem.getPhysicsConfig();
|
||||
this.physicsSystem.updatePhysicsConfig({
|
||||
gravity: config.gravity ?? physicsConfig.gravity,
|
||||
groundFriction: config.friction ?? physicsConfig.groundFriction
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
13
examples/core-demos/src/demos/index.ts
Normal file
13
examples/core-demos/src/demos/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { DemoBase } from './DemoBase';
|
||||
import { SerializationDemo } from './SerializationDemo';
|
||||
import { IncrementalSerializationDemo } from './IncrementalSerializationDemo';
|
||||
import { WorkerSystemDemo } from './WorkerSystemDemo';
|
||||
|
||||
export { DemoBase, SerializationDemo, IncrementalSerializationDemo, WorkerSystemDemo };
|
||||
|
||||
// Demo注册表
|
||||
export const DEMO_REGISTRY: typeof DemoBase[] = [
|
||||
SerializationDemo,
|
||||
IncrementalSerializationDemo,
|
||||
WorkerSystemDemo
|
||||
];
|
||||
171
examples/core-demos/src/main.ts
Normal file
171
examples/core-demos/src/main.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { DEMO_REGISTRY, DemoBase } from './demos';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
class DemoManager {
|
||||
private demos: Map<string, typeof DemoBase> = new Map();
|
||||
private currentDemo: DemoBase | null = null;
|
||||
private canvas: HTMLCanvasElement;
|
||||
private controlPanel: HTMLElement;
|
||||
|
||||
constructor() {
|
||||
// 初始化ECS Core
|
||||
Core.create({
|
||||
debug: true,
|
||||
enableEntitySystems: true
|
||||
});
|
||||
|
||||
this.canvas = document.getElementById('demoCanvas') as HTMLCanvasElement;
|
||||
this.controlPanel = document.getElementById('controlPanel') as HTMLElement;
|
||||
|
||||
// 注册所有demos
|
||||
for (const DemoClass of DEMO_REGISTRY) {
|
||||
const tempInstance = new DemoClass(this.canvas, this.controlPanel);
|
||||
const info = tempInstance.getInfo();
|
||||
this.demos.set(info.id, DemoClass);
|
||||
tempInstance.destroy();
|
||||
}
|
||||
|
||||
// 渲染demo列表
|
||||
this.renderDemoList();
|
||||
|
||||
// 自动加载第一个demo
|
||||
const firstDemo = DEMO_REGISTRY[0];
|
||||
if (firstDemo) {
|
||||
const tempInstance = new firstDemo(this.canvas, this.controlPanel);
|
||||
const info = tempInstance.getInfo();
|
||||
tempInstance.destroy();
|
||||
this.loadDemo(info.id);
|
||||
}
|
||||
}
|
||||
|
||||
private renderDemoList() {
|
||||
const demoList = document.getElementById('demoList')!;
|
||||
|
||||
// 按分类组织demos
|
||||
const categories = new Map<string, typeof DemoBase[]>();
|
||||
|
||||
for (const DemoClass of DEMO_REGISTRY) {
|
||||
const tempInstance = new DemoClass(this.canvas, this.controlPanel);
|
||||
const info = tempInstance.getInfo();
|
||||
tempInstance.destroy();
|
||||
|
||||
if (!categories.has(info.category)) {
|
||||
categories.set(info.category, []);
|
||||
}
|
||||
categories.get(info.category)!.push(DemoClass);
|
||||
}
|
||||
|
||||
// 渲染分类和demos
|
||||
let html = '';
|
||||
for (const [category, demoClasses] of categories) {
|
||||
html += `<div class="demo-category">`;
|
||||
html += `<div class="category-title">${category}</div>`;
|
||||
|
||||
for (const DemoClass of demoClasses) {
|
||||
const tempInstance = new DemoClass(this.canvas, this.controlPanel);
|
||||
const info = tempInstance.getInfo();
|
||||
tempInstance.destroy();
|
||||
|
||||
html += `
|
||||
<div class="demo-item" data-demo-id="${info.id}">
|
||||
<div class="demo-icon">${info.icon}</div>
|
||||
<div class="demo-info">
|
||||
<div class="demo-name">${info.name}</div>
|
||||
<div class="demo-desc">${info.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
html += `</div>`;
|
||||
}
|
||||
|
||||
demoList.innerHTML = html;
|
||||
|
||||
// 绑定点击事件
|
||||
demoList.querySelectorAll('.demo-item').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
const demoId = item.getAttribute('data-demo-id')!;
|
||||
this.loadDemo(demoId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private loadDemo(demoId: string) {
|
||||
// 停止并销毁当前demo
|
||||
if (this.currentDemo) {
|
||||
this.currentDemo.destroy();
|
||||
this.currentDemo = null;
|
||||
}
|
||||
|
||||
// 显示加载动画
|
||||
const loading = document.getElementById('loading')!;
|
||||
loading.style.display = 'block';
|
||||
|
||||
// 延迟加载,给用户反馈
|
||||
setTimeout(() => {
|
||||
const DemoClass = this.demos.get(demoId);
|
||||
if (!DemoClass) {
|
||||
console.error(`Demo ${demoId} not found`);
|
||||
loading.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 创建新demo
|
||||
this.currentDemo = new DemoClass(this.canvas, this.controlPanel);
|
||||
const info = this.currentDemo.getInfo();
|
||||
|
||||
// 更新页面标题和描述
|
||||
document.getElementById('demoTitle')!.textContent = info.name;
|
||||
document.getElementById('demoDescription')!.textContent = info.description;
|
||||
|
||||
// 设置demo
|
||||
this.currentDemo.setup();
|
||||
|
||||
// 显示控制面板
|
||||
this.controlPanel.style.display = 'block';
|
||||
|
||||
// 启动demo
|
||||
this.currentDemo.start();
|
||||
|
||||
// 更新菜单选中状态
|
||||
document.querySelectorAll('.demo-item').forEach(item => {
|
||||
item.classList.remove('active');
|
||||
if (item.getAttribute('data-demo-id') === demoId) {
|
||||
item.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
loading.style.display = 'none';
|
||||
|
||||
console.log(`✅ Demo "${info.name}" loaded successfully`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to load demo ${demoId}:`, error);
|
||||
loading.style.display = 'none';
|
||||
this.showError('加载演示失败:' + (error as Error).message);
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
private showError(message: string) {
|
||||
const toast = document.getElementById('toast')!;
|
||||
const toastMessage = document.getElementById('toastMessage')!;
|
||||
const toastIcon = toast.querySelector('.toast-icon')!;
|
||||
|
||||
toastIcon.textContent = '❌';
|
||||
toastMessage.textContent = message;
|
||||
toast.style.borderColor = '#f5576c';
|
||||
|
||||
toast.classList.add('show');
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('show');
|
||||
toast.style.borderColor = '#667eea';
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
new DemoManager();
|
||||
});
|
||||
204
examples/core-demos/src/platform/BrowserAdapter.ts
Normal file
204
examples/core-demos/src/platform/BrowserAdapter.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import type {
|
||||
IPlatformAdapter,
|
||||
PlatformWorker,
|
||||
WorkerCreationOptions,
|
||||
PlatformConfig
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* 浏览器平台适配器
|
||||
*/
|
||||
export class BrowserAdapter implements IPlatformAdapter {
|
||||
public readonly name = 'browser';
|
||||
public readonly version: string;
|
||||
|
||||
constructor() {
|
||||
this.version = this.getBrowserInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否支持Worker
|
||||
*/
|
||||
public isWorkerSupported(): boolean {
|
||||
return typeof Worker !== 'undefined';
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否支持SharedArrayBuffer
|
||||
*/
|
||||
public isSharedArrayBufferSupported(): boolean {
|
||||
return typeof SharedArrayBuffer !== 'undefined' && this.checkSharedArrayBufferEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取硬件并发数(CPU核心数)
|
||||
*/
|
||||
public getHardwareConcurrency(): number {
|
||||
return navigator.hardwareConcurrency || 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建Worker
|
||||
*/
|
||||
public createWorker(script: string, options: WorkerCreationOptions = {}): PlatformWorker {
|
||||
if (!this.isWorkerSupported()) {
|
||||
throw new Error('浏览器不支持Worker');
|
||||
}
|
||||
|
||||
try {
|
||||
return new BrowserWorker(script, options);
|
||||
} catch (error) {
|
||||
throw new Error(`创建浏览器Worker失败: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建SharedArrayBuffer
|
||||
*/
|
||||
public createSharedArrayBuffer(length: number): SharedArrayBuffer | null {
|
||||
if (!this.isSharedArrayBufferSupported()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return new SharedArrayBuffer(length);
|
||||
} catch (error) {
|
||||
console.warn('SharedArrayBuffer创建失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取高精度时间戳
|
||||
*/
|
||||
public getHighResTimestamp(): number {
|
||||
return performance.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取平台配置
|
||||
*/
|
||||
public getPlatformConfig(): PlatformConfig {
|
||||
return {
|
||||
maxWorkerCount: this.getHardwareConcurrency(),
|
||||
supportsModuleWorker: false,
|
||||
supportsTransferableObjects: true,
|
||||
maxSharedArrayBufferSize: 1024 * 1024 * 1024, // 1GB
|
||||
workerScriptPrefix: '',
|
||||
limitations: {
|
||||
noEval: false,
|
||||
requiresWorkerInit: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取浏览器信息
|
||||
*/
|
||||
private getBrowserInfo(): string {
|
||||
const userAgent = navigator.userAgent;
|
||||
if (userAgent.includes('Chrome')) {
|
||||
const match = userAgent.match(/Chrome\/([0-9.]+)/);
|
||||
return match ? `Chrome ${match[1]}` : 'Chrome';
|
||||
} else if (userAgent.includes('Firefox')) {
|
||||
const match = userAgent.match(/Firefox\/([0-9.]+)/);
|
||||
return match ? `Firefox ${match[1]}` : 'Firefox';
|
||||
} else if (userAgent.includes('Safari')) {
|
||||
const match = userAgent.match(/Version\/([0-9.]+)/);
|
||||
return match ? `Safari ${match[1]}` : 'Safari';
|
||||
}
|
||||
return 'Unknown Browser';
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查SharedArrayBuffer是否真正可用
|
||||
*/
|
||||
private checkSharedArrayBufferEnabled(): boolean {
|
||||
try {
|
||||
new SharedArrayBuffer(8);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 浏览器Worker封装
|
||||
*/
|
||||
class BrowserWorker implements PlatformWorker {
|
||||
private _state: 'running' | 'terminated' = 'running';
|
||||
private worker: Worker;
|
||||
|
||||
constructor(script: string, options: WorkerCreationOptions = {}) {
|
||||
this.worker = this.createBrowserWorker(script, options);
|
||||
}
|
||||
|
||||
public get state(): 'running' | 'terminated' {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
public postMessage(message: any, transfer?: Transferable[]): void {
|
||||
if (this._state === 'terminated') {
|
||||
throw new Error('Worker已被终止');
|
||||
}
|
||||
|
||||
try {
|
||||
if (transfer && transfer.length > 0) {
|
||||
this.worker.postMessage(message, transfer);
|
||||
} else {
|
||||
this.worker.postMessage(message);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`发送消息到Worker失败: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public onMessage(handler: (event: { data: any }) => void): void {
|
||||
this.worker.onmessage = (event: MessageEvent) => {
|
||||
handler({ data: event.data });
|
||||
};
|
||||
}
|
||||
|
||||
public onError(handler: (error: ErrorEvent) => void): void {
|
||||
this.worker.onerror = handler;
|
||||
}
|
||||
|
||||
public terminate(): void {
|
||||
if (this._state === 'running') {
|
||||
try {
|
||||
this.worker.terminate();
|
||||
this._state = 'terminated';
|
||||
} catch (error) {
|
||||
console.error('终止Worker失败:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建浏览器Worker
|
||||
*/
|
||||
private createBrowserWorker(script: string, options: WorkerCreationOptions): Worker {
|
||||
try {
|
||||
// 创建Blob URL
|
||||
const blob = new Blob([script], { type: 'application/javascript' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
// 创建Worker
|
||||
const worker = new Worker(url, {
|
||||
type: options.type || 'classic',
|
||||
credentials: options.credentials,
|
||||
name: options.name
|
||||
});
|
||||
|
||||
// 清理Blob URL(延迟清理,确保Worker已加载)
|
||||
setTimeout(() => {
|
||||
URL.revokeObjectURL(url);
|
||||
}, 1000);
|
||||
|
||||
return worker;
|
||||
} catch (error) {
|
||||
throw new Error(`无法创建浏览器Worker: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
21
examples/core-demos/tsconfig.json
Normal file
21
examples/core-demos/tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
15
examples/core-demos/vite.config.ts
Normal file
15
examples/core-demos/vite.config.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
server: {
|
||||
port: 3003,
|
||||
headers: {
|
||||
'Cross-Origin-Opener-Policy': 'same-origin',
|
||||
'Cross-Origin-Embedder-Policy': 'require-corp'
|
||||
}
|
||||
},
|
||||
build: {
|
||||
target: 'es2020',
|
||||
outDir: 'dist'
|
||||
}
|
||||
});
|
||||
1
examples/lawn-mower-demo
Submodule
1
examples/lawn-mower-demo
Submodule
Submodule examples/lawn-mower-demo added at 5a4976b192
@@ -0,0 +1,2 @@
|
||||
[InternetShortcut]
|
||||
URL=https://docs.cocos.com/creator/manual/en/scripting/setup.html#custom-script-template
|
||||
24
extensions/cocos/cocos-ecs/.gitignore
vendored
Normal file
24
extensions/cocos/cocos-ecs/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
|
||||
#///////////////////////////
|
||||
# Cocos Creator 3D Project
|
||||
#///////////////////////////
|
||||
library/
|
||||
temp/
|
||||
local/
|
||||
build/
|
||||
profiles/
|
||||
native
|
||||
#//////////////////////////
|
||||
# NPM
|
||||
#//////////////////////////
|
||||
node_modules/
|
||||
|
||||
#//////////////////////////
|
||||
# VSCode
|
||||
#//////////////////////////
|
||||
.vscode/
|
||||
|
||||
#//////////////////////////
|
||||
# WebStorm
|
||||
#//////////////////////////
|
||||
.idea/
|
||||
14
extensions/cocos/cocos-ecs/assets/resources.meta
Normal file
14
extensions/cocos/cocos-ecs/assets/resources.meta
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "2a691dda-d56d-4a72-9fef-111a999415db",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {
|
||||
"isBundle": true,
|
||||
"bundleConfigID": "default",
|
||||
"bundleName": "resources",
|
||||
"priority": 8
|
||||
}
|
||||
}
|
||||
9
extensions/cocos/cocos-ecs/assets/resources/effects.meta
Normal file
9
extensions/cocos/cocos-ecs/assets/resources/effects.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "8c25761f-50d6-498b-a95f-d863bf1fbff1",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "3a66cbbc-6612-4408-838b-875d0bb2e9a3",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node_15iffhg4p",
|
||||
"type": "root",
|
||||
"name": "根节点",
|
||||
"description": "行为树的根节点,每棵树只能有一个根节点",
|
||||
"children": [
|
||||
"node_o6tsnrxyg"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "node_o6tsnrxyg",
|
||||
"type": "selector",
|
||||
"name": "选择器",
|
||||
"description": "按顺序执行子节点,任一成功则整体成功",
|
||||
"properties": {
|
||||
"abortType": "LowerPriority"
|
||||
},
|
||||
"children": [
|
||||
"node_tljchzbno",
|
||||
"node_txhx0hau5",
|
||||
"node_r9kvcwv8u",
|
||||
"node_520hedw22"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "node_tljchzbno",
|
||||
"type": "conditional-decorator",
|
||||
"name": "休息条件装饰器",
|
||||
"description": "基于条件执行子节点(拖拽条件节点到此装饰器来配置条件)",
|
||||
"properties": {
|
||||
"conditionType": "blackboardCompare",
|
||||
"executeWhenTrue": true,
|
||||
"abortType": "LowerPriority",
|
||||
"shouldReevaluate": true,
|
||||
"variableName": "{{isLowStamina}}",
|
||||
"operator": "equal",
|
||||
"compareValue": "true"
|
||||
},
|
||||
"children": [
|
||||
"node_ulp8qx68h"
|
||||
],
|
||||
"condition": {
|
||||
"type": "blackboard-value-comparison",
|
||||
"properties": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node_txhx0hau5",
|
||||
"type": "conditional-decorator",
|
||||
"name": "存储条件装饰器",
|
||||
"description": "基于条件执行子节点(拖拽条件节点到此装饰器来配置条件)",
|
||||
"properties": {
|
||||
"conditionType": "blackboardCompare",
|
||||
"executeWhenTrue": true,
|
||||
"abortType": "LowerPriority",
|
||||
"shouldReevaluate": true,
|
||||
"variableName": "{{hasOre}}",
|
||||
"operator": "equal",
|
||||
"compareValue": "true"
|
||||
},
|
||||
"children": [
|
||||
"node_dhsz8rgl1"
|
||||
],
|
||||
"condition": {
|
||||
"type": "blackboard-value-comparison",
|
||||
"properties": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node_r9kvcwv8u",
|
||||
"type": "conditional-decorator",
|
||||
"name": "挖矿条件装饰器",
|
||||
"description": "基于条件执行子节点(拖拽条件节点到此装饰器来配置条件)",
|
||||
"properties": {
|
||||
"conditionType": "blackboardCompare",
|
||||
"executeWhenTrue": true,
|
||||
"abortType": "LowerPriority",
|
||||
"shouldReevaluate": true,
|
||||
"variableName": "{{isLowStamina}}",
|
||||
"operator": "equal",
|
||||
"compareValue": "false"
|
||||
},
|
||||
"children": [
|
||||
"node_zguxml6u7"
|
||||
],
|
||||
"condition": {
|
||||
"type": "blackboard-value-comparison",
|
||||
"properties": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node_ulp8qx68h",
|
||||
"type": "sequence",
|
||||
"name": "序列器",
|
||||
"description": "按顺序执行子节点,任一失败则整体失败",
|
||||
"properties": {
|
||||
"abortType": "None"
|
||||
},
|
||||
"children": [
|
||||
"node_0fgq85ovw",
|
||||
"node_9v13vpqyr"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "node_0fgq85ovw",
|
||||
"type": "event-action",
|
||||
"name": "回家休息",
|
||||
"description": "执行已注册的事件处理函数(推荐)",
|
||||
"properties": {
|
||||
"eventName": "go-home-rest",
|
||||
"parameters": "{}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node_9v13vpqyr",
|
||||
"type": "event-action",
|
||||
"name": "恢复体力",
|
||||
"description": "执行已注册的事件处理函数(推荐)",
|
||||
"properties": {
|
||||
"eventName": "recover-stamina",
|
||||
"parameters": "{}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node_ui4ja9mlj",
|
||||
"type": "event-action",
|
||||
"name": "前往仓库存储",
|
||||
"description": "执行已注册的事件处理函数(推荐)",
|
||||
"properties": {
|
||||
"eventName": "store-ore",
|
||||
"parameters": "{}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node_969njccy2",
|
||||
"type": "event-action",
|
||||
"name": "挖掘金矿",
|
||||
"description": "执行已注册的事件处理函数(推荐)",
|
||||
"properties": {
|
||||
"eventName": "mine-gold-ore",
|
||||
"parameters": "{}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node_520hedw22",
|
||||
"type": "event-action",
|
||||
"name": "默认待机",
|
||||
"description": "执行已注册的事件处理函数(推荐)",
|
||||
"properties": {
|
||||
"eventName": "idle-behavior",
|
||||
"parameters": "{}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node_o5c7hv5wx",
|
||||
"type": "set-blackboard-value",
|
||||
"name": "设置黑板变量",
|
||||
"description": "设置黑板变量的值",
|
||||
"properties": {
|
||||
"variableName": "{{hasOre}}",
|
||||
"value": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node_zf0sgkqev",
|
||||
"type": "set-blackboard-value",
|
||||
"name": "设置黑板变量",
|
||||
"description": "设置黑板变量的值",
|
||||
"properties": {
|
||||
"variableName": "{{hasOre}}",
|
||||
"value": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node_dhsz8rgl1",
|
||||
"type": "sequence",
|
||||
"name": "序列器",
|
||||
"description": "按顺序执行子节点,任一失败则整体失败",
|
||||
"properties": {
|
||||
"abortType": "None"
|
||||
},
|
||||
"children": [
|
||||
"node_ui4ja9mlj",
|
||||
"node_o5c7hv5wx"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "node_zguxml6u7",
|
||||
"type": "sequence",
|
||||
"name": "序列器",
|
||||
"description": "按顺序执行子节点,任一失败则整体失败",
|
||||
"properties": {
|
||||
"abortType": "None"
|
||||
},
|
||||
"children": [
|
||||
"node_969njccy2",
|
||||
"node_zf0sgkqev"
|
||||
]
|
||||
}
|
||||
],
|
||||
"blackboard": [
|
||||
{
|
||||
"name": "unitType",
|
||||
"type": "string",
|
||||
"value": "miner",
|
||||
"description": "单位类型",
|
||||
"group": "基础属性"
|
||||
},
|
||||
{
|
||||
"name": "currentHealth",
|
||||
"type": "number",
|
||||
"value": 100,
|
||||
"description": "当前生命值",
|
||||
"group": "基础属性"
|
||||
},
|
||||
{
|
||||
"name": "maxHealth",
|
||||
"type": "number",
|
||||
"value": 100,
|
||||
"description": "最大生命值",
|
||||
"group": "基础属性"
|
||||
},
|
||||
{
|
||||
"name": "stamina",
|
||||
"type": "number",
|
||||
"value": 100,
|
||||
"description": "当前体力值 - 挖矿会消耗体力",
|
||||
"group": "体力系统"
|
||||
},
|
||||
{
|
||||
"name": "maxStamina",
|
||||
"type": "number",
|
||||
"value": 100,
|
||||
"description": "最大体力值",
|
||||
"group": "体力系统"
|
||||
},
|
||||
{
|
||||
"name": "staminaPercentage",
|
||||
"type": "number",
|
||||
"value": 1,
|
||||
"description": "体力百分比",
|
||||
"group": "体力系统"
|
||||
},
|
||||
{
|
||||
"name": "isLowStamina",
|
||||
"type": "boolean",
|
||||
"value": false,
|
||||
"description": "是否低体力 - 体力低于20%时为true",
|
||||
"group": "体力系统"
|
||||
},
|
||||
{
|
||||
"name": "isResting",
|
||||
"type": "boolean",
|
||||
"value": false,
|
||||
"description": "是否正在休息",
|
||||
"group": "体力系统"
|
||||
},
|
||||
{
|
||||
"name": "homePosition",
|
||||
"type": "vector3",
|
||||
"value": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"description": "家的位置 - 矿工休息的地方",
|
||||
"group": "体力系统"
|
||||
},
|
||||
{
|
||||
"name": "hasOre",
|
||||
"type": "boolean",
|
||||
"value": false,
|
||||
"description": "是否携带矿石",
|
||||
"group": "工作状态"
|
||||
},
|
||||
{
|
||||
"name": "currentCommand",
|
||||
"type": "string",
|
||||
"value": "mine",
|
||||
"description": "当前命令",
|
||||
"group": "工作状态"
|
||||
},
|
||||
{
|
||||
"name": "hasTarget",
|
||||
"type": "boolean",
|
||||
"value": false,
|
||||
"description": "是否有目标",
|
||||
"group": "工作状态"
|
||||
},
|
||||
{
|
||||
"name": "targetPosition",
|
||||
"type": "vector3",
|
||||
"value": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"description": "目标位置",
|
||||
"group": "移动属性"
|
||||
},
|
||||
{
|
||||
"name": "isMoving",
|
||||
"type": "boolean",
|
||||
"value": false,
|
||||
"description": "是否正在移动",
|
||||
"group": "移动属性"
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"name": "behavior-tree",
|
||||
"created": "2025-06-25T14:06:55.596Z",
|
||||
"version": "1.0",
|
||||
"exportType": "clean"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"ver": "2.0.1",
|
||||
"importer": "json",
|
||||
"imported": true,
|
||||
"uuid": "598e1450-8c7a-46c7-9540-398f9809d627",
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
1395
extensions/cocos/cocos-ecs/assets/resources/miner-stamina-ai.btree
Normal file
1395
extensions/cocos/cocos-ecs/assets/resources/miner-stamina-ai.btree
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"ver": "1.0.0",
|
||||
"importer": "*",
|
||||
"imported": true,
|
||||
"uuid": "24c6e7e6-4ff0-4e7b-b470-9468bfa66b5d",
|
||||
"files": [
|
||||
".btree",
|
||||
".json"
|
||||
],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
9
extensions/cocos/cocos-ecs/assets/resources/prefabs.meta
Normal file
9
extensions/cocos/cocos-ecs/assets/resources/prefabs.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "2bf3ded8-4054-4d8f-a367-c76b21eaf538",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user