Compare commits
75 Commits
issue-151-
...
issue-203-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
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]
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,12 @@
|
||||
"arrow-parens": ["error", "always"],
|
||||
"no-multiple-empty-lines": ["error", { "max": 2, "maxEOF": 1 }],
|
||||
"no-console": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-explicit-any": "error",
|
||||
"@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/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
|
||||
"@typescript-eslint/no-non-null-assertion": "off"
|
||||
|
||||
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
|
||||
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"
|
||||
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -37,6 +37,12 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Type check
|
||||
run: npm run type-check
|
||||
|
||||
- name: Lint check
|
||||
run: npm run lint
|
||||
|
||||
- name: Build core package first
|
||||
run: npm run build:core
|
||||
|
||||
|
||||
146
.github/workflows/cleanup-dependabot.yml
vendored
Normal file
146
.github/workflows/cleanup-dependabot.yml
vendored
Normal file
@@ -0,0 +1,146 @@
|
||||
name: Cleanup Old Dependabot PRs
|
||||
|
||||
# 手动触发的 workflow,用于清理堆积的 Dependabot PR
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
days_old:
|
||||
description: '关闭多少天前创建的 PR(默认 7 天)'
|
||||
required: false
|
||||
default: '7'
|
||||
dry_run:
|
||||
description: '试运行模式(true=仅显示,不关闭)'
|
||||
required: false
|
||||
default: 'true'
|
||||
type: choice
|
||||
options:
|
||||
- 'true'
|
||||
- 'false'
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: List and Close Old Dependabot PRs
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const daysOld = parseInt('${{ github.event.inputs.days_old }}') || 7;
|
||||
const dryRun = '${{ github.event.inputs.dry_run }}' === 'true';
|
||||
const cutoffDate = new Date();
|
||||
cutoffDate.setDate(cutoffDate.getDate() - daysOld);
|
||||
|
||||
console.log(`🔍 查找超过 ${daysOld} 天的 Dependabot PR...`);
|
||||
console.log(`📅 截止日期: ${cutoffDate.toISOString()}`);
|
||||
console.log(`🏃 模式: ${dryRun ? '试运行(不会实际关闭)' : '实际执行'}`);
|
||||
console.log('---');
|
||||
|
||||
// 获取所有 Dependabot PR
|
||||
const { data: pulls } = await github.rest.pulls.list({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'open',
|
||||
per_page: 100
|
||||
});
|
||||
|
||||
const dependabotPRs = pulls.filter(pr =>
|
||||
pr.user.login === 'dependabot[bot]' &&
|
||||
new Date(pr.created_at) < cutoffDate
|
||||
);
|
||||
|
||||
console.log(`📊 找到 ${dependabotPRs.length} 个符合条件的 Dependabot PR`);
|
||||
console.log('');
|
||||
|
||||
if (dependabotPRs.length === 0) {
|
||||
console.log('✅ 没有需要清理的 PR');
|
||||
return;
|
||||
}
|
||||
|
||||
// 按类型分组
|
||||
const byType = {
|
||||
dev: [],
|
||||
prod: [],
|
||||
actions: [],
|
||||
other: []
|
||||
};
|
||||
|
||||
for (const pr of dependabotPRs) {
|
||||
const title = pr.title.toLowerCase();
|
||||
const labels = pr.labels.map(l => l.name);
|
||||
|
||||
let type = 'other';
|
||||
if (title.includes('dev-dependencies') || title.includes('development')) {
|
||||
type = 'dev';
|
||||
} else if (title.includes('production-dependencies')) {
|
||||
type = 'prod';
|
||||
} else if (labels.includes('github-actions')) {
|
||||
type = 'actions';
|
||||
}
|
||||
|
||||
byType[type].push(pr);
|
||||
}
|
||||
|
||||
console.log('📋 PR 分类统计:');
|
||||
console.log(` 🔧 开发依赖: ${byType.dev.length} 个`);
|
||||
console.log(` 📦 生产依赖: ${byType.prod.length} 个`);
|
||||
console.log(` ⚙️ GitHub Actions: ${byType.actions.length} 个`);
|
||||
console.log(` ❓ 其他: ${byType.other.length} 个`);
|
||||
console.log('');
|
||||
|
||||
// 处理每个 PR
|
||||
for (const pr of dependabotPRs) {
|
||||
const age = Math.floor((Date.now() - new Date(pr.created_at)) / (1000 * 60 * 60 * 24));
|
||||
|
||||
console.log(`${dryRun ? '🔍' : '🗑️ '} #${pr.number}: ${pr.title}`);
|
||||
console.log(` 创建时间: ${pr.created_at} (${age} 天前)`);
|
||||
console.log(` 链接: ${pr.html_url}`);
|
||||
|
||||
if (!dryRun) {
|
||||
await github.rest.pulls.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: pr.number,
|
||||
state: 'closed'
|
||||
});
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr.number,
|
||||
body: `🤖 **自动关闭旧的 Dependabot PR**
|
||||
|
||||
此 PR 已超过 ${daysOld} 天未合并,已被自动关闭以清理积压。
|
||||
|
||||
📌 **下一步:**
|
||||
- Dependabot 已配置为月度运行,届时会创建新的分组更新
|
||||
- 新的 Mergify 规则会智能处理不同类型的依赖更新
|
||||
- 开发依赖和 GitHub Actions 会自动合并(即使 CI 失败)
|
||||
- 生产依赖需要 CI 通过才会自动合并
|
||||
|
||||
如果需要立即应用此更新,请手动更新依赖。
|
||||
|
||||
---
|
||||
*此操作由仓库维护者手动触发的清理工作流执行*`
|
||||
});
|
||||
|
||||
console.log(' ✅ 已关闭并添加说明');
|
||||
} else {
|
||||
console.log(' ℹ️ 试运行模式 - 未执行操作');
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
console.log('---');
|
||||
if (dryRun) {
|
||||
console.log(`✨ 试运行完成!共发现 ${dependabotPRs.length} 个待清理的 PR`);
|
||||
console.log('💡 要实际执行清理,请将 dry_run 参数设为 false 重新运行');
|
||||
} else {
|
||||
console.log(`✅ 清理完成!已关闭 ${dependabotPRs.length} 个 Dependabot PR`);
|
||||
}
|
||||
45
.github/workflows/codecov.yml
vendored
Normal file
45
.github/workflows/codecov.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
name: Code Coverage
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master, main ]
|
||||
pull_request:
|
||||
branches: [ master, main ]
|
||||
|
||||
jobs:
|
||||
coverage:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run tests with coverage
|
||||
run: |
|
||||
cd packages/core
|
||||
npm run test:coverage
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./packages/core/coverage/coverage-final.json
|
||||
flags: core
|
||||
name: core-coverage
|
||||
fail_ci_if_error: true
|
||||
verbose: true
|
||||
|
||||
- name: Upload coverage artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-report
|
||||
path: packages/core/coverage/
|
||||
41
.github/workflows/codeql.yml
vendored
Normal file
41
.github/workflows/codeql.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master", "main" ]
|
||||
pull_request:
|
||||
branches: [ "master", "main" ]
|
||||
schedule:
|
||||
- cron: '0 0 * * 1' # 每周一运行
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript' ]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: security-and-quality
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
31
.github/workflows/commitlint.yml
vendored
Normal file
31
.github/workflows/commitlint.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
name: Commit Lint
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
commitlint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install commitlint
|
||||
run: |
|
||||
npm install --save-dev @commitlint/config-conventional @commitlint/cli
|
||||
|
||||
- name: Validate PR commits
|
||||
run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose
|
||||
23
.github/workflows/issue-labeler.yml
vendored
Normal file
23
.github/workflows/issue-labeler.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
name: Issue Labeler
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
label:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Label Issues
|
||||
uses: github/issue-labeler@v3.4
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
configuration-path: .github/labeler.yml
|
||||
enable-versioned-regex: 1
|
||||
28
.github/workflows/issue-translator.yml
vendored
Normal file
28
.github/workflows/issue-translator.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: Issue Translator
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
translate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Translate Issues
|
||||
uses: tomsun28/issues-translate-action@v2.7
|
||||
with:
|
||||
IS_MODIFY_TITLE: false
|
||||
# 设置为 true 会修改标题,false 只在评论中添加翻译
|
||||
CUSTOM_BOT_NOTE: |
|
||||
<details>
|
||||
<summary>🌏 Translation / 翻译</summary>
|
||||
|
||||
Bot detected the issue body's language is not English, translate it automatically.
|
||||
机器人检测到 issue 内容非英文,自动翻译。
|
||||
|
||||
</details>
|
||||
8
.github/workflows/release-editor.yml
vendored
8
.github/workflows/release-editor.yml
vendored
@@ -73,7 +73,8 @@ jobs:
|
||||
path: |
|
||||
packages/core/bin
|
||||
packages/editor-core/dist
|
||||
key: ${{ runner.os }}-ts-build-${{ hashFiles('packages/core/src/**', 'packages/editor-core/src/**') }}
|
||||
packages/behavior-tree/bin
|
||||
key: ${{ runner.os }}-ts-build-${{ hashFiles('packages/core/src/**', 'packages/editor-core/src/**', 'packages/behavior-tree/src/**') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-ts-build-
|
||||
|
||||
@@ -85,6 +86,11 @@ jobs:
|
||||
cd packages/editor-core
|
||||
npm run build
|
||||
|
||||
- name: Build behavior-tree package
|
||||
run: |
|
||||
cd packages/behavior-tree
|
||||
npm run build
|
||||
|
||||
- name: Build Tauri app
|
||||
uses: tauri-apps/tauri-action@v0.5
|
||||
env:
|
||||
|
||||
112
.github/workflows/release.yml
vendored
Normal file
112
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,112 @@
|
||||
name: Release NPM Packages
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
package:
|
||||
description: '选择要发布的包'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- core
|
||||
- behavior-tree
|
||||
- editor-core
|
||||
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: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build core package (if needed)
|
||||
if: ${{ github.event.inputs.package == 'behavior-tree' || github.event.inputs.package == 'editor-core' }}
|
||||
run: |
|
||||
cd packages/core
|
||||
npm 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
|
||||
npm version ${{ github.event.inputs.custom_version }} --no-git-tag-version --allow-same-version
|
||||
else
|
||||
npm version ${{ github.event.inputs.version_type }} --no-git-tag-version
|
||||
fi
|
||||
NEW_VERSION=$(node -p "require('./package.json').version")
|
||||
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "发布版本: $NEW_VERSION"
|
||||
|
||||
- name: Build package
|
||||
run: |
|
||||
cd packages/${{ github.event.inputs.package }}
|
||||
npm run build:npm
|
||||
|
||||
- name: Publish to npm
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: |
|
||||
cd packages/${{ github.event.inputs.package }}/dist
|
||||
npm publish --access public
|
||||
|
||||
- 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
|
||||
43
.github/workflows/size-limit.yml
vendored
Normal file
43
.github/workflows/size-limit.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
name: Size Limit
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
paths:
|
||||
- 'packages/core/src/**'
|
||||
- 'packages/core/package.json'
|
||||
- '.size-limit.json'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
size:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build core package
|
||||
run: |
|
||||
cd packages/core
|
||||
npm run build:npm
|
||||
|
||||
- name: Check bundle size
|
||||
uses: andresz1/size-limit-action@v1
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
skip_step: install
|
||||
60
.github/workflows/stale.yml
vendored
Normal file
60
.github/workflows/stale.yml
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
name: Stale Issues and PRs
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *' # 每天运行一次
|
||||
workflow_dispatch: # 允许手动触发
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Stale Bot
|
||||
uses: actions/stale@v9
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Issue 配置
|
||||
stale-issue-message: |
|
||||
这个 issue 已经 60 天没有活动了,将在 14 天后自动关闭。
|
||||
如果这个问题仍然存在,请留言说明情况。
|
||||
|
||||
This issue has been inactive for 60 days and will be closed in 14 days.
|
||||
If this issue is still relevant, please leave a comment.
|
||||
close-issue-message: |
|
||||
由于长时间无活动,这个 issue 已被自动关闭。
|
||||
如需重新打开,请留言说明。
|
||||
|
||||
This issue has been automatically closed due to inactivity.
|
||||
Please comment if you'd like to reopen it.
|
||||
days-before-issue-stale: 60
|
||||
days-before-issue-close: 14
|
||||
stale-issue-label: 'stale'
|
||||
exempt-issue-labels: 'pinned,security,enhancement,help wanted'
|
||||
|
||||
# PR 配置
|
||||
stale-pr-message: |
|
||||
这个 PR 已经 30 天没有活动了,将在 7 天后自动关闭。
|
||||
如果你还在处理这个 PR,请更新一下。
|
||||
|
||||
This PR has been inactive for 30 days and will be closed in 7 days.
|
||||
If you're still working on it, please update it.
|
||||
close-pr-message: |
|
||||
由于长时间无活动,这个 PR 已被自动关闭。
|
||||
如需继续,请重新打开或创建新的 PR。
|
||||
|
||||
This PR has been automatically closed due to inactivity.
|
||||
Please reopen or create a new PR to continue.
|
||||
days-before-pr-stale: 30
|
||||
days-before-pr-close: 7
|
||||
stale-pr-label: 'stale'
|
||||
exempt-pr-labels: 'pinned,security,wip'
|
||||
|
||||
# 其他配置
|
||||
operations-per-run: 100
|
||||
remove-stale-when-updated: true
|
||||
ascending: true
|
||||
58
.github/workflows/welcome.yml
vendored
Normal file
58
.github/workflows/welcome.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
name: Welcome
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
welcome:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Welcome new contributors
|
||||
uses: actions/first-interaction@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-message: |
|
||||
👋 你好!感谢你提交第一个 issue!
|
||||
|
||||
我们会尽快查看并回复。同时,建议你:
|
||||
- 📚 查看[文档](https://esengine.github.io/ecs-framework/)
|
||||
- 🤖 使用 [AI 文档助手](https://deepwiki.com/esengine/ecs-framework)
|
||||
- 💬 加入 [QQ 交流群](https://jq.qq.com/?_wv=1027&k=29w1Nud6)
|
||||
|
||||
---
|
||||
|
||||
👋 Hello! Thanks for opening your first issue!
|
||||
|
||||
We'll review it as soon as possible. Meanwhile, you might want to:
|
||||
- 📚 Check the [documentation](https://esengine.github.io/ecs-framework/)
|
||||
- 🤖 Use [AI documentation assistant](https://deepwiki.com/esengine/ecs-framework)
|
||||
|
||||
pr-message: |
|
||||
👋 你好!感谢你提交第一个 Pull Request!
|
||||
|
||||
在我们 Review 之前,请确保:
|
||||
- ✅ 代码遵循项目规范
|
||||
- ✅ 通过所有测试
|
||||
- ✅ 更新了相关文档
|
||||
- ✅ Commit 遵循 [Conventional Commits](https://www.conventionalcommits.org/) 规范
|
||||
|
||||
查看完整的[贡献指南](https://github.com/esengine/ecs-framework/blob/master/CONTRIBUTING.md)。
|
||||
|
||||
---
|
||||
|
||||
👋 Hello! Thanks for your first Pull Request!
|
||||
|
||||
Before we review, please ensure:
|
||||
- ✅ Code follows project conventions
|
||||
- ✅ All tests pass
|
||||
- ✅ Documentation is updated
|
||||
- ✅ Commits follow [Conventional Commits](https://www.conventionalcommits.org/)
|
||||
|
||||
See the full [Contributing Guide](https://github.com/esengine/ecs-framework/blob/master/CONTRIBUTING.md).
|
||||
15
.gitmodules
vendored
15
.gitmodules
vendored
@@ -4,27 +4,12 @@
|
||||
[submodule "thirdparty/admin-backend"]
|
||||
path = thirdparty/admin-backend
|
||||
url = https://github.com/esengine/admin-backend.git
|
||||
[submodule "extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension"]
|
||||
path = extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension
|
||||
url = https://github.com/esengine/cocos-ecs-extension.git
|
||||
[submodule "extensions/cocos/cocos-ecs/extensions/behaviour-tree"]
|
||||
path = extensions/cocos/cocos-ecs/extensions/behaviour-tree
|
||||
url = https://github.com/esengine/behaviour-tree.git
|
||||
[submodule "extensions/cocos/cocos-ecs/extensions/cocos-terrain-gen"]
|
||||
path = extensions/cocos/cocos-ecs/extensions/cocos-terrain-gen
|
||||
url = https://github.com/esengine/cocos-terrain-gen.git
|
||||
[submodule "extensions/cocos/cocos-ecs/extensions/mvvm-designer"]
|
||||
path = extensions/cocos/cocos-ecs/extensions/mvvm-designer
|
||||
url = https://github.com/esengine/mvvm-designer.git
|
||||
[submodule "thirdparty/mvvm-ui-framework"]
|
||||
path = thirdparty/mvvm-ui-framework
|
||||
url = https://github.com/esengine/mvvm-ui-framework.git
|
||||
[submodule "thirdparty/cocos-nexus"]
|
||||
path = thirdparty/cocos-nexus
|
||||
url = https://github.com/esengine/cocos-nexus.git
|
||||
[submodule "extensions/cocos/cocos-ecs/extensions/utilityai_designer"]
|
||||
path = extensions/cocos/cocos-ecs/extensions/utilityai_designer
|
||||
url = https://github.com/esengine/utilityai_designer.git
|
||||
[submodule "thirdparty/ecs-astar"]
|
||||
path = thirdparty/ecs-astar
|
||||
url = https://github.com/esengine/ecs-astar.git
|
||||
|
||||
25
.size-limit.json
Normal file
25
.size-limit.json
Normal file
@@ -0,0 +1,25 @@
|
||||
[
|
||||
{
|
||||
"name": "@esengine/ecs-framework (ESM)",
|
||||
"path": "packages/core/dist/esm/index.js",
|
||||
"import": "*",
|
||||
"limit": "50 KB",
|
||||
"webpack": false,
|
||||
"gzip": true
|
||||
},
|
||||
{
|
||||
"name": "@esengine/ecs-framework (UMD)",
|
||||
"path": "packages/core/dist/umd/ecs-framework.js",
|
||||
"limit": "60 KB",
|
||||
"webpack": false,
|
||||
"gzip": true
|
||||
},
|
||||
{
|
||||
"name": "Core Runtime (Tree-shaking)",
|
||||
"path": "packages/core/dist/esm/index.js",
|
||||
"import": "{ Core, Scene, Entity, Component }",
|
||||
"limit": "30 KB",
|
||||
"webpack": false,
|
||||
"gzip": true
|
||||
}
|
||||
]
|
||||
133
CONTRIBUTING.md
Normal file
133
CONTRIBUTING.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# 贡献指南 / Contributing Guide
|
||||
|
||||
感谢你对 ECS Framework 的关注!
|
||||
|
||||
Thank you for your interest in contributing to ECS Framework!
|
||||
|
||||
## Commit 规范 / Commit Convention
|
||||
|
||||
本项目使用 [Conventional Commits](https://www.conventionalcommits.org/) 规范。
|
||||
|
||||
This project follows the [Conventional Commits](https://www.conventionalcommits.org/) specification.
|
||||
|
||||
### 格式 / Format
|
||||
|
||||
```
|
||||
<type>(<scope>): <subject>
|
||||
|
||||
<body>
|
||||
|
||||
<footer>
|
||||
```
|
||||
|
||||
### 类型 / Types
|
||||
|
||||
- **feat**: 新功能 / New feature
|
||||
- **fix**: 错误修复 / Bug fix
|
||||
- **docs**: 文档变更 / Documentation changes
|
||||
- **style**: 代码格式(不影响代码运行) / Code style changes
|
||||
- **refactor**: 重构(既不是新功能也不是修复) / Code refactoring
|
||||
- **perf**: 性能优化 / Performance improvements
|
||||
- **test**: 测试相关 / Test changes
|
||||
- **build**: 构建系统或依赖变更 / Build system changes
|
||||
- **ci**: CI 配置变更 / CI configuration changes
|
||||
- **chore**: 其他变更 / Other changes
|
||||
|
||||
### 范围 / Scope
|
||||
|
||||
- **core**: 核心包 @esengine/ecs-framework
|
||||
- **math**: 数学库包
|
||||
- **network-client**: 网络客户端包
|
||||
- **network-server**: 网络服务端包
|
||||
- **network-shared**: 网络共享包
|
||||
- **editor**: 编辑器
|
||||
- **docs**: 文档
|
||||
|
||||
### 示例 / Examples
|
||||
|
||||
```bash
|
||||
# 新功能
|
||||
feat(core): add component pooling system
|
||||
|
||||
# 错误修复
|
||||
fix(core): fix entity deletion memory leak
|
||||
|
||||
# 破坏性变更
|
||||
feat(core): redesign system lifecycle
|
||||
|
||||
BREAKING CHANGE: System.initialize() now requires Scene parameter
|
||||
```
|
||||
|
||||
## 自动发布 / Automatic Release
|
||||
|
||||
本项目使用 Semantic Release 自动发布。
|
||||
|
||||
This project uses Semantic Release for automatic publishing.
|
||||
|
||||
### 版本规则 / Versioning Rules
|
||||
|
||||
根据你的 commit 类型,版本号会自动更新:
|
||||
|
||||
Based on your commit type, the version will be automatically updated:
|
||||
|
||||
- `feat`: 增加 **minor** 版本 (0.x.0)
|
||||
- `fix`, `perf`, `refactor`: 增加 **patch** 版本 (0.0.x)
|
||||
- `BREAKING CHANGE`: 增加 **major** 版本 (x.0.0)
|
||||
|
||||
### 发布流程 / Release Process
|
||||
|
||||
1. 提交代码到 `master` 分支 / Push commits to `master` branch
|
||||
2. GitHub Actions 自动运行测试 / GitHub Actions runs tests automatically
|
||||
3. Semantic Release 分析 commits / Semantic Release analyzes commits
|
||||
4. 自动更新版本号 / Version is automatically updated
|
||||
5. 自动生成 CHANGELOG.md / CHANGELOG.md is automatically generated
|
||||
6. 自动发布到 npm / Package is automatically published to npm
|
||||
7. 自动创建 GitHub Release / GitHub Release is automatically created
|
||||
|
||||
## 开发流程 / Development Workflow
|
||||
|
||||
1. Fork 本仓库 / Fork this repository
|
||||
2. 创建特性分支 / Create a feature branch
|
||||
```bash
|
||||
git checkout -b feat/my-feature
|
||||
```
|
||||
3. 提交你的变更 / Commit your changes
|
||||
```bash
|
||||
git commit -m "feat(core): add new feature"
|
||||
```
|
||||
4. 推送到你的 Fork / Push to your fork
|
||||
```bash
|
||||
git push origin feat/my-feature
|
||||
```
|
||||
5. 创建 Pull Request / Create a Pull Request
|
||||
|
||||
## 本地测试 / Local Testing
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# 运行测试
|
||||
npm test
|
||||
|
||||
# 构建
|
||||
npm run build
|
||||
|
||||
# 代码检查
|
||||
npm run lint
|
||||
|
||||
# 代码格式化
|
||||
npm run format
|
||||
```
|
||||
|
||||
## 问题反馈 / Issue Reporting
|
||||
|
||||
如果你发现了 bug 或有新功能建议,请[创建 Issue](https://github.com/esengine/ecs-framework/issues/new)。
|
||||
|
||||
If you find a bug or have a feature request, please [create an issue](https://github.com/esengine/ecs-framework/issues/new).
|
||||
|
||||
## 许可证 / License
|
||||
|
||||
通过贡献代码,你同意你的贡献将遵循 MIT 许可证。
|
||||
|
||||
By contributing, you agree that your contributions will be licensed under the MIT License.
|
||||
214
LICENSE
214
LICENSE
@@ -1,201 +1,21 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
MIT License
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
Copyright (c) 2025 ECS Framework
|
||||
|
||||
1. Definitions.
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
115
README.md
115
README.md
@@ -1,12 +1,53 @@
|
||||
# ECS Framework
|
||||
|
||||
[](https://github.com/esengine/ecs-framework/actions)
|
||||
[](https://codecov.io/gh/esengine/ecs-framework)
|
||||
[](https://badge.fury.io/js/%40esengine%2Fecs-framework)
|
||||
[](https://www.npmjs.com/package/@esengine/ecs-framework)
|
||||
[](https://bundlephobia.com/package/@esengine/ecs-framework)
|
||||
[](https://www.typescriptlang.org/)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](#contributors)
|
||||
[](https://github.com/esengine/ecs-framework/stargazers)
|
||||
[](https://deepwiki.com/esengine/ecs-framework)
|
||||
|
||||
一个高性能的 TypeScript ECS (Entity-Component-System) 框架,专为现代游戏开发而设计。
|
||||
<div align="center">
|
||||
|
||||
<p>一个高性能的 TypeScript ECS (Entity-Component-System) 框架,专为现代游戏开发而设计。</p>
|
||||
|
||||
<p>A high-performance TypeScript ECS (Entity-Component-System) framework designed for modern game development.</p>
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 📊 项目统计 / Project Stats
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://star-history.com/#esengine/ecs-framework&Date)
|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
||||
<a href="https://github.com/esengine/ecs-framework/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=esengine/ecs-framework" />
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
### 📈 下载趋势 / Download Trends
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://www.npmjs.com/package/@esengine/ecs-framework)
|
||||
|
||||
[](https://npmtrends.com/@esengine/ecs-framework)
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 特性
|
||||
|
||||
@@ -91,6 +132,30 @@ function gameLoop(deltaTime: number) {
|
||||
- **多场景** - 支持 World/Scene 分层架构
|
||||
- **时间管理** - 内置定时器和时间控制系统
|
||||
|
||||
## 🏗️ 架构设计 / Architecture
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
A[Core 核心] --> B[World 世界]
|
||||
B --> C[Scene 场景]
|
||||
C --> D[EntityManager 实体管理器]
|
||||
C --> E[SystemManager 系统管理器]
|
||||
D --> F[Entity 实体]
|
||||
F --> G[Component 组件]
|
||||
E --> H[EntitySystem 实体系统]
|
||||
E --> I[WorkerSystem 工作线程系统]
|
||||
|
||||
style A fill:#e1f5ff
|
||||
style B fill:#fff3e0
|
||||
style C fill:#f3e5f5
|
||||
style D fill:#e8f5e9
|
||||
style E fill:#fff9c4
|
||||
style F fill:#ffebee
|
||||
style G fill:#e0f2f1
|
||||
style H fill:#fce4ec
|
||||
style I fill:#f1f8e9
|
||||
```
|
||||
|
||||
## 平台支持
|
||||
|
||||
支持主流游戏引擎和 Web 平台:
|
||||
@@ -144,6 +209,7 @@ function gameLoop(deltaTime: number) {
|
||||
|
||||
## 文档
|
||||
|
||||
- [📚 AI智能文档](https://deepwiki.com/esengine/ecs-framework) - AI助手随时解答你的问题
|
||||
- [快速入门](https://esengine.github.io/ecs-framework/guide/getting-started.html) - 详细教程和平台集成
|
||||
- [完整指南](https://esengine.github.io/ecs-framework/guide/) - ECS 概念和使用指南
|
||||
- [API 参考](https://esengine.github.io/ecs-framework/api/) - 完整 API 文档
|
||||
@@ -153,11 +219,56 @@ function gameLoop(deltaTime: number) {
|
||||
- [路径寻找](https://github.com/esengine/ecs-astar) - A*、BFS、Dijkstra 算法
|
||||
- [AI 系统](https://github.com/esengine/BehaviourTree-ai) - 行为树、效用 AI
|
||||
|
||||
## 💪 支持项目 / Support the Project
|
||||
|
||||
如果这个项目对你有帮助,请考虑:
|
||||
|
||||
If this project helps you, please consider:
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://github.com/sponsors/esengine)
|
||||
[](https://github.com/esengine/ecs-framework)
|
||||
|
||||
</div>
|
||||
|
||||
- ⭐ 给项目点个 Star
|
||||
- 🐛 报告 Bug 或提出新功能
|
||||
- 📝 改进文档
|
||||
- 💖 成为赞助者
|
||||
|
||||
## 社区与支持
|
||||
|
||||
- [问题反馈](https://github.com/esengine/ecs-framework/issues) - Bug 报告和功能建议
|
||||
- [讨论区](https://github.com/esengine/ecs-framework/discussions) - 提问、分享想法
|
||||
- [QQ 交流群](https://jq.qq.com/?_wv=1027&k=29w1Nud6) - ecs游戏框架交流
|
||||
|
||||
## 贡献者 / Contributors
|
||||
|
||||
感谢所有为这个项目做出贡献的人!
|
||||
|
||||
Thanks goes to these wonderful people:
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||
<!-- prettier-ignore-start -->
|
||||
<!-- markdownlint-disable -->
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/esengine"><img src="https://avatars.githubusercontent.com/esengine?s=100" width="100px;" alt="esengine"/><br /><sub><b>esengine</b></sub></a><br /><a href="#maintenance-esengine" title="Maintenance">🚧</a> <a href="https://github.com/esengine/ecs-framework/commits?author=esengine" title="Code">💻</a> <a href="#design-esengine" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/foxling"><img src="https://avatars.githubusercontent.com/foxling?s=100" width="100px;" alt="LING YE"/><br /><sub><b>LING YE</b></sub></a><br /><a href="https://github.com/esengine/ecs-framework/commits?author=foxling" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MirageTank"><img src="https://avatars.githubusercontent.com/MirageTank?s=100" width="100px;" alt="MirageTank"/><br /><sub><b>MirageTank</b></sub></a><br /><a href="https://github.com/esengine/ecs-framework/commits?author=MirageTank" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- markdownlint-restore -->
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
本项目遵循 [all-contributors](https://github.com/all-contributors/all-contributors) 规范。欢迎任何形式的贡献!
|
||||
|
||||
## 许可证
|
||||
|
||||
[MIT](LICENSE) © 2025 ECS Framework
|
||||
[MIT](LICENSE) © 2025 ECS Framework
|
||||
|
||||
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
|
||||
@@ -82,6 +82,21 @@ export default defineConfig({
|
||||
{ text: 'WorldManager', link: '/guide/world-manager' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '行为树系统 (Behavior Tree)',
|
||||
link: '/guide/behavior-tree/',
|
||||
items: [
|
||||
{ text: '快速开始', link: '/guide/behavior-tree/getting-started' },
|
||||
{ text: '核心概念', link: '/guide/behavior-tree/core-concepts' },
|
||||
{ text: '编辑器指南', link: '/guide/behavior-tree/editor-guide' },
|
||||
{ text: '编辑器工作流', link: '/guide/behavior-tree/editor-workflow' },
|
||||
{ text: '自定义动作组件', link: '/guide/behavior-tree/custom-actions' },
|
||||
{ text: 'Cocos Creator集成', link: '/guide/behavior-tree/cocos-integration' },
|
||||
{ text: 'Laya引擎集成', link: '/guide/behavior-tree/laya-integration' },
|
||||
{ text: '高级用法', link: '/guide/behavior-tree/advanced-usage' },
|
||||
{ text: '最佳实践', link: '/guide/behavior-tree/best-practices' }
|
||||
]
|
||||
},
|
||||
{ text: '序列化系统 (Serialization)', link: '/guide/serialization' },
|
||||
{ text: '事件系统 (Event)', link: '/guide/event-system' },
|
||||
{ text: '时间和定时器 (Time)', link: '/guide/time-and-timers' },
|
||||
|
||||
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
|
||||
0
docs/guide/editor-plugin-system.md
Normal file
0
docs/guide/editor-plugin-system.md
Normal file
@@ -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
|
||||
@@ -547,4 +591,4 @@ class LoggingConfiguration {
|
||||
LoggingConfiguration.setupLogging();
|
||||
```
|
||||
|
||||
日志系统是调试和监控应用的重要工具,正确使用日志系统能大大提高开发效率和问题排查能力。
|
||||
日志系统是调试和监控应用的重要工具,正确使用日志系统能大大提高开发效率和问题排查能力。
|
||||
|
||||
66
eslint.config.mjs
Normal file
66
eslint.config.mjs
Normal file
@@ -0,0 +1,66 @@
|
||||
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'
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
'semi': 'warn',
|
||||
'quotes': 'warn',
|
||||
'indent': 'off',
|
||||
'no-trailing-spaces': 'warn',
|
||||
'eol-last': 'warn',
|
||||
'comma-dangle': 'warn',
|
||||
'object-curly-spacing': 'warn',
|
||||
'array-bracket-spacing': 'warn',
|
||||
'arrow-parens': 'warn',
|
||||
'prefer-const': 'warn',
|
||||
'no-multiple-empty-lines': 'warn',
|
||||
'no-console': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
'@typescript-eslint/no-unsafe-function-type': 'warn',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||
'@typescript-eslint/no-unsafe-call': 'off',
|
||||
'@typescript-eslint/no-unsafe-return': 'off',
|
||||
'@typescript-eslint/no-unsafe-argument': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/no-require-imports': 'warn',
|
||||
'@typescript-eslint/no-this-alias': 'warn',
|
||||
'no-case-declarations': 'warn',
|
||||
'no-prototype-builtins': 'warn',
|
||||
'no-empty': 'warn',
|
||||
'no-useless-catch': 'warn'
|
||||
}
|
||||
},
|
||||
{
|
||||
ignores: [
|
||||
'node_modules/**',
|
||||
'**/node_modules/**',
|
||||
'dist/**',
|
||||
'**/dist/**',
|
||||
'bin/**',
|
||||
'**/bin/**',
|
||||
'build/**',
|
||||
'**/build/**',
|
||||
'coverage/**',
|
||||
'**/coverage/**',
|
||||
'thirdparty/**',
|
||||
'examples/lawn-mower-demo/**',
|
||||
'extensions/**',
|
||||
'**/*.min.js',
|
||||
'**/*.d.ts'
|
||||
]
|
||||
}
|
||||
];
|
||||
@@ -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": {}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "1556cd72-9618-4f9f-b9e7-28152a33bde9",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
import { _decorator, Component, Node, Vec3, Color } from 'cc';
|
||||
import { SimplePrefabFactory } from './components/SimplePrefabFactory';
|
||||
import { BehaviorTreeComponent } from './components/BehaviorTreeComponent';
|
||||
import { StatusUIManager } from './components/StatusUIManager';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/**
|
||||
* 矿工AI演示场景
|
||||
*/
|
||||
@ccclass('SimpleMinerDemo')
|
||||
export class SimpleMinerDemo extends Component {
|
||||
|
||||
@property
|
||||
minerCount: number = 1;
|
||||
|
||||
@property
|
||||
goldMineCount: number = 3;
|
||||
|
||||
private miners: Node[] = [];
|
||||
private goldMines: Node[] = [];
|
||||
private warehouse: Node | null = null;
|
||||
private ground: Node | null = null;
|
||||
private totalOresCollected: number = 0;
|
||||
private warehouseUI: any = null;
|
||||
|
||||
start() {
|
||||
this.createWorld();
|
||||
this.createWarehouse();
|
||||
this.createGoldMines();
|
||||
this.createMiners();
|
||||
}
|
||||
|
||||
private createWorld() {
|
||||
this.ground = SimplePrefabFactory.createGround(new Vec3(20, 0.2, 20));
|
||||
this.node.addChild(this.ground);
|
||||
this.ground.setWorldPosition(new Vec3(0, 0, 0));
|
||||
}
|
||||
|
||||
private createWarehouse() {
|
||||
this.warehouse = SimplePrefabFactory.createBuilding('Warehouse', new Vec3(2, 2, 2), Color.GRAY);
|
||||
this.node.addChild(this.warehouse);
|
||||
this.warehouse.setWorldPosition(new Vec3(0, 1, 0));
|
||||
this.createWarehouseUI();
|
||||
}
|
||||
|
||||
private createGoldMines() {
|
||||
for (let i = 0; i < this.goldMineCount; i++) {
|
||||
const angle = (i / this.goldMineCount) * Math.PI * 2;
|
||||
const radius = 6 + Math.random() * 2;
|
||||
const position = new Vec3(
|
||||
Math.cos(angle) * radius,
|
||||
0.8,
|
||||
Math.sin(angle) * radius
|
||||
);
|
||||
|
||||
const goldMine = SimplePrefabFactory.createResource(`GoldMine_${i + 1}`, Color.YELLOW);
|
||||
this.node.addChild(goldMine);
|
||||
goldMine.setWorldPosition(position);
|
||||
goldMine.setScale(new Vec3(1.2, 1.2, 1.2));
|
||||
this.goldMines.push(goldMine);
|
||||
}
|
||||
}
|
||||
|
||||
private createMiners() {
|
||||
for (let i = 0; i < this.minerCount; i++) {
|
||||
const angle = (i / this.minerCount) * Math.PI * 2;
|
||||
const radius = 3;
|
||||
const position = new Vec3(
|
||||
Math.cos(angle) * radius,
|
||||
1,
|
||||
Math.sin(angle) * radius
|
||||
);
|
||||
|
||||
const miner = SimplePrefabFactory.createUnit(`Miner_${i + 1}`, Color.BLUE);
|
||||
this.node.addChild(miner);
|
||||
miner.setWorldPosition(position);
|
||||
|
||||
const behaviorTree = miner.addComponent(BehaviorTreeComponent);
|
||||
behaviorTree.behaviorTreeFile = 'miner-stamina-ai.bt';
|
||||
behaviorTree.debugMode = true;
|
||||
|
||||
this.scheduleOnce(() => {
|
||||
const blackboard = behaviorTree.getBlackboard();
|
||||
if (blackboard) {
|
||||
blackboard.setValue('homePosition', position.clone());
|
||||
}
|
||||
}, 0.5);
|
||||
|
||||
this.miners.push(miner);
|
||||
}
|
||||
}
|
||||
|
||||
public getAllGoldMines(): Node[] {
|
||||
return this.goldMines.filter(mine => mine && mine.isValid);
|
||||
}
|
||||
|
||||
public getWarehouse(): Node | null {
|
||||
return this.warehouse;
|
||||
}
|
||||
|
||||
public mineGoldOre(miner: Node): boolean {
|
||||
this.totalOresCollected++;
|
||||
this.updateWarehouseUI();
|
||||
return true;
|
||||
}
|
||||
|
||||
public getTotalOresCollected(): number {
|
||||
return this.totalOresCollected;
|
||||
}
|
||||
|
||||
private createWarehouseUI() {
|
||||
if (!this.warehouse) return;
|
||||
|
||||
this.warehouseUI = StatusUIManager.createWarehouseUI(this.warehouse);
|
||||
if (this.warehouseUI) {
|
||||
this.updateWarehouseUI();
|
||||
}
|
||||
}
|
||||
|
||||
private updateWarehouseUI() {
|
||||
if (this.warehouseUI && this.warehouseUI.warehouseCountLabel) {
|
||||
this.warehouseUI.warehouseCountLabel.string = `🏭 总存储: ${this.totalOresCollected}`;
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
this.unscheduleAllCallbacks();
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "c3386f4a-9bef-416f-b770-fcec91cedbc4",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "d07d95ad-f180-4b6e-9d0a-7248e75ec795",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -1,518 +0,0 @@
|
||||
import { Node, resources, JsonAsset, Component, _decorator, Vec3, tween, instantiate, Prefab } from 'cc';
|
||||
import { BehaviorTree, BehaviorTreeBuilder, Blackboard, TaskStatus, BehaviorTreeJSONConfig, EventRegistry, IBehaviorTreeContext, ActionResult } from '@esengine/ai';
|
||||
import { MinerStatusUI } from './MinerStatusUI';
|
||||
import { StatusUIManager } from './StatusUIManager';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
@ccclass('BehaviorTreeComponent')
|
||||
export class BehaviorTreeComponent extends Component {
|
||||
|
||||
@property
|
||||
behaviorTreeFile: string = '';
|
||||
|
||||
@property
|
||||
autoStart: boolean = true;
|
||||
|
||||
@property
|
||||
debugMode: boolean = false;
|
||||
|
||||
@property
|
||||
showStatusUI: boolean = true;
|
||||
|
||||
@property(Prefab)
|
||||
statusUIPrefab: Prefab | null = null;
|
||||
|
||||
private behaviorTree: BehaviorTree<any> | null = null;
|
||||
private statusUI: MinerStatusUI | null = null;
|
||||
private blackboard: Blackboard | null = null;
|
||||
private context: any = null;
|
||||
private eventRegistry: EventRegistry | null = null;
|
||||
private isLoaded: boolean = false;
|
||||
private isRunning: boolean = false;
|
||||
|
||||
private actionStates: Map<string, {
|
||||
isExecuting: boolean;
|
||||
startTime: number;
|
||||
duration: number;
|
||||
}> = new Map();
|
||||
|
||||
start() {
|
||||
if (this.autoStart && this.behaviorTreeFile) {
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
if (this.showStatusUI) {
|
||||
this.createStatusUI();
|
||||
}
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
if (!this.behaviorTreeFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.loadBehaviorTree();
|
||||
this.isLoaded = true;
|
||||
this.isRunning = true;
|
||||
} catch (error) {
|
||||
// 静默处理
|
||||
}
|
||||
}
|
||||
|
||||
private async loadBehaviorTree(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let jsonPath = this.behaviorTreeFile;
|
||||
resources.load(jsonPath, JsonAsset, (err, asset) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const treeData = asset.json as BehaviorTreeJSONConfig;
|
||||
this.buildBehaviorTree(treeData);
|
||||
resolve();
|
||||
} catch (buildError) {
|
||||
reject(buildError);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private buildBehaviorTree(treeData: BehaviorTreeJSONConfig) {
|
||||
this.eventRegistry = new EventRegistry();
|
||||
this.setupEventHandlers();
|
||||
|
||||
const baseContext = {
|
||||
node: this.node,
|
||||
component: this,
|
||||
eventRegistry: this.eventRegistry
|
||||
};
|
||||
|
||||
const result = BehaviorTreeBuilder.fromBehaviorTreeConfig(treeData, baseContext);
|
||||
this.behaviorTree = result.tree;
|
||||
this.blackboard = result.blackboard;
|
||||
this.context = result.context;
|
||||
|
||||
this.initializeBlackboard();
|
||||
}
|
||||
|
||||
private setupEventHandlers() {
|
||||
if (!this.eventRegistry) return;
|
||||
|
||||
this.eventRegistry.registerAction('go-home-rest', (context, params) => {
|
||||
return this.handleGoHomeRest(context, params);
|
||||
});
|
||||
|
||||
this.eventRegistry.registerAction('recover-stamina', (context, params) => {
|
||||
return this.handleRecoverStamina(context, params);
|
||||
});
|
||||
|
||||
this.eventRegistry.registerAction('store-ore', (context, params) => {
|
||||
return this.handleStoreOre(context, params);
|
||||
});
|
||||
|
||||
this.eventRegistry.registerAction('mine-gold-ore', (context, params) => {
|
||||
return this.handleMineGoldOre(context, params);
|
||||
});
|
||||
|
||||
this.eventRegistry.registerAction('idle-behavior', (context, params) => {
|
||||
return this.handleIdleBehavior(context, params);
|
||||
});
|
||||
}
|
||||
|
||||
private initializeBlackboard() {
|
||||
if (!this.blackboard) return;
|
||||
|
||||
this.blackboard.setValue('stamina', 100);
|
||||
this.blackboard.setValue('staminaPercentage', 1.0);
|
||||
this.blackboard.setValue('isLowStamina', false);
|
||||
this.blackboard.setValue('hasOre', false);
|
||||
this.blackboard.setValue('isResting', false);
|
||||
this.blackboard.setValue('homePosition', this.node.worldPosition);
|
||||
}
|
||||
|
||||
|
||||
private createStatusUI() {
|
||||
if (!this.statusUIPrefab) {
|
||||
this.createSimpleStatusUI();
|
||||
return;
|
||||
}
|
||||
|
||||
const uiNode = instantiate(this.statusUIPrefab);
|
||||
const canvas = this.node.scene?.getChildByName('Canvas');
|
||||
if (canvas) {
|
||||
canvas.addChild(uiNode);
|
||||
this.statusUI = uiNode.getComponent(MinerStatusUI);
|
||||
if (this.statusUI) {
|
||||
this.statusUI.setFollowTarget(this.node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private createSimpleStatusUI() {
|
||||
this.statusUI = StatusUIManager.createStatusUIForMiner(this.node);
|
||||
}
|
||||
|
||||
private updateStatusUI() {
|
||||
if (!this.statusUI || !this.blackboard) return;
|
||||
|
||||
const stamina = this.blackboard.getValue('stamina') || 0;
|
||||
const maxStamina = this.blackboard.getValue('maxStamina') || 100;
|
||||
const hasOre = this.blackboard.getValue('hasOre') || false;
|
||||
const isResting = this.blackboard.getValue('isResting') || false;
|
||||
|
||||
// 更新体力
|
||||
this.statusUI.updateStamina(stamina, maxStamina);
|
||||
|
||||
// 更新状态文本
|
||||
let status = '';
|
||||
if (isResting) {
|
||||
status = '😴休息中';
|
||||
} else if (hasOre) {
|
||||
status = '🚚运输中';
|
||||
} else {
|
||||
status = '⛏️挖矿中';
|
||||
}
|
||||
this.statusUI.updateStatus(status);
|
||||
|
||||
// 获取仓库矿石总数
|
||||
const gameManager = this.node.parent?.getComponent('SimpleMinerDemo');
|
||||
const warehouseTotal = (gameManager as any)?.getTotalOresCollected() || 0;
|
||||
|
||||
// 更新矿石数量显示
|
||||
this.statusUI.updateOreCount(hasOre, warehouseTotal);
|
||||
|
||||
// 更新动作进度
|
||||
this.updateActionProgressUI();
|
||||
}
|
||||
|
||||
private updateActionProgressUI() {
|
||||
if (!this.statusUI) return;
|
||||
|
||||
let actionName = '';
|
||||
let progress = 0;
|
||||
|
||||
// 检查当前正在执行的动作
|
||||
for (const [key, state] of this.actionStates.entries()) {
|
||||
if (state.isExecuting) {
|
||||
const elapsed = Date.now() - state.startTime;
|
||||
progress = Math.min(elapsed / state.duration, 1.0);
|
||||
|
||||
switch (key) {
|
||||
case 'mine-gold-ore':
|
||||
actionName = '⛏️ 挖掘中';
|
||||
break;
|
||||
case 'store-ore':
|
||||
actionName = '📦 存储中';
|
||||
break;
|
||||
case 'recover-stamina':
|
||||
actionName = '💤 恢复体力';
|
||||
break;
|
||||
default:
|
||||
actionName = key;
|
||||
}
|
||||
break; // 只显示第一个正在执行的动作
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有正在执行的动作,清空进度显示
|
||||
this.statusUI.updateActionProgress(actionName, progress);
|
||||
}
|
||||
|
||||
// ==================== 行为树事件处理器 ====================
|
||||
|
||||
/**
|
||||
* 清理动作状态 - 当动作被中止时调用
|
||||
*/
|
||||
private clearActionState(actionKey: string) {
|
||||
if (this.actionStates.has(actionKey)) {
|
||||
this.actionStates.delete(actionKey);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 回家休息 - 包含体力恢复逻辑
|
||||
*/
|
||||
private handleGoHomeRest(context: any, params: any): ActionResult {
|
||||
const blackboard = this.blackboard;
|
||||
if (!blackboard) return 'failure';
|
||||
|
||||
// 检查是否已经在家了
|
||||
const homePos = blackboard.getValue('homePosition') || this.node.worldPosition;
|
||||
const distance = Vec3.distance(this.node.worldPosition, homePos);
|
||||
|
||||
|
||||
|
||||
if (distance > 1.0) {
|
||||
// 还没到家,继续移动
|
||||
this.moveToPosition(homePos, 2.0);
|
||||
return 'running';
|
||||
} else {
|
||||
this.clearActionState('mine-gold-ore');
|
||||
this.clearActionState('store-ore');
|
||||
blackboard.setValue('isResting', true);
|
||||
const actionKey = 'go-home-rest';
|
||||
const currentTime = Date.now();
|
||||
|
||||
// 初始化休息状态
|
||||
if (!this.actionStates.has(actionKey)) {
|
||||
this.actionStates.set(actionKey, {
|
||||
isExecuting: true,
|
||||
startTime: currentTime,
|
||||
duration: 2000 // 2秒恢复一次
|
||||
});
|
||||
return 'running';
|
||||
}
|
||||
|
||||
const actionState = this.actionStates.get(actionKey)!;
|
||||
const elapsed = currentTime - actionState.startTime;
|
||||
|
||||
if (elapsed >= actionState.duration) {
|
||||
const currentStamina = blackboard.getValue('stamina');
|
||||
const newStamina = Math.min(100, currentStamina + 10);
|
||||
|
||||
blackboard.setValue('stamina', newStamina);
|
||||
blackboard.setValue('staminaPercentage', newStamina / 100);
|
||||
|
||||
if (newStamina >= 80) {
|
||||
blackboard.setValue('isResting', false);
|
||||
blackboard.setValue('isLowStamina', false);
|
||||
this.actionStates.delete(actionKey);
|
||||
return 'success';
|
||||
}
|
||||
|
||||
actionState.startTime = currentTime;
|
||||
}
|
||||
|
||||
return 'running';
|
||||
}
|
||||
}
|
||||
|
||||
private handleRecoverStamina(context: any, params: any): ActionResult {
|
||||
return 'success';
|
||||
}
|
||||
|
||||
private handleMineGoldOre(context: any, params: any): ActionResult {
|
||||
const blackboard = this.blackboard;
|
||||
if (!blackboard) return 'failure';
|
||||
|
||||
const hasOre = blackboard.getValue('hasOre');
|
||||
const isLowStamina = blackboard.getValue('isLowStamina');
|
||||
const isResting = blackboard.getValue('isResting');
|
||||
|
||||
if (hasOre || isLowStamina || isResting) {
|
||||
return 'failure';
|
||||
}
|
||||
|
||||
const gameManager = this.node.parent?.getComponent('SimpleMinerDemo');
|
||||
const goldMines = (gameManager as any)?.getAllGoldMines();
|
||||
if (!goldMines?.length) return 'failure';
|
||||
|
||||
let nearestMine = goldMines[0];
|
||||
let minDistance = Vec3.distance(this.node.worldPosition, nearestMine.worldPosition);
|
||||
|
||||
for (const mine of goldMines) {
|
||||
const distance = Vec3.distance(this.node.worldPosition, mine.worldPosition);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
nearestMine = mine;
|
||||
}
|
||||
}
|
||||
|
||||
if (minDistance > 2.0) {
|
||||
this.moveToPosition(nearestMine.worldPosition, 2.0);
|
||||
return 'running';
|
||||
} else {
|
||||
const actionKey = 'mine-gold-ore';
|
||||
const currentTime = Date.now();
|
||||
|
||||
if (!this.actionStates.has(actionKey)) {
|
||||
this.actionStates.set(actionKey, {
|
||||
isExecuting: true,
|
||||
startTime: currentTime,
|
||||
duration: 3000
|
||||
});
|
||||
return 'running';
|
||||
}
|
||||
|
||||
const actionState = this.actionStates.get(actionKey)!;
|
||||
const elapsed = currentTime - actionState.startTime;
|
||||
|
||||
if (elapsed >= actionState.duration) {
|
||||
const currentStamina = blackboard.getValue('stamina');
|
||||
const newStamina = Math.max(0, currentStamina - 15);
|
||||
|
||||
blackboard.setValue('stamina', newStamina);
|
||||
blackboard.setValue('staminaPercentage', newStamina / 100);
|
||||
blackboard.setValue('hasOre', true);
|
||||
blackboard.setValue('isLowStamina', newStamina < 20);
|
||||
|
||||
this.actionStates.delete(actionKey);
|
||||
return 'failure';
|
||||
}
|
||||
|
||||
return 'running';
|
||||
}
|
||||
}
|
||||
|
||||
private handleStoreOre(context: any, params: any): ActionResult {
|
||||
const blackboard = this.blackboard;
|
||||
if (!blackboard) return 'failure';
|
||||
|
||||
const hasOre = blackboard.getValue('hasOre');
|
||||
if (!hasOre) {
|
||||
return 'failure';
|
||||
}
|
||||
|
||||
const isLowStamina = blackboard.getValue('isLowStamina');
|
||||
if (isLowStamina) {
|
||||
return 'failure';
|
||||
}
|
||||
|
||||
this.clearActionState('mine-gold-ore');
|
||||
const gameManager = this.node.parent?.getComponent('SimpleMinerDemo');
|
||||
const warehouse = (gameManager as any)?.getWarehouse();
|
||||
if (!warehouse) return 'failure';
|
||||
|
||||
const distance = Vec3.distance(this.node.worldPosition, warehouse.worldPosition);
|
||||
|
||||
if (distance > 2.0) {
|
||||
this.moveToPosition(warehouse.worldPosition, 2.0);
|
||||
return 'running';
|
||||
} else {
|
||||
const actionKey = 'store-ore';
|
||||
const currentTime = Date.now();
|
||||
|
||||
if (!this.actionStates.has(actionKey)) {
|
||||
this.actionStates.set(actionKey, {
|
||||
isExecuting: true,
|
||||
startTime: currentTime,
|
||||
duration: 1500
|
||||
});
|
||||
return 'running';
|
||||
}
|
||||
|
||||
const actionState = this.actionStates.get(actionKey)!;
|
||||
const elapsed = currentTime - actionState.startTime;
|
||||
|
||||
if (elapsed >= actionState.duration) {
|
||||
blackboard.setValue('hasOre', false);
|
||||
(gameManager as any).mineGoldOre(this.node);
|
||||
this.actionStates.delete(actionKey);
|
||||
return 'success';
|
||||
}
|
||||
|
||||
return 'running';
|
||||
}
|
||||
}
|
||||
|
||||
private handleIdleBehavior(context: any, params: any): ActionResult {
|
||||
return 'success';
|
||||
}
|
||||
|
||||
private moveToPosition(targetPos: Vec3, duration: number) {
|
||||
tween(this.node).stop();
|
||||
tween(this.node).to(duration, { worldPosition: targetPos }).start();
|
||||
}
|
||||
|
||||
update(deltaTime: number) {
|
||||
if (this.behaviorTree && this.isRunning) {
|
||||
this.behaviorTree.tick(deltaTime);
|
||||
}
|
||||
|
||||
if (this.showStatusUI) {
|
||||
this.updateStatusUI();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取黑板
|
||||
*/
|
||||
getBlackboard(): Blackboard | null {
|
||||
return this.blackboard;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取行为树
|
||||
*/
|
||||
getBehaviorTree(): BehaviorTree<any> | null {
|
||||
return this.behaviorTree;
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停行为树
|
||||
*/
|
||||
pause() {
|
||||
this.isRunning = false;
|
||||
if (this.debugMode) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复行为树
|
||||
*/
|
||||
resume() {
|
||||
if (this.isLoaded) {
|
||||
this.isRunning = true;
|
||||
if (this.debugMode) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止行为树
|
||||
*/
|
||||
stop() {
|
||||
this.isRunning = false;
|
||||
if (this.behaviorTree) {
|
||||
this.behaviorTree.reset();
|
||||
}
|
||||
if (this.debugMode) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新加载行为树
|
||||
*/
|
||||
async reload() {
|
||||
this.stop();
|
||||
await this.initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置行为树状态
|
||||
*/
|
||||
reset() {
|
||||
if (this.behaviorTree) {
|
||||
this.behaviorTree.reset();
|
||||
}
|
||||
if (this.debugMode) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
this.stop();
|
||||
if (this.eventRegistry) {
|
||||
this.eventRegistry.clear();
|
||||
}
|
||||
|
||||
// 清理UI
|
||||
if (this.statusUI) {
|
||||
this.statusUI.node.destroy();
|
||||
this.statusUI = null;
|
||||
}
|
||||
|
||||
this.behaviorTree = null;
|
||||
this.blackboard = null;
|
||||
this.context = null;
|
||||
this.eventRegistry = null;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "8efd182b-9891-4903-bef2-eb07b5184263",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -1,299 +0,0 @@
|
||||
import { _decorator, Component, resources, JsonAsset, Vec3 } from 'cc';
|
||||
import { BehaviorTree, BehaviorTreeBuilder, Blackboard, BehaviorTreeJSONConfig, ExecutionContext, EventRegistry, ActionResult } from '@esengine/ai';
|
||||
import { UnitController } from './UnitController';
|
||||
import { RTSBehaviorHandler } from './RTSBehaviorHandler';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/**
|
||||
* 游戏执行上下文接口
|
||||
* 继承框架的ExecutionContext,添加游戏特定的属性
|
||||
*/
|
||||
interface GameExecutionContext extends ExecutionContext {
|
||||
unitController: UnitController;
|
||||
gameObject: any;
|
||||
eventRegistry?: EventRegistry;
|
||||
// 确保继承索引签名
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* 行为树管理器 - 使用@esengine/ai包管理行为树
|
||||
*/
|
||||
@ccclass('BehaviorTreeManager')
|
||||
export class BehaviorTreeManager extends Component {
|
||||
|
||||
@property
|
||||
debugMode: boolean = true;
|
||||
|
||||
@property
|
||||
tickInterval: number = 0.1; // 行为树更新间隔(秒)- 10fps更新频率,平衡性能和响应性
|
||||
|
||||
private behaviorTree: BehaviorTree<GameExecutionContext> | null = null;
|
||||
private blackboard: Blackboard | null = null;
|
||||
private context: GameExecutionContext | null = null;
|
||||
private eventRegistry: EventRegistry | null = null;
|
||||
private isLoaded: boolean = false;
|
||||
private isRunning: boolean = false;
|
||||
private lastTickTime: number = 0;
|
||||
private unitController: UnitController | null = null;
|
||||
private currentBehaviorTreeName: string = '';
|
||||
private behaviorHandler: RTSBehaviorHandler | null = null;
|
||||
|
||||
/**
|
||||
* 初始化行为树
|
||||
*/
|
||||
async initializeBehaviorTree(behaviorTreeName: string, unitController: UnitController) {
|
||||
this.currentBehaviorTreeName = behaviorTreeName;
|
||||
this.unitController = unitController;
|
||||
|
||||
// 获取RTSBehaviorHandler组件
|
||||
this.behaviorHandler = this.getComponent(RTSBehaviorHandler);
|
||||
if (!this.behaviorHandler) {
|
||||
console.error(`BehaviorTreeManager: 未找到RTSBehaviorHandler组件 - ${this.node.name}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.loadBehaviorTree(behaviorTreeName);
|
||||
this.setupBlackboard();
|
||||
this.isLoaded = true;
|
||||
this.isRunning = true;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`行为树初始化失败: ${behaviorTreeName}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载行为树文件
|
||||
*/
|
||||
private async loadBehaviorTree(behaviorTreeName: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const jsonPath = `${behaviorTreeName}.bt`;
|
||||
|
||||
|
||||
resources.load(jsonPath, JsonAsset, (err, asset) => {
|
||||
if (err) {
|
||||
console.error(`加载行为树文件失败: ${jsonPath}`, err);
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const behaviorTreeData = asset.json as BehaviorTreeJSONConfig;
|
||||
|
||||
// 创建执行上下文
|
||||
this.blackboard = new Blackboard();
|
||||
this.eventRegistry = this.createEventRegistry();
|
||||
this.context = {
|
||||
blackboard: this.blackboard,
|
||||
unitController: this.unitController!,
|
||||
gameObject: this.node,
|
||||
eventRegistry: this.eventRegistry
|
||||
} as GameExecutionContext;
|
||||
|
||||
// 从JSON数据创建行为树
|
||||
const buildResult = BehaviorTreeBuilder.fromBehaviorTreeConfig<GameExecutionContext>(behaviorTreeData, this.context);
|
||||
this.behaviorTree = buildResult.tree;
|
||||
this.blackboard = buildResult.blackboard;
|
||||
|
||||
resolve();
|
||||
} catch (parseError) {
|
||||
console.error(`创建行为树失败: ${jsonPath}`, parseError);
|
||||
reject(parseError);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建事件注册表
|
||||
*/
|
||||
private createEventRegistry(): EventRegistry {
|
||||
const registry = new EventRegistry();
|
||||
|
||||
// 注册体力系统矿工行为事件处理器
|
||||
const eventHandlers = {
|
||||
// 矿工体力系统核心行为
|
||||
'mine-gold-ore': (context: any, params?: any) => this.callBehaviorHandler('onMineGoldOre', params),
|
||||
'store-ore': (context: any, params?: any) => this.callBehaviorHandler('onStoreOre', params),
|
||||
'go-home-rest': (context: any, params?: any) => this.callBehaviorHandler('onGoHomeRest', params),
|
||||
'recover-stamina': (context: any, params?: any) => this.callBehaviorHandler('onRecoverStamina', params),
|
||||
'idle-behavior': (context: any, params?: any) => this.callBehaviorHandler('onIdleBehavior', params)
|
||||
};
|
||||
|
||||
// 将事件处理器注册到EventRegistry
|
||||
Object.entries(eventHandlers).forEach(([eventName, handler]) => {
|
||||
registry.registerAction(eventName, handler);
|
||||
});
|
||||
|
||||
return registry;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用行为处理器的方法
|
||||
*/
|
||||
private callBehaviorHandler(methodName: string, params: any = {}): ActionResult {
|
||||
if (!this.behaviorHandler) {
|
||||
console.error(`BehaviorTreeManager: RTSBehaviorHandler未初始化 - ${this.node.name}`);
|
||||
return 'failure';
|
||||
}
|
||||
|
||||
try {
|
||||
// 直接调用RTSBehaviorHandler的方法
|
||||
const method = (this.behaviorHandler as any)[methodName];
|
||||
if (typeof method === 'function') {
|
||||
const result = method.call(this.behaviorHandler, params);
|
||||
return result || 'success'; // 确保有返回值
|
||||
} else {
|
||||
console.error(`BehaviorTreeManager: 方法不存在: ${methodName}`);
|
||||
return 'failure';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`BehaviorTreeManager: 调用方法失败: ${methodName}`, error);
|
||||
return 'failure';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置黑板基础信息
|
||||
*/
|
||||
private setupBlackboard() {
|
||||
if (!this.unitController || !this.blackboard) return;
|
||||
|
||||
// 设置矿工基础信息
|
||||
this.blackboard.setValue('unitType', this.unitController.unitType);
|
||||
this.blackboard.setValue('currentHealth', this.unitController.currentHealth);
|
||||
this.blackboard.setValue('maxHealth', this.unitController.maxHealth);
|
||||
this.blackboard.setValue('currentCommand', 'mine');
|
||||
this.blackboard.setValue('hasOre', false);
|
||||
this.blackboard.setValue('hasTarget', false);
|
||||
this.blackboard.setValue('targetPosition', null);
|
||||
this.blackboard.setValue('isMoving', false);
|
||||
|
||||
// 设置体力系统信息
|
||||
this.blackboard.setValue('stamina', this.unitController.currentStamina);
|
||||
this.blackboard.setValue('maxStamina', this.unitController.maxStamina);
|
||||
this.blackboard.setValue('staminaPercentage', this.unitController.currentStamina / this.unitController.maxStamina);
|
||||
this.blackboard.setValue('isLowStamina', this.unitController.currentStamina < this.unitController.maxStamina * 0.2);
|
||||
this.blackboard.setValue('isResting', false);
|
||||
this.blackboard.setValue('homePosition', this.unitController.homePosition);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新黑板值
|
||||
*/
|
||||
updateBlackboardValue(key: string, value: any) {
|
||||
if (this.blackboard) {
|
||||
this.blackboard.setValue(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取黑板值
|
||||
*/
|
||||
getBlackboardValue(key: string): any {
|
||||
return this.blackboard?.getValue(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取黑板
|
||||
*/
|
||||
getBlackboard(): Blackboard | null {
|
||||
return this.blackboard;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取行为树
|
||||
*/
|
||||
getBehaviorTree(): BehaviorTree<GameExecutionContext> | null {
|
||||
return this.behaviorTree;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新行为树
|
||||
*/
|
||||
update(deltaTime: number) {
|
||||
if (!this.isLoaded || !this.isRunning || !this.behaviorTree || !this.blackboard) return;
|
||||
|
||||
// 控制更新频率
|
||||
this.lastTickTime += deltaTime;
|
||||
if (this.lastTickTime < this.tickInterval) return;
|
||||
|
||||
this.lastTickTime = 0;
|
||||
|
||||
// 更新矿工状态信息
|
||||
if (this.unitController) {
|
||||
// 基础属性
|
||||
this.blackboard.setValue('currentHealth', this.unitController.currentHealth);
|
||||
this.blackboard.setValue('currentCommand', this.unitController.currentCommand);
|
||||
this.blackboard.setValue('hasTarget', this.unitController.targetPosition && !this.unitController.targetPosition.equals(Vec3.ZERO));
|
||||
this.blackboard.setValue('targetPosition', this.unitController.targetPosition);
|
||||
this.blackboard.setValue('isMoving', this.unitController.targetPosition && !this.unitController.targetPosition.equals(Vec3.ZERO));
|
||||
|
||||
// 体力系统状态
|
||||
this.blackboard.setValue('stamina', this.unitController.currentStamina);
|
||||
this.blackboard.setValue('maxStamina', this.unitController.maxStamina);
|
||||
this.blackboard.setValue('staminaPercentage', this.unitController.currentStamina / this.unitController.maxStamina);
|
||||
this.blackboard.setValue('isLowStamina', this.unitController.currentStamina < this.unitController.maxStamina * 0.2);
|
||||
this.blackboard.setValue('homePosition', this.unitController.homePosition);
|
||||
}
|
||||
|
||||
// 执行行为树
|
||||
try {
|
||||
this.behaviorTree.tick(deltaTime);
|
||||
} catch (error) {
|
||||
console.error(`行为树执行错误: ${this.node.name}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停行为树
|
||||
*/
|
||||
pause() {
|
||||
this.isRunning = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复行为树
|
||||
*/
|
||||
resume() {
|
||||
if (this.isLoaded) {
|
||||
this.isRunning = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止行为树
|
||||
*/
|
||||
stop() {
|
||||
this.isRunning = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新加载行为树
|
||||
*/
|
||||
async reloadBehaviorTree() {
|
||||
if (this.currentBehaviorTreeName && this.unitController) {
|
||||
this.stop();
|
||||
await this.initializeBehaviorTree(this.currentBehaviorTreeName, this.unitController);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置行为树
|
||||
*/
|
||||
reset() {
|
||||
if (this.behaviorTree) {
|
||||
this.behaviorTree.reset();
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
this.stop();
|
||||
this.behaviorTree = null;
|
||||
this.blackboard = null;
|
||||
this.context = null;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "891c88fc-282d-4791-a961-8d85244bfee7",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
import { Component, _decorator, Label, ProgressBar, Node, UITransform, Canvas, find, Camera, Vec3, director, Color, Layers, Graphics } from 'cc';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/**
|
||||
* 矿工状态UI组件
|
||||
*/
|
||||
@ccclass('MinerStatusUI')
|
||||
export class MinerStatusUI extends Component {
|
||||
|
||||
nameLabel: Label | null = null;
|
||||
statusLabel: Label | null = null;
|
||||
staminaBar: ProgressBar | null = null;
|
||||
actionProgressBar: ProgressBar | null = null;
|
||||
actionLabel: Label | null = null;
|
||||
oreCountLabel: Label | null = null;
|
||||
warehouseCountLabel: Label | null = null;
|
||||
|
||||
@property
|
||||
followTarget: Node | null = null;
|
||||
|
||||
@property
|
||||
yOffset: number = 100;
|
||||
|
||||
private camera: Camera | null = null;
|
||||
private canvas: Canvas | null = null;
|
||||
|
||||
start() {
|
||||
this.node.layer = Layers.Enum.UI_2D;
|
||||
|
||||
this.camera = find('Main Camera')?.getComponent(Camera) || director.getScene()?.getComponentInChildren(Camera);
|
||||
this.canvas = find('Canvas')?.getComponent(Canvas) || director.getScene()?.getComponentInChildren(Canvas);
|
||||
|
||||
|
||||
|
||||
if (this.nameLabel && this.followTarget) {
|
||||
this.nameLabel.string = this.followTarget.name;
|
||||
}
|
||||
|
||||
this.updateStamina(100, 100);
|
||||
this.updateStatus('待机中');
|
||||
this.updateActionProgress('', 0);
|
||||
}
|
||||
|
||||
update() {
|
||||
if (this.followTarget && this.camera && this.canvas) {
|
||||
this.updateUIPosition();
|
||||
}
|
||||
}
|
||||
|
||||
private updateUIPosition() {
|
||||
if (!this.followTarget || !this.camera || !this.canvas) return;
|
||||
|
||||
const targetWorldPos = this.followTarget.worldPosition.clone();
|
||||
// 根据目标类型设置不同的Y偏移
|
||||
if (this.followTarget.name.includes('Warehouse')) {
|
||||
targetWorldPos.y += 3.0; // 仓库偏移更高
|
||||
} else {
|
||||
targetWorldPos.y += 2.0; // 矿工偏移
|
||||
}
|
||||
|
||||
// 将世界坐标直接转换为UI坐标
|
||||
const uiPos = new Vec3();
|
||||
this.camera.convertToUINode(targetWorldPos, this.canvas.node, uiPos);
|
||||
this.node.setPosition(uiPos);
|
||||
}
|
||||
|
||||
setFollowTarget(target: Node) {
|
||||
this.followTarget = target;
|
||||
if (this.nameLabel) {
|
||||
this.nameLabel.string = target.name;
|
||||
}
|
||||
}
|
||||
|
||||
updateStamina(current: number, max: number) {
|
||||
if (this.staminaBar) {
|
||||
this.staminaBar.progress = current / max;
|
||||
}
|
||||
|
||||
if (this.staminaBar) {
|
||||
const percentage = current / max;
|
||||
const fillNode = this.staminaBar.node.getChildByName('Bar');
|
||||
if (fillNode) {
|
||||
const graphics = fillNode.getComponent(Graphics);
|
||||
if (graphics) {
|
||||
let color: Color;
|
||||
if (percentage > 0.6) {
|
||||
color = new Color(0, 255, 0, 255);
|
||||
} else if (percentage > 0.3) {
|
||||
color = new Color(255, 255, 0, 255);
|
||||
} else {
|
||||
color = new Color(255, 0, 0, 255);
|
||||
}
|
||||
|
||||
graphics.clear();
|
||||
graphics.fillColor = color;
|
||||
graphics.rect(-75, -4, 150 * percentage, 8);
|
||||
graphics.fill();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateStatus(status: string) {
|
||||
if (this.statusLabel) {
|
||||
this.statusLabel.string = status;
|
||||
}
|
||||
}
|
||||
|
||||
updateActionProgress(actionName: string, progress: number) {
|
||||
if (this.actionLabel) {
|
||||
this.actionLabel.string = actionName;
|
||||
}
|
||||
|
||||
if (this.actionProgressBar) {
|
||||
this.actionProgressBar.progress = Math.max(0, Math.min(1, progress));
|
||||
this.actionProgressBar.node.active = actionName !== '' && progress > 0;
|
||||
}
|
||||
}
|
||||
|
||||
setVisible(visible: boolean) {
|
||||
this.node.active = visible;
|
||||
}
|
||||
|
||||
updateOreCount(hasOre: boolean, warehouseTotal: number) {
|
||||
if (this.oreCountLabel) {
|
||||
this.oreCountLabel.string = hasOre ? '💎1' : '💎0';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "5f877c25-5c26-49c6-bbb5-7ff36323e0a1",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -1,334 +0,0 @@
|
||||
import { _decorator, Component, Vec3, Node } from 'cc';
|
||||
import { UnitController } from './UnitController';
|
||||
|
||||
const { ccclass } = _decorator;
|
||||
|
||||
/**
|
||||
* 矿工体力系统行为处理器 - 处理挖矿、休息、存储的完整循环
|
||||
* 展示体力驱动的工作-休息循环系统
|
||||
*/
|
||||
@ccclass('RTSBehaviorHandler')
|
||||
export class RTSBehaviorHandler extends Component {
|
||||
|
||||
private unitController: UnitController | null = null;
|
||||
private minerDemo: any = null; // MinerDemo组件引用
|
||||
private lastActionTime: number = 0;
|
||||
private actionCooldown: number = 0.5; // 动作冷却时间,避免频繁切换
|
||||
private minerIndex: number = -1; // 矿工索引,用于找到对应的家
|
||||
|
||||
start() {
|
||||
this.unitController = this.getComponent(UnitController);
|
||||
// 获取场景中的MinerDemo组件
|
||||
this.minerDemo = this.node.parent?.getComponent('MinerDemo');
|
||||
|
||||
if (!this.unitController) {
|
||||
console.error('RTSBehaviorHandler: 未找到UnitController组件');
|
||||
}
|
||||
if (!this.minerDemo) {
|
||||
console.error('RTSBehaviorHandler: 未找到MinerDemo组件');
|
||||
}
|
||||
|
||||
// 从节点名称中提取矿工索引
|
||||
const match = this.node.name.match(/Miner_(\d+)/);
|
||||
if (match) {
|
||||
this.minerIndex = parseInt(match[1]) - 1; // 转换为0基索引
|
||||
}
|
||||
|
||||
this.lastActionTime = Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查动作冷却
|
||||
*/
|
||||
private isActionOnCooldown(): boolean {
|
||||
return (Date.now() - this.lastActionTime) < (this.actionCooldown * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新动作时间
|
||||
*/
|
||||
private updateActionTime() {
|
||||
this.lastActionTime = Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 挖掘金矿(永不枯竭)
|
||||
* @param params 事件参数,包含黑板变量值
|
||||
*/
|
||||
onMineGoldOre(params: any = {}): string {
|
||||
if (!this.unitController || !this.minerDemo) {
|
||||
return 'failure';
|
||||
}
|
||||
|
||||
// 检查体力是否充足
|
||||
if (this.unitController.currentStamina < this.unitController.staminaCostPerMining) {
|
||||
return 'failure';
|
||||
}
|
||||
|
||||
// 检查是否已经携带矿石
|
||||
const hasOre = this.unitController.getBlackboardValue('hasOre');
|
||||
if (hasOre) {
|
||||
return 'failure';
|
||||
}
|
||||
|
||||
// 动作冷却检查
|
||||
if (this.isActionOnCooldown()) {
|
||||
return 'running';
|
||||
}
|
||||
|
||||
// 获取所有金矿
|
||||
const goldMines = this.minerDemo.getAllGoldMines();
|
||||
if (goldMines.length === 0) {
|
||||
return 'failure';
|
||||
}
|
||||
|
||||
// 寻找最近的金矿
|
||||
const currentPos = this.node.worldPosition;
|
||||
let nearestMine: Node | null = null;
|
||||
let minDistance = Infinity;
|
||||
|
||||
for (const mine of goldMines) {
|
||||
if (!mine || !mine.isValid) continue;
|
||||
|
||||
const distance = Vec3.distance(currentPos, mine.worldPosition);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
nearestMine = mine;
|
||||
}
|
||||
}
|
||||
|
||||
if (!nearestMine) {
|
||||
return 'failure';
|
||||
}
|
||||
|
||||
// 检查是否已经到达金矿位置
|
||||
if (minDistance < 2.0) {
|
||||
// 检查是否正在移动
|
||||
const isMoving = this.unitController.getBlackboardValue('isMoving');
|
||||
if (isMoving) {
|
||||
return 'running';
|
||||
}
|
||||
|
||||
// 消耗体力
|
||||
this.unitController.currentStamina = Math.max(0, this.unitController.currentStamina - this.unitController.staminaCostPerMining);
|
||||
|
||||
// 设置携带矿石状态
|
||||
this.unitController.setBlackboardValue('hasOre', true);
|
||||
|
||||
// 通知演示管理器
|
||||
this.minerDemo.mineGoldOre(this.node);
|
||||
|
||||
// 清除移动目标
|
||||
this.unitController.clearTarget();
|
||||
this.unitController.setBlackboardValue('isMoving', false);
|
||||
|
||||
this.updateActionTime();
|
||||
return 'success';
|
||||
} else {
|
||||
// 设置移动目标
|
||||
this.unitController.setTarget(nearestMine.worldPosition);
|
||||
return 'running';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 前往仓库存储矿石
|
||||
* @param params 事件参数,包含黑板变量值
|
||||
*/
|
||||
onStoreOre(params: any = {}): string {
|
||||
if (!this.unitController || !this.minerDemo) {
|
||||
return 'failure';
|
||||
}
|
||||
|
||||
// 检查是否携带矿石
|
||||
const hasOre = this.unitController.getBlackboardValue('hasOre');
|
||||
if (!hasOre) {
|
||||
return 'failure';
|
||||
}
|
||||
|
||||
// 动作冷却检查
|
||||
if (this.isActionOnCooldown()) {
|
||||
return 'running';
|
||||
}
|
||||
|
||||
const warehouse = this.minerDemo.getWarehouse();
|
||||
if (!warehouse || !warehouse.isValid) {
|
||||
return 'failure';
|
||||
}
|
||||
|
||||
// 计算到仓库的距离
|
||||
const currentPos = this.node.worldPosition;
|
||||
const warehousePos = warehouse.worldPosition;
|
||||
const distance = Vec3.distance(currentPos, warehousePos);
|
||||
|
||||
// 检查是否已经到达仓库
|
||||
if (distance < 2.5) {
|
||||
// 检查是否正在移动
|
||||
const isMoving = this.unitController.getBlackboardValue('isMoving');
|
||||
if (isMoving) {
|
||||
return 'running';
|
||||
}
|
||||
|
||||
// 清除携带矿石状态
|
||||
this.unitController.setBlackboardValue('hasOre', false);
|
||||
|
||||
// 清除移动目标
|
||||
this.unitController.clearTarget();
|
||||
this.unitController.setBlackboardValue('isMoving', false);
|
||||
|
||||
this.updateActionTime();
|
||||
return 'success';
|
||||
} else {
|
||||
// 设置移动目标
|
||||
this.unitController.setTarget(warehousePos);
|
||||
return 'running';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 回家休息
|
||||
* @param params 事件参数,包含黑板变量值
|
||||
*/
|
||||
onGoHomeRest(params: any = {}): string {
|
||||
if (!this.unitController || !this.minerDemo) {
|
||||
return 'failure';
|
||||
}
|
||||
|
||||
// 动作冷却检查
|
||||
if (this.isActionOnCooldown()) {
|
||||
return 'running';
|
||||
}
|
||||
|
||||
// 获取矿工的家
|
||||
const home = this.minerDemo.getMinerHome(this.minerIndex);
|
||||
if (!home || !home.isValid) {
|
||||
return 'failure';
|
||||
}
|
||||
|
||||
// 计算到家的距离
|
||||
const currentPos = this.node.worldPosition;
|
||||
const homePos = home.worldPosition;
|
||||
const distance = Vec3.distance(currentPos, homePos);
|
||||
|
||||
// 检查是否已经到达家
|
||||
if (distance < 2.0) {
|
||||
// 检查是否正在移动
|
||||
const isMoving = this.unitController.getBlackboardValue('isMoving');
|
||||
if (isMoving) {
|
||||
return 'running';
|
||||
}
|
||||
|
||||
// 设置休息状态
|
||||
this.unitController.setBlackboardValue('isResting', true);
|
||||
|
||||
// 清除移动目标
|
||||
this.unitController.clearTarget();
|
||||
this.unitController.setBlackboardValue('isMoving', false);
|
||||
|
||||
this.updateActionTime();
|
||||
return 'success';
|
||||
} else {
|
||||
// 设置移动目标
|
||||
this.unitController.setTarget(homePos);
|
||||
return 'running';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复体力
|
||||
* @param params 事件参数,包含黑板变量值
|
||||
*/
|
||||
onRecoverStamina(params: any = {}): string {
|
||||
if (!this.unitController) {
|
||||
return 'failure';
|
||||
}
|
||||
|
||||
// 检查是否在家中
|
||||
const isResting = this.unitController.getBlackboardValue('isResting');
|
||||
if (!isResting) {
|
||||
return 'failure';
|
||||
}
|
||||
|
||||
// 恢复体力
|
||||
const oldStamina = this.unitController.currentStamina;
|
||||
this.unitController.currentStamina = Math.min(this.unitController.maxStamina,
|
||||
this.unitController.currentStamina + this.unitController.staminaRecoveryRate * 0.1); // 每次恢复2点体力
|
||||
|
||||
const isFullyRested = this.unitController.currentStamina >= this.unitController.maxStamina;
|
||||
|
||||
if (isFullyRested) {
|
||||
// 清除休息状态
|
||||
this.unitController.setBlackboardValue('isResting', false);
|
||||
|
||||
// 通知演示管理器
|
||||
this.minerDemo.completeRestCycle();
|
||||
|
||||
this.updateActionTime();
|
||||
return 'success';
|
||||
} else {
|
||||
// 体力还在恢复中
|
||||
return 'running';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 待机行为
|
||||
* @param params 事件参数,包含黑板变量值
|
||||
*/
|
||||
onIdleBehavior(params: any = {}): string {
|
||||
if (!this.unitController) {
|
||||
return 'failure';
|
||||
}
|
||||
|
||||
// 清除移动目标,确保停止移动
|
||||
this.unitController.clearTarget();
|
||||
this.unitController.setBlackboardValue('isMoving', false);
|
||||
|
||||
|
||||
|
||||
return 'success';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取矿工状态摘要
|
||||
*/
|
||||
getMinerStatus(): string {
|
||||
if (!this.unitController) return 'Unknown';
|
||||
|
||||
const hasOre = this.unitController.getBlackboardValue('hasOre');
|
||||
const isMoving = this.unitController.getBlackboardValue('isMoving');
|
||||
const isResting = this.unitController.getBlackboardValue('isResting');
|
||||
const stamina = this.unitController.currentStamina;
|
||||
const maxStamina = this.unitController.maxStamina;
|
||||
|
||||
let status = '';
|
||||
if (isResting) {
|
||||
status = '😴休息中';
|
||||
} else if (hasOre) {
|
||||
status = isMoving ? '🚚运输中' : '📦携带矿石';
|
||||
} else {
|
||||
status = isMoving ? '🚶移动中' : '⛏️挖矿';
|
||||
}
|
||||
|
||||
return `${status} (体力:${stamina.toFixed(0)}/${maxStamina})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调试信息
|
||||
*/
|
||||
getDebugInfo(): any {
|
||||
if (!this.unitController) return {};
|
||||
|
||||
return {
|
||||
name: this.node.name,
|
||||
hasOre: this.unitController.getBlackboardValue('hasOre'),
|
||||
isMoving: this.unitController.getBlackboardValue('isMoving'),
|
||||
isResting: this.unitController.getBlackboardValue('isResting'),
|
||||
stamina: this.unitController.currentStamina,
|
||||
maxStamina: this.unitController.maxStamina,
|
||||
staminaPercentage: this.unitController.currentStamina / this.unitController.maxStamina,
|
||||
isLowStamina: this.unitController.currentStamina < this.unitController.maxStamina * 0.2,
|
||||
status: this.getMinerStatus()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "739ff9ee-42d5-4542-bb5b-3e7611c729e2",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
import { _decorator, Component, Node, Vec3, MeshRenderer, BoxCollider, RigidBody, Material, Color, primitives, utils } from 'cc';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/**
|
||||
* 简单预制体工厂
|
||||
*/
|
||||
@ccclass('SimplePrefabFactory')
|
||||
export class SimplePrefabFactory extends Component {
|
||||
|
||||
/**
|
||||
* 创建单位节点
|
||||
*/
|
||||
static createUnit(name: string, color: Color = Color.WHITE): Node {
|
||||
const unit = new Node(name);
|
||||
|
||||
// 添加网格渲染器
|
||||
const meshRenderer = unit.addComponent(MeshRenderer);
|
||||
|
||||
// 创建立方体网格
|
||||
const mesh = utils.createMesh(primitives.box({ width: 1, height: 1, length: 1 }));
|
||||
meshRenderer.mesh = mesh;
|
||||
|
||||
// 创建材质
|
||||
const material = new Material();
|
||||
material.initialize({ effectName: 'builtin-unlit' });
|
||||
material.setProperty('mainColor', color);
|
||||
meshRenderer.material = material;
|
||||
|
||||
// 添加碰撞器
|
||||
const collider = unit.addComponent(BoxCollider);
|
||||
collider.size = new Vec3(1, 1, 1);
|
||||
|
||||
// 添加刚体
|
||||
const rigidBody = unit.addComponent(RigidBody);
|
||||
rigidBody.type = RigidBody.Type.KINEMATIC;
|
||||
|
||||
return unit;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建建筑节点
|
||||
*/
|
||||
static createBuilding(name: string, size: Vec3 = new Vec3(2, 2, 2), color: Color = Color.GRAY): Node {
|
||||
const building = new Node(name);
|
||||
|
||||
// 添加网格渲染器
|
||||
const meshRenderer = building.addComponent(MeshRenderer);
|
||||
|
||||
// 创建立方体网格
|
||||
const mesh = utils.createMesh(primitives.box({
|
||||
width: size.x,
|
||||
height: size.y,
|
||||
length: size.z
|
||||
}));
|
||||
meshRenderer.mesh = mesh;
|
||||
|
||||
// 创建材质
|
||||
const material = new Material();
|
||||
material.initialize({ effectName: 'builtin-unlit' });
|
||||
material.setProperty('mainColor', color);
|
||||
meshRenderer.material = material;
|
||||
|
||||
// 添加碰撞器
|
||||
const collider = building.addComponent(BoxCollider);
|
||||
collider.size = size;
|
||||
|
||||
return building;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建资源节点
|
||||
*/
|
||||
static createResource(name: string, color: Color = Color.YELLOW): Node {
|
||||
const resource = new Node(name);
|
||||
|
||||
// 添加网格渲染器
|
||||
const meshRenderer = resource.addComponent(MeshRenderer);
|
||||
|
||||
// 创建球体网格
|
||||
const mesh = utils.createMesh(primitives.sphere(0.5));
|
||||
meshRenderer.mesh = mesh;
|
||||
|
||||
// 创建材质
|
||||
const material = new Material();
|
||||
material.initialize({ effectName: 'builtin-unlit' });
|
||||
material.setProperty('mainColor', color);
|
||||
meshRenderer.material = material;
|
||||
|
||||
// 添加碰撞器
|
||||
const collider = resource.addComponent(BoxCollider);
|
||||
collider.size = new Vec3(1, 1, 1);
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建地面节点
|
||||
*/
|
||||
static createGround(size: Vec3 = new Vec3(50, 0.1, 50), color: Color = new Color(100, 150, 100, 255)): Node {
|
||||
const ground = new Node('Ground');
|
||||
|
||||
// 添加网格渲染器
|
||||
const meshRenderer = ground.addComponent(MeshRenderer);
|
||||
|
||||
// 创建平面网格
|
||||
const mesh = utils.createMesh(primitives.box({
|
||||
width: size.x,
|
||||
height: size.y,
|
||||
length: size.z
|
||||
}));
|
||||
meshRenderer.mesh = mesh;
|
||||
|
||||
// 创建材质
|
||||
const material = new Material();
|
||||
material.initialize({ effectName: 'builtin-unlit' });
|
||||
material.setProperty('mainColor', color);
|
||||
meshRenderer.material = material;
|
||||
|
||||
// 添加碰撞器
|
||||
const collider = ground.addComponent(BoxCollider);
|
||||
collider.size = size;
|
||||
|
||||
return ground;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "ac45cfc7-cf47-4315-bdf0-ba002b45b4b6",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -1,282 +0,0 @@
|
||||
import { Component, _decorator, Node, Label, ProgressBar, UITransform, Widget, Canvas, find, director, Color, Sprite, Layers, Graphics } from 'cc';
|
||||
import { MinerStatusUI } from './MinerStatusUI';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/**
|
||||
* 状态UI管理器
|
||||
* 负责创建和管理游戏对象的状态显示界面
|
||||
*/
|
||||
@ccclass('StatusUIManager')
|
||||
export class StatusUIManager extends Component {
|
||||
|
||||
/**
|
||||
* 为矿工创建状态显示UI
|
||||
*/
|
||||
static createStatusUIForMiner(miner: Node): MinerStatusUI | null {
|
||||
const canvas = find('Canvas') || director.getScene()?.getChildByName('Canvas');
|
||||
if (!canvas) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const uiRoot = new Node(`${miner.name}_StatusUI`);
|
||||
canvas.addChild(uiRoot);
|
||||
|
||||
const uiTransform = uiRoot.addComponent(UITransform);
|
||||
uiTransform.setContentSize(200, 100);
|
||||
|
||||
const borderNode = new Node('Border');
|
||||
uiRoot.addChild(borderNode);
|
||||
const borderTransform = borderNode.addComponent(UITransform);
|
||||
borderTransform.setContentSize(202, 102);
|
||||
const borderGraphics = borderNode.addComponent(Graphics);
|
||||
borderGraphics.fillColor = new Color(100, 100, 100, 120);
|
||||
borderGraphics.rect(-101, -51, 202, 102);
|
||||
borderGraphics.fill();
|
||||
|
||||
const borderWidget = borderNode.addComponent(Widget);
|
||||
borderWidget.isAlignTop = true;
|
||||
borderWidget.isAlignBottom = true;
|
||||
borderWidget.isAlignLeft = true;
|
||||
borderWidget.isAlignRight = true;
|
||||
borderWidget.top = -1;
|
||||
borderWidget.bottom = -1;
|
||||
borderWidget.left = -1;
|
||||
borderWidget.right = -1;
|
||||
borderWidget.updateAlignment();
|
||||
|
||||
const backgroundNode = new Node('Background');
|
||||
uiRoot.addChild(backgroundNode);
|
||||
const backgroundTransform = backgroundNode.addComponent(UITransform);
|
||||
backgroundTransform.setContentSize(200, 100);
|
||||
const backgroundGraphics = backgroundNode.addComponent(Graphics);
|
||||
backgroundGraphics.fillColor = new Color(0, 0, 0, 100);
|
||||
backgroundGraphics.rect(-100, -50, 200, 100);
|
||||
backgroundGraphics.fill();
|
||||
|
||||
const statusUI = uiRoot.addComponent(MinerStatusUI);
|
||||
statusUI.setFollowTarget(miner);
|
||||
|
||||
const nameNode = new Node('NameLabel');
|
||||
uiRoot.addChild(nameNode);
|
||||
const nameTransform = nameNode.addComponent(UITransform);
|
||||
nameTransform.setContentSize(200, 25);
|
||||
const nameLabel = nameNode.addComponent(Label);
|
||||
nameLabel.string = miner.name;
|
||||
nameLabel.fontSize = 16;
|
||||
nameLabel.color = new Color(255, 255, 255, 255);
|
||||
|
||||
const nameWidget = nameNode.addComponent(Widget);
|
||||
nameWidget.isAlignTop = true;
|
||||
nameWidget.top = 0;
|
||||
nameWidget.isAlignHorizontalCenter = true;
|
||||
nameWidget.updateAlignment();
|
||||
|
||||
// 创建状态标签
|
||||
const statusNode = new Node('StatusLabel');
|
||||
uiRoot.addChild(statusNode);
|
||||
const statusTransform = statusNode.addComponent(UITransform);
|
||||
statusTransform.setContentSize(200, 20);
|
||||
const statusLabel = statusNode.addComponent(Label);
|
||||
statusLabel.string = '待机中';
|
||||
statusLabel.fontSize = 14;
|
||||
statusLabel.color = new Color(200, 200, 200, 255);
|
||||
|
||||
// 设置状态标签位置
|
||||
const statusWidget = statusNode.addComponent(Widget);
|
||||
statusWidget.isAlignTop = true;
|
||||
statusWidget.top = 25;
|
||||
statusWidget.isAlignHorizontalCenter = true;
|
||||
statusWidget.updateAlignment();
|
||||
|
||||
// 创建体力进度条
|
||||
const staminaBarNode = new Node('StaminaBar');
|
||||
uiRoot.addChild(staminaBarNode);
|
||||
const staminaBarTransform = staminaBarNode.addComponent(UITransform);
|
||||
staminaBarTransform.setContentSize(150, 8);
|
||||
const staminaBar = staminaBarNode.addComponent(ProgressBar);
|
||||
staminaBar.progress = 1.0;
|
||||
|
||||
// 创建体力进度条背景
|
||||
const staminaBgNode = new Node('Background');
|
||||
staminaBarNode.addChild(staminaBgNode);
|
||||
const staminaBgTransform = staminaBgNode.addComponent(UITransform);
|
||||
staminaBgTransform.setContentSize(150, 8);
|
||||
const staminaBgGraphics = staminaBgNode.addComponent(Graphics);
|
||||
staminaBgGraphics.fillColor = new Color(50, 50, 50, 255);
|
||||
staminaBgGraphics.rect(-75, -4, 150, 8);
|
||||
staminaBgGraphics.fill();
|
||||
|
||||
// 创建体力进度条填充
|
||||
const staminaFillNode = new Node('Bar');
|
||||
staminaBarNode.addChild(staminaFillNode);
|
||||
const staminaFillTransform = staminaFillNode.addComponent(UITransform);
|
||||
staminaFillTransform.setContentSize(150, 8);
|
||||
const staminaFillGraphics = staminaFillNode.addComponent(Graphics);
|
||||
staminaFillGraphics.fillColor = new Color(0, 255, 0, 255);
|
||||
staminaFillGraphics.rect(-75, -4, 150, 8);
|
||||
staminaFillGraphics.fill();
|
||||
|
||||
// 设置体力进度条位置
|
||||
const staminaWidget = staminaBarNode.addComponent(Widget);
|
||||
staminaWidget.isAlignTop = true;
|
||||
staminaWidget.top = 45;
|
||||
staminaWidget.isAlignHorizontalCenter = true;
|
||||
staminaWidget.updateAlignment();
|
||||
|
||||
// 创建动作进度条
|
||||
const actionBarNode = new Node('ActionProgressBar');
|
||||
uiRoot.addChild(actionBarNode);
|
||||
const actionBarTransform = actionBarNode.addComponent(UITransform);
|
||||
actionBarTransform.setContentSize(150, 6);
|
||||
const actionBar = actionBarNode.addComponent(ProgressBar);
|
||||
actionBar.progress = 0;
|
||||
actionBarNode.active = false; // 初始隐藏
|
||||
|
||||
// 创建动作进度条背景
|
||||
const actionBgNode = new Node('Background');
|
||||
actionBarNode.addChild(actionBgNode);
|
||||
const actionBgTransform = actionBgNode.addComponent(UITransform);
|
||||
actionBgTransform.setContentSize(150, 6);
|
||||
const actionBgGraphics = actionBgNode.addComponent(Graphics);
|
||||
actionBgGraphics.fillColor = new Color(50, 50, 50, 255);
|
||||
actionBgGraphics.rect(-75, -3, 150, 6);
|
||||
actionBgGraphics.fill();
|
||||
|
||||
// 创建动作进度条填充
|
||||
const actionFillNode = new Node('Bar');
|
||||
actionBarNode.addChild(actionFillNode);
|
||||
const actionFillTransform = actionFillNode.addComponent(UITransform);
|
||||
actionFillTransform.setContentSize(150, 6);
|
||||
const actionFillGraphics = actionFillNode.addComponent(Graphics);
|
||||
actionFillGraphics.fillColor = new Color(255, 255, 0, 255);
|
||||
actionFillGraphics.rect(-75, -3, 150, 6);
|
||||
actionFillGraphics.fill();
|
||||
|
||||
// 设置动作进度条位置
|
||||
const actionWidget = actionBarNode.addComponent(Widget);
|
||||
actionWidget.isAlignTop = true;
|
||||
actionWidget.top = 55;
|
||||
actionWidget.isAlignHorizontalCenter = true;
|
||||
actionWidget.updateAlignment();
|
||||
|
||||
// 创建动作标签
|
||||
const actionLabelNode = new Node('ActionLabel');
|
||||
uiRoot.addChild(actionLabelNode);
|
||||
const actionLabelTransform = actionLabelNode.addComponent(UITransform);
|
||||
actionLabelTransform.setContentSize(200, 15);
|
||||
const actionLabel = actionLabelNode.addComponent(Label);
|
||||
actionLabel.string = '';
|
||||
actionLabel.fontSize = 12;
|
||||
actionLabel.color = new Color(255, 255, 0, 255);
|
||||
|
||||
// 设置动作标签位置
|
||||
const actionLabelWidget = actionLabelNode.addComponent(Widget);
|
||||
actionLabelWidget.isAlignTop = true;
|
||||
actionLabelWidget.top = 65;
|
||||
actionLabelWidget.isAlignHorizontalCenter = true;
|
||||
actionLabelWidget.updateAlignment();
|
||||
|
||||
// 创建矿石数量标签
|
||||
const oreCountNode = new Node('OreCountLabel');
|
||||
uiRoot.addChild(oreCountNode);
|
||||
const oreCountTransform = oreCountNode.addComponent(UITransform);
|
||||
oreCountTransform.setContentSize(100, 15);
|
||||
const oreCountLabel = oreCountNode.addComponent(Label);
|
||||
oreCountLabel.string = '💎0';
|
||||
oreCountLabel.fontSize = 12;
|
||||
oreCountLabel.color = new Color(255, 215, 0, 255); // 金色
|
||||
|
||||
// 设置矿石数量标签位置(居中显示)
|
||||
const oreCountWidget = oreCountNode.addComponent(Widget);
|
||||
oreCountWidget.isAlignTop = true;
|
||||
oreCountWidget.top = 80;
|
||||
oreCountWidget.isAlignHorizontalCenter = true;
|
||||
oreCountWidget.updateAlignment();
|
||||
|
||||
statusUI.nameLabel = nameLabel;
|
||||
statusUI.statusLabel = statusLabel;
|
||||
statusUI.staminaBar = staminaBar;
|
||||
statusUI.actionProgressBar = actionBar;
|
||||
statusUI.actionLabel = actionLabel;
|
||||
statusUI.oreCountLabel = oreCountLabel;
|
||||
statusUI.warehouseCountLabel = null;
|
||||
|
||||
StatusUIManager.setNodeLayerRecursively(uiRoot, Layers.Enum.UI_2D);
|
||||
return statusUI;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归设置节点及其子节点的层级
|
||||
*/
|
||||
private static setNodeLayerRecursively(node: Node, layer: number) {
|
||||
node.layer = layer;
|
||||
for (const child of node.children) {
|
||||
StatusUIManager.setNodeLayerRecursively(child, layer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从矿工名字中提取索引号
|
||||
*/
|
||||
private static extractMinerIndex(minerName: string): number {
|
||||
const match = minerName.match(/Miner_(\d+)/);
|
||||
if (match) {
|
||||
return parseInt(match[1]) - 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为仓库创建存储量显示UI
|
||||
*/
|
||||
static createWarehouseUI(warehouse: Node): MinerStatusUI | null {
|
||||
const canvas = find('Canvas') || director.getScene()?.getChildByName('Canvas');
|
||||
if (!canvas) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const uiRoot = new Node(`${warehouse.name}_StorageUI`);
|
||||
canvas.addChild(uiRoot);
|
||||
|
||||
const uiTransform = uiRoot.addComponent(UITransform);
|
||||
uiTransform.setContentSize(120, 40);
|
||||
|
||||
const backgroundNode = new Node('Background');
|
||||
uiRoot.addChild(backgroundNode);
|
||||
const backgroundTransform = backgroundNode.addComponent(UITransform);
|
||||
backgroundTransform.setContentSize(120, 40);
|
||||
const backgroundGraphics = backgroundNode.addComponent(Graphics);
|
||||
backgroundGraphics.fillColor = new Color(0, 0, 0, 120);
|
||||
backgroundGraphics.rect(-60, -20, 120, 40);
|
||||
backgroundGraphics.fill();
|
||||
|
||||
const storageNode = new Node('StorageLabel');
|
||||
uiRoot.addChild(storageNode);
|
||||
const storageTransform = storageNode.addComponent(UITransform);
|
||||
storageTransform.setContentSize(120, 30);
|
||||
const storageLabel = storageNode.addComponent(Label);
|
||||
storageLabel.string = '🏭 总存储: 0';
|
||||
storageLabel.fontSize = 14;
|
||||
storageLabel.color = new Color(255, 255, 255, 255);
|
||||
|
||||
const storageWidget = storageNode.addComponent(Widget);
|
||||
storageWidget.isAlignHorizontalCenter = true;
|
||||
storageWidget.isAlignVerticalCenter = true;
|
||||
storageWidget.updateAlignment();
|
||||
|
||||
const statusUI = uiRoot.addComponent(MinerStatusUI);
|
||||
statusUI.setFollowTarget(warehouse);
|
||||
|
||||
statusUI.nameLabel = null;
|
||||
statusUI.statusLabel = null;
|
||||
statusUI.staminaBar = null;
|
||||
statusUI.actionProgressBar = null;
|
||||
statusUI.actionLabel = null;
|
||||
statusUI.oreCountLabel = null;
|
||||
statusUI.warehouseCountLabel = storageLabel;
|
||||
|
||||
StatusUIManager.setNodeLayerRecursively(uiRoot, Layers.Enum.UI_2D);
|
||||
return statusUI;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "7478e794-dd80-4661-9421-8e147d33c51e",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -1,353 +0,0 @@
|
||||
import { _decorator, Component, Node, Vec3, MeshRenderer, Color, tween } from 'cc';
|
||||
import { BehaviorTreeManager } from './BehaviorTreeManager';
|
||||
import { RTSBehaviorHandler } from './RTSBehaviorHandler';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/**
|
||||
* 单位配置接口
|
||||
*/
|
||||
export interface UnitConfig {
|
||||
unitType: string;
|
||||
behaviorTreeName: string;
|
||||
maxHealth: number;
|
||||
moveSpeed: number;
|
||||
attackRange: number;
|
||||
attackDamage: number;
|
||||
color: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 单位控制器
|
||||
*/
|
||||
@ccclass('UnitController')
|
||||
export class UnitController extends Component {
|
||||
|
||||
@property
|
||||
showDebugInfo: boolean = true;
|
||||
|
||||
// 单位属性
|
||||
public unitType: string = '';
|
||||
public maxHealth: number = 100;
|
||||
public currentHealth: number = 100;
|
||||
public moveSpeed: number = 1.5;
|
||||
public attackRange: number = 2;
|
||||
public attackDamage: number = 25;
|
||||
public isSelected: boolean = false;
|
||||
public currentCommand: string = 'idle';
|
||||
public targetPosition: Vec3 = Vec3.ZERO.clone();
|
||||
public targetNode: Node | null = null;
|
||||
public lastAttackTime: number = 0;
|
||||
public attackCooldown: number = 1.5;
|
||||
public color: string = 'white';
|
||||
|
||||
// 体力系统属性
|
||||
public maxStamina: number = 100;
|
||||
public currentStamina: number = 100;
|
||||
public homePosition: Vec3 = Vec3.ZERO.clone();
|
||||
public staminaRecoveryRate: number = 20; // 每秒恢复的体力
|
||||
public staminaCostPerMining: number = 15; // 每次挖矿消耗的体力
|
||||
|
||||
// 移动状态管理
|
||||
private isMoving: boolean = false;
|
||||
private moveStartTime: number = 0;
|
||||
private lastTargetUpdateTime: number = 0;
|
||||
|
||||
private behaviorTreeManager: BehaviorTreeManager | null = null;
|
||||
private behaviorHandler: Component | null = null;
|
||||
private meshRenderer: MeshRenderer | null = null;
|
||||
|
||||
onLoad() {
|
||||
this.meshRenderer = this.getComponent(MeshRenderer);
|
||||
|
||||
// 创建行为树管理器
|
||||
this.behaviorTreeManager = this.addComponent(BehaviorTreeManager);
|
||||
|
||||
// 添加RTS行为处理器
|
||||
try {
|
||||
// 添加RTSBehaviorHandler组件
|
||||
this.behaviorHandler = this.addComponent(RTSBehaviorHandler);
|
||||
} catch (error) {
|
||||
console.warn('RTSBehaviorHandler组件添加失败', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置单位配置
|
||||
*/
|
||||
setup(config: UnitConfig) {
|
||||
this.unitType = config.unitType;
|
||||
this.maxHealth = config.maxHealth;
|
||||
this.currentHealth = config.maxHealth;
|
||||
this.moveSpeed = config.moveSpeed;
|
||||
this.attackRange = config.attackRange;
|
||||
this.attackDamage = config.attackDamage;
|
||||
this.color = config.color;
|
||||
|
||||
// 设置材质颜色
|
||||
this.setUnitColor(config.color);
|
||||
|
||||
// 设置节点名称显示单位类型
|
||||
this.node.name = `${config.unitType.toUpperCase()}_${this.node.name}`;
|
||||
|
||||
// 初始化行为树
|
||||
if (this.behaviorTreeManager) {
|
||||
this.behaviorTreeManager.initializeBehaviorTree(config.behaviorTreeName, this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置单位颜色
|
||||
*/
|
||||
private setUnitColor(colorName: string) {
|
||||
if (!this.meshRenderer || !this.meshRenderer.material) return;
|
||||
|
||||
const colorMap: { [key: string]: Color } = {
|
||||
'red': Color.RED,
|
||||
'green': Color.GREEN,
|
||||
'blue': Color.BLUE,
|
||||
'yellow': Color.YELLOW,
|
||||
'white': Color.WHITE,
|
||||
'cyan': Color.CYAN,
|
||||
'magenta': Color.MAGENTA
|
||||
};
|
||||
|
||||
const color = colorMap[colorName] || Color.WHITE;
|
||||
this.meshRenderer.material.setProperty('mainColor', color);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置选择状态
|
||||
*/
|
||||
setSelected(selected: boolean) {
|
||||
this.isSelected = selected;
|
||||
|
||||
// 视觉效果
|
||||
if (selected) {
|
||||
this.showSelectionEffect();
|
||||
} else {
|
||||
this.hideSelectionEffect();
|
||||
}
|
||||
|
||||
// 更新行为树黑板
|
||||
if (this.behaviorTreeManager) {
|
||||
this.behaviorTreeManager.updateBlackboardValue('isSelected', selected);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示选择效果
|
||||
*/
|
||||
private showSelectionEffect() {
|
||||
// 添加选择圈效果
|
||||
tween(this.node)
|
||||
.to(0.3, { scale: new Vec3(1.1, 1.1, 1.1) })
|
||||
.to(0.3, { scale: Vec3.ONE })
|
||||
.union()
|
||||
.repeatForever()
|
||||
.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏选择效果
|
||||
*/
|
||||
private hideSelectionEffect() {
|
||||
// 停止所有缩放动画
|
||||
tween(this.node).stop();
|
||||
this.node.setScale(Vec3.ONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布命令
|
||||
*/
|
||||
issueCommand(command: string, target?: Vec3 | Node) {
|
||||
this.currentCommand = command;
|
||||
|
||||
// 设置目标
|
||||
if (target instanceof Vec3) {
|
||||
this.targetPosition = target.clone();
|
||||
this.targetNode = null;
|
||||
} else if (target instanceof Node) {
|
||||
this.targetPosition = target.worldPosition.clone();
|
||||
this.targetNode = target;
|
||||
}
|
||||
|
||||
// 更新行为树黑板
|
||||
if (this.behaviorTreeManager) {
|
||||
this.behaviorTreeManager.updateBlackboardValue('currentCommand', command);
|
||||
this.behaviorTreeManager.updateBlackboardValue('hasTarget', target !== undefined);
|
||||
this.behaviorTreeManager.updateBlackboardValue('targetPosition', this.targetPosition);
|
||||
|
||||
if (target instanceof Node) {
|
||||
this.behaviorTreeManager.updateBlackboardValue('targetType',
|
||||
target.name.includes('Resource') ? 'resource' :
|
||||
target.name.includes('Building') ? 'building' : 'unit');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置黑板变量值
|
||||
*/
|
||||
setBlackboardValue(key: string, value: any) {
|
||||
if (this.behaviorTreeManager) {
|
||||
this.behaviorTreeManager.updateBlackboardValue(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取黑板变量值
|
||||
*/
|
||||
getBlackboardValue(key: string): any {
|
||||
return this.behaviorTreeManager?.getBlackboardValue(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置移动目标
|
||||
*/
|
||||
setTarget(position: Vec3) {
|
||||
this.targetPosition = position.clone();
|
||||
this.isMoving = true;
|
||||
this.moveStartTime = Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除移动目标
|
||||
*/
|
||||
clearTarget() {
|
||||
this.targetPosition = Vec3.ZERO.clone();
|
||||
this.isMoving = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 受到伤害
|
||||
*/
|
||||
takeDamage(damage: number) {
|
||||
this.currentHealth = Math.max(0, this.currentHealth - damage);
|
||||
|
||||
// 更新行为树黑板
|
||||
if (this.behaviorTreeManager) {
|
||||
this.behaviorTreeManager.updateBlackboardValue('currentHealth', this.currentHealth);
|
||||
this.behaviorTreeManager.updateBlackboardValue('healthPercentage', this.currentHealth / this.maxHealth);
|
||||
this.behaviorTreeManager.updateBlackboardValue('isLowHealth', this.currentHealth < this.maxHealth * 0.3);
|
||||
}
|
||||
|
||||
// 视觉效果
|
||||
this.showDamageEffect();
|
||||
|
||||
if (this.currentHealth <= 0) {
|
||||
this.die();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示受伤效果
|
||||
*/
|
||||
private showDamageEffect() {
|
||||
if (!this.meshRenderer || !this.meshRenderer.material) return;
|
||||
|
||||
// 闪红效果
|
||||
const originalColor = this.meshRenderer.material.getProperty('mainColor') as Color;
|
||||
this.meshRenderer.material.setProperty('mainColor', Color.RED);
|
||||
|
||||
this.scheduleOnce(() => {
|
||||
if (this.meshRenderer && this.meshRenderer.material) {
|
||||
this.meshRenderer.material.setProperty('mainColor', originalColor);
|
||||
}
|
||||
}, 0.2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 单位死亡
|
||||
*/
|
||||
private die() {
|
||||
console.log(`单位 ${this.node.name} 死亡`);
|
||||
|
||||
// 播放死亡动画后销毁节点
|
||||
tween(this.node)
|
||||
.to(0.5, { scale: Vec3.ZERO })
|
||||
.call(() => {
|
||||
this.node.destroy();
|
||||
})
|
||||
.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动到目标位置(只在水平面移动,不改变Y轴)
|
||||
*/
|
||||
moveToTarget(targetPos: Vec3, speed?: number, deltaTime?: number): boolean {
|
||||
const currentPos = this.node.worldPosition;
|
||||
const distance = Vec3.distance(currentPos, targetPos);
|
||||
|
||||
if (distance < 0.5) {
|
||||
this.isMoving = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
const actualSpeed = speed || this.moveSpeed;
|
||||
const actualDeltaTime = deltaTime || 0.016;
|
||||
const direction = new Vec3();
|
||||
Vec3.subtract(direction, targetPos, currentPos);
|
||||
direction.normalize();
|
||||
|
||||
const moveDistance = actualSpeed * actualDeltaTime;
|
||||
const newPosition = new Vec3();
|
||||
Vec3.scaleAndAdd(newPosition, currentPos, direction, moveDistance);
|
||||
|
||||
this.node.setWorldPosition(newPosition);
|
||||
this.isMoving = true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 攻击目标
|
||||
*/
|
||||
attackTarget(): boolean {
|
||||
const currentTime = Date.now();
|
||||
if (currentTime - this.lastAttackTime < this.attackCooldown * 1000) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.targetNode && this.targetNode.isValid) {
|
||||
const distance = Vec3.distance(this.node.worldPosition, this.targetNode.worldPosition);
|
||||
if (distance <= this.attackRange) {
|
||||
this.lastAttackTime = currentTime;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
update(deltaTime: number) {
|
||||
if (this.behaviorTreeManager) {
|
||||
this.behaviorTreeManager.update(deltaTime);
|
||||
}
|
||||
|
||||
if (this.isMoving && !this.targetPosition.equals(Vec3.ZERO)) {
|
||||
const reached = this.moveToTarget(this.targetPosition, this.moveSpeed, deltaTime);
|
||||
if (reached) {
|
||||
this.clearTarget();
|
||||
}
|
||||
}
|
||||
|
||||
// 调试信息显示
|
||||
if (this.showDebugInfo) {
|
||||
this.updateDebugInfo();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新调试信息
|
||||
*/
|
||||
private updateDebugInfo() {
|
||||
// 可以在这里添加调试信息的显示逻辑
|
||||
// 比如在单位上方显示状态文本等
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
// 停止所有动画
|
||||
tween(this.node).stop();
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "4ac64480-2d09-4de6-a22c-add022790676",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "a1f43720-46e1-4d07-b56a-c9307e45726c",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import { Component, _decorator } from 'cc';
|
||||
import { GameScene } from './scenes/GameScene';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/**
|
||||
* ECS管理器 - Cocos Creator组件
|
||||
* 将此组件添加到场景中的任意节点上即可启动ECS框架
|
||||
*
|
||||
* 使用说明:
|
||||
* 1. 在Cocos Creator场景中创建一个空节点
|
||||
* 2. 将此ECSManager组件添加到该节点
|
||||
* 3. 运行场景即可自动启动ECS框架
|
||||
*/
|
||||
@ccclass('ECSManager')
|
||||
export class ECSManager extends Component {
|
||||
|
||||
@property({
|
||||
tooltip: '是否启用调试模式(建议开发阶段开启)'
|
||||
})
|
||||
public debugMode: boolean = true;
|
||||
|
||||
private isInitialized: boolean = false;
|
||||
|
||||
/**
|
||||
* 组件启动时初始化ECS
|
||||
*/
|
||||
start() {
|
||||
this.initializeECS();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化ECS框架
|
||||
*/
|
||||
private initializeECS(): void {
|
||||
if (this.isInitialized) return;
|
||||
|
||||
// ECS框架初始化开始
|
||||
|
||||
try {
|
||||
// 1. 创建Core实例,启用调试功能
|
||||
if (this.debugMode) {
|
||||
Core.create({
|
||||
debug: true,
|
||||
enableEntitySystems: true,
|
||||
debugConfig: {
|
||||
enabled: true,
|
||||
websocketUrl: 'ws://localhost:8080',
|
||||
autoReconnect: true,
|
||||
debugFrameRate: 30,
|
||||
channels: {
|
||||
entities: true,
|
||||
systems: true,
|
||||
performance: true,
|
||||
components: true,
|
||||
scenes: true
|
||||
}
|
||||
}
|
||||
});
|
||||
console.log('✅ ECS调试模式已启用');
|
||||
} else {
|
||||
Core.create({
|
||||
debug: false,
|
||||
enableEntitySystems: true
|
||||
});
|
||||
console.log('ℹ️ ECS调试模式已禁用');
|
||||
}
|
||||
|
||||
// 2. 创建游戏场景
|
||||
const gameScene = new GameScene();
|
||||
|
||||
// 3. 设置为当前场景(会自动调用scene.begin())
|
||||
Core.setScene(gameScene);
|
||||
|
||||
this.isInitialized = true;
|
||||
// ECS框架初始化完成
|
||||
|
||||
} catch (error) {
|
||||
console.error('ECS框架初始化失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 每帧更新ECS框架
|
||||
*/
|
||||
update(deltaTime: number) {
|
||||
if (this.isInitialized) {
|
||||
// 更新ECS核心系统
|
||||
Core.update(deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件销毁时清理ECS
|
||||
*/
|
||||
onDestroy() {
|
||||
if (this.isInitialized) {
|
||||
// ECS框架清理
|
||||
this.isInitialized = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "b89656f0-6320-4b6d-81cd-447bf811230c",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
# ECS框架启动模板
|
||||
|
||||
欢迎使用ECS框架!这是一个最基础的启动模板,帮助您快速开始ECS项目开发。
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
```
|
||||
ecs/
|
||||
├── components/ # 组件目录(请在此添加您的组件)
|
||||
├── systems/ # 系统目录(请在此添加您的系统)
|
||||
├── scenes/ # 场景目录
|
||||
│ └── GameScene.ts # 主游戏场景
|
||||
├── ECSManager.ts # ECS管理器组件
|
||||
└── README.md # 本文档
|
||||
```
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 启动ECS框架
|
||||
|
||||
ECS框架已经配置完成!您只需要:
|
||||
|
||||
1. 在Cocos Creator中打开您的场景
|
||||
2. 创建一个空节点(例如命名为"ECSManager")
|
||||
3. 将 `ECSManager` 组件添加到该节点
|
||||
4. 运行场景,ECS框架将自动启动
|
||||
|
||||
### 2. 查看控制台输出
|
||||
|
||||
如果一切正常,您将在控制台看到:
|
||||
|
||||
```
|
||||
🎮 正在初始化ECS框架...
|
||||
🔧 ECS调试模式已启用,可在Cocos Creator扩展面板中查看调试信息
|
||||
🎯 游戏场景已创建
|
||||
✅ ECS框架初始化成功!
|
||||
🚀 游戏场景已启动
|
||||
```
|
||||
|
||||
### 3. 使用调试面板
|
||||
|
||||
ECS框架已启用调试功能,您可以:
|
||||
|
||||
1. 在Cocos Creator编辑器菜单中选择 "扩展" → "ECS Framework" → "调试面板"
|
||||
2. 调试面板将显示实时的ECS运行状态:
|
||||
- 实体数量和状态
|
||||
- 系统执行信息
|
||||
- 性能监控数据
|
||||
- 组件统计信息
|
||||
|
||||
**注意**:调试功能会消耗一定性能,正式发布时建议关闭调试模式。
|
||||
|
||||
## 📚 下一步开发
|
||||
|
||||
### 创建您的第一个组件
|
||||
|
||||
在 `components/` 目录下创建组件:
|
||||
|
||||
```typescript
|
||||
// components/PositionComponent.ts
|
||||
import { Component } from '@esengine/ecs-framework';
|
||||
import { Vec3 } from 'cc';
|
||||
|
||||
export class PositionComponent extends Component {
|
||||
public position: Vec3 = new Vec3();
|
||||
|
||||
constructor(x: number = 0, y: number = 0, z: number = 0) {
|
||||
super();
|
||||
this.position.set(x, y, z);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 创建您的第一个系统
|
||||
|
||||
在 `systems/` 目录下创建系统:
|
||||
|
||||
```typescript
|
||||
// systems/MovementSystem.ts
|
||||
import { EntitySystem, Entity, Matcher } from '@esengine/ecs-framework';
|
||||
import { PositionComponent } from '../components/PositionComponent';
|
||||
|
||||
export class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(PositionComponent));
|
||||
}
|
||||
|
||||
protected process(entities: Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const position = entity.getComponent(PositionComponent);
|
||||
if (position) {
|
||||
// TODO: 在这里编写移动逻辑
|
||||
console.log(`实体 ${entity.name} 位置: ${position.position}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 在场景中注册系统
|
||||
|
||||
在 `scenes/GameScene.ts` 的 `initialize()` 方法中添加:
|
||||
|
||||
```typescript
|
||||
import { MovementSystem } from '../systems/MovementSystem';
|
||||
|
||||
public initialize(): void {
|
||||
super.initialize();
|
||||
this.name = "MainGameScene";
|
||||
|
||||
// 添加系统
|
||||
this.addEntityProcessor(new MovementSystem());
|
||||
|
||||
// 创建测试实体
|
||||
const testEntity = this.createEntity("TestEntity");
|
||||
testEntity.addComponent(new PositionComponent(0, 0, 0));
|
||||
}
|
||||
```
|
||||
|
||||
## 🔗 学习资源
|
||||
|
||||
- [ECS框架完整文档](https://github.com/esengine/ecs-framework)
|
||||
- [ECS概念详解](https://github.com/esengine/ecs-framework/blob/master/docs/concepts-explained.md)
|
||||
- [新手教程](https://github.com/esengine/ecs-framework/blob/master/docs/beginner-tutorials.md)
|
||||
- [组件设计指南](https://github.com/esengine/ecs-framework/blob/master/docs/component-design-guide.md)
|
||||
- [系统开发指南](https://github.com/esengine/ecs-framework/blob/master/docs/system-guide.md)
|
||||
|
||||
## 💡 开发提示
|
||||
|
||||
1. **组件只存储数据**:避免在组件中编写复杂逻辑
|
||||
2. **系统处理逻辑**:所有业务逻辑应该在系统中实现
|
||||
3. **使用Matcher过滤实体**:系统通过Matcher指定需要处理的实体类型
|
||||
4. **性能优化**:大量实体时考虑使用位掩码查询和组件索引
|
||||
|
||||
## ❓ 常见问题
|
||||
|
||||
### Q: 如何创建实体?
|
||||
A: 在场景中使用 `this.createEntity("实体名称")`
|
||||
|
||||
### Q: 如何给实体添加组件?
|
||||
A: 使用 `entity.addComponent(new YourComponent())`
|
||||
|
||||
### Q: 如何获取实体的组件?
|
||||
A: 使用 `entity.getComponent(YourComponent)`
|
||||
|
||||
### Q: 如何删除实体?
|
||||
A: 使用 `entity.destroy()` 或 `this.destroyEntity(entity)`
|
||||
|
||||
---
|
||||
|
||||
🎮 **开始您的ECS开发之旅吧!**
|
||||
|
||||
如有问题,请查阅官方文档或提交Issue。
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"ver": "1.0.1",
|
||||
"importer": "text",
|
||||
"imported": true,
|
||||
"uuid": "ca94b460-6c6a-4f72-9ec1-ab5fcd2e0e0a",
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "3c7bd2b3-6781-482c-be41-21f3dde0e2ab",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -1,328 +0,0 @@
|
||||
import { Component } from '@esengine/ecs-framework';
|
||||
import { Vec3 } from 'cc';
|
||||
|
||||
/**
|
||||
* AI组件 - 复杂的AI行为和状态管理
|
||||
*/
|
||||
export class AIComponent extends Component {
|
||||
/** AI状态 */
|
||||
public currentState: 'idle' | 'patrol' | 'chase' | 'attack' | 'flee' | 'dead' = 'idle';
|
||||
|
||||
/** 目标ID(避免循环引用) */
|
||||
public targetId: number | null = null;
|
||||
|
||||
/** AI伙伴ID列表(避免循环引用) */
|
||||
public allyIds: number[] = [];
|
||||
|
||||
/** 敌人ID列表 */
|
||||
public enemyIds: number[] = [];
|
||||
|
||||
/** 复杂的AI配置 */
|
||||
public config: {
|
||||
personality: {
|
||||
aggression: number; // 攻击性 0-1
|
||||
curiosity: number; // 好奇心 0-1
|
||||
loyalty: number; // 忠诚度 0-1
|
||||
intelligence: number; // 智力 0-1
|
||||
};
|
||||
capabilities: {
|
||||
sightRange: number;
|
||||
hearingRange: number;
|
||||
movementSpeed: number;
|
||||
attackDamage: number;
|
||||
health: number;
|
||||
};
|
||||
behaviorTree: {
|
||||
rootNode: BehaviorNode;
|
||||
blackboard: Map<string, any>;
|
||||
executionHistory: BehaviorExecution[];
|
||||
};
|
||||
memory: {
|
||||
lastSeenEnemyPosition: Vec3 | null;
|
||||
lastSeenEnemyTime: number;
|
||||
knownLocations: Array<{
|
||||
position: Vec3;
|
||||
type: 'safe' | 'danger' | 'resource' | 'patrol';
|
||||
confidence: number;
|
||||
lastVisited: number;
|
||||
}>;
|
||||
relationships: Map<number, {
|
||||
entityId: number;
|
||||
relationship: 'ally' | 'enemy' | 'neutral';
|
||||
trustLevel: number;
|
||||
lastInteraction: number;
|
||||
interactionHistory: Array<{
|
||||
type: 'friendly' | 'hostile' | 'neutral';
|
||||
timestamp: number;
|
||||
impact: number;
|
||||
}>;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
|
||||
/** 状态机 */
|
||||
public stateMachine: {
|
||||
states: Map<string, AIState>;
|
||||
transitions: Map<string, Array<{
|
||||
targetState: string;
|
||||
condition: () => boolean;
|
||||
priority: number;
|
||||
}>>;
|
||||
stateHistory: Array<{
|
||||
state: string;
|
||||
enterTime: number;
|
||||
exitTime: number;
|
||||
data: any;
|
||||
}>;
|
||||
};
|
||||
|
||||
/** 感知系统 */
|
||||
public perception: {
|
||||
visibleEntities: Array<{
|
||||
entityId: number;
|
||||
position: Vec3;
|
||||
distance: number;
|
||||
angle: number;
|
||||
lastSeen: number;
|
||||
componentId?: number; // 使用组件ID避免循环引用
|
||||
}>;
|
||||
audibleSounds: Array<{
|
||||
source: Vec3;
|
||||
volume: number;
|
||||
type: string;
|
||||
timestamp: number;
|
||||
}>;
|
||||
tacticleInfo: Array<{
|
||||
entityId: number;
|
||||
contactPoint: Vec3;
|
||||
force: number;
|
||||
timestamp: number;
|
||||
}>;
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// 初始化AI配置
|
||||
this.config = {
|
||||
personality: {
|
||||
aggression: Math.random(),
|
||||
curiosity: Math.random(),
|
||||
loyalty: Math.random(),
|
||||
intelligence: Math.random()
|
||||
},
|
||||
capabilities: {
|
||||
sightRange: 100 + Math.random() * 100,
|
||||
hearingRange: 50 + Math.random() * 50,
|
||||
movementSpeed: 80 + Math.random() * 40,
|
||||
attackDamage: 10 + Math.random() * 20,
|
||||
health: 80 + Math.random() * 40
|
||||
},
|
||||
behaviorTree: {
|
||||
rootNode: new BehaviorNode('root'),
|
||||
blackboard: new Map(),
|
||||
executionHistory: []
|
||||
},
|
||||
memory: {
|
||||
lastSeenEnemyPosition: null,
|
||||
lastSeenEnemyTime: 0,
|
||||
knownLocations: [],
|
||||
relationships: new Map()
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化状态机
|
||||
this.stateMachine = {
|
||||
states: new Map(),
|
||||
transitions: new Map(),
|
||||
stateHistory: []
|
||||
};
|
||||
|
||||
// 初始化感知系统
|
||||
this.perception = {
|
||||
visibleEntities: [],
|
||||
audibleSounds: [],
|
||||
tacticleInfo: []
|
||||
};
|
||||
|
||||
this.initializeStateMachine();
|
||||
this.initializeBehaviorTree();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化状态机
|
||||
*/
|
||||
private initializeStateMachine(): void {
|
||||
// 添加基本状态
|
||||
this.stateMachine.states.set('idle', new AIState('idle', this));
|
||||
this.stateMachine.states.set('patrol', new AIState('patrol', this));
|
||||
this.stateMachine.states.set('chase', new AIState('chase', this));
|
||||
this.stateMachine.states.set('attack', new AIState('attack', this));
|
||||
this.stateMachine.states.set('flee', new AIState('flee', this));
|
||||
|
||||
// 设置状态转换
|
||||
this.stateMachine.transitions.set('idle', [
|
||||
{ targetState: 'patrol', condition: () => Math.random() > 0.8, priority: 1 },
|
||||
{ targetState: 'chase', condition: () => this.perception.visibleEntities.length > 0, priority: 3 }
|
||||
]);
|
||||
|
||||
this.stateMachine.transitions.set('patrol', [
|
||||
{ targetState: 'idle', condition: () => Math.random() > 0.9, priority: 1 },
|
||||
{ targetState: 'chase', condition: () => this.hasVisibleEnemies(), priority: 3 }
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化行为树
|
||||
*/
|
||||
private initializeBehaviorTree(): void {
|
||||
const root = this.config.behaviorTree.rootNode;
|
||||
|
||||
// 构建简单的行为树结构
|
||||
const selectorNode = new BehaviorNode('selector');
|
||||
const sequenceNode = new BehaviorNode('sequence');
|
||||
const conditionNode = new BehaviorNode('condition');
|
||||
const actionNode = new BehaviorNode('action');
|
||||
|
||||
root.addChild(selectorNode);
|
||||
selectorNode.addChild(sequenceNode);
|
||||
sequenceNode.addChild(conditionNode);
|
||||
sequenceNode.addChild(actionNode);
|
||||
|
||||
// 设置黑板数据
|
||||
this.config.behaviorTree.blackboard.set('lastPatrolPoint', new Vec3());
|
||||
this.config.behaviorTree.blackboard.set('alertLevel', 0);
|
||||
this.config.behaviorTree.blackboard.set('energy', 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加盟友(避免循环引用)
|
||||
*/
|
||||
public addAlly(allyEntityId: number): void {
|
||||
if (!this.allyIds.includes(allyEntityId)) {
|
||||
this.allyIds.push(allyEntityId);
|
||||
|
||||
// 更新关系记录
|
||||
this.config.memory.relationships.set(allyEntityId, {
|
||||
entityId: allyEntityId,
|
||||
relationship: 'ally',
|
||||
trustLevel: 0.8,
|
||||
lastInteraction: Date.now(),
|
||||
interactionHistory: []
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置目标(避免循环引用)
|
||||
*/
|
||||
public setTarget(targetEntityId: number): void {
|
||||
this.targetId = targetEntityId;
|
||||
// 不再需要双向引用
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新感知信息
|
||||
*/
|
||||
public updatePerception(deltaTime: number): void {
|
||||
// 清理过期的感知信息
|
||||
const currentTime = Date.now();
|
||||
this.perception.visibleEntities = this.perception.visibleEntities.filter(
|
||||
entity => currentTime - entity.lastSeen < 5000
|
||||
);
|
||||
|
||||
this.perception.audibleSounds = this.perception.audibleSounds.filter(
|
||||
sound => currentTime - sound.timestamp < 2000
|
||||
);
|
||||
|
||||
// 更新记忆中的位置信息
|
||||
this.config.memory.knownLocations.forEach(location => {
|
||||
location.confidence *= 0.999; // 随时间衰减可信度
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有可见敌人
|
||||
*/
|
||||
private hasVisibleEnemies(): boolean {
|
||||
return this.perception.visibleEntities.some(entity =>
|
||||
this.config.memory.relationships.get(entity.entityId)?.relationship === 'enemy'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置组件
|
||||
*/
|
||||
public reset(): void {
|
||||
// 清理ID数组(不再需要处理循环引用)
|
||||
this.allyIds = [];
|
||||
this.enemyIds = [];
|
||||
this.targetId = null;
|
||||
this.currentState = 'idle';
|
||||
|
||||
this.config.behaviorTree.blackboard.clear();
|
||||
this.config.memory.relationships.clear();
|
||||
this.perception.visibleEntities = [];
|
||||
this.perception.audibleSounds = [];
|
||||
this.perception.tacticleInfo = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 行为树节点
|
||||
*/
|
||||
class BehaviorNode {
|
||||
public name: string;
|
||||
public children: BehaviorNode[] = [];
|
||||
public parent: BehaviorNode | null = null;
|
||||
public data: Map<string, any> = new Map();
|
||||
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public addChild(child: BehaviorNode): void {
|
||||
this.children.push(child);
|
||||
child.parent = this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 行为执行记录
|
||||
*/
|
||||
interface BehaviorExecution {
|
||||
nodeName: string;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
result: 'success' | 'failure' | 'running';
|
||||
data: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* AI状态
|
||||
*/
|
||||
class AIState {
|
||||
public name: string;
|
||||
public owner: AIComponent;
|
||||
public enterTime: number = 0;
|
||||
public data: Map<string, any> = new Map();
|
||||
|
||||
constructor(name: string, owner: AIComponent) {
|
||||
this.name = name;
|
||||
this.owner = owner;
|
||||
}
|
||||
|
||||
public enter(): void {
|
||||
this.enterTime = Date.now();
|
||||
}
|
||||
|
||||
public exit(): void {
|
||||
// 记录状态历史
|
||||
this.owner.stateMachine.stateHistory.push({
|
||||
state: this.name,
|
||||
enterTime: this.enterTime,
|
||||
exitTime: Date.now(),
|
||||
data: Object.fromEntries(this.data)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "cc0d3d0d-0c12-4007-8568-11b2cafdfb8f",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
import { Component } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* 生命值组件
|
||||
* 管理实体的生命值、最大生命值等
|
||||
*/
|
||||
export class Health extends Component {
|
||||
/** 当前生命值 */
|
||||
public currentHealth: number = 100;
|
||||
|
||||
/** 最大生命值 */
|
||||
public maxHealth: number = 100;
|
||||
|
||||
/** 是否死亡 */
|
||||
public isDead: boolean = false;
|
||||
|
||||
/** 生命值回复速度 (每秒) */
|
||||
public regenRate: number = 0;
|
||||
|
||||
constructor(maxHealth: number = 100) {
|
||||
super();
|
||||
this.maxHealth = maxHealth;
|
||||
this.currentHealth = maxHealth;
|
||||
}
|
||||
|
||||
/**
|
||||
* 受到伤害
|
||||
*/
|
||||
public takeDamage(damage: number): void {
|
||||
this.currentHealth = Math.max(0, this.currentHealth - damage);
|
||||
this.isDead = this.currentHealth <= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 治疗
|
||||
*/
|
||||
public heal(amount: number): void {
|
||||
if (!this.isDead) {
|
||||
this.currentHealth = Math.min(this.maxHealth, this.currentHealth + amount);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 复活
|
||||
*/
|
||||
public revive(healthPercent: number = 1.0): void {
|
||||
this.isDead = false;
|
||||
this.currentHealth = Math.floor(this.maxHealth * Math.max(0, Math.min(1, healthPercent)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取生命值百分比
|
||||
*/
|
||||
public getHealthPercent(): number {
|
||||
return this.maxHealth > 0 ? this.currentHealth / this.maxHealth : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置组件
|
||||
*/
|
||||
public reset(): void {
|
||||
this.currentHealth = this.maxHealth;
|
||||
this.isDead = false;
|
||||
this.regenRate = 0;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "90369635-a6cb-4313-adf1-64117b50f2bc",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -1,413 +0,0 @@
|
||||
import { Component } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* 网络组件 - 模拟复杂的网络连接和数据同步(已移除循环引用)
|
||||
*/
|
||||
export class NetworkComponent extends Component {
|
||||
/** 网络ID */
|
||||
public networkId: string = '';
|
||||
|
||||
/** 连接状态 */
|
||||
public connectionState: 'disconnected' | 'connecting' | 'connected' | 'error' = 'disconnected';
|
||||
|
||||
/** 网络连接信息 */
|
||||
public connection: {
|
||||
sessionId: string;
|
||||
serverId: string;
|
||||
roomId: string;
|
||||
playerId: string;
|
||||
ping: number;
|
||||
packetLoss: number;
|
||||
bandwidth: number;
|
||||
lastHeartbeat: number;
|
||||
};
|
||||
|
||||
/** 同步数据 */
|
||||
public syncData: {
|
||||
dirtyFlags: Set<string>;
|
||||
lastSyncTime: number;
|
||||
syncHistory: Array<{
|
||||
timestamp: number;
|
||||
dataSize: number;
|
||||
properties: string[];
|
||||
success: boolean;
|
||||
}>;
|
||||
queuedUpdates: Array<{
|
||||
property: string;
|
||||
value: any;
|
||||
timestamp: number;
|
||||
priority: number;
|
||||
}>;
|
||||
};
|
||||
|
||||
/** 网络统计 */
|
||||
public networkStats: {
|
||||
totalBytesSent: number;
|
||||
totalBytesReceived: number;
|
||||
packetsPerSecond: number;
|
||||
averageLatency: number;
|
||||
latencyHistory: number[];
|
||||
connectionQuality: 'excellent' | 'good' | 'fair' | 'poor';
|
||||
errorCount: number;
|
||||
reconnectCount: number;
|
||||
lastErrorTime: number;
|
||||
errorLog: Array<{
|
||||
timestamp: number;
|
||||
errorType: string;
|
||||
message: string;
|
||||
stack?: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
/** 连接的玩家ID列表(避免循环引用) */
|
||||
public connectedPlayerIds: Set<string> = new Set();
|
||||
|
||||
/** 群组成员ID(避免循环引用) */
|
||||
public groupMemberIds: string[] = [];
|
||||
|
||||
/** 群组领导者ID(避免循环引用) */
|
||||
public groupLeaderId: string | null = null;
|
||||
|
||||
/** 复杂的网络配置 */
|
||||
public config: {
|
||||
autoReconnect: boolean;
|
||||
maxReconnectAttempts: number;
|
||||
heartbeatInterval: number;
|
||||
syncFrequency: number;
|
||||
compressionEnabled: boolean;
|
||||
encryptionEnabled: boolean;
|
||||
priorityLevels: Map<string, number>;
|
||||
filters: Array<{
|
||||
property: string;
|
||||
condition: (value: any) => boolean;
|
||||
action: 'allow' | 'deny' | 'transform';
|
||||
transformer?: (value: any) => any;
|
||||
}>;
|
||||
bufferSettings: {
|
||||
maxBufferSize: number;
|
||||
flushInterval: number;
|
||||
compressionThreshold: number;
|
||||
};
|
||||
};
|
||||
|
||||
/** 消息队列 */
|
||||
public messageQueue: {
|
||||
incoming: Array<{
|
||||
senderId: string;
|
||||
messageType: string;
|
||||
data: any;
|
||||
timestamp: number;
|
||||
processed: boolean;
|
||||
}>;
|
||||
outgoing: Array<{
|
||||
targetId: string;
|
||||
messageType: string;
|
||||
data: any;
|
||||
priority: number;
|
||||
attempts: number;
|
||||
maxAttempts: number;
|
||||
}>;
|
||||
processing: Map<string, {
|
||||
messageId: string;
|
||||
startTime: number;
|
||||
expectedDuration: number;
|
||||
status: 'processing' | 'completed' | 'failed';
|
||||
}>;
|
||||
};
|
||||
|
||||
/** 复杂的网络缓存系统 */
|
||||
public cacheSystem: {
|
||||
playerCache: Map<string, {
|
||||
playerId: string;
|
||||
lastSeen: number;
|
||||
cachedData: any;
|
||||
cacheExpiry: number;
|
||||
}>;
|
||||
messageCache: Map<string, {
|
||||
messageId: string;
|
||||
content: any;
|
||||
timestamp: number;
|
||||
recipients: string[];
|
||||
}>;
|
||||
syncCache: Map<string, {
|
||||
propertyPath: string;
|
||||
value: any;
|
||||
lastUpdated: number;
|
||||
version: number;
|
||||
}>;
|
||||
};
|
||||
|
||||
constructor(networkId: string = '') {
|
||||
super();
|
||||
|
||||
this.networkId = networkId || this.generateNetworkId();
|
||||
|
||||
this.connection = {
|
||||
sessionId: '',
|
||||
serverId: '',
|
||||
roomId: '',
|
||||
playerId: '',
|
||||
ping: 0,
|
||||
packetLoss: 0,
|
||||
bandwidth: 0,
|
||||
lastHeartbeat: 0
|
||||
};
|
||||
|
||||
this.syncData = {
|
||||
dirtyFlags: new Set(),
|
||||
lastSyncTime: 0,
|
||||
syncHistory: [],
|
||||
queuedUpdates: []
|
||||
};
|
||||
|
||||
this.networkStats = {
|
||||
totalBytesSent: 0,
|
||||
totalBytesReceived: 0,
|
||||
packetsPerSecond: 0,
|
||||
averageLatency: 0,
|
||||
latencyHistory: [],
|
||||
connectionQuality: 'excellent',
|
||||
errorCount: 0,
|
||||
reconnectCount: 0,
|
||||
lastErrorTime: 0,
|
||||
errorLog: []
|
||||
};
|
||||
|
||||
this.config = {
|
||||
autoReconnect: true,
|
||||
maxReconnectAttempts: 5,
|
||||
heartbeatInterval: 1000,
|
||||
syncFrequency: 60,
|
||||
compressionEnabled: true,
|
||||
encryptionEnabled: false,
|
||||
priorityLevels: new Map([
|
||||
['critical', 10],
|
||||
['high', 7],
|
||||
['medium', 5],
|
||||
['low', 2]
|
||||
]),
|
||||
filters: [],
|
||||
bufferSettings: {
|
||||
maxBufferSize: 1024 * 1024, // 1MB
|
||||
flushInterval: 100,
|
||||
compressionThreshold: 1024
|
||||
}
|
||||
};
|
||||
|
||||
this.messageQueue = {
|
||||
incoming: [],
|
||||
outgoing: [],
|
||||
processing: new Map()
|
||||
};
|
||||
|
||||
this.cacheSystem = {
|
||||
playerCache: new Map(),
|
||||
messageCache: new Map(),
|
||||
syncCache: new Map()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成网络ID
|
||||
*/
|
||||
private generateNetworkId(): string {
|
||||
return 'net_' + Math.random().toString(36).substring(2, 15) +
|
||||
Math.random().toString(36).substring(2, 15);
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接到其他网络组件(避免循环引用)
|
||||
*/
|
||||
public connectToPlayer(playerNetworkId: string): void {
|
||||
if (!this.connectedPlayerIds.has(playerNetworkId)) {
|
||||
this.connectedPlayerIds.add(playerNetworkId);
|
||||
|
||||
// 记录连接事件
|
||||
this.logNetworkEvent('player_connected', {
|
||||
playerId: playerNetworkId,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加入群组(避免循环引用)
|
||||
*/
|
||||
public joinGroup(memberIds: string[], leaderId?: string): void {
|
||||
this.groupMemberIds = [...memberIds];
|
||||
this.groupLeaderId = leaderId || null;
|
||||
|
||||
// 更新缓存
|
||||
memberIds.forEach(memberId => {
|
||||
this.cacheSystem.playerCache.set(memberId, {
|
||||
playerId: memberId,
|
||||
lastSeen: Date.now(),
|
||||
cachedData: {},
|
||||
cacheExpiry: Date.now() + 300000 // 5分钟缓存
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
*/
|
||||
public sendMessage(targetId: string, messageType: string, data: any, priority: number = 5): void {
|
||||
const message = {
|
||||
targetId,
|
||||
messageType,
|
||||
data: this.processOutgoingData(data),
|
||||
priority,
|
||||
attempts: 0,
|
||||
maxAttempts: 3
|
||||
};
|
||||
|
||||
this.messageQueue.outgoing.push(message);
|
||||
this.messageQueue.outgoing.sort((a, b) => b.priority - a.priority);
|
||||
|
||||
// 更新统计
|
||||
this.networkStats.totalBytesSent += JSON.stringify(data).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理传出数据
|
||||
*/
|
||||
private processOutgoingData(data: any): any {
|
||||
let processedData = data;
|
||||
|
||||
// 应用过滤器
|
||||
this.config.filters.forEach(filter => {
|
||||
if (filter.condition(processedData)) {
|
||||
if (filter.action === 'transform' && filter.transformer) {
|
||||
processedData = filter.transformer(processedData);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 压缩数据
|
||||
if (this.config.compressionEnabled) {
|
||||
processedData = this.compressData(processedData);
|
||||
}
|
||||
|
||||
return processedData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 压缩数据(模拟)
|
||||
*/
|
||||
private compressData(data: any): any {
|
||||
// 模拟压缩算法
|
||||
const serialized = JSON.stringify(data);
|
||||
if (serialized.length > this.config.bufferSettings.compressionThreshold) {
|
||||
// 模拟压缩
|
||||
return {
|
||||
compressed: true,
|
||||
originalSize: serialized.length,
|
||||
compressedSize: Math.floor(serialized.length * 0.6),
|
||||
data: serialized.substring(0, Math.floor(serialized.length * 0.6))
|
||||
};
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记属性为脏
|
||||
*/
|
||||
public markDirty(property: string): void {
|
||||
this.syncData.dirtyFlags.add(property);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新网络统计
|
||||
*/
|
||||
public updateNetworkStats(deltaTime: number): void {
|
||||
// 更新延迟历史
|
||||
if (this.networkStats.latencyHistory.length > 100) {
|
||||
this.networkStats.latencyHistory.shift();
|
||||
}
|
||||
this.networkStats.latencyHistory.push(this.connection.ping);
|
||||
|
||||
// 计算平均延迟
|
||||
this.networkStats.averageLatency = this.networkStats.latencyHistory.reduce((a, b) => a + b, 0) /
|
||||
this.networkStats.latencyHistory.length;
|
||||
|
||||
// 更新连接质量
|
||||
if (this.networkStats.averageLatency < 50) {
|
||||
this.networkStats.connectionQuality = 'excellent';
|
||||
} else if (this.networkStats.averageLatency < 100) {
|
||||
this.networkStats.connectionQuality = 'good';
|
||||
} else if (this.networkStats.averageLatency < 200) {
|
||||
this.networkStats.connectionQuality = 'fair';
|
||||
} else {
|
||||
this.networkStats.connectionQuality = 'poor';
|
||||
}
|
||||
|
||||
// 更新包率
|
||||
this.networkStats.packetsPerSecond = this.messageQueue.outgoing.length / deltaTime;
|
||||
|
||||
// 清理过期缓存
|
||||
this.cleanupExpiredCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期缓存
|
||||
*/
|
||||
private cleanupExpiredCache(): void {
|
||||
const now = Date.now();
|
||||
|
||||
// 清理玩家缓存
|
||||
for (const [key, value] of this.cacheSystem.playerCache) {
|
||||
if (value.cacheExpiry < now) {
|
||||
this.cacheSystem.playerCache.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
// 清理消息缓存
|
||||
for (const [key, value] of this.cacheSystem.messageCache) {
|
||||
if (value.timestamp < now - 600000) { // 10分钟过期
|
||||
this.cacheSystem.messageCache.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录网络事件
|
||||
*/
|
||||
private logNetworkEvent(eventType: string, data: any): void {
|
||||
this.networkStats.errorLog.push({
|
||||
timestamp: Date.now(),
|
||||
errorType: eventType,
|
||||
message: JSON.stringify(data)
|
||||
});
|
||||
|
||||
// 限制日志大小
|
||||
if (this.networkStats.errorLog.length > 1000) {
|
||||
this.networkStats.errorLog = this.networkStats.errorLog.slice(-500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置组件
|
||||
*/
|
||||
public reset(): void {
|
||||
// 清理ID列表(不再需要处理循环引用)
|
||||
this.connectedPlayerIds.clear();
|
||||
this.groupMemberIds = [];
|
||||
this.groupLeaderId = null;
|
||||
this.connectionState = 'disconnected';
|
||||
|
||||
this.syncData.dirtyFlags.clear();
|
||||
this.syncData.syncHistory = [];
|
||||
this.syncData.queuedUpdates = [];
|
||||
|
||||
this.messageQueue.incoming = [];
|
||||
this.messageQueue.outgoing = [];
|
||||
this.messageQueue.processing.clear();
|
||||
|
||||
this.cacheSystem.playerCache.clear();
|
||||
this.cacheSystem.messageCache.clear();
|
||||
this.cacheSystem.syncCache.clear();
|
||||
|
||||
this.networkStats.errorLog = [];
|
||||
this.networkStats.latencyHistory = [];
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "d9263549-7b26-4b4f-9a15-b82e7af5fbd5",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -1,346 +0,0 @@
|
||||
import { Component } from '@esengine/ecs-framework';
|
||||
import { Node, Vec3, Color, Sprite, Label } from 'cc';
|
||||
|
||||
/**
|
||||
* Node组件 - 包含Cocos Creator节点引用(已移除循环引用)
|
||||
*/
|
||||
export class NodeComponent extends Component {
|
||||
/** Cocos Creator节点引用 */
|
||||
public node: Node | null = null;
|
||||
|
||||
/** 子节点列表 */
|
||||
public children: Node[] = [];
|
||||
|
||||
/** 节点配置信息 */
|
||||
public nodeConfig: {
|
||||
name: string;
|
||||
layer: number;
|
||||
tag: string;
|
||||
userData: Record<string, any>;
|
||||
transformData: {
|
||||
position: Vec3;
|
||||
rotation: Vec3;
|
||||
scale: Vec3;
|
||||
};
|
||||
renderData: {
|
||||
color: Color;
|
||||
opacity: number;
|
||||
visible: boolean;
|
||||
};
|
||||
parentId: number | null; // 避免循环引用:使用父节点实体ID
|
||||
childIds: number[]; // 避免循环引用:使用子节点实体ID列表
|
||||
};
|
||||
|
||||
/** 渲染组件引用 */
|
||||
public sprite: Sprite | null = null;
|
||||
public label: Label | null = null;
|
||||
|
||||
/** 复杂嵌套对象 */
|
||||
public complexData: {
|
||||
statistics: {
|
||||
frameCount: number;
|
||||
lastUpdateTime: number;
|
||||
performance: {
|
||||
avgRenderTime: number;
|
||||
maxRenderTime: number;
|
||||
renderHistory: number[];
|
||||
};
|
||||
};
|
||||
cache: {
|
||||
textureCache: Map<string, any>;
|
||||
materialCache: Map<string, any>;
|
||||
shaderCache: Map<string, any>;
|
||||
};
|
||||
hierarchy: {
|
||||
parentId: number | null; // 避免循环引用:使用ID
|
||||
rootId: number | null; // 避免循环引用:使用ID
|
||||
depth: number;
|
||||
siblingIndex: number;
|
||||
};
|
||||
animation: {
|
||||
isPlaying: boolean;
|
||||
currentFrame: number;
|
||||
totalFrames: number;
|
||||
loopCount: number;
|
||||
animationQueue: Array<{
|
||||
name: string;
|
||||
duration: number;
|
||||
delay: number;
|
||||
easing: string;
|
||||
}>;
|
||||
};
|
||||
interaction: {
|
||||
isInteractable: boolean;
|
||||
touchEnabled: boolean;
|
||||
hitTestResults: Array<{
|
||||
position: Vec3;
|
||||
timestamp: number;
|
||||
result: boolean;
|
||||
}>;
|
||||
boundingBox: {
|
||||
min: Vec3;
|
||||
max: Vec3;
|
||||
center: Vec3;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
/** 复杂的渲染状态 */
|
||||
public renderState: {
|
||||
layerInfo: {
|
||||
currentLayer: number;
|
||||
layerStack: number[];
|
||||
sortingOrder: number;
|
||||
cullingMask: number;
|
||||
};
|
||||
materials: Array<{
|
||||
materialId: string;
|
||||
properties: Map<string, any>;
|
||||
textures: Map<string, any>;
|
||||
shaderParams: Record<string, any>;
|
||||
}>;
|
||||
lightingData: {
|
||||
ambientColor: Color;
|
||||
diffuseColor: Color;
|
||||
specularColor: Color;
|
||||
lightDirection: Vec3;
|
||||
shadowData: {
|
||||
castShadows: boolean;
|
||||
receiveShadows: boolean;
|
||||
shadowQuality: 'low' | 'medium' | 'high';
|
||||
shadowDistance: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
constructor(name: string = "DefaultNode") {
|
||||
super();
|
||||
|
||||
this.nodeConfig = {
|
||||
name: name,
|
||||
layer: 0,
|
||||
tag: "default",
|
||||
userData: {},
|
||||
transformData: {
|
||||
position: new Vec3(),
|
||||
rotation: new Vec3(),
|
||||
scale: new Vec3(1, 1, 1)
|
||||
},
|
||||
renderData: {
|
||||
color: new Color(255, 255, 255, 255),
|
||||
opacity: 1.0,
|
||||
visible: true
|
||||
},
|
||||
parentId: null,
|
||||
childIds: []
|
||||
};
|
||||
|
||||
this.complexData = {
|
||||
statistics: {
|
||||
frameCount: 0,
|
||||
lastUpdateTime: 0,
|
||||
performance: {
|
||||
avgRenderTime: 0,
|
||||
maxRenderTime: 0,
|
||||
renderHistory: []
|
||||
}
|
||||
},
|
||||
cache: {
|
||||
textureCache: new Map(),
|
||||
materialCache: new Map(),
|
||||
shaderCache: new Map()
|
||||
},
|
||||
hierarchy: {
|
||||
parentId: null,
|
||||
rootId: null,
|
||||
depth: 0,
|
||||
siblingIndex: 0
|
||||
},
|
||||
animation: {
|
||||
isPlaying: false,
|
||||
currentFrame: 0,
|
||||
totalFrames: 60,
|
||||
loopCount: 0,
|
||||
animationQueue: []
|
||||
},
|
||||
interaction: {
|
||||
isInteractable: true,
|
||||
touchEnabled: true,
|
||||
hitTestResults: [],
|
||||
boundingBox: {
|
||||
min: new Vec3(-1, -1, -1),
|
||||
max: new Vec3(1, 1, 1),
|
||||
center: new Vec3()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.renderState = {
|
||||
layerInfo: {
|
||||
currentLayer: 0,
|
||||
layerStack: [0],
|
||||
sortingOrder: 0,
|
||||
cullingMask: 0xFFFFFFFF
|
||||
},
|
||||
materials: [],
|
||||
lightingData: {
|
||||
ambientColor: new Color(128, 128, 128, 255),
|
||||
diffuseColor: new Color(255, 255, 255, 255),
|
||||
specularColor: new Color(255, 255, 255, 255),
|
||||
lightDirection: new Vec3(0, -1, 0),
|
||||
shadowData: {
|
||||
castShadows: true,
|
||||
receiveShadows: true,
|
||||
shadowQuality: 'medium',
|
||||
shadowDistance: 100
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置父节点组件(避免循环引用)
|
||||
*/
|
||||
public setParent(parentEntityId: number): void {
|
||||
this.nodeConfig.parentId = parentEntityId;
|
||||
this.complexData.hierarchy.parentId = parentEntityId;
|
||||
// 深度需要通过其他方式计算,避免引用
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加子节点
|
||||
*/
|
||||
public addChild(childEntityId: number): void {
|
||||
if (!this.nodeConfig.childIds.includes(childEntityId)) {
|
||||
this.nodeConfig.childIds.push(childEntityId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新性能统计
|
||||
*/
|
||||
public updatePerformance(renderTime: number): void {
|
||||
this.complexData.statistics.frameCount++;
|
||||
this.complexData.statistics.lastUpdateTime = Date.now();
|
||||
|
||||
const perf = this.complexData.statistics.performance;
|
||||
perf.renderHistory.push(renderTime);
|
||||
|
||||
// 保持历史记录在合理范围内
|
||||
if (perf.renderHistory.length > 100) {
|
||||
perf.renderHistory.shift();
|
||||
}
|
||||
|
||||
// 计算平均值和最大值
|
||||
perf.avgRenderTime = perf.renderHistory.reduce((a, b) => a + b, 0) / perf.renderHistory.length;
|
||||
perf.maxRenderTime = Math.max(perf.maxRenderTime, renderTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新动画状态
|
||||
*/
|
||||
public updateAnimation(deltaTime: number): void {
|
||||
if (this.complexData.animation.isPlaying) {
|
||||
this.complexData.animation.currentFrame++;
|
||||
|
||||
if (this.complexData.animation.currentFrame >= this.complexData.animation.totalFrames) {
|
||||
this.complexData.animation.currentFrame = 0;
|
||||
this.complexData.animation.loopCount++;
|
||||
|
||||
// 处理动画队列
|
||||
if (this.complexData.animation.animationQueue.length > 0) {
|
||||
const nextAnim = this.complexData.animation.animationQueue.shift();
|
||||
if (nextAnim) {
|
||||
this.complexData.animation.totalFrames = Math.floor(nextAnim.duration * 60); // 假设60FPS
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加材质
|
||||
*/
|
||||
public addMaterial(materialId: string, properties: Record<string, any>): void {
|
||||
this.renderState.materials.push({
|
||||
materialId,
|
||||
properties: new Map(Object.entries(properties)),
|
||||
textures: new Map(),
|
||||
shaderParams: {}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新包围盒
|
||||
*/
|
||||
public updateBoundingBox(): void {
|
||||
if (this.node) {
|
||||
const worldPos = this.node.getWorldPosition();
|
||||
const scale = this.node.getScale();
|
||||
|
||||
this.complexData.interaction.boundingBox.center = new Vec3(worldPos.x, worldPos.y, worldPos.z);
|
||||
this.complexData.interaction.boundingBox.min = new Vec3(
|
||||
worldPos.x - scale.x * 0.5,
|
||||
worldPos.y - scale.y * 0.5,
|
||||
worldPos.z - scale.z * 0.5
|
||||
);
|
||||
this.complexData.interaction.boundingBox.max = new Vec3(
|
||||
worldPos.x + scale.x * 0.5,
|
||||
worldPos.y + scale.y * 0.5,
|
||||
worldPos.z + scale.z * 0.5
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行点击测试
|
||||
*/
|
||||
public hitTest(point: Vec3): boolean {
|
||||
const bbox = this.complexData.interaction.boundingBox;
|
||||
const result = point.x >= bbox.min.x && point.x <= bbox.max.x &&
|
||||
point.y >= bbox.min.y && point.y <= bbox.max.y &&
|
||||
point.z >= bbox.min.z && point.z <= bbox.max.z;
|
||||
|
||||
// 记录测试结果
|
||||
this.complexData.interaction.hitTestResults.push({
|
||||
position: new Vec3(point.x, point.y, point.z),
|
||||
timestamp: Date.now(),
|
||||
result
|
||||
});
|
||||
|
||||
// 限制历史记录大小
|
||||
if (this.complexData.interaction.hitTestResults.length > 50) {
|
||||
this.complexData.interaction.hitTestResults.shift();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置组件
|
||||
*/
|
||||
public reset(): void {
|
||||
this.node = null;
|
||||
this.children = [];
|
||||
this.sprite = null;
|
||||
this.label = null;
|
||||
|
||||
// 清理ID列表(不再需要处理循环引用)
|
||||
this.nodeConfig.parentId = null;
|
||||
this.nodeConfig.childIds = [];
|
||||
this.complexData.hierarchy.parentId = null;
|
||||
this.complexData.hierarchy.rootId = null;
|
||||
this.complexData.hierarchy.depth = 0;
|
||||
|
||||
this.complexData.cache.textureCache.clear();
|
||||
this.complexData.cache.materialCache.clear();
|
||||
this.complexData.cache.shaderCache.clear();
|
||||
|
||||
this.complexData.animation.isPlaying = false;
|
||||
this.complexData.animation.currentFrame = 0;
|
||||
this.complexData.animation.animationQueue = [];
|
||||
|
||||
this.complexData.interaction.hitTestResults = [];
|
||||
this.renderState.materials = [];
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "28e7e8cd-591e-4fde-bb14-d668724a6201",
|
||||
"files": [],
|
||||
"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