Compare commits
136 Commits
v2.2.16
...
@esengine/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27b9e174eb | ||
|
|
ede440d277 | ||
|
|
5cb83f0743 | ||
|
|
7cbf92b8c7 | ||
|
|
a049bbe2f5 | ||
|
|
ec72df7af5 | ||
|
|
9327c1cef5 | ||
|
|
da5bf2116a | ||
|
|
67e97f89c6 | ||
|
|
31fd34b221 | ||
|
|
c4f7a13b74 | ||
|
|
155411e743 | ||
|
|
a84ff902e4 | ||
|
|
54038e3250 | ||
|
|
5544fca002 | ||
|
|
88b5ffc0a7 | ||
|
|
0c0a5f10f7 | ||
|
|
56e322de7f | ||
|
|
c2ebd387f2 | ||
|
|
e5e647f1a4 | ||
|
|
4d501ba448 | ||
|
|
275124b66c | ||
|
|
25936c19e9 | ||
|
|
f43631a1e1 | ||
|
|
f8c181836e | ||
|
|
840eb3452e | ||
|
|
0bf849e193 | ||
|
|
ebb984d354 | ||
|
|
068ca4bf69 | ||
|
|
4089051731 | ||
|
|
6b8b65ae16 | ||
|
|
a75c61c049 | ||
|
|
770c05402d | ||
|
|
9d581ccd8d | ||
|
|
235c432edb | ||
|
|
dbc6793dc4 | ||
|
|
58f70a5783 | ||
|
|
828ff969e1 | ||
|
|
49dd6a91c6 | ||
|
|
1e048d5c04 | ||
|
|
dff2ec564b | ||
|
|
2381919a5c | ||
|
|
66d9f428b3 | ||
|
|
a1e1189f9d | ||
|
|
96b5403d14 | ||
|
|
4b74db3f2d | ||
|
|
e24c850568 | ||
|
|
ecdb8f2021 | ||
|
|
536c4c5593 | ||
|
|
958933cd76 | ||
|
|
fbc911463a | ||
|
|
5b7746af79 | ||
|
|
9e195ae3fd | ||
|
|
a18eb5aa3c | ||
|
|
48d3d14af2 | ||
|
|
ed8f6e283b | ||
|
|
d834ca5e77 | ||
|
|
85e95ec18c | ||
|
|
ff9bc00729 | ||
|
|
7451a78e60 | ||
|
|
23ee2393c6 | ||
|
|
cd6ef222d1 | ||
|
|
b5158b6ac6 | ||
|
|
beaa1d09de | ||
|
|
a716d8006c | ||
|
|
1b0d38edce | ||
|
|
995fa2d514 | ||
|
|
c71a47f2b0 | ||
|
|
6c99b811ec | ||
|
|
40a38b8b88 | ||
|
|
ad96edfad0 | ||
|
|
240b165970 | ||
|
|
c3b7250f85 | ||
|
|
2476379af1 | ||
|
|
e0d659fe46 | ||
|
|
9ff03c04f3 | ||
|
|
7b45fbeab3 | ||
|
|
a733a53d3e | ||
|
|
dfd0dfc7f9 | ||
|
|
52bbccd53c | ||
|
|
d92c2a7b66 | ||
|
|
568b327425 | ||
|
|
1fb702169e | ||
|
|
3617f40309 | ||
|
|
0c03b13d74 | ||
|
|
3cbfa1e4cb | ||
|
|
397f79caa5 | ||
|
|
972c1d5357 | ||
|
|
32d35ef2ee | ||
|
|
57e165779e | ||
|
|
690d7859c8 | ||
|
|
8f9a7d8581 | ||
|
|
3d5fcc1a55 | ||
|
|
823e0c1d94 | ||
|
|
13a149c3a2 | ||
|
|
dd130eacb0 | ||
|
|
d0238add2d | ||
|
|
fe96d72ac6 | ||
|
|
b2b8df9340 | ||
|
|
2d56eaf11a | ||
|
|
2cb9c471f9 | ||
|
|
e8fc7f497b | ||
|
|
6702f0bfad | ||
|
|
d7454e3ca4 | ||
|
|
0d9bab910e | ||
|
|
3d16bbdc64 | ||
|
|
b4e7ba2abd | ||
|
|
374b26f7c6 | ||
|
|
dbebb4f4fb | ||
|
|
eec89b626c | ||
|
|
763d23e960 | ||
|
|
3b56ed17fe | ||
|
|
4b8d22ac32 | ||
|
|
9cd873da14 | ||
|
|
c1799bf7b3 | ||
|
|
85be826b62 | ||
|
|
dd1ae97de7 | ||
|
|
63f006ab62 | ||
|
|
caf7622aa0 | ||
|
|
d746cf3bb8 | ||
|
|
88af781d78 | ||
|
|
15d5d37e50 | ||
|
|
b9aaf894d7 | ||
|
|
460cdb5af4 | ||
|
|
290bd9858e | ||
|
|
b42a7b4e43 | ||
|
|
189714c727 | ||
|
|
987051acd4 | ||
|
|
374e08a79e | ||
|
|
359886c72f | ||
|
|
f03b73b58e | ||
|
|
18d20df4da | ||
|
|
c5642a8605 | ||
|
|
673f5e5855 | ||
|
|
cabb625a17 | ||
|
|
b8f05b79b0 |
8
.changeset/README.md
Normal file
8
.changeset/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Changesets
|
||||
|
||||
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
||||
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
||||
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
||||
|
||||
We have a quick list of common questions to get you started engaging with this project in
|
||||
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
||||
58
.changeset/config.json
Normal file
58
.changeset/config.json
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@3.1.2/schema.json",
|
||||
"changelog": [
|
||||
"@changesets/changelog-github",
|
||||
{ "repo": "esengine/esengine" }
|
||||
],
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [
|
||||
["@esengine/ecs-framework", "@esengine/ecs-framework-math"]
|
||||
],
|
||||
"access": "public",
|
||||
"baseBranch": "master",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": [
|
||||
"@esengine/engine-core",
|
||||
"@esengine/runtime-core",
|
||||
"@esengine/asset-system",
|
||||
"@esengine/material-system",
|
||||
"@esengine/ecs-engine-bindgen",
|
||||
"@esengine/script-runtime",
|
||||
"@esengine/platform-common",
|
||||
"@esengine/platform-web",
|
||||
"@esengine/platform-wechat",
|
||||
"@esengine/sprite",
|
||||
"@esengine/camera",
|
||||
"@esengine/particle",
|
||||
"@esengine/tilemap",
|
||||
"@esengine/mesh-3d",
|
||||
"@esengine/effect",
|
||||
"@esengine/audio",
|
||||
"@esengine/fairygui",
|
||||
"@esengine/physics-rapier2d",
|
||||
"@esengine/rapier2d",
|
||||
"@esengine/world-streaming",
|
||||
"@esengine/network-server",
|
||||
"@esengine/editor-core",
|
||||
"@esengine/editor-runtime",
|
||||
"@esengine/editor-app",
|
||||
"@esengine/sprite-editor",
|
||||
"@esengine/camera-editor",
|
||||
"@esengine/particle-editor",
|
||||
"@esengine/tilemap-editor",
|
||||
"@esengine/mesh-3d-editor",
|
||||
"@esengine/fairygui-editor",
|
||||
"@esengine/physics-rapier2d-editor",
|
||||
"@esengine/behavior-tree-editor",
|
||||
"@esengine/blueprint-editor",
|
||||
"@esengine/asset-system-editor",
|
||||
"@esengine/material-editor",
|
||||
"@esengine/shader-editor",
|
||||
"@esengine/world-streaming-editor",
|
||||
"@esengine/node-editor",
|
||||
"@esengine/sdk",
|
||||
"@esengine/worker-generator",
|
||||
"@esengine/engine"
|
||||
]
|
||||
}
|
||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -9,4 +9,4 @@ community_bridge: # Replace with a single Community Bridge project-name e.g., cl
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: ['https://github.com/esengine/ecs-framework/blob/master/sponsor/alipay.jpg', 'https://github.com/esengine/ecs-framework/blob/master/sponsor/wechatpay.png'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
custom: ['https://github.com/esengine/esengine/blob/master/sponsor/alipay.jpg', 'https://github.com/esengine/esengine/blob/master/sponsor/wechatpay.png'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/config.yml
vendored
4
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -5,7 +5,7 @@ contact_links:
|
||||
about: 查看完整文档和教程 / View full documentation and tutorials
|
||||
|
||||
- name: 🤖 AI 文档助手 / AI Documentation Assistant
|
||||
url: https://deepwiki.com/esengine/ecs-framework
|
||||
url: https://deepwiki.com/esengine/esengine
|
||||
about: 使用 AI 助手快速找到答案 / Use AI assistant to quickly find answers
|
||||
|
||||
- name: 💬 QQ 交流群 / QQ Group
|
||||
@@ -13,5 +13,5 @@ contact_links:
|
||||
about: 加入社区交流群 / Join the community group
|
||||
|
||||
- name: 🌟 GitHub Discussions
|
||||
url: https://github.com/esengine/ecs-framework/discussions
|
||||
url: https://github.com/esengine/esengine/discussions
|
||||
about: 参与社区讨论 / Join community discussions
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/question.yml
vendored
4
.github/ISSUE_TEMPLATE/question.yml
vendored
@@ -8,12 +8,12 @@ body:
|
||||
value: |
|
||||
💡 提示:如果是简单问题,可以先查看:
|
||||
- [📚 文档](https://esengine.github.io/ecs-framework/)
|
||||
- [📖 AI 文档助手](https://deepwiki.com/esengine/ecs-framework)
|
||||
- [📖 AI 文档助手](https://deepwiki.com/esengine/esengine)
|
||||
- [💬 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)
|
||||
- [📖 AI Documentation](https://deepwiki.com/esengine/esengine)
|
||||
|
||||
- type: textarea
|
||||
id: question
|
||||
|
||||
13
.github/codeql/codeql-config.yml
vendored
Normal file
13
.github/codeql/codeql-config.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
name: "CodeQL Config"
|
||||
|
||||
# Paths to exclude from analysis
|
||||
paths-ignore:
|
||||
- thirdparty
|
||||
- "**/node_modules"
|
||||
- "**/dist"
|
||||
- "**/bin"
|
||||
- "**/tests"
|
||||
- "**/*.test.ts"
|
||||
- "**/*.spec.ts"
|
||||
- "**/test"
|
||||
- "**/__tests__"
|
||||
32
.github/labeler.yml
vendored
32
.github/labeler.yml
vendored
@@ -1,32 +0,0 @@
|
||||
# 自动标签配置
|
||||
# 根据 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'
|
||||
73
.github/workflows/ai-batch-analyze-issues.yml
vendored
73
.github/workflows/ai-batch-analyze-issues.yml
vendored
@@ -1,73 +0,0 @@
|
||||
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
61
.github/workflows/ai-helper-tip.yml
vendored
@@ -1,61 +0,0 @@
|
||||
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
85
.github/workflows/ai-issue-helper.yml
vendored
@@ -1,85 +0,0 @@
|
||||
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
56
.github/workflows/ai-issue-moderator.yml
vendored
@@ -1,56 +0,0 @@
|
||||
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
160
.github/workflows/batch-label-issues.yml
vendored
@@ -1,160 +0,0 @@
|
||||
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"
|
||||
139
.github/workflows/ci.yml
vendored
139
.github/workflows/ci.yml
vendored
@@ -8,30 +8,47 @@ on:
|
||||
- 'package.json'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'tsconfig.json'
|
||||
- 'turbo.json'
|
||||
- 'jest.config.*'
|
||||
- '.github/workflows/ci.yml'
|
||||
pull_request:
|
||||
branches: [ master, main, develop ]
|
||||
paths:
|
||||
- 'packages/**'
|
||||
- 'package.json'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'tsconfig.json'
|
||||
- 'jest.config.*'
|
||||
- '.github/workflows/ci.yml'
|
||||
# Run on all PRs to satisfy branch protection, but skip build if no code changes
|
||||
|
||||
jobs:
|
||||
test:
|
||||
# Check if we need to run the full CI
|
||||
check-changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should-run: ${{ steps.filter.outputs.code }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
code:
|
||||
- 'packages/**'
|
||||
- 'package.json'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'tsconfig.json'
|
||||
- 'turbo.json'
|
||||
- 'jest.config.*'
|
||||
|
||||
ci:
|
||||
needs: check-changes
|
||||
if: needs.check-changes.outputs.should-run == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
@@ -40,53 +57,34 @@ jobs:
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Install wasm-pack
|
||||
run: cargo install wasm-pack
|
||||
|
||||
- name: Build core package first
|
||||
run: pnpm run build:core
|
||||
|
||||
- name: Build engine WASM package
|
||||
# 构建 framework 包 (可独立发布的通用库,无外部依赖)
|
||||
- name: Build framework packages
|
||||
run: |
|
||||
cd packages/engine
|
||||
pnpm run build
|
||||
pnpm --filter @esengine/ecs-framework build
|
||||
pnpm --filter @esengine/ecs-framework-math build
|
||||
pnpm --filter @esengine/behavior-tree build
|
||||
pnpm --filter @esengine/blueprint build
|
||||
pnpm --filter @esengine/fsm build
|
||||
pnpm --filter @esengine/timer build
|
||||
pnpm --filter @esengine/spatial build
|
||||
pnpm --filter @esengine/procgen build
|
||||
pnpm --filter @esengine/pathfinding build
|
||||
pnpm --filter @esengine/network-protocols build
|
||||
pnpm --filter @esengine/network build
|
||||
|
||||
- name: Copy WASM files to ecs-engine-bindgen
|
||||
run: |
|
||||
mkdir -p packages/ecs-engine-bindgen/src/wasm
|
||||
cp packages/engine/pkg/es_engine.js packages/ecs-engine-bindgen/src/wasm/
|
||||
cp packages/engine/pkg/es_engine.d.ts packages/ecs-engine-bindgen/src/wasm/
|
||||
cp packages/engine/pkg/es_engine_bg.wasm packages/ecs-engine-bindgen/src/wasm/
|
||||
cp packages/engine/pkg/es_engine_bg.wasm.d.ts packages/ecs-engine-bindgen/src/wasm/
|
||||
# 类型检查 (仅 framework 包)
|
||||
- name: Type check (framework packages)
|
||||
run: pnpm run type-check:framework
|
||||
|
||||
- name: Build dependent packages for type declarations
|
||||
run: |
|
||||
cd packages/platform-common && pnpm run build
|
||||
cd ../asset-system && pnpm run build
|
||||
cd ../components && pnpm run build
|
||||
cd ../editor-core && pnpm run build
|
||||
cd ../ui && pnpm run build
|
||||
cd ../editor-runtime && pnpm run build
|
||||
cd ../behavior-tree && pnpm run build
|
||||
cd ../tilemap && pnpm run build
|
||||
|
||||
- name: Build ecs-engine-bindgen
|
||||
run: |
|
||||
cd packages/ecs-engine-bindgen && pnpm run build
|
||||
|
||||
- name: Type check
|
||||
run: pnpm run type-check
|
||||
|
||||
- name: Lint check
|
||||
run: pnpm run lint
|
||||
# Lint 检查 (仅 framework 包)
|
||||
- name: Lint check (framework packages)
|
||||
run: pnpm run lint:framework
|
||||
|
||||
# 测试 (仅 framework 包)
|
||||
- name: Run tests with coverage
|
||||
run: pnpm run test:ci
|
||||
run: pnpm run test:ci:framework
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
@@ -97,39 +95,18 @@ jobs:
|
||||
name: codecov-umbrella
|
||||
fail_ci_if_error: false
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build project
|
||||
run: pnpm run build
|
||||
|
||||
- name: Build npm package
|
||||
run: pnpm run build:npm
|
||||
# 构建 npm 包 (core 和 math)
|
||||
- name: Build npm packages
|
||||
run: |
|
||||
pnpm run build:npm:core
|
||||
pnpm run build:npm:math
|
||||
|
||||
# 上传构建产物
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-artifacts
|
||||
path: |
|
||||
bin/
|
||||
dist/
|
||||
packages/framework/**/dist/
|
||||
packages/framework/**/bin/
|
||||
retention-days: 7
|
||||
|
||||
146
.github/workflows/cleanup-dependabot.yml
vendored
146
.github/workflows/cleanup-dependabot.yml
vendored
@@ -1,146 +0,0 @@
|
||||
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`);
|
||||
}
|
||||
10
.github/workflows/codecov.yml
vendored
10
.github/workflows/codecov.yml
vendored
@@ -15,9 +15,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
@@ -30,7 +28,7 @@ jobs:
|
||||
|
||||
- name: Run tests with coverage
|
||||
run: |
|
||||
cd packages/core
|
||||
cd packages/framework/core
|
||||
pnpm run test:coverage
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
@@ -38,7 +36,7 @@ jobs:
|
||||
continue-on-error: true
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./packages/core/coverage/coverage-final.json
|
||||
files: ./packages/framework/core/coverage/coverage-final.json
|
||||
flags: core
|
||||
name: core-coverage
|
||||
fail_ci_if_error: false
|
||||
@@ -48,4 +46,4 @@ jobs:
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-report
|
||||
path: packages/core/coverage/
|
||||
path: packages/framework/core/coverage/
|
||||
|
||||
1
.github/workflows/codeql.yml
vendored
1
.github/workflows/codeql.yml
vendored
@@ -31,6 +31,7 @@ jobs:
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: security-and-quality
|
||||
config-file: ./.github/codeql/codeql-config.yml
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
4
.github/workflows/commitlint.yml
vendored
4
.github/workflows/commitlint.yml
vendored
@@ -18,9 +18,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
4
.github/workflows/docs.yml
vendored
4
.github/workflows/docs.yml
vendored
@@ -30,9 +30,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
23
.github/workflows/issue-labeler.yml
vendored
23
.github/workflows/issue-labeler.yml
vendored
@@ -1,23 +0,0 @@
|
||||
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
28
.github/workflows/issue-translator.yml
vendored
@@ -1,28 +0,0 @@
|
||||
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>
|
||||
70
.github/workflows/release-changesets.yml
vendored
Normal file
70
.github/workflows/release-changesets.yml
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
name: Release (Changesets)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- '.changeset/**'
|
||||
- 'packages/*/package.json'
|
||||
- 'packages/*/*/package.json'
|
||||
- 'packages/*/*/*/package.json'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build framework packages
|
||||
run: |
|
||||
# Only build packages managed by Changesets (not in ignore list)
|
||||
pnpm --filter "@esengine/ecs-framework" build
|
||||
pnpm --filter "@esengine/ecs-framework-math" build
|
||||
pnpm --filter "@esengine/behavior-tree" build
|
||||
pnpm --filter "@esengine/blueprint" build
|
||||
pnpm --filter "@esengine/fsm" build
|
||||
pnpm --filter "@esengine/timer" build
|
||||
pnpm --filter "@esengine/spatial" build
|
||||
pnpm --filter "@esengine/procgen" build
|
||||
pnpm --filter "@esengine/pathfinding" build
|
||||
pnpm --filter "@esengine/network-protocols" build
|
||||
pnpm --filter "@esengine/network" build
|
||||
pnpm --filter "@esengine/cli" build
|
||||
|
||||
- name: Create Release Pull Request or Publish
|
||||
id: changesets
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
version: pnpm changeset:version
|
||||
publish: pnpm changeset:publish
|
||||
title: 'chore: release packages'
|
||||
commit: 'chore: release packages'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
209
.github/workflows/release-editor.yml
vendored
209
.github/workflows/release-editor.yml
vendored
@@ -34,9 +34,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
@@ -52,7 +50,7 @@ jobs:
|
||||
- name: Rust cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: packages/editor-app/src-tauri
|
||||
workspaces: packages/editor/editor-app/src-tauri
|
||||
cache-on-failure: true
|
||||
|
||||
- name: Install dependencies (Ubuntu)
|
||||
@@ -67,118 +65,149 @@ jobs:
|
||||
- name: Update version in config files (for manual trigger)
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |
|
||||
cd packages/editor-app
|
||||
cd packages/editor/editor-app
|
||||
node -e "const pkg=require('./package.json'); pkg.version='${{ github.event.inputs.version }}'; require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2)+'\n')"
|
||||
node scripts/sync-version.js
|
||||
|
||||
# ===== 第一层:基础包(无依赖) =====
|
||||
- name: Build core package
|
||||
run: pnpm run build:core
|
||||
|
||||
- name: Build math package
|
||||
run: |
|
||||
cd packages/math
|
||||
pnpm run build
|
||||
|
||||
- name: Build platform-common package
|
||||
run: |
|
||||
cd packages/platform-common
|
||||
pnpm run build
|
||||
|
||||
# ===== 第二层:依赖 core 的包 =====
|
||||
- name: Build asset-system package
|
||||
run: |
|
||||
cd packages/asset-system
|
||||
pnpm run build
|
||||
|
||||
- name: Build components package
|
||||
run: |
|
||||
cd packages/components
|
||||
pnpm run build
|
||||
|
||||
# ===== 第三层:Rust WASM 引擎 =====
|
||||
- name: Install wasm-pack
|
||||
run: cargo install wasm-pack
|
||||
|
||||
- name: Build engine package (Rust WASM)
|
||||
run: |
|
||||
cd packages/engine
|
||||
pnpm run build
|
||||
# 使用 Turborepo 自动按依赖顺序构建所有包
|
||||
# 这会自动处理:core -> asset-system -> editor-core -> ui -> 等等
|
||||
- name: Build all packages with Turborepo
|
||||
run: pnpm run build
|
||||
|
||||
- name: Copy WASM files to ecs-engine-bindgen
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p packages/ecs-engine-bindgen/src/wasm
|
||||
cp packages/engine/pkg/es_engine.js packages/ecs-engine-bindgen/src/wasm/
|
||||
cp packages/engine/pkg/es_engine.d.ts packages/ecs-engine-bindgen/src/wasm/
|
||||
cp packages/engine/pkg/es_engine_bg.wasm packages/ecs-engine-bindgen/src/wasm/
|
||||
cp packages/engine/pkg/es_engine_bg.wasm.d.ts packages/ecs-engine-bindgen/src/wasm/
|
||||
|
||||
- name: Build ecs-engine-bindgen package
|
||||
run: |
|
||||
cd packages/ecs-engine-bindgen
|
||||
pnpm run build
|
||||
|
||||
# ===== 第四层:依赖 ecs-engine-bindgen/asset-system 的包 =====
|
||||
- name: Build editor-core package
|
||||
run: |
|
||||
cd packages/editor-core
|
||||
pnpm run build
|
||||
|
||||
# ===== 第五层:依赖 editor-core 的包 =====
|
||||
- name: Build UI package
|
||||
run: |
|
||||
cd packages/ui
|
||||
pnpm run build
|
||||
|
||||
- name: Build tilemap package
|
||||
run: |
|
||||
cd packages/tilemap
|
||||
pnpm run build
|
||||
|
||||
- name: Build editor-runtime package
|
||||
run: |
|
||||
cd packages/editor-runtime
|
||||
pnpm run build
|
||||
|
||||
# ===== 第六层:依赖 editor-runtime 的包 =====
|
||||
- name: Build behavior-tree package
|
||||
run: |
|
||||
cd packages/behavior-tree
|
||||
pnpm run build
|
||||
|
||||
# ===== 第七层:平台包(依赖 ui, tilemap) =====
|
||||
- name: Build platform-web package
|
||||
run: |
|
||||
cd packages/platform-web
|
||||
pnpm run build
|
||||
mkdir -p packages/engine/ecs-engine-bindgen/src/wasm
|
||||
cp packages/rust/engine/pkg/es_engine.js packages/engine/ecs-engine-bindgen/src/wasm/
|
||||
cp packages/rust/engine/pkg/es_engine.d.ts packages/engine/ecs-engine-bindgen/src/wasm/
|
||||
cp packages/rust/engine/pkg/es_engine_bg.wasm packages/engine/ecs-engine-bindgen/src/wasm/
|
||||
cp packages/rust/engine/pkg/es_engine_bg.wasm.d.ts packages/engine/ecs-engine-bindgen/src/wasm/
|
||||
|
||||
- name: Bundle runtime files for Tauri
|
||||
run: |
|
||||
cd packages/editor-app
|
||||
cd packages/editor/editor-app
|
||||
node scripts/bundle-runtime.mjs
|
||||
|
||||
- name: Build Tauri app
|
||||
id: tauri
|
||||
uses: tauri-apps/tauri-action@v0.5
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
with:
|
||||
projectPath: packages/editor-app
|
||||
projectPath: packages/editor/editor-app
|
||||
tagName: ${{ github.event_name == 'workflow_dispatch' && format('editor-v{0}', github.event.inputs.version) || github.ref_name }}
|
||||
releaseName: 'ECS Editor v${{ github.event.inputs.version || github.ref_name }}'
|
||||
releaseBody: 'See the assets to download this version and install.'
|
||||
releaseDraft: false
|
||||
releaseDraft: true
|
||||
prerelease: false
|
||||
includeUpdaterJson: true
|
||||
updaterJsonKeepUniversal: false
|
||||
args: ${{ matrix.platform == 'macos-latest' && format('--target {0}', matrix.target) || '' }}
|
||||
|
||||
# 构建成功后,创建 PR 更新版本号
|
||||
update-version-pr:
|
||||
# Windows 构建上传 artifact 供 SignPath 签名
|
||||
- name: Upload Windows artifacts for signing
|
||||
if: matrix.platform == 'windows-latest'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-unsigned
|
||||
path: |
|
||||
packages/editor/editor-app/src-tauri/target/release/bundle/nsis/*.exe
|
||||
packages/editor/editor-app/src-tauri/target/release/bundle/msi/*.msi
|
||||
retention-days: 1
|
||||
|
||||
# SignPath 代码签名(Windows)
|
||||
# SignPath OSS code signing for Windows
|
||||
#
|
||||
# 配置步骤 | Setup Steps:
|
||||
# 1. 在 SignPath 门户创建项目 | Create project in SignPath portal
|
||||
# 2. 导入 .signpath/artifact-configuration.xml | Import artifact configuration
|
||||
# 3. 使用 'test-signing' 策略测试 | Use 'test-signing' policy for testing
|
||||
# 生产环境改为 'release-signing' | Change to 'release-signing' for production
|
||||
# 4. 配置 GitHub Secrets | Configure GitHub Secrets:
|
||||
# - SIGNPATH_API_TOKEN: API token from SignPath
|
||||
# - SIGNPATH_ORGANIZATION_ID: Your organization ID
|
||||
#
|
||||
# 文档 | Documentation: https://about.signpath.io/documentation/trusted-build-systems/github
|
||||
sign-windows:
|
||||
needs: build-tauri
|
||||
if: github.event_name == 'workflow_dispatch' && success()
|
||||
runs-on: ubuntu-latest
|
||||
# 只有在构建成功时才运行 | Only run on successful build
|
||||
if: success()
|
||||
|
||||
steps:
|
||||
- name: Check SignPath configuration
|
||||
id: check-signpath
|
||||
run: |
|
||||
if [ -n "${{ secrets.SIGNPATH_API_TOKEN }}" ] && [ -n "${{ secrets.SIGNPATH_ORGANIZATION_ID }}" ]; then
|
||||
echo "enabled=true" >> $GITHUB_OUTPUT
|
||||
echo "SignPath is configured, proceeding with code signing"
|
||||
else
|
||||
echo "enabled=false" >> $GITHUB_OUTPUT
|
||||
echo "SignPath secrets not configured, skipping code signing"
|
||||
echo "To enable: add SIGNPATH_API_TOKEN and SIGNPATH_ORGANIZATION_ID secrets"
|
||||
fi
|
||||
|
||||
- name: Checkout
|
||||
if: steps.check-signpath.outputs.enabled == 'true'
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get artifact ID
|
||||
if: steps.check-signpath.outputs.enabled == 'true'
|
||||
id: get-artifact
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
# 获取 windows-unsigned artifact 的 ID
|
||||
ARTIFACT_ID=$(gh api \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"/repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts" \
|
||||
--jq '.artifacts[] | select(.name == "windows-unsigned") | .id')
|
||||
|
||||
if [ -z "$ARTIFACT_ID" ]; then
|
||||
echo "Error: Could not find artifact 'windows-unsigned'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "artifact-id=$ARTIFACT_ID" >> $GITHUB_OUTPUT
|
||||
echo "Found artifact ID: $ARTIFACT_ID"
|
||||
|
||||
- name: Submit to SignPath for code signing
|
||||
if: steps.check-signpath.outputs.enabled == 'true'
|
||||
id: signpath
|
||||
uses: signpath/github-action-submit-signing-request@v1
|
||||
with:
|
||||
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
|
||||
organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }}
|
||||
project-slug: 'ecs-framework'
|
||||
signing-policy-slug: 'test-signing'
|
||||
artifact-configuration-slug: 'initial'
|
||||
github-artifact-id: ${{ steps.get-artifact.outputs.artifact-id }}
|
||||
wait-for-completion: true
|
||||
wait-for-completion-timeout-in-seconds: 600
|
||||
output-artifact-directory: './signed'
|
||||
|
||||
- name: Upload signed artifacts to release
|
||||
if: steps.check-signpath.outputs.enabled == 'true'
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: ./signed/*
|
||||
tag_name: ${{ github.event_name == 'workflow_dispatch' && format('editor-v{0}', github.event.inputs.version) || github.ref_name }}
|
||||
# 保持 Draft 状态,需要手动发布 | Keep as draft, require manual publish
|
||||
draft: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# 构建成功后,创建 PR 更新版本号
|
||||
# Create PR to update version after successful build
|
||||
update-version-pr:
|
||||
needs: [build-tauri, sign-windows]
|
||||
# 即使签名跳过也要运行 | Run even if signing is skipped
|
||||
if: github.event_name == 'workflow_dispatch' && !failure()
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
@@ -192,7 +221,7 @@ jobs:
|
||||
|
||||
- name: Update version files
|
||||
run: |
|
||||
cd packages/editor-app
|
||||
cd packages/editor/editor-app
|
||||
node -e "const pkg=require('./package.json'); pkg.version='${{ github.event.inputs.version }}'; require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2)+'\n')"
|
||||
node scripts/sync-version.js
|
||||
|
||||
@@ -205,16 +234,16 @@ jobs:
|
||||
delete-branch: true
|
||||
title: "chore(editor): Release v${{ github.event.inputs.version }}"
|
||||
body: |
|
||||
## 🚀 Release v${{ github.event.inputs.version }}
|
||||
## Release v${{ github.event.inputs.version }}
|
||||
|
||||
This PR updates the editor version after successful release build.
|
||||
|
||||
### Changes
|
||||
- ✅ Updated `packages/editor-app/package.json` → `${{ github.event.inputs.version }}`
|
||||
- ✅ Updated `packages/editor-app/src-tauri/tauri.conf.json` → `${{ github.event.inputs.version }}`
|
||||
- Updated `packages/editor/editor-app/package.json` → `${{ github.event.inputs.version }}`
|
||||
- Updated `packages/editor/editor-app/src-tauri/tauri.conf.json` → `${{ github.event.inputs.version }}`
|
||||
|
||||
### Release
|
||||
- 📦 [GitHub Release](https://github.com/${{ github.repository }}/releases/tag/editor-v${{ github.event.inputs.version }})
|
||||
- [GitHub Release](https://github.com/${{ github.repository }}/releases/tag/editor-v${{ github.event.inputs.version }})
|
||||
|
||||
---
|
||||
*This PR was automatically created by the release workflow.*
|
||||
|
||||
196
.github/workflows/release.yml
vendored
196
.github/workflows/release.yml
vendored
@@ -1,29 +1,43 @@
|
||||
name: Release NPM Packages
|
||||
|
||||
on:
|
||||
# Tag trigger: supports v* and {package}-v* formats
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
- 'core-v*'
|
||||
- 'behavior-tree-v*'
|
||||
- 'editor-core-v*'
|
||||
- 'node-editor-v*'
|
||||
- 'blueprint-v*'
|
||||
- 'tilemap-v*'
|
||||
- 'physics-rapier2d-v*'
|
||||
- 'worker-generator-v*'
|
||||
|
||||
# Manual trigger option
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
package:
|
||||
description: '选择要发布的包'
|
||||
description: 'Select package to publish'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- core
|
||||
- behavior-tree
|
||||
- editor-core
|
||||
- node-editor
|
||||
- blueprint
|
||||
- tilemap
|
||||
- physics-rapier2d
|
||||
- worker-generator
|
||||
version_type:
|
||||
description: '版本更新类型'
|
||||
description: 'Version bump type'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- patch
|
||||
- minor
|
||||
- major
|
||||
- custom
|
||||
custom_version:
|
||||
description: '自定义版本号 (仅当选择 custom 时使用,例如: 2.2.9)'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -32,7 +46,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
release-package:
|
||||
name: Release ${{ github.event.inputs.package }} Package
|
||||
name: Release Package
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -41,10 +55,42 @@ jobs:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Parse tag or input
|
||||
id: parse
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "push" ]; then
|
||||
# Parse package and version from tag
|
||||
TAG="${GITHUB_REF#refs/tags/}"
|
||||
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
# Parse format: v1.0.0 or package-v1.0.0
|
||||
if [[ "$TAG" =~ ^v([0-9]+\.[0-9]+\.[0-9]+.*)$ ]]; then
|
||||
PACKAGE="core"
|
||||
VERSION="${BASH_REMATCH[1]}"
|
||||
elif [[ "$TAG" =~ ^([a-z-]+)-v([0-9]+\.[0-9]+\.[0-9]+.*)$ ]]; then
|
||||
PACKAGE="${BASH_REMATCH[1]}"
|
||||
VERSION="${BASH_REMATCH[2]}"
|
||||
else
|
||||
echo "::error::Invalid tag format: $TAG"
|
||||
echo "Expected: v1.0.0 or package-v1.0.0"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "package=$PACKAGE" >> $GITHUB_OUTPUT
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "mode=tag" >> $GITHUB_OUTPUT
|
||||
echo "Package: $PACKAGE"
|
||||
echo "Version: $VERSION"
|
||||
else
|
||||
# Manual trigger: read from package.json and bump version
|
||||
PACKAGE="${{ github.event.inputs.package }}"
|
||||
echo "package=$PACKAGE" >> $GITHUB_OUTPUT
|
||||
echo "mode=manual" >> $GITHUB_OUTPUT
|
||||
echo "version_type=${{ github.event.inputs.version_type }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
@@ -56,70 +102,124 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build core package (if needed)
|
||||
if: ${{ github.event.inputs.package == 'behavior-tree' || github.event.inputs.package == 'editor-core' }}
|
||||
- name: Verify version (tag mode)
|
||||
if: steps.parse.outputs.mode == 'tag'
|
||||
run: |
|
||||
cd packages/core
|
||||
pnpm run build
|
||||
PACKAGE="${{ steps.parse.outputs.package }}"
|
||||
EXPECTED_VERSION="${{ steps.parse.outputs.version }}"
|
||||
|
||||
# - name: Run tests
|
||||
# run: |
|
||||
# cd packages/${{ github.event.inputs.package }}
|
||||
# npm run test:ci
|
||||
# Get version from package.json
|
||||
ACTUAL_VERSION=$(node -p "require('./packages/$PACKAGE/package.json').version")
|
||||
|
||||
- name: Update version
|
||||
if [ "$EXPECTED_VERSION" != "$ACTUAL_VERSION" ]; then
|
||||
echo "::error::Version mismatch!"
|
||||
echo "Tag version: $EXPECTED_VERSION"
|
||||
echo "package.json version: $ACTUAL_VERSION"
|
||||
echo ""
|
||||
echo "Please update packages/$PACKAGE/package.json to version $EXPECTED_VERSION before tagging."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Version verified: $EXPECTED_VERSION"
|
||||
|
||||
- name: Bump version (manual mode)
|
||||
if: steps.parse.outputs.mode == 'manual'
|
||||
id: bump
|
||||
run: |
|
||||
PACKAGE="${{ steps.parse.outputs.package }}"
|
||||
cd packages/$PACKAGE
|
||||
|
||||
CURRENT=$(node -p "require('./package.json').version")
|
||||
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT"
|
||||
|
||||
case "${{ steps.parse.outputs.version_type }}" in
|
||||
major) NEW_VERSION="$((MAJOR+1)).0.0" ;;
|
||||
minor) NEW_VERSION="$MAJOR.$((MINOR+1)).0" ;;
|
||||
patch) NEW_VERSION="$MAJOR.$MINOR.$((PATCH+1))" ;;
|
||||
esac
|
||||
|
||||
# Update package.json
|
||||
node -e "const fs=require('fs'); const pkg=JSON.parse(fs.readFileSync('package.json')); pkg.version='$NEW_VERSION'; fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2)+'\n')"
|
||||
|
||||
echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Bumped version: $CURRENT -> $NEW_VERSION"
|
||||
|
||||
- name: Set final version
|
||||
id: version
|
||||
run: |
|
||||
cd packages/${{ github.event.inputs.package }}
|
||||
if [ "${{ github.event.inputs.version_type }}" = "custom" ]; then
|
||||
NEW_VERSION=${{ github.event.inputs.custom_version }}
|
||||
if [ "${{ steps.parse.outputs.mode }}" = "tag" ]; then
|
||||
echo "value=${{ steps.parse.outputs.version }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
# Get current version and bump it
|
||||
CURRENT=$(node -p "require('./package.json').version")
|
||||
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT"
|
||||
case "${{ github.event.inputs.version_type }}" in
|
||||
major) NEW_VERSION="$((MAJOR+1)).0.0" ;;
|
||||
minor) NEW_VERSION="$MAJOR.$((MINOR+1)).0" ;;
|
||||
patch) NEW_VERSION="$MAJOR.$MINOR.$((PATCH+1))" ;;
|
||||
esac
|
||||
echo "value=${{ steps.bump.outputs.version }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
# Update package.json using node
|
||||
node -e "const fs=require('fs'); const pkg=JSON.parse(fs.readFileSync('package.json')); pkg.version='$NEW_VERSION'; fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2)+'\n')"
|
||||
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "发布版本: $NEW_VERSION"
|
||||
|
||||
- name: Build core package (if needed)
|
||||
if: ${{ steps.parse.outputs.package != 'core' && steps.parse.outputs.package != 'node-editor' && steps.parse.outputs.package != 'worker-generator' }}
|
||||
run: |
|
||||
cd packages/framework/core
|
||||
pnpm run build
|
||||
|
||||
- name: Build node-editor package (if needed for blueprint)
|
||||
if: ${{ steps.parse.outputs.package == 'blueprint' }}
|
||||
run: |
|
||||
cd packages/node-editor
|
||||
pnpm run build
|
||||
|
||||
- name: Build package
|
||||
run: |
|
||||
cd packages/${{ github.event.inputs.package }}
|
||||
cd packages/${{ steps.parse.outputs.package }}
|
||||
pnpm run build:npm
|
||||
|
||||
- name: Publish to npm
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: |
|
||||
cd packages/${{ github.event.inputs.package }}/dist
|
||||
cd packages/${{ steps.parse.outputs.package }}/dist
|
||||
pnpm publish --access public --no-git-checks
|
||||
|
||||
- name: Create Pull Request
|
||||
- name: Create GitHub Release (tag mode)
|
||||
if: steps.parse.outputs.mode == 'tag'
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: ${{ steps.parse.outputs.tag }}
|
||||
name: "${{ steps.parse.outputs.package }} v${{ steps.version.outputs.value }}"
|
||||
make_latest: false
|
||||
body: |
|
||||
## @esengine/${{ steps.parse.outputs.package }} v${{ steps.version.outputs.value }}
|
||||
|
||||
**NPM**: [@esengine/${{ steps.parse.outputs.package }}@${{ steps.version.outputs.value }}](https://www.npmjs.com/package/@esengine/${{ steps.parse.outputs.package }}/v/${{ steps.version.outputs.value }})
|
||||
|
||||
```bash
|
||||
npm install @esengine/${{ steps.parse.outputs.package }}@${{ steps.version.outputs.value }}
|
||||
```
|
||||
|
||||
---
|
||||
*Auto-released by GitHub Actions*
|
||||
generate_release_notes: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create Pull Request (manual mode)
|
||||
if: steps.parse.outputs.mode == 'manual'
|
||||
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 }}
|
||||
commit-message: "chore(${{ steps.parse.outputs.package }}): release v${{ steps.version.outputs.value }}"
|
||||
branch: release/${{ steps.parse.outputs.package }}-v${{ steps.version.outputs.value }}
|
||||
delete-branch: true
|
||||
title: "chore(${{ github.event.inputs.package }}): Release v${{ steps.version.outputs.new_version }}"
|
||||
title: "chore(${{ steps.parse.outputs.package }}): Release v${{ steps.version.outputs.value }}"
|
||||
body: |
|
||||
## 🚀 Release v${{ steps.version.outputs.new_version }}
|
||||
## Release v${{ steps.version.outputs.value }}
|
||||
|
||||
此 PR 更新 `@esengine/${{ github.event.inputs.package }}` 包的版本号
|
||||
This PR updates `@esengine/${{ steps.parse.outputs.package }}` package version.
|
||||
|
||||
### 变更
|
||||
- ✅ 已发布到 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 }}`
|
||||
### Changes
|
||||
- Published to npm: [@esengine/${{ steps.parse.outputs.package }}@${{ steps.version.outputs.value }}](https://www.npmjs.com/package/@esengine/${{ steps.parse.outputs.package }}/v/${{ steps.version.outputs.value }})
|
||||
- Updated `packages/${{ steps.parse.outputs.package }}/package.json` to `${{ steps.version.outputs.value }}`
|
||||
|
||||
---
|
||||
*此 PR 由发布工作流自动创建*
|
||||
*This PR was automatically created by the release workflow*
|
||||
labels: |
|
||||
release
|
||||
${{ github.event.inputs.package }}
|
||||
${{ steps.parse.outputs.package }}
|
||||
automated pr
|
||||
|
||||
48
.github/workflows/size-limit.yml
vendored
48
.github/workflows/size-limit.yml
vendored
@@ -1,48 +0,0 @@
|
||||
name: Size Limit
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
paths:
|
||||
- 'packages/core/src/**'
|
||||
- 'packages/core/package.json'
|
||||
- '.size-limit.json'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
size:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build core package
|
||||
run: |
|
||||
cd packages/core
|
||||
pnpm run build:npm
|
||||
|
||||
- name: Check bundle size
|
||||
uses: andresz1/size-limit-action@v1
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
skip_step: install
|
||||
58
.github/workflows/welcome.yml
vendored
58
.github/workflows/welcome.yml
vendored
@@ -1,58 +0,0 @@
|
||||
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
.gitignore
vendored
15
.gitignore
vendored
@@ -18,6 +18,9 @@ dist/
|
||||
.cache/
|
||||
.build-cache/
|
||||
|
||||
# Turborepo
|
||||
.turbo/
|
||||
|
||||
# IDE 配置
|
||||
.idea/
|
||||
.vscode/
|
||||
@@ -45,6 +48,14 @@ logs/
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# 代码签名证书(敏感文件)
|
||||
certs/
|
||||
*.pfx
|
||||
*.p12
|
||||
*.cer
|
||||
*.pem
|
||||
*.key
|
||||
|
||||
# 测试覆盖率
|
||||
coverage/
|
||||
*.lcov
|
||||
@@ -79,3 +90,7 @@ docs/.vitepress/dist/
|
||||
# Tauri 捆绑输出
|
||||
**/src-tauri/target/release/bundle/
|
||||
**/src-tauri/target/debug/bundle/
|
||||
|
||||
# Rust 构建产物
|
||||
**/engine-shared/target/
|
||||
external/
|
||||
|
||||
@@ -37,9 +37,6 @@ This project follows the [Conventional Commits](https://www.conventionalcommits.
|
||||
|
||||
- **core**: 核心包 @esengine/ecs-framework
|
||||
- **math**: 数学库包
|
||||
- **network-client**: 网络客户端包
|
||||
- **network-server**: 网络服务端包
|
||||
- **network-shared**: 网络共享包
|
||||
- **editor**: 编辑器
|
||||
- **docs**: 文档
|
||||
|
||||
@@ -122,9 +119,9 @@ npm run format
|
||||
|
||||
## 问题反馈 / Issue Reporting
|
||||
|
||||
如果你发现了 bug 或有新功能建议,请[创建 Issue](https://github.com/esengine/ecs-framework/issues/new)。
|
||||
如果你发现了 bug 或有新功能建议,请[创建 Issue](https://github.com/esengine/esengine/issues/new)。
|
||||
|
||||
If you find a bug or have a feature request, please [create an 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/esengine/issues/new).
|
||||
|
||||
## 许可证 / License
|
||||
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 ECS Framework
|
||||
Copyright (c) 2025 ESEngine Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
424
README.md
424
README.md
@@ -1,90 +1,93 @@
|
||||
# ECS Framework
|
||||
<h1 align="center">
|
||||
<img src="https://raw.githubusercontent.com/esengine/esengine/master/docs/public/logo.svg" alt="ESEngine" width="180">
|
||||
<br>
|
||||
ESEngine
|
||||
</h1>
|
||||
|
||||
[](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)
|
||||
<p align="center">
|
||||
<strong>Modular Game Framework for TypeScript</strong>
|
||||
</p>
|
||||
|
||||
<div align="center">
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/package/@esengine/ecs-framework"><img src="https://img.shields.io/npm/v/@esengine/ecs-framework?style=flat-square&color=blue" alt="npm"></a>
|
||||
<a href="https://github.com/esengine/esengine/actions"><img src="https://img.shields.io/github/actions/workflow/status/esengine/esengine/ci.yml?branch=master&style=flat-square" alt="build"></a>
|
||||
<a href="https://github.com/esengine/esengine/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="license"></a>
|
||||
<a href="https://github.com/esengine/esengine/stargazers"><img src="https://img.shields.io/github/stars/esengine/esengine?style=flat-square" alt="stars"></a>
|
||||
<img src="https://img.shields.io/badge/TypeScript-5.0+-blue?style=flat-square&logo=typescript&logoColor=white" alt="TypeScript">
|
||||
</p>
|
||||
|
||||
<p>一个高性能的 TypeScript ECS (Entity-Component-System) 框架,专为现代游戏开发而设计。</p>
|
||||
<p align="center">
|
||||
<b>English</b> | <a href="./README_CN.md">中文</a>
|
||||
</p>
|
||||
|
||||
<p>A high-performance TypeScript ECS (Entity-Component-System) framework designed for modern game development.</p>
|
||||
|
||||
</div>
|
||||
<p align="center">
|
||||
<a href="https://esengine.cn/">Documentation</a> ·
|
||||
<a href="https://esengine.cn/api/README">API Reference</a> ·
|
||||
<a href="./examples/">Examples</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 📊 项目统计 / Project Stats
|
||||
## What is ESEngine?
|
||||
|
||||
<div align="center">
|
||||
ESEngine is a collection of **engine-agnostic game development modules** for TypeScript. Use them with Cocos Creator, Laya, Phaser, PixiJS, or any JavaScript game engine.
|
||||
|
||||
[](https://star-history.com/#esengine/ecs-framework&Date)
|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
||||
<a href="https://github.com/esengine/ecs-framework/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=esengine/ecs-framework" />
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
### 📈 下载趋势 / Download Trends
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://www.npmjs.com/package/@esengine/ecs-framework)
|
||||
|
||||
[](https://npmtrends.com/@esengine/ecs-framework)
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 特性
|
||||
|
||||
- **高性能** - 针对大规模实体优化,支持SoA存储和批量处理
|
||||
- **多线程计算** - Worker系统支持真正的并行处理,充分利用多核CPU性能
|
||||
- **类型安全** - 完整的TypeScript支持,编译时类型检查
|
||||
- **现代架构** - 支持多World、多Scene的分层架构设计
|
||||
- **开发友好** - 内置调试工具和性能监控
|
||||
- **跨平台** - 支持Cocos Creator、Laya引擎和Web平台
|
||||
|
||||
## 安装
|
||||
The core is a high-performance **ECS (Entity-Component-System)** framework, accompanied by optional modules for AI, networking, physics, and more.
|
||||
|
||||
```bash
|
||||
npm install @esengine/ecs-framework
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
## Features
|
||||
|
||||
| Module | Description | Engine Required |
|
||||
|--------|-------------|:---------------:|
|
||||
| **ECS Core** | Entity-Component-System framework with reactive queries | No |
|
||||
| **Behavior Tree** | AI behavior trees with visual editor support | No |
|
||||
| **Blueprint** | Visual scripting system | No |
|
||||
| **FSM** | Finite state machine | No |
|
||||
| **Timer** | Timer and cooldown systems | No |
|
||||
| **Spatial** | Spatial indexing and queries (QuadTree, Grid) | No |
|
||||
| **Pathfinding** | A* and navigation mesh pathfinding | No |
|
||||
| **Network** | Client/server networking with TSRPC | No |
|
||||
|
||||
> All framework modules can be used standalone with any rendering engine.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Using CLI (Recommended)
|
||||
|
||||
The easiest way to add ECS to your existing project:
|
||||
|
||||
```bash
|
||||
# In your project directory
|
||||
npx @esengine/cli init
|
||||
```
|
||||
|
||||
The CLI automatically detects your project type (Cocos Creator 2.x/3.x, LayaAir 3.x, or Node.js) and generates the necessary integration code.
|
||||
|
||||
### Manual Setup
|
||||
|
||||
```typescript
|
||||
import { Core, Scene, Component, EntitySystem, ECSComponent, ECSSystem, Matcher, Time } from '@esengine/ecs-framework';
|
||||
import {
|
||||
Core, Scene, Entity, Component, EntitySystem,
|
||||
Matcher, Time, ECSComponent, ECSSystem
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
// 定义组件
|
||||
// Define components (data only)
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
constructor(public x = 0, public y = 0) {
|
||||
super();
|
||||
}
|
||||
x = 0;
|
||||
y = 0;
|
||||
}
|
||||
|
||||
@ECSComponent('Velocity')
|
||||
class Velocity extends Component {
|
||||
constructor(public dx = 0, public dy = 0) {
|
||||
super();
|
||||
}
|
||||
dx = 0;
|
||||
dy = 0;
|
||||
}
|
||||
|
||||
// 创建系统
|
||||
// Define system (logic)
|
||||
@ECSSystem('Movement')
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
@@ -93,182 +96,201 @@ class MovementSystem extends EntitySystem {
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const position = entity.getComponent(Position)!;
|
||||
const velocity = entity.getComponent(Velocity)!;
|
||||
|
||||
position.x += velocity.dx * Time.deltaTime;
|
||||
position.y += velocity.dy * Time.deltaTime;
|
||||
const pos = entity.getComponent(Position);
|
||||
const vel = entity.getComponent(Velocity);
|
||||
pos.x += vel.dx * Time.deltaTime;
|
||||
pos.y += vel.dy * Time.deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建场景并启动
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.addSystem(new MovementSystem());
|
||||
// Initialize
|
||||
Core.create();
|
||||
const scene = new Scene();
|
||||
scene.addSystem(new MovementSystem());
|
||||
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Position(100, 100));
|
||||
player.addComponent(new Velocity(50, 0));
|
||||
const player = scene.createEntity('Player');
|
||||
player.addComponent(new Position());
|
||||
player.addComponent(new Velocity());
|
||||
|
||||
Core.setScene(scene);
|
||||
|
||||
// Integrate with your game loop
|
||||
function gameLoop(currentTime: number) {
|
||||
Core.update(currentTime / 1000);
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
requestAnimationFrame(gameLoop);
|
||||
```
|
||||
|
||||
## Using with Other Engines
|
||||
|
||||
ESEngine's framework modules are designed to work alongside your preferred rendering engine:
|
||||
|
||||
### With Cocos Creator
|
||||
|
||||
```typescript
|
||||
import { Component as CCComponent, _decorator } from 'cc';
|
||||
import { Core, Scene, Matcher, EntitySystem } from '@esengine/ecs-framework';
|
||||
import { BehaviorTreeExecutionSystem } from '@esengine/behavior-tree';
|
||||
|
||||
const { ccclass } = _decorator;
|
||||
|
||||
@ccclass('GameManager')
|
||||
export class GameManager extends CCComponent {
|
||||
private ecsScene!: Scene;
|
||||
|
||||
start() {
|
||||
Core.create();
|
||||
this.ecsScene = new Scene();
|
||||
|
||||
// Add ECS systems
|
||||
this.ecsScene.addSystem(new BehaviorTreeExecutionSystem());
|
||||
this.ecsScene.addSystem(new MyGameSystem());
|
||||
|
||||
Core.setScene(this.ecsScene);
|
||||
}
|
||||
|
||||
update(dt: number) {
|
||||
Core.update(dt);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
// 启动游戏
|
||||
Core.create();
|
||||
Core.setScene(new GameScene());
|
||||
### With Laya 3.x
|
||||
|
||||
// 游戏循环中更新
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime);
|
||||
```typescript
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
import { FSMSystem } from '@esengine/fsm';
|
||||
|
||||
const { regClass } = Laya;
|
||||
|
||||
@regClass()
|
||||
export class ECSManager extends Laya.Script {
|
||||
private ecsScene = new Scene();
|
||||
|
||||
onAwake(): void {
|
||||
Core.create();
|
||||
this.ecsScene.addSystem(new FSMSystem());
|
||||
Core.setScene(this.ecsScene);
|
||||
}
|
||||
|
||||
onUpdate(): void {
|
||||
Core.update(Laya.timer.delta / 1000);
|
||||
}
|
||||
|
||||
onDestroy(): void {
|
||||
Core.destroy();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 核心特性
|
||||
## Packages
|
||||
|
||||
- **实体查询** - 使用 Matcher API 进行高效的实体过滤
|
||||
- **事件系统** - 类型安全的事件发布/订阅机制
|
||||
- **性能优化** - SoA 存储优化,支持大规模实体处理
|
||||
- **多线程支持** - Worker系统实现真正的并行计算,充分利用多核CPU
|
||||
- **多场景** - 支持 World/Scene 分层架构
|
||||
- **时间管理** - 内置定时器和时间控制系统
|
||||
### Framework (Engine-Agnostic)
|
||||
|
||||
## 🏗️ 架构设计 / Architecture
|
||||
These packages have **zero rendering dependencies** and work with any engine:
|
||||
|
||||
```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
|
||||
```bash
|
||||
npm install @esengine/ecs-framework # Core ECS
|
||||
npm install @esengine/behavior-tree # AI behavior trees
|
||||
npm install @esengine/blueprint # Visual scripting
|
||||
npm install @esengine/fsm # State machines
|
||||
npm install @esengine/timer # Timers & cooldowns
|
||||
npm install @esengine/spatial # Spatial indexing
|
||||
npm install @esengine/pathfinding # Pathfinding
|
||||
npm install @esengine/network # Networking
|
||||
```
|
||||
|
||||
## 平台支持
|
||||
### ESEngine Runtime (Optional)
|
||||
|
||||
支持主流游戏引擎和 Web 平台:
|
||||
If you want a complete engine solution with rendering:
|
||||
|
||||
- **Cocos Creator**
|
||||
- **Laya 引擎**
|
||||
- **原生 Web** - 浏览器环境直接运行
|
||||
- **小游戏平台** - 微信、支付宝等小游戏
|
||||
| Category | Packages |
|
||||
|----------|----------|
|
||||
| **Core** | `engine-core`, `asset-system`, `material-system` |
|
||||
| **Rendering** | `sprite`, `tilemap`, `particle`, `camera`, `mesh-3d` |
|
||||
| **Physics** | `physics-rapier2d` |
|
||||
| **Platform** | `platform-web`, `platform-wechat` |
|
||||
|
||||
## ECS Framework Editor
|
||||
### Editor (Optional)
|
||||
|
||||
跨平台桌面编辑器,提供可视化开发和调试工具。
|
||||
A visual editor built with Tauri for scene management:
|
||||
|
||||
### 主要功能
|
||||
- Download from [Releases](https://github.com/esengine/esengine/releases)
|
||||
- Supports behavior tree editing, tilemap painting, visual scripting
|
||||
|
||||
- **场景管理** - 可视化场景层级和实体管理
|
||||
- **组件检视** - 实时查看和编辑实体组件
|
||||
- **性能分析** - 内置 Profiler 监控系统性能
|
||||
- **插件系统** - 可扩展的插件架构
|
||||
- **远程调试** - 连接运行中的游戏进行实时调试
|
||||
- **自动更新** - 支持热更新,自动获取最新版本
|
||||
## Project Structure
|
||||
|
||||
### 下载
|
||||
```
|
||||
esengine/
|
||||
├── packages/
|
||||
│ ├── framework/ # Engine-agnostic modules (NPM publishable)
|
||||
│ │ ├── core/ # ECS Framework
|
||||
│ │ ├── math/ # Math utilities
|
||||
│ │ ├── behavior-tree/ # AI behavior trees
|
||||
│ │ ├── blueprint/ # Visual scripting
|
||||
│ │ ├── fsm/ # Finite state machine
|
||||
│ │ ├── timer/ # Timer system
|
||||
│ │ ├── spatial/ # Spatial queries
|
||||
│ │ ├── pathfinding/ # Pathfinding
|
||||
│ │ ├── procgen/ # Procedural generation
|
||||
│ │ └── network/ # Networking
|
||||
│ │
|
||||
│ ├── engine/ # ESEngine runtime
|
||||
│ ├── rendering/ # Rendering modules
|
||||
│ ├── physics/ # Physics modules
|
||||
│ ├── editor/ # Visual editor
|
||||
│ └── rust/ # WASM renderer
|
||||
│
|
||||
├── docs/ # Documentation
|
||||
└── examples/ # Examples
|
||||
```
|
||||
|
||||
[](https://github.com/esengine/ecs-framework/releases/latest)
|
||||
## Building from Source
|
||||
|
||||
支持 Windows、macOS (Intel & Apple Silicon)
|
||||
```bash
|
||||
git clone https://github.com/esengine/esengine.git
|
||||
cd esengine
|
||||
|
||||
### 截图
|
||||
pnpm install
|
||||
pnpm build
|
||||
|
||||
<img src="screenshots/main_screetshot.png" alt="ECS Framework Editor" width="800">
|
||||
# Type check framework packages
|
||||
pnpm type-check:framework
|
||||
|
||||
<details>
|
||||
<summary>查看更多截图</summary>
|
||||
# Run tests
|
||||
pnpm test
|
||||
```
|
||||
|
||||
**性能分析器**
|
||||
<img src="screenshots/performance_profiler.png" alt="Performance Profiler" width="600">
|
||||
## Documentation
|
||||
|
||||
**插件管理**
|
||||
<img src="screenshots/plugin_manager.png" alt="Plugin Manager" width="600">
|
||||
- [ECS Framework Guide](./packages/framework/core/README.md)
|
||||
- [Behavior Tree Guide](./packages/framework/behavior-tree/README.md)
|
||||
- [API Reference](https://esengine.cn/api/README)
|
||||
|
||||
**设置界面**
|
||||
<img src="screenshots/settings.png" alt="Settings" width="600">
|
||||
## Community
|
||||
|
||||
</details>
|
||||
- [GitHub Issues](https://github.com/esengine/esengine/issues) - Bug reports and feature requests
|
||||
- [GitHub Discussions](https://github.com/esengine/esengine/discussions) - Questions and ideas
|
||||
- [Discord](https://discord.gg/gCAgzXFW) - Chat with the community
|
||||
|
||||
## 示例项目
|
||||
## Contributing
|
||||
|
||||
- [Worker系统演示](https://esengine.github.io/ecs-framework/demos/worker-system/) - 多线程物理系统演示,展示高性能并行计算
|
||||
- [割草机演示](https://github.com/esengine/lawn-mower-demo) - 完整的游戏示例
|
||||
Contributions are welcome! Please read our contributing guidelines before submitting a pull request.
|
||||
|
||||
## 文档
|
||||
1. Fork the repository
|
||||
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
||||
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
||||
4. Push to the branch (`git push origin feature/amazing-feature`)
|
||||
5. Open a Pull Request
|
||||
|
||||
- [📚 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 文档
|
||||
## License
|
||||
|
||||
## 生态系统
|
||||
ESEngine is licensed under the [MIT License](LICENSE). Free for personal and commercial use.
|
||||
|
||||
- [路径寻找](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
|
||||
<p align="center">
|
||||
Made with care by the ESEngine community
|
||||
</p>
|
||||
|
||||
297
README_CN.md
Normal file
297
README_CN.md
Normal file
@@ -0,0 +1,297 @@
|
||||
<h1 align="center">
|
||||
<img src="https://raw.githubusercontent.com/esengine/esengine/master/docs/public/logo.svg" alt="ESEngine" width="180">
|
||||
<br>
|
||||
ESEngine
|
||||
</h1>
|
||||
|
||||
<p align="center">
|
||||
<strong>TypeScript 模块化游戏框架</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/package/@esengine/ecs-framework"><img src="https://img.shields.io/npm/v/@esengine/ecs-framework?style=flat-square&color=blue" alt="npm"></a>
|
||||
<a href="https://github.com/esengine/esengine/actions"><img src="https://img.shields.io/github/actions/workflow/status/esengine/esengine/ci.yml?branch=master&style=flat-square" alt="build"></a>
|
||||
<a href="https://github.com/esengine/esengine/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="license"></a>
|
||||
<a href="https://github.com/esengine/esengine/stargazers"><img src="https://img.shields.io/github/stars/esengine/esengine?style=flat-square" alt="stars"></a>
|
||||
<img src="https://img.shields.io/badge/TypeScript-5.0+-blue?style=flat-square&logo=typescript&logoColor=white" alt="TypeScript">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="./README.md">English</a> | <b>中文</b>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://esengine.cn/">文档</a> ·
|
||||
<a href="https://esengine.cn/api/README">API 参考</a> ·
|
||||
<a href="./examples/">示例</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## ESEngine 是什么?
|
||||
|
||||
ESEngine 是一套**引擎无关的游戏开发模块**,可与 Cocos Creator、Laya、Phaser、PixiJS 等任何 JavaScript 游戏引擎配合使用。
|
||||
|
||||
核心是一个高性能的 **ECS(实体-组件-系统)** 框架,配套 AI、网络、物理等可选模块。
|
||||
|
||||
```bash
|
||||
npm install @esengine/ecs-framework
|
||||
```
|
||||
|
||||
## 功能模块
|
||||
|
||||
| 模块 | 描述 | 需要渲染引擎 |
|
||||
|------|------|:----------:|
|
||||
| **ECS 核心** | 实体-组件-系统框架,支持响应式查询 | 否 |
|
||||
| **行为树** | AI 行为树,支持可视化编辑 | 否 |
|
||||
| **蓝图** | 可视化脚本系统 | 否 |
|
||||
| **状态机** | 有限状态机 | 否 |
|
||||
| **定时器** | 定时器和冷却系统 | 否 |
|
||||
| **空间索引** | 空间查询(四叉树、网格) | 否 |
|
||||
| **寻路** | A* 和导航网格寻路 | 否 |
|
||||
| **网络** | 客户端/服务端网络通信 (TSRPC) | 否 |
|
||||
|
||||
> 所有框架模块都可以独立使用,无需依赖特定渲染引擎。
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 使用 CLI(推荐)
|
||||
|
||||
在现有项目中添加 ECS 的最简单方式:
|
||||
|
||||
```bash
|
||||
# 在项目目录中运行
|
||||
npx @esengine/cli init
|
||||
```
|
||||
|
||||
CLI 会自动检测项目类型(Cocos Creator 2.x/3.x、LayaAir 3.x 或 Node.js)并生成相应的集成代码。
|
||||
|
||||
### 手动配置
|
||||
|
||||
```typescript
|
||||
import {
|
||||
Core, Scene, Entity, Component, EntitySystem,
|
||||
Matcher, Time, ECSComponent, ECSSystem
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
// 定义组件(纯数据)
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
x = 0;
|
||||
y = 0;
|
||||
}
|
||||
|
||||
@ECSComponent('Velocity')
|
||||
class Velocity extends Component {
|
||||
dx = 0;
|
||||
dy = 0;
|
||||
}
|
||||
|
||||
// 定义系统(逻辑)
|
||||
@ECSSystem('Movement')
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Position, Velocity));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const pos = entity.getComponent(Position);
|
||||
const vel = entity.getComponent(Velocity);
|
||||
pos.x += vel.dx * Time.deltaTime;
|
||||
pos.y += vel.dy * Time.deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
Core.create();
|
||||
const scene = new Scene();
|
||||
scene.addSystem(new MovementSystem());
|
||||
|
||||
const player = scene.createEntity('Player');
|
||||
player.addComponent(new Position());
|
||||
player.addComponent(new Velocity());
|
||||
|
||||
Core.setScene(scene);
|
||||
|
||||
// 集成到你的游戏循环
|
||||
function gameLoop(currentTime: number) {
|
||||
Core.update(currentTime / 1000);
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
requestAnimationFrame(gameLoop);
|
||||
```
|
||||
|
||||
## 与其他引擎配合使用
|
||||
|
||||
ESEngine 的框架模块设计为可与你喜欢的渲染引擎配合使用:
|
||||
|
||||
### 与 Cocos Creator 配合
|
||||
|
||||
```typescript
|
||||
import { Component as CCComponent, _decorator } from 'cc';
|
||||
import { Core, Scene, Matcher, EntitySystem } from '@esengine/ecs-framework';
|
||||
import { BehaviorTreeExecutionSystem } from '@esengine/behavior-tree';
|
||||
|
||||
const { ccclass } = _decorator;
|
||||
|
||||
@ccclass('GameManager')
|
||||
export class GameManager extends CCComponent {
|
||||
private ecsScene!: Scene;
|
||||
|
||||
start() {
|
||||
Core.create();
|
||||
this.ecsScene = new Scene();
|
||||
|
||||
// 添加 ECS 系统
|
||||
this.ecsScene.addSystem(new BehaviorTreeExecutionSystem());
|
||||
this.ecsScene.addSystem(new MyGameSystem());
|
||||
|
||||
Core.setScene(this.ecsScene);
|
||||
}
|
||||
|
||||
update(dt: number) {
|
||||
Core.update(dt);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 与 Laya 3.x 配合
|
||||
|
||||
```typescript
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
import { FSMSystem } from '@esengine/fsm';
|
||||
|
||||
const { regClass } = Laya;
|
||||
|
||||
@regClass()
|
||||
export class ECSManager extends Laya.Script {
|
||||
private ecsScene = new Scene();
|
||||
|
||||
onAwake(): void {
|
||||
Core.create();
|
||||
this.ecsScene.addSystem(new FSMSystem());
|
||||
Core.setScene(this.ecsScene);
|
||||
}
|
||||
|
||||
onUpdate(): void {
|
||||
Core.update(Laya.timer.delta / 1000);
|
||||
}
|
||||
|
||||
onDestroy(): void {
|
||||
Core.destroy();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 包列表
|
||||
|
||||
### 框架包(引擎无关)
|
||||
|
||||
这些包**零渲染依赖**,可与任何引擎配合使用:
|
||||
|
||||
```bash
|
||||
npm install @esengine/ecs-framework # ECS 核心
|
||||
npm install @esengine/behavior-tree # AI 行为树
|
||||
npm install @esengine/blueprint # 可视化脚本
|
||||
npm install @esengine/fsm # 状态机
|
||||
npm install @esengine/timer # 定时器和冷却
|
||||
npm install @esengine/spatial # 空间索引
|
||||
npm install @esengine/pathfinding # 寻路
|
||||
npm install @esengine/network # 网络
|
||||
```
|
||||
|
||||
### ESEngine 运行时(可选)
|
||||
|
||||
如果你需要完整的引擎解决方案:
|
||||
|
||||
| 分类 | 包名 |
|
||||
|------|------|
|
||||
| **核心** | `engine-core`, `asset-system`, `material-system` |
|
||||
| **渲染** | `sprite`, `tilemap`, `particle`, `camera`, `mesh-3d` |
|
||||
| **物理** | `physics-rapier2d` |
|
||||
| **平台** | `platform-web`, `platform-wechat` |
|
||||
|
||||
### 编辑器(可选)
|
||||
|
||||
基于 Tauri 构建的可视化编辑器:
|
||||
|
||||
- 从 [Releases](https://github.com/esengine/esengine/releases) 下载
|
||||
- 支持行为树编辑、Tilemap 绘制、可视化脚本
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
esengine/
|
||||
├── packages/
|
||||
│ ├── framework/ # 引擎无关模块(可发布到 NPM)
|
||||
│ │ ├── core/ # ECS 框架
|
||||
│ │ ├── math/ # 数学工具
|
||||
│ │ ├── behavior-tree/ # AI 行为树
|
||||
│ │ ├── blueprint/ # 可视化脚本
|
||||
│ │ ├── fsm/ # 有限状态机
|
||||
│ │ ├── timer/ # 定时器系统
|
||||
│ │ ├── spatial/ # 空间查询
|
||||
│ │ ├── pathfinding/ # 寻路
|
||||
│ │ ├── procgen/ # 程序化生成
|
||||
│ │ └── network/ # 网络
|
||||
│ │
|
||||
│ ├── engine/ # ESEngine 运行时
|
||||
│ ├── rendering/ # 渲染模块
|
||||
│ ├── physics/ # 物理模块
|
||||
│ ├── editor/ # 可视化编辑器
|
||||
│ └── rust/ # WASM 渲染器
|
||||
│
|
||||
├── docs/ # 文档
|
||||
└── examples/ # 示例
|
||||
```
|
||||
|
||||
## 从源码构建
|
||||
|
||||
```bash
|
||||
git clone https://github.com/esengine/esengine.git
|
||||
cd esengine
|
||||
|
||||
pnpm install
|
||||
pnpm build
|
||||
|
||||
# 框架包类型检查
|
||||
pnpm type-check:framework
|
||||
|
||||
# 运行测试
|
||||
pnpm test
|
||||
```
|
||||
|
||||
## 文档
|
||||
|
||||
- [ECS 框架指南](./packages/framework/core/README.md)
|
||||
- [行为树指南](./packages/framework/behavior-tree/README.md)
|
||||
- [API 参考](https://esengine.cn/api/README)
|
||||
|
||||
## 社区
|
||||
|
||||
- [QQ 交流群](https://jq.qq.com/?_wv=1027&k=29w1Nud6) - 中文社区
|
||||
- [Discord](https://discord.gg/gCAgzXFW) - 国际社区
|
||||
- [GitHub Issues](https://github.com/esengine/esengine/issues) - Bug 反馈和功能建议
|
||||
- [GitHub Discussions](https://github.com/esengine/esengine/discussions) - 问题和想法
|
||||
|
||||
## 贡献
|
||||
|
||||
欢迎贡献代码!提交 PR 前请阅读贡献指南。
|
||||
|
||||
1. Fork 仓库
|
||||
2. 创建功能分支 (`git checkout -b feature/amazing-feature`)
|
||||
3. 提交修改 (`git commit -m 'Add amazing feature'`)
|
||||
4. 推送分支 (`git push origin feature/amazing-feature`)
|
||||
5. 发起 Pull Request
|
||||
|
||||
## 许可证
|
||||
|
||||
ESEngine 基于 [MIT 协议](LICENSE) 开源,个人和商业使用均免费。
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
由 ESEngine 社区用心打造
|
||||
</p>
|
||||
76
SECURITY.md
76
SECURITY.md
@@ -1,13 +1,71 @@
|
||||
# Security Policy / 安全政策
|
||||
|
||||
**English** | [中文](#安全政策-1)
|
||||
|
||||
## Supported Versions
|
||||
|
||||
We provide security updates for the following versions:
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 2.x.x | :white_check_mark: |
|
||||
| 1.x.x | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you discover a security vulnerability, please report it through the following channels:
|
||||
|
||||
### Reporting Channels
|
||||
|
||||
- **GitHub Security Advisories**: [Report a vulnerability](https://github.com/esengine/esengine/security/advisories/new) (Recommended)
|
||||
- **Email**: security@esengine.dev
|
||||
|
||||
### Reporting Guidelines
|
||||
|
||||
1. **Do NOT** report security vulnerabilities in public issues
|
||||
2. Provide a detailed description of the vulnerability, including:
|
||||
- Affected versions
|
||||
- Steps to reproduce
|
||||
- Potential impact
|
||||
- Suggested fix (if available)
|
||||
|
||||
### Response Timeline
|
||||
|
||||
- **Acknowledgment**: Within 72 hours
|
||||
- **Initial Assessment**: Within 1 week
|
||||
- **Fix Release**: Typically within 2-4 weeks, depending on severity
|
||||
|
||||
### Process
|
||||
|
||||
1. We will confirm the existence and severity of the vulnerability
|
||||
2. Develop and test a fix
|
||||
3. Release a security update
|
||||
4. Publicly disclose the vulnerability details after the fix is released
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
When using ESEngine, please follow these security recommendations:
|
||||
|
||||
- Always use the latest stable version
|
||||
- Regularly update dependencies
|
||||
- Disable debug mode in production
|
||||
- Validate all external input data
|
||||
- Do not store sensitive information on the client side
|
||||
|
||||
---
|
||||
|
||||
# 安全政策
|
||||
|
||||
[English](#security-policy--安全政策) | **中文**
|
||||
|
||||
## 支持的版本
|
||||
|
||||
我们为以下版本提供安全更新:
|
||||
|
||||
| 版本 | 支持状态 |
|
||||
| ------- | ------------------ |
|
||||
| 2.0.x | :white_check_mark: |
|
||||
| 1.0.x | :x: |
|
||||
| 2.x.x | :white_check_mark: |
|
||||
| 1.x.x | :x: |
|
||||
|
||||
## 报告漏洞
|
||||
|
||||
@@ -15,10 +73,10 @@
|
||||
|
||||
### 报告渠道
|
||||
|
||||
- **邮箱**: [安全邮箱将在实际部署时提供]
|
||||
- **GitHub**: 创建私有安全报告(推荐)
|
||||
- **GitHub 安全公告**: [报告漏洞](https://github.com/esengine/esengine/security/advisories/new)(推荐)
|
||||
- **邮箱**: security@esengine.dev
|
||||
|
||||
### 报告流程
|
||||
### 报告指南
|
||||
|
||||
1. **不要**在公开的 issue 中报告安全漏洞
|
||||
2. 提供详细的漏洞描述,包括:
|
||||
@@ -40,9 +98,9 @@
|
||||
3. 发布安全更新
|
||||
4. 在修复发布后,会在相关渠道公布漏洞详情
|
||||
|
||||
### 安全最佳实践
|
||||
## 安全最佳实践
|
||||
|
||||
使用 ECS Framework 时,请遵循以下安全建议:
|
||||
使用 ESEngine 时,请遵循以下安全建议:
|
||||
|
||||
- 始终使用最新的稳定版本
|
||||
- 定期更新依赖项
|
||||
@@ -50,4 +108,6 @@
|
||||
- 验证所有外部输入数据
|
||||
- 不要在客户端存储敏感信息
|
||||
|
||||
感谢您帮助保持 ECS Framework 的安全性!
|
||||
感谢您帮助保持 ESEngine 的安全性!
|
||||
|
||||
Thank you for helping keep ESEngine secure!
|
||||
|
||||
@@ -6,9 +6,189 @@ import { fileURLToPath } from 'url'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const corePackageJson = JSON.parse(
|
||||
readFileSync(join(__dirname, '../../packages/core/package.json'), 'utf-8')
|
||||
readFileSync(join(__dirname, '../../packages/framework/core/package.json'), 'utf-8')
|
||||
)
|
||||
|
||||
// Import i18n messages
|
||||
import en from './i18n/en.json' with { type: 'json' }
|
||||
import zh from './i18n/zh.json' with { type: 'json' }
|
||||
|
||||
// 创建侧边栏配置 | Create sidebar config
|
||||
// prefix: 路径前缀,如 '' 或 '/en' | Path prefix like '' or '/en'
|
||||
function createSidebar(t, prefix = '') {
|
||||
return {
|
||||
[`${prefix}/guide/`]: [
|
||||
{
|
||||
text: t.sidebar.gettingStarted,
|
||||
items: [
|
||||
{ text: t.sidebar.quickStart, link: `${prefix}/guide/getting-started` },
|
||||
{ text: t.sidebar.guideOverview, link: `${prefix}/guide/` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.coreConcepts,
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: t.sidebar.entity, link: `${prefix}/guide/entity` },
|
||||
{ text: t.sidebar.hierarchy, link: `${prefix}/guide/hierarchy` },
|
||||
{ text: t.sidebar.component, link: `${prefix}/guide/component` },
|
||||
{ text: t.sidebar.entityQuery, link: `${prefix}/guide/entity-query` },
|
||||
{
|
||||
text: t.sidebar.system,
|
||||
link: `${prefix}/guide/system`,
|
||||
items: [
|
||||
{ text: t.sidebar.workerSystem, link: `${prefix}/guide/worker-system` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.scene,
|
||||
link: `${prefix}/guide/scene`,
|
||||
items: [
|
||||
{ text: t.sidebar.sceneManager, link: `${prefix}/guide/scene-manager` },
|
||||
{ text: t.sidebar.worldManager, link: `${prefix}/guide/world-manager` },
|
||||
{ text: t.sidebar.persistentEntity, link: `${prefix}/guide/persistent-entity` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.behaviorTree,
|
||||
link: `${prefix}/guide/behavior-tree/`,
|
||||
items: [
|
||||
{ text: t.sidebar.btGettingStarted, link: `${prefix}/guide/behavior-tree/getting-started` },
|
||||
{ text: t.sidebar.btCoreConcepts, link: `${prefix}/guide/behavior-tree/core-concepts` },
|
||||
{ text: t.sidebar.btEditorGuide, link: `${prefix}/guide/behavior-tree/editor-guide` },
|
||||
{ text: t.sidebar.btEditorWorkflow, link: `${prefix}/guide/behavior-tree/editor-workflow` },
|
||||
{ text: t.sidebar.btCustomActions, link: `${prefix}/guide/behavior-tree/custom-actions` },
|
||||
{ text: t.sidebar.btCocosIntegration, link: `${prefix}/guide/behavior-tree/cocos-integration` },
|
||||
{ text: t.sidebar.btLayaIntegration, link: `${prefix}/guide/behavior-tree/laya-integration` },
|
||||
{ text: t.sidebar.btAdvancedUsage, link: `${prefix}/guide/behavior-tree/advanced-usage` },
|
||||
{ text: t.sidebar.btBestPractices, link: `${prefix}/guide/behavior-tree/best-practices` }
|
||||
]
|
||||
},
|
||||
{ text: t.sidebar.serialization, link: `${prefix}/guide/serialization` },
|
||||
{ text: t.sidebar.eventSystem, link: `${prefix}/guide/event-system` },
|
||||
{ text: t.sidebar.timeAndTimers, link: `${prefix}/guide/time-and-timers` },
|
||||
{ text: t.sidebar.logging, link: `${prefix}/guide/logging` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.advancedFeatures,
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: t.sidebar.serviceContainer, link: `${prefix}/guide/service-container` },
|
||||
{ text: t.sidebar.pluginSystem, link: `${prefix}/guide/plugin-system` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.platformAdapters,
|
||||
link: `${prefix}/guide/platform-adapter`,
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: t.sidebar.browserAdapter, link: `${prefix}/guide/platform-adapter/browser` },
|
||||
{ text: t.sidebar.wechatAdapter, link: `${prefix}/guide/platform-adapter/wechat-minigame` },
|
||||
{ text: t.sidebar.nodejsAdapter, link: `${prefix}/guide/platform-adapter/nodejs` }
|
||||
]
|
||||
}
|
||||
],
|
||||
[`${prefix}/examples/`]: [
|
||||
{
|
||||
text: t.sidebar.examples,
|
||||
items: [
|
||||
{ text: t.sidebar.examplesOverview, link: `${prefix}/examples/` },
|
||||
{ text: t.nav.workerDemo, link: `${prefix}/examples/worker-system-demo` }
|
||||
]
|
||||
}
|
||||
],
|
||||
[`${prefix}/api/`]: [
|
||||
{
|
||||
text: t.sidebar.apiReference,
|
||||
items: [
|
||||
{ text: t.sidebar.overview, link: `${prefix}/api/README` },
|
||||
{
|
||||
text: t.sidebar.coreClasses,
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: 'Core', link: `${prefix}/api/classes/Core` },
|
||||
{ text: 'Scene', link: `${prefix}/api/classes/Scene` },
|
||||
{ text: 'World', link: `${prefix}/api/classes/World` },
|
||||
{ text: 'Entity', link: `${prefix}/api/classes/Entity` },
|
||||
{ text: 'Component', link: `${prefix}/api/classes/Component` },
|
||||
{ text: 'EntitySystem', link: `${prefix}/api/classes/EntitySystem` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.systemClasses,
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'PassiveSystem', link: `${prefix}/api/classes/PassiveSystem` },
|
||||
{ text: 'ProcessingSystem', link: `${prefix}/api/classes/ProcessingSystem` },
|
||||
{ text: 'IntervalSystem', link: `${prefix}/api/classes/IntervalSystem` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.utilities,
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Matcher', link: `${prefix}/api/classes/Matcher` },
|
||||
{ text: 'Time', link: `${prefix}/api/classes/Time` },
|
||||
{ text: 'PerformanceMonitor', link: `${prefix}/api/classes/PerformanceMonitor` },
|
||||
{ text: 'DebugManager', link: `${prefix}/api/classes/DebugManager` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.interfaces,
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'IScene', link: `${prefix}/api/interfaces/IScene` },
|
||||
{ text: 'IComponent', link: `${prefix}/api/interfaces/IComponent` },
|
||||
{ text: 'ISystemBase', link: `${prefix}/api/interfaces/ISystemBase` },
|
||||
{ text: 'ICoreConfig', link: `${prefix}/api/interfaces/ICoreConfig` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.decorators,
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: '@ECSComponent', link: `${prefix}/api/functions/ECSComponent` },
|
||||
{ text: '@ECSSystem', link: `${prefix}/api/functions/ECSSystem` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.enums,
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'ECSEventType', link: `${prefix}/api/enumerations/ECSEventType` },
|
||||
{ text: 'LogLevel', link: `${prefix}/api/enumerations/LogLevel` }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// 创建导航配置 | Create nav config
|
||||
// prefix: 路径前缀,如 '' 或 '/en' | Path prefix like '' or '/en'
|
||||
function createNav(t, prefix = '') {
|
||||
return [
|
||||
{ text: t.nav.home, link: `${prefix}/` },
|
||||
{ text: t.nav.quickStart, link: `${prefix}/guide/getting-started` },
|
||||
{ text: t.nav.guide, link: `${prefix}/guide/` },
|
||||
{ text: t.nav.api, link: `${prefix}/api/README` },
|
||||
{
|
||||
text: t.nav.examples,
|
||||
items: [
|
||||
{ text: t.nav.workerDemo, link: `${prefix}/examples/worker-system-demo` },
|
||||
{ text: t.nav.lawnMowerDemo, link: 'https://github.com/esengine/lawn-mower-demo' }
|
||||
]
|
||||
},
|
||||
{ text: t.nav.changelog, link: `${prefix}/changelog` },
|
||||
{
|
||||
text: `v${corePackageJson.version}`,
|
||||
link: 'https://github.com/esengine/esengine/releases'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
vite: {
|
||||
plugins: [
|
||||
@@ -28,178 +208,52 @@ export default defineConfig({
|
||||
}
|
||||
}
|
||||
},
|
||||
title: 'ECS Framework',
|
||||
description: '高性能TypeScript ECS框架 - 为游戏开发而生',
|
||||
lang: 'zh-CN',
|
||||
title: 'ESEngine',
|
||||
appearance: 'force-dark',
|
||||
|
||||
locales: {
|
||||
root: {
|
||||
label: '简体中文',
|
||||
lang: 'zh-CN',
|
||||
description: '高性能 TypeScript ECS 框架 - 为游戏开发而生',
|
||||
themeConfig: {
|
||||
nav: createNav(zh, ''),
|
||||
sidebar: createSidebar(zh, ''),
|
||||
editLink: {
|
||||
pattern: 'https://github.com/esengine/esengine/edit/master/docs/:path',
|
||||
text: zh.common.editOnGithub
|
||||
},
|
||||
outline: {
|
||||
level: [2, 3],
|
||||
label: zh.common.onThisPage
|
||||
}
|
||||
}
|
||||
},
|
||||
en: {
|
||||
label: 'English',
|
||||
lang: 'en',
|
||||
link: '/en/',
|
||||
description: 'High-performance TypeScript ECS Framework for Game Development',
|
||||
themeConfig: {
|
||||
nav: createNav(en, '/en'),
|
||||
sidebar: createSidebar(en, '/en'),
|
||||
editLink: {
|
||||
pattern: 'https://github.com/esengine/esengine/edit/master/docs/:path',
|
||||
text: en.common.editOnGithub
|
||||
},
|
||||
outline: {
|
||||
level: [2, 3],
|
||||
label: en.common.onThisPage
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
themeConfig: {
|
||||
nav: [
|
||||
{ text: '首页', link: '/' },
|
||||
{ text: '快速开始', link: '/guide/getting-started' },
|
||||
{ text: '指南', link: '/guide/' },
|
||||
{ text: 'API', link: '/api/README' },
|
||||
{
|
||||
text: '示例',
|
||||
items: [
|
||||
{ text: 'Worker系统演示', link: '/examples/worker-system-demo' },
|
||||
{ text: '割草机演示', link: 'https://github.com/esengine/lawn-mower-demo' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: `v${corePackageJson.version}`,
|
||||
link: 'https://github.com/esengine/ecs-framework/releases'
|
||||
}
|
||||
],
|
||||
|
||||
sidebar: {
|
||||
'/guide/': [
|
||||
{
|
||||
text: '开始使用',
|
||||
items: [
|
||||
{ text: '快速开始', link: '/guide/getting-started' },
|
||||
{ text: '指南概览', link: '/guide/' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '核心概念',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: '实体类 (Entity)', link: '/guide/entity' },
|
||||
{ text: '组件系统 (Component)', link: '/guide/component' },
|
||||
{ text: '实体查询系统', link: '/guide/entity-query' },
|
||||
{
|
||||
text: '系统架构 (System)',
|
||||
link: '/guide/system',
|
||||
items: [
|
||||
{ text: 'Worker系统 (多线程)', link: '/guide/worker-system' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '场景管理 (Scene)',
|
||||
link: '/guide/scene',
|
||||
items: [
|
||||
{ text: 'SceneManager', link: '/guide/scene-manager' },
|
||||
{ text: 'WorldManager', link: '/guide/world-manager' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '行为树系统 (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' },
|
||||
{ text: '日志系统 (Logger)', link: '/guide/logging' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '高级特性',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: '服务容器 (Service Container)', link: '/guide/service-container' },
|
||||
{ text: '插件系统 (Plugin System)', link: '/guide/plugin-system' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '平台适配器',
|
||||
link: '/guide/platform-adapter',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: '浏览器适配器', link: '/guide/platform-adapter/browser' },
|
||||
{ text: '微信小游戏适配器', link: '/guide/platform-adapter/wechat-minigame' },
|
||||
{ text: 'Node.js适配器', link: '/guide/platform-adapter/nodejs' }
|
||||
]
|
||||
}
|
||||
],
|
||||
'/examples/': [
|
||||
{
|
||||
text: '示例',
|
||||
items: [
|
||||
{ text: '示例概览', link: '/examples/' },
|
||||
{ text: 'Worker系统演示', link: '/examples/worker-system-demo' }
|
||||
]
|
||||
}
|
||||
],
|
||||
'/api/': [
|
||||
{
|
||||
text: 'API 参考',
|
||||
items: [
|
||||
{ text: '概述', link: '/api/README' },
|
||||
{
|
||||
text: '核心类',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: 'Core', link: '/api/classes/Core' },
|
||||
{ text: 'Scene', link: '/api/classes/Scene' },
|
||||
{ text: 'World', link: '/api/classes/World' },
|
||||
{ text: 'Entity', link: '/api/classes/Entity' },
|
||||
{ text: 'Component', link: '/api/classes/Component' },
|
||||
{ text: 'EntitySystem', link: '/api/classes/EntitySystem' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '系统类',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'PassiveSystem', link: '/api/classes/PassiveSystem' },
|
||||
{ text: 'ProcessingSystem', link: '/api/classes/ProcessingSystem' },
|
||||
{ text: 'IntervalSystem', link: '/api/classes/IntervalSystem' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '工具类',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Matcher', link: '/api/classes/Matcher' },
|
||||
{ text: 'Time', link: '/api/classes/Time' },
|
||||
{ text: 'PerformanceMonitor', link: '/api/classes/PerformanceMonitor' },
|
||||
{ text: 'DebugManager', link: '/api/classes/DebugManager' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '接口',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'IScene', link: '/api/interfaces/IScene' },
|
||||
{ text: 'IComponent', link: '/api/interfaces/IComponent' },
|
||||
{ text: 'ISystemBase', link: '/api/interfaces/ISystemBase' },
|
||||
{ text: 'ICoreConfig', link: '/api/interfaces/ICoreConfig' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '装饰器',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: '@ECSComponent', link: '/api/functions/ECSComponent' },
|
||||
{ text: '@ECSSystem', link: '/api/functions/ECSSystem' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '枚举',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'ECSEventType', link: '/api/enumerations/ECSEventType' },
|
||||
{ text: 'LogLevel', link: '/api/enumerations/LogLevel' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
siteTitle: 'ESEngine',
|
||||
|
||||
socialLinks: [
|
||||
{ icon: 'github', link: 'https://github.com/esengine/ecs-framework' }
|
||||
{ icon: 'github', link: 'https://github.com/esengine/esengine' }
|
||||
],
|
||||
|
||||
footer: {
|
||||
@@ -207,18 +261,8 @@ export default defineConfig({
|
||||
copyright: 'Copyright © 2025 ECS Framework'
|
||||
},
|
||||
|
||||
editLink: {
|
||||
pattern: 'https://github.com/esengine/ecs-framework/edit/master/docs/:path',
|
||||
text: '在 GitHub 上编辑此页'
|
||||
},
|
||||
|
||||
search: {
|
||||
provider: 'local'
|
||||
},
|
||||
|
||||
outline: {
|
||||
level: [2, 3],
|
||||
label: '目录'
|
||||
}
|
||||
},
|
||||
|
||||
@@ -227,8 +271,9 @@ export default defineConfig({
|
||||
['link', { rel: 'icon', href: '/favicon.ico' }]
|
||||
],
|
||||
|
||||
base: '/ecs-framework/',
|
||||
base: '/',
|
||||
cleanUrls: true,
|
||||
ignoreDeadLinks: true,
|
||||
|
||||
markdown: {
|
||||
lineNumbers: true,
|
||||
@@ -237,4 +282,4 @@ export default defineConfig({
|
||||
dark: 'github-dark'
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
87
docs/.vitepress/i18n/en.json
Normal file
87
docs/.vitepress/i18n/en.json
Normal file
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"nav": {
|
||||
"home": "Home",
|
||||
"quickStart": "Quick Start",
|
||||
"guide": "Guide",
|
||||
"api": "API",
|
||||
"examples": "Examples",
|
||||
"workerDemo": "Worker System Demo",
|
||||
"lawnMowerDemo": "Lawn Mower Demo",
|
||||
"changelog": "Changelog"
|
||||
},
|
||||
"sidebar": {
|
||||
"gettingStarted": "Getting Started",
|
||||
"quickStart": "Quick Start",
|
||||
"guideOverview": "Guide Overview",
|
||||
"coreConcepts": "Core Concepts",
|
||||
"entity": "Entity",
|
||||
"hierarchy": "Hierarchy",
|
||||
"component": "Component",
|
||||
"entityQuery": "Entity Query",
|
||||
"system": "System",
|
||||
"workerSystem": "Worker System (Multithreading)",
|
||||
"scene": "Scene",
|
||||
"sceneManager": "SceneManager",
|
||||
"worldManager": "WorldManager",
|
||||
"persistentEntity": "Persistent Entity",
|
||||
"behaviorTree": "Behavior Tree",
|
||||
"btGettingStarted": "Getting Started",
|
||||
"btCoreConcepts": "Core Concepts",
|
||||
"btEditorGuide": "Editor Guide",
|
||||
"btEditorWorkflow": "Editor Workflow",
|
||||
"btCustomActions": "Custom Actions",
|
||||
"btCocosIntegration": "Cocos Creator Integration",
|
||||
"btLayaIntegration": "Laya Engine Integration",
|
||||
"btAdvancedUsage": "Advanced Usage",
|
||||
"btBestPractices": "Best Practices",
|
||||
"serialization": "Serialization",
|
||||
"eventSystem": "Event System",
|
||||
"timeAndTimers": "Time and Timers",
|
||||
"logging": "Logging",
|
||||
"advancedFeatures": "Advanced Features",
|
||||
"serviceContainer": "Service Container",
|
||||
"pluginSystem": "Plugin System",
|
||||
"platformAdapters": "Platform Adapters",
|
||||
"browserAdapter": "Browser Adapter",
|
||||
"wechatAdapter": "WeChat Mini Game Adapter",
|
||||
"nodejsAdapter": "Node.js Adapter",
|
||||
"examples": "Examples",
|
||||
"examplesOverview": "Examples Overview",
|
||||
"apiReference": "API Reference",
|
||||
"overview": "Overview",
|
||||
"coreClasses": "Core Classes",
|
||||
"systemClasses": "System Classes",
|
||||
"utilities": "Utilities",
|
||||
"interfaces": "Interfaces",
|
||||
"decorators": "Decorators",
|
||||
"enums": "Enums"
|
||||
},
|
||||
"home": {
|
||||
"title": "ESEngine - High-performance TypeScript ECS Framework",
|
||||
"quickLinks": "Quick Links",
|
||||
"viewDocs": "View Docs",
|
||||
"getStarted": "Get Started",
|
||||
"getStartedDesc": "From installation to your first ECS app, learn the core concepts in 5 minutes.",
|
||||
"aiSystem": "AI System",
|
||||
"behaviorTreeEditor": "Visual Behavior Tree Editor",
|
||||
"behaviorTreeDesc": "Built-in AI behavior tree system with visual editing and real-time debugging.",
|
||||
"coreFeatures": "Core Features",
|
||||
"ecsArchitecture": "High-performance ECS Architecture",
|
||||
"ecsArchitectureDesc": "Data-driven entity component system for large-scale entity processing with cache-friendly memory layout.",
|
||||
"typeSupport": "Full Type Support",
|
||||
"typeSupportDesc": "100% TypeScript with complete type definitions and compile-time checking for the best development experience.",
|
||||
"visualBehaviorTree": "Visual Behavior Tree",
|
||||
"visualBehaviorTreeDesc": "Built-in AI behavior tree system with visual editor, custom nodes, and real-time debugging.",
|
||||
"multiPlatform": "Multi-Platform Support",
|
||||
"multiPlatformDesc": "Support for browsers, Node.js, WeChat Mini Games, and seamless integration with major game engines.",
|
||||
"modularDesign": "Modular Design",
|
||||
"modularDesignDesc": "Core features packaged independently, import only what you need. Support for custom plugin extensions.",
|
||||
"devTools": "Developer Tools",
|
||||
"devToolsDesc": "Built-in performance monitoring, debugging tools, serialization system, and complete development toolchain.",
|
||||
"learnMore": "Learn more →"
|
||||
},
|
||||
"common": {
|
||||
"editOnGithub": "Edit this page on GitHub",
|
||||
"onThisPage": "On this page"
|
||||
}
|
||||
}
|
||||
21
docs/.vitepress/i18n/index.ts
Normal file
21
docs/.vitepress/i18n/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import en from './en.json'
|
||||
import zh from './zh.json'
|
||||
|
||||
export const messages = { en, zh }
|
||||
|
||||
export type Locale = 'en' | 'zh'
|
||||
|
||||
export function getLocaleMessages(locale: Locale) {
|
||||
return messages[locale] || messages.en
|
||||
}
|
||||
|
||||
// Helper to get nested key value
|
||||
export function t(messages: typeof en, key: string): string {
|
||||
const keys = key.split('.')
|
||||
let result: any = messages
|
||||
for (const k of keys) {
|
||||
result = result?.[k]
|
||||
if (result === undefined) return key
|
||||
}
|
||||
return result
|
||||
}
|
||||
87
docs/.vitepress/i18n/zh.json
Normal file
87
docs/.vitepress/i18n/zh.json
Normal file
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"nav": {
|
||||
"home": "首页",
|
||||
"quickStart": "快速开始",
|
||||
"guide": "指南",
|
||||
"api": "API",
|
||||
"examples": "示例",
|
||||
"workerDemo": "Worker系统演示",
|
||||
"lawnMowerDemo": "割草机演示",
|
||||
"changelog": "更新日志"
|
||||
},
|
||||
"sidebar": {
|
||||
"gettingStarted": "开始使用",
|
||||
"quickStart": "快速开始",
|
||||
"guideOverview": "指南概览",
|
||||
"coreConcepts": "核心概念",
|
||||
"entity": "实体类 (Entity)",
|
||||
"hierarchy": "层级系统 (Hierarchy)",
|
||||
"component": "组件系统 (Component)",
|
||||
"entityQuery": "实体查询系统",
|
||||
"system": "系统架构 (System)",
|
||||
"workerSystem": "Worker系统 (多线程)",
|
||||
"scene": "场景管理 (Scene)",
|
||||
"sceneManager": "SceneManager",
|
||||
"worldManager": "WorldManager",
|
||||
"persistentEntity": "持久化实体 (Persistent Entity)",
|
||||
"behaviorTree": "行为树系统 (Behavior Tree)",
|
||||
"btGettingStarted": "快速开始",
|
||||
"btCoreConcepts": "核心概念",
|
||||
"btEditorGuide": "编辑器指南",
|
||||
"btEditorWorkflow": "编辑器工作流",
|
||||
"btCustomActions": "自定义动作组件",
|
||||
"btCocosIntegration": "Cocos Creator集成",
|
||||
"btLayaIntegration": "Laya引擎集成",
|
||||
"btAdvancedUsage": "高级用法",
|
||||
"btBestPractices": "最佳实践",
|
||||
"serialization": "序列化系统 (Serialization)",
|
||||
"eventSystem": "事件系统 (Event)",
|
||||
"timeAndTimers": "时间和定时器 (Time)",
|
||||
"logging": "日志系统 (Logger)",
|
||||
"advancedFeatures": "高级特性",
|
||||
"serviceContainer": "服务容器 (Service Container)",
|
||||
"pluginSystem": "插件系统 (Plugin System)",
|
||||
"platformAdapters": "平台适配器",
|
||||
"browserAdapter": "浏览器适配器",
|
||||
"wechatAdapter": "微信小游戏适配器",
|
||||
"nodejsAdapter": "Node.js适配器",
|
||||
"examples": "示例",
|
||||
"examplesOverview": "示例概览",
|
||||
"apiReference": "API 参考",
|
||||
"overview": "概述",
|
||||
"coreClasses": "核心类",
|
||||
"systemClasses": "系统类",
|
||||
"utilities": "工具类",
|
||||
"interfaces": "接口",
|
||||
"decorators": "装饰器",
|
||||
"enums": "枚举"
|
||||
},
|
||||
"home": {
|
||||
"title": "ESEngine - 高性能 TypeScript ECS 框架",
|
||||
"quickLinks": "快速入口",
|
||||
"viewDocs": "查看文档",
|
||||
"getStarted": "快速开始",
|
||||
"getStartedDesc": "从安装到创建第一个 ECS 应用,快速了解核心概念。",
|
||||
"aiSystem": "AI 系统",
|
||||
"behaviorTreeEditor": "行为树可视化编辑器",
|
||||
"behaviorTreeDesc": "内置 AI 行为树系统,支持可视化编辑和实时调试。",
|
||||
"coreFeatures": "核心特性",
|
||||
"ecsArchitecture": "高性能 ECS 架构",
|
||||
"ecsArchitectureDesc": "基于数据驱动的实体组件系统,支持大规模实体处理,缓存友好的内存布局。",
|
||||
"typeSupport": "完整类型支持",
|
||||
"typeSupportDesc": "100% TypeScript 编写,完整的类型定义和编译时检查,提供最佳的开发体验。",
|
||||
"visualBehaviorTree": "可视化行为树",
|
||||
"visualBehaviorTreeDesc": "内置 AI 行为树系统,提供可视化编辑器,支持自定义节点和实时调试。",
|
||||
"multiPlatform": "多平台支持",
|
||||
"multiPlatformDesc": "支持浏览器、Node.js、微信小游戏等多平台,可与主流游戏引擎无缝集成。",
|
||||
"modularDesign": "模块化设计",
|
||||
"modularDesignDesc": "核心功能独立打包,按需引入。支持自定义插件扩展,灵活适配不同项目。",
|
||||
"devTools": "开发者工具",
|
||||
"devToolsDesc": "内置性能监控、调试工具、序列化系统等,提供完整的开发工具链。",
|
||||
"learnMore": "了解更多 →"
|
||||
},
|
||||
"common": {
|
||||
"editOnGithub": "在 GitHub 上编辑此页",
|
||||
"onThisPage": "在这个页面上"
|
||||
}
|
||||
}
|
||||
93
docs/.vitepress/theme/components/FeatureCard.vue
Normal file
93
docs/.vitepress/theme/components/FeatureCard.vue
Normal file
@@ -0,0 +1,93 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
title: String,
|
||||
description: String,
|
||||
icon: String,
|
||||
link: String,
|
||||
image: String
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a :href="link" class="feature-card">
|
||||
<div class="card-image" v-if="image">
|
||||
<img :src="image" :alt="title" />
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="card-icon" v-if="icon && !image">{{ icon }}</div>
|
||||
<h3 class="card-title">{{ title }}</h3>
|
||||
<p class="card-description">{{ description }}</p>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.feature-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--es-bg-elevated, #252526);
|
||||
border: 1px solid var(--es-border-default, #3e3e42);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
border-color: var(--es-primary, #007acc);
|
||||
background: var(--es-bg-overlay, #2d2d2d);
|
||||
}
|
||||
|
||||
.card-image {
|
||||
width: 100%;
|
||||
height: 160px;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #1e3a5f 0%, #1e1e1e 100%);
|
||||
}
|
||||
|
||||
.card-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.feature-card:hover .card-image img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 16px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 12px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--es-bg-input, #3c3c3c);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--es-text-inverse, #ffffff);
|
||||
margin: 0 0 8px 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.card-description {
|
||||
font-size: 12px;
|
||||
color: var(--es-text-secondary, #9d9d9d);
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
422
docs/.vitepress/theme/components/ParticleHero.vue
Normal file
422
docs/.vitepress/theme/components/ParticleHero.vue
Normal file
@@ -0,0 +1,422 @@
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
const canvasRef = ref(null)
|
||||
let animationId = null
|
||||
let particles = []
|
||||
let animationStartTime = null
|
||||
let glowStartTime = null
|
||||
|
||||
// ESEngine 粒子颜色 - VS Code 风格配色(与编辑器统一)
|
||||
const colors = ['#569CD6', '#4EC9B0', '#9CDCFE', '#C586C0', '#DCDCAA']
|
||||
|
||||
class Particle {
|
||||
constructor(x, y, targetX, targetY) {
|
||||
this.x = x
|
||||
this.y = y
|
||||
this.targetX = targetX
|
||||
this.targetY = targetY
|
||||
this.size = Math.random() * 2 + 1.5
|
||||
this.alpha = Math.random() * 0.5 + 0.5
|
||||
this.color = colors[Math.floor(Math.random() * colors.length)]
|
||||
}
|
||||
}
|
||||
|
||||
function createParticles(canvas, text, fontSize) {
|
||||
const tempCanvas = document.createElement('canvas')
|
||||
const tempCtx = tempCanvas.getContext('2d')
|
||||
if (!tempCtx) return []
|
||||
|
||||
tempCtx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||
const textMetrics = tempCtx.measureText(text)
|
||||
const textWidth = textMetrics.width
|
||||
const textHeight = fontSize
|
||||
|
||||
tempCanvas.width = textWidth + 40
|
||||
tempCanvas.height = textHeight + 40
|
||||
tempCtx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||
tempCtx.textAlign = 'center'
|
||||
tempCtx.textBaseline = 'middle'
|
||||
tempCtx.fillStyle = '#ffffff'
|
||||
tempCtx.fillText(text, tempCanvas.width / 2, tempCanvas.height / 2)
|
||||
|
||||
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height)
|
||||
const pixels = imageData.data
|
||||
const newParticles = []
|
||||
const gap = 3
|
||||
|
||||
const width = canvas.width / (window.devicePixelRatio || 1)
|
||||
const height = canvas.height / (window.devicePixelRatio || 1)
|
||||
const offsetX = (width - tempCanvas.width) / 2
|
||||
const offsetY = (height - tempCanvas.height) / 2
|
||||
|
||||
for (let y = 0; y < tempCanvas.height; y += gap) {
|
||||
for (let x = 0; x < tempCanvas.width; x += gap) {
|
||||
const index = (y * tempCanvas.width + x) * 4
|
||||
const alpha = pixels[index + 3] || 0
|
||||
|
||||
if (alpha > 128) {
|
||||
const angle = Math.random() * Math.PI * 2
|
||||
const distance = Math.random() * Math.max(width, height)
|
||||
|
||||
newParticles.push(new Particle(
|
||||
width / 2 + Math.cos(angle) * distance,
|
||||
height / 2 + Math.sin(angle) * distance,
|
||||
offsetX + x,
|
||||
offsetY + y
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newParticles
|
||||
}
|
||||
|
||||
function easeOutQuart(t) {
|
||||
return 1 - Math.pow(1 - t, 4)
|
||||
}
|
||||
|
||||
function easeOutCubic(t) {
|
||||
return 1 - Math.pow(1 - t, 3)
|
||||
}
|
||||
|
||||
function animate(canvas, ctx) {
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
const width = canvas.width / dpr
|
||||
const height = canvas.height / dpr
|
||||
|
||||
const currentTime = performance.now()
|
||||
const duration = 2500
|
||||
const glowDuration = 600
|
||||
|
||||
const elapsed = currentTime - animationStartTime
|
||||
const progress = Math.min(elapsed / duration, 1)
|
||||
const easedProgress = easeOutQuart(progress)
|
||||
|
||||
// 透明背景
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
|
||||
// 计算发光进度
|
||||
let glowProgress = 0
|
||||
if (progress >= 1) {
|
||||
if (glowStartTime === null) {
|
||||
glowStartTime = currentTime
|
||||
}
|
||||
glowProgress = Math.min((currentTime - glowStartTime) / glowDuration, 1)
|
||||
glowProgress = easeOutCubic(glowProgress)
|
||||
}
|
||||
|
||||
const text = 'ESEngine'
|
||||
const fontSize = Math.min(width / 4, height / 3, 80)
|
||||
const textY = height / 2
|
||||
|
||||
for (const particle of particles) {
|
||||
const moveProgress = Math.min(easedProgress * 1.2, 1)
|
||||
const currentX = particle.x + (particle.targetX - particle.x) * moveProgress
|
||||
const currentY = particle.y + (particle.targetY - particle.y) * moveProgress
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.arc(currentX, currentY, particle.size, 0, Math.PI * 2)
|
||||
ctx.fillStyle = particle.color
|
||||
ctx.globalAlpha = particle.alpha * (1 - glowProgress * 0.3)
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
ctx.globalAlpha = 1
|
||||
|
||||
if (glowProgress > 0) {
|
||||
ctx.save()
|
||||
ctx.shadowColor = '#3b9eff'
|
||||
ctx.shadowBlur = 30 * glowProgress
|
||||
ctx.fillStyle = `rgba(255, 255, 255, ${glowProgress})`
|
||||
ctx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillText(text, width / 2, textY)
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
if (glowProgress >= 1) {
|
||||
const breathe = 0.8 + Math.sin(currentTime / 1000) * 0.2
|
||||
ctx.save()
|
||||
ctx.shadowColor = '#3b9eff'
|
||||
ctx.shadowBlur = 20 * breathe
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillText(text, width / 2, textY)
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
animationId = requestAnimationFrame(() => animate(canvas, ctx))
|
||||
}
|
||||
|
||||
function initCanvas() {
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas) return
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
const container = canvas.parentElement
|
||||
const width = container.offsetWidth
|
||||
const height = container.offsetHeight
|
||||
|
||||
canvas.width = width * dpr
|
||||
canvas.height = height * dpr
|
||||
canvas.style.width = `${width}px`
|
||||
canvas.style.height = `${height}px`
|
||||
ctx.scale(dpr, dpr)
|
||||
|
||||
const text = 'ESEngine'
|
||||
const fontSize = Math.min(width / 4, height / 3, 80)
|
||||
|
||||
particles = createParticles(canvas, text, fontSize)
|
||||
animationStartTime = performance.now()
|
||||
glowStartTime = null
|
||||
|
||||
if (animationId) {
|
||||
cancelAnimationFrame(animationId)
|
||||
}
|
||||
|
||||
animate(canvas, ctx)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initCanvas()
|
||||
window.addEventListener('resize', initCanvas)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (animationId) {
|
||||
cancelAnimationFrame(animationId)
|
||||
}
|
||||
window.removeEventListener('resize', initCanvas)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="hero-section">
|
||||
<div class="hero-container">
|
||||
<!-- 左侧文字区域 -->
|
||||
<div class="hero-text">
|
||||
<div class="hero-logo">
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="16" cy="16" r="14" stroke="#9147ff" stroke-width="2"/>
|
||||
<path d="M10 10h8v2h-6v3h5v2h-5v3h6v2h-8v-12z" fill="#9147ff"/>
|
||||
</svg>
|
||||
<span>ESENGINE</span>
|
||||
</div>
|
||||
<h1 class="hero-title">
|
||||
我们构建框架。<br/>
|
||||
而你将创造游戏。
|
||||
</h1>
|
||||
<p class="hero-description">
|
||||
ESEngine 是一个高性能的 TypeScript ECS 框架,为游戏开发者提供现代化的实体组件系统。
|
||||
无论是 2D 还是 3D 游戏,都能帮助你快速构建可扩展的游戏架构。
|
||||
</p>
|
||||
<div class="hero-actions">
|
||||
<a href="/guide/getting-started" class="btn-primary">开始使用</a>
|
||||
<a href="https://github.com/esengine/esengine" class="btn-secondary" target="_blank">了解更多</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧粒子动画区域 -->
|
||||
<div class="hero-visual">
|
||||
<div class="visual-container">
|
||||
<canvas ref="canvasRef" class="particle-canvas"></canvas>
|
||||
<div class="visual-label">
|
||||
<span class="label-title">Entity Component System</span>
|
||||
<span class="label-subtitle">High Performance Framework</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.hero-section {
|
||||
background: #0d0d0d;
|
||||
padding: 80px 0;
|
||||
min-height: calc(100vh - 64px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.hero-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 48px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.2fr;
|
||||
gap: 64px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 左侧文字 */
|
||||
.hero-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.hero-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: #ffffff;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
line-height: 1.2;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.hero-description {
|
||||
font-size: 1.125rem;
|
||||
color: #707070;
|
||||
line-height: 1.7;
|
||||
margin: 0;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.btn-primary,
|
||||
.btn-secondary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 14px 28px;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
font-size: 0.9375rem;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #3b9eff;
|
||||
color: #ffffff;
|
||||
border: 1px solid #3b9eff;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #5aadff;
|
||||
border-color: #5aadff;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #1a1a1a;
|
||||
color: #a0a0a0;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #252525;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.hero-visual {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.visual-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
aspect-ratio: 4 / 3;
|
||||
background: linear-gradient(135deg, #1a2a3a 0%, #1a1a1a 50%, #0d0d0d 100%);
|
||||
border-radius: 12px;
|
||||
border: 1px solid #2a2a2a;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20px 60px rgba(59, 158, 255, 0.1);
|
||||
}
|
||||
|
||||
.particle-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.visual-label {
|
||||
position: absolute;
|
||||
bottom: 24px;
|
||||
left: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.label-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.label-subtitle {
|
||||
font-size: 0.875rem;
|
||||
color: #737373;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 1024px) {
|
||||
.hero-container {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 48px;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.hero-section {
|
||||
padding: 48px 0;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 2.25rem;
|
||||
}
|
||||
|
||||
.hero-description {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.visual-container {
|
||||
max-width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.hero-title {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn-primary,
|
||||
.btn-secondary {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
422
docs/.vitepress/theme/components/ParticleHeroEn.vue
Normal file
422
docs/.vitepress/theme/components/ParticleHeroEn.vue
Normal file
@@ -0,0 +1,422 @@
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
const canvasRef = ref(null)
|
||||
let animationId = null
|
||||
let particles = []
|
||||
let animationStartTime = null
|
||||
let glowStartTime = null
|
||||
|
||||
// ESEngine particle colors - VS Code style colors (unified with editor)
|
||||
const colors = ['#569CD6', '#4EC9B0', '#9CDCFE', '#C586C0', '#DCDCAA']
|
||||
|
||||
class Particle {
|
||||
constructor(x, y, targetX, targetY) {
|
||||
this.x = x
|
||||
this.y = y
|
||||
this.targetX = targetX
|
||||
this.targetY = targetY
|
||||
this.size = Math.random() * 2 + 1.5
|
||||
this.alpha = Math.random() * 0.5 + 0.5
|
||||
this.color = colors[Math.floor(Math.random() * colors.length)]
|
||||
}
|
||||
}
|
||||
|
||||
function createParticles(canvas, text, fontSize) {
|
||||
const tempCanvas = document.createElement('canvas')
|
||||
const tempCtx = tempCanvas.getContext('2d')
|
||||
if (!tempCtx) return []
|
||||
|
||||
tempCtx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||
const textMetrics = tempCtx.measureText(text)
|
||||
const textWidth = textMetrics.width
|
||||
const textHeight = fontSize
|
||||
|
||||
tempCanvas.width = textWidth + 40
|
||||
tempCanvas.height = textHeight + 40
|
||||
tempCtx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||
tempCtx.textAlign = 'center'
|
||||
tempCtx.textBaseline = 'middle'
|
||||
tempCtx.fillStyle = '#ffffff'
|
||||
tempCtx.fillText(text, tempCanvas.width / 2, tempCanvas.height / 2)
|
||||
|
||||
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height)
|
||||
const pixels = imageData.data
|
||||
const newParticles = []
|
||||
const gap = 3
|
||||
|
||||
const width = canvas.width / (window.devicePixelRatio || 1)
|
||||
const height = canvas.height / (window.devicePixelRatio || 1)
|
||||
const offsetX = (width - tempCanvas.width) / 2
|
||||
const offsetY = (height - tempCanvas.height) / 2
|
||||
|
||||
for (let y = 0; y < tempCanvas.height; y += gap) {
|
||||
for (let x = 0; x < tempCanvas.width; x += gap) {
|
||||
const index = (y * tempCanvas.width + x) * 4
|
||||
const alpha = pixels[index + 3] || 0
|
||||
|
||||
if (alpha > 128) {
|
||||
const angle = Math.random() * Math.PI * 2
|
||||
const distance = Math.random() * Math.max(width, height)
|
||||
|
||||
newParticles.push(new Particle(
|
||||
width / 2 + Math.cos(angle) * distance,
|
||||
height / 2 + Math.sin(angle) * distance,
|
||||
offsetX + x,
|
||||
offsetY + y
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newParticles
|
||||
}
|
||||
|
||||
function easeOutQuart(t) {
|
||||
return 1 - Math.pow(1 - t, 4)
|
||||
}
|
||||
|
||||
function easeOutCubic(t) {
|
||||
return 1 - Math.pow(1 - t, 3)
|
||||
}
|
||||
|
||||
function animate(canvas, ctx) {
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
const width = canvas.width / dpr
|
||||
const height = canvas.height / dpr
|
||||
|
||||
const currentTime = performance.now()
|
||||
const duration = 2500
|
||||
const glowDuration = 600
|
||||
|
||||
const elapsed = currentTime - animationStartTime
|
||||
const progress = Math.min(elapsed / duration, 1)
|
||||
const easedProgress = easeOutQuart(progress)
|
||||
|
||||
// Transparent background
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
|
||||
// Calculate glow progress
|
||||
let glowProgress = 0
|
||||
if (progress >= 1) {
|
||||
if (glowStartTime === null) {
|
||||
glowStartTime = currentTime
|
||||
}
|
||||
glowProgress = Math.min((currentTime - glowStartTime) / glowDuration, 1)
|
||||
glowProgress = easeOutCubic(glowProgress)
|
||||
}
|
||||
|
||||
const text = 'ESEngine'
|
||||
const fontSize = Math.min(width / 4, height / 3, 80)
|
||||
const textY = height / 2
|
||||
|
||||
for (const particle of particles) {
|
||||
const moveProgress = Math.min(easedProgress * 1.2, 1)
|
||||
const currentX = particle.x + (particle.targetX - particle.x) * moveProgress
|
||||
const currentY = particle.y + (particle.targetY - particle.y) * moveProgress
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.arc(currentX, currentY, particle.size, 0, Math.PI * 2)
|
||||
ctx.fillStyle = particle.color
|
||||
ctx.globalAlpha = particle.alpha * (1 - glowProgress * 0.3)
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
ctx.globalAlpha = 1
|
||||
|
||||
if (glowProgress > 0) {
|
||||
ctx.save()
|
||||
ctx.shadowColor = '#3b9eff'
|
||||
ctx.shadowBlur = 30 * glowProgress
|
||||
ctx.fillStyle = `rgba(255, 255, 255, ${glowProgress})`
|
||||
ctx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillText(text, width / 2, textY)
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
if (glowProgress >= 1) {
|
||||
const breathe = 0.8 + Math.sin(currentTime / 1000) * 0.2
|
||||
ctx.save()
|
||||
ctx.shadowColor = '#3b9eff'
|
||||
ctx.shadowBlur = 20 * breathe
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillText(text, width / 2, textY)
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
animationId = requestAnimationFrame(() => animate(canvas, ctx))
|
||||
}
|
||||
|
||||
function initCanvas() {
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas) return
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
const container = canvas.parentElement
|
||||
const width = container.offsetWidth
|
||||
const height = container.offsetHeight
|
||||
|
||||
canvas.width = width * dpr
|
||||
canvas.height = height * dpr
|
||||
canvas.style.width = `${width}px`
|
||||
canvas.style.height = `${height}px`
|
||||
ctx.scale(dpr, dpr)
|
||||
|
||||
const text = 'ESEngine'
|
||||
const fontSize = Math.min(width / 4, height / 3, 80)
|
||||
|
||||
particles = createParticles(canvas, text, fontSize)
|
||||
animationStartTime = performance.now()
|
||||
glowStartTime = null
|
||||
|
||||
if (animationId) {
|
||||
cancelAnimationFrame(animationId)
|
||||
}
|
||||
|
||||
animate(canvas, ctx)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initCanvas()
|
||||
window.addEventListener('resize', initCanvas)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (animationId) {
|
||||
cancelAnimationFrame(animationId)
|
||||
}
|
||||
window.removeEventListener('resize', initCanvas)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="hero-section">
|
||||
<div class="hero-container">
|
||||
<!-- Left text area -->
|
||||
<div class="hero-text">
|
||||
<div class="hero-logo">
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="16" cy="16" r="14" stroke="#9147ff" stroke-width="2"/>
|
||||
<path d="M10 10h8v2h-6v3h5v2h-5v3h6v2h-8v-12z" fill="#9147ff"/>
|
||||
</svg>
|
||||
<span>ESENGINE</span>
|
||||
</div>
|
||||
<h1 class="hero-title">
|
||||
We build the framework.<br/>
|
||||
You create the game.
|
||||
</h1>
|
||||
<p class="hero-description">
|
||||
ESEngine is a high-performance TypeScript ECS framework for game developers.
|
||||
Whether 2D or 3D games, it helps you build scalable game architecture quickly.
|
||||
</p>
|
||||
<div class="hero-actions">
|
||||
<a href="/en/guide/getting-started" class="btn-primary">Get Started</a>
|
||||
<a href="https://github.com/esengine/esengine" class="btn-secondary" target="_blank">Learn More</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right particle animation area -->
|
||||
<div class="hero-visual">
|
||||
<div class="visual-container">
|
||||
<canvas ref="canvasRef" class="particle-canvas"></canvas>
|
||||
<div class="visual-label">
|
||||
<span class="label-title">Entity Component System</span>
|
||||
<span class="label-subtitle">High Performance Framework</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.hero-section {
|
||||
background: #0d0d0d;
|
||||
padding: 80px 0;
|
||||
min-height: calc(100vh - 64px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.hero-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 48px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.2fr;
|
||||
gap: 64px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Left text */
|
||||
.hero-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.hero-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: #ffffff;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
line-height: 1.2;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.hero-description {
|
||||
font-size: 1.125rem;
|
||||
color: #707070;
|
||||
line-height: 1.7;
|
||||
margin: 0;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.btn-primary,
|
||||
.btn-secondary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 14px 28px;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
font-size: 0.9375rem;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #3b9eff;
|
||||
color: #ffffff;
|
||||
border: 1px solid #3b9eff;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #5aadff;
|
||||
border-color: #5aadff;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #1a1a1a;
|
||||
color: #a0a0a0;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #252525;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.hero-visual {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.visual-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
aspect-ratio: 4 / 3;
|
||||
background: linear-gradient(135deg, #1a2a3a 0%, #1a1a1a 50%, #0d0d0d 100%);
|
||||
border-radius: 12px;
|
||||
border: 1px solid #2a2a2a;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20px 60px rgba(59, 158, 255, 0.1);
|
||||
}
|
||||
|
||||
.particle-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.visual-label {
|
||||
position: absolute;
|
||||
bottom: 24px;
|
||||
left: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.label-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.label-subtitle {
|
||||
font-size: 0.875rem;
|
||||
color: #737373;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 1024px) {
|
||||
.hero-container {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 48px;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.hero-section {
|
||||
padding: 48px 0;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 2.25rem;
|
||||
}
|
||||
|
||||
.hero-description {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.visual-container {
|
||||
max-width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.hero-title {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn-primary,
|
||||
.btn-secondary {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
594
docs/.vitepress/theme/custom.css
Normal file
594
docs/.vitepress/theme/custom.css
Normal file
@@ -0,0 +1,594 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--vp-nav-height: 64px;
|
||||
|
||||
--es-bg-base: #1e1e1e;
|
||||
--es-bg-elevated: #252526;
|
||||
--es-bg-overlay: #2d2d2d;
|
||||
--es-bg-input: #3c3c3c;
|
||||
--es-bg-inset: #181818;
|
||||
--es-bg-hover: #2a2d2e;
|
||||
--es-bg-active: #37373d;
|
||||
--es-bg-sidebar: #262626;
|
||||
--es-bg-card: #2a2a2a;
|
||||
--es-bg-header: #2d2d2d;
|
||||
|
||||
--es-text-primary: #cccccc;
|
||||
--es-text-secondary: #9d9d9d;
|
||||
--es-text-tertiary: #6a6a6a;
|
||||
--es-text-inverse: #ffffff;
|
||||
--es-text-muted: #aaaaaa;
|
||||
--es-text-dim: #6a6a6a;
|
||||
|
||||
--es-font-xs: 11px;
|
||||
--es-font-sm: 12px;
|
||||
--es-font-base: 13px;
|
||||
--es-font-md: 14px;
|
||||
--es-font-lg: 16px;
|
||||
|
||||
--es-border-default: #3a3a3a;
|
||||
--es-border-subtle: #1a1a1a;
|
||||
--es-border-strong: #4a4a4a;
|
||||
|
||||
--es-primary: #3b82f6;
|
||||
--es-primary-hover: #2563eb;
|
||||
--es-success: #4ade80;
|
||||
--es-warning: #f59e0b;
|
||||
--es-error: #ef4444;
|
||||
--es-info: #3b82f6;
|
||||
|
||||
--es-selected: #3d5a80;
|
||||
--es-selected-hover: #4a6a90;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--es-bg-base) !important;
|
||||
}
|
||||
|
||||
html,
|
||||
html.dark {
|
||||
--vp-c-bg: var(--es-bg-base);
|
||||
--vp-c-bg-soft: var(--es-bg-elevated);
|
||||
--vp-c-bg-mute: var(--es-bg-overlay);
|
||||
--vp-c-bg-alt: var(--es-bg-sidebar);
|
||||
--vp-c-text-1: var(--es-text-primary);
|
||||
--vp-c-text-2: var(--es-text-tertiary);
|
||||
--vp-c-text-3: var(--es-text-muted);
|
||||
--vp-c-divider: var(--es-border-default);
|
||||
--vp-c-divider-light: var(--es-border-subtle);
|
||||
}
|
||||
|
||||
html:not(.dark) {
|
||||
--vp-c-bg: var(--es-bg-base) !important;
|
||||
--vp-c-bg-soft: var(--es-bg-elevated) !important;
|
||||
--vp-c-bg-mute: var(--es-bg-overlay) !important;
|
||||
--vp-c-bg-alt: var(--es-bg-sidebar) !important;
|
||||
--vp-c-text-1: var(--es-text-primary) !important;
|
||||
--vp-c-text-2: var(--es-text-tertiary) !important;
|
||||
--vp-c-text-3: var(--es-text-muted) !important;
|
||||
}
|
||||
|
||||
.VPNav {
|
||||
background: var(--es-bg-header) !important;
|
||||
border-bottom: 1px solid var(--es-border-subtle) !important;
|
||||
}
|
||||
|
||||
.VPNav .VPNavBar {
|
||||
background: var(--es-bg-header) !important;
|
||||
}
|
||||
|
||||
.VPNav .VPNavBar .wrapper {
|
||||
background: var(--es-bg-header) !important;
|
||||
}
|
||||
|
||||
.VPNav .VPNavBar::before,
|
||||
.VPNav .VPNavBar::after {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.VPNavBar {
|
||||
background: var(--es-bg-header) !important;
|
||||
}
|
||||
|
||||
.VPNavBar::before {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.VPNavBarTitle .title {
|
||||
color: var(--es-text-primary);
|
||||
font-weight: 500;
|
||||
font-size: var(--es-font-base);
|
||||
}
|
||||
|
||||
.VPNavBarMenuLink {
|
||||
color: var(--es-text-secondary) !important;
|
||||
font-size: var(--es-font-sm) !important;
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
|
||||
.VPNavBarMenuLink:hover {
|
||||
color: var(--es-text-primary) !important;
|
||||
}
|
||||
|
||||
.VPNavBarMenuLink.active {
|
||||
color: var(--es-text-primary) !important;
|
||||
}
|
||||
|
||||
.VPNavBarSearch .DocSearch-Button {
|
||||
background: var(--es-bg-input) !important;
|
||||
border: 1px solid var(--es-border-default) !important;
|
||||
border-radius: 2px;
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
.VPSidebar {
|
||||
background: var(--es-bg-sidebar) !important;
|
||||
border-right: 1px solid var(--es-border-subtle) !important;
|
||||
}
|
||||
|
||||
.VPSidebarItem.level-0 > .item {
|
||||
padding: 8px 0 4px 0;
|
||||
}
|
||||
|
||||
.VPSidebarItem.level-0 > .item > .text {
|
||||
font-weight: 600;
|
||||
font-size: var(--es-font-xs);
|
||||
color: var(--es-text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.VPSidebarItem .link {
|
||||
padding: 4px 8px;
|
||||
margin: 1px 0;
|
||||
border-radius: 2px;
|
||||
color: var(--es-text-primary);
|
||||
font-size: var(--es-font-sm);
|
||||
transition: all 0.1s ease;
|
||||
border-left: 2px solid transparent;
|
||||
}
|
||||
|
||||
.VPSidebarItem .link:hover {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
color: var(--es-text-inverse);
|
||||
}
|
||||
|
||||
.VPSidebarItem.is-active > .item > .link {
|
||||
background: var(--es-selected);
|
||||
color: var(--es-text-inverse);
|
||||
border-left: 2px solid var(--es-primary);
|
||||
}
|
||||
|
||||
.VPSidebarItem.is-active > .item > .link:hover {
|
||||
background: var(--es-selected-hover);
|
||||
}
|
||||
|
||||
.VPSidebarItem.level-1 .link {
|
||||
padding-left: 20px;
|
||||
font-size: var(--es-font-sm);
|
||||
}
|
||||
|
||||
.VPSidebarItem.level-2 .link {
|
||||
padding-left: 32px;
|
||||
font-size: var(--es-font-sm);
|
||||
}
|
||||
|
||||
.VPSidebarItem .caret {
|
||||
color: var(--es-text-secondary);
|
||||
}
|
||||
|
||||
.VPSidebarItem .caret:hover {
|
||||
color: var(--es-text-primary);
|
||||
}
|
||||
|
||||
.VPContent {
|
||||
background: var(--es-bg-card) !important;
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
.VPContent.has-sidebar {
|
||||
background: var(--es-bg-card) !important;
|
||||
}
|
||||
|
||||
/* 首页布局修复 | Home page layout fix */
|
||||
.VPPage {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
.Layout > .VPContent {
|
||||
padding-top: var(--vp-nav-height) !important;
|
||||
}
|
||||
|
||||
.VPDoc {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.VPNavBar .content {
|
||||
background: var(--es-bg-header) !important;
|
||||
}
|
||||
|
||||
.VPNavBar .content-body {
|
||||
background: var(--es-bg-header) !important;
|
||||
}
|
||||
|
||||
.VPNavBar .divider {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.VPLocalNav {
|
||||
background: var(--es-bg-header) !important;
|
||||
border-bottom: 1px solid var(--es-border-subtle) !important;
|
||||
}
|
||||
|
||||
.VPNavScreenMenu {
|
||||
background: var(--es-bg-base) !important;
|
||||
}
|
||||
|
||||
.VPNavScreen {
|
||||
background: var(--es-bg-base) !important;
|
||||
}
|
||||
|
||||
.curtain {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.VPNav .curtain,
|
||||
.VPNavBar .curtain {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
[class*="curtain"] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.VPNav > div::before,
|
||||
.VPNav > div::after {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.vp-doc {
|
||||
color: var(--es-text-primary);
|
||||
}
|
||||
|
||||
.vp-doc h1 {
|
||||
font-size: var(--es-font-lg);
|
||||
font-weight: 600;
|
||||
color: var(--es-text-inverse);
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
margin-bottom: 16px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.vp-doc h2 {
|
||||
font-size: var(--es-font-md);
|
||||
font-weight: 600;
|
||||
color: var(--es-text-inverse);
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
margin-top: 32px;
|
||||
margin-bottom: 12px;
|
||||
padding: 6px 12px;
|
||||
background: var(--es-bg-header);
|
||||
border-left: 3px solid var(--es-primary);
|
||||
}
|
||||
|
||||
.vp-doc h3 {
|
||||
font-size: var(--es-font-base);
|
||||
font-weight: 600;
|
||||
color: var(--es-text-primary);
|
||||
margin-top: 20px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.vp-doc p {
|
||||
color: var(--es-text-primary);
|
||||
line-height: 1.7;
|
||||
font-size: var(--es-font-base);
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.vp-doc ul,
|
||||
.vp-doc ol {
|
||||
padding-left: 20px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.vp-doc li {
|
||||
line-height: 1.7;
|
||||
margin: 4px 0;
|
||||
color: var(--es-text-primary);
|
||||
font-size: var(--es-font-base);
|
||||
}
|
||||
|
||||
.vp-doc li::marker {
|
||||
color: var(--es-text-secondary);
|
||||
}
|
||||
|
||||
.vp-doc strong {
|
||||
color: var(--es-text-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.vp-doc a {
|
||||
color: var(--es-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.vp-doc a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.VPDocAside {
|
||||
padding-left: 16px;
|
||||
border-left: 1px solid var(--es-border-subtle);
|
||||
}
|
||||
|
||||
.VPDocAsideOutline {
|
||||
padding: 0;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.VPDocAsideOutline .content {
|
||||
border: none !important;
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
.VPDocAsideOutline .outline-title {
|
||||
font-size: var(--es-font-xs);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--es-text-secondary);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.VPDocAsideOutline .outline-link {
|
||||
color: var(--es-text-secondary);
|
||||
font-size: var(--es-font-xs);
|
||||
padding: 4px 0;
|
||||
line-height: 1.4;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.VPDocAsideOutline .outline-link:hover {
|
||||
color: var(--es-text-primary);
|
||||
}
|
||||
|
||||
.VPDocAsideOutline .outline-link.active {
|
||||
color: var(--es-primary);
|
||||
}
|
||||
|
||||
.VPDocAsideOutline .outline-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div[class*='language-'] {
|
||||
background: var(--es-bg-inset) !important;
|
||||
border: 1px solid var(--es-border-default);
|
||||
border-radius: 2px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.vp-code-group .tabs {
|
||||
background: var(--es-bg-header);
|
||||
border-bottom: 1px solid var(--es-border-subtle);
|
||||
}
|
||||
|
||||
.vp-doc :not(pre) > code {
|
||||
background: var(--es-bg-input);
|
||||
color: var(--es-primary);
|
||||
padding: 2px 6px;
|
||||
border-radius: 2px;
|
||||
font-size: var(--es-font-xs);
|
||||
}
|
||||
|
||||
.vp-doc table {
|
||||
display: table;
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-collapse: collapse;
|
||||
margin: 16px 0;
|
||||
font-size: var(--es-font-sm);
|
||||
}
|
||||
|
||||
.vp-doc tr {
|
||||
border-bottom: 1px solid var(--es-border-subtle);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.vp-doc tr:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.vp-doc tr:hover {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
.vp-doc th {
|
||||
background: var(--es-bg-header);
|
||||
font-weight: 600;
|
||||
font-size: var(--es-font-xs);
|
||||
color: var(--es-text-secondary);
|
||||
text-align: left;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--es-border-subtle);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.vp-doc td {
|
||||
font-size: var(--es-font-sm);
|
||||
color: var(--es-text-primary);
|
||||
padding: 8px 12px;
|
||||
vertical-align: top;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.vp-doc td:first-child {
|
||||
font-weight: 500;
|
||||
color: var(--es-text-primary);
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.vp-doc .warning,
|
||||
.vp-doc .custom-block.warning {
|
||||
background: rgba(245, 158, 11, 0.08);
|
||||
border: none;
|
||||
border-left: 3px solid var(--es-warning);
|
||||
border-radius: 0 2px 2px 0;
|
||||
padding: 10px 12px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.vp-doc .warning .custom-block-title,
|
||||
.vp-doc .custom-block.warning .custom-block-title {
|
||||
color: var(--es-warning);
|
||||
font-weight: 600;
|
||||
font-size: var(--es-font-xs);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.vp-doc .warning p {
|
||||
color: var(--es-text-primary);
|
||||
margin: 0;
|
||||
font-size: var(--es-font-xs);
|
||||
}
|
||||
|
||||
.vp-doc .tip,
|
||||
.vp-doc .custom-block.tip {
|
||||
background: rgba(59, 130, 246, 0.08);
|
||||
border: none;
|
||||
border-left: 3px solid var(--es-primary);
|
||||
border-radius: 0 2px 2px 0;
|
||||
padding: 10px 12px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.vp-doc .tip .custom-block-title,
|
||||
.vp-doc .custom-block.tip .custom-block-title {
|
||||
color: var(--es-primary);
|
||||
font-weight: 600;
|
||||
font-size: var(--es-font-xs);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.vp-doc .tip p {
|
||||
color: var(--es-text-primary);
|
||||
margin: 0;
|
||||
font-size: var(--es-font-xs);
|
||||
}
|
||||
|
||||
.vp-doc .info,
|
||||
.vp-doc .custom-block.info {
|
||||
background: rgba(74, 222, 128, 0.08);
|
||||
border: none;
|
||||
border-left: 3px solid var(--es-success);
|
||||
border-radius: 0 2px 2px 0;
|
||||
padding: 10px 12px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.vp-doc .info .custom-block-title,
|
||||
.vp-doc .custom-block.info .custom-block-title {
|
||||
color: var(--es-success);
|
||||
font-weight: 600;
|
||||
font-size: var(--es-font-xs);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.vp-doc .danger,
|
||||
.vp-doc .custom-block.danger {
|
||||
background: rgba(239, 68, 68, 0.08);
|
||||
border: none;
|
||||
border-left: 3px solid var(--es-error);
|
||||
border-radius: 0 2px 2px 0;
|
||||
padding: 10px 12px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.vp-doc .danger .custom-block-title,
|
||||
.vp-doc .custom-block.danger .custom-block-title {
|
||||
color: var(--es-error);
|
||||
font-weight: 600;
|
||||
font-size: var(--es-font-xs);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.vp-doc .card {
|
||||
background: var(--es-bg-sidebar);
|
||||
border: 1px solid var(--es-border-subtle);
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.vp-doc .card-title {
|
||||
font-size: var(--es-font-sm);
|
||||
font-weight: 600;
|
||||
color: var(--es-text-primary);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.vp-doc .card-description {
|
||||
font-size: var(--es-font-xs);
|
||||
color: var(--es-text-muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.vp-doc .tag {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
background: transparent;
|
||||
border: 1px solid var(--es-border-default);
|
||||
border-radius: 2px;
|
||||
color: var(--es-text-secondary);
|
||||
font-size: var(--es-font-xs);
|
||||
margin-right: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.VPFooter {
|
||||
background: var(--es-bg-sidebar) !important;
|
||||
border-top: 1px solid var(--es-border-subtle) !important;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--es-bg-card);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--es-border-strong);
|
||||
border-radius: 4px;
|
||||
border: 2px solid var(--es-bg-card);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #5a5a5a;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.home-container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.home-section {
|
||||
padding: 32px 0;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.VPDoc .content {
|
||||
padding: 16px !important;
|
||||
}
|
||||
}
|
||||
14
docs/.vitepress/theme/index.js
Normal file
14
docs/.vitepress/theme/index.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
import ParticleHero from './components/ParticleHero.vue'
|
||||
import ParticleHeroEn from './components/ParticleHeroEn.vue'
|
||||
import FeatureCard from './components/FeatureCard.vue'
|
||||
import './custom.css'
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
enhanceApp({ app }) {
|
||||
app.component('ParticleHero', ParticleHero)
|
||||
app.component('ParticleHeroEn', ParticleHeroEn)
|
||||
app.component('FeatureCard', FeatureCard)
|
||||
}
|
||||
}
|
||||
663
docs/architecture/material-system-refactor.md
Normal file
663
docs/architecture/material-system-refactor.md
Normal file
@@ -0,0 +1,663 @@
|
||||
# ESEngine 材质系统统一架构重构方案
|
||||
|
||||
## 问题概述
|
||||
|
||||
当前 UI 和 Scene (Sprite) 两套渲染系统存在大量代码重复:
|
||||
|
||||
| 重复项 | Sprite | UI | 重复度 |
|
||||
|--------|--------|----|----|
|
||||
| 材质属性覆盖接口 | `MaterialPropertyOverride` | `UIMaterialPropertyOverride` | 100% |
|
||||
| 材质方法 (12个) | `SpriteComponent` | `UIRenderComponent` | 100% |
|
||||
| ShinyEffect 组件 | `ShinyEffectComponent` | `UIShinyEffectComponent` | 99% |
|
||||
| ShinyEffect 系统 | `ShinyEffectSystem` | `UIShinyEffectSystem` | 98% |
|
||||
|
||||
**根本原因**:缺乏统一的材质覆盖接口抽象层。
|
||||
|
||||
---
|
||||
|
||||
## 一、统一材质覆盖接口
|
||||
|
||||
### 1.1 定义通用接口
|
||||
|
||||
在 `@esengine/material-system` 包中定义统一接口:
|
||||
|
||||
```typescript
|
||||
// packages/material-system/src/interfaces/IMaterialOverridable.ts
|
||||
|
||||
/**
|
||||
* Material property override definition.
|
||||
* 材质属性覆盖定义。
|
||||
*/
|
||||
export interface MaterialPropertyOverride {
|
||||
type: 'float' | 'vec2' | 'vec3' | 'vec4' | 'color' | 'int';
|
||||
value: number | number[];
|
||||
}
|
||||
|
||||
export type MaterialOverrides = Record<string, MaterialPropertyOverride>;
|
||||
|
||||
/**
|
||||
* Interface for components that support material property overrides.
|
||||
* 支持材质属性覆盖的组件接口。
|
||||
*/
|
||||
export interface IMaterialOverridable {
|
||||
/** Material GUID for asset reference | 材质资产引用的 GUID */
|
||||
materialGuid: string;
|
||||
|
||||
/** Current material overrides | 当前材质覆盖 */
|
||||
readonly materialOverrides: MaterialOverrides;
|
||||
|
||||
/** Get current material ID | 获取当前材质 ID */
|
||||
getMaterialId(): number;
|
||||
|
||||
/** Set material ID | 设置材质 ID */
|
||||
setMaterialId(id: number): void;
|
||||
|
||||
// Uniform setters
|
||||
setOverrideFloat(name: string, value: number): this;
|
||||
setOverrideVec2(name: string, x: number, y: number): this;
|
||||
setOverrideVec3(name: string, x: number, y: number, z: number): this;
|
||||
setOverrideVec4(name: string, x: number, y: number, z: number, w: number): this;
|
||||
setOverrideColor(name: string, r: number, g: number, b: number, a?: number): this;
|
||||
setOverrideInt(name: string, value: number): this;
|
||||
|
||||
// Uniform getters
|
||||
getOverride(name: string): MaterialPropertyOverride | undefined;
|
||||
removeOverride(name: string): this;
|
||||
clearOverrides(): this;
|
||||
hasOverrides(): boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 创建 Mixin 实现
|
||||
|
||||
使用 Mixin 模式避免代码重复:
|
||||
|
||||
```typescript
|
||||
// packages/material-system/src/mixins/MaterialOverridableMixin.ts
|
||||
|
||||
import type { MaterialPropertyOverride, MaterialOverrides } from '../interfaces/IMaterialOverridable';
|
||||
|
||||
/**
|
||||
* Mixin that provides material override functionality.
|
||||
* 提供材质覆盖功能的 Mixin。
|
||||
*/
|
||||
export function MaterialOverridableMixin<TBase extends new (...args: any[]) => {}>(Base: TBase) {
|
||||
return class extends Base {
|
||||
materialGuid: string = '';
|
||||
private _materialId: number = 0;
|
||||
private _materialOverrides: MaterialOverrides = {};
|
||||
|
||||
get materialOverrides(): MaterialOverrides {
|
||||
return this._materialOverrides;
|
||||
}
|
||||
|
||||
getMaterialId(): number {
|
||||
return this._materialId;
|
||||
}
|
||||
|
||||
setMaterialId(id: number): void {
|
||||
this._materialId = id;
|
||||
}
|
||||
|
||||
setOverrideFloat(name: string, value: number): this {
|
||||
this._materialOverrides[name] = { type: 'float', value };
|
||||
return this;
|
||||
}
|
||||
|
||||
setOverrideVec2(name: string, x: number, y: number): this {
|
||||
this._materialOverrides[name] = { type: 'vec2', value: [x, y] };
|
||||
return this;
|
||||
}
|
||||
|
||||
setOverrideVec3(name: string, x: number, y: number, z: number): this {
|
||||
this._materialOverrides[name] = { type: 'vec3', value: [x, y, z] };
|
||||
return this;
|
||||
}
|
||||
|
||||
setOverrideVec4(name: string, x: number, y: number, z: number, w: number): this {
|
||||
this._materialOverrides[name] = { type: 'vec4', value: [x, y, z, w] };
|
||||
return this;
|
||||
}
|
||||
|
||||
setOverrideColor(name: string, r: number, g: number, b: number, a: number = 1.0): this {
|
||||
this._materialOverrides[name] = { type: 'color', value: [r, g, b, a] };
|
||||
return this;
|
||||
}
|
||||
|
||||
setOverrideInt(name: string, value: number): this {
|
||||
this._materialOverrides[name] = { type: 'int', value: Math.floor(value) };
|
||||
return this;
|
||||
}
|
||||
|
||||
getOverride(name: string): MaterialPropertyOverride | undefined {
|
||||
return this._materialOverrides[name];
|
||||
}
|
||||
|
||||
removeOverride(name: string): this {
|
||||
delete this._materialOverrides[name];
|
||||
return this;
|
||||
}
|
||||
|
||||
clearOverrides(): this {
|
||||
this._materialOverrides = {};
|
||||
return this;
|
||||
}
|
||||
|
||||
hasOverrides(): boolean {
|
||||
return Object.keys(this._materialOverrides).length > 0;
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、Shader Property 元数据系统
|
||||
|
||||
### 2.1 定义属性元数据接口
|
||||
|
||||
```typescript
|
||||
// packages/material-system/src/interfaces/IShaderProperty.ts
|
||||
|
||||
/**
|
||||
* Shader property UI metadata.
|
||||
* 着色器属性 UI 元数据。
|
||||
*/
|
||||
export interface ShaderPropertyMeta {
|
||||
/** Property type | 属性类型 */
|
||||
type: 'float' | 'vec2' | 'vec3' | 'vec4' | 'color' | 'int' | 'texture';
|
||||
|
||||
/** Display label (supports i18n key) | 显示标签(支持 i18n 键) */
|
||||
label: string;
|
||||
|
||||
/** Property group for organization | 属性分组 */
|
||||
group?: string;
|
||||
|
||||
/** Default value | 默认值 */
|
||||
default?: number | number[] | string;
|
||||
|
||||
// Numeric constraints
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
|
||||
/** UI hints | UI 提示 */
|
||||
hint?: 'range' | 'angle' | 'hdr' | 'normal';
|
||||
|
||||
/** Tooltip description | 工具提示描述 */
|
||||
tooltip?: string;
|
||||
|
||||
/** Whether to hide in inspector | 是否在检查器中隐藏 */
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extended shader definition with property metadata.
|
||||
* 带属性元数据的扩展着色器定义。
|
||||
*/
|
||||
export interface ShaderAssetDefinition {
|
||||
/** Shader name | 着色器名称 */
|
||||
name: string;
|
||||
|
||||
/** Display name for UI | UI 显示名称 */
|
||||
displayName?: string;
|
||||
|
||||
/** Shader description | 着色器描述 */
|
||||
description?: string;
|
||||
|
||||
/** Vertex shader source (inline or path) | 顶点着色器源(内联或路径)*/
|
||||
vertexSource: string;
|
||||
|
||||
/** Fragment shader source (inline or path) | 片段着色器源(内联或路径)*/
|
||||
fragmentSource: string;
|
||||
|
||||
/** Property metadata for inspector | 检查器属性元数据 */
|
||||
properties?: Record<string, ShaderPropertyMeta>;
|
||||
|
||||
/** Render queue / order | 渲染队列/顺序 */
|
||||
renderQueue?: number;
|
||||
|
||||
/** Preset blend mode | 预设混合模式 */
|
||||
blendMode?: 'alpha' | 'additive' | 'multiply' | 'opaque';
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 .shader 资产文件格式
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "esengine://schemas/shader.json",
|
||||
"version": 1,
|
||||
"name": "Shiny",
|
||||
"displayName": "闪光效果 | Shiny Effect",
|
||||
"description": "扫光高亮动画着色器 | Sweeping highlight animation shader",
|
||||
|
||||
"vertexSource": "./shaders/sprite.vert",
|
||||
"fragmentSource": "./shaders/shiny.frag",
|
||||
|
||||
"blendMode": "alpha",
|
||||
"renderQueue": 2000,
|
||||
|
||||
"properties": {
|
||||
"u_shinyProgress": {
|
||||
"type": "float",
|
||||
"label": "进度 | Progress",
|
||||
"group": "Animation",
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"hidden": true
|
||||
},
|
||||
"u_shinyWidth": {
|
||||
"type": "float",
|
||||
"label": "宽度 | Width",
|
||||
"group": "Effect",
|
||||
"default": 0.25,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"tooltip": "闪光带宽度 | Width of the shiny band"
|
||||
},
|
||||
"u_shinyRotation": {
|
||||
"type": "float",
|
||||
"label": "角度 | Rotation",
|
||||
"group": "Effect",
|
||||
"default": 2.25,
|
||||
"min": 0,
|
||||
"max": 6.28,
|
||||
"step": 0.01,
|
||||
"hint": "angle"
|
||||
},
|
||||
"u_shinySoftness": {
|
||||
"type": "float",
|
||||
"label": "柔和度 | Softness",
|
||||
"group": "Effect",
|
||||
"default": 1.0,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01
|
||||
},
|
||||
"u_shinyBrightness": {
|
||||
"type": "float",
|
||||
"label": "亮度 | Brightness",
|
||||
"group": "Effect",
|
||||
"default": 1.0,
|
||||
"min": 0,
|
||||
"max": 2,
|
||||
"step": 0.01
|
||||
},
|
||||
"u_shinyGloss": {
|
||||
"type": "float",
|
||||
"label": "光泽度 | Gloss",
|
||||
"group": "Effect",
|
||||
"default": 1.0,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"tooltip": "0=白色高光, 1=带颜色 | 0=white shine, 1=color-tinted"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、统一效果组件/系统架构
|
||||
|
||||
### 3.1 抽取通用 ShinyEffect 基类
|
||||
|
||||
```typescript
|
||||
// packages/material-system/src/effects/BaseShinyEffect.ts
|
||||
|
||||
import { Component, Property, Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* Base shiny effect configuration (shared between UI and Sprite).
|
||||
* 基础闪光效果配置(UI 和 Sprite 共享)。
|
||||
*/
|
||||
export abstract class BaseShinyEffect extends Component {
|
||||
// ============= Effect Parameters =============
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Width', min: 0, max: 1, step: 0.01 })
|
||||
public width: number = 0.25;
|
||||
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Rotation', min: 0, max: 360, step: 1 })
|
||||
public rotation: number = 129;
|
||||
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Softness', min: 0, max: 1, step: 0.01 })
|
||||
public softness: number = 1.0;
|
||||
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Brightness', min: 0, max: 2, step: 0.01 })
|
||||
public brightness: number = 1.0;
|
||||
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Gloss', min: 0, max: 2, step: 0.01 })
|
||||
public gloss: number = 1.0;
|
||||
|
||||
// ============= Animation Settings =============
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Play' })
|
||||
public play: boolean = true;
|
||||
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Loop' })
|
||||
public loop: boolean = true;
|
||||
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Duration', min: 0.1, step: 0.1 })
|
||||
public duration: number = 2.0;
|
||||
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Loop Delay', min: 0, step: 0.1 })
|
||||
public loopDelay: number = 2.0;
|
||||
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Initial Delay', min: 0, step: 0.1 })
|
||||
public initialDelay: number = 0;
|
||||
|
||||
// ============= Runtime State =============
|
||||
public progress: number = 0;
|
||||
public elapsedTime: number = 0;
|
||||
public inDelay: boolean = false;
|
||||
public delayRemaining: number = 0;
|
||||
public initialDelayProcessed: boolean = false;
|
||||
|
||||
reset(): void {
|
||||
this.progress = 0;
|
||||
this.elapsedTime = 0;
|
||||
this.inDelay = false;
|
||||
this.delayRemaining = 0;
|
||||
this.initialDelayProcessed = false;
|
||||
}
|
||||
|
||||
start(): void {
|
||||
this.reset();
|
||||
this.play = true;
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
this.play = false;
|
||||
}
|
||||
|
||||
getRotationRadians(): number {
|
||||
return this.rotation * Math.PI / 180;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 通用动画更新逻辑
|
||||
|
||||
```typescript
|
||||
// packages/material-system/src/effects/ShinyEffectAnimator.ts
|
||||
|
||||
import type { BaseShinyEffect } from './BaseShinyEffect';
|
||||
import type { IMaterialOverridable } from '../interfaces/IMaterialOverridable';
|
||||
import { BuiltInShaders } from '../types';
|
||||
|
||||
/**
|
||||
* Shared animator logic for shiny effect.
|
||||
* 闪光效果共享的动画逻辑。
|
||||
*/
|
||||
export class ShinyEffectAnimator {
|
||||
/**
|
||||
* Update animation state.
|
||||
* 更新动画状态。
|
||||
*/
|
||||
static updateAnimation(shiny: BaseShinyEffect, deltaTime: number): void {
|
||||
if (!shiny.initialDelayProcessed && shiny.initialDelay > 0) {
|
||||
shiny.delayRemaining = shiny.initialDelay;
|
||||
shiny.inDelay = true;
|
||||
shiny.initialDelayProcessed = true;
|
||||
}
|
||||
|
||||
if (shiny.inDelay) {
|
||||
shiny.delayRemaining -= deltaTime;
|
||||
if (shiny.delayRemaining <= 0) {
|
||||
shiny.inDelay = false;
|
||||
shiny.elapsedTime = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
shiny.elapsedTime += deltaTime;
|
||||
shiny.progress = Math.min(shiny.elapsedTime / shiny.duration, 1.0);
|
||||
|
||||
if (shiny.progress >= 1.0) {
|
||||
if (shiny.loop) {
|
||||
shiny.inDelay = true;
|
||||
shiny.delayRemaining = shiny.loopDelay;
|
||||
shiny.progress = 0;
|
||||
shiny.elapsedTime = 0;
|
||||
} else {
|
||||
shiny.play = false;
|
||||
shiny.progress = 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply material overrides.
|
||||
* 应用材质覆盖。
|
||||
*/
|
||||
static applyMaterialOverrides(shiny: BaseShinyEffect, target: IMaterialOverridable): void {
|
||||
if (target.getMaterialId() === 0) {
|
||||
target.setMaterialId(BuiltInShaders.Shiny);
|
||||
}
|
||||
|
||||
target.setOverrideFloat('u_shinyProgress', shiny.progress);
|
||||
target.setOverrideFloat('u_shinyWidth', shiny.width);
|
||||
target.setOverrideFloat('u_shinyRotation', shiny.getRotationRadians());
|
||||
target.setOverrideFloat('u_shinySoftness', shiny.softness);
|
||||
target.setOverrideFloat('u_shinyBrightness', shiny.brightness);
|
||||
target.setOverrideFloat('u_shinyGloss', shiny.gloss);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、Material Inspector 设计
|
||||
|
||||
### 4.1 组件架构
|
||||
|
||||
```
|
||||
MaterialPropertiesEditor (容器组件)
|
||||
├── ShaderSelector (着色器选择器)
|
||||
├── PropertyGroup (属性分组)
|
||||
│ ├── FloatProperty (浮点属性)
|
||||
│ ├── VectorProperty (向量属性)
|
||||
│ ├── ColorProperty (颜色属性)
|
||||
│ └── TextureProperty (纹理属性)
|
||||
└── OverrideIndicator (覆盖指示器)
|
||||
```
|
||||
|
||||
### 4.2 核心组件
|
||||
|
||||
```typescript
|
||||
// packages/editor-app/src/components/inspectors/material/MaterialPropertiesEditor.tsx
|
||||
|
||||
interface MaterialPropertiesEditorProps {
|
||||
/** Target component implementing IMaterialOverridable */
|
||||
target: IMaterialOverridable;
|
||||
/** Current shader definition with property metadata */
|
||||
shaderDef?: ShaderAssetDefinition;
|
||||
/** Callback when property changes */
|
||||
onChange?: (name: string, value: MaterialPropertyOverride) => void;
|
||||
}
|
||||
|
||||
export const MaterialPropertiesEditor: React.FC<MaterialPropertiesEditorProps> = ({
|
||||
target,
|
||||
shaderDef,
|
||||
onChange
|
||||
}) => {
|
||||
// Group properties by their group field
|
||||
const groupedProps = useMemo(() => {
|
||||
if (!shaderDef?.properties) return {};
|
||||
|
||||
const groups: Record<string, Array<[string, ShaderPropertyMeta]>> = {};
|
||||
for (const [name, meta] of Object.entries(shaderDef.properties)) {
|
||||
if (meta.hidden) continue;
|
||||
const group = meta.group || 'Default';
|
||||
if (!groups[group]) groups[group] = [];
|
||||
groups[group].push([name, meta]);
|
||||
}
|
||||
return groups;
|
||||
}, [shaderDef]);
|
||||
|
||||
return (
|
||||
<div className="material-properties-editor">
|
||||
<ShaderSelector
|
||||
currentShaderId={target.getMaterialId()}
|
||||
onSelect={(id) => target.setMaterialId(id)}
|
||||
/>
|
||||
|
||||
{Object.entries(groupedProps).map(([group, props]) => (
|
||||
<PropertyGroup key={group} title={group}>
|
||||
{props.map(([name, meta]) => (
|
||||
<PropertyField
|
||||
key={name}
|
||||
name={name}
|
||||
meta={meta}
|
||||
value={target.getOverride(name)?.value ?? meta.default}
|
||||
onChange={(value) => {
|
||||
applyOverride(target, name, meta.type, value);
|
||||
onChange?.(name, target.getOverride(name)!);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</PropertyGroup>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、实施计划
|
||||
|
||||
### Phase 1: 接口层 (1-2 天)
|
||||
|
||||
1. **创建 IMaterialOverridable 接口** (`packages/material-system/src/interfaces/`)
|
||||
2. **创建 MaterialOverridableMixin** (`packages/material-system/src/mixins/`)
|
||||
3. **导出新接口** (`packages/material-system/src/index.ts`)
|
||||
|
||||
### Phase 2: 重构现有组件 (2-3 天)
|
||||
|
||||
1. **修改 SpriteComponent**:实现 `IMaterialOverridable`,使用 Mixin
|
||||
2. **修改 UIRenderComponent**:实现 `IMaterialOverridable`,使用 Mixin
|
||||
3. **删除重复代码**:移除各组件中的重复材质方法
|
||||
|
||||
### Phase 3: 统一效果系统 (2-3 天)
|
||||
|
||||
1. **创建 BaseShinyEffect** (`packages/material-system/src/effects/`)
|
||||
2. **创建 ShinyEffectAnimator** (`packages/material-system/src/effects/`)
|
||||
3. **重构 ShinyEffectComponent**:继承 BaseShinyEffect
|
||||
4. **重构 UIShinyEffectComponent**:继承 BaseShinyEffect
|
||||
5. **重构系统**:使用 ShinyEffectAnimator
|
||||
|
||||
### Phase 4: Shader Property 系统 (2-3 天)
|
||||
|
||||
1. **定义 ShaderPropertyMeta 接口**
|
||||
2. **扩展 ShaderDefinition** 添加 properties 字段
|
||||
3. **创建 ShaderLoader** 支持 .shader 文件
|
||||
4. **注册内置着色器属性元数据**
|
||||
|
||||
### Phase 5: Material Inspector (3-4 天)
|
||||
|
||||
1. **创建 MaterialPropertiesEditor 组件**
|
||||
2. **创建 PropertyField 组件** (Float, Vector, Color, Texture)
|
||||
3. **集成到现有 Inspector 系统**
|
||||
4. **支持实时预览**
|
||||
|
||||
---
|
||||
|
||||
## 六、文件修改清单
|
||||
|
||||
| 优先级 | 包 | 文件 | 操作 |
|
||||
|--------|-----|------|------|
|
||||
| P0 | material-system | `src/interfaces/IMaterialOverridable.ts` | 新建 |
|
||||
| P0 | material-system | `src/mixins/MaterialOverridableMixin.ts` | 新建 |
|
||||
| P0 | material-system | `src/interfaces/IShaderProperty.ts` | 新建 |
|
||||
| P1 | material-system | `src/effects/BaseShinyEffect.ts` | 新建 |
|
||||
| P1 | material-system | `src/effects/ShinyEffectAnimator.ts` | 新建 |
|
||||
| P1 | sprite | `src/SpriteComponent.ts` | 重构 |
|
||||
| P1 | ui | `src/components/UIRenderComponent.ts` | 重构 |
|
||||
| P2 | sprite | `src/ShinyEffectComponent.ts` | 重构 |
|
||||
| P2 | ui | `src/components/UIShinyEffectComponent.ts` | 重构 |
|
||||
| P2 | sprite | `src/systems/ShinyEffectSystem.ts` | 重构 |
|
||||
| P2 | ui | `src/systems/render/UIShinyEffectSystem.ts` | 重构 |
|
||||
| P3 | material-system | `src/loaders/ShaderLoader.ts` | 扩展 |
|
||||
| P3 | editor-app | `src/components/inspectors/material/*` | 新建 |
|
||||
|
||||
---
|
||||
|
||||
## 七、Transform 组件统一(可选)
|
||||
|
||||
### 7.1 现状分析
|
||||
|
||||
| 特性 | TransformComponent | UITransformComponent |
|
||||
|------|-------------------|---------------------|
|
||||
| **坐标系** | 绝对坐标 (position.x/y/z) | 相对锚点坐标 (x/y + anchor) |
|
||||
| **尺寸** | ❌ 无 | ✅ width/height + 约束 |
|
||||
| **锚点系统** | ❌ 无 | ✅ anchorMin/Max |
|
||||
| **3D 支持** | ✅ IVector3 | ❌ 纯 2D |
|
||||
| **可见性** | ❌ 无 | ✅ visible, alpha |
|
||||
|
||||
### 7.2 结论
|
||||
|
||||
**不建议完全合并**,但可提取公共基类:
|
||||
|
||||
```typescript
|
||||
// packages/engine-core/src/interfaces/ITransformBase.ts
|
||||
|
||||
export interface ITransformBase {
|
||||
/** 旋转角度(度) | Rotation in degrees */
|
||||
rotation: number;
|
||||
|
||||
/** X 缩放 | Scale X */
|
||||
scaleX: number;
|
||||
|
||||
/** Y 缩放 | Scale Y */
|
||||
scaleY: number;
|
||||
|
||||
/** 本地到世界矩阵 | Local to world matrix */
|
||||
readonly localToWorldMatrix: Matrix2D;
|
||||
|
||||
/** 是否需要更新 | Dirty flag */
|
||||
isDirty: boolean;
|
||||
|
||||
/** 世界坐标 X | World position X */
|
||||
readonly worldX: number;
|
||||
|
||||
/** 世界坐标 Y | World position Y */
|
||||
readonly worldY: number;
|
||||
|
||||
/** 世界旋转 | World rotation */
|
||||
readonly worldRotation: number;
|
||||
|
||||
/** 世界缩放 X | World scale X */
|
||||
readonly worldScaleX: number;
|
||||
|
||||
/** 世界缩放 Y | World scale Y */
|
||||
readonly worldScaleY: number;
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3 收益
|
||||
|
||||
- 渲染系统可以统一处理 `ITransformBase`
|
||||
- 减少 SpriteRenderSystem 和 UIRenderSystem 的重复
|
||||
- Gizmo 系统可以共享变换操作逻辑
|
||||
|
||||
---
|
||||
|
||||
## 八、向后兼容性
|
||||
|
||||
1. **接口兼容**:现有组件的 API 保持不变
|
||||
2. **序列化兼容**:不改变现有序列化格式
|
||||
3. **渐进迁移**:可分阶段进行,不影响现有功能
|
||||
293
docs/changelog.md
Normal file
293
docs/changelog.md
Normal file
@@ -0,0 +1,293 @@
|
||||
# Changelog
|
||||
|
||||
本文档记录 `@esengine/ecs-framework` 核心库的版本更新历史。
|
||||
|
||||
---
|
||||
|
||||
## v2.4.2 (2025-12-25)
|
||||
|
||||
### Features
|
||||
|
||||
- **IncrementalSerializer 实体过滤**: 增量序列化支持 `entityFilter` 选项 (#335)
|
||||
- 创建快照时可按条件过滤实体
|
||||
- 支持按标签、组件类型等自定义过滤逻辑
|
||||
- 适用于只同步部分实体的场景(如只同步玩家)
|
||||
|
||||
```typescript
|
||||
// 只快照玩家实体
|
||||
const snapshot = IncrementalSerializer.createSnapshot(scene, {
|
||||
entityFilter: (entity) => entity.tag === PLAYER_TAG
|
||||
});
|
||||
|
||||
// 只快照有特定组件的实体
|
||||
const snapshot = IncrementalSerializer.createSnapshot(scene, {
|
||||
entityFilter: (entity) => entity.hasComponent(PlayerMarker)
|
||||
});
|
||||
```
|
||||
|
||||
### Refactor
|
||||
|
||||
- 优化 `PlatformWorkerPool` 代码规范,提取为独立模块 (#335)
|
||||
- 优化 `WorkerEntitySystem` 实现,改进代码结构 (#334)
|
||||
- 代码规范化与依赖清理 (#317)
|
||||
- 代码结构优化,添加 `GlobalTypes.ts` 统一类型定义 (#316)
|
||||
|
||||
---
|
||||
|
||||
## v2.4.1 (2025-12-23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复 `IntervalSystem` 时间累加 bug,间隔计时更加准确
|
||||
- 修复 Cocos Creator 兼容性问题,类型导出更完整
|
||||
|
||||
### Documentation
|
||||
|
||||
- 新增 `Core.paused` 属性文档说明
|
||||
|
||||
---
|
||||
|
||||
## v2.4.0 (2025-12-15)
|
||||
|
||||
### Features
|
||||
|
||||
- **EntityHandle 实体句柄**: 轻量级实体引用抽象 (#304)
|
||||
- 28位索引 + 20位代数(generation)设计,高效复用已销毁实体槽位
|
||||
- `EntityHandleManager` 管理句柄生命周期和有效性验证
|
||||
- 支持句柄转换为实体引用,检测悬空引用
|
||||
|
||||
- **SystemScheduler 系统调度器**: 声明式系统调度 (#304)
|
||||
- 新增 `@Stage(name)` 装饰器指定系统执行阶段
|
||||
- 新增 `@Before(SystemClass)` / `@After(SystemClass)` 装饰器声明系统依赖
|
||||
- 新增 `@InSet(setName)` 装饰器将系统归入逻辑分组
|
||||
- 基于拓扑排序自动解析执行顺序,检测循环依赖
|
||||
|
||||
- **EpochManager 变更检测**: 帧级变更追踪机制 (#304)
|
||||
- 跟踪组件添加/修改时间戳(epoch)
|
||||
- 支持查询"自上次检查以来变化的组件"
|
||||
- 适用于脏检测、增量更新等优化场景
|
||||
|
||||
- **CompiledQuery 编译查询**: 预编译类型安全查询 (#304)
|
||||
- 编译时生成优化的查询逻辑,减少运行时开销
|
||||
- 完整的 TypeScript 类型推断支持
|
||||
- 支持 `With`、`Without`、`Changed` 等查询条件组合
|
||||
|
||||
- **PluginServiceRegistry**: 类型安全的插件服务注册表 (#300)
|
||||
- 通过 `Core.pluginServices` 访问
|
||||
- 支持 `ServiceToken<T>` 模式获取服务
|
||||
|
||||
- **组件自动注册**: `@ECSComponent` 装饰器增强 (#302)
|
||||
- 装饰器现在自动注册到 `ComponentRegistry`
|
||||
- 解决 `Decorators ↔ ComponentRegistry` 循环依赖
|
||||
- 新建 `ComponentTypeUtils.ts` 作为底层无依赖模块
|
||||
|
||||
### API Changes
|
||||
|
||||
- `EntitySystem` 添加 `getBefore()` / `getAfter()` / `getSets()` getter 方法
|
||||
- `Entity` 添加 `markDirty()` 辅助方法用于手动触发变更检测
|
||||
- `IScene` 添加 `epochManager` 属性
|
||||
- `CommandBuffer.pendingCount` 修正为返回实际操作数(而非实体数)
|
||||
|
||||
### Documentation
|
||||
|
||||
- 更新系统调度文档,添加声明式依赖配置章节
|
||||
- 更新实体查询文档,添加编译查询使用说明
|
||||
|
||||
---
|
||||
|
||||
## v2.3.2 (2025-12-08)
|
||||
|
||||
### Features
|
||||
|
||||
- **微信小游戏 Worker 支持**: 添加对微信小游戏平台 Worker 的完整支持 (#297)
|
||||
- 新增 `workerScriptPath` 配置项,支持预编译 Worker 脚本路径
|
||||
- 修复微信小游戏 Worker 消息格式差异(`res` 直接是数据,无需 `.data`)
|
||||
- 适用于微信小游戏等不支持动态脚本的平台
|
||||
|
||||
### New Package
|
||||
|
||||
- **@esengine/worker-generator** `v1.0.2`: CLI 工具,从 `WorkerEntitySystem` 子类自动生成 Worker 文件
|
||||
- 自动扫描并提取 `workerProcess` 方法体
|
||||
- 支持 `--wechat` 模式,使用 TypeScript 编译器转换为 ES5 语法
|
||||
- 读取代码中的 `workerScriptPath` 配置,生成到指定路径
|
||||
- 生成 `worker-mapping.json` 映射文件
|
||||
|
||||
### Documentation
|
||||
|
||||
- 更新 Worker 系统文档,添加微信小游戏支持章节
|
||||
- 新增英文版 Worker 系统文档 (`docs/en/guide/worker-system.md`)
|
||||
|
||||
---
|
||||
|
||||
## v2.3.1 (2025-12-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **类型导出修复**: 修复 v2.3.0 中的类型导出问题
|
||||
- 解决 `ServiceToken` 跨包类型兼容性问题
|
||||
- 修复 `editor-app` 和 `behavior-tree-editor` 中的类型引用
|
||||
|
||||
---
|
||||
|
||||
## v2.3.0 (2025-12-06) ⚠️ DEPRECATED
|
||||
|
||||
> **警告**: 此版本存在类型导出问题,请升级到 v2.3.1 或更高版本。
|
||||
>
|
||||
> **Warning**: This version has type export issues. Please upgrade to v2.3.1 or later.
|
||||
|
||||
### Features
|
||||
|
||||
- **持久化实体**: 添加实体跨场景迁移支持 (#285)
|
||||
- 新增 `EEntityLifecyclePolicy` 枚举(`SceneLocal`/`Persistent`)
|
||||
- Entity 添加 `setPersistent()`、`setSceneLocal()`、`isPersistent` API
|
||||
- Scene 添加 `findPersistentEntities()`、`extractPersistentEntities()`、`receiveMigratedEntities()`
|
||||
- `SceneManager.setScene()` 自动处理持久化实体迁移
|
||||
- 适用场景:全局管理器、玩家角色、跨场景状态保持
|
||||
|
||||
- **CommandBuffer 延迟命令系统**: 在帧末统一执行实体操作 (#281)
|
||||
- 支持延迟添加/移除组件、销毁实体、设置实体激活状态
|
||||
- 每个系统拥有独立的 `commands` 属性
|
||||
- 避免在迭代过程中修改实体列表,防止迭代问题
|
||||
- Scene 在 `lateUpdate` 后自动刷新所有命令缓冲区
|
||||
|
||||
### Performance
|
||||
|
||||
- **ReactiveQuery 快照优化**: 优化实体查询迭代性能 (#281)
|
||||
- 添加快照机制,避免每帧拷贝数组
|
||||
- 只在实体列表变化时创建新快照
|
||||
- 静态场景下多个系统共享同一快照
|
||||
|
||||
---
|
||||
|
||||
## v2.2.21 (2025-12-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **迭代安全修复**: 修复 `process`/`lateProcess` 迭代时组件变化导致跳过实体的问题 (#272)
|
||||
- 在系统处理过程中添加/移除组件不再导致实体被意外跳过
|
||||
|
||||
### Performance
|
||||
|
||||
- **HierarchySystem 性能优化**: 优化层级系统避免每帧遍历所有实体 (#279)
|
||||
- 使用脏实体集合代替每帧遍历所有实体
|
||||
- 静态场景下 `process()` 从 O(n) 优化为 O(1)
|
||||
- 1000 实体静态场景: 81.79μs → 0.07μs (快 1168 倍)
|
||||
- 10000 实体静态场景: 939.43μs → 0.56μs (快 1677 倍)
|
||||
- 服务端模拟 (100房间 x 100实体): 2.7ms → 1.4ms 每 tick
|
||||
|
||||
---
|
||||
|
||||
## v2.2.20 (2025-12-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **系统 onAdded 回调修复**: 修复系统 `onAdded` 回调受注册顺序影响的问题 (#270)
|
||||
- 系统初始化时会对已存在的匹配实体触发 `onAdded` 回调
|
||||
- 新增 `matchesEntity(entity)` 方法,用于检查实体是否匹配系统的查询条件
|
||||
- Scene 新增 `notifySystemsEntityAdded/Removed` 方法,确保所有系统都能收到实体变更通知
|
||||
|
||||
---
|
||||
|
||||
## v2.2.19 (2025-12-03)
|
||||
|
||||
### Features
|
||||
|
||||
- **系统稳定排序**: 添加系统稳定排序支持 (#257)
|
||||
- 新增 `addOrder` 属性,用于 `updateOrder` 相同时的稳定排序
|
||||
- 确保相同优先级的系统按添加顺序执行
|
||||
|
||||
- **模块配置**: 添加 `module.json` 配置文件 (#256)
|
||||
- 定义模块 ID、名称、版本等元信息
|
||||
- 支持模块依赖声明和导出配置
|
||||
|
||||
---
|
||||
|
||||
## v2.2.18 (2025-11-30)
|
||||
|
||||
### Features
|
||||
|
||||
- **高级性能分析器**: 实现全新的性能分析 SDK (#248)
|
||||
- `ProfilerSDK`: 统一的性能分析接口
|
||||
- 手动采样标记 (`beginSample`/`endSample`)
|
||||
- 自动作用域测量 (`measure`/`measureAsync`)
|
||||
- 调用层级追踪和调用图生成
|
||||
- 计数器和仪表支持
|
||||
- `AdvancedProfilerCollector`: 高级性能数据收集器
|
||||
- 帧时间统计和历史记录
|
||||
- 内存快照和 GC 检测
|
||||
- 长任务检测 (Long Task API)
|
||||
- 性能报告生成
|
||||
- `DebugManager`: 调试管理器
|
||||
- 统一的调试工具入口
|
||||
- 性能分析器集成
|
||||
|
||||
- **属性装饰器增强**: 扩展 `@serialize` 装饰器功能 (#247)
|
||||
- 支持更多序列化选项配置
|
||||
|
||||
### Improvements
|
||||
|
||||
- **EntitySystem 测试覆盖**: 添加完整的系统测试用例 (#240)
|
||||
- 覆盖实体查询、缓存、生命周期等场景
|
||||
|
||||
- **Matcher 增强**: 优化匹配器功能 (#240)
|
||||
- 改进匹配逻辑和性能
|
||||
|
||||
---
|
||||
|
||||
## v2.2.17 (2025-11-28)
|
||||
|
||||
### Features
|
||||
|
||||
- **ComponentRegistry 增强**: 添加组件注册表新功能 (#244)
|
||||
- 支持通过名称注册和查询组件类型
|
||||
- 添加组件掩码缓存优化性能
|
||||
|
||||
- **序列化装饰器改进**: 增强 `@serialize` 装饰器 (#244)
|
||||
- 支持更灵活的序列化配置
|
||||
- 改进嵌套对象序列化
|
||||
|
||||
- **EntitySystem 生命周期**: 新增系统生命周期方法 (#244)
|
||||
- `onSceneStart()`: 场景开始时调用
|
||||
- `onSceneStop()`: 场景停止时调用
|
||||
|
||||
---
|
||||
|
||||
## v2.2.16 (2025-11-27)
|
||||
|
||||
### Features
|
||||
|
||||
- **组件生命周期**: 添加组件生命周期回调支持 (#237)
|
||||
- `onDeserialized()`: 组件从场景文件加载或快照恢复后调用,用于恢复运行时数据
|
||||
|
||||
- **ServiceContainer 增强**: 改进服务容器功能 (#237)
|
||||
- 支持 `Symbol.for()` 模式的服务标识
|
||||
- 新增 `@InjectProperty` 属性注入装饰器
|
||||
- 改进服务解析和生命周期管理
|
||||
|
||||
- **SceneSerializer 增强**: 场景序列化器新功能 (#237)
|
||||
- 支持更多组件类型的序列化
|
||||
- 改进反序列化错误处理
|
||||
|
||||
- **属性装饰器扩展**: 扩展 `@serialize` 装饰器 (#238)
|
||||
- 支持 `@range`、`@slider` 等编辑器提示
|
||||
- 支持 `@dropdown`、`@color` 等 UI 类型
|
||||
- 支持 `@asset` 资源引用类型
|
||||
|
||||
### Improvements
|
||||
|
||||
- **Matcher 测试**: 添加 Matcher 匹配器测试用例 (#240)
|
||||
- **EntitySystem 测试**: 添加实体系统完整测试覆盖 (#240)
|
||||
|
||||
---
|
||||
|
||||
## 版本说明
|
||||
|
||||
- **主版本号**: 重大不兼容更新
|
||||
- **次版本号**: 新功能添加(向后兼容)
|
||||
- **修订版本号**: Bug 修复和小改进
|
||||
|
||||
## 相关链接
|
||||
|
||||
- [GitHub Releases](https://github.com/esengine/esengine/releases)
|
||||
- [NPM Package](https://www.npmjs.com/package/@esengine/ecs-framework)
|
||||
- [文档首页](./index.md)
|
||||
291
docs/en/changelog.md
Normal file
291
docs/en/changelog.md
Normal file
@@ -0,0 +1,291 @@
|
||||
# Changelog
|
||||
|
||||
This document records the version update history of the `@esengine/ecs-framework` core library.
|
||||
|
||||
---
|
||||
|
||||
## v2.4.2 (2025-12-25)
|
||||
|
||||
### Features
|
||||
|
||||
- **IncrementalSerializer Entity Filter**: Incremental serialization supports `entityFilter` option (#335)
|
||||
- Filter entities by condition when creating snapshots
|
||||
- Support custom filter logic by tag, component type, etc.
|
||||
- Suitable for scenarios that only sync partial entities (e.g., only sync players)
|
||||
|
||||
```typescript
|
||||
// Only snapshot player entities
|
||||
const snapshot = IncrementalSerializer.createSnapshot(scene, {
|
||||
entityFilter: (entity) => entity.tag === PLAYER_TAG
|
||||
});
|
||||
|
||||
// Only snapshot entities with specific component
|
||||
const snapshot = IncrementalSerializer.createSnapshot(scene, {
|
||||
entityFilter: (entity) => entity.hasComponent(PlayerMarker)
|
||||
});
|
||||
```
|
||||
|
||||
### Refactor
|
||||
|
||||
- Optimize `PlatformWorkerPool` code style, extract as standalone module (#335)
|
||||
- Optimize `WorkerEntitySystem` implementation, improve code structure (#334)
|
||||
- Code standardization and dependency cleanup (#317)
|
||||
- Code structure optimization, add `GlobalTypes.ts` for unified type definitions (#316)
|
||||
|
||||
---
|
||||
|
||||
## v2.4.1 (2025-12-23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix `IntervalSystem` time accumulation bug, interval timing is now more accurate
|
||||
- Fix Cocos Creator compatibility issue, more complete type exports
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add `Core.paused` property documentation
|
||||
|
||||
---
|
||||
|
||||
## v2.4.0 (2025-12-15)
|
||||
|
||||
### Features
|
||||
|
||||
- **EntityHandle**: Lightweight entity reference abstraction (#304)
|
||||
- 28-bit index + 20-bit generation design for efficient reuse of destroyed entity slots
|
||||
- `EntityHandleManager` manages handle lifecycle and validity verification
|
||||
- Support handle-to-entity conversion with dangling reference detection
|
||||
|
||||
- **SystemScheduler**: Declarative system scheduling (#304)
|
||||
- New `@Stage(name)` decorator to specify system execution stage
|
||||
- New `@Before(SystemClass)` / `@After(SystemClass)` decorators to declare dependencies
|
||||
- New `@InSet(setName)` decorator to group systems logically
|
||||
- Automatic execution order resolution via topological sort with cycle detection
|
||||
|
||||
- **EpochManager**: Frame-level change detection mechanism (#304)
|
||||
- Track component add/modify timestamps (epochs)
|
||||
- Support querying "components changed since last check"
|
||||
- Suitable for dirty checking, incremental updates, and other optimization scenarios
|
||||
|
||||
- **CompiledQuery**: Pre-compiled type-safe queries (#304)
|
||||
- Compile-time generated optimized query logic, reducing runtime overhead
|
||||
- Full TypeScript type inference support
|
||||
- Support `With`, `Without`, `Changed` and other query condition combinations
|
||||
|
||||
- **PluginServiceRegistry**: Type-safe plugin service registry (#300)
|
||||
- Accessible via `Core.pluginServices`
|
||||
- Support `ServiceToken<T>` pattern for service retrieval
|
||||
|
||||
- **Component Auto-Registration**: `@ECSComponent` decorator enhancement (#302)
|
||||
- Decorator now automatically registers to `ComponentRegistry`
|
||||
- Resolved `Decorators ↔ ComponentRegistry` circular dependency
|
||||
- New `ComponentTypeUtils.ts` as low-level dependency-free module
|
||||
|
||||
### API Changes
|
||||
|
||||
- `EntitySystem` adds `getBefore()` / `getAfter()` / `getSets()` getter methods
|
||||
- `Entity` adds `markDirty()` helper method for manual change detection triggering
|
||||
- `IScene` adds `epochManager` property
|
||||
- `CommandBuffer.pendingCount` corrected to return actual operation count (not entity count)
|
||||
|
||||
### Documentation
|
||||
|
||||
- Updated system scheduling documentation with declarative dependency configuration
|
||||
- Updated entity query documentation with compiled query usage
|
||||
|
||||
---
|
||||
|
||||
## v2.3.2 (2025-12-08)
|
||||
|
||||
### Features
|
||||
|
||||
- **WeChat Mini Game Worker Support**: Add complete Worker support for WeChat Mini Game platform (#297)
|
||||
- New `workerScriptPath` config option for pre-compiled Worker script path
|
||||
- Fix WeChat Mini Game Worker message format difference (`res` is data directly, no `.data` wrapper)
|
||||
- Applicable to WeChat Mini Game and other platforms that don't support dynamic scripts
|
||||
|
||||
### New Package
|
||||
|
||||
- **@esengine/worker-generator** `v1.0.2`: CLI tool to auto-generate Worker files from `WorkerEntitySystem` subclasses
|
||||
- Automatically scan and extract `workerProcess` method body
|
||||
- Support `--wechat` mode, use TypeScript compiler to convert to ES5 syntax
|
||||
- Read `workerScriptPath` config from code, generate to specified path
|
||||
- Generate `worker-mapping.json` mapping file
|
||||
|
||||
### Documentation
|
||||
|
||||
- Updated Worker system documentation with WeChat Mini Game support section
|
||||
- Added English Worker system documentation (`docs/en/guide/worker-system.md`)
|
||||
|
||||
---
|
||||
|
||||
## v2.3.1 (2025-12-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Type export fix**: Fix type export issues in v2.3.0
|
||||
- Resolve `ServiceToken` cross-package type compatibility issues
|
||||
- Fix type references in `editor-app` and `behavior-tree-editor`
|
||||
|
||||
---
|
||||
|
||||
## v2.3.0 (2025-12-06) ⚠️ DEPRECATED
|
||||
|
||||
> **Warning**: This version has type export issues. Please upgrade to v2.3.1 or later.
|
||||
|
||||
### Features
|
||||
|
||||
- **Persistent Entity**: Add entity cross-scene migration support (#285)
|
||||
- New `EEntityLifecyclePolicy` enum (`SceneLocal`/`Persistent`)
|
||||
- Entity adds `setPersistent()`, `setSceneLocal()`, `isPersistent` API
|
||||
- Scene adds `findPersistentEntities()`, `extractPersistentEntities()`, `receiveMigratedEntities()`
|
||||
- `SceneManager.setScene()` automatically handles persistent entity migration
|
||||
- Use cases: global managers, player characters, cross-scene state persistence
|
||||
|
||||
- **CommandBuffer Deferred Command System**: Execute entity operations uniformly at end of frame (#281)
|
||||
- Support deferred add/remove components, destroy entities, set entity active state
|
||||
- Each system has its own `commands` property
|
||||
- Avoid modifying entity list during iteration, preventing iteration issues
|
||||
- Scene automatically flushes all command buffers after `lateUpdate`
|
||||
|
||||
### Performance
|
||||
|
||||
- **ReactiveQuery Snapshot Optimization**: Optimize entity query iteration performance (#281)
|
||||
- Add snapshot mechanism to avoid copying arrays every frame
|
||||
- Only create new snapshots when entity list changes
|
||||
- Multiple systems share the same snapshot in static scenes
|
||||
|
||||
---
|
||||
|
||||
## v2.2.21 (2025-12-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Iteration safety fix**: Fix issue where component changes during `process`/`lateProcess` iteration caused entities to be skipped (#272)
|
||||
- Adding/removing components during system processing no longer causes entities to be unexpectedly skipped
|
||||
|
||||
### Performance
|
||||
|
||||
- **HierarchySystem optimization**: Optimize hierarchy system to avoid iterating all entities every frame (#279)
|
||||
- Use dirty entity set instead of iterating all entities
|
||||
- Static scene `process()` optimized from O(n) to O(1)
|
||||
- 1000 entities static scene: 81.79μs → 0.07μs (1168x faster)
|
||||
- 10000 entities static scene: 939.43μs → 0.56μs (1677x faster)
|
||||
- Server simulation (100 rooms x 100 entities): 2.7ms → 1.4ms per tick
|
||||
|
||||
---
|
||||
|
||||
## v2.2.20 (2025-12-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **System onAdded callback fix**: Fix issue where system `onAdded` callback was affected by registration order (#270)
|
||||
- System initialization now triggers `onAdded` callback for existing matching entities
|
||||
- Added `matchesEntity(entity)` method to check if an entity matches the system's query condition
|
||||
- Scene added `notifySystemsEntityAdded/Removed` methods to ensure all systems receive entity change notifications
|
||||
|
||||
---
|
||||
|
||||
## v2.2.19 (2025-12-03)
|
||||
|
||||
### Features
|
||||
|
||||
- **System stable sorting**: Add stable sorting support for systems (#257)
|
||||
- Added `addOrder` property for stable sorting when `updateOrder` is the same
|
||||
- Ensures systems with same priority execute in add order
|
||||
|
||||
- **Module configuration**: Add `module.json` configuration file (#256)
|
||||
- Define module ID, name, version and other metadata
|
||||
- Support module dependency declaration and export configuration
|
||||
|
||||
---
|
||||
|
||||
## v2.2.18 (2025-11-30)
|
||||
|
||||
### Features
|
||||
|
||||
- **Advanced Performance Profiler**: Implement new performance analysis SDK (#248)
|
||||
- `ProfilerSDK`: Unified performance analysis interface
|
||||
- Manual sampling markers (`beginSample`/`endSample`)
|
||||
- Automatic scope measurement (`measure`/`measureAsync`)
|
||||
- Call hierarchy tracking and call graph generation
|
||||
- Counter and gauge support
|
||||
- `AdvancedProfilerCollector`: Advanced performance data collector
|
||||
- Frame time statistics and history
|
||||
- Memory snapshots and GC detection
|
||||
- Long task detection (Long Task API)
|
||||
- Performance report generation
|
||||
- `DebugManager`: Debug manager
|
||||
- Unified debug tool entry point
|
||||
- Profiler integration
|
||||
|
||||
- **Property decorator enhancement**: Extend `@serialize` decorator functionality (#247)
|
||||
- Support more serialization option configurations
|
||||
|
||||
### Improvements
|
||||
|
||||
- **EntitySystem test coverage**: Add complete system test cases (#240)
|
||||
- Cover entity query, cache, lifecycle scenarios
|
||||
|
||||
- **Matcher enhancement**: Optimize matcher functionality (#240)
|
||||
- Improved matching logic and performance
|
||||
|
||||
---
|
||||
|
||||
## v2.2.17 (2025-11-28)
|
||||
|
||||
### Features
|
||||
|
||||
- **ComponentRegistry enhancement**: Add new component registry features (#244)
|
||||
- Support registering and querying component types by name
|
||||
- Add component mask caching for performance optimization
|
||||
|
||||
- **Serialization decorator improvements**: Enhance `@serialize` decorator (#244)
|
||||
- Support more flexible serialization configuration
|
||||
- Improved nested object serialization
|
||||
|
||||
- **EntitySystem lifecycle**: Add new system lifecycle methods (#244)
|
||||
- `onSceneStart()`: Called when scene starts
|
||||
- `onSceneStop()`: Called when scene stops
|
||||
|
||||
---
|
||||
|
||||
## v2.2.16 (2025-11-27)
|
||||
|
||||
### Features
|
||||
|
||||
- **Component lifecycle**: Add component lifecycle callback support (#237)
|
||||
- `onDeserialized()`: Called after component is loaded from scene file or snapshot restore, used to restore runtime data
|
||||
|
||||
- **ServiceContainer enhancement**: Improve service container functionality (#237)
|
||||
- Support `Symbol.for()` pattern for service identifiers
|
||||
- Added `@InjectProperty` property injection decorator
|
||||
- Improved service resolution and lifecycle management
|
||||
|
||||
- **SceneSerializer enhancement**: New scene serializer features (#237)
|
||||
- Support serialization of more component types
|
||||
- Improved deserialization error handling
|
||||
|
||||
- **Property decorator extension**: Extend `@serialize` decorator (#238)
|
||||
- Support `@range`, `@slider` and other editor hints
|
||||
- Support `@dropdown`, `@color` and other UI types
|
||||
- Support `@asset` resource reference type
|
||||
|
||||
### Improvements
|
||||
|
||||
- **Matcher tests**: Add Matcher test cases (#240)
|
||||
- **EntitySystem tests**: Add complete entity system test coverage (#240)
|
||||
|
||||
---
|
||||
|
||||
## Version Notes
|
||||
|
||||
- **Major version**: Breaking changes
|
||||
- **Minor version**: New features (backward compatible)
|
||||
- **Patch version**: Bug fixes and improvements
|
||||
|
||||
## Related Links
|
||||
|
||||
- [GitHub Releases](https://github.com/esengine/esengine/releases)
|
||||
- [NPM Package](https://www.npmjs.com/package/@esengine/ecs-framework)
|
||||
- [Documentation Home](./index.md)
|
||||
444
docs/en/guide/entity.md
Normal file
444
docs/en/guide/entity.md
Normal file
@@ -0,0 +1,444 @@
|
||||
# Entity
|
||||
|
||||
In ECS architecture, an Entity is the basic object in the game world. An entity itself does not contain game logic or data - it's just a container that combines different components to achieve various functionalities.
|
||||
|
||||
## Basic Concepts
|
||||
|
||||
An entity is a lightweight object mainly used for:
|
||||
- Serving as a container for components
|
||||
- Providing a unique identifier (ID)
|
||||
- Managing component lifecycle
|
||||
|
||||
::: tip About Parent-Child Hierarchy
|
||||
Parent-child hierarchy relationships between entities are managed through `HierarchyComponent` and `HierarchySystem`, not built-in Entity properties. This design follows ECS composition principles - only entities that need hierarchy relationships add this component.
|
||||
|
||||
See [Hierarchy System](./hierarchy.md) documentation.
|
||||
:::
|
||||
|
||||
## Creating Entities
|
||||
|
||||
**Important: Entities must be created through Scene, manual creation is not supported!**
|
||||
|
||||
Entities must be created through the scene's `createEntity()` method to ensure:
|
||||
- Entity is properly added to the scene's entity management system
|
||||
- Entity is added to the query system for system use
|
||||
- Entity gets the correct scene reference
|
||||
- Related lifecycle events are triggered
|
||||
|
||||
```typescript
|
||||
// Correct way: create entity through scene
|
||||
const player = scene.createEntity("Player");
|
||||
|
||||
// Wrong way: manually create entity
|
||||
// const entity = new Entity("MyEntity", 1); // System cannot manage such entities
|
||||
```
|
||||
|
||||
## Adding Components
|
||||
|
||||
Entities gain functionality by adding components:
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent } from '@esengine/ecs-framework';
|
||||
|
||||
// Define position component
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
x: number = 0;
|
||||
y: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
|
||||
// Define health component
|
||||
@ECSComponent('Health')
|
||||
class Health extends Component {
|
||||
current: number = 100;
|
||||
max: number = 100;
|
||||
|
||||
constructor(max: number = 100) {
|
||||
super();
|
||||
this.max = max;
|
||||
this.current = max;
|
||||
}
|
||||
}
|
||||
|
||||
// Add components to entity
|
||||
const player = scene.createEntity("Player");
|
||||
player.addComponent(new Position(100, 200));
|
||||
player.addComponent(new Health(150));
|
||||
```
|
||||
|
||||
## Getting Components
|
||||
|
||||
```typescript
|
||||
// Get component (pass component class, not instance)
|
||||
const position = player.getComponent(Position); // Returns Position | null
|
||||
const health = player.getComponent(Health); // Returns Health | null
|
||||
|
||||
// Check if component exists
|
||||
if (position) {
|
||||
console.log(`Player position: x=${position.x}, y=${position.y}`);
|
||||
}
|
||||
|
||||
// Check if entity has a component
|
||||
if (player.hasComponent(Position)) {
|
||||
console.log("Player has position component");
|
||||
}
|
||||
|
||||
// Get all component instances (read-only property)
|
||||
const allComponents = player.components; // readonly Component[]
|
||||
|
||||
// Get all components of specified type (supports multiple components of same type)
|
||||
const allHealthComponents = player.getComponents(Health); // Health[]
|
||||
|
||||
// Get or create component (creates automatically if not exists)
|
||||
const position = player.getOrCreateComponent(Position, 0, 0); // Pass constructor arguments
|
||||
const health = player.getOrCreateComponent(Health, 100); // Returns existing if present, creates new if not
|
||||
```
|
||||
|
||||
## Removing Components
|
||||
|
||||
```typescript
|
||||
// Method 1: Remove by component type
|
||||
const removedHealth = player.removeComponentByType(Health);
|
||||
if (removedHealth) {
|
||||
console.log("Health component removed");
|
||||
}
|
||||
|
||||
// Method 2: Remove by component instance
|
||||
const healthComponent = player.getComponent(Health);
|
||||
if (healthComponent) {
|
||||
player.removeComponent(healthComponent);
|
||||
}
|
||||
|
||||
// Batch remove multiple component types
|
||||
const removedComponents = player.removeComponentsByTypes([Position, Health]);
|
||||
|
||||
// Check if component was removed
|
||||
if (!player.hasComponent(Health)) {
|
||||
console.log("Health component has been removed");
|
||||
}
|
||||
```
|
||||
|
||||
## Finding Entities
|
||||
|
||||
Scene provides multiple ways to find entities:
|
||||
|
||||
### Find by Name
|
||||
|
||||
```typescript
|
||||
// Find single entity
|
||||
const player = scene.findEntity("Player");
|
||||
// Or use alias method
|
||||
const player2 = scene.getEntityByName("Player");
|
||||
|
||||
if (player) {
|
||||
console.log("Found player entity");
|
||||
}
|
||||
```
|
||||
|
||||
### Find by ID
|
||||
|
||||
```typescript
|
||||
// Find by entity ID
|
||||
const entity = scene.findEntityById(123);
|
||||
```
|
||||
|
||||
### Find by Tag
|
||||
|
||||
Entities support a tag system for quick categorization and lookup:
|
||||
|
||||
```typescript
|
||||
// Set tags
|
||||
player.tag = 1; // Player tag
|
||||
enemy.tag = 2; // Enemy tag
|
||||
|
||||
// Find all entities by tag
|
||||
const players = scene.findEntitiesByTag(1);
|
||||
const enemies = scene.findEntitiesByTag(2);
|
||||
// Or use alias method
|
||||
const allPlayers = scene.getEntitiesByTag(1);
|
||||
```
|
||||
|
||||
## Entity Lifecycle
|
||||
|
||||
```typescript
|
||||
// Destroy entity
|
||||
player.destroy();
|
||||
|
||||
// Check if entity is destroyed
|
||||
if (player.isDestroyed) {
|
||||
console.log("Entity has been destroyed");
|
||||
}
|
||||
```
|
||||
|
||||
## Entity Events
|
||||
|
||||
Component changes on entities trigger events:
|
||||
|
||||
```typescript
|
||||
// Listen for component added event
|
||||
scene.eventSystem.on('component:added', (data) => {
|
||||
console.log('Component added:', data);
|
||||
});
|
||||
|
||||
// Listen for entity created event
|
||||
scene.eventSystem.on('entity:created', (data) => {
|
||||
console.log('Entity created:', data.entityName);
|
||||
});
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Batch Entity Creation
|
||||
|
||||
The framework provides high-performance batch creation methods:
|
||||
|
||||
```typescript
|
||||
// Batch create 100 bullet entities (high-performance version)
|
||||
const bullets = scene.createEntities(100, "Bullet");
|
||||
|
||||
// Add components to each bullet
|
||||
bullets.forEach((bullet, index) => {
|
||||
bullet.addComponent(new Position(Math.random() * 800, Math.random() * 600));
|
||||
bullet.addComponent(new Velocity(Math.random() * 100 - 50, Math.random() * 100 - 50));
|
||||
});
|
||||
```
|
||||
|
||||
`createEntities()` method will:
|
||||
- Batch allocate entity IDs
|
||||
- Batch add to entity list
|
||||
- Optimize query system updates
|
||||
- Reduce system cache clearing times
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Appropriate Component Granularity
|
||||
|
||||
```typescript
|
||||
// Good practice: single-purpose components
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
x: number = 0;
|
||||
y: number = 0;
|
||||
}
|
||||
|
||||
@ECSComponent('Velocity')
|
||||
class Velocity extends Component {
|
||||
dx: number = 0;
|
||||
dy: number = 0;
|
||||
}
|
||||
|
||||
// Avoid: overly complex components
|
||||
@ECSComponent('Player')
|
||||
class Player extends Component {
|
||||
// Avoid including too many unrelated properties in one component
|
||||
x: number;
|
||||
y: number;
|
||||
health: number;
|
||||
inventory: Item[];
|
||||
skills: Skill[];
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Use Decorators
|
||||
|
||||
Always use `@ECSComponent` decorator:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Transform')
|
||||
class Transform extends Component {
|
||||
// Component implementation
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Proper Naming
|
||||
|
||||
```typescript
|
||||
// Clear entity naming
|
||||
const mainCharacter = scene.createEntity("MainCharacter");
|
||||
const enemy1 = scene.createEntity("Goblin_001");
|
||||
const collectible = scene.createEntity("HealthPotion");
|
||||
```
|
||||
|
||||
### 4. Timely Cleanup
|
||||
|
||||
```typescript
|
||||
// Destroy entities that are no longer needed
|
||||
if (enemy.getComponent(Health).current <= 0) {
|
||||
enemy.destroy();
|
||||
}
|
||||
```
|
||||
|
||||
## Debugging Entities
|
||||
|
||||
The framework provides debugging features to help development:
|
||||
|
||||
```typescript
|
||||
// Get entity debug info
|
||||
const debugInfo = entity.getDebugInfo();
|
||||
console.log('Entity info:', debugInfo);
|
||||
|
||||
// List all components of entity
|
||||
entity.components.forEach(component => {
|
||||
console.log('Component:', component.constructor.name);
|
||||
});
|
||||
```
|
||||
|
||||
Entities are one of the core concepts in ECS architecture. Understanding how to use entities correctly will help you build efficient, maintainable game code.
|
||||
|
||||
## Entity Handle (EntityHandle)
|
||||
|
||||
Entity handles provide a safe way to reference entities, solving the "referencing destroyed entity" problem.
|
||||
|
||||
### Problem Scenario
|
||||
|
||||
Suppose your AI system needs to track a target enemy:
|
||||
|
||||
```typescript
|
||||
// Wrong approach: directly store entity reference
|
||||
class AISystem extends EntitySystem {
|
||||
private targetEnemy: Entity | null = null;
|
||||
|
||||
setTarget(enemy: Entity) {
|
||||
this.targetEnemy = enemy;
|
||||
}
|
||||
|
||||
process() {
|
||||
if (this.targetEnemy) {
|
||||
// Dangerous! Enemy might be destroyed, but reference still exists
|
||||
// Worse: this memory location might be reused by a new entity
|
||||
const health = this.targetEnemy.getComponent(Health);
|
||||
// Might operate on the wrong entity!
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Correct Approach Using Handles
|
||||
|
||||
Each entity is automatically assigned a handle when created, accessible via `entity.handle`:
|
||||
|
||||
```typescript
|
||||
import { EntityHandle, NULL_HANDLE, isValidHandle } from '@esengine/ecs-framework';
|
||||
|
||||
class AISystem extends EntitySystem {
|
||||
// Store handle instead of entity reference
|
||||
private targetHandle: EntityHandle = NULL_HANDLE;
|
||||
|
||||
setTarget(enemy: Entity) {
|
||||
// Save enemy's handle
|
||||
this.targetHandle = enemy.handle;
|
||||
}
|
||||
|
||||
process() {
|
||||
if (!isValidHandle(this.targetHandle)) {
|
||||
return; // No target
|
||||
}
|
||||
|
||||
// Get entity through handle (automatically checks validity)
|
||||
const enemy = this.scene.findEntityByHandle(this.targetHandle);
|
||||
|
||||
if (!enemy) {
|
||||
// Enemy was destroyed, clear reference
|
||||
this.targetHandle = NULL_HANDLE;
|
||||
return;
|
||||
}
|
||||
|
||||
// Safe operation
|
||||
const health = enemy.getComponent(Health);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Complete Example: Skill Target Locking
|
||||
|
||||
```typescript
|
||||
import {
|
||||
EntitySystem, Entity, EntityHandle, NULL_HANDLE, isValidHandle
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
@ECSSystem('SkillTargeting')
|
||||
class SkillTargetingSystem extends EntitySystem {
|
||||
// Store handles for multiple targets
|
||||
private lockedTargets: Map<Entity, EntityHandle> = new Map();
|
||||
|
||||
// Lock target
|
||||
lockTarget(caster: Entity, target: Entity) {
|
||||
this.lockedTargets.set(caster, target.handle);
|
||||
}
|
||||
|
||||
// Get locked target
|
||||
getLockedTarget(caster: Entity): Entity | null {
|
||||
const handle = this.lockedTargets.get(caster);
|
||||
|
||||
if (!handle || !isValidHandle(handle)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// findEntityByHandle checks if handle is valid
|
||||
const target = this.scene.findEntityByHandle(handle);
|
||||
|
||||
if (!target) {
|
||||
// Target died, clear lock
|
||||
this.lockedTargets.delete(caster);
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
// Cast skill
|
||||
castSkill(caster: Entity) {
|
||||
const target = this.getLockedTarget(caster);
|
||||
|
||||
if (!target) {
|
||||
console.log('Target lost, skill cancelled');
|
||||
return;
|
||||
}
|
||||
|
||||
// Safely deal damage to target
|
||||
const health = target.getComponent(Health);
|
||||
if (health) {
|
||||
health.current -= 10;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Handle vs Entity Reference
|
||||
|
||||
| Scenario | Recommended Approach |
|
||||
|----------|---------------------|
|
||||
| Temporary use within same frame | Use `Entity` reference directly |
|
||||
| Cross-frame storage (e.g., AI target, skill target) | Use `EntityHandle` |
|
||||
| Needs serialization | Use `EntityHandle` (numeric type) |
|
||||
| Network synchronization | Use `EntityHandle` (can be transmitted directly) |
|
||||
|
||||
### API Quick Reference
|
||||
|
||||
```typescript
|
||||
// Get entity's handle
|
||||
const handle = entity.handle;
|
||||
|
||||
// Check if handle is non-null
|
||||
if (isValidHandle(handle)) { ... }
|
||||
|
||||
// Get entity through handle (automatically checks validity)
|
||||
const entity = scene.findEntityByHandle(handle);
|
||||
|
||||
// Check if entity corresponding to handle is alive
|
||||
const alive = scene.handleManager.isAlive(handle);
|
||||
|
||||
// Null handle constant
|
||||
const emptyHandle = NULL_HANDLE;
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Learn about [Hierarchy System](./hierarchy.md) to establish parent-child relationships
|
||||
- Learn about [Component System](./component.md) to add functionality to entities
|
||||
- Learn about [Scene Management](./scene.md) to organize and manage entities
|
||||
441
docs/en/guide/getting-started.md
Normal file
441
docs/en/guide/getting-started.md
Normal file
@@ -0,0 +1,441 @@
|
||||
# Quick Start
|
||||
|
||||
This guide will help you get started with ECS Framework, from installation to creating your first ECS application.
|
||||
|
||||
## Installation
|
||||
|
||||
### Using CLI (Recommended)
|
||||
|
||||
The easiest way to add ECS to your existing project:
|
||||
|
||||
```bash
|
||||
# In your project directory
|
||||
npx @esengine/cli init
|
||||
```
|
||||
|
||||
The CLI automatically detects your project type (Cocos Creator 2.x/3.x, LayaAir 3.x, or Node.js) and generates the necessary integration code, including:
|
||||
|
||||
- `ECSManager` component/script - Manages ECS lifecycle
|
||||
- Example components and systems - Helps you get started quickly
|
||||
- Automatic dependency installation
|
||||
|
||||
### Manual NPM Installation
|
||||
|
||||
If you prefer manual configuration:
|
||||
|
||||
```bash
|
||||
# Using npm
|
||||
npm install @esengine/ecs-framework
|
||||
```
|
||||
|
||||
## Initialize Core
|
||||
|
||||
### Basic Initialization
|
||||
|
||||
The core of ECS Framework is the `Core` class, a singleton that manages the entire framework lifecycle.
|
||||
|
||||
```typescript
|
||||
import { Core } from '@esengine/ecs-framework'
|
||||
|
||||
// Method 1: Using config object (recommended)
|
||||
const core = Core.create({
|
||||
debug: true, // Enable debug mode for detailed logs and performance monitoring
|
||||
debugConfig: { // Optional: Advanced debug configuration
|
||||
enabled: false, // Whether to enable WebSocket debug server
|
||||
websocketUrl: 'ws://localhost:8080',
|
||||
debugFrameRate: 30, // Debug data send frame rate
|
||||
channels: {
|
||||
entities: true,
|
||||
systems: true,
|
||||
performance: true,
|
||||
components: true,
|
||||
scenes: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Method 2: Simplified creation (backward compatible)
|
||||
const core = Core.create(true); // Equivalent to { debug: true }
|
||||
|
||||
// Method 3: Production environment configuration
|
||||
const core = Core.create({
|
||||
debug: false // Disable debug in production
|
||||
});
|
||||
```
|
||||
|
||||
### Core Configuration Details
|
||||
|
||||
```typescript
|
||||
interface ICoreConfig {
|
||||
/** Enable debug mode - affects log level and performance monitoring */
|
||||
debug?: boolean;
|
||||
|
||||
/** Advanced debug configuration - for dev tools integration */
|
||||
debugConfig?: {
|
||||
enabled: boolean; // Enable debug server
|
||||
websocketUrl: string; // WebSocket server URL
|
||||
autoReconnect?: boolean; // Auto reconnect
|
||||
debugFrameRate?: 60 | 30 | 15; // Debug data send frame rate
|
||||
channels: { // Data channel configuration
|
||||
entities: boolean; // Entity data
|
||||
systems: boolean; // System data
|
||||
performance: boolean; // Performance data
|
||||
components: boolean; // Component data
|
||||
scenes: boolean; // Scene data
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Core Instance Management
|
||||
|
||||
Core uses singleton pattern, accessible via static property after creation:
|
||||
|
||||
```typescript
|
||||
// Create instance
|
||||
const core = Core.create(true);
|
||||
|
||||
// Get created instance
|
||||
const instance = Core.Instance; // Returns current instance, null if not created
|
||||
```
|
||||
|
||||
### Game Loop Integration
|
||||
|
||||
**Important**: Before creating entities and systems, you need to understand how to integrate ECS Framework into your game engine.
|
||||
|
||||
`Core.update(deltaTime)` is the framework heartbeat, must be called every frame. It handles:
|
||||
- Updating the built-in Time class
|
||||
- Updating all global managers (timers, object pools, etc.)
|
||||
- Updating all entity systems in all scenes
|
||||
- Processing entity creation and destruction
|
||||
- Collecting performance data (in debug mode)
|
||||
|
||||
See engine integration examples: [Game Engine Integration](#game-engine-integration)
|
||||
|
||||
## Create Your First ECS Application
|
||||
|
||||
### 1. Define Components
|
||||
|
||||
Components are pure data containers that store entity state:
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent } from '@esengine/ecs-framework'
|
||||
|
||||
// Position component
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
x: number = 0
|
||||
y: number = 0
|
||||
|
||||
constructor(x: number = 0, y: number = 0) {
|
||||
super()
|
||||
this.x = x
|
||||
this.y = y
|
||||
}
|
||||
}
|
||||
|
||||
// Velocity component
|
||||
@ECSComponent('Velocity')
|
||||
class Velocity extends Component {
|
||||
dx: number = 0
|
||||
dy: number = 0
|
||||
|
||||
constructor(dx: number = 0, dy: number = 0) {
|
||||
super()
|
||||
this.dx = dx
|
||||
this.dy = dy
|
||||
}
|
||||
}
|
||||
|
||||
// Sprite component
|
||||
@ECSComponent('Sprite')
|
||||
class Sprite extends Component {
|
||||
texture: string = ''
|
||||
width: number = 32
|
||||
height: number = 32
|
||||
|
||||
constructor(texture: string, width: number = 32, height: number = 32) {
|
||||
super()
|
||||
this.texture = texture
|
||||
this.width = width
|
||||
this.height = height
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Create Entity Systems
|
||||
|
||||
Systems contain game logic and process entities with specific components. ECS Framework provides Matcher-based entity filtering:
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, Matcher, Time, ECSSystem } from '@esengine/ecs-framework'
|
||||
|
||||
// Movement system - handles position and velocity
|
||||
@ECSSystem('MovementSystem')
|
||||
class MovementSystem extends EntitySystem {
|
||||
|
||||
constructor() {
|
||||
// Use Matcher to define target entities: must have both Position and Velocity
|
||||
super(Matcher.empty().all(Position, Velocity))
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// process method receives all matching entities
|
||||
for (const entity of entities) {
|
||||
const position = entity.getComponent(Position)!
|
||||
const velocity = entity.getComponent(Velocity)!
|
||||
|
||||
// Update position (using framework's Time class)
|
||||
position.x += velocity.dx * Time.deltaTime
|
||||
position.y += velocity.dy * Time.deltaTime
|
||||
|
||||
// Boundary check example
|
||||
if (position.x < 0) position.x = 0
|
||||
if (position.y < 0) position.y = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Render system - handles visible objects
|
||||
@ECSSystem('RenderSystem')
|
||||
class RenderSystem extends EntitySystem {
|
||||
|
||||
constructor() {
|
||||
// Must have Position and Sprite, optional Velocity (for direction)
|
||||
super(Matcher.empty().all(Position, Sprite).any(Velocity))
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const position = entity.getComponent(Position)!
|
||||
const sprite = entity.getComponent(Sprite)!
|
||||
const velocity = entity.getComponent(Velocity) // May be null
|
||||
|
||||
// Flip sprite based on velocity direction (optional logic)
|
||||
let flipX = false
|
||||
if (velocity && velocity.dx < 0) {
|
||||
flipX = true
|
||||
}
|
||||
|
||||
// Render logic (pseudocode here)
|
||||
this.drawSprite(sprite.texture, position.x, position.y, sprite.width, sprite.height, flipX)
|
||||
}
|
||||
}
|
||||
|
||||
private drawSprite(texture: string, x: number, y: number, width: number, height: number, flipX: boolean = false) {
|
||||
// Actual render implementation depends on your game engine
|
||||
const direction = flipX ? '<-' : '->'
|
||||
console.log(`Render ${texture} at (${x.toFixed(1)}, ${y.toFixed(1)}) direction: ${direction}`)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### 3. Create Scene
|
||||
|
||||
Recommended to extend Scene class for custom scenes:
|
||||
|
||||
```typescript
|
||||
import { Scene } from '@esengine/ecs-framework'
|
||||
|
||||
// Recommended: Extend Scene for custom scene
|
||||
class GameScene extends Scene {
|
||||
|
||||
initialize(): void {
|
||||
// Scene initialization logic
|
||||
this.name = "MainScene";
|
||||
|
||||
// Add systems to scene
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
}
|
||||
|
||||
onStart(): void {
|
||||
// Logic when scene starts running
|
||||
console.log("Game scene started");
|
||||
}
|
||||
|
||||
unload(): void {
|
||||
// Cleanup logic when scene unloads
|
||||
console.log("Game scene unloaded");
|
||||
}
|
||||
}
|
||||
|
||||
// Create and set scene
|
||||
const gameScene = new GameScene();
|
||||
Core.setScene(gameScene);
|
||||
```
|
||||
|
||||
### 4. Create Entities
|
||||
|
||||
```typescript
|
||||
// Create player entity
|
||||
const player = gameScene.createEntity("Player");
|
||||
player.addComponent(new Position(100, 100));
|
||||
player.addComponent(new Velocity(50, 30)); // Move 50px/sec (x), 30px/sec (y)
|
||||
player.addComponent(new Sprite("player.png", 64, 64));
|
||||
```
|
||||
|
||||
## Scene Management
|
||||
|
||||
Core has built-in scene management, very simple to use:
|
||||
|
||||
```typescript
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// Initialize Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// Create and set scene
|
||||
class GameScene extends Scene {
|
||||
initialize(): void {
|
||||
this.name = "GamePlay";
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
}
|
||||
}
|
||||
|
||||
const gameScene = new GameScene();
|
||||
Core.setScene(gameScene);
|
||||
|
||||
// Game loop (auto-updates scene)
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // Auto-updates global services and scene
|
||||
}
|
||||
|
||||
// Switch scenes
|
||||
Core.loadScene(new MenuScene()); // Delayed switch (next frame)
|
||||
Core.setScene(new GameScene()); // Immediate switch
|
||||
|
||||
// Access current scene
|
||||
const currentScene = Core.scene;
|
||||
|
||||
// Using fluent API
|
||||
const player = Core.ecsAPI?.createEntity('Player')
|
||||
.addComponent(Position, 100, 100)
|
||||
.addComponent(Velocity, 50, 0);
|
||||
```
|
||||
|
||||
### Advanced: Using WorldManager for Multi-World
|
||||
|
||||
Only for complex server-side applications (MMO game servers, game room systems, etc.):
|
||||
|
||||
```typescript
|
||||
import { Core, WorldManager } from '@esengine/ecs-framework';
|
||||
|
||||
// Initialize Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// Get WorldManager from service container (Core auto-creates and registers it)
|
||||
const worldManager = Core.services.resolve(WorldManager);
|
||||
|
||||
// Create multiple independent game worlds
|
||||
const room1 = worldManager.createWorld('room_001');
|
||||
const room2 = worldManager.createWorld('room_002');
|
||||
|
||||
// Create scenes in each world
|
||||
const gameScene1 = room1.createScene('game', new GameScene());
|
||||
const gameScene2 = room2.createScene('game', new GameScene());
|
||||
|
||||
// Activate scenes
|
||||
room1.setSceneActive('game', true);
|
||||
room2.setSceneActive('game', true);
|
||||
|
||||
// Game loop (need to manually update worlds)
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // Update global services
|
||||
worldManager.updateAll(); // Manually update all worlds
|
||||
}
|
||||
```
|
||||
|
||||
## Game Engine Integration
|
||||
|
||||
### Laya 3.x Engine Integration
|
||||
|
||||
Using `Laya.Script` component to manage ECS lifecycle is recommended:
|
||||
|
||||
```typescript
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
const { regClass } = Laya;
|
||||
|
||||
@regClass()
|
||||
export class ECSManager extends Laya.Script {
|
||||
private ecsScene = new GameScene();
|
||||
|
||||
onAwake(): void {
|
||||
// Initialize ECS
|
||||
Core.create({ debug: true });
|
||||
Core.setScene(this.ecsScene);
|
||||
}
|
||||
|
||||
onUpdate(): void {
|
||||
// Auto-updates global services and scene
|
||||
Core.update(Laya.timer.delta / 1000);
|
||||
}
|
||||
|
||||
onDestroy(): void {
|
||||
// Cleanup resources
|
||||
Core.destroy();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In Laya IDE, attach the `ECSManager` script to a node in your scene.
|
||||
|
||||
### Cocos Creator Integration
|
||||
|
||||
```typescript
|
||||
import { Component, _decorator } from 'cc';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
const { ccclass } = _decorator;
|
||||
|
||||
@ccclass('ECSGameManager')
|
||||
export class ECSGameManager extends Component {
|
||||
onLoad() {
|
||||
// Initialize ECS
|
||||
Core.create(true);
|
||||
Core.setScene(new GameScene());
|
||||
}
|
||||
|
||||
update(deltaTime: number) {
|
||||
// Auto-updates global services and scene
|
||||
Core.update(deltaTime);
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
// Cleanup resources
|
||||
Core.destroy();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Next Steps
|
||||
|
||||
You've successfully created your first ECS application! Next you can:
|
||||
|
||||
- Check the complete [API Documentation](/api/README)
|
||||
- Explore more [practical examples](/examples/)
|
||||
|
||||
## FAQ
|
||||
|
||||
### Why isn't my system executing?
|
||||
|
||||
Ensure:
|
||||
1. System is added to scene: `this.addSystem(system)` (in Scene's initialize method)
|
||||
2. Scene is set: `Core.setScene(scene)`
|
||||
3. Game loop is calling: `Core.update(deltaTime)`
|
||||
|
||||
### How to debug ECS applications?
|
||||
|
||||
Enable debug mode:
|
||||
|
||||
```typescript
|
||||
Core.create({ debug: true })
|
||||
|
||||
// Get debug data
|
||||
const debugData = Core.getDebugData()
|
||||
console.log(debugData)
|
||||
```
|
||||
43
docs/en/guide/index.md
Normal file
43
docs/en/guide/index.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Guide
|
||||
|
||||
Welcome to the ECS Framework Guide. This guide covers the core concepts and usage of the framework.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### [Entity](/guide/entity)
|
||||
Learn the basics of ECS architecture - how to use entities, lifecycle management, and best practices.
|
||||
|
||||
### [Component](/guide/component)
|
||||
Learn how to create and use components for modular game feature design.
|
||||
|
||||
### [System](/guide/system)
|
||||
Master system development to implement game logic processing.
|
||||
|
||||
### [Entity Query & Matcher](/guide/entity-query)
|
||||
Learn to use Matcher for entity filtering and queries with `all`, `any`, `none`, `nothing` conditions.
|
||||
|
||||
### [Scene](/guide/scene)
|
||||
Understand scene lifecycle, system management, and entity container features.
|
||||
|
||||
### [Event System](/guide/event-system)
|
||||
Master the type-safe event system for component communication and system coordination.
|
||||
|
||||
### [Serialization](/guide/serialization)
|
||||
Master serialization for scenes, entities, and components. Supports full and incremental serialization for game saves, network sync, and more.
|
||||
|
||||
### [Time and Timers](/guide/time-and-timers)
|
||||
Learn time management and timer systems for precise game logic timing control.
|
||||
|
||||
### [Logging](/guide/logging)
|
||||
Master the leveled logging system for debugging, monitoring, and error tracking.
|
||||
|
||||
### [Platform Adapter](/guide/platform-adapter)
|
||||
Learn how to implement and register platform adapters for browsers, mini-games, Node.js, and more.
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### [Service Container](/guide/service-container)
|
||||
Master dependency injection and service management for loosely-coupled architecture.
|
||||
|
||||
### [Plugin System](/guide/plugin-system)
|
||||
Learn how to develop and use plugins to extend framework functionality.
|
||||
360
docs/en/guide/persistent-entity.md
Normal file
360
docs/en/guide/persistent-entity.md
Normal file
@@ -0,0 +1,360 @@
|
||||
# Persistent Entity
|
||||
|
||||
> **Version**: v2.3.0+
|
||||
|
||||
Persistent Entity is a special type of entity that automatically migrates to the new scene during scene transitions. It is suitable for game objects that need to maintain state across scenes, such as players, game managers, audio managers, etc.
|
||||
|
||||
## Basic Concepts
|
||||
|
||||
In the ECS framework, entities have two lifecycle policies:
|
||||
|
||||
| Policy | Description | Default |
|
||||
|--------|-------------|---------|
|
||||
| `SceneLocal` | Scene-local entity, destroyed when scene changes | ✓ |
|
||||
| `Persistent` | Persistent entity, automatically migrates during scene transitions | |
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Creating a Persistent Entity
|
||||
|
||||
```typescript
|
||||
import { Scene } from '@esengine/ecs-framework';
|
||||
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Create a persistent player entity
|
||||
const player = this.createEntity('Player').setPersistent();
|
||||
player.addComponent(new Position(100, 200));
|
||||
player.addComponent(new PlayerData('Hero', 500));
|
||||
|
||||
// Create a normal enemy entity (destroyed when scene changes)
|
||||
const enemy = this.createEntity('Enemy');
|
||||
enemy.addComponent(new Position(300, 200));
|
||||
enemy.addComponent(new EnemyAI());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Behavior During Scene Transitions
|
||||
|
||||
```typescript
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// Initial scene
|
||||
class Level1Scene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Player - persistent, will migrate to the next scene
|
||||
const player = this.createEntity('Player').setPersistent();
|
||||
player.addComponent(new Position(0, 0));
|
||||
player.addComponent(new Health(100));
|
||||
|
||||
// Enemy - scene-local, destroyed when scene changes
|
||||
const enemy = this.createEntity('Enemy');
|
||||
enemy.addComponent(new Position(100, 100));
|
||||
}
|
||||
}
|
||||
|
||||
// Target scene
|
||||
class Level2Scene extends Scene {
|
||||
protected initialize(): void {
|
||||
// New enemy
|
||||
const enemy = this.createEntity('Boss');
|
||||
enemy.addComponent(new Position(200, 200));
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
// Player has automatically migrated to this scene
|
||||
const player = this.findEntity('Player');
|
||||
console.log(player !== null); // true
|
||||
|
||||
// Position and health data are fully preserved
|
||||
const position = player?.getComponent(Position);
|
||||
const health = player?.getComponent(Health);
|
||||
console.log(position?.x, position?.y); // 0, 0
|
||||
console.log(health?.value); // 100
|
||||
}
|
||||
}
|
||||
|
||||
// Switch scenes
|
||||
Core.create({ debug: true });
|
||||
Core.setScene(new Level1Scene());
|
||||
|
||||
// Later switch to Level2
|
||||
Core.loadScene(new Level2Scene());
|
||||
// Player entity migrates automatically, Enemy entity is destroyed
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Entity Methods
|
||||
|
||||
#### setPersistent()
|
||||
|
||||
Marks the entity as persistent, preventing destruction during scene transitions.
|
||||
|
||||
```typescript
|
||||
public setPersistent(): this
|
||||
```
|
||||
|
||||
**Returns**: Returns the entity itself for method chaining
|
||||
|
||||
**Example**:
|
||||
```typescript
|
||||
const player = scene.createEntity('Player')
|
||||
.setPersistent();
|
||||
|
||||
player.addComponent(new Position(100, 200));
|
||||
```
|
||||
|
||||
#### setSceneLocal()
|
||||
|
||||
Restores the entity to scene-local policy (default).
|
||||
|
||||
```typescript
|
||||
public setSceneLocal(): this
|
||||
```
|
||||
|
||||
**Returns**: Returns the entity itself for method chaining
|
||||
|
||||
**Example**:
|
||||
```typescript
|
||||
// Dynamically cancel persistence
|
||||
player.setSceneLocal();
|
||||
```
|
||||
|
||||
#### isPersistent
|
||||
|
||||
Checks if the entity is persistent.
|
||||
|
||||
```typescript
|
||||
public get isPersistent(): boolean
|
||||
```
|
||||
|
||||
**Example**:
|
||||
```typescript
|
||||
if (entity.isPersistent) {
|
||||
console.log('This is a persistent entity');
|
||||
}
|
||||
```
|
||||
|
||||
#### lifecyclePolicy
|
||||
|
||||
Gets the entity's lifecycle policy.
|
||||
|
||||
```typescript
|
||||
public get lifecyclePolicy(): EEntityLifecyclePolicy
|
||||
```
|
||||
|
||||
**Example**:
|
||||
```typescript
|
||||
import { EEntityLifecyclePolicy } from '@esengine/ecs-framework';
|
||||
|
||||
if (entity.lifecyclePolicy === EEntityLifecyclePolicy.Persistent) {
|
||||
console.log('Persistent entity');
|
||||
}
|
||||
```
|
||||
|
||||
### Scene Methods
|
||||
|
||||
#### findPersistentEntities()
|
||||
|
||||
Finds all persistent entities in the scene.
|
||||
|
||||
```typescript
|
||||
public findPersistentEntities(): Entity[]
|
||||
```
|
||||
|
||||
**Returns**: Array of persistent entities
|
||||
|
||||
**Example**:
|
||||
```typescript
|
||||
const persistentEntities = scene.findPersistentEntities();
|
||||
console.log(`Scene has ${persistentEntities.length} persistent entities`);
|
||||
```
|
||||
|
||||
#### extractPersistentEntities()
|
||||
|
||||
Extracts and removes all persistent entities from the scene (typically called internally by the framework).
|
||||
|
||||
```typescript
|
||||
public extractPersistentEntities(): Entity[]
|
||||
```
|
||||
|
||||
**Returns**: Array of extracted persistent entities
|
||||
|
||||
#### receiveMigratedEntities()
|
||||
|
||||
Receives migrated entities (typically called internally by the framework).
|
||||
|
||||
```typescript
|
||||
public receiveMigratedEntities(entities: Entity[]): void
|
||||
```
|
||||
|
||||
**Parameters**:
|
||||
- `entities` - Array of entities to receive
|
||||
|
||||
## Use Cases
|
||||
|
||||
### 1. Player Entity Across Levels
|
||||
|
||||
```typescript
|
||||
class PlayerSetupScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Player maintains state across all levels
|
||||
const player = this.createEntity('Player').setPersistent();
|
||||
player.addComponent(new Transform(0, 0));
|
||||
player.addComponent(new Health(100));
|
||||
player.addComponent(new Inventory());
|
||||
player.addComponent(new PlayerStats());
|
||||
}
|
||||
}
|
||||
|
||||
class Level1 extends Scene { /* ... */ }
|
||||
class Level2 extends Scene { /* ... */ }
|
||||
class Level3 extends Scene { /* ... */ }
|
||||
|
||||
// Player entity automatically migrates between all levels
|
||||
Core.setScene(new PlayerSetupScene());
|
||||
// ... game progresses
|
||||
Core.loadScene(new Level1());
|
||||
// ... level complete
|
||||
Core.loadScene(new Level2());
|
||||
// Player data (health, inventory, stats) fully preserved
|
||||
```
|
||||
|
||||
### 2. Global Managers
|
||||
|
||||
```typescript
|
||||
class BootstrapScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Audio manager - persists across scenes
|
||||
const audioManager = this.createEntity('AudioManager').setPersistent();
|
||||
audioManager.addComponent(new AudioController());
|
||||
|
||||
// Achievement manager - persists across scenes
|
||||
const achievementManager = this.createEntity('AchievementManager').setPersistent();
|
||||
achievementManager.addComponent(new AchievementTracker());
|
||||
|
||||
// Game settings - persists across scenes
|
||||
const settings = this.createEntity('GameSettings').setPersistent();
|
||||
settings.addComponent(new SettingsData());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Dynamically Toggling Persistence
|
||||
|
||||
```typescript
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Initially created as a normal entity
|
||||
const companion = this.createEntity('Companion');
|
||||
companion.addComponent(new Transform(0, 0));
|
||||
companion.addComponent(new CompanionAI());
|
||||
|
||||
// Listen for recruitment event
|
||||
this.eventSystem.on('companion:recruited', () => {
|
||||
// After recruitment, become persistent
|
||||
companion.setPersistent();
|
||||
console.log('Companion joined the party, will follow player across scenes');
|
||||
});
|
||||
|
||||
// Listen for dismissal event
|
||||
this.eventSystem.on('companion:dismissed', () => {
|
||||
// After dismissal, restore to scene-local
|
||||
companion.setSceneLocal();
|
||||
console.log('Companion left the party, will no longer persist across scenes');
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Clearly Identify Persistent Entities
|
||||
|
||||
```typescript
|
||||
// Recommended: Mark immediately when creating
|
||||
const player = this.createEntity('Player').setPersistent();
|
||||
|
||||
// Not recommended: Marking after creation (easy to forget)
|
||||
const player = this.createEntity('Player');
|
||||
// ... lots of code ...
|
||||
player.setPersistent(); // Easy to forget
|
||||
```
|
||||
|
||||
### 2. Use Persistence Appropriately
|
||||
|
||||
```typescript
|
||||
// ✓ Entities suitable for persistence
|
||||
const player = this.createEntity('Player').setPersistent(); // Player
|
||||
const gameManager = this.createEntity('GameManager').setPersistent(); // Global manager
|
||||
const audioManager = this.createEntity('AudioManager').setPersistent(); // Audio system
|
||||
|
||||
// ✗ Entities that should NOT be persistent
|
||||
const bullet = this.createEntity('Bullet'); // Temporary objects
|
||||
const enemy = this.createEntity('Enemy'); // Level-specific enemies
|
||||
const particle = this.createEntity('Particle'); // Effect particles
|
||||
```
|
||||
|
||||
### 3. Check Migrated Entities
|
||||
|
||||
```typescript
|
||||
class NewScene extends Scene {
|
||||
public onStart(): void {
|
||||
// Check if expected persistent entities exist
|
||||
const player = this.findEntity('Player');
|
||||
if (!player) {
|
||||
console.error('Player entity did not migrate correctly!');
|
||||
// Handle error case
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Avoid Circular References
|
||||
|
||||
```typescript
|
||||
// ✗ Avoid: Persistent entity referencing scene-local entity
|
||||
class BadScene extends Scene {
|
||||
protected initialize(): void {
|
||||
const player = this.createEntity('Player').setPersistent();
|
||||
const enemy = this.createEntity('Enemy');
|
||||
|
||||
// Dangerous: player is persistent but enemy is not
|
||||
// After scene change, enemy is destroyed, reference becomes invalid
|
||||
player.addComponent(new TargetComponent(enemy));
|
||||
}
|
||||
}
|
||||
|
||||
// ✓ Recommended: Use ID references or event system
|
||||
class GoodScene extends Scene {
|
||||
protected initialize(): void {
|
||||
const player = this.createEntity('Player').setPersistent();
|
||||
const enemy = this.createEntity('Enemy');
|
||||
|
||||
// Store ID instead of direct reference
|
||||
player.addComponent(new TargetComponent(enemy.id));
|
||||
|
||||
// Or use event system for communication
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
1. **Destroyed entities will not migrate**: If an entity is destroyed before scene transition, it will not migrate even if marked as persistent.
|
||||
|
||||
2. **Component data is fully preserved**: All components and their state are preserved during migration.
|
||||
|
||||
3. **Scene reference is updated**: After migration, the entity's `scene` property will point to the new scene.
|
||||
|
||||
4. **Query system is updated**: Migrated entities are automatically registered in the new scene's query system.
|
||||
|
||||
5. **Delayed transitions also work**: Persistent entities migrate when using `Core.loadScene()` for delayed transitions as well.
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Scene](./scene) - Learn the basics of scenes
|
||||
- [SceneManager](./scene-manager) - Learn about scene transitions
|
||||
- [WorldManager](./world-manager) - Learn about multi-world management
|
||||
436
docs/en/guide/scene-manager.md
Normal file
436
docs/en/guide/scene-manager.md
Normal file
@@ -0,0 +1,436 @@
|
||||
# SceneManager
|
||||
|
||||
SceneManager is a lightweight scene manager provided by ECS Framework, suitable for 95% of game applications. It provides a simple and intuitive API with support for scene transitions and delayed loading.
|
||||
|
||||
## Use Cases
|
||||
|
||||
SceneManager is suitable for:
|
||||
- Single-player games
|
||||
- Simple multiplayer games
|
||||
- Mobile games
|
||||
- Games requiring scene transitions (menu, game, pause, etc.)
|
||||
- Projects that don't need multi-World isolation
|
||||
|
||||
## Features
|
||||
|
||||
- Lightweight, zero extra overhead
|
||||
- Simple and intuitive API
|
||||
- Supports delayed scene transitions (avoids switching mid-frame)
|
||||
- Automatic ECS fluent API management
|
||||
- Automatic scene lifecycle handling
|
||||
- Integrated with Core, auto-updated
|
||||
- Supports [Persistent Entity](./persistent-entity) migration across scenes (v2.3.0+)
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Recommended: Using Core's Static Methods
|
||||
|
||||
This is the simplest and recommended approach, suitable for most applications:
|
||||
|
||||
```typescript
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// 1. Initialize Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 2. Create and set scene
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "GameScene";
|
||||
|
||||
// Add systems
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
|
||||
// Create initial entities
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Transform(400, 300));
|
||||
player.addComponent(new Health(100));
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log("Game scene started");
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Set scene
|
||||
Core.setScene(new GameScene());
|
||||
|
||||
// 4. Game loop (Core.update automatically updates the scene)
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // Automatically updates all services and scenes
|
||||
}
|
||||
|
||||
// Laya engine integration
|
||||
Laya.timer.frameLoop(1, this, () => {
|
||||
const deltaTime = Laya.timer.delta / 1000;
|
||||
Core.update(deltaTime);
|
||||
});
|
||||
|
||||
// Cocos Creator integration
|
||||
update(deltaTime: number) {
|
||||
Core.update(deltaTime);
|
||||
}
|
||||
```
|
||||
|
||||
### Advanced: Using SceneManager Directly
|
||||
|
||||
If you need more control, you can use SceneManager directly:
|
||||
|
||||
```typescript
|
||||
import { Core, SceneManager, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// Initialize Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// Get SceneManager (already auto-created and registered by Core)
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
|
||||
// Set scene
|
||||
const gameScene = new GameScene();
|
||||
sceneManager.setScene(gameScene);
|
||||
|
||||
// Game loop (still use Core.update)
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // Core automatically calls sceneManager.update()
|
||||
}
|
||||
```
|
||||
|
||||
**Important**: Regardless of which approach you use, you should only call `Core.update()` in the game loop. It automatically updates SceneManager and scenes. You don't need to manually call `sceneManager.update()`.
|
||||
|
||||
## Scene Transitions
|
||||
|
||||
### Immediate Transition
|
||||
|
||||
Use `Core.setScene()` or `sceneManager.setScene()` to immediately switch scenes:
|
||||
|
||||
```typescript
|
||||
// Method 1: Using Core (recommended)
|
||||
Core.setScene(new MenuScene());
|
||||
|
||||
// Method 2: Using SceneManager
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
sceneManager.setScene(new MenuScene());
|
||||
```
|
||||
|
||||
### Delayed Transition
|
||||
|
||||
Use `Core.loadScene()` or `sceneManager.loadScene()` for delayed scene transition, which takes effect on the next frame:
|
||||
|
||||
```typescript
|
||||
// Method 1: Using Core (recommended)
|
||||
Core.loadScene(new GameOverScene());
|
||||
|
||||
// Method 2: Using SceneManager
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
sceneManager.loadScene(new GameOverScene());
|
||||
```
|
||||
|
||||
When switching scenes from within a System, use delayed transitions:
|
||||
|
||||
```typescript
|
||||
class GameOverSystem extends EntitySystem {
|
||||
process(entities: readonly Entity[]): void {
|
||||
const player = entities.find(e => e.name === 'Player');
|
||||
const health = player?.getComponent(Health);
|
||||
|
||||
if (health && health.value <= 0) {
|
||||
// Delayed transition to game over scene (takes effect next frame)
|
||||
Core.loadScene(new GameOverScene());
|
||||
// Current frame continues execution, won't interrupt current system processing
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Core Static Methods (Recommended)
|
||||
|
||||
#### Core.setScene()
|
||||
|
||||
Immediately switch scenes.
|
||||
|
||||
```typescript
|
||||
public static setScene<T extends IScene>(scene: T): T
|
||||
```
|
||||
|
||||
**Parameters**:
|
||||
- `scene` - The scene instance to set
|
||||
|
||||
**Returns**:
|
||||
- Returns the set scene instance
|
||||
|
||||
**Example**:
|
||||
```typescript
|
||||
const gameScene = Core.setScene(new GameScene());
|
||||
console.log(gameScene.name);
|
||||
```
|
||||
|
||||
#### Core.loadScene()
|
||||
|
||||
Delayed scene loading (switches on next frame).
|
||||
|
||||
```typescript
|
||||
public static loadScene<T extends IScene>(scene: T): void
|
||||
```
|
||||
|
||||
**Parameters**:
|
||||
- `scene` - The scene instance to load
|
||||
|
||||
**Example**:
|
||||
```typescript
|
||||
Core.loadScene(new GameOverScene());
|
||||
```
|
||||
|
||||
#### Core.scene
|
||||
|
||||
Get the currently active scene.
|
||||
|
||||
```typescript
|
||||
public static get scene(): IScene | null
|
||||
```
|
||||
|
||||
**Returns**:
|
||||
- Current scene instance, or null if no scene
|
||||
|
||||
**Example**:
|
||||
```typescript
|
||||
const currentScene = Core.scene;
|
||||
if (currentScene) {
|
||||
console.log(`Current scene: ${currentScene.name}`);
|
||||
}
|
||||
```
|
||||
|
||||
### SceneManager Methods (Advanced)
|
||||
|
||||
If you need to use SceneManager directly, get it through the service container:
|
||||
|
||||
```typescript
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
```
|
||||
|
||||
#### setScene()
|
||||
|
||||
Immediately switch scenes.
|
||||
|
||||
```typescript
|
||||
public setScene<T extends IScene>(scene: T): T
|
||||
```
|
||||
|
||||
#### loadScene()
|
||||
|
||||
Delayed scene loading.
|
||||
|
||||
```typescript
|
||||
public loadScene<T extends IScene>(scene: T): void
|
||||
```
|
||||
|
||||
#### currentScene
|
||||
|
||||
Get the current scene.
|
||||
|
||||
```typescript
|
||||
public get currentScene(): IScene | null
|
||||
```
|
||||
|
||||
#### hasScene
|
||||
|
||||
Check if there's an active scene.
|
||||
|
||||
```typescript
|
||||
public get hasScene(): boolean
|
||||
```
|
||||
|
||||
#### hasPendingScene
|
||||
|
||||
Check if there's a pending scene transition.
|
||||
|
||||
```typescript
|
||||
public get hasPendingScene(): boolean
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Use Core's Static Methods
|
||||
|
||||
```typescript
|
||||
// Recommended: Use Core's static methods
|
||||
Core.setScene(new GameScene());
|
||||
Core.loadScene(new MenuScene());
|
||||
const currentScene = Core.scene;
|
||||
|
||||
// Not recommended: Don't directly use SceneManager unless you have special needs
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
sceneManager.setScene(new GameScene());
|
||||
```
|
||||
|
||||
### 2. Only Call Core.update()
|
||||
|
||||
```typescript
|
||||
// Correct: Only call Core.update()
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // Automatically updates all services and scenes
|
||||
}
|
||||
|
||||
// Incorrect: Don't manually call sceneManager.update()
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime);
|
||||
sceneManager.update(); // Duplicate update, will cause issues!
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Use Delayed Transitions to Avoid Issues
|
||||
|
||||
When switching scenes from within a System, use `loadScene()` instead of `setScene()`:
|
||||
|
||||
```typescript
|
||||
// Recommended: Delayed transition
|
||||
class HealthSystem extends EntitySystem {
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health);
|
||||
if (health.value <= 0) {
|
||||
Core.loadScene(new GameOverScene());
|
||||
// Current frame continues processing other entities
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Not recommended: Immediate transition may cause issues
|
||||
class HealthSystem extends EntitySystem {
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health);
|
||||
if (health.value <= 0) {
|
||||
Core.setScene(new GameOverScene());
|
||||
// Scene switches immediately, other entities in current frame may not process correctly
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Scene Responsibility Separation
|
||||
|
||||
Each scene should be responsible for only one specific game state:
|
||||
|
||||
```typescript
|
||||
// Good design - clear responsibilities
|
||||
class MenuScene extends Scene {
|
||||
// Only handles menu-related logic
|
||||
}
|
||||
|
||||
class GameScene extends Scene {
|
||||
// Only handles gameplay logic
|
||||
}
|
||||
|
||||
class PauseScene extends Scene {
|
||||
// Only handles pause screen logic
|
||||
}
|
||||
|
||||
// Avoid this design - mixed responsibilities
|
||||
class MegaScene extends Scene {
|
||||
// Contains menu, game, pause, and all other logic
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Resource Management
|
||||
|
||||
Clean up resources in the scene's `unload()` method:
|
||||
|
||||
```typescript
|
||||
class GameScene extends Scene {
|
||||
private textures: Map<string, any> = new Map();
|
||||
private sounds: Map<string, any> = new Map();
|
||||
|
||||
protected initialize(): void {
|
||||
this.loadResources();
|
||||
}
|
||||
|
||||
private loadResources(): void {
|
||||
this.textures.set('player', loadTexture('player.png'));
|
||||
this.sounds.set('bgm', loadSound('bgm.mp3'));
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// Cleanup resources
|
||||
this.textures.clear();
|
||||
this.sounds.clear();
|
||||
console.log('Scene resources cleaned up');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Event-Driven Scene Transitions
|
||||
|
||||
Use the event system to trigger scene transitions, keeping code decoupled:
|
||||
|
||||
```typescript
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Listen to scene transition events
|
||||
this.eventSystem.on('goto:menu', () => {
|
||||
Core.loadScene(new MenuScene());
|
||||
});
|
||||
|
||||
this.eventSystem.on('goto:gameover', (data) => {
|
||||
Core.loadScene(new GameOverScene());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger events in System
|
||||
class GameLogicSystem extends EntitySystem {
|
||||
process(entities: readonly Entity[]): void {
|
||||
if (levelComplete) {
|
||||
this.scene.eventSystem.emitSync('goto:gameover', {
|
||||
score: 1000,
|
||||
level: 5
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
SceneManager's position in ECS Framework:
|
||||
|
||||
```
|
||||
Core (Global Services)
|
||||
└── SceneManager (Scene Management, auto-updated)
|
||||
└── Scene (Current Scene)
|
||||
├── EntitySystem (Systems)
|
||||
├── Entity (Entities)
|
||||
└── Component (Components)
|
||||
```
|
||||
|
||||
## Comparison with WorldManager
|
||||
|
||||
| Feature | SceneManager | WorldManager |
|
||||
|---------|--------------|--------------|
|
||||
| Use Case | 95% of game applications | Advanced multi-world isolation scenarios |
|
||||
| Complexity | Simple | Complex |
|
||||
| Scene Count | Single scene (switchable) | Multiple Worlds, each with multiple scenes |
|
||||
| Performance Overhead | Minimal | Higher |
|
||||
| Usage | `Core.setScene()` | `worldManager.createWorld()` |
|
||||
|
||||
**When to use SceneManager**:
|
||||
- Single-player games
|
||||
- Simple multiplayer games
|
||||
- Mobile games
|
||||
- Scenes that need transitions but don't need to run simultaneously
|
||||
|
||||
**When to use WorldManager**:
|
||||
- MMO game servers (one World per room)
|
||||
- Game lobby systems (complete isolation per game room)
|
||||
- Need to run multiple completely independent game instances
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Persistent Entity](./persistent-entity) - Learn how to keep entities across scene transitions
|
||||
- [WorldManager](./world-manager) - Learn about advanced multi-world isolation features
|
||||
|
||||
SceneManager provides simple yet powerful scene management capabilities for most games. Through Core's static methods, you can easily manage scene transitions.
|
||||
364
docs/en/guide/scene.md
Normal file
364
docs/en/guide/scene.md
Normal file
@@ -0,0 +1,364 @@
|
||||
# Scene Management
|
||||
|
||||
In the ECS architecture, a Scene is a container for the game world, responsible for managing the lifecycle of entities, systems, and components. Scenes provide a complete ECS runtime environment.
|
||||
|
||||
## Basic Concepts
|
||||
|
||||
Scene is the core container of the ECS framework, providing:
|
||||
- Entity creation, management, and destruction
|
||||
- System registration and execution scheduling
|
||||
- Component storage and querying
|
||||
- Event system support
|
||||
- Performance monitoring and debugging information
|
||||
|
||||
## Scene Management Options
|
||||
|
||||
ECS Framework provides two scene management approaches:
|
||||
|
||||
1. **[SceneManager](./scene-manager)** - Suitable for 95% of game applications
|
||||
- Single-player games, simple multiplayer games, mobile games
|
||||
- Lightweight, simple and intuitive API
|
||||
- Supports scene transitions
|
||||
|
||||
2. **[WorldManager](./world-manager)** - Suitable for advanced multi-world isolation scenarios
|
||||
- MMO game servers, game room systems
|
||||
- Multi-World management, each World can contain multiple scenes
|
||||
- Completely isolated independent environments
|
||||
|
||||
This document focuses on the usage of the Scene class itself. For detailed information about scene managers, please refer to the corresponding documentation.
|
||||
|
||||
## Creating a Scene
|
||||
|
||||
### Inheriting the Scene Class
|
||||
|
||||
**Recommended: Inherit the Scene class to create custom scenes**
|
||||
|
||||
```typescript
|
||||
import { Scene, EntitySystem } from '@esengine/ecs-framework';
|
||||
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Set scene name
|
||||
this.name = "GameScene";
|
||||
|
||||
// Add systems
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
this.addSystem(new PhysicsSystem());
|
||||
|
||||
// Create initial entities
|
||||
this.createInitialEntities();
|
||||
}
|
||||
|
||||
private createInitialEntities(): void {
|
||||
// Create player
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Position(400, 300));
|
||||
player.addComponent(new Health(100));
|
||||
player.addComponent(new PlayerController());
|
||||
|
||||
// Create enemies
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const enemy = this.createEntity(`Enemy_${i}`);
|
||||
enemy.addComponent(new Position(Math.random() * 800, Math.random() * 600));
|
||||
enemy.addComponent(new Health(50));
|
||||
enemy.addComponent(new EnemyAI());
|
||||
}
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log("Game scene started");
|
||||
// Logic when scene starts
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
console.log("Game scene unloaded");
|
||||
// Cleanup logic when scene unloads
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using Scene Configuration
|
||||
|
||||
```typescript
|
||||
import { ISceneConfig } from '@esengine/ecs-framework';
|
||||
|
||||
const config: ISceneConfig = {
|
||||
name: "MainGame",
|
||||
enableEntityDirectUpdate: false
|
||||
};
|
||||
|
||||
class ConfiguredScene extends Scene {
|
||||
constructor() {
|
||||
super(config);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Scene Lifecycle
|
||||
|
||||
Scene provides complete lifecycle management:
|
||||
|
||||
```typescript
|
||||
class ExampleScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Scene initialization: setup systems and initial entities
|
||||
console.log("Scene initializing");
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
// Scene starts running: game logic begins execution
|
||||
console.log("Scene starting");
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// Scene unloading: cleanup resources
|
||||
console.log("Scene unloading");
|
||||
}
|
||||
}
|
||||
|
||||
// Using scenes (lifecycle automatically managed by framework)
|
||||
const scene = new ExampleScene();
|
||||
// Scene's initialize(), begin(), update(), end() are automatically called by the framework
|
||||
```
|
||||
|
||||
**Lifecycle Methods**:
|
||||
|
||||
1. `initialize()` - Scene initialization, setup systems and initial entities
|
||||
2. `begin()` / `onStart()` - Scene starts running
|
||||
3. `update()` - Per-frame update (called by scene manager)
|
||||
4. `end()` / `unload()` - Scene unloading, cleanup resources
|
||||
|
||||
## Entity Management
|
||||
|
||||
### Creating Entities
|
||||
|
||||
```typescript
|
||||
class EntityScene extends Scene {
|
||||
createGameEntities(): void {
|
||||
// Create single entity
|
||||
const player = this.createEntity("Player");
|
||||
|
||||
// Batch create entities (high performance)
|
||||
const bullets = this.createEntities(100, "Bullet");
|
||||
|
||||
// Add components to batch-created entities
|
||||
bullets.forEach((bullet, index) => {
|
||||
bullet.addComponent(new Position(index * 10, 100));
|
||||
bullet.addComponent(new Velocity(Math.random() * 200 - 100, -300));
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Finding Entities
|
||||
|
||||
```typescript
|
||||
class SearchScene extends Scene {
|
||||
findEntities(): void {
|
||||
// Find by name
|
||||
const player = this.findEntity("Player");
|
||||
const player2 = this.getEntityByName("Player"); // Alias method
|
||||
|
||||
// Find by ID
|
||||
const entity = this.findEntityById(123);
|
||||
|
||||
// Find by tag
|
||||
const enemies = this.findEntitiesByTag(2);
|
||||
const enemies2 = this.getEntitiesByTag(2); // Alias method
|
||||
|
||||
if (player) {
|
||||
console.log(`Found player: ${player.name}`);
|
||||
}
|
||||
|
||||
console.log(`Found ${enemies.length} enemies`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Destroying Entities
|
||||
|
||||
```typescript
|
||||
class DestroyScene extends Scene {
|
||||
cleanupEntities(): void {
|
||||
// Destroy all entities
|
||||
this.destroyAllEntities();
|
||||
|
||||
// Single entity destruction through the entity itself
|
||||
const enemy = this.findEntity("Enemy_1");
|
||||
if (enemy) {
|
||||
enemy.destroy(); // Entity is automatically removed from the scene
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## System Management
|
||||
|
||||
### Adding and Removing Systems
|
||||
|
||||
```typescript
|
||||
class SystemScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Add systems
|
||||
const movementSystem = new MovementSystem();
|
||||
this.addSystem(movementSystem);
|
||||
|
||||
// Set system update order
|
||||
movementSystem.updateOrder = 1;
|
||||
|
||||
// Add more systems
|
||||
this.addSystem(new PhysicsSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
}
|
||||
|
||||
public removeUnnecessarySystems(): void {
|
||||
// Get system
|
||||
const physicsSystem = this.getEntityProcessor(PhysicsSystem);
|
||||
|
||||
// Remove system
|
||||
if (physicsSystem) {
|
||||
this.removeSystem(physicsSystem);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Event System
|
||||
|
||||
Scene has a built-in type-safe event system:
|
||||
|
||||
```typescript
|
||||
class EventScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Listen to events
|
||||
this.eventSystem.on('player_died', this.onPlayerDied.bind(this));
|
||||
this.eventSystem.on('enemy_spawned', this.onEnemySpawned.bind(this));
|
||||
this.eventSystem.on('level_complete', this.onLevelComplete.bind(this));
|
||||
}
|
||||
|
||||
private onPlayerDied(data: any): void {
|
||||
console.log('Player died event');
|
||||
// Handle player death
|
||||
}
|
||||
|
||||
private onEnemySpawned(data: any): void {
|
||||
console.log('Enemy spawned event');
|
||||
// Handle enemy spawn
|
||||
}
|
||||
|
||||
private onLevelComplete(data: any): void {
|
||||
console.log('Level complete event');
|
||||
// Handle level completion
|
||||
}
|
||||
|
||||
public triggerGameEvent(): void {
|
||||
// Send event (synchronous)
|
||||
this.eventSystem.emitSync('custom_event', {
|
||||
message: "This is a custom event",
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
// Send event (asynchronous)
|
||||
this.eventSystem.emit('async_event', {
|
||||
data: "Async event data"
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Scene Responsibility Separation
|
||||
|
||||
```typescript
|
||||
// Good scene design - clear responsibilities
|
||||
class MenuScene extends Scene {
|
||||
// Only handles menu-related logic
|
||||
}
|
||||
|
||||
class GameScene extends Scene {
|
||||
// Only handles gameplay logic
|
||||
}
|
||||
|
||||
class InventoryScene extends Scene {
|
||||
// Only handles inventory logic
|
||||
}
|
||||
|
||||
// Avoid this design - mixed responsibilities
|
||||
class MegaScene extends Scene {
|
||||
// Contains menu, game, inventory, and all other logic
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Proper System Organization
|
||||
|
||||
```typescript
|
||||
class OrganizedScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Add systems by function and dependencies
|
||||
this.addInputSystems();
|
||||
this.addLogicSystems();
|
||||
this.addRenderSystems();
|
||||
}
|
||||
|
||||
private addInputSystems(): void {
|
||||
this.addSystem(new InputSystem());
|
||||
}
|
||||
|
||||
private addLogicSystems(): void {
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new PhysicsSystem());
|
||||
this.addSystem(new CollisionSystem());
|
||||
}
|
||||
|
||||
private addRenderSystems(): void {
|
||||
this.addSystem(new RenderSystem());
|
||||
this.addSystem(new UISystem());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Resource Management
|
||||
|
||||
```typescript
|
||||
class ResourceScene extends Scene {
|
||||
private textures: Map<string, any> = new Map();
|
||||
private sounds: Map<string, any> = new Map();
|
||||
|
||||
protected initialize(): void {
|
||||
this.loadResources();
|
||||
}
|
||||
|
||||
private loadResources(): void {
|
||||
// Load resources needed by the scene
|
||||
this.textures.set('player', this.loadTexture('player.png'));
|
||||
this.sounds.set('bgm', this.loadSound('bgm.mp3'));
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// Cleanup resources
|
||||
this.textures.clear();
|
||||
this.sounds.clear();
|
||||
console.log('Scene resources cleaned up');
|
||||
}
|
||||
|
||||
private loadTexture(path: string): any {
|
||||
// Load texture
|
||||
return null;
|
||||
}
|
||||
|
||||
private loadSound(path: string): any {
|
||||
// Load sound
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Learn about [SceneManager](./scene-manager) - Simple scene management for most games
|
||||
- Learn about [WorldManager](./world-manager) - For scenarios requiring multi-world isolation
|
||||
- Learn about [Persistent Entity](./persistent-entity) - Keep entities across scene transitions (v2.3.0+)
|
||||
|
||||
Scene is the core container of the ECS framework. Proper scene management makes your game architecture clearer, more modular, and easier to maintain.
|
||||
1161
docs/en/guide/system.md
Normal file
1161
docs/en/guide/system.md
Normal file
File diff suppressed because it is too large
Load Diff
402
docs/en/guide/time-and-timers.md
Normal file
402
docs/en/guide/time-and-timers.md
Normal file
@@ -0,0 +1,402 @@
|
||||
# Time and Timer System
|
||||
|
||||
The ECS framework provides a complete time management and timer system, including time scaling, frame time calculation, and flexible timer scheduling.
|
||||
|
||||
## Time Class
|
||||
|
||||
The Time class is the core of the framework's time management, providing all game time-related functionality.
|
||||
|
||||
### Basic Time Properties
|
||||
|
||||
```typescript
|
||||
import { Time } from '@esengine/ecs-framework';
|
||||
|
||||
class GameSystem extends EntitySystem {
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Get frame time (seconds)
|
||||
const deltaTime = Time.deltaTime;
|
||||
|
||||
// Get unscaled frame time
|
||||
const unscaledDelta = Time.unscaledDeltaTime;
|
||||
|
||||
// Get total game time
|
||||
const totalTime = Time.totalTime;
|
||||
|
||||
// Get current frame count
|
||||
const frameCount = Time.frameCount;
|
||||
|
||||
console.log(`Frame ${frameCount}, delta: ${deltaTime}s, total: ${totalTime}s`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Game Pause
|
||||
|
||||
The framework provides two pause methods for different scenarios:
|
||||
|
||||
#### Core.paused (Recommended)
|
||||
|
||||
`Core.paused` is a **true pause** - when set, the entire game loop stops:
|
||||
|
||||
```typescript
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
class PauseMenuSystem extends EntitySystem {
|
||||
public pauseGame(): void {
|
||||
// True pause - all systems stop executing
|
||||
Core.paused = true;
|
||||
console.log('Game paused');
|
||||
}
|
||||
|
||||
public resumeGame(): void {
|
||||
// Resume game
|
||||
Core.paused = false;
|
||||
console.log('Game resumed');
|
||||
}
|
||||
|
||||
public togglePause(): void {
|
||||
Core.paused = !Core.paused;
|
||||
console.log(Core.paused ? 'Game paused' : 'Game resumed');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Time.timeScale = 0
|
||||
|
||||
`Time.timeScale = 0` only makes `deltaTime` become 0, **systems still execute**:
|
||||
|
||||
```typescript
|
||||
class SlowMotionSystem extends EntitySystem {
|
||||
public freezeTime(): void {
|
||||
// Time freeze - systems still execute, just deltaTime = 0
|
||||
Time.timeScale = 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Comparison
|
||||
|
||||
| Feature | `Core.paused = true` | `Time.timeScale = 0` |
|
||||
|---------|---------------------|---------------------|
|
||||
| System Execution | Completely stopped | Still running |
|
||||
| CPU Overhead | Zero | Normal overhead |
|
||||
| Time Updates | Stopped | Continues (deltaTime=0) |
|
||||
| Timers | Stopped | Continues (but time doesn't advance) |
|
||||
| Use Cases | Pause menu, game pause | Slow motion, bullet time effects |
|
||||
|
||||
**Recommendations**:
|
||||
- Pause menu, true game pause → Use `Core.paused = true`
|
||||
- Slow motion, bullet time effects → Use `Time.timeScale`
|
||||
|
||||
### Time Scaling
|
||||
|
||||
The Time class supports time scaling for slow motion, fast forward, and other effects:
|
||||
|
||||
```typescript
|
||||
class TimeControlSystem extends EntitySystem {
|
||||
public enableSlowMotion(): void {
|
||||
// Set to slow motion (50% speed)
|
||||
Time.timeScale = 0.5;
|
||||
console.log('Slow motion enabled');
|
||||
}
|
||||
|
||||
public enableFastForward(): void {
|
||||
// Set to fast forward (200% speed)
|
||||
Time.timeScale = 2.0;
|
||||
console.log('Fast forward enabled');
|
||||
}
|
||||
|
||||
public enableBulletTime(): void {
|
||||
// Bullet time effect (10% speed)
|
||||
Time.timeScale = 0.1;
|
||||
console.log('Bullet time enabled');
|
||||
}
|
||||
|
||||
public resumeNormalSpeed(): void {
|
||||
// Resume normal speed
|
||||
Time.timeScale = 1.0;
|
||||
console.log('Normal speed resumed');
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// deltaTime is affected by timeScale
|
||||
const scaledDelta = Time.deltaTime; // Affected by time scale
|
||||
const realDelta = Time.unscaledDeltaTime; // Not affected by time scale
|
||||
|
||||
for (const entity of entities) {
|
||||
const movement = entity.getComponent(Movement);
|
||||
if (movement) {
|
||||
// Use scaled time for game logic updates
|
||||
movement.update(scaledDelta);
|
||||
}
|
||||
|
||||
const ui = entity.getComponent(UIComponent);
|
||||
if (ui) {
|
||||
// UI animations use real time, not affected by game time scale
|
||||
ui.update(realDelta);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Time Check Utilities
|
||||
|
||||
```typescript
|
||||
class CooldownSystem extends EntitySystem {
|
||||
private lastAttackTime = 0;
|
||||
private lastSpawnTime = 0;
|
||||
|
||||
constructor() {
|
||||
super(Matcher.all(Weapon));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Check attack cooldown
|
||||
if (Time.checkEvery(1.5, this.lastAttackTime)) {
|
||||
this.performAttack();
|
||||
this.lastAttackTime = Time.totalTime;
|
||||
}
|
||||
|
||||
// Check spawn interval
|
||||
if (Time.checkEvery(3.0, this.lastSpawnTime)) {
|
||||
this.spawnEnemy();
|
||||
this.lastSpawnTime = Time.totalTime;
|
||||
}
|
||||
}
|
||||
|
||||
private performAttack(): void {
|
||||
console.log('Performing attack!');
|
||||
}
|
||||
|
||||
private spawnEnemy(): void {
|
||||
console.log('Spawning enemy!');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Core.schedule Timer System
|
||||
|
||||
Core provides powerful timer scheduling functionality for creating one-time or repeating timers.
|
||||
|
||||
### Basic Timer Usage
|
||||
|
||||
```typescript
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Create one-time timers
|
||||
this.createOneTimeTimers();
|
||||
|
||||
// Create repeating timers
|
||||
this.createRepeatingTimers();
|
||||
|
||||
// Create timers with context
|
||||
this.createContextTimers();
|
||||
}
|
||||
|
||||
private createOneTimeTimers(): void {
|
||||
// Execute once after 2 seconds
|
||||
Core.schedule(2.0, false, null, (timer) => {
|
||||
console.log('Executed after 2 second delay');
|
||||
});
|
||||
|
||||
// Show tip after 5 seconds
|
||||
Core.schedule(5.0, false, this, (timer) => {
|
||||
const scene = timer.getContext<GameScene>();
|
||||
scene.showTip('Game tip: 5 seconds have passed!');
|
||||
});
|
||||
}
|
||||
|
||||
private createRepeatingTimers(): void {
|
||||
// Execute every second
|
||||
const heartbeatTimer = Core.schedule(1.0, true, null, (timer) => {
|
||||
console.log(`Game heartbeat - Total time: ${Time.totalTime.toFixed(1)}s`);
|
||||
});
|
||||
|
||||
// Save timer reference for later control
|
||||
this.saveTimerReference(heartbeatTimer);
|
||||
}
|
||||
|
||||
private createContextTimers(): void {
|
||||
const gameData = { score: 0, level: 1 };
|
||||
|
||||
// Add score every 2 seconds
|
||||
Core.schedule(2.0, true, gameData, (timer) => {
|
||||
const data = timer.getContext<typeof gameData>();
|
||||
data.score += 10;
|
||||
console.log(`Score increased! Current score: ${data.score}`);
|
||||
});
|
||||
}
|
||||
|
||||
private saveTimerReference(timer: any): void {
|
||||
// Can stop timer later
|
||||
setTimeout(() => {
|
||||
timer.stop();
|
||||
console.log('Timer stopped');
|
||||
}, 10000); // Stop after 10 seconds
|
||||
}
|
||||
|
||||
private showTip(message: string): void {
|
||||
console.log('Tip:', message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Timer Control
|
||||
|
||||
```typescript
|
||||
class TimerControlExample {
|
||||
private attackTimer: any;
|
||||
private spawnerTimer: any;
|
||||
|
||||
public startCombat(): void {
|
||||
// Start attack timer
|
||||
this.attackTimer = Core.schedule(0.5, true, this, (timer) => {
|
||||
const self = timer.getContext<TimerControlExample>();
|
||||
self.performAttack();
|
||||
});
|
||||
|
||||
// Start enemy spawn timer
|
||||
this.spawnerTimer = Core.schedule(3.0, true, null, (timer) => {
|
||||
this.spawnEnemy();
|
||||
});
|
||||
}
|
||||
|
||||
public stopCombat(): void {
|
||||
// Stop all combat-related timers
|
||||
if (this.attackTimer) {
|
||||
this.attackTimer.stop();
|
||||
console.log('Attack timer stopped');
|
||||
}
|
||||
|
||||
if (this.spawnerTimer) {
|
||||
this.spawnerTimer.stop();
|
||||
console.log('Spawn timer stopped');
|
||||
}
|
||||
}
|
||||
|
||||
public resetAttackTimer(): void {
|
||||
// Reset attack timer
|
||||
if (this.attackTimer) {
|
||||
this.attackTimer.reset();
|
||||
console.log('Attack timer reset');
|
||||
}
|
||||
}
|
||||
|
||||
private performAttack(): void {
|
||||
console.log('Performing attack');
|
||||
}
|
||||
|
||||
private spawnEnemy(): void {
|
||||
console.log('Spawning enemy');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Use Appropriate Time Types
|
||||
|
||||
```typescript
|
||||
class MovementSystem extends EntitySystem {
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const movement = entity.getComponent(Movement);
|
||||
|
||||
// Use scaled time for game logic
|
||||
movement.position.x += movement.velocity.x * Time.deltaTime;
|
||||
|
||||
// Use real time for UI animations (not affected by game pause)
|
||||
const ui = entity.getComponent(UIAnimation);
|
||||
if (ui) {
|
||||
ui.update(Time.unscaledDeltaTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Timer Management
|
||||
|
||||
```typescript
|
||||
class TimerManager {
|
||||
private timers: any[] = [];
|
||||
|
||||
public createManagedTimer(duration: number, repeats: boolean, callback: () => void): any {
|
||||
const timer = Core.schedule(duration, repeats, null, callback);
|
||||
this.timers.push(timer);
|
||||
return timer;
|
||||
}
|
||||
|
||||
public stopAllTimers(): void {
|
||||
for (const timer of this.timers) {
|
||||
timer.stop();
|
||||
}
|
||||
this.timers = [];
|
||||
}
|
||||
|
||||
public cleanupCompletedTimers(): void {
|
||||
this.timers = this.timers.filter(timer => !timer.isDone);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Avoid Too Many Timers
|
||||
|
||||
```typescript
|
||||
// Avoid: Creating a timer for each entity
|
||||
class BadExample extends EntitySystem {
|
||||
protected onAdded(entity: Entity): void {
|
||||
Core.schedule(1.0, true, entity, (timer) => {
|
||||
// One timer per entity - poor performance
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Recommended: Manage time uniformly in the system
|
||||
class GoodExample extends EntitySystem {
|
||||
private lastUpdateTime = 0;
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Execute logic once per second
|
||||
if (Time.checkEvery(1.0, this.lastUpdateTime)) {
|
||||
this.processAllEntities(entities);
|
||||
this.lastUpdateTime = Time.totalTime;
|
||||
}
|
||||
}
|
||||
|
||||
private processAllEntities(entities: readonly Entity[]): void {
|
||||
// Batch process all entities
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Timer Context Usage
|
||||
|
||||
```typescript
|
||||
interface TimerContext {
|
||||
entityId: number;
|
||||
duration: number;
|
||||
onComplete: () => void;
|
||||
}
|
||||
|
||||
class ContextualTimerExample {
|
||||
public createEntityTimer(entityId: number, duration: number, onComplete: () => void): void {
|
||||
const context: TimerContext = {
|
||||
entityId,
|
||||
duration,
|
||||
onComplete
|
||||
};
|
||||
|
||||
Core.schedule(duration, false, context, (timer) => {
|
||||
const ctx = timer.getContext<TimerContext>();
|
||||
console.log(`Timer for entity ${ctx.entityId} completed`);
|
||||
ctx.onComplete();
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The time and timer system is an essential tool in game development. Using these features correctly will make your game logic more precise and controllable.
|
||||
570
docs/en/guide/worker-system.md
Normal file
570
docs/en/guide/worker-system.md
Normal file
@@ -0,0 +1,570 @@
|
||||
# Worker System
|
||||
|
||||
The Worker System (WorkerEntitySystem) is a multi-threaded processing system based on Web Workers in the ECS framework. It's designed for compute-intensive tasks, fully utilizing multi-core CPU performance for true parallel computing.
|
||||
|
||||
## Core Features
|
||||
|
||||
- **True Parallel Computing**: Execute compute-intensive tasks in background threads using Web Workers
|
||||
- **Automatic Load Balancing**: Automatically distribute workload based on CPU core count
|
||||
- **SharedArrayBuffer Optimization**: Zero-copy data sharing for improved large-scale computation performance
|
||||
- **Graceful Degradation**: Automatic fallback to main thread processing when Workers are not supported
|
||||
- **Type Safety**: Full TypeScript support and type checking
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Simple Physics System Example
|
||||
|
||||
```typescript
|
||||
interface PhysicsData {
|
||||
id: number;
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
mass: number;
|
||||
radius: number;
|
||||
}
|
||||
|
||||
@ECSSystem('Physics')
|
||||
class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsData> {
|
||||
constructor() {
|
||||
super(Matcher.all(Position, Velocity, Physics), {
|
||||
enableWorker: true, // Enable Worker parallel processing
|
||||
workerCount: 8, // Worker count, auto-limited to hardware capacity
|
||||
entitiesPerWorker: 100, // Entities per Worker
|
||||
useSharedArrayBuffer: true, // Enable SharedArrayBuffer optimization
|
||||
entityDataSize: 7, // Data size per entity
|
||||
maxEntities: 10000, // Maximum entity count
|
||||
systemConfig: { // Configuration passed to Worker
|
||||
gravity: 100,
|
||||
friction: 0.95
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Data extraction: Convert Entity to serializable data
|
||||
protected extractEntityData(entity: Entity): PhysicsData {
|
||||
const position = entity.getComponent(Position);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
const physics = entity.getComponent(Physics);
|
||||
|
||||
return {
|
||||
id: entity.id,
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
vx: velocity.x,
|
||||
vy: velocity.y,
|
||||
mass: physics.mass,
|
||||
radius: physics.radius
|
||||
};
|
||||
}
|
||||
|
||||
// Worker processing function: Pure function executed in Worker
|
||||
protected workerProcess(
|
||||
entities: PhysicsData[],
|
||||
deltaTime: number,
|
||||
config: any
|
||||
): PhysicsData[] {
|
||||
return entities.map(entity => {
|
||||
// Apply gravity
|
||||
entity.vy += config.gravity * deltaTime;
|
||||
|
||||
// Update position
|
||||
entity.x += entity.vx * deltaTime;
|
||||
entity.y += entity.vy * deltaTime;
|
||||
|
||||
// Apply friction
|
||||
entity.vx *= config.friction;
|
||||
entity.vy *= config.friction;
|
||||
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
// Apply results: Apply Worker processing results back to Entity
|
||||
protected applyResult(entity: Entity, result: PhysicsData): void {
|
||||
const position = entity.getComponent(Position);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
position.x = result.x;
|
||||
position.y = result.y;
|
||||
velocity.x = result.vx;
|
||||
velocity.y = result.vy;
|
||||
}
|
||||
|
||||
// SharedArrayBuffer optimization support
|
||||
protected getDefaultEntityDataSize(): number {
|
||||
return 7; // id, x, y, vx, vy, mass, radius
|
||||
}
|
||||
|
||||
protected writeEntityToBuffer(entityData: PhysicsData, offset: number): void {
|
||||
if (!this.sharedFloatArray) return;
|
||||
|
||||
this.sharedFloatArray[offset + 0] = entityData.id;
|
||||
this.sharedFloatArray[offset + 1] = entityData.x;
|
||||
this.sharedFloatArray[offset + 2] = entityData.y;
|
||||
this.sharedFloatArray[offset + 3] = entityData.vx;
|
||||
this.sharedFloatArray[offset + 4] = entityData.vy;
|
||||
this.sharedFloatArray[offset + 5] = entityData.mass;
|
||||
this.sharedFloatArray[offset + 6] = entityData.radius;
|
||||
}
|
||||
|
||||
protected readEntityFromBuffer(offset: number): PhysicsData | null {
|
||||
if (!this.sharedFloatArray) return null;
|
||||
|
||||
return {
|
||||
id: this.sharedFloatArray[offset + 0],
|
||||
x: this.sharedFloatArray[offset + 1],
|
||||
y: this.sharedFloatArray[offset + 2],
|
||||
vx: this.sharedFloatArray[offset + 3],
|
||||
vy: this.sharedFloatArray[offset + 4],
|
||||
mass: this.sharedFloatArray[offset + 5],
|
||||
radius: this.sharedFloatArray[offset + 6]
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
The Worker system supports rich configuration options:
|
||||
|
||||
```typescript
|
||||
interface WorkerSystemConfig {
|
||||
/** Enable Worker parallel processing */
|
||||
enableWorker?: boolean;
|
||||
/** Worker count, defaults to CPU core count, auto-limited to system maximum */
|
||||
workerCount?: number;
|
||||
/** Entities per Worker for load distribution control */
|
||||
entitiesPerWorker?: number;
|
||||
/** System configuration data passed to Worker */
|
||||
systemConfig?: any;
|
||||
/** Enable SharedArrayBuffer optimization */
|
||||
useSharedArrayBuffer?: boolean;
|
||||
/** Float32 count per entity in SharedArrayBuffer */
|
||||
entityDataSize?: number;
|
||||
/** Maximum entity count (for SharedArrayBuffer pre-allocation) */
|
||||
maxEntities?: number;
|
||||
/** Pre-compiled Worker script path (for platforms like WeChat Mini Game that don't support dynamic scripts) */
|
||||
workerScriptPath?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration Recommendations
|
||||
|
||||
```typescript
|
||||
constructor() {
|
||||
super(matcher, {
|
||||
// Decide based on task complexity
|
||||
enableWorker: this.shouldUseWorker(),
|
||||
|
||||
// Worker count: System auto-limits to hardware capacity
|
||||
workerCount: 8, // Request 8 Workers, actual count limited by CPU cores
|
||||
|
||||
// Entities per Worker (optional)
|
||||
entitiesPerWorker: 200, // Precise load distribution control
|
||||
|
||||
// Enable SharedArrayBuffer for many simple calculations
|
||||
useSharedArrayBuffer: this.entityCount > 1000,
|
||||
|
||||
// Set according to actual data structure
|
||||
entityDataSize: 8, // Ensure it matches data structure
|
||||
|
||||
// Estimated maximum entity count
|
||||
maxEntities: 10000,
|
||||
|
||||
// Global configuration passed to Worker
|
||||
systemConfig: {
|
||||
gravity: 9.8,
|
||||
friction: 0.95,
|
||||
worldBounds: { width: 1920, height: 1080 }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private shouldUseWorker(): boolean {
|
||||
// Decide based on entity count and complexity
|
||||
return this.expectedEntityCount > 100;
|
||||
}
|
||||
|
||||
// Get system info
|
||||
getSystemInfo() {
|
||||
const info = this.getWorkerInfo();
|
||||
console.log(`Worker count: ${info.workerCount}/${info.maxSystemWorkerCount}`);
|
||||
console.log(`Entities per Worker: ${info.entitiesPerWorker || 'auto'}`);
|
||||
console.log(`Current mode: ${info.currentMode}`);
|
||||
}
|
||||
```
|
||||
|
||||
## Processing Modes
|
||||
|
||||
The Worker system supports two processing modes:
|
||||
|
||||
### 1. Traditional Worker Mode
|
||||
|
||||
Data is serialized and passed between main thread and Workers:
|
||||
|
||||
```typescript
|
||||
// Suitable for: Complex computation logic, moderate entity count
|
||||
constructor() {
|
||||
super(matcher, {
|
||||
enableWorker: true,
|
||||
useSharedArrayBuffer: false, // Use traditional mode
|
||||
workerCount: 2
|
||||
});
|
||||
}
|
||||
|
||||
protected workerProcess(entities: EntityData[], deltaTime: number): EntityData[] {
|
||||
// Complex algorithm logic
|
||||
return entities.map(entity => {
|
||||
// AI decisions, pathfinding, etc.
|
||||
return this.complexAILogic(entity, deltaTime);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 2. SharedArrayBuffer Mode
|
||||
|
||||
Zero-copy data sharing, suitable for many simple calculations:
|
||||
|
||||
```typescript
|
||||
// Suitable for: Many entities with simple calculations
|
||||
constructor() {
|
||||
super(matcher, {
|
||||
enableWorker: true,
|
||||
useSharedArrayBuffer: true, // Enable shared memory
|
||||
entityDataSize: 6,
|
||||
maxEntities: 10000
|
||||
});
|
||||
}
|
||||
|
||||
protected getSharedArrayBufferProcessFunction(): SharedArrayBufferProcessFunction {
|
||||
return function(sharedFloatArray: Float32Array, startIndex: number, endIndex: number, deltaTime: number, config: any) {
|
||||
const entitySize = 6;
|
||||
for (let i = startIndex; i < endIndex; i++) {
|
||||
const offset = i * entitySize;
|
||||
|
||||
// Read data
|
||||
let x = sharedFloatArray[offset];
|
||||
let y = sharedFloatArray[offset + 1];
|
||||
let vx = sharedFloatArray[offset + 2];
|
||||
let vy = sharedFloatArray[offset + 3];
|
||||
|
||||
// Physics calculations
|
||||
vy += config.gravity * deltaTime;
|
||||
x += vx * deltaTime;
|
||||
y += vy * deltaTime;
|
||||
|
||||
// Write back data
|
||||
sharedFloatArray[offset] = x;
|
||||
sharedFloatArray[offset + 1] = y;
|
||||
sharedFloatArray[offset + 2] = vx;
|
||||
sharedFloatArray[offset + 3] = vy;
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
The Worker system is particularly suitable for:
|
||||
|
||||
### 1. Physics Simulation
|
||||
- **Gravity systems**: Gravity calculations for many entities
|
||||
- **Collision detection**: Complex collision algorithms
|
||||
- **Fluid simulation**: Particle fluid systems
|
||||
- **Cloth simulation**: Vertex physics calculations
|
||||
|
||||
### 2. AI Computation
|
||||
- **Pathfinding**: A*, Dijkstra algorithms
|
||||
- **Behavior trees**: Complex AI decision logic
|
||||
- **Swarm intelligence**: Boid, fish school algorithms
|
||||
- **Neural networks**: Simple AI inference
|
||||
|
||||
### 3. Data Processing
|
||||
- **Bulk entity updates**: State machines, lifecycle management
|
||||
- **Statistical calculations**: Game data analysis
|
||||
- **Image processing**: Texture generation, effect calculations
|
||||
- **Audio processing**: Sound synthesis, spectrum analysis
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Worker Function Requirements
|
||||
|
||||
```typescript
|
||||
// Recommended: Worker processing function is a pure function
|
||||
protected workerProcess(entities: PhysicsData[], deltaTime: number, config: any): PhysicsData[] {
|
||||
// Only use parameters and standard JavaScript APIs
|
||||
return entities.map(entity => {
|
||||
// Pure computation logic, no external state dependencies
|
||||
entity.y += entity.velocity * deltaTime;
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
// Avoid: Using external references in Worker function
|
||||
protected workerProcess(entities: PhysicsData[], deltaTime: number): PhysicsData[] {
|
||||
// this and external variables are not available in Worker
|
||||
return entities.map(entity => {
|
||||
entity.y += this.someProperty; // Error
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Data Design
|
||||
|
||||
```typescript
|
||||
// Recommended: Reasonable data design
|
||||
interface SimplePhysicsData {
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
// Keep data structure simple for easy serialization
|
||||
}
|
||||
|
||||
// Avoid: Complex nested objects
|
||||
interface ComplexData {
|
||||
transform: {
|
||||
position: { x: number; y: number };
|
||||
rotation: { angle: number };
|
||||
};
|
||||
// Complex nested structures increase serialization overhead
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Worker Count Control
|
||||
|
||||
```typescript
|
||||
// Recommended: Flexible Worker configuration
|
||||
constructor() {
|
||||
super(matcher, {
|
||||
// Specify needed Worker count, system auto-limits to hardware capacity
|
||||
workerCount: 8, // Request 8 Workers
|
||||
entitiesPerWorker: 100, // 100 entities per Worker
|
||||
enableWorker: this.shouldUseWorker(), // Conditional enable
|
||||
});
|
||||
}
|
||||
|
||||
private shouldUseWorker(): boolean {
|
||||
// Decide based on entity count and complexity
|
||||
return this.expectedEntityCount > 100;
|
||||
}
|
||||
|
||||
// Get actual Worker info
|
||||
checkWorkerConfiguration() {
|
||||
const info = this.getWorkerInfo();
|
||||
console.log(`Requested Workers: 8`);
|
||||
console.log(`Actual Workers: ${info.workerCount}`);
|
||||
console.log(`System maximum: ${info.maxSystemWorkerCount}`);
|
||||
console.log(`Entities per Worker: ${info.entitiesPerWorker || 'auto'}`);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Performance Monitoring
|
||||
|
||||
```typescript
|
||||
// Recommended: Performance monitoring
|
||||
public getPerformanceMetrics(): WorkerPerformanceMetrics {
|
||||
return {
|
||||
...this.getWorkerInfo(),
|
||||
entityCount: this.entities.length,
|
||||
averageProcessTime: this.getAverageProcessTime(),
|
||||
workerUtilization: this.getWorkerUtilization()
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Optimization Tips
|
||||
|
||||
### 1. Compute Intensity Assessment
|
||||
Only use Workers for compute-intensive tasks to avoid thread overhead for simple calculations.
|
||||
|
||||
### 2. Data Transfer Optimization
|
||||
- Use SharedArrayBuffer to reduce serialization overhead
|
||||
- Keep data structures simple and flat
|
||||
- Avoid frequent large data transfers
|
||||
|
||||
### 3. Degradation Strategy
|
||||
Always provide main thread fallback to ensure normal operation in environments without Worker support.
|
||||
|
||||
### 4. Memory Management
|
||||
Clean up Worker pools and shared buffers promptly to avoid memory leaks.
|
||||
|
||||
### 5. Load Balancing
|
||||
Use `entitiesPerWorker` parameter to precisely control load distribution, avoiding idle Workers while others are overloaded.
|
||||
|
||||
## WeChat Mini Game Support
|
||||
|
||||
WeChat Mini Game has special Worker limitations and doesn't support dynamic Worker script creation. ESEngine provides the `@esengine/worker-generator` CLI tool to solve this problem.
|
||||
|
||||
### WeChat Mini Game Worker Limitations
|
||||
|
||||
| Feature | Browser | WeChat Mini Game |
|
||||
|---------|---------|------------------|
|
||||
| Dynamic scripts (Blob URL) | Supported | Not supported |
|
||||
| Worker count | Multiple | Maximum 1 |
|
||||
| Script source | Any | Must be in code package |
|
||||
| SharedArrayBuffer | Requires COOP/COEP | Limited support |
|
||||
|
||||
### Using Worker Generator CLI
|
||||
|
||||
#### 1. Install the Tool
|
||||
|
||||
```bash
|
||||
pnpm add -D @esengine/worker-generator
|
||||
```
|
||||
|
||||
#### 2. Configure workerScriptPath
|
||||
|
||||
Configure `workerScriptPath` in your WorkerEntitySystem subclass:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Physics')
|
||||
class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsData> {
|
||||
constructor() {
|
||||
super(Matcher.all(Position, Velocity, Physics), {
|
||||
enableWorker: true,
|
||||
workerScriptPath: 'workers/physics-worker.js', // Specify Worker file path
|
||||
systemConfig: {
|
||||
gravity: 100,
|
||||
friction: 0.95
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected workerProcess(
|
||||
entities: PhysicsData[],
|
||||
deltaTime: number,
|
||||
config: any
|
||||
): PhysicsData[] {
|
||||
// Physics calculation logic
|
||||
return entities.map(entity => {
|
||||
entity.vy += config.gravity * deltaTime;
|
||||
entity.x += entity.vx * deltaTime;
|
||||
entity.y += entity.vy * deltaTime;
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
// ... other methods
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Generate Worker Files
|
||||
|
||||
Run the CLI tool to automatically extract `workerProcess` functions and generate WeChat Mini Game compatible Worker files:
|
||||
|
||||
```bash
|
||||
# Basic usage
|
||||
npx esengine-worker-gen --src ./src --wechat
|
||||
|
||||
# Full options
|
||||
npx esengine-worker-gen \
|
||||
--src ./src \ # Source directory
|
||||
--wechat \ # Generate WeChat Mini Game compatible code
|
||||
--mapping \ # Generate worker-mapping.json
|
||||
--verbose # Verbose output
|
||||
```
|
||||
|
||||
The CLI tool will:
|
||||
1. Scan source directory for all `WorkerEntitySystem` subclasses
|
||||
2. Read each class's `workerScriptPath` configuration
|
||||
3. Extract `workerProcess` method body
|
||||
4. Convert to ES5 syntax (WeChat Mini Game compatible)
|
||||
5. Generate to configured path
|
||||
|
||||
#### 4. Configure game.json
|
||||
|
||||
Configure workers directory in WeChat Mini Game's `game.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"deviceOrientation": "portrait",
|
||||
"workers": "workers"
|
||||
}
|
||||
```
|
||||
|
||||
#### 5. Project Structure
|
||||
|
||||
```
|
||||
your-game/
|
||||
├── game.js
|
||||
├── game.json # Configure "workers": "workers"
|
||||
├── src/
|
||||
│ └── systems/
|
||||
│ └── PhysicsSystem.ts # workerScriptPath: 'workers/physics-worker.js'
|
||||
└── workers/
|
||||
├── physics-worker.js # Auto-generated
|
||||
└── worker-mapping.json # Auto-generated
|
||||
```
|
||||
|
||||
### Temporarily Disabling Workers
|
||||
|
||||
If you need to temporarily disable Workers (e.g., for debugging), there are two ways:
|
||||
|
||||
#### Method 1: Configuration Disable
|
||||
|
||||
```typescript
|
||||
constructor() {
|
||||
super(matcher, {
|
||||
enableWorker: false, // Disable Worker, use main thread processing
|
||||
// ...
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
#### Method 2: Platform Adapter Disable
|
||||
|
||||
Return Worker not supported in custom platform adapter:
|
||||
|
||||
```typescript
|
||||
class MyPlatformAdapter implements IPlatformAdapter {
|
||||
isWorkerSupported(): boolean {
|
||||
return false; // Return false to disable Worker
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Important Notes
|
||||
|
||||
1. **Re-run CLI tool after each `workerProcess` modification** to generate new Worker files
|
||||
|
||||
2. **Worker functions must be pure functions**, cannot depend on `this` or external variables:
|
||||
```typescript
|
||||
// Correct: Only use parameters
|
||||
protected workerProcess(entities, deltaTime, config) {
|
||||
return entities.map(e => {
|
||||
e.y += config.gravity * deltaTime;
|
||||
return e;
|
||||
});
|
||||
}
|
||||
|
||||
// Wrong: Using this
|
||||
protected workerProcess(entities, deltaTime, config) {
|
||||
return entities.map(e => {
|
||||
e.y += this.gravity * deltaTime; // Cannot access this in Worker
|
||||
return e;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
3. **Pass configuration data via `systemConfig`**, not class properties
|
||||
|
||||
4. **Developer tool warnings can be ignored**:
|
||||
- `getNetworkType:fail not support` - WeChat DevTools internal behavior
|
||||
- `SharedArrayBuffer will require cross-origin isolation` - Development environment warning, won't appear on real devices
|
||||
|
||||
## Online Demo
|
||||
|
||||
See the complete Worker system demo: [Worker System Demo](https://esengine.github.io/ecs-framework/demos/worker-system/)
|
||||
|
||||
The demo showcases:
|
||||
- Multi-threaded physics computation
|
||||
- Real-time performance comparison
|
||||
- SharedArrayBuffer optimization
|
||||
- Parallel processing of many entities
|
||||
|
||||
The Worker system provides powerful parallel computing capabilities for the ECS framework, allowing you to fully utilize modern multi-core processor performance, offering efficient solutions for complex game logic and compute-intensive tasks.
|
||||
317
docs/en/index.md
Normal file
317
docs/en/index.md
Normal file
@@ -0,0 +1,317 @@
|
||||
---
|
||||
layout: page
|
||||
title: ESEngine - High-performance TypeScript ECS Framework
|
||||
---
|
||||
|
||||
<ParticleHeroEn />
|
||||
|
||||
<section class="news-section">
|
||||
<div class="news-container">
|
||||
<div class="news-header">
|
||||
<h2 class="news-title">Quick Links</h2>
|
||||
<a href="/en/guide/" class="news-more">View Docs</a>
|
||||
</div>
|
||||
<div class="news-grid">
|
||||
<a href="/en/guide/getting-started" class="news-card">
|
||||
<div class="news-card-image" style="background: linear-gradient(135deg, #1e3a5f 0%, #1e1e1e 100%);">
|
||||
<div class="news-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24"><path fill="#4fc1ff" d="M12 3L1 9l4 2.18v6L12 21l7-3.82v-6l2-1.09V17h2V9zm6.82 6L12 12.72L5.18 9L12 5.28zM17 16l-5 2.72L7 16v-3.73L12 15l5-2.73z"/></svg>
|
||||
</div>
|
||||
<span class="news-badge">Quick Start</span>
|
||||
</div>
|
||||
<div class="news-card-content">
|
||||
<h3>Get Started in 5 Minutes</h3>
|
||||
<p>From installation to your first ECS app, learn the core concepts quickly.</p>
|
||||
</div>
|
||||
</a>
|
||||
<a href="/en/guide/behavior-tree/" class="news-card">
|
||||
<div class="news-card-image" style="background: linear-gradient(135deg, #1e3a5f 0%, #1e1e1e 100%);">
|
||||
<div class="news-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24"><path fill="#4ec9b0" d="M12 2a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m3 20h-1v-7l-2-2l-2 2v7H9v-7.5l-2 2V22H6v-6l3-3l1-3.5c-.3.4-.6.7-1 1L6 9v1H4V8l5-3c.5-.3 1.1-.5 1.7-.5H11c.6 0 1.2.2 1.7.5l5 3v2h-2V9l-3 1.5c-.4-.3-.7-.6-1-1l1 3.5l3 3v6Z"/></svg>
|
||||
</div>
|
||||
<span class="news-badge">AI System</span>
|
||||
</div>
|
||||
<div class="news-card-content">
|
||||
<h3>Visual Behavior Tree Editor</h3>
|
||||
<p>Built-in AI behavior tree system with visual editing and real-time debugging.</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="features-section">
|
||||
<div class="features-container">
|
||||
<h2 class="features-title">Core Features</h2>
|
||||
<div class="features-grid">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#4fc1ff" d="M13 2.05v2.02c3.95.49 7 3.85 7 7.93c0 1.45-.39 2.79-1.06 3.95l1.59 1.09A9.94 9.94 0 0 0 22 12c0-5.18-3.95-9.45-9-9.95M12 19c-3.87 0-7-3.13-7-7c0-3.53 2.61-6.43 6-6.92V2.05c-5.06.5-9 4.76-9 9.95c0 5.52 4.47 10 9.99 10c3.31 0 6.24-1.61 8.06-4.09l-1.6-1.1A7.93 7.93 0 0 1 12 19"/><path fill="#4fc1ff" d="M12 6a6 6 0 0 0-6 6c0 3.31 2.69 6 6 6a6 6 0 0 0 0-12m0 10c-2.21 0-4-1.79-4-4s1.79-4 4-4s4 1.79 4 4s-1.79 4-4 4"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">High-performance ECS Architecture</h3>
|
||||
<p class="feature-desc">Data-driven entity component system for large-scale entity processing with cache-friendly memory layout.</p>
|
||||
<a href="/en/guide/entity" class="feature-link">Learn more</a>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#569cd6" d="M3 3h18v18H3zm16.525 13.707c0-.795-.272-1.425-.816-1.89c-.544-.465-1.404-.804-2.58-1.016l-1.704-.296c-.616-.104-1.052-.26-1.308-.468c-.256-.21-.384-.468-.384-.776c0-.392.168-.7.504-.924c.336-.224.8-.336 1.392-.336c.56 0 1.008.124 1.344.372c.336.248.536.584.6 1.008h2.016c-.08-.96-.464-1.716-1.152-2.268c-.688-.552-1.6-.828-2.736-.828c-1.2 0-2.148.3-2.844.9c-.696.6-1.044 1.38-1.044 2.34c0 .76.252 1.368.756 1.824c.504.456 1.308.792 2.412.996l1.704.312c.624.12 1.068.28 1.332.48c.264.2.396.46.396.78c0 .424-.192.756-.576.996c-.384.24-.9.36-1.548.36c-.672 0-1.2-.14-1.584-.42c-.384-.28-.608-.668-.672-1.164H8.868c.048 1.016.46 1.808 1.236 2.376c.776.568 1.796.852 3.06.852c1.24 0 2.22-.292 2.94-.876c.72-.584 1.08-1.364 1.08-2.34z"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">Full Type Support</h3>
|
||||
<p class="feature-desc">100% TypeScript with complete type definitions and compile-time checking for the best development experience.</p>
|
||||
<a href="/en/guide/component" class="feature-link">Learn more</a>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#4ec9b0" d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10s10-4.5 10-10S17.5 2 12 2m0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8s8 3.59 8 8s-3.59 8-8 8m-5-8l4-4v3h4v2h-4v3z"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">Visual Behavior Tree</h3>
|
||||
<p class="feature-desc">Built-in AI behavior tree system with visual editor, custom nodes, and real-time debugging.</p>
|
||||
<a href="/en/guide/behavior-tree/" class="feature-link">Learn more</a>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#c586c0" d="M4 6h18V4H4c-1.1 0-2 .9-2 2v11H0v3h14v-3H4zm19 2h-6c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h6c.55 0 1-.45 1-1V9c0-.55-.45-1-1-1m-1 9h-4v-7h4z"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">Multi-Platform Support</h3>
|
||||
<p class="feature-desc">Support for browsers, Node.js, WeChat Mini Games, and seamless integration with major game engines.</p>
|
||||
<a href="/en/guide/platform-adapter" class="feature-link">Learn more</a>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#dcdcaa" d="M4 3h6v2H4v14h6v2H4c-1.1 0-2-.9-2-2V5c0-1.1.9-2 2-2m9 0h6c1.1 0 2 .9 2 2v14c0 1.1-.9 2-2 2h-6v-2h6V5h-6zm-1 7h4v2h-4z"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">Modular Design</h3>
|
||||
<p class="feature-desc">Core features packaged independently, import only what you need. Support for custom plugin extensions.</p>
|
||||
<a href="/en/guide/plugin-system" class="feature-link">Learn more</a>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#9cdcfe" d="M22.7 19l-9.1-9.1c.9-2.3.4-5-1.5-6.9c-2-2-5-2.4-7.4-1.3L9 6L6 9L1.6 4.7C.4 7.1.9 10.1 2.9 12.1c1.9 1.9 4.6 2.4 6.9 1.5l9.1 9.1c.4.4 1 .4 1.4 0l2.3-2.3c.5-.4.5-1.1.1-1.4"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">Developer Tools</h3>
|
||||
<p class="feature-desc">Built-in performance monitoring, debugging tools, serialization system, and complete development toolchain.</p>
|
||||
<a href="/en/guide/logging" class="feature-link">Learn more</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style scoped>
|
||||
/* Home page specific styles */
|
||||
.news-section {
|
||||
background: #0d0d0d;
|
||||
padding: 64px 0;
|
||||
border-top: 1px solid #2a2a2a;
|
||||
}
|
||||
|
||||
.news-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 48px;
|
||||
}
|
||||
|
||||
.news-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.news-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.news-more {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 20px;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 6px;
|
||||
color: #a0a0a0;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.news-more:hover {
|
||||
background: #252525;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.news-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.news-card {
|
||||
display: flex;
|
||||
background: #1f1f1f;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.news-card:hover {
|
||||
border-color: #3b9eff;
|
||||
}
|
||||
|
||||
.news-card-image {
|
||||
width: 200px;
|
||||
min-height: 140px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.news-icon {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.news-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
background: transparent;
|
||||
border: 1px solid #3a3a3a;
|
||||
border-radius: 16px;
|
||||
color: #a0a0a0;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.news-card-content {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.news-card-content h3 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.news-card-content p {
|
||||
font-size: 0.875rem;
|
||||
color: #707070;
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.features-section {
|
||||
background: #0d0d0d;
|
||||
padding: 64px 0;
|
||||
}
|
||||
|
||||
.features-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 48px;
|
||||
}
|
||||
|
||||
.features-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
margin: 0 0 32px 0;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: #1f1f1f;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
border-color: #3b9eff;
|
||||
background: #252525;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: #0d0d0d;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.feature-desc {
|
||||
font-size: 14px;
|
||||
color: #707070;
|
||||
line-height: 1.7;
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
|
||||
.feature-link {
|
||||
font-size: 14px;
|
||||
color: #3b9eff;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.feature-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.news-container,
|
||||
.features-container {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.news-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.news-card {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.news-card-image {
|
||||
width: 100%;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -192,6 +192,6 @@ export class AttackAction implements INodeExecutor {
|
||||
|
||||
## 获取帮助
|
||||
|
||||
- 提交 [Issue](https://github.com/esengine/ecs-framework/issues)
|
||||
- 提交 [Issue](https://github.com/esengine/esengine/issues)
|
||||
- 加入社区讨论
|
||||
- 参考文档中的完整代码示例
|
||||
|
||||
@@ -55,25 +55,92 @@ class Health extends Component {
|
||||
}
|
||||
```
|
||||
|
||||
### 组件装饰器
|
||||
### @ECSComponent 装饰器
|
||||
|
||||
**必须使用 `@ECSComponent` 装饰器**,这确保了:
|
||||
- 组件在代码混淆后仍能正确识别
|
||||
- 提供稳定的类型名称用于序列化和调试
|
||||
- 框架能正确管理组件注册
|
||||
`@ECSComponent` 是组件类必须使用的装饰器,它为组件提供了类型标识和元数据管理。
|
||||
|
||||
#### 为什么必须使用
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| **类型识别** | 提供稳定的类型名称,代码混淆后仍能正确识别 |
|
||||
| **序列化支持** | 序列化/反序列化时使用该名称作为类型标识 |
|
||||
| **组件注册** | 自动注册到 ComponentRegistry,分配唯一的位掩码 |
|
||||
| **调试支持** | 在调试工具和日志中显示可读的组件名称 |
|
||||
|
||||
#### 基本语法
|
||||
|
||||
```typescript
|
||||
// 正确的用法
|
||||
@ECSComponent(typeName: string)
|
||||
```
|
||||
|
||||
- `typeName`: 组件的类型名称,建议使用与类名相同或相近的名称
|
||||
|
||||
#### 使用示例
|
||||
|
||||
```typescript
|
||||
// ✅ 正确的用法
|
||||
@ECSComponent('Velocity')
|
||||
class Velocity extends Component {
|
||||
dx: number = 0;
|
||||
dy: number = 0;
|
||||
}
|
||||
|
||||
// 错误的用法 - 没有装饰器
|
||||
class BadComponent extends Component {
|
||||
// 这样定义的组件可能在生产环境出现问题
|
||||
// ✅ 推荐:类型名与类名保持一致
|
||||
@ECSComponent('PlayerController')
|
||||
class PlayerController extends Component {
|
||||
speed: number = 5;
|
||||
}
|
||||
|
||||
// ❌ 错误的用法 - 没有装饰器
|
||||
class BadComponent extends Component {
|
||||
// 这样定义的组件可能在生产环境出现问题:
|
||||
// 1. 代码压缩后类名变化,无法正确序列化
|
||||
// 2. 组件未注册到框架,查询和匹配可能失效
|
||||
}
|
||||
```
|
||||
|
||||
#### 与 @Serializable 配合使用
|
||||
|
||||
当组件需要支持序列化时,`@ECSComponent` 和 `@Serializable` 需要一起使用:
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent, Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSComponent('Player')
|
||||
@Serializable({ version: 1 })
|
||||
class PlayerComponent extends Component {
|
||||
@Serialize()
|
||||
name: string = '';
|
||||
|
||||
@Serialize()
|
||||
level: number = 1;
|
||||
|
||||
// 不使用 @Serialize() 的字段不会被序列化
|
||||
private _cachedData: any = null;
|
||||
}
|
||||
```
|
||||
|
||||
> **注意**:`@ECSComponent` 的 `typeName` 和 `@Serializable` 的 `typeId` 可以不同。如果 `@Serializable` 没有指定 `typeId`,则默认使用 `@ECSComponent` 的 `typeName`。
|
||||
|
||||
#### 组件类型名的唯一性
|
||||
|
||||
每个组件的类型名应该是唯一的:
|
||||
|
||||
```typescript
|
||||
// ❌ 错误:两个组件使用相同的类型名
|
||||
@ECSComponent('Health')
|
||||
class HealthComponent extends Component { }
|
||||
|
||||
@ECSComponent('Health') // 冲突!
|
||||
class EnemyHealthComponent extends Component { }
|
||||
|
||||
// ✅ 正确:使用不同的类型名
|
||||
@ECSComponent('PlayerHealth')
|
||||
class PlayerHealthComponent extends Component { }
|
||||
|
||||
@ECSComponent('EnemyHealth')
|
||||
class EnemyHealthComponent extends Component { }
|
||||
```
|
||||
|
||||
## 组件生命周期
|
||||
|
||||
@@ -121,6 +121,65 @@ class CombatSystem extends EntitySystem {
|
||||
}
|
||||
```
|
||||
|
||||
#### nothing() - 不匹配任何实体
|
||||
|
||||
用于创建只需要生命周期方法(`onBegin`、`onEnd`)但不需要处理实体的系统。
|
||||
|
||||
```typescript
|
||||
class FrameTimerSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// 不匹配任何实体
|
||||
super(Matcher.nothing());
|
||||
}
|
||||
|
||||
protected onBegin(): void {
|
||||
// 每帧开始时执行
|
||||
Performance.markFrameStart();
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 永远不会被调用,因为没有匹配的实体
|
||||
}
|
||||
|
||||
protected onEnd(): void {
|
||||
// 每帧结束时执行
|
||||
Performance.markFrameEnd();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### empty() vs nothing() 的区别
|
||||
|
||||
| 方法 | 行为 | 使用场景 |
|
||||
|------|------|----------|
|
||||
| `Matcher.empty()` | 匹配**所有**实体 | 需要处理场景中所有实体 |
|
||||
| `Matcher.nothing()` | 不匹配**任何**实体 | 只需要生命周期回调,不处理实体 |
|
||||
|
||||
```typescript
|
||||
// empty() - 返回场景中的所有实体
|
||||
class AllEntitiesSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty());
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// entities 包含场景中的所有实体
|
||||
console.log(`场景中共有 ${entities.length} 个实体`);
|
||||
}
|
||||
}
|
||||
|
||||
// nothing() - 不返回任何实体
|
||||
class NoEntitiesSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.nothing());
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// entities 永远是空数组,此方法不会被调用
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 按标签查询
|
||||
|
||||
```typescript
|
||||
@@ -371,6 +430,137 @@ class GameManager {
|
||||
}
|
||||
```
|
||||
|
||||
## 编译查询 (CompiledQuery)
|
||||
|
||||
> **v2.4.0+**
|
||||
|
||||
CompiledQuery 是一个轻量级的查询工具,提供类型安全的组件访问和变更检测支持。适合临时查询、工具开发和简单的迭代场景。
|
||||
|
||||
### 基本用法
|
||||
|
||||
```typescript
|
||||
// 创建编译查询
|
||||
const query = scene.querySystem.compile(Position, Velocity);
|
||||
|
||||
// 类型安全的遍历 - 组件参数自动推断类型
|
||||
query.forEach((entity, pos, vel) => {
|
||||
pos.x += vel.vx * deltaTime;
|
||||
pos.y += vel.vy * deltaTime;
|
||||
});
|
||||
|
||||
// 获取实体数量
|
||||
console.log(`匹配实体数: ${query.count}`);
|
||||
|
||||
// 获取第一个匹配的实体
|
||||
const first = query.first();
|
||||
if (first) {
|
||||
const [entity, pos, vel] = first;
|
||||
console.log(`第一个实体: ${entity.name}`);
|
||||
}
|
||||
```
|
||||
|
||||
### 变更检测
|
||||
|
||||
CompiledQuery 支持基于 epoch 的变更检测:
|
||||
|
||||
```typescript
|
||||
class RenderSystem extends EntitySystem {
|
||||
private _query: CompiledQuery<[typeof Transform, typeof Sprite]>;
|
||||
private _lastEpoch = 0;
|
||||
|
||||
protected onInitialize(): void {
|
||||
this._query = this.scene!.querySystem.compile(Transform, Sprite);
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 只处理 Transform 或 Sprite 发生变化的实体
|
||||
this._query.forEachChanged(this._lastEpoch, (entity, transform, sprite) => {
|
||||
this.updateRenderData(entity, transform, sprite);
|
||||
});
|
||||
|
||||
// 保存当前 epoch 作为下次检查的起点
|
||||
this._lastEpoch = this.scene!.epochManager.current;
|
||||
}
|
||||
|
||||
private updateRenderData(entity: Entity, transform: Transform, sprite: Sprite): void {
|
||||
// 更新渲染数据
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 函数式 API
|
||||
|
||||
CompiledQuery 提供了丰富的函数式 API:
|
||||
|
||||
```typescript
|
||||
const query = scene.querySystem.compile(Position, Health);
|
||||
|
||||
// map - 转换实体数据
|
||||
const positions = query.map((entity, pos, health) => ({
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
healthPercent: health.current / health.max
|
||||
}));
|
||||
|
||||
// filter - 过滤实体
|
||||
const lowHealthEntities = query.filter((entity, pos, health) => {
|
||||
return health.current < health.max * 0.2;
|
||||
});
|
||||
|
||||
// find - 查找第一个匹配的实体
|
||||
const target = query.find((entity, pos, health) => {
|
||||
return health.current > 0 && pos.x > 100;
|
||||
});
|
||||
|
||||
// toArray - 转换为数组
|
||||
const allData = query.toArray();
|
||||
for (const [entity, pos, health] of allData) {
|
||||
console.log(`${entity.name}: ${pos.x}, ${pos.y}`);
|
||||
}
|
||||
|
||||
// any/empty - 检查是否有匹配
|
||||
if (query.any()) {
|
||||
console.log('有匹配的实体');
|
||||
}
|
||||
if (query.empty()) {
|
||||
console.log('没有匹配的实体');
|
||||
}
|
||||
```
|
||||
|
||||
### CompiledQuery vs EntitySystem
|
||||
|
||||
| 特性 | CompiledQuery | EntitySystem |
|
||||
|------|---------------|--------------|
|
||||
| **用途** | 轻量级查询工具 | 完整的系统逻辑 |
|
||||
| **生命周期** | 无 | 完整 (onInitialize, onDestroy 等) |
|
||||
| **调度集成** | 无 | 支持 @Stage, @Before, @After |
|
||||
| **变更检测** | forEachChanged | forEachChanged |
|
||||
| **事件监听** | 无 | addEventListener |
|
||||
| **命令缓冲** | 无 | this.commands |
|
||||
| **类型安全组件** | forEach 参数自动推断 | 需要手动 getComponent |
|
||||
| **适用场景** | 临时查询、工具、原型 | 核心游戏逻辑 |
|
||||
|
||||
**选择建议**:
|
||||
|
||||
- 使用 **EntitySystem** 处理核心游戏逻辑(移动、战斗、AI 等)
|
||||
- 使用 **CompiledQuery** 进行一次性查询、工具开发或简单迭代
|
||||
|
||||
### CompiledQuery API 参考
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `forEach(callback)` | 遍历所有匹配实体,类型安全的组件参数 |
|
||||
| `forEachChanged(sinceEpoch, callback)` | 只遍历变更的实体 |
|
||||
| `first()` | 获取第一个匹配的实体和组件 |
|
||||
| `toArray()` | 转换为 [entity, ...components] 数组 |
|
||||
| `map(callback)` | 映射转换 |
|
||||
| `filter(predicate)` | 过滤实体 |
|
||||
| `find(predicate)` | 查找第一个满足条件的实体 |
|
||||
| `any()` | 是否有任何匹配 |
|
||||
| `empty()` | 是否没有匹配 |
|
||||
| `count` | 匹配的实体数量 |
|
||||
| `entities` | 匹配的实体列表(只读) |
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 优先使用 EntitySystem
|
||||
@@ -493,6 +683,65 @@ const matcher2 = matcher.any(VelocityComponent);
|
||||
console.log(matcher === matcher2); // false
|
||||
```
|
||||
|
||||
## Matcher API 快速参考
|
||||
|
||||
### 静态创建方法
|
||||
|
||||
| 方法 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `Matcher.all(...types)` | 必须包含所有指定组件 | `Matcher.all(Position, Velocity)` |
|
||||
| `Matcher.any(...types)` | 至少包含一个指定组件 | `Matcher.any(Health, Shield)` |
|
||||
| `Matcher.none(...types)` | 不能包含任何指定组件 | `Matcher.none(Dead)` |
|
||||
| `Matcher.byTag(tag)` | 按标签查询 | `Matcher.byTag(1)` |
|
||||
| `Matcher.byName(name)` | 按名称查询 | `Matcher.byName("Player")` |
|
||||
| `Matcher.byComponent(type)` | 按单个组件查询 | `Matcher.byComponent(Health)` |
|
||||
| `Matcher.empty()` | 创建空匹配器(匹配所有实体) | `Matcher.empty()` |
|
||||
| `Matcher.nothing()` | 不匹配任何实体 | `Matcher.nothing()` |
|
||||
| `Matcher.complex()` | 创建复杂查询构建器 | `Matcher.complex()` |
|
||||
|
||||
### 链式方法
|
||||
|
||||
| 方法 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `.all(...types)` | 添加必须包含的组件 | `.all(Position)` |
|
||||
| `.any(...types)` | 添加可选组件(至少一个) | `.any(Weapon, Magic)` |
|
||||
| `.none(...types)` | 添加排除的组件 | `.none(Dead)` |
|
||||
| `.exclude(...types)` | `.none()` 的别名 | `.exclude(Disabled)` |
|
||||
| `.one(...types)` | `.any()` 的别名 | `.one(Player, Enemy)` |
|
||||
| `.withTag(tag)` | 添加标签条件 | `.withTag(1)` |
|
||||
| `.withName(name)` | 添加名称条件 | `.withName("Boss")` |
|
||||
| `.withComponent(type)` | 添加单组件条件 | `.withComponent(Health)` |
|
||||
|
||||
### 实用方法
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `.getCondition()` | 获取查询条件(只读) |
|
||||
| `.isEmpty()` | 检查是否为空条件 |
|
||||
| `.isNothing()` | 检查是否为 nothing 匹配器 |
|
||||
| `.clone()` | 克隆匹配器 |
|
||||
| `.reset()` | 重置所有条件 |
|
||||
| `.toString()` | 获取字符串表示 |
|
||||
|
||||
### 常用组合示例
|
||||
|
||||
```typescript
|
||||
// 基础移动系统
|
||||
Matcher.all(Position, Velocity)
|
||||
|
||||
// 可攻击的活着的实体
|
||||
Matcher.all(Position, Health)
|
||||
.any(Weapon, Magic)
|
||||
.none(Dead, Disabled)
|
||||
|
||||
// 所有带标签的敌人
|
||||
Matcher.byTag(Tags.ENEMY)
|
||||
.all(AIComponent)
|
||||
|
||||
// 只需要生命周期的系统
|
||||
Matcher.nothing()
|
||||
```
|
||||
|
||||
## 相关 API
|
||||
|
||||
- [Matcher](../api/classes/Matcher.md) - 查询条件描述符 API 参考
|
||||
|
||||
@@ -9,6 +9,12 @@
|
||||
- 提供唯一标识(ID)
|
||||
- 管理组件的生命周期
|
||||
|
||||
::: tip 关于父子层级关系
|
||||
实体间的父子层级关系通过 `HierarchyComponent` 和 `HierarchySystem` 管理,而非 Entity 内置属性。这种设计遵循 ECS 组合原则 —— 只有需要层级关系的实体才添加此组件。
|
||||
|
||||
详见 [层级系统](./hierarchy.md) 文档。
|
||||
:::
|
||||
|
||||
## 创建实体
|
||||
|
||||
**重要提示:实体必须通过场景创建,不支持手动创建!**
|
||||
@@ -285,4 +291,156 @@ entity.components.forEach(component => {
|
||||
});
|
||||
```
|
||||
|
||||
实体是 ECS 架构的核心概念之一,理解如何正确使用实体将帮助你构建高效、可维护的游戏代码。
|
||||
实体是 ECS 架构的核心概念之一,理解如何正确使用实体将帮助你构建高效、可维护的游戏代码。
|
||||
|
||||
## 实体句柄 (EntityHandle)
|
||||
|
||||
实体句柄是一种安全的实体引用方式,用于解决"引用已销毁实体"的问题。
|
||||
|
||||
### 问题场景
|
||||
|
||||
假设你的 AI 系统需要追踪一个目标敌人:
|
||||
|
||||
```typescript
|
||||
// 错误做法:直接存储实体引用
|
||||
class AISystem extends EntitySystem {
|
||||
private targetEnemy: Entity | null = null;
|
||||
|
||||
setTarget(enemy: Entity) {
|
||||
this.targetEnemy = enemy;
|
||||
}
|
||||
|
||||
process() {
|
||||
if (this.targetEnemy) {
|
||||
// 危险!敌人可能已被销毁,但引用还在
|
||||
// 更糟糕:这个内存位置可能被新实体复用了
|
||||
const health = this.targetEnemy.getComponent(Health);
|
||||
// 可能操作了错误的实体!
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用句柄的正确做法
|
||||
|
||||
每个实体创建时会自动分配一个句柄,通过 `entity.handle` 获取:
|
||||
|
||||
```typescript
|
||||
import { EntityHandle, NULL_HANDLE, isValidHandle } from '@esengine/ecs-framework';
|
||||
|
||||
class AISystem extends EntitySystem {
|
||||
// 存储句柄而非实体引用
|
||||
private targetHandle: EntityHandle = NULL_HANDLE;
|
||||
|
||||
setTarget(enemy: Entity) {
|
||||
// 保存敌人的句柄
|
||||
this.targetHandle = enemy.handle;
|
||||
}
|
||||
|
||||
process() {
|
||||
if (!isValidHandle(this.targetHandle)) {
|
||||
return; // 没有目标
|
||||
}
|
||||
|
||||
// 通过句柄获取实体(自动检测是否有效)
|
||||
const enemy = this.scene.findEntityByHandle(this.targetHandle);
|
||||
|
||||
if (!enemy) {
|
||||
// 敌人已被销毁,清空引用
|
||||
this.targetHandle = NULL_HANDLE;
|
||||
return;
|
||||
}
|
||||
|
||||
// 安全操作
|
||||
const health = enemy.getComponent(Health);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 完整示例:技能目标锁定
|
||||
|
||||
```typescript
|
||||
import {
|
||||
EntitySystem, Entity, EntityHandle, NULL_HANDLE, isValidHandle
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
@ECSSystem('SkillTargeting')
|
||||
class SkillTargetingSystem extends EntitySystem {
|
||||
// 存储多个目标的句柄
|
||||
private lockedTargets: Map<Entity, EntityHandle> = new Map();
|
||||
|
||||
// 锁定目标
|
||||
lockTarget(caster: Entity, target: Entity) {
|
||||
this.lockedTargets.set(caster, target.handle);
|
||||
}
|
||||
|
||||
// 获取锁定的目标
|
||||
getLockedTarget(caster: Entity): Entity | null {
|
||||
const handle = this.lockedTargets.get(caster);
|
||||
|
||||
if (!handle || !isValidHandle(handle)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// findEntityByHandle 会检查句柄是否有效
|
||||
const target = this.scene.findEntityByHandle(handle);
|
||||
|
||||
if (!target) {
|
||||
// 目标已死亡,清除锁定
|
||||
this.lockedTargets.delete(caster);
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
// 释放技能
|
||||
castSkill(caster: Entity) {
|
||||
const target = this.getLockedTarget(caster);
|
||||
|
||||
if (!target) {
|
||||
console.log('目标丢失,技能取消');
|
||||
return;
|
||||
}
|
||||
|
||||
// 安全地对目标造成伤害
|
||||
const health = target.getComponent(Health);
|
||||
if (health) {
|
||||
health.current -= 10;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 句柄 vs 实体引用
|
||||
|
||||
| 场景 | 推荐方式 |
|
||||
|-----|---------|
|
||||
| 同一帧内临时使用 | 直接用 `Entity` 引用 |
|
||||
| 跨帧存储(如 AI 目标、技能目标) | 使用 `EntityHandle` |
|
||||
| 需要序列化保存 | 使用 `EntityHandle`(数字类型) |
|
||||
| 网络同步 | 使用 `EntityHandle`(可直接传输) |
|
||||
|
||||
### API 速查
|
||||
|
||||
```typescript
|
||||
// 获取实体的句柄
|
||||
const handle = entity.handle;
|
||||
|
||||
// 检查句柄是否非空
|
||||
if (isValidHandle(handle)) { ... }
|
||||
|
||||
// 通过句柄获取实体(自动检测有效性)
|
||||
const entity = scene.findEntityByHandle(handle);
|
||||
|
||||
// 检查句柄对应的实体是否存活
|
||||
const alive = scene.handleManager.isAlive(handle);
|
||||
|
||||
// 空句柄常量
|
||||
const emptyHandle = NULL_HANDLE;
|
||||
```
|
||||
|
||||
## 下一步
|
||||
|
||||
- 了解 [层级系统](./hierarchy.md) 建立实体间的父子关系
|
||||
- 了解 [组件系统](./component.md) 为实体添加功能
|
||||
- 了解 [场景管理](./scene.md) 组织和管理实体
|
||||
@@ -4,7 +4,24 @@
|
||||
|
||||
## 安装
|
||||
|
||||
### NPM 安装
|
||||
### 使用 CLI(推荐)
|
||||
|
||||
在现有项目中添加 ECS 的最简单方式:
|
||||
|
||||
```bash
|
||||
# 在项目目录中运行
|
||||
npx @esengine/cli init
|
||||
```
|
||||
|
||||
CLI 会自动检测项目类型(Cocos Creator 2.x/3.x、LayaAir 3.x 或 Node.js)并生成相应的集成代码,包括:
|
||||
|
||||
- `ECSManager` 组件/脚本 - 负责 ECS 生命周期管理
|
||||
- 示例组件和系统 - 帮助快速上手
|
||||
- 自动安装依赖
|
||||
|
||||
### NPM 手动安装
|
||||
|
||||
如果你更喜欢手动配置,可以直接安装:
|
||||
|
||||
```bash
|
||||
# 使用 npm
|
||||
@@ -23,7 +40,6 @@ import { Core } from '@esengine/ecs-framework'
|
||||
// 方式1:使用配置对象(推荐)
|
||||
const core = Core.create({
|
||||
debug: true, // 启用调试模式,提供详细的日志和性能监控
|
||||
enableEntitySystems: true, // 启用实体系统,这是ECS的核心功能
|
||||
debugConfig: { // 可选:高级调试配置
|
||||
enabled: false, // 是否启用WebSocket调试服务器
|
||||
websocketUrl: 'ws://localhost:8080',
|
||||
@@ -39,12 +55,11 @@ const core = Core.create({
|
||||
});
|
||||
|
||||
// 方式2:简化创建(向后兼容)
|
||||
const core = Core.create(true); // 等同于 { debug: true, enableEntitySystems: true }
|
||||
const core = Core.create(true); // 等同于 { debug: true }
|
||||
|
||||
// 方式3:生产环境配置
|
||||
const core = Core.create({
|
||||
debug: false, // 生产环境关闭调试
|
||||
enableEntitySystems: true
|
||||
debug: false // 生产环境关闭调试
|
||||
});
|
||||
```
|
||||
|
||||
@@ -55,9 +70,6 @@ interface ICoreConfig {
|
||||
/** 是否启用调试模式 - 影响日志级别和性能监控 */
|
||||
debug?: boolean;
|
||||
|
||||
/** 是否启用实体系统 - 核心ECS功能开关 */
|
||||
enableEntitySystems?: boolean;
|
||||
|
||||
/** 高级调试配置 - 用于开发工具集成 */
|
||||
debugConfig?: {
|
||||
enabled: boolean; // 是否启用调试服务器
|
||||
@@ -338,27 +350,39 @@ function gameLoop(deltaTime: number) {
|
||||
|
||||
## 与游戏引擎集成
|
||||
|
||||
### Laya 引擎集成
|
||||
### Laya 3.x 引擎集成
|
||||
|
||||
推荐使用 `Laya.Script` 组件来管理 ECS 生命周期:
|
||||
|
||||
```typescript
|
||||
import { Stage } from "laya/display/Stage";
|
||||
import { Laya } from "Laya";
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始化 Laya
|
||||
Laya.init(800, 600).then(() => {
|
||||
// 初始化 ECS
|
||||
Core.create(true);
|
||||
Core.setScene(new GameScene());
|
||||
const { regClass } = Laya;
|
||||
|
||||
// 启动游戏循环
|
||||
Laya.timer.frameLoop(1, this, () => {
|
||||
const deltaTime = Laya.timer.delta / 1000;
|
||||
Core.update(deltaTime); // 自动更新全局服务和场景
|
||||
});
|
||||
});
|
||||
@regClass()
|
||||
export class ECSManager extends Laya.Script {
|
||||
private ecsScene = new GameScene();
|
||||
|
||||
onAwake(): void {
|
||||
// 初始化 ECS
|
||||
Core.create({ debug: true });
|
||||
Core.setScene(this.ecsScene);
|
||||
}
|
||||
|
||||
onUpdate(): void {
|
||||
// 自动更新全局服务和场景
|
||||
Core.update(Laya.timer.delta / 1000);
|
||||
}
|
||||
|
||||
onDestroy(): void {
|
||||
// 清理资源
|
||||
Core.destroy();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在 Laya IDE 中,将 `ECSManager` 脚本挂载到场景中的节点上即可。
|
||||
|
||||
### Cocos Creator 集成
|
||||
|
||||
```typescript
|
||||
|
||||
437
docs/guide/hierarchy.md
Normal file
437
docs/guide/hierarchy.md
Normal file
@@ -0,0 +1,437 @@
|
||||
# 层级系统
|
||||
|
||||
在游戏开发中,实体间的父子层级关系是常见需求。ECS Framework 采用组件化方式管理层级关系,通过 `HierarchyComponent` 和 `HierarchySystem` 实现,完全遵循 ECS 组合原则。
|
||||
|
||||
## 设计理念
|
||||
|
||||
### 为什么不在 Entity 中内置层级?
|
||||
|
||||
传统的游戏对象模型将层级关系内置于实体中。ECS Framework 选择组件化方案的原因:
|
||||
|
||||
1. **ECS 组合原则**:层级是一种"功能",应该通过组件添加,而非所有实体都具备
|
||||
2. **按需使用**:只有需要层级关系的实体才添加 `HierarchyComponent`
|
||||
3. **数据与逻辑分离**:`HierarchyComponent` 存储数据,`HierarchySystem` 处理逻辑
|
||||
4. **序列化友好**:层级关系作为组件数据可以轻松序列化和反序列化
|
||||
|
||||
## 基本概念
|
||||
|
||||
### HierarchyComponent
|
||||
|
||||
存储层级关系数据的组件:
|
||||
|
||||
```typescript
|
||||
import { HierarchyComponent } from '@esengine/ecs-framework';
|
||||
|
||||
// HierarchyComponent 的核心属性
|
||||
interface HierarchyComponent {
|
||||
parentId: number | null; // 父实体 ID,null 表示根实体
|
||||
childIds: number[]; // 子实体 ID 列表
|
||||
depth: number; // 在层级中的深度(由系统维护)
|
||||
bActiveInHierarchy: boolean; // 在层级中是否激活(由系统维护)
|
||||
}
|
||||
```
|
||||
|
||||
### HierarchySystem
|
||||
|
||||
处理层级逻辑的系统,提供所有层级操作的 API:
|
||||
|
||||
```typescript
|
||||
import { HierarchySystem } from '@esengine/ecs-framework';
|
||||
|
||||
// 获取系统
|
||||
const hierarchySystem = scene.getEntityProcessor(HierarchySystem);
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 添加系统到场景
|
||||
|
||||
```typescript
|
||||
import { Scene, HierarchySystem } from '@esengine/ecs-framework';
|
||||
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 添加层级系统
|
||||
this.addSystem(new HierarchySystem());
|
||||
|
||||
// 添加其他系统...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 建立父子关系
|
||||
|
||||
```typescript
|
||||
// 创建实体
|
||||
const parent = scene.createEntity("Parent");
|
||||
const child1 = scene.createEntity("Child1");
|
||||
const child2 = scene.createEntity("Child2");
|
||||
|
||||
// 获取层级系统
|
||||
const hierarchySystem = scene.getEntityProcessor(HierarchySystem);
|
||||
|
||||
// 设置父子关系(自动添加 HierarchyComponent)
|
||||
hierarchySystem.setParent(child1, parent);
|
||||
hierarchySystem.setParent(child2, parent);
|
||||
|
||||
// 现在 parent 有两个子实体
|
||||
```
|
||||
|
||||
### 查询层级
|
||||
|
||||
```typescript
|
||||
// 获取父实体
|
||||
const parentEntity = hierarchySystem.getParent(child1);
|
||||
|
||||
// 获取所有子实体
|
||||
const children = hierarchySystem.getChildren(parent);
|
||||
|
||||
// 获取子实体数量
|
||||
const count = hierarchySystem.getChildCount(parent);
|
||||
|
||||
// 检查是否有子实体
|
||||
const hasKids = hierarchySystem.hasChildren(parent);
|
||||
|
||||
// 获取在层级中的深度
|
||||
const depth = hierarchySystem.getDepth(child1); // 返回 1
|
||||
```
|
||||
|
||||
## API 参考
|
||||
|
||||
### 父子关系操作
|
||||
|
||||
#### setParent
|
||||
|
||||
设置实体的父级:
|
||||
|
||||
```typescript
|
||||
// 设置父级
|
||||
hierarchySystem.setParent(child, parent);
|
||||
|
||||
// 移动到根级(无父级)
|
||||
hierarchySystem.setParent(child, null);
|
||||
```
|
||||
|
||||
#### insertChildAt
|
||||
|
||||
在指定位置插入子实体:
|
||||
|
||||
```typescript
|
||||
// 在第一个位置插入
|
||||
hierarchySystem.insertChildAt(parent, child, 0);
|
||||
|
||||
// 追加到末尾
|
||||
hierarchySystem.insertChildAt(parent, child, -1);
|
||||
```
|
||||
|
||||
#### removeChild
|
||||
|
||||
从父级移除子实体(子实体变为根级):
|
||||
|
||||
```typescript
|
||||
const success = hierarchySystem.removeChild(parent, child);
|
||||
```
|
||||
|
||||
#### removeAllChildren
|
||||
|
||||
移除所有子实体:
|
||||
|
||||
```typescript
|
||||
hierarchySystem.removeAllChildren(parent);
|
||||
```
|
||||
|
||||
### 层级查询
|
||||
|
||||
#### getParent / getChildren
|
||||
|
||||
```typescript
|
||||
const parent = hierarchySystem.getParent(entity);
|
||||
const children = hierarchySystem.getChildren(entity);
|
||||
```
|
||||
|
||||
#### getRoot
|
||||
|
||||
获取实体的根节点:
|
||||
|
||||
```typescript
|
||||
const root = hierarchySystem.getRoot(deepChild);
|
||||
```
|
||||
|
||||
#### getRootEntities
|
||||
|
||||
获取所有根实体(没有父级的实体):
|
||||
|
||||
```typescript
|
||||
const roots = hierarchySystem.getRootEntities();
|
||||
```
|
||||
|
||||
#### isAncestorOf / isDescendantOf
|
||||
|
||||
检查祖先/后代关系:
|
||||
|
||||
```typescript
|
||||
// grandparent -> parent -> child
|
||||
const isAncestor = hierarchySystem.isAncestorOf(grandparent, child); // true
|
||||
const isDescendant = hierarchySystem.isDescendantOf(child, grandparent); // true
|
||||
```
|
||||
|
||||
### 层级遍历
|
||||
|
||||
#### findChild
|
||||
|
||||
根据名称查找子实体:
|
||||
|
||||
```typescript
|
||||
// 直接子级中查找
|
||||
const child = hierarchySystem.findChild(parent, "ChildName");
|
||||
|
||||
// 递归查找所有后代
|
||||
const deepChild = hierarchySystem.findChild(parent, "DeepChild", true);
|
||||
```
|
||||
|
||||
#### findChildrenByTag
|
||||
|
||||
根据标签查找子实体:
|
||||
|
||||
```typescript
|
||||
// 查找直接子级
|
||||
const tagged = hierarchySystem.findChildrenByTag(parent, TAG_ENEMY);
|
||||
|
||||
// 递归查找
|
||||
const allTagged = hierarchySystem.findChildrenByTag(parent, TAG_ENEMY, true);
|
||||
```
|
||||
|
||||
#### forEachChild
|
||||
|
||||
遍历子实体:
|
||||
|
||||
```typescript
|
||||
// 遍历直接子级
|
||||
hierarchySystem.forEachChild(parent, (child) => {
|
||||
console.log(child.name);
|
||||
});
|
||||
|
||||
// 递归遍历所有后代
|
||||
hierarchySystem.forEachChild(parent, (child) => {
|
||||
console.log(child.name);
|
||||
}, true);
|
||||
```
|
||||
|
||||
### 层级状态
|
||||
|
||||
#### isActiveInHierarchy
|
||||
|
||||
检查实体在层级中是否激活(考虑所有祖先的激活状态):
|
||||
|
||||
```typescript
|
||||
// 如果 parent.active = false,即使 child.active = true
|
||||
// isActiveInHierarchy(child) 也会返回 false
|
||||
const activeInHierarchy = hierarchySystem.isActiveInHierarchy(child);
|
||||
```
|
||||
|
||||
#### getDepth
|
||||
|
||||
获取实体在层级中的深度(根实体深度为 0):
|
||||
|
||||
```typescript
|
||||
const depth = hierarchySystem.getDepth(entity);
|
||||
```
|
||||
|
||||
### 扁平化层级(用于 UI 渲染)
|
||||
|
||||
```typescript
|
||||
// 用于实现可展开/折叠的层级树视图
|
||||
const expandedIds = new Set([parent.id]);
|
||||
|
||||
const flatNodes = hierarchySystem.flattenHierarchy(expandedIds);
|
||||
// 返回 [{ entity, depth, bHasChildren, bIsExpanded }, ...]
|
||||
```
|
||||
|
||||
## 完整示例
|
||||
|
||||
### 创建游戏角色层级
|
||||
|
||||
```typescript
|
||||
import {
|
||||
Scene,
|
||||
HierarchySystem,
|
||||
HierarchyComponent
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
class GameScene extends Scene {
|
||||
private hierarchySystem!: HierarchySystem;
|
||||
|
||||
protected initialize(): void {
|
||||
// 添加层级系统
|
||||
this.hierarchySystem = new HierarchySystem();
|
||||
this.addSystem(this.hierarchySystem);
|
||||
|
||||
// 创建角色层级
|
||||
this.createPlayerHierarchy();
|
||||
}
|
||||
|
||||
private createPlayerHierarchy(): void {
|
||||
// 根实体
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Transform(0, 0));
|
||||
|
||||
// 身体部件
|
||||
const body = this.createEntity("Body");
|
||||
body.addComponent(new Sprite("body.png"));
|
||||
this.hierarchySystem.setParent(body, player);
|
||||
|
||||
// 武器(挂载在身体上)
|
||||
const weapon = this.createEntity("Weapon");
|
||||
weapon.addComponent(new Sprite("sword.png"));
|
||||
this.hierarchySystem.setParent(weapon, body);
|
||||
|
||||
// 特效(挂载在武器上)
|
||||
const effect = this.createEntity("WeaponEffect");
|
||||
effect.addComponent(new ParticleEmitter());
|
||||
this.hierarchySystem.setParent(effect, weapon);
|
||||
|
||||
// 查询层级信息
|
||||
console.log(`Player 层级深度: ${this.hierarchySystem.getDepth(player)}`); // 0
|
||||
console.log(`Weapon 层级深度: ${this.hierarchySystem.getDepth(weapon)}`); // 2
|
||||
console.log(`Effect 层级深度: ${this.hierarchySystem.getDepth(effect)}`); // 3
|
||||
}
|
||||
|
||||
public equipNewWeapon(weaponName: string): void {
|
||||
const body = this.findEntity("Body");
|
||||
const oldWeapon = this.hierarchySystem.findChild(body!, "Weapon");
|
||||
|
||||
if (oldWeapon) {
|
||||
// 移除旧武器的所有子实体
|
||||
this.hierarchySystem.removeAllChildren(oldWeapon);
|
||||
oldWeapon.destroy();
|
||||
}
|
||||
|
||||
// 创建新武器
|
||||
const newWeapon = this.createEntity("Weapon");
|
||||
newWeapon.addComponent(new Sprite(`${weaponName}.png`));
|
||||
this.hierarchySystem.setParent(newWeapon, body!);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 层级变换系统
|
||||
|
||||
结合 Transform 组件实现层级变换:
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, Matcher, HierarchySystem, HierarchyComponent } from '@esengine/ecs-framework';
|
||||
|
||||
class HierarchyTransformSystem extends EntitySystem {
|
||||
private hierarchySystem!: HierarchySystem;
|
||||
|
||||
constructor() {
|
||||
super(Matcher.empty().all(Transform, HierarchyComponent));
|
||||
}
|
||||
|
||||
public onAddedToScene(): void {
|
||||
// 获取层级系统引用
|
||||
this.hierarchySystem = this.scene!.getEntityProcessor(HierarchySystem)!;
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 按深度排序,确保父级先更新
|
||||
const sorted = [...entities].sort((a, b) => {
|
||||
return this.hierarchySystem.getDepth(a) - this.hierarchySystem.getDepth(b);
|
||||
});
|
||||
|
||||
for (const entity of sorted) {
|
||||
const transform = entity.getComponent(Transform)!;
|
||||
const parent = this.hierarchySystem.getParent(entity);
|
||||
|
||||
if (parent) {
|
||||
const parentTransform = parent.getComponent(Transform);
|
||||
if (parentTransform) {
|
||||
// 计算世界坐标
|
||||
transform.worldX = parentTransform.worldX + transform.localX;
|
||||
transform.worldY = parentTransform.worldY + transform.localY;
|
||||
}
|
||||
} else {
|
||||
// 根实体,本地坐标即世界坐标
|
||||
transform.worldX = transform.localX;
|
||||
transform.worldY = transform.localY;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 缓存机制
|
||||
|
||||
`HierarchySystem` 内置了缓存机制:
|
||||
|
||||
- `depth` 和 `bActiveInHierarchy` 由系统自动维护
|
||||
- 使用 `bCacheDirty` 标记优化更新
|
||||
- 层级变化时自动标记所有子级缓存为脏
|
||||
|
||||
### 最佳实践
|
||||
|
||||
1. **避免深层嵌套**:系统限制最大深度为 32 层
|
||||
2. **批量操作**:构建复杂层级时,尽量一次性设置好所有父子关系
|
||||
3. **按需添加**:只有真正需要层级关系的实体才添加 `HierarchyComponent`
|
||||
4. **缓存系统引用**:避免每次调用都获取 `HierarchySystem`
|
||||
|
||||
```typescript
|
||||
// 好的做法
|
||||
class MySystem extends EntitySystem {
|
||||
private hierarchySystem!: HierarchySystem;
|
||||
|
||||
onAddedToScene() {
|
||||
this.hierarchySystem = this.scene!.getEntityProcessor(HierarchySystem)!;
|
||||
}
|
||||
|
||||
process() {
|
||||
// 使用缓存的引用
|
||||
const parent = this.hierarchySystem.getParent(entity);
|
||||
}
|
||||
}
|
||||
|
||||
// 避免的做法
|
||||
process() {
|
||||
// 每次都获取,性能较差
|
||||
const system = this.scene!.getEntityProcessor(HierarchySystem);
|
||||
}
|
||||
```
|
||||
|
||||
## 迁移指南
|
||||
|
||||
如果你之前使用的是旧版 Entity 内置的层级 API,请参考以下迁移指南:
|
||||
|
||||
| 旧 API (已移除) | 新 API |
|
||||
|----------------|--------|
|
||||
| `entity.parent` | `hierarchySystem.getParent(entity)` |
|
||||
| `entity.children` | `hierarchySystem.getChildren(entity)` |
|
||||
| `entity.addChild(child)` | `hierarchySystem.setParent(child, entity)` |
|
||||
| `entity.removeChild(child)` | `hierarchySystem.removeChild(entity, child)` |
|
||||
| `entity.findChild(name)` | `hierarchySystem.findChild(entity, name)` |
|
||||
| `entity.activeInHierarchy` | `hierarchySystem.isActiveInHierarchy(entity)` |
|
||||
|
||||
### 迁移示例
|
||||
|
||||
```typescript
|
||||
// 旧代码
|
||||
const parent = scene.createEntity("Parent");
|
||||
const child = scene.createEntity("Child");
|
||||
parent.addChild(child);
|
||||
const found = parent.findChild("Child");
|
||||
|
||||
// 新代码
|
||||
const hierarchySystem = scene.getEntityProcessor(HierarchySystem);
|
||||
|
||||
const parent = scene.createEntity("Parent");
|
||||
const child = scene.createEntity("Child");
|
||||
hierarchySystem.setParent(child, parent);
|
||||
const found = hierarchySystem.findChild(parent, "Child");
|
||||
```
|
||||
|
||||
## 下一步
|
||||
|
||||
- 了解 [实体类](./entity.md) 的其他功能
|
||||
- 了解 [场景管理](./scene.md) 如何组织实体和系统
|
||||
- 了解 [组件系统](./component.md) 如何定义和使用组件
|
||||
@@ -13,6 +13,9 @@
|
||||
### [系统架构 (System)](./system.md)
|
||||
掌握系统的编写方法,实现游戏逻辑的处理。
|
||||
|
||||
### [实体查询与 Matcher](./entity-query.md)
|
||||
学习使用 Matcher 进行实体筛选和查询,掌握 `all`、`any`、`none`、`nothing` 等匹配条件。
|
||||
|
||||
### [场景管理 (Scene)](./scene.md)
|
||||
了解场景的生命周期、系统管理和实体容器功能。
|
||||
|
||||
|
||||
360
docs/guide/persistent-entity.md
Normal file
360
docs/guide/persistent-entity.md
Normal file
@@ -0,0 +1,360 @@
|
||||
# 持久化实体
|
||||
|
||||
> **版本**: v2.3.0+
|
||||
|
||||
持久化实体(Persistent Entity)是一种可以在场景切换时自动迁移到新场景的特殊实体。适用于需要跨场景保持状态的游戏对象,如玩家、游戏管理器、音频管理器等。
|
||||
|
||||
## 基本概念
|
||||
|
||||
在 ECS 框架中,实体有两种生命周期策略:
|
||||
|
||||
| 策略 | 说明 | 默认 |
|
||||
|-----|------|------|
|
||||
| `SceneLocal` | 场景本地实体,场景切换时销毁 | ✓ |
|
||||
| `Persistent` | 持久化实体,场景切换时自动迁移 | |
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 创建持久化实体
|
||||
|
||||
```typescript
|
||||
import { Scene } from '@esengine/ecs-framework';
|
||||
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 创建持久化玩家实体
|
||||
const player = this.createEntity('Player').setPersistent();
|
||||
player.addComponent(new Position(100, 200));
|
||||
player.addComponent(new PlayerData('Hero', 500));
|
||||
|
||||
// 创建普通敌人实体(场景切换时销毁)
|
||||
const enemy = this.createEntity('Enemy');
|
||||
enemy.addComponent(new Position(300, 200));
|
||||
enemy.addComponent(new EnemyAI());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 场景切换时的行为
|
||||
|
||||
```typescript
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始场景
|
||||
class Level1Scene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 玩家 - 持久化,会迁移到下一个场景
|
||||
const player = this.createEntity('Player').setPersistent();
|
||||
player.addComponent(new Position(0, 0));
|
||||
player.addComponent(new Health(100));
|
||||
|
||||
// 敌人 - 场景本地,切换时销毁
|
||||
const enemy = this.createEntity('Enemy');
|
||||
enemy.addComponent(new Position(100, 100));
|
||||
}
|
||||
}
|
||||
|
||||
// 目标场景
|
||||
class Level2Scene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 新的敌人
|
||||
const enemy = this.createEntity('Boss');
|
||||
enemy.addComponent(new Position(200, 200));
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
// 玩家已自动迁移到此场景
|
||||
const player = this.findEntity('Player');
|
||||
console.log(player !== null); // true
|
||||
|
||||
// 位置和血量数据完整保留
|
||||
const position = player?.getComponent(Position);
|
||||
const health = player?.getComponent(Health);
|
||||
console.log(position?.x, position?.y); // 0, 0
|
||||
console.log(health?.value); // 100
|
||||
}
|
||||
}
|
||||
|
||||
// 切换场景
|
||||
Core.create({ debug: true });
|
||||
Core.setScene(new Level1Scene());
|
||||
|
||||
// 稍后切换到 Level2
|
||||
Core.loadScene(new Level2Scene());
|
||||
// Player 实体自动迁移,Enemy 实体被销毁
|
||||
```
|
||||
|
||||
## API 参考
|
||||
|
||||
### Entity 方法
|
||||
|
||||
#### setPersistent()
|
||||
|
||||
将实体标记为持久化,场景切换时不会被销毁。
|
||||
|
||||
```typescript
|
||||
public setPersistent(): this
|
||||
```
|
||||
|
||||
**返回**: 返回实体本身,支持链式调用
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
const player = scene.createEntity('Player')
|
||||
.setPersistent();
|
||||
|
||||
player.addComponent(new Position(100, 200));
|
||||
```
|
||||
|
||||
#### setSceneLocal()
|
||||
|
||||
将实体恢复为场景本地策略(默认)。
|
||||
|
||||
```typescript
|
||||
public setSceneLocal(): this
|
||||
```
|
||||
|
||||
**返回**: 返回实体本身,支持链式调用
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
// 动态取消持久化
|
||||
player.setSceneLocal();
|
||||
```
|
||||
|
||||
#### isPersistent
|
||||
|
||||
检查实体是否为持久化实体。
|
||||
|
||||
```typescript
|
||||
public get isPersistent(): boolean
|
||||
```
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
if (entity.isPersistent) {
|
||||
console.log('这是持久化实体');
|
||||
}
|
||||
```
|
||||
|
||||
#### lifecyclePolicy
|
||||
|
||||
获取实体的生命周期策略。
|
||||
|
||||
```typescript
|
||||
public get lifecyclePolicy(): EEntityLifecyclePolicy
|
||||
```
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
import { EEntityLifecyclePolicy } from '@esengine/ecs-framework';
|
||||
|
||||
if (entity.lifecyclePolicy === EEntityLifecyclePolicy.Persistent) {
|
||||
console.log('持久化实体');
|
||||
}
|
||||
```
|
||||
|
||||
### Scene 方法
|
||||
|
||||
#### findPersistentEntities()
|
||||
|
||||
查找场景中所有持久化实体。
|
||||
|
||||
```typescript
|
||||
public findPersistentEntities(): Entity[]
|
||||
```
|
||||
|
||||
**返回**: 持久化实体数组
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
const persistentEntities = scene.findPersistentEntities();
|
||||
console.log(`场景中有 ${persistentEntities.length} 个持久化实体`);
|
||||
```
|
||||
|
||||
#### extractPersistentEntities()
|
||||
|
||||
提取并从场景中移除所有持久化实体(通常由框架内部调用)。
|
||||
|
||||
```typescript
|
||||
public extractPersistentEntities(): Entity[]
|
||||
```
|
||||
|
||||
**返回**: 被提取的持久化实体数组
|
||||
|
||||
#### receiveMigratedEntities()
|
||||
|
||||
接收迁移过来的实体(通常由框架内部调用)。
|
||||
|
||||
```typescript
|
||||
public receiveMigratedEntities(entities: Entity[]): void
|
||||
```
|
||||
|
||||
**参数**:
|
||||
- `entities` - 要接收的实体数组
|
||||
|
||||
## 使用场景
|
||||
|
||||
### 1. 玩家实体跨关卡
|
||||
|
||||
```typescript
|
||||
class PlayerSetupScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 玩家在所有关卡中保持状态
|
||||
const player = this.createEntity('Player').setPersistent();
|
||||
player.addComponent(new Transform(0, 0));
|
||||
player.addComponent(new Health(100));
|
||||
player.addComponent(new Inventory());
|
||||
player.addComponent(new PlayerStats());
|
||||
}
|
||||
}
|
||||
|
||||
class Level1 extends Scene { /* ... */ }
|
||||
class Level2 extends Scene { /* ... */ }
|
||||
class Level3 extends Scene { /* ... */ }
|
||||
|
||||
// 玩家实体会自动在所有关卡间迁移
|
||||
Core.setScene(new PlayerSetupScene());
|
||||
// ... 游戏进行
|
||||
Core.loadScene(new Level1());
|
||||
// ... 关卡完成
|
||||
Core.loadScene(new Level2());
|
||||
// 玩家数据(血量、物品栏、属性)完整保留
|
||||
```
|
||||
|
||||
### 2. 全局管理器
|
||||
|
||||
```typescript
|
||||
class BootstrapScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 音频管理器 - 跨场景保持
|
||||
const audioManager = this.createEntity('AudioManager').setPersistent();
|
||||
audioManager.addComponent(new AudioController());
|
||||
|
||||
// 成就管理器 - 跨场景保持
|
||||
const achievementManager = this.createEntity('AchievementManager').setPersistent();
|
||||
achievementManager.addComponent(new AchievementTracker());
|
||||
|
||||
// 游戏设置 - 跨场景保持
|
||||
const settings = this.createEntity('GameSettings').setPersistent();
|
||||
settings.addComponent(new SettingsData());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 动态切换持久化状态
|
||||
|
||||
```typescript
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 初始创建为普通实体
|
||||
const companion = this.createEntity('Companion');
|
||||
companion.addComponent(new Transform(0, 0));
|
||||
companion.addComponent(new CompanionAI());
|
||||
|
||||
// 监听招募事件
|
||||
this.eventSystem.on('companion:recruited', () => {
|
||||
// 招募后变为持久化实体
|
||||
companion.setPersistent();
|
||||
console.log('同伴已加入队伍,将跟随玩家跨场景');
|
||||
});
|
||||
|
||||
// 监听解散事件
|
||||
this.eventSystem.on('companion:dismissed', () => {
|
||||
// 解散后恢复为场景本地实体
|
||||
companion.setSceneLocal();
|
||||
console.log('同伴已离队,不再跨场景');
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 明确标识持久化实体
|
||||
|
||||
```typescript
|
||||
// 推荐:在创建时立即标记
|
||||
const player = this.createEntity('Player').setPersistent();
|
||||
|
||||
// 不推荐:创建后再标记(容易遗漏)
|
||||
const player = this.createEntity('Player');
|
||||
// ... 很多代码 ...
|
||||
player.setPersistent(); // 容易忘记
|
||||
```
|
||||
|
||||
### 2. 合理使用持久化
|
||||
|
||||
```typescript
|
||||
// ✓ 适合持久化的实体
|
||||
const player = this.createEntity('Player').setPersistent(); // 玩家
|
||||
const gameManager = this.createEntity('GameManager').setPersistent(); // 全局管理器
|
||||
const audioManager = this.createEntity('AudioManager').setPersistent(); // 音频系统
|
||||
|
||||
// ✗ 不应持久化的实体
|
||||
const bullet = this.createEntity('Bullet'); // 临时对象
|
||||
const enemy = this.createEntity('Enemy'); // 关卡特定敌人
|
||||
const particle = this.createEntity('Particle'); // 特效粒子
|
||||
```
|
||||
|
||||
### 3. 检查迁移后的实体
|
||||
|
||||
```typescript
|
||||
class NewScene extends Scene {
|
||||
public onStart(): void {
|
||||
// 检查预期的持久化实体是否存在
|
||||
const player = this.findEntity('Player');
|
||||
if (!player) {
|
||||
console.error('玩家实体未正确迁移!');
|
||||
// 处理错误情况
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 避免循环引用
|
||||
|
||||
```typescript
|
||||
// ✗ 避免:持久化实体引用场景本地实体
|
||||
class BadScene extends Scene {
|
||||
protected initialize(): void {
|
||||
const player = this.createEntity('Player').setPersistent();
|
||||
const enemy = this.createEntity('Enemy');
|
||||
|
||||
// 危险:player 持久化但 enemy 不是
|
||||
// 场景切换后 enemy 被销毁,引用失效
|
||||
player.addComponent(new TargetComponent(enemy));
|
||||
}
|
||||
}
|
||||
|
||||
// ✓ 推荐:使用 ID 引用或事件系统
|
||||
class GoodScene extends Scene {
|
||||
protected initialize(): void {
|
||||
const player = this.createEntity('Player').setPersistent();
|
||||
const enemy = this.createEntity('Enemy');
|
||||
|
||||
// 存储 ID 而非直接引用
|
||||
player.addComponent(new TargetComponent(enemy.id));
|
||||
|
||||
// 或使用事件系统通信
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **已销毁的实体不会迁移**:如果实体在场景切换前被销毁,即使标记为持久化也不会迁移。
|
||||
|
||||
2. **组件数据完整保留**:迁移时所有组件及其状态都会保留。
|
||||
|
||||
3. **场景引用会更新**:迁移后实体的 `scene` 属性会指向新场景。
|
||||
|
||||
4. **查询系统会更新**:迁移的实体会自动注册到新场景的查询系统中。
|
||||
|
||||
5. **延迟切换同样生效**:使用 `Core.loadScene()` 延迟切换时,持久化实体同样会迁移。
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [场景管理](./scene.md) - 了解场景的基本使用
|
||||
- [SceneManager](./scene-manager.md) - 了解场景切换
|
||||
- [WorldManager](./world-manager.md) - 了解多世界管理
|
||||
@@ -6,409 +6,198 @@
|
||||
|
||||
## 特性支持
|
||||
|
||||
- ✅ **Worker**: 支持(通过 `wx.createWorker` 创建,需要配置 game.json)
|
||||
- ❌ **SharedArrayBuffer**: 不支持
|
||||
- ❌ **Transferable Objects**: 不支持(只支持可序列化对象)
|
||||
- ✅ **高精度时间**: 使用 `Date.now()` 或 `wx.getPerformance()`
|
||||
- ✅ **设备信息**: 完整的微信小游戏设备信息
|
||||
| 特性 | 支持情况 | 说明 |
|
||||
|------|----------|------|
|
||||
| **Worker** | ✅ 支持 | 需要使用预编译文件,配置 `workerScriptPath` |
|
||||
| **SharedArrayBuffer** | ❌ 不支持 | 微信小游戏环境不支持 |
|
||||
| **Transferable Objects** | ❌ 不支持 | 只支持可序列化对象 |
|
||||
| **高精度时间** | ✅ 支持 | 使用 `wx.getPerformance()` |
|
||||
| **设备信息** | ✅ 支持 | 完整的微信小游戏设备信息 |
|
||||
|
||||
## 完整实现
|
||||
## WorkerEntitySystem 使用方式
|
||||
|
||||
```typescript
|
||||
import type {
|
||||
IPlatformAdapter,
|
||||
PlatformWorker,
|
||||
WorkerCreationOptions,
|
||||
PlatformConfig,
|
||||
WeChatDeviceInfo
|
||||
} from '@esengine/ecs-framework';
|
||||
### 重要:微信小游戏 Worker 限制
|
||||
|
||||
/**
|
||||
* 微信小游戏平台适配器
|
||||
* 支持微信小游戏环境
|
||||
*/
|
||||
export class WeChatMiniGameAdapter implements IPlatformAdapter {
|
||||
public readonly name = 'wechat-minigame';
|
||||
public readonly version: string;
|
||||
private systemInfo: any;
|
||||
微信小游戏的 Worker 有以下限制:
|
||||
- **Worker 脚本必须在代码包内**,不能动态生成
|
||||
- **必须在 `game.json` 中配置** `workers` 目录
|
||||
- **最多只能创建 1 个 Worker**
|
||||
|
||||
constructor() {
|
||||
// 获取微信小游戏版本信息
|
||||
this.systemInfo = this.getSystemInfo();
|
||||
this.version = this.systemInfo.version || 'unknown';
|
||||
}
|
||||
因此,使用 `WorkerEntitySystem` 时有两种方式:
|
||||
1. **推荐:使用 CLI 工具自动生成** Worker 文件
|
||||
2. 手动创建 Worker 文件
|
||||
|
||||
/**
|
||||
* 检查是否支持Worker
|
||||
*/
|
||||
public isWorkerSupported(): boolean {
|
||||
// 微信小游戏支持Worker,通过wx.createWorker创建
|
||||
return typeof wx !== 'undefined' && typeof wx.createWorker === 'function';
|
||||
}
|
||||
### 方式一:使用 CLI 工具自动生成(推荐)
|
||||
|
||||
/**
|
||||
* 检查是否支持SharedArrayBuffer(不支持)
|
||||
*/
|
||||
public isSharedArrayBufferSupported(): boolean {
|
||||
return false; // 微信小游戏不支持SharedArrayBuffer
|
||||
}
|
||||
我们提供了 `@esengine/worker-generator` 工具,可以自动从你的 TypeScript 代码中提取 `workerProcess` 函数并生成微信小游戏兼容的 Worker 文件。
|
||||
|
||||
/**
|
||||
* 获取硬件并发数
|
||||
*/
|
||||
public getHardwareConcurrency(): number {
|
||||
// 微信小游戏官方限制:最多只能创建 1 个 Worker
|
||||
return 1;
|
||||
}
|
||||
#### 安装
|
||||
|
||||
/**
|
||||
* 创建Worker
|
||||
* @param script 脚本内容或文件路径
|
||||
* @param options Worker创建选项
|
||||
*/
|
||||
public createWorker(script: string, options: WorkerCreationOptions = {}): PlatformWorker {
|
||||
if (!this.isWorkerSupported()) {
|
||||
throw new Error('微信小游戏不支持Worker');
|
||||
}
|
||||
```bash
|
||||
pnpm add -D @esengine/worker-generator
|
||||
# 或
|
||||
npm install --save-dev @esengine/worker-generator
|
||||
```
|
||||
|
||||
try {
|
||||
return new WeChatWorker(script, options);
|
||||
} catch (error) {
|
||||
throw new Error(`创建微信Worker失败: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
#### 使用
|
||||
|
||||
/**
|
||||
* 创建SharedArrayBuffer(不支持)
|
||||
*/
|
||||
public createSharedArrayBuffer(length: number): SharedArrayBuffer | null {
|
||||
return null; // 微信小游戏不支持SharedArrayBuffer
|
||||
}
|
||||
```bash
|
||||
# 扫描 src 目录,生成 Worker 文件到 workers 目录
|
||||
npx esengine-worker-gen --src ./src --out ./workers --wechat
|
||||
|
||||
/**
|
||||
* 获取高精度时间戳
|
||||
*/
|
||||
public getHighResTimestamp(): number {
|
||||
// 尝试使用微信的性能API,否则使用Date.now()
|
||||
if (typeof wx !== 'undefined' && wx.getPerformance) {
|
||||
const performance = wx.getPerformance();
|
||||
return performance.now();
|
||||
}
|
||||
return Date.now();
|
||||
}
|
||||
# 查看帮助
|
||||
npx esengine-worker-gen --help
|
||||
```
|
||||
|
||||
/**
|
||||
* 获取平台配置
|
||||
*/
|
||||
public getPlatformConfig(): PlatformConfig {
|
||||
return {
|
||||
maxWorkerCount: 1, // 微信小游戏最多支持 1 个 Worker
|
||||
supportsModuleWorker: false, // 不支持模块Worker
|
||||
supportsTransferableObjects: this.checkTransferableObjectsSupport(),
|
||||
maxSharedArrayBufferSize: 0,
|
||||
workerScriptPrefix: '',
|
||||
limitations: {
|
||||
noEval: true, // 微信小游戏限制eval使用
|
||||
requiresWorkerInit: false,
|
||||
memoryLimit: this.getMemoryLimit(),
|
||||
workerNotSupported: false,
|
||||
workerLimitations: [
|
||||
'最多只能创建 1 个 Worker',
|
||||
'创建新Worker前必须先调用 Worker.terminate()',
|
||||
'Worker脚本必须为项目内相对路径',
|
||||
'需要在 game.json 中配置 workers 路径',
|
||||
'使用 worker.onMessage() 而不是 self.onmessage',
|
||||
'需要基础库 1.9.90 及以上版本'
|
||||
]
|
||||
},
|
||||
extensions: {
|
||||
platform: 'wechat-minigame',
|
||||
systemInfo: this.systemInfo,
|
||||
appId: this.systemInfo.host?.appId || 'unknown'
|
||||
}
|
||||
};
|
||||
}
|
||||
#### 参数说明
|
||||
|
||||
/**
|
||||
* 获取微信小游戏设备信息
|
||||
*/
|
||||
public getDeviceInfo(): WeChatDeviceInfo {
|
||||
return {
|
||||
// 设备基础信息
|
||||
brand: this.systemInfo.brand,
|
||||
model: this.systemInfo.model,
|
||||
platform: this.systemInfo.platform,
|
||||
system: this.systemInfo.system,
|
||||
benchmarkLevel: this.systemInfo.benchmarkLevel,
|
||||
cpuType: this.systemInfo.cpuType,
|
||||
memorySize: this.systemInfo.memorySize,
|
||||
deviceAbi: this.systemInfo.deviceAbi,
|
||||
abi: this.systemInfo.abi,
|
||||
| 参数 | 说明 | 默认值 |
|
||||
|------|------|--------|
|
||||
| `-s, --src <dir>` | 源代码目录 | `./src` |
|
||||
| `-o, --out <dir>` | 输出目录 | `./workers` |
|
||||
| `-w, --wechat` | 生成微信小游戏兼容代码 | `false` |
|
||||
| `-m, --mapping` | 生成 worker-mapping.json | `true` |
|
||||
| `-t, --tsconfig <path>` | TypeScript 配置文件路径 | 自动查找 |
|
||||
| `-v, --verbose` | 详细输出 | `false` |
|
||||
|
||||
// 窗口信息
|
||||
screenWidth: this.systemInfo.screenWidth,
|
||||
screenHeight: this.systemInfo.screenHeight,
|
||||
screenTop: this.systemInfo.screenTop,
|
||||
windowWidth: this.systemInfo.windowWidth,
|
||||
windowHeight: this.systemInfo.windowHeight,
|
||||
pixelRatio: this.systemInfo.pixelRatio,
|
||||
statusBarHeight: this.systemInfo.statusBarHeight,
|
||||
safeArea: this.systemInfo.safeArea,
|
||||
#### 示例输出
|
||||
|
||||
// 应用信息
|
||||
version: this.systemInfo.version,
|
||||
language: this.systemInfo.language,
|
||||
theme: this.systemInfo.theme,
|
||||
SDKVersion: this.systemInfo.SDKVersion,
|
||||
enableDebug: this.systemInfo.enableDebug,
|
||||
fontSizeSetting: this.systemInfo.fontSizeSetting,
|
||||
host: this.systemInfo.host
|
||||
};
|
||||
}
|
||||
```
|
||||
🔧 ESEngine Worker Generator
|
||||
|
||||
/**
|
||||
* 异步获取完整的平台配置
|
||||
*/
|
||||
public async getPlatformConfigAsync(): Promise<PlatformConfig> {
|
||||
// 可以在这里添加异步获取设备性能信息的逻辑
|
||||
const baseConfig = this.getPlatformConfig();
|
||||
Source directory: /project/src
|
||||
Output directory: /project/workers
|
||||
WeChat mode: Yes
|
||||
|
||||
// 尝试获取设备性能信息
|
||||
try {
|
||||
const benchmarkLevel = await this.getBenchmarkLevel();
|
||||
baseConfig.extensions = {
|
||||
...baseConfig.extensions,
|
||||
benchmarkLevel
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn('获取性能基准失败:', error);
|
||||
}
|
||||
Scanning for WorkerEntitySystem classes...
|
||||
|
||||
return baseConfig;
|
||||
}
|
||||
✓ Found 1 WorkerEntitySystem class(es):
|
||||
- PhysicsSystem (src/systems/PhysicsSystem.ts)
|
||||
|
||||
/**
|
||||
* 检查是否支持Transferable Objects
|
||||
*/
|
||||
private checkTransferableObjectsSupport(): boolean {
|
||||
// 微信小游戏不支持 Transferable Objects
|
||||
// 基础库 2.20.2 之前只支持可序列化的 key-value 对象
|
||||
// 2.20.2 之后支持任意类型数据,但仍然不支持 Transferable Objects
|
||||
return false;
|
||||
}
|
||||
Generating Worker files...
|
||||
|
||||
/**
|
||||
* 获取系统信息
|
||||
*/
|
||||
private getSystemInfo(): any {
|
||||
if (typeof wx !== 'undefined' && wx.getSystemInfoSync) {
|
||||
try {
|
||||
return wx.getSystemInfoSync();
|
||||
} catch (error) {
|
||||
console.warn('获取微信系统信息失败:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
✓ Successfully generated 1 Worker file(s):
|
||||
- PhysicsSystem -> workers/physics-system-worker.js
|
||||
|
||||
/**
|
||||
* 获取内存限制
|
||||
*/
|
||||
private getMemoryLimit(): number {
|
||||
// 微信小游戏通常有内存限制
|
||||
const memorySize = this.systemInfo.memorySize;
|
||||
if (memorySize) {
|
||||
// 解析内存大小字符串(如 "4GB")
|
||||
const match = memorySize.match(/(\d+)([GM]B)?/i);
|
||||
if (match) {
|
||||
const value = parseInt(match[1], 10);
|
||||
const unit = match[2]?.toUpperCase();
|
||||
📝 Usage:
|
||||
1. Copy the generated files to your project's workers/ directory
|
||||
2. Configure game.json (WeChat): { "workers": "workers" }
|
||||
3. In your System constructor, add:
|
||||
workerScriptPath: 'workers/physics-system-worker.js'
|
||||
```
|
||||
|
||||
if (unit === 'GB') {
|
||||
return value * 1024 * 1024 * 1024;
|
||||
} else if (unit === 'MB') {
|
||||
return value * 1024 * 1024;
|
||||
}
|
||||
}
|
||||
}
|
||||
#### 在构建流程中集成
|
||||
|
||||
// 默认限制为512MB
|
||||
return 512 * 1024 * 1024;
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步获取设备性能基准
|
||||
*/
|
||||
private async getBenchmarkLevel(): Promise<number> {
|
||||
return new Promise((resolve) => {
|
||||
if (typeof wx !== 'undefined' && wx.getDeviceInfo) {
|
||||
wx.getDeviceInfo({
|
||||
success: (res: any) => {
|
||||
resolve(res.benchmarkLevel || 0);
|
||||
},
|
||||
fail: () => {
|
||||
resolve(0);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resolve(this.systemInfo.benchmarkLevel || 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信Worker封装
|
||||
*/
|
||||
class WeChatWorker implements PlatformWorker {
|
||||
private _state: 'running' | 'terminated' = 'running';
|
||||
private worker: any;
|
||||
private scriptPath: string;
|
||||
private isTemporaryFile: boolean = false;
|
||||
|
||||
constructor(script: string, options: WorkerCreationOptions = {}) {
|
||||
if (typeof wx === 'undefined' || typeof wx.createWorker !== 'function') {
|
||||
throw new Error('微信小游戏不支持Worker');
|
||||
}
|
||||
|
||||
try {
|
||||
// 判断 script 是文件路径还是脚本内容
|
||||
if (this.isFilePath(script)) {
|
||||
// 直接使用文件路径
|
||||
this.scriptPath = script;
|
||||
this.isTemporaryFile = false;
|
||||
this.worker = wx.createWorker(this.scriptPath, {
|
||||
useExperimentalWorker: true // 启用实验性Worker获得更好性能
|
||||
});
|
||||
} else {
|
||||
// 微信小游戏不支持动态脚本内容,只能使用文件路径
|
||||
// 将脚本内容写入文件系统
|
||||
this.scriptPath = this.writeScriptToFile(script, options.name);
|
||||
this.isTemporaryFile = true;
|
||||
this.worker = wx.createWorker(this.scriptPath, {
|
||||
useExperimentalWorker: true
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`创建微信Worker失败: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为文件路径
|
||||
*/
|
||||
private isFilePath(script: string): boolean {
|
||||
// 简单判断:如果包含 .js 后缀且不包含换行符或分号,认为是文件路径
|
||||
return script.endsWith('.js') &&
|
||||
!script.includes('\n') &&
|
||||
!script.includes(';') &&
|
||||
script.length < 200; // 文件路径通常不会太长
|
||||
}
|
||||
|
||||
/**
|
||||
* 将脚本内容写入文件系统
|
||||
* 注意:微信小游戏不支持blob URL,只能使用文件系统
|
||||
*/
|
||||
private writeScriptToFile(script: string, name?: string): string {
|
||||
const fs = wx.getFileSystemManager();
|
||||
const fileName = name ? `worker-${name}.js` : `worker-${Date.now()}.js`;
|
||||
const filePath = `${wx.env.USER_DATA_PATH}/${fileName}`;
|
||||
|
||||
try {
|
||||
fs.writeFileSync(filePath, script, 'utf8');
|
||||
return filePath;
|
||||
} catch (error) {
|
||||
throw new Error(`写入Worker脚本文件失败: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public get state(): 'running' | 'terminated' {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
public postMessage(message: any, transfer?: Transferable[]): void {
|
||||
if (this._state === 'terminated') {
|
||||
throw new Error('Worker已被终止');
|
||||
}
|
||||
|
||||
try {
|
||||
// 微信小游戏 Worker 只支持可序列化对象,忽略 transfer 参数
|
||||
this.worker.postMessage(message);
|
||||
} catch (error) {
|
||||
throw new Error(`发送消息到微信Worker失败: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public onMessage(handler: (event: { data: any }) => void): void {
|
||||
// 微信小游戏使用 onMessage 方法,不是 onmessage 属性
|
||||
this.worker.onMessage((res: any) => {
|
||||
handler({ data: res });
|
||||
});
|
||||
}
|
||||
|
||||
public onError(handler: (error: ErrorEvent) => void): void {
|
||||
// 注意:微信小游戏 Worker 的错误处理可能与标准不同
|
||||
if (this.worker.onError) {
|
||||
this.worker.onError(handler);
|
||||
}
|
||||
}
|
||||
|
||||
public terminate(): void {
|
||||
if (this._state === 'running') {
|
||||
try {
|
||||
this.worker.terminate();
|
||||
this._state = 'terminated';
|
||||
|
||||
// 清理临时脚本文件
|
||||
this.cleanupScriptFile();
|
||||
} catch (error) {
|
||||
console.error('终止微信Worker失败:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理临时脚本文件
|
||||
*/
|
||||
private cleanupScriptFile(): void {
|
||||
// 只清理临时创建的文件,不清理用户提供的文件路径
|
||||
if (this.scriptPath && this.isTemporaryFile) {
|
||||
try {
|
||||
const fs = wx.getFileSystemManager();
|
||||
fs.unlinkSync(this.scriptPath);
|
||||
} catch (error) {
|
||||
console.warn('清理Worker脚本文件失败:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
```json
|
||||
// package.json
|
||||
{
|
||||
"scripts": {
|
||||
"build:workers": "esengine-worker-gen --src ./src --out ./workers --wechat",
|
||||
"build": "pnpm build:workers && your-build-command"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
### 方式二:手动创建 Worker 文件
|
||||
|
||||
### 1. 复制代码
|
||||
如果你不想使用 CLI 工具,也可以手动创建 Worker 文件。
|
||||
|
||||
将上述代码复制到你的项目中,例如 `src/platform/WeChatMiniGameAdapter.ts`。
|
||||
在项目中创建 `workers/entity-worker.js`:
|
||||
|
||||
### 2. 注册适配器
|
||||
```javascript
|
||||
// workers/entity-worker.js
|
||||
// 微信小游戏 WorkerEntitySystem 通用 Worker 模板
|
||||
|
||||
```typescript
|
||||
import { PlatformManager } from '@esengine/ecs-framework';
|
||||
import { WeChatMiniGameAdapter } from './platform/WeChatMiniGameAdapter';
|
||||
let sharedFloatArray = null;
|
||||
|
||||
// 检查是否在微信小游戏环境
|
||||
if (typeof wx !== 'undefined') {
|
||||
const wechatAdapter = new WeChatMiniGameAdapter();
|
||||
PlatformManager.getInstance().registerAdapter(wechatAdapter);
|
||||
worker.onMessage(function(e) {
|
||||
const { type, id, entities, deltaTime, systemConfig, startIndex, endIndex, sharedBuffer } = e.data;
|
||||
|
||||
try {
|
||||
// 处理 SharedArrayBuffer 初始化
|
||||
if (type === 'init' && sharedBuffer) {
|
||||
sharedFloatArray = new Float32Array(sharedBuffer);
|
||||
worker.postMessage({ type: 'init', success: true });
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理 SharedArrayBuffer 数据
|
||||
if (type === 'shared' && sharedFloatArray) {
|
||||
processSharedArrayBuffer(startIndex, endIndex, deltaTime, systemConfig);
|
||||
worker.postMessage({ id, result: null });
|
||||
return;
|
||||
}
|
||||
|
||||
// 传统处理方式
|
||||
if (entities) {
|
||||
const result = workerProcess(entities, deltaTime, systemConfig);
|
||||
|
||||
// 处理 Promise 返回值
|
||||
if (result && typeof result.then === 'function') {
|
||||
result.then(function(finalResult) {
|
||||
worker.postMessage({ id, result: finalResult });
|
||||
}).catch(function(error) {
|
||||
worker.postMessage({ id, error: error.message });
|
||||
});
|
||||
} else {
|
||||
worker.postMessage({ id, result: result });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
worker.postMessage({ id, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 实体处理函数 - 根据你的业务逻辑修改此函数
|
||||
* @param {Array} entities - 实体数据数组
|
||||
* @param {number} deltaTime - 帧间隔时间
|
||||
* @param {Object} systemConfig - 系统配置
|
||||
* @returns {Array} 处理后的实体数据
|
||||
*/
|
||||
function workerProcess(entities, deltaTime, systemConfig) {
|
||||
// ====== 在这里编写你的处理逻辑 ======
|
||||
// 示例:物理计算
|
||||
return entities.map(function(entity) {
|
||||
// 应用重力
|
||||
entity.vy += (systemConfig.gravity || 100) * deltaTime;
|
||||
|
||||
// 更新位置
|
||||
entity.x += entity.vx * deltaTime;
|
||||
entity.y += entity.vy * deltaTime;
|
||||
|
||||
// 应用摩擦力
|
||||
entity.vx *= (systemConfig.friction || 0.95);
|
||||
entity.vy *= (systemConfig.friction || 0.95);
|
||||
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* SharedArrayBuffer 处理函数(可选)
|
||||
*/
|
||||
function processSharedArrayBuffer(startIndex, endIndex, deltaTime, systemConfig) {
|
||||
if (!sharedFloatArray) return;
|
||||
|
||||
// ====== 根据需要实现 SharedArrayBuffer 处理逻辑 ======
|
||||
// 注意:微信小游戏不支持 SharedArrayBuffer,此函数通常不会被调用
|
||||
}
|
||||
```
|
||||
|
||||
### 3. WorkerEntitySystem 使用方式
|
||||
### 步骤 2:配置 game.json
|
||||
|
||||
微信小游戏适配器与 WorkerEntitySystem 配合使用,自动处理 Worker 脚本创建:
|
||||
在 `game.json` 中添加 workers 配置:
|
||||
|
||||
#### 基本使用方式(推荐)
|
||||
```json
|
||||
{
|
||||
"deviceOrientation": "portrait",
|
||||
"showStatusBar": false,
|
||||
"workers": "workers"
|
||||
}
|
||||
```
|
||||
|
||||
### 步骤 3:使用 WorkerEntitySystem
|
||||
|
||||
```typescript
|
||||
import { WorkerEntitySystem, Matcher, Entity } from '@esengine/ecs-framework';
|
||||
@@ -426,13 +215,17 @@ class PhysicsSystem extends WorkerEntitySystem<PhysicsData> {
|
||||
constructor() {
|
||||
super(Matcher.all(Transform, Velocity), {
|
||||
enableWorker: true,
|
||||
workerCount: 1, // 微信小游戏限制只能创建1个Worker
|
||||
systemConfig: { gravity: 100, friction: 0.95 }
|
||||
workerCount: 1, // 微信小游戏限制只能创建 1 个 Worker
|
||||
workerScriptPath: 'workers/entity-worker.js', // 指定预编译的 Worker 文件
|
||||
systemConfig: {
|
||||
gravity: 100,
|
||||
friction: 0.95
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected getDefaultEntityDataSize(): number {
|
||||
return 6; // id, x, y, vx, vy, mass
|
||||
return 6;
|
||||
}
|
||||
|
||||
protected extractEntityData(entity: Entity): PhysicsData {
|
||||
@@ -450,20 +243,15 @@ class PhysicsSystem extends WorkerEntitySystem<PhysicsData> {
|
||||
};
|
||||
}
|
||||
|
||||
// WorkerEntitySystem 会自动将此函数序列化并写入临时文件
|
||||
// 注意:在微信小游戏中,此方法不会被使用
|
||||
// Worker 的处理逻辑在 workers/entity-worker.js 中的 workerProcess 函数里
|
||||
protected workerProcess(entities: PhysicsData[], deltaTime: number, config: any): PhysicsData[] {
|
||||
return entities.map(entity => {
|
||||
// 应用重力
|
||||
entity.vy += config.gravity * deltaTime;
|
||||
|
||||
// 更新位置
|
||||
entity.x += entity.vx * deltaTime;
|
||||
entity.y += entity.vy * deltaTime;
|
||||
|
||||
// 应用摩擦力
|
||||
entity.vx *= config.friction;
|
||||
entity.vy *= config.friction;
|
||||
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
@@ -477,201 +265,219 @@ class PhysicsSystem extends WorkerEntitySystem<PhysicsData> {
|
||||
velocity.x = result.vx;
|
||||
velocity.y = result.vy;
|
||||
}
|
||||
|
||||
// SharedArrayBuffer 相关方法(微信小游戏不支持,可省略)
|
||||
protected writeEntityToBuffer(data: PhysicsData, offset: number): void {}
|
||||
protected readEntityFromBuffer(offset: number): PhysicsData | null { return null; }
|
||||
}
|
||||
```
|
||||
|
||||
#### 使用预先创建的 Worker 文件(可选)
|
||||
### 临时禁用 Worker(降级到同步模式)
|
||||
|
||||
如果你希望使用预先创建的 Worker 文件:
|
||||
如果遇到问题,可以临时禁用 Worker:
|
||||
|
||||
```typescript
|
||||
// 1. 在 game.json 中配置 Worker 路径
|
||||
/*
|
||||
{
|
||||
"workers": "workers"
|
||||
class PhysicsSystem extends WorkerEntitySystem<PhysicsData> {
|
||||
constructor() {
|
||||
super(Matcher.all(Transform, Velocity), {
|
||||
enableWorker: false, // 禁用 Worker,使用主线程同步处理
|
||||
// ... 其他配置
|
||||
});
|
||||
}
|
||||
}
|
||||
*/
|
||||
```
|
||||
|
||||
// 2. 创建 workers/physics.js 文件
|
||||
// workers/physics.js 内容:
|
||||
/*
|
||||
// 微信小游戏 Worker 使用标准的 self.onmessage
|
||||
self.onmessage = function(e) {
|
||||
const { type, id, entities, deltaTime, systemConfig } = e.data;
|
||||
## 完整适配器实现
|
||||
|
||||
if (entities) {
|
||||
// 处理物理计算
|
||||
const results = entities.map(entity => {
|
||||
entity.vy += systemConfig.gravity * deltaTime;
|
||||
entity.x += entity.vx * deltaTime;
|
||||
entity.y += entity.vy * deltaTime;
|
||||
return entity;
|
||||
```typescript
|
||||
import type {
|
||||
IPlatformAdapter,
|
||||
PlatformWorker,
|
||||
WorkerCreationOptions,
|
||||
PlatformConfig
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* 微信小游戏平台适配器
|
||||
*/
|
||||
export class WeChatMiniGameAdapter implements IPlatformAdapter {
|
||||
public readonly name = 'wechat-minigame';
|
||||
public readonly version: string;
|
||||
private systemInfo: any;
|
||||
|
||||
constructor() {
|
||||
this.systemInfo = this.getSystemInfo();
|
||||
this.version = this.systemInfo.SDKVersion || 'unknown';
|
||||
}
|
||||
|
||||
public isWorkerSupported(): boolean {
|
||||
return typeof wx !== 'undefined' && typeof wx.createWorker === 'function';
|
||||
}
|
||||
|
||||
public isSharedArrayBufferSupported(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
public getHardwareConcurrency(): number {
|
||||
return 1; // 微信小游戏最多 1 个 Worker
|
||||
}
|
||||
|
||||
public createWorker(scriptPath: string, options: WorkerCreationOptions = {}): PlatformWorker {
|
||||
if (!this.isWorkerSupported()) {
|
||||
throw new Error('微信小游戏环境不支持 Worker');
|
||||
}
|
||||
|
||||
// scriptPath 必须是代码包内的文件路径
|
||||
const worker = wx.createWorker(scriptPath, {
|
||||
useExperimentalWorker: true
|
||||
});
|
||||
|
||||
self.postMessage({ id, result: results });
|
||||
return new WeChatWorker(worker);
|
||||
}
|
||||
};
|
||||
*/
|
||||
|
||||
// 3. 通过平台适配器直接创建(不推荐,WorkerEntitySystem会自动处理)
|
||||
const adapter = PlatformManager.getInstance().getAdapter();
|
||||
const worker = adapter.createWorker('workers/physics.js');
|
||||
public createSharedArrayBuffer(length: number): SharedArrayBuffer | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
public getHighResTimestamp(): number {
|
||||
if (typeof wx !== 'undefined' && wx.getPerformance) {
|
||||
return wx.getPerformance().now();
|
||||
}
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
public getPlatformConfig(): PlatformConfig {
|
||||
return {
|
||||
maxWorkerCount: 1,
|
||||
supportsModuleWorker: false,
|
||||
supportsTransferableObjects: false,
|
||||
maxSharedArrayBufferSize: 0,
|
||||
workerScriptPrefix: '',
|
||||
limitations: {
|
||||
noEval: true, // 重要:标记不支持动态脚本
|
||||
requiresWorkerInit: false,
|
||||
memoryLimit: 512 * 1024 * 1024,
|
||||
workerNotSupported: false,
|
||||
workerLimitations: [
|
||||
'最多只能创建 1 个 Worker',
|
||||
'Worker 脚本必须在代码包内',
|
||||
'需要在 game.json 中配置 workers 路径',
|
||||
'需要使用 workerScriptPath 配置'
|
||||
]
|
||||
},
|
||||
extensions: {
|
||||
platform: 'wechat-minigame',
|
||||
sdkVersion: this.systemInfo.SDKVersion
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private getSystemInfo(): any {
|
||||
if (typeof wx !== 'undefined' && wx.getSystemInfoSync) {
|
||||
try {
|
||||
return wx.getSystemInfoSync();
|
||||
} catch (error) {
|
||||
console.warn('获取微信系统信息失败:', error);
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信 Worker 封装
|
||||
*/
|
||||
class WeChatWorker implements PlatformWorker {
|
||||
private _state: 'running' | 'terminated' = 'running';
|
||||
private worker: any;
|
||||
|
||||
constructor(worker: any) {
|
||||
this.worker = worker;
|
||||
}
|
||||
|
||||
public get state(): 'running' | 'terminated' {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
public postMessage(message: any, transfer?: Transferable[]): void {
|
||||
if (this._state === 'terminated') {
|
||||
throw new Error('Worker 已被终止');
|
||||
}
|
||||
this.worker.postMessage(message);
|
||||
}
|
||||
|
||||
public onMessage(handler: (event: { data: any }) => void): void {
|
||||
this.worker.onMessage((res: any) => {
|
||||
handler({ data: res });
|
||||
});
|
||||
}
|
||||
|
||||
public onError(handler: (error: ErrorEvent) => void): void {
|
||||
if (this.worker.onError) {
|
||||
this.worker.onError(handler);
|
||||
}
|
||||
}
|
||||
|
||||
public terminate(): void {
|
||||
if (this._state === 'running') {
|
||||
this.worker.terminate();
|
||||
this._state = 'terminated';
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 获取设备信息
|
||||
## 注册适配器
|
||||
|
||||
```typescript
|
||||
const manager = PlatformManager.getInstance();
|
||||
if (manager.hasAdapter()) {
|
||||
const adapter = manager.getAdapter();
|
||||
console.log('微信设备信息:', adapter.getDeviceInfo());
|
||||
import { PlatformManager } from '@esengine/ecs-framework';
|
||||
import { WeChatMiniGameAdapter } from './platform/WeChatMiniGameAdapter';
|
||||
|
||||
// 在游戏启动时注册适配器
|
||||
if (typeof wx !== 'undefined') {
|
||||
const adapter = new WeChatMiniGameAdapter();
|
||||
PlatformManager.getInstance().registerAdapter(adapter);
|
||||
}
|
||||
```
|
||||
|
||||
## 官方文档参考
|
||||
|
||||
在使用微信小游戏 Worker 之前,建议先阅读官方文档:
|
||||
|
||||
- [wx.createWorker API](https://developers.weixin.qq.com/minigame/dev/api/worker/wx.createWorker.html)
|
||||
- [Worker.postMessage API](https://developers.weixin.qq.com/minigame/dev/api/worker/Worker.postMessage.html)
|
||||
- [Worker.onMessage API](https://developers.weixin.qq.com/minigame/dev/api/worker/Worker.onMessage.html)
|
||||
- [Worker.terminate API](https://developers.weixin.qq.com/minigame/dev/api/worker/Worker.terminate.html)
|
||||
|
||||
## 重要注意事项
|
||||
|
||||
### Worker 限制和配置
|
||||
### Worker 限制
|
||||
|
||||
微信小游戏的 Worker 有以下限制:
|
||||
|
||||
- **数量限制**: 最多只能创建 1 个 Worker
|
||||
- **版本要求**: 需要基础库 1.9.90 及以上版本
|
||||
- **脚本支持**: 不支持 blob URL,只能使用文件路径或写入文件系统
|
||||
- **文件路径**: Worker 脚本路径必须为绝对路径,但不能以 "/" 开头
|
||||
- **生命周期**: 创建新 Worker 前必须先调用 `Worker.terminate()` 终止当前 Worker
|
||||
- **消息处理**: Worker 内使用标准的 `self.onmessage`,主线程使用 `worker.onMessage()`
|
||||
- **实验性功能**: 支持 `useExperimentalWorker` 选项获得更好的 iOS 性能
|
||||
|
||||
#### Worker 配置(可选)
|
||||
|
||||
如果使用预先创建的 Worker 文件,需要在 `game.json` 中添加 workers 配置:
|
||||
|
||||
```json
|
||||
{
|
||||
"deviceOrientation": "portrait",
|
||||
"showStatusBar": false,
|
||||
"workers": "workers",
|
||||
"subpackages": []
|
||||
}
|
||||
```
|
||||
|
||||
**注意**: 使用 WorkerEntitySystem 时无需此配置,框架会自动将脚本写入临时文件。
|
||||
| 限制项 | 说明 |
|
||||
|--------|------|
|
||||
| 数量限制 | 最多只能创建 1 个 Worker |
|
||||
| 版本要求 | 需要基础库 1.9.90 及以上 |
|
||||
| 脚本位置 | 必须在代码包内,不支持动态生成 |
|
||||
| 生命周期 | 创建新 Worker 前必须先 terminate() |
|
||||
|
||||
### 内存限制
|
||||
|
||||
微信小游戏有严格的内存限制:
|
||||
|
||||
- 通常限制在 256MB - 512MB
|
||||
- 需要及时释放不用的资源
|
||||
- 避免内存泄漏
|
||||
|
||||
### API 限制
|
||||
|
||||
- 不支持 `eval()` 函数
|
||||
- 不支持 `Function` 构造器
|
||||
- DOM API 受限
|
||||
- 文件系统 API 受限
|
||||
|
||||
## 性能优化建议
|
||||
|
||||
### 1. 分帧处理
|
||||
- 建议监听内存警告:
|
||||
|
||||
```typescript
|
||||
class FramedProcessor {
|
||||
private tasks: (() => void)[] = [];
|
||||
private isProcessing = false;
|
||||
|
||||
public addTask(task: () => void): void {
|
||||
this.tasks.push(task);
|
||||
if (!this.isProcessing) {
|
||||
this.processNextFrame();
|
||||
}
|
||||
}
|
||||
|
||||
private processNextFrame(): void {
|
||||
this.isProcessing = true;
|
||||
const startTime = Date.now();
|
||||
const frameTime = 16; // 16ms per frame
|
||||
|
||||
while (this.tasks.length > 0 && Date.now() - startTime < frameTime) {
|
||||
const task = this.tasks.shift();
|
||||
if (task) task();
|
||||
}
|
||||
|
||||
if (this.tasks.length > 0) {
|
||||
setTimeout(() => this.processNextFrame(), 0);
|
||||
} else {
|
||||
this.isProcessing = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 内存管理
|
||||
|
||||
```typescript
|
||||
class MemoryManager {
|
||||
private static readonly MAX_MEMORY = 256 * 1024 * 1024; // 256MB
|
||||
|
||||
public static checkMemoryUsage(): void {
|
||||
if (typeof wx !== 'undefined' && wx.getPerformance) {
|
||||
const performance = wx.getPerformance();
|
||||
const memoryInfo = performance.getEntries().find(
|
||||
(entry: any) => entry.entryType === 'memory'
|
||||
);
|
||||
|
||||
if (memoryInfo && memoryInfo.usedJSHeapSize > this.MAX_MEMORY * 0.8) {
|
||||
console.warn('内存使用率过高,建议清理资源');
|
||||
// 触发垃圾回收或资源清理
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
wx.onMemoryWarning(() => {
|
||||
console.warn('收到内存警告,开始清理资源');
|
||||
// 清理不必要的资源
|
||||
});
|
||||
```
|
||||
|
||||
## 调试技巧
|
||||
|
||||
```typescript
|
||||
// 检查微信小游戏环境
|
||||
if (typeof wx !== 'undefined') {
|
||||
const adapter = new WeChatMiniGameAdapter();
|
||||
// 检查 Worker 配置
|
||||
const adapter = PlatformManager.getInstance().getAdapter();
|
||||
const config = adapter.getPlatformConfig();
|
||||
|
||||
console.log('微信版本:', adapter.version);
|
||||
console.log('设备信息:', adapter.getDeviceInfo());
|
||||
console.log('平台配置:', adapter.getPlatformConfig());
|
||||
|
||||
// 检查功能支持
|
||||
console.log('Worker支持:', adapter.isWorkerSupported());
|
||||
console.log('SharedArrayBuffer支持:', adapter.isSharedArrayBufferSupported());
|
||||
}
|
||||
console.log('Worker 支持:', adapter.isWorkerSupported());
|
||||
console.log('最大 Worker 数:', config.maxWorkerCount);
|
||||
console.log('平台限制:', config.limitations);
|
||||
```
|
||||
|
||||
## 微信小游戏特殊API
|
||||
|
||||
```typescript
|
||||
// 获取设备性能等级
|
||||
if (typeof wx !== 'undefined' && wx.getDeviceInfo) {
|
||||
wx.getDeviceInfo({
|
||||
success: (res) => {
|
||||
console.log('设备性能等级:', res.benchmarkLevel);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 监听内存警告
|
||||
if (typeof wx !== 'undefined' && wx.onMemoryWarning) {
|
||||
wx.onMemoryWarning(() => {
|
||||
console.warn('收到内存警告,开始清理资源');
|
||||
// 清理不必要的资源
|
||||
});
|
||||
}
|
||||
```
|
||||
@@ -1,4 +1,4 @@
|
||||
# SceneManager
|
||||
# SceneManager
|
||||
|
||||
SceneManager 是 ECS Framework 提供的轻量级场景管理器,适用于 95% 的游戏应用。它提供简单直观的 API,支持场景切换和延迟加载。
|
||||
|
||||
@@ -19,6 +19,7 @@ SceneManager 适合以下场景:
|
||||
- 自动管理 ECS 流式 API
|
||||
- 自动处理场景生命周期
|
||||
- 集成在 Core 中,自动更新
|
||||
- 支持[持久化实体](./persistent-entity.md)跨场景迁移(v2.3.0+)
|
||||
|
||||
## 基本使用
|
||||
|
||||
@@ -672,4 +673,9 @@ setTimeout(() => {
|
||||
}, 3000);
|
||||
```
|
||||
|
||||
SceneManager 为大多数游戏提供了简单而强大的场景管理能力。通过 Core 的静态方法,你可以轻松地管理场景切换。如果你需要更高级的多世界隔离功能,请参考 [WorldManager](./world-manager.md) 文档。
|
||||
SceneManager 为大多数游戏提供了简单而强大的场景管理能力。通过 Core 的静态方法,你可以轻松地管理场景切换。
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [持久化实体](./persistent-entity.md) - 了解如何让实体跨场景保持状态
|
||||
- [WorldManager](./world-manager.md) - 了解更高级的多世界隔离功能
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# 场景管理
|
||||
# 场景管理
|
||||
|
||||
在 ECS 架构中,场景(Scene)是游戏世界的容器,负责管理实体、系统和组件的生命周期。场景提供了完整的 ECS 运行环境。
|
||||
|
||||
@@ -657,5 +657,6 @@ world.setSceneActive('main', true);
|
||||
|
||||
- 了解 [SceneManager](./scene-manager.md) - 适用于大多数游戏的简单场景管理
|
||||
- 了解 [WorldManager](./world-manager.md) - 适用于需要多世界隔离的高级场景
|
||||
- 了解 [持久化实体](./persistent-entity.md) - 让实体跨场景保持状态(v2.3.0+)
|
||||
|
||||
场景是 ECS 框架的核心容器,正确使用场景管理能让你的游戏架构更加清晰、模块化和易于维护。
|
||||
|
||||
@@ -190,6 +190,106 @@ class CollectionsComponent extends Component {
|
||||
}
|
||||
```
|
||||
|
||||
### 组件继承与序列化
|
||||
|
||||
框架完整支持组件类的继承,子类会自动继承父类的序列化字段,同时可以添加自己的字段。
|
||||
|
||||
#### 基础继承
|
||||
|
||||
```typescript
|
||||
// 基类组件
|
||||
@ECSComponent('Collider2DBase')
|
||||
@Serializable({ version: 1, typeId: 'Collider2DBase' })
|
||||
abstract class Collider2DBase extends Component {
|
||||
@Serialize()
|
||||
public friction: number = 0.5;
|
||||
|
||||
@Serialize()
|
||||
public restitution: number = 0.0;
|
||||
|
||||
@Serialize()
|
||||
public isTrigger: boolean = false;
|
||||
}
|
||||
|
||||
// 子类组件 - 自动继承父类的序列化字段
|
||||
@ECSComponent('BoxCollider2D')
|
||||
@Serializable({ version: 1, typeId: 'BoxCollider2D' })
|
||||
class BoxCollider2DComponent extends Collider2DBase {
|
||||
@Serialize()
|
||||
public width: number = 1.0;
|
||||
|
||||
@Serialize()
|
||||
public height: number = 1.0;
|
||||
}
|
||||
|
||||
// 另一个子类组件
|
||||
@ECSComponent('CircleCollider2D')
|
||||
@Serializable({ version: 1, typeId: 'CircleCollider2D' })
|
||||
class CircleCollider2DComponent extends Collider2DBase {
|
||||
@Serialize()
|
||||
public radius: number = 0.5;
|
||||
}
|
||||
```
|
||||
|
||||
#### 继承规则
|
||||
|
||||
1. **字段继承**:子类自动继承父类所有被 `@Serialize()` 标记的字段
|
||||
2. **独立元数据**:每个子类维护独立的序列化元数据,修改子类不会影响父类或其他子类
|
||||
3. **typeId 区分**:使用 `typeId` 选项为每个类指定唯一标识,确保反序列化时能正确识别组件类型
|
||||
|
||||
#### 使用 typeId 的重要性
|
||||
|
||||
当使用组件继承时,**强烈建议**为每个类设置唯一的 `typeId`:
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:明确指定 typeId
|
||||
@Serializable({ version: 1, typeId: 'BoxCollider2D' })
|
||||
class BoxCollider2DComponent extends Collider2DBase { }
|
||||
|
||||
@Serializable({ version: 1, typeId: 'CircleCollider2D' })
|
||||
class CircleCollider2DComponent extends Collider2DBase { }
|
||||
|
||||
// ⚠️ 不推荐:依赖类名作为 typeId
|
||||
// 在代码压缩后类名可能变化,导致反序列化失败
|
||||
@Serializable({ version: 1 })
|
||||
class BoxCollider2DComponent extends Collider2DBase { }
|
||||
```
|
||||
|
||||
#### 子类覆盖父类字段
|
||||
|
||||
子类可以重新声明父类的字段以修改其序列化选项:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('SpecialCollider')
|
||||
@Serializable({ version: 1, typeId: 'SpecialCollider' })
|
||||
class SpecialColliderComponent extends Collider2DBase {
|
||||
// 覆盖父类字段,使用不同的别名
|
||||
@Serialize({ alias: 'fric' })
|
||||
public override friction: number = 0.8;
|
||||
|
||||
@Serialize()
|
||||
public specialProperty: string = '';
|
||||
}
|
||||
```
|
||||
|
||||
#### 忽略继承的字段
|
||||
|
||||
使用 `@IgnoreSerialization()` 可以在子类中忽略从父类继承的字段:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('TriggerOnly')
|
||||
@Serializable({ version: 1, typeId: 'TriggerOnly' })
|
||||
class TriggerOnlyCollider extends Collider2DBase {
|
||||
// 忽略父类的 friction 和 restitution 字段
|
||||
// 因为 Trigger 不需要物理材质属性
|
||||
@IgnoreSerialization()
|
||||
public override friction: number = 0;
|
||||
|
||||
@IgnoreSerialization()
|
||||
public override restitution: number = 0;
|
||||
}
|
||||
```
|
||||
|
||||
### 场景自定义数据
|
||||
|
||||
除了实体和组件,还可以序列化场景级别的配置数据:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# 系统架构
|
||||
# 系统架构
|
||||
|
||||
在 ECS 架构中,系统(System)是处理业务逻辑的地方。系统负责对拥有特定组件组合的实体执行操作,是 ECS 架构的逻辑处理单元。
|
||||
|
||||
@@ -157,8 +157,45 @@ const nameMatcher = Matcher.byName("Player"); // 匹配名称为 "Player" 的实
|
||||
|
||||
// 单组件匹配
|
||||
const componentMatcher = Matcher.byComponent(Health); // 匹配拥有 Health 组件的实体
|
||||
|
||||
// 不匹配任何实体
|
||||
const nothingMatcher = Matcher.nothing(); // 用于只需要生命周期回调的系统
|
||||
```
|
||||
|
||||
### 空匹配器 vs Nothing 匹配器
|
||||
|
||||
```typescript
|
||||
// empty() - 空条件,匹配所有实体
|
||||
const emptyMatcher = Matcher.empty();
|
||||
|
||||
// nothing() - 不匹配任何实体,用于只需要生命周期方法的系统
|
||||
const nothingMatcher = Matcher.nothing();
|
||||
|
||||
// 使用场景:只需要 onBegin/onEnd 生命周期的系统
|
||||
@ECSSystem('FrameTimer')
|
||||
class FrameTimerSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.nothing()); // 不处理任何实体
|
||||
}
|
||||
|
||||
protected onBegin(): void {
|
||||
// 每帧开始时执行,例如:记录帧开始时间
|
||||
console.log('帧开始');
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 永远不会被调用,因为没有匹配的实体
|
||||
}
|
||||
|
||||
protected onEnd(): void {
|
||||
// 每帧结束时执行
|
||||
console.log('帧结束');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> 💡 **提示**:更多关于 Matcher 和实体查询的详细用法,请参考 [实体查询系统](/guide/entity-query) 文档。
|
||||
|
||||
## 系统生命周期
|
||||
|
||||
系统提供了完整的生命周期回调:
|
||||
@@ -179,11 +216,13 @@ class ExampleSystem extends EntitySystem {
|
||||
// 主要的处理逻辑
|
||||
for (const entity of entities) {
|
||||
// 处理每个实体
|
||||
// ✅ 可以安全地在这里添加/移除组件,不会影响当前迭代
|
||||
}
|
||||
}
|
||||
|
||||
protected lateProcess(entities: readonly Entity[]): void {
|
||||
// 主处理之后的后期处理
|
||||
// ✅ 可以安全地在这里添加/移除组件,不会影响当前迭代
|
||||
}
|
||||
|
||||
protected onEnd(): void {
|
||||
@@ -233,6 +272,172 @@ class EnemyManagerSystem extends EntitySystem {
|
||||
}
|
||||
```
|
||||
|
||||
### 重要:onAdded/onRemoved 的调用时机
|
||||
|
||||
> ⚠️ **注意**:`onAdded` 和 `onRemoved` 回调是**同步调用**的,会在 `addComponent`/`removeComponent` 返回**之前**立即执行。
|
||||
|
||||
这意味着:
|
||||
|
||||
```typescript
|
||||
// ❌ 错误的用法:链式赋值在 onAdded 之后才执行
|
||||
const comp = entity.addComponent(new ClickComponent());
|
||||
comp.element = this._element; // 此时 onAdded 已经执行完了!
|
||||
|
||||
// ✅ 正确的用法:通过构造函数传入初始值
|
||||
const comp = entity.addComponent(new ClickComponent(this._element));
|
||||
|
||||
// ✅ 或者使用 createComponent 方法
|
||||
const comp = entity.createComponent(ClickComponent, this._element);
|
||||
```
|
||||
|
||||
**为什么这样设计?**
|
||||
|
||||
事件驱动设计确保 `onAdded`/`onRemoved` 回调不受系统注册顺序的影响。当组件被添加时,所有监听该组件的系统都会立即收到通知,而不是等到下一帧。
|
||||
|
||||
**最佳实践:**
|
||||
|
||||
1. 组件的初始值应该通过**构造函数**传入
|
||||
2. 不要依赖 `addComponent` 返回后再设置属性
|
||||
3. 如果需要在 `onAdded` 中访问组件属性,确保这些属性在构造时已经设置
|
||||
|
||||
### 在 process/lateProcess 中安全地修改组件
|
||||
|
||||
在 `process` 或 `lateProcess` 中迭代实体时,可以安全地添加或移除组件,不会影响当前的迭代过程:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Damage')
|
||||
class DamageSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Health, DamageReceiver));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health);
|
||||
const damage = entity.getComponent(DamageReceiver);
|
||||
|
||||
if (health && damage) {
|
||||
health.current -= damage.amount;
|
||||
|
||||
// ✅ 安全:移除组件不会影响当前迭代
|
||||
entity.removeComponent(damage);
|
||||
|
||||
if (health.current <= 0) {
|
||||
// ✅ 安全:添加组件也不会影响当前迭代
|
||||
entity.addComponent(new Dead());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
框架会在每次 `process`/`lateProcess` 调用前创建实体列表的快照,确保迭代过程中的组件变化不会导致跳过实体或重复处理。
|
||||
|
||||
## 命令缓冲区 (CommandBuffer)
|
||||
|
||||
> **v2.3.0+**
|
||||
|
||||
CommandBuffer 提供了一种延迟执行实体操作的机制。当你需要在迭代过程中销毁实体或进行其他可能影响迭代的操作时,使用 CommandBuffer 可以将这些操作推迟到帧末统一执行。
|
||||
|
||||
### 基本用法
|
||||
|
||||
每个 EntitySystem 都内置了 `commands` 属性:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Damage')
|
||||
class DamageSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Health, DamageReceiver));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health);
|
||||
const damage = entity.getComponent(DamageReceiver);
|
||||
|
||||
if (health && damage) {
|
||||
health.current -= damage.amount;
|
||||
|
||||
// 使用命令缓冲区延迟移除组件
|
||||
this.commands.removeComponent(entity, DamageReceiver);
|
||||
|
||||
if (health.current <= 0) {
|
||||
// 延迟添加死亡标记
|
||||
this.commands.addComponent(entity, new Dead());
|
||||
// 延迟销毁实体
|
||||
this.commands.destroyEntity(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 支持的命令
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `addComponent(entity, component)` | 延迟添加组件 |
|
||||
| `removeComponent(entity, ComponentType)` | 延迟移除组件 |
|
||||
| `destroyEntity(entity)` | 延迟销毁实体 |
|
||||
| `setEntityActive(entity, active)` | 延迟设置实体激活状态 |
|
||||
|
||||
### 执行时机
|
||||
|
||||
命令缓冲区中的命令会在每帧的 `lateUpdate` 阶段之后自动执行。执行顺序与命令入队顺序一致。
|
||||
|
||||
```
|
||||
场景更新流程:
|
||||
1. onBegin()
|
||||
2. process()
|
||||
3. lateProcess()
|
||||
4. onEnd()
|
||||
5. flushCommandBuffers() <-- 命令在这里执行
|
||||
```
|
||||
|
||||
### 使用场景
|
||||
|
||||
CommandBuffer 适用于以下场景:
|
||||
|
||||
1. **在迭代中销毁实体**:避免修改正在遍历的集合
|
||||
2. **批量延迟操作**:将多个操作合并到帧末执行
|
||||
3. **跨系统协调**:一个系统标记,另一个系统响应
|
||||
|
||||
```typescript
|
||||
// 示例:敌人死亡系统
|
||||
@ECSSystem('EnemyDeath')
|
||||
class EnemyDeathSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Enemy, Health));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health);
|
||||
if (health && health.current <= 0) {
|
||||
// 播放死亡动画、掉落物品等
|
||||
this.spawnLoot(entity);
|
||||
|
||||
// 延迟销毁,不影响当前迭代
|
||||
this.commands.destroyEntity(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private spawnLoot(entity: Entity): void {
|
||||
// 掉落物品逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 注意事项
|
||||
|
||||
- 命令会跳过已销毁的实体(安全检查)
|
||||
- 单个命令执行失败不会影响其他命令
|
||||
- 命令按入队顺序执行
|
||||
- 每次 `flush()` 后命令队列会清空
|
||||
|
||||
## 系统属性和方法
|
||||
|
||||
### 重要属性
|
||||
@@ -420,6 +625,8 @@ class GameScene extends Scene {
|
||||
|
||||
### 系统更新顺序
|
||||
|
||||
系统的执行顺序由 `updateOrder` 属性决定,数值越小越先执行:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Input')
|
||||
class InputSystem extends EntitySystem {
|
||||
@@ -446,6 +653,262 @@ class RenderSystem extends EntitySystem {
|
||||
}
|
||||
```
|
||||
|
||||
#### 稳定排序:addOrder
|
||||
|
||||
当多个系统的 `updateOrder` 相同时,框架使用 `addOrder`(添加顺序)作为第二排序条件,确保排序结果稳定可预测:
|
||||
|
||||
```typescript
|
||||
// 这两个系统 updateOrder 都是默认值 0
|
||||
@ECSSystem('SystemA')
|
||||
class SystemA extends EntitySystem { /* ... */ }
|
||||
|
||||
@ECSSystem('SystemB')
|
||||
class SystemB extends EntitySystem { /* ... */ }
|
||||
|
||||
// 添加顺序决定了执行顺序
|
||||
scene.addSystem(new SystemA()); // addOrder = 0,先执行
|
||||
scene.addSystem(new SystemB()); // addOrder = 1,后执行
|
||||
```
|
||||
|
||||
> **注意**:`addOrder` 由框架在 `addSystem` 时自动设置,无需手动管理。这确保了相同 `updateOrder` 的系统按照添加顺序执行,避免了排序不稳定导致的随机行为。
|
||||
|
||||
## 声明式系统调度
|
||||
|
||||
> **v2.4.0+**
|
||||
|
||||
除了使用 `updateOrder` 手动控制执行顺序外,框架还提供了声明式的系统调度机制,让你可以通过依赖关系来定义系统的执行顺序。
|
||||
|
||||
### 调度装饰器
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, ECSSystem, Stage, Before, After, InSet } from '@esengine/ecs-framework';
|
||||
|
||||
// 使用装饰器声明系统调度
|
||||
@ECSSystem('Movement')
|
||||
@Stage('update') // 在 update 阶段执行
|
||||
@After('InputSystem') // 在 InputSystem 之后执行
|
||||
@Before('RenderSystem') // 在 RenderSystem 之前执行
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Position, Velocity));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 移动逻辑
|
||||
}
|
||||
}
|
||||
|
||||
// 使用系统集合进行分组
|
||||
@ECSSystem('Physics')
|
||||
@Stage('update')
|
||||
@InSet('CoreSystems') // 属于 CoreSystems 集合
|
||||
class PhysicsSystem extends EntitySystem {
|
||||
// ...
|
||||
}
|
||||
|
||||
@ECSSystem('Collision')
|
||||
@Stage('update')
|
||||
@After('set:CoreSystems') // 在 CoreSystems 集合的所有系统之后执行
|
||||
class CollisionSystem extends EntitySystem {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 系统执行阶段
|
||||
|
||||
框架定义了以下系统执行阶段,按顺序执行:
|
||||
|
||||
| 阶段 | 说明 | 典型用途 |
|
||||
|------|------|----------|
|
||||
| `startup` | 启动阶段 | 一次性初始化 |
|
||||
| `preUpdate` | 更新前阶段 | 输入处理、状态准备 |
|
||||
| `update` | 主更新阶段(默认) | 核心游戏逻辑 |
|
||||
| `postUpdate` | 更新后阶段 | 物理、碰撞检测 |
|
||||
| `cleanup` | 清理阶段 | 资源清理、状态重置 |
|
||||
|
||||
### Fluent API 配置
|
||||
|
||||
如果不想使用装饰器,也可以使用 Fluent API 在运行时配置调度:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Movement')
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Position, Velocity));
|
||||
|
||||
// 使用 Fluent API 配置调度
|
||||
this.stage('update')
|
||||
.after('InputSystem')
|
||||
.before('RenderSystem')
|
||||
.inSet('CoreSystems');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 循环依赖检测
|
||||
|
||||
框架会自动检测循环依赖并抛出明确的错误:
|
||||
|
||||
```typescript
|
||||
// 这会导致循环依赖错误
|
||||
@ECSSystem('SystemA')
|
||||
@Before('SystemB')
|
||||
class SystemA extends EntitySystem { }
|
||||
|
||||
@ECSSystem('SystemB')
|
||||
@Before('SystemA') // 错误:A -> B -> A 形成循环
|
||||
class SystemB extends EntitySystem { }
|
||||
|
||||
// 错误信息:Cyclic dependency detected: SystemA -> SystemB -> SystemA
|
||||
```
|
||||
|
||||
## 帧级变更检测
|
||||
|
||||
> **v2.4.0+**
|
||||
|
||||
框架提供了基于 epoch 的帧级变更检测机制,让系统可以只处理发生变化的实体,大幅提升性能。
|
||||
|
||||
### 核心概念
|
||||
|
||||
- **Epoch**:全局帧计数器,每帧递增
|
||||
- **lastWriteEpoch**:组件最后被修改时的 epoch
|
||||
- **变更检测**:通过比较 epoch 判断组件是否在指定时间点后发生变化
|
||||
|
||||
### 标记组件为已修改
|
||||
|
||||
修改组件数据后,需要标记组件为已变更。有两种方式:
|
||||
|
||||
**方式 1:通过 Entity 辅助方法(推荐)**
|
||||
|
||||
```typescript
|
||||
// 修改组件后通过 entity.markDirty() 标记
|
||||
const pos = entity.getComponent(Position)!;
|
||||
pos.x = 100;
|
||||
pos.y = 200;
|
||||
entity.markDirty(pos);
|
||||
|
||||
// 可以同时标记多个组件
|
||||
const vel = entity.getComponent(Velocity)!;
|
||||
vel.vx = 10;
|
||||
entity.markDirty(pos, vel);
|
||||
```
|
||||
|
||||
**方式 2:在组件内部封装**
|
||||
|
||||
```typescript
|
||||
class VelocityComponent extends Component {
|
||||
private _vx: number = 0;
|
||||
private _vy: number = 0;
|
||||
|
||||
// 提供修改方法,接收 epoch 参数
|
||||
public setVelocity(vx: number, vy: number, epoch: number): void {
|
||||
this._vx = vx;
|
||||
this._vy = vy;
|
||||
this.markDirty(epoch);
|
||||
}
|
||||
|
||||
public get vx(): number { return this._vx; }
|
||||
public get vy(): number { return this._vy; }
|
||||
}
|
||||
|
||||
// 在系统中使用
|
||||
const vel = entity.getComponent(VelocityComponent)!;
|
||||
vel.setVelocity(10, 20, this.currentEpoch);
|
||||
```
|
||||
|
||||
### 在系统中使用变更检测
|
||||
|
||||
EntitySystem 提供了多个变更检测辅助方法:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Physics')
|
||||
class PhysicsSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Position, Velocity));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 方式1:使用 forEachChanged 只处理变更的实体
|
||||
// 自动保存 epoch 检查点
|
||||
this.forEachChanged(entities, [Velocity], (entity) => {
|
||||
const pos = this.requireComponent(entity, Position);
|
||||
const vel = this.requireComponent(entity, Velocity);
|
||||
|
||||
// 只有 Velocity 变化时才更新位置
|
||||
pos.x += vel.vx * Time.deltaTime;
|
||||
pos.y += vel.vy * Time.deltaTime;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ECSSystem('Transform')
|
||||
class TransformSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Transform, RigidBody));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 方式2:使用 filterChanged 获取变更的实体列表
|
||||
const changedEntities = this.filterChanged(entities, [RigidBody]);
|
||||
|
||||
for (const entity of changedEntities) {
|
||||
// 处理物理状态变化的实体
|
||||
this.updatePhysics(entity);
|
||||
}
|
||||
|
||||
// 手动保存 epoch 检查点
|
||||
this.saveEpoch();
|
||||
}
|
||||
|
||||
protected updatePhysics(entity: Entity): void {
|
||||
// 物理更新逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 变更检测 API 参考
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `forEachChanged(entities, [Types], callback)` | 遍历指定组件发生变更的实体,自动保存检查点 |
|
||||
| `filterChanged(entities, [Types])` | 返回指定组件发生变更的实体数组 |
|
||||
| `hasChanged(entity, [Types])` | 检查单个实体的指定组件是否发生变更 |
|
||||
| `saveEpoch()` | 手动保存当前 epoch 作为检查点 |
|
||||
| `lastProcessEpoch` | 获取上次保存的 epoch 检查点 |
|
||||
| `currentEpoch` | 获取当前场景的 epoch |
|
||||
|
||||
### 使用场景
|
||||
|
||||
变更检测特别适合以下场景:
|
||||
|
||||
1. **脏标记优化**:只在数据变化时更新渲染
|
||||
2. **物理同步**:只同步位置/速度发生变化的实体
|
||||
3. **网络同步**:只发送变化的组件数据
|
||||
4. **缓存失效**:只在依赖数据变化时重新计算
|
||||
|
||||
```typescript
|
||||
@ECSSystem('NetworkSync')
|
||||
class NetworkSyncSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(NetworkComponent, Transform));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 只同步变化的实体,大幅减少网络流量
|
||||
this.forEachChanged(entities, [Transform], (entity) => {
|
||||
const transform = this.requireComponent(entity, Transform);
|
||||
const network = this.requireComponent(entity, NetworkComponent);
|
||||
|
||||
this.sendTransformUpdate(network.id, transform);
|
||||
});
|
||||
}
|
||||
|
||||
private sendTransformUpdate(id: string, transform: Transform): void {
|
||||
// 发送网络更新
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 复杂系统示例
|
||||
|
||||
### 碰撞检测系统
|
||||
@@ -563,9 +1026,28 @@ class GameSystem extends EntitySystem {
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 使用装饰器
|
||||
### 2. 使用 @ECSSystem 装饰器
|
||||
|
||||
**必须使用 `@ECSSystem` 装饰器**:
|
||||
`@ECSSystem` 是系统类必须使用的装饰器,它为系统提供类型标识和元数据管理。
|
||||
|
||||
#### 为什么必须使用
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| **类型识别** | 提供稳定的系统名称,代码混淆后仍能正确识别 |
|
||||
| **调试支持** | 在性能监控、日志和调试工具中显示可读的系统名称 |
|
||||
| **系统管理** | 通过名称查找和管理系统 |
|
||||
| **序列化支持** | 场景序列化时可以记录系统配置 |
|
||||
|
||||
#### 基本语法
|
||||
|
||||
```typescript
|
||||
@ECSSystem(systemName: string)
|
||||
```
|
||||
|
||||
- `systemName`: 系统的名称,建议使用描述性的名称
|
||||
|
||||
#### 使用示例
|
||||
|
||||
```typescript
|
||||
// ✅ 正确的用法
|
||||
@@ -574,12 +1056,41 @@ class PhysicsSystem extends EntitySystem {
|
||||
// 系统实现
|
||||
}
|
||||
|
||||
// ✅ 推荐:使用描述性的名称
|
||||
@ECSSystem('PlayerMovement')
|
||||
class PlayerMovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Player, Position, Velocity));
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ 错误的用法 - 没有装饰器
|
||||
class BadSystem extends EntitySystem {
|
||||
// 这样定义的系统可能在生产环境出现问题
|
||||
// 这样定义的系统可能在生产环境出现问题:
|
||||
// 1. 代码压缩后类名变化,无法正确识别
|
||||
// 2. 性能监控和调试工具显示不正确的名称
|
||||
}
|
||||
```
|
||||
|
||||
#### 系统名称的作用
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Combat')
|
||||
class CombatSystem extends EntitySystem {
|
||||
protected onInitialize(): void {
|
||||
// 使用 systemName 属性访问系统名称
|
||||
console.log(`系统 ${this.systemName} 已初始化`); // 输出: 系统 Combat 已初始化
|
||||
}
|
||||
}
|
||||
|
||||
// 通过名称查找系统
|
||||
const combat = scene.getSystemByName('Combat');
|
||||
|
||||
// 性能监控中会显示系统名称
|
||||
const perfData = combatSystem.getPerformanceData();
|
||||
console.log(`${combatSystem.systemName} 执行时间: ${perfData?.executionTime}ms`);
|
||||
```
|
||||
|
||||
### 3. 合理的更新顺序
|
||||
|
||||
```typescript
|
||||
@@ -647,4 +1158,4 @@ class ResourceSystem extends EntitySystem {
|
||||
}
|
||||
```
|
||||
|
||||
系统是 ECS 架构的逻辑处理核心,正确设计和使用系统能让你的游戏代码更加模块化、高效和易于维护。
|
||||
系统是 ECS 架构的逻辑处理核心,正确设计和使用系统能让你的游戏代码更加模块化、高效和易于维护。
|
||||
|
||||
@@ -30,6 +30,64 @@ class GameSystem extends EntitySystem {
|
||||
}
|
||||
```
|
||||
|
||||
### 游戏暂停
|
||||
|
||||
框架提供两种暂停方式,适用于不同场景:
|
||||
|
||||
#### Core.paused(推荐)
|
||||
|
||||
`Core.paused` 是**真正的暂停**,设置后整个游戏循环停止:
|
||||
|
||||
```typescript
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
class PauseMenuSystem extends EntitySystem {
|
||||
public pauseGame(): void {
|
||||
// 真正暂停 - 所有系统停止执行
|
||||
Core.paused = true;
|
||||
console.log('游戏已暂停');
|
||||
}
|
||||
|
||||
public resumeGame(): void {
|
||||
// 恢复游戏
|
||||
Core.paused = false;
|
||||
console.log('游戏已恢复');
|
||||
}
|
||||
|
||||
public togglePause(): void {
|
||||
Core.paused = !Core.paused;
|
||||
console.log(Core.paused ? '游戏已暂停' : '游戏已恢复');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Time.timeScale = 0
|
||||
|
||||
`Time.timeScale = 0` 只是让 `deltaTime` 变为 0,**系统仍然在执行**:
|
||||
|
||||
```typescript
|
||||
class SlowMotionSystem extends EntitySystem {
|
||||
public freezeTime(): void {
|
||||
// 时间冻结 - 系统仍在执行,只是 deltaTime = 0
|
||||
Time.timeScale = 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 两种方式对比
|
||||
|
||||
| 特性 | `Core.paused = true` | `Time.timeScale = 0` |
|
||||
|------|---------------------|---------------------|
|
||||
| 系统执行 | ❌ 完全停止 | ✅ 仍在执行 |
|
||||
| CPU 开销 | 零 | 正常开销 |
|
||||
| Time 更新 | ❌ 停止 | ✅ 继续(deltaTime=0) |
|
||||
| 定时器 | ❌ 停止 | ✅ 继续(但时间不走) |
|
||||
| 适用场景 | 暂停菜单、游戏暂停 | 慢动作、时间冻结特效 |
|
||||
|
||||
**推荐**:
|
||||
- 暂停菜单、真正的游戏暂停 → 使用 `Core.paused = true`
|
||||
- 慢动作、子弹时间等特效 → 使用 `Time.timeScale`
|
||||
|
||||
### 时间缩放
|
||||
|
||||
Time 类支持时间缩放功能,可以实现慢动作、快进等效果:
|
||||
@@ -48,10 +106,10 @@ class TimeControlSystem extends EntitySystem {
|
||||
console.log('快进模式启用');
|
||||
}
|
||||
|
||||
public pauseGame(): void {
|
||||
// 暂停游戏(时间静止)
|
||||
Time.timeScale = 0;
|
||||
console.log('游戏暂停');
|
||||
public enableBulletTime(): void {
|
||||
// 子弹时间效果(10%速度)
|
||||
Time.timeScale = 0.1;
|
||||
console.log('子弹时间启用');
|
||||
}
|
||||
|
||||
public resumeNormalSpeed(): void {
|
||||
|
||||
@@ -145,6 +145,8 @@ interface WorkerSystemConfig {
|
||||
entityDataSize?: number;
|
||||
/** 最大实体数量(用于预分配SharedArrayBuffer) */
|
||||
maxEntities?: number;
|
||||
/** 预编译的Worker脚本路径(用于微信小游戏等不支持动态脚本的平台) */
|
||||
workerScriptPath?: string;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -605,4 +607,166 @@ public getPerformanceMetrics(): WorkerPerformanceMetrics {
|
||||
- SharedArrayBuffer优化
|
||||
- 大量实体的并行处理
|
||||
|
||||
## 微信小游戏支持
|
||||
|
||||
微信小游戏对 Worker 有特殊限制,不支持动态创建 Worker 脚本。ESEngine 提供了 `@esengine/worker-generator` CLI 工具来解决这个问题。
|
||||
|
||||
### 微信小游戏 Worker 限制
|
||||
|
||||
| 特性 | 浏览器 | 微信小游戏 |
|
||||
|------|--------|-----------|
|
||||
| 动态脚本 (Blob URL) | ✅ 支持 | ❌ 不支持 |
|
||||
| Worker 数量 | 多个 | 最多 1 个 |
|
||||
| 脚本来源 | 任意 | 必须是代码包内文件 |
|
||||
| SharedArrayBuffer | 需要 COOP/COEP | 有限支持 |
|
||||
|
||||
### 使用 Worker Generator CLI
|
||||
|
||||
#### 1. 安装工具
|
||||
|
||||
```bash
|
||||
pnpm add -D @esengine/worker-generator
|
||||
```
|
||||
|
||||
#### 2. 配置 workerScriptPath
|
||||
|
||||
在你的 WorkerEntitySystem 子类中配置 `workerScriptPath`:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Physics')
|
||||
class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsData> {
|
||||
constructor() {
|
||||
super(Matcher.all(Position, Velocity, Physics), {
|
||||
enableWorker: true,
|
||||
workerScriptPath: 'workers/physics-worker.js', // 指定 Worker 文件路径
|
||||
systemConfig: {
|
||||
gravity: 100,
|
||||
friction: 0.95
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected workerProcess(
|
||||
entities: PhysicsData[],
|
||||
deltaTime: number,
|
||||
config: any
|
||||
): PhysicsData[] {
|
||||
// 物理计算逻辑
|
||||
return entities.map(entity => {
|
||||
entity.vy += config.gravity * deltaTime;
|
||||
entity.x += entity.vx * deltaTime;
|
||||
entity.y += entity.vy * deltaTime;
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
// ... 其他方法
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. 生成 Worker 文件
|
||||
|
||||
运行 CLI 工具自动提取 `workerProcess` 函数并生成兼容微信小游戏的 Worker 文件:
|
||||
|
||||
```bash
|
||||
# 基本用法
|
||||
npx esengine-worker-gen --src ./src --wechat
|
||||
|
||||
# 完整选项
|
||||
npx esengine-worker-gen \
|
||||
--src ./src \ # 源码目录
|
||||
--wechat \ # 生成微信小游戏兼容代码
|
||||
--mapping \ # 生成 worker-mapping.json
|
||||
--verbose # 详细输出
|
||||
```
|
||||
|
||||
CLI 工具会:
|
||||
1. 扫描源码目录,找到所有 `WorkerEntitySystem` 子类
|
||||
2. 读取每个类的 `workerScriptPath` 配置
|
||||
3. 提取 `workerProcess` 方法体
|
||||
4. 转换为 ES5 语法(微信小游戏兼容)
|
||||
5. 生成到配置的路径
|
||||
|
||||
#### 4. 配置 game.json
|
||||
|
||||
在微信小游戏的 `game.json` 中配置 workers 目录:
|
||||
|
||||
```json
|
||||
{
|
||||
"deviceOrientation": "portrait",
|
||||
"workers": "workers"
|
||||
}
|
||||
```
|
||||
|
||||
#### 5. 项目结构
|
||||
|
||||
```
|
||||
your-game/
|
||||
├── game.js
|
||||
├── game.json # 配置 "workers": "workers"
|
||||
├── src/
|
||||
│ └── systems/
|
||||
│ └── PhysicsSystem.ts # workerScriptPath: 'workers/physics-worker.js'
|
||||
└── workers/
|
||||
├── physics-worker.js # 自动生成
|
||||
└── worker-mapping.json # 自动生成
|
||||
```
|
||||
|
||||
### 临时禁用 Worker
|
||||
|
||||
如果需要临时禁用 Worker(例如调试时),有两种方式:
|
||||
|
||||
#### 方式 1:配置禁用
|
||||
|
||||
```typescript
|
||||
constructor() {
|
||||
super(matcher, {
|
||||
enableWorker: false, // 禁用 Worker,使用主线程处理
|
||||
// ...
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
#### 方式 2:平台适配器禁用
|
||||
|
||||
在自定义平台适配器中返回不支持 Worker:
|
||||
|
||||
```typescript
|
||||
class MyPlatformAdapter implements IPlatformAdapter {
|
||||
isWorkerSupported(): boolean {
|
||||
return false; // 返回 false 禁用 Worker
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 注意事项
|
||||
|
||||
1. **每次修改 `workerProcess` 后都需要重新运行 CLI 工具**生成新的 Worker 文件
|
||||
|
||||
2. **Worker 函数必须是纯函数**,不能依赖 `this` 或外部变量:
|
||||
```typescript
|
||||
// ✅ 正确:只使用参数
|
||||
protected workerProcess(entities, deltaTime, config) {
|
||||
return entities.map(e => {
|
||||
e.y += config.gravity * deltaTime;
|
||||
return e;
|
||||
});
|
||||
}
|
||||
|
||||
// ❌ 错误:使用 this
|
||||
protected workerProcess(entities, deltaTime, config) {
|
||||
return entities.map(e => {
|
||||
e.y += this.gravity * deltaTime; // Worker 中无法访问 this
|
||||
return e;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
3. **配置数据通过 `systemConfig` 传递**,而不是类属性
|
||||
|
||||
4. **开发者工具中的警告可以忽略**:
|
||||
- `getNetworkType:fail not support` - 微信开发者工具内部行为
|
||||
- `SharedArrayBuffer will require cross-origin isolation` - 开发环境警告,真机不会出现
|
||||
|
||||
Worker系统为ECS框架提供了强大的并行计算能力,让你能够充分利用现代多核处理器的性能,为复杂的游戏逻辑和计算密集型任务提供了高效的解决方案。
|
||||
334
docs/index.md
334
docs/index.md
@@ -1,23 +1,317 @@
|
||||
---
|
||||
layout: home
|
||||
layout: page
|
||||
title: ESEngine - 高性能 TypeScript ECS 框架
|
||||
---
|
||||
|
||||
hero:
|
||||
name: "ECS Framework"
|
||||
text: "高性能ECS框架"
|
||||
tagline: "为Javascript游戏开发而设计"
|
||||
actions:
|
||||
- theme: brand
|
||||
text: 快速开始
|
||||
link: /guide/getting-started
|
||||
- theme: alt
|
||||
text: 查看示例
|
||||
link: https://github.com/esengine/lawn-mower-demo
|
||||
<ParticleHero />
|
||||
|
||||
features:
|
||||
- title: 高性能
|
||||
details: 支持大规模实体处理
|
||||
- title: 类型安全
|
||||
details: 完整的TypeScript支持,编译时类型检查
|
||||
- title: 模块化设计
|
||||
details: 核心功能独立打包,支持多平台
|
||||
---
|
||||
<section class="news-section">
|
||||
<div class="news-container">
|
||||
<div class="news-header">
|
||||
<h2 class="news-title">快速入口</h2>
|
||||
<a href="/guide/" class="news-more">查看文档</a>
|
||||
</div>
|
||||
<div class="news-grid">
|
||||
<a href="/guide/getting-started" class="news-card">
|
||||
<div class="news-card-image" style="background: linear-gradient(135deg, #1e3a5f 0%, #1e1e1e 100%);">
|
||||
<div class="news-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24"><path fill="#4fc1ff" d="M12 3L1 9l4 2.18v6L12 21l7-3.82v-6l2-1.09V17h2V9zm6.82 6L12 12.72L5.18 9L12 5.28zM17 16l-5 2.72L7 16v-3.73L12 15l5-2.73z"/></svg>
|
||||
</div>
|
||||
<span class="news-badge">快速开始</span>
|
||||
</div>
|
||||
<div class="news-card-content">
|
||||
<h3>5 分钟上手 ESEngine</h3>
|
||||
<p>从安装到创建第一个 ECS 应用,快速了解核心概念。</p>
|
||||
</div>
|
||||
</a>
|
||||
<a href="/guide/behavior-tree/" class="news-card">
|
||||
<div class="news-card-image" style="background: linear-gradient(135deg, #1e3a5f 0%, #1e1e1e 100%);">
|
||||
<div class="news-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24"><path fill="#4ec9b0" d="M12 2a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m3 20h-1v-7l-2-2l-2 2v7H9v-7.5l-2 2V22H6v-6l3-3l1-3.5c-.3.4-.6.7-1 1L6 9v1H4V8l5-3c.5-.3 1.1-.5 1.7-.5H11c.6 0 1.2.2 1.7.5l5 3v2h-2V9l-3 1.5c-.4-.3-.7-.6-1-1l1 3.5l3 3v6Z"/></svg>
|
||||
</div>
|
||||
<span class="news-badge">AI 系统</span>
|
||||
</div>
|
||||
<div class="news-card-content">
|
||||
<h3>行为树可视化编辑器</h3>
|
||||
<p>内置 AI 行为树系统,支持可视化编辑和实时调试。</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="features-section">
|
||||
<div class="features-container">
|
||||
<h2 class="features-title">核心特性</h2>
|
||||
<div class="features-grid">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#4fc1ff" d="M13 2.05v2.02c3.95.49 7 3.85 7 7.93c0 1.45-.39 2.79-1.06 3.95l1.59 1.09A9.94 9.94 0 0 0 22 12c0-5.18-3.95-9.45-9-9.95M12 19c-3.87 0-7-3.13-7-7c0-3.53 2.61-6.43 6-6.92V2.05c-5.06.5-9 4.76-9 9.95c0 5.52 4.47 10 9.99 10c3.31 0 6.24-1.61 8.06-4.09l-1.6-1.1A7.93 7.93 0 0 1 12 19"/><path fill="#4fc1ff" d="M12 6a6 6 0 0 0-6 6c0 3.31 2.69 6 6 6a6 6 0 0 0 0-12m0 10c-2.21 0-4-1.79-4-4s1.79-4 4-4s4 1.79 4 4s-1.79 4-4 4"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">高性能 ECS 架构</h3>
|
||||
<p class="feature-desc">基于数据驱动的实体组件系统,支持大规模实体处理,缓存友好的内存布局。</p>
|
||||
<a href="/guide/entity" class="feature-link">了解更多 →</a>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#569cd6" d="M3 3h18v18H3zm16.525 13.707c0-.795-.272-1.425-.816-1.89c-.544-.465-1.404-.804-2.58-1.016l-1.704-.296c-.616-.104-1.052-.26-1.308-.468c-.256-.21-.384-.468-.384-.776c0-.392.168-.7.504-.924c.336-.224.8-.336 1.392-.336c.56 0 1.008.124 1.344.372c.336.248.536.584.6 1.008h2.016c-.08-.96-.464-1.716-1.152-2.268c-.688-.552-1.6-.828-2.736-.828c-1.2 0-2.148.3-2.844.9c-.696.6-1.044 1.38-1.044 2.34c0 .76.252 1.368.756 1.824c.504.456 1.308.792 2.412.996l1.704.312c.624.12 1.068.28 1.332.48c.264.2.396.46.396.78c0 .424-.192.756-.576.996c-.384.24-.9.36-1.548.36c-.672 0-1.2-.14-1.584-.42c-.384-.28-.608-.668-.672-1.164H8.868c.048 1.016.46 1.808 1.236 2.376c.776.568 1.796.852 3.06.852c1.24 0 2.22-.292 2.94-.876c.72-.584 1.08-1.364 1.08-2.34z"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">完整类型支持</h3>
|
||||
<p class="feature-desc">100% TypeScript 编写,完整的类型定义和编译时检查,提供最佳的开发体验。</p>
|
||||
<a href="/guide/component" class="feature-link">了解更多 →</a>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#4ec9b0" d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10s10-4.5 10-10S17.5 2 12 2m0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8s8 3.59 8 8s-3.59 8-8 8m-5-8l4-4v3h4v2h-4v3z"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">可视化行为树</h3>
|
||||
<p class="feature-desc">内置 AI 行为树系统,提供可视化编辑器,支持自定义节点和实时调试。</p>
|
||||
<a href="/guide/behavior-tree/" class="feature-link">了解更多 →</a>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#c586c0" d="M4 6h18V4H4c-1.1 0-2 .9-2 2v11H0v3h14v-3H4zm19 2h-6c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h6c.55 0 1-.45 1-1V9c0-.55-.45-1-1-1m-1 9h-4v-7h4z"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">多平台支持</h3>
|
||||
<p class="feature-desc">支持浏览器、Node.js、微信小游戏等多平台,可与主流游戏引擎无缝集成。</p>
|
||||
<a href="/guide/platform-adapter" class="feature-link">了解更多 →</a>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#dcdcaa" d="M4 3h6v2H4v14h6v2H4c-1.1 0-2-.9-2-2V5c0-1.1.9-2 2-2m9 0h6c1.1 0 2 .9 2 2v14c0 1.1-.9 2-2 2h-6v-2h6V5h-6zm-1 7h4v2h-4z"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">模块化设计</h3>
|
||||
<p class="feature-desc">核心功能独立打包,按需引入。支持自定义插件扩展,灵活适配不同项目。</p>
|
||||
<a href="/guide/plugin-system" class="feature-link">了解更多 →</a>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#9cdcfe" d="M22.7 19l-9.1-9.1c.9-2.3.4-5-1.5-6.9c-2-2-5-2.4-7.4-1.3L9 6L6 9L1.6 4.7C.4 7.1.9 10.1 2.9 12.1c1.9 1.9 4.6 2.4 6.9 1.5l9.1 9.1c.4.4 1 .4 1.4 0l2.3-2.3c.5-.4.5-1.1.1-1.4"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">开发者工具</h3>
|
||||
<p class="feature-desc">内置性能监控、调试工具、序列化系统等,提供完整的开发工具链。</p>
|
||||
<a href="/guide/logging" class="feature-link">了解更多 →</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style scoped>
|
||||
/* 首页专用样式 | Home page specific styles */
|
||||
.news-section {
|
||||
background: #0d0d0d;
|
||||
padding: 64px 0;
|
||||
border-top: 1px solid #2a2a2a;
|
||||
}
|
||||
|
||||
.news-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 48px;
|
||||
}
|
||||
|
||||
.news-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.news-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.news-more {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 20px;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 6px;
|
||||
color: #a0a0a0;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.news-more:hover {
|
||||
background: #252525;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.news-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.news-card {
|
||||
display: flex;
|
||||
background: #1f1f1f;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.news-card:hover {
|
||||
border-color: #3b9eff;
|
||||
}
|
||||
|
||||
.news-card-image {
|
||||
width: 200px;
|
||||
min-height: 140px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.news-icon {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.news-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
background: transparent;
|
||||
border: 1px solid #3a3a3a;
|
||||
border-radius: 16px;
|
||||
color: #a0a0a0;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.news-card-content {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.news-card-content h3 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.news-card-content p {
|
||||
font-size: 0.875rem;
|
||||
color: #707070;
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.features-section {
|
||||
background: #0d0d0d;
|
||||
padding: 64px 0;
|
||||
}
|
||||
|
||||
.features-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 48px;
|
||||
}
|
||||
|
||||
.features-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
margin: 0 0 32px 0;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: #1f1f1f;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
border-color: #3b9eff;
|
||||
background: #252525;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: #0d0d0d;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.feature-desc {
|
||||
font-size: 14px;
|
||||
color: #707070;
|
||||
line-height: 1.7;
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
|
||||
.feature-link {
|
||||
font-size: 14px;
|
||||
color: #3b9eff;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.feature-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.news-container,
|
||||
.features-container {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.news-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.news-card {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.news-card-image {
|
||||
width: 100%;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
1
docs/public/CNAME
Normal file
1
docs/public/CNAME
Normal file
@@ -0,0 +1 @@
|
||||
esengine.cn
|
||||
45
docs/public/logo.svg
Normal file
45
docs/public/logo.svg
Normal file
@@ -0,0 +1,45 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<defs>
|
||||
<!-- Dark gradient background -->
|
||||
<linearGradient id="bgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#2d2d2d"/>
|
||||
<stop offset="100%" style="stop-color:#1a1a1a"/>
|
||||
</linearGradient>
|
||||
|
||||
<!-- Clean white text -->
|
||||
<linearGradient id="whiteGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#ffffff"/>
|
||||
<stop offset="100%" style="stop-color:#e8e8e8"/>
|
||||
</linearGradient>
|
||||
|
||||
<!-- Subtle inner shadow -->
|
||||
<filter id="innerShadow">
|
||||
<feOffset dx="0" dy="2"/>
|
||||
<feGaussianBlur stdDeviation="1" result="offset-blur"/>
|
||||
<feComposite operator="out" in="SourceGraphic" in2="offset-blur" result="inverse"/>
|
||||
<feFlood flood-color="black" flood-opacity="0.2" result="color"/>
|
||||
<feComposite operator="in" in="color" in2="inverse" result="shadow"/>
|
||||
<feComposite operator="over" in="shadow" in2="SourceGraphic"/>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- Background -->
|
||||
<rect width="512" height="512" fill="url(#bgGrad)"/>
|
||||
|
||||
<!-- Subtle border -->
|
||||
<rect x="1" y="1" width="510" height="510" fill="none" stroke="#3d3d3d" stroke-width="2"/>
|
||||
|
||||
<!-- ES Text -->
|
||||
<g filter="url(#innerShadow)">
|
||||
<!-- E -->
|
||||
<polygon points="72,120 72,392 240,392 240,340 140,340 140,282 220,282 220,230 140,230 140,172 240,172 240,120"
|
||||
fill="url(#whiteGrad)"/>
|
||||
|
||||
<!-- S -->
|
||||
<path d="M 280 172 Q 280 120 340 120 L 420 120 Q 450 120 450 160 L 450 186 L 398 186 L 398 168 Q 398 158 384 158 L 350 158 Q 320 158 320 188 Q 320 218 350 218 L 400 218 Q 450 218 450 274 L 450 332 Q 450 392 390 392 L 310 392 Q 270 392 270 340 L 270 314 L 322 314 L 322 340 Q 322 354 340 354 L 384 354 Q 404 354 404 324 L 404 290 Q 404 260 380 260 L 330 260 Q 280 260 280 208 Z"
|
||||
fill="url(#whiteGrad)"/>
|
||||
</g>
|
||||
|
||||
<!-- Accent line -->
|
||||
<rect x="72" y="424" width="368" height="4" fill="#ffffff" opacity="0.6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -123,7 +123,9 @@ class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsEntityData> {
|
||||
enableWorker,
|
||||
workerCount: isSharedArrayBufferAvailable ? (navigator.hardwareConcurrency || 2) : 1,
|
||||
systemConfig: defaultConfig,
|
||||
useSharedArrayBuffer: true
|
||||
useSharedArrayBuffer: true,
|
||||
// 微信小游戏等平台需要配置此路径,CLI 工具会根据此路径生成 Worker 文件
|
||||
workerScriptPath: 'workers/physics-worker.js'
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
277
examples/core-demos/workers/physics-worker.js
Normal file
277
examples/core-demos/workers/physics-worker.js
Normal file
@@ -0,0 +1,277 @@
|
||||
/**
|
||||
* Auto-generated Worker file for PhysicsWorkerSystem
|
||||
* 自动生成的 Worker 文件
|
||||
*
|
||||
* Source: F:/ecs-framework/examples/core-demos/src/demos/WorkerSystemDemo.ts
|
||||
* Generated by @esengine/worker-generator
|
||||
*
|
||||
* 使用方式 | Usage:
|
||||
* 1. 将此文件放入 workers/ 目录
|
||||
* 2. 在 game.json 中配置 "workers": "workers"
|
||||
* 3. 在 System 中配置 workerScriptPath: 'workers/physics-worker-system-worker.js'
|
||||
*/
|
||||
|
||||
// 微信小游戏 Worker 环境
|
||||
// WeChat Mini Game Worker environment
|
||||
let sharedFloatArray = null;
|
||||
const ENTITY_DATA_SIZE = 9;
|
||||
|
||||
worker.onMessage(function(res) {
|
||||
// 微信小游戏 Worker 消息直接传递数据,不需要 .data
|
||||
// WeChat Mini Game Worker passes data directly, no .data wrapper
|
||||
var type = res.type;
|
||||
var id = res.id;
|
||||
var entities = res.entities;
|
||||
var deltaTime = res.deltaTime;
|
||||
var systemConfig = res.systemConfig;
|
||||
var startIndex = res.startIndex;
|
||||
var endIndex = res.endIndex;
|
||||
var sharedBuffer = res.sharedBuffer;
|
||||
|
||||
try {
|
||||
// 处理 SharedArrayBuffer 初始化
|
||||
// Handle SharedArrayBuffer initialization
|
||||
if (type === 'init' && sharedBuffer) {
|
||||
sharedFloatArray = new Float32Array(sharedBuffer);
|
||||
worker.postMessage({ type: 'init', success: true });
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理 SharedArrayBuffer 数据
|
||||
// Handle SharedArrayBuffer data
|
||||
if (type === 'shared' && sharedFloatArray) {
|
||||
processSharedArrayBuffer(startIndex, endIndex, deltaTime, systemConfig);
|
||||
worker.postMessage({ id: id, result: null });
|
||||
return;
|
||||
}
|
||||
|
||||
// 传统处理方式
|
||||
// Traditional processing
|
||||
if (entities) {
|
||||
var result = workerProcess(entities, deltaTime, systemConfig);
|
||||
|
||||
// 处理 Promise 返回值
|
||||
// Handle Promise return value
|
||||
if (result && typeof result.then === 'function') {
|
||||
result.then(function(finalResult) {
|
||||
worker.postMessage({ id: id, result: finalResult });
|
||||
}).catch(function(error) {
|
||||
worker.postMessage({ id: id, error: error.message });
|
||||
});
|
||||
} else {
|
||||
worker.postMessage({ id: id, result: result });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
worker.postMessage({ id: id, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 实体处理函数 - 从 PhysicsWorkerSystem.workerProcess 提取
|
||||
* Entity processing function - extracted from PhysicsWorkerSystem.workerProcess
|
||||
*/
|
||||
function workerProcess(entities, deltaTime, systemConfig) {
|
||||
var __assign = (this && this.__assign) || function () {
|
||||
__assign = Object.assign || function(t) {
|
||||
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
||||
s = arguments[i];
|
||||
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
||||
t[p] = s[p];
|
||||
}
|
||||
return t;
|
||||
};
|
||||
return __assign.apply(this, arguments);
|
||||
};
|
||||
var config = systemConfig || this.physicsConfig;
|
||||
var result = entities.map(function (e) { return (__assign({}, e)); });
|
||||
for (var i = 0; i < result.length; i++) {
|
||||
var entity = result[i];
|
||||
entity.dy += config.gravity * deltaTime;
|
||||
entity.x += entity.dx * deltaTime;
|
||||
entity.y += entity.dy * deltaTime;
|
||||
if (entity.x <= entity.radius) {
|
||||
entity.x = entity.radius;
|
||||
entity.dx = -entity.dx * entity.bounce;
|
||||
}
|
||||
else if (entity.x >= config.canvasWidth - entity.radius) {
|
||||
entity.x = config.canvasWidth - entity.radius;
|
||||
entity.dx = -entity.dx * entity.bounce;
|
||||
}
|
||||
if (entity.y <= entity.radius) {
|
||||
entity.y = entity.radius;
|
||||
entity.dy = -entity.dy * entity.bounce;
|
||||
}
|
||||
else if (entity.y >= config.canvasHeight - entity.radius) {
|
||||
entity.y = config.canvasHeight - entity.radius;
|
||||
entity.dy = -entity.dy * entity.bounce;
|
||||
entity.dx *= config.groundFriction;
|
||||
}
|
||||
entity.dx *= entity.friction;
|
||||
entity.dy *= entity.friction;
|
||||
}
|
||||
for (var i = 0; i < result.length; i++) {
|
||||
for (var j = i + 1; j < result.length; j++) {
|
||||
var ball1 = result[i];
|
||||
var ball2 = result[j];
|
||||
var dx = ball2.x - ball1.x;
|
||||
var dy = ball2.y - ball1.y;
|
||||
var distance = Math.sqrt(dx * dx + dy * dy);
|
||||
var minDistance = ball1.radius + ball2.radius;
|
||||
if (distance < minDistance && distance > 0) {
|
||||
var nx = dx / distance;
|
||||
var ny = dy / distance;
|
||||
var overlap = minDistance - distance;
|
||||
var separationX = nx * overlap * 0.5;
|
||||
var separationY = ny * overlap * 0.5;
|
||||
ball1.x -= separationX;
|
||||
ball1.y -= separationY;
|
||||
ball2.x += separationX;
|
||||
ball2.y += separationY;
|
||||
var relativeVelocityX = ball2.dx - ball1.dx;
|
||||
var relativeVelocityY = ball2.dy - ball1.dy;
|
||||
var velocityAlongNormal = relativeVelocityX * nx + relativeVelocityY * ny;
|
||||
if (velocityAlongNormal > 0)
|
||||
continue;
|
||||
var restitution = (ball1.bounce + ball2.bounce) * 0.5;
|
||||
var impulseScalar = -(1 + restitution) * velocityAlongNormal / (1 / ball1.mass + 1 / ball2.mass);
|
||||
var impulseX = impulseScalar * nx;
|
||||
var impulseY = impulseScalar * ny;
|
||||
ball1.dx -= impulseX / ball1.mass;
|
||||
ball1.dy -= impulseY / ball1.mass;
|
||||
ball2.dx += impulseX / ball2.mass;
|
||||
ball2.dy += impulseY / ball2.mass;
|
||||
var energyLoss = 0.98;
|
||||
ball1.dx *= energyLoss;
|
||||
ball1.dy *= energyLoss;
|
||||
ball2.dx *= energyLoss;
|
||||
ball2.dy *= energyLoss;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* SharedArrayBuffer 处理函数
|
||||
* SharedArrayBuffer processing function
|
||||
*/
|
||||
function processSharedArrayBuffer(startIndex, endIndex, deltaTime, systemConfig) {
|
||||
if (!sharedFloatArray) return;
|
||||
var config = systemConfig || {
|
||||
gravity: 100,
|
||||
canvasWidth: 800,
|
||||
canvasHeight: 600,
|
||||
groundFriction: 0.98
|
||||
};
|
||||
var actualEntityCount = sharedFloatArray[0];
|
||||
// 基础物理更新
|
||||
for (var i = startIndex; i < endIndex && i < actualEntityCount; i++) {
|
||||
var offset = i * 9 + 9;
|
||||
var id = sharedFloatArray[offset + 0];
|
||||
if (id === 0)
|
||||
continue;
|
||||
var x = sharedFloatArray[offset + 1];
|
||||
var y = sharedFloatArray[offset + 2];
|
||||
var dx = sharedFloatArray[offset + 3];
|
||||
var dy = sharedFloatArray[offset + 4];
|
||||
var bounce = sharedFloatArray[offset + 6];
|
||||
var friction = sharedFloatArray[offset + 7];
|
||||
var radius = sharedFloatArray[offset + 8];
|
||||
// 应用重力
|
||||
dy += config.gravity * deltaTime;
|
||||
// 更新位置
|
||||
x += dx * deltaTime;
|
||||
y += dy * deltaTime;
|
||||
// 边界碰撞
|
||||
if (x <= radius) {
|
||||
x = radius;
|
||||
dx = -dx * bounce;
|
||||
}
|
||||
else if (x >= config.canvasWidth - radius) {
|
||||
x = config.canvasWidth - radius;
|
||||
dx = -dx * bounce;
|
||||
}
|
||||
if (y <= radius) {
|
||||
y = radius;
|
||||
dy = -dy * bounce;
|
||||
}
|
||||
else if (y >= config.canvasHeight - radius) {
|
||||
y = config.canvasHeight - radius;
|
||||
dy = -dy * bounce;
|
||||
dx *= config.groundFriction;
|
||||
}
|
||||
// 空气阻力
|
||||
dx *= friction;
|
||||
dy *= friction;
|
||||
// 写回数据
|
||||
sharedFloatArray[offset + 1] = x;
|
||||
sharedFloatArray[offset + 2] = y;
|
||||
sharedFloatArray[offset + 3] = dx;
|
||||
sharedFloatArray[offset + 4] = dy;
|
||||
}
|
||||
// 碰撞检测
|
||||
for (var i = startIndex; i < endIndex && i < actualEntityCount; i++) {
|
||||
var offset1 = i * 9 + 9;
|
||||
var id1 = sharedFloatArray[offset1 + 0];
|
||||
if (id1 === 0)
|
||||
continue;
|
||||
var x1 = sharedFloatArray[offset1 + 1];
|
||||
var y1 = sharedFloatArray[offset1 + 2];
|
||||
var dx1 = sharedFloatArray[offset1 + 3];
|
||||
var dy1 = sharedFloatArray[offset1 + 4];
|
||||
var mass1 = sharedFloatArray[offset1 + 5];
|
||||
var bounce1 = sharedFloatArray[offset1 + 6];
|
||||
var radius1 = sharedFloatArray[offset1 + 8];
|
||||
for (var j = 0; j < actualEntityCount; j++) {
|
||||
if (i === j)
|
||||
continue;
|
||||
var offset2 = j * 9 + 9;
|
||||
var id2 = sharedFloatArray[offset2 + 0];
|
||||
if (id2 === 0)
|
||||
continue;
|
||||
var x2 = sharedFloatArray[offset2 + 1];
|
||||
var y2 = sharedFloatArray[offset2 + 2];
|
||||
var dx2 = sharedFloatArray[offset2 + 3];
|
||||
var dy2 = sharedFloatArray[offset2 + 4];
|
||||
var mass2 = sharedFloatArray[offset2 + 5];
|
||||
var bounce2 = sharedFloatArray[offset2 + 6];
|
||||
var radius2 = sharedFloatArray[offset2 + 8];
|
||||
if (isNaN(x2) || isNaN(y2) || isNaN(radius2) || radius2 <= 0)
|
||||
continue;
|
||||
var deltaX = x2 - x1;
|
||||
var deltaY = y2 - y1;
|
||||
var distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
||||
var minDistance = radius1 + radius2;
|
||||
if (distance < minDistance && distance > 0) {
|
||||
var nx = deltaX / distance;
|
||||
var ny = deltaY / distance;
|
||||
var overlap = minDistance - distance;
|
||||
var separationX = nx * overlap * 0.5;
|
||||
var separationY = ny * overlap * 0.5;
|
||||
x1 -= separationX;
|
||||
y1 -= separationY;
|
||||
var relativeVelocityX = dx2 - dx1;
|
||||
var relativeVelocityY = dy2 - dy1;
|
||||
var velocityAlongNormal = relativeVelocityX * nx + relativeVelocityY * ny;
|
||||
if (velocityAlongNormal > 0)
|
||||
continue;
|
||||
var restitution = (bounce1 + bounce2) * 0.5;
|
||||
var impulseScalar = -(1 + restitution) * velocityAlongNormal / (1 / mass1 + 1 / mass2);
|
||||
var impulseX = impulseScalar * nx;
|
||||
var impulseY = impulseScalar * ny;
|
||||
dx1 -= impulseX / mass1;
|
||||
dy1 -= impulseY / mass1;
|
||||
var energyLoss = 0.98;
|
||||
dx1 *= energyLoss;
|
||||
dy1 *= energyLoss;
|
||||
}
|
||||
}
|
||||
sharedFloatArray[offset1 + 1] = x1;
|
||||
sharedFloatArray[offset1 + 2] = y1;
|
||||
sharedFloatArray[offset1 + 3] = dx1;
|
||||
sharedFloatArray[offset1 + 4] = dy1;
|
||||
}
|
||||
|
||||
}
|
||||
6
examples/core-demos/workers/worker-mapping.json
Normal file
6
examples/core-demos/workers/worker-mapping.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"generatedAt": "2025-12-08T09:16:10.529Z",
|
||||
"mappings": {
|
||||
"PhysicsWorkerSystem": "physics-worker.js"
|
||||
}
|
||||
}
|
||||
30
examples/wechat-worker-demo/build.js
Normal file
30
examples/wechat-worker-demo/build.js
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* 构建脚本 - 打包为微信小游戏可用的 JS 文件
|
||||
* Build script - bundle for WeChat Mini Game
|
||||
*/
|
||||
const esbuild = require('esbuild');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const outDir = path.join(__dirname, 'dist');
|
||||
|
||||
// 确保输出目录存在
|
||||
if (!fs.existsSync(outDir)) {
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
}
|
||||
|
||||
// 打包主程序
|
||||
esbuild.buildSync({
|
||||
entryPoints: ['src/index.ts'],
|
||||
bundle: true,
|
||||
outfile: 'dist/game-bundle.js',
|
||||
format: 'iife',
|
||||
globalName: 'GameDemo',
|
||||
target: ['es2015'],
|
||||
platform: 'browser',
|
||||
external: [], // 不排除任何依赖,全部打包
|
||||
minify: false,
|
||||
sourcemap: true,
|
||||
});
|
||||
|
||||
console.log('Build complete: dist/game-bundle.js');
|
||||
351
examples/wechat-worker-demo/deploy.js
Normal file
351
examples/wechat-worker-demo/deploy.js
Normal file
@@ -0,0 +1,351 @@
|
||||
/**
|
||||
* 部署脚本 - 复制文件到微信小游戏项目
|
||||
* Deploy script - copy files to WeChat Mini Game project
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// 微信小游戏项目路径
|
||||
const WECHAT_PROJECT = 'F:/MiniGame';
|
||||
|
||||
// 需要复制的文件
|
||||
const filesToCopy = [
|
||||
// Worker 文件
|
||||
{ src: 'workers/physics-worker.js', dest: 'workers/physics-worker.js' },
|
||||
// 注意:worker-mapping.json 不要放在 workers 目录,微信会把它当 JS 编译
|
||||
// Note: Don't put worker-mapping.json in workers dir, WeChat will try to compile it as JS
|
||||
];
|
||||
|
||||
// ECS 框架库
|
||||
const ecsFrameworkSrc = path.join(__dirname, '../../packages/core/dist/index.umd.js');
|
||||
const ecsFrameworkDest = path.join(WECHAT_PROJECT, 'libs/ecs-framework.js');
|
||||
|
||||
// 确保目录存在
|
||||
function ensureDir(filePath) {
|
||||
const dir = path.dirname(filePath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Deploying to WeChat Mini Game project:', WECHAT_PROJECT);
|
||||
console.log('');
|
||||
|
||||
// 复制 ECS 框架
|
||||
ensureDir(ecsFrameworkDest);
|
||||
if (fs.existsSync(ecsFrameworkSrc)) {
|
||||
fs.copyFileSync(ecsFrameworkSrc, ecsFrameworkDest);
|
||||
console.log('Copied: ecs-framework.js');
|
||||
} else {
|
||||
console.warn('Warning: ECS framework not found at', ecsFrameworkSrc);
|
||||
console.warn('Please run "pnpm build" in packages/core first');
|
||||
}
|
||||
|
||||
// 复制 Worker 文件
|
||||
for (const file of filesToCopy) {
|
||||
const srcPath = path.join(__dirname, file.src);
|
||||
const destPath = path.join(WECHAT_PROJECT, file.dest);
|
||||
|
||||
if (fs.existsSync(srcPath)) {
|
||||
ensureDir(destPath);
|
||||
fs.copyFileSync(srcPath, destPath);
|
||||
console.log('Copied:', file.dest);
|
||||
} else {
|
||||
console.warn('Warning: File not found:', srcPath);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建 game.js - 完整物理球可视化演示
|
||||
// Create game.js - Full physics ball visualization demo
|
||||
const gameJs = `/**
|
||||
* ESEngine Worker System 微信小游戏物理演示
|
||||
* ESEngine Worker System WeChat Mini Game Physics Demo
|
||||
*
|
||||
* 演示 Worker 线程处理物理计算,主线程渲染
|
||||
* Demonstrates Worker thread physics + main thread rendering
|
||||
*/
|
||||
|
||||
// ============ 配置 | Configuration ============
|
||||
var CONFIG = {
|
||||
BALL_COUNT: 20, // 球数量 | Number of balls
|
||||
GRAVITY: 400, // 重力 | Gravity
|
||||
GROUND_FRICTION: 0.98, // 地面摩擦 | Ground friction
|
||||
BALL_BOUNCE: 0.85, // 弹性系数 | Bounce factor
|
||||
MIN_RADIUS: 8, // 最小半径 | Min radius
|
||||
MAX_RADIUS: 20, // 最大半径 | Max radius
|
||||
COLORS: ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', '#98D8C8', '#F7DC6F']
|
||||
};
|
||||
|
||||
// ============ 全局状态 | Global State ============
|
||||
var canvas = wx.createCanvas();
|
||||
var ctx = canvas.getContext('2d');
|
||||
var worker = null;
|
||||
var entities = [];
|
||||
var lastTime = Date.now();
|
||||
var frameCount = 0;
|
||||
var fps = 0;
|
||||
var workerReady = false;
|
||||
var pendingRequest = false;
|
||||
|
||||
console.log('====================================');
|
||||
console.log('ESEngine Worker 物理演示');
|
||||
console.log('Canvas:', canvas.width, 'x', canvas.height);
|
||||
console.log('====================================');
|
||||
|
||||
// ============ 初始化实体 | Initialize Entities ============
|
||||
function initEntities() {
|
||||
entities = [];
|
||||
for (var i = 0; i < CONFIG.BALL_COUNT; i++) {
|
||||
var radius = CONFIG.MIN_RADIUS + Math.random() * (CONFIG.MAX_RADIUS - CONFIG.MIN_RADIUS);
|
||||
entities.push({
|
||||
id: i + 1,
|
||||
x: radius + Math.random() * (canvas.width - radius * 2),
|
||||
y: radius + Math.random() * (canvas.height * 0.5), // 上半部分生成
|
||||
dx: (Math.random() - 0.5) * 200,
|
||||
dy: Math.random() * 100,
|
||||
mass: radius * 0.1,
|
||||
bounce: CONFIG.BALL_BOUNCE,
|
||||
friction: CONFIG.GROUND_FRICTION,
|
||||
radius: radius,
|
||||
color: CONFIG.COLORS[i % CONFIG.COLORS.length]
|
||||
});
|
||||
}
|
||||
console.log('Created', entities.length, 'balls');
|
||||
}
|
||||
|
||||
// ============ 创建 Worker | Create Worker ============
|
||||
function createWorker() {
|
||||
try {
|
||||
worker = wx.createWorker('workers/physics-worker.js', {
|
||||
useExperimentalWorker: true
|
||||
});
|
||||
|
||||
worker.onMessage(function(res) {
|
||||
pendingRequest = false;
|
||||
|
||||
if (res.error) {
|
||||
console.error('Worker error:', res.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (res.result && Array.isArray(res.result)) {
|
||||
// 更新实体位置(保留颜色等渲染属性)
|
||||
// Update entity positions (keep rendering properties like color)
|
||||
for (var i = 0; i < res.result.length; i++) {
|
||||
var updated = res.result[i];
|
||||
var entity = entities[i];
|
||||
if (entity && updated) {
|
||||
entity.x = updated.x;
|
||||
entity.y = updated.y;
|
||||
entity.dx = updated.dx;
|
||||
entity.dy = updated.dy;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
workerReady = true;
|
||||
console.log('Worker created successfully!');
|
||||
} catch (error) {
|
||||
console.error('Worker creation failed:', error.message);
|
||||
workerReady = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 发送物理更新到 Worker | Send Physics Update to Worker ============
|
||||
function sendToWorker(deltaTime) {
|
||||
if (!worker || !workerReady || pendingRequest) return;
|
||||
|
||||
// 准备发送数据(只发送物理相关属性)
|
||||
// Prepare data (only physics-related properties)
|
||||
var physicsData = [];
|
||||
for (var i = 0; i < entities.length; i++) {
|
||||
var e = entities[i];
|
||||
physicsData.push({
|
||||
id: e.id,
|
||||
x: e.x,
|
||||
y: e.y,
|
||||
dx: e.dx,
|
||||
dy: e.dy,
|
||||
mass: e.mass,
|
||||
bounce: e.bounce,
|
||||
friction: e.friction,
|
||||
radius: e.radius
|
||||
});
|
||||
}
|
||||
|
||||
pendingRequest = true;
|
||||
worker.postMessage({
|
||||
id: Date.now(),
|
||||
entities: physicsData,
|
||||
deltaTime: deltaTime,
|
||||
systemConfig: {
|
||||
gravity: CONFIG.GRAVITY,
|
||||
canvasWidth: canvas.width,
|
||||
canvasHeight: canvas.height,
|
||||
groundFriction: CONFIG.GROUND_FRICTION
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ============ 渲染 | Render ============
|
||||
function render() {
|
||||
// 清屏 - 深色背景
|
||||
// Clear screen - dark background
|
||||
ctx.fillStyle = '#1a1a2e';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// 绘制地面
|
||||
// Draw ground
|
||||
ctx.fillStyle = '#2d3436';
|
||||
ctx.fillRect(0, canvas.height - 10, canvas.width, 10);
|
||||
|
||||
// 绘制所有球
|
||||
// Draw all balls
|
||||
for (var i = 0; i < entities.length; i++) {
|
||||
var e = entities[i];
|
||||
|
||||
// 球体渐变效果
|
||||
// Ball gradient effect
|
||||
var gradient = ctx.createRadialGradient(
|
||||
e.x - e.radius * 0.3, e.y - e.radius * 0.3, 0,
|
||||
e.x, e.y, e.radius
|
||||
);
|
||||
gradient.addColorStop(0, '#ffffff');
|
||||
gradient.addColorStop(0.3, e.color);
|
||||
gradient.addColorStop(1, shadeColor(e.color, -30));
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(e.x, e.y, e.radius, 0, Math.PI * 2);
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fill();
|
||||
|
||||
// 球体边框
|
||||
// Ball border
|
||||
ctx.strokeStyle = shadeColor(e.color, -50);
|
||||
ctx.lineWidth = 1;
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// 绘制 UI
|
||||
// Draw UI
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.font = '14px Arial';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillText('ESEngine Worker Physics Demo', 10, 25);
|
||||
ctx.fillText('FPS: ' + fps + ' | Balls: ' + entities.length, 10, 45);
|
||||
ctx.fillText('Worker: ' + (workerReady ? 'Active' : 'Failed'), 10, 65);
|
||||
|
||||
// 提示文字
|
||||
// Hint text
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillStyle = '#888888';
|
||||
ctx.font = '12px Arial';
|
||||
ctx.fillText('Physics calculated in Worker thread', canvas.width / 2, canvas.height - 20);
|
||||
}
|
||||
|
||||
// 颜色加深/减淡工具函数
|
||||
// Color shade utility function
|
||||
function shadeColor(color, percent) {
|
||||
var num = parseInt(color.replace('#', ''), 16);
|
||||
var amt = Math.round(2.55 * percent);
|
||||
var R = (num >> 16) + amt;
|
||||
var G = (num >> 8 & 0x00FF) + amt;
|
||||
var B = (num & 0x0000FF) + amt;
|
||||
R = Math.max(0, Math.min(255, R));
|
||||
G = Math.max(0, Math.min(255, G));
|
||||
B = Math.max(0, Math.min(255, B));
|
||||
return '#' + (0x1000000 + R * 0x10000 + G * 0x100 + B).toString(16).slice(1);
|
||||
}
|
||||
|
||||
// ============ 游戏循环 | Game Loop ============
|
||||
function gameLoop() {
|
||||
var now = Date.now();
|
||||
var deltaTime = (now - lastTime) / 1000;
|
||||
lastTime = now;
|
||||
|
||||
// 限制 deltaTime 防止跳帧
|
||||
// Clamp deltaTime to prevent frame skip
|
||||
if (deltaTime > 0.1) deltaTime = 0.1;
|
||||
|
||||
// FPS 计算
|
||||
// FPS calculation
|
||||
frameCount++;
|
||||
if (frameCount >= 30) {
|
||||
fps = Math.round(30 / ((now - (lastTime - deltaTime * 1000 * 30)) / 1000));
|
||||
frameCount = 0;
|
||||
}
|
||||
|
||||
// 发送物理计算到 Worker
|
||||
// Send physics calculation to Worker
|
||||
sendToWorker(deltaTime);
|
||||
|
||||
// 渲染
|
||||
// Render
|
||||
render();
|
||||
|
||||
// 下一帧
|
||||
// Next frame
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
// ============ 启动 | Start ============
|
||||
initEntities();
|
||||
createWorker();
|
||||
|
||||
// 简单的 FPS 计算变量
|
||||
var fpsLastTime = Date.now();
|
||||
var fpsFrameCount = 0;
|
||||
|
||||
// 覆盖 FPS 计算逻辑
|
||||
setInterval(function() {
|
||||
var now = Date.now();
|
||||
fps = Math.round(fpsFrameCount * 1000 / (now - fpsLastTime));
|
||||
fpsLastTime = now;
|
||||
fpsFrameCount = 0;
|
||||
}, 1000);
|
||||
|
||||
// 修改 gameLoop 中的帧计数
|
||||
var originalGameLoop = gameLoop;
|
||||
gameLoop = function() {
|
||||
fpsFrameCount++;
|
||||
|
||||
var now = Date.now();
|
||||
var deltaTime = (now - lastTime) / 1000;
|
||||
lastTime = now;
|
||||
|
||||
if (deltaTime > 0.1) deltaTime = 0.1;
|
||||
|
||||
sendToWorker(deltaTime);
|
||||
render();
|
||||
requestAnimationFrame(gameLoop);
|
||||
};
|
||||
|
||||
// 开始游戏循环
|
||||
// Start game loop
|
||||
console.log('Starting game loop...');
|
||||
requestAnimationFrame(gameLoop);
|
||||
|
||||
// 触摸重置
|
||||
// Touch to reset
|
||||
wx.onTouchStart(function(e) {
|
||||
console.log('Touch detected - resetting balls');
|
||||
initEntities();
|
||||
});
|
||||
`;
|
||||
|
||||
const gameJsPath = path.join(WECHAT_PROJECT, 'game.js');
|
||||
fs.writeFileSync(gameJsPath, gameJs);
|
||||
console.log('Created: game.js');
|
||||
|
||||
// 确保 game.json 配置正确
|
||||
const gameJsonPath = path.join(WECHAT_PROJECT, 'game.json');
|
||||
const gameJson = {
|
||||
deviceOrientation: 'portrait',
|
||||
workers: 'workers'
|
||||
};
|
||||
fs.writeFileSync(gameJsonPath, JSON.stringify(gameJson, null, 2));
|
||||
console.log('Updated: game.json');
|
||||
|
||||
console.log('\\nDeploy complete!');
|
||||
console.log('Open WeChat DevTools and load:', WECHAT_PROJECT);
|
||||
15
examples/wechat-worker-demo/package.json
Normal file
15
examples/wechat-worker-demo/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "wechat-worker-demo",
|
||||
"version": "1.0.0",
|
||||
"description": "ESEngine Worker System WeChat Mini Game Demo",
|
||||
"scripts": {
|
||||
"build": "node build.js",
|
||||
"generate-worker": "npx esengine-worker-gen --src ./src --wechat --verbose",
|
||||
"deploy": "node deploy.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@esengine/ecs-framework": "^2.3.2",
|
||||
"@esengine/worker-generator": "^1.0.1",
|
||||
"esbuild": "^0.19.0"
|
||||
}
|
||||
}
|
||||
65
examples/wechat-worker-demo/src/components.ts
Normal file
65
examples/wechat-worker-demo/src/components.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* 组件定义
|
||||
* Component definitions
|
||||
*/
|
||||
import { Component, ECSComponent } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSComponent('Position')
|
||||
export class Position extends Component {
|
||||
x: number = 0;
|
||||
y: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
set(x: number, y: number): void {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('Velocity')
|
||||
export class Velocity extends Component {
|
||||
dx: number = 0;
|
||||
dy: number = 0;
|
||||
|
||||
constructor(dx: number = 0, dy: number = 0) {
|
||||
super();
|
||||
this.dx = dx;
|
||||
this.dy = dy;
|
||||
}
|
||||
|
||||
set(dx: number, dy: number): void {
|
||||
this.dx = dx;
|
||||
this.dy = dy;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('Physics')
|
||||
export class Physics extends Component {
|
||||
mass: number = 1;
|
||||
bounce: number = 0.8;
|
||||
friction: number = 0.95;
|
||||
|
||||
constructor(mass: number = 1, bounce: number = 0.8, friction: number = 0.95) {
|
||||
super();
|
||||
this.mass = mass;
|
||||
this.bounce = bounce;
|
||||
this.friction = friction;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('Renderable')
|
||||
export class Renderable extends Component {
|
||||
color: string = '#ffffff';
|
||||
size: number = 5;
|
||||
|
||||
constructor(color: string = '#ffffff', size: number = 5) {
|
||||
super();
|
||||
this.color = color;
|
||||
this.size = size;
|
||||
}
|
||||
}
|
||||
6
examples/wechat-worker-demo/src/index.ts
Normal file
6
examples/wechat-worker-demo/src/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* ESEngine Worker System 微信小游戏示例
|
||||
* ESEngine Worker System WeChat Mini Game Example
|
||||
*/
|
||||
export { Position, Velocity, Physics, Renderable } from './components';
|
||||
export { PhysicsWorkerSystem } from './systems/PhysicsWorkerSystem';
|
||||
212
examples/wechat-worker-demo/src/systems/PhysicsWorkerSystem.ts
Normal file
212
examples/wechat-worker-demo/src/systems/PhysicsWorkerSystem.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
/**
|
||||
* 物理 Worker 系统
|
||||
* Physics Worker System
|
||||
*
|
||||
* 这个系统会被 worker-generator CLI 扫描,
|
||||
* 自动提取 workerProcess 方法生成 Worker 文件
|
||||
*/
|
||||
import {
|
||||
WorkerEntitySystem,
|
||||
Matcher,
|
||||
Entity,
|
||||
ECSSystem
|
||||
} from '@esengine/ecs-framework';
|
||||
import { Position, Velocity, Physics, Renderable } from '../components';
|
||||
|
||||
/**
|
||||
* 物理实体数据
|
||||
* Physics entity data
|
||||
*/
|
||||
export interface PhysicsEntityData {
|
||||
id: number;
|
||||
x: number;
|
||||
y: number;
|
||||
dx: number;
|
||||
dy: number;
|
||||
mass: number;
|
||||
bounce: number;
|
||||
friction: number;
|
||||
radius: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 物理系统配置
|
||||
* Physics system config
|
||||
*/
|
||||
export interface PhysicsConfig {
|
||||
gravity: number;
|
||||
canvasWidth: number;
|
||||
canvasHeight: number;
|
||||
groundFriction: number;
|
||||
}
|
||||
|
||||
@ECSSystem('PhysicsWorkerSystem')
|
||||
export class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsEntityData> {
|
||||
constructor(canvasWidth: number = 375, canvasHeight: number = 667) {
|
||||
super(
|
||||
Matcher.empty().all(Position, Velocity, Physics),
|
||||
{
|
||||
enableWorker: true,
|
||||
workerCount: 1,
|
||||
// 重要:这个路径会被 CLI 工具读取,生成 Worker 文件到此位置
|
||||
// Important: CLI tool reads this path to generate Worker file
|
||||
workerScriptPath: 'workers/physics-worker.js',
|
||||
systemConfig: {
|
||||
gravity: 200,
|
||||
canvasWidth,
|
||||
canvasHeight,
|
||||
groundFriction: 0.98
|
||||
} as PhysicsConfig
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取实体数据
|
||||
* Extract entity data
|
||||
*/
|
||||
protected extractEntityData(entity: Entity): PhysicsEntityData {
|
||||
const position = entity.getComponent(Position)!;
|
||||
const velocity = entity.getComponent(Velocity)!;
|
||||
const physics = entity.getComponent(Physics)!;
|
||||
const renderable = entity.getComponent(Renderable);
|
||||
|
||||
return {
|
||||
id: entity.id,
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
dx: velocity.dx,
|
||||
dy: velocity.dy,
|
||||
mass: physics.mass,
|
||||
bounce: physics.bounce,
|
||||
friction: physics.friction,
|
||||
radius: renderable?.size || 5
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Worker 处理函数 - 会被 CLI 工具提取
|
||||
* Worker process function - will be extracted by CLI tool
|
||||
*
|
||||
* 注意:这个函数必须是纯函数,不能使用 this 或外部变量
|
||||
* Note: This function must be pure, cannot use this or external variables
|
||||
*/
|
||||
protected workerProcess(
|
||||
entities: PhysicsEntityData[],
|
||||
deltaTime: number,
|
||||
config: PhysicsConfig
|
||||
): PhysicsEntityData[] {
|
||||
const gravity = config.gravity;
|
||||
const canvasWidth = config.canvasWidth;
|
||||
const canvasHeight = config.canvasHeight;
|
||||
const groundFriction = config.groundFriction;
|
||||
|
||||
// 复制实体数组避免修改原数据
|
||||
// Copy entity array to avoid modifying original data
|
||||
const result = entities.map(e => ({ ...e }));
|
||||
|
||||
// 物理更新
|
||||
// Physics update
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
const entity = result[i];
|
||||
|
||||
// 应用重力
|
||||
// Apply gravity
|
||||
entity.dy += gravity * deltaTime;
|
||||
|
||||
// 更新位置
|
||||
// Update position
|
||||
entity.x += entity.dx * deltaTime;
|
||||
entity.y += entity.dy * deltaTime;
|
||||
|
||||
// 边界碰撞
|
||||
// Boundary collision
|
||||
if (entity.x <= entity.radius) {
|
||||
entity.x = entity.radius;
|
||||
entity.dx = -entity.dx * entity.bounce;
|
||||
} else if (entity.x >= canvasWidth - entity.radius) {
|
||||
entity.x = canvasWidth - entity.radius;
|
||||
entity.dx = -entity.dx * entity.bounce;
|
||||
}
|
||||
|
||||
if (entity.y <= entity.radius) {
|
||||
entity.y = entity.radius;
|
||||
entity.dy = -entity.dy * entity.bounce;
|
||||
} else if (entity.y >= canvasHeight - entity.radius) {
|
||||
entity.y = canvasHeight - entity.radius;
|
||||
entity.dy = -entity.dy * entity.bounce;
|
||||
entity.dx *= groundFriction;
|
||||
}
|
||||
|
||||
// 空气阻力
|
||||
// Air friction
|
||||
entity.dx *= entity.friction;
|
||||
entity.dy *= entity.friction;
|
||||
}
|
||||
|
||||
// 简单碰撞检测
|
||||
// Simple collision detection
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
for (let j = i + 1; j < result.length; j++) {
|
||||
const ball1 = result[i];
|
||||
const ball2 = result[j];
|
||||
|
||||
const dx = ball2.x - ball1.x;
|
||||
const dy = ball2.y - ball1.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
const minDistance = ball1.radius + ball2.radius;
|
||||
|
||||
if (distance < minDistance && distance > 0) {
|
||||
// 分离两个球
|
||||
// Separate two balls
|
||||
const nx = dx / distance;
|
||||
const ny = dy / distance;
|
||||
const overlap = minDistance - distance;
|
||||
|
||||
ball1.x -= nx * overlap * 0.5;
|
||||
ball1.y -= ny * overlap * 0.5;
|
||||
ball2.x += nx * overlap * 0.5;
|
||||
ball2.y += ny * overlap * 0.5;
|
||||
|
||||
// 弹性碰撞
|
||||
// Elastic collision
|
||||
const relVx = ball2.dx - ball1.dx;
|
||||
const relVy = ball2.dy - ball1.dy;
|
||||
const velAlongNormal = relVx * nx + relVy * ny;
|
||||
|
||||
if (velAlongNormal > 0) continue;
|
||||
|
||||
const restitution = (ball1.bounce + ball2.bounce) * 0.5;
|
||||
const impulse = -(1 + restitution) * velAlongNormal / (1/ball1.mass + 1/ball2.mass);
|
||||
|
||||
ball1.dx -= impulse * nx / ball1.mass;
|
||||
ball1.dy -= impulse * ny / ball1.mass;
|
||||
ball2.dx += impulse * nx / ball2.mass;
|
||||
ball2.dy += impulse * ny / ball2.mass;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用处理结果
|
||||
* Apply processing result
|
||||
*/
|
||||
protected applyResult(entity: Entity, result: PhysicsEntityData): void {
|
||||
if (!entity?.enabled) return;
|
||||
|
||||
const position = entity.getComponent(Position);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
if (position && velocity) {
|
||||
position.set(result.x, result.y);
|
||||
velocity.set(result.dx, result.dy);
|
||||
}
|
||||
}
|
||||
|
||||
protected getDefaultEntityDataSize(): number {
|
||||
return 9;
|
||||
}
|
||||
}
|
||||
17
examples/wechat-worker-demo/tsconfig.json
Normal file
17
examples/wechat-worker-demo/tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2015",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"declaration": false,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
6
examples/wechat-worker-demo/worker-mapping.json
Normal file
6
examples/wechat-worker-demo/worker-mapping.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"generatedAt": "2025-12-08T10:33:50.647Z",
|
||||
"mappings": {
|
||||
"PhysicsWorkerSystem": "workers/physics-worker.js"
|
||||
}
|
||||
}
|
||||
175
examples/wechat-worker-demo/workers/physics-worker.js
Normal file
175
examples/wechat-worker-demo/workers/physics-worker.js
Normal file
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* Auto-generated Worker file for PhysicsWorkerSystem
|
||||
* 自动生成的 Worker 文件
|
||||
*
|
||||
* Source: F:/ecs-framework/examples/wechat-worker-demo/src/systems/PhysicsWorkerSystem.ts
|
||||
* Generated by @esengine/worker-generator
|
||||
*
|
||||
* 使用方式 | Usage:
|
||||
* 1. 将此文件放入 workers/ 目录
|
||||
* 2. 在 game.json 中配置 "workers": "workers"
|
||||
* 3. 在 System 中配置 workerScriptPath: 'workers/physics-worker-system-worker.js'
|
||||
*/
|
||||
|
||||
// 微信小游戏 Worker 环境
|
||||
// WeChat Mini Game Worker environment
|
||||
let sharedFloatArray = null;
|
||||
const ENTITY_DATA_SIZE = 9;
|
||||
|
||||
worker.onMessage(function(res) {
|
||||
// 微信小游戏 Worker 消息直接传递数据,不需要 .data
|
||||
// WeChat Mini Game Worker passes data directly, no .data wrapper
|
||||
var type = res.type;
|
||||
var id = res.id;
|
||||
var entities = res.entities;
|
||||
var deltaTime = res.deltaTime;
|
||||
var systemConfig = res.systemConfig;
|
||||
var startIndex = res.startIndex;
|
||||
var endIndex = res.endIndex;
|
||||
var sharedBuffer = res.sharedBuffer;
|
||||
|
||||
try {
|
||||
// 处理 SharedArrayBuffer 初始化
|
||||
// Handle SharedArrayBuffer initialization
|
||||
if (type === 'init' && sharedBuffer) {
|
||||
sharedFloatArray = new Float32Array(sharedBuffer);
|
||||
worker.postMessage({ type: 'init', success: true });
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理 SharedArrayBuffer 数据
|
||||
// Handle SharedArrayBuffer data
|
||||
if (type === 'shared' && sharedFloatArray) {
|
||||
processSharedArrayBuffer(startIndex, endIndex, deltaTime, systemConfig);
|
||||
worker.postMessage({ id: id, result: null });
|
||||
return;
|
||||
}
|
||||
|
||||
// 传统处理方式
|
||||
// Traditional processing
|
||||
if (entities) {
|
||||
var result = workerProcess(entities, deltaTime, systemConfig);
|
||||
|
||||
// 处理 Promise 返回值
|
||||
// Handle Promise return value
|
||||
if (result && typeof result.then === 'function') {
|
||||
result.then(function(finalResult) {
|
||||
worker.postMessage({ id: id, result: finalResult });
|
||||
}).catch(function(error) {
|
||||
worker.postMessage({ id: id, error: error.message });
|
||||
});
|
||||
} else {
|
||||
worker.postMessage({ id: id, result: result });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
worker.postMessage({ id: id, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 实体处理函数 - 从 PhysicsWorkerSystem.workerProcess 提取
|
||||
* Entity processing function - extracted from PhysicsWorkerSystem.workerProcess
|
||||
*/
|
||||
function workerProcess(entities, deltaTime, config) {
|
||||
var __assign = (this && this.__assign) || function () {
|
||||
__assign = Object.assign || function(t) {
|
||||
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
||||
s = arguments[i];
|
||||
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
||||
t[p] = s[p];
|
||||
}
|
||||
return t;
|
||||
};
|
||||
return __assign.apply(this, arguments);
|
||||
};
|
||||
var gravity = config.gravity;
|
||||
var canvasWidth = config.canvasWidth;
|
||||
var canvasHeight = config.canvasHeight;
|
||||
var groundFriction = config.groundFriction;
|
||||
// 复制实体数组避免修改原数据
|
||||
// Copy entity array to avoid modifying original data
|
||||
var result = entities.map(function (e) { return (__assign({}, e)); });
|
||||
// 物理更新
|
||||
// Physics update
|
||||
for (var i = 0; i < result.length; i++) {
|
||||
var entity = result[i];
|
||||
// 应用重力
|
||||
// Apply gravity
|
||||
entity.dy += gravity * deltaTime;
|
||||
// 更新位置
|
||||
// Update position
|
||||
entity.x += entity.dx * deltaTime;
|
||||
entity.y += entity.dy * deltaTime;
|
||||
// 边界碰撞
|
||||
// Boundary collision
|
||||
if (entity.x <= entity.radius) {
|
||||
entity.x = entity.radius;
|
||||
entity.dx = -entity.dx * entity.bounce;
|
||||
}
|
||||
else if (entity.x >= canvasWidth - entity.radius) {
|
||||
entity.x = canvasWidth - entity.radius;
|
||||
entity.dx = -entity.dx * entity.bounce;
|
||||
}
|
||||
if (entity.y <= entity.radius) {
|
||||
entity.y = entity.radius;
|
||||
entity.dy = -entity.dy * entity.bounce;
|
||||
}
|
||||
else if (entity.y >= canvasHeight - entity.radius) {
|
||||
entity.y = canvasHeight - entity.radius;
|
||||
entity.dy = -entity.dy * entity.bounce;
|
||||
entity.dx *= groundFriction;
|
||||
}
|
||||
// 空气阻力
|
||||
// Air friction
|
||||
entity.dx *= entity.friction;
|
||||
entity.dy *= entity.friction;
|
||||
}
|
||||
// 简单碰撞检测
|
||||
// Simple collision detection
|
||||
for (var i = 0; i < result.length; i++) {
|
||||
for (var j = i + 1; j < result.length; j++) {
|
||||
var ball1 = result[i];
|
||||
var ball2 = result[j];
|
||||
var dx = ball2.x - ball1.x;
|
||||
var dy = ball2.y - ball1.y;
|
||||
var distance = Math.sqrt(dx * dx + dy * dy);
|
||||
var minDistance = ball1.radius + ball2.radius;
|
||||
if (distance < minDistance && distance > 0) {
|
||||
// 分离两个球
|
||||
// Separate two balls
|
||||
var nx = dx / distance;
|
||||
var ny = dy / distance;
|
||||
var overlap = minDistance - distance;
|
||||
ball1.x -= nx * overlap * 0.5;
|
||||
ball1.y -= ny * overlap * 0.5;
|
||||
ball2.x += nx * overlap * 0.5;
|
||||
ball2.y += ny * overlap * 0.5;
|
||||
// 弹性碰撞
|
||||
// Elastic collision
|
||||
var relVx = ball2.dx - ball1.dx;
|
||||
var relVy = ball2.dy - ball1.dy;
|
||||
var velAlongNormal = relVx * nx + relVy * ny;
|
||||
if (velAlongNormal > 0)
|
||||
continue;
|
||||
var restitution = (ball1.bounce + ball2.bounce) * 0.5;
|
||||
var impulse = -(1 + restitution) * velAlongNormal / (1 / ball1.mass + 1 / ball2.mass);
|
||||
ball1.dx -= impulse * nx / ball1.mass;
|
||||
ball1.dy -= impulse * ny / ball1.mass;
|
||||
ball2.dx += impulse * nx / ball2.mass;
|
||||
ball2.dy += impulse * ny / ball2.mass;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* SharedArrayBuffer 处理函数
|
||||
* SharedArrayBuffer processing function
|
||||
*/
|
||||
function processSharedArrayBuffer(startIndex, endIndex, deltaTime, systemConfig) {
|
||||
if (!sharedFloatArray) return;
|
||||
// No SharedArrayBuffer processing defined
|
||||
}
|
||||
6
examples/wechat-worker-demo/workers/worker-mapping.json
Normal file
6
examples/wechat-worker-demo/workers/worker-mapping.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"generatedAt": "2025-12-08T09:57:08.855Z",
|
||||
"mappings": {
|
||||
"PhysicsWorkerSystem": "physics-worker.js"
|
||||
}
|
||||
}
|
||||
66
package.json
66
package.json
@@ -3,8 +3,18 @@
|
||||
"version": "2.1.29",
|
||||
"description": "ECS Framework Monorepo - 高性能ECS框架及其网络插件",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.22.0",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
"packages/framework/*",
|
||||
"packages/engine/*",
|
||||
"packages/rendering/*",
|
||||
"packages/physics/*",
|
||||
"packages/streaming/*",
|
||||
"packages/network-ext/*",
|
||||
"packages/editor/*",
|
||||
"packages/editor/plugins/*",
|
||||
"packages/rust/*",
|
||||
"packages/tools/*"
|
||||
],
|
||||
"keywords": [
|
||||
"ecs",
|
||||
@@ -16,29 +26,35 @@
|
||||
"egret"
|
||||
],
|
||||
"scripts": {
|
||||
"changeset": "changeset",
|
||||
"changeset:version": "changeset version",
|
||||
"changeset:publish": "changeset publish",
|
||||
"bootstrap": "lerna bootstrap",
|
||||
"clean": "lerna run clean",
|
||||
"build": "npm run build:core && npm run build:math",
|
||||
"build:core": "cd packages/core && npm run build",
|
||||
"build:math": "cd packages/math && npm run build",
|
||||
"build:npm": "npm run build:npm:core && npm run build:npm:math",
|
||||
"build:npm:core": "cd packages/core && npm run build:npm",
|
||||
"build:npm:math": "cd packages/math && npm run build:npm",
|
||||
"test": "lerna run test",
|
||||
"test:coverage": "lerna run test:coverage",
|
||||
"test:ci": "lerna run test:ci",
|
||||
"clean": "turbo run clean",
|
||||
"build": "turbo run build",
|
||||
"build:filter": "turbo run build --filter",
|
||||
"build:core": "turbo run build --filter=@esengine/ecs-framework",
|
||||
"build:math": "turbo run build --filter=@esengine/ecs-framework-math",
|
||||
"build:editor": "turbo run build --filter=@esengine/editor-app...",
|
||||
"build:npm": "turbo run build:npm",
|
||||
"build:npm:core": "cd packages/framework/core && npm run build:npm",
|
||||
"build:npm:math": "cd packages/framework/math && npm run build:npm",
|
||||
"test": "turbo run test",
|
||||
"test:coverage": "turbo run test:coverage",
|
||||
"test:ci": "turbo run test:ci",
|
||||
"test:ci:framework": "turbo run test:ci --filter=@esengine/ecs-framework --filter=@esengine/ecs-framework-math --filter=@esengine/behavior-tree --filter=@esengine/blueprint --filter=@esengine/fsm --filter=@esengine/timer --filter=@esengine/spatial --filter=@esengine/procgen --filter=@esengine/pathfinding --filter=@esengine/network-protocols --filter=@esengine/network",
|
||||
"prepare:publish": "npm run build:npm && node scripts/pre-publish-check.cjs",
|
||||
"sync:versions": "node scripts/sync-versions.cjs",
|
||||
"publish:all": "npm run prepare:publish && npm run publish:all:dist",
|
||||
"publish:all:dist": "npm run publish:core && npm run publish:math",
|
||||
"publish:core": "cd packages/core && npm run publish:npm",
|
||||
"publish:core:patch": "cd packages/core && npm run publish:patch",
|
||||
"publish:math": "cd packages/math && npm run publish:npm",
|
||||
"publish:math:patch": "cd packages/math && npm run publish:patch",
|
||||
"publish:core": "cd packages/framework/core && npm run publish:npm",
|
||||
"publish:core:patch": "cd packages/framework/core && npm run publish:patch",
|
||||
"publish:math": "cd packages/framework/math && npm run publish:npm",
|
||||
"publish:math:patch": "cd packages/framework/math && npm run publish:patch",
|
||||
"publish": "lerna publish",
|
||||
"version": "lerna version",
|
||||
"release": "semantic-release",
|
||||
"release:core": "cd packages/core && semantic-release",
|
||||
"release:core": "cd packages/framework/core && semantic-release",
|
||||
"contributors:add": "all-contributors add",
|
||||
"contributors:generate": "all-contributors generate",
|
||||
"contributors:check": "all-contributors check",
|
||||
@@ -51,15 +67,20 @@
|
||||
"copy:worker-demo": "node scripts/update-worker-demo.js",
|
||||
"format": "prettier --write \"packages/**/src/**/*.{ts,tsx,js,jsx}\"",
|
||||
"format:check": "prettier --check \"packages/**/src/**/*.{ts,tsx,js,jsx}\"",
|
||||
"type-check": "lerna run type-check",
|
||||
"lint": "eslint \"packages/**/src/**/*.{ts,tsx,js,jsx}\"",
|
||||
"lint:fix": "eslint \"packages/**/src/**/*.{ts,tsx,js,jsx}\" --fix",
|
||||
"build:wasm": "cd packages/engine && wasm-pack build --dev --out-dir pkg",
|
||||
"build:wasm:release": "cd packages/engine && wasm-pack build --release --out-dir pkg"
|
||||
"type-check": "turbo run type-check",
|
||||
"type-check:framework": "turbo run type-check --filter=@esengine/ecs-framework --filter=@esengine/ecs-framework-math --filter=@esengine/behavior-tree --filter=@esengine/blueprint --filter=@esengine/fsm --filter=@esengine/timer --filter=@esengine/spatial --filter=@esengine/procgen --filter=@esengine/pathfinding --filter=@esengine/network-protocols --filter=@esengine/network",
|
||||
"lint": "turbo run lint",
|
||||
"lint:framework": "turbo run lint --filter=@esengine/ecs-framework --filter=@esengine/ecs-framework-math --filter=@esengine/behavior-tree --filter=@esengine/blueprint --filter=@esengine/fsm --filter=@esengine/timer --filter=@esengine/spatial --filter=@esengine/procgen --filter=@esengine/pathfinding --filter=@esengine/network-protocols --filter=@esengine/network",
|
||||
"lint:fix": "turbo run lint:fix",
|
||||
"build:wasm": "cd packages/rust/engine && wasm-pack build --dev --out-dir pkg",
|
||||
"build:wasm:release": "cd packages/rust/engine && wasm-pack build --release --out-dir pkg",
|
||||
"copy-modules": "node scripts/copy-engine-modules.mjs"
|
||||
},
|
||||
"author": "yhh",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@changesets/changelog-github": "^0.5.2",
|
||||
"@changesets/cli": "^2.29.8",
|
||||
"@commitlint/cli": "^18.6.0",
|
||||
"@commitlint/config-conventional": "^18.6.0",
|
||||
"@eslint/js": "^9.39.1",
|
||||
@@ -92,6 +113,7 @@
|
||||
"semver": "^7.6.3",
|
||||
"size-limit": "^11.0.2",
|
||||
"ts-jest": "^29.4.0",
|
||||
"turbo": "^2.6.1",
|
||||
"typedoc": "^0.28.13",
|
||||
"typedoc-plugin-markdown": "^4.9.0",
|
||||
"typescript": "^5.8.3",
|
||||
@@ -104,7 +126,7 @@
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/esengine/ecs-framework.git"
|
||||
"url": "https://github.com/esengine/esengine.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/multer": "^1.4.13",
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import typescript from '@rollup/plugin-typescript';
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import dts from 'rollup-plugin-dts';
|
||||
|
||||
const external = ['@esengine/ecs-framework'];
|
||||
|
||||
export default [
|
||||
// ESM and CJS builds
|
||||
{
|
||||
input: 'src/index.ts',
|
||||
output: [
|
||||
{
|
||||
file: 'dist/index.js',
|
||||
format: 'cjs',
|
||||
sourcemap: true,
|
||||
exports: 'named'
|
||||
},
|
||||
{
|
||||
file: 'dist/index.mjs',
|
||||
format: 'es',
|
||||
sourcemap: true
|
||||
}
|
||||
],
|
||||
external,
|
||||
plugins: [
|
||||
resolve({
|
||||
preferBuiltins: false,
|
||||
browser: true
|
||||
}),
|
||||
commonjs(),
|
||||
typescript({
|
||||
tsconfig: './tsconfig.json',
|
||||
declaration: false,
|
||||
declarationMap: false
|
||||
})
|
||||
]
|
||||
},
|
||||
// Type definitions
|
||||
{
|
||||
input: 'src/index.ts',
|
||||
output: {
|
||||
file: 'dist/index.d.ts',
|
||||
format: 'es'
|
||||
},
|
||||
external,
|
||||
plugins: [dts()]
|
||||
}
|
||||
];
|
||||
@@ -1,59 +0,0 @@
|
||||
/**
|
||||
* Asset System for ECS Framework
|
||||
* ECS框架的资产系统
|
||||
*/
|
||||
|
||||
// Types
|
||||
export * from './types/AssetTypes';
|
||||
|
||||
// Interfaces
|
||||
export * from './interfaces/IAssetLoader';
|
||||
export * from './interfaces/IAssetManager';
|
||||
export * from './interfaces/IResourceComponent';
|
||||
|
||||
// Core
|
||||
export { AssetManager } from './core/AssetManager';
|
||||
export { AssetCache } from './core/AssetCache';
|
||||
export { AssetDatabase } from './core/AssetDatabase';
|
||||
export { AssetLoadQueue } from './core/AssetLoadQueue';
|
||||
export { AssetReference, WeakAssetReference, AssetReferenceArray } from './core/AssetReference';
|
||||
export { AssetPathResolver, globalPathResolver } from './core/AssetPathResolver';
|
||||
export type { IAssetPathConfig } from './core/AssetPathResolver';
|
||||
|
||||
// Loaders
|
||||
export { AssetLoaderFactory } from './loaders/AssetLoaderFactory';
|
||||
export { TextureLoader } from './loaders/TextureLoader';
|
||||
export { JsonLoader } from './loaders/JsonLoader';
|
||||
export { TextLoader } from './loaders/TextLoader';
|
||||
export { BinaryLoader } from './loaders/BinaryLoader';
|
||||
|
||||
// Integration
|
||||
export { EngineIntegration } from './integration/EngineIntegration';
|
||||
export type { IEngineBridge } from './integration/EngineIntegration';
|
||||
|
||||
// Services
|
||||
export { SceneResourceManager } from './services/SceneResourceManager';
|
||||
export type { IResourceLoader } from './services/SceneResourceManager';
|
||||
|
||||
// Utils
|
||||
export { UVHelper } from './utils/UVHelper';
|
||||
|
||||
// Default instance
|
||||
import { AssetManager } from './core/AssetManager';
|
||||
|
||||
/**
|
||||
* Default asset manager instance
|
||||
* 默认资产管理器实例
|
||||
*/
|
||||
export const assetManager = new AssetManager();
|
||||
|
||||
/**
|
||||
* Initialize asset system with catalog
|
||||
* 使用目录初始化资产系统
|
||||
*/
|
||||
export function initializeAssetSystem(catalog?: any): AssetManager {
|
||||
if (catalog) {
|
||||
return new AssetManager(catalog);
|
||||
}
|
||||
return assetManager;
|
||||
}
|
||||
@@ -1,248 +0,0 @@
|
||||
/**
|
||||
* Engine integration for asset system
|
||||
* 资产系统的引擎集成
|
||||
*/
|
||||
|
||||
import { AssetManager } from '../core/AssetManager';
|
||||
import { AssetGUID } from '../types/AssetTypes';
|
||||
import { ITextureAsset } from '../interfaces/IAssetLoader';
|
||||
import { globalPathResolver } from '../core/AssetPathResolver';
|
||||
|
||||
/**
|
||||
* Engine bridge interface
|
||||
* 引擎桥接接口
|
||||
*/
|
||||
export interface IEngineBridge {
|
||||
/**
|
||||
* Load texture to GPU
|
||||
* 加载纹理到GPU
|
||||
*/
|
||||
loadTexture(id: number, url: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Load multiple textures
|
||||
* 批量加载纹理
|
||||
*/
|
||||
loadTextures(requests: Array<{ id: number; url: string }>): Promise<void>;
|
||||
|
||||
/**
|
||||
* Unload texture from GPU
|
||||
* 从GPU卸载纹理
|
||||
*/
|
||||
unloadTexture(id: number): void;
|
||||
|
||||
/**
|
||||
* Get texture info
|
||||
* 获取纹理信息
|
||||
*/
|
||||
getTextureInfo(id: number): { width: number; height: number } | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset system engine integration
|
||||
* 资产系统引擎集成
|
||||
*/
|
||||
export class EngineIntegration {
|
||||
private _assetManager: AssetManager;
|
||||
private _engineBridge?: IEngineBridge;
|
||||
private _textureIdMap = new Map<AssetGUID, number>();
|
||||
private _pathToTextureId = new Map<string, number>();
|
||||
|
||||
constructor(assetManager: AssetManager, engineBridge?: IEngineBridge) {
|
||||
this._assetManager = assetManager;
|
||||
this._engineBridge = engineBridge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set engine bridge
|
||||
* 设置引擎桥接
|
||||
*/
|
||||
setEngineBridge(bridge: IEngineBridge): void {
|
||||
this._engineBridge = bridge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load texture for component
|
||||
* 为组件加载纹理
|
||||
*
|
||||
* 统一的路径解析入口:相对路径会被转换为 Tauri 可用的 asset:// URL
|
||||
* Unified path resolution entry: relative paths will be converted to Tauri-compatible asset:// URLs
|
||||
*/
|
||||
async loadTextureForComponent(texturePath: string): Promise<number> {
|
||||
// 检查缓存(使用原始路径作为键)
|
||||
// Check cache (using original path as key)
|
||||
const existingId = this._pathToTextureId.get(texturePath);
|
||||
if (existingId) {
|
||||
return existingId;
|
||||
}
|
||||
|
||||
// 使用 globalPathResolver 转换路径
|
||||
// Use globalPathResolver to transform the path
|
||||
const resolvedPath = globalPathResolver.resolve(texturePath);
|
||||
|
||||
// 通过资产系统加载(使用解析后的路径)
|
||||
// Load through asset system (using resolved path)
|
||||
const result = await this._assetManager.loadAssetByPath<ITextureAsset>(resolvedPath);
|
||||
const textureAsset = result.asset;
|
||||
|
||||
// 如果有引擎桥接,上传到GPU(使用解析后的路径)
|
||||
// Upload to GPU if bridge exists (using resolved path)
|
||||
if (this._engineBridge && textureAsset.data) {
|
||||
await this._engineBridge.loadTexture(textureAsset.textureId, resolvedPath);
|
||||
}
|
||||
|
||||
// 缓存映射(使用原始路径作为键,避免重复解析)
|
||||
// Cache mapping (using original path as key to avoid re-resolving)
|
||||
this._pathToTextureId.set(texturePath, textureAsset.textureId);
|
||||
|
||||
return textureAsset.textureId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load texture by GUID
|
||||
* 通过GUID加载纹理
|
||||
*/
|
||||
async loadTextureByGuid(guid: AssetGUID): Promise<number> {
|
||||
// 检查是否已有纹理ID / Check if texture ID exists
|
||||
const existingId = this._textureIdMap.get(guid);
|
||||
if (existingId) {
|
||||
return existingId;
|
||||
}
|
||||
|
||||
// 通过资产系统加载 / Load through asset system
|
||||
const result = await this._assetManager.loadAsset<ITextureAsset>(guid);
|
||||
const textureAsset = result.asset;
|
||||
|
||||
// 如果有引擎桥接,上传到GPU / Upload to GPU if bridge exists
|
||||
if (this._engineBridge && textureAsset.data) {
|
||||
const metadata = result.metadata;
|
||||
await this._engineBridge.loadTexture(textureAsset.textureId, metadata.path);
|
||||
}
|
||||
|
||||
// 缓存映射 / Cache mapping
|
||||
this._textureIdMap.set(guid, textureAsset.textureId);
|
||||
|
||||
return textureAsset.textureId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch load textures
|
||||
* 批量加载纹理
|
||||
*/
|
||||
async loadTexturesBatch(paths: string[]): Promise<Map<string, number>> {
|
||||
const results = new Map<string, number>();
|
||||
|
||||
// 收集需要加载的纹理 / Collect textures to load
|
||||
const toLoad: string[] = [];
|
||||
for (const path of paths) {
|
||||
const existingId = this._pathToTextureId.get(path);
|
||||
if (existingId) {
|
||||
results.set(path, existingId);
|
||||
} else {
|
||||
toLoad.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
if (toLoad.length === 0) {
|
||||
return results;
|
||||
}
|
||||
|
||||
// 并行加载所有纹理 / Load all textures in parallel
|
||||
const loadPromises = toLoad.map(async (path) => {
|
||||
try {
|
||||
const id = await this.loadTextureForComponent(path);
|
||||
results.set(path, id);
|
||||
} catch (error) {
|
||||
console.error(`Failed to load texture: ${path}`, error);
|
||||
results.set(path, 0); // 使用默认纹理ID / Use default texture ID
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(loadPromises);
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量加载资源(通用方法,支持 IResourceLoader 接口)
|
||||
* Load resources in batch (generic method for IResourceLoader interface)
|
||||
*
|
||||
* @param paths 资源路径数组 / Array of resource paths
|
||||
* @param type 资源类型 / Resource type
|
||||
* @returns 路径到运行时 ID 的映射 / Map of paths to runtime IDs
|
||||
*/
|
||||
async loadResourcesBatch(paths: string[], type: 'texture' | 'audio' | 'font' | 'data'): Promise<Map<string, number>> {
|
||||
// 目前只支持纹理 / Currently only supports textures
|
||||
if (type === 'texture') {
|
||||
return this.loadTexturesBatch(paths);
|
||||
}
|
||||
|
||||
// 其他资源类型暂未实现 / Other resource types not yet implemented
|
||||
console.warn(`[EngineIntegration] Resource type '${type}' not yet supported`);
|
||||
return new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unload texture
|
||||
* 卸载纹理
|
||||
*/
|
||||
unloadTexture(textureId: number): void {
|
||||
// 从引擎卸载 / Unload from engine
|
||||
if (this._engineBridge) {
|
||||
this._engineBridge.unloadTexture(textureId);
|
||||
}
|
||||
|
||||
// 清理映射 / Clean up mappings
|
||||
for (const [path, id] of this._pathToTextureId.entries()) {
|
||||
if (id === textureId) {
|
||||
this._pathToTextureId.delete(path);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (const [guid, id] of this._textureIdMap.entries()) {
|
||||
if (id === textureId) {
|
||||
this._textureIdMap.delete(guid);
|
||||
// 也从资产管理器卸载 / Also unload from asset manager
|
||||
this._assetManager.unloadAsset(guid);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get texture ID for path
|
||||
* 获取路径的纹理ID
|
||||
*/
|
||||
getTextureId(path: string): number | null {
|
||||
return this._pathToTextureId.get(path) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Preload textures for scene
|
||||
* 为场景预加载纹理
|
||||
*/
|
||||
async preloadSceneTextures(texturePaths: string[]): Promise<void> {
|
||||
await this.loadTexturesBatch(texturePaths);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all texture mappings
|
||||
* 清空所有纹理映射
|
||||
*/
|
||||
clearTextureMappings(): void {
|
||||
this._textureIdMap.clear();
|
||||
this._pathToTextureId.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get statistics
|
||||
* 获取统计信息
|
||||
*/
|
||||
getStatistics(): {
|
||||
loadedTextures: number;
|
||||
} {
|
||||
return {
|
||||
loadedTextures: this._pathToTextureId.size
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,222 +0,0 @@
|
||||
/**
|
||||
* Asset loader interfaces
|
||||
* 资产加载器接口
|
||||
*/
|
||||
|
||||
import {
|
||||
AssetType,
|
||||
AssetGUID,
|
||||
IAssetLoadOptions,
|
||||
IAssetMetadata,
|
||||
IAssetLoadResult
|
||||
} from '../types/AssetTypes';
|
||||
|
||||
/**
|
||||
* Base asset loader interface
|
||||
* 基础资产加载器接口
|
||||
*/
|
||||
export interface IAssetLoader<T = unknown> {
|
||||
/** 支持的资产类型 / Supported asset type */
|
||||
readonly supportedType: AssetType;
|
||||
|
||||
/** 支持的文件扩展名 / Supported file extensions */
|
||||
readonly supportedExtensions: string[];
|
||||
|
||||
/**
|
||||
* Load an asset from the given path
|
||||
* 从指定路径加载资产
|
||||
*/
|
||||
load(
|
||||
path: string,
|
||||
metadata: IAssetMetadata,
|
||||
options?: IAssetLoadOptions
|
||||
): Promise<IAssetLoadResult<T>>;
|
||||
|
||||
/**
|
||||
* Validate if the loader can handle this asset
|
||||
* 验证加载器是否可以处理此资产
|
||||
*/
|
||||
canLoad(path: string, metadata: IAssetMetadata): boolean;
|
||||
|
||||
/**
|
||||
* Dispose loaded asset and free resources
|
||||
* 释放已加载的资产并释放资源
|
||||
*/
|
||||
dispose(asset: T): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset loader factory interface
|
||||
* 资产加载器工厂接口
|
||||
*/
|
||||
export interface IAssetLoaderFactory {
|
||||
/**
|
||||
* Create loader for specific asset type
|
||||
* 为特定资产类型创建加载器
|
||||
*/
|
||||
createLoader(type: AssetType): IAssetLoader | null;
|
||||
|
||||
/**
|
||||
* Register custom loader
|
||||
* 注册自定义加载器
|
||||
*/
|
||||
registerLoader(type: AssetType, loader: IAssetLoader): void;
|
||||
|
||||
/**
|
||||
* Unregister loader
|
||||
* 注销加载器
|
||||
*/
|
||||
unregisterLoader(type: AssetType): void;
|
||||
|
||||
/**
|
||||
* Check if loader exists for type
|
||||
* 检查类型是否有加载器
|
||||
*/
|
||||
hasLoader(type: AssetType): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Texture asset interface
|
||||
* 纹理资产接口
|
||||
*/
|
||||
export interface ITextureAsset {
|
||||
/** WebGL纹理ID / WebGL texture ID */
|
||||
textureId: number;
|
||||
/** 宽度 / Width */
|
||||
width: number;
|
||||
/** 高度 / Height */
|
||||
height: number;
|
||||
/** 格式 / Format */
|
||||
format: 'rgba' | 'rgb' | 'alpha';
|
||||
/** 是否有Mipmap / Has mipmaps */
|
||||
hasMipmaps: boolean;
|
||||
/** 原始数据(如果可用) / Raw image data if available */
|
||||
data?: ImageData | HTMLImageElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mesh asset interface
|
||||
* 网格资产接口
|
||||
*/
|
||||
export interface IMeshAsset {
|
||||
/** 顶点数据 / Vertex data */
|
||||
vertices: Float32Array;
|
||||
/** 索引数据 / Index data */
|
||||
indices: Uint16Array | Uint32Array;
|
||||
/** 法线数据 / Normal data */
|
||||
normals?: Float32Array;
|
||||
/** UV坐标 / UV coordinates */
|
||||
uvs?: Float32Array;
|
||||
/** 切线数据 / Tangent data */
|
||||
tangents?: Float32Array;
|
||||
/** 边界盒 / Axis-aligned bounding box */
|
||||
bounds: {
|
||||
min: [number, number, number];
|
||||
max: [number, number, number];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Audio asset interface
|
||||
* 音频资产接口
|
||||
*/
|
||||
export interface IAudioAsset {
|
||||
/** 音频缓冲区 / Audio buffer */
|
||||
buffer: AudioBuffer;
|
||||
/** 时长(秒) / Duration in seconds */
|
||||
duration: number;
|
||||
/** 采样率 / Sample rate */
|
||||
sampleRate: number;
|
||||
/** 声道数 / Number of channels */
|
||||
channels: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Material asset interface
|
||||
* 材质资产接口
|
||||
*/
|
||||
export interface IMaterialAsset {
|
||||
/** 着色器名称 / Shader name */
|
||||
shader: string;
|
||||
/** 材质属性 / Material properties */
|
||||
properties: Map<string, unknown>;
|
||||
/** 纹理映射 / Texture slot mappings */
|
||||
textures: Map<string, AssetGUID>;
|
||||
/** 渲染状态 / Render states */
|
||||
renderStates: {
|
||||
cullMode?: 'none' | 'front' | 'back';
|
||||
blendMode?: 'none' | 'alpha' | 'additive' | 'multiply';
|
||||
depthTest?: boolean;
|
||||
depthWrite?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefab asset interface
|
||||
* 预制体资产接口
|
||||
*/
|
||||
export interface IPrefabAsset {
|
||||
/** 根实体数据 / Serialized entity hierarchy */
|
||||
root: unknown;
|
||||
/** 包含的组件类型 / Component types used in prefab */
|
||||
componentTypes: string[];
|
||||
/** 引用的资产 / All referenced assets */
|
||||
referencedAssets: AssetGUID[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Scene asset interface
|
||||
* 场景资产接口
|
||||
*/
|
||||
export interface ISceneAsset {
|
||||
/** 场景名称 / Scene name */
|
||||
name: string;
|
||||
/** 实体列表 / Serialized entity list */
|
||||
entities: unknown[];
|
||||
/** 场景设置 / Scene settings */
|
||||
settings: {
|
||||
/** 环境光 / Ambient light */
|
||||
ambientLight?: [number, number, number];
|
||||
/** 雾效 / Fog settings */
|
||||
fog?: {
|
||||
enabled: boolean;
|
||||
color: [number, number, number];
|
||||
density: number;
|
||||
};
|
||||
/** 天空盒 / Skybox asset */
|
||||
skybox?: AssetGUID;
|
||||
};
|
||||
/** 引用的资产 / All referenced assets */
|
||||
referencedAssets: AssetGUID[];
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON asset interface
|
||||
* JSON资产接口
|
||||
*/
|
||||
export interface IJsonAsset {
|
||||
/** JSON数据 / JSON data */
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Text asset interface
|
||||
* 文本资产接口
|
||||
*/
|
||||
export interface ITextAsset {
|
||||
/** 文本内容 / Text content */
|
||||
content: string;
|
||||
/** 编码格式 / Encoding */
|
||||
encoding: 'utf8' | 'utf16' | 'ascii';
|
||||
}
|
||||
|
||||
/**
|
||||
* Binary asset interface
|
||||
* 二进制资产接口
|
||||
*/
|
||||
export interface IBinaryAsset {
|
||||
/** 二进制数据 / Binary data */
|
||||
data: ArrayBuffer;
|
||||
/** MIME类型 / MIME type */
|
||||
mimeType?: string;
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
/**
|
||||
* Asset loader factory implementation
|
||||
* 资产加载器工厂实现
|
||||
*/
|
||||
|
||||
import { AssetType } from '../types/AssetTypes';
|
||||
import { IAssetLoader, IAssetLoaderFactory } from '../interfaces/IAssetLoader';
|
||||
import { TextureLoader } from './TextureLoader';
|
||||
import { JsonLoader } from './JsonLoader';
|
||||
import { TextLoader } from './TextLoader';
|
||||
import { BinaryLoader } from './BinaryLoader';
|
||||
|
||||
/**
|
||||
* Asset loader factory
|
||||
* 资产加载器工厂
|
||||
*/
|
||||
export class AssetLoaderFactory implements IAssetLoaderFactory {
|
||||
private readonly _loaders = new Map<AssetType, IAssetLoader>();
|
||||
|
||||
constructor() {
|
||||
// 注册默认加载器 / Register default loaders
|
||||
this.registerDefaultLoaders();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register default loaders
|
||||
* 注册默认加载器
|
||||
*/
|
||||
private registerDefaultLoaders(): void {
|
||||
// 纹理加载器 / Texture loader
|
||||
this._loaders.set(AssetType.Texture, new TextureLoader());
|
||||
|
||||
// JSON加载器 / JSON loader
|
||||
this._loaders.set(AssetType.Json, new JsonLoader());
|
||||
|
||||
// 文本加载器 / Text loader
|
||||
this._loaders.set(AssetType.Text, new TextLoader());
|
||||
|
||||
// 二进制加载器 / Binary loader
|
||||
this._loaders.set(AssetType.Binary, new BinaryLoader());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create loader for specific asset type
|
||||
* 为特定资产类型创建加载器
|
||||
*/
|
||||
createLoader(type: AssetType): IAssetLoader | null {
|
||||
return this._loaders.get(type) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register custom loader
|
||||
* 注册自定义加载器
|
||||
*/
|
||||
registerLoader(type: AssetType, loader: IAssetLoader): void {
|
||||
this._loaders.set(type, loader);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister loader
|
||||
* 注销加载器
|
||||
*/
|
||||
unregisterLoader(type: AssetType): void {
|
||||
this._loaders.delete(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if loader exists for type
|
||||
* 检查类型是否有加载器
|
||||
*/
|
||||
hasLoader(type: AssetType): boolean {
|
||||
return this._loaders.has(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered loaders
|
||||
* 获取所有注册的加载器
|
||||
*/
|
||||
getRegisteredTypes(): AssetType[] {
|
||||
return Array.from(this._loaders.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all loaders
|
||||
* 清空所有加载器
|
||||
*/
|
||||
clear(): void {
|
||||
this._loaders.clear();
|
||||
}
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
/**
|
||||
* Binary asset loader
|
||||
* 二进制资产加载器
|
||||
*/
|
||||
|
||||
import {
|
||||
AssetType,
|
||||
IAssetLoadOptions,
|
||||
IAssetMetadata,
|
||||
IAssetLoadResult,
|
||||
AssetLoadError
|
||||
} from '../types/AssetTypes';
|
||||
import { IAssetLoader, IBinaryAsset } from '../interfaces/IAssetLoader';
|
||||
|
||||
/**
|
||||
* Binary loader implementation
|
||||
* 二进制加载器实现
|
||||
*/
|
||||
export class BinaryLoader implements IAssetLoader<IBinaryAsset> {
|
||||
readonly supportedType = AssetType.Binary;
|
||||
readonly supportedExtensions = [
|
||||
'.bin', '.dat', '.raw', '.bytes',
|
||||
'.wasm', '.so', '.dll', '.dylib'
|
||||
];
|
||||
|
||||
/**
|
||||
* Load binary asset
|
||||
* 加载二进制资产
|
||||
*/
|
||||
async load(
|
||||
path: string,
|
||||
metadata: IAssetMetadata,
|
||||
options?: IAssetLoadOptions
|
||||
): Promise<IAssetLoadResult<IBinaryAsset>> {
|
||||
const startTime = performance.now();
|
||||
|
||||
try {
|
||||
const response = await this.fetchWithTimeout(path, options?.timeout);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
// 获取MIME类型 / Get MIME type
|
||||
const mimeType = response.headers.get('content-type') || undefined;
|
||||
|
||||
// 获取总大小用于进度回调 / Get total size for progress callback
|
||||
const contentLength = response.headers.get('content-length');
|
||||
const total = contentLength ? parseInt(contentLength, 10) : 0;
|
||||
|
||||
// 读取响应 / Read response
|
||||
let data: ArrayBuffer;
|
||||
if (options?.onProgress && total > 0) {
|
||||
data = await this.readResponseWithProgress(response, total, options.onProgress);
|
||||
} else {
|
||||
data = await response.arrayBuffer();
|
||||
}
|
||||
|
||||
const asset: IBinaryAsset = {
|
||||
data,
|
||||
mimeType
|
||||
};
|
||||
|
||||
return {
|
||||
asset,
|
||||
handle: 0,
|
||||
metadata,
|
||||
loadTime: performance.now() - startTime
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw new AssetLoadError(
|
||||
`Failed to load binary: ${error.message}`,
|
||||
metadata.guid,
|
||||
AssetType.Binary,
|
||||
error
|
||||
);
|
||||
}
|
||||
throw AssetLoadError.fileNotFound(metadata.guid, path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch with timeout
|
||||
* 带超时的fetch
|
||||
*/
|
||||
private async fetchWithTimeout(url: string, timeout = 30000): Promise<Response> {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
signal: controller.signal,
|
||||
mode: 'cors',
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
return response;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read response with progress
|
||||
* 带进度读取响应
|
||||
*/
|
||||
private async readResponseWithProgress(
|
||||
response: Response,
|
||||
total: number,
|
||||
onProgress: (progress: number) => void
|
||||
): Promise<ArrayBuffer> {
|
||||
const reader = response.body?.getReader();
|
||||
if (!reader) {
|
||||
return response.arrayBuffer();
|
||||
}
|
||||
|
||||
const chunks: Uint8Array[] = [];
|
||||
let receivedLength = 0;
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) break;
|
||||
|
||||
chunks.push(value);
|
||||
receivedLength += value.length;
|
||||
|
||||
// 报告进度 / Report progress
|
||||
onProgress(receivedLength / total);
|
||||
}
|
||||
|
||||
// 合并chunks到ArrayBuffer / Merge chunks into ArrayBuffer
|
||||
const result = new Uint8Array(receivedLength);
|
||||
let position = 0;
|
||||
for (const chunk of chunks) {
|
||||
result.set(chunk, position);
|
||||
position += chunk.length;
|
||||
}
|
||||
|
||||
return result.buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if the loader can handle this asset
|
||||
* 验证加载器是否可以处理此资产
|
||||
*/
|
||||
canLoad(path: string, _metadata: IAssetMetadata): boolean {
|
||||
const ext = path.toLowerCase().substring(path.lastIndexOf('.'));
|
||||
return this.supportedExtensions.includes(ext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate memory usage for the asset
|
||||
* 估算资产的内存使用量
|
||||
*/
|
||||
|
||||
/**
|
||||
* Dispose loaded asset
|
||||
* 释放已加载的资产
|
||||
*/
|
||||
dispose(asset: IBinaryAsset): void {
|
||||
// ArrayBuffer无法直接释放,但可以清空引用 / Can't directly release ArrayBuffer, but clear reference
|
||||
(asset as any).data = null;
|
||||
}
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
/**
|
||||
* JSON asset loader
|
||||
* JSON资产加载器
|
||||
*/
|
||||
|
||||
import {
|
||||
AssetType,
|
||||
IAssetLoadOptions,
|
||||
IAssetMetadata,
|
||||
IAssetLoadResult,
|
||||
AssetLoadError
|
||||
} from '../types/AssetTypes';
|
||||
import { IAssetLoader, IJsonAsset } from '../interfaces/IAssetLoader';
|
||||
|
||||
/**
|
||||
* JSON loader implementation
|
||||
* JSON加载器实现
|
||||
*/
|
||||
export class JsonLoader implements IAssetLoader<IJsonAsset> {
|
||||
readonly supportedType = AssetType.Json;
|
||||
readonly supportedExtensions = ['.json', '.jsonc'];
|
||||
|
||||
/**
|
||||
* Load JSON asset
|
||||
* 加载JSON资产
|
||||
*/
|
||||
async load(
|
||||
path: string,
|
||||
metadata: IAssetMetadata,
|
||||
options?: IAssetLoadOptions
|
||||
): Promise<IAssetLoadResult<IJsonAsset>> {
|
||||
const startTime = performance.now();
|
||||
|
||||
try {
|
||||
const response = await this.fetchWithTimeout(path, options?.timeout);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
// 获取总大小用于进度回调 / Get total size for progress callback
|
||||
const contentLength = response.headers.get('content-length');
|
||||
const total = contentLength ? parseInt(contentLength, 10) : 0;
|
||||
|
||||
// 读取响应 / Read response
|
||||
let jsonData: unknown;
|
||||
if (options?.onProgress && total > 0) {
|
||||
jsonData = await this.readResponseWithProgress(response, total, options.onProgress);
|
||||
} else {
|
||||
jsonData = await response.json();
|
||||
}
|
||||
|
||||
const asset: IJsonAsset = {
|
||||
data: jsonData
|
||||
};
|
||||
|
||||
return {
|
||||
asset,
|
||||
handle: 0,
|
||||
metadata,
|
||||
loadTime: performance.now() - startTime
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw new AssetLoadError(
|
||||
`Failed to load JSON: ${error.message}`,
|
||||
metadata.guid,
|
||||
AssetType.Json,
|
||||
error
|
||||
);
|
||||
}
|
||||
throw AssetLoadError.fileNotFound(metadata.guid, path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch with timeout
|
||||
* 带超时的fetch
|
||||
*/
|
||||
private async fetchWithTimeout(url: string, timeout = 30000): Promise<Response> {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
signal: controller.signal,
|
||||
mode: 'cors',
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
return response;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read response with progress
|
||||
* 带进度读取响应
|
||||
*/
|
||||
private async readResponseWithProgress(
|
||||
response: Response,
|
||||
total: number,
|
||||
onProgress: (progress: number) => void
|
||||
): Promise<unknown> {
|
||||
const reader = response.body?.getReader();
|
||||
if (!reader) {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
const chunks: Uint8Array[] = [];
|
||||
let receivedLength = 0;
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) break;
|
||||
|
||||
chunks.push(value);
|
||||
receivedLength += value.length;
|
||||
|
||||
// 报告进度 / Report progress
|
||||
onProgress(receivedLength / total);
|
||||
}
|
||||
|
||||
// 合并chunks / Merge chunks
|
||||
const allChunks = new Uint8Array(receivedLength);
|
||||
let position = 0;
|
||||
for (const chunk of chunks) {
|
||||
allChunks.set(chunk, position);
|
||||
position += chunk.length;
|
||||
}
|
||||
|
||||
// 解码为字符串并解析JSON / Decode to string and parse JSON
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
const jsonString = decoder.decode(allChunks);
|
||||
return JSON.parse(jsonString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if the loader can handle this asset
|
||||
* 验证加载器是否可以处理此资产
|
||||
*/
|
||||
canLoad(path: string, _metadata: IAssetMetadata): boolean {
|
||||
const ext = path.toLowerCase().substring(path.lastIndexOf('.'));
|
||||
return this.supportedExtensions.includes(ext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate memory usage for the asset
|
||||
* 估算资产的内存使用量
|
||||
*/
|
||||
|
||||
/**
|
||||
* Dispose loaded asset
|
||||
* 释放已加载的资产
|
||||
*/
|
||||
dispose(asset: IJsonAsset): void {
|
||||
// JSON资产通常不需要特殊清理 / JSON assets usually don't need special cleanup
|
||||
// 但可以清空引用以帮助GC / But can clear references to help GC
|
||||
(asset as any).data = null;
|
||||
}
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
/**
|
||||
* Text asset loader
|
||||
* 文本资产加载器
|
||||
*/
|
||||
|
||||
import {
|
||||
AssetType,
|
||||
IAssetLoadOptions,
|
||||
IAssetMetadata,
|
||||
IAssetLoadResult,
|
||||
AssetLoadError
|
||||
} from '../types/AssetTypes';
|
||||
import { IAssetLoader, ITextAsset } from '../interfaces/IAssetLoader';
|
||||
|
||||
/**
|
||||
* Text loader implementation
|
||||
* 文本加载器实现
|
||||
*/
|
||||
export class TextLoader implements IAssetLoader<ITextAsset> {
|
||||
readonly supportedType = AssetType.Text;
|
||||
readonly supportedExtensions = ['.txt', '.text', '.md', '.csv', '.xml', '.html', '.css', '.js', '.ts'];
|
||||
|
||||
/**
|
||||
* Load text asset
|
||||
* 加载文本资产
|
||||
*/
|
||||
async load(
|
||||
path: string,
|
||||
metadata: IAssetMetadata,
|
||||
options?: IAssetLoadOptions
|
||||
): Promise<IAssetLoadResult<ITextAsset>> {
|
||||
const startTime = performance.now();
|
||||
|
||||
try {
|
||||
const response = await this.fetchWithTimeout(path, options?.timeout);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
// 获取总大小用于进度回调 / Get total size for progress callback
|
||||
const contentLength = response.headers.get('content-length');
|
||||
const total = contentLength ? parseInt(contentLength, 10) : 0;
|
||||
|
||||
// 读取响应 / Read response
|
||||
let content: string;
|
||||
if (options?.onProgress && total > 0) {
|
||||
content = await this.readResponseWithProgress(response, total, options.onProgress);
|
||||
} else {
|
||||
content = await response.text();
|
||||
}
|
||||
|
||||
// 检测编码 / Detect encoding
|
||||
const encoding = this.detectEncoding(content);
|
||||
|
||||
const asset: ITextAsset = {
|
||||
content,
|
||||
encoding
|
||||
};
|
||||
|
||||
return {
|
||||
asset,
|
||||
handle: 0,
|
||||
metadata,
|
||||
loadTime: performance.now() - startTime
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw new AssetLoadError(
|
||||
`Failed to load text: ${error.message}`,
|
||||
metadata.guid,
|
||||
AssetType.Text,
|
||||
error
|
||||
);
|
||||
}
|
||||
throw AssetLoadError.fileNotFound(metadata.guid, path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch with timeout
|
||||
* 带超时的fetch
|
||||
*/
|
||||
private async fetchWithTimeout(url: string, timeout = 30000): Promise<Response> {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
signal: controller.signal,
|
||||
mode: 'cors',
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
return response;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read response with progress
|
||||
* 带进度读取响应
|
||||
*/
|
||||
private async readResponseWithProgress(
|
||||
response: Response,
|
||||
total: number,
|
||||
onProgress: (progress: number) => void
|
||||
): Promise<string> {
|
||||
const reader = response.body?.getReader();
|
||||
if (!reader) {
|
||||
return response.text();
|
||||
}
|
||||
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
let result = '';
|
||||
let receivedLength = 0;
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) break;
|
||||
|
||||
receivedLength += value.length;
|
||||
result += decoder.decode(value, { stream: true });
|
||||
|
||||
// 报告进度 / Report progress
|
||||
onProgress(receivedLength / total);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect text encoding
|
||||
* 检测文本编码
|
||||
*/
|
||||
private detectEncoding(content: string): 'utf8' | 'utf16' | 'ascii' {
|
||||
// 简单的编码检测 / Simple encoding detection
|
||||
// 检查是否包含非ASCII字符 / Check for non-ASCII characters
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
const charCode = content.charCodeAt(i);
|
||||
if (charCode > 127) {
|
||||
// 包含非ASCII字符,可能是UTF-8或UTF-16 / Contains non-ASCII, likely UTF-8 or UTF-16
|
||||
return charCode > 255 ? 'utf16' : 'utf8';
|
||||
}
|
||||
}
|
||||
return 'ascii';
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if the loader can handle this asset
|
||||
* 验证加载器是否可以处理此资产
|
||||
*/
|
||||
canLoad(path: string, _metadata: IAssetMetadata): boolean {
|
||||
const ext = path.toLowerCase().substring(path.lastIndexOf('.'));
|
||||
return this.supportedExtensions.includes(ext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate memory usage for the asset
|
||||
* 估算资产的内存使用量
|
||||
*/
|
||||
|
||||
/**
|
||||
* Dispose loaded asset
|
||||
* 释放已加载的资产
|
||||
*/
|
||||
dispose(asset: ITextAsset): void {
|
||||
// 清空内容以帮助GC / Clear content to help GC
|
||||
(asset as any).content = '';
|
||||
}
|
||||
}
|
||||
@@ -1,216 +0,0 @@
|
||||
/**
|
||||
* Texture asset loader
|
||||
* 纹理资产加载器
|
||||
*/
|
||||
|
||||
import {
|
||||
AssetType,
|
||||
IAssetLoadOptions,
|
||||
IAssetMetadata,
|
||||
IAssetLoadResult,
|
||||
AssetLoadError
|
||||
} from '../types/AssetTypes';
|
||||
import { IAssetLoader, ITextureAsset } from '../interfaces/IAssetLoader';
|
||||
|
||||
/**
|
||||
* Texture loader implementation
|
||||
* 纹理加载器实现
|
||||
*/
|
||||
export class TextureLoader implements IAssetLoader<ITextureAsset> {
|
||||
readonly supportedType = AssetType.Texture;
|
||||
readonly supportedExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp'];
|
||||
|
||||
private static _nextTextureId = 1;
|
||||
private readonly _loadedTextures = new Map<string, ITextureAsset>();
|
||||
|
||||
/**
|
||||
* Load texture asset
|
||||
* 加载纹理资产
|
||||
*/
|
||||
async load(
|
||||
path: string,
|
||||
metadata: IAssetMetadata,
|
||||
options?: IAssetLoadOptions
|
||||
): Promise<IAssetLoadResult<ITextureAsset>> {
|
||||
const startTime = performance.now();
|
||||
|
||||
// 检查缓存 / Check cache
|
||||
if (!options?.forceReload && this._loadedTextures.has(path)) {
|
||||
const cached = this._loadedTextures.get(path)!;
|
||||
return {
|
||||
asset: cached,
|
||||
handle: cached.textureId,
|
||||
metadata,
|
||||
loadTime: 0
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// 创建图像元素 / Create image element
|
||||
const image = await this.loadImage(path, options);
|
||||
|
||||
// 创建纹理资产 / Create texture asset
|
||||
const textureAsset: ITextureAsset = {
|
||||
textureId: TextureLoader._nextTextureId++,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
format: 'rgba', // 默认格式 / Default format
|
||||
hasMipmaps: false,
|
||||
data: image
|
||||
};
|
||||
|
||||
// 缓存纹理 / Cache texture
|
||||
this._loadedTextures.set(path, textureAsset);
|
||||
|
||||
// 触发引擎纹理加载(如果有引擎桥接) / Trigger engine texture loading if bridge exists
|
||||
if (typeof window !== 'undefined' && (window as any).engineBridge) {
|
||||
await this.uploadToGPU(textureAsset, path);
|
||||
}
|
||||
|
||||
return {
|
||||
asset: textureAsset,
|
||||
handle: textureAsset.textureId,
|
||||
metadata,
|
||||
loadTime: performance.now() - startTime
|
||||
};
|
||||
} catch (error) {
|
||||
throw AssetLoadError.fileNotFound(metadata.guid, path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load image from URL
|
||||
* 从URL加载图像
|
||||
*/
|
||||
private async loadImage(url: string, options?: IAssetLoadOptions): Promise<HTMLImageElement> {
|
||||
// For Tauri asset URLs, use fetch to load the image
|
||||
// 对于Tauri资产URL,使用fetch加载图像
|
||||
if (url.startsWith('http://asset.localhost/') || url.startsWith('asset://')) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch image: ${response.statusText}`);
|
||||
}
|
||||
const blob = await response.blob();
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const image = new Image();
|
||||
image.onload = () => {
|
||||
// Clean up blob URL after loading
|
||||
// 加载后清理blob URL
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
resolve(image);
|
||||
};
|
||||
image.onerror = () => {
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
reject(new Error(`Failed to load image from blob: ${url}`));
|
||||
};
|
||||
image.src = blobUrl;
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to load Tauri asset: ${url} - ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
// For regular URLs, use standard Image loading
|
||||
// 对于常规URL,使用标准Image加载
|
||||
return new Promise((resolve, reject) => {
|
||||
const image = new Image();
|
||||
image.crossOrigin = 'anonymous';
|
||||
|
||||
// 超时处理 / Timeout handling
|
||||
const timeout = options?.timeout || 30000;
|
||||
const timeoutId = setTimeout(() => {
|
||||
reject(new Error(`Image load timeout: ${url}`));
|
||||
}, timeout);
|
||||
|
||||
// 进度回调 / Progress callback
|
||||
if (options?.onProgress) {
|
||||
// 图像加载没有真正的进度事件,模拟进度 / Images don't have real progress events, simulate
|
||||
let progress = 0;
|
||||
const progressInterval = setInterval(() => {
|
||||
progress = Math.min(progress + 0.1, 0.9);
|
||||
options.onProgress!(progress);
|
||||
}, 100);
|
||||
|
||||
image.onload = () => {
|
||||
clearInterval(progressInterval);
|
||||
clearTimeout(timeoutId);
|
||||
options.onProgress!(1);
|
||||
resolve(image);
|
||||
};
|
||||
|
||||
image.onerror = () => {
|
||||
clearInterval(progressInterval);
|
||||
clearTimeout(timeoutId);
|
||||
reject(new Error(`Failed to load image: ${url}`));
|
||||
};
|
||||
} else {
|
||||
image.onload = () => {
|
||||
clearTimeout(timeoutId);
|
||||
resolve(image);
|
||||
};
|
||||
|
||||
image.onerror = () => {
|
||||
clearTimeout(timeoutId);
|
||||
reject(new Error(`Failed to load image: ${url}`));
|
||||
};
|
||||
}
|
||||
|
||||
image.src = url;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload texture to GPU
|
||||
* 上传纹理到GPU
|
||||
*/
|
||||
private async uploadToGPU(textureAsset: ITextureAsset, path: string): Promise<void> {
|
||||
const bridge = (window as any).engineBridge;
|
||||
if (bridge && bridge.loadTexture) {
|
||||
await bridge.loadTexture(textureAsset.textureId, path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if the loader can handle this asset
|
||||
* 验证加载器是否可以处理此资产
|
||||
*/
|
||||
canLoad(path: string, _metadata: IAssetMetadata): boolean {
|
||||
const ext = path.toLowerCase().substring(path.lastIndexOf('.'));
|
||||
return this.supportedExtensions.includes(ext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate memory usage for the asset
|
||||
* 估算资产的内存使用量
|
||||
*/
|
||||
|
||||
/**
|
||||
* Dispose loaded asset
|
||||
* 释放已加载的资产
|
||||
*/
|
||||
dispose(asset: ITextureAsset): void {
|
||||
// 从缓存中移除 / Remove from cache
|
||||
for (const [path, cached] of this._loadedTextures.entries()) {
|
||||
if (cached === asset) {
|
||||
this._loadedTextures.delete(path);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 释放GPU资源 / Release GPU resources
|
||||
if (typeof window !== 'undefined' && (window as any).engineBridge) {
|
||||
const bridge = (window as any).engineBridge;
|
||||
if (bridge.unloadTexture) {
|
||||
bridge.unloadTexture(asset.textureId);
|
||||
}
|
||||
}
|
||||
|
||||
// 清理图像数据 / Clean up image data
|
||||
if (asset.data instanceof HTMLImageElement) {
|
||||
asset.data.src = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
/**
|
||||
* 场景资源管理器 - 集中式场景资源加载
|
||||
* SceneResourceManager - Centralized resource loading for scenes
|
||||
*
|
||||
* 扫描场景中所有组件,收集资源引用,批量加载资源,并将运行时 ID 分配回组件
|
||||
* Scans all components in a scene, collects resource references, batch-loads them, and assigns runtime IDs back to components
|
||||
*/
|
||||
|
||||
import type { Scene } from '@esengine/ecs-framework';
|
||||
import { isResourceComponent, type ResourceReference } from '../interfaces/IResourceComponent';
|
||||
|
||||
/**
|
||||
* 资源加载器接口
|
||||
* Resource loader interface
|
||||
*/
|
||||
export interface IResourceLoader {
|
||||
/**
|
||||
* 批量加载资源并返回路径到 ID 的映射
|
||||
* Load a batch of resources and return path-to-ID mapping
|
||||
* @param paths 资源路径数组 / Array of resource paths
|
||||
* @param type 资源类型 / Resource type
|
||||
* @returns 路径到运行时 ID 的映射 / Map of paths to runtime IDs
|
||||
*/
|
||||
loadResourcesBatch(paths: string[], type: ResourceReference['type']): Promise<Map<string, number>>;
|
||||
}
|
||||
|
||||
export class SceneResourceManager {
|
||||
private resourceLoader: IResourceLoader | null = null;
|
||||
|
||||
/**
|
||||
* 设置资源加载器实现
|
||||
* Set the resource loader implementation
|
||||
*
|
||||
* 应由引擎集成层调用
|
||||
* This should be called by the engine integration layer
|
||||
*
|
||||
* @param loader 资源加载器实例 / Resource loader instance
|
||||
*/
|
||||
setResourceLoader(loader: IResourceLoader): void {
|
||||
this.resourceLoader = loader;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载场景所需的所有资源
|
||||
* Load all resources required by a scene
|
||||
*
|
||||
* 流程 / Process:
|
||||
* 1. 扫描所有实体并从 IResourceComponent 实现中收集资源引用
|
||||
* Scan all entities and collect resource references from IResourceComponent implementations
|
||||
* 2. 按类型分组资源(纹理、音频等)
|
||||
* Group resources by type (texture, audio, etc.)
|
||||
* 3. 批量加载每种资源类型
|
||||
* Batch load each resource type
|
||||
* 4. 将运行时 ID 分配回组件
|
||||
* Assign runtime IDs back to components
|
||||
*
|
||||
* @param scene 要加载资源的场景 / The scene to load resources for
|
||||
* @returns 当所有资源加载完成时解析的 Promise / Promise that resolves when all resources are loaded
|
||||
*/
|
||||
async loadSceneResources(scene: Scene): Promise<void> {
|
||||
if (!this.resourceLoader) {
|
||||
console.warn('[SceneResourceManager] No resource loader set, skipping resource loading');
|
||||
return;
|
||||
}
|
||||
|
||||
// 从组件收集所有资源引用 / Collect all resource references from components
|
||||
const resourceRefs = this.collectResourceReferences(scene);
|
||||
|
||||
if (resourceRefs.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 按资源类型分组 / Group by resource type
|
||||
const resourcesByType = new Map<ResourceReference['type'], Set<string>>();
|
||||
for (const ref of resourceRefs) {
|
||||
if (!resourcesByType.has(ref.type)) {
|
||||
resourcesByType.set(ref.type, new Set());
|
||||
}
|
||||
resourcesByType.get(ref.type)!.add(ref.path);
|
||||
}
|
||||
|
||||
// 批量加载每种资源类型 / Load each resource type in batch
|
||||
const allResourceIds = new Map<string, number>();
|
||||
|
||||
for (const [type, paths] of resourcesByType) {
|
||||
const pathsArray = Array.from(paths);
|
||||
|
||||
try {
|
||||
const resourceIds = await this.resourceLoader.loadResourcesBatch(pathsArray, type);
|
||||
|
||||
// 合并到总映射表 / Merge into combined map
|
||||
for (const [path, id] of resourceIds) {
|
||||
allResourceIds.set(path, id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[SceneResourceManager] Failed to load ${type} resources:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// 将资源 ID 分配回组件 / Assign resource IDs back to components
|
||||
this.assignResourceIds(scene, allResourceIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从场景实体收集所有资源引用
|
||||
* Collect all resource references from scene entities
|
||||
*/
|
||||
private collectResourceReferences(scene: Scene): ResourceReference[] {
|
||||
const refs: ResourceReference[] = [];
|
||||
|
||||
for (const entity of scene.entities.buffer) {
|
||||
for (const component of entity.components) {
|
||||
if (isResourceComponent(component)) {
|
||||
const componentRefs = component.getResourceReferences();
|
||||
refs.push(...componentRefs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return refs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将已加载的资源 ID 分配回组件
|
||||
* Assign loaded resource IDs back to components
|
||||
*
|
||||
* @param scene 场景 / Scene
|
||||
* @param pathToId 路径到 ID 的映射 / Path to ID mapping
|
||||
*/
|
||||
private assignResourceIds(scene: Scene, pathToId: Map<string, number>): void {
|
||||
for (const entity of scene.entities.buffer) {
|
||||
for (const component of entity.components) {
|
||||
if (isResourceComponent(component)) {
|
||||
component.setResourceIds(pathToId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 卸载场景使用的所有资源
|
||||
* Unload all resources used by a scene
|
||||
*
|
||||
* 在场景销毁时调用
|
||||
* Called when a scene is being destroyed
|
||||
*
|
||||
* @param scene 要卸载资源的场景 / The scene to unload resources for
|
||||
*/
|
||||
async unloadSceneResources(_scene: Scene): Promise<void> {
|
||||
// TODO: 实现资源卸载 / Implement resource unloading
|
||||
// 需要跟踪资源引用计数,仅在不再使用时卸载
|
||||
// Need to track resource reference counts and only unload when no longer used
|
||||
console.log('[SceneResourceManager] Scene resource unloading not yet implemented');
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
{
|
||||
"name": "@esengine/behavior-tree",
|
||||
"version": "1.0.1",
|
||||
"description": "ECS-based AI behavior tree system with visual editor and runtime execution",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
},
|
||||
"./runtime": {
|
||||
"types": "./dist/runtime.d.ts",
|
||||
"import": "./dist/runtime.js"
|
||||
},
|
||||
"./editor": {
|
||||
"types": "./dist/editor/index.d.ts",
|
||||
"import": "./dist/editor/index.js"
|
||||
},
|
||||
"./plugin.json": "./plugin.json"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"plugin.json"
|
||||
],
|
||||
"keywords": [
|
||||
"ecs",
|
||||
"behavior-tree",
|
||||
"ai",
|
||||
"game-ai",
|
||||
"entity-component-system"
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "rimraf dist tsconfig.tsbuildinfo",
|
||||
"build": "vite build",
|
||||
"build:watch": "vite build --watch",
|
||||
"type-check": "tsc --noEmit",
|
||||
"test": "jest --config jest.config.cjs",
|
||||
"test:watch": "jest --watch --config jest.config.cjs"
|
||||
},
|
||||
"author": "yhh",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@esengine/ecs-framework": ">=2.0.0",
|
||||
"@esengine/ecs-components": "workspace:*",
|
||||
"@esengine/editor-runtime": "workspace:*",
|
||||
"lucide-react": "^0.545.0",
|
||||
"react": "^18.3.1",
|
||||
"zustand": "^4.5.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@esengine/ecs-components": {
|
||||
"optional": true
|
||||
},
|
||||
"@esengine/editor-runtime": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"lucide-react": {
|
||||
"optional": true
|
||||
},
|
||||
"zustand": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/plugin-fs": "^2.4.2",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^20.19.17",
|
||||
"@types/react": "^18.3.12",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"jest": "^29.7.0",
|
||||
"rimraf": "^5.0.0",
|
||||
"ts-jest": "^29.4.0",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.0.7",
|
||||
"vite-plugin-dts": "^3.7.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/esengine/ecs-framework.git",
|
||||
"directory": "packages/behavior-tree"
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
/**
|
||||
* Behavior Tree Runtime Module (Pure runtime, no editor dependencies)
|
||||
* 行为树运行时模块(纯运行时,无编辑器依赖)
|
||||
*/
|
||||
|
||||
import type { IScene, ServiceContainer } from '@esengine/ecs-framework';
|
||||
import { ComponentRegistry, Core } from '@esengine/ecs-framework';
|
||||
import type { IRuntimeModuleLoader, SystemContext } from '@esengine/ecs-components';
|
||||
|
||||
import { BehaviorTreeRuntimeComponent } from './execution/BehaviorTreeRuntimeComponent';
|
||||
import { BehaviorTreeExecutionSystem } from './execution/BehaviorTreeExecutionSystem';
|
||||
import { BehaviorTreeAssetManager } from './execution/BehaviorTreeAssetManager';
|
||||
import { GlobalBlackboardService } from './Services/GlobalBlackboardService';
|
||||
|
||||
/**
|
||||
* Behavior Tree Runtime Module
|
||||
* 行为树运行时模块
|
||||
*/
|
||||
export class BehaviorTreeRuntimeModule implements IRuntimeModuleLoader {
|
||||
registerComponents(registry: typeof ComponentRegistry): void {
|
||||
registry.register(BehaviorTreeRuntimeComponent);
|
||||
}
|
||||
|
||||
registerServices(services: ServiceContainer): void {
|
||||
if (!services.isRegistered(GlobalBlackboardService)) {
|
||||
services.registerSingleton(GlobalBlackboardService);
|
||||
}
|
||||
if (!services.isRegistered(BehaviorTreeAssetManager)) {
|
||||
services.registerSingleton(BehaviorTreeAssetManager);
|
||||
}
|
||||
}
|
||||
|
||||
createSystems(scene: IScene, context: SystemContext): void {
|
||||
const behaviorTreeSystem = new BehaviorTreeExecutionSystem(Core);
|
||||
|
||||
if (context.isEditor) {
|
||||
behaviorTreeSystem.enabled = false;
|
||||
}
|
||||
|
||||
scene.addSystem(behaviorTreeSystem);
|
||||
context.behaviorTreeSystem = behaviorTreeSystem;
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
/**
|
||||
* Behavior Tree Unified Plugin
|
||||
* 行为树统一插件
|
||||
*/
|
||||
|
||||
import type { IScene, ServiceContainer } from '@esengine/ecs-framework';
|
||||
import { ComponentRegistry, Core } from '@esengine/ecs-framework';
|
||||
import type {
|
||||
IPluginLoader,
|
||||
IRuntimeModuleLoader,
|
||||
PluginDescriptor,
|
||||
SystemContext
|
||||
} from '@esengine/editor-runtime';
|
||||
|
||||
// Runtime imports
|
||||
import { BehaviorTreeRuntimeComponent } from '../execution/BehaviorTreeRuntimeComponent';
|
||||
import { BehaviorTreeExecutionSystem } from '../execution/BehaviorTreeExecutionSystem';
|
||||
import { BehaviorTreeAssetManager } from '../execution/BehaviorTreeAssetManager';
|
||||
import { GlobalBlackboardService } from '../Services/GlobalBlackboardService';
|
||||
|
||||
/**
|
||||
* 插件描述符
|
||||
*/
|
||||
export const descriptor: PluginDescriptor = {
|
||||
id: '@esengine/behavior-tree',
|
||||
name: 'Behavior Tree System',
|
||||
version: '1.0.0',
|
||||
description: 'AI 行为树系统,支持可视化编辑和运行时执行',
|
||||
category: 'ai',
|
||||
enabledByDefault: true,
|
||||
canContainContent: false,
|
||||
isEnginePlugin: false,
|
||||
modules: [
|
||||
{
|
||||
name: 'BehaviorTreeRuntime',
|
||||
type: 'runtime',
|
||||
loadingPhase: 'default',
|
||||
entry: './src/index.ts'
|
||||
},
|
||||
{
|
||||
name: 'BehaviorTreeEditor',
|
||||
type: 'editor',
|
||||
loadingPhase: 'default',
|
||||
entry: './src/editor/index.ts'
|
||||
}
|
||||
],
|
||||
dependencies: [
|
||||
{ id: '@esengine/core', version: '>=1.0.0' }
|
||||
],
|
||||
icon: 'GitBranch'
|
||||
};
|
||||
|
||||
/**
|
||||
* Behavior Tree Runtime Module
|
||||
* 行为树运行时模块
|
||||
*/
|
||||
export class BehaviorTreeRuntimeModule implements IRuntimeModuleLoader {
|
||||
registerComponents(registry: typeof ComponentRegistry): void {
|
||||
registry.register(BehaviorTreeRuntimeComponent);
|
||||
}
|
||||
|
||||
registerServices(services: ServiceContainer): void {
|
||||
if (!services.isRegistered(GlobalBlackboardService)) {
|
||||
services.registerSingleton(GlobalBlackboardService);
|
||||
}
|
||||
if (!services.isRegistered(BehaviorTreeAssetManager)) {
|
||||
services.registerSingleton(BehaviorTreeAssetManager);
|
||||
}
|
||||
}
|
||||
|
||||
createSystems(scene: IScene, context: SystemContext): void {
|
||||
const behaviorTreeSystem = new BehaviorTreeExecutionSystem(Core);
|
||||
|
||||
// 编辑器模式下默认禁用
|
||||
if (context.isEditor) {
|
||||
behaviorTreeSystem.enabled = false;
|
||||
}
|
||||
|
||||
scene.addSystem(behaviorTreeSystem);
|
||||
|
||||
// 保存引用
|
||||
context.behaviorTreeSystem = behaviorTreeSystem;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Behavior Tree Plugin Loader
|
||||
* 行为树插件加载器
|
||||
*
|
||||
* 注意:editorModule 在 ./index.ts 中通过 createBehaviorTreePlugin() 设置
|
||||
*/
|
||||
export const BehaviorTreePlugin: IPluginLoader = {
|
||||
descriptor,
|
||||
runtimeModule: new BehaviorTreeRuntimeModule(),
|
||||
// editorModule 将在 index.ts 中设置
|
||||
};
|
||||
|
||||
export default BehaviorTreePlugin;
|
||||
@@ -1,60 +0,0 @@
|
||||
import { type RefObject, createLogger } from '@esengine/editor-runtime';
|
||||
import { Node as BehaviorTreeNode } from '../domain/models/Node';
|
||||
|
||||
const logger = createLogger('portUtils');
|
||||
|
||||
/**
|
||||
* 获取端口在画布世界坐标系中的位置
|
||||
* 直接从 DOM 元素获取实际渲染位置,避免硬编码和手动计算
|
||||
*/
|
||||
export function getPortPosition(
|
||||
canvasRef: RefObject<HTMLDivElement>,
|
||||
canvasOffset: { x: number; y: number },
|
||||
canvasScale: number,
|
||||
nodes: BehaviorTreeNode[],
|
||||
nodeId: string,
|
||||
propertyName?: string,
|
||||
portType: 'input' | 'output' = 'output',
|
||||
draggingNodeId?: string | null,
|
||||
dragDelta?: { dx: number; dy: number },
|
||||
selectedNodeIds?: string[]
|
||||
): { x: number; y: number } | null {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return null;
|
||||
|
||||
const node = nodes.find((n: BehaviorTreeNode) => n.id === nodeId);
|
||||
if (!node) return null;
|
||||
|
||||
// 构造端口选择器
|
||||
let portSelector: string;
|
||||
|
||||
if (propertyName) {
|
||||
// 属性端口:使用 data-property 属性定位
|
||||
portSelector = `[data-node-id="${nodeId}"][data-property="${propertyName}"]`;
|
||||
} else {
|
||||
// 节点端口:使用 data-port-type 属性定位
|
||||
const portTypeAttr = portType === 'input' ? 'node-input' : 'node-output';
|
||||
portSelector = `[data-node-id="${nodeId}"][data-port-type="${portTypeAttr}"]`;
|
||||
}
|
||||
|
||||
const portElement = canvas.querySelector(portSelector) as HTMLElement;
|
||||
if (!portElement) {
|
||||
logger.warn(`Port not found: ${portSelector}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取端口和画布的屏幕矩形
|
||||
const portRect = portElement.getBoundingClientRect();
|
||||
const canvasRect = canvas.getBoundingClientRect();
|
||||
|
||||
// 计算端口中心相对于画布的屏幕坐标
|
||||
const screenX = portRect.left + portRect.width / 2 - canvasRect.left;
|
||||
const screenY = portRect.top + portRect.height / 2 - canvasRect.top;
|
||||
|
||||
// 转换为世界坐标
|
||||
// 屏幕坐标到世界坐标的转换:world = (screen - offset) / scale
|
||||
const worldX = (screenX - canvasOffset.x) / canvasScale;
|
||||
const worldY = (screenY - canvasOffset.y) / canvasScale;
|
||||
|
||||
return { x: worldX, y: worldY };
|
||||
}
|
||||
@@ -1,237 +0,0 @@
|
||||
import { EntitySystem, Matcher, Entity, Time, Core, ECSSystem } from '@esengine/ecs-framework';
|
||||
import { BehaviorTreeRuntimeComponent } from './BehaviorTreeRuntimeComponent';
|
||||
import { BehaviorTreeAssetManager } from './BehaviorTreeAssetManager';
|
||||
import { NodeExecutorRegistry, NodeExecutionContext } from './NodeExecutor';
|
||||
import { BehaviorTreeData, BehaviorNodeData } from './BehaviorTreeData';
|
||||
import { TaskStatus } from '../Types/TaskStatus';
|
||||
import { NodeMetadataRegistry } from './NodeMetadata';
|
||||
import './Executors';
|
||||
|
||||
/**
|
||||
* 行为树执行系统
|
||||
*
|
||||
* 统一处理所有行为树的执行
|
||||
*/
|
||||
@ECSSystem('BehaviorTreeExecution')
|
||||
export class BehaviorTreeExecutionSystem extends EntitySystem {
|
||||
private assetManager: BehaviorTreeAssetManager | null = null;
|
||||
private executorRegistry: NodeExecutorRegistry;
|
||||
private coreInstance: typeof Core | null = null;
|
||||
|
||||
constructor(coreInstance?: typeof Core) {
|
||||
super(Matcher.empty().all(BehaviorTreeRuntimeComponent));
|
||||
this.coreInstance = coreInstance || null;
|
||||
this.executorRegistry = new NodeExecutorRegistry();
|
||||
this.registerBuiltInExecutors();
|
||||
}
|
||||
|
||||
private getAssetManager(): BehaviorTreeAssetManager {
|
||||
if (!this.assetManager) {
|
||||
const core = this.coreInstance || Core;
|
||||
this.assetManager = core.services.resolve(BehaviorTreeAssetManager);
|
||||
}
|
||||
return this.assetManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册所有执行器(包括内置和插件提供的)
|
||||
*/
|
||||
private registerBuiltInExecutors(): void {
|
||||
const constructors = NodeMetadataRegistry.getAllExecutorConstructors();
|
||||
|
||||
for (const [implementationType, ExecutorClass] of constructors) {
|
||||
try {
|
||||
const instance = new ExecutorClass();
|
||||
this.executorRegistry.register(implementationType, instance);
|
||||
} catch (error) {
|
||||
this.logger.error(`注册执行器失败: ${implementationType}`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取执行器注册表
|
||||
*/
|
||||
getExecutorRegistry(): NodeExecutorRegistry {
|
||||
return this.executorRegistry;
|
||||
}
|
||||
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent)!;
|
||||
|
||||
if (!runtime.isRunning) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const treeData = this.getAssetManager().getAsset(runtime.treeAssetId);
|
||||
if (!treeData) {
|
||||
this.logger.warn(`未找到行为树资产: ${runtime.treeAssetId}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 如果标记了需要重置,先重置状态
|
||||
if (runtime.needsReset) {
|
||||
runtime.resetAllStates();
|
||||
runtime.needsReset = false;
|
||||
}
|
||||
|
||||
this.executeTree(entity, runtime, treeData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行整个行为树
|
||||
*/
|
||||
private executeTree(
|
||||
entity: Entity,
|
||||
runtime: BehaviorTreeRuntimeComponent,
|
||||
treeData: BehaviorTreeData
|
||||
): void {
|
||||
const rootNode = treeData.nodes.get(treeData.rootNodeId);
|
||||
if (!rootNode) {
|
||||
this.logger.error(`未找到根节点: ${treeData.rootNodeId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const status = this.executeNode(entity, runtime, rootNode, treeData);
|
||||
|
||||
// 如果树完成了,标记在下一个tick时重置状态
|
||||
// 这样UI可以看到节点的最终状态
|
||||
if (status !== TaskStatus.Running) {
|
||||
runtime.needsReset = true;
|
||||
} else {
|
||||
runtime.needsReset = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行单个节点
|
||||
*/
|
||||
private executeNode(
|
||||
entity: Entity,
|
||||
runtime: BehaviorTreeRuntimeComponent,
|
||||
nodeData: BehaviorNodeData,
|
||||
treeData: BehaviorTreeData
|
||||
): TaskStatus {
|
||||
const state = runtime.getNodeState(nodeData.id);
|
||||
|
||||
if (runtime.shouldAbort(nodeData.id)) {
|
||||
runtime.clearAbortRequest(nodeData.id);
|
||||
state.isAborted = true;
|
||||
|
||||
const executor = this.executorRegistry.get(nodeData.implementationType);
|
||||
if (executor && executor.reset) {
|
||||
const context = this.createContext(entity, runtime, nodeData, treeData);
|
||||
executor.reset(context);
|
||||
}
|
||||
|
||||
runtime.activeNodeIds.delete(nodeData.id);
|
||||
state.status = TaskStatus.Failure;
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
runtime.activeNodeIds.add(nodeData.id);
|
||||
state.isAborted = false;
|
||||
|
||||
if (state.executionOrder === undefined) {
|
||||
runtime.executionOrderCounter++;
|
||||
state.executionOrder = runtime.executionOrderCounter;
|
||||
}
|
||||
|
||||
const executor = this.executorRegistry.get(nodeData.implementationType);
|
||||
if (!executor) {
|
||||
this.logger.error(`未找到执行器: ${nodeData.implementationType}`);
|
||||
state.status = TaskStatus.Failure;
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const context = this.createContext(entity, runtime, nodeData, treeData);
|
||||
|
||||
try {
|
||||
const status = executor.execute(context);
|
||||
state.status = status;
|
||||
|
||||
if (status !== TaskStatus.Running) {
|
||||
runtime.activeNodeIds.delete(nodeData.id);
|
||||
|
||||
if (executor.reset) {
|
||||
executor.reset(context);
|
||||
}
|
||||
}
|
||||
|
||||
return status;
|
||||
} catch (error) {
|
||||
this.logger.error(`执行节点时发生错误: ${nodeData.name}`, error);
|
||||
state.status = TaskStatus.Failure;
|
||||
runtime.activeNodeIds.delete(nodeData.id);
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建执行上下文
|
||||
*/
|
||||
private createContext(
|
||||
entity: Entity,
|
||||
runtime: BehaviorTreeRuntimeComponent,
|
||||
nodeData: BehaviorNodeData,
|
||||
treeData: BehaviorTreeData
|
||||
): NodeExecutionContext {
|
||||
return {
|
||||
entity,
|
||||
nodeData,
|
||||
state: runtime.getNodeState(nodeData.id),
|
||||
runtime,
|
||||
treeData,
|
||||
deltaTime: Time.deltaTime,
|
||||
totalTime: Time.totalTime,
|
||||
executeChild: (childId: string) => {
|
||||
const childData = treeData.nodes.get(childId);
|
||||
if (!childData) {
|
||||
this.logger.warn(`未找到子节点: ${childId}`);
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
return this.executeNode(entity, runtime, childData, treeData);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行子节点列表
|
||||
*/
|
||||
executeChildren(
|
||||
context: NodeExecutionContext,
|
||||
childIndices?: number[]
|
||||
): TaskStatus[] {
|
||||
const { nodeData, treeData, entity, runtime } = context;
|
||||
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const results: TaskStatus[] = [];
|
||||
const indicesToExecute = childIndices ||
|
||||
Array.from({ length: nodeData.children.length }, (_, i) => i);
|
||||
|
||||
for (const index of indicesToExecute) {
|
||||
if (index >= nodeData.children.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const childId = nodeData.children[index]!;
|
||||
const childData = treeData.nodes.get(childId);
|
||||
|
||||
if (!childData) {
|
||||
this.logger.warn(`未找到子节点: ${childId}`);
|
||||
results.push(TaskStatus.Failure);
|
||||
continue;
|
||||
}
|
||||
|
||||
const status = this.executeNode(entity, runtime, childData, treeData);
|
||||
results.push(status);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user