Compare commits
223 Commits
v2.2.4
...
feat/githu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
566e1977fd | ||
|
|
17f6259f43 | ||
|
|
5d3483fc65 | ||
|
|
d07a5d81fc | ||
|
|
6a4e6fbc04 | ||
|
|
4b8d22ac32 | ||
|
|
9cd873da14 | ||
|
|
c1799bf7b3 | ||
|
|
85be826b62 | ||
|
|
dd1ae97de7 | ||
|
|
63f006ab62 | ||
|
|
caf7622aa0 | ||
|
|
d746cf3bb8 | ||
|
|
88af781d78 | ||
|
|
15d5d37e50 | ||
|
|
b9aaf894d7 | ||
|
|
460cdb5af4 | ||
|
|
290bd9858e | ||
|
|
b42a7b4e43 | ||
|
|
189714c727 | ||
|
|
987051acd4 | ||
|
|
374e08a79e | ||
|
|
359886c72f | ||
|
|
f03b73b58e | ||
|
|
18d20df4da | ||
|
|
c5642a8605 | ||
|
|
673f5e5855 | ||
|
|
cabb625a17 | ||
|
|
b8f05b79b0 | ||
|
|
b22faaac86 | ||
|
|
107439d70c | ||
|
|
71869b1a58 | ||
|
|
9aed3134cf | ||
|
|
3ff57aff37 | ||
|
|
152c0541b8 | ||
|
|
7b14fa2da4 | ||
|
|
3fb6f919f8 | ||
|
|
551ca7805d | ||
|
|
8ab25fe293 | ||
|
|
eea7ed9e58 | ||
|
|
0279cf6d27 | ||
|
|
0dff1ad2ad | ||
|
|
95fbcca66f | ||
|
|
a61baa83a7 | ||
|
|
afebeecd68 | ||
|
|
f4e9925319 | ||
|
|
32460ac133 | ||
|
|
4d95a7f044 | ||
|
|
57f919fbe0 | ||
|
|
1cb9a0e58f | ||
|
|
1da43ee822 | ||
|
|
f4c7563763 | ||
|
|
a3f7cc38b1 | ||
|
|
b15cbab313 | ||
|
|
504b9ffb66 | ||
|
|
6226e3ff06 | ||
|
|
2621d7f659 | ||
|
|
a768b890fd | ||
|
|
8b9616837d | ||
|
|
0d2948e60c | ||
|
|
ecfef727c8 | ||
|
|
caed5428d5 | ||
|
|
bce3a6e253 | ||
|
|
eac660b1a0 | ||
|
|
af49870084 | ||
|
|
e2b316b3cc | ||
|
|
3a0544629d | ||
|
|
609baace73 | ||
|
|
b12cfba353 | ||
|
|
6242c6daf3 | ||
|
|
b5337de278 | ||
|
|
3512199ff4 | ||
|
|
e03b106652 | ||
|
|
f9afa22406 | ||
|
|
adfc7e91b3 | ||
|
|
40cde9c050 | ||
|
|
ddc7a7750e | ||
|
|
50a01d9dd3 | ||
|
|
793aad0a5e | ||
|
|
9c1bf8dbed | ||
|
|
620f3eecc7 | ||
|
|
4355538d8d | ||
|
|
3ad5dc9ca3 | ||
|
|
57c7e7be3f | ||
|
|
6778ccace4 | ||
|
|
1264232533 | ||
|
|
61813e67b6 | ||
|
|
c58e3411fd | ||
|
|
011d795361 | ||
|
|
3f40a04370 | ||
|
|
fc042bb7d9 | ||
|
|
d051e52131 | ||
|
|
fb4316aeb9 | ||
|
|
683203919f | ||
|
|
a0cddbcae6 | ||
|
|
4e81fc7eba | ||
|
|
b410e2de47 | ||
|
|
9868c746e1 | ||
|
|
f0b4453a5f | ||
|
|
6b49471734 | ||
|
|
fe791e83a8 | ||
|
|
edbc9eb27f | ||
|
|
2f63034d9a | ||
|
|
dee0e0284a | ||
|
|
890e591f2a | ||
|
|
cb6561e27b | ||
|
|
7ef70d7f9a | ||
|
|
86405c1dcd | ||
|
|
60fa259285 | ||
|
|
27f86eece2 | ||
|
|
4cee396ea9 | ||
|
|
d2ad295b48 | ||
|
|
009f8af4e1 | ||
|
|
0cd99209c4 | ||
|
|
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 |
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
|
||||||
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
|
||||||
8
.github/codeql/codeql-config.yml
vendored
Normal file
8
.github/codeql/codeql-config.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
name: "CodeQL Config"
|
||||||
|
|
||||||
|
# Paths to exclude from analysis
|
||||||
|
paths-ignore:
|
||||||
|
- thirdparty
|
||||||
|
- "**/node_modules"
|
||||||
|
- "**/dist"
|
||||||
|
- "**/bin"
|
||||||
32
.github/labeler.yml
vendored
Normal file
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"
|
||||||
111
.github/workflows/ci.yml
vendored
111
.github/workflows/ci.yml
vendored
@@ -6,8 +6,9 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- 'packages/**'
|
- 'packages/**'
|
||||||
- 'package.json'
|
- 'package.json'
|
||||||
- 'package-lock.json'
|
- 'pnpm-lock.yaml'
|
||||||
- 'tsconfig.json'
|
- 'tsconfig.json'
|
||||||
|
- 'turbo.json'
|
||||||
- 'jest.config.*'
|
- 'jest.config.*'
|
||||||
- '.github/workflows/ci.yml'
|
- '.github/workflows/ci.yml'
|
||||||
pull_request:
|
pull_request:
|
||||||
@@ -15,70 +16,112 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- 'packages/**'
|
- 'packages/**'
|
||||||
- 'package.json'
|
- 'package.json'
|
||||||
- 'package-lock.json'
|
- 'pnpm-lock.yaml'
|
||||||
- 'tsconfig.json'
|
- 'tsconfig.json'
|
||||||
|
- 'turbo.json'
|
||||||
- 'jest.config.*'
|
- 'jest.config.*'
|
||||||
- '.github/workflows/ci.yml'
|
- '.github/workflows/ci.yml'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
ci:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v2
|
||||||
|
with:
|
||||||
|
version: 10
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '20.x'
|
node-version: '20.x'
|
||||||
cache: 'npm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
|
- name: Install Rust stable
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
targets: wasm32-unknown-unknown
|
||||||
|
|
||||||
|
# 缓存 Rust 编译结果
|
||||||
|
- name: Cache Rust dependencies
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: packages/engine
|
||||||
|
cache-on-failure: true
|
||||||
|
|
||||||
|
# 缓存 wasm-pack
|
||||||
|
- name: Cache wasm-pack
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.cargo/bin/wasm-pack
|
||||||
|
key: wasm-pack-${{ runner.os }}
|
||||||
|
|
||||||
|
- name: Install wasm-pack
|
||||||
|
run: |
|
||||||
|
if ! command -v wasm-pack &> /dev/null; then
|
||||||
|
cargo install wasm-pack
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: pnpm install --no-frozen-lockfile
|
||||||
|
|
||||||
- name: Build core package first
|
# 缓存 Turbo
|
||||||
run: npm run build:core
|
- name: Cache Turbo
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: .turbo
|
||||||
|
key: turbo-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.sha }}
|
||||||
|
restore-keys: |
|
||||||
|
turbo-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
|
||||||
|
turbo-${{ runner.os }}-
|
||||||
|
|
||||||
|
# 构建所有包
|
||||||
|
- name: Build all packages
|
||||||
|
run: pnpm run build
|
||||||
|
|
||||||
|
- name: Copy WASM files to ecs-engine-bindgen
|
||||||
|
run: |
|
||||||
|
mkdir -p packages/ecs-engine-bindgen/src/wasm
|
||||||
|
cp packages/engine/pkg/es_engine.js packages/ecs-engine-bindgen/src/wasm/
|
||||||
|
cp packages/engine/pkg/es_engine.d.ts packages/ecs-engine-bindgen/src/wasm/
|
||||||
|
cp packages/engine/pkg/es_engine_bg.wasm packages/ecs-engine-bindgen/src/wasm/
|
||||||
|
cp packages/engine/pkg/es_engine_bg.wasm.d.ts packages/ecs-engine-bindgen/src/wasm/
|
||||||
|
|
||||||
|
# 类型检查
|
||||||
|
- name: Type check
|
||||||
|
run: pnpm run type-check
|
||||||
|
|
||||||
|
# Lint 检查
|
||||||
|
- name: Lint check
|
||||||
|
run: pnpm run lint
|
||||||
|
|
||||||
|
# 测试
|
||||||
- name: Run tests with coverage
|
- name: Run tests with coverage
|
||||||
run: npm run test:ci
|
run: pnpm run test:ci
|
||||||
|
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
uses: codecov/codecov-action@v4
|
uses: codecov/codecov-action@v4
|
||||||
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
file: ./coverage/lcov.info
|
file: ./coverage/lcov.info
|
||||||
flags: unittests
|
flags: unittests
|
||||||
name: codecov-umbrella
|
name: codecov-umbrella
|
||||||
fail_ci_if_error: false
|
fail_ci_if_error: false
|
||||||
|
|
||||||
build:
|
# 构建 npm 包
|
||||||
runs-on: ubuntu-latest
|
- name: Build npm packages
|
||||||
needs: test
|
run: pnpm run build:npm
|
||||||
|
|
||||||
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
|
- name: Upload build artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: build-artifacts
|
name: build-artifacts
|
||||||
path: |
|
path: |
|
||||||
bin/
|
packages/*/dist/
|
||||||
dist/
|
packages/*/bin/
|
||||||
retention-days: 7
|
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`);
|
||||||
|
}
|
||||||
51
.github/workflows/codecov.yml
vendored
Normal file
51
.github/workflows/codecov.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
name: Code Coverage
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master, main ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ master, main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
coverage:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v2
|
||||||
|
with:
|
||||||
|
version: 10
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20.x'
|
||||||
|
cache: 'pnpm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install
|
||||||
|
|
||||||
|
- name: Run tests with coverage
|
||||||
|
run: |
|
||||||
|
cd packages/core
|
||||||
|
pnpm run test:coverage
|
||||||
|
|
||||||
|
- name: Upload coverage to Codecov
|
||||||
|
uses: codecov/codecov-action@v4
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
files: ./packages/core/coverage/coverage-final.json
|
||||||
|
flags: core
|
||||||
|
name: core-coverage
|
||||||
|
fail_ci_if_error: false
|
||||||
|
verbose: true
|
||||||
|
|
||||||
|
- name: Upload coverage artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: coverage-report
|
||||||
|
path: packages/core/coverage/
|
||||||
42
.github/workflows/codeql.yml
vendored
Normal file
42
.github/workflows/codeql.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
name: "CodeQL"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "master", "main" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "master", "main" ]
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * 1' # 每周一运行
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
language: [ 'javascript' ]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v3
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
queries: security-and-quality
|
||||||
|
config-file: ./.github/codeql/codeql-config.yml
|
||||||
|
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v3
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v3
|
||||||
|
with:
|
||||||
|
category: "/language:${{matrix.language}}"
|
||||||
36
.github/workflows/commitlint.yml
vendored
Normal file
36
.github/workflows/commitlint.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
name: Commit Lint
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize, reopened]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
commitlint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v2
|
||||||
|
with:
|
||||||
|
version: 10
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20.x'
|
||||||
|
cache: 'pnpm'
|
||||||
|
|
||||||
|
- name: Install commitlint
|
||||||
|
run: |
|
||||||
|
pnpm add -D @commitlint/config-conventional @commitlint/cli
|
||||||
|
|
||||||
|
- name: Validate PR commits
|
||||||
|
run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose
|
||||||
15
.github/workflows/docs.yml
vendored
15
.github/workflows/docs.yml
vendored
@@ -29,26 +29,31 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v2
|
||||||
|
with:
|
||||||
|
version: 10
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '20.x'
|
node-version: '20.x'
|
||||||
cache: 'npm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Setup Pages
|
- name: Setup Pages
|
||||||
uses: actions/configure-pages@v4
|
uses: actions/configure-pages@v4
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: pnpm install
|
||||||
|
|
||||||
- name: Build core package
|
- name: Build core package
|
||||||
run: npm run build:core
|
run: pnpm run build:core
|
||||||
|
|
||||||
- name: Generate API documentation
|
- name: Generate API documentation
|
||||||
run: npm run docs:api
|
run: pnpm run docs:api
|
||||||
|
|
||||||
- name: Build documentation
|
- name: Build documentation
|
||||||
run: npm run docs:build
|
run: pnpm run docs:build
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-pages-artifact@v3
|
uses: actions/upload-pages-artifact@v3
|
||||||
|
|||||||
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>
|
||||||
159
.github/workflows/release-editor.yml
vendored
Normal file
159
.github/workflows/release-editor.yml
vendored
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
name: Release Editor App
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'editor-v*'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: 'Release version (e.g., 1.0.0)'
|
||||||
|
required: true
|
||||||
|
default: '1.0.0'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-tauri:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- platform: windows-latest
|
||||||
|
target: x86_64-pc-windows-msvc
|
||||||
|
arch: x64
|
||||||
|
- platform: macos-latest
|
||||||
|
target: x86_64-apple-darwin
|
||||||
|
arch: x64
|
||||||
|
- platform: macos-latest
|
||||||
|
target: aarch64-apple-darwin
|
||||||
|
arch: arm64
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.platform }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v2
|
||||||
|
with:
|
||||||
|
version: 10
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20.x'
|
||||||
|
cache: 'pnpm'
|
||||||
|
|
||||||
|
- name: Install Rust stable
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
targets: ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Rust cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: packages/editor-app/src-tauri
|
||||||
|
cache-on-failure: true
|
||||||
|
|
||||||
|
- name: Install dependencies (Ubuntu)
|
||||||
|
if: matrix.platform == 'ubuntu-latest'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
|
||||||
|
|
||||||
|
- name: Install frontend dependencies
|
||||||
|
run: pnpm install
|
||||||
|
|
||||||
|
- name: Update version in config files (for manual trigger)
|
||||||
|
if: github.event_name == 'workflow_dispatch'
|
||||||
|
run: |
|
||||||
|
cd packages/editor-app
|
||||||
|
node -e "const pkg=require('./package.json'); pkg.version='${{ github.event.inputs.version }}'; require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2)+'\n')"
|
||||||
|
node scripts/sync-version.js
|
||||||
|
|
||||||
|
- name: Install wasm-pack
|
||||||
|
run: cargo install wasm-pack
|
||||||
|
|
||||||
|
# 使用 Turborepo 自动按依赖顺序构建所有包
|
||||||
|
# 这会自动处理:core -> asset-system -> editor-core -> ui -> 等等
|
||||||
|
- name: Build all packages with Turborepo
|
||||||
|
run: pnpm run build
|
||||||
|
|
||||||
|
- name: Copy WASM files to ecs-engine-bindgen
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
mkdir -p packages/ecs-engine-bindgen/src/wasm
|
||||||
|
cp packages/engine/pkg/es_engine.js packages/ecs-engine-bindgen/src/wasm/
|
||||||
|
cp packages/engine/pkg/es_engine.d.ts packages/ecs-engine-bindgen/src/wasm/
|
||||||
|
cp packages/engine/pkg/es_engine_bg.wasm packages/ecs-engine-bindgen/src/wasm/
|
||||||
|
cp packages/engine/pkg/es_engine_bg.wasm.d.ts packages/ecs-engine-bindgen/src/wasm/
|
||||||
|
|
||||||
|
- name: Bundle runtime files for Tauri
|
||||||
|
run: |
|
||||||
|
cd packages/editor-app
|
||||||
|
node scripts/bundle-runtime.mjs
|
||||||
|
|
||||||
|
- name: Build Tauri app
|
||||||
|
uses: tauri-apps/tauri-action@v0.5
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||||
|
with:
|
||||||
|
projectPath: packages/editor-app
|
||||||
|
tagName: ${{ github.event_name == 'workflow_dispatch' && format('editor-v{0}', github.event.inputs.version) || github.ref_name }}
|
||||||
|
releaseName: 'ECS Editor v${{ github.event.inputs.version || github.ref_name }}'
|
||||||
|
releaseBody: 'See the assets to download this version and install.'
|
||||||
|
releaseDraft: false
|
||||||
|
prerelease: false
|
||||||
|
includeUpdaterJson: true
|
||||||
|
updaterJsonKeepUniversal: false
|
||||||
|
args: ${{ matrix.platform == 'macos-latest' && format('--target {0}', matrix.target) || '' }}
|
||||||
|
|
||||||
|
# 构建成功后,创建 PR 更新版本号
|
||||||
|
update-version-pr:
|
||||||
|
needs: build-tauri
|
||||||
|
if: github.event_name == 'workflow_dispatch' && success()
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20.x'
|
||||||
|
|
||||||
|
- name: Update version files
|
||||||
|
run: |
|
||||||
|
cd packages/editor-app
|
||||||
|
node -e "const pkg=require('./package.json'); pkg.version='${{ github.event.inputs.version }}'; require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2)+'\n')"
|
||||||
|
node scripts/sync-version.js
|
||||||
|
|
||||||
|
- name: Create Pull Request
|
||||||
|
uses: peter-evans/create-pull-request@v6
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
commit-message: "chore(editor): bump version to ${{ github.event.inputs.version }}"
|
||||||
|
branch: release/editor-v${{ github.event.inputs.version }}
|
||||||
|
delete-branch: true
|
||||||
|
title: "chore(editor): Release v${{ github.event.inputs.version }}"
|
||||||
|
body: |
|
||||||
|
## Release v${{ github.event.inputs.version }}
|
||||||
|
|
||||||
|
This PR updates the editor version after successful release build.
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
- Updated `packages/editor-app/package.json` → `${{ github.event.inputs.version }}`
|
||||||
|
- Updated `packages/editor-app/src-tauri/tauri.conf.json` → `${{ github.event.inputs.version }}`
|
||||||
|
|
||||||
|
### Release
|
||||||
|
- [GitHub Release](https://github.com/${{ github.repository }}/releases/tag/editor-v${{ github.event.inputs.version }})
|
||||||
|
|
||||||
|
---
|
||||||
|
*This PR was automatically created by the release workflow.*
|
||||||
|
labels: |
|
||||||
|
release
|
||||||
|
editor
|
||||||
|
automated pr
|
||||||
135
.github/workflows/release.yml
vendored
Normal file
135
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
name: Release NPM Packages
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
package:
|
||||||
|
description: '选择要发布的包'
|
||||||
|
required: true
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- core
|
||||||
|
- behavior-tree
|
||||||
|
- editor-core
|
||||||
|
- node-editor
|
||||||
|
- blueprint
|
||||||
|
- tilemap
|
||||||
|
- physics-rapier2d
|
||||||
|
version_type:
|
||||||
|
description: '版本更新类型'
|
||||||
|
required: true
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- patch
|
||||||
|
- minor
|
||||||
|
- major
|
||||||
|
- custom
|
||||||
|
custom_version:
|
||||||
|
description: '自定义版本号 (仅当选择 custom 时使用,例如: 2.2.9)'
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release-package:
|
||||||
|
name: Release ${{ github.event.inputs.package }} Package
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v2
|
||||||
|
with:
|
||||||
|
version: 10
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20.x'
|
||||||
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
cache: 'pnpm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install
|
||||||
|
|
||||||
|
- name: Build core package (if needed)
|
||||||
|
if: ${{ github.event.inputs.package != 'core' && github.event.inputs.package != 'node-editor' }}
|
||||||
|
run: |
|
||||||
|
cd packages/core
|
||||||
|
pnpm run build
|
||||||
|
|
||||||
|
- name: Build node-editor package (if needed for blueprint)
|
||||||
|
if: ${{ github.event.inputs.package == 'blueprint' }}
|
||||||
|
run: |
|
||||||
|
cd packages/node-editor
|
||||||
|
pnpm run build
|
||||||
|
|
||||||
|
# - name: Run tests
|
||||||
|
# run: |
|
||||||
|
# cd packages/${{ github.event.inputs.package }}
|
||||||
|
# npm run test:ci
|
||||||
|
|
||||||
|
- name: Update version
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
cd packages/${{ github.event.inputs.package }}
|
||||||
|
if [ "${{ github.event.inputs.version_type }}" = "custom" ]; then
|
||||||
|
NEW_VERSION=${{ github.event.inputs.custom_version }}
|
||||||
|
else
|
||||||
|
# Get current version and bump it
|
||||||
|
CURRENT=$(node -p "require('./package.json').version")
|
||||||
|
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT"
|
||||||
|
case "${{ github.event.inputs.version_type }}" in
|
||||||
|
major) NEW_VERSION="$((MAJOR+1)).0.0" ;;
|
||||||
|
minor) NEW_VERSION="$MAJOR.$((MINOR+1)).0" ;;
|
||||||
|
patch) NEW_VERSION="$MAJOR.$MINOR.$((PATCH+1))" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
# Update package.json using node
|
||||||
|
node -e "const fs=require('fs'); const pkg=JSON.parse(fs.readFileSync('package.json')); pkg.version='$NEW_VERSION'; fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2)+'\n')"
|
||||||
|
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "发布版本: $NEW_VERSION"
|
||||||
|
|
||||||
|
- name: Build package
|
||||||
|
run: |
|
||||||
|
cd packages/${{ github.event.inputs.package }}
|
||||||
|
pnpm run build:npm
|
||||||
|
|
||||||
|
- name: Publish to npm
|
||||||
|
env:
|
||||||
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
run: |
|
||||||
|
cd packages/${{ github.event.inputs.package }}/dist
|
||||||
|
pnpm publish --access public --no-git-checks
|
||||||
|
|
||||||
|
- name: Create Pull Request
|
||||||
|
uses: peter-evans/create-pull-request@v6
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
commit-message: "chore(${{ github.event.inputs.package }}): release v${{ steps.version.outputs.new_version }}"
|
||||||
|
branch: release/${{ github.event.inputs.package }}-v${{ steps.version.outputs.new_version }}
|
||||||
|
delete-branch: true
|
||||||
|
title: "chore(${{ github.event.inputs.package }}): Release v${{ steps.version.outputs.new_version }}"
|
||||||
|
body: |
|
||||||
|
## 🚀 Release v${{ steps.version.outputs.new_version }}
|
||||||
|
|
||||||
|
此 PR 更新 `@esengine/${{ github.event.inputs.package }}` 包的版本号
|
||||||
|
|
||||||
|
### 变更
|
||||||
|
- ✅ 已发布到 npm: [@esengine/${{ github.event.inputs.package }}@${{ steps.version.outputs.new_version }}](https://www.npmjs.com/package/@esengine/${{ github.event.inputs.package }}/v/${{ steps.version.outputs.new_version }})
|
||||||
|
- ✅ 更新 `packages/${{ github.event.inputs.package }}/package.json` → `${{ steps.version.outputs.new_version }}`
|
||||||
|
|
||||||
|
---
|
||||||
|
*此 PR 由发布工作流自动创建*
|
||||||
|
labels: |
|
||||||
|
release
|
||||||
|
${{ github.event.inputs.package }}
|
||||||
|
automated pr
|
||||||
48
.github/workflows/size-limit.yml
vendored
Normal file
48
.github/workflows/size-limit.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
name: Size Limit
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- 'packages/core/src/**'
|
||||||
|
- 'packages/core/package.json'
|
||||||
|
- '.size-limit.json'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
size:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v2
|
||||||
|
with:
|
||||||
|
version: 10
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20.x'
|
||||||
|
cache: 'pnpm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install
|
||||||
|
|
||||||
|
- name: Build core package
|
||||||
|
run: |
|
||||||
|
cd packages/core
|
||||||
|
pnpm run build:npm
|
||||||
|
|
||||||
|
- name: Check bundle size
|
||||||
|
uses: andresz1/size-limit-action@v1
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
skip_step: install
|
||||||
60
.github/workflows/stale.yml
vendored
Normal file
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).
|
||||||
17
.gitignore
vendored
17
.gitignore
vendored
@@ -16,6 +16,10 @@ dist/
|
|||||||
*.tmp
|
*.tmp
|
||||||
*.temp
|
*.temp
|
||||||
.cache/
|
.cache/
|
||||||
|
.build-cache/
|
||||||
|
|
||||||
|
# Turborepo
|
||||||
|
.turbo/
|
||||||
|
|
||||||
# IDE 配置
|
# IDE 配置
|
||||||
.idea/
|
.idea/
|
||||||
@@ -48,9 +52,9 @@ logs/
|
|||||||
coverage/
|
coverage/
|
||||||
*.lcov
|
*.lcov
|
||||||
|
|
||||||
# 包管理器锁文件(保留npm的,忽略其他的)
|
# 包管理器锁文件(忽略yarn,保留pnpm)
|
||||||
yarn.lock
|
yarn.lock
|
||||||
pnpm-lock.yaml
|
package-lock.json
|
||||||
|
|
||||||
# 文档生成
|
# 文档生成
|
||||||
docs/api/
|
docs/api/
|
||||||
@@ -69,3 +73,12 @@ docs/.vitepress/dist/
|
|||||||
/demo/.idea/
|
/demo/.idea/
|
||||||
/demo/.vscode/
|
/demo/.vscode/
|
||||||
/demo_wxgame/
|
/demo_wxgame/
|
||||||
|
|
||||||
|
# Tauri 构建产物
|
||||||
|
**/src-tauri/target/
|
||||||
|
**/src-tauri/WixTools/
|
||||||
|
**/src-tauri/gen/
|
||||||
|
|
||||||
|
# Tauri 捆绑输出
|
||||||
|
**/src-tauri/target/release/bundle/
|
||||||
|
**/src-tauri/target/debug/bundle/
|
||||||
|
|||||||
15
.gitmodules
vendored
15
.gitmodules
vendored
@@ -4,27 +4,12 @@
|
|||||||
[submodule "thirdparty/admin-backend"]
|
[submodule "thirdparty/admin-backend"]
|
||||||
path = thirdparty/admin-backend
|
path = thirdparty/admin-backend
|
||||||
url = https://github.com/esengine/admin-backend.git
|
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"]
|
[submodule "thirdparty/mvvm-ui-framework"]
|
||||||
path = thirdparty/mvvm-ui-framework
|
path = thirdparty/mvvm-ui-framework
|
||||||
url = https://github.com/esengine/mvvm-ui-framework.git
|
url = https://github.com/esengine/mvvm-ui-framework.git
|
||||||
[submodule "thirdparty/cocos-nexus"]
|
[submodule "thirdparty/cocos-nexus"]
|
||||||
path = thirdparty/cocos-nexus
|
path = thirdparty/cocos-nexus
|
||||||
url = https://github.com/esengine/cocos-nexus.git
|
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"]
|
[submodule "thirdparty/ecs-astar"]
|
||||||
path = thirdparty/ecs-astar
|
path = thirdparty/ecs-astar
|
||||||
url = https://github.com/esengine/ecs-astar.git
|
url = https://github.com/esengine/ecs-astar.git
|
||||||
|
|||||||
2
.npmrc
Normal file
2
.npmrc
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
link-workspace-packages=true
|
||||||
|
prefer-workspace-packages=true
|
||||||
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"
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
|
]
|
||||||
130
CONTRIBUTING.md
Normal file
130
CONTRIBUTING.md
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
# 贡献指南 / Contributing Guide
|
||||||
|
|
||||||
|
感谢你对 ECS Framework 的关注!
|
||||||
|
|
||||||
|
Thank you for your interest in contributing to ECS Framework!
|
||||||
|
|
||||||
|
## Commit 规范 / Commit Convention
|
||||||
|
|
||||||
|
本项目使用 [Conventional Commits](https://www.conventionalcommits.org/) 规范。
|
||||||
|
|
||||||
|
This project follows the [Conventional Commits](https://www.conventionalcommits.org/) specification.
|
||||||
|
|
||||||
|
### 格式 / Format
|
||||||
|
|
||||||
|
```
|
||||||
|
<type>(<scope>): <subject>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 类型 / Types
|
||||||
|
|
||||||
|
- **feat**: 新功能 / New feature
|
||||||
|
- **fix**: 错误修复 / Bug fix
|
||||||
|
- **docs**: 文档变更 / Documentation changes
|
||||||
|
- **style**: 代码格式(不影响代码运行) / Code style changes
|
||||||
|
- **refactor**: 重构(既不是新功能也不是修复) / Code refactoring
|
||||||
|
- **perf**: 性能优化 / Performance improvements
|
||||||
|
- **test**: 测试相关 / Test changes
|
||||||
|
- **build**: 构建系统或依赖变更 / Build system changes
|
||||||
|
- **ci**: CI 配置变更 / CI configuration changes
|
||||||
|
- **chore**: 其他变更 / Other changes
|
||||||
|
|
||||||
|
### 范围 / Scope
|
||||||
|
|
||||||
|
- **core**: 核心包 @esengine/ecs-framework
|
||||||
|
- **math**: 数学库包
|
||||||
|
- **editor**: 编辑器
|
||||||
|
- **docs**: 文档
|
||||||
|
|
||||||
|
### 示例 / Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 新功能
|
||||||
|
feat(core): add component pooling system
|
||||||
|
|
||||||
|
# 错误修复
|
||||||
|
fix(core): fix entity deletion memory leak
|
||||||
|
|
||||||
|
# 破坏性变更
|
||||||
|
feat(core): redesign system lifecycle
|
||||||
|
|
||||||
|
BREAKING CHANGE: System.initialize() now requires Scene parameter
|
||||||
|
```
|
||||||
|
|
||||||
|
## 自动发布 / Automatic Release
|
||||||
|
|
||||||
|
本项目使用 Semantic Release 自动发布。
|
||||||
|
|
||||||
|
This project uses Semantic Release for automatic publishing.
|
||||||
|
|
||||||
|
### 版本规则 / Versioning Rules
|
||||||
|
|
||||||
|
根据你的 commit 类型,版本号会自动更新:
|
||||||
|
|
||||||
|
Based on your commit type, the version will be automatically updated:
|
||||||
|
|
||||||
|
- `feat`: 增加 **minor** 版本 (0.x.0)
|
||||||
|
- `fix`, `perf`, `refactor`: 增加 **patch** 版本 (0.0.x)
|
||||||
|
- `BREAKING CHANGE`: 增加 **major** 版本 (x.0.0)
|
||||||
|
|
||||||
|
### 发布流程 / Release Process
|
||||||
|
|
||||||
|
1. 提交代码到 `master` 分支 / Push commits to `master` branch
|
||||||
|
2. GitHub Actions 自动运行测试 / GitHub Actions runs tests automatically
|
||||||
|
3. Semantic Release 分析 commits / Semantic Release analyzes commits
|
||||||
|
4. 自动更新版本号 / Version is automatically updated
|
||||||
|
5. 自动生成 CHANGELOG.md / CHANGELOG.md is automatically generated
|
||||||
|
6. 自动发布到 npm / Package is automatically published to npm
|
||||||
|
7. 自动创建 GitHub Release / GitHub Release is automatically created
|
||||||
|
|
||||||
|
## 开发流程 / Development Workflow
|
||||||
|
|
||||||
|
1. Fork 本仓库 / Fork this repository
|
||||||
|
2. 创建特性分支 / Create a feature branch
|
||||||
|
```bash
|
||||||
|
git checkout -b feat/my-feature
|
||||||
|
```
|
||||||
|
3. 提交你的变更 / Commit your changes
|
||||||
|
```bash
|
||||||
|
git commit -m "feat(core): add new feature"
|
||||||
|
```
|
||||||
|
4. 推送到你的 Fork / Push to your fork
|
||||||
|
```bash
|
||||||
|
git push origin feat/my-feature
|
||||||
|
```
|
||||||
|
5. 创建 Pull Request / Create a Pull Request
|
||||||
|
|
||||||
|
## 本地测试 / Local Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装依赖
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 运行测试
|
||||||
|
npm test
|
||||||
|
|
||||||
|
# 构建
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# 代码检查
|
||||||
|
npm run lint
|
||||||
|
|
||||||
|
# 代码格式化
|
||||||
|
npm run format
|
||||||
|
```
|
||||||
|
|
||||||
|
## 问题反馈 / Issue Reporting
|
||||||
|
|
||||||
|
如果你发现了 bug 或有新功能建议,请[创建 Issue](https://github.com/esengine/ecs-framework/issues/new)。
|
||||||
|
|
||||||
|
If you find a bug or have a feature request, please [create an issue](https://github.com/esengine/ecs-framework/issues/new).
|
||||||
|
|
||||||
|
## 许可证 / License
|
||||||
|
|
||||||
|
通过贡献代码,你同意你的贡献将遵循 MIT 许可证。
|
||||||
|
|
||||||
|
By contributing, you agree that your contributions will be licensed under the MIT License.
|
||||||
214
LICENSE
214
LICENSE
@@ -1,201 +1,21 @@
|
|||||||
Apache License
|
MIT License
|
||||||
Version 2.0, January 2004
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
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,
|
The above copyright notice and this permission notice shall be included in all
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
the copyright owner that is granting the License.
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
other entities that control, are controlled by, or are under common
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
control with that entity. For the purposes of this definition,
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
"control" means (i) the power, direct or indirect, to cause the
|
SOFTWARE.
|
||||||
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.
|
|
||||||
|
|||||||
264
README.md
264
README.md
@@ -1,49 +1,61 @@
|
|||||||
# ECS Framework
|
# ESEngine
|
||||||
|
|
||||||
[](https://github.com/esengine/ecs-framework/actions)
|
**English** | [中文](./README_CN.md)
|
||||||
[](https://badge.fury.io/js/%40esengine%2Fecs-framework)
|
|
||||||
[](https://www.typescriptlang.org/)
|
|
||||||
[](https://opensource.org/licenses/MIT)
|
|
||||||
[](https://github.com/esengine/ecs-framework/stargazers)
|
|
||||||
|
|
||||||
一个高性能的 TypeScript ECS (Entity-Component-System) 框架,专为现代游戏开发而设计。
|
**[Documentation](https://esengine.github.io/ecs-framework/) | [API Reference](https://esengine.github.io/ecs-framework/api/) | [Examples](./examples/)**
|
||||||
|
|
||||||
## 特性
|
ESEngine is a cross-platform 2D game engine for creating games from a unified interface. It provides a comprehensive set of common tools so that developers can focus on making games without having to reinvent the wheel.
|
||||||
|
|
||||||
- **高性能** - 针对大规模实体优化,支持SoA存储和批量处理
|
Games can be exported to multiple platforms including Web browsers, WeChat Mini Games, and other mini-game platforms.
|
||||||
- **多线程计算** - Worker系统支持真正的并行处理,充分利用多核CPU性能
|
|
||||||
- **类型安全** - 完整的TypeScript支持,编译时类型检查
|
|
||||||
- **现代架构** - 支持多World、多Scene的分层架构设计
|
|
||||||
- **开发友好** - 内置调试工具和性能监控
|
|
||||||
- **跨平台** - 支持Cocos Creator、Laya引擎和Web平台
|
|
||||||
|
|
||||||
## 安装
|
## Free and Open Source
|
||||||
|
|
||||||
|
ESEngine is completely free and open source under the MIT license. No strings attached, no royalties. Your games are yours.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Data-Driven Architecture**: Built on Entity-Component-System (ECS) pattern for flexible and performant game logic
|
||||||
|
- **High-Performance Rendering**: Rust/WebAssembly 2D renderer with sprite batching and WebGL 2.0 backend
|
||||||
|
- **Visual Editor**: Cross-platform desktop editor with scene management, asset browser, and visual tools
|
||||||
|
- **Modular Design**: Use only what you need. Each feature is a separate module that can be included independently
|
||||||
|
- **Multi-Platform**: Deploy to Web, WeChat Mini Games, and more from a single codebase
|
||||||
|
|
||||||
|
## Getting the Engine
|
||||||
|
|
||||||
|
### Using npm
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install @esengine/ecs-framework
|
npm install @esengine/ecs-framework
|
||||||
```
|
```
|
||||||
|
|
||||||
## 快速开始
|
### Building from Source
|
||||||
|
|
||||||
|
See [Building from Source](#building-from-source) for detailed instructions.
|
||||||
|
|
||||||
|
### Editor Download
|
||||||
|
|
||||||
|
Pre-built editor binaries are available on the [Releases](https://github.com/esengine/ecs-framework/releases) page for Windows and macOS.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Core, Scene, Component, EntitySystem, ECSComponent, ECSSystem, Matcher, Time } from '@esengine/ecs-framework';
|
import {
|
||||||
|
Core, Scene, Entity, Component, EntitySystem,
|
||||||
|
Matcher, Time, ECSComponent, ECSSystem
|
||||||
|
} from '@esengine/ecs-framework';
|
||||||
|
|
||||||
// 定义组件
|
|
||||||
@ECSComponent('Position')
|
@ECSComponent('Position')
|
||||||
class Position extends Component {
|
class Position extends Component {
|
||||||
constructor(public x = 0, public y = 0) {
|
x = 0;
|
||||||
super();
|
y = 0;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ECSComponent('Velocity')
|
@ECSComponent('Velocity')
|
||||||
class Velocity extends Component {
|
class Velocity extends Component {
|
||||||
constructor(public dx = 0, public dy = 0) {
|
dx = 0;
|
||||||
super();
|
dy = 0;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建系统
|
|
||||||
@ECSSystem('Movement')
|
@ECSSystem('Movement')
|
||||||
class MovementSystem extends EntitySystem {
|
class MovementSystem extends EntitySystem {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -52,76 +64,182 @@ class MovementSystem extends EntitySystem {
|
|||||||
|
|
||||||
protected process(entities: readonly Entity[]): void {
|
protected process(entities: readonly Entity[]): void {
|
||||||
for (const entity of entities) {
|
for (const entity of entities) {
|
||||||
const position = entity.getComponent(Position)!;
|
const pos = entity.getComponent(Position);
|
||||||
const velocity = entity.getComponent(Velocity)!;
|
const vel = entity.getComponent(Velocity);
|
||||||
|
pos.x += vel.dx * Time.deltaTime;
|
||||||
position.x += velocity.dx * Time.deltaTime;
|
pos.y += vel.dy * Time.deltaTime;
|
||||||
position.y += velocity.dy * Time.deltaTime;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建场景并启动
|
|
||||||
class GameScene extends Scene {
|
|
||||||
protected initialize(): void {
|
|
||||||
this.addSystem(new MovementSystem());
|
|
||||||
|
|
||||||
const player = this.createEntity("Player");
|
|
||||||
player.addComponent(new Position(100, 100));
|
|
||||||
player.addComponent(new Velocity(50, 0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 启动游戏
|
|
||||||
Core.create();
|
Core.create();
|
||||||
Core.setScene(new GameScene());
|
const scene = new Scene();
|
||||||
|
scene.addSystem(new MovementSystem());
|
||||||
|
|
||||||
|
const player = scene.createEntity('Player');
|
||||||
|
player.addComponent(new Position());
|
||||||
|
player.addComponent(new Velocity());
|
||||||
|
|
||||||
|
Core.setScene(scene);
|
||||||
|
|
||||||
|
// Game loop
|
||||||
|
let lastTime = 0;
|
||||||
|
function gameLoop(currentTime: number) {
|
||||||
|
const deltaTime = (currentTime - lastTime) / 1000;
|
||||||
|
lastTime = currentTime;
|
||||||
|
|
||||||
// 游戏循环中更新
|
|
||||||
function gameLoop(deltaTime: number) {
|
|
||||||
Core.update(deltaTime);
|
Core.update(deltaTime);
|
||||||
|
requestAnimationFrame(gameLoop);
|
||||||
}
|
}
|
||||||
|
requestAnimationFrame(gameLoop);
|
||||||
```
|
```
|
||||||
|
|
||||||
## 核心特性
|
## Modules
|
||||||
|
|
||||||
- **实体查询** - 使用 Matcher API 进行高效的实体过滤
|
ESEngine is organized into modular packages. Each feature has a runtime module and an optional editor extension.
|
||||||
- **事件系统** - 类型安全的事件发布/订阅机制
|
|
||||||
- **性能优化** - SoA 存储优化,支持大规模实体处理
|
|
||||||
- **多线程支持** - Worker系统实现真正的并行计算,充分利用多核CPU
|
|
||||||
- **多场景** - 支持 World/Scene 分层架构
|
|
||||||
- **时间管理** - 内置定时器和时间控制系统
|
|
||||||
|
|
||||||
## 平台支持
|
### Core
|
||||||
|
|
||||||
支持主流游戏引擎和 Web 平台:
|
| Package | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `@esengine/ecs-framework` | Core ECS framework with entity management, component system, and queries |
|
||||||
|
| `@esengine/math` | Vector, matrix, and mathematical utilities |
|
||||||
|
| `@esengine/engine` | Rust/WASM 2D renderer |
|
||||||
|
| `@esengine/engine-core` | Engine module system and lifecycle management |
|
||||||
|
|
||||||
- **Cocos Creator** - 内置引擎集成支持,提供[专用调试插件](https://store.cocos.com/app/detail/7823)
|
### Runtime Modules
|
||||||
- **Laya 引擎** - 完整的生命周期管理
|
|
||||||
- **原生 Web** - 浏览器环境直接运行
|
|
||||||
- **小游戏平台** - 微信、支付宝等小游戏
|
|
||||||
|
|
||||||
|
| Package | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `@esengine/sprite` | 2D sprite rendering and animation |
|
||||||
|
| `@esengine/tilemap` | Tile-based map rendering with animation support |
|
||||||
|
| `@esengine/physics-rapier2d` | 2D physics simulation powered by Rapier |
|
||||||
|
| `@esengine/behavior-tree` | Behavior tree AI system |
|
||||||
|
| `@esengine/blueprint` | Visual scripting runtime |
|
||||||
|
| `@esengine/camera` | Camera control and management |
|
||||||
|
| `@esengine/audio` | Audio playback |
|
||||||
|
| `@esengine/ui` | UI components |
|
||||||
|
| `@esengine/material-system` | Material and shader system |
|
||||||
|
| `@esengine/asset-system` | Asset loading and management |
|
||||||
|
|
||||||
## 示例项目
|
### Editor Extensions
|
||||||
|
|
||||||
- [Worker系统演示](https://esengine.github.io/ecs-framework/demos/worker-system/) - 多线程物理系统演示,展示高性能并行计算
|
| Package | Description |
|
||||||
- [割草机演示](https://github.com/esengine/lawn-mower-demo) - 完整的游戏示例
|
|---------|-------------|
|
||||||
|
| `@esengine/sprite-editor` | Sprite inspector and tools |
|
||||||
|
| `@esengine/tilemap-editor` | Visual tilemap editor with brush tools |
|
||||||
|
| `@esengine/physics-rapier2d-editor` | Physics collider visualization and editing |
|
||||||
|
| `@esengine/behavior-tree-editor` | Visual behavior tree editor |
|
||||||
|
| `@esengine/blueprint-editor` | Visual scripting editor |
|
||||||
|
| `@esengine/material-editor` | Material and shader editor |
|
||||||
|
| `@esengine/shader-editor` | Shader code editor |
|
||||||
|
|
||||||
## 文档
|
### Platform
|
||||||
|
|
||||||
- [快速入门](https://esengine.github.io/ecs-framework/guide/getting-started.html) - 详细教程和平台集成
|
| Package | Description |
|
||||||
- [完整指南](https://esengine.github.io/ecs-framework/guide/) - ECS 概念和使用指南
|
|---------|-------------|
|
||||||
- [API 参考](https://esengine.github.io/ecs-framework/api/) - 完整 API 文档
|
| `@esengine/platform-common` | Platform abstraction interfaces |
|
||||||
|
| `@esengine/platform-web` | Web browser runtime |
|
||||||
|
| `@esengine/platform-wechat` | WeChat Mini Game runtime |
|
||||||
|
|
||||||
## 生态系统
|
## Editor
|
||||||
|
|
||||||
- [路径寻找](https://github.com/esengine/ecs-astar) - A*、BFS、Dijkstra 算法
|
ESEngine Editor is a cross-platform desktop application built with Tauri and React.
|
||||||
- [AI 系统](https://github.com/esengine/BehaviourTree-ai) - 行为树、效用 AI
|
|
||||||
|
|
||||||
## 社区与支持
|
### Features
|
||||||
|
|
||||||
- [问题反馈](https://github.com/esengine/ecs-framework/issues) - Bug 报告和功能建议
|
- Scene hierarchy and entity management
|
||||||
- [QQ 交流群](https://jq.qq.com/?_wv=1027&k=29w1Nud6) - ecs游戏框架交流
|
- Component inspector with custom editors
|
||||||
|
- Asset browser with drag-and-drop support
|
||||||
|
- Tilemap editor with paint, fill, and selection tools
|
||||||
|
- Behavior tree visual editor
|
||||||
|
- Blueprint visual scripting
|
||||||
|
- Material and shader editing
|
||||||
|
- Built-in performance profiler
|
||||||
|
- Localization support (English, Chinese)
|
||||||
|
|
||||||
## 许可证
|
### Screenshot
|
||||||
|
|
||||||
[MIT](LICENSE) © 2025 ECS Framework
|

|
||||||
|
|
||||||
|
## Supported Platforms
|
||||||
|
|
||||||
|
| Platform | Runtime | Editor |
|
||||||
|
|----------|---------|--------|
|
||||||
|
| Web Browser | Yes | - |
|
||||||
|
| Windows | - | Yes |
|
||||||
|
| macOS | - | Yes |
|
||||||
|
| WeChat Mini Game | In Progress | - |
|
||||||
|
| Playable Ads | Planned | - |
|
||||||
|
| Android | Planned | - |
|
||||||
|
| iOS | Planned | - |
|
||||||
|
| Windows Native | Planned | - |
|
||||||
|
| Other Platforms | Planned | - |
|
||||||
|
|
||||||
|
## Building from Source
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Node.js 18 or later
|
||||||
|
- pnpm 10 or later
|
||||||
|
- Rust toolchain (for WASM renderer)
|
||||||
|
- wasm-pack
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone repository
|
||||||
|
git clone https://github.com/esengine/ecs-framework.git
|
||||||
|
cd ecs-framework
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# Build all packages
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
# Build WASM renderer (optional)
|
||||||
|
pnpm build:wasm
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running the Editor
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd packages/editor-app
|
||||||
|
pnpm tauri:dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
ecs-framework/
|
||||||
|
├── packages/ Engine packages (runtime, editor, platform)
|
||||||
|
├── docs/ Documentation source
|
||||||
|
├── examples/ Example projects
|
||||||
|
├── scripts/ Build utilities
|
||||||
|
└── thirdparty/ Third-party dependencies
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- [Getting Started](https://esengine.github.io/ecs-framework/guide/getting-started.html)
|
||||||
|
- [Architecture Guide](https://esengine.github.io/ecs-framework/guide/)
|
||||||
|
- [API Reference](https://esengine.github.io/ecs-framework/api/)
|
||||||
|
|
||||||
|
## Community
|
||||||
|
|
||||||
|
- [GitHub Issues](https://github.com/esengine/ecs-framework/issues) - Bug reports and feature requests
|
||||||
|
- [GitHub Discussions](https://github.com/esengine/ecs-framework/discussions) - Questions and ideas
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are welcome. Please read the contributing guidelines before submitting a pull request.
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch
|
||||||
|
3. Make changes with tests
|
||||||
|
4. Submit a pull request
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
ESEngine is licensed under the [MIT License](LICENSE).
|
||||||
|
|||||||
246
README_CN.md
Normal file
246
README_CN.md
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
# ESEngine
|
||||||
|
|
||||||
|
[English](./README.md) | **中文**
|
||||||
|
|
||||||
|
**[文档](https://esengine.github.io/ecs-framework/) | [API 参考](https://esengine.github.io/ecs-framework/api/) | [示例](./examples/)**
|
||||||
|
|
||||||
|
ESEngine 是一个跨平台 2D 游戏引擎,提供统一的开发界面。它包含完整的常用工具集,让开发者专注于游戏创作本身。
|
||||||
|
|
||||||
|
游戏可以导出到多个平台,包括 Web 浏览器、微信小游戏等小游戏平台。
|
||||||
|
|
||||||
|
## 免费开源
|
||||||
|
|
||||||
|
ESEngine 基于 MIT 协议完全免费开源。无附加条件,无版税。你的游戏完全属于你。
|
||||||
|
|
||||||
|
## 特性
|
||||||
|
|
||||||
|
- **数据驱动架构**:基于 ECS(实体-组件-系统)模式构建,提供灵活高效的游戏逻辑
|
||||||
|
- **高性能渲染**:Rust/WebAssembly 2D 渲染器,支持精灵批处理和 WebGL 2.0
|
||||||
|
- **可视化编辑器**:跨平台桌面编辑器,包含场景管理、资源浏览器和可视化工具
|
||||||
|
- **模块化设计**:按需使用,每个功能都是独立模块,可单独引入
|
||||||
|
- **多平台支持**:一套代码部署到 Web、微信小游戏等多个平台
|
||||||
|
|
||||||
|
## 获取引擎
|
||||||
|
|
||||||
|
### 通过 npm 安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @esengine/ecs-framework
|
||||||
|
```
|
||||||
|
|
||||||
|
### 从源码构建
|
||||||
|
|
||||||
|
详见 [从源码构建](#从源码构建) 章节。
|
||||||
|
|
||||||
|
### 编辑器下载
|
||||||
|
|
||||||
|
预编译的编辑器可在 [Releases](https://github.com/esengine/ecs-framework/releases) 页面下载,支持 Windows 和 macOS。
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
Core, Scene, Entity, Component, EntitySystem,
|
||||||
|
Matcher, Time, ECSComponent, ECSSystem
|
||||||
|
} from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
@ECSComponent('Position')
|
||||||
|
class Position extends Component {
|
||||||
|
x = 0;
|
||||||
|
y = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ECSComponent('Velocity')
|
||||||
|
class Velocity extends Component {
|
||||||
|
dx = 0;
|
||||||
|
dy = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ECSSystem('Movement')
|
||||||
|
class MovementSystem extends EntitySystem {
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.all(Position, Velocity));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected process(entities: readonly Entity[]): void {
|
||||||
|
for (const entity of entities) {
|
||||||
|
const pos = entity.getComponent(Position);
|
||||||
|
const vel = entity.getComponent(Velocity);
|
||||||
|
pos.x += vel.dx * Time.deltaTime;
|
||||||
|
pos.y += vel.dy * Time.deltaTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Core.create();
|
||||||
|
const scene = new Scene();
|
||||||
|
scene.addSystem(new MovementSystem());
|
||||||
|
|
||||||
|
const player = scene.createEntity('Player');
|
||||||
|
player.addComponent(new Position());
|
||||||
|
player.addComponent(new Velocity());
|
||||||
|
|
||||||
|
Core.setScene(scene);
|
||||||
|
|
||||||
|
// 游戏循环
|
||||||
|
let lastTime = 0;
|
||||||
|
function gameLoop(currentTime: number) {
|
||||||
|
const deltaTime = (currentTime - lastTime) / 1000;
|
||||||
|
lastTime = currentTime;
|
||||||
|
|
||||||
|
Core.update(deltaTime);
|
||||||
|
requestAnimationFrame(gameLoop);
|
||||||
|
}
|
||||||
|
requestAnimationFrame(gameLoop);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 模块
|
||||||
|
|
||||||
|
ESEngine 采用模块化组织。每个功能都有运行时模块和可选的编辑器扩展。
|
||||||
|
|
||||||
|
### 核心
|
||||||
|
|
||||||
|
| 包名 | 描述 |
|
||||||
|
|------|------|
|
||||||
|
| `@esengine/ecs-framework` | ECS 框架核心,包含实体管理、组件系统和查询 |
|
||||||
|
| `@esengine/math` | 向量、矩阵和数学工具 |
|
||||||
|
| `@esengine/engine` | Rust/WASM 2D 渲染器 |
|
||||||
|
| `@esengine/engine-core` | 引擎模块系统和生命周期管理 |
|
||||||
|
|
||||||
|
### 运行时模块
|
||||||
|
|
||||||
|
| 包名 | 描述 |
|
||||||
|
|------|------|
|
||||||
|
| `@esengine/sprite` | 2D 精灵渲染和动画 |
|
||||||
|
| `@esengine/tilemap` | Tilemap 渲染,支持动画 |
|
||||||
|
| `@esengine/physics-rapier2d` | 基于 Rapier 的 2D 物理模拟 |
|
||||||
|
| `@esengine/behavior-tree` | 行为树 AI 系统 |
|
||||||
|
| `@esengine/blueprint` | 可视化脚本运行时 |
|
||||||
|
| `@esengine/camera` | 相机控制和管理 |
|
||||||
|
| `@esengine/audio` | 音频播放 |
|
||||||
|
| `@esengine/ui` | UI 组件 |
|
||||||
|
| `@esengine/material-system` | 材质和着色器系统 |
|
||||||
|
| `@esengine/asset-system` | 资源加载和管理 |
|
||||||
|
|
||||||
|
### 编辑器扩展
|
||||||
|
|
||||||
|
| 包名 | 描述 |
|
||||||
|
|------|------|
|
||||||
|
| `@esengine/sprite-editor` | 精灵检视器和工具 |
|
||||||
|
| `@esengine/tilemap-editor` | 可视化 Tilemap 编辑器,支持笔刷工具 |
|
||||||
|
| `@esengine/physics-rapier2d-editor` | 物理碰撞体可视化和编辑 |
|
||||||
|
| `@esengine/behavior-tree-editor` | 可视化行为树编辑器 |
|
||||||
|
| `@esengine/blueprint-editor` | 可视化脚本编辑器 |
|
||||||
|
| `@esengine/material-editor` | 材质和着色器编辑器 |
|
||||||
|
| `@esengine/shader-editor` | 着色器代码编辑器 |
|
||||||
|
|
||||||
|
### 平台
|
||||||
|
|
||||||
|
| 包名 | 描述 |
|
||||||
|
|------|------|
|
||||||
|
| `@esengine/platform-common` | 平台抽象接口 |
|
||||||
|
| `@esengine/platform-web` | Web 浏览器运行时 |
|
||||||
|
| `@esengine/platform-wechat` | 微信小游戏运行时 |
|
||||||
|
|
||||||
|
## 编辑器
|
||||||
|
|
||||||
|
ESEngine 编辑器是基于 Tauri 和 React 构建的跨平台桌面应用。
|
||||||
|
|
||||||
|
### 功能
|
||||||
|
|
||||||
|
- 场景层级和实体管理
|
||||||
|
- 组件检视器,支持自定义编辑器
|
||||||
|
- 资源浏览器,支持拖放
|
||||||
|
- Tilemap 编辑器,支持绘制、填充、选择工具
|
||||||
|
- 行为树可视化编辑器
|
||||||
|
- 蓝图可视化脚本
|
||||||
|
- 材质和着色器编辑
|
||||||
|
- 内置性能分析器
|
||||||
|
- 多语言支持(英文、中文)
|
||||||
|
|
||||||
|
### 截图
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 支持的平台
|
||||||
|
|
||||||
|
| 平台 | 运行时 | 编辑器 |
|
||||||
|
|------|--------|--------|
|
||||||
|
| Web 浏览器 | 支持 | - |
|
||||||
|
| Windows | - | 支持 |
|
||||||
|
| macOS | - | 支持 |
|
||||||
|
| 微信小游戏 | 开发中 | - |
|
||||||
|
| Playable 可玩广告 | 计划中 | - |
|
||||||
|
| Android | 计划中 | - |
|
||||||
|
| iOS | 计划中 | - |
|
||||||
|
| Windows 原生 | 计划中 | - |
|
||||||
|
| 其他平台 | 计划中 | - |
|
||||||
|
|
||||||
|
## 从源码构建
|
||||||
|
|
||||||
|
### 前置要求
|
||||||
|
|
||||||
|
- Node.js 18 或更高版本
|
||||||
|
- pnpm 10 或更高版本
|
||||||
|
- Rust 工具链(用于 WASM 渲染器)
|
||||||
|
- wasm-pack
|
||||||
|
|
||||||
|
### 安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 克隆仓库
|
||||||
|
git clone https://github.com/esengine/ecs-framework.git
|
||||||
|
cd ecs-framework
|
||||||
|
|
||||||
|
# 安装依赖
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# 构建所有包
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
# 构建 WASM 渲染器(可选)
|
||||||
|
pnpm build:wasm
|
||||||
|
```
|
||||||
|
|
||||||
|
### 运行编辑器
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd packages/editor-app
|
||||||
|
pnpm tauri:dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
ecs-framework/
|
||||||
|
├── packages/ 引擎包(运行时、编辑器、平台)
|
||||||
|
├── docs/ 文档源码
|
||||||
|
├── examples/ 示例项目
|
||||||
|
├── scripts/ 构建工具
|
||||||
|
└── thirdparty/ 第三方依赖
|
||||||
|
```
|
||||||
|
|
||||||
|
## 文档
|
||||||
|
|
||||||
|
- [快速入门](https://esengine.github.io/ecs-framework/guide/getting-started.html)
|
||||||
|
- [架构指南](https://esengine.github.io/ecs-framework/guide/)
|
||||||
|
- [API 参考](https://esengine.github.io/ecs-framework/api/)
|
||||||
|
|
||||||
|
## 社区
|
||||||
|
|
||||||
|
- [GitHub Issues](https://github.com/esengine/ecs-framework/issues) - Bug 反馈和功能建议
|
||||||
|
- [GitHub Discussions](https://github.com/esengine/ecs-framework/discussions) - 问题和想法
|
||||||
|
- [QQ 交流群](https://jq.qq.com/?_wv=1027&k=29w1Nud6) - 中文社区
|
||||||
|
|
||||||
|
## 贡献
|
||||||
|
|
||||||
|
欢迎贡献代码。提交 PR 前请阅读贡献指南。
|
||||||
|
|
||||||
|
1. Fork 仓库
|
||||||
|
2. 创建功能分支
|
||||||
|
3. 修改代码并测试
|
||||||
|
4. 提交 PR
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
ESEngine 基于 [MIT 协议](LICENSE) 开源。
|
||||||
53
codecov.yml
Normal file
53
codecov.yml
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# Codecov 配置文件
|
||||||
|
# https://docs.codecov.com/docs/codecov-yaml
|
||||||
|
|
||||||
|
coverage:
|
||||||
|
status:
|
||||||
|
# 项目整体覆盖率要求
|
||||||
|
project:
|
||||||
|
default:
|
||||||
|
target: auto
|
||||||
|
threshold: 1%
|
||||||
|
base: auto
|
||||||
|
|
||||||
|
# 补丁覆盖率要求(针对 PR 中的新代码)
|
||||||
|
patch:
|
||||||
|
default:
|
||||||
|
target: 50% # 降低补丁覆盖率要求到 50%
|
||||||
|
threshold: 5%
|
||||||
|
base: auto
|
||||||
|
|
||||||
|
# 精确度设置
|
||||||
|
precision: 2
|
||||||
|
round: down
|
||||||
|
range: "70...100"
|
||||||
|
|
||||||
|
# 注释设置
|
||||||
|
comment:
|
||||||
|
layout: "reach,diff,flags,tree,files"
|
||||||
|
behavior: default
|
||||||
|
require_changes: false
|
||||||
|
require_base: false
|
||||||
|
require_head: true
|
||||||
|
|
||||||
|
# 忽略的文件/目录
|
||||||
|
ignore:
|
||||||
|
- "tests/**/*"
|
||||||
|
- "**/*.test.ts"
|
||||||
|
- "**/*.spec.ts"
|
||||||
|
- "**/test/**/*"
|
||||||
|
- "**/tests/**/*"
|
||||||
|
- "bin/**/*"
|
||||||
|
- "dist/**/*"
|
||||||
|
- "node_modules/**/*"
|
||||||
|
|
||||||
|
# 标志组
|
||||||
|
flags:
|
||||||
|
core:
|
||||||
|
paths:
|
||||||
|
- packages/core/src/
|
||||||
|
carryforward: true
|
||||||
|
|
||||||
|
# GitHub Checks 配置
|
||||||
|
github_checks:
|
||||||
|
annotations: true
|
||||||
@@ -9,6 +9,184 @@ const corePackageJson = JSON.parse(
|
|||||||
readFileSync(join(__dirname, '../../packages/core/package.json'), 'utf-8')
|
readFileSync(join(__dirname, '../../packages/core/package.json'), 'utf-8')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Import i18n messages
|
||||||
|
import en from './i18n/en.json' with { type: 'json' }
|
||||||
|
import zh from './i18n/zh.json' with { type: 'json' }
|
||||||
|
|
||||||
|
// 创建侧边栏配置 | Create sidebar config
|
||||||
|
// prefix: 路径前缀,如 '' 或 '/en' | Path prefix like '' or '/en'
|
||||||
|
function createSidebar(t, prefix = '') {
|
||||||
|
return {
|
||||||
|
[`${prefix}/guide/`]: [
|
||||||
|
{
|
||||||
|
text: t.sidebar.gettingStarted,
|
||||||
|
items: [
|
||||||
|
{ text: t.sidebar.quickStart, link: `${prefix}/guide/getting-started` },
|
||||||
|
{ text: t.sidebar.guideOverview, link: `${prefix}/guide/` }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t.sidebar.coreConcepts,
|
||||||
|
collapsed: false,
|
||||||
|
items: [
|
||||||
|
{ text: t.sidebar.entity, link: `${prefix}/guide/entity` },
|
||||||
|
{ text: t.sidebar.hierarchy, link: `${prefix}/guide/hierarchy` },
|
||||||
|
{ text: t.sidebar.component, link: `${prefix}/guide/component` },
|
||||||
|
{ text: t.sidebar.entityQuery, link: `${prefix}/guide/entity-query` },
|
||||||
|
{
|
||||||
|
text: t.sidebar.system,
|
||||||
|
link: `${prefix}/guide/system`,
|
||||||
|
items: [
|
||||||
|
{ text: t.sidebar.workerSystem, link: `${prefix}/guide/worker-system` }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t.sidebar.scene,
|
||||||
|
link: `${prefix}/guide/scene`,
|
||||||
|
items: [
|
||||||
|
{ text: t.sidebar.sceneManager, link: `${prefix}/guide/scene-manager` },
|
||||||
|
{ text: t.sidebar.worldManager, link: `${prefix}/guide/world-manager` }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t.sidebar.behaviorTree,
|
||||||
|
link: `${prefix}/guide/behavior-tree/`,
|
||||||
|
items: [
|
||||||
|
{ text: t.sidebar.btGettingStarted, link: `${prefix}/guide/behavior-tree/getting-started` },
|
||||||
|
{ text: t.sidebar.btCoreConcepts, link: `${prefix}/guide/behavior-tree/core-concepts` },
|
||||||
|
{ text: t.sidebar.btEditorGuide, link: `${prefix}/guide/behavior-tree/editor-guide` },
|
||||||
|
{ text: t.sidebar.btEditorWorkflow, link: `${prefix}/guide/behavior-tree/editor-workflow` },
|
||||||
|
{ text: t.sidebar.btCustomActions, link: `${prefix}/guide/behavior-tree/custom-actions` },
|
||||||
|
{ text: t.sidebar.btCocosIntegration, link: `${prefix}/guide/behavior-tree/cocos-integration` },
|
||||||
|
{ text: t.sidebar.btLayaIntegration, link: `${prefix}/guide/behavior-tree/laya-integration` },
|
||||||
|
{ text: t.sidebar.btAdvancedUsage, link: `${prefix}/guide/behavior-tree/advanced-usage` },
|
||||||
|
{ text: t.sidebar.btBestPractices, link: `${prefix}/guide/behavior-tree/best-practices` }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ text: t.sidebar.serialization, link: `${prefix}/guide/serialization` },
|
||||||
|
{ text: t.sidebar.eventSystem, link: `${prefix}/guide/event-system` },
|
||||||
|
{ text: t.sidebar.timeAndTimers, link: `${prefix}/guide/time-and-timers` },
|
||||||
|
{ text: t.sidebar.logging, link: `${prefix}/guide/logging` }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t.sidebar.advancedFeatures,
|
||||||
|
collapsed: false,
|
||||||
|
items: [
|
||||||
|
{ text: t.sidebar.serviceContainer, link: `${prefix}/guide/service-container` },
|
||||||
|
{ text: t.sidebar.pluginSystem, link: `${prefix}/guide/plugin-system` }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t.sidebar.platformAdapters,
|
||||||
|
link: `${prefix}/guide/platform-adapter`,
|
||||||
|
collapsed: false,
|
||||||
|
items: [
|
||||||
|
{ text: t.sidebar.browserAdapter, link: `${prefix}/guide/platform-adapter/browser` },
|
||||||
|
{ text: t.sidebar.wechatAdapter, link: `${prefix}/guide/platform-adapter/wechat-minigame` },
|
||||||
|
{ text: t.sidebar.nodejsAdapter, link: `${prefix}/guide/platform-adapter/nodejs` }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[`${prefix}/examples/`]: [
|
||||||
|
{
|
||||||
|
text: t.sidebar.examples,
|
||||||
|
items: [
|
||||||
|
{ text: t.sidebar.examplesOverview, link: `${prefix}/examples/` },
|
||||||
|
{ text: t.nav.workerDemo, link: `${prefix}/examples/worker-system-demo` }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[`${prefix}/api/`]: [
|
||||||
|
{
|
||||||
|
text: t.sidebar.apiReference,
|
||||||
|
items: [
|
||||||
|
{ text: t.sidebar.overview, link: `${prefix}/api/README` },
|
||||||
|
{
|
||||||
|
text: t.sidebar.coreClasses,
|
||||||
|
collapsed: false,
|
||||||
|
items: [
|
||||||
|
{ text: 'Core', link: `${prefix}/api/classes/Core` },
|
||||||
|
{ text: 'Scene', link: `${prefix}/api/classes/Scene` },
|
||||||
|
{ text: 'World', link: `${prefix}/api/classes/World` },
|
||||||
|
{ text: 'Entity', link: `${prefix}/api/classes/Entity` },
|
||||||
|
{ text: 'Component', link: `${prefix}/api/classes/Component` },
|
||||||
|
{ text: 'EntitySystem', link: `${prefix}/api/classes/EntitySystem` }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t.sidebar.systemClasses,
|
||||||
|
collapsed: true,
|
||||||
|
items: [
|
||||||
|
{ text: 'PassiveSystem', link: `${prefix}/api/classes/PassiveSystem` },
|
||||||
|
{ text: 'ProcessingSystem', link: `${prefix}/api/classes/ProcessingSystem` },
|
||||||
|
{ text: 'IntervalSystem', link: `${prefix}/api/classes/IntervalSystem` }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t.sidebar.utilities,
|
||||||
|
collapsed: true,
|
||||||
|
items: [
|
||||||
|
{ text: 'Matcher', link: `${prefix}/api/classes/Matcher` },
|
||||||
|
{ text: 'Time', link: `${prefix}/api/classes/Time` },
|
||||||
|
{ text: 'PerformanceMonitor', link: `${prefix}/api/classes/PerformanceMonitor` },
|
||||||
|
{ text: 'DebugManager', link: `${prefix}/api/classes/DebugManager` }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t.sidebar.interfaces,
|
||||||
|
collapsed: true,
|
||||||
|
items: [
|
||||||
|
{ text: 'IScene', link: `${prefix}/api/interfaces/IScene` },
|
||||||
|
{ text: 'IComponent', link: `${prefix}/api/interfaces/IComponent` },
|
||||||
|
{ text: 'ISystemBase', link: `${prefix}/api/interfaces/ISystemBase` },
|
||||||
|
{ text: 'ICoreConfig', link: `${prefix}/api/interfaces/ICoreConfig` }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t.sidebar.decorators,
|
||||||
|
collapsed: true,
|
||||||
|
items: [
|
||||||
|
{ text: '@ECSComponent', link: `${prefix}/api/functions/ECSComponent` },
|
||||||
|
{ text: '@ECSSystem', link: `${prefix}/api/functions/ECSSystem` }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t.sidebar.enums,
|
||||||
|
collapsed: true,
|
||||||
|
items: [
|
||||||
|
{ text: 'ECSEventType', link: `${prefix}/api/enumerations/ECSEventType` },
|
||||||
|
{ text: 'LogLevel', link: `${prefix}/api/enumerations/LogLevel` }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建导航配置 | Create nav config
|
||||||
|
// prefix: 路径前缀,如 '' 或 '/en' | Path prefix like '' or '/en'
|
||||||
|
function createNav(t, prefix = '') {
|
||||||
|
return [
|
||||||
|
{ text: t.nav.home, link: `${prefix}/` },
|
||||||
|
{ text: t.nav.quickStart, link: `${prefix}/guide/getting-started` },
|
||||||
|
{ text: t.nav.guide, link: `${prefix}/guide/` },
|
||||||
|
{ text: t.nav.api, link: `${prefix}/api/README` },
|
||||||
|
{
|
||||||
|
text: t.nav.examples,
|
||||||
|
items: [
|
||||||
|
{ text: t.nav.workerDemo, link: `${prefix}/examples/worker-system-demo` },
|
||||||
|
{ text: t.nav.lawnMowerDemo, link: 'https://github.com/esengine/lawn-mower-demo' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: `v${corePackageJson.version}`,
|
||||||
|
link: 'https://github.com/esengine/ecs-framework/releases'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
vite: {
|
vite: {
|
||||||
plugins: [
|
plugins: [
|
||||||
@@ -28,160 +206,49 @@ export default defineConfig({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
title: 'ECS Framework',
|
title: 'ESEngine',
|
||||||
description: '高性能TypeScript ECS框架 - 为游戏开发而生',
|
appearance: 'force-dark',
|
||||||
lang: 'zh-CN',
|
|
||||||
|
locales: {
|
||||||
|
root: {
|
||||||
|
label: '简体中文',
|
||||||
|
lang: 'zh-CN',
|
||||||
|
description: '高性能 TypeScript ECS 框架 - 为游戏开发而生',
|
||||||
|
themeConfig: {
|
||||||
|
nav: createNav(zh, ''),
|
||||||
|
sidebar: createSidebar(zh, ''),
|
||||||
|
editLink: {
|
||||||
|
pattern: 'https://github.com/esengine/ecs-framework/edit/master/docs/:path',
|
||||||
|
text: zh.common.editOnGithub
|
||||||
|
},
|
||||||
|
outline: {
|
||||||
|
level: [2, 3],
|
||||||
|
label: zh.common.onThisPage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
label: 'English',
|
||||||
|
lang: 'en',
|
||||||
|
link: '/en/',
|
||||||
|
description: 'High-performance TypeScript ECS Framework for Game Development',
|
||||||
|
themeConfig: {
|
||||||
|
nav: createNav(en, '/en'),
|
||||||
|
sidebar: createSidebar(en, '/en'),
|
||||||
|
editLink: {
|
||||||
|
pattern: 'https://github.com/esengine/ecs-framework/edit/master/docs/:path',
|
||||||
|
text: en.common.editOnGithub
|
||||||
|
},
|
||||||
|
outline: {
|
||||||
|
level: [2, 3],
|
||||||
|
label: en.common.onThisPage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
themeConfig: {
|
themeConfig: {
|
||||||
nav: [
|
siteTitle: 'ESEngine',
|
||||||
{ 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: [
|
socialLinks: [
|
||||||
{ icon: 'github', link: 'https://github.com/esengine/ecs-framework' }
|
{ icon: 'github', link: 'https://github.com/esengine/ecs-framework' }
|
||||||
@@ -192,18 +259,8 @@ export default defineConfig({
|
|||||||
copyright: 'Copyright © 2025 ECS Framework'
|
copyright: 'Copyright © 2025 ECS Framework'
|
||||||
},
|
},
|
||||||
|
|
||||||
editLink: {
|
|
||||||
pattern: 'https://github.com/esengine/ecs-framework/edit/master/docs/:path',
|
|
||||||
text: '在 GitHub 上编辑此页'
|
|
||||||
},
|
|
||||||
|
|
||||||
search: {
|
search: {
|
||||||
provider: 'local'
|
provider: 'local'
|
||||||
},
|
|
||||||
|
|
||||||
outline: {
|
|
||||||
level: [2, 3],
|
|
||||||
label: '目录'
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -212,7 +269,7 @@ export default defineConfig({
|
|||||||
['link', { rel: 'icon', href: '/favicon.ico' }]
|
['link', { rel: 'icon', href: '/favicon.ico' }]
|
||||||
],
|
],
|
||||||
|
|
||||||
base: '/ecs-framework/',
|
base: '/',
|
||||||
cleanUrls: true,
|
cleanUrls: true,
|
||||||
|
|
||||||
markdown: {
|
markdown: {
|
||||||
@@ -222,4 +279,4 @@ export default defineConfig({
|
|||||||
dark: 'github-dark'
|
dark: 'github-dark'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
85
docs/.vitepress/i18n/en.json
Normal file
85
docs/.vitepress/i18n/en.json
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"home": "Home",
|
||||||
|
"quickStart": "Quick Start",
|
||||||
|
"guide": "Guide",
|
||||||
|
"api": "API",
|
||||||
|
"examples": "Examples",
|
||||||
|
"workerDemo": "Worker System Demo",
|
||||||
|
"lawnMowerDemo": "Lawn Mower Demo"
|
||||||
|
},
|
||||||
|
"sidebar": {
|
||||||
|
"gettingStarted": "Getting Started",
|
||||||
|
"quickStart": "Quick Start",
|
||||||
|
"guideOverview": "Guide Overview",
|
||||||
|
"coreConcepts": "Core Concepts",
|
||||||
|
"entity": "Entity",
|
||||||
|
"hierarchy": "Hierarchy",
|
||||||
|
"component": "Component",
|
||||||
|
"entityQuery": "Entity Query",
|
||||||
|
"system": "System",
|
||||||
|
"workerSystem": "Worker System (Multithreading)",
|
||||||
|
"scene": "Scene",
|
||||||
|
"sceneManager": "SceneManager",
|
||||||
|
"worldManager": "WorldManager",
|
||||||
|
"behaviorTree": "Behavior Tree",
|
||||||
|
"btGettingStarted": "Getting Started",
|
||||||
|
"btCoreConcepts": "Core Concepts",
|
||||||
|
"btEditorGuide": "Editor Guide",
|
||||||
|
"btEditorWorkflow": "Editor Workflow",
|
||||||
|
"btCustomActions": "Custom Actions",
|
||||||
|
"btCocosIntegration": "Cocos Creator Integration",
|
||||||
|
"btLayaIntegration": "Laya Engine Integration",
|
||||||
|
"btAdvancedUsage": "Advanced Usage",
|
||||||
|
"btBestPractices": "Best Practices",
|
||||||
|
"serialization": "Serialization",
|
||||||
|
"eventSystem": "Event System",
|
||||||
|
"timeAndTimers": "Time and Timers",
|
||||||
|
"logging": "Logging",
|
||||||
|
"advancedFeatures": "Advanced Features",
|
||||||
|
"serviceContainer": "Service Container",
|
||||||
|
"pluginSystem": "Plugin System",
|
||||||
|
"platformAdapters": "Platform Adapters",
|
||||||
|
"browserAdapter": "Browser Adapter",
|
||||||
|
"wechatAdapter": "WeChat Mini Game Adapter",
|
||||||
|
"nodejsAdapter": "Node.js Adapter",
|
||||||
|
"examples": "Examples",
|
||||||
|
"examplesOverview": "Examples Overview",
|
||||||
|
"apiReference": "API Reference",
|
||||||
|
"overview": "Overview",
|
||||||
|
"coreClasses": "Core Classes",
|
||||||
|
"systemClasses": "System Classes",
|
||||||
|
"utilities": "Utilities",
|
||||||
|
"interfaces": "Interfaces",
|
||||||
|
"decorators": "Decorators",
|
||||||
|
"enums": "Enums"
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"title": "ESEngine - High-performance TypeScript ECS Framework",
|
||||||
|
"quickLinks": "Quick Links",
|
||||||
|
"viewDocs": "View Docs",
|
||||||
|
"getStarted": "Get Started",
|
||||||
|
"getStartedDesc": "From installation to your first ECS app, learn the core concepts in 5 minutes.",
|
||||||
|
"aiSystem": "AI System",
|
||||||
|
"behaviorTreeEditor": "Visual Behavior Tree Editor",
|
||||||
|
"behaviorTreeDesc": "Built-in AI behavior tree system with visual editing and real-time debugging.",
|
||||||
|
"coreFeatures": "Core Features",
|
||||||
|
"ecsArchitecture": "High-performance ECS Architecture",
|
||||||
|
"ecsArchitectureDesc": "Data-driven entity component system for large-scale entity processing with cache-friendly memory layout.",
|
||||||
|
"typeSupport": "Full Type Support",
|
||||||
|
"typeSupportDesc": "100% TypeScript with complete type definitions and compile-time checking for the best development experience.",
|
||||||
|
"visualBehaviorTree": "Visual Behavior Tree",
|
||||||
|
"visualBehaviorTreeDesc": "Built-in AI behavior tree system with visual editor, custom nodes, and real-time debugging.",
|
||||||
|
"multiPlatform": "Multi-Platform Support",
|
||||||
|
"multiPlatformDesc": "Support for browsers, Node.js, WeChat Mini Games, and seamless integration with major game engines.",
|
||||||
|
"modularDesign": "Modular Design",
|
||||||
|
"modularDesignDesc": "Core features packaged independently, import only what you need. Support for custom plugin extensions.",
|
||||||
|
"devTools": "Developer Tools",
|
||||||
|
"devToolsDesc": "Built-in performance monitoring, debugging tools, serialization system, and complete development toolchain.",
|
||||||
|
"learnMore": "Learn more →"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"editOnGithub": "Edit this page on GitHub",
|
||||||
|
"onThisPage": "On this page"
|
||||||
|
}
|
||||||
|
}
|
||||||
21
docs/.vitepress/i18n/index.ts
Normal file
21
docs/.vitepress/i18n/index.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import en from './en.json'
|
||||||
|
import zh from './zh.json'
|
||||||
|
|
||||||
|
export const messages = { en, zh }
|
||||||
|
|
||||||
|
export type Locale = 'en' | 'zh'
|
||||||
|
|
||||||
|
export function getLocaleMessages(locale: Locale) {
|
||||||
|
return messages[locale] || messages.en
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to get nested key value
|
||||||
|
export function t(messages: typeof en, key: string): string {
|
||||||
|
const keys = key.split('.')
|
||||||
|
let result: any = messages
|
||||||
|
for (const k of keys) {
|
||||||
|
result = result?.[k]
|
||||||
|
if (result === undefined) return key
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
85
docs/.vitepress/i18n/zh.json
Normal file
85
docs/.vitepress/i18n/zh.json
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"home": "首页",
|
||||||
|
"quickStart": "快速开始",
|
||||||
|
"guide": "指南",
|
||||||
|
"api": "API",
|
||||||
|
"examples": "示例",
|
||||||
|
"workerDemo": "Worker系统演示",
|
||||||
|
"lawnMowerDemo": "割草机演示"
|
||||||
|
},
|
||||||
|
"sidebar": {
|
||||||
|
"gettingStarted": "开始使用",
|
||||||
|
"quickStart": "快速开始",
|
||||||
|
"guideOverview": "指南概览",
|
||||||
|
"coreConcepts": "核心概念",
|
||||||
|
"entity": "实体类 (Entity)",
|
||||||
|
"hierarchy": "层级系统 (Hierarchy)",
|
||||||
|
"component": "组件系统 (Component)",
|
||||||
|
"entityQuery": "实体查询系统",
|
||||||
|
"system": "系统架构 (System)",
|
||||||
|
"workerSystem": "Worker系统 (多线程)",
|
||||||
|
"scene": "场景管理 (Scene)",
|
||||||
|
"sceneManager": "SceneManager",
|
||||||
|
"worldManager": "WorldManager",
|
||||||
|
"behaviorTree": "行为树系统 (Behavior Tree)",
|
||||||
|
"btGettingStarted": "快速开始",
|
||||||
|
"btCoreConcepts": "核心概念",
|
||||||
|
"btEditorGuide": "编辑器指南",
|
||||||
|
"btEditorWorkflow": "编辑器工作流",
|
||||||
|
"btCustomActions": "自定义动作组件",
|
||||||
|
"btCocosIntegration": "Cocos Creator集成",
|
||||||
|
"btLayaIntegration": "Laya引擎集成",
|
||||||
|
"btAdvancedUsage": "高级用法",
|
||||||
|
"btBestPractices": "最佳实践",
|
||||||
|
"serialization": "序列化系统 (Serialization)",
|
||||||
|
"eventSystem": "事件系统 (Event)",
|
||||||
|
"timeAndTimers": "时间和定时器 (Time)",
|
||||||
|
"logging": "日志系统 (Logger)",
|
||||||
|
"advancedFeatures": "高级特性",
|
||||||
|
"serviceContainer": "服务容器 (Service Container)",
|
||||||
|
"pluginSystem": "插件系统 (Plugin System)",
|
||||||
|
"platformAdapters": "平台适配器",
|
||||||
|
"browserAdapter": "浏览器适配器",
|
||||||
|
"wechatAdapter": "微信小游戏适配器",
|
||||||
|
"nodejsAdapter": "Node.js适配器",
|
||||||
|
"examples": "示例",
|
||||||
|
"examplesOverview": "示例概览",
|
||||||
|
"apiReference": "API 参考",
|
||||||
|
"overview": "概述",
|
||||||
|
"coreClasses": "核心类",
|
||||||
|
"systemClasses": "系统类",
|
||||||
|
"utilities": "工具类",
|
||||||
|
"interfaces": "接口",
|
||||||
|
"decorators": "装饰器",
|
||||||
|
"enums": "枚举"
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"title": "ESEngine - 高性能 TypeScript ECS 框架",
|
||||||
|
"quickLinks": "快速入口",
|
||||||
|
"viewDocs": "查看文档",
|
||||||
|
"getStarted": "快速开始",
|
||||||
|
"getStartedDesc": "从安装到创建第一个 ECS 应用,快速了解核心概念。",
|
||||||
|
"aiSystem": "AI 系统",
|
||||||
|
"behaviorTreeEditor": "行为树可视化编辑器",
|
||||||
|
"behaviorTreeDesc": "内置 AI 行为树系统,支持可视化编辑和实时调试。",
|
||||||
|
"coreFeatures": "核心特性",
|
||||||
|
"ecsArchitecture": "高性能 ECS 架构",
|
||||||
|
"ecsArchitectureDesc": "基于数据驱动的实体组件系统,支持大规模实体处理,缓存友好的内存布局。",
|
||||||
|
"typeSupport": "完整类型支持",
|
||||||
|
"typeSupportDesc": "100% TypeScript 编写,完整的类型定义和编译时检查,提供最佳的开发体验。",
|
||||||
|
"visualBehaviorTree": "可视化行为树",
|
||||||
|
"visualBehaviorTreeDesc": "内置 AI 行为树系统,提供可视化编辑器,支持自定义节点和实时调试。",
|
||||||
|
"multiPlatform": "多平台支持",
|
||||||
|
"multiPlatformDesc": "支持浏览器、Node.js、微信小游戏等多平台,可与主流游戏引擎无缝集成。",
|
||||||
|
"modularDesign": "模块化设计",
|
||||||
|
"modularDesignDesc": "核心功能独立打包,按需引入。支持自定义插件扩展,灵活适配不同项目。",
|
||||||
|
"devTools": "开发者工具",
|
||||||
|
"devToolsDesc": "内置性能监控、调试工具、序列化系统等,提供完整的开发工具链。",
|
||||||
|
"learnMore": "了解更多 →"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"editOnGithub": "在 GitHub 上编辑此页",
|
||||||
|
"onThisPage": "在这个页面上"
|
||||||
|
}
|
||||||
|
}
|
||||||
93
docs/.vitepress/theme/components/FeatureCard.vue
Normal file
93
docs/.vitepress/theme/components/FeatureCard.vue
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
title: String,
|
||||||
|
description: String,
|
||||||
|
icon: String,
|
||||||
|
link: String,
|
||||||
|
image: String
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<a :href="link" class="feature-card">
|
||||||
|
<div class="card-image" v-if="image">
|
||||||
|
<img :src="image" :alt="title" />
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="card-icon" v-if="icon && !image">{{ icon }}</div>
|
||||||
|
<h3 class="card-title">{{ title }}</h3>
|
||||||
|
<p class="card-description">{{ description }}</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.feature-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: var(--es-bg-elevated, #252526);
|
||||||
|
border: 1px solid var(--es-border-default, #3e3e42);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card:hover {
|
||||||
|
border-color: var(--es-primary, #007acc);
|
||||||
|
background: var(--es-bg-overlay, #2d2d2d);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 160px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: linear-gradient(135deg, #1e3a5f 0%, #1e1e1e 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-image img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card:hover .card-image img {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
padding: 16px;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--es-bg-input, #3c3c3c);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--es-text-inverse, #ffffff);
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-description {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--es-text-secondary, #9d9d9d);
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.6;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
422
docs/.vitepress/theme/components/ParticleHero.vue
Normal file
422
docs/.vitepress/theme/components/ParticleHero.vue
Normal file
@@ -0,0 +1,422 @@
|
|||||||
|
<script setup>
|
||||||
|
import { onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
|
||||||
|
const canvasRef = ref(null)
|
||||||
|
let animationId = null
|
||||||
|
let particles = []
|
||||||
|
let animationStartTime = null
|
||||||
|
let glowStartTime = null
|
||||||
|
|
||||||
|
// ESEngine 粒子颜色 - VS Code 风格配色(与编辑器统一)
|
||||||
|
const colors = ['#569CD6', '#4EC9B0', '#9CDCFE', '#C586C0', '#DCDCAA']
|
||||||
|
|
||||||
|
class Particle {
|
||||||
|
constructor(x, y, targetX, targetY) {
|
||||||
|
this.x = x
|
||||||
|
this.y = y
|
||||||
|
this.targetX = targetX
|
||||||
|
this.targetY = targetY
|
||||||
|
this.size = Math.random() * 2 + 1.5
|
||||||
|
this.alpha = Math.random() * 0.5 + 0.5
|
||||||
|
this.color = colors[Math.floor(Math.random() * colors.length)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createParticles(canvas, text, fontSize) {
|
||||||
|
const tempCanvas = document.createElement('canvas')
|
||||||
|
const tempCtx = tempCanvas.getContext('2d')
|
||||||
|
if (!tempCtx) return []
|
||||||
|
|
||||||
|
tempCtx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||||
|
const textMetrics = tempCtx.measureText(text)
|
||||||
|
const textWidth = textMetrics.width
|
||||||
|
const textHeight = fontSize
|
||||||
|
|
||||||
|
tempCanvas.width = textWidth + 40
|
||||||
|
tempCanvas.height = textHeight + 40
|
||||||
|
tempCtx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||||
|
tempCtx.textAlign = 'center'
|
||||||
|
tempCtx.textBaseline = 'middle'
|
||||||
|
tempCtx.fillStyle = '#ffffff'
|
||||||
|
tempCtx.fillText(text, tempCanvas.width / 2, tempCanvas.height / 2)
|
||||||
|
|
||||||
|
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height)
|
||||||
|
const pixels = imageData.data
|
||||||
|
const newParticles = []
|
||||||
|
const gap = 3
|
||||||
|
|
||||||
|
const width = canvas.width / (window.devicePixelRatio || 1)
|
||||||
|
const height = canvas.height / (window.devicePixelRatio || 1)
|
||||||
|
const offsetX = (width - tempCanvas.width) / 2
|
||||||
|
const offsetY = (height - tempCanvas.height) / 2
|
||||||
|
|
||||||
|
for (let y = 0; y < tempCanvas.height; y += gap) {
|
||||||
|
for (let x = 0; x < tempCanvas.width; x += gap) {
|
||||||
|
const index = (y * tempCanvas.width + x) * 4
|
||||||
|
const alpha = pixels[index + 3] || 0
|
||||||
|
|
||||||
|
if (alpha > 128) {
|
||||||
|
const angle = Math.random() * Math.PI * 2
|
||||||
|
const distance = Math.random() * Math.max(width, height)
|
||||||
|
|
||||||
|
newParticles.push(new Particle(
|
||||||
|
width / 2 + Math.cos(angle) * distance,
|
||||||
|
height / 2 + Math.sin(angle) * distance,
|
||||||
|
offsetX + x,
|
||||||
|
offsetY + y
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newParticles
|
||||||
|
}
|
||||||
|
|
||||||
|
function easeOutQuart(t) {
|
||||||
|
return 1 - Math.pow(1 - t, 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
function easeOutCubic(t) {
|
||||||
|
return 1 - Math.pow(1 - t, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
function animate(canvas, ctx) {
|
||||||
|
const dpr = window.devicePixelRatio || 1
|
||||||
|
const width = canvas.width / dpr
|
||||||
|
const height = canvas.height / dpr
|
||||||
|
|
||||||
|
const currentTime = performance.now()
|
||||||
|
const duration = 2500
|
||||||
|
const glowDuration = 600
|
||||||
|
|
||||||
|
const elapsed = currentTime - animationStartTime
|
||||||
|
const progress = Math.min(elapsed / duration, 1)
|
||||||
|
const easedProgress = easeOutQuart(progress)
|
||||||
|
|
||||||
|
// 透明背景
|
||||||
|
ctx.clearRect(0, 0, width, height)
|
||||||
|
|
||||||
|
// 计算发光进度
|
||||||
|
let glowProgress = 0
|
||||||
|
if (progress >= 1) {
|
||||||
|
if (glowStartTime === null) {
|
||||||
|
glowStartTime = currentTime
|
||||||
|
}
|
||||||
|
glowProgress = Math.min((currentTime - glowStartTime) / glowDuration, 1)
|
||||||
|
glowProgress = easeOutCubic(glowProgress)
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = 'ESEngine'
|
||||||
|
const fontSize = Math.min(width / 4, height / 3, 80)
|
||||||
|
const textY = height / 2
|
||||||
|
|
||||||
|
for (const particle of particles) {
|
||||||
|
const moveProgress = Math.min(easedProgress * 1.2, 1)
|
||||||
|
const currentX = particle.x + (particle.targetX - particle.x) * moveProgress
|
||||||
|
const currentY = particle.y + (particle.targetY - particle.y) * moveProgress
|
||||||
|
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(currentX, currentY, particle.size, 0, Math.PI * 2)
|
||||||
|
ctx.fillStyle = particle.color
|
||||||
|
ctx.globalAlpha = particle.alpha * (1 - glowProgress * 0.3)
|
||||||
|
ctx.fill()
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.globalAlpha = 1
|
||||||
|
|
||||||
|
if (glowProgress > 0) {
|
||||||
|
ctx.save()
|
||||||
|
ctx.shadowColor = '#3b9eff'
|
||||||
|
ctx.shadowBlur = 30 * glowProgress
|
||||||
|
ctx.fillStyle = `rgba(255, 255, 255, ${glowProgress})`
|
||||||
|
ctx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||||
|
ctx.textAlign = 'center'
|
||||||
|
ctx.textBaseline = 'middle'
|
||||||
|
ctx.fillText(text, width / 2, textY)
|
||||||
|
ctx.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (glowProgress >= 1) {
|
||||||
|
const breathe = 0.8 + Math.sin(currentTime / 1000) * 0.2
|
||||||
|
ctx.save()
|
||||||
|
ctx.shadowColor = '#3b9eff'
|
||||||
|
ctx.shadowBlur = 20 * breathe
|
||||||
|
ctx.fillStyle = '#ffffff'
|
||||||
|
ctx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||||
|
ctx.textAlign = 'center'
|
||||||
|
ctx.textBaseline = 'middle'
|
||||||
|
ctx.fillText(text, width / 2, textY)
|
||||||
|
ctx.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
animationId = requestAnimationFrame(() => animate(canvas, ctx))
|
||||||
|
}
|
||||||
|
|
||||||
|
function initCanvas() {
|
||||||
|
const canvas = canvasRef.value
|
||||||
|
if (!canvas) return
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
if (!ctx) return
|
||||||
|
|
||||||
|
const dpr = window.devicePixelRatio || 1
|
||||||
|
const container = canvas.parentElement
|
||||||
|
const width = container.offsetWidth
|
||||||
|
const height = container.offsetHeight
|
||||||
|
|
||||||
|
canvas.width = width * dpr
|
||||||
|
canvas.height = height * dpr
|
||||||
|
canvas.style.width = `${width}px`
|
||||||
|
canvas.style.height = `${height}px`
|
||||||
|
ctx.scale(dpr, dpr)
|
||||||
|
|
||||||
|
const text = 'ESEngine'
|
||||||
|
const fontSize = Math.min(width / 4, height / 3, 80)
|
||||||
|
|
||||||
|
particles = createParticles(canvas, text, fontSize)
|
||||||
|
animationStartTime = performance.now()
|
||||||
|
glowStartTime = null
|
||||||
|
|
||||||
|
if (animationId) {
|
||||||
|
cancelAnimationFrame(animationId)
|
||||||
|
}
|
||||||
|
|
||||||
|
animate(canvas, ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initCanvas()
|
||||||
|
window.addEventListener('resize', initCanvas)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (animationId) {
|
||||||
|
cancelAnimationFrame(animationId)
|
||||||
|
}
|
||||||
|
window.removeEventListener('resize', initCanvas)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="hero-section">
|
||||||
|
<div class="hero-container">
|
||||||
|
<!-- 左侧文字区域 -->
|
||||||
|
<div class="hero-text">
|
||||||
|
<div class="hero-logo">
|
||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="16" cy="16" r="14" stroke="#9147ff" stroke-width="2"/>
|
||||||
|
<path d="M10 10h8v2h-6v3h5v2h-5v3h6v2h-8v-12z" fill="#9147ff"/>
|
||||||
|
</svg>
|
||||||
|
<span>ESENGINE</span>
|
||||||
|
</div>
|
||||||
|
<h1 class="hero-title">
|
||||||
|
我们构建框架。<br/>
|
||||||
|
而你将创造游戏。
|
||||||
|
</h1>
|
||||||
|
<p class="hero-description">
|
||||||
|
ESEngine 是一个高性能的 TypeScript ECS 框架,为游戏开发者提供现代化的实体组件系统。
|
||||||
|
无论是 2D 还是 3D 游戏,都能帮助你快速构建可扩展的游戏架构。
|
||||||
|
</p>
|
||||||
|
<div class="hero-actions">
|
||||||
|
<a href="/guide/getting-started" class="btn-primary">开始使用</a>
|
||||||
|
<a href="https://github.com/esengine/ecs-framework" class="btn-secondary" target="_blank">了解更多</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧粒子动画区域 -->
|
||||||
|
<div class="hero-visual">
|
||||||
|
<div class="visual-container">
|
||||||
|
<canvas ref="canvasRef" class="particle-canvas"></canvas>
|
||||||
|
<div class="visual-label">
|
||||||
|
<span class="label-title">Entity Component System</span>
|
||||||
|
<span class="label-subtitle">High Performance Framework</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.hero-section {
|
||||||
|
background: #0d0d0d;
|
||||||
|
padding: 80px 0;
|
||||||
|
min-height: calc(100vh - 64px);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-container {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 48px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1.2fr;
|
||||||
|
gap: 64px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左侧文字 */
|
||||||
|
.hero-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
font-size: 3rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ffffff;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-description {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
color: #707070;
|
||||||
|
line-height: 1.7;
|
||||||
|
margin: 0;
|
||||||
|
max-width: 480px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary,
|
||||||
|
.btn-secondary {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 14px 28px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: #3b9eff;
|
||||||
|
color: #ffffff;
|
||||||
|
border: 1px solid #3b9eff;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: #5aadff;
|
||||||
|
border-color: #5aadff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: #a0a0a0;
|
||||||
|
border: 1px solid #2a2a2a;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #252525;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-visual {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visual-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
aspect-ratio: 4 / 3;
|
||||||
|
background: linear-gradient(135deg, #1a2a3a 0%, #1a1a1a 50%, #0d0d0d 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #2a2a2a;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 20px 60px rgba(59, 158, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.particle-canvas {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visual-label {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 24px;
|
||||||
|
left: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-title {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-subtitle {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #737373;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式 */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.hero-container {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 48px;
|
||||||
|
padding: 0 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section {
|
||||||
|
padding: 48px 0;
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
font-size: 2.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-description {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visual-container {
|
||||||
|
max-width: 100%;
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.hero-title {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary,
|
||||||
|
.btn-secondary {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
422
docs/.vitepress/theme/components/ParticleHeroEn.vue
Normal file
422
docs/.vitepress/theme/components/ParticleHeroEn.vue
Normal file
@@ -0,0 +1,422 @@
|
|||||||
|
<script setup>
|
||||||
|
import { onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
|
||||||
|
const canvasRef = ref(null)
|
||||||
|
let animationId = null
|
||||||
|
let particles = []
|
||||||
|
let animationStartTime = null
|
||||||
|
let glowStartTime = null
|
||||||
|
|
||||||
|
// ESEngine particle colors - VS Code style colors (unified with editor)
|
||||||
|
const colors = ['#569CD6', '#4EC9B0', '#9CDCFE', '#C586C0', '#DCDCAA']
|
||||||
|
|
||||||
|
class Particle {
|
||||||
|
constructor(x, y, targetX, targetY) {
|
||||||
|
this.x = x
|
||||||
|
this.y = y
|
||||||
|
this.targetX = targetX
|
||||||
|
this.targetY = targetY
|
||||||
|
this.size = Math.random() * 2 + 1.5
|
||||||
|
this.alpha = Math.random() * 0.5 + 0.5
|
||||||
|
this.color = colors[Math.floor(Math.random() * colors.length)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createParticles(canvas, text, fontSize) {
|
||||||
|
const tempCanvas = document.createElement('canvas')
|
||||||
|
const tempCtx = tempCanvas.getContext('2d')
|
||||||
|
if (!tempCtx) return []
|
||||||
|
|
||||||
|
tempCtx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||||
|
const textMetrics = tempCtx.measureText(text)
|
||||||
|
const textWidth = textMetrics.width
|
||||||
|
const textHeight = fontSize
|
||||||
|
|
||||||
|
tempCanvas.width = textWidth + 40
|
||||||
|
tempCanvas.height = textHeight + 40
|
||||||
|
tempCtx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||||
|
tempCtx.textAlign = 'center'
|
||||||
|
tempCtx.textBaseline = 'middle'
|
||||||
|
tempCtx.fillStyle = '#ffffff'
|
||||||
|
tempCtx.fillText(text, tempCanvas.width / 2, tempCanvas.height / 2)
|
||||||
|
|
||||||
|
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height)
|
||||||
|
const pixels = imageData.data
|
||||||
|
const newParticles = []
|
||||||
|
const gap = 3
|
||||||
|
|
||||||
|
const width = canvas.width / (window.devicePixelRatio || 1)
|
||||||
|
const height = canvas.height / (window.devicePixelRatio || 1)
|
||||||
|
const offsetX = (width - tempCanvas.width) / 2
|
||||||
|
const offsetY = (height - tempCanvas.height) / 2
|
||||||
|
|
||||||
|
for (let y = 0; y < tempCanvas.height; y += gap) {
|
||||||
|
for (let x = 0; x < tempCanvas.width; x += gap) {
|
||||||
|
const index = (y * tempCanvas.width + x) * 4
|
||||||
|
const alpha = pixels[index + 3] || 0
|
||||||
|
|
||||||
|
if (alpha > 128) {
|
||||||
|
const angle = Math.random() * Math.PI * 2
|
||||||
|
const distance = Math.random() * Math.max(width, height)
|
||||||
|
|
||||||
|
newParticles.push(new Particle(
|
||||||
|
width / 2 + Math.cos(angle) * distance,
|
||||||
|
height / 2 + Math.sin(angle) * distance,
|
||||||
|
offsetX + x,
|
||||||
|
offsetY + y
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newParticles
|
||||||
|
}
|
||||||
|
|
||||||
|
function easeOutQuart(t) {
|
||||||
|
return 1 - Math.pow(1 - t, 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
function easeOutCubic(t) {
|
||||||
|
return 1 - Math.pow(1 - t, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
function animate(canvas, ctx) {
|
||||||
|
const dpr = window.devicePixelRatio || 1
|
||||||
|
const width = canvas.width / dpr
|
||||||
|
const height = canvas.height / dpr
|
||||||
|
|
||||||
|
const currentTime = performance.now()
|
||||||
|
const duration = 2500
|
||||||
|
const glowDuration = 600
|
||||||
|
|
||||||
|
const elapsed = currentTime - animationStartTime
|
||||||
|
const progress = Math.min(elapsed / duration, 1)
|
||||||
|
const easedProgress = easeOutQuart(progress)
|
||||||
|
|
||||||
|
// Transparent background
|
||||||
|
ctx.clearRect(0, 0, width, height)
|
||||||
|
|
||||||
|
// Calculate glow progress
|
||||||
|
let glowProgress = 0
|
||||||
|
if (progress >= 1) {
|
||||||
|
if (glowStartTime === null) {
|
||||||
|
glowStartTime = currentTime
|
||||||
|
}
|
||||||
|
glowProgress = Math.min((currentTime - glowStartTime) / glowDuration, 1)
|
||||||
|
glowProgress = easeOutCubic(glowProgress)
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = 'ESEngine'
|
||||||
|
const fontSize = Math.min(width / 4, height / 3, 80)
|
||||||
|
const textY = height / 2
|
||||||
|
|
||||||
|
for (const particle of particles) {
|
||||||
|
const moveProgress = Math.min(easedProgress * 1.2, 1)
|
||||||
|
const currentX = particle.x + (particle.targetX - particle.x) * moveProgress
|
||||||
|
const currentY = particle.y + (particle.targetY - particle.y) * moveProgress
|
||||||
|
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(currentX, currentY, particle.size, 0, Math.PI * 2)
|
||||||
|
ctx.fillStyle = particle.color
|
||||||
|
ctx.globalAlpha = particle.alpha * (1 - glowProgress * 0.3)
|
||||||
|
ctx.fill()
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.globalAlpha = 1
|
||||||
|
|
||||||
|
if (glowProgress > 0) {
|
||||||
|
ctx.save()
|
||||||
|
ctx.shadowColor = '#3b9eff'
|
||||||
|
ctx.shadowBlur = 30 * glowProgress
|
||||||
|
ctx.fillStyle = `rgba(255, 255, 255, ${glowProgress})`
|
||||||
|
ctx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||||
|
ctx.textAlign = 'center'
|
||||||
|
ctx.textBaseline = 'middle'
|
||||||
|
ctx.fillText(text, width / 2, textY)
|
||||||
|
ctx.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (glowProgress >= 1) {
|
||||||
|
const breathe = 0.8 + Math.sin(currentTime / 1000) * 0.2
|
||||||
|
ctx.save()
|
||||||
|
ctx.shadowColor = '#3b9eff'
|
||||||
|
ctx.shadowBlur = 20 * breathe
|
||||||
|
ctx.fillStyle = '#ffffff'
|
||||||
|
ctx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||||
|
ctx.textAlign = 'center'
|
||||||
|
ctx.textBaseline = 'middle'
|
||||||
|
ctx.fillText(text, width / 2, textY)
|
||||||
|
ctx.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
animationId = requestAnimationFrame(() => animate(canvas, ctx))
|
||||||
|
}
|
||||||
|
|
||||||
|
function initCanvas() {
|
||||||
|
const canvas = canvasRef.value
|
||||||
|
if (!canvas) return
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
if (!ctx) return
|
||||||
|
|
||||||
|
const dpr = window.devicePixelRatio || 1
|
||||||
|
const container = canvas.parentElement
|
||||||
|
const width = container.offsetWidth
|
||||||
|
const height = container.offsetHeight
|
||||||
|
|
||||||
|
canvas.width = width * dpr
|
||||||
|
canvas.height = height * dpr
|
||||||
|
canvas.style.width = `${width}px`
|
||||||
|
canvas.style.height = `${height}px`
|
||||||
|
ctx.scale(dpr, dpr)
|
||||||
|
|
||||||
|
const text = 'ESEngine'
|
||||||
|
const fontSize = Math.min(width / 4, height / 3, 80)
|
||||||
|
|
||||||
|
particles = createParticles(canvas, text, fontSize)
|
||||||
|
animationStartTime = performance.now()
|
||||||
|
glowStartTime = null
|
||||||
|
|
||||||
|
if (animationId) {
|
||||||
|
cancelAnimationFrame(animationId)
|
||||||
|
}
|
||||||
|
|
||||||
|
animate(canvas, ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initCanvas()
|
||||||
|
window.addEventListener('resize', initCanvas)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (animationId) {
|
||||||
|
cancelAnimationFrame(animationId)
|
||||||
|
}
|
||||||
|
window.removeEventListener('resize', initCanvas)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="hero-section">
|
||||||
|
<div class="hero-container">
|
||||||
|
<!-- Left text area -->
|
||||||
|
<div class="hero-text">
|
||||||
|
<div class="hero-logo">
|
||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="16" cy="16" r="14" stroke="#9147ff" stroke-width="2"/>
|
||||||
|
<path d="M10 10h8v2h-6v3h5v2h-5v3h6v2h-8v-12z" fill="#9147ff"/>
|
||||||
|
</svg>
|
||||||
|
<span>ESENGINE</span>
|
||||||
|
</div>
|
||||||
|
<h1 class="hero-title">
|
||||||
|
We build the framework.<br/>
|
||||||
|
You create the game.
|
||||||
|
</h1>
|
||||||
|
<p class="hero-description">
|
||||||
|
ESEngine is a high-performance TypeScript ECS framework for game developers.
|
||||||
|
Whether 2D or 3D games, it helps you build scalable game architecture quickly.
|
||||||
|
</p>
|
||||||
|
<div class="hero-actions">
|
||||||
|
<a href="/en/guide/getting-started" class="btn-primary">Get Started</a>
|
||||||
|
<a href="https://github.com/esengine/ecs-framework" class="btn-secondary" target="_blank">Learn More</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right particle animation area -->
|
||||||
|
<div class="hero-visual">
|
||||||
|
<div class="visual-container">
|
||||||
|
<canvas ref="canvasRef" class="particle-canvas"></canvas>
|
||||||
|
<div class="visual-label">
|
||||||
|
<span class="label-title">Entity Component System</span>
|
||||||
|
<span class="label-subtitle">High Performance Framework</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.hero-section {
|
||||||
|
background: #0d0d0d;
|
||||||
|
padding: 80px 0;
|
||||||
|
min-height: calc(100vh - 64px);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-container {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 48px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1.2fr;
|
||||||
|
gap: 64px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Left text */
|
||||||
|
.hero-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
font-size: 3rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ffffff;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-description {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
color: #707070;
|
||||||
|
line-height: 1.7;
|
||||||
|
margin: 0;
|
||||||
|
max-width: 480px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary,
|
||||||
|
.btn-secondary {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 14px 28px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: #3b9eff;
|
||||||
|
color: #ffffff;
|
||||||
|
border: 1px solid #3b9eff;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: #5aadff;
|
||||||
|
border-color: #5aadff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: #a0a0a0;
|
||||||
|
border: 1px solid #2a2a2a;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #252525;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-visual {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visual-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
aspect-ratio: 4 / 3;
|
||||||
|
background: linear-gradient(135deg, #1a2a3a 0%, #1a1a1a 50%, #0d0d0d 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #2a2a2a;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 20px 60px rgba(59, 158, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.particle-canvas {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visual-label {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 24px;
|
||||||
|
left: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-title {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-subtitle {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #737373;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.hero-container {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 48px;
|
||||||
|
padding: 0 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section {
|
||||||
|
padding: 48px 0;
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
font-size: 2.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-description {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visual-container {
|
||||||
|
max-width: 100%;
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.hero-title {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary,
|
||||||
|
.btn-secondary {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
594
docs/.vitepress/theme/custom.css
Normal file
594
docs/.vitepress/theme/custom.css
Normal file
@@ -0,0 +1,594 @@
|
|||||||
|
:root {
|
||||||
|
color-scheme: dark;
|
||||||
|
--vp-nav-height: 64px;
|
||||||
|
|
||||||
|
--es-bg-base: #1e1e1e;
|
||||||
|
--es-bg-elevated: #252526;
|
||||||
|
--es-bg-overlay: #2d2d2d;
|
||||||
|
--es-bg-input: #3c3c3c;
|
||||||
|
--es-bg-inset: #181818;
|
||||||
|
--es-bg-hover: #2a2d2e;
|
||||||
|
--es-bg-active: #37373d;
|
||||||
|
--es-bg-sidebar: #262626;
|
||||||
|
--es-bg-card: #2a2a2a;
|
||||||
|
--es-bg-header: #2d2d2d;
|
||||||
|
|
||||||
|
--es-text-primary: #cccccc;
|
||||||
|
--es-text-secondary: #9d9d9d;
|
||||||
|
--es-text-tertiary: #6a6a6a;
|
||||||
|
--es-text-inverse: #ffffff;
|
||||||
|
--es-text-muted: #aaaaaa;
|
||||||
|
--es-text-dim: #6a6a6a;
|
||||||
|
|
||||||
|
--es-font-xs: 11px;
|
||||||
|
--es-font-sm: 12px;
|
||||||
|
--es-font-base: 13px;
|
||||||
|
--es-font-md: 14px;
|
||||||
|
--es-font-lg: 16px;
|
||||||
|
|
||||||
|
--es-border-default: #3a3a3a;
|
||||||
|
--es-border-subtle: #1a1a1a;
|
||||||
|
--es-border-strong: #4a4a4a;
|
||||||
|
|
||||||
|
--es-primary: #3b82f6;
|
||||||
|
--es-primary-hover: #2563eb;
|
||||||
|
--es-success: #4ade80;
|
||||||
|
--es-warning: #f59e0b;
|
||||||
|
--es-error: #ef4444;
|
||||||
|
--es-info: #3b82f6;
|
||||||
|
|
||||||
|
--es-selected: #3d5a80;
|
||||||
|
--es-selected-hover: #4a6a90;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--es-bg-base) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
html.dark {
|
||||||
|
--vp-c-bg: var(--es-bg-base);
|
||||||
|
--vp-c-bg-soft: var(--es-bg-elevated);
|
||||||
|
--vp-c-bg-mute: var(--es-bg-overlay);
|
||||||
|
--vp-c-bg-alt: var(--es-bg-sidebar);
|
||||||
|
--vp-c-text-1: var(--es-text-primary);
|
||||||
|
--vp-c-text-2: var(--es-text-tertiary);
|
||||||
|
--vp-c-text-3: var(--es-text-muted);
|
||||||
|
--vp-c-divider: var(--es-border-default);
|
||||||
|
--vp-c-divider-light: var(--es-border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
html:not(.dark) {
|
||||||
|
--vp-c-bg: var(--es-bg-base) !important;
|
||||||
|
--vp-c-bg-soft: var(--es-bg-elevated) !important;
|
||||||
|
--vp-c-bg-mute: var(--es-bg-overlay) !important;
|
||||||
|
--vp-c-bg-alt: var(--es-bg-sidebar) !important;
|
||||||
|
--vp-c-text-1: var(--es-text-primary) !important;
|
||||||
|
--vp-c-text-2: var(--es-text-tertiary) !important;
|
||||||
|
--vp-c-text-3: var(--es-text-muted) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPNav {
|
||||||
|
background: var(--es-bg-header) !important;
|
||||||
|
border-bottom: 1px solid var(--es-border-subtle) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPNav .VPNavBar {
|
||||||
|
background: var(--es-bg-header) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPNav .VPNavBar .wrapper {
|
||||||
|
background: var(--es-bg-header) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPNav .VPNavBar::before,
|
||||||
|
.VPNav .VPNavBar::after {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPNavBar {
|
||||||
|
background: var(--es-bg-header) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPNavBar::before {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPNavBarTitle .title {
|
||||||
|
color: var(--es-text-primary);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: var(--es-font-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPNavBarMenuLink {
|
||||||
|
color: var(--es-text-secondary) !important;
|
||||||
|
font-size: var(--es-font-sm) !important;
|
||||||
|
font-weight: 400 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPNavBarMenuLink:hover {
|
||||||
|
color: var(--es-text-primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPNavBarMenuLink.active {
|
||||||
|
color: var(--es-text-primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPNavBarSearch .DocSearch-Button {
|
||||||
|
background: var(--es-bg-input) !important;
|
||||||
|
border: 1px solid var(--es-border-default) !important;
|
||||||
|
border-radius: 2px;
|
||||||
|
height: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPSidebar {
|
||||||
|
background: var(--es-bg-sidebar) !important;
|
||||||
|
border-right: 1px solid var(--es-border-subtle) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPSidebarItem.level-0 > .item {
|
||||||
|
padding: 8px 0 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPSidebarItem.level-0 > .item > .text {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: var(--es-font-xs);
|
||||||
|
color: var(--es-text-secondary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPSidebarItem .link {
|
||||||
|
padding: 4px 8px;
|
||||||
|
margin: 1px 0;
|
||||||
|
border-radius: 2px;
|
||||||
|
color: var(--es-text-primary);
|
||||||
|
font-size: var(--es-font-sm);
|
||||||
|
transition: all 0.1s ease;
|
||||||
|
border-left: 2px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPSidebarItem .link:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
color: var(--es-text-inverse);
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPSidebarItem.is-active > .item > .link {
|
||||||
|
background: var(--es-selected);
|
||||||
|
color: var(--es-text-inverse);
|
||||||
|
border-left: 2px solid var(--es-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPSidebarItem.is-active > .item > .link:hover {
|
||||||
|
background: var(--es-selected-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPSidebarItem.level-1 .link {
|
||||||
|
padding-left: 20px;
|
||||||
|
font-size: var(--es-font-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPSidebarItem.level-2 .link {
|
||||||
|
padding-left: 32px;
|
||||||
|
font-size: var(--es-font-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPSidebarItem .caret {
|
||||||
|
color: var(--es-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPSidebarItem .caret:hover {
|
||||||
|
color: var(--es-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPContent {
|
||||||
|
background: var(--es-bg-card) !important;
|
||||||
|
padding-top: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPContent.has-sidebar {
|
||||||
|
background: var(--es-bg-card) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 首页布局修复 | Home page layout fix */
|
||||||
|
.VPPage {
|
||||||
|
padding-top: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Layout > .VPContent {
|
||||||
|
padding-top: var(--vp-nav-height) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPDoc {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPNavBar .content {
|
||||||
|
background: var(--es-bg-header) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPNavBar .content-body {
|
||||||
|
background: var(--es-bg-header) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPNavBar .divider {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPLocalNav {
|
||||||
|
background: var(--es-bg-header) !important;
|
||||||
|
border-bottom: 1px solid var(--es-border-subtle) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPNavScreenMenu {
|
||||||
|
background: var(--es-bg-base) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPNavScreen {
|
||||||
|
background: var(--es-bg-base) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.curtain {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPNav .curtain,
|
||||||
|
.VPNavBar .curtain {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[class*="curtain"] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPNav > div::before,
|
||||||
|
.VPNav > div::after {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc {
|
||||||
|
color: var(--es-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc h1 {
|
||||||
|
font-size: var(--es-font-lg);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--es-text-inverse);
|
||||||
|
border-bottom: none;
|
||||||
|
padding-bottom: 0;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc h2 {
|
||||||
|
font-size: var(--es-font-md);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--es-text-inverse);
|
||||||
|
border-bottom: none;
|
||||||
|
padding-bottom: 0;
|
||||||
|
margin-top: 32px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: var(--es-bg-header);
|
||||||
|
border-left: 3px solid var(--es-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc h3 {
|
||||||
|
font-size: var(--es-font-base);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--es-text-primary);
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc p {
|
||||||
|
color: var(--es-text-primary);
|
||||||
|
line-height: 1.7;
|
||||||
|
font-size: var(--es-font-base);
|
||||||
|
margin: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc ul,
|
||||||
|
.vp-doc ol {
|
||||||
|
padding-left: 20px;
|
||||||
|
margin: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc li {
|
||||||
|
line-height: 1.7;
|
||||||
|
margin: 4px 0;
|
||||||
|
color: var(--es-text-primary);
|
||||||
|
font-size: var(--es-font-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc li::marker {
|
||||||
|
color: var(--es-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc strong {
|
||||||
|
color: var(--es-text-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc a {
|
||||||
|
color: var(--es-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPDocAside {
|
||||||
|
padding-left: 16px;
|
||||||
|
border-left: 1px solid var(--es-border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPDocAsideOutline {
|
||||||
|
padding: 0;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPDocAsideOutline .content {
|
||||||
|
border: none !important;
|
||||||
|
padding-left: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPDocAsideOutline .outline-title {
|
||||||
|
font-size: var(--es-font-xs);
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: var(--es-text-secondary);
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPDocAsideOutline .outline-link {
|
||||||
|
color: var(--es-text-secondary);
|
||||||
|
font-size: var(--es-font-xs);
|
||||||
|
padding: 4px 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPDocAsideOutline .outline-link:hover {
|
||||||
|
color: var(--es-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPDocAsideOutline .outline-link.active {
|
||||||
|
color: var(--es-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPDocAsideOutline .outline-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div[class*='language-'] {
|
||||||
|
background: var(--es-bg-inset) !important;
|
||||||
|
border: 1px solid var(--es-border-default);
|
||||||
|
border-radius: 2px;
|
||||||
|
margin: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-code-group .tabs {
|
||||||
|
background: var(--es-bg-header);
|
||||||
|
border-bottom: 1px solid var(--es-border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc :not(pre) > code {
|
||||||
|
background: var(--es-bg-input);
|
||||||
|
color: var(--es-primary);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 2px;
|
||||||
|
font-size: var(--es-font-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc table {
|
||||||
|
display: table;
|
||||||
|
width: 100%;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 16px 0;
|
||||||
|
font-size: var(--es-font-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc tr {
|
||||||
|
border-bottom: 1px solid var(--es-border-subtle);
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc tr:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc tr:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc th {
|
||||||
|
background: var(--es-bg-header);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: var(--es-font-xs);
|
||||||
|
color: var(--es-text-secondary);
|
||||||
|
text-align: left;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-bottom: 1px solid var(--es-border-subtle);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc td {
|
||||||
|
font-size: var(--es-font-sm);
|
||||||
|
color: var(--es-text-primary);
|
||||||
|
padding: 8px 12px;
|
||||||
|
vertical-align: top;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc td:first-child {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--es-text-primary);
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc .warning,
|
||||||
|
.vp-doc .custom-block.warning {
|
||||||
|
background: rgba(245, 158, 11, 0.08);
|
||||||
|
border: none;
|
||||||
|
border-left: 3px solid var(--es-warning);
|
||||||
|
border-radius: 0 2px 2px 0;
|
||||||
|
padding: 10px 12px;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc .warning .custom-block-title,
|
||||||
|
.vp-doc .custom-block.warning .custom-block-title {
|
||||||
|
color: var(--es-warning);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: var(--es-font-xs);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc .warning p {
|
||||||
|
color: var(--es-text-primary);
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--es-font-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc .tip,
|
||||||
|
.vp-doc .custom-block.tip {
|
||||||
|
background: rgba(59, 130, 246, 0.08);
|
||||||
|
border: none;
|
||||||
|
border-left: 3px solid var(--es-primary);
|
||||||
|
border-radius: 0 2px 2px 0;
|
||||||
|
padding: 10px 12px;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc .tip .custom-block-title,
|
||||||
|
.vp-doc .custom-block.tip .custom-block-title {
|
||||||
|
color: var(--es-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: var(--es-font-xs);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc .tip p {
|
||||||
|
color: var(--es-text-primary);
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--es-font-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc .info,
|
||||||
|
.vp-doc .custom-block.info {
|
||||||
|
background: rgba(74, 222, 128, 0.08);
|
||||||
|
border: none;
|
||||||
|
border-left: 3px solid var(--es-success);
|
||||||
|
border-radius: 0 2px 2px 0;
|
||||||
|
padding: 10px 12px;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc .info .custom-block-title,
|
||||||
|
.vp-doc .custom-block.info .custom-block-title {
|
||||||
|
color: var(--es-success);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: var(--es-font-xs);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc .danger,
|
||||||
|
.vp-doc .custom-block.danger {
|
||||||
|
background: rgba(239, 68, 68, 0.08);
|
||||||
|
border: none;
|
||||||
|
border-left: 3px solid var(--es-error);
|
||||||
|
border-radius: 0 2px 2px 0;
|
||||||
|
padding: 10px 12px;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc .danger .custom-block-title,
|
||||||
|
.vp-doc .custom-block.danger .custom-block-title {
|
||||||
|
color: var(--es-error);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: var(--es-font-xs);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc .card {
|
||||||
|
background: var(--es-bg-sidebar);
|
||||||
|
border: 1px solid var(--es-border-subtle);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 12px;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc .card-title {
|
||||||
|
font-size: var(--es-font-sm);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--es-text-primary);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc .card-description {
|
||||||
|
font-size: var(--es-font-xs);
|
||||||
|
color: var(--es-text-muted);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc .tag {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--es-border-default);
|
||||||
|
border-radius: 2px;
|
||||||
|
color: var(--es-text-secondary);
|
||||||
|
font-size: var(--es-font-xs);
|
||||||
|
margin-right: 4px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPFooter {
|
||||||
|
background: var(--es-bg-sidebar) !important;
|
||||||
|
border-top: 1px solid var(--es-border-subtle) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: var(--es-bg-card);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--es-border-strong);
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 2px solid var(--es-bg-card);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #5a5a5a;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-corner {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-container {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-section {
|
||||||
|
padding: 32px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.VPDoc .content {
|
||||||
|
padding: 16px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
14
docs/.vitepress/theme/index.js
Normal file
14
docs/.vitepress/theme/index.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import DefaultTheme from 'vitepress/theme'
|
||||||
|
import ParticleHero from './components/ParticleHero.vue'
|
||||||
|
import ParticleHeroEn from './components/ParticleHeroEn.vue'
|
||||||
|
import FeatureCard from './components/FeatureCard.vue'
|
||||||
|
import './custom.css'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
extends: DefaultTheme,
|
||||||
|
enhanceApp({ app }) {
|
||||||
|
app.component('ParticleHero', ParticleHero)
|
||||||
|
app.component('ParticleHeroEn', ParticleHeroEn)
|
||||||
|
app.component('FeatureCard', FeatureCard)
|
||||||
|
}
|
||||||
|
}
|
||||||
412
docs/en/guide/getting-started.md
Normal file
412
docs/en/guide/getting-started.md
Normal file
@@ -0,0 +1,412 @@
|
|||||||
|
# Quick Start
|
||||||
|
|
||||||
|
This guide will help you get started with ECS Framework, from installation to creating your first ECS application.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### NPM Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using npm
|
||||||
|
npm install @esengine/ecs-framework
|
||||||
|
```
|
||||||
|
|
||||||
|
## Initialize Core
|
||||||
|
|
||||||
|
### Basic Initialization
|
||||||
|
|
||||||
|
The core of ECS Framework is the `Core` class, a singleton that manages the entire framework lifecycle.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Core } from '@esengine/ecs-framework'
|
||||||
|
|
||||||
|
// Method 1: Using config object (recommended)
|
||||||
|
const core = Core.create({
|
||||||
|
debug: true, // Enable debug mode for detailed logs and performance monitoring
|
||||||
|
debugConfig: { // Optional: Advanced debug configuration
|
||||||
|
enabled: false, // Whether to enable WebSocket debug server
|
||||||
|
websocketUrl: 'ws://localhost:8080',
|
||||||
|
debugFrameRate: 30, // Debug data send frame rate
|
||||||
|
channels: {
|
||||||
|
entities: true,
|
||||||
|
systems: true,
|
||||||
|
performance: true,
|
||||||
|
components: true,
|
||||||
|
scenes: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Method 2: Simplified creation (backward compatible)
|
||||||
|
const core = Core.create(true); // Equivalent to { debug: true }
|
||||||
|
|
||||||
|
// Method 3: Production environment configuration
|
||||||
|
const core = Core.create({
|
||||||
|
debug: false // Disable debug in production
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Core Configuration Details
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ICoreConfig {
|
||||||
|
/** Enable debug mode - affects log level and performance monitoring */
|
||||||
|
debug?: boolean;
|
||||||
|
|
||||||
|
/** Advanced debug configuration - for dev tools integration */
|
||||||
|
debugConfig?: {
|
||||||
|
enabled: boolean; // Enable debug server
|
||||||
|
websocketUrl: string; // WebSocket server URL
|
||||||
|
autoReconnect?: boolean; // Auto reconnect
|
||||||
|
debugFrameRate?: 60 | 30 | 15; // Debug data send frame rate
|
||||||
|
channels: { // Data channel configuration
|
||||||
|
entities: boolean; // Entity data
|
||||||
|
systems: boolean; // System data
|
||||||
|
performance: boolean; // Performance data
|
||||||
|
components: boolean; // Component data
|
||||||
|
scenes: boolean; // Scene data
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Core Instance Management
|
||||||
|
|
||||||
|
Core uses singleton pattern, accessible via static property after creation:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Create instance
|
||||||
|
const core = Core.create(true);
|
||||||
|
|
||||||
|
// Get created instance
|
||||||
|
const instance = Core.Instance; // Returns current instance, null if not created
|
||||||
|
```
|
||||||
|
|
||||||
|
### Game Loop Integration
|
||||||
|
|
||||||
|
**Important**: Before creating entities and systems, you need to understand how to integrate ECS Framework into your game engine.
|
||||||
|
|
||||||
|
`Core.update(deltaTime)` is the framework heartbeat, must be called every frame. It handles:
|
||||||
|
- Updating the built-in Time class
|
||||||
|
- Updating all global managers (timers, object pools, etc.)
|
||||||
|
- Updating all entity systems in all scenes
|
||||||
|
- Processing entity creation and destruction
|
||||||
|
- Collecting performance data (in debug mode)
|
||||||
|
|
||||||
|
See engine integration examples: [Game Engine Integration](#game-engine-integration)
|
||||||
|
|
||||||
|
## Create Your First ECS Application
|
||||||
|
|
||||||
|
### 1. Define Components
|
||||||
|
|
||||||
|
Components are pure data containers that store entity state:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Component, ECSComponent } from '@esengine/ecs-framework'
|
||||||
|
|
||||||
|
// Position component
|
||||||
|
@ECSComponent('Position')
|
||||||
|
class Position extends Component {
|
||||||
|
x: number = 0
|
||||||
|
y: number = 0
|
||||||
|
|
||||||
|
constructor(x: number = 0, y: number = 0) {
|
||||||
|
super()
|
||||||
|
this.x = x
|
||||||
|
this.y = y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Velocity component
|
||||||
|
@ECSComponent('Velocity')
|
||||||
|
class Velocity extends Component {
|
||||||
|
dx: number = 0
|
||||||
|
dy: number = 0
|
||||||
|
|
||||||
|
constructor(dx: number = 0, dy: number = 0) {
|
||||||
|
super()
|
||||||
|
this.dx = dx
|
||||||
|
this.dy = dy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sprite component
|
||||||
|
@ECSComponent('Sprite')
|
||||||
|
class Sprite extends Component {
|
||||||
|
texture: string = ''
|
||||||
|
width: number = 32
|
||||||
|
height: number = 32
|
||||||
|
|
||||||
|
constructor(texture: string, width: number = 32, height: number = 32) {
|
||||||
|
super()
|
||||||
|
this.texture = texture
|
||||||
|
this.width = width
|
||||||
|
this.height = height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Create Entity Systems
|
||||||
|
|
||||||
|
Systems contain game logic and process entities with specific components. ECS Framework provides Matcher-based entity filtering:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { EntitySystem, Matcher, Time, ECSSystem } from '@esengine/ecs-framework'
|
||||||
|
|
||||||
|
// Movement system - handles position and velocity
|
||||||
|
@ECSSystem('MovementSystem')
|
||||||
|
class MovementSystem extends EntitySystem {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// Use Matcher to define target entities: must have both Position and Velocity
|
||||||
|
super(Matcher.empty().all(Position, Velocity))
|
||||||
|
}
|
||||||
|
|
||||||
|
protected process(entities: readonly Entity[]): void {
|
||||||
|
// process method receives all matching entities
|
||||||
|
for (const entity of entities) {
|
||||||
|
const position = entity.getComponent(Position)!
|
||||||
|
const velocity = entity.getComponent(Velocity)!
|
||||||
|
|
||||||
|
// Update position (using framework's Time class)
|
||||||
|
position.x += velocity.dx * Time.deltaTime
|
||||||
|
position.y += velocity.dy * Time.deltaTime
|
||||||
|
|
||||||
|
// Boundary check example
|
||||||
|
if (position.x < 0) position.x = 0
|
||||||
|
if (position.y < 0) position.y = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render system - handles visible objects
|
||||||
|
@ECSSystem('RenderSystem')
|
||||||
|
class RenderSystem extends EntitySystem {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// Must have Position and Sprite, optional Velocity (for direction)
|
||||||
|
super(Matcher.empty().all(Position, Sprite).any(Velocity))
|
||||||
|
}
|
||||||
|
|
||||||
|
protected process(entities: readonly Entity[]): void {
|
||||||
|
for (const entity of entities) {
|
||||||
|
const position = entity.getComponent(Position)!
|
||||||
|
const sprite = entity.getComponent(Sprite)!
|
||||||
|
const velocity = entity.getComponent(Velocity) // May be null
|
||||||
|
|
||||||
|
// Flip sprite based on velocity direction (optional logic)
|
||||||
|
let flipX = false
|
||||||
|
if (velocity && velocity.dx < 0) {
|
||||||
|
flipX = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render logic (pseudocode here)
|
||||||
|
this.drawSprite(sprite.texture, position.x, position.y, sprite.width, sprite.height, flipX)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawSprite(texture: string, x: number, y: number, width: number, height: number, flipX: boolean = false) {
|
||||||
|
// Actual render implementation depends on your game engine
|
||||||
|
const direction = flipX ? '<-' : '->'
|
||||||
|
console.log(`Render ${texture} at (${x.toFixed(1)}, ${y.toFixed(1)}) direction: ${direction}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### 3. Create Scene
|
||||||
|
|
||||||
|
Recommended to extend Scene class for custom scenes:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Scene } from '@esengine/ecs-framework'
|
||||||
|
|
||||||
|
// Recommended: Extend Scene for custom scene
|
||||||
|
class GameScene extends Scene {
|
||||||
|
|
||||||
|
initialize(): void {
|
||||||
|
// Scene initialization logic
|
||||||
|
this.name = "MainScene";
|
||||||
|
|
||||||
|
// Add systems to scene
|
||||||
|
this.addSystem(new MovementSystem());
|
||||||
|
this.addSystem(new RenderSystem());
|
||||||
|
}
|
||||||
|
|
||||||
|
onStart(): void {
|
||||||
|
// Logic when scene starts running
|
||||||
|
console.log("Game scene started");
|
||||||
|
}
|
||||||
|
|
||||||
|
unload(): void {
|
||||||
|
// Cleanup logic when scene unloads
|
||||||
|
console.log("Game scene unloaded");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and set scene
|
||||||
|
const gameScene = new GameScene();
|
||||||
|
Core.setScene(gameScene);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Create Entities
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Create player entity
|
||||||
|
const player = gameScene.createEntity("Player");
|
||||||
|
player.addComponent(new Position(100, 100));
|
||||||
|
player.addComponent(new Velocity(50, 30)); // Move 50px/sec (x), 30px/sec (y)
|
||||||
|
player.addComponent(new Sprite("player.png", 64, 64));
|
||||||
|
```
|
||||||
|
|
||||||
|
## Scene Management
|
||||||
|
|
||||||
|
Core has built-in scene management, very simple to use:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Core, Scene } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
// Initialize Core
|
||||||
|
Core.create({ debug: true });
|
||||||
|
|
||||||
|
// Create and set scene
|
||||||
|
class GameScene extends Scene {
|
||||||
|
initialize(): void {
|
||||||
|
this.name = "GamePlay";
|
||||||
|
this.addSystem(new MovementSystem());
|
||||||
|
this.addSystem(new RenderSystem());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const gameScene = new GameScene();
|
||||||
|
Core.setScene(gameScene);
|
||||||
|
|
||||||
|
// Game loop (auto-updates scene)
|
||||||
|
function gameLoop(deltaTime: number) {
|
||||||
|
Core.update(deltaTime); // Auto-updates global services and scene
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch scenes
|
||||||
|
Core.loadScene(new MenuScene()); // Delayed switch (next frame)
|
||||||
|
Core.setScene(new GameScene()); // Immediate switch
|
||||||
|
|
||||||
|
// Access current scene
|
||||||
|
const currentScene = Core.scene;
|
||||||
|
|
||||||
|
// Using fluent API
|
||||||
|
const player = Core.ecsAPI?.createEntity('Player')
|
||||||
|
.addComponent(Position, 100, 100)
|
||||||
|
.addComponent(Velocity, 50, 0);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Advanced: Using WorldManager for Multi-World
|
||||||
|
|
||||||
|
Only for complex server-side applications (MMO game servers, game room systems, etc.):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Core, WorldManager } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
// Initialize Core
|
||||||
|
Core.create({ debug: true });
|
||||||
|
|
||||||
|
// Get WorldManager from service container (Core auto-creates and registers it)
|
||||||
|
const worldManager = Core.services.resolve(WorldManager);
|
||||||
|
|
||||||
|
// Create multiple independent game worlds
|
||||||
|
const room1 = worldManager.createWorld('room_001');
|
||||||
|
const room2 = worldManager.createWorld('room_002');
|
||||||
|
|
||||||
|
// Create scenes in each world
|
||||||
|
const gameScene1 = room1.createScene('game', new GameScene());
|
||||||
|
const gameScene2 = room2.createScene('game', new GameScene());
|
||||||
|
|
||||||
|
// Activate scenes
|
||||||
|
room1.setSceneActive('game', true);
|
||||||
|
room2.setSceneActive('game', true);
|
||||||
|
|
||||||
|
// Game loop (need to manually update worlds)
|
||||||
|
function gameLoop(deltaTime: number) {
|
||||||
|
Core.update(deltaTime); // Update global services
|
||||||
|
worldManager.updateAll(); // Manually update all worlds
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Game Engine Integration
|
||||||
|
|
||||||
|
### Laya Engine Integration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Stage } from "laya/display/Stage";
|
||||||
|
import { Laya } from "Laya";
|
||||||
|
import { Core } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
// Initialize Laya
|
||||||
|
Laya.init(800, 600).then(() => {
|
||||||
|
// Initialize ECS
|
||||||
|
Core.create(true);
|
||||||
|
Core.setScene(new GameScene());
|
||||||
|
|
||||||
|
// Start game loop
|
||||||
|
Laya.timer.frameLoop(1, this, () => {
|
||||||
|
const deltaTime = Laya.timer.delta / 1000;
|
||||||
|
Core.update(deltaTime); // Auto-updates global services and scene
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cocos Creator Integration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Component, _decorator } from 'cc';
|
||||||
|
import { Core } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
const { ccclass } = _decorator;
|
||||||
|
|
||||||
|
@ccclass('ECSGameManager')
|
||||||
|
export class ECSGameManager extends Component {
|
||||||
|
onLoad() {
|
||||||
|
// Initialize ECS
|
||||||
|
Core.create(true);
|
||||||
|
Core.setScene(new GameScene());
|
||||||
|
}
|
||||||
|
|
||||||
|
update(deltaTime: number) {
|
||||||
|
// Auto-updates global services and scene
|
||||||
|
Core.update(deltaTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
onDestroy() {
|
||||||
|
// Cleanup resources
|
||||||
|
Core.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
You've successfully created your first ECS application! Next you can:
|
||||||
|
|
||||||
|
- Check the complete [API Documentation](/api/README)
|
||||||
|
- Explore more [practical examples](/examples/)
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
### Why isn't my system executing?
|
||||||
|
|
||||||
|
Ensure:
|
||||||
|
1. System is added to scene: `this.addSystem(system)` (in Scene's initialize method)
|
||||||
|
2. Scene is set: `Core.setScene(scene)`
|
||||||
|
3. Game loop is calling: `Core.update(deltaTime)`
|
||||||
|
|
||||||
|
### How to debug ECS applications?
|
||||||
|
|
||||||
|
Enable debug mode:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
Core.create({ debug: true })
|
||||||
|
|
||||||
|
// Get debug data
|
||||||
|
const debugData = Core.getDebugData()
|
||||||
|
console.log(debugData)
|
||||||
|
```
|
||||||
43
docs/en/guide/index.md
Normal file
43
docs/en/guide/index.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Guide
|
||||||
|
|
||||||
|
Welcome to the ECS Framework Guide. This guide covers the core concepts and usage of the framework.
|
||||||
|
|
||||||
|
## Core Concepts
|
||||||
|
|
||||||
|
### [Entity](/guide/entity)
|
||||||
|
Learn the basics of ECS architecture - how to use entities, lifecycle management, and best practices.
|
||||||
|
|
||||||
|
### [Component](/guide/component)
|
||||||
|
Learn how to create and use components for modular game feature design.
|
||||||
|
|
||||||
|
### [System](/guide/system)
|
||||||
|
Master system development to implement game logic processing.
|
||||||
|
|
||||||
|
### [Entity Query & Matcher](/guide/entity-query)
|
||||||
|
Learn to use Matcher for entity filtering and queries with `all`, `any`, `none`, `nothing` conditions.
|
||||||
|
|
||||||
|
### [Scene](/guide/scene)
|
||||||
|
Understand scene lifecycle, system management, and entity container features.
|
||||||
|
|
||||||
|
### [Event System](/guide/event-system)
|
||||||
|
Master the type-safe event system for component communication and system coordination.
|
||||||
|
|
||||||
|
### [Serialization](/guide/serialization)
|
||||||
|
Master serialization for scenes, entities, and components. Supports full and incremental serialization for game saves, network sync, and more.
|
||||||
|
|
||||||
|
### [Time and Timers](/guide/time-and-timers)
|
||||||
|
Learn time management and timer systems for precise game logic timing control.
|
||||||
|
|
||||||
|
### [Logging](/guide/logging)
|
||||||
|
Master the leveled logging system for debugging, monitoring, and error tracking.
|
||||||
|
|
||||||
|
### [Platform Adapter](/guide/platform-adapter)
|
||||||
|
Learn how to implement and register platform adapters for browsers, mini-games, Node.js, and more.
|
||||||
|
|
||||||
|
## Advanced Features
|
||||||
|
|
||||||
|
### [Service Container](/guide/service-container)
|
||||||
|
Master dependency injection and service management for loosely-coupled architecture.
|
||||||
|
|
||||||
|
### [Plugin System](/guide/plugin-system)
|
||||||
|
Learn how to develop and use plugins to extend framework functionality.
|
||||||
317
docs/en/index.md
Normal file
317
docs/en/index.md
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
---
|
||||||
|
layout: page
|
||||||
|
title: ESEngine - High-performance TypeScript ECS Framework
|
||||||
|
---
|
||||||
|
|
||||||
|
<ParticleHeroEn />
|
||||||
|
|
||||||
|
<section class="news-section">
|
||||||
|
<div class="news-container">
|
||||||
|
<div class="news-header">
|
||||||
|
<h2 class="news-title">Quick Links</h2>
|
||||||
|
<a href="/en/guide/" class="news-more">View Docs</a>
|
||||||
|
</div>
|
||||||
|
<div class="news-grid">
|
||||||
|
<a href="/en/guide/getting-started" class="news-card">
|
||||||
|
<div class="news-card-image" style="background: linear-gradient(135deg, #1e3a5f 0%, #1e1e1e 100%);">
|
||||||
|
<div class="news-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24"><path fill="#4fc1ff" d="M12 3L1 9l4 2.18v6L12 21l7-3.82v-6l2-1.09V17h2V9zm6.82 6L12 12.72L5.18 9L12 5.28zM17 16l-5 2.72L7 16v-3.73L12 15l5-2.73z"/></svg>
|
||||||
|
</div>
|
||||||
|
<span class="news-badge">Quick Start</span>
|
||||||
|
</div>
|
||||||
|
<div class="news-card-content">
|
||||||
|
<h3>Get Started in 5 Minutes</h3>
|
||||||
|
<p>From installation to your first ECS app, learn the core concepts quickly.</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a href="/en/guide/behavior-tree/" class="news-card">
|
||||||
|
<div class="news-card-image" style="background: linear-gradient(135deg, #1e3a5f 0%, #1e1e1e 100%);">
|
||||||
|
<div class="news-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24"><path fill="#4ec9b0" d="M12 2a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m3 20h-1v-7l-2-2l-2 2v7H9v-7.5l-2 2V22H6v-6l3-3l1-3.5c-.3.4-.6.7-1 1L6 9v1H4V8l5-3c.5-.3 1.1-.5 1.7-.5H11c.6 0 1.2.2 1.7.5l5 3v2h-2V9l-3 1.5c-.4-.3-.7-.6-1-1l1 3.5l3 3v6Z"/></svg>
|
||||||
|
</div>
|
||||||
|
<span class="news-badge">AI System</span>
|
||||||
|
</div>
|
||||||
|
<div class="news-card-content">
|
||||||
|
<h3>Visual Behavior Tree Editor</h3>
|
||||||
|
<p>Built-in AI behavior tree system with visual editing and real-time debugging.</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="features-section">
|
||||||
|
<div class="features-container">
|
||||||
|
<h2 class="features-title">Core Features</h2>
|
||||||
|
<div class="features-grid">
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#4fc1ff" d="M13 2.05v2.02c3.95.49 7 3.85 7 7.93c0 1.45-.39 2.79-1.06 3.95l1.59 1.09A9.94 9.94 0 0 0 22 12c0-5.18-3.95-9.45-9-9.95M12 19c-3.87 0-7-3.13-7-7c0-3.53 2.61-6.43 6-6.92V2.05c-5.06.5-9 4.76-9 9.95c0 5.52 4.47 10 9.99 10c3.31 0 6.24-1.61 8.06-4.09l-1.6-1.1A7.93 7.93 0 0 1 12 19"/><path fill="#4fc1ff" d="M12 6a6 6 0 0 0-6 6c0 3.31 2.69 6 6 6a6 6 0 0 0 0-12m0 10c-2.21 0-4-1.79-4-4s1.79-4 4-4s4 1.79 4 4s-1.79 4-4 4"/></svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="feature-title">High-performance ECS Architecture</h3>
|
||||||
|
<p class="feature-desc">Data-driven entity component system for large-scale entity processing with cache-friendly memory layout.</p>
|
||||||
|
<a href="/en/guide/entity" class="feature-link">Learn more</a>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#569cd6" d="M3 3h18v18H3zm16.525 13.707c0-.795-.272-1.425-.816-1.89c-.544-.465-1.404-.804-2.58-1.016l-1.704-.296c-.616-.104-1.052-.26-1.308-.468c-.256-.21-.384-.468-.384-.776c0-.392.168-.7.504-.924c.336-.224.8-.336 1.392-.336c.56 0 1.008.124 1.344.372c.336.248.536.584.6 1.008h2.016c-.08-.96-.464-1.716-1.152-2.268c-.688-.552-1.6-.828-2.736-.828c-1.2 0-2.148.3-2.844.9c-.696.6-1.044 1.38-1.044 2.34c0 .76.252 1.368.756 1.824c.504.456 1.308.792 2.412.996l1.704.312c.624.12 1.068.28 1.332.48c.264.2.396.46.396.78c0 .424-.192.756-.576.996c-.384.24-.9.36-1.548.36c-.672 0-1.2-.14-1.584-.42c-.384-.28-.608-.668-.672-1.164H8.868c.048 1.016.46 1.808 1.236 2.376c.776.568 1.796.852 3.06.852c1.24 0 2.22-.292 2.94-.876c.72-.584 1.08-1.364 1.08-2.34z"/></svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="feature-title">Full Type Support</h3>
|
||||||
|
<p class="feature-desc">100% TypeScript with complete type definitions and compile-time checking for the best development experience.</p>
|
||||||
|
<a href="/en/guide/component" class="feature-link">Learn more</a>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#4ec9b0" d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10s10-4.5 10-10S17.5 2 12 2m0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8s8 3.59 8 8s-3.59 8-8 8m-5-8l4-4v3h4v2h-4v3z"/></svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="feature-title">Visual Behavior Tree</h3>
|
||||||
|
<p class="feature-desc">Built-in AI behavior tree system with visual editor, custom nodes, and real-time debugging.</p>
|
||||||
|
<a href="/en/guide/behavior-tree/" class="feature-link">Learn more</a>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#c586c0" d="M4 6h18V4H4c-1.1 0-2 .9-2 2v11H0v3h14v-3H4zm19 2h-6c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h6c.55 0 1-.45 1-1V9c0-.55-.45-1-1-1m-1 9h-4v-7h4z"/></svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="feature-title">Multi-Platform Support</h3>
|
||||||
|
<p class="feature-desc">Support for browsers, Node.js, WeChat Mini Games, and seamless integration with major game engines.</p>
|
||||||
|
<a href="/en/guide/platform-adapter" class="feature-link">Learn more</a>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#dcdcaa" d="M4 3h6v2H4v14h6v2H4c-1.1 0-2-.9-2-2V5c0-1.1.9-2 2-2m9 0h6c1.1 0 2 .9 2 2v14c0 1.1-.9 2-2 2h-6v-2h6V5h-6zm-1 7h4v2h-4z"/></svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="feature-title">Modular Design</h3>
|
||||||
|
<p class="feature-desc">Core features packaged independently, import only what you need. Support for custom plugin extensions.</p>
|
||||||
|
<a href="/en/guide/plugin-system" class="feature-link">Learn more</a>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#9cdcfe" d="M22.7 19l-9.1-9.1c.9-2.3.4-5-1.5-6.9c-2-2-5-2.4-7.4-1.3L9 6L6 9L1.6 4.7C.4 7.1.9 10.1 2.9 12.1c1.9 1.9 4.6 2.4 6.9 1.5l9.1 9.1c.4.4 1 .4 1.4 0l2.3-2.3c.5-.4.5-1.1.1-1.4"/></svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="feature-title">Developer Tools</h3>
|
||||||
|
<p class="feature-desc">Built-in performance monitoring, debugging tools, serialization system, and complete development toolchain.</p>
|
||||||
|
<a href="/en/guide/logging" class="feature-link">Learn more</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Home page specific styles */
|
||||||
|
.news-section {
|
||||||
|
background: #0d0d0d;
|
||||||
|
padding: 64px 0;
|
||||||
|
border-top: 1px solid #2a2a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-container {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ffffff;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-more {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 1px solid #2a2a2a;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #a0a0a0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-more:hover {
|
||||||
|
background: #252525;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card {
|
||||||
|
display: flex;
|
||||||
|
background: #1f1f1f;
|
||||||
|
border: 1px solid #2a2a2a;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card:hover {
|
||||||
|
border-color: #3b9eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card-image {
|
||||||
|
width: 200px;
|
||||||
|
min-height: 140px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 16px;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-icon {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 12px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #3a3a3a;
|
||||||
|
border-radius: 16px;
|
||||||
|
color: #a0a0a0;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card-content {
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card-content h3 {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card-content p {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #707070;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-section {
|
||||||
|
background: #0d0d0d;
|
||||||
|
padding: 64px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-container {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ffffff;
|
||||||
|
margin: 0 0 32px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card {
|
||||||
|
background: #1f1f1f;
|
||||||
|
border: 1px solid #2a2a2a;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card:hover {
|
||||||
|
border-color: #3b9eff;
|
||||||
|
background: #252525;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
background: #0d0d0d;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-desc {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #707070;
|
||||||
|
line-height: 1.7;
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-link {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #3b9eff;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.news-container,
|
||||||
|
.features-container {
|
||||||
|
padding: 0 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.news-card {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card-image {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
392
docs/guide/behavior-tree/advanced-usage.md
Normal file
392
docs/guide/behavior-tree/advanced-usage.md
Normal file
@@ -0,0 +1,392 @@
|
|||||||
|
# 高级用法
|
||||||
|
|
||||||
|
本文介绍行为树系统的高级功能和使用技巧。
|
||||||
|
|
||||||
|
## 全局黑板
|
||||||
|
|
||||||
|
全局黑板在所有行为树实例之间共享数据。
|
||||||
|
|
||||||
|
### 使用全局黑板
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { GlobalBlackboardService } from '@esengine/behavior-tree';
|
||||||
|
import { Core } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
// 获取全局黑板服务
|
||||||
|
const globalBlackboard = Core.services.resolve(GlobalBlackboardService);
|
||||||
|
|
||||||
|
// 设置全局变量
|
||||||
|
globalBlackboard.setValue('gameState', 'playing');
|
||||||
|
globalBlackboard.setValue('playerCount', 4);
|
||||||
|
globalBlackboard.setValue('difficulty', 'hard');
|
||||||
|
|
||||||
|
// 读取全局变量
|
||||||
|
const gameState = globalBlackboard.getValue('gameState');
|
||||||
|
const playerCount = globalBlackboard.getValue<number>('playerCount');
|
||||||
|
```
|
||||||
|
|
||||||
|
### 在自定义执行器中访问全局黑板
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '@esengine/behavior-tree';
|
||||||
|
import { GlobalBlackboardService } from '@esengine/behavior-tree';
|
||||||
|
import { Core } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
export class CheckGameState implements INodeExecutor {
|
||||||
|
execute(context: NodeExecutionContext): TaskStatus {
|
||||||
|
const globalBlackboard = Core.services.resolve(GlobalBlackboardService);
|
||||||
|
const gameState = globalBlackboard.getValue('gameState');
|
||||||
|
|
||||||
|
if (gameState === 'paused') {
|
||||||
|
return TaskStatus.Failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TaskStatus.Success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 性能优化
|
||||||
|
|
||||||
|
### 1. 降低更新频率
|
||||||
|
|
||||||
|
对于不需要每帧更新的AI,可以使用冷却装饰器:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 每0.1秒执行一次
|
||||||
|
const ai = BehaviorTreeBuilder.create('ThrottledAI')
|
||||||
|
.cooldown(0.1, 'ThrottleRoot')
|
||||||
|
.selector('MainLogic')
|
||||||
|
// AI逻辑...
|
||||||
|
.end()
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 条件缓存
|
||||||
|
|
||||||
|
在自定义执行器中缓存昂贵的条件检查结果:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export class CachedCheck implements INodeExecutor {
|
||||||
|
execute(context: NodeExecutionContext): TaskStatus {
|
||||||
|
const { state, runtime, totalTime } = context;
|
||||||
|
const cacheTime = state.lastCheckTime || 0;
|
||||||
|
|
||||||
|
// 如果缓存未过期(1秒内),直接使用缓存结果
|
||||||
|
if (totalTime - cacheTime < 1.0) {
|
||||||
|
return state.cachedResult || TaskStatus.Failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行昂贵的检查
|
||||||
|
const result = performExpensiveCheck();
|
||||||
|
const status = result ? TaskStatus.Success : TaskStatus.Failure;
|
||||||
|
|
||||||
|
// 缓存结果
|
||||||
|
state.cachedResult = status;
|
||||||
|
state.lastCheckTime = totalTime;
|
||||||
|
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
reset(context: NodeExecutionContext): void {
|
||||||
|
context.state.cachedResult = undefined;
|
||||||
|
context.state.lastCheckTime = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 分帧执行
|
||||||
|
|
||||||
|
将大量计算分散到多帧:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export class ProcessLargeDataset implements INodeExecutor {
|
||||||
|
execute(context: NodeExecutionContext): TaskStatus {
|
||||||
|
const { state, runtime } = context;
|
||||||
|
|
||||||
|
const data = runtime.getBlackboardValue<any[]>('dataset') || [];
|
||||||
|
let processedIndex = state.processedIndex || 0;
|
||||||
|
|
||||||
|
const batchSize = 100; // 每帧处理100个
|
||||||
|
const endIndex = Math.min(processedIndex + batchSize, data.length);
|
||||||
|
|
||||||
|
for (let i = processedIndex; i < endIndex; i++) {
|
||||||
|
processItem(data[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.processedIndex = endIndex;
|
||||||
|
|
||||||
|
if (endIndex >= data.length) {
|
||||||
|
return TaskStatus.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TaskStatus.Running;
|
||||||
|
}
|
||||||
|
|
||||||
|
reset(context: NodeExecutionContext): void {
|
||||||
|
context.state.processedIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 调试技巧
|
||||||
|
|
||||||
|
### 1. 使用日志节点
|
||||||
|
|
||||||
|
在关键位置添加日志:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const tree = BehaviorTreeBuilder.create('Debug')
|
||||||
|
.log('开始战斗序列', 'StartCombat')
|
||||||
|
.sequence('Combat')
|
||||||
|
.log('检查生命值', 'CheckHealth')
|
||||||
|
.blackboardCompare('health', 0, 'greater')
|
||||||
|
.log('执行攻击', 'Attack')
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 监控黑板状态
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||||
|
|
||||||
|
// 输出所有黑板变量
|
||||||
|
console.log('黑板变量:', runtime?.getAllBlackboardVariables());
|
||||||
|
|
||||||
|
// 输出活动节点
|
||||||
|
console.log('活动节点:', Array.from(runtime?.activeNodeIds || []));
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 在自定义执行器中调试
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export class DebugAction implements INodeExecutor {
|
||||||
|
execute(context: NodeExecutionContext): TaskStatus {
|
||||||
|
const { nodeData, runtime, state } = context;
|
||||||
|
|
||||||
|
console.log(`[${nodeData.name}] 开始执行`);
|
||||||
|
console.log('配置:', nodeData.config);
|
||||||
|
console.log('状态:', state);
|
||||||
|
console.log('黑板:', runtime.getAllBlackboardVariables());
|
||||||
|
|
||||||
|
// 执行逻辑...
|
||||||
|
|
||||||
|
return TaskStatus.Success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 性能分析
|
||||||
|
|
||||||
|
测量节点执行时间:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export class ProfiledAction implements INodeExecutor {
|
||||||
|
execute(context: NodeExecutionContext): TaskStatus {
|
||||||
|
const startTime = performance.now();
|
||||||
|
|
||||||
|
// 执行操作
|
||||||
|
doSomething();
|
||||||
|
|
||||||
|
const elapsed = performance.now() - startTime;
|
||||||
|
console.log(`[${context.nodeData.name}] 耗时: ${elapsed.toFixed(2)}ms`);
|
||||||
|
|
||||||
|
return TaskStatus.Success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见模式
|
||||||
|
|
||||||
|
### 1. 状态机模式
|
||||||
|
|
||||||
|
使用行为树实现状态机:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const fsm = BehaviorTreeBuilder.create('StateMachine')
|
||||||
|
.defineBlackboardVariable('currentState', 'idle')
|
||||||
|
.selector('StateSwitch')
|
||||||
|
// Idle状态
|
||||||
|
.sequence('IdleState')
|
||||||
|
.blackboardCompare('currentState', 'idle', 'equals')
|
||||||
|
.log('执行Idle行为', 'IdleBehavior')
|
||||||
|
.end()
|
||||||
|
// Move状态
|
||||||
|
.sequence('MoveState')
|
||||||
|
.blackboardCompare('currentState', 'move', 'equals')
|
||||||
|
.log('执行Move行为', 'MoveBehavior')
|
||||||
|
.end()
|
||||||
|
// Attack状态
|
||||||
|
.sequence('AttackState')
|
||||||
|
.blackboardCompare('currentState', 'attack', 'equals')
|
||||||
|
.log('执行Attack行为', 'AttackBehavior')
|
||||||
|
.end()
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
状态转换通过修改黑板变量实现:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||||
|
runtime?.setBlackboardValue('currentState', 'move');
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 优先级队列模式
|
||||||
|
|
||||||
|
按优先级执行任务:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const tree = BehaviorTreeBuilder.create('PriorityQueue')
|
||||||
|
.selector('Priorities')
|
||||||
|
// 最高优先级:生存
|
||||||
|
.sequence('Survive')
|
||||||
|
.blackboardCompare('health', 20, 'less')
|
||||||
|
.log('治疗', 'Heal')
|
||||||
|
.end()
|
||||||
|
// 中优先级:战斗
|
||||||
|
.sequence('Combat')
|
||||||
|
.blackboardExists('nearbyEnemy')
|
||||||
|
.log('战斗', 'Fight')
|
||||||
|
.end()
|
||||||
|
// 低优先级:收集资源
|
||||||
|
.sequence('Gather')
|
||||||
|
.log('收集资源', 'CollectResources')
|
||||||
|
.end()
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 并行任务模式
|
||||||
|
|
||||||
|
同时执行多个任务:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const tree = BehaviorTreeBuilder.create('ParallelTasks')
|
||||||
|
.parallel('Effects', { successPolicy: 'all' })
|
||||||
|
.log('播放动画', 'PlayAnimation')
|
||||||
|
.log('播放音效', 'PlaySound')
|
||||||
|
.log('生成粒子', 'SpawnParticles')
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 重试模式
|
||||||
|
|
||||||
|
失败时重试:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 使用自定义重试装饰器(参见custom-actions.md中的RetryDecorator示例)
|
||||||
|
// 或者使用UntilSuccess装饰器
|
||||||
|
const tree = BehaviorTreeBuilder.create('Retry')
|
||||||
|
.untilSuccess('RetryUntilSuccess')
|
||||||
|
.log('尝试操作', 'TryOperation')
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 超时模式
|
||||||
|
|
||||||
|
限制任务执行时间:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const tree = BehaviorTreeBuilder.create('Timeout')
|
||||||
|
.timeout(5.0, 'TimeLimit')
|
||||||
|
.log('长时间运行的任务', 'LongTask')
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
## 与游戏引擎集成
|
||||||
|
|
||||||
|
### Cocos Creator集成
|
||||||
|
|
||||||
|
参见[Cocos Creator集成指南](./cocos-integration.md)
|
||||||
|
|
||||||
|
### LayaAir集成
|
||||||
|
|
||||||
|
参见[LayaAir集成指南](./laya-integration.md)
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
### 1. 合理使用黑板
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 好的做法:使用类型化的黑板访问
|
||||||
|
const health = runtime.getBlackboardValue<number>('health');
|
||||||
|
|
||||||
|
// 好的做法:定义所有黑板变量
|
||||||
|
const tree = BehaviorTreeBuilder.create('AI')
|
||||||
|
.defineBlackboardVariable('health', 100)
|
||||||
|
.defineBlackboardVariable('target', null)
|
||||||
|
.defineBlackboardVariable('state', 'idle')
|
||||||
|
// ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 避免过深的树结构
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 不好:嵌套过深
|
||||||
|
.selector()
|
||||||
|
.sequence()
|
||||||
|
.selector()
|
||||||
|
.sequence()
|
||||||
|
.selector()
|
||||||
|
// 太深了!
|
||||||
|
.end()
|
||||||
|
.end()
|
||||||
|
.end()
|
||||||
|
.end()
|
||||||
|
.end()
|
||||||
|
|
||||||
|
// 好:使用合理的深度
|
||||||
|
.selector()
|
||||||
|
.sequence()
|
||||||
|
.log('Action1')
|
||||||
|
.log('Action2')
|
||||||
|
.end()
|
||||||
|
.sequence()
|
||||||
|
.log('Action3')
|
||||||
|
.log('Action4')
|
||||||
|
.end()
|
||||||
|
.end()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 使用有意义的节点名称
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 好的做法
|
||||||
|
.selector('CombatDecision')
|
||||||
|
.sequence('AttackEnemy')
|
||||||
|
.blackboardExists('target', 'HasTarget')
|
||||||
|
.log('执行攻击', 'Attack')
|
||||||
|
.end()
|
||||||
|
.end()
|
||||||
|
|
||||||
|
// 不好的做法
|
||||||
|
.selector('Node1')
|
||||||
|
.sequence('Node2')
|
||||||
|
.blackboardExists('target', 'Node3')
|
||||||
|
.log('Attack', 'Node4')
|
||||||
|
.end()
|
||||||
|
.end()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 模块化设计
|
||||||
|
|
||||||
|
将复杂逻辑分解为多个独立的行为树,在需要时组合使用。
|
||||||
|
|
||||||
|
### 5. 性能考虑
|
||||||
|
|
||||||
|
- 避免在每帧执行昂贵的操作
|
||||||
|
- 使用冷却装饰器控制执行频率
|
||||||
|
- 缓存计算结果
|
||||||
|
- 合理使用并行节点
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
- 查看[自定义节点执行器](./custom-actions.md)学习如何创建自定义节点
|
||||||
|
- 阅读[最佳实践](./best-practices.md)了解行为树设计技巧
|
||||||
|
- 参考[编辑器使用指南](./editor-guide.md)学习可视化编辑
|
||||||
506
docs/guide/behavior-tree/asset-management.md
Normal file
506
docs/guide/behavior-tree/asset-management.md
Normal file
@@ -0,0 +1,506 @@
|
|||||||
|
# 资产管理
|
||||||
|
|
||||||
|
本文介绍如何加载、管理和复用行为树资产。
|
||||||
|
|
||||||
|
## 为什么需要资产管理?
|
||||||
|
|
||||||
|
在实际游戏开发中,你可能会遇到以下场景:
|
||||||
|
|
||||||
|
1. **多个实体共享同一个行为树** - 100个敌人使用同一套AI逻辑
|
||||||
|
2. **动态加载行为树** - 从JSON文件加载行为树配置
|
||||||
|
3. **子树复用** - 将常用的行为片段(如"巡逻"、"追击")做成独立的子树
|
||||||
|
4. **运行时切换行为树** - 敌人在不同阶段使用不同的行为树
|
||||||
|
|
||||||
|
## BehaviorTreeAssetManager
|
||||||
|
|
||||||
|
框架提供了 `BehaviorTreeAssetManager` 服务来统一管理行为树资产。
|
||||||
|
|
||||||
|
### 核心概念
|
||||||
|
|
||||||
|
- **BehaviorTreeData(行为树数据)**:行为树的定义,可以被多个实体共享
|
||||||
|
- **BehaviorTreeRuntimeComponent(运行时组件)**:每个实体独立的运行时状态
|
||||||
|
- **AssetManager(资产管理器)**:统一管理所有 BehaviorTreeData
|
||||||
|
|
||||||
|
### 基本使用
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Core } from '@esengine/ecs-framework';
|
||||||
|
import {
|
||||||
|
BehaviorTreeAssetManager,
|
||||||
|
BehaviorTreeBuilder,
|
||||||
|
BehaviorTreeStarter
|
||||||
|
} from '@esengine/behavior-tree';
|
||||||
|
|
||||||
|
// 1. 获取资产管理器(插件已自动注册)
|
||||||
|
const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||||
|
|
||||||
|
// 2. 创建并注册行为树资产
|
||||||
|
const enemyAI = BehaviorTreeBuilder.create('EnemyAI')
|
||||||
|
.defineBlackboardVariable('health', 100)
|
||||||
|
.selector('MainBehavior')
|
||||||
|
.log('攻击')
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assetManager.loadAsset(enemyAI);
|
||||||
|
|
||||||
|
// 3. 为多个实体使用同一份资产
|
||||||
|
const enemy1 = scene.createEntity('Enemy1');
|
||||||
|
const enemy2 = scene.createEntity('Enemy2');
|
||||||
|
const enemy3 = scene.createEntity('Enemy3');
|
||||||
|
|
||||||
|
// 获取共享的资产
|
||||||
|
const sharedTree = assetManager.getAsset('EnemyAI');
|
||||||
|
|
||||||
|
if (sharedTree) {
|
||||||
|
BehaviorTreeStarter.start(enemy1, sharedTree);
|
||||||
|
BehaviorTreeStarter.start(enemy2, sharedTree);
|
||||||
|
BehaviorTreeStarter.start(enemy3, sharedTree);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 资产管理器 API
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 加载资产
|
||||||
|
assetManager.loadAsset(treeData);
|
||||||
|
|
||||||
|
// 获取资产
|
||||||
|
const tree = assetManager.getAsset('TreeID');
|
||||||
|
|
||||||
|
// 检查资产是否存在
|
||||||
|
if (assetManager.hasAsset('TreeID')) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
// 卸载资产
|
||||||
|
assetManager.unloadAsset('TreeID');
|
||||||
|
|
||||||
|
// 获取所有资产ID
|
||||||
|
const allIds = assetManager.getAllAssetIds();
|
||||||
|
|
||||||
|
// 清空所有资产
|
||||||
|
assetManager.clearAll();
|
||||||
|
```
|
||||||
|
|
||||||
|
## 从文件加载行为树
|
||||||
|
|
||||||
|
### JSON 格式
|
||||||
|
|
||||||
|
行为树可以导出为 JSON 格式:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"metadata": {
|
||||||
|
"name": "EnemyAI",
|
||||||
|
"description": "敌人AI行为树"
|
||||||
|
},
|
||||||
|
"rootNodeId": "root-1",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "root-1",
|
||||||
|
"name": "RootSelector",
|
||||||
|
"nodeType": "Composite",
|
||||||
|
"data": {
|
||||||
|
"compositeType": "Selector"
|
||||||
|
},
|
||||||
|
"children": ["combat-1", "patrol-1"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "combat-1",
|
||||||
|
"name": "Combat",
|
||||||
|
"nodeType": "Action",
|
||||||
|
"data": {
|
||||||
|
"actionType": "LogAction",
|
||||||
|
"message": "攻击敌人"
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"blackboard": [
|
||||||
|
{
|
||||||
|
"name": "health",
|
||||||
|
"type": "number",
|
||||||
|
"defaultValue": 100
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 加载 JSON 文件
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
BehaviorTreeAssetSerializer,
|
||||||
|
BehaviorTreeAssetManager
|
||||||
|
} from '@esengine/behavior-tree';
|
||||||
|
|
||||||
|
async function loadTreeFromFile(filePath: string) {
|
||||||
|
const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||||
|
|
||||||
|
// 1. 读取文件内容
|
||||||
|
const jsonContent = await fetch(filePath).then(res => res.text());
|
||||||
|
|
||||||
|
// 2. 反序列化
|
||||||
|
const treeData = BehaviorTreeAssetSerializer.deserialize(jsonContent);
|
||||||
|
|
||||||
|
// 3. 加载到资产管理器
|
||||||
|
assetManager.loadAsset(treeData);
|
||||||
|
|
||||||
|
return treeData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用
|
||||||
|
const tree = await loadTreeFromFile('/assets/enemy-ai.btree.json');
|
||||||
|
BehaviorTreeStarter.start(entity, tree);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 子树(SubTree)
|
||||||
|
|
||||||
|
子树允许你将常用的行为片段做成独立的树,然后在其他树中引用。
|
||||||
|
|
||||||
|
### 为什么使用子树?
|
||||||
|
|
||||||
|
1. **代码复用** - 避免重复定义相同的行为
|
||||||
|
2. **模块化** - 将复杂的行为树拆分成小的可管理单元
|
||||||
|
3. **团队协作** - 不同成员可以独立开发不同的子树
|
||||||
|
|
||||||
|
### 创建子树
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 1. 创建巡逻子树
|
||||||
|
const patrolTree = BehaviorTreeBuilder.create('PatrolBehavior')
|
||||||
|
.sequence('Patrol')
|
||||||
|
.log('选择巡逻点', 'PickWaypoint')
|
||||||
|
.log('移动到巡逻点', 'MoveToWaypoint')
|
||||||
|
.wait(2.0, 'WaitAtWaypoint')
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 2. 创建追击子树
|
||||||
|
const chaseTree = BehaviorTreeBuilder.create('ChaseBehavior')
|
||||||
|
.sequence('Chase')
|
||||||
|
.log('锁定目标', 'LockTarget')
|
||||||
|
.log('追击目标', 'ChaseTarget')
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 3. 注册子树到资产管理器
|
||||||
|
const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||||
|
assetManager.loadAsset(patrolTree);
|
||||||
|
assetManager.loadAsset(chaseTree);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 使用子树
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 在主行为树中使用子树
|
||||||
|
const mainTree = BehaviorTreeBuilder.create('EnemyAI')
|
||||||
|
.defineBlackboardVariable('hasTarget', false)
|
||||||
|
|
||||||
|
.selector('MainBehavior')
|
||||||
|
// 条件:发现目标时执行追击子树
|
||||||
|
.sequence('CombatBranch')
|
||||||
|
.blackboardExists('hasTarget')
|
||||||
|
.subTree('ChaseBehavior', { shareBlackboard: true })
|
||||||
|
.end()
|
||||||
|
|
||||||
|
// 默认:执行巡逻子树
|
||||||
|
.subTree('PatrolBehavior', { shareBlackboard: true })
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assetManager.loadAsset(mainTree);
|
||||||
|
|
||||||
|
// 启动主行为树
|
||||||
|
const enemy = scene.createEntity('Enemy');
|
||||||
|
BehaviorTreeStarter.start(enemy, mainTree);
|
||||||
|
```
|
||||||
|
|
||||||
|
### SubTree 配置选项
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
.subTree('SubTreeID', {
|
||||||
|
shareBlackboard: true, // 是否共享黑板(默认true)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
- **shareBlackboard: true** - 子树和父树共享黑板变量
|
||||||
|
- **shareBlackboard: false** - 子树使用独立的黑板
|
||||||
|
|
||||||
|
## 资源预加载
|
||||||
|
|
||||||
|
在游戏启动时预加载所有行为树资产:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class BehaviorTreePreloader {
|
||||||
|
private assetManager: BehaviorTreeAssetManager;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
async preloadAll() {
|
||||||
|
// 定义所有行为树文件
|
||||||
|
const treeFiles = [
|
||||||
|
'/assets/ai/enemy-ai.btree.json',
|
||||||
|
'/assets/ai/boss-ai.btree.json',
|
||||||
|
'/assets/ai/patrol.btree.json',
|
||||||
|
'/assets/ai/chase.btree.json'
|
||||||
|
];
|
||||||
|
|
||||||
|
// 并行加载所有文件
|
||||||
|
const loadPromises = treeFiles.map(file => this.loadTree(file));
|
||||||
|
await Promise.all(loadPromises);
|
||||||
|
|
||||||
|
console.log(`已加载 ${this.assetManager.getAssetCount()} 个行为树资产`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadTree(filePath: string) {
|
||||||
|
const jsonContent = await fetch(filePath).then(res => res.text());
|
||||||
|
const treeData = BehaviorTreeAssetSerializer.deserialize(jsonContent);
|
||||||
|
this.assetManager.loadAsset(treeData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 游戏启动时调用
|
||||||
|
const preloader = new BehaviorTreePreloader();
|
||||||
|
await preloader.preloadAll();
|
||||||
|
```
|
||||||
|
|
||||||
|
## 运行时切换行为树
|
||||||
|
|
||||||
|
敌人在不同阶段使用不同的行为树:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class EnemyAI {
|
||||||
|
private entity: Entity;
|
||||||
|
private assetManager: BehaviorTreeAssetManager;
|
||||||
|
|
||||||
|
constructor(entity: Entity) {
|
||||||
|
this.entity = entity;
|
||||||
|
this.assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换到巡逻AI
|
||||||
|
switchToPatrol() {
|
||||||
|
const tree = this.assetManager.getAsset('PatrolAI');
|
||||||
|
if (tree) {
|
||||||
|
BehaviorTreeStarter.stop(this.entity);
|
||||||
|
BehaviorTreeStarter.start(this.entity, tree);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换到战斗AI
|
||||||
|
switchToCombat() {
|
||||||
|
const tree = this.assetManager.getAsset('CombatAI');
|
||||||
|
if (tree) {
|
||||||
|
BehaviorTreeStarter.stop(this.entity);
|
||||||
|
BehaviorTreeStarter.start(this.entity, tree);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换到狂暴模式
|
||||||
|
switchToBerserk() {
|
||||||
|
const tree = this.assetManager.getAsset('BerserkAI');
|
||||||
|
if (tree) {
|
||||||
|
BehaviorTreeStarter.stop(this.entity);
|
||||||
|
BehaviorTreeStarter.start(this.entity, tree);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用
|
||||||
|
const enemyAI = new EnemyAI(enemyEntity);
|
||||||
|
|
||||||
|
// Boss血量低于30%时进入狂暴
|
||||||
|
const runtime = enemyEntity.getComponent(BehaviorTreeRuntimeComponent);
|
||||||
|
const health = runtime?.getBlackboardValue<number>('health');
|
||||||
|
|
||||||
|
if (health && health < 30) {
|
||||||
|
enemyAI.switchToBerserk();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 内存优化
|
||||||
|
|
||||||
|
### 1. 共享行为树数据
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 好的做法:100个敌人共享1份BehaviorTreeData
|
||||||
|
const sharedTree = assetManager.getAsset('EnemyAI');
|
||||||
|
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
const enemy = scene.createEntity(`Enemy${i}`);
|
||||||
|
BehaviorTreeStarter.start(enemy, sharedTree!); // 共享数据
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不好的做法:每个敌人创建独立的BehaviorTreeData
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
const enemy = scene.createEntity(`Enemy${i}`);
|
||||||
|
const tree = BehaviorTreeBuilder.create('EnemyAI') // 重复创建
|
||||||
|
// ... 节点定义
|
||||||
|
.build();
|
||||||
|
BehaviorTreeStarter.start(enemy, tree);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 及时卸载不用的资产
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 关卡结束时卸载该关卡的AI
|
||||||
|
function onLevelEnd() {
|
||||||
|
assetManager.unloadAsset('Level1BossAI');
|
||||||
|
assetManager.unloadAsset('Level1EnemyAI');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载新关卡的AI
|
||||||
|
function onLevelStart() {
|
||||||
|
const boss2AI = await loadTreeFromFile('/assets/level2-boss.btree.json');
|
||||||
|
assetManager.loadAsset(boss2AI);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 完整示例:多敌人类型的游戏
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Core, Scene } from '@esengine/ecs-framework';
|
||||||
|
import {
|
||||||
|
BehaviorTreePlugin,
|
||||||
|
BehaviorTreeAssetManager,
|
||||||
|
BehaviorTreeBuilder,
|
||||||
|
BehaviorTreeStarter
|
||||||
|
} from '@esengine/behavior-tree';
|
||||||
|
|
||||||
|
async function setupGame() {
|
||||||
|
// 1. 初始化
|
||||||
|
Core.create();
|
||||||
|
const plugin = new BehaviorTreePlugin();
|
||||||
|
await Core.installPlugin(plugin);
|
||||||
|
|
||||||
|
const scene = new Scene();
|
||||||
|
plugin.setupScene(scene);
|
||||||
|
Core.setScene(scene);
|
||||||
|
|
||||||
|
const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||||
|
|
||||||
|
// 2. 创建共享的子树
|
||||||
|
const patrolTree = BehaviorTreeBuilder.create('Patrol')
|
||||||
|
.sequence('PatrolLoop')
|
||||||
|
.log('巡逻')
|
||||||
|
.wait(1.0)
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
const combatTree = BehaviorTreeBuilder.create('Combat')
|
||||||
|
.sequence('CombatLoop')
|
||||||
|
.log('战斗')
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assetManager.loadAsset(patrolTree);
|
||||||
|
assetManager.loadAsset(combatTree);
|
||||||
|
|
||||||
|
// 3. 创建不同类型敌人的AI
|
||||||
|
const meleeEnemyAI = BehaviorTreeBuilder.create('MeleeEnemyAI')
|
||||||
|
.selector('MeleeBehavior')
|
||||||
|
.sequence('Attack')
|
||||||
|
.blackboardExists('target')
|
||||||
|
.log('近战攻击')
|
||||||
|
.end()
|
||||||
|
.subTree('Patrol')
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
const rangedEnemyAI = BehaviorTreeBuilder.create('RangedEnemyAI')
|
||||||
|
.selector('RangedBehavior')
|
||||||
|
.sequence('Attack')
|
||||||
|
.blackboardExists('target')
|
||||||
|
.log('远程攻击')
|
||||||
|
.end()
|
||||||
|
.subTree('Patrol')
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assetManager.loadAsset(meleeEnemyAI);
|
||||||
|
assetManager.loadAsset(rangedEnemyAI);
|
||||||
|
|
||||||
|
// 4. 创建多个敌人实体
|
||||||
|
// 10个近战敌人共享同一份AI
|
||||||
|
const meleeAI = assetManager.getAsset('MeleeEnemyAI')!;
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
const enemy = scene.createEntity(`MeleeEnemy${i}`);
|
||||||
|
BehaviorTreeStarter.start(enemy, meleeAI);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5个远程敌人共享同一份AI
|
||||||
|
const rangedAI = assetManager.getAsset('RangedEnemyAI')!;
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
const enemy = scene.createEntity(`RangedEnemy${i}`);
|
||||||
|
BehaviorTreeStarter.start(enemy, rangedAI);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`已创建 15 个敌人,使用 ${assetManager.getAssetCount()} 个行为树资产`);
|
||||||
|
|
||||||
|
// 5. 游戏循环
|
||||||
|
setInterval(() => {
|
||||||
|
Core.update(0.016);
|
||||||
|
}, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
setupGame();
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### 如何检查资产是否已加载?
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||||
|
|
||||||
|
if (!assetManager.hasAsset('EnemyAI')) {
|
||||||
|
// 加载资产
|
||||||
|
const tree = await loadTreeFromFile('/assets/enemy-ai.btree.json');
|
||||||
|
assetManager.loadAsset(tree);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 子树找不到怎么办?
|
||||||
|
|
||||||
|
确保子树已经加载到资产管理器中:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 1. 先加载子树
|
||||||
|
const subTree = BehaviorTreeBuilder.create('SubTreeID')
|
||||||
|
// ...
|
||||||
|
.build();
|
||||||
|
assetManager.loadAsset(subTree);
|
||||||
|
|
||||||
|
// 2. 再加载使用子树的主树
|
||||||
|
const mainTree = BehaviorTreeBuilder.create('MainTree')
|
||||||
|
.subTree('SubTreeID')
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 如何导出行为树为 JSON?
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { BehaviorTreeAssetSerializer } from '@esengine/behavior-tree';
|
||||||
|
|
||||||
|
const tree = BehaviorTreeBuilder.create('MyTree')
|
||||||
|
// ... 节点定义
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 序列化为JSON字符串
|
||||||
|
const json = BehaviorTreeAssetSerializer.serialize(tree);
|
||||||
|
|
||||||
|
// 保存到文件或发送到服务器
|
||||||
|
console.log(json);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
- 学习[Cocos Creator 集成](./cocos-integration.md)了解如何在游戏引擎中加载资源
|
||||||
|
- 查看[自定义节点执行器](./custom-actions.md)创建自定义行为
|
||||||
|
- 阅读[最佳实践](./best-practices.md)优化你的行为树设计
|
||||||
468
docs/guide/behavior-tree/best-practices.md
Normal file
468
docs/guide/behavior-tree/best-practices.md
Normal file
@@ -0,0 +1,468 @@
|
|||||||
|
# 最佳实践
|
||||||
|
|
||||||
|
本文介绍行为树设计和使用的最佳实践,帮助你构建高效、可维护的AI系统。
|
||||||
|
|
||||||
|
## 行为树设计原则
|
||||||
|
|
||||||
|
### 1. 保持树的层次清晰
|
||||||
|
|
||||||
|
将复杂行为分解成清晰的层次结构:
|
||||||
|
|
||||||
|
```
|
||||||
|
Root Selector
|
||||||
|
├── Emergency (高优先级:紧急情况)
|
||||||
|
│ ├── FleeFromDanger
|
||||||
|
│ └── CallForHelp
|
||||||
|
├── Combat (中优先级:战斗)
|
||||||
|
│ ├── Attack
|
||||||
|
│ └── Defend
|
||||||
|
└── Idle (低优先级:空闲)
|
||||||
|
├── Patrol
|
||||||
|
└── Rest
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### 2. 单一职责原则
|
||||||
|
|
||||||
|
每个节点应该只做一件事。要实现复杂动作,创建自定义执行器,参见[自定义节点执行器](./custom-actions.md)。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 好的设计 - 使用内置节点
|
||||||
|
.sequence('AttackSequence')
|
||||||
|
.blackboardExists('target', 'CheckTarget')
|
||||||
|
.log('瞄准', 'Aim')
|
||||||
|
.log('开火', 'Fire')
|
||||||
|
.end()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 使用描述性名称
|
||||||
|
|
||||||
|
节点名称应该清楚地表达其功能:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 好的命名
|
||||||
|
.blackboardCompare('health', 20, 'less', 'CheckHealthLow')
|
||||||
|
.log('寻找最近的医疗包', 'FindHealthPack')
|
||||||
|
.log('移动到医疗包', 'MoveToHealthPack')
|
||||||
|
|
||||||
|
// 不好的命名
|
||||||
|
.blackboardCompare('health', 20, 'less', 'C1')
|
||||||
|
.log('Do something', 'Action1')
|
||||||
|
.log('Move', 'A2')
|
||||||
|
```
|
||||||
|
|
||||||
|
## 黑板变量管理
|
||||||
|
|
||||||
|
### 1. 变量命名规范
|
||||||
|
|
||||||
|
使用清晰的命名约定:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const tree = BehaviorTreeBuilder.create('AI')
|
||||||
|
// 状态变量
|
||||||
|
.defineBlackboardVariable('currentState', 'idle')
|
||||||
|
.defineBlackboardVariable('isMoving', false)
|
||||||
|
|
||||||
|
// 目标和引用
|
||||||
|
.defineBlackboardVariable('targetEnemy', null)
|
||||||
|
.defineBlackboardVariable('patrolPoints', [])
|
||||||
|
|
||||||
|
// 配置参数
|
||||||
|
.defineBlackboardVariable('attackRange', 5.0)
|
||||||
|
.defineBlackboardVariable('moveSpeed', 10.0)
|
||||||
|
|
||||||
|
// 临时数据
|
||||||
|
.defineBlackboardVariable('lastAttackTime', 0)
|
||||||
|
.defineBlackboardVariable('searchAttempts', 0)
|
||||||
|
// ...
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 避免过度使用黑板
|
||||||
|
|
||||||
|
只在需要跨节点共享的数据才放入黑板。在自定义执行器中使用局部变量:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 好的做法 - 使用局部变量
|
||||||
|
export class CalculateAction implements INodeExecutor {
|
||||||
|
execute(context: NodeExecutionContext): TaskStatus {
|
||||||
|
// 局部计算
|
||||||
|
const temp1 = 10;
|
||||||
|
const temp2 = 20;
|
||||||
|
const result = temp1 + temp2;
|
||||||
|
|
||||||
|
// 只保存需要共享的结果
|
||||||
|
context.runtime.setBlackboardValue('calculationResult', result);
|
||||||
|
|
||||||
|
return TaskStatus.Success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 使用类型安全的访问
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export class TypeSafeAction implements INodeExecutor {
|
||||||
|
execute(context: NodeExecutionContext): TaskStatus {
|
||||||
|
const { runtime } = context;
|
||||||
|
|
||||||
|
// 使用泛型进行类型安全访问
|
||||||
|
const health = runtime.getBlackboardValue<number>('health');
|
||||||
|
const target = runtime.getBlackboardValue<Entity | null>('target');
|
||||||
|
const state = runtime.getBlackboardValue<string>('currentState');
|
||||||
|
|
||||||
|
if (health !== undefined && health < 30) {
|
||||||
|
runtime.setBlackboardValue('currentState', 'flee');
|
||||||
|
}
|
||||||
|
|
||||||
|
return TaskStatus.Success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 执行器设计
|
||||||
|
|
||||||
|
### 1. 保持执行器无状态
|
||||||
|
|
||||||
|
状态必须存储在`context.state`中,而不是执行器实例:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 正确的做法
|
||||||
|
export class TimedAction implements INodeExecutor {
|
||||||
|
execute(context: NodeExecutionContext): TaskStatus {
|
||||||
|
if (!context.state.startTime) {
|
||||||
|
context.state.startTime = context.totalTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
const elapsed = context.totalTime - context.state.startTime;
|
||||||
|
|
||||||
|
if (elapsed >= 3.0) {
|
||||||
|
return TaskStatus.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TaskStatus.Running;
|
||||||
|
}
|
||||||
|
|
||||||
|
reset(context: NodeExecutionContext): void {
|
||||||
|
context.state.startTime = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 条件应该是无副作用的
|
||||||
|
|
||||||
|
条件检查不应该修改状态:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 好的做法 - 只读检查
|
||||||
|
@NodeExecutorMetadata({
|
||||||
|
implementationType: 'IsHealthLow',
|
||||||
|
nodeType: NodeType.Condition,
|
||||||
|
displayName: '检查生命值低',
|
||||||
|
category: '条件',
|
||||||
|
configSchema: {
|
||||||
|
threshold: {
|
||||||
|
type: 'number',
|
||||||
|
default: 30,
|
||||||
|
supportBinding: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
export class IsHealthLow implements INodeExecutor {
|
||||||
|
execute(context: NodeExecutionContext): TaskStatus {
|
||||||
|
const threshold = BindingHelper.getValue<number>(context, 'threshold', 30);
|
||||||
|
const health = context.runtime.getBlackboardValue<number>('health') || 0;
|
||||||
|
|
||||||
|
return health < threshold ? TaskStatus.Success : TaskStatus.Failure;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 错误处理
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export class SafeAction implements INodeExecutor {
|
||||||
|
execute(context: NodeExecutionContext): TaskStatus {
|
||||||
|
try {
|
||||||
|
const resourceId = context.runtime.getBlackboardValue('resourceId');
|
||||||
|
|
||||||
|
if (!resourceId) {
|
||||||
|
console.error('[SafeAction] 资源ID未设置');
|
||||||
|
return TaskStatus.Failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行操作...
|
||||||
|
|
||||||
|
return TaskStatus.Success;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[SafeAction] 执行失败:', error);
|
||||||
|
context.runtime.setBlackboardValue('lastError', error.message);
|
||||||
|
return TaskStatus.Failure;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 性能优化技巧
|
||||||
|
|
||||||
|
### 1. 使用冷却装饰器
|
||||||
|
|
||||||
|
避免高频执行昂贵操作:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const tree = BehaviorTreeBuilder.create('ThrottledAI')
|
||||||
|
.cooldown(1.0, 'ThrottleSearch') // 最多每秒执行一次
|
||||||
|
.log('昂贵的搜索操作', 'ExpensiveSearch')
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 缓存计算结果
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export class CachedFindNearest implements INodeExecutor {
|
||||||
|
execute(context: NodeExecutionContext): TaskStatus {
|
||||||
|
const { state, runtime, totalTime } = context;
|
||||||
|
|
||||||
|
// 检查缓存是否有效
|
||||||
|
const cacheTime = state.enemyCacheTime || 0;
|
||||||
|
|
||||||
|
if (totalTime - cacheTime < 0.5) { // 缓存0.5秒
|
||||||
|
const cached = runtime.getBlackboardValue('nearestEnemy');
|
||||||
|
return cached ? TaskStatus.Success : TaskStatus.Failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行搜索
|
||||||
|
const nearest = findNearestEnemy();
|
||||||
|
runtime.setBlackboardValue('nearestEnemy', nearest);
|
||||||
|
state.enemyCacheTime = totalTime;
|
||||||
|
|
||||||
|
return nearest ? TaskStatus.Success : TaskStatus.Failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
reset(context: NodeExecutionContext): void {
|
||||||
|
context.state.enemyCacheTime = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 使用早期退出
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const tree = BehaviorTreeBuilder.create('EarlyExit')
|
||||||
|
.selector('FindTarget')
|
||||||
|
// 先检查缓存的目标
|
||||||
|
.blackboardExists('cachedTarget', 'HasCachedTarget')
|
||||||
|
|
||||||
|
// 没有缓存才进行搜索(需要自定义执行器)
|
||||||
|
.log('执行昂贵的搜索', 'SearchNewTarget')
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
## 可维护性
|
||||||
|
|
||||||
|
### 1. 使用有意义的节点名称
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 好的做法
|
||||||
|
const tree = BehaviorTreeBuilder.create('CombatAI')
|
||||||
|
.selector('CombatDecision')
|
||||||
|
.sequence('AttackEnemy')
|
||||||
|
.blackboardExists('target', 'HasTarget')
|
||||||
|
.log('执行攻击', 'Attack')
|
||||||
|
.end()
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 不好的做法
|
||||||
|
const tree = BehaviorTreeBuilder.create('AI')
|
||||||
|
.selector('Node1')
|
||||||
|
.sequence('Node2')
|
||||||
|
.blackboardExists('target', 'Node3')
|
||||||
|
.log('Attack', 'Node4')
|
||||||
|
.end()
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 使用编辑器创建复杂树
|
||||||
|
|
||||||
|
对于复杂的AI,使用可视化编辑器:
|
||||||
|
|
||||||
|
- 更直观的结构
|
||||||
|
- 方便非程序员调整
|
||||||
|
- 易于版本控制
|
||||||
|
- 支持实时调试
|
||||||
|
|
||||||
|
|
||||||
|
### 3. 添加注释和文档
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 为行为树添加清晰的注释
|
||||||
|
const bossAI = BehaviorTreeBuilder.create('BossAI')
|
||||||
|
.defineBlackboardVariable('phase', 1) // 1=正常, 2=狂暴, 3=濒死
|
||||||
|
|
||||||
|
.selector('MainBehavior')
|
||||||
|
// 阶段3: 生命值<20%,使用终极技能
|
||||||
|
.sequence('Phase3')
|
||||||
|
.blackboardCompare('phase', 3, 'equals')
|
||||||
|
.log('使用终极技能', 'UltimateAbility')
|
||||||
|
.end()
|
||||||
|
|
||||||
|
// 阶段2: 生命值<50%,进入狂暴
|
||||||
|
.sequence('Phase2')
|
||||||
|
.blackboardCompare('phase', 2, 'equals')
|
||||||
|
.log('进入狂暴模式', 'BerserkMode')
|
||||||
|
.end()
|
||||||
|
|
||||||
|
// 阶段1: 正常战斗
|
||||||
|
.sequence('Phase1')
|
||||||
|
.log('普通攻击', 'NormalAttack')
|
||||||
|
.end()
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
## 调试技巧
|
||||||
|
|
||||||
|
### 1. 使用日志节点
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const tree = BehaviorTreeBuilder.create('Debug')
|
||||||
|
.log('开始攻击序列', 'StartAttack')
|
||||||
|
.sequence('Attack')
|
||||||
|
.log('检查目标', 'CheckTarget')
|
||||||
|
.blackboardExists('target')
|
||||||
|
.log('执行攻击', 'DoAttack')
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 在自定义执行器中调试
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export class DebugAction implements INodeExecutor {
|
||||||
|
execute(context: NodeExecutionContext): TaskStatus {
|
||||||
|
const { nodeData, runtime, state } = context;
|
||||||
|
|
||||||
|
console.group(`[${nodeData.name}]`);
|
||||||
|
console.log('配置:', nodeData.config);
|
||||||
|
console.log('状态:', state);
|
||||||
|
console.log('黑板:', runtime.getAllBlackboardVariables());
|
||||||
|
console.log('活动节点:', Array.from(runtime.activeNodeIds));
|
||||||
|
console.groupEnd();
|
||||||
|
|
||||||
|
return TaskStatus.Success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 状态可视化
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export class VisualizeState implements INodeExecutor {
|
||||||
|
execute(context: NodeExecutionContext): TaskStatus {
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.group('AI State');
|
||||||
|
console.log('Entity:', context.entity.name);
|
||||||
|
console.log('Health:', context.runtime.getBlackboardValue('health'));
|
||||||
|
console.log('State:', context.runtime.getBlackboardValue('currentState'));
|
||||||
|
console.log('Target:', context.runtime.getBlackboardValue('target'));
|
||||||
|
console.groupEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
return TaskStatus.Success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见反模式
|
||||||
|
|
||||||
|
### 1. 过深的嵌套
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 不好 - 太深的嵌套
|
||||||
|
.selector()
|
||||||
|
.sequence()
|
||||||
|
.sequence()
|
||||||
|
.sequence()
|
||||||
|
.log('太深了', 'DeepAction')
|
||||||
|
.end()
|
||||||
|
.end()
|
||||||
|
.end()
|
||||||
|
.end()
|
||||||
|
|
||||||
|
// 好 - 使用合理的深度
|
||||||
|
.selector()
|
||||||
|
.sequence()
|
||||||
|
.log('Action1')
|
||||||
|
.log('Action2')
|
||||||
|
.end()
|
||||||
|
.sequence()
|
||||||
|
.log('Action3')
|
||||||
|
.log('Action4')
|
||||||
|
.end()
|
||||||
|
.end()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 在执行器中存储状态
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 错误 - 状态存储在执行器中
|
||||||
|
export class BadAction implements INodeExecutor {
|
||||||
|
private startTime = 0; // 错误!多个节点会共享这个值
|
||||||
|
|
||||||
|
execute(context: NodeExecutionContext): TaskStatus {
|
||||||
|
this.startTime = context.totalTime; // 错误!
|
||||||
|
return TaskStatus.Success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 正确 - 状态存储在context.state中
|
||||||
|
export class GoodAction implements INodeExecutor {
|
||||||
|
execute(context: NodeExecutionContext): TaskStatus {
|
||||||
|
if (!context.state.startTime) {
|
||||||
|
context.state.startTime = context.totalTime; // 正确!
|
||||||
|
}
|
||||||
|
return TaskStatus.Success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 频繁修改黑板
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 不好 - 每帧都修改黑板
|
||||||
|
export class FrequentUpdate implements INodeExecutor {
|
||||||
|
execute(context: NodeExecutionContext): TaskStatus {
|
||||||
|
const pos = getCurrentPosition();
|
||||||
|
context.runtime.setBlackboardValue('position', pos); // 每帧都set
|
||||||
|
context.runtime.setBlackboardValue('velocity', getVelocity());
|
||||||
|
context.runtime.setBlackboardValue('rotation', getRotation());
|
||||||
|
return TaskStatus.Running;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 好 - 只在需要时修改
|
||||||
|
export class SmartUpdate implements INodeExecutor {
|
||||||
|
execute(context: NodeExecutionContext): TaskStatus {
|
||||||
|
const oldPos = context.runtime.getBlackboardValue('position');
|
||||||
|
const newPos = getCurrentPosition();
|
||||||
|
|
||||||
|
// 只在位置变化时更新
|
||||||
|
if (!positionsEqual(oldPos, newPos)) {
|
||||||
|
context.runtime.setBlackboardValue('position', newPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
return TaskStatus.Running;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
- 学习[自定义节点执行器](./custom-actions.md)扩展行为树功能
|
||||||
|
- 探索[高级用法](./advanced-usage.md)了解更多技巧
|
||||||
|
- 参考[核心概念](./core-concepts.md)深入理解原理
|
||||||
683
docs/guide/behavior-tree/cocos-integration.md
Normal file
683
docs/guide/behavior-tree/cocos-integration.md
Normal file
@@ -0,0 +1,683 @@
|
|||||||
|
# Cocos Creator 集成
|
||||||
|
|
||||||
|
本教程将引导你在 Cocos Creator 项目中集成和使用行为树系统。
|
||||||
|
|
||||||
|
## 前置要求
|
||||||
|
|
||||||
|
- Cocos Creator 3.x 或更高版本
|
||||||
|
- 基本的 TypeScript 知识
|
||||||
|
- 已完成[快速开始](./getting-started.md)教程
|
||||||
|
|
||||||
|
## 安装
|
||||||
|
|
||||||
|
### 步骤1:安装依赖
|
||||||
|
|
||||||
|
在你的 Cocos Creator 项目根目录下:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @esengine/ecs-framework @esengine/behavior-tree
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤2:配置 tsconfig.json
|
||||||
|
|
||||||
|
确保 `tsconfig.json` 中包含以下配置:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"moduleResolution": "node"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
建议的项目结构:
|
||||||
|
|
||||||
|
```
|
||||||
|
assets/
|
||||||
|
├── scripts/
|
||||||
|
│ ├── ai/
|
||||||
|
│ │ ├── EnemyAIComponent.ts # AI 组件
|
||||||
|
│ │ └── PlayerDetector.ts # 检测器
|
||||||
|
│ ├── systems/
|
||||||
|
│ │ └── BehaviorTreeSystem.ts # 行为树系统
|
||||||
|
│ └── Main.ts # 主入口
|
||||||
|
├── resources/
|
||||||
|
│ └── behaviors/
|
||||||
|
│ ├── enemy-ai.btree.json # 行为树资产
|
||||||
|
│ └── patrol.btree.json # 子树资产
|
||||||
|
└── types/
|
||||||
|
└── enemy-ai.ts # 类型定义
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 初始化 ECS 和行为树
|
||||||
|
|
||||||
|
### 创建主入口组件
|
||||||
|
|
||||||
|
创建 `assets/scripts/Main.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { _decorator, Component } from 'cc';
|
||||||
|
import { Core, Scene } from '@esengine/ecs-framework';
|
||||||
|
import { BehaviorTreePlugin } from '@esengine/behavior-tree';
|
||||||
|
|
||||||
|
const { ccclass } = _decorator;
|
||||||
|
|
||||||
|
@ccclass('Main')
|
||||||
|
export class Main extends Component {
|
||||||
|
async onLoad() {
|
||||||
|
// 初始化 ECS Core
|
||||||
|
Core.create();
|
||||||
|
|
||||||
|
// 安装行为树插件
|
||||||
|
const behaviorTreePlugin = new BehaviorTreePlugin();
|
||||||
|
await Core.installPlugin(behaviorTreePlugin);
|
||||||
|
|
||||||
|
// 创建并设置场景
|
||||||
|
const scene = new Scene();
|
||||||
|
behaviorTreePlugin.setupScene(scene);
|
||||||
|
Core.setScene(scene);
|
||||||
|
|
||||||
|
console.log('ECS 和行为树系统初始化完成');
|
||||||
|
}
|
||||||
|
|
||||||
|
update(deltaTime: number) {
|
||||||
|
// 更新 ECS(会自动更新场景)
|
||||||
|
Core.update(deltaTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
onDestroy() {
|
||||||
|
// 清理资源
|
||||||
|
Core.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### 添加组件到场景
|
||||||
|
|
||||||
|
1. 在场景中创建一个空节点(命名为 `GameManager`)
|
||||||
|
2. 添加 `Main` 组件到该节点
|
||||||
|
|
||||||
|
|
||||||
|
## 创建 AI 组件
|
||||||
|
|
||||||
|
创建 `assets/scripts/ai/EnemyAIComponent.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { _decorator, Component, Node } from 'cc';
|
||||||
|
import { Core, Entity } from '@esengine/ecs-framework';
|
||||||
|
import {
|
||||||
|
BehaviorTreeBuilder,
|
||||||
|
BehaviorTreeStarter,
|
||||||
|
BehaviorTreeRuntimeComponent
|
||||||
|
} from '@esengine/behavior-tree';
|
||||||
|
|
||||||
|
const { ccclass, property } = _decorator;
|
||||||
|
|
||||||
|
@ccclass('EnemyAIComponent')
|
||||||
|
export class EnemyAIComponent extends Component {
|
||||||
|
private aiEntity: Entity | null = null;
|
||||||
|
|
||||||
|
async start() {
|
||||||
|
// 创建行为树
|
||||||
|
await this.createBehaviorTree();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createBehaviorTree() {
|
||||||
|
try {
|
||||||
|
// 获取Core管理的场景
|
||||||
|
const scene = Core.scene;
|
||||||
|
if (!scene) {
|
||||||
|
console.error('场景未初始化');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用Builder API创建行为树
|
||||||
|
const tree = BehaviorTreeBuilder.create('EnemyAI')
|
||||||
|
.defineBlackboardVariable('cocosNode', this.node)
|
||||||
|
.defineBlackboardVariable('health', 100)
|
||||||
|
.defineBlackboardVariable('playerNode', null)
|
||||||
|
.defineBlackboardVariable('detectionRange', 10)
|
||||||
|
.defineBlackboardVariable('attackRange', 2)
|
||||||
|
.selector('MainBehavior')
|
||||||
|
.sequence('Combat')
|
||||||
|
.blackboardExists('playerNode')
|
||||||
|
.blackboardCompare('health', 30, 'greater')
|
||||||
|
.log('攻击玩家', 'AttackPlayer')
|
||||||
|
.end()
|
||||||
|
.sequence('Flee')
|
||||||
|
.blackboardCompare('health', 30, 'lessOrEqual')
|
||||||
|
.log('逃跑', 'RunAway')
|
||||||
|
.end()
|
||||||
|
.log('巡逻', 'Patrol')
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 创建AI实体并启动
|
||||||
|
this.aiEntity = scene.createEntity(`AI_${this.node.name}`);
|
||||||
|
BehaviorTreeStarter.start(this.aiEntity, tree);
|
||||||
|
|
||||||
|
console.log('敌人 AI 已启动');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('初始化行为树失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDestroy() {
|
||||||
|
// 停止 AI
|
||||||
|
if (this.aiEntity) {
|
||||||
|
BehaviorTreeStarter.stop(this.aiEntity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 与 Cocos 节点交互
|
||||||
|
|
||||||
|
### 创建自定义执行器
|
||||||
|
|
||||||
|
要实现与Cocos节点的交互,需要创建自定义执行器:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
INodeExecutor,
|
||||||
|
NodeExecutionContext,
|
||||||
|
NodeExecutorMetadata
|
||||||
|
} from '@esengine/behavior-tree';
|
||||||
|
import { TaskStatus, NodeType } from '@esengine/behavior-tree';
|
||||||
|
import { Animation } from 'cc';
|
||||||
|
|
||||||
|
@NodeExecutorMetadata({
|
||||||
|
implementationType: 'PlayAnimation',
|
||||||
|
nodeType: NodeType.Action,
|
||||||
|
displayName: '播放动画',
|
||||||
|
description: '播放Cocos节点上的动画',
|
||||||
|
category: 'Cocos',
|
||||||
|
configSchema: {
|
||||||
|
animationName: {
|
||||||
|
type: 'string',
|
||||||
|
default: 'attack'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
export class PlayAnimationAction implements INodeExecutor {
|
||||||
|
execute(context: NodeExecutionContext): TaskStatus {
|
||||||
|
const cocosNode = context.runtime.getBlackboardValue('cocosNode');
|
||||||
|
const animationName = context.nodeData.config.animationName;
|
||||||
|
|
||||||
|
if (!cocosNode) {
|
||||||
|
return TaskStatus.Failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
const animation = cocosNode.getComponent(Animation);
|
||||||
|
if (animation) {
|
||||||
|
animation.play(animationName);
|
||||||
|
return TaskStatus.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TaskStatus.Failure;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 完整示例:敌人 AI
|
||||||
|
|
||||||
|
### 行为树设计
|
||||||
|
|
||||||
|
使用编辑器创建 `enemy-ai.btree.json`:
|
||||||
|
|
||||||
|
```
|
||||||
|
RootSelector
|
||||||
|
├── CombatSequence
|
||||||
|
│ ├── CheckPlayerInRange (Condition)
|
||||||
|
│ ├── CheckHealthGood (Condition)
|
||||||
|
│ └── AttackPlayer (Action)
|
||||||
|
├── FleeSequence
|
||||||
|
│ ├── CheckHealthLow (Condition)
|
||||||
|
│ └── RunAway (Action)
|
||||||
|
└── PatrolSequence
|
||||||
|
├── PickWaypoint (Action)
|
||||||
|
├── MoveToWaypoint (Action)
|
||||||
|
└── Wait (Action)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### 黑板变量
|
||||||
|
|
||||||
|
定义以下黑板变量:
|
||||||
|
|
||||||
|
- `cocosNode`:Node - Cocos 节点引用
|
||||||
|
- `health`:Number - 生命值
|
||||||
|
- `playerNode`:Object - 玩家节点引用
|
||||||
|
- `detectionRange`:Number - 检测范围
|
||||||
|
- `attackRange`:Number - 攻击范围
|
||||||
|
- `currentWaypoint`:Number - 当前路点索引
|
||||||
|
|
||||||
|
|
||||||
|
### 实现检测系统
|
||||||
|
|
||||||
|
创建 `assets/scripts/ai/PlayerDetector.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { _decorator, Component, Node, Vec3 } from 'cc';
|
||||||
|
import { BehaviorTreeRuntimeComponent } from '@esengine/behavior-tree';
|
||||||
|
|
||||||
|
const { ccclass, property } = _decorator;
|
||||||
|
|
||||||
|
@ccclass('PlayerDetector')
|
||||||
|
export class PlayerDetector extends Component {
|
||||||
|
@property(Node)
|
||||||
|
player: Node = null;
|
||||||
|
|
||||||
|
@property
|
||||||
|
detectionRange: number = 10;
|
||||||
|
|
||||||
|
private runtime: BehaviorTreeRuntimeComponent | null = null;
|
||||||
|
|
||||||
|
start() {
|
||||||
|
// 假设AI组件在同一节点上
|
||||||
|
const aiComponent = this.node.getComponent('EnemyAIComponent') as any;
|
||||||
|
if (aiComponent && aiComponent.aiEntity) {
|
||||||
|
this.runtime = aiComponent.aiEntity.getComponent(BehaviorTreeRuntimeComponent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update(deltaTime: number) {
|
||||||
|
if (!this.runtime || !this.player) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算距离
|
||||||
|
const distance = Vec3.distance(this.node.position, this.player.position);
|
||||||
|
|
||||||
|
// 更新黑板
|
||||||
|
this.runtime.setBlackboardValue('playerNode', this.player);
|
||||||
|
this.runtime.setBlackboardValue('playerInRange', distance <= this.detectionRange);
|
||||||
|
this.runtime.setBlackboardValue('distanceToPlayer', distance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 资源管理
|
||||||
|
|
||||||
|
### 使用 BehaviorTreeAssetManager
|
||||||
|
|
||||||
|
框架提供了 `BehaviorTreeAssetManager` 来统一管理行为树资产,避免重复创建:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Core } from '@esengine/ecs-framework';
|
||||||
|
import {
|
||||||
|
BehaviorTreeAssetManager,
|
||||||
|
BehaviorTreeBuilder,
|
||||||
|
BehaviorTreeStarter
|
||||||
|
} from '@esengine/behavior-tree';
|
||||||
|
|
||||||
|
// 获取资产管理器(插件已自动注册)
|
||||||
|
const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||||
|
|
||||||
|
// 创建并注册行为树(只创建一次)
|
||||||
|
const enemyAI = BehaviorTreeBuilder.create('EnemyAI')
|
||||||
|
.defineBlackboardVariable('health', 100)
|
||||||
|
.selector('MainBehavior')
|
||||||
|
.log('攻击')
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assetManager.loadAsset(enemyAI);
|
||||||
|
|
||||||
|
// 为多个敌人实体使用同一份资产
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
const enemy = scene.createEntity(`Enemy${i}`);
|
||||||
|
const tree = assetManager.getAsset('EnemyAI')!;
|
||||||
|
BehaviorTreeStarter.start(enemy, tree); // 10个敌人共享1份数据
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 从 Cocos Creator 资源加载
|
||||||
|
|
||||||
|
#### 1. 将行为树 JSON 放入 resources 目录
|
||||||
|
|
||||||
|
```
|
||||||
|
assets/
|
||||||
|
└── resources/
|
||||||
|
└── behaviors/
|
||||||
|
├── enemy-ai.btree.json
|
||||||
|
└── boss-ai.btree.json
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 创建资源加载器
|
||||||
|
|
||||||
|
创建 `assets/scripts/BehaviorTreeLoader.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { resources, JsonAsset } from 'cc';
|
||||||
|
import { Core } from '@esengine/ecs-framework';
|
||||||
|
import {
|
||||||
|
BehaviorTreeAssetManager,
|
||||||
|
BehaviorTreeAssetSerializer,
|
||||||
|
BehaviorTreeData
|
||||||
|
} from '@esengine/behavior-tree';
|
||||||
|
|
||||||
|
export class BehaviorTreeLoader {
|
||||||
|
private assetManager: BehaviorTreeAssetManager;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 resources 目录加载行为树
|
||||||
|
* @param path 相对于 resources 的路径,不带扩展名
|
||||||
|
* @example await loader.load('behaviors/enemy-ai')
|
||||||
|
*/
|
||||||
|
async load(path: string): Promise<BehaviorTreeData | null> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
resources.load(path, JsonAsset, (err, jsonAsset) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(`加载行为树失败: ${path}`, err);
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 反序列化 JSON 为 BehaviorTreeData
|
||||||
|
const jsonStr = JSON.stringify(jsonAsset.json);
|
||||||
|
const treeData = BehaviorTreeAssetSerializer.deserialize(jsonStr);
|
||||||
|
|
||||||
|
// 加载到资产管理器
|
||||||
|
this.assetManager.loadAsset(treeData);
|
||||||
|
|
||||||
|
console.log(`行为树已加载: ${treeData.name}`);
|
||||||
|
resolve(treeData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`解析行为树失败: ${path}`, error);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预加载所有行为树
|
||||||
|
*/
|
||||||
|
async preloadAll(paths: string[]): Promise<void> {
|
||||||
|
const promises = paths.map(path => this.load(path));
|
||||||
|
await Promise.all(promises);
|
||||||
|
console.log(`已预加载 ${paths.length} 个行为树`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 在游戏启动时预加载
|
||||||
|
|
||||||
|
修改 `Main.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { _decorator, Component } from 'cc';
|
||||||
|
import { Core, Scene } from '@esengine/ecs-framework';
|
||||||
|
import { BehaviorTreePlugin } from '@esengine/behavior-tree';
|
||||||
|
import { BehaviorTreeLoader } from './BehaviorTreeLoader';
|
||||||
|
|
||||||
|
const { ccclass } = _decorator;
|
||||||
|
|
||||||
|
@ccclass('Main')
|
||||||
|
export class Main extends Component {
|
||||||
|
private loader: BehaviorTreeLoader | null = null;
|
||||||
|
|
||||||
|
async onLoad() {
|
||||||
|
// 初始化 ECS Core
|
||||||
|
Core.create();
|
||||||
|
|
||||||
|
// 安装行为树插件
|
||||||
|
const behaviorTreePlugin = new BehaviorTreePlugin();
|
||||||
|
await Core.installPlugin(behaviorTreePlugin);
|
||||||
|
|
||||||
|
// 创建场景
|
||||||
|
const scene = new Scene();
|
||||||
|
behaviorTreePlugin.setupScene(scene);
|
||||||
|
Core.setScene(scene);
|
||||||
|
|
||||||
|
// 创建加载器并预加载所有行为树
|
||||||
|
this.loader = new BehaviorTreeLoader();
|
||||||
|
await this.loader.preloadAll([
|
||||||
|
'behaviors/enemy-ai',
|
||||||
|
'behaviors/boss-ai',
|
||||||
|
'behaviors/patrol', // 子树
|
||||||
|
'behaviors/chase' // 子树
|
||||||
|
]);
|
||||||
|
|
||||||
|
console.log('游戏初始化完成');
|
||||||
|
}
|
||||||
|
|
||||||
|
update(deltaTime: number) {
|
||||||
|
Core.update(deltaTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
onDestroy() {
|
||||||
|
Core.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. 在敌人组件中使用
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { _decorator, Component } from 'cc';
|
||||||
|
import { Core, Entity } from '@esengine/ecs-framework';
|
||||||
|
import {
|
||||||
|
BehaviorTreeAssetManager,
|
||||||
|
BehaviorTreeStarter
|
||||||
|
} from '@esengine/behavior-tree';
|
||||||
|
|
||||||
|
const { ccclass, property } = _decorator;
|
||||||
|
|
||||||
|
@ccclass('EnemyAIComponent')
|
||||||
|
export class EnemyAIComponent extends Component {
|
||||||
|
@property
|
||||||
|
aiType: string = 'enemy-ai'; // 在编辑器中配置使用哪个AI
|
||||||
|
|
||||||
|
private aiEntity: Entity | null = null;
|
||||||
|
|
||||||
|
start() {
|
||||||
|
const scene = Core.scene;
|
||||||
|
if (!scene) return;
|
||||||
|
|
||||||
|
// 从资产管理器获取已加载的行为树
|
||||||
|
const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||||
|
const tree = assetManager.getAsset(this.aiType);
|
||||||
|
|
||||||
|
if (tree) {
|
||||||
|
this.aiEntity = scene.createEntity(`AI_${this.node.name}`);
|
||||||
|
BehaviorTreeStarter.start(this.aiEntity, tree);
|
||||||
|
|
||||||
|
// 设置黑板变量
|
||||||
|
const runtime = this.aiEntity.getComponent(BehaviorTreeRuntimeComponent);
|
||||||
|
runtime?.setBlackboardValue('cocosNode', this.node);
|
||||||
|
} else {
|
||||||
|
console.error(`找不到行为树资产: ${this.aiType}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDestroy() {
|
||||||
|
if (this.aiEntity) {
|
||||||
|
BehaviorTreeStarter.stop(this.aiEntity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 调试
|
||||||
|
|
||||||
|
### 可视化调试信息
|
||||||
|
|
||||||
|
创建调试组件显示 AI 状态:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { _decorator, Component, Label } from 'cc';
|
||||||
|
import { BehaviorTreeRuntimeComponent } from '@esengine/behavior-tree';
|
||||||
|
|
||||||
|
const { ccclass, property } = _decorator;
|
||||||
|
|
||||||
|
@ccclass('AIDebugger')
|
||||||
|
export class AIDebugger extends Component {
|
||||||
|
@property(Label)
|
||||||
|
debugLabel: Label = null;
|
||||||
|
|
||||||
|
private runtime: BehaviorTreeRuntimeComponent | null = null;
|
||||||
|
|
||||||
|
start() {
|
||||||
|
const aiComponent = this.node.getComponent('EnemyAIComponent') as any;
|
||||||
|
if (aiComponent && aiComponent.aiEntity) {
|
||||||
|
this.runtime = aiComponent.aiEntity.getComponent(BehaviorTreeRuntimeComponent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
if (!this.runtime || !this.debugLabel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const health = this.runtime.getBlackboardValue('health');
|
||||||
|
const playerNode = this.runtime.getBlackboardValue('playerNode');
|
||||||
|
|
||||||
|
this.debugLabel.string = `Health: ${health}\nHas Target: ${playerNode ? 'Yes' : 'No'}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 性能优化
|
||||||
|
|
||||||
|
### 1. 限制行为树数量
|
||||||
|
|
||||||
|
合理控制同时运行的行为树数量:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class AIManager {
|
||||||
|
private activeAIs: Entity[] = [];
|
||||||
|
private maxAIs: number = 20;
|
||||||
|
|
||||||
|
addAI(entity: Entity, tree: BehaviorTreeData) {
|
||||||
|
if (this.activeAIs.length >= this.maxAIs) {
|
||||||
|
// 移除最远的AI
|
||||||
|
const furthest = this.findFurthestAI();
|
||||||
|
if (furthest) {
|
||||||
|
BehaviorTreeStarter.stop(furthest);
|
||||||
|
this.activeAIs = this.activeAIs.filter(e => e !== furthest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BehaviorTreeStarter.start(entity, tree);
|
||||||
|
this.activeAIs.push(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeAI(entity: Entity) {
|
||||||
|
BehaviorTreeStarter.stop(entity);
|
||||||
|
this.activeAIs = this.activeAIs.filter(e => e !== entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private findFurthestAI(): Entity | null {
|
||||||
|
// 根据距离找到最远的AI
|
||||||
|
// 实现细节略
|
||||||
|
return this.activeAIs[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 使用冷却装饰器
|
||||||
|
|
||||||
|
对于不需要每帧更新的AI,使用冷却装饰器:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const tree = BehaviorTreeBuilder.create('ThrottledAI')
|
||||||
|
.cooldown(0.2, 'ThrottleRoot') // 每0.2秒执行一次
|
||||||
|
.selector('MainBehavior')
|
||||||
|
// AI逻辑...
|
||||||
|
.end()
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 缓存计算结果
|
||||||
|
|
||||||
|
在自定义执行器中缓存昂贵的计算:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export class CachedFindTarget implements INodeExecutor {
|
||||||
|
execute(context: NodeExecutionContext): TaskStatus {
|
||||||
|
const { state, runtime, totalTime } = context;
|
||||||
|
const cacheTime = state.lastFindTime || 0;
|
||||||
|
|
||||||
|
if (totalTime - cacheTime < 1.0) {
|
||||||
|
const cached = runtime.getBlackboardValue('target');
|
||||||
|
return cached ? TaskStatus.Success : TaskStatus.Failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = findNearestTarget();
|
||||||
|
runtime.setBlackboardValue('target', target);
|
||||||
|
state.lastFindTime = totalTime;
|
||||||
|
|
||||||
|
return target ? TaskStatus.Success : TaskStatus.Failure;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 多平台注意事项
|
||||||
|
|
||||||
|
### 性能考虑
|
||||||
|
|
||||||
|
不同平台的性能差异:
|
||||||
|
|
||||||
|
- **Web平台**: 受浏览器性能限制,建议减少同时运行的AI数量
|
||||||
|
- **原生平台**: 性能较好,可以运行更多AI
|
||||||
|
- **小游戏平台**: 内存受限,注意控制行为树数量和复杂度
|
||||||
|
|
||||||
|
### 平台适配
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { sys } from 'cc';
|
||||||
|
|
||||||
|
// 根据平台调整AI数量
|
||||||
|
const maxAIs = sys.isNative ? 50 : (sys.isBrowser ? 20 : 30);
|
||||||
|
|
||||||
|
// 根据平台调整更新频率
|
||||||
|
const updateInterval = sys.isNative ? 0.016 : 0.05;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### 行为树无法加载?
|
||||||
|
|
||||||
|
检查:
|
||||||
|
1. 资源路径是否正确(相对于 `resources` 目录)
|
||||||
|
2. 文件是否已添加到项目中
|
||||||
|
3. 检查控制台错误信息
|
||||||
|
|
||||||
|
### AI 不执行?
|
||||||
|
|
||||||
|
确保:
|
||||||
|
1. `Main` 组件的 `update` 方法被调用
|
||||||
|
2. `Scene.update()` 在每帧被调用
|
||||||
|
3. 行为树已通过 `BehaviorTreeStarter.start()` 启动
|
||||||
|
|
||||||
|
### 黑板变量不更新?
|
||||||
|
|
||||||
|
检查:
|
||||||
|
1. 变量名拼写是否正确
|
||||||
|
2. 是否在正确的时机更新变量
|
||||||
|
3. 使用 `BehaviorTreeRuntimeComponent.getBlackboardValue()` 和 `setBlackboardValue()` 方法
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
- 查看[资产管理](./asset-management.md)了解如何加载和管理行为树资产、使用子树
|
||||||
|
- 学习[高级用法](./advanced-usage.md)了解性能优化和调试技巧
|
||||||
|
- 阅读[最佳实践](./best-practices.md)优化你的 AI
|
||||||
|
- 学习[自定义节点执行器](./custom-actions.md)创建自定义行为
|
||||||
491
docs/guide/behavior-tree/core-concepts.md
Normal file
491
docs/guide/behavior-tree/core-concepts.md
Normal file
@@ -0,0 +1,491 @@
|
|||||||
|
# 核心概念
|
||||||
|
|
||||||
|
本文介绍行为树系统的核心概念和工作原理。
|
||||||
|
|
||||||
|
## 什么是行为树?
|
||||||
|
|
||||||
|
行为树(Behavior Tree)是一种用于控制AI和自动化系统的决策结构。它通过树状层次结构组织任务,从根节点开始逐层执行,直到找到合适的行为。
|
||||||
|
|
||||||
|
### 与状态机的对比
|
||||||
|
|
||||||
|
传统状态机:
|
||||||
|
- 基于状态和转换
|
||||||
|
- 状态之间的转换复杂
|
||||||
|
- 难以扩展和维护
|
||||||
|
- 不便于复用
|
||||||
|
|
||||||
|
行为树:
|
||||||
|
- 基于任务和层次结构
|
||||||
|
- 模块化、易于复用
|
||||||
|
- 可视化编辑
|
||||||
|
- 灵活的决策逻辑
|
||||||
|
|
||||||
|
|
||||||
|
## 树结构
|
||||||
|
|
||||||
|
行为树由节点组成,形成树状结构:
|
||||||
|
|
||||||
|
```
|
||||||
|
Root (根节点)
|
||||||
|
├── Selector (选择器)
|
||||||
|
│ ├── Sequence (序列)
|
||||||
|
│ │ ├── Condition (条件)
|
||||||
|
│ │ └── Action (动作)
|
||||||
|
│ └── Action (动作)
|
||||||
|
└── Sequence (序列)
|
||||||
|
├── Action (动作)
|
||||||
|
└── Wait (等待)
|
||||||
|
```
|
||||||
|
|
||||||
|
每个节点都有:
|
||||||
|
- 父节点(除了根节点)
|
||||||
|
- 零个或多个子节点
|
||||||
|
- 执行状态
|
||||||
|
- 返回结果
|
||||||
|
|
||||||
|
|
||||||
|
## 节点类型
|
||||||
|
|
||||||
|
### 复合节点(Composite)
|
||||||
|
|
||||||
|
复合节点有多个子节点,按特定规则执行它们。
|
||||||
|
|
||||||
|
#### Selector(选择器)
|
||||||
|
|
||||||
|
按顺序尝试执行子节点,直到某个子节点成功。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const tree = BehaviorTreeBuilder.create('FindFood')
|
||||||
|
.selector('FindFoodSelector')
|
||||||
|
.log('尝试吃附近的食物', 'EatNearby')
|
||||||
|
.log('搜索食物', 'SearchFood')
|
||||||
|
.log('放弃', 'GiveUp')
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
执行逻辑:
|
||||||
|
1. 尝试第一个子节点
|
||||||
|
2. 如果返回Success,选择器成功
|
||||||
|
3. 如果返回Failure,尝试下一个子节点
|
||||||
|
4. 如果返回Running,选择器返回Running
|
||||||
|
5. 所有子节点都失败时,选择器失败
|
||||||
|
|
||||||
|
|
||||||
|
#### Sequence(序列)
|
||||||
|
|
||||||
|
按顺序执行所有子节点,直到某个子节点失败。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const tree = BehaviorTreeBuilder.create('Attack')
|
||||||
|
.sequence('AttackSequence')
|
||||||
|
.blackboardExists('target') // 检查是否有目标
|
||||||
|
.log('瞄准', 'Aim')
|
||||||
|
.log('开火', 'Fire')
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
执行逻辑:
|
||||||
|
1. 依次执行子节点
|
||||||
|
2. 如果子节点返回Failure,序列失败
|
||||||
|
3. 如果子节点返回Running,序列返回Running
|
||||||
|
4. 如果子节点返回Success,继续下一个子节点
|
||||||
|
5. 所有子节点都成功时,序列成功
|
||||||
|
|
||||||
|
|
||||||
|
#### Parallel(并行)
|
||||||
|
|
||||||
|
同时执行多个子节点。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const tree = BehaviorTreeBuilder.create('PlayEffects')
|
||||||
|
.parallel('Effects', {
|
||||||
|
successPolicy: 'all', // 所有任务都要成功
|
||||||
|
failurePolicy: 'one' // 任一失败则失败
|
||||||
|
})
|
||||||
|
.log('播放动画', 'PlayAnimation')
|
||||||
|
.log('播放音效', 'PlaySound')
|
||||||
|
.log('生成粒子', 'SpawnEffect')
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
策略类型:
|
||||||
|
- `successPolicy: 'all'`: 所有子节点都成功才成功
|
||||||
|
- `successPolicy: 'one'`: 任意一个子节点成功就成功
|
||||||
|
- `failurePolicy: 'all'`: 所有子节点都失败才失败
|
||||||
|
- `failurePolicy: 'one'`: 任意一个子节点失败就失败
|
||||||
|
|
||||||
|
|
||||||
|
### 装饰器节点(Decorator)
|
||||||
|
|
||||||
|
装饰器节点只有一个子节点,用于修改子节点的行为或结果。
|
||||||
|
|
||||||
|
#### Inverter(反转)
|
||||||
|
|
||||||
|
反转子节点的结果:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const tree = BehaviorTreeBuilder.create('CheckSafe')
|
||||||
|
.inverter('NotHasEnemy')
|
||||||
|
.blackboardExists('enemy')
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Repeater(重复)
|
||||||
|
|
||||||
|
重复执行子节点:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const tree = BehaviorTreeBuilder.create('Jump3Times')
|
||||||
|
.repeater(3, 'RepeatJump')
|
||||||
|
.log('跳跃', 'Jump')
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Cooldown(冷却)
|
||||||
|
|
||||||
|
限制子节点的执行频率:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const tree = BehaviorTreeBuilder.create('UseSkill')
|
||||||
|
.cooldown(5.0, 'SkillCooldown')
|
||||||
|
.log('使用特殊技能', 'UseSpecialAbility')
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Timeout(超时)
|
||||||
|
|
||||||
|
限制子节点的执行时间:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const tree = BehaviorTreeBuilder.create('TimedTask')
|
||||||
|
.timeout(10.0, 'TaskTimeout')
|
||||||
|
.log('长时间运行的任务', 'ComplexTask')
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### 叶节点(Leaf)
|
||||||
|
|
||||||
|
叶节点没有子节点,执行具体的任务。
|
||||||
|
|
||||||
|
#### Action(动作)
|
||||||
|
|
||||||
|
执行具体操作。内置动作节点包括:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const tree = BehaviorTreeBuilder.create('Actions')
|
||||||
|
.sequence()
|
||||||
|
.wait(2.0) // 等待2秒
|
||||||
|
.log('Hello', 'LogAction') // 输出日志
|
||||||
|
.setBlackboardValue('score', 100) // 设置黑板值
|
||||||
|
.modifyBlackboardValue('score', 'add', 10) // 修改黑板值
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
要实现自定义动作,需要创建自定义执行器,参见[自定义节点执行器](./custom-actions.md)。
|
||||||
|
|
||||||
|
#### Condition(条件)
|
||||||
|
|
||||||
|
检查条件。内置条件节点包括:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const tree = BehaviorTreeBuilder.create('Conditions')
|
||||||
|
.selector()
|
||||||
|
.blackboardExists('player') // 检查变量是否存在
|
||||||
|
.blackboardCompare('health', 50, 'greater') // 比较变量值
|
||||||
|
.randomProbability(0.5) // 50%概率
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Wait(等待)
|
||||||
|
|
||||||
|
等待指定时间:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const tree = BehaviorTreeBuilder.create('WaitExample')
|
||||||
|
.wait(2.0, 'Wait2Seconds')
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 任务状态
|
||||||
|
|
||||||
|
每个节点执行后返回以下状态之一:
|
||||||
|
|
||||||
|
### Success(成功)
|
||||||
|
|
||||||
|
任务成功完成。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 内置节点会根据逻辑自动返回Success
|
||||||
|
.log('任务完成') // 总是返回Success
|
||||||
|
.blackboardCompare('score', 100, 'greater') // 条件满足时返回Success
|
||||||
|
```
|
||||||
|
|
||||||
|
### Failure(失败)
|
||||||
|
|
||||||
|
任务执行失败。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
.blackboardCompare('score', 100, 'greater') // 条件不满足返回Failure
|
||||||
|
.blackboardExists('nonExistent') // 变量不存在返回Failure
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running(运行中)
|
||||||
|
|
||||||
|
任务需要多帧完成,仍在执行中。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
.wait(3.0) // 等待过程中返回Running,3秒后返回Success
|
||||||
|
```
|
||||||
|
|
||||||
|
### Invalid(无效)
|
||||||
|
|
||||||
|
节点未初始化或已重置。通常不需要手动处理此状态。
|
||||||
|
|
||||||
|
|
||||||
|
## 黑板系统
|
||||||
|
|
||||||
|
黑板(Blackboard)是行为树的数据存储系统,用于在节点之间共享数据。
|
||||||
|
|
||||||
|
### 本地黑板
|
||||||
|
|
||||||
|
每个行为树实例都有自己的本地黑板:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const tree = BehaviorTreeBuilder.create('EnemyAI')
|
||||||
|
.defineBlackboardVariable('health', 100)
|
||||||
|
.defineBlackboardVariable('target', null)
|
||||||
|
.defineBlackboardVariable('state', 'idle')
|
||||||
|
// ...
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 支持的数据类型
|
||||||
|
|
||||||
|
黑板支持以下数据类型:
|
||||||
|
- String:字符串
|
||||||
|
- Number:数字
|
||||||
|
- Boolean:布尔值
|
||||||
|
- Vector2:二维向量
|
||||||
|
- Vector3:三维向量
|
||||||
|
- Object:对象引用
|
||||||
|
- Array:数组
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const tree = BehaviorTreeBuilder.create('Variables')
|
||||||
|
.defineBlackboardVariable('name', 'Enemy') // 字符串
|
||||||
|
.defineBlackboardVariable('count', 0) // 数字
|
||||||
|
.defineBlackboardVariable('isActive', true) // 布尔值
|
||||||
|
.defineBlackboardVariable('position', { x: 0, y: 0 }) // 对象(也可用于Vector2)
|
||||||
|
.defineBlackboardVariable('velocity', { x: 0, y: 0, z: 0 }) // 对象(也可用于Vector3)
|
||||||
|
.defineBlackboardVariable('items', []) // 数组
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 读写变量
|
||||||
|
|
||||||
|
通过`BehaviorTreeRuntimeComponent`访问黑板:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||||
|
|
||||||
|
// 读取变量
|
||||||
|
const health = runtime?.getBlackboardValue('health');
|
||||||
|
const target = runtime?.getBlackboardValue('target');
|
||||||
|
|
||||||
|
// 写入变量
|
||||||
|
runtime?.setBlackboardValue('health', 50);
|
||||||
|
runtime?.setBlackboardValue('lastAttackTime', Date.now());
|
||||||
|
|
||||||
|
// 获取所有变量
|
||||||
|
const allVars = runtime?.getAllBlackboardVariables();
|
||||||
|
```
|
||||||
|
|
||||||
|
也可以使用内置节点操作黑板:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const tree = BehaviorTreeBuilder.create('BlackboardOps')
|
||||||
|
.sequence()
|
||||||
|
.setBlackboardValue('score', 100) // 设置值
|
||||||
|
.modifyBlackboardValue('score', 'add', 10) // 增加10
|
||||||
|
.blackboardCompare('score', 110, 'equals') // 检查是否等于110
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 全局黑板
|
||||||
|
|
||||||
|
所有行为树实例共享的黑板,通过`GlobalBlackboardService`访问:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { GlobalBlackboardService } from '@esengine/behavior-tree';
|
||||||
|
import { Core } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
const globalBlackboard = Core.services.resolve(GlobalBlackboardService);
|
||||||
|
|
||||||
|
// 设置全局变量
|
||||||
|
globalBlackboard.setValue('gameState', 'playing');
|
||||||
|
globalBlackboard.setValue('difficulty', 5);
|
||||||
|
|
||||||
|
// 读取全局变量
|
||||||
|
const gameState = globalBlackboard.getValue('gameState');
|
||||||
|
```
|
||||||
|
|
||||||
|
在自定义执行器中访问全局黑板:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { GlobalBlackboardService } from '@esengine/behavior-tree';
|
||||||
|
import { Core } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
export class CheckGameState implements INodeExecutor {
|
||||||
|
execute(context: NodeExecutionContext): TaskStatus {
|
||||||
|
const globalBlackboard = Core.services.resolve(GlobalBlackboardService);
|
||||||
|
const gameState = globalBlackboard.getValue('gameState');
|
||||||
|
|
||||||
|
if (gameState === 'paused') {
|
||||||
|
return TaskStatus.Failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TaskStatus.Success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 执行流程
|
||||||
|
|
||||||
|
### 初始化
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 1. 初始化Core和插件
|
||||||
|
Core.create();
|
||||||
|
const plugin = new BehaviorTreePlugin();
|
||||||
|
await Core.installPlugin(plugin);
|
||||||
|
|
||||||
|
// 2. 创建场景
|
||||||
|
const scene = new Scene();
|
||||||
|
plugin.setupScene(scene);
|
||||||
|
Core.setScene(scene);
|
||||||
|
|
||||||
|
// 3. 构建行为树
|
||||||
|
const tree = BehaviorTreeBuilder.create('AI')
|
||||||
|
// ... 定义节点
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 4. 创建实体并启动
|
||||||
|
const entity = scene.createEntity('AIEntity');
|
||||||
|
BehaviorTreeStarter.start(entity, tree);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 更新循环
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 每帧更新
|
||||||
|
gameLoop(() => {
|
||||||
|
const deltaTime = getDeltaTime();
|
||||||
|
Core.update(deltaTime); // Core会自动更新场景和所有行为树
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 执行顺序
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 从根节点开始
|
||||||
|
2. 根节点执行其逻辑(通常是Selector或Sequence)
|
||||||
|
3. 根节点的子节点按顺序执行
|
||||||
|
4. 每个子节点可能有自己的子节点
|
||||||
|
5. 叶节点执行具体操作并返回状态
|
||||||
|
6. 状态向上传播到父节点
|
||||||
|
7. 父节点根据策略决定如何处理子节点的状态
|
||||||
|
8. 最终根节点返回整体状态
|
||||||
|
```
|
||||||
|
|
||||||
|
### 执行示例
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const tree = BehaviorTreeBuilder.create('Example')
|
||||||
|
.selector('Root') // 1. 执行选择器
|
||||||
|
.sequence('Branch1') // 2. 尝试第一个分支
|
||||||
|
.blackboardCompare('ready', true, 'equals', 'CheckReady') // 3. 条件失败
|
||||||
|
.end() // 4. 序列失败,选择器继续下一个分支
|
||||||
|
.sequence('Branch2') // 5. 尝试第二个分支
|
||||||
|
.blackboardCompare('active', true, 'equals', 'CheckActive') // 6. 条件成功
|
||||||
|
.log('执行动作', 'DoAction') // 7. 动作成功
|
||||||
|
.end() // 8. 序列成功,选择器成功
|
||||||
|
.end() // 9. 整个树成功
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
执行流程图:
|
||||||
|
|
||||||
|
```
|
||||||
|
Root(Selector)
|
||||||
|
→ Branch1(Sequence)
|
||||||
|
→ CheckReady: Failure
|
||||||
|
→ Branch1 fails
|
||||||
|
→ Branch2(Sequence)
|
||||||
|
→ CheckActive: Success
|
||||||
|
→ DoAction: Success
|
||||||
|
→ Branch2 succeeds
|
||||||
|
→ Root succeeds
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Runtime架构
|
||||||
|
|
||||||
|
本框架的行为树采用Runtime执行器架构:
|
||||||
|
|
||||||
|
### 核心组件
|
||||||
|
|
||||||
|
- **BehaviorTreeData**: 纯数据结构,描述行为树的结构和配置
|
||||||
|
- **BehaviorTreeRuntimeComponent**: 运行时组件,管理执行状态和黑板
|
||||||
|
- **BehaviorTreeExecutionSystem**: 执行系统,驱动行为树运行
|
||||||
|
- **INodeExecutor**: 节点执行器接口,定义节点的执行逻辑
|
||||||
|
- **NodeExecutionContext**: 执行上下文,包含执行所需的所有信息
|
||||||
|
|
||||||
|
### 架构特点
|
||||||
|
|
||||||
|
1. **数据与逻辑分离**: BehaviorTreeData是纯数据,执行逻辑在执行器中
|
||||||
|
2. **无状态执行器**: 执行器实例可以在多个节点间共享,状态存储在Runtime中
|
||||||
|
3. **类型安全**: 通过TypeScript类型系统保证类型安全
|
||||||
|
4. **高性能**: 避免不必要的对象创建,优化内存使用
|
||||||
|
|
||||||
|
### 数据流
|
||||||
|
|
||||||
|
```
|
||||||
|
BehaviorTreeBuilder
|
||||||
|
↓ (构建)
|
||||||
|
BehaviorTreeData
|
||||||
|
↓ (加载到)
|
||||||
|
BehaviorTreeAssetManager
|
||||||
|
↓ (读取)
|
||||||
|
BehaviorTreeExecutionSystem
|
||||||
|
↓ (执行)
|
||||||
|
INodeExecutor.execute(context)
|
||||||
|
↓ (返回)
|
||||||
|
TaskStatus
|
||||||
|
↓ (更新)
|
||||||
|
NodeRuntimeState
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
现在你已经理解了行为树的核心概念,接下来可以:
|
||||||
|
|
||||||
|
- 查看[快速开始](./getting-started.md)创建第一个行为树
|
||||||
|
- 学习[自定义节点执行器](./custom-actions.md)创建自定义节点
|
||||||
|
- 探索[高级用法](./advanced-usage.md)了解更多功能
|
||||||
|
- 阅读[最佳实践](./best-practices.md)学习设计模式
|
||||||
1025
docs/guide/behavior-tree/custom-actions.md
Normal file
1025
docs/guide/behavior-tree/custom-actions.md
Normal file
File diff suppressed because it is too large
Load Diff
119
docs/guide/behavior-tree/editor-guide.md
Normal file
119
docs/guide/behavior-tree/editor-guide.md
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
# 行为树编辑器使用指南
|
||||||
|
|
||||||
|
行为树编辑器提供了可视化的方式来创建和编辑行为树。
|
||||||
|
|
||||||
|
## 启动编辑器
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd packages/editor-app
|
||||||
|
npm run tauri:dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## 基本操作
|
||||||
|
|
||||||
|
### 打开行为树编辑器
|
||||||
|
|
||||||
|
通过以下方式打开行为树编辑器窗口:
|
||||||
|
|
||||||
|
1. 在资产浏览器中双击 `.btree` 文件
|
||||||
|
2. 菜单栏:`窗口` → 选择行为树编辑器相关插件
|
||||||
|
|
||||||
|
### 创建新行为树
|
||||||
|
|
||||||
|
在行为树编辑器窗口的工具栏中点击"新建"按钮(加号图标)
|
||||||
|
|
||||||
|
### 保存行为树
|
||||||
|
|
||||||
|
在行为树编辑器窗口的工具栏中点击"保存"按钮(磁盘图标)
|
||||||
|
|
||||||
|
### 添加节点
|
||||||
|
|
||||||
|
从左侧节点面板拖拽节点到画布:
|
||||||
|
- 复合节点:Selector、Sequence、Parallel
|
||||||
|
- 装饰器:Inverter、Repeater、UntilFail等
|
||||||
|
- 动作节点:ExecuteAction、Wait等
|
||||||
|
- 条件节点:Condition
|
||||||
|
|
||||||
|
### 连接节点
|
||||||
|
|
||||||
|
拖拽父节点底部的连接点到子节点顶部建立连接
|
||||||
|
|
||||||
|
### 删除节点
|
||||||
|
|
||||||
|
选中节点后按 `Delete` 或 `Backspace` 键
|
||||||
|
|
||||||
|
### 编辑属性
|
||||||
|
|
||||||
|
点击节点后在右侧属性面板中编辑节点参数
|
||||||
|
|
||||||
|
## 黑板变量
|
||||||
|
|
||||||
|
在黑板面板中管理共享数据:
|
||||||
|
|
||||||
|
1. 点击"添加变量"按钮
|
||||||
|
2. 输入变量名、选择类型并设置默认值
|
||||||
|
3. 在节点中通过变量名引用黑板变量
|
||||||
|
|
||||||
|
支持的变量类型:
|
||||||
|
- String:字符串
|
||||||
|
- Number:数字
|
||||||
|
- Boolean:布尔值
|
||||||
|
- Vector2:二维向量
|
||||||
|
- Vector3:三维向量
|
||||||
|
- Object:对象引用
|
||||||
|
- Array:数组
|
||||||
|
|
||||||
|
## 导出运行时资产
|
||||||
|
|
||||||
|
### 导出步骤
|
||||||
|
|
||||||
|
1. 点击工具栏的"导出"按钮
|
||||||
|
2. 选择导出模式:
|
||||||
|
- 当前文件:仅导出当前打开的行为树
|
||||||
|
- 工作区导出:导出项目中所有行为树
|
||||||
|
3. 选择资产输出路径
|
||||||
|
4. 选择TypeScript类型定义输出路径
|
||||||
|
5. 为每个文件选择导出格式:
|
||||||
|
- 二进制:.btree.bin(默认,文件更小,加载更快)
|
||||||
|
- JSON:.btree.json(可读性好,便于调试)
|
||||||
|
6. 点击"导出"按钮
|
||||||
|
|
||||||
|
### 加载运行时资产
|
||||||
|
|
||||||
|
编辑器导出的文件是编辑器格式,包含UI布局信息。当前版本中,从编辑器导出的资产可以使用Builder API在代码中重新构建,或者等待资产加载系统的完善。
|
||||||
|
|
||||||
|
推荐使用Builder API创建行为树:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { BehaviorTreeBuilder, BehaviorTreeStarter } from '@esengine/behavior-tree';
|
||||||
|
import { Core, Scene } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
// 使用Builder创建行为树
|
||||||
|
const tree = BehaviorTreeBuilder.create('EnemyAI')
|
||||||
|
.defineBlackboardVariable('health', 100)
|
||||||
|
.defineBlackboardVariable('target', null)
|
||||||
|
.selector('MainBehavior')
|
||||||
|
.sequence('AttackBranch')
|
||||||
|
.blackboardCompare('health', 50, 'greater')
|
||||||
|
.log('攻击玩家', 'Attack')
|
||||||
|
.end()
|
||||||
|
.log('逃离战斗', 'Flee')
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 启动行为树
|
||||||
|
const entity = scene.createEntity('Enemy');
|
||||||
|
BehaviorTreeStarter.start(entity, tree);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 支持的操作
|
||||||
|
|
||||||
|
- `Delete` / `Backspace`:删除选中的节点或连线
|
||||||
|
- `Ctrl` + 点击:多选节点
|
||||||
|
- 框选:拖拽空白区域进行框选
|
||||||
|
- 拖拽画布:按住鼠标中键或空格键拖拽
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
- 查看[编辑器工作流](./editor-workflow.md)了解完整的开发流程
|
||||||
|
- 查看[自定义节点执行器](./custom-actions.md)学习如何扩展节点
|
||||||
253
docs/guide/behavior-tree/editor-workflow.md
Normal file
253
docs/guide/behavior-tree/editor-workflow.md
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
# 编辑器工作流
|
||||||
|
|
||||||
|
本教程介绍如何使用行为树编辑器创建AI,并在游戏中加载使用。
|
||||||
|
|
||||||
|
## 完整流程
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 启动编辑器
|
||||||
|
2. 创建行为树并定义黑板变量
|
||||||
|
3. 添加和配置节点
|
||||||
|
4. 导出JSON文件
|
||||||
|
5. 在游戏中加载并使用
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用编辑器创建
|
||||||
|
|
||||||
|
### 启动编辑器
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd packages/editor-app
|
||||||
|
npm run tauri:dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 基本操作
|
||||||
|
|
||||||
|
1. **创建行为树**:`文件` → `新建项目` → 创建行为树文件
|
||||||
|
2. **定义黑板变量**:在黑板面板中添加共享变量
|
||||||
|
3. **添加节点**:从节点面板拖拽到画布
|
||||||
|
4. **连接节点**:拖拽连接点建立父子关系
|
||||||
|
5. **配置属性**:选中节点后在属性面板编辑
|
||||||
|
6. **导出**:`文件` → `导出` → `JSON格式`
|
||||||
|
|
||||||
|
### 示例:敌人AI的黑板变量
|
||||||
|
|
||||||
|
在编辑器黑板面板中定义:
|
||||||
|
|
||||||
|
```
|
||||||
|
health: Number = 100
|
||||||
|
target: Object = null
|
||||||
|
moveSpeed: Number = 5.0
|
||||||
|
attackRange: Number = 2.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例:行为树结构
|
||||||
|
|
||||||
|
```
|
||||||
|
Root: Selector
|
||||||
|
├── Combat Sequence
|
||||||
|
│ ├── CheckHasTarget (Condition)
|
||||||
|
│ ├── CheckInAttackRange (Condition)
|
||||||
|
│ └── ExecuteAttack (Action)
|
||||||
|
├── Patrol Sequence
|
||||||
|
│ ├── MoveToNextPatrolPoint (Action)
|
||||||
|
│ └── Wait 2s
|
||||||
|
└── Idle (Action)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 在游戏中使用
|
||||||
|
|
||||||
|
### 使用Builder API创建
|
||||||
|
|
||||||
|
推荐使用Builder API在代码中创建行为树:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Core, Scene } from '@esengine/ecs-framework';
|
||||||
|
import {
|
||||||
|
BehaviorTreePlugin,
|
||||||
|
BehaviorTreeBuilder,
|
||||||
|
BehaviorTreeStarter,
|
||||||
|
BehaviorTreeRuntimeComponent
|
||||||
|
} from '@esengine/behavior-tree';
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
Core.create();
|
||||||
|
const plugin = new BehaviorTreePlugin();
|
||||||
|
await Core.installPlugin(plugin);
|
||||||
|
|
||||||
|
const scene = new Scene();
|
||||||
|
plugin.setupScene(scene);
|
||||||
|
Core.setScene(scene);
|
||||||
|
|
||||||
|
// 使用Builder创建行为树
|
||||||
|
const tree = BehaviorTreeBuilder.create('EnemyAI')
|
||||||
|
.defineBlackboardVariable('health', 100)
|
||||||
|
.defineBlackboardVariable('target', null)
|
||||||
|
.defineBlackboardVariable('moveSpeed', 5.0)
|
||||||
|
.selector('MainBehavior')
|
||||||
|
.sequence('AttackBranch')
|
||||||
|
.blackboardExists('target')
|
||||||
|
.blackboardCompare('health', 30, 'greater')
|
||||||
|
.log('攻击目标', 'Attack')
|
||||||
|
.end()
|
||||||
|
.log('巡逻', 'Patrol')
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 创建实体并启动行为树
|
||||||
|
const entity = scene.createEntity('Enemy');
|
||||||
|
BehaviorTreeStarter.start(entity, tree);
|
||||||
|
|
||||||
|
// 访问和修改黑板
|
||||||
|
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||||
|
runtime?.setBlackboardValue('target', someTarget);
|
||||||
|
|
||||||
|
// 游戏循环
|
||||||
|
setInterval(() => {
|
||||||
|
Core.update(0.016); // 60 FPS
|
||||||
|
}, 16);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 实现自定义执行器
|
||||||
|
|
||||||
|
要扩展行为树的功能,需要创建自定义执行器(详见[自定义节点执行器](./custom-actions.md)):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
INodeExecutor,
|
||||||
|
NodeExecutionContext,
|
||||||
|
BindingHelper,
|
||||||
|
NodeExecutorMetadata
|
||||||
|
} from '@esengine/behavior-tree';
|
||||||
|
import { TaskStatus, NodeType } from '@esengine/behavior-tree';
|
||||||
|
|
||||||
|
@NodeExecutorMetadata({
|
||||||
|
implementationType: 'AttackAction',
|
||||||
|
nodeType: NodeType.Action,
|
||||||
|
displayName: '攻击目标',
|
||||||
|
description: '对目标造成伤害',
|
||||||
|
category: '战斗',
|
||||||
|
configSchema: {
|
||||||
|
damage: {
|
||||||
|
type: 'number',
|
||||||
|
default: 10,
|
||||||
|
supportBinding: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
export class AttackAction implements INodeExecutor {
|
||||||
|
execute(context: NodeExecutionContext): TaskStatus {
|
||||||
|
const damage = BindingHelper.getValue<number>(context, 'damage', 10);
|
||||||
|
const target = context.runtime.getBlackboardValue('target');
|
||||||
|
|
||||||
|
if (!target) {
|
||||||
|
return TaskStatus.Failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行攻击逻辑
|
||||||
|
performAttack(context.entity, target, damage);
|
||||||
|
return TaskStatus.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
reset(context: NodeExecutionContext): void {
|
||||||
|
// 清理状态
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 调试技巧
|
||||||
|
|
||||||
|
### 1. 使用日志节点
|
||||||
|
|
||||||
|
在行为树中添加Log节点输出调试信息:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const tree = BehaviorTreeBuilder.create('DebugAI')
|
||||||
|
.log('开始战斗序列', 'StartCombat')
|
||||||
|
.sequence('Combat')
|
||||||
|
.blackboardCompare('health', 0, 'greater')
|
||||||
|
.log('执行攻击', 'Attack')
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 监控黑板状态
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||||
|
console.log('黑板变量:', runtime?.getAllBlackboardVariables());
|
||||||
|
console.log('活动节点:', Array.from(runtime?.activeNodeIds || []));
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 在自定义执行器中调试
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export class DebugAction implements INodeExecutor {
|
||||||
|
execute(context: NodeExecutionContext): TaskStatus {
|
||||||
|
const { nodeData, runtime, state } = context;
|
||||||
|
|
||||||
|
console.group(`[${nodeData.name}]`);
|
||||||
|
console.log('配置:', nodeData.config);
|
||||||
|
console.log('状态:', state);
|
||||||
|
console.log('黑板:', runtime.getAllBlackboardVariables());
|
||||||
|
console.groupEnd();
|
||||||
|
|
||||||
|
return TaskStatus.Success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 完整示例
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Core, Scene } from '@esengine/ecs-framework';
|
||||||
|
import {
|
||||||
|
BehaviorTreePlugin,
|
||||||
|
BehaviorTreeBuilder,
|
||||||
|
BehaviorTreeStarter,
|
||||||
|
BehaviorTreeRuntimeComponent
|
||||||
|
} from '@esengine/behavior-tree';
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
Core.create();
|
||||||
|
const plugin = new BehaviorTreePlugin();
|
||||||
|
await Core.installPlugin(plugin);
|
||||||
|
|
||||||
|
const scene = new Scene();
|
||||||
|
plugin.setupScene(scene);
|
||||||
|
Core.setScene(scene);
|
||||||
|
|
||||||
|
// 使用Builder API构建行为树
|
||||||
|
const tree = BehaviorTreeBuilder.create('EnemyAI')
|
||||||
|
.defineBlackboardVariable('health', 100)
|
||||||
|
.defineBlackboardVariable('hasTarget', false)
|
||||||
|
.selector('Root')
|
||||||
|
.sequence('Combat')
|
||||||
|
.blackboardCompare('hasTarget', true, 'equals')
|
||||||
|
.log('攻击玩家', 'Attack')
|
||||||
|
.end()
|
||||||
|
.log('空闲', 'Idle')
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 创建实体并启动
|
||||||
|
const entity = scene.createEntity('Enemy');
|
||||||
|
BehaviorTreeStarter.start(entity, tree);
|
||||||
|
|
||||||
|
// 模拟发现目标
|
||||||
|
setTimeout(() => {
|
||||||
|
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||||
|
runtime?.setBlackboardValue('hasTarget', true);
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
// 游戏循环
|
||||||
|
setInterval(() => {
|
||||||
|
Core.update(0.016);
|
||||||
|
}, 16);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
- 查看[自定义节点执行器](./custom-actions.md)学习如何创建自定义节点
|
||||||
|
- 查看[高级用法](./advanced-usage.md)了解性能优化等高级特性
|
||||||
|
- 查看[最佳实践](./best-practices.md)优化你的AI设计
|
||||||
385
docs/guide/behavior-tree/getting-started.md
Normal file
385
docs/guide/behavior-tree/getting-started.md
Normal file
@@ -0,0 +1,385 @@
|
|||||||
|
# 快速开始
|
||||||
|
|
||||||
|
本教程将引导你在5分钟内创建第一个行为树。
|
||||||
|
|
||||||
|
## 安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @esengine/behavior-tree
|
||||||
|
```
|
||||||
|
|
||||||
|
## 第一个行为树
|
||||||
|
|
||||||
|
让我们创建一个简单的AI行为树,实现"巡逻-发现敌人-攻击"的逻辑。
|
||||||
|
|
||||||
|
### 步骤1: 导入依赖
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Core, Scene, Entity } from '@esengine/ecs-framework';
|
||||||
|
import {
|
||||||
|
BehaviorTreeBuilder,
|
||||||
|
BehaviorTreeStarter,
|
||||||
|
BehaviorTreePlugin
|
||||||
|
} from '@esengine/behavior-tree';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤2: 初始化Core并安装插件
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
Core.create();
|
||||||
|
const plugin = new BehaviorTreePlugin();
|
||||||
|
await Core.installPlugin(plugin);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤3: 创建场景并设置行为树系统
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const scene = new Scene();
|
||||||
|
plugin.setupScene(scene);
|
||||||
|
Core.setScene(scene);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤4: 构建行为树数据
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const guardAITree = BehaviorTreeBuilder.create('GuardAI')
|
||||||
|
// 定义黑板变量
|
||||||
|
.defineBlackboardVariable('health', 100)
|
||||||
|
.defineBlackboardVariable('hasEnemy', false)
|
||||||
|
.defineBlackboardVariable('patrolPoint', 0)
|
||||||
|
|
||||||
|
// 根选择器
|
||||||
|
.selector('RootSelector')
|
||||||
|
// 分支1: 如果发现敌人且生命值高,则攻击
|
||||||
|
.selector('CombatBranch')
|
||||||
|
.blackboardExists('hasEnemy', 'CheckEnemy')
|
||||||
|
.blackboardCompare('health', 30, 'greater', 'CheckHealth')
|
||||||
|
.log('守卫正在攻击敌人', 'Attack')
|
||||||
|
.end()
|
||||||
|
|
||||||
|
// 分支2: 如果生命值低,则逃跑
|
||||||
|
.selector('FleeBranch')
|
||||||
|
.blackboardCompare('health', 30, 'lessOrEqual', 'CheckLowHealth')
|
||||||
|
.log('守卫生命值过低,正在逃跑', 'Flee')
|
||||||
|
.end()
|
||||||
|
|
||||||
|
// 分支3: 默认巡逻
|
||||||
|
.selector('PatrolBranch')
|
||||||
|
.modifyBlackboardValue('patrolPoint', 'add', 1, 'IncrementPatrol')
|
||||||
|
.log('守卫正在巡逻', 'Patrol')
|
||||||
|
.wait(2.0, 'WaitAtPoint')
|
||||||
|
.end()
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤5: 创建实体并启动行为树
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 创建守卫实体
|
||||||
|
const guardEntity = scene.createEntity('Guard');
|
||||||
|
|
||||||
|
// 启动行为树
|
||||||
|
BehaviorTreeStarter.start(guardEntity, guardAITree);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤6: 运行游戏循环
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 模拟游戏循环
|
||||||
|
setInterval(() => {
|
||||||
|
Core.update(0.1); // 传入deltaTime(秒)
|
||||||
|
}, 100); // 每100ms更新一次
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤7: 模拟游戏事件
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 5秒后模拟发现敌人
|
||||||
|
setTimeout(() => {
|
||||||
|
const runtime = guardEntity.getComponent(BehaviorTreeRuntimeComponent);
|
||||||
|
runtime?.setBlackboardValue('hasEnemy', true);
|
||||||
|
console.log('发现敌人!');
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
// 10秒后模拟受伤
|
||||||
|
setTimeout(() => {
|
||||||
|
const runtime = guardEntity.getComponent(BehaviorTreeRuntimeComponent);
|
||||||
|
runtime?.setBlackboardValue('health', 20);
|
||||||
|
console.log('守卫受伤!');
|
||||||
|
}, 10000);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 完整代码
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Core, Scene } from '@esengine/ecs-framework';
|
||||||
|
import {
|
||||||
|
BehaviorTreeBuilder,
|
||||||
|
BehaviorTreeStarter,
|
||||||
|
BehaviorTreePlugin,
|
||||||
|
BehaviorTreeRuntimeComponent
|
||||||
|
} from '@esengine/behavior-tree';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
// 1. 创建核心并安装插件
|
||||||
|
Core.create();
|
||||||
|
const plugin = new BehaviorTreePlugin();
|
||||||
|
await Core.installPlugin(plugin);
|
||||||
|
|
||||||
|
// 2. 创建场景
|
||||||
|
const scene = new Scene();
|
||||||
|
plugin.setupScene(scene);
|
||||||
|
Core.setScene(scene);
|
||||||
|
|
||||||
|
// 3. 构建行为树数据
|
||||||
|
const guardAITree = BehaviorTreeBuilder.create('GuardAI')
|
||||||
|
.defineBlackboardVariable('health', 100)
|
||||||
|
.defineBlackboardVariable('hasEnemy', false)
|
||||||
|
.defineBlackboardVariable('patrolPoint', 0)
|
||||||
|
.selector('RootSelector')
|
||||||
|
.selector('CombatBranch')
|
||||||
|
.blackboardExists('hasEnemy')
|
||||||
|
.blackboardCompare('health', 30, 'greater')
|
||||||
|
.log('守卫正在攻击敌人')
|
||||||
|
.end()
|
||||||
|
.selector('FleeBranch')
|
||||||
|
.blackboardCompare('health', 30, 'lessOrEqual')
|
||||||
|
.log('守卫生命值过低,正在逃跑')
|
||||||
|
.end()
|
||||||
|
.selector('PatrolBranch')
|
||||||
|
.modifyBlackboardValue('patrolPoint', 'add', 1)
|
||||||
|
.log('守卫正在巡逻')
|
||||||
|
.wait(2.0)
|
||||||
|
.end()
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 4. 创建守卫实体并启动行为树
|
||||||
|
const guardEntity = scene.createEntity('Guard');
|
||||||
|
BehaviorTreeStarter.start(guardEntity, guardAITree);
|
||||||
|
|
||||||
|
// 5. 运行游戏循环
|
||||||
|
setInterval(() => {
|
||||||
|
Core.update(0.1);
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// 6. 模拟游戏事件
|
||||||
|
setTimeout(() => {
|
||||||
|
const runtime = guardEntity.getComponent(BehaviorTreeRuntimeComponent);
|
||||||
|
runtime?.setBlackboardValue('hasEnemy', true);
|
||||||
|
console.log('发现敌人!');
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const runtime = guardEntity.getComponent(BehaviorTreeRuntimeComponent);
|
||||||
|
runtime?.setBlackboardValue('health', 20);
|
||||||
|
console.log('守卫受伤!');
|
||||||
|
}, 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
|
```
|
||||||
|
|
||||||
|
## 运行结果
|
||||||
|
|
||||||
|
运行程序后,你会看到类似的输出:
|
||||||
|
|
||||||
|
```
|
||||||
|
守卫正在巡逻
|
||||||
|
守卫正在巡逻
|
||||||
|
守卫正在巡逻
|
||||||
|
发现敌人!
|
||||||
|
守卫正在攻击敌人
|
||||||
|
守卫正在攻击敌人
|
||||||
|
守卫受伤!
|
||||||
|
守卫生命值过低,正在逃跑
|
||||||
|
```
|
||||||
|
|
||||||
|
## 理解代码
|
||||||
|
|
||||||
|
### 黑板变量
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
.defineBlackboardVariable('health', 100)
|
||||||
|
.defineBlackboardVariable('hasEnemy', false)
|
||||||
|
.defineBlackboardVariable('patrolPoint', 0)
|
||||||
|
```
|
||||||
|
|
||||||
|
黑板用于在节点之间共享数据。这里定义了三个变量:
|
||||||
|
- `health`: 守卫的生命值
|
||||||
|
- `hasEnemy`: 是否发现敌人
|
||||||
|
- `patrolPoint`: 当前巡逻点编号
|
||||||
|
|
||||||
|
### 选择器节点
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
.selector('RootSelector')
|
||||||
|
// 分支1
|
||||||
|
// 分支2
|
||||||
|
// 分支3
|
||||||
|
.end()
|
||||||
|
```
|
||||||
|
|
||||||
|
选择器按顺序尝试执行子节点,直到某个子节点返回成功。类似于编程中的 `if-else if-else`。
|
||||||
|
|
||||||
|
### 条件节点
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
.blackboardExists('hasEnemy') // 检查变量是否存在
|
||||||
|
.blackboardCompare('health', 30, 'greater') // 比较变量值
|
||||||
|
```
|
||||||
|
|
||||||
|
条件节点用于检查黑板变量的值。
|
||||||
|
|
||||||
|
### 动作节点
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
.log('守卫正在攻击敌人') // 输出日志
|
||||||
|
.wait(2.0) // 等待2秒
|
||||||
|
.modifyBlackboardValue('patrolPoint', 'add', 1) // 修改黑板值
|
||||||
|
```
|
||||||
|
|
||||||
|
动作节点执行具体的操作。
|
||||||
|
|
||||||
|
### Runtime组件
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const runtime = guardEntity.getComponent(BehaviorTreeRuntimeComponent);
|
||||||
|
runtime?.setBlackboardValue('hasEnemy', true);
|
||||||
|
runtime?.getBlackboardValue('health');
|
||||||
|
```
|
||||||
|
|
||||||
|
通过`BehaviorTreeRuntimeComponent`访问和修改黑板变量。
|
||||||
|
|
||||||
|
## 常见任务状态
|
||||||
|
|
||||||
|
行为树的每个节点返回以下状态之一:
|
||||||
|
|
||||||
|
- **Success**: 任务成功完成
|
||||||
|
- **Failure**: 任务执行失败
|
||||||
|
- **Running**: 任务正在执行,需要在后续帧继续
|
||||||
|
- **Invalid**: 无效状态(未初始化或已重置)
|
||||||
|
|
||||||
|
## 内置节点
|
||||||
|
|
||||||
|
### 复合节点
|
||||||
|
|
||||||
|
- `sequence()` - 序列节点,按顺序执行所有子节点
|
||||||
|
- `selector()` - 选择器节点,按顺序尝试子节点直到成功
|
||||||
|
- `parallel()` - 并行节点,同时执行多个子节点
|
||||||
|
- `parallelSelector()` - 并行选择器
|
||||||
|
- `randomSequence()` - 随机序列
|
||||||
|
- `randomSelector()` - 随机选择器
|
||||||
|
|
||||||
|
### 装饰器节点
|
||||||
|
|
||||||
|
- `inverter()` - 反转子节点结果
|
||||||
|
- `repeater(count)` - 重复执行子节点
|
||||||
|
- `alwaysSucceed()` - 总是返回成功
|
||||||
|
- `alwaysFail()` - 总是返回失败
|
||||||
|
- `untilSuccess()` - 重复直到成功
|
||||||
|
- `untilFail()` - 重复直到失败
|
||||||
|
- `conditional(key, value, operator)` - 条件装饰器
|
||||||
|
- `cooldown(time)` - 冷却装饰器
|
||||||
|
- `timeout(time)` - 超时装饰器
|
||||||
|
|
||||||
|
### 动作节点
|
||||||
|
|
||||||
|
- `wait(duration)` - 等待指定时间
|
||||||
|
- `log(message)` - 输出日志
|
||||||
|
- `setBlackboardValue(key, value)` - 设置黑板值
|
||||||
|
- `modifyBlackboardValue(key, operation, value)` - 修改黑板值
|
||||||
|
- `executeAction(actionName)` - 执行自定义动作
|
||||||
|
|
||||||
|
### 条件节点
|
||||||
|
|
||||||
|
- `blackboardExists(key)` - 检查变量是否存在
|
||||||
|
- `blackboardCompare(key, value, operator)` - 比较黑板值
|
||||||
|
- `randomProbability(probability)` - 随机概率
|
||||||
|
- `executeCondition(conditionName)` - 执行自定义条件
|
||||||
|
|
||||||
|
## 控制行为树
|
||||||
|
|
||||||
|
### 启动
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
BehaviorTreeStarter.start(entity, treeData);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 停止
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
BehaviorTreeStarter.stop(entity);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 暂停和恢复
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
BehaviorTreeStarter.pause(entity);
|
||||||
|
// ... 一段时间后
|
||||||
|
BehaviorTreeStarter.resume(entity);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 重启
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
BehaviorTreeStarter.restart(entity);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
现在你已经创建了第一个行为树,接下来可以:
|
||||||
|
|
||||||
|
1. 学习[核心概念](./core-concepts.md)深入理解行为树原理
|
||||||
|
2. 学习[资产管理](./asset-management.md)了解如何加载和复用行为树、使用子树
|
||||||
|
3. 查看[自定义节点执行器](./custom-actions.md)学习如何创建自定义节点
|
||||||
|
4. 根据你的场景查看集成教程:[Cocos Creator](./cocos-integration.md) 或 [Node.js](./nodejs-usage.md)
|
||||||
|
5. 查看[高级用法](./advanced-usage.md)了解更多功能
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### 为什么行为树不执行?
|
||||||
|
|
||||||
|
确保:
|
||||||
|
1. 已经安装了 `BehaviorTreePlugin`
|
||||||
|
2. 调用了 `plugin.setupScene(scene)`
|
||||||
|
3. 调用了 `BehaviorTreeStarter.start(entity, treeData)`
|
||||||
|
4. 在游戏循环中调用了 `Core.update(deltaTime)`
|
||||||
|
|
||||||
|
### 如何访问黑板变量?
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||||
|
|
||||||
|
// 读取
|
||||||
|
const health = runtime?.getBlackboardValue('health');
|
||||||
|
|
||||||
|
// 写入
|
||||||
|
runtime?.setBlackboardValue('health', 50);
|
||||||
|
|
||||||
|
// 获取所有变量
|
||||||
|
const allVars = runtime?.getAllBlackboardVariables();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 如何调试行为树?
|
||||||
|
|
||||||
|
使用日志节点:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
.log('到达这个节点', 'DebugLog')
|
||||||
|
```
|
||||||
|
|
||||||
|
或者在代码中监控黑板:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||||
|
console.log('黑板变量:', runtime?.getAllBlackboardVariables());
|
||||||
|
console.log('活动节点:', Array.from(runtime?.activeNodeIds || []));
|
||||||
|
```
|
||||||
|
|
||||||
|
### 如何使用自定义逻辑?
|
||||||
|
|
||||||
|
内置的`executeAction`和`executeCondition`节点只是占位符。要实现真正的自定义逻辑,你需要创建自定义执行器:
|
||||||
|
|
||||||
|
参见[自定义节点执行器](./custom-actions.md)学习如何创建。
|
||||||
197
docs/guide/behavior-tree/index.md
Normal file
197
docs/guide/behavior-tree/index.md
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
# 行为树系统
|
||||||
|
|
||||||
|
行为树(Behavior Tree)是一种用于游戏AI和自动化控制的强大工具。本框架提供了基于Runtime执行器架构的行为树系统,具有高性能、类型安全、易于扩展的特点。
|
||||||
|
|
||||||
|
## 什么是行为树?
|
||||||
|
|
||||||
|
行为树是一种层次化的任务执行结构,由多个节点组成,每个节点负责特定的任务。行为树特别适合于:
|
||||||
|
|
||||||
|
- 游戏AI(敌人、NPC行为)
|
||||||
|
- 状态机的替代方案
|
||||||
|
- 复杂的决策逻辑
|
||||||
|
- 可视化的行为设计
|
||||||
|
|
||||||
|
## 核心特性
|
||||||
|
|
||||||
|
### Runtime执行器架构
|
||||||
|
- 数据与逻辑分离
|
||||||
|
- 无状态执行器设计
|
||||||
|
- 高性能执行
|
||||||
|
- 类型安全
|
||||||
|
|
||||||
|
### 可视化编辑器
|
||||||
|
- 图形化节点编辑
|
||||||
|
- 实时预览和调试
|
||||||
|
- 拖拽式节点创建
|
||||||
|
- 属性连接和绑定
|
||||||
|
|
||||||
|
### 灵活的黑板系统
|
||||||
|
- 本地黑板(单个行为树)
|
||||||
|
- 全局黑板(所有行为树共享)
|
||||||
|
- 类型安全的变量访问
|
||||||
|
- 支持属性绑定
|
||||||
|
|
||||||
|
### 插件系统
|
||||||
|
- 自动注册机制
|
||||||
|
- 装饰器声明元数据
|
||||||
|
- 支持多语言
|
||||||
|
- 易于扩展
|
||||||
|
|
||||||
|
## 文档导航
|
||||||
|
|
||||||
|
### 入门教程
|
||||||
|
|
||||||
|
- **[快速开始](./getting-started.md)** - 5分钟上手行为树
|
||||||
|
- **[核心概念](./core-concepts.md)** - 理解行为树的基本原理
|
||||||
|
|
||||||
|
### 编辑器使用
|
||||||
|
|
||||||
|
- **[编辑器使用指南](./editor-guide.md)** - 可视化创建行为树
|
||||||
|
- **[编辑器工作流](./editor-workflow.md)** - 完整的开发流程
|
||||||
|
|
||||||
|
### 资源管理
|
||||||
|
|
||||||
|
- **[资产管理](./asset-management.md)** - 加载、管理和复用行为树资产、使用子树
|
||||||
|
|
||||||
|
### 引擎集成
|
||||||
|
|
||||||
|
- **[Cocos Creator 集成](./cocos-integration.md)** - 在 Cocos Creator 中使用行为树
|
||||||
|
- **[Laya 引擎集成](./laya-integration.md)** - 在 Laya 中使用行为树
|
||||||
|
- **[Node.js 服务端使用](./nodejs-usage.md)** - 在服务器、聊天机器人等场景中使用行为树
|
||||||
|
|
||||||
|
### 高级主题
|
||||||
|
|
||||||
|
- **[高级用法](./advanced-usage.md)** - 性能优化、调试技巧
|
||||||
|
- **[自定义节点执行器](./custom-actions.md)** - 创建自定义行为节点
|
||||||
|
- **[最佳实践](./best-practices.md)** - 行为树设计模式和技巧
|
||||||
|
|
||||||
|
## 快速示例
|
||||||
|
|
||||||
|
### 使用Builder创建
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Core, Scene } from '@esengine/ecs-framework';
|
||||||
|
import {
|
||||||
|
BehaviorTreeBuilder,
|
||||||
|
BehaviorTreeStarter,
|
||||||
|
BehaviorTreePlugin
|
||||||
|
} from '@esengine/behavior-tree';
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
Core.create();
|
||||||
|
const plugin = new BehaviorTreePlugin();
|
||||||
|
await Core.installPlugin(plugin);
|
||||||
|
|
||||||
|
const scene = new Scene();
|
||||||
|
plugin.setupScene(scene);
|
||||||
|
Core.setScene(scene);
|
||||||
|
|
||||||
|
// 创建行为树
|
||||||
|
const enemyAI = BehaviorTreeBuilder.create('EnemyAI')
|
||||||
|
.defineBlackboardVariable('health', 100)
|
||||||
|
.defineBlackboardVariable('target', null)
|
||||||
|
.selector('MainBehavior')
|
||||||
|
// 如果生命值高,则攻击
|
||||||
|
.sequence('AttackBranch')
|
||||||
|
.blackboardCompare('health', 50, 'greater')
|
||||||
|
.log('攻击玩家', 'Attack')
|
||||||
|
.end()
|
||||||
|
// 否则逃跑
|
||||||
|
.log('逃离战斗', 'Flee')
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 启动AI
|
||||||
|
const entity = scene.createEntity('Enemy');
|
||||||
|
BehaviorTreeStarter.start(entity, enemyAI);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 使用编辑器创建
|
||||||
|
|
||||||
|
1. 打开行为树编辑器
|
||||||
|
2. 创建新的行为树资产
|
||||||
|
3. 拖拽节点到画布
|
||||||
|
4. 配置节点属性和连接
|
||||||
|
5. 保存并在代码中使用
|
||||||
|
|
||||||
|
## 架构说明
|
||||||
|
|
||||||
|
### Runtime执行器架构
|
||||||
|
|
||||||
|
本框架采用Runtime执行器架构,将节点定义和执行逻辑分离:
|
||||||
|
|
||||||
|
**核心组件:**
|
||||||
|
- `BehaviorTreeData`: 纯数据结构,描述行为树
|
||||||
|
- `BehaviorTreeRuntimeComponent`: 运行时组件,管理状态和黑板
|
||||||
|
- `BehaviorTreeExecutionSystem`: 执行系统,驱动行为树运行
|
||||||
|
- `INodeExecutor`: 节点执行器接口
|
||||||
|
- `NodeExecutionContext`: 执行上下文
|
||||||
|
|
||||||
|
**优势:**
|
||||||
|
- 数据与逻辑分离,易于序列化
|
||||||
|
- 执行器无状态,可复用
|
||||||
|
- 类型安全,编译时检查
|
||||||
|
- 高性能执行
|
||||||
|
|
||||||
|
### 自定义执行器
|
||||||
|
|
||||||
|
创建自定义节点非常简单:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
INodeExecutor,
|
||||||
|
NodeExecutionContext,
|
||||||
|
BindingHelper,
|
||||||
|
NodeExecutorMetadata
|
||||||
|
} from '@esengine/behavior-tree';
|
||||||
|
import { TaskStatus, NodeType } from '@esengine/behavior-tree';
|
||||||
|
|
||||||
|
@NodeExecutorMetadata({
|
||||||
|
implementationType: 'AttackAction',
|
||||||
|
nodeType: NodeType.Action,
|
||||||
|
displayName: '攻击',
|
||||||
|
description: '攻击目标',
|
||||||
|
category: '战斗',
|
||||||
|
configSchema: {
|
||||||
|
damage: {
|
||||||
|
type: 'number',
|
||||||
|
default: 10,
|
||||||
|
supportBinding: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
export class AttackAction implements INodeExecutor {
|
||||||
|
execute(context: NodeExecutionContext): TaskStatus {
|
||||||
|
const damage = BindingHelper.getValue<number>(context, 'damage', 10);
|
||||||
|
const target = context.runtime.getBlackboardValue('target');
|
||||||
|
|
||||||
|
if (!target) {
|
||||||
|
return TaskStatus.Failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`造成 ${damage} 点伤害`);
|
||||||
|
return TaskStatus.Success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
详细说明请参见[自定义节点执行器](./custom-actions.md)。
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
建议按照以下顺序学习:
|
||||||
|
|
||||||
|
1. 阅读[快速开始](./getting-started.md)了解基础用法
|
||||||
|
2. 学习[核心概念](./core-concepts.md)理解行为树原理
|
||||||
|
3. 学习[资产管理](./asset-management.md)了解如何加载和复用行为树、使用子树
|
||||||
|
4. 根据你的场景查看集成教程:
|
||||||
|
- 客户端游戏:[Cocos Creator](./cocos-integration.md) 或 [Laya](./laya-integration.md)
|
||||||
|
- 服务端应用:[Node.js 服务端使用](./nodejs-usage.md)
|
||||||
|
5. 尝试[编辑器使用指南](./editor-guide.md)可视化创建行为树
|
||||||
|
6. 探索[高级用法](./advanced-usage.md)和[自定义节点执行器](./custom-actions.md)提升技能
|
||||||
|
|
||||||
|
## 获取帮助
|
||||||
|
|
||||||
|
- 提交 [Issue](https://github.com/esengine/ecs-framework/issues)
|
||||||
|
- 加入社区讨论
|
||||||
|
- 参考文档中的完整代码示例
|
||||||
313
docs/guide/behavior-tree/laya-integration.md
Normal file
313
docs/guide/behavior-tree/laya-integration.md
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
# Laya 引擎集成
|
||||||
|
|
||||||
|
本教程将引导你在 Laya 引擎项目中集成和使用行为树系统。
|
||||||
|
|
||||||
|
## 前置要求
|
||||||
|
|
||||||
|
- LayaAir 3.x 或更高版本
|
||||||
|
- 基本的 TypeScript 知识
|
||||||
|
- 已完成[快速开始](./getting-started.md)教程
|
||||||
|
|
||||||
|
## 安装
|
||||||
|
|
||||||
|
在你的 Laya 项目根目录下:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @esengine/ecs-framework @esengine/behavior-tree
|
||||||
|
```
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
建议的项目结构:
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── ai/
|
||||||
|
│ ├── EnemyAI.ts
|
||||||
|
│ └── BossAI.ts
|
||||||
|
├── systems/
|
||||||
|
│ └── AISystem.ts
|
||||||
|
└── Main.ts
|
||||||
|
resources/
|
||||||
|
└── behaviors/
|
||||||
|
├── enemy.btree.json
|
||||||
|
└── boss.btree.json
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 初始化
|
||||||
|
|
||||||
|
### 在Main.ts中初始化
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Core, Scene } from '@esengine/ecs-framework';
|
||||||
|
import { BehaviorTreePlugin } from '@esengine/behavior-tree';
|
||||||
|
|
||||||
|
export class Main {
|
||||||
|
constructor() {
|
||||||
|
Laya.init(1280, 720).then(() => {
|
||||||
|
this.initECS();
|
||||||
|
this.startGame();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async initECS() {
|
||||||
|
// 初始化 ECS
|
||||||
|
Core.create();
|
||||||
|
|
||||||
|
// 安装行为树插件
|
||||||
|
const btPlugin = new BehaviorTreePlugin();
|
||||||
|
await Core.installPlugin(btPlugin);
|
||||||
|
|
||||||
|
// 创建并设置场景
|
||||||
|
const scene = new Scene();
|
||||||
|
btPlugin.setupScene(scene);
|
||||||
|
Core.setScene(scene);
|
||||||
|
|
||||||
|
// 启动更新循环
|
||||||
|
Laya.timer.frameLoop(1, this, this.update);
|
||||||
|
}
|
||||||
|
|
||||||
|
private update() {
|
||||||
|
// Core.update会自动更新场景
|
||||||
|
Core.update(Laya.timer.delta / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private startGame() {
|
||||||
|
// 加载场景
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new Main();
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 创建AI组件
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Core, Entity } from '@esengine/ecs-framework';
|
||||||
|
import {
|
||||||
|
BehaviorTreeBuilder,
|
||||||
|
BehaviorTreeStarter,
|
||||||
|
BehaviorTreeRuntimeComponent
|
||||||
|
} from '@esengine/behavior-tree';
|
||||||
|
|
||||||
|
export class EnemyAI extends Laya.Script {
|
||||||
|
private aiEntity: Entity;
|
||||||
|
|
||||||
|
onEnable() {
|
||||||
|
this.createBehaviorTree();
|
||||||
|
}
|
||||||
|
|
||||||
|
private createBehaviorTree() {
|
||||||
|
// 获取Core管理的场景
|
||||||
|
const scene = Core.scene;
|
||||||
|
if (!scene) {
|
||||||
|
console.error('场景未初始化');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sprite = this.owner as Laya.Sprite;
|
||||||
|
|
||||||
|
// 使用Builder API创建行为树
|
||||||
|
const tree = BehaviorTreeBuilder.create('EnemyAI')
|
||||||
|
.defineBlackboardVariable('layaSprite', sprite)
|
||||||
|
.defineBlackboardVariable('health', 100)
|
||||||
|
.defineBlackboardVariable('position', { x: sprite.x, y: sprite.y })
|
||||||
|
.selector('MainBehavior')
|
||||||
|
.sequence('Combat')
|
||||||
|
.blackboardCompare('health', 30, 'greater')
|
||||||
|
.log('攻击', 'Attack')
|
||||||
|
.end()
|
||||||
|
.log('巡逻', 'Patrol')
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 创建AI实体并启动
|
||||||
|
this.aiEntity = scene.createEntity(`AI_${sprite.name}`);
|
||||||
|
BehaviorTreeStarter.start(this.aiEntity, tree);
|
||||||
|
}
|
||||||
|
|
||||||
|
onDisable() {
|
||||||
|
// 停止AI
|
||||||
|
if (this.aiEntity) {
|
||||||
|
BehaviorTreeStarter.stop(this.aiEntity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 与Laya节点交互
|
||||||
|
|
||||||
|
要实现与Laya节点的交互,需要创建自定义执行器。下面展示一个完整示例。
|
||||||
|
|
||||||
|
## 完整示例
|
||||||
|
|
||||||
|
创建一个使用自定义执行器的敌人AI系统:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
BehaviorTreeBuilder,
|
||||||
|
BehaviorTreeStarter,
|
||||||
|
INodeExecutor,
|
||||||
|
NodeExecutionContext,
|
||||||
|
NodeExecutorMetadata,
|
||||||
|
BehaviorTreeRuntimeComponent
|
||||||
|
} from '@esengine/behavior-tree';
|
||||||
|
import { TaskStatus, NodeType } from '@esengine/behavior-tree';
|
||||||
|
import { Core, Entity } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
// 自定义移动执行器
|
||||||
|
@NodeExecutorMetadata({
|
||||||
|
implementationType: 'MoveToTarget',
|
||||||
|
nodeType: NodeType.Action,
|
||||||
|
displayName: '移动到目标',
|
||||||
|
category: 'Laya',
|
||||||
|
configSchema: {
|
||||||
|
speed: {
|
||||||
|
type: 'number',
|
||||||
|
default: 50,
|
||||||
|
supportBinding: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
export class MoveToTargetAction implements INodeExecutor {
|
||||||
|
execute(context: NodeExecutionContext): TaskStatus {
|
||||||
|
const sprite = context.runtime.getBlackboardValue('layaSprite');
|
||||||
|
const targetPos = context.runtime.getBlackboardValue('targetPosition');
|
||||||
|
const speed = context.nodeData.config.speed;
|
||||||
|
|
||||||
|
if (!sprite || !targetPos) {
|
||||||
|
return TaskStatus.Failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dx = targetPos.x - sprite.x;
|
||||||
|
const dy = targetPos.y - sprite.y;
|
||||||
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
|
||||||
|
if (distance < 10) {
|
||||||
|
return TaskStatus.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
sprite.x += (dx / distance) * speed * context.deltaTime;
|
||||||
|
sprite.y += (dy / distance) * speed * context.deltaTime;
|
||||||
|
|
||||||
|
return TaskStatus.Running;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SimpleEnemyAI extends Laya.Script {
|
||||||
|
public player: Laya.Sprite;
|
||||||
|
|
||||||
|
private aiEntity: Entity;
|
||||||
|
|
||||||
|
onEnable() {
|
||||||
|
this.buildAI();
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildAI() {
|
||||||
|
const scene = Core.scene;
|
||||||
|
if (!scene) {
|
||||||
|
console.error('场景未初始化');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sprite = this.owner as Laya.Sprite;
|
||||||
|
|
||||||
|
const tree = BehaviorTreeBuilder.create('EnemyAI')
|
||||||
|
.defineBlackboardVariable('layaSprite', sprite)
|
||||||
|
.defineBlackboardVariable('health', 100)
|
||||||
|
.defineBlackboardVariable('player', this.player)
|
||||||
|
.defineBlackboardVariable('targetPosition', { x: 0, y: 0 })
|
||||||
|
.selector('MainBehavior')
|
||||||
|
.sequence('Attack')
|
||||||
|
.blackboardExists('player')
|
||||||
|
.log('攻击玩家', 'DoAttack')
|
||||||
|
.end()
|
||||||
|
.log('巡逻', 'Patrol')
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
this.aiEntity = scene.createEntity(`AI_${sprite.name}`);
|
||||||
|
BehaviorTreeStarter.start(this.aiEntity, tree);
|
||||||
|
|
||||||
|
// 可以在帧更新中修改黑板
|
||||||
|
Laya.timer.frameLoop(1, this, () => {
|
||||||
|
const runtime = this.aiEntity?.getComponent(BehaviorTreeRuntimeComponent);
|
||||||
|
if (runtime && this.player) {
|
||||||
|
runtime.setBlackboardValue('targetPosition', {
|
||||||
|
x: this.player.x,
|
||||||
|
y: this.player.y
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onDisable() {
|
||||||
|
if (this.aiEntity) {
|
||||||
|
BehaviorTreeStarter.stop(this.aiEntity);
|
||||||
|
}
|
||||||
|
Laya.timer.clearAll(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 性能优化
|
||||||
|
|
||||||
|
### 使用冷却装饰器
|
||||||
|
|
||||||
|
对于不需要每帧更新的AI,使用冷却装饰器:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const tree = BehaviorTreeBuilder.create('ThrottledAI')
|
||||||
|
.cooldown(0.2, 'ThrottleRoot') // 每0.2秒执行一次
|
||||||
|
.selector('MainBehavior')
|
||||||
|
// AI逻辑...
|
||||||
|
.end()
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 限制同时运行的AI数量
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class AIManager {
|
||||||
|
private activeAIs: Entity[] = [];
|
||||||
|
private maxAIs: number = 20;
|
||||||
|
|
||||||
|
addAI(entity: Entity, tree: BehaviorTreeData) {
|
||||||
|
if (this.activeAIs.length >= this.maxAIs) {
|
||||||
|
const furthest = this.activeAIs.shift();
|
||||||
|
if (furthest) {
|
||||||
|
BehaviorTreeStarter.stop(furthest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BehaviorTreeStarter.start(entity, tree);
|
||||||
|
this.activeAIs.push(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### 资源加载失败?
|
||||||
|
|
||||||
|
确保:
|
||||||
|
1. 资源路径正确
|
||||||
|
2. 资源已添加到项目中
|
||||||
|
3. 使用 `Laya.loader.load()` 加载
|
||||||
|
|
||||||
|
### AI不执行?
|
||||||
|
|
||||||
|
检查:
|
||||||
|
1. `onUpdate()` 是否被调用
|
||||||
|
2. `Scene.update()` 是否执行
|
||||||
|
3. 行为树是否已启动
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
- 查看[高级用法](./advanced-usage.md)
|
||||||
|
- 学习[最佳实践](./best-practices.md)
|
||||||
580
docs/guide/behavior-tree/nodejs-usage.md
Normal file
580
docs/guide/behavior-tree/nodejs-usage.md
Normal file
@@ -0,0 +1,580 @@
|
|||||||
|
# Node.js 服务端使用
|
||||||
|
|
||||||
|
本文介绍如何在 Node.js 服务端环境(如游戏服务器、机器人、自动化工具)中使用行为树系统。
|
||||||
|
|
||||||
|
## 使用场景
|
||||||
|
|
||||||
|
行为树不仅适用于游戏客户端AI,在服务端也有广泛应用:
|
||||||
|
|
||||||
|
1. **游戏服务器** - NPC AI逻辑、副本关卡脚本
|
||||||
|
2. **聊天机器人** - 对话流程控制、智能回复
|
||||||
|
3. **自动化测试** - 测试用例执行流程
|
||||||
|
4. **工作流引擎** - 业务流程自动化
|
||||||
|
5. **爬虫系统** - 数据采集流程控制
|
||||||
|
|
||||||
|
## 基础设置
|
||||||
|
|
||||||
|
### 安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @esengine/ecs-framework @esengine/behavior-tree
|
||||||
|
```
|
||||||
|
|
||||||
|
### TypeScript 配置
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "commonjs",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 简单的游戏服务器 NPC
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Core, Scene } from '@esengine/ecs-framework';
|
||||||
|
import {
|
||||||
|
BehaviorTreePlugin,
|
||||||
|
BehaviorTreeBuilder,
|
||||||
|
BehaviorTreeStarter,
|
||||||
|
BehaviorTreeRuntimeComponent
|
||||||
|
} from '@esengine/behavior-tree';
|
||||||
|
|
||||||
|
async function startServer() {
|
||||||
|
// 1. 初始化 ECS Core
|
||||||
|
Core.create();
|
||||||
|
|
||||||
|
// 2. 安装行为树插件
|
||||||
|
const plugin = new BehaviorTreePlugin();
|
||||||
|
await Core.installPlugin(plugin);
|
||||||
|
|
||||||
|
// 3. 创建场景
|
||||||
|
const scene = new Scene();
|
||||||
|
plugin.setupScene(scene);
|
||||||
|
Core.setScene(scene);
|
||||||
|
|
||||||
|
// 4. 创建 NPC 行为树
|
||||||
|
const npcAI = BehaviorTreeBuilder.create('MerchantNPC')
|
||||||
|
.defineBlackboardVariable('mood', 'friendly')
|
||||||
|
.defineBlackboardVariable('goldAmount', 1000)
|
||||||
|
|
||||||
|
.selector('NPCBehavior')
|
||||||
|
// 如果玩家触发对话
|
||||||
|
.sequence('Dialogue')
|
||||||
|
.blackboardExists('playerRequest')
|
||||||
|
.log('NPC: 欢迎光临!')
|
||||||
|
.end()
|
||||||
|
|
||||||
|
// 默认行为:闲置
|
||||||
|
.sequence('Idle')
|
||||||
|
.log('NPC: 正在整理商品...')
|
||||||
|
.wait(5.0)
|
||||||
|
.end()
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 5. 创建 NPC 实体
|
||||||
|
const npc = scene.createEntity('Merchant');
|
||||||
|
BehaviorTreeStarter.start(npc, npcAI);
|
||||||
|
|
||||||
|
// 6. 启动游戏循环(20 TPS)
|
||||||
|
setInterval(() => {
|
||||||
|
Core.update(0.05); // 50ms = 1/20秒
|
||||||
|
}, 50);
|
||||||
|
|
||||||
|
// 7. 模拟玩家交互
|
||||||
|
setTimeout(() => {
|
||||||
|
const runtime = npc.getComponent(BehaviorTreeRuntimeComponent);
|
||||||
|
runtime?.setBlackboardValue('playerRequest', 'buy_sword');
|
||||||
|
console.log('玩家发起交易请求');
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
console.log('游戏服务器已启动');
|
||||||
|
}
|
||||||
|
|
||||||
|
startServer();
|
||||||
|
```
|
||||||
|
|
||||||
|
## 实战示例:聊天机器人
|
||||||
|
|
||||||
|
创建一个基于行为树的智能聊天机器人:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Core, Scene, Entity } from '@esengine/ecs-framework';
|
||||||
|
import {
|
||||||
|
BehaviorTreePlugin,
|
||||||
|
BehaviorTreeBuilder,
|
||||||
|
BehaviorTreeStarter,
|
||||||
|
BehaviorTreeRuntimeComponent,
|
||||||
|
INodeExecutor,
|
||||||
|
NodeExecutionContext,
|
||||||
|
TaskStatus,
|
||||||
|
NodeType,
|
||||||
|
NodeExecutorMetadata
|
||||||
|
} from '@esengine/behavior-tree';
|
||||||
|
|
||||||
|
// 1. 创建自定义节点:回复消息
|
||||||
|
@NodeExecutorMetadata({
|
||||||
|
implementationType: 'SendMessage',
|
||||||
|
nodeType: NodeType.Action,
|
||||||
|
displayName: '发送消息',
|
||||||
|
configSchema: {
|
||||||
|
message: { type: 'string', default: '' }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
class SendMessageAction implements INodeExecutor {
|
||||||
|
execute(context: NodeExecutionContext): TaskStatus {
|
||||||
|
const message = context.nodeData.config['message'] as string;
|
||||||
|
const userMessage = context.runtime.getBlackboardValue<string>('userMessage');
|
||||||
|
|
||||||
|
console.log(`[机器人回复]: ${message}`);
|
||||||
|
console.log(` 回复给: ${userMessage}`);
|
||||||
|
|
||||||
|
return TaskStatus.Success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 创建自定义节点:匹配关键词
|
||||||
|
@NodeExecutorMetadata({
|
||||||
|
implementationType: 'MatchKeyword',
|
||||||
|
nodeType: NodeType.Condition,
|
||||||
|
displayName: '匹配关键词',
|
||||||
|
configSchema: {
|
||||||
|
keyword: { type: 'string', default: '' }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
class MatchKeywordCondition implements INodeExecutor {
|
||||||
|
execute(context: NodeExecutionContext): TaskStatus {
|
||||||
|
const keyword = context.nodeData.config['keyword'] as string;
|
||||||
|
const userMessage = context.runtime.getBlackboardValue<string>('userMessage') || '';
|
||||||
|
|
||||||
|
return userMessage.includes(keyword) ? TaskStatus.Success : TaskStatus.Failure;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 创建聊天机器人类
|
||||||
|
class ChatBot {
|
||||||
|
private botEntity: Entity;
|
||||||
|
private runtime: BehaviorTreeRuntimeComponent | null = null;
|
||||||
|
|
||||||
|
constructor(scene: Scene) {
|
||||||
|
// 创建机器人行为树
|
||||||
|
const botBehavior = BehaviorTreeBuilder.create('ChatBotAI')
|
||||||
|
.defineBlackboardVariable('userMessage', '')
|
||||||
|
.defineBlackboardVariable('userName', 'Guest')
|
||||||
|
|
||||||
|
.selector('ResponseSelector')
|
||||||
|
// 问候语
|
||||||
|
.sequence('Greeting')
|
||||||
|
.executeCondition('MatchKeyword', { keyword: '你好' })
|
||||||
|
.executeAction('SendMessage', { message: '你好!我是智能助手,有什么可以帮你的吗?' })
|
||||||
|
.end()
|
||||||
|
|
||||||
|
// 帮助请求
|
||||||
|
.sequence('Help')
|
||||||
|
.executeCondition('MatchKeyword', { keyword: '帮助' })
|
||||||
|
.executeAction('SendMessage', { message: '我可以帮你回答问题、查询信息。试试问我一些问题吧!' })
|
||||||
|
.end()
|
||||||
|
|
||||||
|
// 查询天气
|
||||||
|
.sequence('Weather')
|
||||||
|
.executeCondition('MatchKeyword', { keyword: '天气' })
|
||||||
|
.executeAction('SendMessage', { message: '今天天气不错,晴天,温度适宜。' })
|
||||||
|
.end()
|
||||||
|
|
||||||
|
// 查询时间
|
||||||
|
.sequence('Time')
|
||||||
|
.executeCondition('MatchKeyword', { keyword: '时间' })
|
||||||
|
.executeAction('SendMessage', { message: `现在时间是 ${new Date().toLocaleString()}` })
|
||||||
|
.end()
|
||||||
|
|
||||||
|
// 默认回复
|
||||||
|
.executeAction('SendMessage', { message: '抱歉,我还不太理解你的意思。可以换个方式问我吗?' })
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 创建实体并启动
|
||||||
|
this.botEntity = scene.createEntity('ChatBot');
|
||||||
|
BehaviorTreeStarter.start(this.botEntity, botBehavior);
|
||||||
|
this.runtime = this.botEntity.getComponent(BehaviorTreeRuntimeComponent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理用户消息
|
||||||
|
async handleMessage(userName: string, message: string) {
|
||||||
|
if (this.runtime) {
|
||||||
|
this.runtime.setBlackboardValue('userName', userName);
|
||||||
|
this.runtime.setBlackboardValue('userMessage', message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待一帧让行为树执行
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 主程序
|
||||||
|
async function main() {
|
||||||
|
// 初始化
|
||||||
|
Core.create();
|
||||||
|
const plugin = new BehaviorTreePlugin();
|
||||||
|
await Core.installPlugin(plugin);
|
||||||
|
|
||||||
|
const scene = new Scene();
|
||||||
|
plugin.setupScene(scene);
|
||||||
|
Core.setScene(scene);
|
||||||
|
|
||||||
|
// 注册自定义节点
|
||||||
|
const system = scene.getSystem(BehaviorTreeExecutionSystem);
|
||||||
|
if (system) {
|
||||||
|
const registry = system.getExecutorRegistry();
|
||||||
|
registry.register('SendMessage', new SendMessageAction());
|
||||||
|
registry.register('MatchKeyword', new MatchKeywordCondition());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建聊天机器人
|
||||||
|
const bot = new ChatBot(scene);
|
||||||
|
|
||||||
|
// 启动更新循环
|
||||||
|
setInterval(() => {
|
||||||
|
Core.update(0.1);
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// 模拟用户对话
|
||||||
|
console.log('\n=== 聊天机器人测试 ===\n');
|
||||||
|
|
||||||
|
await bot.handleMessage('Alice', '你好');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 200));
|
||||||
|
|
||||||
|
await bot.handleMessage('Bob', '现在几点了?');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 200));
|
||||||
|
|
||||||
|
await bot.handleMessage('Charlie', '今天天气怎么样');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 200));
|
||||||
|
|
||||||
|
await bot.handleMessage('David', '你能帮我做什么');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 200));
|
||||||
|
|
||||||
|
await bot.handleMessage('Eve', '你好吗?');
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
|
```
|
||||||
|
|
||||||
|
## 实战示例:多人游戏服务器
|
||||||
|
|
||||||
|
### 房间管理系统
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Core, Scene, Entity } from '@esengine/ecs-framework';
|
||||||
|
import {
|
||||||
|
BehaviorTreePlugin,
|
||||||
|
BehaviorTreeBuilder,
|
||||||
|
BehaviorTreeStarter,
|
||||||
|
BehaviorTreeAssetManager
|
||||||
|
} from '@esengine/behavior-tree';
|
||||||
|
|
||||||
|
// 游戏房间
|
||||||
|
class GameRoom {
|
||||||
|
private scene: Scene;
|
||||||
|
private assetManager: BehaviorTreeAssetManager;
|
||||||
|
private monsters: Entity[] = [];
|
||||||
|
|
||||||
|
constructor(roomId: string) {
|
||||||
|
// 创建房间场景
|
||||||
|
this.scene = new Scene();
|
||||||
|
const plugin = new BehaviorTreePlugin();
|
||||||
|
plugin.setupScene(this.scene);
|
||||||
|
|
||||||
|
this.assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||||
|
|
||||||
|
// 初始化房间
|
||||||
|
this.spawnMonsters();
|
||||||
|
console.log(`房间 ${roomId} 已创建,怪物数量: ${this.monsters.length}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private spawnMonsters() {
|
||||||
|
// 从资产管理器获取怪物AI(所有房间共享)
|
||||||
|
const monsterAI = this.assetManager.getAsset('MonsterAI');
|
||||||
|
if (!monsterAI) return;
|
||||||
|
|
||||||
|
// 生成10个怪物
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
const monster = this.scene.createEntity(`Monster_${i}`);
|
||||||
|
BehaviorTreeStarter.start(monster, monsterAI);
|
||||||
|
this.monsters.push(monster);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update(deltaTime: number) {
|
||||||
|
this.scene.update(deltaTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.monsters.forEach(m => m.destroy());
|
||||||
|
this.monsters = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 房间管理器
|
||||||
|
class RoomManager {
|
||||||
|
private rooms: Map<string, GameRoom> = new Map();
|
||||||
|
|
||||||
|
createRoom(roomId: string): GameRoom {
|
||||||
|
const room = new GameRoom(roomId);
|
||||||
|
this.rooms.set(roomId, room);
|
||||||
|
return room;
|
||||||
|
}
|
||||||
|
|
||||||
|
getRoom(roomId: string): GameRoom | undefined {
|
||||||
|
return this.rooms.get(roomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroyRoom(roomId: string) {
|
||||||
|
const room = this.rooms.get(roomId);
|
||||||
|
if (room) {
|
||||||
|
room.destroy();
|
||||||
|
this.rooms.delete(roomId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update(deltaTime: number) {
|
||||||
|
this.rooms.forEach(room => room.update(deltaTime));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主程序
|
||||||
|
async function startGameServer() {
|
||||||
|
// 初始化
|
||||||
|
Core.create();
|
||||||
|
const plugin = new BehaviorTreePlugin();
|
||||||
|
await Core.installPlugin(plugin);
|
||||||
|
|
||||||
|
// 预加载怪物AI(所有房间共享)
|
||||||
|
const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||||
|
const monsterAI = BehaviorTreeBuilder.create('MonsterAI')
|
||||||
|
.defineBlackboardVariable('health', 100)
|
||||||
|
.selector('Behavior')
|
||||||
|
.log('攻击玩家')
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
assetManager.loadAsset(monsterAI);
|
||||||
|
|
||||||
|
// 创建房间管理器
|
||||||
|
const roomManager = new RoomManager();
|
||||||
|
|
||||||
|
// 模拟房间创建
|
||||||
|
roomManager.createRoom('room_1');
|
||||||
|
roomManager.createRoom('room_2');
|
||||||
|
|
||||||
|
// 服务器主循环(60 TPS)
|
||||||
|
setInterval(() => {
|
||||||
|
roomManager.update(1/60);
|
||||||
|
}, 1000 / 60);
|
||||||
|
|
||||||
|
console.log('游戏服务器已启动');
|
||||||
|
}
|
||||||
|
|
||||||
|
startGameServer();
|
||||||
|
```
|
||||||
|
|
||||||
|
## 性能优化
|
||||||
|
|
||||||
|
### 1. 控制更新频率
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 不同类型的AI使用不同的更新频率
|
||||||
|
class AIManager {
|
||||||
|
private importantAIs: Entity[] = []; // Boss等重要AI,60 TPS
|
||||||
|
private normalAIs: Entity[] = []; // 普通敌人,20 TPS
|
||||||
|
private backgroundAIs: Entity[] = []; // 背景NPC,5 TPS
|
||||||
|
|
||||||
|
update() {
|
||||||
|
// 重要AI每帧更新
|
||||||
|
this.updateAIs(this.importantAIs, 1/60);
|
||||||
|
|
||||||
|
// 普通AI每3帧更新一次
|
||||||
|
if (frameCount % 3 === 0) {
|
||||||
|
this.updateAIs(this.normalAIs, 3/60);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 背景AI每12帧更新一次
|
||||||
|
if (frameCount % 12 === 0) {
|
||||||
|
this.updateAIs(this.backgroundAIs, 12/60);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 资源管理
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 使用资产管理器避免重复创建
|
||||||
|
const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||||
|
|
||||||
|
// 预加载所有AI
|
||||||
|
const enemyAI = BehaviorTreeBuilder.create('EnemyAI').build();
|
||||||
|
const bossAI = BehaviorTreeBuilder.create('BossAI').build();
|
||||||
|
|
||||||
|
assetManager.loadAsset(enemyAI);
|
||||||
|
assetManager.loadAsset(bossAI);
|
||||||
|
|
||||||
|
// 创建1000个敌人,但只使用1份BehaviorTreeData
|
||||||
|
for (let i = 0; i < 1000; i++) {
|
||||||
|
const enemy = scene.createEntity(`Enemy${i}`);
|
||||||
|
const ai = assetManager.getAsset('EnemyAI')!;
|
||||||
|
BehaviorTreeStarter.start(enemy, ai);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 使用对象池
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class EntityPool {
|
||||||
|
private pool: Entity[] = [];
|
||||||
|
private active: Entity[] = [];
|
||||||
|
|
||||||
|
spawn(scene: Scene, treeId: string): Entity {
|
||||||
|
let entity = this.pool.pop();
|
||||||
|
|
||||||
|
if (!entity) {
|
||||||
|
entity = scene.createEntity();
|
||||||
|
const tree = assetManager.getAsset(treeId)!;
|
||||||
|
BehaviorTreeStarter.start(entity, tree);
|
||||||
|
} else {
|
||||||
|
BehaviorTreeStarter.restart(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.active.push(entity);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
recycle(entity: Entity) {
|
||||||
|
BehaviorTreeStarter.pause(entity);
|
||||||
|
const index = this.active.indexOf(entity);
|
||||||
|
if (index >= 0) {
|
||||||
|
this.active.splice(index, 1);
|
||||||
|
this.pool.push(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
### 1. 使用环境变量控制调试
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const DEBUG = process.env.NODE_ENV === 'development';
|
||||||
|
|
||||||
|
const aiTree = BehaviorTreeBuilder.create('AI')
|
||||||
|
.selector('Main')
|
||||||
|
.when(DEBUG, builder =>
|
||||||
|
builder.log('调试信息:开始AI逻辑')
|
||||||
|
)
|
||||||
|
// AI 逻辑...
|
||||||
|
.end()
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 错误处理
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
try {
|
||||||
|
const tree = BehaviorTreeBuilder.create('AI')
|
||||||
|
// ... 构建逻辑
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assetManager.loadAsset(tree);
|
||||||
|
BehaviorTreeStarter.start(entity, tree);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('启动AI失败:', error);
|
||||||
|
// 使用默认AI或进行降级处理
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 监控和日志
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 定期输出AI状态
|
||||||
|
setInterval(() => {
|
||||||
|
const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||||
|
const count = assetManager.getAssetCount();
|
||||||
|
const entities = scene.getEntitiesFor(Matcher.empty().all(BehaviorTreeRuntimeComponent));
|
||||||
|
|
||||||
|
console.log(`[AI监控] 行为树资产: ${count}, 活跃实体: ${entities.length}`);
|
||||||
|
}, 10000);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### 如何与 Express/Koa 等框架集成?
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import express from 'express';
|
||||||
|
import { Core, Scene } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const scene = new Scene();
|
||||||
|
|
||||||
|
// 在单独的循环中更新ECS
|
||||||
|
setInterval(() => {
|
||||||
|
Core.update(0.016);
|
||||||
|
}, 16);
|
||||||
|
|
||||||
|
app.post('/npc/:id/interact', (req, res) => {
|
||||||
|
const npcId = req.params.id;
|
||||||
|
const npc = scene.findEntity(npcId);
|
||||||
|
|
||||||
|
if (npc) {
|
||||||
|
const runtime = npc.getComponent(BehaviorTreeRuntimeComponent);
|
||||||
|
runtime?.setBlackboardValue('playerRequest', req.body);
|
||||||
|
|
||||||
|
res.json({ success: true });
|
||||||
|
} else {
|
||||||
|
res.status(404).json({ error: 'NPC not found' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(3000);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 如何持久化行为树状态?
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 保存状态
|
||||||
|
function saveAIState(entity: Entity) {
|
||||||
|
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||||
|
if (runtime) {
|
||||||
|
return {
|
||||||
|
treeId: runtime.treeId,
|
||||||
|
blackboard: runtime.getAllBlackboardVariables(),
|
||||||
|
activeNodes: Array.from(runtime.activeNodeIds)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 恢复状态
|
||||||
|
function loadAIState(entity: Entity, savedState: any) {
|
||||||
|
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||||
|
if (runtime) {
|
||||||
|
// 恢复黑板变量
|
||||||
|
Object.entries(savedState.blackboard).forEach(([key, value]) => {
|
||||||
|
runtime.setBlackboardValue(key, value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
- 查看[资产管理](./asset-management.md)了解资源加载和子树
|
||||||
|
- 学习[自定义节点执行器](./custom-actions.md)创建自定义行为
|
||||||
|
- 阅读[最佳实践](./best-practices.md)优化你的服务端AI
|
||||||
@@ -55,25 +55,92 @@ class Health extends Component {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 组件装饰器
|
### @ECSComponent 装饰器
|
||||||
|
|
||||||
**必须使用 `@ECSComponent` 装饰器**,这确保了:
|
`@ECSComponent` 是组件类必须使用的装饰器,它为组件提供了类型标识和元数据管理。
|
||||||
- 组件在代码混淆后仍能正确识别
|
|
||||||
- 提供稳定的类型名称用于序列化和调试
|
#### 为什么必须使用
|
||||||
- 框架能正确管理组件注册
|
|
||||||
|
| 功能 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| **类型识别** | 提供稳定的类型名称,代码混淆后仍能正确识别 |
|
||||||
|
| **序列化支持** | 序列化/反序列化时使用该名称作为类型标识 |
|
||||||
|
| **组件注册** | 自动注册到 ComponentRegistry,分配唯一的位掩码 |
|
||||||
|
| **调试支持** | 在调试工具和日志中显示可读的组件名称 |
|
||||||
|
|
||||||
|
#### 基本语法
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// 正确的用法
|
@ECSComponent(typeName: string)
|
||||||
|
```
|
||||||
|
|
||||||
|
- `typeName`: 组件的类型名称,建议使用与类名相同或相近的名称
|
||||||
|
|
||||||
|
#### 使用示例
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ 正确的用法
|
||||||
@ECSComponent('Velocity')
|
@ECSComponent('Velocity')
|
||||||
class Velocity extends Component {
|
class Velocity extends Component {
|
||||||
dx: number = 0;
|
dx: number = 0;
|
||||||
dy: number = 0;
|
dy: number = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 错误的用法 - 没有装饰器
|
// ✅ 推荐:类型名与类名保持一致
|
||||||
class BadComponent extends Component {
|
@ECSComponent('PlayerController')
|
||||||
// 这样定义的组件可能在生产环境出现问题
|
class PlayerController extends Component {
|
||||||
|
speed: number = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ❌ 错误的用法 - 没有装饰器
|
||||||
|
class BadComponent extends Component {
|
||||||
|
// 这样定义的组件可能在生产环境出现问题:
|
||||||
|
// 1. 代码压缩后类名变化,无法正确序列化
|
||||||
|
// 2. 组件未注册到框架,查询和匹配可能失效
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 与 @Serializable 配合使用
|
||||||
|
|
||||||
|
当组件需要支持序列化时,`@ECSComponent` 和 `@Serializable` 需要一起使用:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Component, ECSComponent, Serializable, Serialize } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
@ECSComponent('Player')
|
||||||
|
@Serializable({ version: 1 })
|
||||||
|
class PlayerComponent extends Component {
|
||||||
|
@Serialize()
|
||||||
|
name: string = '';
|
||||||
|
|
||||||
|
@Serialize()
|
||||||
|
level: number = 1;
|
||||||
|
|
||||||
|
// 不使用 @Serialize() 的字段不会被序列化
|
||||||
|
private _cachedData: any = null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **注意**:`@ECSComponent` 的 `typeName` 和 `@Serializable` 的 `typeId` 可以不同。如果 `@Serializable` 没有指定 `typeId`,则默认使用 `@ECSComponent` 的 `typeName`。
|
||||||
|
|
||||||
|
#### 组件类型名的唯一性
|
||||||
|
|
||||||
|
每个组件的类型名应该是唯一的:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ 错误:两个组件使用相同的类型名
|
||||||
|
@ECSComponent('Health')
|
||||||
|
class HealthComponent extends Component { }
|
||||||
|
|
||||||
|
@ECSComponent('Health') // 冲突!
|
||||||
|
class EnemyHealthComponent extends Component { }
|
||||||
|
|
||||||
|
// ✅ 正确:使用不同的类型名
|
||||||
|
@ECSComponent('PlayerHealth')
|
||||||
|
class PlayerHealthComponent extends Component { }
|
||||||
|
|
||||||
|
@ECSComponent('EnemyHealth')
|
||||||
|
class EnemyHealthComponent extends Component { }
|
||||||
```
|
```
|
||||||
|
|
||||||
## 组件生命周期
|
## 组件生命周期
|
||||||
|
|||||||
0
docs/guide/editor-plugin-system.md
Normal file
0
docs/guide/editor-plugin-system.md
Normal file
@@ -121,6 +121,65 @@ class CombatSystem extends EntitySystem {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### nothing() - 不匹配任何实体
|
||||||
|
|
||||||
|
用于创建只需要生命周期方法(`onBegin`、`onEnd`)但不需要处理实体的系统。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class FrameTimerSystem extends EntitySystem {
|
||||||
|
constructor() {
|
||||||
|
// 不匹配任何实体
|
||||||
|
super(Matcher.nothing());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onBegin(): void {
|
||||||
|
// 每帧开始时执行
|
||||||
|
Performance.markFrameStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected process(entities: readonly Entity[]): void {
|
||||||
|
// 永远不会被调用,因为没有匹配的实体
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onEnd(): void {
|
||||||
|
// 每帧结束时执行
|
||||||
|
Performance.markFrameEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### empty() vs nothing() 的区别
|
||||||
|
|
||||||
|
| 方法 | 行为 | 使用场景 |
|
||||||
|
|------|------|----------|
|
||||||
|
| `Matcher.empty()` | 匹配**所有**实体 | 需要处理场景中所有实体 |
|
||||||
|
| `Matcher.nothing()` | 不匹配**任何**实体 | 只需要生命周期回调,不处理实体 |
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// empty() - 返回场景中的所有实体
|
||||||
|
class AllEntitiesSystem extends EntitySystem {
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected process(entities: readonly Entity[]): void {
|
||||||
|
// entities 包含场景中的所有实体
|
||||||
|
console.log(`场景中共有 ${entities.length} 个实体`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// nothing() - 不返回任何实体
|
||||||
|
class NoEntitiesSystem extends EntitySystem {
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.nothing());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected process(entities: readonly Entity[]): void {
|
||||||
|
// entities 永远是空数组,此方法不会被调用
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### 按标签查询
|
### 按标签查询
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
@@ -493,6 +552,65 @@ const matcher2 = matcher.any(VelocityComponent);
|
|||||||
console.log(matcher === matcher2); // false
|
console.log(matcher === matcher2); // false
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Matcher API 快速参考
|
||||||
|
|
||||||
|
### 静态创建方法
|
||||||
|
|
||||||
|
| 方法 | 说明 | 示例 |
|
||||||
|
|------|------|------|
|
||||||
|
| `Matcher.all(...types)` | 必须包含所有指定组件 | `Matcher.all(Position, Velocity)` |
|
||||||
|
| `Matcher.any(...types)` | 至少包含一个指定组件 | `Matcher.any(Health, Shield)` |
|
||||||
|
| `Matcher.none(...types)` | 不能包含任何指定组件 | `Matcher.none(Dead)` |
|
||||||
|
| `Matcher.byTag(tag)` | 按标签查询 | `Matcher.byTag(1)` |
|
||||||
|
| `Matcher.byName(name)` | 按名称查询 | `Matcher.byName("Player")` |
|
||||||
|
| `Matcher.byComponent(type)` | 按单个组件查询 | `Matcher.byComponent(Health)` |
|
||||||
|
| `Matcher.empty()` | 创建空匹配器(匹配所有实体) | `Matcher.empty()` |
|
||||||
|
| `Matcher.nothing()` | 不匹配任何实体 | `Matcher.nothing()` |
|
||||||
|
| `Matcher.complex()` | 创建复杂查询构建器 | `Matcher.complex()` |
|
||||||
|
|
||||||
|
### 链式方法
|
||||||
|
|
||||||
|
| 方法 | 说明 | 示例 |
|
||||||
|
|------|------|------|
|
||||||
|
| `.all(...types)` | 添加必须包含的组件 | `.all(Position)` |
|
||||||
|
| `.any(...types)` | 添加可选组件(至少一个) | `.any(Weapon, Magic)` |
|
||||||
|
| `.none(...types)` | 添加排除的组件 | `.none(Dead)` |
|
||||||
|
| `.exclude(...types)` | `.none()` 的别名 | `.exclude(Disabled)` |
|
||||||
|
| `.one(...types)` | `.any()` 的别名 | `.one(Player, Enemy)` |
|
||||||
|
| `.withTag(tag)` | 添加标签条件 | `.withTag(1)` |
|
||||||
|
| `.withName(name)` | 添加名称条件 | `.withName("Boss")` |
|
||||||
|
| `.withComponent(type)` | 添加单组件条件 | `.withComponent(Health)` |
|
||||||
|
|
||||||
|
### 实用方法
|
||||||
|
|
||||||
|
| 方法 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `.getCondition()` | 获取查询条件(只读) |
|
||||||
|
| `.isEmpty()` | 检查是否为空条件 |
|
||||||
|
| `.isNothing()` | 检查是否为 nothing 匹配器 |
|
||||||
|
| `.clone()` | 克隆匹配器 |
|
||||||
|
| `.reset()` | 重置所有条件 |
|
||||||
|
| `.toString()` | 获取字符串表示 |
|
||||||
|
|
||||||
|
### 常用组合示例
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 基础移动系统
|
||||||
|
Matcher.all(Position, Velocity)
|
||||||
|
|
||||||
|
// 可攻击的活着的实体
|
||||||
|
Matcher.all(Position, Health)
|
||||||
|
.any(Weapon, Magic)
|
||||||
|
.none(Dead, Disabled)
|
||||||
|
|
||||||
|
// 所有带标签的敌人
|
||||||
|
Matcher.byTag(Tags.ENEMY)
|
||||||
|
.all(AIComponent)
|
||||||
|
|
||||||
|
// 只需要生命周期的系统
|
||||||
|
Matcher.nothing()
|
||||||
|
```
|
||||||
|
|
||||||
## 相关 API
|
## 相关 API
|
||||||
|
|
||||||
- [Matcher](../api/classes/Matcher.md) - 查询条件描述符 API 参考
|
- [Matcher](../api/classes/Matcher.md) - 查询条件描述符 API 参考
|
||||||
|
|||||||
@@ -9,6 +9,12 @@
|
|||||||
- 提供唯一标识(ID)
|
- 提供唯一标识(ID)
|
||||||
- 管理组件的生命周期
|
- 管理组件的生命周期
|
||||||
|
|
||||||
|
::: tip 关于父子层级关系
|
||||||
|
实体间的父子层级关系通过 `HierarchyComponent` 和 `HierarchySystem` 管理,而非 Entity 内置属性。这种设计遵循 ECS 组合原则 —— 只有需要层级关系的实体才添加此组件。
|
||||||
|
|
||||||
|
详见 [层级系统](./hierarchy.md) 文档。
|
||||||
|
:::
|
||||||
|
|
||||||
## 创建实体
|
## 创建实体
|
||||||
|
|
||||||
**重要提示:实体必须通过场景创建,不支持手动创建!**
|
**重要提示:实体必须通过场景创建,不支持手动创建!**
|
||||||
@@ -285,4 +291,10 @@ entity.components.forEach(component => {
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
实体是 ECS 架构的核心概念之一,理解如何正确使用实体将帮助你构建高效、可维护的游戏代码。
|
实体是 ECS 架构的核心概念之一,理解如何正确使用实体将帮助你构建高效、可维护的游戏代码。
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
- 了解 [层级系统](./hierarchy.md) 建立实体间的父子关系
|
||||||
|
- 了解 [组件系统](./component.md) 为实体添加功能
|
||||||
|
- 了解 [场景管理](./scene.md) 组织和管理实体
|
||||||
@@ -23,7 +23,6 @@ import { Core } from '@esengine/ecs-framework'
|
|||||||
// 方式1:使用配置对象(推荐)
|
// 方式1:使用配置对象(推荐)
|
||||||
const core = Core.create({
|
const core = Core.create({
|
||||||
debug: true, // 启用调试模式,提供详细的日志和性能监控
|
debug: true, // 启用调试模式,提供详细的日志和性能监控
|
||||||
enableEntitySystems: true, // 启用实体系统,这是ECS的核心功能
|
|
||||||
debugConfig: { // 可选:高级调试配置
|
debugConfig: { // 可选:高级调试配置
|
||||||
enabled: false, // 是否启用WebSocket调试服务器
|
enabled: false, // 是否启用WebSocket调试服务器
|
||||||
websocketUrl: 'ws://localhost:8080',
|
websocketUrl: 'ws://localhost:8080',
|
||||||
@@ -39,12 +38,11 @@ const core = Core.create({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 方式2:简化创建(向后兼容)
|
// 方式2:简化创建(向后兼容)
|
||||||
const core = Core.create(true); // 等同于 { debug: true, enableEntitySystems: true }
|
const core = Core.create(true); // 等同于 { debug: true }
|
||||||
|
|
||||||
// 方式3:生产环境配置
|
// 方式3:生产环境配置
|
||||||
const core = Core.create({
|
const core = Core.create({
|
||||||
debug: false, // 生产环境关闭调试
|
debug: false // 生产环境关闭调试
|
||||||
enableEntitySystems: true
|
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -55,9 +53,6 @@ interface ICoreConfig {
|
|||||||
/** 是否启用调试模式 - 影响日志级别和性能监控 */
|
/** 是否启用调试模式 - 影响日志级别和性能监控 */
|
||||||
debug?: boolean;
|
debug?: boolean;
|
||||||
|
|
||||||
/** 是否启用实体系统 - 核心ECS功能开关 */
|
|
||||||
enableEntitySystems?: boolean;
|
|
||||||
|
|
||||||
/** 高级调试配置 - 用于开发工具集成 */
|
/** 高级调试配置 - 用于开发工具集成 */
|
||||||
debugConfig?: {
|
debugConfig?: {
|
||||||
enabled: boolean; // 是否启用调试服务器
|
enabled: boolean; // 是否启用调试服务器
|
||||||
|
|||||||
437
docs/guide/hierarchy.md
Normal file
437
docs/guide/hierarchy.md
Normal file
@@ -0,0 +1,437 @@
|
|||||||
|
# 层级系统
|
||||||
|
|
||||||
|
在游戏开发中,实体间的父子层级关系是常见需求。ECS Framework 采用组件化方式管理层级关系,通过 `HierarchyComponent` 和 `HierarchySystem` 实现,完全遵循 ECS 组合原则。
|
||||||
|
|
||||||
|
## 设计理念
|
||||||
|
|
||||||
|
### 为什么不在 Entity 中内置层级?
|
||||||
|
|
||||||
|
传统的游戏对象模型(如 Unity 的 GameObject)将层级关系内置于实体中。ECS Framework 选择组件化方案的原因:
|
||||||
|
|
||||||
|
1. **ECS 组合原则**:层级是一种"功能",应该通过组件添加,而非所有实体都具备
|
||||||
|
2. **按需使用**:只有需要层级关系的实体才添加 `HierarchyComponent`
|
||||||
|
3. **数据与逻辑分离**:`HierarchyComponent` 存储数据,`HierarchySystem` 处理逻辑
|
||||||
|
4. **序列化友好**:层级关系作为组件数据可以轻松序列化和反序列化
|
||||||
|
|
||||||
|
## 基本概念
|
||||||
|
|
||||||
|
### HierarchyComponent
|
||||||
|
|
||||||
|
存储层级关系数据的组件:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { HierarchyComponent } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
// HierarchyComponent 的核心属性
|
||||||
|
interface HierarchyComponent {
|
||||||
|
parentId: number | null; // 父实体 ID,null 表示根实体
|
||||||
|
childIds: number[]; // 子实体 ID 列表
|
||||||
|
depth: number; // 在层级中的深度(由系统维护)
|
||||||
|
bActiveInHierarchy: boolean; // 在层级中是否激活(由系统维护)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### HierarchySystem
|
||||||
|
|
||||||
|
处理层级逻辑的系统,提供所有层级操作的 API:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { HierarchySystem } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
// 获取系统
|
||||||
|
const hierarchySystem = scene.getEntityProcessor(HierarchySystem);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 添加系统到场景
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Scene, HierarchySystem } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
class GameScene extends Scene {
|
||||||
|
protected initialize(): void {
|
||||||
|
// 添加层级系统
|
||||||
|
this.addSystem(new HierarchySystem());
|
||||||
|
|
||||||
|
// 添加其他系统...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 建立父子关系
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 创建实体
|
||||||
|
const parent = scene.createEntity("Parent");
|
||||||
|
const child1 = scene.createEntity("Child1");
|
||||||
|
const child2 = scene.createEntity("Child2");
|
||||||
|
|
||||||
|
// 获取层级系统
|
||||||
|
const hierarchySystem = scene.getEntityProcessor(HierarchySystem);
|
||||||
|
|
||||||
|
// 设置父子关系(自动添加 HierarchyComponent)
|
||||||
|
hierarchySystem.setParent(child1, parent);
|
||||||
|
hierarchySystem.setParent(child2, parent);
|
||||||
|
|
||||||
|
// 现在 parent 有两个子实体
|
||||||
|
```
|
||||||
|
|
||||||
|
### 查询层级
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 获取父实体
|
||||||
|
const parentEntity = hierarchySystem.getParent(child1);
|
||||||
|
|
||||||
|
// 获取所有子实体
|
||||||
|
const children = hierarchySystem.getChildren(parent);
|
||||||
|
|
||||||
|
// 获取子实体数量
|
||||||
|
const count = hierarchySystem.getChildCount(parent);
|
||||||
|
|
||||||
|
// 检查是否有子实体
|
||||||
|
const hasKids = hierarchySystem.hasChildren(parent);
|
||||||
|
|
||||||
|
// 获取在层级中的深度
|
||||||
|
const depth = hierarchySystem.getDepth(child1); // 返回 1
|
||||||
|
```
|
||||||
|
|
||||||
|
## API 参考
|
||||||
|
|
||||||
|
### 父子关系操作
|
||||||
|
|
||||||
|
#### setParent
|
||||||
|
|
||||||
|
设置实体的父级:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 设置父级
|
||||||
|
hierarchySystem.setParent(child, parent);
|
||||||
|
|
||||||
|
// 移动到根级(无父级)
|
||||||
|
hierarchySystem.setParent(child, null);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### insertChildAt
|
||||||
|
|
||||||
|
在指定位置插入子实体:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 在第一个位置插入
|
||||||
|
hierarchySystem.insertChildAt(parent, child, 0);
|
||||||
|
|
||||||
|
// 追加到末尾
|
||||||
|
hierarchySystem.insertChildAt(parent, child, -1);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### removeChild
|
||||||
|
|
||||||
|
从父级移除子实体(子实体变为根级):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const success = hierarchySystem.removeChild(parent, child);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### removeAllChildren
|
||||||
|
|
||||||
|
移除所有子实体:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
hierarchySystem.removeAllChildren(parent);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 层级查询
|
||||||
|
|
||||||
|
#### getParent / getChildren
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const parent = hierarchySystem.getParent(entity);
|
||||||
|
const children = hierarchySystem.getChildren(entity);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### getRoot
|
||||||
|
|
||||||
|
获取实体的根节点:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const root = hierarchySystem.getRoot(deepChild);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### getRootEntities
|
||||||
|
|
||||||
|
获取所有根实体(没有父级的实体):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const roots = hierarchySystem.getRootEntities();
|
||||||
|
```
|
||||||
|
|
||||||
|
#### isAncestorOf / isDescendantOf
|
||||||
|
|
||||||
|
检查祖先/后代关系:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// grandparent -> parent -> child
|
||||||
|
const isAncestor = hierarchySystem.isAncestorOf(grandparent, child); // true
|
||||||
|
const isDescendant = hierarchySystem.isDescendantOf(child, grandparent); // true
|
||||||
|
```
|
||||||
|
|
||||||
|
### 层级遍历
|
||||||
|
|
||||||
|
#### findChild
|
||||||
|
|
||||||
|
根据名称查找子实体:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 直接子级中查找
|
||||||
|
const child = hierarchySystem.findChild(parent, "ChildName");
|
||||||
|
|
||||||
|
// 递归查找所有后代
|
||||||
|
const deepChild = hierarchySystem.findChild(parent, "DeepChild", true);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### findChildrenByTag
|
||||||
|
|
||||||
|
根据标签查找子实体:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 查找直接子级
|
||||||
|
const tagged = hierarchySystem.findChildrenByTag(parent, TAG_ENEMY);
|
||||||
|
|
||||||
|
// 递归查找
|
||||||
|
const allTagged = hierarchySystem.findChildrenByTag(parent, TAG_ENEMY, true);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### forEachChild
|
||||||
|
|
||||||
|
遍历子实体:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 遍历直接子级
|
||||||
|
hierarchySystem.forEachChild(parent, (child) => {
|
||||||
|
console.log(child.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 递归遍历所有后代
|
||||||
|
hierarchySystem.forEachChild(parent, (child) => {
|
||||||
|
console.log(child.name);
|
||||||
|
}, true);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 层级状态
|
||||||
|
|
||||||
|
#### isActiveInHierarchy
|
||||||
|
|
||||||
|
检查实体在层级中是否激活(考虑所有祖先的激活状态):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 如果 parent.active = false,即使 child.active = true
|
||||||
|
// isActiveInHierarchy(child) 也会返回 false
|
||||||
|
const activeInHierarchy = hierarchySystem.isActiveInHierarchy(child);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### getDepth
|
||||||
|
|
||||||
|
获取实体在层级中的深度(根实体深度为 0):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const depth = hierarchySystem.getDepth(entity);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 扁平化层级(用于 UI 渲染)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 用于实现可展开/折叠的层级树视图
|
||||||
|
const expandedIds = new Set([parent.id]);
|
||||||
|
|
||||||
|
const flatNodes = hierarchySystem.flattenHierarchy(expandedIds);
|
||||||
|
// 返回 [{ entity, depth, bHasChildren, bIsExpanded }, ...]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 完整示例
|
||||||
|
|
||||||
|
### 创建游戏角色层级
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
Scene,
|
||||||
|
HierarchySystem,
|
||||||
|
HierarchyComponent
|
||||||
|
} from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
class GameScene extends Scene {
|
||||||
|
private hierarchySystem!: HierarchySystem;
|
||||||
|
|
||||||
|
protected initialize(): void {
|
||||||
|
// 添加层级系统
|
||||||
|
this.hierarchySystem = new HierarchySystem();
|
||||||
|
this.addSystem(this.hierarchySystem);
|
||||||
|
|
||||||
|
// 创建角色层级
|
||||||
|
this.createPlayerHierarchy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private createPlayerHierarchy(): void {
|
||||||
|
// 根实体
|
||||||
|
const player = this.createEntity("Player");
|
||||||
|
player.addComponent(new Transform(0, 0));
|
||||||
|
|
||||||
|
// 身体部件
|
||||||
|
const body = this.createEntity("Body");
|
||||||
|
body.addComponent(new Sprite("body.png"));
|
||||||
|
this.hierarchySystem.setParent(body, player);
|
||||||
|
|
||||||
|
// 武器(挂载在身体上)
|
||||||
|
const weapon = this.createEntity("Weapon");
|
||||||
|
weapon.addComponent(new Sprite("sword.png"));
|
||||||
|
this.hierarchySystem.setParent(weapon, body);
|
||||||
|
|
||||||
|
// 特效(挂载在武器上)
|
||||||
|
const effect = this.createEntity("WeaponEffect");
|
||||||
|
effect.addComponent(new ParticleEmitter());
|
||||||
|
this.hierarchySystem.setParent(effect, weapon);
|
||||||
|
|
||||||
|
// 查询层级信息
|
||||||
|
console.log(`Player 层级深度: ${this.hierarchySystem.getDepth(player)}`); // 0
|
||||||
|
console.log(`Weapon 层级深度: ${this.hierarchySystem.getDepth(weapon)}`); // 2
|
||||||
|
console.log(`Effect 层级深度: ${this.hierarchySystem.getDepth(effect)}`); // 3
|
||||||
|
}
|
||||||
|
|
||||||
|
public equipNewWeapon(weaponName: string): void {
|
||||||
|
const body = this.findEntity("Body");
|
||||||
|
const oldWeapon = this.hierarchySystem.findChild(body!, "Weapon");
|
||||||
|
|
||||||
|
if (oldWeapon) {
|
||||||
|
// 移除旧武器的所有子实体
|
||||||
|
this.hierarchySystem.removeAllChildren(oldWeapon);
|
||||||
|
oldWeapon.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新武器
|
||||||
|
const newWeapon = this.createEntity("Weapon");
|
||||||
|
newWeapon.addComponent(new Sprite(`${weaponName}.png`));
|
||||||
|
this.hierarchySystem.setParent(newWeapon, body!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 层级变换系统
|
||||||
|
|
||||||
|
结合 Transform 组件实现层级变换:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { EntitySystem, Matcher, HierarchySystem, HierarchyComponent } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
class HierarchyTransformSystem extends EntitySystem {
|
||||||
|
private hierarchySystem!: HierarchySystem;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.empty().all(Transform, HierarchyComponent));
|
||||||
|
}
|
||||||
|
|
||||||
|
public onAddedToScene(): void {
|
||||||
|
// 获取层级系统引用
|
||||||
|
this.hierarchySystem = this.scene!.getEntityProcessor(HierarchySystem)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected process(entities: readonly Entity[]): void {
|
||||||
|
// 按深度排序,确保父级先更新
|
||||||
|
const sorted = [...entities].sort((a, b) => {
|
||||||
|
return this.hierarchySystem.getDepth(a) - this.hierarchySystem.getDepth(b);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const entity of sorted) {
|
||||||
|
const transform = entity.getComponent(Transform)!;
|
||||||
|
const parent = this.hierarchySystem.getParent(entity);
|
||||||
|
|
||||||
|
if (parent) {
|
||||||
|
const parentTransform = parent.getComponent(Transform);
|
||||||
|
if (parentTransform) {
|
||||||
|
// 计算世界坐标
|
||||||
|
transform.worldX = parentTransform.worldX + transform.localX;
|
||||||
|
transform.worldY = parentTransform.worldY + transform.localY;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 根实体,本地坐标即世界坐标
|
||||||
|
transform.worldX = transform.localX;
|
||||||
|
transform.worldY = transform.localY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 性能优化
|
||||||
|
|
||||||
|
### 缓存机制
|
||||||
|
|
||||||
|
`HierarchySystem` 内置了缓存机制:
|
||||||
|
|
||||||
|
- `depth` 和 `bActiveInHierarchy` 由系统自动维护
|
||||||
|
- 使用 `bCacheDirty` 标记优化更新
|
||||||
|
- 层级变化时自动标记所有子级缓存为脏
|
||||||
|
|
||||||
|
### 最佳实践
|
||||||
|
|
||||||
|
1. **避免深层嵌套**:系统限制最大深度为 32 层
|
||||||
|
2. **批量操作**:构建复杂层级时,尽量一次性设置好所有父子关系
|
||||||
|
3. **按需添加**:只有真正需要层级关系的实体才添加 `HierarchyComponent`
|
||||||
|
4. **缓存系统引用**:避免每次调用都获取 `HierarchySystem`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 好的做法
|
||||||
|
class MySystem extends EntitySystem {
|
||||||
|
private hierarchySystem!: HierarchySystem;
|
||||||
|
|
||||||
|
onAddedToScene() {
|
||||||
|
this.hierarchySystem = this.scene!.getEntityProcessor(HierarchySystem)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
process() {
|
||||||
|
// 使用缓存的引用
|
||||||
|
const parent = this.hierarchySystem.getParent(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 避免的做法
|
||||||
|
process() {
|
||||||
|
// 每次都获取,性能较差
|
||||||
|
const system = this.scene!.getEntityProcessor(HierarchySystem);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 迁移指南
|
||||||
|
|
||||||
|
如果你之前使用的是旧版 Entity 内置的层级 API,请参考以下迁移指南:
|
||||||
|
|
||||||
|
| 旧 API (已移除) | 新 API |
|
||||||
|
|----------------|--------|
|
||||||
|
| `entity.parent` | `hierarchySystem.getParent(entity)` |
|
||||||
|
| `entity.children` | `hierarchySystem.getChildren(entity)` |
|
||||||
|
| `entity.addChild(child)` | `hierarchySystem.setParent(child, entity)` |
|
||||||
|
| `entity.removeChild(child)` | `hierarchySystem.removeChild(entity, child)` |
|
||||||
|
| `entity.findChild(name)` | `hierarchySystem.findChild(entity, name)` |
|
||||||
|
| `entity.activeInHierarchy` | `hierarchySystem.isActiveInHierarchy(entity)` |
|
||||||
|
|
||||||
|
### 迁移示例
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 旧代码
|
||||||
|
const parent = scene.createEntity("Parent");
|
||||||
|
const child = scene.createEntity("Child");
|
||||||
|
parent.addChild(child);
|
||||||
|
const found = parent.findChild("Child");
|
||||||
|
|
||||||
|
// 新代码
|
||||||
|
const hierarchySystem = scene.getEntityProcessor(HierarchySystem);
|
||||||
|
|
||||||
|
const parent = scene.createEntity("Parent");
|
||||||
|
const child = scene.createEntity("Child");
|
||||||
|
hierarchySystem.setParent(child, parent);
|
||||||
|
const found = hierarchySystem.findChild(parent, "Child");
|
||||||
|
```
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
- 了解 [实体类](./entity.md) 的其他功能
|
||||||
|
- 了解 [场景管理](./scene.md) 如何组织实体和系统
|
||||||
|
- 了解 [组件系统](./component.md) 如何定义和使用组件
|
||||||
@@ -13,6 +13,9 @@
|
|||||||
### [系统架构 (System)](./system.md)
|
### [系统架构 (System)](./system.md)
|
||||||
掌握系统的编写方法,实现游戏逻辑的处理。
|
掌握系统的编写方法,实现游戏逻辑的处理。
|
||||||
|
|
||||||
|
### [实体查询与 Matcher](./entity-query.md)
|
||||||
|
学习使用 Matcher 进行实体筛选和查询,掌握 `all`、`any`、`none`、`nothing` 等匹配条件。
|
||||||
|
|
||||||
### [场景管理 (Scene)](./scene.md)
|
### [场景管理 (Scene)](./scene.md)
|
||||||
了解场景的生命周期、系统管理和实体容器功能。
|
了解场景的生命周期、系统管理和实体容器功能。
|
||||||
|
|
||||||
|
|||||||
@@ -238,6 +238,50 @@ class HierarchicalLoggingExample {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 集成第三方日志库
|
||||||
|
|
||||||
|
通过 `setLoggerFactory` 可以将业务代码中的日志器替换为第三方日志库(如 winston、pino、nestjs Logger 等)。
|
||||||
|
|
||||||
|
**说明**: 目前框架内部日志仍使用 ConsoleLogger,自定义日志器仅影响业务代码(如 EntitySystem)。
|
||||||
|
|
||||||
|
#### 基本用法
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { setLoggerFactory } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
setLoggerFactory((name?: string) => {
|
||||||
|
// 返回实现 ILogger 接口的日志器实例
|
||||||
|
return yourLogger;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 使用示例
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 集成 Winston
|
||||||
|
setLoggerFactory((name?: string) => winston.createLogger({ /* ... */ }));
|
||||||
|
|
||||||
|
// 集成 Pino
|
||||||
|
setLoggerFactory((name?: string) => pino({ name }));
|
||||||
|
|
||||||
|
// 集成 NestJS Logger
|
||||||
|
setLoggerFactory((name?: string) => new Logger(name));
|
||||||
|
```
|
||||||
|
|
||||||
|
#### EntitySystem 中的使用
|
||||||
|
|
||||||
|
EntitySystem 会自动使用类名创建日志器:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class PlayerMovementSystem extends EntitySystem {
|
||||||
|
// this.logger 自动使用 'PlayerMovementSystem' 作为名称
|
||||||
|
|
||||||
|
protected process(entities: readonly Entity[]): void {
|
||||||
|
this.logger.info(`处理 ${entities.length} 个玩家实体`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### 自定义输出
|
### 自定义输出
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
@@ -547,4 +591,4 @@ class LoggingConfiguration {
|
|||||||
LoggingConfiguration.setupLogging();
|
LoggingConfiguration.setupLogging();
|
||||||
```
|
```
|
||||||
|
|
||||||
日志系统是调试和监控应用的重要工具,正确使用日志系统能大大提高开发效率和问题排查能力。
|
日志系统是调试和监控应用的重要工具,正确使用日志系统能大大提高开发效率和问题排查能力。
|
||||||
|
|||||||
@@ -190,6 +190,106 @@ class CollectionsComponent extends Component {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 组件继承与序列化
|
||||||
|
|
||||||
|
框架完整支持组件类的继承,子类会自动继承父类的序列化字段,同时可以添加自己的字段。
|
||||||
|
|
||||||
|
#### 基础继承
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 基类组件
|
||||||
|
@ECSComponent('Collider2DBase')
|
||||||
|
@Serializable({ version: 1, typeId: 'Collider2DBase' })
|
||||||
|
abstract class Collider2DBase extends Component {
|
||||||
|
@Serialize()
|
||||||
|
public friction: number = 0.5;
|
||||||
|
|
||||||
|
@Serialize()
|
||||||
|
public restitution: number = 0.0;
|
||||||
|
|
||||||
|
@Serialize()
|
||||||
|
public isTrigger: boolean = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 子类组件 - 自动继承父类的序列化字段
|
||||||
|
@ECSComponent('BoxCollider2D')
|
||||||
|
@Serializable({ version: 1, typeId: 'BoxCollider2D' })
|
||||||
|
class BoxCollider2DComponent extends Collider2DBase {
|
||||||
|
@Serialize()
|
||||||
|
public width: number = 1.0;
|
||||||
|
|
||||||
|
@Serialize()
|
||||||
|
public height: number = 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 另一个子类组件
|
||||||
|
@ECSComponent('CircleCollider2D')
|
||||||
|
@Serializable({ version: 1, typeId: 'CircleCollider2D' })
|
||||||
|
class CircleCollider2DComponent extends Collider2DBase {
|
||||||
|
@Serialize()
|
||||||
|
public radius: number = 0.5;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 继承规则
|
||||||
|
|
||||||
|
1. **字段继承**:子类自动继承父类所有被 `@Serialize()` 标记的字段
|
||||||
|
2. **独立元数据**:每个子类维护独立的序列化元数据,修改子类不会影响父类或其他子类
|
||||||
|
3. **typeId 区分**:使用 `typeId` 选项为每个类指定唯一标识,确保反序列化时能正确识别组件类型
|
||||||
|
|
||||||
|
#### 使用 typeId 的重要性
|
||||||
|
|
||||||
|
当使用组件继承时,**强烈建议**为每个类设置唯一的 `typeId`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ 推荐:明确指定 typeId
|
||||||
|
@Serializable({ version: 1, typeId: 'BoxCollider2D' })
|
||||||
|
class BoxCollider2DComponent extends Collider2DBase { }
|
||||||
|
|
||||||
|
@Serializable({ version: 1, typeId: 'CircleCollider2D' })
|
||||||
|
class CircleCollider2DComponent extends Collider2DBase { }
|
||||||
|
|
||||||
|
// ⚠️ 不推荐:依赖类名作为 typeId
|
||||||
|
// 在代码压缩后类名可能变化,导致反序列化失败
|
||||||
|
@Serializable({ version: 1 })
|
||||||
|
class BoxCollider2DComponent extends Collider2DBase { }
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 子类覆盖父类字段
|
||||||
|
|
||||||
|
子类可以重新声明父类的字段以修改其序列化选项:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@ECSComponent('SpecialCollider')
|
||||||
|
@Serializable({ version: 1, typeId: 'SpecialCollider' })
|
||||||
|
class SpecialColliderComponent extends Collider2DBase {
|
||||||
|
// 覆盖父类字段,使用不同的别名
|
||||||
|
@Serialize({ alias: 'fric' })
|
||||||
|
public override friction: number = 0.8;
|
||||||
|
|
||||||
|
@Serialize()
|
||||||
|
public specialProperty: string = '';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 忽略继承的字段
|
||||||
|
|
||||||
|
使用 `@IgnoreSerialization()` 可以在子类中忽略从父类继承的字段:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@ECSComponent('TriggerOnly')
|
||||||
|
@Serializable({ version: 1, typeId: 'TriggerOnly' })
|
||||||
|
class TriggerOnlyCollider extends Collider2DBase {
|
||||||
|
// 忽略父类的 friction 和 restitution 字段
|
||||||
|
// 因为 Trigger 不需要物理材质属性
|
||||||
|
@IgnoreSerialization()
|
||||||
|
public override friction: number = 0;
|
||||||
|
|
||||||
|
@IgnoreSerialization()
|
||||||
|
public override restitution: number = 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### 场景自定义数据
|
### 场景自定义数据
|
||||||
|
|
||||||
除了实体和组件,还可以序列化场景级别的配置数据:
|
除了实体和组件,还可以序列化场景级别的配置数据:
|
||||||
|
|||||||
@@ -33,6 +33,26 @@ class MyService implements IService {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### 服务标识符(ServiceIdentifier)
|
||||||
|
|
||||||
|
服务标识符用于在容器中唯一标识一个服务,支持两种类型:
|
||||||
|
|
||||||
|
- **类构造函数**: 直接使用服务类作为标识符,适用于具体实现类
|
||||||
|
- **Symbol**: 使用 Symbol 作为标识符,适用于接口抽象(推荐用于插件和跨包场景)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 方式1: 使用类作为标识符
|
||||||
|
Core.services.registerSingleton(DataService);
|
||||||
|
const data = Core.services.resolve(DataService);
|
||||||
|
|
||||||
|
// 方式2: 使用 Symbol 作为标识符(推荐用于接口)
|
||||||
|
const IFileSystem = Symbol.for('IFileSystem');
|
||||||
|
Core.services.registerInstance(IFileSystem, new TauriFileSystem());
|
||||||
|
const fs = Core.services.resolve<IFileSystem>(IFileSystem);
|
||||||
|
```
|
||||||
|
|
||||||
|
> **提示**: 使用 `Symbol.for()` 而非 `Symbol()` 可确保跨包/跨模块共享同一个标识符。详见[高级用法 - 接口与 Symbol 标识符模式](#接口与-symbol-标识符模式)。
|
||||||
|
|
||||||
#### 生命周期
|
#### 生命周期
|
||||||
|
|
||||||
服务容器支持两种生命周期:
|
服务容器支持两种生命周期:
|
||||||
@@ -44,7 +64,13 @@ class MyService implements IService {
|
|||||||
|
|
||||||
### 访问服务容器
|
### 访问服务容器
|
||||||
|
|
||||||
Core 类内置了服务容器,可以通过 `Core.services` 访问:
|
ECS Framework 提供了三级服务容器:
|
||||||
|
|
||||||
|
> **版本说明**:World 服务容器功能在 v2.2.13+ 版本中可用
|
||||||
|
|
||||||
|
#### Core 级别服务容器
|
||||||
|
|
||||||
|
应用程序全局服务容器,可以通过 `Core.services` 访问:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Core } from '@esengine/ecs-framework';
|
import { Core } from '@esengine/ecs-framework';
|
||||||
@@ -52,10 +78,53 @@ import { Core } from '@esengine/ecs-framework';
|
|||||||
// 初始化Core
|
// 初始化Core
|
||||||
Core.create({ debug: true });
|
Core.create({ debug: true });
|
||||||
|
|
||||||
// 访问服务容器
|
// 访问全局服务容器
|
||||||
const container = Core.services;
|
const container = Core.services;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### World 级别服务容器
|
||||||
|
|
||||||
|
每个 World 拥有独立的服务容器,用于管理 World 范围内的服务:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { World } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
// 创建 World
|
||||||
|
const world = new World({ name: 'GameWorld' });
|
||||||
|
|
||||||
|
// 访问 World 级别的服务容器
|
||||||
|
const worldContainer = world.services;
|
||||||
|
|
||||||
|
// 注册 World 级别的服务
|
||||||
|
world.services.registerSingleton(RoomManager);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Scene 级别服务容器
|
||||||
|
|
||||||
|
每个 Scene 拥有独立的服务容器,用于管理 Scene 范围内的服务:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 访问 Scene 级别的服务容器
|
||||||
|
const sceneContainer = scene.services;
|
||||||
|
|
||||||
|
// 注册 Scene 级别的服务
|
||||||
|
scene.services.registerSingleton(PhysicsSystem);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 服务容器层级
|
||||||
|
|
||||||
|
```
|
||||||
|
Core.services (应用程序全局)
|
||||||
|
└─ World.services (World 级别)
|
||||||
|
└─ Scene.services (Scene 级别)
|
||||||
|
```
|
||||||
|
|
||||||
|
不同级别的服务容器是独立的,服务不会自动向上或向下查找。选择合适的容器级别:
|
||||||
|
|
||||||
|
- **Core.services**: 应用程序级别的全局服务(配置、插件管理器等)
|
||||||
|
- **World.services**: World 级别的服务(房间管理器、多人游戏状态等)
|
||||||
|
- **Scene.services**: Scene 级别的服务(ECS 系统、场景特定逻辑等)
|
||||||
|
|
||||||
### 注册服务
|
### 注册服务
|
||||||
|
|
||||||
#### 注册单例服务
|
#### 注册单例服务
|
||||||
@@ -284,21 +353,20 @@ class GameService implements IService {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### @Inject 装饰器
|
### @InjectProperty 装饰器
|
||||||
|
|
||||||
在构造函数中注入依赖:
|
通过属性装饰器注入依赖。注入时机是在构造函数执行后、`onInitialize()` 调用前完成:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Injectable, Inject, IService } from '@esengine/ecs-framework';
|
import { Injectable, InjectProperty, IService } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
class PlayerService implements IService {
|
class PlayerService implements IService {
|
||||||
constructor(
|
@InjectProperty(DataService)
|
||||||
@Inject(DataService) private data: DataService,
|
private data!: DataService;
|
||||||
@Inject(GameService) private game: GameService
|
|
||||||
) {
|
@InjectProperty(GameService)
|
||||||
// data 和 game 会自动从容器中解析
|
private game!: GameService;
|
||||||
}
|
|
||||||
|
|
||||||
dispose(): void {
|
dispose(): void {
|
||||||
// 清理资源
|
// 清理资源
|
||||||
@@ -306,6 +374,35 @@ class PlayerService implements IService {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
在 EntitySystem 中使用属性注入:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@Injectable()
|
||||||
|
class CombatSystem extends EntitySystem {
|
||||||
|
@InjectProperty(TimeService)
|
||||||
|
private timeService!: TimeService;
|
||||||
|
|
||||||
|
@InjectProperty(AudioService)
|
||||||
|
private audio!: AudioService;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.all(Health, Attack));
|
||||||
|
}
|
||||||
|
|
||||||
|
onInitialize(): void {
|
||||||
|
// 此时属性已注入完成,可以安全使用
|
||||||
|
console.log('Delta time:', this.timeService.getDeltaTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
processEntity(entity: Entity): void {
|
||||||
|
// 使用注入的服务
|
||||||
|
this.audio.playSound('attack');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **注意**: 属性声明时使用 `!` 断言(如 `private data!: DataService`),表示该属性会在使用前被注入。
|
||||||
|
|
||||||
### 注册可注入服务
|
### 注册可注入服务
|
||||||
|
|
||||||
使用 `registerInjectable` 自动处理依赖注入:
|
使用 `registerInjectable` 自动处理依赖注入:
|
||||||
@@ -313,10 +410,10 @@ class PlayerService implements IService {
|
|||||||
```typescript
|
```typescript
|
||||||
import { registerInjectable } from '@esengine/ecs-framework';
|
import { registerInjectable } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
// 注册服务(会自动解析@Inject依赖)
|
// 注册服务(会自动解析 @InjectProperty 依赖)
|
||||||
registerInjectable(Core.services, PlayerService);
|
registerInjectable(Core.services, PlayerService);
|
||||||
|
|
||||||
// 解析时会自动注入依赖
|
// 解析时会自动注入属性依赖
|
||||||
const player = Core.services.resolve(PlayerService);
|
const player = Core.services.resolve(PlayerService);
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -444,22 +541,164 @@ registerInjectable(Core.services, NetworkService);
|
|||||||
|
|
||||||
## 高级用法
|
## 高级用法
|
||||||
|
|
||||||
### 服务替换(测试)
|
### 接口与 Symbol 标识符模式
|
||||||
|
|
||||||
在测试中替换真实服务为模拟服务:
|
在大型项目或需要跨平台适配的游戏中,推荐使用"接口 + Symbol.for 标识符"模式。这种模式实现了真正的依赖倒置,让代码依赖于抽象而非具体实现。
|
||||||
|
|
||||||
|
#### 为什么使用 Symbol.for
|
||||||
|
|
||||||
|
- **跨包共享**: `Symbol.for('key')` 在全局 Symbol 注册表中创建/获取 Symbol,确保不同包中使用相同的标识符
|
||||||
|
- **接口解耦**: 消费者只依赖接口定义,不依赖具体实现类
|
||||||
|
- **可替换实现**: 可以在运行时注入不同的实现(如测试 Mock、不同平台适配)
|
||||||
|
|
||||||
|
#### 定义接口和标识符
|
||||||
|
|
||||||
|
以音频服务为例,游戏需要在不同平台(Web、微信小游戏、原生App)使用不同的音频实现:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// 测试代码
|
// IAudioService.ts - 定义接口和标识符
|
||||||
class MockDataService implements IService {
|
export interface IAudioService {
|
||||||
getData(key: string) {
|
dispose(): void;
|
||||||
return 'mock data';
|
playSound(id: string): void;
|
||||||
}
|
playMusic(id: string, loop?: boolean): void;
|
||||||
|
stopMusic(): void;
|
||||||
dispose(): void {}
|
setVolume(volume: number): void;
|
||||||
|
preload(id: string, url: string): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 注册模拟服务(用于测试)
|
// 使用 Symbol.for 确保跨包共享同一个 Symbol
|
||||||
Core.services.registerInstance(DataService, new MockDataService());
|
export const IAudioService = Symbol.for('IAudioService');
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 实现接口
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// WebAudioService.ts - Web 平台实现
|
||||||
|
import { IAudioService } from './IAudioService';
|
||||||
|
|
||||||
|
export class WebAudioService implements IAudioService {
|
||||||
|
private audioContext: AudioContext;
|
||||||
|
private sounds: Map<string, AudioBuffer> = new Map();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.audioContext = new AudioContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
playSound(id: string): void {
|
||||||
|
const buffer = this.sounds.get(id);
|
||||||
|
if (buffer) {
|
||||||
|
const source = this.audioContext.createBufferSource();
|
||||||
|
source.buffer = buffer;
|
||||||
|
source.connect(this.audioContext.destination);
|
||||||
|
source.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async preload(id: string, url: string): Promise<void> {
|
||||||
|
const response = await fetch(url);
|
||||||
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
|
const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
|
||||||
|
this.sounds.set(id, audioBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... 其他方法实现
|
||||||
|
|
||||||
|
dispose(): void {
|
||||||
|
this.audioContext.close();
|
||||||
|
this.sounds.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// WechatAudioService.ts - 微信小游戏平台实现
|
||||||
|
export class WechatAudioService implements IAudioService {
|
||||||
|
private innerAudioContexts: Map<string, WechatMinigame.InnerAudioContext> = new Map();
|
||||||
|
|
||||||
|
playSound(id: string): void {
|
||||||
|
const ctx = this.innerAudioContexts.get(id);
|
||||||
|
if (ctx) {
|
||||||
|
ctx.play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async preload(id: string, url: string): Promise<void> {
|
||||||
|
const ctx = wx.createInnerAudioContext();
|
||||||
|
ctx.src = url;
|
||||||
|
this.innerAudioContexts.set(id, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... 其他方法实现
|
||||||
|
|
||||||
|
dispose(): void {
|
||||||
|
for (const ctx of this.innerAudioContexts.values()) {
|
||||||
|
ctx.destroy();
|
||||||
|
}
|
||||||
|
this.innerAudioContexts.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 注册和使用
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { IAudioService } from './IAudioService';
|
||||||
|
import { WebAudioService } from './WebAudioService';
|
||||||
|
import { WechatAudioService } from './WechatAudioService';
|
||||||
|
|
||||||
|
// 根据平台注册不同实现
|
||||||
|
if (typeof wx !== 'undefined') {
|
||||||
|
Core.services.registerInstance(IAudioService, new WechatAudioService());
|
||||||
|
} else {
|
||||||
|
Core.services.registerInstance(IAudioService, new WebAudioService());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 业务代码中使用 - 不关心具体实现
|
||||||
|
const audio = Core.services.resolve<IAudioService>(IAudioService);
|
||||||
|
await audio.preload('explosion', '/sounds/explosion.mp3');
|
||||||
|
audio.playSound('explosion');
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 跨模块使用
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 在游戏系统中使用
|
||||||
|
import { IAudioService } from '@mygame/core';
|
||||||
|
|
||||||
|
class CombatSystem extends EntitySystem {
|
||||||
|
private audio: IAudioService;
|
||||||
|
|
||||||
|
initialize(): void {
|
||||||
|
// 获取音频服务,不需要知道具体实现
|
||||||
|
this.audio = this.scene.services.resolve<IAudioService>(IAudioService);
|
||||||
|
}
|
||||||
|
|
||||||
|
onEntityDeath(entity: Entity): void {
|
||||||
|
this.audio.playSound('death');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Symbol vs Symbol.for
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Symbol() - 每次创建唯一的 Symbol
|
||||||
|
const sym1 = Symbol('test');
|
||||||
|
const sym2 = Symbol('test');
|
||||||
|
console.log(sym1 === sym2); // false - 不同的 Symbol
|
||||||
|
|
||||||
|
// Symbol.for() - 在全局注册表中共享
|
||||||
|
const sym3 = Symbol.for('test');
|
||||||
|
const sym4 = Symbol.for('test');
|
||||||
|
console.log(sym3 === sym4); // true - 同一个 Symbol
|
||||||
|
|
||||||
|
// 跨包场景
|
||||||
|
// package-a/index.ts
|
||||||
|
export const IMyService = Symbol.for('IMyService');
|
||||||
|
|
||||||
|
// package-b/index.ts (不同的包)
|
||||||
|
const IMyService = Symbol.for('IMyService');
|
||||||
|
// 与 package-a 中的是同一个 Symbol!
|
||||||
```
|
```
|
||||||
|
|
||||||
### 循环依赖检测
|
### 循环依赖检测
|
||||||
|
|||||||
@@ -157,8 +157,45 @@ const nameMatcher = Matcher.byName("Player"); // 匹配名称为 "Player" 的实
|
|||||||
|
|
||||||
// 单组件匹配
|
// 单组件匹配
|
||||||
const componentMatcher = Matcher.byComponent(Health); // 匹配拥有 Health 组件的实体
|
const componentMatcher = Matcher.byComponent(Health); // 匹配拥有 Health 组件的实体
|
||||||
|
|
||||||
|
// 不匹配任何实体
|
||||||
|
const nothingMatcher = Matcher.nothing(); // 用于只需要生命周期回调的系统
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 空匹配器 vs Nothing 匹配器
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// empty() - 空条件,匹配所有实体
|
||||||
|
const emptyMatcher = Matcher.empty();
|
||||||
|
|
||||||
|
// nothing() - 不匹配任何实体,用于只需要生命周期方法的系统
|
||||||
|
const nothingMatcher = Matcher.nothing();
|
||||||
|
|
||||||
|
// 使用场景:只需要 onBegin/onEnd 生命周期的系统
|
||||||
|
@ECSSystem('FrameTimer')
|
||||||
|
class FrameTimerSystem extends EntitySystem {
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.nothing()); // 不处理任何实体
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onBegin(): void {
|
||||||
|
// 每帧开始时执行,例如:记录帧开始时间
|
||||||
|
console.log('帧开始');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected process(entities: readonly Entity[]): void {
|
||||||
|
// 永远不会被调用,因为没有匹配的实体
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onEnd(): void {
|
||||||
|
// 每帧结束时执行
|
||||||
|
console.log('帧结束');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> 💡 **提示**:更多关于 Matcher 和实体查询的详细用法,请参考 [实体查询系统](/guide/entity-query) 文档。
|
||||||
|
|
||||||
## 系统生命周期
|
## 系统生命周期
|
||||||
|
|
||||||
系统提供了完整的生命周期回调:
|
系统提供了完整的生命周期回调:
|
||||||
@@ -563,9 +600,28 @@ class GameSystem extends EntitySystem {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. 使用装饰器
|
### 2. 使用 @ECSSystem 装饰器
|
||||||
|
|
||||||
**必须使用 `@ECSSystem` 装饰器**:
|
`@ECSSystem` 是系统类必须使用的装饰器,它为系统提供类型标识和元数据管理。
|
||||||
|
|
||||||
|
#### 为什么必须使用
|
||||||
|
|
||||||
|
| 功能 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| **类型识别** | 提供稳定的系统名称,代码混淆后仍能正确识别 |
|
||||||
|
| **调试支持** | 在性能监控、日志和调试工具中显示可读的系统名称 |
|
||||||
|
| **系统管理** | 通过名称查找和管理系统 |
|
||||||
|
| **序列化支持** | 场景序列化时可以记录系统配置 |
|
||||||
|
|
||||||
|
#### 基本语法
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@ECSSystem(systemName: string)
|
||||||
|
```
|
||||||
|
|
||||||
|
- `systemName`: 系统的名称,建议使用描述性的名称
|
||||||
|
|
||||||
|
#### 使用示例
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// ✅ 正确的用法
|
// ✅ 正确的用法
|
||||||
@@ -574,12 +630,41 @@ class PhysicsSystem extends EntitySystem {
|
|||||||
// 系统实现
|
// 系统实现
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ✅ 推荐:使用描述性的名称
|
||||||
|
@ECSSystem('PlayerMovement')
|
||||||
|
class PlayerMovementSystem extends EntitySystem {
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.all(Player, Position, Velocity));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ❌ 错误的用法 - 没有装饰器
|
// ❌ 错误的用法 - 没有装饰器
|
||||||
class BadSystem extends EntitySystem {
|
class BadSystem extends EntitySystem {
|
||||||
// 这样定义的系统可能在生产环境出现问题
|
// 这样定义的系统可能在生产环境出现问题:
|
||||||
|
// 1. 代码压缩后类名变化,无法正确识别
|
||||||
|
// 2. 性能监控和调试工具显示不正确的名称
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### 系统名称的作用
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@ECSSystem('Combat')
|
||||||
|
class CombatSystem extends EntitySystem {
|
||||||
|
protected onInitialize(): void {
|
||||||
|
// 使用 systemName 属性访问系统名称
|
||||||
|
console.log(`系统 ${this.systemName} 已初始化`); // 输出: 系统 Combat 已初始化
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通过名称查找系统
|
||||||
|
const combat = scene.getSystemByName('Combat');
|
||||||
|
|
||||||
|
// 性能监控中会显示系统名称
|
||||||
|
const perfData = combatSystem.getPerformanceData();
|
||||||
|
console.log(`${combatSystem.systemName} 执行时间: ${perfData?.executionTime}ms`);
|
||||||
|
```
|
||||||
|
|
||||||
### 3. 合理的更新顺序
|
### 3. 合理的更新顺序
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
|
|||||||
@@ -435,7 +435,7 @@ const worldManager = Core.services.resolve(WorldManager);
|
|||||||
// {
|
// {
|
||||||
// maxWorlds: 50,
|
// maxWorlds: 50,
|
||||||
// autoCleanup: true,
|
// autoCleanup: true,
|
||||||
// cleanupInterval: 30000 // 30 秒
|
// cleanupFrameInterval: 1800 // 间隔多少帧清理闲置 World
|
||||||
// }
|
// }
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
334
docs/index.md
334
docs/index.md
@@ -1,23 +1,317 @@
|
|||||||
---
|
---
|
||||||
layout: home
|
layout: page
|
||||||
|
title: ESEngine - 高性能 TypeScript ECS 框架
|
||||||
|
---
|
||||||
|
|
||||||
hero:
|
<ParticleHero />
|
||||||
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:
|
<section class="news-section">
|
||||||
- title: 高性能
|
<div class="news-container">
|
||||||
details: 支持大规模实体处理
|
<div class="news-header">
|
||||||
- title: 类型安全
|
<h2 class="news-title">快速入口</h2>
|
||||||
details: 完整的TypeScript支持,编译时类型检查
|
<a href="/guide/" class="news-more">查看文档</a>
|
||||||
- title: 模块化设计
|
</div>
|
||||||
details: 核心功能独立打包,支持多平台
|
<div class="news-grid">
|
||||||
---
|
<a href="/guide/getting-started" class="news-card">
|
||||||
|
<div class="news-card-image" style="background: linear-gradient(135deg, #1e3a5f 0%, #1e1e1e 100%);">
|
||||||
|
<div class="news-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24"><path fill="#4fc1ff" d="M12 3L1 9l4 2.18v6L12 21l7-3.82v-6l2-1.09V17h2V9zm6.82 6L12 12.72L5.18 9L12 5.28zM17 16l-5 2.72L7 16v-3.73L12 15l5-2.73z"/></svg>
|
||||||
|
</div>
|
||||||
|
<span class="news-badge">快速开始</span>
|
||||||
|
</div>
|
||||||
|
<div class="news-card-content">
|
||||||
|
<h3>5 分钟上手 ESEngine</h3>
|
||||||
|
<p>从安装到创建第一个 ECS 应用,快速了解核心概念。</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a href="/guide/behavior-tree/" class="news-card">
|
||||||
|
<div class="news-card-image" style="background: linear-gradient(135deg, #1e3a5f 0%, #1e1e1e 100%);">
|
||||||
|
<div class="news-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24"><path fill="#4ec9b0" d="M12 2a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m3 20h-1v-7l-2-2l-2 2v7H9v-7.5l-2 2V22H6v-6l3-3l1-3.5c-.3.4-.6.7-1 1L6 9v1H4V8l5-3c.5-.3 1.1-.5 1.7-.5H11c.6 0 1.2.2 1.7.5l5 3v2h-2V9l-3 1.5c-.4-.3-.7-.6-1-1l1 3.5l3 3v6Z"/></svg>
|
||||||
|
</div>
|
||||||
|
<span class="news-badge">AI 系统</span>
|
||||||
|
</div>
|
||||||
|
<div class="news-card-content">
|
||||||
|
<h3>行为树可视化编辑器</h3>
|
||||||
|
<p>内置 AI 行为树系统,支持可视化编辑和实时调试。</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="features-section">
|
||||||
|
<div class="features-container">
|
||||||
|
<h2 class="features-title">核心特性</h2>
|
||||||
|
<div class="features-grid">
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#4fc1ff" d="M13 2.05v2.02c3.95.49 7 3.85 7 7.93c0 1.45-.39 2.79-1.06 3.95l1.59 1.09A9.94 9.94 0 0 0 22 12c0-5.18-3.95-9.45-9-9.95M12 19c-3.87 0-7-3.13-7-7c0-3.53 2.61-6.43 6-6.92V2.05c-5.06.5-9 4.76-9 9.95c0 5.52 4.47 10 9.99 10c3.31 0 6.24-1.61 8.06-4.09l-1.6-1.1A7.93 7.93 0 0 1 12 19"/><path fill="#4fc1ff" d="M12 6a6 6 0 0 0-6 6c0 3.31 2.69 6 6 6a6 6 0 0 0 0-12m0 10c-2.21 0-4-1.79-4-4s1.79-4 4-4s4 1.79 4 4s-1.79 4-4 4"/></svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="feature-title">高性能 ECS 架构</h3>
|
||||||
|
<p class="feature-desc">基于数据驱动的实体组件系统,支持大规模实体处理,缓存友好的内存布局。</p>
|
||||||
|
<a href="/guide/entity" class="feature-link">了解更多 →</a>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#569cd6" d="M3 3h18v18H3zm16.525 13.707c0-.795-.272-1.425-.816-1.89c-.544-.465-1.404-.804-2.58-1.016l-1.704-.296c-.616-.104-1.052-.26-1.308-.468c-.256-.21-.384-.468-.384-.776c0-.392.168-.7.504-.924c.336-.224.8-.336 1.392-.336c.56 0 1.008.124 1.344.372c.336.248.536.584.6 1.008h2.016c-.08-.96-.464-1.716-1.152-2.268c-.688-.552-1.6-.828-2.736-.828c-1.2 0-2.148.3-2.844.9c-.696.6-1.044 1.38-1.044 2.34c0 .76.252 1.368.756 1.824c.504.456 1.308.792 2.412.996l1.704.312c.624.12 1.068.28 1.332.48c.264.2.396.46.396.78c0 .424-.192.756-.576.996c-.384.24-.9.36-1.548.36c-.672 0-1.2-.14-1.584-.42c-.384-.28-.608-.668-.672-1.164H8.868c.048 1.016.46 1.808 1.236 2.376c.776.568 1.796.852 3.06.852c1.24 0 2.22-.292 2.94-.876c.72-.584 1.08-1.364 1.08-2.34z"/></svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="feature-title">完整类型支持</h3>
|
||||||
|
<p class="feature-desc">100% TypeScript 编写,完整的类型定义和编译时检查,提供最佳的开发体验。</p>
|
||||||
|
<a href="/guide/component" class="feature-link">了解更多 →</a>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#4ec9b0" d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10s10-4.5 10-10S17.5 2 12 2m0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8s8 3.59 8 8s-3.59 8-8 8m-5-8l4-4v3h4v2h-4v3z"/></svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="feature-title">可视化行为树</h3>
|
||||||
|
<p class="feature-desc">内置 AI 行为树系统,提供可视化编辑器,支持自定义节点和实时调试。</p>
|
||||||
|
<a href="/guide/behavior-tree/" class="feature-link">了解更多 →</a>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#c586c0" d="M4 6h18V4H4c-1.1 0-2 .9-2 2v11H0v3h14v-3H4zm19 2h-6c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h6c.55 0 1-.45 1-1V9c0-.55-.45-1-1-1m-1 9h-4v-7h4z"/></svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="feature-title">多平台支持</h3>
|
||||||
|
<p class="feature-desc">支持浏览器、Node.js、微信小游戏等多平台,可与主流游戏引擎无缝集成。</p>
|
||||||
|
<a href="/guide/platform-adapter" class="feature-link">了解更多 →</a>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#dcdcaa" d="M4 3h6v2H4v14h6v2H4c-1.1 0-2-.9-2-2V5c0-1.1.9-2 2-2m9 0h6c1.1 0 2 .9 2 2v14c0 1.1-.9 2-2 2h-6v-2h6V5h-6zm-1 7h4v2h-4z"/></svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="feature-title">模块化设计</h3>
|
||||||
|
<p class="feature-desc">核心功能独立打包,按需引入。支持自定义插件扩展,灵活适配不同项目。</p>
|
||||||
|
<a href="/guide/plugin-system" class="feature-link">了解更多 →</a>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#9cdcfe" d="M22.7 19l-9.1-9.1c.9-2.3.4-5-1.5-6.9c-2-2-5-2.4-7.4-1.3L9 6L6 9L1.6 4.7C.4 7.1.9 10.1 2.9 12.1c1.9 1.9 4.6 2.4 6.9 1.5l9.1 9.1c.4.4 1 .4 1.4 0l2.3-2.3c.5-.4.5-1.1.1-1.4"/></svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="feature-title">开发者工具</h3>
|
||||||
|
<p class="feature-desc">内置性能监控、调试工具、序列化系统等,提供完整的开发工具链。</p>
|
||||||
|
<a href="/guide/logging" class="feature-link">了解更多 →</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 首页专用样式 | Home page specific styles */
|
||||||
|
.news-section {
|
||||||
|
background: #0d0d0d;
|
||||||
|
padding: 64px 0;
|
||||||
|
border-top: 1px solid #2a2a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-container {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ffffff;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-more {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 1px solid #2a2a2a;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #a0a0a0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-more:hover {
|
||||||
|
background: #252525;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card {
|
||||||
|
display: flex;
|
||||||
|
background: #1f1f1f;
|
||||||
|
border: 1px solid #2a2a2a;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card:hover {
|
||||||
|
border-color: #3b9eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card-image {
|
||||||
|
width: 200px;
|
||||||
|
min-height: 140px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 16px;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-icon {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 12px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #3a3a3a;
|
||||||
|
border-radius: 16px;
|
||||||
|
color: #a0a0a0;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card-content {
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card-content h3 {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card-content p {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #707070;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-section {
|
||||||
|
background: #0d0d0d;
|
||||||
|
padding: 64px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-container {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ffffff;
|
||||||
|
margin: 0 0 32px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card {
|
||||||
|
background: #1f1f1f;
|
||||||
|
border: 1px solid #2a2a2a;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card:hover {
|
||||||
|
border-color: #3b9eff;
|
||||||
|
background: #252525;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
background: #0d0d0d;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-desc {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #707070;
|
||||||
|
line-height: 1.7;
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-link {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #3b9eff;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.news-container,
|
||||||
|
.features-container {
|
||||||
|
padding: 0 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.news-card {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card-image {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
1
docs/public/CNAME
Normal file
1
docs/public/CNAME
Normal file
@@ -0,0 +1 @@
|
|||||||
|
esengine.cn
|
||||||
72
eslint.config.mjs
Normal file
72
eslint.config.mjs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import eslint from '@eslint/js';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
eslint.configs.recommended,
|
||||||
|
...tseslint.configs.recommended,
|
||||||
|
{
|
||||||
|
files: ['packages/**/src/**/*.{ts,tsx,js,jsx}'],
|
||||||
|
languageOptions: {
|
||||||
|
parser: tseslint.parser,
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
sourceType: 'module',
|
||||||
|
project: true,
|
||||||
|
tsconfigRootDir: import.meta.dirname
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'semi': ['warn', 'always'],
|
||||||
|
'quotes': ['warn', 'single', { avoidEscape: true }],
|
||||||
|
'indent': ['warn', 4, {
|
||||||
|
SwitchCase: 1,
|
||||||
|
ignoredNodes: [
|
||||||
|
'PropertyDefinition[decorators.length > 0]',
|
||||||
|
'TSTypeParameterInstantiation'
|
||||||
|
]
|
||||||
|
}],
|
||||||
|
'no-trailing-spaces': 'warn',
|
||||||
|
'eol-last': ['warn', 'always'],
|
||||||
|
'comma-dangle': ['warn', 'never'],
|
||||||
|
'object-curly-spacing': ['warn', 'always'],
|
||||||
|
'array-bracket-spacing': ['warn', 'never'],
|
||||||
|
'arrow-parens': ['warn', 'always'],
|
||||||
|
'no-multiple-empty-lines': ['warn', { max: 2, maxEOF: 1 }],
|
||||||
|
'no-console': 'off',
|
||||||
|
'no-empty': 'warn',
|
||||||
|
'no-case-declarations': 'warn',
|
||||||
|
'no-useless-catch': 'warn',
|
||||||
|
'no-prototype-builtins': 'warn',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'warn',
|
||||||
|
'@typescript-eslint/no-unsafe-assignment': 'warn',
|
||||||
|
'@typescript-eslint/no-unsafe-member-access': 'warn',
|
||||||
|
'@typescript-eslint/no-unsafe-call': 'warn',
|
||||||
|
'@typescript-eslint/no-unsafe-return': 'warn',
|
||||||
|
'@typescript-eslint/no-unsafe-argument': 'warn',
|
||||||
|
'@typescript-eslint/no-unsafe-function-type': 'warn',
|
||||||
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
|
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
|
||||||
|
'@typescript-eslint/no-non-null-assertion': 'off'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ignores: [
|
||||||
|
'node_modules/**',
|
||||||
|
'**/node_modules/**',
|
||||||
|
'dist/**',
|
||||||
|
'**/dist/**',
|
||||||
|
'bin/**',
|
||||||
|
'**/bin/**',
|
||||||
|
'build/**',
|
||||||
|
'**/build/**',
|
||||||
|
'coverage/**',
|
||||||
|
'**/coverage/**',
|
||||||
|
'thirdparty/**',
|
||||||
|
'examples/lawn-mower-demo/**',
|
||||||
|
'extensions/**',
|
||||||
|
'**/*.min.js',
|
||||||
|
'**/*.d.ts',
|
||||||
|
'**/wasm/**'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
352
examples/core-demos/pnpm-lock.yaml
generated
Normal file
352
examples/core-demos/pnpm-lock.yaml
generated
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
lockfileVersion: '9.0'
|
||||||
|
|
||||||
|
settings:
|
||||||
|
autoInstallPeers: true
|
||||||
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
|
importers:
|
||||||
|
|
||||||
|
.:
|
||||||
|
dependencies:
|
||||||
|
'@esengine/ecs-framework':
|
||||||
|
specifier: file:../../packages/core
|
||||||
|
version: file:../../packages/core
|
||||||
|
devDependencies:
|
||||||
|
typescript:
|
||||||
|
specifier: ^5.0.0
|
||||||
|
version: 5.9.3
|
||||||
|
vite:
|
||||||
|
specifier: ^4.0.0
|
||||||
|
version: 4.5.14
|
||||||
|
|
||||||
|
packages:
|
||||||
|
|
||||||
|
'@esbuild/android-arm64@0.18.20':
|
||||||
|
resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@esbuild/android-arm@0.18.20':
|
||||||
|
resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@esbuild/android-x64@0.18.20':
|
||||||
|
resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@esbuild/darwin-arm64@0.18.20':
|
||||||
|
resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@esbuild/darwin-x64@0.18.20':
|
||||||
|
resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@esbuild/freebsd-arm64@0.18.20':
|
||||||
|
resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@esbuild/freebsd-x64@0.18.20':
|
||||||
|
resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@esbuild/linux-arm64@0.18.20':
|
||||||
|
resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-arm@0.18.20':
|
||||||
|
resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-ia32@0.18.20':
|
||||||
|
resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-loong64@0.18.20':
|
||||||
|
resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [loong64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-mips64el@0.18.20':
|
||||||
|
resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [mips64el]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-ppc64@0.18.20':
|
||||||
|
resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-riscv64@0.18.20':
|
||||||
|
resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [riscv64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-s390x@0.18.20':
|
||||||
|
resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [s390x]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-x64@0.18.20':
|
||||||
|
resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/netbsd-x64@0.18.20':
|
||||||
|
resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [netbsd]
|
||||||
|
|
||||||
|
'@esbuild/openbsd-x64@0.18.20':
|
||||||
|
resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [openbsd]
|
||||||
|
|
||||||
|
'@esbuild/sunos-x64@0.18.20':
|
||||||
|
resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [sunos]
|
||||||
|
|
||||||
|
'@esbuild/win32-arm64@0.18.20':
|
||||||
|
resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@esbuild/win32-ia32@0.18.20':
|
||||||
|
resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@esbuild/win32-x64@0.18.20':
|
||||||
|
resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@esengine/ecs-framework@file:../../packages/core':
|
||||||
|
resolution: {directory: ../../packages/core, type: directory}
|
||||||
|
|
||||||
|
esbuild@0.18.20:
|
||||||
|
resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
fsevents@2.3.3:
|
||||||
|
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||||
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
nanoid@3.3.11:
|
||||||
|
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
|
||||||
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
picocolors@1.1.1:
|
||||||
|
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||||
|
|
||||||
|
postcss@8.5.6:
|
||||||
|
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
||||||
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
|
|
||||||
|
rollup@3.29.5:
|
||||||
|
resolution: {integrity: sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==}
|
||||||
|
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
source-map-js@1.2.1:
|
||||||
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
tslib@2.8.1:
|
||||||
|
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||||
|
|
||||||
|
typescript@5.9.3:
|
||||||
|
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||||
|
engines: {node: '>=14.17'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
vite@4.5.14:
|
||||||
|
resolution: {integrity: sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==}
|
||||||
|
engines: {node: ^14.18.0 || >=16.0.0}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
'@types/node': '>= 14'
|
||||||
|
less: '*'
|
||||||
|
lightningcss: ^1.21.0
|
||||||
|
sass: '*'
|
||||||
|
stylus: '*'
|
||||||
|
sugarss: '*'
|
||||||
|
terser: ^5.4.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/node':
|
||||||
|
optional: true
|
||||||
|
less:
|
||||||
|
optional: true
|
||||||
|
lightningcss:
|
||||||
|
optional: true
|
||||||
|
sass:
|
||||||
|
optional: true
|
||||||
|
stylus:
|
||||||
|
optional: true
|
||||||
|
sugarss:
|
||||||
|
optional: true
|
||||||
|
terser:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
snapshots:
|
||||||
|
|
||||||
|
'@esbuild/android-arm64@0.18.20':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/android-arm@0.18.20':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/android-x64@0.18.20':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/darwin-arm64@0.18.20':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/darwin-x64@0.18.20':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/freebsd-arm64@0.18.20':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/freebsd-x64@0.18.20':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-arm64@0.18.20':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-arm@0.18.20':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-ia32@0.18.20':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-loong64@0.18.20':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-mips64el@0.18.20':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-ppc64@0.18.20':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-riscv64@0.18.20':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-s390x@0.18.20':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-x64@0.18.20':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/netbsd-x64@0.18.20':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/openbsd-x64@0.18.20':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/sunos-x64@0.18.20':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/win32-arm64@0.18.20':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/win32-ia32@0.18.20':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/win32-x64@0.18.20':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esengine/ecs-framework@file:../../packages/core':
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
esbuild@0.18.20:
|
||||||
|
optionalDependencies:
|
||||||
|
'@esbuild/android-arm': 0.18.20
|
||||||
|
'@esbuild/android-arm64': 0.18.20
|
||||||
|
'@esbuild/android-x64': 0.18.20
|
||||||
|
'@esbuild/darwin-arm64': 0.18.20
|
||||||
|
'@esbuild/darwin-x64': 0.18.20
|
||||||
|
'@esbuild/freebsd-arm64': 0.18.20
|
||||||
|
'@esbuild/freebsd-x64': 0.18.20
|
||||||
|
'@esbuild/linux-arm': 0.18.20
|
||||||
|
'@esbuild/linux-arm64': 0.18.20
|
||||||
|
'@esbuild/linux-ia32': 0.18.20
|
||||||
|
'@esbuild/linux-loong64': 0.18.20
|
||||||
|
'@esbuild/linux-mips64el': 0.18.20
|
||||||
|
'@esbuild/linux-ppc64': 0.18.20
|
||||||
|
'@esbuild/linux-riscv64': 0.18.20
|
||||||
|
'@esbuild/linux-s390x': 0.18.20
|
||||||
|
'@esbuild/linux-x64': 0.18.20
|
||||||
|
'@esbuild/netbsd-x64': 0.18.20
|
||||||
|
'@esbuild/openbsd-x64': 0.18.20
|
||||||
|
'@esbuild/sunos-x64': 0.18.20
|
||||||
|
'@esbuild/win32-arm64': 0.18.20
|
||||||
|
'@esbuild/win32-ia32': 0.18.20
|
||||||
|
'@esbuild/win32-x64': 0.18.20
|
||||||
|
|
||||||
|
fsevents@2.3.3:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
nanoid@3.3.11: {}
|
||||||
|
|
||||||
|
picocolors@1.1.1: {}
|
||||||
|
|
||||||
|
postcss@8.5.6:
|
||||||
|
dependencies:
|
||||||
|
nanoid: 3.3.11
|
||||||
|
picocolors: 1.1.1
|
||||||
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
|
rollup@3.29.5:
|
||||||
|
optionalDependencies:
|
||||||
|
fsevents: 2.3.3
|
||||||
|
|
||||||
|
source-map-js@1.2.1: {}
|
||||||
|
|
||||||
|
tslib@2.8.1: {}
|
||||||
|
|
||||||
|
typescript@5.9.3: {}
|
||||||
|
|
||||||
|
vite@4.5.14:
|
||||||
|
dependencies:
|
||||||
|
esbuild: 0.18.20
|
||||||
|
postcss: 8.5.6
|
||||||
|
rollup: 3.29.5
|
||||||
|
optionalDependencies:
|
||||||
|
fsevents: 2.3.3
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
[InternetShortcut]
|
|
||||||
URL=https://docs.cocos.com/creator/manual/en/scripting/setup.html#custom-script-template
|
|
||||||
24
extensions/cocos/cocos-ecs/.gitignore
vendored
24
extensions/cocos/cocos-ecs/.gitignore
vendored
@@ -1,24 +0,0 @@
|
|||||||
|
|
||||||
#///////////////////////////
|
|
||||||
# Cocos Creator 3D Project
|
|
||||||
#///////////////////////////
|
|
||||||
library/
|
|
||||||
temp/
|
|
||||||
local/
|
|
||||||
build/
|
|
||||||
profiles/
|
|
||||||
native
|
|
||||||
#//////////////////////////
|
|
||||||
# NPM
|
|
||||||
#//////////////////////////
|
|
||||||
node_modules/
|
|
||||||
|
|
||||||
#//////////////////////////
|
|
||||||
# VSCode
|
|
||||||
#//////////////////////////
|
|
||||||
.vscode/
|
|
||||||
|
|
||||||
#//////////////////////////
|
|
||||||
# WebStorm
|
|
||||||
#//////////////////////////
|
|
||||||
.idea/
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"ver": "1.2.0",
|
|
||||||
"importer": "directory",
|
|
||||||
"imported": true,
|
|
||||||
"uuid": "8c25761f-50d6-498b-a95f-d863bf1fbff1",
|
|
||||||
"files": [],
|
|
||||||
"subMetas": {},
|
|
||||||
"userData": {}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"ver": "1.2.0",
|
|
||||||
"importer": "directory",
|
|
||||||
"imported": true,
|
|
||||||
"uuid": "3a66cbbc-6612-4408-838b-875d0bb2e9a3",
|
|
||||||
"files": [],
|
|
||||||
"subMetas": {},
|
|
||||||
"userData": {}
|
|
||||||
}
|
|
||||||
@@ -1,317 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"ver": "2.0.1",
|
|
||||||
"importer": "json",
|
|
||||||
"imported": true,
|
|
||||||
"uuid": "598e1450-8c7a-46c7-9540-398f9809d627",
|
|
||||||
"files": [
|
|
||||||
".json"
|
|
||||||
],
|
|
||||||
"subMetas": {},
|
|
||||||
"userData": {}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"ver": "1.0.0",
|
|
||||||
"importer": "*",
|
|
||||||
"imported": true,
|
|
||||||
"uuid": "24c6e7e6-4ff0-4e7b-b470-9468bfa66b5d",
|
|
||||||
"files": [
|
|
||||||
".btree",
|
|
||||||
".json"
|
|
||||||
],
|
|
||||||
"subMetas": {},
|
|
||||||
"userData": {}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"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
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"ver": "1.1.50",
|
|
||||||
"importer": "prefab",
|
|
||||||
"imported": true,
|
|
||||||
"uuid": "51a6e245-2983-4258-be9f-9e21378f7f9f",
|
|
||||||
"files": [
|
|
||||||
".json"
|
|
||||||
],
|
|
||||||
"subMetas": {},
|
|
||||||
"userData": {
|
|
||||||
"syncNodeName": "Panel_Node"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"ver": "1.2.0",
|
|
||||||
"importer": "directory",
|
|
||||||
"imported": true,
|
|
||||||
"uuid": "4fd895f7-6b0f-4357-aa3a-7c2e88ffac9a",
|
|
||||||
"files": [],
|
|
||||||
"subMetas": {},
|
|
||||||
"userData": {}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"ver": "1.2.0",
|
|
||||||
"importer": "directory",
|
|
||||||
"imported": true,
|
|
||||||
"uuid": "829183be-61a1-4494-bf64-3df359c0e8e7",
|
|
||||||
"files": [],
|
|
||||||
"subMetas": {},
|
|
||||||
"userData": {}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"ver": "1.2.0",
|
|
||||||
"importer": "directory",
|
|
||||||
"imported": true,
|
|
||||||
"uuid": "240e4a78-e55f-47a8-84de-39220bba1321",
|
|
||||||
"files": [],
|
|
||||||
"subMetas": {},
|
|
||||||
"userData": {}
|
|
||||||
}
|
|
||||||
@@ -1,757 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"__type__": "cc.SceneAsset",
|
|
||||||
"_name": "behaviour-example-scene",
|
|
||||||
"_objFlags": 0,
|
|
||||||
"__editorExtras__": {},
|
|
||||||
"_native": "",
|
|
||||||
"scene": {
|
|
||||||
"__id__": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"__type__": "cc.Scene",
|
|
||||||
"_name": "behaviour-example-scene",
|
|
||||||
"_objFlags": 0,
|
|
||||||
"__editorExtras__": {},
|
|
||||||
"_parent": null,
|
|
||||||
"_children": [
|
|
||||||
{
|
|
||||||
"__id__": 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"__id__": 5
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"__id__": 7
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"__id__": 8
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"__id__": 14
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"_active": true,
|
|
||||||
"_components": [],
|
|
||||||
"_prefab": null,
|
|
||||||
"_lpos": {
|
|
||||||
"__type__": "cc.Vec3",
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"_lrot": {
|
|
||||||
"__type__": "cc.Quat",
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0,
|
|
||||||
"w": 1
|
|
||||||
},
|
|
||||||
"_lscale": {
|
|
||||||
"__type__": "cc.Vec3",
|
|
||||||
"x": 1,
|
|
||||||
"y": 1,
|
|
||||||
"z": 1
|
|
||||||
},
|
|
||||||
"_mobility": 0,
|
|
||||||
"_layer": 1073741824,
|
|
||||||
"_euler": {
|
|
||||||
"__type__": "cc.Vec3",
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"autoReleaseAssets": false,
|
|
||||||
"_globals": {
|
|
||||||
"__id__": 16
|
|
||||||
},
|
|
||||||
"_id": "ff354f0b-c2f5-4dea-8ffb-0152d175d11c"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"__type__": "cc.Node",
|
|
||||||
"_name": "Main Light",
|
|
||||||
"_objFlags": 0,
|
|
||||||
"__editorExtras__": {},
|
|
||||||
"_parent": {
|
|
||||||
"__id__": 1
|
|
||||||
},
|
|
||||||
"_children": [],
|
|
||||||
"_active": true,
|
|
||||||
"_components": [
|
|
||||||
{
|
|
||||||
"__id__": 3
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"_prefab": null,
|
|
||||||
"_lpos": {
|
|
||||||
"__type__": "cc.Vec3",
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"_lrot": {
|
|
||||||
"__type__": "cc.Quat",
|
|
||||||
"x": -0.06397656665577071,
|
|
||||||
"y": -0.44608233363525845,
|
|
||||||
"z": -0.8239028751062036,
|
|
||||||
"w": -0.3436591377065261
|
|
||||||
},
|
|
||||||
"_lscale": {
|
|
||||||
"__type__": "cc.Vec3",
|
|
||||||
"x": 1,
|
|
||||||
"y": 1,
|
|
||||||
"z": 1
|
|
||||||
},
|
|
||||||
"_mobility": 0,
|
|
||||||
"_layer": 1073741824,
|
|
||||||
"_euler": {
|
|
||||||
"__type__": "cc.Vec3",
|
|
||||||
"x": -117.894,
|
|
||||||
"y": -194.909,
|
|
||||||
"z": 38.562
|
|
||||||
},
|
|
||||||
"_id": "c0y6F5f+pAvI805TdmxIjx"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"__type__": "cc.DirectionalLight",
|
|
||||||
"_name": "",
|
|
||||||
"_objFlags": 0,
|
|
||||||
"__editorExtras__": {},
|
|
||||||
"node": {
|
|
||||||
"__id__": 2
|
|
||||||
},
|
|
||||||
"_enabled": true,
|
|
||||||
"__prefab": null,
|
|
||||||
"_color": {
|
|
||||||
"__type__": "cc.Color",
|
|
||||||
"r": 255,
|
|
||||||
"g": 250,
|
|
||||||
"b": 240,
|
|
||||||
"a": 255
|
|
||||||
},
|
|
||||||
"_useColorTemperature": false,
|
|
||||||
"_colorTemperature": 6550,
|
|
||||||
"_staticSettings": {
|
|
||||||
"__id__": 4
|
|
||||||
},
|
|
||||||
"_visibility": -325058561,
|
|
||||||
"_illuminanceHDR": 65000,
|
|
||||||
"_illuminance": 65000,
|
|
||||||
"_illuminanceLDR": 1.6927083333333335,
|
|
||||||
"_shadowEnabled": false,
|
|
||||||
"_shadowPcf": 0,
|
|
||||||
"_shadowBias": 0.00001,
|
|
||||||
"_shadowNormalBias": 0,
|
|
||||||
"_shadowSaturation": 1,
|
|
||||||
"_shadowDistance": 50,
|
|
||||||
"_shadowInvisibleOcclusionRange": 200,
|
|
||||||
"_csmLevel": 4,
|
|
||||||
"_csmLayerLambda": 0.75,
|
|
||||||
"_csmOptimizationMode": 2,
|
|
||||||
"_csmAdvancedOptions": false,
|
|
||||||
"_csmLayersTransition": false,
|
|
||||||
"_csmTransitionRange": 0.05,
|
|
||||||
"_shadowFixedArea": false,
|
|
||||||
"_shadowNear": 0.1,
|
|
||||||
"_shadowFar": 10,
|
|
||||||
"_shadowOrthoSize": 5,
|
|
||||||
"_id": "597uMYCbhEtJQc0ffJlcgA"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"__type__": "cc.StaticLightSettings",
|
|
||||||
"_baked": false,
|
|
||||||
"_editorOnly": false,
|
|
||||||
"_castShadow": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"__type__": "cc.Node",
|
|
||||||
"_name": "Main Camera",
|
|
||||||
"_objFlags": 0,
|
|
||||||
"__editorExtras__": {},
|
|
||||||
"_parent": {
|
|
||||||
"__id__": 1
|
|
||||||
},
|
|
||||||
"_children": [],
|
|
||||||
"_active": true,
|
|
||||||
"_components": [
|
|
||||||
{
|
|
||||||
"__id__": 6
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"_prefab": null,
|
|
||||||
"_lpos": {
|
|
||||||
"__type__": "cc.Vec3",
|
|
||||||
"x": -10,
|
|
||||||
"y": 10,
|
|
||||||
"z": 10
|
|
||||||
},
|
|
||||||
"_lrot": {
|
|
||||||
"__type__": "cc.Quat",
|
|
||||||
"x": -0.27781593346944056,
|
|
||||||
"y": -0.36497167621709875,
|
|
||||||
"z": -0.11507512748638377,
|
|
||||||
"w": 0.8811195706053617
|
|
||||||
},
|
|
||||||
"_lscale": {
|
|
||||||
"__type__": "cc.Vec3",
|
|
||||||
"x": 1,
|
|
||||||
"y": 1,
|
|
||||||
"z": 1
|
|
||||||
},
|
|
||||||
"_mobility": 0,
|
|
||||||
"_layer": 1073741824,
|
|
||||||
"_euler": {
|
|
||||||
"__type__": "cc.Vec3",
|
|
||||||
"x": -35,
|
|
||||||
"y": -45,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"_id": "c9DMICJLFO5IeO07EPon7U"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"__type__": "cc.Camera",
|
|
||||||
"_name": "",
|
|
||||||
"_objFlags": 0,
|
|
||||||
"__editorExtras__": {},
|
|
||||||
"node": {
|
|
||||||
"__id__": 5
|
|
||||||
},
|
|
||||||
"_enabled": true,
|
|
||||||
"__prefab": null,
|
|
||||||
"_projection": 1,
|
|
||||||
"_priority": 0,
|
|
||||||
"_fov": 45,
|
|
||||||
"_fovAxis": 0,
|
|
||||||
"_orthoHeight": 10,
|
|
||||||
"_near": 1,
|
|
||||||
"_far": 1000,
|
|
||||||
"_color": {
|
|
||||||
"__type__": "cc.Color",
|
|
||||||
"r": 51,
|
|
||||||
"g": 51,
|
|
||||||
"b": 51,
|
|
||||||
"a": 255
|
|
||||||
},
|
|
||||||
"_depth": 1,
|
|
||||||
"_stencil": 0,
|
|
||||||
"_clearFlags": 14,
|
|
||||||
"_rect": {
|
|
||||||
"__type__": "cc.Rect",
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"width": 1,
|
|
||||||
"height": 1
|
|
||||||
},
|
|
||||||
"_aperture": 19,
|
|
||||||
"_shutter": 7,
|
|
||||||
"_iso": 0,
|
|
||||||
"_screenScale": 1,
|
|
||||||
"_visibility": 1822425087,
|
|
||||||
"_targetTexture": null,
|
|
||||||
"_postProcess": null,
|
|
||||||
"_usePostProcess": false,
|
|
||||||
"_cameraType": -1,
|
|
||||||
"_trackingType": 0,
|
|
||||||
"_id": "7dWQTpwS5LrIHnc1zAPUtf"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"__type__": "cc.Node",
|
|
||||||
"_name": "GameWorld",
|
|
||||||
"_objFlags": 0,
|
|
||||||
"__editorExtras__": {},
|
|
||||||
"_parent": {
|
|
||||||
"__id__": 1
|
|
||||||
},
|
|
||||||
"_children": [],
|
|
||||||
"_active": true,
|
|
||||||
"_components": [],
|
|
||||||
"_prefab": null,
|
|
||||||
"_lpos": {
|
|
||||||
"__type__": "cc.Vec3",
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"_lrot": {
|
|
||||||
"__type__": "cc.Quat",
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0,
|
|
||||||
"w": 1
|
|
||||||
},
|
|
||||||
"_lscale": {
|
|
||||||
"__type__": "cc.Vec3",
|
|
||||||
"x": 1,
|
|
||||||
"y": 1,
|
|
||||||
"z": 1
|
|
||||||
},
|
|
||||||
"_mobility": 0,
|
|
||||||
"_layer": 1073741824,
|
|
||||||
"_euler": {
|
|
||||||
"__type__": "cc.Vec3",
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"_id": "8b9QorrGZIl64tVv0Z0vRQ"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"__type__": "cc.Node",
|
|
||||||
"_name": "Canvas",
|
|
||||||
"_objFlags": 0,
|
|
||||||
"__editorExtras__": {},
|
|
||||||
"_parent": {
|
|
||||||
"__id__": 1
|
|
||||||
},
|
|
||||||
"_children": [
|
|
||||||
{
|
|
||||||
"__id__": 9
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"_active": true,
|
|
||||||
"_components": [
|
|
||||||
{
|
|
||||||
"__id__": 11
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"__id__": 12
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"__id__": 13
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"_prefab": null,
|
|
||||||
"_lpos": {
|
|
||||||
"__type__": "cc.Vec3",
|
|
||||||
"x": 640,
|
|
||||||
"y": 360,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"_lrot": {
|
|
||||||
"__type__": "cc.Quat",
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0,
|
|
||||||
"w": 1
|
|
||||||
},
|
|
||||||
"_lscale": {
|
|
||||||
"__type__": "cc.Vec3",
|
|
||||||
"x": 1,
|
|
||||||
"y": 1,
|
|
||||||
"z": 1
|
|
||||||
},
|
|
||||||
"_mobility": 0,
|
|
||||||
"_layer": 1073741824,
|
|
||||||
"_euler": {
|
|
||||||
"__type__": "cc.Vec3",
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"_id": "4edRVPFLtIz5pR5edsryvx"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"__type__": "cc.Node",
|
|
||||||
"_name": "Camera",
|
|
||||||
"_objFlags": 0,
|
|
||||||
"__editorExtras__": {},
|
|
||||||
"_parent": {
|
|
||||||
"__id__": 8
|
|
||||||
},
|
|
||||||
"_children": [],
|
|
||||||
"_active": true,
|
|
||||||
"_components": [
|
|
||||||
{
|
|
||||||
"__id__": 10
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"_prefab": null,
|
|
||||||
"_lpos": {
|
|
||||||
"__type__": "cc.Vec3",
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 1000
|
|
||||||
},
|
|
||||||
"_lrot": {
|
|
||||||
"__type__": "cc.Quat",
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0,
|
|
||||||
"w": 1
|
|
||||||
},
|
|
||||||
"_lscale": {
|
|
||||||
"__type__": "cc.Vec3",
|
|
||||||
"x": 1,
|
|
||||||
"y": 1,
|
|
||||||
"z": 1
|
|
||||||
},
|
|
||||||
"_mobility": 0,
|
|
||||||
"_layer": 1073741824,
|
|
||||||
"_euler": {
|
|
||||||
"__type__": "cc.Vec3",
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"_id": "dfyZdh0bxJop4PyQrmHEP6"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"__type__": "cc.Camera",
|
|
||||||
"_name": "",
|
|
||||||
"_objFlags": 0,
|
|
||||||
"__editorExtras__": {},
|
|
||||||
"node": {
|
|
||||||
"__id__": 9
|
|
||||||
},
|
|
||||||
"_enabled": true,
|
|
||||||
"__prefab": null,
|
|
||||||
"_projection": 0,
|
|
||||||
"_priority": 1073741824,
|
|
||||||
"_fov": 45,
|
|
||||||
"_fovAxis": 0,
|
|
||||||
"_orthoHeight": 360,
|
|
||||||
"_near": 1,
|
|
||||||
"_far": 2000,
|
|
||||||
"_color": {
|
|
||||||
"__type__": "cc.Color",
|
|
||||||
"r": 0,
|
|
||||||
"g": 0,
|
|
||||||
"b": 0,
|
|
||||||
"a": 255
|
|
||||||
},
|
|
||||||
"_depth": 1,
|
|
||||||
"_stencil": 0,
|
|
||||||
"_clearFlags": 6,
|
|
||||||
"_rect": {
|
|
||||||
"__type__": "cc.Rect",
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"width": 1,
|
|
||||||
"height": 1
|
|
||||||
},
|
|
||||||
"_aperture": 19,
|
|
||||||
"_shutter": 7,
|
|
||||||
"_iso": 0,
|
|
||||||
"_screenScale": 1,
|
|
||||||
"_visibility": 41943040,
|
|
||||||
"_targetTexture": null,
|
|
||||||
"_postProcess": null,
|
|
||||||
"_usePostProcess": false,
|
|
||||||
"_cameraType": -1,
|
|
||||||
"_trackingType": 0,
|
|
||||||
"_id": "48lLOhLY5Onqokj70aNP+E"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"__type__": "cc.UITransform",
|
|
||||||
"_name": "",
|
|
||||||
"_objFlags": 0,
|
|
||||||
"__editorExtras__": {},
|
|
||||||
"node": {
|
|
||||||
"__id__": 8
|
|
||||||
},
|
|
||||||
"_enabled": true,
|
|
||||||
"__prefab": null,
|
|
||||||
"_contentSize": {
|
|
||||||
"__type__": "cc.Size",
|
|
||||||
"width": 1280,
|
|
||||||
"height": 720
|
|
||||||
},
|
|
||||||
"_anchorPoint": {
|
|
||||||
"__type__": "cc.Vec2",
|
|
||||||
"x": 0.5,
|
|
||||||
"y": 0.5
|
|
||||||
},
|
|
||||||
"_id": "c3qBrLTLNImoltQDlZ6coz"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"__type__": "cc.Canvas",
|
|
||||||
"_name": "",
|
|
||||||
"_objFlags": 0,
|
|
||||||
"__editorExtras__": {},
|
|
||||||
"node": {
|
|
||||||
"__id__": 8
|
|
||||||
},
|
|
||||||
"_enabled": true,
|
|
||||||
"__prefab": null,
|
|
||||||
"_cameraComponent": {
|
|
||||||
"__id__": 10
|
|
||||||
},
|
|
||||||
"_alignCanvasWithScreen": true,
|
|
||||||
"_id": "9d3SdE3ORAOZ6AG/imW6NO"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"__type__": "cc.Widget",
|
|
||||||
"_name": "",
|
|
||||||
"_objFlags": 0,
|
|
||||||
"__editorExtras__": {},
|
|
||||||
"node": {
|
|
||||||
"__id__": 8
|
|
||||||
},
|
|
||||||
"_enabled": true,
|
|
||||||
"__prefab": null,
|
|
||||||
"_alignFlags": 45,
|
|
||||||
"_target": null,
|
|
||||||
"_left": 0,
|
|
||||||
"_right": 0,
|
|
||||||
"_top": 0,
|
|
||||||
"_bottom": 0,
|
|
||||||
"_horizontalCenter": 0,
|
|
||||||
"_verticalCenter": 0,
|
|
||||||
"_isAbsLeft": true,
|
|
||||||
"_isAbsRight": true,
|
|
||||||
"_isAbsTop": true,
|
|
||||||
"_isAbsBottom": true,
|
|
||||||
"_isAbsHorizontalCenter": true,
|
|
||||||
"_isAbsVerticalCenter": true,
|
|
||||||
"_originalWidth": 0,
|
|
||||||
"_originalHeight": 0,
|
|
||||||
"_alignMode": 2,
|
|
||||||
"_lockFlags": 0,
|
|
||||||
"_id": "4a8iJypC1J8pMml467hQ6c"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"__type__": "cc.Node",
|
|
||||||
"_name": "RTSDemo",
|
|
||||||
"_objFlags": 0,
|
|
||||||
"__editorExtras__": {},
|
|
||||||
"_parent": {
|
|
||||||
"__id__": 1
|
|
||||||
},
|
|
||||||
"_children": [],
|
|
||||||
"_active": true,
|
|
||||||
"_components": [
|
|
||||||
{
|
|
||||||
"__id__": 15
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"_prefab": null,
|
|
||||||
"_lpos": {
|
|
||||||
"__type__": "cc.Vec3",
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"_lrot": {
|
|
||||||
"__type__": "cc.Quat",
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0,
|
|
||||||
"w": 1
|
|
||||||
},
|
|
||||||
"_lscale": {
|
|
||||||
"__type__": "cc.Vec3",
|
|
||||||
"x": 1,
|
|
||||||
"y": 1,
|
|
||||||
"z": 1
|
|
||||||
},
|
|
||||||
"_mobility": 0,
|
|
||||||
"_layer": 1073741824,
|
|
||||||
"_euler": {
|
|
||||||
"__type__": "cc.Vec3",
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"_id": "89cmsd2gNNsq155xC7mob8"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"__type__": "c33869Km+9Bb7dw/OyRztvE",
|
|
||||||
"_name": "",
|
|
||||||
"_objFlags": 0,
|
|
||||||
"__editorExtras__": {},
|
|
||||||
"node": {
|
|
||||||
"__id__": 14
|
|
||||||
},
|
|
||||||
"_enabled": true,
|
|
||||||
"__prefab": null,
|
|
||||||
"minerCount": 1,
|
|
||||||
"goldMineCount": 3,
|
|
||||||
"_id": "86AIY7iYlMNqJsDC/+LIMU"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"__type__": "cc.SceneGlobals",
|
|
||||||
"ambient": {
|
|
||||||
"__id__": 17
|
|
||||||
},
|
|
||||||
"shadows": {
|
|
||||||
"__id__": 18
|
|
||||||
},
|
|
||||||
"_skybox": {
|
|
||||||
"__id__": 19
|
|
||||||
},
|
|
||||||
"fog": {
|
|
||||||
"__id__": 20
|
|
||||||
},
|
|
||||||
"octree": {
|
|
||||||
"__id__": 21
|
|
||||||
},
|
|
||||||
"skin": {
|
|
||||||
"__id__": 22
|
|
||||||
},
|
|
||||||
"lightProbeInfo": {
|
|
||||||
"__id__": 23
|
|
||||||
},
|
|
||||||
"postSettings": {
|
|
||||||
"__id__": 24
|
|
||||||
},
|
|
||||||
"bakedWithStationaryMainLight": false,
|
|
||||||
"bakedWithHighpLightmap": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"__type__": "cc.AmbientInfo",
|
|
||||||
"_skyColorHDR": {
|
|
||||||
"__type__": "cc.Vec4",
|
|
||||||
"x": 0.2,
|
|
||||||
"y": 0.5,
|
|
||||||
"z": 0.8,
|
|
||||||
"w": 0.520833125
|
|
||||||
},
|
|
||||||
"_skyColor": {
|
|
||||||
"__type__": "cc.Vec4",
|
|
||||||
"x": 0.2,
|
|
||||||
"y": 0.5,
|
|
||||||
"z": 0.8,
|
|
||||||
"w": 0.520833125
|
|
||||||
},
|
|
||||||
"_skyIllumHDR": 20000,
|
|
||||||
"_skyIllum": 20000,
|
|
||||||
"_groundAlbedoHDR": {
|
|
||||||
"__type__": "cc.Vec4",
|
|
||||||
"x": 0.2,
|
|
||||||
"y": 0.2,
|
|
||||||
"z": 0.2,
|
|
||||||
"w": 1
|
|
||||||
},
|
|
||||||
"_groundAlbedo": {
|
|
||||||
"__type__": "cc.Vec4",
|
|
||||||
"x": 0.2,
|
|
||||||
"y": 0.2,
|
|
||||||
"z": 0.2,
|
|
||||||
"w": 1
|
|
||||||
},
|
|
||||||
"_skyColorLDR": {
|
|
||||||
"__type__": "cc.Vec4",
|
|
||||||
"x": 0.452588,
|
|
||||||
"y": 0.607642,
|
|
||||||
"z": 0.755699,
|
|
||||||
"w": 0
|
|
||||||
},
|
|
||||||
"_skyIllumLDR": 0.8,
|
|
||||||
"_groundAlbedoLDR": {
|
|
||||||
"__type__": "cc.Vec4",
|
|
||||||
"x": 0.618555,
|
|
||||||
"y": 0.577848,
|
|
||||||
"z": 0.544564,
|
|
||||||
"w": 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"__type__": "cc.ShadowsInfo",
|
|
||||||
"_enabled": false,
|
|
||||||
"_type": 0,
|
|
||||||
"_normal": {
|
|
||||||
"__type__": "cc.Vec3",
|
|
||||||
"x": 0,
|
|
||||||
"y": 1,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"_distance": 0,
|
|
||||||
"_planeBias": 1,
|
|
||||||
"_shadowColor": {
|
|
||||||
"__type__": "cc.Color",
|
|
||||||
"r": 76,
|
|
||||||
"g": 76,
|
|
||||||
"b": 76,
|
|
||||||
"a": 255
|
|
||||||
},
|
|
||||||
"_maxReceived": 4,
|
|
||||||
"_size": {
|
|
||||||
"__type__": "cc.Vec2",
|
|
||||||
"x": 1024,
|
|
||||||
"y": 1024
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"__type__": "cc.SkyboxInfo",
|
|
||||||
"_envLightingType": 0,
|
|
||||||
"_envmapHDR": {
|
|
||||||
"__uuid__": "d032ac98-05e1-4090-88bb-eb640dcb5fc1@b47c0",
|
|
||||||
"__expectedType__": "cc.TextureCube"
|
|
||||||
},
|
|
||||||
"_envmap": {
|
|
||||||
"__uuid__": "d032ac98-05e1-4090-88bb-eb640dcb5fc1@b47c0",
|
|
||||||
"__expectedType__": "cc.TextureCube"
|
|
||||||
},
|
|
||||||
"_envmapLDR": {
|
|
||||||
"__uuid__": "6f01cf7f-81bf-4a7e-bd5d-0afc19696480@b47c0",
|
|
||||||
"__expectedType__": "cc.TextureCube"
|
|
||||||
},
|
|
||||||
"_diffuseMapHDR": null,
|
|
||||||
"_diffuseMapLDR": null,
|
|
||||||
"_enabled": true,
|
|
||||||
"_useHDR": true,
|
|
||||||
"_editableMaterial": null,
|
|
||||||
"_reflectionHDR": null,
|
|
||||||
"_reflectionLDR": null,
|
|
||||||
"_rotationAngle": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"__type__": "cc.FogInfo",
|
|
||||||
"_type": 0,
|
|
||||||
"_fogColor": {
|
|
||||||
"__type__": "cc.Color",
|
|
||||||
"r": 200,
|
|
||||||
"g": 200,
|
|
||||||
"b": 200,
|
|
||||||
"a": 255
|
|
||||||
},
|
|
||||||
"_enabled": false,
|
|
||||||
"_fogDensity": 0.3,
|
|
||||||
"_fogStart": 0.5,
|
|
||||||
"_fogEnd": 300,
|
|
||||||
"_fogAtten": 5,
|
|
||||||
"_fogTop": 1.5,
|
|
||||||
"_fogRange": 1.2,
|
|
||||||
"_accurate": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"__type__": "cc.OctreeInfo",
|
|
||||||
"_enabled": false,
|
|
||||||
"_minPos": {
|
|
||||||
"__type__": "cc.Vec3",
|
|
||||||
"x": -1024,
|
|
||||||
"y": -1024,
|
|
||||||
"z": -1024
|
|
||||||
},
|
|
||||||
"_maxPos": {
|
|
||||||
"__type__": "cc.Vec3",
|
|
||||||
"x": 1024,
|
|
||||||
"y": 1024,
|
|
||||||
"z": 1024
|
|
||||||
},
|
|
||||||
"_depth": 8
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"__type__": "cc.SkinInfo",
|
|
||||||
"_enabled": true,
|
|
||||||
"_blurRadius": 0.01,
|
|
||||||
"_sssIntensity": 3
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"__type__": "cc.LightProbeInfo",
|
|
||||||
"_giScale": 1,
|
|
||||||
"_giSamples": 1024,
|
|
||||||
"_bounces": 2,
|
|
||||||
"_reduceRinging": 0,
|
|
||||||
"_showProbe": true,
|
|
||||||
"_showWireframe": true,
|
|
||||||
"_showConvex": false,
|
|
||||||
"_data": null,
|
|
||||||
"_lightProbeSphereVolume": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"__type__": "cc.PostSettingsInfo",
|
|
||||||
"_toneMappingType": 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"ver": "1.1.50",
|
|
||||||
"importer": "scene",
|
|
||||||
"imported": true,
|
|
||||||
"uuid": "ff354f0b-c2f5-4dea-8ffb-0152d175d11c",
|
|
||||||
"files": [
|
|
||||||
".json"
|
|
||||||
],
|
|
||||||
"subMetas": {},
|
|
||||||
"userData": {}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"ver": "1.1.50",
|
|
||||||
"importer": "scene",
|
|
||||||
"imported": true,
|
|
||||||
"uuid": "fcbf2917-6d43-4528-8829-7ee089594879",
|
|
||||||
"files": [
|
|
||||||
".json"
|
|
||||||
],
|
|
||||||
"subMetas": {},
|
|
||||||
"userData": {}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user