Compare commits
32 Commits
@esengine/
...
feat/docs-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34de1e5edf | ||
|
|
94e0979941 | ||
|
|
0a3f2a3e21 | ||
|
|
9c30ab26a6 | ||
|
|
3c50795dee | ||
|
|
5a0d67b3f6 | ||
|
|
d1ba10564a | ||
|
|
cf00e062f7 | ||
|
|
293ac2dca3 | ||
|
|
f7535a2aac | ||
|
|
ca18be32a8 | ||
|
|
025ce89ded | ||
|
|
2311419e71 | ||
|
|
373bdd5d2b | ||
|
|
b58e75d9a4 | ||
|
|
099809a98c | ||
|
|
83aee02540 | ||
|
|
cb1b171216 | ||
|
|
b64b489b89 | ||
|
|
13cb670a16 | ||
|
|
37ab494e4a | ||
|
|
e1d494b415 | ||
|
|
243b929d5e | ||
|
|
4a2362edf2 | ||
|
|
0c590d7c12 | ||
|
|
c2f8cb5272 | ||
|
|
55f644a091 | ||
|
|
d3dfaa7aac | ||
|
|
25e70a1d7b | ||
|
|
e2cca5e490 | ||
|
|
b3f7676452 | ||
|
|
e6fb80d0be |
@@ -1,8 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,56 +0,0 @@
|
||||
{
|
||||
"$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/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/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/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']
|
||||
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']
|
||||
|
||||
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/esengine
|
||||
url: https://deepwiki.com/esengine/ecs-framework
|
||||
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/esengine/discussions
|
||||
url: https://github.com/esengine/ecs-framework/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/esengine)
|
||||
- [📖 AI 文档助手](https://deepwiki.com/esengine/ecs-framework)
|
||||
- [💬 QQ 交流群](https://jq.qq.com/?_wv=1027&k=29w1Nud6)
|
||||
|
||||
💡 Tip: For simple questions, please check first:
|
||||
- [📚 Documentation](https://esengine.github.io/ecs-framework/)
|
||||
- [📖 AI Documentation](https://deepwiki.com/esengine/esengine)
|
||||
- [📖 AI Documentation](https://deepwiki.com/esengine/ecs-framework)
|
||||
|
||||
- type: textarea
|
||||
id: question
|
||||
|
||||
5
.github/codeql/codeql-config.yml
vendored
5
.github/codeql/codeql-config.yml
vendored
@@ -6,8 +6,3 @@ paths-ignore:
|
||||
- "**/node_modules"
|
||||
- "**/dist"
|
||||
- "**/bin"
|
||||
- "**/tests"
|
||||
- "**/*.test.ts"
|
||||
- "**/*.spec.ts"
|
||||
- "**/test"
|
||||
- "**/__tests__"
|
||||
|
||||
32
.github/labeler.yml
vendored
Normal file
32
.github/labeler.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
# 自动标签配置
|
||||
# 根据 issue/PR 内容自动打标签
|
||||
|
||||
'bug':
|
||||
- '/(bug|错误|崩溃|crash|error|exception|问题)/i'
|
||||
|
||||
'enhancement':
|
||||
- '/(feature|功能|enhancement|improve|优化|建议)/i'
|
||||
|
||||
'documentation':
|
||||
- '/(doc|文档|readme|guide|tutorial|教程)/i'
|
||||
|
||||
'question':
|
||||
- '/(question|疑问|how to|如何|怎么)/i'
|
||||
|
||||
'performance':
|
||||
- '/(performance|性能|slow|慢|lag|卡顿|optimize)/i'
|
||||
|
||||
'core':
|
||||
- '/(@esengine\/ecs-framework|packages\/core|core package)/i'
|
||||
|
||||
'editor':
|
||||
- '/(editor|编辑器|tauri)/i'
|
||||
|
||||
'network':
|
||||
- '/(network|网络|multiplayer|多人)/i'
|
||||
|
||||
'help wanted':
|
||||
- '/(help wanted|需要帮助|求助)/i'
|
||||
|
||||
'good first issue':
|
||||
- '/(good first issue|新手友好|beginner)/i'
|
||||
73
.github/workflows/ai-batch-analyze-issues.yml
vendored
Normal file
73
.github/workflows/ai-batch-analyze-issues.yml
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
name: AI Batch Analyze Issues
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
mode:
|
||||
description: '分析模式'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- 'recent' # 最近 10 个 issue
|
||||
- 'open' # 所有打开的 issue
|
||||
- 'all' # 所有 issue(慎用)
|
||||
default: 'recent'
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
|
||||
- name: Install GitHub CLI
|
||||
run: |
|
||||
gh --version || (curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
|
||||
sudo apt update
|
||||
sudo apt install gh)
|
||||
|
||||
- name: Batch Analyze Issues
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
MODE="${{ github.event.inputs.mode }}"
|
||||
|
||||
# 获取 issue 列表
|
||||
if [ "$MODE" = "recent" ]; then
|
||||
echo "📊 分析最近 10 个 issue..."
|
||||
ISSUES=$(gh issue list --limit 10 --json number --jq '.[].number')
|
||||
elif [ "$MODE" = "open" ]; then
|
||||
echo "📊 分析所有打开的 issue..."
|
||||
ISSUES=$(gh issue list --state open --json number --jq '.[].number')
|
||||
else
|
||||
echo "📊 分析所有 issue(这可能需要很长时间)..."
|
||||
ISSUES=$(gh issue list --state all --limit 100 --json number --jq '.[].number')
|
||||
fi
|
||||
|
||||
# 为每个 issue 添加 AI 分析评论
|
||||
for issue_num in $ISSUES; do
|
||||
echo "🤖 分析 Issue #$issue_num..."
|
||||
|
||||
# 获取 issue 内容
|
||||
ISSUE_BODY=$(gh issue view $issue_num --json body --jq '.body')
|
||||
ISSUE_TITLE=$(gh issue view $issue_num --json title --jq '.title')
|
||||
|
||||
# 添加触发评论
|
||||
gh issue comment $issue_num --body "@ai-helper 请帮我分析这个 issue" || true
|
||||
|
||||
# 避免 API 限制
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "✅ 批量分析完成!"
|
||||
echo "查看结果:https://github.com/${{ github.repository }}/issues"
|
||||
61
.github/workflows/ai-helper-tip.yml
vendored
Normal file
61
.github/workflows/ai-helper-tip.yml
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
name: AI Helper Tip
|
||||
|
||||
# 对所有新创建的 issue 自动回复 AI 助手使用说明(新老用户都适用)
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
tip:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Post AI Helper Usage Tip
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const message = [
|
||||
"## 🤖 AI 助手可用 | AI Helper Available",
|
||||
"",
|
||||
"**中文说明:**",
|
||||
"",
|
||||
"本项目配备了 AI 智能助手,可以帮助你快速获得解答!",
|
||||
"",
|
||||
"**使用方法:** 在评论中提及 `@ai-helper`,AI 会自动搜索项目代码并提供解决方案。",
|
||||
"",
|
||||
"**示例:**",
|
||||
"```",
|
||||
"@ai-helper 如何创建一个新的 System?",
|
||||
"@ai-helper 这个报错是什么原因?",
|
||||
"```",
|
||||
"",
|
||||
"---",
|
||||
"",
|
||||
"**English:**",
|
||||
"",
|
||||
"This project has an AI assistant to help you get answers quickly!",
|
||||
"",
|
||||
"**How to use:** Mention `@ai-helper` in a comment, and AI will automatically search the codebase and provide solutions.",
|
||||
"",
|
||||
"**Examples:**",
|
||||
"```",
|
||||
"@ai-helper How do I create a new System?",
|
||||
"@ai-helper What causes this error?",
|
||||
"```",
|
||||
"",
|
||||
"---",
|
||||
"",
|
||||
"💡 *AI 助手基于代码库提供建议,复杂问题建议等待维护者回复*",
|
||||
"💡 *AI suggestions are based on the codebase. For complex issues, please wait for maintainer responses*"
|
||||
].join('\n');
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: message
|
||||
});
|
||||
|
||||
console.log('✅ AI helper tip posted successfully');
|
||||
85
.github/workflows/ai-issue-helper.yml
vendored
Normal file
85
.github/workflows/ai-issue-helper.yml
vendored
Normal file
@@ -0,0 +1,85 @@
|
||||
name: AI Issue Helper
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
contents: read
|
||||
models: read
|
||||
|
||||
jobs:
|
||||
ai-helper:
|
||||
runs-on: ubuntu-latest
|
||||
# 只在真实用户提到 @ai-helper 时触发,忽略机器人评论
|
||||
if: |
|
||||
contains(github.event.comment.body, '@ai-helper') &&
|
||||
github.event.comment.user.type != 'Bot'
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get Issue Details
|
||||
id: issue
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const issue = await github.rest.issues.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number
|
||||
});
|
||||
|
||||
// 限制长度,避免超过 token 限制
|
||||
const maxLength = 1000;
|
||||
const truncate = (str, max) => {
|
||||
if (!str) return '';
|
||||
return str.length > max ? str.substring(0, max) + '...[内容过长已截断]' : str;
|
||||
};
|
||||
|
||||
core.exportVariable('ISSUE_TITLE', truncate(issue.data.title || '', 200));
|
||||
core.exportVariable('ISSUE_BODY', truncate(issue.data.body || '', maxLength));
|
||||
core.exportVariable('COMMENT_BODY', truncate(context.payload.comment.body || '', 500));
|
||||
core.exportVariable('ISSUE_NUMBER', context.issue.number);
|
||||
|
||||
- name: Create Prompt
|
||||
id: prompt
|
||||
run: |
|
||||
cat > prompt.txt << 'PROMPT_EOF'
|
||||
Issue #${{ env.ISSUE_NUMBER }}
|
||||
|
||||
标题: ${{ env.ISSUE_TITLE }}
|
||||
|
||||
内容: ${{ env.ISSUE_BODY }}
|
||||
|
||||
评论: ${{ env.COMMENT_BODY }}
|
||||
|
||||
请搜索项目代码并提供解决方案。
|
||||
PROMPT_EOF
|
||||
|
||||
- name: AI Analysis
|
||||
uses: actions/ai-inference@v1
|
||||
id: ai
|
||||
with:
|
||||
model: 'gpt-4o'
|
||||
enable-github-mcp: true
|
||||
max-tokens: 1500
|
||||
system-prompt: |
|
||||
你是 ECS Framework (TypeScript ECS 框架) 的 AI 助手。
|
||||
主要代码在 packages/core/src。
|
||||
搜索相关代码后,用中文简洁回答问题,包含问题分析、解决方案和代码引用。
|
||||
prompt-file: prompt.txt
|
||||
|
||||
- name: Post AI Response
|
||||
env:
|
||||
AI_RESPONSE: ${{ steps.ai.outputs.response }}
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: process.env.AI_RESPONSE
|
||||
});
|
||||
56
.github/workflows/ai-issue-moderator.yml
vendored
Normal file
56
.github/workflows/ai-issue-moderator.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
name: AI Issue Moderator
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
contents: read
|
||||
models: read
|
||||
|
||||
jobs:
|
||||
moderate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check Content
|
||||
uses: actions/ai-inference@v1
|
||||
id: check
|
||||
with:
|
||||
model: 'gpt-4o-mini'
|
||||
system-prompt: |
|
||||
你是一个内容审查助手。
|
||||
检查内容是否包含:
|
||||
1. 垃圾信息或广告
|
||||
2. 恶意或攻击性内容
|
||||
3. 与项目完全无关的内容
|
||||
|
||||
只返回 "SPAM" 或 "OK",不要其他内容。
|
||||
prompt: |
|
||||
标题:${{ github.event.issue.title || github.event.comment.body }}
|
||||
|
||||
内容:
|
||||
${{ github.event.issue.body || github.event.comment.body }}
|
||||
|
||||
- name: Mark as Spam
|
||||
if: contains(steps.check.outputs.response, 'SPAM')
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
// 添加 spam 标签
|
||||
github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
labels: ['spam']
|
||||
});
|
||||
|
||||
// 添加评论
|
||||
github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: '🤖 这个内容被 AI 检测为可能的垃圾内容。如果这是误判,请联系维护者。\n\n🤖 This content was detected as potential spam by AI. If this is a false positive, please contact the maintainers.'
|
||||
});
|
||||
160
.github/workflows/batch-label-issues.yml
vendored
Normal file
160
.github/workflows/batch-label-issues.yml
vendored
Normal file
@@ -0,0 +1,160 @@
|
||||
name: Batch Label Issues
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
mode:
|
||||
description: '标签模式'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- 'recent' # 最近 20 个 issue
|
||||
- 'open' # 所有打开的 issue
|
||||
- 'unlabeled' # 只处理没有标签的 issue
|
||||
- 'all' # 所有 issue(慎用)
|
||||
default: 'recent'
|
||||
|
||||
skip_labeled:
|
||||
description: '跳过已有标签的 issue'
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
batch-label:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
|
||||
- name: Batch Label Issues
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
MODE="${{ github.event.inputs.mode }}"
|
||||
SKIP_LABELED="${{ github.event.inputs.skip_labeled }}"
|
||||
|
||||
echo "📊 开始批量打标签..."
|
||||
echo "模式: $MODE"
|
||||
echo "跳过已标签: $SKIP_LABELED"
|
||||
|
||||
# 获取 issue 列表
|
||||
if [ "$MODE" = "recent" ]; then
|
||||
echo "📋 获取最近 20 个 issue..."
|
||||
ISSUES=$(gh issue list --limit 20 --json number,labels,title,body --jq '.[] | {number, labels: [.labels[].name], title, body}')
|
||||
elif [ "$MODE" = "open" ]; then
|
||||
echo "📋 获取所有打开的 issue..."
|
||||
ISSUES=$(gh issue list --state open --json number,labels,title,body --jq '.[] | {number, labels: [.labels[].name], title, body}')
|
||||
elif [ "$MODE" = "unlabeled" ]; then
|
||||
echo "📋 获取没有标签的 issue..."
|
||||
ISSUES=$(gh issue list --state all --json number,labels,title,body --jq '.[] | select(.labels | length == 0) | {number, labels: [.labels[].name], title, body}')
|
||||
else
|
||||
echo "📋 获取所有 issue(限制 100 个)..."
|
||||
ISSUES=$(gh issue list --state all --limit 100 --json number,labels,title,body --jq '.[] | {number, labels: [.labels[].name], title, body}')
|
||||
fi
|
||||
|
||||
# 临时文件
|
||||
echo "$ISSUES" > /tmp/issues.json
|
||||
|
||||
# 处理每个 issue
|
||||
cat /tmp/issues.json | jq -c '.' | while read -r issue; do
|
||||
ISSUE_NUM=$(echo "$issue" | jq -r '.number')
|
||||
EXISTING_LABELS=$(echo "$issue" | jq -r '.labels | join(",")')
|
||||
TITLE=$(echo "$issue" | jq -r '.title')
|
||||
BODY=$(echo "$issue" | jq -r '.body')
|
||||
|
||||
echo ""
|
||||
echo "🔍 处理 Issue #$ISSUE_NUM: $TITLE"
|
||||
echo " 现有标签: $EXISTING_LABELS"
|
||||
|
||||
# 跳过已有标签的 issue
|
||||
if [ "$SKIP_LABELED" = "true" ] && [ ! -z "$EXISTING_LABELS" ]; then
|
||||
echo " ⏭️ 跳过(已有标签)"
|
||||
continue
|
||||
fi
|
||||
|
||||
# 分析内容并打标签
|
||||
LABELS_TO_ADD=""
|
||||
|
||||
# 检测 bug
|
||||
if echo "$TITLE $BODY" | grep -iE "(bug|错误|崩溃|crash|error|exception|问题|fix)" > /dev/null; then
|
||||
LABELS_TO_ADD="$LABELS_TO_ADD bug"
|
||||
echo " 🐛 检测到: bug"
|
||||
fi
|
||||
|
||||
# 检测 feature request
|
||||
if echo "$TITLE $BODY" | grep -iE "(feature|功能|enhancement|improve|优化|建议|新增|添加|add)" > /dev/null; then
|
||||
LABELS_TO_ADD="$LABELS_TO_ADD enhancement"
|
||||
echo " ✨ 检测到: enhancement"
|
||||
fi
|
||||
|
||||
# 检测 question
|
||||
if echo "$TITLE $BODY" | grep -iE "(question|疑问|how to|如何|怎么|为什么|why|咨询|\?|?)" > /dev/null; then
|
||||
LABELS_TO_ADD="$LABELS_TO_ADD question"
|
||||
echo " ❓ 检测到: question"
|
||||
fi
|
||||
|
||||
# 检测 documentation
|
||||
if echo "$TITLE $BODY" | grep -iE "(doc|文档|readme|guide|tutorial|教程|说明)" > /dev/null; then
|
||||
LABELS_TO_ADD="$LABELS_TO_ADD documentation"
|
||||
echo " 📖 检测到: documentation"
|
||||
fi
|
||||
|
||||
# 检测 performance
|
||||
if echo "$TITLE $BODY" | grep -iE "(performance|性能|slow|慢|lag|卡顿|optimize|优化)" > /dev/null; then
|
||||
LABELS_TO_ADD="$LABELS_TO_ADD performance"
|
||||
echo " ⚡ 检测到: performance"
|
||||
fi
|
||||
|
||||
# 检测 core
|
||||
if echo "$TITLE $BODY" | grep -iE "(@esengine/ecs-framework|packages/core|core package|核心包)" > /dev/null; then
|
||||
LABELS_TO_ADD="$LABELS_TO_ADD core"
|
||||
echo " 🎯 检测到: core"
|
||||
fi
|
||||
|
||||
# 检测 editor
|
||||
if echo "$TITLE $BODY" | grep -iE "(editor|编辑器|tauri)" > /dev/null; then
|
||||
LABELS_TO_ADD="$LABELS_TO_ADD editor"
|
||||
echo " 🎨 检测到: editor"
|
||||
fi
|
||||
|
||||
# 检测 network
|
||||
if echo "$TITLE $BODY" | grep -iE "(network|网络|multiplayer|多人|同步)" > /dev/null; then
|
||||
LABELS_TO_ADD="$LABELS_TO_ADD network"
|
||||
echo " 🌐 检测到: network"
|
||||
fi
|
||||
|
||||
# 检测 help wanted
|
||||
if echo "$TITLE $BODY" | grep -iE "(help wanted|需要帮助|求助)" > /dev/null; then
|
||||
LABELS_TO_ADD="$LABELS_TO_ADD help wanted"
|
||||
echo " 🆘 检测到: help wanted"
|
||||
fi
|
||||
|
||||
# 添加标签
|
||||
if [ ! -z "$LABELS_TO_ADD" ]; then
|
||||
echo " ✅ 添加标签: $LABELS_TO_ADD"
|
||||
for label in $LABELS_TO_ADD; do
|
||||
gh issue edit $ISSUE_NUM --add-label "$label" 2>&1 | grep -v "already exists" || true
|
||||
done
|
||||
echo " 💬 添加说明评论..."
|
||||
gh issue comment $ISSUE_NUM --body $'🤖 自动标签系统检测到此 issue 并添加了相关标签。如有误判,请告知维护者。\n\n🤖 Auto-labeling system detected and labeled this issue. Please let maintainers know if this is incorrect.' || true
|
||||
else
|
||||
echo " ℹ️ 未检测到明确类型"
|
||||
fi
|
||||
|
||||
# 避免 API 限制
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "✅ 批量标签完成!"
|
||||
echo "查看结果: https://github.com/${{ github.repository }}/issues"
|
||||
122
.github/workflows/ci.yml
vendored
122
.github/workflows/ci.yml
vendored
@@ -13,42 +13,27 @@ on:
|
||||
- '.github/workflows/ci.yml'
|
||||
pull_request:
|
||||
branches: [ master, main, develop ]
|
||||
# Run on all PRs to satisfy branch protection, but skip build if no code changes
|
||||
paths:
|
||||
- 'packages/**'
|
||||
- 'package.json'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'tsconfig.json'
|
||||
- 'turbo.json'
|
||||
- 'jest.config.*'
|
||||
- '.github/workflows/ci.yml'
|
||||
|
||||
jobs:
|
||||
# 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@v4
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
@@ -56,36 +41,67 @@ jobs:
|
||||
node-version: '20.x'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: wasm32-unknown-unknown
|
||||
|
||||
# 缓存 Rust 编译结果
|
||||
- name: Cache Rust dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: packages/engine
|
||||
cache-on-failure: true
|
||||
|
||||
# 缓存 wasm-pack
|
||||
- name: Cache wasm-pack
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cargo/bin/wasm-pack
|
||||
key: wasm-pack-${{ runner.os }}
|
||||
|
||||
- name: Install wasm-pack
|
||||
run: |
|
||||
if ! command -v wasm-pack &> /dev/null; then
|
||||
cargo install wasm-pack
|
||||
fi
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
|
||||
# 构建 framework 包 (可独立发布的通用库,无外部依赖)
|
||||
- name: Build framework packages
|
||||
# 缓存 Turbo
|
||||
- name: Cache Turbo
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .turbo
|
||||
key: turbo-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
turbo-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
|
||||
turbo-${{ runner.os }}-
|
||||
|
||||
# 构建所有包
|
||||
- name: Build all packages
|
||||
run: pnpm run build
|
||||
|
||||
- name: Copy WASM files to ecs-engine-bindgen
|
||||
run: |
|
||||
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/rpc build
|
||||
pnpm --filter @esengine/network build
|
||||
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: Type check
|
||||
run: pnpm run type-check
|
||||
|
||||
# Lint 检查 (仅 framework 包)
|
||||
- name: Lint check (framework packages)
|
||||
run: pnpm run lint:framework
|
||||
# Lint 检查
|
||||
- name: Lint check
|
||||
run: pnpm run lint
|
||||
|
||||
# 测试 (仅 framework 包)
|
||||
# 测试
|
||||
- name: Run tests with coverage
|
||||
run: pnpm run test:ci:framework
|
||||
run: pnpm run test:ci
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
@@ -96,11 +112,9 @@ jobs:
|
||||
name: codecov-umbrella
|
||||
fail_ci_if_error: false
|
||||
|
||||
# 构建 npm 包 (core 和 math)
|
||||
# 构建 npm 包
|
||||
- name: Build npm packages
|
||||
run: |
|
||||
pnpm run build:npm:core
|
||||
pnpm run build:npm:math
|
||||
run: pnpm run build:npm
|
||||
|
||||
# 上传构建产物
|
||||
- name: Upload build artifacts
|
||||
@@ -108,6 +122,6 @@ jobs:
|
||||
with:
|
||||
name: build-artifacts
|
||||
path: |
|
||||
packages/framework/**/dist/
|
||||
packages/framework/**/bin/
|
||||
packages/*/dist/
|
||||
packages/*/bin/
|
||||
retention-days: 7
|
||||
|
||||
146
.github/workflows/cleanup-dependabot.yml
vendored
Normal file
146
.github/workflows/cleanup-dependabot.yml
vendored
Normal file
@@ -0,0 +1,146 @@
|
||||
name: Cleanup Old Dependabot PRs
|
||||
|
||||
# 手动触发的 workflow,用于清理堆积的 Dependabot PR
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
days_old:
|
||||
description: '关闭多少天前创建的 PR(默认 7 天)'
|
||||
required: false
|
||||
default: '7'
|
||||
dry_run:
|
||||
description: '试运行模式(true=仅显示,不关闭)'
|
||||
required: false
|
||||
default: 'true'
|
||||
type: choice
|
||||
options:
|
||||
- 'true'
|
||||
- 'false'
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: List and Close Old Dependabot PRs
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const daysOld = parseInt('${{ github.event.inputs.days_old }}') || 7;
|
||||
const dryRun = '${{ github.event.inputs.dry_run }}' === 'true';
|
||||
const cutoffDate = new Date();
|
||||
cutoffDate.setDate(cutoffDate.getDate() - daysOld);
|
||||
|
||||
console.log(`🔍 查找超过 ${daysOld} 天的 Dependabot PR...`);
|
||||
console.log(`📅 截止日期: ${cutoffDate.toISOString()}`);
|
||||
console.log(`🏃 模式: ${dryRun ? '试运行(不会实际关闭)' : '实际执行'}`);
|
||||
console.log('---');
|
||||
|
||||
// 获取所有 Dependabot PR
|
||||
const { data: pulls } = await github.rest.pulls.list({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'open',
|
||||
per_page: 100
|
||||
});
|
||||
|
||||
const dependabotPRs = pulls.filter(pr =>
|
||||
pr.user.login === 'dependabot[bot]' &&
|
||||
new Date(pr.created_at) < cutoffDate
|
||||
);
|
||||
|
||||
console.log(`📊 找到 ${dependabotPRs.length} 个符合条件的 Dependabot PR`);
|
||||
console.log('');
|
||||
|
||||
if (dependabotPRs.length === 0) {
|
||||
console.log('✅ 没有需要清理的 PR');
|
||||
return;
|
||||
}
|
||||
|
||||
// 按类型分组
|
||||
const byType = {
|
||||
dev: [],
|
||||
prod: [],
|
||||
actions: [],
|
||||
other: []
|
||||
};
|
||||
|
||||
for (const pr of dependabotPRs) {
|
||||
const title = pr.title.toLowerCase();
|
||||
const labels = pr.labels.map(l => l.name);
|
||||
|
||||
let type = 'other';
|
||||
if (title.includes('dev-dependencies') || title.includes('development')) {
|
||||
type = 'dev';
|
||||
} else if (title.includes('production-dependencies')) {
|
||||
type = 'prod';
|
||||
} else if (labels.includes('github-actions')) {
|
||||
type = 'actions';
|
||||
}
|
||||
|
||||
byType[type].push(pr);
|
||||
}
|
||||
|
||||
console.log('📋 PR 分类统计:');
|
||||
console.log(` 🔧 开发依赖: ${byType.dev.length} 个`);
|
||||
console.log(` 📦 生产依赖: ${byType.prod.length} 个`);
|
||||
console.log(` ⚙️ GitHub Actions: ${byType.actions.length} 个`);
|
||||
console.log(` ❓ 其他: ${byType.other.length} 个`);
|
||||
console.log('');
|
||||
|
||||
// 处理每个 PR
|
||||
for (const pr of dependabotPRs) {
|
||||
const age = Math.floor((Date.now() - new Date(pr.created_at)) / (1000 * 60 * 60 * 24));
|
||||
|
||||
console.log(`${dryRun ? '🔍' : '🗑️ '} #${pr.number}: ${pr.title}`);
|
||||
console.log(` 创建时间: ${pr.created_at} (${age} 天前)`);
|
||||
console.log(` 链接: ${pr.html_url}`);
|
||||
|
||||
if (!dryRun) {
|
||||
await github.rest.pulls.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: pr.number,
|
||||
state: 'closed'
|
||||
});
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr.number,
|
||||
body: `🤖 **自动关闭旧的 Dependabot PR**
|
||||
|
||||
此 PR 已超过 ${daysOld} 天未合并,已被自动关闭以清理积压。
|
||||
|
||||
📌 **下一步:**
|
||||
- Dependabot 已配置为月度运行,届时会创建新的分组更新
|
||||
- 新的 Mergify 规则会智能处理不同类型的依赖更新
|
||||
- 开发依赖和 GitHub Actions 会自动合并(即使 CI 失败)
|
||||
- 生产依赖需要 CI 通过才会自动合并
|
||||
|
||||
如果需要立即应用此更新,请手动更新依赖。
|
||||
|
||||
---
|
||||
*此操作由仓库维护者手动触发的清理工作流执行*`
|
||||
});
|
||||
|
||||
console.log(' ✅ 已关闭并添加说明');
|
||||
} else {
|
||||
console.log(' ℹ️ 试运行模式 - 未执行操作');
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
console.log('---');
|
||||
if (dryRun) {
|
||||
console.log(`✨ 试运行完成!共发现 ${dependabotPRs.length} 个待清理的 PR`);
|
||||
console.log('💡 要实际执行清理,请将 dry_run 参数设为 false 重新运行');
|
||||
} else {
|
||||
console.log(`✅ 清理完成!已关闭 ${dependabotPRs.length} 个 Dependabot PR`);
|
||||
}
|
||||
10
.github/workflows/codecov.yml
vendored
10
.github/workflows/codecov.yml
vendored
@@ -15,7 +15,9 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
@@ -28,7 +30,7 @@ jobs:
|
||||
|
||||
- name: Run tests with coverage
|
||||
run: |
|
||||
cd packages/framework/core
|
||||
cd packages/core
|
||||
pnpm run test:coverage
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
@@ -36,7 +38,7 @@ jobs:
|
||||
continue-on-error: true
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./packages/framework/core/coverage/coverage-final.json
|
||||
files: ./packages/core/coverage/coverage-final.json
|
||||
flags: core
|
||||
name: core-coverage
|
||||
fail_ci_if_error: false
|
||||
@@ -46,4 +48,4 @@ jobs:
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-report
|
||||
path: packages/framework/core/coverage/
|
||||
path: packages/core/coverage/
|
||||
|
||||
4
.github/workflows/commitlint.yml
vendored
4
.github/workflows/commitlint.yml
vendored
@@ -18,7 +18,9 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
6
.github/workflows/docs.yml
vendored
6
.github/workflows/docs.yml
vendored
@@ -30,7 +30,9 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
@@ -56,7 +58,7 @@ jobs:
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: docs/dist
|
||||
path: docs/.vitepress/dist
|
||||
|
||||
deploy:
|
||||
environment:
|
||||
|
||||
23
.github/workflows/issue-labeler.yml
vendored
Normal file
23
.github/workflows/issue-labeler.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
name: Issue Labeler
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
label:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Label Issues
|
||||
uses: github/issue-labeler@v3.4
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
configuration-path: .github/labeler.yml
|
||||
enable-versioned-regex: 1
|
||||
28
.github/workflows/issue-translator.yml
vendored
Normal file
28
.github/workflows/issue-translator.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: Issue Translator
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
translate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Translate Issues
|
||||
uses: tomsun28/issues-translate-action@v2.7
|
||||
with:
|
||||
IS_MODIFY_TITLE: false
|
||||
# 设置为 true 会修改标题,false 只在评论中添加翻译
|
||||
CUSTOM_BOT_NOTE: |
|
||||
<details>
|
||||
<summary>🌏 Translation / 翻译</summary>
|
||||
|
||||
Bot detected the issue body's language is not English, translate it automatically.
|
||||
机器人检测到 issue 内容非英文,自动翻译。
|
||||
|
||||
</details>
|
||||
77
.github/workflows/release-changesets.yml
vendored
77
.github/workflows/release-changesets.yml
vendored
@@ -1,77 +0,0 @@
|
||||
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/rpc" build
|
||||
pnpm --filter "@esengine/network" build
|
||||
pnpm --filter "@esengine/server" build
|
||||
pnpm --filter "@esengine/database-drivers" build
|
||||
pnpm --filter "@esengine/database" build
|
||||
pnpm --filter "@esengine/transaction" build
|
||||
pnpm --filter "@esengine/cli" build
|
||||
pnpm --filter "create-esengine-server" build
|
||||
pnpm --filter "@esengine/node-editor" 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 }}
|
||||
130
.github/workflows/release-editor.yml
vendored
130
.github/workflows/release-editor.yml
vendored
@@ -34,7 +34,9 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
@@ -50,7 +52,7 @@ jobs:
|
||||
- name: Rust cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: packages/editor/editor-app/src-tauri
|
||||
workspaces: packages/editor-app/src-tauri
|
||||
cache-on-failure: true
|
||||
|
||||
- name: Install dependencies (Ubuntu)
|
||||
@@ -65,7 +67,7 @@ jobs:
|
||||
- name: Update version in config files (for manual trigger)
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |
|
||||
cd packages/editor/editor-app
|
||||
cd packages/editor-app
|
||||
node -e "const pkg=require('./package.json'); pkg.version='${{ github.event.inputs.version }}'; require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2)+'\n')"
|
||||
node scripts/sync-version.js
|
||||
|
||||
@@ -80,134 +82,38 @@ jobs:
|
||||
- name: Copy WASM files to ecs-engine-bindgen
|
||||
shell: bash
|
||||
run: |
|
||||
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/
|
||||
mkdir -p packages/ecs-engine-bindgen/src/wasm
|
||||
cp packages/engine/pkg/es_engine.js packages/ecs-engine-bindgen/src/wasm/
|
||||
cp packages/engine/pkg/es_engine.d.ts packages/ecs-engine-bindgen/src/wasm/
|
||||
cp packages/engine/pkg/es_engine_bg.wasm packages/ecs-engine-bindgen/src/wasm/
|
||||
cp packages/engine/pkg/es_engine_bg.wasm.d.ts packages/ecs-engine-bindgen/src/wasm/
|
||||
|
||||
- name: Bundle runtime files for Tauri
|
||||
run: |
|
||||
cd packages/editor/editor-app
|
||||
cd packages/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/editor-app
|
||||
projectPath: packages/editor-app
|
||||
tagName: ${{ github.event_name == 'workflow_dispatch' && format('editor-v{0}', github.event.inputs.version) || github.ref_name }}
|
||||
releaseName: 'ECS Editor v${{ github.event.inputs.version || github.ref_name }}'
|
||||
releaseBody: 'See the assets to download this version and install.'
|
||||
releaseDraft: true
|
||||
releaseDraft: false
|
||||
prerelease: false
|
||||
includeUpdaterJson: true
|
||||
updaterJsonKeepUniversal: false
|
||||
args: ${{ matrix.platform == 'macos-latest' && format('--target {0}', matrix.target) || '' }}
|
||||
|
||||
# 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
|
||||
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()
|
||||
needs: build-tauri
|
||||
if: github.event_name == 'workflow_dispatch' && success()
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
@@ -221,7 +127,7 @@ jobs:
|
||||
|
||||
- name: Update version files
|
||||
run: |
|
||||
cd packages/editor/editor-app
|
||||
cd packages/editor-app
|
||||
node -e "const pkg=require('./package.json'); pkg.version='${{ github.event.inputs.version }}'; require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2)+'\n')"
|
||||
node scripts/sync-version.js
|
||||
|
||||
@@ -239,8 +145,8 @@ jobs:
|
||||
This PR updates the editor version after successful release build.
|
||||
|
||||
### Changes
|
||||
- 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 }}`
|
||||
- Updated `packages/editor-app/package.json` → `${{ github.event.inputs.version }}`
|
||||
- Updated `packages/editor-app/src-tauri/tauri.conf.json` → `${{ github.event.inputs.version }}`
|
||||
|
||||
### Release
|
||||
- [GitHub Release](https://github.com/${{ github.repository }}/releases/tag/editor-v${{ github.event.inputs.version }})
|
||||
|
||||
196
.github/workflows/release.yml
vendored
196
.github/workflows/release.yml
vendored
@@ -1,24 +1,10 @@
|
||||
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: 'Select package to publish'
|
||||
description: '选择要发布的包'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
@@ -29,15 +15,19 @@ on:
|
||||
- blueprint
|
||||
- tilemap
|
||||
- physics-rapier2d
|
||||
- worker-generator
|
||||
version_type:
|
||||
description: 'Version bump type'
|
||||
description: '版本更新类型'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- patch
|
||||
- minor
|
||||
- major
|
||||
- custom
|
||||
custom_version:
|
||||
description: '自定义版本号 (仅当选择 custom 时使用,例如: 2.2.9)'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -46,7 +36,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
release-package:
|
||||
name: Release Package
|
||||
name: Release ${{ github.event.inputs.package }} Package
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -55,42 +45,10 @@ 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@v4
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
@@ -102,124 +60,76 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Verify version (tag mode)
|
||||
if: steps.parse.outputs.mode == 'tag'
|
||||
run: |
|
||||
PACKAGE="${{ steps.parse.outputs.package }}"
|
||||
EXPECTED_VERSION="${{ steps.parse.outputs.version }}"
|
||||
|
||||
# Get version from package.json
|
||||
ACTUAL_VERSION=$(node -p "require('./packages/$PACKAGE/package.json').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: |
|
||||
if [ "${{ steps.parse.outputs.mode }}" = "tag" ]; then
|
||||
echo "value=${{ steps.parse.outputs.version }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "value=${{ steps.bump.outputs.version }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Build core package (if needed)
|
||||
if: ${{ steps.parse.outputs.package != 'core' && steps.parse.outputs.package != 'node-editor' && steps.parse.outputs.package != 'worker-generator' }}
|
||||
if: ${{ github.event.inputs.package != 'core' && github.event.inputs.package != 'node-editor' }}
|
||||
run: |
|
||||
cd packages/framework/core
|
||||
cd packages/core
|
||||
pnpm run build
|
||||
|
||||
- name: Build node-editor package (if needed for blueprint)
|
||||
if: ${{ steps.parse.outputs.package == 'blueprint' }}
|
||||
if: ${{ github.event.inputs.package == 'blueprint' }}
|
||||
run: |
|
||||
cd packages/node-editor
|
||||
pnpm run build
|
||||
|
||||
# - name: Run tests
|
||||
# run: |
|
||||
# cd packages/${{ github.event.inputs.package }}
|
||||
# npm run test:ci
|
||||
|
||||
- name: Update version
|
||||
id: version
|
||||
run: |
|
||||
cd packages/${{ github.event.inputs.package }}
|
||||
if [ "${{ github.event.inputs.version_type }}" = "custom" ]; then
|
||||
NEW_VERSION=${{ github.event.inputs.custom_version }}
|
||||
else
|
||||
# Get current version and bump it
|
||||
CURRENT=$(node -p "require('./package.json').version")
|
||||
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT"
|
||||
case "${{ github.event.inputs.version_type }}" in
|
||||
major) NEW_VERSION="$((MAJOR+1)).0.0" ;;
|
||||
minor) NEW_VERSION="$MAJOR.$((MINOR+1)).0" ;;
|
||||
patch) NEW_VERSION="$MAJOR.$MINOR.$((PATCH+1))" ;;
|
||||
esac
|
||||
fi
|
||||
# Update package.json using node
|
||||
node -e "const fs=require('fs'); const pkg=JSON.parse(fs.readFileSync('package.json')); pkg.version='$NEW_VERSION'; fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2)+'\n')"
|
||||
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "发布版本: $NEW_VERSION"
|
||||
|
||||
- name: Build package
|
||||
run: |
|
||||
cd packages/${{ steps.parse.outputs.package }}
|
||||
cd packages/${{ github.event.inputs.package }}
|
||||
pnpm run build:npm
|
||||
|
||||
- name: Publish to npm
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: |
|
||||
cd packages/${{ steps.parse.outputs.package }}/dist
|
||||
cd packages/${{ github.event.inputs.package }}/dist
|
||||
pnpm publish --access public --no-git-checks
|
||||
|
||||
- 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'
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: "chore(${{ steps.parse.outputs.package }}): release v${{ steps.version.outputs.value }}"
|
||||
branch: release/${{ steps.parse.outputs.package }}-v${{ steps.version.outputs.value }}
|
||||
commit-message: "chore(${{ github.event.inputs.package }}): release v${{ steps.version.outputs.new_version }}"
|
||||
branch: release/${{ github.event.inputs.package }}-v${{ steps.version.outputs.new_version }}
|
||||
delete-branch: true
|
||||
title: "chore(${{ steps.parse.outputs.package }}): Release v${{ steps.version.outputs.value }}"
|
||||
title: "chore(${{ github.event.inputs.package }}): Release v${{ steps.version.outputs.new_version }}"
|
||||
body: |
|
||||
## Release v${{ steps.version.outputs.value }}
|
||||
## 🚀 Release v${{ steps.version.outputs.new_version }}
|
||||
|
||||
This PR updates `@esengine/${{ steps.parse.outputs.package }}` package version.
|
||||
此 PR 更新 `@esengine/${{ github.event.inputs.package }}` 包的版本号
|
||||
|
||||
### 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 }}`
|
||||
### 变更
|
||||
- ✅ 已发布到 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 }}`
|
||||
|
||||
---
|
||||
*This PR was automatically created by the release workflow*
|
||||
*此 PR 由发布工作流自动创建*
|
||||
labels: |
|
||||
release
|
||||
${{ steps.parse.outputs.package }}
|
||||
${{ github.event.inputs.package }}
|
||||
automated pr
|
||||
|
||||
48
.github/workflows/size-limit.yml
vendored
Normal file
48
.github/workflows/size-limit.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
name: Size Limit
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
paths:
|
||||
- 'packages/core/src/**'
|
||||
- 'packages/core/package.json'
|
||||
- '.size-limit.json'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
size:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build core package
|
||||
run: |
|
||||
cd packages/core
|
||||
pnpm run build:npm
|
||||
|
||||
- name: Check bundle size
|
||||
uses: andresz1/size-limit-action@v1
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
skip_step: install
|
||||
58
.github/workflows/welcome.yml
vendored
Normal file
58
.github/workflows/welcome.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
name: Welcome
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
welcome:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Welcome new contributors
|
||||
uses: actions/first-interaction@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-message: |
|
||||
👋 你好!感谢你提交第一个 issue!
|
||||
|
||||
我们会尽快查看并回复。同时,建议你:
|
||||
- 📚 查看[文档](https://esengine.github.io/ecs-framework/)
|
||||
- 🤖 使用 [AI 文档助手](https://deepwiki.com/esengine/ecs-framework)
|
||||
- 💬 加入 [QQ 交流群](https://jq.qq.com/?_wv=1027&k=29w1Nud6)
|
||||
|
||||
---
|
||||
|
||||
👋 Hello! Thanks for opening your first issue!
|
||||
|
||||
We'll review it as soon as possible. Meanwhile, you might want to:
|
||||
- 📚 Check the [documentation](https://esengine.github.io/ecs-framework/)
|
||||
- 🤖 Use [AI documentation assistant](https://deepwiki.com/esengine/ecs-framework)
|
||||
|
||||
pr-message: |
|
||||
👋 你好!感谢你提交第一个 Pull Request!
|
||||
|
||||
在我们 Review 之前,请确保:
|
||||
- ✅ 代码遵循项目规范
|
||||
- ✅ 通过所有测试
|
||||
- ✅ 更新了相关文档
|
||||
- ✅ Commit 遵循 [Conventional Commits](https://www.conventionalcommits.org/) 规范
|
||||
|
||||
查看完整的[贡献指南](https://github.com/esengine/ecs-framework/blob/master/CONTRIBUTING.md)。
|
||||
|
||||
---
|
||||
|
||||
👋 Hello! Thanks for your first Pull Request!
|
||||
|
||||
Before we review, please ensure:
|
||||
- ✅ Code follows project conventions
|
||||
- ✅ All tests pass
|
||||
- ✅ Documentation is updated
|
||||
- ✅ Commits follow [Conventional Commits](https://www.conventionalcommits.org/)
|
||||
|
||||
See the full [Contributing Guide](https://github.com/esengine/ecs-framework/blob/master/CONTRIBUTING.md).
|
||||
12
.gitignore
vendored
12
.gitignore
vendored
@@ -48,14 +48,6 @@ logs/
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# 代码签名证书(敏感文件)
|
||||
certs/
|
||||
*.pfx
|
||||
*.p12
|
||||
*.cer
|
||||
*.pem
|
||||
*.key
|
||||
|
||||
# 测试覆盖率
|
||||
coverage/
|
||||
*.lcov
|
||||
@@ -90,7 +82,3 @@ docs/.vitepress/dist/
|
||||
# Tauri 捆绑输出
|
||||
**/src-tauri/target/release/bundle/
|
||||
**/src-tauri/target/debug/bundle/
|
||||
|
||||
# Rust 构建产物
|
||||
**/engine-shared/target/
|
||||
external/
|
||||
|
||||
@@ -119,9 +119,9 @@ npm run format
|
||||
|
||||
## 问题反馈 / Issue Reporting
|
||||
|
||||
如果你发现了 bug 或有新功能建议,请[创建 Issue](https://github.com/esengine/esengine/issues/new)。
|
||||
如果你发现了 bug 或有新功能建议,请[创建 Issue](https://github.com/esengine/ecs-framework/issues/new)。
|
||||
|
||||
If you find a bug or have a feature request, please [create an issue](https://github.com/esengine/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).
|
||||
|
||||
## 许可证 / License
|
||||
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 ESEngine Contributors
|
||||
Copyright (c) 2025 ECS Framework
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
341
README.md
341
README.md
@@ -1,85 +1,49 @@
|
||||
<h1 align="center">
|
||||
<img src="https://raw.githubusercontent.com/esengine/esengine/master/docs/public/logo.svg" alt="ESEngine" width="180">
|
||||
<br>
|
||||
ESEngine
|
||||
</h1>
|
||||
# ESEngine
|
||||
|
||||
<p align="center">
|
||||
<strong>Modular Game Framework for TypeScript</strong>
|
||||
</p>
|
||||
**English** | [中文](./README_CN.md)
|
||||
|
||||
<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>
|
||||
**[Documentation](https://esengine.github.io/ecs-framework/) | [API Reference](https://esengine.github.io/ecs-framework/api/) | [Examples](./examples/)**
|
||||
|
||||
<p align="center">
|
||||
<b>English</b> | <a href="./README_CN.md">中文</a>
|
||||
</p>
|
||||
ESEngine is a cross-platform 2D game engine for creating games from a unified interface. It provides a comprehensive set of common tools so that developers can focus on making games without having to reinvent the wheel.
|
||||
|
||||
<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>
|
||||
Games can be exported to multiple platforms including Web browsers, WeChat Mini Games, and other mini-game platforms.
|
||||
|
||||
---
|
||||
## Free and Open Source
|
||||
|
||||
## What is ESEngine?
|
||||
ESEngine is completely free and open source under the MIT license. No strings attached, no royalties. Your games are yours.
|
||||
|
||||
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.
|
||||
## Features
|
||||
|
||||
The core is a high-performance **ECS (Entity-Component-System)** framework, accompanied by optional modules for AI, networking, physics, and more.
|
||||
- **Data-Driven Architecture**: Built on Entity-Component-System (ECS) pattern for flexible and performant game logic
|
||||
- **High-Performance Rendering**: Rust/WebAssembly 2D renderer with sprite batching and WebGL 2.0 backend
|
||||
- **Visual Editor**: Cross-platform desktop editor with scene management, asset browser, and visual tools
|
||||
- **Modular Design**: Use only what you need. Each feature is a separate module that can be included independently
|
||||
- **Multi-Platform**: Deploy to Web, WeChat Mini Games, and more from a single codebase
|
||||
|
||||
## Getting the Engine
|
||||
|
||||
### Using npm
|
||||
|
||||
```bash
|
||||
npm install @esengine/ecs-framework
|
||||
```
|
||||
|
||||
## Features
|
||||
### Building from Source
|
||||
|
||||
| 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 |
|
||||
| **Procgen** | Procedural generation (noise, random, sampling) | No |
|
||||
| **RPC** | High-performance RPC communication framework | No |
|
||||
| **Server** | Game server framework with rooms, auth, rate limiting | No |
|
||||
| **Network** | Client networking with prediction, AOI, delta compression | No |
|
||||
| **Transaction** | Game transaction system with Redis/Memory storage | No |
|
||||
| **World Streaming** | Open world chunk loading and streaming | No |
|
||||
See [Building from Source](#building-from-source) for detailed instructions.
|
||||
|
||||
> All framework modules can be used standalone with any rendering engine.
|
||||
### Editor Download
|
||||
|
||||
Pre-built editor binaries are available on the [Releases](https://github.com/esengine/ecs-framework/releases) page for Windows and macOS.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 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, Entity, Component, EntitySystem,
|
||||
Matcher, Time, ECSComponent, ECSSystem
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
// Define components (data only)
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
x = 0;
|
||||
@@ -92,7 +56,6 @@ class Velocity extends Component {
|
||||
dy = 0;
|
||||
}
|
||||
|
||||
// Define system (logic)
|
||||
@ECSSystem('Movement')
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
@@ -109,7 +72,6 @@ class MovementSystem extends EntitySystem {
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize
|
||||
Core.create();
|
||||
const scene = new Scene();
|
||||
scene.addSystem(new MovementSystem());
|
||||
@@ -120,193 +82,164 @@ player.addComponent(new Velocity());
|
||||
|
||||
Core.setScene(scene);
|
||||
|
||||
// Integrate with your game loop
|
||||
// Game loop
|
||||
let lastTime = 0;
|
||||
function gameLoop(currentTime: number) {
|
||||
Core.update(currentTime / 1000);
|
||||
const deltaTime = (currentTime - lastTime) / 1000;
|
||||
lastTime = currentTime;
|
||||
|
||||
Core.update(deltaTime);
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
requestAnimationFrame(gameLoop);
|
||||
```
|
||||
|
||||
## Using with Other Engines
|
||||
## Modules
|
||||
|
||||
ESEngine's framework modules are designed to work alongside your preferred rendering engine:
|
||||
ESEngine is organized into modular packages. Each feature has a runtime module and an optional editor extension.
|
||||
|
||||
### With Cocos Creator
|
||||
### Core
|
||||
|
||||
```typescript
|
||||
import { Component as CCComponent, _decorator } from 'cc';
|
||||
import { Core, Scene, Matcher, EntitySystem } from '@esengine/ecs-framework';
|
||||
import { BehaviorTreeExecutionSystem } from '@esengine/behavior-tree';
|
||||
| Package | Description |
|
||||
|---------|-------------|
|
||||
| `@esengine/ecs-framework` | Core ECS framework with entity management, component system, and queries |
|
||||
| `@esengine/math` | Vector, matrix, and mathematical utilities |
|
||||
| `@esengine/engine` | Rust/WASM 2D renderer |
|
||||
| `@esengine/engine-core` | Engine module system and lifecycle management |
|
||||
|
||||
const { ccclass } = _decorator;
|
||||
### Runtime Modules
|
||||
|
||||
@ccclass('GameManager')
|
||||
export class GameManager extends CCComponent {
|
||||
private ecsScene!: Scene;
|
||||
| Package | Description |
|
||||
|---------|-------------|
|
||||
| `@esengine/sprite` | 2D sprite rendering and animation |
|
||||
| `@esengine/tilemap` | Tile-based map rendering with animation support |
|
||||
| `@esengine/physics-rapier2d` | 2D physics simulation powered by Rapier |
|
||||
| `@esengine/behavior-tree` | Behavior tree AI system |
|
||||
| `@esengine/blueprint` | Visual scripting runtime |
|
||||
| `@esengine/camera` | Camera control and management |
|
||||
| `@esengine/audio` | Audio playback |
|
||||
| `@esengine/ui` | UI components |
|
||||
| `@esengine/material-system` | Material and shader system |
|
||||
| `@esengine/asset-system` | Asset loading and management |
|
||||
|
||||
start() {
|
||||
Core.create();
|
||||
this.ecsScene = new Scene();
|
||||
### Editor Extensions
|
||||
|
||||
// Add ECS systems
|
||||
this.ecsScene.addSystem(new BehaviorTreeExecutionSystem());
|
||||
this.ecsScene.addSystem(new MyGameSystem());
|
||||
| Package | Description |
|
||||
|---------|-------------|
|
||||
| `@esengine/sprite-editor` | Sprite inspector and tools |
|
||||
| `@esengine/tilemap-editor` | Visual tilemap editor with brush tools |
|
||||
| `@esengine/physics-rapier2d-editor` | Physics collider visualization and editing |
|
||||
| `@esengine/behavior-tree-editor` | Visual behavior tree editor |
|
||||
| `@esengine/blueprint-editor` | Visual scripting editor |
|
||||
| `@esengine/material-editor` | Material and shader editor |
|
||||
| `@esengine/shader-editor` | Shader code editor |
|
||||
|
||||
Core.setScene(this.ecsScene);
|
||||
}
|
||||
### Platform
|
||||
|
||||
update(dt: number) {
|
||||
Core.update(dt);
|
||||
}
|
||||
}
|
||||
```
|
||||
| Package | Description |
|
||||
|---------|-------------|
|
||||
| `@esengine/platform-common` | Platform abstraction interfaces |
|
||||
| `@esengine/platform-web` | Web browser runtime |
|
||||
| `@esengine/platform-wechat` | WeChat Mini Game runtime |
|
||||
|
||||
### With Laya 3.x
|
||||
## Editor
|
||||
|
||||
```typescript
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
import { FSMSystem } from '@esengine/fsm';
|
||||
ESEngine Editor is a cross-platform desktop application built with Tauri and React.
|
||||
|
||||
const { regClass } = Laya;
|
||||
### Features
|
||||
|
||||
@regClass()
|
||||
export class ECSManager extends Laya.Script {
|
||||
private ecsScene = new Scene();
|
||||
- Scene hierarchy and entity management
|
||||
- Component inspector with custom editors
|
||||
- Asset browser with drag-and-drop support
|
||||
- Tilemap editor with paint, fill, and selection tools
|
||||
- Behavior tree visual editor
|
||||
- Blueprint visual scripting
|
||||
- Material and shader editing
|
||||
- Built-in performance profiler
|
||||
- Localization support (English, Chinese)
|
||||
|
||||
onAwake(): void {
|
||||
Core.create();
|
||||
this.ecsScene.addSystem(new FSMSystem());
|
||||
Core.setScene(this.ecsScene);
|
||||
}
|
||||
### Screenshot
|
||||
|
||||
onUpdate(): void {
|
||||
Core.update(Laya.timer.delta / 1000);
|
||||
}
|
||||

|
||||
|
||||
onDestroy(): void {
|
||||
Core.destroy();
|
||||
}
|
||||
}
|
||||
```
|
||||
## Supported Platforms
|
||||
|
||||
## Packages
|
||||
|
||||
### Framework (Engine-Agnostic)
|
||||
|
||||
These packages have **zero rendering dependencies** and work with any engine:
|
||||
|
||||
```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/procgen # Procedural generation
|
||||
npm install @esengine/rpc # RPC framework
|
||||
npm install @esengine/server # Game server
|
||||
npm install @esengine/network # Client networking
|
||||
npm install @esengine/transaction # Transaction system
|
||||
npm install @esengine/world-streaming # World streaming
|
||||
```
|
||||
|
||||
### ESEngine Runtime (Optional)
|
||||
|
||||
If you want a complete engine solution with rendering:
|
||||
|
||||
| Category | Packages |
|
||||
|----------|----------|
|
||||
| **Core** | `engine-core`, `asset-system`, `material-system` |
|
||||
| **Rendering** | `sprite`, `tilemap`, `particle`, `camera`, `mesh-3d` |
|
||||
| **Physics** | `physics-rapier2d` |
|
||||
| **Platform** | `platform-web`, `platform-wechat` |
|
||||
|
||||
### Editor (Optional)
|
||||
|
||||
A visual editor built with Tauri for scene management:
|
||||
|
||||
- Download from [Releases](https://github.com/esengine/esengine/releases)
|
||||
- [Build from source](./packages/editor/editor-app/README.md)
|
||||
- Supports behavior tree editing, tilemap painting, visual scripting
|
||||
|
||||
## 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
|
||||
│ │ ├── rpc/ # RPC framework
|
||||
│ │ ├── server/ # Game server
|
||||
│ │ ├── network/ # Client networking
|
||||
│ │ ├── transaction/ # Transaction system
|
||||
│ │ └── world-streaming/ # World streaming
|
||||
│ │
|
||||
│ ├── engine/ # ESEngine runtime
|
||||
│ ├── rendering/ # Rendering modules
|
||||
│ ├── physics/ # Physics modules
|
||||
│ ├── editor/ # Visual editor
|
||||
│ └── rust/ # WASM renderer
|
||||
│
|
||||
├── docs/ # Documentation
|
||||
└── examples/ # Examples
|
||||
```
|
||||
| Platform | Runtime | Editor |
|
||||
|----------|---------|--------|
|
||||
| Web Browser | Yes | - |
|
||||
| Windows | - | Yes |
|
||||
| macOS | - | Yes |
|
||||
| WeChat Mini Game | In Progress | - |
|
||||
| Playable Ads | Planned | - |
|
||||
| Android | Planned | - |
|
||||
| iOS | Planned | - |
|
||||
| Windows Native | Planned | - |
|
||||
| Other Platforms | Planned | - |
|
||||
|
||||
## Building from Source
|
||||
|
||||
```bash
|
||||
git clone https://github.com/esengine/esengine.git
|
||||
cd esengine
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 18 or later
|
||||
- pnpm 10 or later
|
||||
- Rust toolchain (for WASM renderer)
|
||||
- wasm-pack
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone https://github.com/esengine/ecs-framework.git
|
||||
cd ecs-framework
|
||||
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Build all packages
|
||||
pnpm build
|
||||
|
||||
# Type check framework packages
|
||||
pnpm type-check:framework
|
||||
# Build WASM renderer (optional)
|
||||
pnpm build:wasm
|
||||
```
|
||||
|
||||
# Run tests
|
||||
pnpm test
|
||||
### Running the Editor
|
||||
|
||||
```bash
|
||||
cd packages/editor-app
|
||||
pnpm tauri:dev
|
||||
```
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
ecs-framework/
|
||||
├── packages/ Engine packages (runtime, editor, platform)
|
||||
├── docs/ Documentation source
|
||||
├── examples/ Example projects
|
||||
├── scripts/ Build utilities
|
||||
└── thirdparty/ Third-party dependencies
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- [ECS Framework Guide](./packages/framework/core/README.md)
|
||||
- [Behavior Tree Guide](./packages/framework/behavior-tree/README.md)
|
||||
- [Editor Setup Guide](./packages/editor/editor-app/README.md) ([中文](./packages/editor/editor-app/README_CN.md))
|
||||
- [API Reference](https://esengine.cn/api/README)
|
||||
- [Getting Started](https://esengine.github.io/ecs-framework/guide/getting-started.html)
|
||||
- [Architecture Guide](https://esengine.github.io/ecs-framework/guide/)
|
||||
- [API Reference](https://esengine.github.io/ecs-framework/api/)
|
||||
|
||||
## Community
|
||||
|
||||
- [GitHub Issues](https://github.com/esengine/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
|
||||
- [GitHub Issues](https://github.com/esengine/ecs-framework/issues) - Bug reports and feature requests
|
||||
- [GitHub Discussions](https://github.com/esengine/ecs-framework/discussions) - Questions and ideas
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please read our contributing guidelines before submitting a pull request.
|
||||
Contributions are welcome. Please read the 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
|
||||
2. Create a feature branch
|
||||
3. Make changes with tests
|
||||
4. Submit a pull request
|
||||
|
||||
## License
|
||||
|
||||
ESEngine is licensed under the [MIT License](LICENSE). Free for personal and commercial use.
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
Made with care by the ESEngine community
|
||||
</p>
|
||||
ESEngine is licensed under the [MIT License](LICENSE).
|
||||
|
||||
357
README_CN.md
357
README_CN.md
@@ -1,85 +1,49 @@
|
||||
<h1 align="center">
|
||||
<img src="https://raw.githubusercontent.com/esengine/esengine/master/docs/public/logo.svg" alt="ESEngine" width="180">
|
||||
<br>
|
||||
ESEngine
|
||||
</h1>
|
||||
# ESEngine
|
||||
|
||||
<p align="center">
|
||||
<strong>TypeScript 模块化游戏框架</strong>
|
||||
</p>
|
||||
[English](./README.md) | **中文**
|
||||
|
||||
<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>
|
||||
**[文档](https://esengine.github.io/ecs-framework/) | [API 参考](https://esengine.github.io/ecs-framework/api/) | [示例](./examples/)**
|
||||
|
||||
<p align="center">
|
||||
<a href="./README.md">English</a> | <b>中文</b>
|
||||
</p>
|
||||
ESEngine 是一个跨平台 2D 游戏引擎,提供统一的开发界面。它包含完整的常用工具集,让开发者专注于游戏创作本身。
|
||||
|
||||
<p align="center">
|
||||
<a href="https://esengine.cn/">文档</a> ·
|
||||
<a href="https://esengine.cn/api/README">API 参考</a> ·
|
||||
<a href="./examples/">示例</a>
|
||||
</p>
|
||||
游戏可以导出到多个平台,包括 Web 浏览器、微信小游戏等小游戏平台。
|
||||
|
||||
---
|
||||
## 免费开源
|
||||
|
||||
## ESEngine 是什么?
|
||||
ESEngine 基于 MIT 协议完全免费开源。无附加条件,无版税。你的游戏完全属于你。
|
||||
|
||||
ESEngine 是一套**引擎无关的游戏开发模块**,可与 Cocos Creator、Laya、Phaser、PixiJS 等任何 JavaScript 游戏引擎配合使用。
|
||||
## 特性
|
||||
|
||||
核心是一个高性能的 **ECS(实体-组件-系统)** 框架,配套 AI、网络、物理等可选模块。
|
||||
- **数据驱动架构**:基于 ECS(实体-组件-系统)模式构建,提供灵活高效的游戏逻辑
|
||||
- **高性能渲染**:Rust/WebAssembly 2D 渲染器,支持精灵批处理和 WebGL 2.0
|
||||
- **可视化编辑器**:跨平台桌面编辑器,包含场景管理、资源浏览器和可视化工具
|
||||
- **模块化设计**:按需使用,每个功能都是独立模块,可单独引入
|
||||
- **多平台支持**:一套代码部署到 Web、微信小游戏等多个平台
|
||||
|
||||
## 获取引擎
|
||||
|
||||
### 通过 npm 安装
|
||||
|
||||
```bash
|
||||
npm install @esengine/ecs-framework
|
||||
```
|
||||
|
||||
## 功能模块
|
||||
### 从源码构建
|
||||
|
||||
| 模块 | 描述 | 需要渲染引擎 |
|
||||
|------|------|:----------:|
|
||||
| **ECS 核心** | 实体-组件-系统框架,支持响应式查询 | 否 |
|
||||
| **行为树** | AI 行为树,支持可视化编辑 | 否 |
|
||||
| **蓝图** | 可视化脚本系统 | 否 |
|
||||
| **状态机** | 有限状态机 | 否 |
|
||||
| **定时器** | 定时器和冷却系统 | 否 |
|
||||
| **空间索引** | 空间查询(四叉树、网格) | 否 |
|
||||
| **寻路** | A* 和导航网格寻路 | 否 |
|
||||
| **程序化生成** | 噪声、随机、采样等生成算法 | 否 |
|
||||
| **RPC** | 高性能 RPC 通信框架 | 否 |
|
||||
| **服务端** | 游戏服务器框架,支持房间、认证、速率限制 | 否 |
|
||||
| **网络** | 客户端网络,支持预测、AOI、增量压缩 | 否 |
|
||||
| **事务系统** | 游戏事务系统,支持 Redis/内存存储 | 否 |
|
||||
| **世界流送** | 开放世界分块加载和流送 | 否 |
|
||||
详见 [从源码构建](#从源码构建) 章节。
|
||||
|
||||
> 所有框架模块都可以独立使用,无需依赖特定渲染引擎。
|
||||
### 编辑器下载
|
||||
|
||||
预编译的编辑器可在 [Releases](https://github.com/esengine/ecs-framework/releases) 页面下载,支持 Windows 和 macOS。
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 使用 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;
|
||||
@@ -92,7 +56,6 @@ class Velocity extends Component {
|
||||
dy = 0;
|
||||
}
|
||||
|
||||
// 定义系统(逻辑)
|
||||
@ECSSystem('Movement')
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
@@ -109,7 +72,6 @@ class MovementSystem extends EntitySystem {
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
Core.create();
|
||||
const scene = new Scene();
|
||||
scene.addSystem(new MovementSystem());
|
||||
@@ -120,194 +82,165 @@ player.addComponent(new Velocity());
|
||||
|
||||
Core.setScene(scene);
|
||||
|
||||
// 集成到你的游戏循环
|
||||
// 游戏循环
|
||||
let lastTime = 0;
|
||||
function gameLoop(currentTime: number) {
|
||||
Core.update(currentTime / 1000);
|
||||
const deltaTime = (currentTime - lastTime) / 1000;
|
||||
lastTime = currentTime;
|
||||
|
||||
Core.update(deltaTime);
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
requestAnimationFrame(gameLoop);
|
||||
```
|
||||
|
||||
## 与其他引擎配合使用
|
||||
## 模块
|
||||
|
||||
ESEngine 的框架模块设计为可与你喜欢的渲染引擎配合使用:
|
||||
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/procgen # 程序化生成
|
||||
npm install @esengine/rpc # RPC 框架
|
||||
npm install @esengine/server # 游戏服务器
|
||||
npm install @esengine/network # 客户端网络
|
||||
npm install @esengine/transaction # 事务系统
|
||||
npm install @esengine/world-streaming # 世界流送
|
||||
```
|
||||
|
||||
### ESEngine 运行时(可选)
|
||||
|
||||
如果你需要完整的引擎解决方案:
|
||||
|
||||
| 分类 | 包名 |
|
||||
| 包名 | 描述 |
|
||||
|------|------|
|
||||
| **核心** | `engine-core`, `asset-system`, `material-system` |
|
||||
| **渲染** | `sprite`, `tilemap`, `particle`, `camera`, `mesh-3d` |
|
||||
| **物理** | `physics-rapier2d` |
|
||||
| **平台** | `platform-web`, `platform-wechat` |
|
||||
| `@esengine/ecs-framework` | ECS 框架核心,包含实体管理、组件系统和查询 |
|
||||
| `@esengine/math` | 向量、矩阵和数学工具 |
|
||||
| `@esengine/engine` | Rust/WASM 2D 渲染器 |
|
||||
| `@esengine/engine-core` | 引擎模块系统和生命周期管理 |
|
||||
|
||||
### 编辑器(可选)
|
||||
### 运行时模块
|
||||
|
||||
基于 Tauri 构建的可视化编辑器:
|
||||
| 包名 | 描述 |
|
||||
|------|------|
|
||||
| `@esengine/sprite` | 2D 精灵渲染和动画 |
|
||||
| `@esengine/tilemap` | Tilemap 渲染,支持动画 |
|
||||
| `@esengine/physics-rapier2d` | 基于 Rapier 的 2D 物理模拟 |
|
||||
| `@esengine/behavior-tree` | 行为树 AI 系统 |
|
||||
| `@esengine/blueprint` | 可视化脚本运行时 |
|
||||
| `@esengine/camera` | 相机控制和管理 |
|
||||
| `@esengine/audio` | 音频播放 |
|
||||
| `@esengine/ui` | UI 组件 |
|
||||
| `@esengine/material-system` | 材质和着色器系统 |
|
||||
| `@esengine/asset-system` | 资源加载和管理 |
|
||||
|
||||
- 从 [Releases](https://github.com/esengine/esengine/releases) 下载
|
||||
- [从源码构建](./packages/editor/editor-app/README.md)
|
||||
- 支持行为树编辑、Tilemap 绘制、可视化脚本
|
||||
### 编辑器扩展
|
||||
|
||||
## 项目结构
|
||||
| 包名 | 描述 |
|
||||
|------|------|
|
||||
| `@esengine/sprite-editor` | 精灵检视器和工具 |
|
||||
| `@esengine/tilemap-editor` | 可视化 Tilemap 编辑器,支持笔刷工具 |
|
||||
| `@esengine/physics-rapier2d-editor` | 物理碰撞体可视化和编辑 |
|
||||
| `@esengine/behavior-tree-editor` | 可视化行为树编辑器 |
|
||||
| `@esengine/blueprint-editor` | 可视化脚本编辑器 |
|
||||
| `@esengine/material-editor` | 材质和着色器编辑器 |
|
||||
| `@esengine/shader-editor` | 着色器代码编辑器 |
|
||||
|
||||
```
|
||||
esengine/
|
||||
├── packages/
|
||||
│ ├── framework/ # 引擎无关模块(可发布到 NPM)
|
||||
│ │ ├── core/ # ECS 框架
|
||||
│ │ ├── math/ # 数学工具
|
||||
│ │ ├── behavior-tree/ # AI 行为树
|
||||
│ │ ├── blueprint/ # 可视化脚本
|
||||
│ │ ├── fsm/ # 有限状态机
|
||||
│ │ ├── timer/ # 定时器系统
|
||||
│ │ ├── spatial/ # 空间查询
|
||||
│ │ ├── pathfinding/ # 寻路
|
||||
│ │ ├── procgen/ # 程序化生成
|
||||
│ │ ├── rpc/ # RPC 框架
|
||||
│ │ ├── server/ # 游戏服务器
|
||||
│ │ ├── network/ # 客户端网络
|
||||
│ │ ├── transaction/ # 事务系统
|
||||
│ │ └── world-streaming/ # 世界流送
|
||||
│ │
|
||||
│ ├── engine/ # ESEngine 运行时
|
||||
│ ├── rendering/ # 渲染模块
|
||||
│ ├── physics/ # 物理模块
|
||||
│ ├── editor/ # 可视化编辑器
|
||||
│ └── rust/ # WASM 渲染器
|
||||
│
|
||||
├── docs/ # 文档
|
||||
└── examples/ # 示例
|
||||
```
|
||||
### 平台
|
||||
|
||||
| 包名 | 描述 |
|
||||
|------|------|
|
||||
| `@esengine/platform-common` | 平台抽象接口 |
|
||||
| `@esengine/platform-web` | Web 浏览器运行时 |
|
||||
| `@esengine/platform-wechat` | 微信小游戏运行时 |
|
||||
|
||||
## 编辑器
|
||||
|
||||
ESEngine 编辑器是基于 Tauri 和 React 构建的跨平台桌面应用。
|
||||
|
||||
### 功能
|
||||
|
||||
- 场景层级和实体管理
|
||||
- 组件检视器,支持自定义编辑器
|
||||
- 资源浏览器,支持拖放
|
||||
- Tilemap 编辑器,支持绘制、填充、选择工具
|
||||
- 行为树可视化编辑器
|
||||
- 蓝图可视化脚本
|
||||
- 材质和着色器编辑
|
||||
- 内置性能分析器
|
||||
- 多语言支持(英文、中文)
|
||||
|
||||
### 截图
|
||||
|
||||

|
||||
|
||||
## 支持的平台
|
||||
|
||||
| 平台 | 运行时 | 编辑器 |
|
||||
|------|--------|--------|
|
||||
| Web 浏览器 | 支持 | - |
|
||||
| Windows | - | 支持 |
|
||||
| macOS | - | 支持 |
|
||||
| 微信小游戏 | 开发中 | - |
|
||||
| Playable 可玩广告 | 计划中 | - |
|
||||
| Android | 计划中 | - |
|
||||
| iOS | 计划中 | - |
|
||||
| Windows 原生 | 计划中 | - |
|
||||
| 其他平台 | 计划中 | - |
|
||||
|
||||
## 从源码构建
|
||||
|
||||
```bash
|
||||
git clone https://github.com/esengine/esengine.git
|
||||
cd esengine
|
||||
### 前置要求
|
||||
|
||||
- Node.js 18 或更高版本
|
||||
- pnpm 10 或更高版本
|
||||
- Rust 工具链(用于 WASM 渲染器)
|
||||
- wasm-pack
|
||||
|
||||
### 安装
|
||||
|
||||
```bash
|
||||
# 克隆仓库
|
||||
git clone https://github.com/esengine/ecs-framework.git
|
||||
cd ecs-framework
|
||||
|
||||
# 安装依赖
|
||||
pnpm install
|
||||
|
||||
# 构建所有包
|
||||
pnpm build
|
||||
|
||||
# 框架包类型检查
|
||||
pnpm type-check:framework
|
||||
# 构建 WASM 渲染器(可选)
|
||||
pnpm build:wasm
|
||||
```
|
||||
|
||||
# 运行测试
|
||||
pnpm test
|
||||
### 运行编辑器
|
||||
|
||||
```bash
|
||||
cd packages/editor-app
|
||||
pnpm tauri:dev
|
||||
```
|
||||
|
||||
### 项目结构
|
||||
|
||||
```
|
||||
ecs-framework/
|
||||
├── packages/ 引擎包(运行时、编辑器、平台)
|
||||
├── docs/ 文档源码
|
||||
├── examples/ 示例项目
|
||||
├── scripts/ 构建工具
|
||||
└── thirdparty/ 第三方依赖
|
||||
```
|
||||
|
||||
## 文档
|
||||
|
||||
- [ECS 框架指南](./packages/framework/core/README.md)
|
||||
- [行为树指南](./packages/framework/behavior-tree/README.md)
|
||||
- [编辑器启动指南](./packages/editor/editor-app/README_CN.md) ([English](./packages/editor/editor-app/README.md))
|
||||
- [API 参考](https://esengine.cn/api/README)
|
||||
- [快速入门](https://esengine.github.io/ecs-framework/guide/getting-started.html)
|
||||
- [架构指南](https://esengine.github.io/ecs-framework/guide/)
|
||||
- [API 参考](https://esengine.github.io/ecs-framework/api/)
|
||||
|
||||
## 社区
|
||||
|
||||
- [GitHub Issues](https://github.com/esengine/ecs-framework/issues) - Bug 反馈和功能建议
|
||||
- [GitHub Discussions](https://github.com/esengine/ecs-framework/discussions) - 问题和想法
|
||||
- [QQ 交流群](https://jq.qq.com/?_wv=1027&k=29w1Nud6) - 中文社区
|
||||
- [Discord](https://discord.gg/gCAgzXFW) - 国际社区
|
||||
- [GitHub Issues](https://github.com/esengine/esengine/issues) - Bug 反馈和功能建议
|
||||
- [GitHub Discussions](https://github.com/esengine/esengine/discussions) - 问题和想法
|
||||
|
||||
## 贡献
|
||||
|
||||
欢迎贡献代码!提交 PR 前请阅读贡献指南。
|
||||
欢迎贡献代码。提交 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
|
||||
2. 创建功能分支
|
||||
3. 修改代码并测试
|
||||
4. 提交 PR
|
||||
|
||||
## 许可证
|
||||
|
||||
ESEngine 基于 [MIT 协议](LICENSE) 开源,个人和商业使用均免费。
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
由 ESEngine 社区用心打造
|
||||
</p>
|
||||
ESEngine 基于 [MIT 协议](LICENSE) 开源。
|
||||
|
||||
76
SECURITY.md
76
SECURITY.md
@@ -1,71 +1,13 @@
|
||||
# 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.x.x | :white_check_mark: |
|
||||
| 1.x.x | :x: |
|
||||
| 2.0.x | :white_check_mark: |
|
||||
| 1.0.x | :x: |
|
||||
|
||||
## 报告漏洞
|
||||
|
||||
@@ -73,10 +15,10 @@ When using ESEngine, please follow these security recommendations:
|
||||
|
||||
### 报告渠道
|
||||
|
||||
- **GitHub 安全公告**: [报告漏洞](https://github.com/esengine/esengine/security/advisories/new)(推荐)
|
||||
- **邮箱**: security@esengine.dev
|
||||
- **邮箱**: [安全邮箱将在实际部署时提供]
|
||||
- **GitHub**: 创建私有安全报告(推荐)
|
||||
|
||||
### 报告指南
|
||||
### 报告流程
|
||||
|
||||
1. **不要**在公开的 issue 中报告安全漏洞
|
||||
2. 提供详细的漏洞描述,包括:
|
||||
@@ -98,9 +40,9 @@ When using ESEngine, please follow these security recommendations:
|
||||
3. 发布安全更新
|
||||
4. 在修复发布后,会在相关渠道公布漏洞详情
|
||||
|
||||
## 安全最佳实践
|
||||
### 安全最佳实践
|
||||
|
||||
使用 ESEngine 时,请遵循以下安全建议:
|
||||
使用 ECS Framework 时,请遵循以下安全建议:
|
||||
|
||||
- 始终使用最新的稳定版本
|
||||
- 定期更新依赖项
|
||||
@@ -108,6 +50,4 @@ When using ESEngine, please follow these security recommendations:
|
||||
- 验证所有外部输入数据
|
||||
- 不要在客户端存储敏感信息
|
||||
|
||||
感谢您帮助保持 ESEngine 的安全性!
|
||||
|
||||
Thank you for helping keep ESEngine secure!
|
||||
感谢您帮助保持 ECS Framework 的安全性!
|
||||
|
||||
21
docs/.gitignore
vendored
21
docs/.gitignore
vendored
@@ -1,21 +0,0 @@
|
||||
# build output
|
||||
dist/
|
||||
# generated types
|
||||
.astro/
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
282
docs/.vitepress/config.mjs
Normal file
282
docs/.vitepress/config.mjs
Normal file
@@ -0,0 +1,282 @@
|
||||
import { defineConfig } from 'vitepress'
|
||||
import Icons from 'unplugin-icons/vite'
|
||||
import { readFileSync } from 'fs'
|
||||
import { join, dirname } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const corePackageJson = JSON.parse(
|
||||
readFileSync(join(__dirname, '../../packages/core/package.json'), 'utf-8')
|
||||
)
|
||||
|
||||
// Import i18n messages
|
||||
import en from './i18n/en.json' with { type: 'json' }
|
||||
import zh from './i18n/zh.json' with { type: 'json' }
|
||||
|
||||
// 创建侧边栏配置 | Create sidebar config
|
||||
// prefix: 路径前缀,如 '' 或 '/en' | Path prefix like '' or '/en'
|
||||
function createSidebar(t, prefix = '') {
|
||||
return {
|
||||
[`${prefix}/guide/`]: [
|
||||
{
|
||||
text: t.sidebar.gettingStarted,
|
||||
items: [
|
||||
{ text: t.sidebar.quickStart, link: `${prefix}/guide/getting-started` },
|
||||
{ text: t.sidebar.guideOverview, link: `${prefix}/guide/` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.coreConcepts,
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: t.sidebar.entity, link: `${prefix}/guide/entity` },
|
||||
{ text: t.sidebar.hierarchy, link: `${prefix}/guide/hierarchy` },
|
||||
{ text: t.sidebar.component, link: `${prefix}/guide/component` },
|
||||
{ text: t.sidebar.entityQuery, link: `${prefix}/guide/entity-query` },
|
||||
{
|
||||
text: t.sidebar.system,
|
||||
link: `${prefix}/guide/system`,
|
||||
items: [
|
||||
{ text: t.sidebar.workerSystem, link: `${prefix}/guide/worker-system` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.scene,
|
||||
link: `${prefix}/guide/scene`,
|
||||
items: [
|
||||
{ text: t.sidebar.sceneManager, link: `${prefix}/guide/scene-manager` },
|
||||
{ text: t.sidebar.worldManager, link: `${prefix}/guide/world-manager` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.behaviorTree,
|
||||
link: `${prefix}/guide/behavior-tree/`,
|
||||
items: [
|
||||
{ text: t.sidebar.btGettingStarted, link: `${prefix}/guide/behavior-tree/getting-started` },
|
||||
{ text: t.sidebar.btCoreConcepts, link: `${prefix}/guide/behavior-tree/core-concepts` },
|
||||
{ text: t.sidebar.btEditorGuide, link: `${prefix}/guide/behavior-tree/editor-guide` },
|
||||
{ text: t.sidebar.btEditorWorkflow, link: `${prefix}/guide/behavior-tree/editor-workflow` },
|
||||
{ text: t.sidebar.btCustomActions, link: `${prefix}/guide/behavior-tree/custom-actions` },
|
||||
{ text: t.sidebar.btCocosIntegration, link: `${prefix}/guide/behavior-tree/cocos-integration` },
|
||||
{ text: t.sidebar.btLayaIntegration, link: `${prefix}/guide/behavior-tree/laya-integration` },
|
||||
{ text: t.sidebar.btAdvancedUsage, link: `${prefix}/guide/behavior-tree/advanced-usage` },
|
||||
{ text: t.sidebar.btBestPractices, link: `${prefix}/guide/behavior-tree/best-practices` }
|
||||
]
|
||||
},
|
||||
{ text: t.sidebar.serialization, link: `${prefix}/guide/serialization` },
|
||||
{ text: t.sidebar.eventSystem, link: `${prefix}/guide/event-system` },
|
||||
{ text: t.sidebar.timeAndTimers, link: `${prefix}/guide/time-and-timers` },
|
||||
{ text: t.sidebar.logging, link: `${prefix}/guide/logging` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.advancedFeatures,
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: t.sidebar.serviceContainer, link: `${prefix}/guide/service-container` },
|
||||
{ text: t.sidebar.pluginSystem, link: `${prefix}/guide/plugin-system` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.platformAdapters,
|
||||
link: `${prefix}/guide/platform-adapter`,
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: t.sidebar.browserAdapter, link: `${prefix}/guide/platform-adapter/browser` },
|
||||
{ text: t.sidebar.wechatAdapter, link: `${prefix}/guide/platform-adapter/wechat-minigame` },
|
||||
{ text: t.sidebar.nodejsAdapter, link: `${prefix}/guide/platform-adapter/nodejs` }
|
||||
]
|
||||
}
|
||||
],
|
||||
[`${prefix}/examples/`]: [
|
||||
{
|
||||
text: t.sidebar.examples,
|
||||
items: [
|
||||
{ text: t.sidebar.examplesOverview, link: `${prefix}/examples/` },
|
||||
{ text: t.nav.workerDemo, link: `${prefix}/examples/worker-system-demo` }
|
||||
]
|
||||
}
|
||||
],
|
||||
[`${prefix}/api/`]: [
|
||||
{
|
||||
text: t.sidebar.apiReference,
|
||||
items: [
|
||||
{ text: t.sidebar.overview, link: `${prefix}/api/README` },
|
||||
{
|
||||
text: t.sidebar.coreClasses,
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: 'Core', link: `${prefix}/api/classes/Core` },
|
||||
{ text: 'Scene', link: `${prefix}/api/classes/Scene` },
|
||||
{ text: 'World', link: `${prefix}/api/classes/World` },
|
||||
{ text: 'Entity', link: `${prefix}/api/classes/Entity` },
|
||||
{ text: 'Component', link: `${prefix}/api/classes/Component` },
|
||||
{ text: 'EntitySystem', link: `${prefix}/api/classes/EntitySystem` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.systemClasses,
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'PassiveSystem', link: `${prefix}/api/classes/PassiveSystem` },
|
||||
{ text: 'ProcessingSystem', link: `${prefix}/api/classes/ProcessingSystem` },
|
||||
{ text: 'IntervalSystem', link: `${prefix}/api/classes/IntervalSystem` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.utilities,
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Matcher', link: `${prefix}/api/classes/Matcher` },
|
||||
{ text: 'Time', link: `${prefix}/api/classes/Time` },
|
||||
{ text: 'PerformanceMonitor', link: `${prefix}/api/classes/PerformanceMonitor` },
|
||||
{ text: 'DebugManager', link: `${prefix}/api/classes/DebugManager` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.interfaces,
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'IScene', link: `${prefix}/api/interfaces/IScene` },
|
||||
{ text: 'IComponent', link: `${prefix}/api/interfaces/IComponent` },
|
||||
{ text: 'ISystemBase', link: `${prefix}/api/interfaces/ISystemBase` },
|
||||
{ text: 'ICoreConfig', link: `${prefix}/api/interfaces/ICoreConfig` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.decorators,
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: '@ECSComponent', link: `${prefix}/api/functions/ECSComponent` },
|
||||
{ text: '@ECSSystem', link: `${prefix}/api/functions/ECSSystem` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.enums,
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'ECSEventType', link: `${prefix}/api/enumerations/ECSEventType` },
|
||||
{ text: 'LogLevel', link: `${prefix}/api/enumerations/LogLevel` }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// 创建导航配置 | Create nav config
|
||||
// prefix: 路径前缀,如 '' 或 '/en' | Path prefix like '' or '/en'
|
||||
function createNav(t, prefix = '') {
|
||||
return [
|
||||
{ text: t.nav.home, link: `${prefix}/` },
|
||||
{ text: t.nav.quickStart, link: `${prefix}/guide/getting-started` },
|
||||
{ text: t.nav.guide, link: `${prefix}/guide/` },
|
||||
{ text: t.nav.api, link: `${prefix}/api/README` },
|
||||
{
|
||||
text: t.nav.examples,
|
||||
items: [
|
||||
{ text: t.nav.workerDemo, link: `${prefix}/examples/worker-system-demo` },
|
||||
{ text: t.nav.lawnMowerDemo, link: 'https://github.com/esengine/lawn-mower-demo' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: `v${corePackageJson.version}`,
|
||||
link: 'https://github.com/esengine/ecs-framework/releases'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
vite: {
|
||||
plugins: [
|
||||
Icons({
|
||||
compiler: 'vue3',
|
||||
autoInstall: true
|
||||
})
|
||||
],
|
||||
server: {
|
||||
fs: {
|
||||
allow: ['..']
|
||||
},
|
||||
middlewareMode: false,
|
||||
headers: {
|
||||
'Cross-Origin-Embedder-Policy': 'require-corp',
|
||||
'Cross-Origin-Opener-Policy': 'same-origin'
|
||||
}
|
||||
}
|
||||
},
|
||||
title: '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/ecs-framework/edit/master/docs/:path',
|
||||
text: zh.common.editOnGithub
|
||||
},
|
||||
outline: {
|
||||
level: [2, 3],
|
||||
label: zh.common.onThisPage
|
||||
}
|
||||
}
|
||||
},
|
||||
en: {
|
||||
label: 'English',
|
||||
lang: 'en',
|
||||
link: '/en/',
|
||||
description: 'High-performance TypeScript ECS Framework for Game Development',
|
||||
themeConfig: {
|
||||
nav: createNav(en, '/en'),
|
||||
sidebar: createSidebar(en, '/en'),
|
||||
editLink: {
|
||||
pattern: 'https://github.com/esengine/ecs-framework/edit/master/docs/:path',
|
||||
text: en.common.editOnGithub
|
||||
},
|
||||
outline: {
|
||||
level: [2, 3],
|
||||
label: en.common.onThisPage
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
themeConfig: {
|
||||
siteTitle: 'ESEngine',
|
||||
|
||||
socialLinks: [
|
||||
{ icon: 'github', link: 'https://github.com/esengine/ecs-framework' }
|
||||
],
|
||||
|
||||
footer: {
|
||||
message: 'Released under the MIT License.',
|
||||
copyright: 'Copyright © 2025 ECS Framework'
|
||||
},
|
||||
|
||||
search: {
|
||||
provider: 'local'
|
||||
}
|
||||
},
|
||||
|
||||
head: [
|
||||
['meta', { name: 'theme-color', content: '#646cff' }],
|
||||
['link', { rel: 'icon', href: '/favicon.ico' }]
|
||||
],
|
||||
|
||||
base: '/',
|
||||
cleanUrls: true,
|
||||
|
||||
markdown: {
|
||||
lineNumbers: true,
|
||||
theme: {
|
||||
light: 'github-light',
|
||||
dark: 'github-dark'
|
||||
}
|
||||
}
|
||||
})
|
||||
85
docs/.vitepress/i18n/en.json
Normal file
85
docs/.vitepress/i18n/en.json
Normal file
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"nav": {
|
||||
"home": "Home",
|
||||
"quickStart": "Quick Start",
|
||||
"guide": "Guide",
|
||||
"api": "API",
|
||||
"examples": "Examples",
|
||||
"workerDemo": "Worker System Demo",
|
||||
"lawnMowerDemo": "Lawn Mower Demo"
|
||||
},
|
||||
"sidebar": {
|
||||
"gettingStarted": "Getting Started",
|
||||
"quickStart": "Quick Start",
|
||||
"guideOverview": "Guide Overview",
|
||||
"coreConcepts": "Core Concepts",
|
||||
"entity": "Entity",
|
||||
"hierarchy": "Hierarchy",
|
||||
"component": "Component",
|
||||
"entityQuery": "Entity Query",
|
||||
"system": "System",
|
||||
"workerSystem": "Worker System (Multithreading)",
|
||||
"scene": "Scene",
|
||||
"sceneManager": "SceneManager",
|
||||
"worldManager": "WorldManager",
|
||||
"behaviorTree": "Behavior Tree",
|
||||
"btGettingStarted": "Getting Started",
|
||||
"btCoreConcepts": "Core Concepts",
|
||||
"btEditorGuide": "Editor Guide",
|
||||
"btEditorWorkflow": "Editor Workflow",
|
||||
"btCustomActions": "Custom Actions",
|
||||
"btCocosIntegration": "Cocos Creator Integration",
|
||||
"btLayaIntegration": "Laya Engine Integration",
|
||||
"btAdvancedUsage": "Advanced Usage",
|
||||
"btBestPractices": "Best Practices",
|
||||
"serialization": "Serialization",
|
||||
"eventSystem": "Event System",
|
||||
"timeAndTimers": "Time and Timers",
|
||||
"logging": "Logging",
|
||||
"advancedFeatures": "Advanced Features",
|
||||
"serviceContainer": "Service Container",
|
||||
"pluginSystem": "Plugin System",
|
||||
"platformAdapters": "Platform Adapters",
|
||||
"browserAdapter": "Browser Adapter",
|
||||
"wechatAdapter": "WeChat Mini Game Adapter",
|
||||
"nodejsAdapter": "Node.js Adapter",
|
||||
"examples": "Examples",
|
||||
"examplesOverview": "Examples Overview",
|
||||
"apiReference": "API Reference",
|
||||
"overview": "Overview",
|
||||
"coreClasses": "Core Classes",
|
||||
"systemClasses": "System Classes",
|
||||
"utilities": "Utilities",
|
||||
"interfaces": "Interfaces",
|
||||
"decorators": "Decorators",
|
||||
"enums": "Enums"
|
||||
},
|
||||
"home": {
|
||||
"title": "ESEngine - High-performance TypeScript ECS Framework",
|
||||
"quickLinks": "Quick Links",
|
||||
"viewDocs": "View Docs",
|
||||
"getStarted": "Get Started",
|
||||
"getStartedDesc": "From installation to your first ECS app, learn the core concepts in 5 minutes.",
|
||||
"aiSystem": "AI System",
|
||||
"behaviorTreeEditor": "Visual Behavior Tree Editor",
|
||||
"behaviorTreeDesc": "Built-in AI behavior tree system with visual editing and real-time debugging.",
|
||||
"coreFeatures": "Core Features",
|
||||
"ecsArchitecture": "High-performance ECS Architecture",
|
||||
"ecsArchitectureDesc": "Data-driven entity component system for large-scale entity processing with cache-friendly memory layout.",
|
||||
"typeSupport": "Full Type Support",
|
||||
"typeSupportDesc": "100% TypeScript with complete type definitions and compile-time checking for the best development experience.",
|
||||
"visualBehaviorTree": "Visual Behavior Tree",
|
||||
"visualBehaviorTreeDesc": "Built-in AI behavior tree system with visual editor, custom nodes, and real-time debugging.",
|
||||
"multiPlatform": "Multi-Platform Support",
|
||||
"multiPlatformDesc": "Support for browsers, Node.js, WeChat Mini Games, and seamless integration with major game engines.",
|
||||
"modularDesign": "Modular Design",
|
||||
"modularDesignDesc": "Core features packaged independently, import only what you need. Support for custom plugin extensions.",
|
||||
"devTools": "Developer Tools",
|
||||
"devToolsDesc": "Built-in performance monitoring, debugging tools, serialization system, and complete development toolchain.",
|
||||
"learnMore": "Learn more →"
|
||||
},
|
||||
"common": {
|
||||
"editOnGithub": "Edit this page on GitHub",
|
||||
"onThisPage": "On this page"
|
||||
}
|
||||
}
|
||||
21
docs/.vitepress/i18n/index.ts
Normal file
21
docs/.vitepress/i18n/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import en from './en.json'
|
||||
import zh from './zh.json'
|
||||
|
||||
export const messages = { en, zh }
|
||||
|
||||
export type Locale = 'en' | 'zh'
|
||||
|
||||
export function getLocaleMessages(locale: Locale) {
|
||||
return messages[locale] || messages.en
|
||||
}
|
||||
|
||||
// Helper to get nested key value
|
||||
export function t(messages: typeof en, key: string): string {
|
||||
const keys = key.split('.')
|
||||
let result: any = messages
|
||||
for (const k of keys) {
|
||||
result = result?.[k]
|
||||
if (result === undefined) return key
|
||||
}
|
||||
return result
|
||||
}
|
||||
85
docs/.vitepress/i18n/zh.json
Normal file
85
docs/.vitepress/i18n/zh.json
Normal file
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"nav": {
|
||||
"home": "首页",
|
||||
"quickStart": "快速开始",
|
||||
"guide": "指南",
|
||||
"api": "API",
|
||||
"examples": "示例",
|
||||
"workerDemo": "Worker系统演示",
|
||||
"lawnMowerDemo": "割草机演示"
|
||||
},
|
||||
"sidebar": {
|
||||
"gettingStarted": "开始使用",
|
||||
"quickStart": "快速开始",
|
||||
"guideOverview": "指南概览",
|
||||
"coreConcepts": "核心概念",
|
||||
"entity": "实体类 (Entity)",
|
||||
"hierarchy": "层级系统 (Hierarchy)",
|
||||
"component": "组件系统 (Component)",
|
||||
"entityQuery": "实体查询系统",
|
||||
"system": "系统架构 (System)",
|
||||
"workerSystem": "Worker系统 (多线程)",
|
||||
"scene": "场景管理 (Scene)",
|
||||
"sceneManager": "SceneManager",
|
||||
"worldManager": "WorldManager",
|
||||
"behaviorTree": "行为树系统 (Behavior Tree)",
|
||||
"btGettingStarted": "快速开始",
|
||||
"btCoreConcepts": "核心概念",
|
||||
"btEditorGuide": "编辑器指南",
|
||||
"btEditorWorkflow": "编辑器工作流",
|
||||
"btCustomActions": "自定义动作组件",
|
||||
"btCocosIntegration": "Cocos Creator集成",
|
||||
"btLayaIntegration": "Laya引擎集成",
|
||||
"btAdvancedUsage": "高级用法",
|
||||
"btBestPractices": "最佳实践",
|
||||
"serialization": "序列化系统 (Serialization)",
|
||||
"eventSystem": "事件系统 (Event)",
|
||||
"timeAndTimers": "时间和定时器 (Time)",
|
||||
"logging": "日志系统 (Logger)",
|
||||
"advancedFeatures": "高级特性",
|
||||
"serviceContainer": "服务容器 (Service Container)",
|
||||
"pluginSystem": "插件系统 (Plugin System)",
|
||||
"platformAdapters": "平台适配器",
|
||||
"browserAdapter": "浏览器适配器",
|
||||
"wechatAdapter": "微信小游戏适配器",
|
||||
"nodejsAdapter": "Node.js适配器",
|
||||
"examples": "示例",
|
||||
"examplesOverview": "示例概览",
|
||||
"apiReference": "API 参考",
|
||||
"overview": "概述",
|
||||
"coreClasses": "核心类",
|
||||
"systemClasses": "系统类",
|
||||
"utilities": "工具类",
|
||||
"interfaces": "接口",
|
||||
"decorators": "装饰器",
|
||||
"enums": "枚举"
|
||||
},
|
||||
"home": {
|
||||
"title": "ESEngine - 高性能 TypeScript ECS 框架",
|
||||
"quickLinks": "快速入口",
|
||||
"viewDocs": "查看文档",
|
||||
"getStarted": "快速开始",
|
||||
"getStartedDesc": "从安装到创建第一个 ECS 应用,快速了解核心概念。",
|
||||
"aiSystem": "AI 系统",
|
||||
"behaviorTreeEditor": "行为树可视化编辑器",
|
||||
"behaviorTreeDesc": "内置 AI 行为树系统,支持可视化编辑和实时调试。",
|
||||
"coreFeatures": "核心特性",
|
||||
"ecsArchitecture": "高性能 ECS 架构",
|
||||
"ecsArchitectureDesc": "基于数据驱动的实体组件系统,支持大规模实体处理,缓存友好的内存布局。",
|
||||
"typeSupport": "完整类型支持",
|
||||
"typeSupportDesc": "100% TypeScript 编写,完整的类型定义和编译时检查,提供最佳的开发体验。",
|
||||
"visualBehaviorTree": "可视化行为树",
|
||||
"visualBehaviorTreeDesc": "内置 AI 行为树系统,提供可视化编辑器,支持自定义节点和实时调试。",
|
||||
"multiPlatform": "多平台支持",
|
||||
"multiPlatformDesc": "支持浏览器、Node.js、微信小游戏等多平台,可与主流游戏引擎无缝集成。",
|
||||
"modularDesign": "模块化设计",
|
||||
"modularDesignDesc": "核心功能独立打包,按需引入。支持自定义插件扩展,灵活适配不同项目。",
|
||||
"devTools": "开发者工具",
|
||||
"devToolsDesc": "内置性能监控、调试工具、序列化系统等,提供完整的开发工具链。",
|
||||
"learnMore": "了解更多 →"
|
||||
},
|
||||
"common": {
|
||||
"editOnGithub": "在 GitHub 上编辑此页",
|
||||
"onThisPage": "在这个页面上"
|
||||
}
|
||||
}
|
||||
93
docs/.vitepress/theme/components/FeatureCard.vue
Normal file
93
docs/.vitepress/theme/components/FeatureCard.vue
Normal file
@@ -0,0 +1,93 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
title: String,
|
||||
description: String,
|
||||
icon: String,
|
||||
link: String,
|
||||
image: String
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a :href="link" class="feature-card">
|
||||
<div class="card-image" v-if="image">
|
||||
<img :src="image" :alt="title" />
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="card-icon" v-if="icon && !image">{{ icon }}</div>
|
||||
<h3 class="card-title">{{ title }}</h3>
|
||||
<p class="card-description">{{ description }}</p>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.feature-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--es-bg-elevated, #252526);
|
||||
border: 1px solid var(--es-border-default, #3e3e42);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
border-color: var(--es-primary, #007acc);
|
||||
background: var(--es-bg-overlay, #2d2d2d);
|
||||
}
|
||||
|
||||
.card-image {
|
||||
width: 100%;
|
||||
height: 160px;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #1e3a5f 0%, #1e1e1e 100%);
|
||||
}
|
||||
|
||||
.card-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.feature-card:hover .card-image img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 16px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 12px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--es-bg-input, #3c3c3c);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--es-text-inverse, #ffffff);
|
||||
margin: 0 0 8px 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.card-description {
|
||||
font-size: 12px;
|
||||
color: var(--es-text-secondary, #9d9d9d);
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
422
docs/.vitepress/theme/components/ParticleHero.vue
Normal file
422
docs/.vitepress/theme/components/ParticleHero.vue
Normal file
@@ -0,0 +1,422 @@
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
const canvasRef = ref(null)
|
||||
let animationId = null
|
||||
let particles = []
|
||||
let animationStartTime = null
|
||||
let glowStartTime = null
|
||||
|
||||
// ESEngine 粒子颜色 - VS Code 风格配色(与编辑器统一)
|
||||
const colors = ['#569CD6', '#4EC9B0', '#9CDCFE', '#C586C0', '#DCDCAA']
|
||||
|
||||
class Particle {
|
||||
constructor(x, y, targetX, targetY) {
|
||||
this.x = x
|
||||
this.y = y
|
||||
this.targetX = targetX
|
||||
this.targetY = targetY
|
||||
this.size = Math.random() * 2 + 1.5
|
||||
this.alpha = Math.random() * 0.5 + 0.5
|
||||
this.color = colors[Math.floor(Math.random() * colors.length)]
|
||||
}
|
||||
}
|
||||
|
||||
function createParticles(canvas, text, fontSize) {
|
||||
const tempCanvas = document.createElement('canvas')
|
||||
const tempCtx = tempCanvas.getContext('2d')
|
||||
if (!tempCtx) return []
|
||||
|
||||
tempCtx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||
const textMetrics = tempCtx.measureText(text)
|
||||
const textWidth = textMetrics.width
|
||||
const textHeight = fontSize
|
||||
|
||||
tempCanvas.width = textWidth + 40
|
||||
tempCanvas.height = textHeight + 40
|
||||
tempCtx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||
tempCtx.textAlign = 'center'
|
||||
tempCtx.textBaseline = 'middle'
|
||||
tempCtx.fillStyle = '#ffffff'
|
||||
tempCtx.fillText(text, tempCanvas.width / 2, tempCanvas.height / 2)
|
||||
|
||||
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height)
|
||||
const pixels = imageData.data
|
||||
const newParticles = []
|
||||
const gap = 3
|
||||
|
||||
const width = canvas.width / (window.devicePixelRatio || 1)
|
||||
const height = canvas.height / (window.devicePixelRatio || 1)
|
||||
const offsetX = (width - tempCanvas.width) / 2
|
||||
const offsetY = (height - tempCanvas.height) / 2
|
||||
|
||||
for (let y = 0; y < tempCanvas.height; y += gap) {
|
||||
for (let x = 0; x < tempCanvas.width; x += gap) {
|
||||
const index = (y * tempCanvas.width + x) * 4
|
||||
const alpha = pixels[index + 3] || 0
|
||||
|
||||
if (alpha > 128) {
|
||||
const angle = Math.random() * Math.PI * 2
|
||||
const distance = Math.random() * Math.max(width, height)
|
||||
|
||||
newParticles.push(new Particle(
|
||||
width / 2 + Math.cos(angle) * distance,
|
||||
height / 2 + Math.sin(angle) * distance,
|
||||
offsetX + x,
|
||||
offsetY + y
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newParticles
|
||||
}
|
||||
|
||||
function easeOutQuart(t) {
|
||||
return 1 - Math.pow(1 - t, 4)
|
||||
}
|
||||
|
||||
function easeOutCubic(t) {
|
||||
return 1 - Math.pow(1 - t, 3)
|
||||
}
|
||||
|
||||
function animate(canvas, ctx) {
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
const width = canvas.width / dpr
|
||||
const height = canvas.height / dpr
|
||||
|
||||
const currentTime = performance.now()
|
||||
const duration = 2500
|
||||
const glowDuration = 600
|
||||
|
||||
const elapsed = currentTime - animationStartTime
|
||||
const progress = Math.min(elapsed / duration, 1)
|
||||
const easedProgress = easeOutQuart(progress)
|
||||
|
||||
// 透明背景
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
|
||||
// 计算发光进度
|
||||
let glowProgress = 0
|
||||
if (progress >= 1) {
|
||||
if (glowStartTime === null) {
|
||||
glowStartTime = currentTime
|
||||
}
|
||||
glowProgress = Math.min((currentTime - glowStartTime) / glowDuration, 1)
|
||||
glowProgress = easeOutCubic(glowProgress)
|
||||
}
|
||||
|
||||
const text = 'ESEngine'
|
||||
const fontSize = Math.min(width / 4, height / 3, 80)
|
||||
const textY = height / 2
|
||||
|
||||
for (const particle of particles) {
|
||||
const moveProgress = Math.min(easedProgress * 1.2, 1)
|
||||
const currentX = particle.x + (particle.targetX - particle.x) * moveProgress
|
||||
const currentY = particle.y + (particle.targetY - particle.y) * moveProgress
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.arc(currentX, currentY, particle.size, 0, Math.PI * 2)
|
||||
ctx.fillStyle = particle.color
|
||||
ctx.globalAlpha = particle.alpha * (1 - glowProgress * 0.3)
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
ctx.globalAlpha = 1
|
||||
|
||||
if (glowProgress > 0) {
|
||||
ctx.save()
|
||||
ctx.shadowColor = '#3b9eff'
|
||||
ctx.shadowBlur = 30 * glowProgress
|
||||
ctx.fillStyle = `rgba(255, 255, 255, ${glowProgress})`
|
||||
ctx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillText(text, width / 2, textY)
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
if (glowProgress >= 1) {
|
||||
const breathe = 0.8 + Math.sin(currentTime / 1000) * 0.2
|
||||
ctx.save()
|
||||
ctx.shadowColor = '#3b9eff'
|
||||
ctx.shadowBlur = 20 * breathe
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillText(text, width / 2, textY)
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
animationId = requestAnimationFrame(() => animate(canvas, ctx))
|
||||
}
|
||||
|
||||
function initCanvas() {
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas) return
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
const container = canvas.parentElement
|
||||
const width = container.offsetWidth
|
||||
const height = container.offsetHeight
|
||||
|
||||
canvas.width = width * dpr
|
||||
canvas.height = height * dpr
|
||||
canvas.style.width = `${width}px`
|
||||
canvas.style.height = `${height}px`
|
||||
ctx.scale(dpr, dpr)
|
||||
|
||||
const text = 'ESEngine'
|
||||
const fontSize = Math.min(width / 4, height / 3, 80)
|
||||
|
||||
particles = createParticles(canvas, text, fontSize)
|
||||
animationStartTime = performance.now()
|
||||
glowStartTime = null
|
||||
|
||||
if (animationId) {
|
||||
cancelAnimationFrame(animationId)
|
||||
}
|
||||
|
||||
animate(canvas, ctx)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initCanvas()
|
||||
window.addEventListener('resize', initCanvas)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (animationId) {
|
||||
cancelAnimationFrame(animationId)
|
||||
}
|
||||
window.removeEventListener('resize', initCanvas)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="hero-section">
|
||||
<div class="hero-container">
|
||||
<!-- 左侧文字区域 -->
|
||||
<div class="hero-text">
|
||||
<div class="hero-logo">
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="16" cy="16" r="14" stroke="#9147ff" stroke-width="2"/>
|
||||
<path d="M10 10h8v2h-6v3h5v2h-5v3h6v2h-8v-12z" fill="#9147ff"/>
|
||||
</svg>
|
||||
<span>ESENGINE</span>
|
||||
</div>
|
||||
<h1 class="hero-title">
|
||||
我们构建框架。<br/>
|
||||
而你将创造游戏。
|
||||
</h1>
|
||||
<p class="hero-description">
|
||||
ESEngine 是一个高性能的 TypeScript ECS 框架,为游戏开发者提供现代化的实体组件系统。
|
||||
无论是 2D 还是 3D 游戏,都能帮助你快速构建可扩展的游戏架构。
|
||||
</p>
|
||||
<div class="hero-actions">
|
||||
<a href="/guide/getting-started" class="btn-primary">开始使用</a>
|
||||
<a href="https://github.com/esengine/ecs-framework" class="btn-secondary" target="_blank">了解更多</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧粒子动画区域 -->
|
||||
<div class="hero-visual">
|
||||
<div class="visual-container">
|
||||
<canvas ref="canvasRef" class="particle-canvas"></canvas>
|
||||
<div class="visual-label">
|
||||
<span class="label-title">Entity Component System</span>
|
||||
<span class="label-subtitle">High Performance Framework</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.hero-section {
|
||||
background: #0d0d0d;
|
||||
padding: 80px 0;
|
||||
min-height: calc(100vh - 64px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.hero-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 48px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.2fr;
|
||||
gap: 64px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 左侧文字 */
|
||||
.hero-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.hero-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: #ffffff;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
line-height: 1.2;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.hero-description {
|
||||
font-size: 1.125rem;
|
||||
color: #707070;
|
||||
line-height: 1.7;
|
||||
margin: 0;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.btn-primary,
|
||||
.btn-secondary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 14px 28px;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
font-size: 0.9375rem;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #3b9eff;
|
||||
color: #ffffff;
|
||||
border: 1px solid #3b9eff;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #5aadff;
|
||||
border-color: #5aadff;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #1a1a1a;
|
||||
color: #a0a0a0;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #252525;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.hero-visual {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.visual-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
aspect-ratio: 4 / 3;
|
||||
background: linear-gradient(135deg, #1a2a3a 0%, #1a1a1a 50%, #0d0d0d 100%);
|
||||
border-radius: 12px;
|
||||
border: 1px solid #2a2a2a;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20px 60px rgba(59, 158, 255, 0.1);
|
||||
}
|
||||
|
||||
.particle-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.visual-label {
|
||||
position: absolute;
|
||||
bottom: 24px;
|
||||
left: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.label-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.label-subtitle {
|
||||
font-size: 0.875rem;
|
||||
color: #737373;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 1024px) {
|
||||
.hero-container {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 48px;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.hero-section {
|
||||
padding: 48px 0;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 2.25rem;
|
||||
}
|
||||
|
||||
.hero-description {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.visual-container {
|
||||
max-width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.hero-title {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn-primary,
|
||||
.btn-secondary {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
422
docs/.vitepress/theme/components/ParticleHeroEn.vue
Normal file
422
docs/.vitepress/theme/components/ParticleHeroEn.vue
Normal file
@@ -0,0 +1,422 @@
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
const canvasRef = ref(null)
|
||||
let animationId = null
|
||||
let particles = []
|
||||
let animationStartTime = null
|
||||
let glowStartTime = null
|
||||
|
||||
// ESEngine particle colors - VS Code style colors (unified with editor)
|
||||
const colors = ['#569CD6', '#4EC9B0', '#9CDCFE', '#C586C0', '#DCDCAA']
|
||||
|
||||
class Particle {
|
||||
constructor(x, y, targetX, targetY) {
|
||||
this.x = x
|
||||
this.y = y
|
||||
this.targetX = targetX
|
||||
this.targetY = targetY
|
||||
this.size = Math.random() * 2 + 1.5
|
||||
this.alpha = Math.random() * 0.5 + 0.5
|
||||
this.color = colors[Math.floor(Math.random() * colors.length)]
|
||||
}
|
||||
}
|
||||
|
||||
function createParticles(canvas, text, fontSize) {
|
||||
const tempCanvas = document.createElement('canvas')
|
||||
const tempCtx = tempCanvas.getContext('2d')
|
||||
if (!tempCtx) return []
|
||||
|
||||
tempCtx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||
const textMetrics = tempCtx.measureText(text)
|
||||
const textWidth = textMetrics.width
|
||||
const textHeight = fontSize
|
||||
|
||||
tempCanvas.width = textWidth + 40
|
||||
tempCanvas.height = textHeight + 40
|
||||
tempCtx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||
tempCtx.textAlign = 'center'
|
||||
tempCtx.textBaseline = 'middle'
|
||||
tempCtx.fillStyle = '#ffffff'
|
||||
tempCtx.fillText(text, tempCanvas.width / 2, tempCanvas.height / 2)
|
||||
|
||||
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height)
|
||||
const pixels = imageData.data
|
||||
const newParticles = []
|
||||
const gap = 3
|
||||
|
||||
const width = canvas.width / (window.devicePixelRatio || 1)
|
||||
const height = canvas.height / (window.devicePixelRatio || 1)
|
||||
const offsetX = (width - tempCanvas.width) / 2
|
||||
const offsetY = (height - tempCanvas.height) / 2
|
||||
|
||||
for (let y = 0; y < tempCanvas.height; y += gap) {
|
||||
for (let x = 0; x < tempCanvas.width; x += gap) {
|
||||
const index = (y * tempCanvas.width + x) * 4
|
||||
const alpha = pixels[index + 3] || 0
|
||||
|
||||
if (alpha > 128) {
|
||||
const angle = Math.random() * Math.PI * 2
|
||||
const distance = Math.random() * Math.max(width, height)
|
||||
|
||||
newParticles.push(new Particle(
|
||||
width / 2 + Math.cos(angle) * distance,
|
||||
height / 2 + Math.sin(angle) * distance,
|
||||
offsetX + x,
|
||||
offsetY + y
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newParticles
|
||||
}
|
||||
|
||||
function easeOutQuart(t) {
|
||||
return 1 - Math.pow(1 - t, 4)
|
||||
}
|
||||
|
||||
function easeOutCubic(t) {
|
||||
return 1 - Math.pow(1 - t, 3)
|
||||
}
|
||||
|
||||
function animate(canvas, ctx) {
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
const width = canvas.width / dpr
|
||||
const height = canvas.height / dpr
|
||||
|
||||
const currentTime = performance.now()
|
||||
const duration = 2500
|
||||
const glowDuration = 600
|
||||
|
||||
const elapsed = currentTime - animationStartTime
|
||||
const progress = Math.min(elapsed / duration, 1)
|
||||
const easedProgress = easeOutQuart(progress)
|
||||
|
||||
// Transparent background
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
|
||||
// Calculate glow progress
|
||||
let glowProgress = 0
|
||||
if (progress >= 1) {
|
||||
if (glowStartTime === null) {
|
||||
glowStartTime = currentTime
|
||||
}
|
||||
glowProgress = Math.min((currentTime - glowStartTime) / glowDuration, 1)
|
||||
glowProgress = easeOutCubic(glowProgress)
|
||||
}
|
||||
|
||||
const text = 'ESEngine'
|
||||
const fontSize = Math.min(width / 4, height / 3, 80)
|
||||
const textY = height / 2
|
||||
|
||||
for (const particle of particles) {
|
||||
const moveProgress = Math.min(easedProgress * 1.2, 1)
|
||||
const currentX = particle.x + (particle.targetX - particle.x) * moveProgress
|
||||
const currentY = particle.y + (particle.targetY - particle.y) * moveProgress
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.arc(currentX, currentY, particle.size, 0, Math.PI * 2)
|
||||
ctx.fillStyle = particle.color
|
||||
ctx.globalAlpha = particle.alpha * (1 - glowProgress * 0.3)
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
ctx.globalAlpha = 1
|
||||
|
||||
if (glowProgress > 0) {
|
||||
ctx.save()
|
||||
ctx.shadowColor = '#3b9eff'
|
||||
ctx.shadowBlur = 30 * glowProgress
|
||||
ctx.fillStyle = `rgba(255, 255, 255, ${glowProgress})`
|
||||
ctx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillText(text, width / 2, textY)
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
if (glowProgress >= 1) {
|
||||
const breathe = 0.8 + Math.sin(currentTime / 1000) * 0.2
|
||||
ctx.save()
|
||||
ctx.shadowColor = '#3b9eff'
|
||||
ctx.shadowBlur = 20 * breathe
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillText(text, width / 2, textY)
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
animationId = requestAnimationFrame(() => animate(canvas, ctx))
|
||||
}
|
||||
|
||||
function initCanvas() {
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas) return
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
const container = canvas.parentElement
|
||||
const width = container.offsetWidth
|
||||
const height = container.offsetHeight
|
||||
|
||||
canvas.width = width * dpr
|
||||
canvas.height = height * dpr
|
||||
canvas.style.width = `${width}px`
|
||||
canvas.style.height = `${height}px`
|
||||
ctx.scale(dpr, dpr)
|
||||
|
||||
const text = 'ESEngine'
|
||||
const fontSize = Math.min(width / 4, height / 3, 80)
|
||||
|
||||
particles = createParticles(canvas, text, fontSize)
|
||||
animationStartTime = performance.now()
|
||||
glowStartTime = null
|
||||
|
||||
if (animationId) {
|
||||
cancelAnimationFrame(animationId)
|
||||
}
|
||||
|
||||
animate(canvas, ctx)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initCanvas()
|
||||
window.addEventListener('resize', initCanvas)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (animationId) {
|
||||
cancelAnimationFrame(animationId)
|
||||
}
|
||||
window.removeEventListener('resize', initCanvas)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="hero-section">
|
||||
<div class="hero-container">
|
||||
<!-- Left text area -->
|
||||
<div class="hero-text">
|
||||
<div class="hero-logo">
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="16" cy="16" r="14" stroke="#9147ff" stroke-width="2"/>
|
||||
<path d="M10 10h8v2h-6v3h5v2h-5v3h6v2h-8v-12z" fill="#9147ff"/>
|
||||
</svg>
|
||||
<span>ESENGINE</span>
|
||||
</div>
|
||||
<h1 class="hero-title">
|
||||
We build the framework.<br/>
|
||||
You create the game.
|
||||
</h1>
|
||||
<p class="hero-description">
|
||||
ESEngine is a high-performance TypeScript ECS framework for game developers.
|
||||
Whether 2D or 3D games, it helps you build scalable game architecture quickly.
|
||||
</p>
|
||||
<div class="hero-actions">
|
||||
<a href="/en/guide/getting-started" class="btn-primary">Get Started</a>
|
||||
<a href="https://github.com/esengine/ecs-framework" class="btn-secondary" target="_blank">Learn More</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right particle animation area -->
|
||||
<div class="hero-visual">
|
||||
<div class="visual-container">
|
||||
<canvas ref="canvasRef" class="particle-canvas"></canvas>
|
||||
<div class="visual-label">
|
||||
<span class="label-title">Entity Component System</span>
|
||||
<span class="label-subtitle">High Performance Framework</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.hero-section {
|
||||
background: #0d0d0d;
|
||||
padding: 80px 0;
|
||||
min-height: calc(100vh - 64px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.hero-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 48px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.2fr;
|
||||
gap: 64px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Left text */
|
||||
.hero-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.hero-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: #ffffff;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
line-height: 1.2;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.hero-description {
|
||||
font-size: 1.125rem;
|
||||
color: #707070;
|
||||
line-height: 1.7;
|
||||
margin: 0;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.btn-primary,
|
||||
.btn-secondary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 14px 28px;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
font-size: 0.9375rem;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #3b9eff;
|
||||
color: #ffffff;
|
||||
border: 1px solid #3b9eff;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #5aadff;
|
||||
border-color: #5aadff;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #1a1a1a;
|
||||
color: #a0a0a0;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #252525;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.hero-visual {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.visual-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
aspect-ratio: 4 / 3;
|
||||
background: linear-gradient(135deg, #1a2a3a 0%, #1a1a1a 50%, #0d0d0d 100%);
|
||||
border-radius: 12px;
|
||||
border: 1px solid #2a2a2a;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20px 60px rgba(59, 158, 255, 0.1);
|
||||
}
|
||||
|
||||
.particle-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.visual-label {
|
||||
position: absolute;
|
||||
bottom: 24px;
|
||||
left: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.label-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.label-subtitle {
|
||||
font-size: 0.875rem;
|
||||
color: #737373;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 1024px) {
|
||||
.hero-container {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 48px;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.hero-section {
|
||||
padding: 48px 0;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 2.25rem;
|
||||
}
|
||||
|
||||
.hero-description {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.visual-container {
|
||||
max-width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.hero-title {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn-primary,
|
||||
.btn-secondary {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
594
docs/.vitepress/theme/custom.css
Normal file
594
docs/.vitepress/theme/custom.css
Normal file
@@ -0,0 +1,594 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--vp-nav-height: 64px;
|
||||
|
||||
--es-bg-base: #1e1e1e;
|
||||
--es-bg-elevated: #252526;
|
||||
--es-bg-overlay: #2d2d2d;
|
||||
--es-bg-input: #3c3c3c;
|
||||
--es-bg-inset: #181818;
|
||||
--es-bg-hover: #2a2d2e;
|
||||
--es-bg-active: #37373d;
|
||||
--es-bg-sidebar: #262626;
|
||||
--es-bg-card: #2a2a2a;
|
||||
--es-bg-header: #2d2d2d;
|
||||
|
||||
--es-text-primary: #cccccc;
|
||||
--es-text-secondary: #9d9d9d;
|
||||
--es-text-tertiary: #6a6a6a;
|
||||
--es-text-inverse: #ffffff;
|
||||
--es-text-muted: #aaaaaa;
|
||||
--es-text-dim: #6a6a6a;
|
||||
|
||||
--es-font-xs: 11px;
|
||||
--es-font-sm: 12px;
|
||||
--es-font-base: 13px;
|
||||
--es-font-md: 14px;
|
||||
--es-font-lg: 16px;
|
||||
|
||||
--es-border-default: #3a3a3a;
|
||||
--es-border-subtle: #1a1a1a;
|
||||
--es-border-strong: #4a4a4a;
|
||||
|
||||
--es-primary: #3b82f6;
|
||||
--es-primary-hover: #2563eb;
|
||||
--es-success: #4ade80;
|
||||
--es-warning: #f59e0b;
|
||||
--es-error: #ef4444;
|
||||
--es-info: #3b82f6;
|
||||
|
||||
--es-selected: #3d5a80;
|
||||
--es-selected-hover: #4a6a90;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--es-bg-base) !important;
|
||||
}
|
||||
|
||||
html,
|
||||
html.dark {
|
||||
--vp-c-bg: var(--es-bg-base);
|
||||
--vp-c-bg-soft: var(--es-bg-elevated);
|
||||
--vp-c-bg-mute: var(--es-bg-overlay);
|
||||
--vp-c-bg-alt: var(--es-bg-sidebar);
|
||||
--vp-c-text-1: var(--es-text-primary);
|
||||
--vp-c-text-2: var(--es-text-tertiary);
|
||||
--vp-c-text-3: var(--es-text-muted);
|
||||
--vp-c-divider: var(--es-border-default);
|
||||
--vp-c-divider-light: var(--es-border-subtle);
|
||||
}
|
||||
|
||||
html:not(.dark) {
|
||||
--vp-c-bg: var(--es-bg-base) !important;
|
||||
--vp-c-bg-soft: var(--es-bg-elevated) !important;
|
||||
--vp-c-bg-mute: var(--es-bg-overlay) !important;
|
||||
--vp-c-bg-alt: var(--es-bg-sidebar) !important;
|
||||
--vp-c-text-1: var(--es-text-primary) !important;
|
||||
--vp-c-text-2: var(--es-text-tertiary) !important;
|
||||
--vp-c-text-3: var(--es-text-muted) !important;
|
||||
}
|
||||
|
||||
.VPNav {
|
||||
background: var(--es-bg-header) !important;
|
||||
border-bottom: 1px solid var(--es-border-subtle) !important;
|
||||
}
|
||||
|
||||
.VPNav .VPNavBar {
|
||||
background: var(--es-bg-header) !important;
|
||||
}
|
||||
|
||||
.VPNav .VPNavBar .wrapper {
|
||||
background: var(--es-bg-header) !important;
|
||||
}
|
||||
|
||||
.VPNav .VPNavBar::before,
|
||||
.VPNav .VPNavBar::after {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.VPNavBar {
|
||||
background: var(--es-bg-header) !important;
|
||||
}
|
||||
|
||||
.VPNavBar::before {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.VPNavBarTitle .title {
|
||||
color: var(--es-text-primary);
|
||||
font-weight: 500;
|
||||
font-size: var(--es-font-base);
|
||||
}
|
||||
|
||||
.VPNavBarMenuLink {
|
||||
color: var(--es-text-secondary) !important;
|
||||
font-size: var(--es-font-sm) !important;
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
|
||||
.VPNavBarMenuLink:hover {
|
||||
color: var(--es-text-primary) !important;
|
||||
}
|
||||
|
||||
.VPNavBarMenuLink.active {
|
||||
color: var(--es-text-primary) !important;
|
||||
}
|
||||
|
||||
.VPNavBarSearch .DocSearch-Button {
|
||||
background: var(--es-bg-input) !important;
|
||||
border: 1px solid var(--es-border-default) !important;
|
||||
border-radius: 2px;
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
.VPSidebar {
|
||||
background: var(--es-bg-sidebar) !important;
|
||||
border-right: 1px solid var(--es-border-subtle) !important;
|
||||
}
|
||||
|
||||
.VPSidebarItem.level-0 > .item {
|
||||
padding: 8px 0 4px 0;
|
||||
}
|
||||
|
||||
.VPSidebarItem.level-0 > .item > .text {
|
||||
font-weight: 600;
|
||||
font-size: var(--es-font-xs);
|
||||
color: var(--es-text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.VPSidebarItem .link {
|
||||
padding: 4px 8px;
|
||||
margin: 1px 0;
|
||||
border-radius: 2px;
|
||||
color: var(--es-text-primary);
|
||||
font-size: var(--es-font-sm);
|
||||
transition: all 0.1s ease;
|
||||
border-left: 2px solid transparent;
|
||||
}
|
||||
|
||||
.VPSidebarItem .link:hover {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
color: var(--es-text-inverse);
|
||||
}
|
||||
|
||||
.VPSidebarItem.is-active > .item > .link {
|
||||
background: var(--es-selected);
|
||||
color: var(--es-text-inverse);
|
||||
border-left: 2px solid var(--es-primary);
|
||||
}
|
||||
|
||||
.VPSidebarItem.is-active > .item > .link:hover {
|
||||
background: var(--es-selected-hover);
|
||||
}
|
||||
|
||||
.VPSidebarItem.level-1 .link {
|
||||
padding-left: 20px;
|
||||
font-size: var(--es-font-sm);
|
||||
}
|
||||
|
||||
.VPSidebarItem.level-2 .link {
|
||||
padding-left: 32px;
|
||||
font-size: var(--es-font-sm);
|
||||
}
|
||||
|
||||
.VPSidebarItem .caret {
|
||||
color: var(--es-text-secondary);
|
||||
}
|
||||
|
||||
.VPSidebarItem .caret:hover {
|
||||
color: var(--es-text-primary);
|
||||
}
|
||||
|
||||
.VPContent {
|
||||
background: var(--es-bg-card) !important;
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
.VPContent.has-sidebar {
|
||||
background: var(--es-bg-card) !important;
|
||||
}
|
||||
|
||||
/* 首页布局修复 | Home page layout fix */
|
||||
.VPPage {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
.Layout > .VPContent {
|
||||
padding-top: var(--vp-nav-height) !important;
|
||||
}
|
||||
|
||||
.VPDoc {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.VPNavBar .content {
|
||||
background: var(--es-bg-header) !important;
|
||||
}
|
||||
|
||||
.VPNavBar .content-body {
|
||||
background: var(--es-bg-header) !important;
|
||||
}
|
||||
|
||||
.VPNavBar .divider {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.VPLocalNav {
|
||||
background: var(--es-bg-header) !important;
|
||||
border-bottom: 1px solid var(--es-border-subtle) !important;
|
||||
}
|
||||
|
||||
.VPNavScreenMenu {
|
||||
background: var(--es-bg-base) !important;
|
||||
}
|
||||
|
||||
.VPNavScreen {
|
||||
background: var(--es-bg-base) !important;
|
||||
}
|
||||
|
||||
.curtain {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.VPNav .curtain,
|
||||
.VPNavBar .curtain {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
[class*="curtain"] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.VPNav > div::before,
|
||||
.VPNav > div::after {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.vp-doc {
|
||||
color: var(--es-text-primary);
|
||||
}
|
||||
|
||||
.vp-doc h1 {
|
||||
font-size: var(--es-font-lg);
|
||||
font-weight: 600;
|
||||
color: var(--es-text-inverse);
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
margin-bottom: 16px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.vp-doc h2 {
|
||||
font-size: var(--es-font-md);
|
||||
font-weight: 600;
|
||||
color: var(--es-text-inverse);
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
margin-top: 32px;
|
||||
margin-bottom: 12px;
|
||||
padding: 6px 12px;
|
||||
background: var(--es-bg-header);
|
||||
border-left: 3px solid var(--es-primary);
|
||||
}
|
||||
|
||||
.vp-doc h3 {
|
||||
font-size: var(--es-font-base);
|
||||
font-weight: 600;
|
||||
color: var(--es-text-primary);
|
||||
margin-top: 20px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.vp-doc p {
|
||||
color: var(--es-text-primary);
|
||||
line-height: 1.7;
|
||||
font-size: var(--es-font-base);
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.vp-doc ul,
|
||||
.vp-doc ol {
|
||||
padding-left: 20px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.vp-doc li {
|
||||
line-height: 1.7;
|
||||
margin: 4px 0;
|
||||
color: var(--es-text-primary);
|
||||
font-size: var(--es-font-base);
|
||||
}
|
||||
|
||||
.vp-doc li::marker {
|
||||
color: var(--es-text-secondary);
|
||||
}
|
||||
|
||||
.vp-doc strong {
|
||||
color: var(--es-text-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.vp-doc a {
|
||||
color: var(--es-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.vp-doc a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.VPDocAside {
|
||||
padding-left: 16px;
|
||||
border-left: 1px solid var(--es-border-subtle);
|
||||
}
|
||||
|
||||
.VPDocAsideOutline {
|
||||
padding: 0;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.VPDocAsideOutline .content {
|
||||
border: none !important;
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
.VPDocAsideOutline .outline-title {
|
||||
font-size: var(--es-font-xs);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--es-text-secondary);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.VPDocAsideOutline .outline-link {
|
||||
color: var(--es-text-secondary);
|
||||
font-size: var(--es-font-xs);
|
||||
padding: 4px 0;
|
||||
line-height: 1.4;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.VPDocAsideOutline .outline-link:hover {
|
||||
color: var(--es-text-primary);
|
||||
}
|
||||
|
||||
.VPDocAsideOutline .outline-link.active {
|
||||
color: var(--es-primary);
|
||||
}
|
||||
|
||||
.VPDocAsideOutline .outline-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div[class*='language-'] {
|
||||
background: var(--es-bg-inset) !important;
|
||||
border: 1px solid var(--es-border-default);
|
||||
border-radius: 2px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.vp-code-group .tabs {
|
||||
background: var(--es-bg-header);
|
||||
border-bottom: 1px solid var(--es-border-subtle);
|
||||
}
|
||||
|
||||
.vp-doc :not(pre) > code {
|
||||
background: var(--es-bg-input);
|
||||
color: var(--es-primary);
|
||||
padding: 2px 6px;
|
||||
border-radius: 2px;
|
||||
font-size: var(--es-font-xs);
|
||||
}
|
||||
|
||||
.vp-doc table {
|
||||
display: table;
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-collapse: collapse;
|
||||
margin: 16px 0;
|
||||
font-size: var(--es-font-sm);
|
||||
}
|
||||
|
||||
.vp-doc tr {
|
||||
border-bottom: 1px solid var(--es-border-subtle);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.vp-doc tr:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.vp-doc tr:hover {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
.vp-doc th {
|
||||
background: var(--es-bg-header);
|
||||
font-weight: 600;
|
||||
font-size: var(--es-font-xs);
|
||||
color: var(--es-text-secondary);
|
||||
text-align: left;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--es-border-subtle);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.vp-doc td {
|
||||
font-size: var(--es-font-sm);
|
||||
color: var(--es-text-primary);
|
||||
padding: 8px 12px;
|
||||
vertical-align: top;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.vp-doc td:first-child {
|
||||
font-weight: 500;
|
||||
color: var(--es-text-primary);
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.vp-doc .warning,
|
||||
.vp-doc .custom-block.warning {
|
||||
background: rgba(245, 158, 11, 0.08);
|
||||
border: none;
|
||||
border-left: 3px solid var(--es-warning);
|
||||
border-radius: 0 2px 2px 0;
|
||||
padding: 10px 12px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.vp-doc .warning .custom-block-title,
|
||||
.vp-doc .custom-block.warning .custom-block-title {
|
||||
color: var(--es-warning);
|
||||
font-weight: 600;
|
||||
font-size: var(--es-font-xs);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.vp-doc .warning p {
|
||||
color: var(--es-text-primary);
|
||||
margin: 0;
|
||||
font-size: var(--es-font-xs);
|
||||
}
|
||||
|
||||
.vp-doc .tip,
|
||||
.vp-doc .custom-block.tip {
|
||||
background: rgba(59, 130, 246, 0.08);
|
||||
border: none;
|
||||
border-left: 3px solid var(--es-primary);
|
||||
border-radius: 0 2px 2px 0;
|
||||
padding: 10px 12px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.vp-doc .tip .custom-block-title,
|
||||
.vp-doc .custom-block.tip .custom-block-title {
|
||||
color: var(--es-primary);
|
||||
font-weight: 600;
|
||||
font-size: var(--es-font-xs);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.vp-doc .tip p {
|
||||
color: var(--es-text-primary);
|
||||
margin: 0;
|
||||
font-size: var(--es-font-xs);
|
||||
}
|
||||
|
||||
.vp-doc .info,
|
||||
.vp-doc .custom-block.info {
|
||||
background: rgba(74, 222, 128, 0.08);
|
||||
border: none;
|
||||
border-left: 3px solid var(--es-success);
|
||||
border-radius: 0 2px 2px 0;
|
||||
padding: 10px 12px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.vp-doc .info .custom-block-title,
|
||||
.vp-doc .custom-block.info .custom-block-title {
|
||||
color: var(--es-success);
|
||||
font-weight: 600;
|
||||
font-size: var(--es-font-xs);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.vp-doc .danger,
|
||||
.vp-doc .custom-block.danger {
|
||||
background: rgba(239, 68, 68, 0.08);
|
||||
border: none;
|
||||
border-left: 3px solid var(--es-error);
|
||||
border-radius: 0 2px 2px 0;
|
||||
padding: 10px 12px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.vp-doc .danger .custom-block-title,
|
||||
.vp-doc .custom-block.danger .custom-block-title {
|
||||
color: var(--es-error);
|
||||
font-weight: 600;
|
||||
font-size: var(--es-font-xs);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.vp-doc .card {
|
||||
background: var(--es-bg-sidebar);
|
||||
border: 1px solid var(--es-border-subtle);
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.vp-doc .card-title {
|
||||
font-size: var(--es-font-sm);
|
||||
font-weight: 600;
|
||||
color: var(--es-text-primary);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.vp-doc .card-description {
|
||||
font-size: var(--es-font-xs);
|
||||
color: var(--es-text-muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.vp-doc .tag {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
background: transparent;
|
||||
border: 1px solid var(--es-border-default);
|
||||
border-radius: 2px;
|
||||
color: var(--es-text-secondary);
|
||||
font-size: var(--es-font-xs);
|
||||
margin-right: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.VPFooter {
|
||||
background: var(--es-bg-sidebar) !important;
|
||||
border-top: 1px solid var(--es-border-subtle) !important;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--es-bg-card);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--es-border-strong);
|
||||
border-radius: 4px;
|
||||
border: 2px solid var(--es-bg-card);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #5a5a5a;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.home-container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.home-section {
|
||||
padding: 32px 0;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.VPDoc .content {
|
||||
padding: 16px !important;
|
||||
}
|
||||
}
|
||||
14
docs/.vitepress/theme/index.js
Normal file
14
docs/.vitepress/theme/index.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
import ParticleHero from './components/ParticleHero.vue'
|
||||
import ParticleHeroEn from './components/ParticleHeroEn.vue'
|
||||
import FeatureCard from './components/FeatureCard.vue'
|
||||
import './custom.css'
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
enhanceApp({ app }) {
|
||||
app.component('ParticleHero', ParticleHero)
|
||||
app.component('ParticleHeroEn', ParticleHeroEn)
|
||||
app.component('FeatureCard', FeatureCard)
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
# Starlight Starter Kit: Basics
|
||||
|
||||
[](https://starlight.astro.build)
|
||||
|
||||
```
|
||||
npm create astro@latest -- --template starlight
|
||||
```
|
||||
|
||||
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
||||
|
||||
## 🚀 Project Structure
|
||||
|
||||
Inside of your Astro + Starlight project, you'll see the following folders and files:
|
||||
|
||||
```
|
||||
.
|
||||
├── public/
|
||||
├── src/
|
||||
│ ├── assets/
|
||||
│ ├── content/
|
||||
│ │ └── docs/
|
||||
│ └── content.config.ts
|
||||
├── astro.config.mjs
|
||||
├── package.json
|
||||
└── tsconfig.json
|
||||
```
|
||||
|
||||
Starlight looks for `.md` or `.mdx` files in the `src/content/docs/` directory. Each file is exposed as a route based on its file name.
|
||||
|
||||
Images can be added to `src/assets/` and embedded in Markdown with a relative link.
|
||||
|
||||
Static assets, like favicons, can be placed in the `public/` directory.
|
||||
|
||||
## 🧞 Commands
|
||||
|
||||
All commands are run from the root of the project, from a terminal:
|
||||
|
||||
| Command | Action |
|
||||
| :------------------------ | :----------------------------------------------- |
|
||||
| `npm install` | Installs dependencies |
|
||||
| `npm run dev` | Starts local dev server at `localhost:4321` |
|
||||
| `npm run build` | Build your production site to `./dist/` |
|
||||
| `npm run preview` | Preview your build locally, before deploying |
|
||||
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
|
||||
| `npm run astro -- --help` | Get help using the Astro CLI |
|
||||
|
||||
## 👀 Want to learn more?
|
||||
|
||||
Check out [Starlight’s docs](https://starlight.astro.build/), read [the Astro documentation](https://docs.astro.build), or jump into the [Astro Discord server](https://astro.build/chat).
|
||||
@@ -1,365 +0,0 @@
|
||||
// @ts-check
|
||||
import { defineConfig } from 'astro/config';
|
||||
import starlight from '@astrojs/starlight';
|
||||
import vue from '@astrojs/vue';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
|
||||
export default defineConfig({
|
||||
integrations: [
|
||||
starlight({
|
||||
title: 'ESEngine',
|
||||
logo: {
|
||||
src: './src/assets/logo.svg',
|
||||
replacesTitle: false,
|
||||
},
|
||||
social: [
|
||||
{ icon: 'github', label: 'GitHub', href: 'https://github.com/esengine/esengine' }
|
||||
],
|
||||
defaultLocale: 'root',
|
||||
locales: {
|
||||
root: {
|
||||
label: '简体中文',
|
||||
lang: 'zh-CN',
|
||||
},
|
||||
en: {
|
||||
label: 'English',
|
||||
lang: 'en',
|
||||
},
|
||||
},
|
||||
sidebar: [
|
||||
{
|
||||
label: '快速开始',
|
||||
translations: { en: 'Getting Started' },
|
||||
items: [
|
||||
{ label: '快速入门', slug: 'guide/getting-started', translations: { en: 'Quick Start' } },
|
||||
{ label: '指南概览', slug: 'guide', translations: { en: 'Guide Overview' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '核心概念',
|
||||
translations: { en: 'Core Concepts' },
|
||||
items: [
|
||||
{
|
||||
label: '实体',
|
||||
translations: { en: 'Entity' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'guide/entity', translations: { en: 'Overview' } },
|
||||
{ label: '组件操作', slug: 'guide/entity/component-operations', translations: { en: 'Component Operations' } },
|
||||
{ label: '实体句柄', slug: 'guide/entity/entity-handle', translations: { en: 'Entity Handle' } },
|
||||
{ label: '生命周期', slug: 'guide/entity/lifecycle', translations: { en: 'Lifecycle' } },
|
||||
],
|
||||
},
|
||||
{ label: '层级结构', slug: 'guide/hierarchy', translations: { en: 'Hierarchy' } },
|
||||
{
|
||||
label: '组件',
|
||||
translations: { en: 'Component' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'guide/component', translations: { en: 'Overview' } },
|
||||
{ label: '生命周期', slug: 'guide/component/lifecycle', translations: { en: 'Lifecycle' } },
|
||||
{ label: 'EntityRef 装饰器', slug: 'guide/component/entity-ref', translations: { en: 'EntityRef' } },
|
||||
{ label: '最佳实践', slug: 'guide/component/best-practices', translations: { en: 'Best Practices' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '实体查询',
|
||||
translations: { en: 'Entity Query' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'guide/entity-query', translations: { en: 'Overview' } },
|
||||
{ label: 'Matcher API', slug: 'guide/entity-query/matcher-api', translations: { en: 'Matcher API' } },
|
||||
{ label: '编译查询', slug: 'guide/entity-query/compiled-query', translations: { en: 'Compiled Query' } },
|
||||
{ label: '最佳实践', slug: 'guide/entity-query/best-practices', translations: { en: 'Best Practices' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '系统',
|
||||
translations: { en: 'System' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'guide/system', translations: { en: 'Overview' } },
|
||||
{ label: '系统类型', slug: 'guide/system/types', translations: { en: 'System Types' } },
|
||||
{ label: '生命周期', slug: 'guide/system/lifecycle', translations: { en: 'Lifecycle' } },
|
||||
{ label: '命令缓冲区', slug: 'guide/system/command-buffer', translations: { en: 'Command Buffer' } },
|
||||
{ label: '系统调度', slug: 'guide/system/scheduling', translations: { en: 'Scheduling' } },
|
||||
{ label: '变更检测', slug: 'guide/system/change-detection', translations: { en: 'Change Detection' } },
|
||||
{ label: '最佳实践', slug: 'guide/system/best-practices', translations: { en: 'Best Practices' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '场景',
|
||||
translations: { en: 'Scene' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'guide/scene', translations: { en: 'Overview' } },
|
||||
{ label: '生命周期', slug: 'guide/scene/lifecycle', translations: { en: 'Lifecycle' } },
|
||||
{ label: '实体管理', slug: 'guide/scene/entity-management', translations: { en: 'Entity Management' } },
|
||||
{ label: '系统管理', slug: 'guide/scene/system-management', translations: { en: 'System Management' } },
|
||||
{ label: '事件系统', slug: 'guide/scene/events', translations: { en: 'Events' } },
|
||||
{ label: '调试与监控', slug: 'guide/scene/debugging', translations: { en: 'Debugging' } },
|
||||
{ label: '最佳实践', slug: 'guide/scene/best-practices', translations: { en: 'Best Practices' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '序列化',
|
||||
translations: { en: 'Serialization' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'guide/serialization', translations: { en: 'Overview' } },
|
||||
{ label: '装饰器与继承', slug: 'guide/serialization/decorators', translations: { en: 'Decorators & Inheritance' } },
|
||||
{ label: '增量序列化', slug: 'guide/serialization/incremental', translations: { en: 'Incremental' } },
|
||||
{ label: '版本迁移', slug: 'guide/serialization/migration', translations: { en: 'Migration' } },
|
||||
{ label: '使用场景', slug: 'guide/serialization/use-cases', translations: { en: 'Use Cases' } },
|
||||
],
|
||||
},
|
||||
{ label: '事件系统', slug: 'guide/event-system', translations: { en: 'Event System' } },
|
||||
{ label: '时间与定时器', slug: 'guide/time-and-timers', translations: { en: 'Time & Timers' } },
|
||||
{ label: '日志系统', slug: 'guide/logging', translations: { en: 'Logging' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '高级功能',
|
||||
translations: { en: 'Advanced Features' },
|
||||
items: [
|
||||
{
|
||||
label: '服务容器',
|
||||
translations: { en: 'Service Container' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'guide/service-container', translations: { en: 'Overview' } },
|
||||
{ label: '内置服务', slug: 'guide/service-container/built-in-services', translations: { en: 'Built-in Services' } },
|
||||
{ label: '依赖注入', slug: 'guide/service-container/dependency-injection', translations: { en: 'Dependency Injection' } },
|
||||
{ label: 'PluginServiceRegistry', slug: 'guide/service-container/plugin-service-registry', translations: { en: 'PluginServiceRegistry' } },
|
||||
{ label: '高级用法', slug: 'guide/service-container/advanced', translations: { en: 'Advanced' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '插件系统',
|
||||
translations: { en: 'Plugin System' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'guide/plugin-system', translations: { en: 'Overview' } },
|
||||
{ label: '插件开发', slug: 'guide/plugin-system/development', translations: { en: 'Development' } },
|
||||
{ label: '服务与系统', slug: 'guide/plugin-system/services-systems', translations: { en: 'Services & Systems' } },
|
||||
{ label: '依赖管理', slug: 'guide/plugin-system/dependencies', translations: { en: 'Dependencies' } },
|
||||
{ label: '插件管理', slug: 'guide/plugin-system/management', translations: { en: 'Management' } },
|
||||
{ label: '示例插件', slug: 'guide/plugin-system/examples', translations: { en: 'Examples' } },
|
||||
{ label: '最佳实践', slug: 'guide/plugin-system/best-practices', translations: { en: 'Best Practices' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Worker 系统',
|
||||
translations: { en: 'Worker System' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'guide/worker-system', translations: { en: 'Overview' } },
|
||||
{ label: '配置选项', slug: 'guide/worker-system/configuration', translations: { en: 'Configuration' } },
|
||||
{ label: '完整示例', slug: 'guide/worker-system/examples', translations: { en: 'Examples' } },
|
||||
{ label: '微信小游戏', slug: 'guide/worker-system/wechat', translations: { en: 'WeChat' } },
|
||||
{ label: '最佳实践', slug: 'guide/worker-system/best-practices', translations: { en: 'Best Practices' } },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '平台适配器',
|
||||
translations: { en: 'Platform Adapters' },
|
||||
items: [
|
||||
{ label: '概览', slug: 'guide/platform-adapter', translations: { en: 'Overview' } },
|
||||
{ label: '浏览器', slug: 'guide/platform-adapter/browser', translations: { en: 'Browser' } },
|
||||
{ label: '微信小游戏', slug: 'guide/platform-adapter/wechat-minigame', translations: { en: 'WeChat Mini Game' } },
|
||||
{ label: 'Node.js', slug: 'guide/platform-adapter/nodejs', translations: { en: 'Node.js' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '模块',
|
||||
translations: { en: 'Modules' },
|
||||
items: [
|
||||
{ label: '模块总览', slug: 'modules', translations: { en: 'Modules Overview' } },
|
||||
{
|
||||
label: '行为树',
|
||||
translations: { en: 'Behavior Tree' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'modules/behavior-tree', translations: { en: 'Overview' } },
|
||||
{ label: '快速开始', slug: 'modules/behavior-tree/getting-started', translations: { en: 'Getting Started' } },
|
||||
{ label: '核心概念', slug: 'modules/behavior-tree/core-concepts', translations: { en: 'Core Concepts' } },
|
||||
{ label: '编辑器指南', slug: 'modules/behavior-tree/editor-guide', translations: { en: 'Editor Guide' } },
|
||||
{ label: '编辑器工作流', slug: 'modules/behavior-tree/editor-workflow', translations: { en: 'Editor Workflow' } },
|
||||
{ label: '资产管理', slug: 'modules/behavior-tree/asset-management', translations: { en: 'Asset Management' } },
|
||||
{ label: '自定义节点', slug: 'modules/behavior-tree/custom-actions', translations: { en: 'Custom Actions' } },
|
||||
{ label: '高级用法', slug: 'modules/behavior-tree/advanced-usage', translations: { en: 'Advanced Usage' } },
|
||||
{ label: '最佳实践', slug: 'modules/behavior-tree/best-practices', translations: { en: 'Best Practices' } },
|
||||
{ label: 'Cocos 集成', slug: 'modules/behavior-tree/cocos-integration', translations: { en: 'Cocos Integration' } },
|
||||
{ label: 'Laya 集成', slug: 'modules/behavior-tree/laya-integration', translations: { en: 'Laya Integration' } },
|
||||
{ label: 'Node.js 使用', slug: 'modules/behavior-tree/nodejs-usage', translations: { en: 'Node.js Usage' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '状态机',
|
||||
translations: { en: 'FSM' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'modules/fsm', translations: { en: 'Overview' } },
|
||||
{ label: 'API 参考', slug: 'modules/fsm/api', translations: { en: 'API Reference' } },
|
||||
{ label: '实际示例', slug: 'modules/fsm/examples', translations: { en: 'Examples' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '定时器',
|
||||
translations: { en: 'Timer' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'modules/timer', translations: { en: 'Overview' } },
|
||||
{ label: 'API 参考', slug: 'modules/timer/api', translations: { en: 'API Reference' } },
|
||||
{ label: '实际示例', slug: 'modules/timer/examples', translations: { en: 'Examples' } },
|
||||
{ label: '最佳实践', slug: 'modules/timer/best-practices', translations: { en: 'Best Practices' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '空间索引',
|
||||
translations: { en: 'Spatial' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'modules/spatial', translations: { en: 'Overview' } },
|
||||
{ label: '空间索引 API', slug: 'modules/spatial/spatial-index', translations: { en: 'Spatial Index API' } },
|
||||
{ label: 'AOI 兴趣区域', slug: 'modules/spatial/aoi', translations: { en: 'AOI' } },
|
||||
{ label: '实际示例', slug: 'modules/spatial/examples', translations: { en: 'Examples' } },
|
||||
{ label: '工具与优化', slug: 'modules/spatial/utilities', translations: { en: 'Utilities' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '寻路',
|
||||
translations: { en: 'Pathfinding' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'modules/pathfinding', translations: { en: 'Overview' } },
|
||||
{ label: '网格地图 API', slug: 'modules/pathfinding/grid-map', translations: { en: 'Grid Map API' } },
|
||||
{ label: '导航网格 API', slug: 'modules/pathfinding/navmesh', translations: { en: 'NavMesh API' } },
|
||||
{ label: '路径平滑', slug: 'modules/pathfinding/smoothing', translations: { en: 'Path Smoothing' } },
|
||||
{ label: '实际示例', slug: 'modules/pathfinding/examples', translations: { en: 'Examples' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '蓝图',
|
||||
translations: { en: 'Blueprint' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'modules/blueprint', translations: { en: 'Overview' } },
|
||||
{ label: '虚拟机 API', slug: 'modules/blueprint/vm', translations: { en: 'VM API' } },
|
||||
{ label: '自定义节点', slug: 'modules/blueprint/custom-nodes', translations: { en: 'Custom Nodes' } },
|
||||
{ label: '内置节点', slug: 'modules/blueprint/nodes', translations: { en: 'Built-in Nodes' } },
|
||||
{ label: '蓝图组合', slug: 'modules/blueprint/composition', translations: { en: 'Composition' } },
|
||||
{ label: '实际示例', slug: 'modules/blueprint/examples', translations: { en: 'Examples' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '程序生成',
|
||||
translations: { en: 'Procgen' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'modules/procgen', translations: { en: 'Overview' } },
|
||||
{ label: '噪声函数', slug: 'modules/procgen/noise', translations: { en: 'Noise Functions' } },
|
||||
{ label: '种子随机数', slug: 'modules/procgen/random', translations: { en: 'Seeded Random' } },
|
||||
{ label: '采样工具', slug: 'modules/procgen/sampling', translations: { en: 'Sampling' } },
|
||||
{ label: '实际示例', slug: 'modules/procgen/examples', translations: { en: 'Examples' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'RPC 通信',
|
||||
translations: { en: 'RPC' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'modules/rpc', translations: { en: 'Overview' } },
|
||||
{ label: '服务端', slug: 'modules/rpc/server', translations: { en: 'Server' } },
|
||||
{ label: '客户端', slug: 'modules/rpc/client', translations: { en: 'Client' } },
|
||||
{ label: '编解码', slug: 'modules/rpc/codec', translations: { en: 'Codec' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '网络同步',
|
||||
translations: { en: 'Network' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'modules/network', translations: { en: 'Overview' } },
|
||||
{ label: '客户端', slug: 'modules/network/client', translations: { en: 'Client' } },
|
||||
{ label: '服务器', slug: 'modules/network/server', translations: { en: 'Server' } },
|
||||
{ label: 'HTTP 路由', slug: 'modules/network/http', translations: { en: 'HTTP Routing' } },
|
||||
{ label: '认证系统', slug: 'modules/network/auth', translations: { en: 'Authentication' } },
|
||||
{ label: '速率限制', slug: 'modules/network/rate-limit', translations: { en: 'Rate Limiting' } },
|
||||
{ label: '状态同步', slug: 'modules/network/sync', translations: { en: 'State Sync' } },
|
||||
{ label: '客户端预测', slug: 'modules/network/prediction', translations: { en: 'Prediction' } },
|
||||
{ label: 'AOI 兴趣区域', slug: 'modules/network/aoi', translations: { en: 'AOI' } },
|
||||
{ label: '增量压缩', slug: 'modules/network/delta', translations: { en: 'Delta Compression' } },
|
||||
{ label: 'API 参考', slug: 'modules/network/api', translations: { en: 'API Reference' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '事务系统',
|
||||
translations: { en: 'Transaction' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'modules/transaction', translations: { en: 'Overview' } },
|
||||
{ label: '核心概念', slug: 'modules/transaction/core', translations: { en: 'Core Concepts' } },
|
||||
{ label: '存储层', slug: 'modules/transaction/storage', translations: { en: 'Storage Layer' } },
|
||||
{ label: '操作', slug: 'modules/transaction/operations', translations: { en: 'Operations' } },
|
||||
{ label: '分布式事务', slug: 'modules/transaction/distributed', translations: { en: 'Distributed' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '数据库',
|
||||
translations: { en: 'Database' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'modules/database', translations: { en: 'Overview' } },
|
||||
{ label: '仓储模式', slug: 'modules/database/repository', translations: { en: 'Repository' } },
|
||||
{ label: '用户仓储', slug: 'modules/database/user', translations: { en: 'User Repository' } },
|
||||
{ label: '查询构建器', slug: 'modules/database/query', translations: { en: 'Query Builder' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '数据库驱动',
|
||||
translations: { en: 'Database Drivers' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'modules/database-drivers', translations: { en: 'Overview' } },
|
||||
{ label: 'MongoDB', slug: 'modules/database-drivers/mongo', translations: { en: 'MongoDB' } },
|
||||
{ label: 'Redis', slug: 'modules/database-drivers/redis', translations: { en: 'Redis' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '世界流式加载',
|
||||
translations: { en: 'World Streaming' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'modules/world-streaming', translations: { en: 'Overview' } },
|
||||
{ label: '区块管理', slug: 'modules/world-streaming/chunk-manager', translations: { en: 'Chunk Manager' } },
|
||||
{ label: '流式系统', slug: 'modules/world-streaming/streaming-system', translations: { en: 'Streaming System' } },
|
||||
{ label: '序列化', slug: 'modules/world-streaming/serialization', translations: { en: 'Serialization' } },
|
||||
{ label: '实际示例', slug: 'modules/world-streaming/examples', translations: { en: 'Examples' } },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '示例',
|
||||
translations: { en: 'Examples' },
|
||||
items: [
|
||||
{ label: '示例总览', slug: 'examples', translations: { en: 'Examples Overview' } },
|
||||
{ label: 'Worker 系统演示', slug: 'examples/worker-system-demo', translations: { en: 'Worker System Demo' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'API 参考',
|
||||
translations: { en: 'API Reference' },
|
||||
autogenerate: { directory: 'api' },
|
||||
},
|
||||
{
|
||||
label: '更新日志',
|
||||
translations: { en: 'Changelog' },
|
||||
items: [
|
||||
{ label: '@esengine/ecs-framework', link: 'https://github.com/esengine/esengine/blob/master/packages/framework/core/CHANGELOG.md', attrs: { target: '_blank' } },
|
||||
{ label: '@esengine/behavior-tree', link: 'https://github.com/esengine/esengine/blob/master/packages/framework/behavior-tree/CHANGELOG.md', attrs: { target: '_blank' } },
|
||||
{ label: '@esengine/fsm', link: 'https://github.com/esengine/esengine/blob/master/packages/framework/fsm/CHANGELOG.md', attrs: { target: '_blank' } },
|
||||
{ label: '@esengine/timer', link: 'https://github.com/esengine/esengine/blob/master/packages/framework/timer/CHANGELOG.md', attrs: { target: '_blank' } },
|
||||
{ label: '@esengine/network', link: 'https://github.com/esengine/esengine/blob/master/packages/framework/network/CHANGELOG.md', attrs: { target: '_blank' } },
|
||||
{ label: '@esengine/transaction', link: 'https://github.com/esengine/esengine/blob/master/packages/framework/transaction/CHANGELOG.md', attrs: { target: '_blank' } },
|
||||
{ label: '@esengine/rpc', link: 'https://github.com/esengine/esengine/blob/master/packages/framework/rpc/CHANGELOG.md', attrs: { target: '_blank' } },
|
||||
{ label: '@esengine/cli', link: 'https://github.com/esengine/esengine/blob/master/packages/tools/cli/CHANGELOG.md', attrs: { target: '_blank' } },
|
||||
],
|
||||
},
|
||||
],
|
||||
customCss: ['./src/styles/custom.css'],
|
||||
head: [
|
||||
{ tag: 'meta', attrs: { name: 'theme-color', content: '#646cff' } },
|
||||
],
|
||||
components: {
|
||||
Head: './src/components/Head.astro',
|
||||
ThemeSelect: './src/components/ThemeSelect.astro',
|
||||
},
|
||||
}),
|
||||
vue(),
|
||||
],
|
||||
vite: {
|
||||
plugins: [tailwindcss()],
|
||||
},
|
||||
});
|
||||
@@ -1,29 +1,10 @@
|
||||
---
|
||||
title: "Quick Start"
|
||||
---
|
||||
# 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:
|
||||
### NPM Installation
|
||||
|
||||
```bash
|
||||
# Using npm
|
||||
@@ -352,39 +333,27 @@ function gameLoop(deltaTime: number) {
|
||||
|
||||
## Game Engine Integration
|
||||
|
||||
### Laya 3.x Engine Integration
|
||||
|
||||
Using `Laya.Script` component to manage ECS lifecycle is recommended:
|
||||
### Laya Engine Integration
|
||||
|
||||
```typescript
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
import { Stage } from "laya/display/Stage";
|
||||
import { Laya } from "Laya";
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
const { regClass } = Laya;
|
||||
// Initialize Laya
|
||||
Laya.init(800, 600).then(() => {
|
||||
// Initialize ECS
|
||||
Core.create(true);
|
||||
Core.setScene(new GameScene());
|
||||
|
||||
@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();
|
||||
}
|
||||
}
|
||||
// Start game loop
|
||||
Laya.timer.frameLoop(1, this, () => {
|
||||
const deltaTime = Laya.timer.delta / 1000;
|
||||
Core.update(deltaTime); // Auto-updates global services and scene
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
In Laya IDE, attach the `ECSManager` script to a node in your scene.
|
||||
|
||||
### Cocos Creator Integration
|
||||
|
||||
```typescript
|
||||
@@ -418,8 +387,8 @@ export class ECSGameManager extends Component {
|
||||
|
||||
You've successfully created your first ECS application! Next you can:
|
||||
|
||||
- Check the complete [API Documentation](/api/README)
|
||||
- Explore more [practical examples](/examples/)
|
||||
- Check the complete [API Documentation](/en/api/README)
|
||||
- Explore more [practical examples](/en/examples/)
|
||||
|
||||
## FAQ
|
||||
|
||||
@@ -1,45 +1,43 @@
|
||||
---
|
||||
title: "Guide"
|
||||
---
|
||||
# Guide
|
||||
|
||||
Welcome to the ECS Framework Guide. This guide covers the core concepts and usage of the framework.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### [Entity](/guide/entity)
|
||||
### [Entity](./entity.md)
|
||||
Learn the basics of ECS architecture - how to use entities, lifecycle management, and best practices.
|
||||
|
||||
### [Component](/guide/component)
|
||||
### [Component](./component.md)
|
||||
Learn how to create and use components for modular game feature design.
|
||||
|
||||
### [System](/guide/system)
|
||||
### [System](./system.md)
|
||||
Master system development to implement game logic processing.
|
||||
|
||||
### [Entity Query & Matcher](/guide/entity-query)
|
||||
### [Entity Query & Matcher](./entity-query.md)
|
||||
Learn to use Matcher for entity filtering and queries with `all`, `any`, `none`, `nothing` conditions.
|
||||
|
||||
### [Scene](/guide/scene)
|
||||
### [Scene](./scene.md)
|
||||
Understand scene lifecycle, system management, and entity container features.
|
||||
|
||||
### [Event System](/guide/event-system)
|
||||
### [Event System](./event-system.md)
|
||||
Master the type-safe event system for component communication and system coordination.
|
||||
|
||||
### [Serialization](/guide/serialization)
|
||||
### [Serialization](./serialization.md)
|
||||
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)
|
||||
### [Time and Timers](./time-and-timers.md)
|
||||
Learn time management and timer systems for precise game logic timing control.
|
||||
|
||||
### [Logging](/guide/logging)
|
||||
### [Logging](./logging.md)
|
||||
Master the leveled logging system for debugging, monitoring, and error tracking.
|
||||
|
||||
### [Platform Adapter](/guide/platform-adapter)
|
||||
### [Platform Adapter](./platform-adapter.md)
|
||||
Learn how to implement and register platform adapters for browsers, mini-games, Node.js, and more.
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### [Service Container](/guide/service-container)
|
||||
### [Service Container](./service-container.md)
|
||||
Master dependency injection and service management for loosely-coupled architecture.
|
||||
|
||||
### [Plugin System](/guide/plugin-system)
|
||||
### [Plugin System](./plugin-system.md)
|
||||
Learn how to develop and use plugins to extend framework functionality.
|
||||
317
docs/en/index.md
Normal file
317
docs/en/index.md
Normal file
@@ -0,0 +1,317 @@
|
||||
---
|
||||
layout: page
|
||||
title: ESEngine - High-performance TypeScript ECS Framework
|
||||
---
|
||||
|
||||
<ParticleHeroEn />
|
||||
|
||||
<section class="news-section">
|
||||
<div class="news-container">
|
||||
<div class="news-header">
|
||||
<h2 class="news-title">Quick Links</h2>
|
||||
<a href="/en/guide/" class="news-more">View Docs</a>
|
||||
</div>
|
||||
<div class="news-grid">
|
||||
<a href="/en/guide/getting-started" class="news-card">
|
||||
<div class="news-card-image" style="background: linear-gradient(135deg, #1e3a5f 0%, #1e1e1e 100%);">
|
||||
<div class="news-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24"><path fill="#4fc1ff" d="M12 3L1 9l4 2.18v6L12 21l7-3.82v-6l2-1.09V17h2V9zm6.82 6L12 12.72L5.18 9L12 5.28zM17 16l-5 2.72L7 16v-3.73L12 15l5-2.73z"/></svg>
|
||||
</div>
|
||||
<span class="news-badge">Quick Start</span>
|
||||
</div>
|
||||
<div class="news-card-content">
|
||||
<h3>Get Started in 5 Minutes</h3>
|
||||
<p>From installation to your first ECS app, learn the core concepts quickly.</p>
|
||||
</div>
|
||||
</a>
|
||||
<a href="/en/guide/behavior-tree/" class="news-card">
|
||||
<div class="news-card-image" style="background: linear-gradient(135deg, #1e3a5f 0%, #1e1e1e 100%);">
|
||||
<div class="news-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24"><path fill="#4ec9b0" d="M12 2a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m3 20h-1v-7l-2-2l-2 2v7H9v-7.5l-2 2V22H6v-6l3-3l1-3.5c-.3.4-.6.7-1 1L6 9v1H4V8l5-3c.5-.3 1.1-.5 1.7-.5H11c.6 0 1.2.2 1.7.5l5 3v2h-2V9l-3 1.5c-.4-.3-.7-.6-1-1l1 3.5l3 3v6Z"/></svg>
|
||||
</div>
|
||||
<span class="news-badge">AI System</span>
|
||||
</div>
|
||||
<div class="news-card-content">
|
||||
<h3>Visual Behavior Tree Editor</h3>
|
||||
<p>Built-in AI behavior tree system with visual editing and real-time debugging.</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="features-section">
|
||||
<div class="features-container">
|
||||
<h2 class="features-title">Core Features</h2>
|
||||
<div class="features-grid">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#4fc1ff" d="M13 2.05v2.02c3.95.49 7 3.85 7 7.93c0 1.45-.39 2.79-1.06 3.95l1.59 1.09A9.94 9.94 0 0 0 22 12c0-5.18-3.95-9.45-9-9.95M12 19c-3.87 0-7-3.13-7-7c0-3.53 2.61-6.43 6-6.92V2.05c-5.06.5-9 4.76-9 9.95c0 5.52 4.47 10 9.99 10c3.31 0 6.24-1.61 8.06-4.09l-1.6-1.1A7.93 7.93 0 0 1 12 19"/><path fill="#4fc1ff" d="M12 6a6 6 0 0 0-6 6c0 3.31 2.69 6 6 6a6 6 0 0 0 0-12m0 10c-2.21 0-4-1.79-4-4s1.79-4 4-4s4 1.79 4 4s-1.79 4-4 4"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">High-performance ECS Architecture</h3>
|
||||
<p class="feature-desc">Data-driven entity component system for large-scale entity processing with cache-friendly memory layout.</p>
|
||||
<a href="/en/guide/entity" class="feature-link">Learn more</a>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#569cd6" d="M3 3h18v18H3zm16.525 13.707c0-.795-.272-1.425-.816-1.89c-.544-.465-1.404-.804-2.58-1.016l-1.704-.296c-.616-.104-1.052-.26-1.308-.468c-.256-.21-.384-.468-.384-.776c0-.392.168-.7.504-.924c.336-.224.8-.336 1.392-.336c.56 0 1.008.124 1.344.372c.336.248.536.584.6 1.008h2.016c-.08-.96-.464-1.716-1.152-2.268c-.688-.552-1.6-.828-2.736-.828c-1.2 0-2.148.3-2.844.9c-.696.6-1.044 1.38-1.044 2.34c0 .76.252 1.368.756 1.824c.504.456 1.308.792 2.412.996l1.704.312c.624.12 1.068.28 1.332.48c.264.2.396.46.396.78c0 .424-.192.756-.576.996c-.384.24-.9.36-1.548.36c-.672 0-1.2-.14-1.584-.42c-.384-.28-.608-.668-.672-1.164H8.868c.048 1.016.46 1.808 1.236 2.376c.776.568 1.796.852 3.06.852c1.24 0 2.22-.292 2.94-.876c.72-.584 1.08-1.364 1.08-2.34z"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">Full Type Support</h3>
|
||||
<p class="feature-desc">100% TypeScript with complete type definitions and compile-time checking for the best development experience.</p>
|
||||
<a href="/en/guide/component" class="feature-link">Learn more</a>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#4ec9b0" d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10s10-4.5 10-10S17.5 2 12 2m0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8s8 3.59 8 8s-3.59 8-8 8m-5-8l4-4v3h4v2h-4v3z"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">Visual Behavior Tree</h3>
|
||||
<p class="feature-desc">Built-in AI behavior tree system with visual editor, custom nodes, and real-time debugging.</p>
|
||||
<a href="/en/guide/behavior-tree/" class="feature-link">Learn more</a>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#c586c0" d="M4 6h18V4H4c-1.1 0-2 .9-2 2v11H0v3h14v-3H4zm19 2h-6c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h6c.55 0 1-.45 1-1V9c0-.55-.45-1-1-1m-1 9h-4v-7h4z"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">Multi-Platform Support</h3>
|
||||
<p class="feature-desc">Support for browsers, Node.js, WeChat Mini Games, and seamless integration with major game engines.</p>
|
||||
<a href="/en/guide/platform-adapter" class="feature-link">Learn more</a>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#dcdcaa" d="M4 3h6v2H4v14h6v2H4c-1.1 0-2-.9-2-2V5c0-1.1.9-2 2-2m9 0h6c1.1 0 2 .9 2 2v14c0 1.1-.9 2-2 2h-6v-2h6V5h-6zm-1 7h4v2h-4z"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">Modular Design</h3>
|
||||
<p class="feature-desc">Core features packaged independently, import only what you need. Support for custom plugin extensions.</p>
|
||||
<a href="/en/guide/plugin-system" class="feature-link">Learn more</a>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#9cdcfe" d="M22.7 19l-9.1-9.1c.9-2.3.4-5-1.5-6.9c-2-2-5-2.4-7.4-1.3L9 6L6 9L1.6 4.7C.4 7.1.9 10.1 2.9 12.1c1.9 1.9 4.6 2.4 6.9 1.5l9.1 9.1c.4.4 1 .4 1.4 0l2.3-2.3c.5-.4.5-1.1.1-1.4"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">Developer Tools</h3>
|
||||
<p class="feature-desc">Built-in performance monitoring, debugging tools, serialization system, and complete development toolchain.</p>
|
||||
<a href="/en/guide/logging" class="feature-link">Learn more</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style scoped>
|
||||
/* Home page specific styles */
|
||||
.news-section {
|
||||
background: #0d0d0d;
|
||||
padding: 64px 0;
|
||||
border-top: 1px solid #2a2a2a;
|
||||
}
|
||||
|
||||
.news-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 48px;
|
||||
}
|
||||
|
||||
.news-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.news-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.news-more {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 20px;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 6px;
|
||||
color: #a0a0a0;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.news-more:hover {
|
||||
background: #252525;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.news-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.news-card {
|
||||
display: flex;
|
||||
background: #1f1f1f;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.news-card:hover {
|
||||
border-color: #3b9eff;
|
||||
}
|
||||
|
||||
.news-card-image {
|
||||
width: 200px;
|
||||
min-height: 140px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.news-icon {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.news-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
background: transparent;
|
||||
border: 1px solid #3a3a3a;
|
||||
border-radius: 16px;
|
||||
color: #a0a0a0;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.news-card-content {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.news-card-content h3 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.news-card-content p {
|
||||
font-size: 0.875rem;
|
||||
color: #707070;
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.features-section {
|
||||
background: #0d0d0d;
|
||||
padding: 64px 0;
|
||||
}
|
||||
|
||||
.features-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 48px;
|
||||
}
|
||||
|
||||
.features-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
margin: 0 0 32px 0;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: #1f1f1f;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
border-color: #3b9eff;
|
||||
background: #252525;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: #0d0d0d;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.feature-desc {
|
||||
font-size: 14px;
|
||||
color: #707070;
|
||||
line-height: 1.7;
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
|
||||
.feature-link {
|
||||
font-size: 14px;
|
||||
color: #3b9eff;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.feature-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.news-container,
|
||||
.features-container {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.news-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.news-card {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.news-card-image {
|
||||
width: 100%;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,4 @@
|
||||
---
|
||||
title: "示例"
|
||||
---
|
||||
# 示例
|
||||
|
||||
这里展示了ECS Framework的各种使用示例,通过实际的演示帮助您理解框架的功能和最佳实践。
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
---
|
||||
title: "Worker系统演示"
|
||||
---
|
||||
# Worker系统演示
|
||||
|
||||
这是一个展示ECS框架Worker系统功能的交互式演示。
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
---
|
||||
title: "高级用法"
|
||||
---
|
||||
# 高级用法
|
||||
|
||||
本文介绍行为树系统的高级功能和使用技巧。
|
||||
|
||||
@@ -305,11 +303,11 @@ const tree = BehaviorTreeBuilder.create('Timeout')
|
||||
|
||||
### Cocos Creator集成
|
||||
|
||||
参见[Cocos Creator集成指南](./cocos-integration/)
|
||||
参见[Cocos Creator集成指南](./cocos-integration.md)
|
||||
|
||||
### LayaAir集成
|
||||
|
||||
参见[LayaAir集成指南](./laya-integration/)
|
||||
参见[LayaAir集成指南](./laya-integration.md)
|
||||
|
||||
## 最佳实践
|
||||
|
||||
@@ -389,6 +387,6 @@ const tree = BehaviorTreeBuilder.create('AI')
|
||||
|
||||
## 下一步
|
||||
|
||||
- 查看[自定义节点执行器](./custom-actions/)学习如何创建自定义节点
|
||||
- 阅读[最佳实践](./best-practices/)了解行为树设计技巧
|
||||
- 参考[编辑器使用指南](./editor-guide/)学习可视化编辑
|
||||
- 查看[自定义节点执行器](./custom-actions.md)学习如何创建自定义节点
|
||||
- 阅读[最佳实践](./best-practices.md)了解行为树设计技巧
|
||||
- 参考[编辑器使用指南](./editor-guide.md)学习可视化编辑
|
||||
@@ -1,6 +1,4 @@
|
||||
---
|
||||
title: "资产管理"
|
||||
---
|
||||
# 资产管理
|
||||
|
||||
本文介绍如何加载、管理和复用行为树资产。
|
||||
|
||||
@@ -503,6 +501,6 @@ console.log(json);
|
||||
|
||||
## 下一步
|
||||
|
||||
- 学习[Cocos Creator 集成](./cocos-integration/)了解如何在游戏引擎中加载资源
|
||||
- 查看[自定义节点执行器](./custom-actions/)创建自定义行为
|
||||
- 阅读[最佳实践](./best-practices/)优化你的行为树设计
|
||||
- 学习[Cocos Creator 集成](./cocos-integration.md)了解如何在游戏引擎中加载资源
|
||||
- 查看[自定义节点执行器](./custom-actions.md)创建自定义行为
|
||||
- 阅读[最佳实践](./best-practices.md)优化你的行为树设计
|
||||
@@ -1,6 +1,4 @@
|
||||
---
|
||||
title: "最佳实践"
|
||||
---
|
||||
# 最佳实践
|
||||
|
||||
本文介绍行为树设计和使用的最佳实践,帮助你构建高效、可维护的AI系统。
|
||||
|
||||
@@ -26,7 +24,7 @@ Root Selector
|
||||
|
||||
### 2. 单一职责原则
|
||||
|
||||
每个节点应该只做一件事。要实现复杂动作,创建自定义执行器,参见[自定义节点执行器](./custom-actions/)。
|
||||
每个节点应该只做一件事。要实现复杂动作,创建自定义执行器,参见[自定义节点执行器](./custom-actions.md)。
|
||||
|
||||
```typescript
|
||||
// 好的设计 - 使用内置节点
|
||||
@@ -465,6 +463,6 @@ export class SmartUpdate implements INodeExecutor {
|
||||
|
||||
## 下一步
|
||||
|
||||
- 学习[自定义节点执行器](./custom-actions/)扩展行为树功能
|
||||
- 探索[高级用法](./advanced-usage/)了解更多技巧
|
||||
- 参考[核心概念](./core-concepts/)深入理解原理
|
||||
- 学习[自定义节点执行器](./custom-actions.md)扩展行为树功能
|
||||
- 探索[高级用法](./advanced-usage.md)了解更多技巧
|
||||
- 参考[核心概念](./core-concepts.md)深入理解原理
|
||||
@@ -1,6 +1,4 @@
|
||||
---
|
||||
title: "Cocos Creator 集成"
|
||||
---
|
||||
# Cocos Creator 集成
|
||||
|
||||
本教程将引导你在 Cocos Creator 项目中集成和使用行为树系统。
|
||||
|
||||
@@ -8,7 +6,7 @@ title: "Cocos Creator 集成"
|
||||
|
||||
- Cocos Creator 3.x 或更高版本
|
||||
- 基本的 TypeScript 知识
|
||||
- 已完成[快速开始](./getting-started/)教程
|
||||
- 已完成[快速开始](./getting-started.md)教程
|
||||
|
||||
## 安装
|
||||
|
||||
@@ -679,7 +677,7 @@ const updateInterval = sys.isNative ? 0.016 : 0.05;
|
||||
|
||||
## 下一步
|
||||
|
||||
- 查看[资产管理](./asset-management/)了解如何加载和管理行为树资产、使用子树
|
||||
- 学习[高级用法](./advanced-usage/)了解性能优化和调试技巧
|
||||
- 阅读[最佳实践](./best-practices/)优化你的 AI
|
||||
- 学习[自定义节点执行器](./custom-actions/)创建自定义行为
|
||||
- 查看[资产管理](./asset-management.md)了解如何加载和管理行为树资产、使用子树
|
||||
- 学习[高级用法](./advanced-usage.md)了解性能优化和调试技巧
|
||||
- 阅读[最佳实践](./best-practices.md)优化你的 AI
|
||||
- 学习[自定义节点执行器](./custom-actions.md)创建自定义行为
|
||||
@@ -1,6 +1,4 @@
|
||||
---
|
||||
title: "核心概念"
|
||||
---
|
||||
# 核心概念
|
||||
|
||||
本文介绍行为树系统的核心概念和工作原理。
|
||||
|
||||
@@ -192,7 +190,7 @@ const tree = BehaviorTreeBuilder.create('Actions')
|
||||
.build();
|
||||
```
|
||||
|
||||
要实现自定义动作,需要创建自定义执行器,参见[自定义节点执行器](./custom-actions/)。
|
||||
要实现自定义动作,需要创建自定义执行器,参见[自定义节点执行器](./custom-actions.md)。
|
||||
|
||||
#### Condition(条件)
|
||||
|
||||
@@ -487,7 +485,7 @@ NodeRuntimeState
|
||||
|
||||
现在你已经理解了行为树的核心概念,接下来可以:
|
||||
|
||||
- 查看[快速开始](./getting-started/)创建第一个行为树
|
||||
- 学习[自定义节点执行器](./custom-actions/)创建自定义节点
|
||||
- 探索[高级用法](./advanced-usage/)了解更多功能
|
||||
- 阅读[最佳实践](./best-practices/)学习设计模式
|
||||
- 查看[快速开始](./getting-started.md)创建第一个行为树
|
||||
- 学习[自定义节点执行器](./custom-actions.md)创建自定义节点
|
||||
- 探索[高级用法](./advanced-usage.md)了解更多功能
|
||||
- 阅读[最佳实践](./best-practices.md)学习设计模式
|
||||
@@ -1,6 +1,4 @@
|
||||
---
|
||||
title: "自定义节点执行器"
|
||||
---
|
||||
# 自定义节点执行器
|
||||
|
||||
本教程介绍如何为项目创建专用的节点执行器,供策划在编辑器中使用。
|
||||
|
||||
@@ -606,107 +604,6 @@ export class RetryDecorator implements INodeExecutor {
|
||||
}
|
||||
```
|
||||
|
||||
## 在代码中使用自定义执行器
|
||||
|
||||
定义了自定义执行器后,可以通过 `BehaviorTreeBuilder` 的 `.action()` 和 `.condition()` 方法在代码中使用:
|
||||
|
||||
### 使用 action() 方法
|
||||
|
||||
```typescript
|
||||
import { BehaviorTreeBuilder, BehaviorTreeStarter } from '@esengine/behavior-tree';
|
||||
|
||||
// 使用自定义执行器构建行为树
|
||||
const tree = BehaviorTreeBuilder.create('CombatAI')
|
||||
.defineBlackboardVariable('health', 100)
|
||||
.defineBlackboardVariable('target', null)
|
||||
.selector('Root')
|
||||
.sequence('AttackSequence')
|
||||
// 使用自定义动作 - implementationType 匹配装饰器中的定义
|
||||
.action('AttackAction', 'Attack', { damage: 25 })
|
||||
.action('MoveToPosition', 'Chase', { speed: 10 })
|
||||
.end()
|
||||
.action('DelayAction', 'Idle', { duration: 1.0 })
|
||||
.end()
|
||||
.build();
|
||||
|
||||
// 启动行为树
|
||||
const entity = scene.createEntity('Enemy');
|
||||
BehaviorTreeStarter.start(entity, tree);
|
||||
```
|
||||
|
||||
### 使用 condition() 方法
|
||||
|
||||
```typescript
|
||||
const tree = BehaviorTreeBuilder.create('AI')
|
||||
.selector('Root')
|
||||
.sequence('AttackBranch')
|
||||
// 使用自定义条件
|
||||
.condition('CheckHealth', 'IsHealthy', { threshold: 50, operator: 'greater' })
|
||||
.action('AttackAction', 'Attack')
|
||||
.end()
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
### Builder 方法对照表
|
||||
|
||||
| 方法 | 说明 | 使用场景 |
|
||||
|------|------|----------|
|
||||
| `.action(type, name?, config?)` | 使用自定义动作执行器 | 自定义 Action 类 |
|
||||
| `.condition(type, name?, config?)` | 使用自定义条件执行器 | 自定义 Condition 类 |
|
||||
| `.executeAction(name)` | 调用黑板函数 `action_{name}` | 简单逻辑、快速原型 |
|
||||
| `.executeCondition(name)` | 调用黑板函数 `condition_{name}` | 简单条件判断 |
|
||||
|
||||
### 完整示例
|
||||
|
||||
```typescript
|
||||
import {
|
||||
BehaviorTreeBuilder,
|
||||
BehaviorTreeStarter,
|
||||
NodeExecutorMetadata,
|
||||
INodeExecutor,
|
||||
NodeExecutionContext,
|
||||
TaskStatus,
|
||||
NodeType,
|
||||
BindingHelper
|
||||
} from '@esengine/behavior-tree';
|
||||
|
||||
// 1. 定义自定义执行器
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'AttackAction',
|
||||
nodeType: NodeType.Action,
|
||||
displayName: '攻击',
|
||||
category: 'Combat',
|
||||
configSchema: {
|
||||
damage: { type: 'number', default: 10, supportBinding: true }
|
||||
}
|
||||
})
|
||||
class AttackAction implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const damage = BindingHelper.getValue<number>(context, 'damage', 10);
|
||||
console.log(`执行攻击,造成 ${damage} 点伤害!`);
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 构建行为树
|
||||
const enemyAI = BehaviorTreeBuilder.create('EnemyAI')
|
||||
.defineBlackboardVariable('health', 100)
|
||||
.defineBlackboardVariable('target', null)
|
||||
.selector('MainBehavior')
|
||||
.sequence('AttackBranch')
|
||||
.condition('CheckHealth', 'HasEnoughHealth', { threshold: 20, operator: 'greater' })
|
||||
.action('AttackAction', 'Attack', { damage: 50 })
|
||||
.end()
|
||||
.log('逃跑', 'Flee')
|
||||
.end()
|
||||
.build();
|
||||
|
||||
// 3. 启动行为树
|
||||
const entity = scene.createEntity('Enemy');
|
||||
BehaviorTreeStarter.start(entity, enemyAI);
|
||||
```
|
||||
|
||||
## 注册执行器
|
||||
|
||||
### 自动注册
|
||||
@@ -1123,6 +1020,6 @@ execute(context: NodeExecutionContext): TaskStatus {
|
||||
|
||||
## 下一步
|
||||
|
||||
- 学习[编辑器工作流](./editor-workflow/)了解如何在编辑器中使用自定义节点
|
||||
- 阅读[最佳实践](./best-practices/)学习行为树设计模式
|
||||
- 查看[高级用法](./advanced-usage/)了解更多功能
|
||||
- 学习[编辑器工作流](./editor-workflow.md)了解如何在编辑器中使用自定义节点
|
||||
- 阅读[最佳实践](./best-practices.md)学习行为树设计模式
|
||||
- 查看[高级用法](./advanced-usage.md)了解更多功能
|
||||
@@ -1,6 +1,4 @@
|
||||
---
|
||||
title: "行为树编辑器使用指南"
|
||||
---
|
||||
# 行为树编辑器使用指南
|
||||
|
||||
行为树编辑器提供了可视化的方式来创建和编辑行为树。
|
||||
|
||||
@@ -117,5 +115,5 @@ BehaviorTreeStarter.start(entity, tree);
|
||||
|
||||
## 下一步
|
||||
|
||||
- 查看[编辑器工作流](./editor-workflow/)了解完整的开发流程
|
||||
- 查看[自定义节点执行器](./custom-actions/)学习如何扩展节点
|
||||
- 查看[编辑器工作流](./editor-workflow.md)了解完整的开发流程
|
||||
- 查看[自定义节点执行器](./custom-actions.md)学习如何扩展节点
|
||||
@@ -1,6 +1,4 @@
|
||||
---
|
||||
title: "编辑器工作流"
|
||||
---
|
||||
# 编辑器工作流
|
||||
|
||||
本教程介绍如何使用行为树编辑器创建AI,并在游戏中加载使用。
|
||||
|
||||
@@ -112,7 +110,7 @@ setInterval(() => {
|
||||
|
||||
## 实现自定义执行器
|
||||
|
||||
要扩展行为树的功能,需要创建自定义执行器(详见[自定义节点执行器](./custom-actions/)):
|
||||
要扩展行为树的功能,需要创建自定义执行器(详见[自定义节点执行器](./custom-actions.md)):
|
||||
|
||||
```typescript
|
||||
import {
|
||||
@@ -250,6 +248,6 @@ setInterval(() => {
|
||||
|
||||
## 下一步
|
||||
|
||||
- 查看[自定义节点执行器](./custom-actions/)学习如何创建自定义节点
|
||||
- 查看[高级用法](./advanced-usage/)了解性能优化等高级特性
|
||||
- 查看[最佳实践](./best-practices/)优化你的AI设计
|
||||
- 查看[自定义节点执行器](./custom-actions.md)学习如何创建自定义节点
|
||||
- 查看[高级用法](./advanced-usage.md)了解性能优化等高级特性
|
||||
- 查看[最佳实践](./best-practices.md)优化你的AI设计
|
||||
@@ -1,6 +1,4 @@
|
||||
---
|
||||
title: "快速开始"
|
||||
---
|
||||
# 快速开始
|
||||
|
||||
本教程将引导你在5分钟内创建第一个行为树。
|
||||
|
||||
@@ -333,11 +331,11 @@ BehaviorTreeStarter.restart(entity);
|
||||
|
||||
现在你已经创建了第一个行为树,接下来可以:
|
||||
|
||||
1. 学习[核心概念](./core-concepts/)深入理解行为树原理
|
||||
2. 学习[资产管理](./asset-management/)了解如何加载和复用行为树、使用子树
|
||||
3. 查看[自定义节点执行器](./custom-actions/)学习如何创建自定义节点
|
||||
4. 根据你的场景查看集成教程:[Cocos Creator](./cocos-integration/) 或 [Node.js](./nodejs-usage.md)
|
||||
5. 查看[高级用法](./advanced-usage/)了解更多功能
|
||||
1. 学习[核心概念](./core-concepts.md)深入理解行为树原理
|
||||
2. 学习[资产管理](./asset-management.md)了解如何加载和复用行为树、使用子树
|
||||
3. 查看[自定义节点执行器](./custom-actions.md)学习如何创建自定义节点
|
||||
4. 根据你的场景查看集成教程:[Cocos Creator](./cocos-integration.md) 或 [Node.js](./nodejs-usage.md)
|
||||
5. 查看[高级用法](./advanced-usage.md)了解更多功能
|
||||
|
||||
## 常见问题
|
||||
|
||||
@@ -384,4 +382,4 @@ console.log('活动节点:', Array.from(runtime?.activeNodeIds || []));
|
||||
|
||||
内置的`executeAction`和`executeCondition`节点只是占位符。要实现真正的自定义逻辑,你需要创建自定义执行器:
|
||||
|
||||
参见[自定义节点执行器](./custom-actions/)学习如何创建。
|
||||
参见[自定义节点执行器](./custom-actions.md)学习如何创建。
|
||||
@@ -1,6 +1,4 @@
|
||||
---
|
||||
title: "行为树系统"
|
||||
---
|
||||
# 行为树系统
|
||||
|
||||
行为树(Behavior Tree)是一种用于游戏AI和自动化控制的强大工具。本框架提供了基于Runtime执行器架构的行为树系统,具有高性能、类型安全、易于扩展的特点。
|
||||
|
||||
@@ -43,29 +41,29 @@ title: "行为树系统"
|
||||
|
||||
### 入门教程
|
||||
|
||||
- **[快速开始](./getting-started/)** - 5分钟上手行为树
|
||||
- **[核心概念](./core-concepts/)** - 理解行为树的基本原理
|
||||
- **[快速开始](./getting-started.md)** - 5分钟上手行为树
|
||||
- **[核心概念](./core-concepts.md)** - 理解行为树的基本原理
|
||||
|
||||
### 编辑器使用
|
||||
|
||||
- **[编辑器使用指南](./editor-guide/)** - 可视化创建行为树
|
||||
- **[编辑器工作流](./editor-workflow/)** - 完整的开发流程
|
||||
- **[编辑器使用指南](./editor-guide.md)** - 可视化创建行为树
|
||||
- **[编辑器工作流](./editor-workflow.md)** - 完整的开发流程
|
||||
|
||||
### 资源管理
|
||||
|
||||
- **[资产管理](./asset-management/)** - 加载、管理和复用行为树资产、使用子树
|
||||
- **[资产管理](./asset-management.md)** - 加载、管理和复用行为树资产、使用子树
|
||||
|
||||
### 引擎集成
|
||||
|
||||
- **[Cocos Creator 集成](./cocos-integration/)** - 在 Cocos Creator 中使用行为树
|
||||
- **[Laya 引擎集成](./laya-integration/)** - 在 Laya 中使用行为树
|
||||
- **[Node.js 服务端使用](./nodejs-usage/)** - 在服务器、聊天机器人等场景中使用行为树
|
||||
- **[Cocos Creator 集成](./cocos-integration.md)** - 在 Cocos Creator 中使用行为树
|
||||
- **[Laya 引擎集成](./laya-integration.md)** - 在 Laya 中使用行为树
|
||||
- **[Node.js 服务端使用](./nodejs-usage.md)** - 在服务器、聊天机器人等场景中使用行为树
|
||||
|
||||
### 高级主题
|
||||
|
||||
- **[高级用法](./advanced-usage/)** - 性能优化、调试技巧
|
||||
- **[自定义节点执行器](./custom-actions/)** - 创建自定义行为节点
|
||||
- **[最佳实践](./best-practices/)** - 行为树设计模式和技巧
|
||||
- **[高级用法](./advanced-usage.md)** - 性能优化、调试技巧
|
||||
- **[自定义节点执行器](./custom-actions.md)** - 创建自定义行为节点
|
||||
- **[最佳实践](./best-practices.md)** - 行为树设计模式和技巧
|
||||
|
||||
## 快速示例
|
||||
|
||||
@@ -177,23 +175,23 @@ export class AttackAction implements INodeExecutor {
|
||||
}
|
||||
```
|
||||
|
||||
详细说明请参见[自定义节点执行器](./custom-actions/)。
|
||||
详细说明请参见[自定义节点执行器](./custom-actions.md)。
|
||||
|
||||
## 下一步
|
||||
|
||||
建议按照以下顺序学习:
|
||||
|
||||
1. 阅读[快速开始](./getting-started/)了解基础用法
|
||||
2. 学习[核心概念](./core-concepts/)理解行为树原理
|
||||
3. 学习[资产管理](./asset-management/)了解如何加载和复用行为树、使用子树
|
||||
1. 阅读[快速开始](./getting-started.md)了解基础用法
|
||||
2. 学习[核心概念](./core-concepts.md)理解行为树原理
|
||||
3. 学习[资产管理](./asset-management.md)了解如何加载和复用行为树、使用子树
|
||||
4. 根据你的场景查看集成教程:
|
||||
- 客户端游戏:[Cocos Creator](./cocos-integration/) 或 [Laya](./laya-integration.md)
|
||||
- 服务端应用:[Node.js 服务端使用](./nodejs-usage/)
|
||||
5. 尝试[编辑器使用指南](./editor-guide/)可视化创建行为树
|
||||
6. 探索[高级用法](./advanced-usage/)和[自定义节点执行器](./custom-actions.md)提升技能
|
||||
- 客户端游戏:[Cocos Creator](./cocos-integration.md) 或 [Laya](./laya-integration.md)
|
||||
- 服务端应用:[Node.js 服务端使用](./nodejs-usage.md)
|
||||
5. 尝试[编辑器使用指南](./editor-guide.md)可视化创建行为树
|
||||
6. 探索[高级用法](./advanced-usage.md)和[自定义节点执行器](./custom-actions.md)提升技能
|
||||
|
||||
## 获取帮助
|
||||
|
||||
- 提交 [Issue](https://github.com/esengine/esengine/issues)
|
||||
- 提交 [Issue](https://github.com/esengine/ecs-framework/issues)
|
||||
- 加入社区讨论
|
||||
- 参考文档中的完整代码示例
|
||||
@@ -1,6 +1,4 @@
|
||||
---
|
||||
title: "Laya 引擎集成"
|
||||
---
|
||||
# Laya 引擎集成
|
||||
|
||||
本教程将引导你在 Laya 引擎项目中集成和使用行为树系统。
|
||||
|
||||
@@ -8,7 +6,7 @@ title: "Laya 引擎集成"
|
||||
|
||||
- LayaAir 3.x 或更高版本
|
||||
- 基本的 TypeScript 知识
|
||||
- 已完成[快速开始](./getting-started/)教程
|
||||
- 已完成[快速开始](./getting-started.md)教程
|
||||
|
||||
## 安装
|
||||
|
||||
@@ -311,5 +309,5 @@ class AIManager {
|
||||
|
||||
## 下一步
|
||||
|
||||
- 查看[高级用法](./advanced-usage/)
|
||||
- 学习[最佳实践](./best-practices/)
|
||||
- 查看[高级用法](./advanced-usage.md)
|
||||
- 学习[最佳实践](./best-practices.md)
|
||||
@@ -1,6 +1,4 @@
|
||||
---
|
||||
title: "Node.js 服务端使用"
|
||||
---
|
||||
# Node.js 服务端使用
|
||||
|
||||
本文介绍如何在 Node.js 服务端环境(如游戏服务器、机器人、自动化工具)中使用行为树系统。
|
||||
|
||||
@@ -577,6 +575,6 @@ function loadAIState(entity: Entity, savedState: any) {
|
||||
|
||||
## 下一步
|
||||
|
||||
- 查看[资产管理](./asset-management/)了解资源加载和子树
|
||||
- 学习[自定义节点执行器](./custom-actions/)创建自定义行为
|
||||
- 阅读[最佳实践](./best-practices/)优化你的服务端AI
|
||||
- 查看[资产管理](./asset-management.md)了解资源加载和子树
|
||||
- 学习[自定义节点执行器](./custom-actions.md)创建自定义行为
|
||||
- 阅读[最佳实践](./best-practices.md)优化你的服务端AI
|
||||
725
docs/guide/component.md
Normal file
725
docs/guide/component.md
Normal file
@@ -0,0 +1,725 @@
|
||||
# 组件系统
|
||||
|
||||
在 ECS 架构中,组件(Component)是数据和行为的载体。组件定义了实体具有的属性和功能,是 ECS 架构的核心构建块。
|
||||
|
||||
## 基本概念
|
||||
|
||||
组件是继承自 `Component` 抽象基类的具体类,用于:
|
||||
- 存储实体的数据(如位置、速度、健康值等)
|
||||
- 定义与数据相关的行为方法
|
||||
- 提供生命周期回调钩子
|
||||
- 支持序列化和调试
|
||||
|
||||
## 创建组件
|
||||
|
||||
### 基础组件定义
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
x: number = 0;
|
||||
y: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('Health')
|
||||
class Health extends Component {
|
||||
current: number;
|
||||
max: number;
|
||||
|
||||
constructor(max: number = 100) {
|
||||
super();
|
||||
this.max = max;
|
||||
this.current = max;
|
||||
}
|
||||
|
||||
// 组件可以包含行为方法
|
||||
takeDamage(damage: number): void {
|
||||
this.current = Math.max(0, this.current - damage);
|
||||
}
|
||||
|
||||
heal(amount: number): void {
|
||||
this.current = Math.min(this.max, this.current + amount);
|
||||
}
|
||||
|
||||
isDead(): boolean {
|
||||
return this.current <= 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### @ECSComponent 装饰器
|
||||
|
||||
`@ECSComponent` 是组件类必须使用的装饰器,它为组件提供了类型标识和元数据管理。
|
||||
|
||||
#### 为什么必须使用
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| **类型识别** | 提供稳定的类型名称,代码混淆后仍能正确识别 |
|
||||
| **序列化支持** | 序列化/反序列化时使用该名称作为类型标识 |
|
||||
| **组件注册** | 自动注册到 ComponentRegistry,分配唯一的位掩码 |
|
||||
| **调试支持** | 在调试工具和日志中显示可读的组件名称 |
|
||||
|
||||
#### 基本语法
|
||||
|
||||
```typescript
|
||||
@ECSComponent(typeName: string)
|
||||
```
|
||||
|
||||
- `typeName`: 组件的类型名称,建议使用与类名相同或相近的名称
|
||||
|
||||
#### 使用示例
|
||||
|
||||
```typescript
|
||||
// ✅ 正确的用法
|
||||
@ECSComponent('Velocity')
|
||||
class Velocity extends Component {
|
||||
dx: number = 0;
|
||||
dy: number = 0;
|
||||
}
|
||||
|
||||
// ✅ 推荐:类型名与类名保持一致
|
||||
@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 { }
|
||||
```
|
||||
|
||||
## 组件生命周期
|
||||
|
||||
组件提供了生命周期钩子,可以重写来执行特定的逻辑:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('ExampleComponent')
|
||||
class ExampleComponent extends Component {
|
||||
private resource: SomeResource | null = null;
|
||||
|
||||
/**
|
||||
* 组件被添加到实体时调用
|
||||
* 用于初始化资源、建立引用等
|
||||
*/
|
||||
onAddedToEntity(): void {
|
||||
console.log(`组件 ${this.constructor.name} 已添加,实体ID: ${this.entityId}`);
|
||||
this.resource = new SomeResource();
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件从实体移除时调用
|
||||
* 用于清理资源、断开引用等
|
||||
*/
|
||||
onRemovedFromEntity(): void {
|
||||
console.log(`组件 ${this.constructor.name} 已移除`);
|
||||
if (this.resource) {
|
||||
this.resource.cleanup();
|
||||
this.resource = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 组件与实体的关系
|
||||
|
||||
组件存储了所属实体的ID (`entityId`),而不是直接引用实体对象。这是ECS数据导向设计的体现,避免了循环引用。
|
||||
|
||||
在实际使用中,**应该在 System 中处理实体和组件的交互**,而不是在组件内部:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Health')
|
||||
class Health extends Component {
|
||||
current: number;
|
||||
max: number;
|
||||
|
||||
constructor(max: number = 100) {
|
||||
super();
|
||||
this.max = max;
|
||||
this.current = max;
|
||||
}
|
||||
|
||||
isDead(): boolean {
|
||||
return this.current <= 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('Damage')
|
||||
class Damage extends Component {
|
||||
value: number;
|
||||
|
||||
constructor(value: number) {
|
||||
super();
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
// 推荐:在 System 中处理逻辑
|
||||
class DamageSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(new Matcher().all(Health, Damage));
|
||||
}
|
||||
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health)!;
|
||||
const damage = entity.getComponent(Damage)!;
|
||||
|
||||
health.current -= damage.value;
|
||||
|
||||
if (health.isDead()) {
|
||||
entity.destroy();
|
||||
}
|
||||
|
||||
// 应用伤害后移除 Damage 组件
|
||||
entity.removeComponent(damage);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 组件属性
|
||||
|
||||
每个组件都有一些内置属性:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('ExampleComponent')
|
||||
class ExampleComponent extends Component {
|
||||
someData: string = "example";
|
||||
|
||||
onAddedToEntity(): void {
|
||||
console.log(`组件ID: ${this.id}`); // 唯一的组件ID
|
||||
console.log(`所属实体ID: ${this.entityId}`); // 所属实体的ID
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
如果需要访问实体对象,应该在 System 中进行:
|
||||
|
||||
```typescript
|
||||
class ExampleSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(new Matcher().all(ExampleComponent));
|
||||
}
|
||||
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const comp = entity.getComponent(ExampleComponent)!;
|
||||
console.log(`实体名称: ${entity.name}`);
|
||||
console.log(`组件数据: ${comp.someData}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 复杂组件示例
|
||||
|
||||
### 状态机组件
|
||||
|
||||
```typescript
|
||||
enum EntityState {
|
||||
Idle,
|
||||
Moving,
|
||||
Attacking,
|
||||
Dead
|
||||
}
|
||||
|
||||
@ECSComponent('StateMachine')
|
||||
class StateMachine extends Component {
|
||||
private _currentState: EntityState = EntityState.Idle;
|
||||
private _previousState: EntityState = EntityState.Idle;
|
||||
private _stateTimer: number = 0;
|
||||
|
||||
get currentState(): EntityState {
|
||||
return this._currentState;
|
||||
}
|
||||
|
||||
get previousState(): EntityState {
|
||||
return this._previousState;
|
||||
}
|
||||
|
||||
get stateTimer(): number {
|
||||
return this._stateTimer;
|
||||
}
|
||||
|
||||
changeState(newState: EntityState): void {
|
||||
if (this._currentState !== newState) {
|
||||
this._previousState = this._currentState;
|
||||
this._currentState = newState;
|
||||
this._stateTimer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
updateTimer(deltaTime: number): void {
|
||||
this._stateTimer += deltaTime;
|
||||
}
|
||||
|
||||
isInState(state: EntityState): boolean {
|
||||
return this._currentState === state;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 配置数据组件
|
||||
|
||||
```typescript
|
||||
interface WeaponData {
|
||||
damage: number;
|
||||
range: number;
|
||||
fireRate: number;
|
||||
ammo: number;
|
||||
}
|
||||
|
||||
@ECSComponent('WeaponConfig')
|
||||
class WeaponConfig extends Component {
|
||||
data: WeaponData;
|
||||
|
||||
constructor(weaponData: WeaponData) {
|
||||
super();
|
||||
this.data = { ...weaponData }; // 深拷贝避免共享引用
|
||||
}
|
||||
|
||||
// 提供便捷的访问方法
|
||||
getDamage(): number {
|
||||
return this.data.damage;
|
||||
}
|
||||
|
||||
canFire(): boolean {
|
||||
return this.data.ammo > 0;
|
||||
}
|
||||
|
||||
consumeAmmo(): boolean {
|
||||
if (this.data.ammo > 0) {
|
||||
this.data.ammo--;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 保持组件简单
|
||||
|
||||
```typescript
|
||||
// 好的组件设计 - 单一职责
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
x: number = 0;
|
||||
y: number = 0;
|
||||
}
|
||||
|
||||
@ECSComponent('Velocity')
|
||||
class Velocity extends Component {
|
||||
dx: number = 0;
|
||||
dy: number = 0;
|
||||
}
|
||||
|
||||
// 避免的组件设计 - 职责过多
|
||||
@ECSComponent('GameObject')
|
||||
class GameObject extends Component {
|
||||
x: number;
|
||||
y: number;
|
||||
dx: number;
|
||||
dy: number;
|
||||
health: number;
|
||||
damage: number;
|
||||
sprite: string;
|
||||
// 太多不相关的属性
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 使用构造函数初始化
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Transform')
|
||||
class Transform extends Component {
|
||||
x: number;
|
||||
y: number;
|
||||
rotation: number;
|
||||
scale: number;
|
||||
|
||||
constructor(x = 0, y = 0, rotation = 0, scale = 1) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.rotation = rotation;
|
||||
this.scale = scale;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 明确的类型定义
|
||||
|
||||
```typescript
|
||||
interface InventoryItem {
|
||||
id: string;
|
||||
name: string;
|
||||
quantity: number;
|
||||
type: 'weapon' | 'consumable' | 'misc';
|
||||
}
|
||||
|
||||
@ECSComponent('Inventory')
|
||||
class Inventory extends Component {
|
||||
items: InventoryItem[] = [];
|
||||
maxSlots: number;
|
||||
|
||||
constructor(maxSlots: number = 20) {
|
||||
super();
|
||||
this.maxSlots = maxSlots;
|
||||
}
|
||||
|
||||
addItem(item: InventoryItem): boolean {
|
||||
if (this.items.length < this.maxSlots) {
|
||||
this.items.push(item);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
removeItem(itemId: string): InventoryItem | null {
|
||||
const index = this.items.findIndex(item => item.id === itemId);
|
||||
if (index !== -1) {
|
||||
return this.items.splice(index, 1)[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 引用其他实体
|
||||
|
||||
当组件需要关联其他实体时(如父子关系、跟随目标等),**推荐方式是存储实体ID**,然后在 System 中查找:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Follower')
|
||||
class Follower extends Component {
|
||||
targetId: number;
|
||||
followDistance: number = 50;
|
||||
|
||||
constructor(targetId: number) {
|
||||
super();
|
||||
this.targetId = targetId;
|
||||
}
|
||||
}
|
||||
|
||||
// 在 System 中查找目标实体并处理逻辑
|
||||
class FollowerSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(new Matcher().all(Follower, Position));
|
||||
}
|
||||
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const follower = entity.getComponent(Follower)!;
|
||||
const position = entity.getComponent(Position)!;
|
||||
|
||||
// 通过场景查找目标实体
|
||||
const target = entity.scene?.findEntityById(follower.targetId);
|
||||
if (target) {
|
||||
const targetPos = target.getComponent(Position);
|
||||
if (targetPos) {
|
||||
// 跟随逻辑
|
||||
const dx = targetPos.x - position.x;
|
||||
const dy = targetPos.y - position.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance > follower.followDistance) {
|
||||
// 移动靠近目标
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这种方式的优势:
|
||||
- 组件保持简单,只存储基本数据类型
|
||||
- 符合数据导向设计
|
||||
- 在 System 中统一处理查找和逻辑
|
||||
- 易于理解和维护
|
||||
|
||||
**避免在组件中直接存储实体引用**:
|
||||
|
||||
```typescript
|
||||
// 错误示范:直接存储实体引用
|
||||
@ECSComponent('BadFollower')
|
||||
class BadFollower extends Component {
|
||||
target: Entity; // 实体销毁后仍持有引用,可能导致内存泄漏
|
||||
}
|
||||
```
|
||||
|
||||
## 高级特性
|
||||
|
||||
### EntityRef 装饰器 - 自动引用追踪
|
||||
|
||||
框架提供了 `@EntityRef` 装饰器用于**特殊场景**下安全地存储实体引用。这是一个高级特性,一般情况下推荐使用存储ID的方式。
|
||||
|
||||
#### 什么时候需要 EntityRef?
|
||||
|
||||
在以下场景中,`@EntityRef` 可以简化代码:
|
||||
|
||||
1. **父子关系**: 需要在组件中直接访问父实体或子实体
|
||||
2. **复杂关联**: 实体之间有多个引用关系
|
||||
3. **频繁访问**: 需要在多处访问引用的实体,使用ID查找会有性能开销
|
||||
|
||||
#### 核心特性
|
||||
|
||||
`@EntityRef` 装饰器通过 **ReferenceTracker** 自动追踪引用关系:
|
||||
|
||||
- 当被引用的实体销毁时,所有指向它的 `@EntityRef` 属性自动设为 `null`
|
||||
- 防止跨场景引用(会输出警告并拒绝设置)
|
||||
- 防止引用已销毁的实体(会输出警告并设为 `null`)
|
||||
- 使用 WeakRef 避免内存泄漏(自动GC支持)
|
||||
- 组件移除时自动清理引用注册
|
||||
|
||||
#### 基本用法
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent, EntityRef, Entity } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSComponent('Parent')
|
||||
class ParentComponent extends Component {
|
||||
@EntityRef()
|
||||
parent: Entity | null = null;
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
const scene = new Scene();
|
||||
const parent = scene.createEntity('Parent');
|
||||
const child = scene.createEntity('Child');
|
||||
|
||||
const comp = child.addComponent(new ParentComponent());
|
||||
comp.parent = parent;
|
||||
|
||||
console.log(comp.parent); // Entity { name: 'Parent' }
|
||||
|
||||
// 当 parent 被销毁时,comp.parent 自动变为 null
|
||||
parent.destroy();
|
||||
console.log(comp.parent); // null
|
||||
```
|
||||
|
||||
#### 多个引用属性
|
||||
|
||||
一个组件可以有多个 `@EntityRef` 属性:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Combat')
|
||||
class CombatComponent extends Component {
|
||||
@EntityRef()
|
||||
target: Entity | null = null;
|
||||
|
||||
@EntityRef()
|
||||
ally: Entity | null = null;
|
||||
|
||||
@EntityRef()
|
||||
lastAttacker: Entity | null = null;
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
const player = scene.createEntity('Player');
|
||||
const enemy = scene.createEntity('Enemy');
|
||||
const npc = scene.createEntity('NPC');
|
||||
|
||||
const combat = player.addComponent(new CombatComponent());
|
||||
combat.target = enemy;
|
||||
combat.ally = npc;
|
||||
|
||||
// enemy 销毁后,只有 target 变为 null,ally 仍然有效
|
||||
enemy.destroy();
|
||||
console.log(combat.target); // null
|
||||
console.log(combat.ally); // Entity { name: 'NPC' }
|
||||
```
|
||||
|
||||
#### 安全检查
|
||||
|
||||
`@EntityRef` 提供了多重安全检查:
|
||||
|
||||
```typescript
|
||||
const scene1 = new Scene();
|
||||
const scene2 = new Scene();
|
||||
|
||||
const entity1 = scene1.createEntity('Entity1');
|
||||
const entity2 = scene2.createEntity('Entity2');
|
||||
|
||||
const comp = entity1.addComponent(new ParentComponent());
|
||||
|
||||
// 跨场景引用会失败
|
||||
comp.parent = entity2; // 输出错误日志,comp.parent 为 null
|
||||
console.log(comp.parent); // null
|
||||
|
||||
// 引用已销毁的实体会失败
|
||||
const entity3 = scene1.createEntity('Entity3');
|
||||
entity3.destroy();
|
||||
comp.parent = entity3; // 输出警告日志,comp.parent 为 null
|
||||
console.log(comp.parent); // null
|
||||
```
|
||||
|
||||
#### 实现原理
|
||||
|
||||
`@EntityRef` 使用以下机制实现自动引用追踪:
|
||||
|
||||
1. **ReferenceTracker**: Scene 持有一个引用追踪器,记录所有实体引用关系
|
||||
2. **WeakRef**: 使用弱引用存储组件,避免循环引用导致内存泄漏
|
||||
3. **属性拦截**: 通过 `Object.defineProperty` 拦截 getter/setter
|
||||
4. **自动清理**: 实体销毁时,ReferenceTracker 遍历所有引用并设为 null
|
||||
|
||||
```typescript
|
||||
// 简化的实现原理
|
||||
class ReferenceTracker {
|
||||
// entityId -> 引用该实体的所有组件记录
|
||||
private _references: Map<number, Set<{ component: WeakRef<Component>, propertyKey: string }>>;
|
||||
|
||||
// 实体销毁时调用
|
||||
clearReferencesTo(entityId: number): void {
|
||||
const records = this._references.get(entityId);
|
||||
if (records) {
|
||||
for (const record of records) {
|
||||
const component = record.component.deref();
|
||||
if (component) {
|
||||
// 将组件的引用属性设为 null
|
||||
(component as any)[record.propertyKey] = null;
|
||||
}
|
||||
}
|
||||
this._references.delete(entityId);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 性能考虑
|
||||
|
||||
`@EntityRef` 会带来一些性能开销:
|
||||
|
||||
- **写入开销**: 每次设置引用时需要更新 ReferenceTracker
|
||||
- **内存开销**: ReferenceTracker 需要维护引用映射表
|
||||
- **销毁开销**: 实体销毁时需要遍历所有引用并清理
|
||||
|
||||
对于大多数场景,这些开销是可以接受的。但如果有**大量实体和频繁的引用变更**,存储ID可能更高效。
|
||||
|
||||
#### 最佳实践
|
||||
|
||||
```typescript
|
||||
// 推荐:适合使用 @EntityRef 的场景 - 父子关系
|
||||
@ECSComponent('Transform')
|
||||
class Transform extends Component {
|
||||
@EntityRef()
|
||||
parent: Entity | null = null;
|
||||
|
||||
position: { x: number, y: number } = { x: 0, y: 0 };
|
||||
|
||||
// 可以直接访问父实体的组件
|
||||
getWorldPosition(): { x: number, y: number } {
|
||||
if (!this.parent) {
|
||||
return { ...this.position };
|
||||
}
|
||||
|
||||
const parentTransform = this.parent.getComponent(Transform);
|
||||
if (parentTransform) {
|
||||
const parentPos = parentTransform.getWorldPosition();
|
||||
return {
|
||||
x: parentPos.x + this.position.x,
|
||||
y: parentPos.y + this.position.y
|
||||
};
|
||||
}
|
||||
|
||||
return { ...this.position };
|
||||
}
|
||||
}
|
||||
|
||||
// 不推荐:不适合使用 @EntityRef 的场景 - 大量动态目标
|
||||
@ECSComponent('AITarget')
|
||||
class AITarget extends Component {
|
||||
@EntityRef()
|
||||
target: Entity | null = null; // 如果目标频繁变化,用ID更好
|
||||
|
||||
updateCooldown: number = 0;
|
||||
}
|
||||
|
||||
// 推荐:这种场景用ID更好
|
||||
@ECSComponent('AITarget')
|
||||
class AITargetBetter extends Component {
|
||||
targetId: number | null = null; // 存储ID
|
||||
updateCooldown: number = 0;
|
||||
}
|
||||
```
|
||||
|
||||
#### 调试支持
|
||||
|
||||
ReferenceTracker 提供了调试接口:
|
||||
|
||||
```typescript
|
||||
// 查看某个实体被哪些组件引用
|
||||
const references = scene.referenceTracker.getReferencesTo(entity.id);
|
||||
console.log(`实体 ${entity.name} 被 ${references.length} 个组件引用`);
|
||||
|
||||
// 获取完整的调试信息
|
||||
const debugInfo = scene.referenceTracker.getDebugInfo();
|
||||
console.log(debugInfo);
|
||||
```
|
||||
|
||||
#### 总结
|
||||
|
||||
- **推荐做法**: 大部分情况使用存储ID + System查找的方式
|
||||
- **EntityRef 适用场景**: 父子关系、复杂关联、组件内需要直接访问引用实体的场景
|
||||
- **核心优势**: 自动清理、防止悬空引用、代码更简洁
|
||||
- **注意事项**: 有性能开销,不适合大量动态引用的场景
|
||||
|
||||
组件是 ECS 架构的数据载体,正确设计组件能让你的游戏代码更模块化、可维护和高性能。
|
||||
619
docs/guide/entity-query.md
Normal file
619
docs/guide/entity-query.md
Normal file
@@ -0,0 +1,619 @@
|
||||
# 实体查询系统
|
||||
|
||||
实体查询是 ECS 架构的核心功能之一。本指南将介绍如何使用 Matcher 和 QuerySystem 来查询和筛选实体。
|
||||
|
||||
## 核心概念
|
||||
|
||||
### Matcher - 查询条件描述符
|
||||
|
||||
Matcher 是一个链式 API,用于描述实体查询条件。它本身不执行查询,而是作为条件传递给 EntitySystem 或 QuerySystem。
|
||||
|
||||
### QuerySystem - 查询执行引擎
|
||||
|
||||
QuerySystem 负责实际执行查询,内部使用响应式查询机制自动优化性能。
|
||||
|
||||
## 在 EntitySystem 中使用 Matcher
|
||||
|
||||
这是最常见的使用方式。EntitySystem 通过 Matcher 自动筛选和处理符合条件的实体。
|
||||
|
||||
### 基础用法
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, Matcher, Entity, Component } from '@esengine/ecs-framework';
|
||||
|
||||
class PositionComponent extends Component {
|
||||
public x: number = 0;
|
||||
public y: number = 0;
|
||||
}
|
||||
|
||||
class VelocityComponent extends Component {
|
||||
public vx: number = 0;
|
||||
public vy: number = 0;
|
||||
}
|
||||
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// 方式1: 使用 Matcher.empty().all()
|
||||
super(Matcher.empty().all(PositionComponent, VelocityComponent));
|
||||
|
||||
// 方式2: 直接使用 Matcher.all() (等价)
|
||||
// super(Matcher.all(PositionComponent, VelocityComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const pos = entity.getComponent(PositionComponent)!;
|
||||
const vel = entity.getComponent(VelocityComponent)!;
|
||||
|
||||
pos.x += vel.vx;
|
||||
pos.y += vel.vy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加到场景
|
||||
scene.addEntityProcessor(new MovementSystem());
|
||||
```
|
||||
|
||||
### Matcher 链式 API
|
||||
|
||||
#### all() - 必须包含所有组件
|
||||
|
||||
```typescript
|
||||
class HealthSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// 实体必须同时拥有 Health 和 Position 组件
|
||||
super(Matcher.empty().all(HealthComponent, PositionComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 只处理同时拥有两个组件的实体
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### any() - 至少包含一个组件
|
||||
|
||||
```typescript
|
||||
class DamageableSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// 实体至少拥有 Health 或 Shield 其中之一
|
||||
super(Matcher.any(HealthComponent, ShieldComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 处理拥有生命值或护盾的实体
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### none() - 不能包含指定组件
|
||||
|
||||
```typescript
|
||||
class AliveEntitySystem extends EntitySystem {
|
||||
constructor() {
|
||||
// 实体不能拥有 DeadTag 组件
|
||||
super(Matcher.all(HealthComponent).none(DeadTag));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 只处理活着的实体
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 组合条件
|
||||
|
||||
```typescript
|
||||
class CombatSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(
|
||||
Matcher.empty()
|
||||
.all(PositionComponent, HealthComponent) // 必须有位置和生命
|
||||
.any(WeaponComponent, MagicComponent) // 至少有武器或魔法
|
||||
.none(DeadTag, FrozenTag) // 不能是死亡或冰冻状态
|
||||
);
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 处理可以战斗的活着的实体
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 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
|
||||
class PlayerSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// 查询特定标签的实体
|
||||
super(Matcher.empty().withTag(Tags.PLAYER));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 只处理玩家实体
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 按名称查询
|
||||
|
||||
```typescript
|
||||
class BossSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// 查询特定名称的实体
|
||||
super(Matcher.empty().withName('Boss'));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 只处理名为 'Boss' 的实体
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 直接使用 QuerySystem
|
||||
|
||||
如果不需要创建系统,可以直接使用 Scene 的 querySystem 进行查询。
|
||||
|
||||
### 基础查询方法
|
||||
|
||||
```typescript
|
||||
// 获取场景的查询系统
|
||||
const querySystem = scene.querySystem;
|
||||
|
||||
// 查询拥有所有指定组件的实体
|
||||
const result1 = querySystem.queryAll(PositionComponent, VelocityComponent);
|
||||
console.log(`找到 ${result1.count} 个移动实体`);
|
||||
console.log(`查询耗时: ${result1.executionTime.toFixed(2)}ms`);
|
||||
|
||||
// 查询拥有任意指定组件的实体
|
||||
const result2 = querySystem.queryAny(WeaponComponent, MagicComponent);
|
||||
console.log(`找到 ${result2.count} 个战斗单位`);
|
||||
|
||||
// 查询不包含指定组件的实体
|
||||
const result3 = querySystem.queryNone(DeadTag);
|
||||
console.log(`找到 ${result3.count} 个活着的实体`);
|
||||
```
|
||||
|
||||
### 按标签查询
|
||||
|
||||
```typescript
|
||||
const playerResult = querySystem.queryByTag(Tags.PLAYER);
|
||||
for (const player of playerResult.entities) {
|
||||
console.log('玩家:', player.name);
|
||||
}
|
||||
```
|
||||
|
||||
### 按名称查询
|
||||
|
||||
```typescript
|
||||
const bossResult = querySystem.queryByName('Boss');
|
||||
if (bossResult.count > 0) {
|
||||
const boss = bossResult.entities[0];
|
||||
console.log('找到Boss:', boss);
|
||||
}
|
||||
```
|
||||
|
||||
### 按单个组件查询
|
||||
|
||||
```typescript
|
||||
const healthResult = querySystem.queryByComponent(HealthComponent);
|
||||
console.log(`有 ${healthResult.count} 个实体拥有生命值`);
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 自动缓存
|
||||
|
||||
QuerySystem 内部使用响应式查询自动缓存结果,相同的查询条件会直接使用缓存:
|
||||
|
||||
```typescript
|
||||
// 第一次查询,执行实际查询
|
||||
const result1 = querySystem.queryAll(PositionComponent);
|
||||
console.log('fromCache:', result1.fromCache); // false
|
||||
|
||||
// 第二次相同查询,使用缓存
|
||||
const result2 = querySystem.queryAll(PositionComponent);
|
||||
console.log('fromCache:', result2.fromCache); // true
|
||||
```
|
||||
|
||||
### 实体变化自动更新
|
||||
|
||||
当实体添加/移除组件时,查询缓存会自动更新:
|
||||
|
||||
```typescript
|
||||
// 查询拥有武器的实体
|
||||
const before = querySystem.queryAll(WeaponComponent);
|
||||
console.log('之前:', before.count); // 假设为 5
|
||||
|
||||
// 给实体添加武器
|
||||
const enemy = scene.createEntity('Enemy');
|
||||
enemy.addComponent(new WeaponComponent());
|
||||
|
||||
// 再次查询,自动包含新实体
|
||||
const after = querySystem.queryAll(WeaponComponent);
|
||||
console.log('之后:', after.count); // 现在是 6
|
||||
```
|
||||
|
||||
### 查询性能统计
|
||||
|
||||
```typescript
|
||||
const stats = querySystem.getStats();
|
||||
console.log('总查询次数:', stats.queryStats.totalQueries);
|
||||
console.log('缓存命中率:', stats.queryStats.cacheHitRate);
|
||||
console.log('缓存大小:', stats.cacheStats.size);
|
||||
```
|
||||
|
||||
## 实际应用场景
|
||||
|
||||
### 场景1: 物理系统
|
||||
|
||||
```typescript
|
||||
class PhysicsSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(TransformComponent, RigidbodyComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const transform = entity.getComponent(TransformComponent)!;
|
||||
const rigidbody = entity.getComponent(RigidbodyComponent)!;
|
||||
|
||||
// 应用重力
|
||||
rigidbody.velocity.y -= 9.8 * Time.deltaTime;
|
||||
|
||||
// 更新位置
|
||||
transform.position.x += rigidbody.velocity.x * Time.deltaTime;
|
||||
transform.position.y += rigidbody.velocity.y * Time.deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 场景2: 渲染系统
|
||||
|
||||
```typescript
|
||||
class RenderSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(
|
||||
Matcher.empty()
|
||||
.all(TransformComponent, SpriteComponent)
|
||||
.none(InvisibleTag) // 排除不可见实体
|
||||
);
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 按 z-order 排序
|
||||
const sorted = entities.slice().sort((a, b) => {
|
||||
const zA = a.getComponent(TransformComponent)!.z;
|
||||
const zB = b.getComponent(TransformComponent)!.z;
|
||||
return zA - zB;
|
||||
});
|
||||
|
||||
// 渲染实体
|
||||
for (const entity of sorted) {
|
||||
const transform = entity.getComponent(TransformComponent)!;
|
||||
const sprite = entity.getComponent(SpriteComponent)!;
|
||||
|
||||
renderer.drawSprite(sprite.texture, transform.position);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 场景3: 碰撞检测
|
||||
|
||||
```typescript
|
||||
class CollisionSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(TransformComponent, ColliderComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 简单的 O(n²) 碰撞检测
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
for (let j = i + 1; j < entities.length; j++) {
|
||||
this.checkCollision(entities[i], entities[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private checkCollision(a: Entity, b: Entity): void {
|
||||
const transA = a.getComponent(TransformComponent)!;
|
||||
const transB = b.getComponent(TransformComponent)!;
|
||||
const colliderA = a.getComponent(ColliderComponent)!;
|
||||
const colliderB = b.getComponent(ColliderComponent)!;
|
||||
|
||||
if (this.isOverlapping(transA, colliderA, transB, colliderB)) {
|
||||
// 触发碰撞事件
|
||||
scene.eventSystem.emit('collision', { entityA: a, entityB: b });
|
||||
}
|
||||
}
|
||||
|
||||
private isOverlapping(...args: any[]): boolean {
|
||||
// 碰撞检测逻辑
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 场景4: 一次性查询
|
||||
|
||||
```typescript
|
||||
// 在系统外部执行一次性查询
|
||||
class GameManager {
|
||||
private scene: Scene;
|
||||
|
||||
public countEnemies(): number {
|
||||
const result = this.scene.querySystem.queryByTag(Tags.ENEMY);
|
||||
return result.count;
|
||||
}
|
||||
|
||||
public findNearestEnemy(playerPos: Vector2): Entity | null {
|
||||
const enemies = this.scene.querySystem.queryByTag(Tags.ENEMY);
|
||||
|
||||
let nearest: Entity | null = null;
|
||||
let minDistance = Infinity;
|
||||
|
||||
for (const enemy of enemies.entities) {
|
||||
const transform = enemy.getComponent(TransformComponent);
|
||||
if (!transform) continue;
|
||||
|
||||
const distance = Vector2.distance(playerPos, transform.position);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
nearest = enemy;
|
||||
}
|
||||
}
|
||||
|
||||
return nearest;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 优先使用 EntitySystem
|
||||
|
||||
```typescript
|
||||
// 推荐: 使用 EntitySystem
|
||||
class GoodSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(HealthComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 自动获得符合条件的实体,每帧自动更新
|
||||
}
|
||||
}
|
||||
|
||||
// 不推荐: 在 update 中手动查询
|
||||
class BadSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty());
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 每帧手动查询,浪费性能
|
||||
const result = this.scene!.querySystem.queryAll(HealthComponent);
|
||||
for (const entity of result.entities) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 合理使用 none() 排除条件
|
||||
|
||||
```typescript
|
||||
// 排除已死亡的敌人
|
||||
class EnemyAISystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(
|
||||
Matcher.empty()
|
||||
.all(EnemyTag, AIComponent)
|
||||
.none(DeadTag) // 不处理死亡的敌人
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 使用标签优化查询
|
||||
|
||||
```typescript
|
||||
// 不好: 查询所有实体再过滤
|
||||
const allEntities = scene.querySystem.getAllEntities();
|
||||
const players = allEntities.filter(e => e.hasComponent(PlayerTag));
|
||||
|
||||
// 好: 直接按标签查询
|
||||
const players = scene.querySystem.queryByTag(Tags.PLAYER).entities;
|
||||
```
|
||||
|
||||
### 4. 避免过于复杂的查询条件
|
||||
|
||||
```typescript
|
||||
// 不推荐: 过于复杂
|
||||
super(
|
||||
Matcher.empty()
|
||||
.all(A, B, C, D)
|
||||
.any(E, F, G)
|
||||
.none(H, I, J)
|
||||
);
|
||||
|
||||
// 推荐: 拆分成多个简单系统
|
||||
class SystemAB extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(A, B));
|
||||
}
|
||||
}
|
||||
|
||||
class SystemCD extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(C, D));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
### 1. 查询结果是只读的
|
||||
|
||||
```typescript
|
||||
const result = querySystem.queryAll(PositionComponent);
|
||||
|
||||
// 不要修改返回的数组
|
||||
result.entities.push(someEntity); // 错误!
|
||||
|
||||
// 如果需要修改,先复制
|
||||
const mutableArray = [...result.entities];
|
||||
mutableArray.push(someEntity); // 正确
|
||||
```
|
||||
|
||||
### 2. 组件添加/移除后的查询时机
|
||||
|
||||
```typescript
|
||||
// 创建实体并添加组件
|
||||
const entity = scene.createEntity('Player');
|
||||
entity.addComponent(new PositionComponent());
|
||||
|
||||
// 立即查询可能获取到新实体
|
||||
const result = scene.querySystem.queryAll(PositionComponent);
|
||||
// result.entities 包含新创建的实体
|
||||
```
|
||||
|
||||
### 3. Matcher 是不可变的
|
||||
|
||||
```typescript
|
||||
const matcher = Matcher.empty().all(PositionComponent);
|
||||
|
||||
// 链式调用返回新的 Matcher 实例
|
||||
const matcher2 = matcher.any(VelocityComponent);
|
||||
|
||||
// matcher 本身不变
|
||||
console.log(matcher === matcher2); // false
|
||||
```
|
||||
|
||||
## 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 参考
|
||||
- [QuerySystem](../api/classes/QuerySystem.md) - 查询系统 API 参考
|
||||
- [EntitySystem](../api/classes/EntitySystem.md) - 实体系统 API 参考
|
||||
- [Entity](../api/classes/Entity.md) - 实体 API 参考
|
||||
300
docs/guide/entity.md
Normal file
300
docs/guide/entity.md
Normal file
@@ -0,0 +1,300 @@
|
||||
# 实体类
|
||||
|
||||
在 ECS 架构中,实体(Entity)是游戏世界中的基本对象。实体本身不包含游戏逻辑或数据,它只是一个容器,用来组合不同的组件来实现各种功能。
|
||||
|
||||
## 基本概念
|
||||
|
||||
实体是一个轻量级的对象,主要用于:
|
||||
- 作为组件的容器
|
||||
- 提供唯一标识(ID)
|
||||
- 管理组件的生命周期
|
||||
|
||||
::: tip 关于父子层级关系
|
||||
实体间的父子层级关系通过 `HierarchyComponent` 和 `HierarchySystem` 管理,而非 Entity 内置属性。这种设计遵循 ECS 组合原则 —— 只有需要层级关系的实体才添加此组件。
|
||||
|
||||
详见 [层级系统](./hierarchy.md) 文档。
|
||||
:::
|
||||
|
||||
## 创建实体
|
||||
|
||||
**重要提示:实体必须通过场景创建,不支持手动创建!**
|
||||
|
||||
实体必须通过场景的 `createEntity()` 方法来创建,这样才能确保:
|
||||
- 实体被正确添加到场景的实体管理系统中
|
||||
- 实体被添加到查询系统中,供系统使用
|
||||
- 实体获得正确的场景引用
|
||||
- 触发相关的生命周期事件
|
||||
|
||||
```typescript
|
||||
// 正确的方式:通过场景创建实体
|
||||
const player = scene.createEntity("Player");
|
||||
|
||||
// ❌ 错误的方式:手动创建实体
|
||||
// const entity = new Entity("MyEntity", 1); // 这样创建的实体系统无法管理
|
||||
```
|
||||
|
||||
## 添加组件
|
||||
|
||||
实体通过添加组件来获得功能:
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent } from '@esengine/ecs-framework';
|
||||
|
||||
// 定义位置组件
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
x: number = 0;
|
||||
y: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
|
||||
// 定义健康组件
|
||||
@ECSComponent('Health')
|
||||
class Health extends Component {
|
||||
current: number = 100;
|
||||
max: number = 100;
|
||||
|
||||
constructor(max: number = 100) {
|
||||
super();
|
||||
this.max = max;
|
||||
this.current = max;
|
||||
}
|
||||
}
|
||||
|
||||
// 给实体添加组件
|
||||
const player = scene.createEntity("Player");
|
||||
player.addComponent(new Position(100, 200));
|
||||
player.addComponent(new Health(150));
|
||||
```
|
||||
|
||||
## 获取组件
|
||||
|
||||
```typescript
|
||||
// 获取组件(传入组件类,不是实例)
|
||||
const position = player.getComponent(Position); // 返回 Position | null
|
||||
const health = player.getComponent(Health); // 返回 Health | null
|
||||
|
||||
// 检查组件是否存在
|
||||
if (position) {
|
||||
console.log(`玩家位置: x=${position.x}, y=${position.y}`);
|
||||
}
|
||||
|
||||
// 检查是否有某个组件
|
||||
if (player.hasComponent(Position)) {
|
||||
console.log("玩家有位置组件");
|
||||
}
|
||||
|
||||
// 获取所有组件实例(只读属性)
|
||||
const allComponents = player.components; // readonly Component[]
|
||||
|
||||
// 获取指定类型的所有组件(支持同类型多组件)
|
||||
const allHealthComponents = player.getComponents(Health); // Health[]
|
||||
|
||||
// 获取或创建组件(如果不存在则自动创建)
|
||||
const position = player.getOrCreateComponent(Position, 0, 0); // 传入构造参数
|
||||
const health = player.getOrCreateComponent(Health, 100); // 如果存在则返回现有的,不存在则创建新的
|
||||
```
|
||||
|
||||
## 移除组件
|
||||
|
||||
```typescript
|
||||
// 方式1:通过组件类型移除
|
||||
const removedHealth = player.removeComponentByType(Health);
|
||||
if (removedHealth) {
|
||||
console.log("健康组件已被移除");
|
||||
}
|
||||
|
||||
// 方式2:通过组件实例移除
|
||||
const healthComponent = player.getComponent(Health);
|
||||
if (healthComponent) {
|
||||
player.removeComponent(healthComponent);
|
||||
}
|
||||
|
||||
// 批量移除多种组件类型
|
||||
const removedComponents = player.removeComponentsByTypes([Position, Health]);
|
||||
|
||||
// 检查组件是否被移除
|
||||
if (!player.hasComponent(Health)) {
|
||||
console.log("健康组件已被移除");
|
||||
}
|
||||
```
|
||||
|
||||
## 实体查找
|
||||
|
||||
场景提供了多种方式来查找实体:
|
||||
|
||||
### 通过名称查找
|
||||
|
||||
```typescript
|
||||
// 查找单个实体
|
||||
const player = scene.findEntity("Player");
|
||||
// 或使用别名方法
|
||||
const player2 = scene.getEntityByName("Player");
|
||||
|
||||
if (player) {
|
||||
console.log("找到玩家实体");
|
||||
}
|
||||
```
|
||||
|
||||
### 通过 ID 查找
|
||||
|
||||
```typescript
|
||||
// 通过实体 ID 查找
|
||||
const entity = scene.findEntityById(123);
|
||||
```
|
||||
|
||||
### 通过标签查找
|
||||
|
||||
实体支持标签系统,用于快速分类和查找:
|
||||
|
||||
```typescript
|
||||
// 设置标签
|
||||
player.tag = 1; // 玩家标签
|
||||
enemy.tag = 2; // 敌人标签
|
||||
|
||||
// 通过标签查找所有相关实体
|
||||
const players = scene.findEntitiesByTag(1);
|
||||
const enemies = scene.findEntitiesByTag(2);
|
||||
// 或使用别名方法
|
||||
const allPlayers = scene.getEntitiesByTag(1);
|
||||
```
|
||||
|
||||
|
||||
## 实体生命周期
|
||||
|
||||
```typescript
|
||||
// 销毁实体
|
||||
player.destroy();
|
||||
|
||||
// 检查实体是否已销毁
|
||||
if (player.isDestroyed) {
|
||||
console.log("实体已被销毁");
|
||||
}
|
||||
```
|
||||
|
||||
## 实体事件
|
||||
|
||||
实体的组件变化会触发事件:
|
||||
|
||||
```typescript
|
||||
// 监听组件添加事件
|
||||
scene.eventSystem.on('component:added', (data) => {
|
||||
console.log('组件已添加:', data);
|
||||
});
|
||||
|
||||
// 监听实体创建事件
|
||||
scene.eventSystem.on('entity:created', (data) => {
|
||||
console.log('实体已创建:', data.entityName);
|
||||
});
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
|
||||
### 批量创建实体
|
||||
|
||||
框架提供了高性能的批量创建方法:
|
||||
|
||||
```typescript
|
||||
// 批量创建 100 个子弹实体(高性能版本)
|
||||
const bullets = scene.createEntities(100, "Bullet");
|
||||
|
||||
// 为每个子弹添加组件
|
||||
bullets.forEach((bullet, index) => {
|
||||
bullet.addComponent(new Position(Math.random() * 800, Math.random() * 600));
|
||||
bullet.addComponent(new Velocity(Math.random() * 100 - 50, Math.random() * 100 - 50));
|
||||
});
|
||||
```
|
||||
|
||||
`createEntities()` 方法会:
|
||||
- 批量分配实体 ID
|
||||
- 批量添加到实体列表
|
||||
- 优化查询系统更新
|
||||
- 减少系统缓存清理次数
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 合理的组件粒度
|
||||
|
||||
```typescript
|
||||
// 好的做法:功能单一的组件
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
x: number = 0;
|
||||
y: number = 0;
|
||||
}
|
||||
|
||||
@ECSComponent('Velocity')
|
||||
class Velocity extends Component {
|
||||
dx: number = 0;
|
||||
dy: number = 0;
|
||||
}
|
||||
|
||||
// 避免:功能过于复杂的组件
|
||||
@ECSComponent('Player')
|
||||
class Player extends Component {
|
||||
// 避免在一个组件中包含太多不相关的属性
|
||||
x: number;
|
||||
y: number;
|
||||
health: number;
|
||||
inventory: Item[];
|
||||
skills: Skill[];
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 使用装饰器
|
||||
|
||||
始终使用 `@ECSComponent` 装饰器:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Transform')
|
||||
class Transform extends Component {
|
||||
// 组件实现
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 合理命名
|
||||
|
||||
```typescript
|
||||
// 清晰的实体命名
|
||||
const mainCharacter = scene.createEntity("MainCharacter");
|
||||
const enemy1 = scene.createEntity("Goblin_001");
|
||||
const collectible = scene.createEntity("HealthPotion");
|
||||
```
|
||||
|
||||
### 4. 及时清理
|
||||
|
||||
```typescript
|
||||
// 不再需要的实体应该及时销毁
|
||||
if (enemy.getComponent(Health).current <= 0) {
|
||||
enemy.destroy();
|
||||
}
|
||||
```
|
||||
|
||||
## 调试实体
|
||||
|
||||
框架提供了调试功能来帮助开发:
|
||||
|
||||
```typescript
|
||||
// 获取实体调试信息
|
||||
const debugInfo = entity.getDebugInfo();
|
||||
console.log('实体信息:', debugInfo);
|
||||
|
||||
// 列出实体的所有组件
|
||||
entity.components.forEach(component => {
|
||||
console.log('组件:', component.constructor.name);
|
||||
});
|
||||
```
|
||||
|
||||
实体是 ECS 架构的核心概念之一,理解如何正确使用实体将帮助你构建高效、可维护的游戏代码。
|
||||
|
||||
## 下一步
|
||||
|
||||
- 了解 [层级系统](./hierarchy.md) 建立实体间的父子关系
|
||||
- 了解 [组件系统](./component.md) 为实体添加功能
|
||||
- 了解 [场景管理](./scene.md) 组织和管理实体
|
||||
@@ -1,6 +1,4 @@
|
||||
---
|
||||
title: "事件系统"
|
||||
---
|
||||
# 事件系统
|
||||
|
||||
ECS 框架内置了强大的类型安全事件系统,支持同步/异步事件、优先级、批处理等高级功能。事件系统是实现组件间通信、系统间协作的核心机制。
|
||||
|
||||
@@ -1,30 +1,10 @@
|
||||
---
|
||||
title: 快速开始
|
||||
description: 5 分钟上手 ESEngine ECS 框架
|
||||
---
|
||||
# 快速开始
|
||||
|
||||
本指南将帮助你快速上手 ECS Framework,从安装到创建第一个 ECS 应用。
|
||||
|
||||
## 安装
|
||||
|
||||
### 使用 CLI(推荐)
|
||||
|
||||
在现有项目中添加 ECS 的最简单方式:
|
||||
|
||||
```bash
|
||||
# 在项目目录中运行
|
||||
npx @esengine/cli init
|
||||
```
|
||||
|
||||
CLI 会自动检测项目类型(Cocos Creator 2.x/3.x、LayaAir 3.x 或 Node.js)并生成相应的集成代码,包括:
|
||||
|
||||
- `ECSManager` 组件/脚本 - 负责 ECS 生命周期管理
|
||||
- 示例组件和系统 - 帮助快速上手
|
||||
- 自动安装依赖
|
||||
|
||||
### NPM 手动安装
|
||||
|
||||
如果你更喜欢手动配置,可以直接安装:
|
||||
### NPM 安装
|
||||
|
||||
```bash
|
||||
# 使用 npm
|
||||
@@ -353,39 +333,27 @@ function gameLoop(deltaTime: number) {
|
||||
|
||||
## 与游戏引擎集成
|
||||
|
||||
### Laya 3.x 引擎集成
|
||||
|
||||
推荐使用 `Laya.Script` 组件来管理 ECS 生命周期:
|
||||
### Laya 引擎集成
|
||||
|
||||
```typescript
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
import { Stage } from "laya/display/Stage";
|
||||
import { Laya } from "Laya";
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
const { regClass } = Laya;
|
||||
// 初始化 Laya
|
||||
Laya.init(800, 600).then(() => {
|
||||
// 初始化 ECS
|
||||
Core.create(true);
|
||||
Core.setScene(new GameScene());
|
||||
|
||||
@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.timer.frameLoop(1, this, () => {
|
||||
const deltaTime = Laya.timer.delta / 1000;
|
||||
Core.update(deltaTime); // 自动更新全局服务和场景
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
在 Laya IDE 中,将 `ECSManager` 脚本挂载到场景中的节点上即可。
|
||||
|
||||
### Cocos Creator 集成
|
||||
|
||||
```typescript
|
||||
@@ -1,6 +1,4 @@
|
||||
---
|
||||
title: "层级系统"
|
||||
---
|
||||
# 层级系统
|
||||
|
||||
在游戏开发中,实体间的父子层级关系是常见需求。ECS Framework 采用组件化方式管理层级关系,通过 `HierarchyComponent` 和 `HierarchySystem` 实现,完全遵循 ECS 组合原则。
|
||||
|
||||
@@ -8,7 +6,7 @@ title: "层级系统"
|
||||
|
||||
### 为什么不在 Entity 中内置层级?
|
||||
|
||||
传统的游戏对象模型将层级关系内置于实体中。ECS Framework 选择组件化方案的原因:
|
||||
传统的游戏对象模型(如 Unity 的 GameObject)将层级关系内置于实体中。ECS Framework 选择组件化方案的原因:
|
||||
|
||||
1. **ECS 组合原则**:层级是一种"功能",应该通过组件添加,而非所有实体都具备
|
||||
2. **按需使用**:只有需要层级关系的实体才添加 `HierarchyComponent`
|
||||
@@ -434,6 +432,6 @@ const found = hierarchySystem.findChild(parent, "Child");
|
||||
|
||||
## 下一步
|
||||
|
||||
- 了解 [实体类](./entity/) 的其他功能
|
||||
- 了解 [场景管理](./scene/) 如何组织实体和系统
|
||||
- 了解 [组件系统](./component/) 如何定义和使用组件
|
||||
- 了解 [实体类](./entity.md) 的其他功能
|
||||
- 了解 [场景管理](./scene.md) 如何组织实体和系统
|
||||
- 了解 [组件系统](./component.md) 如何定义和使用组件
|
||||
@@ -1,46 +1,43 @@
|
||||
---
|
||||
title: 指南
|
||||
description: ESEngine ECS 框架完整指南
|
||||
---
|
||||
# 指南
|
||||
|
||||
欢迎使用 ECS Framework 指南。这里将详细介绍框架的各个核心概念和使用方法。
|
||||
|
||||
## 核心概念
|
||||
|
||||
### [实体类 (Entity)](./entity/)
|
||||
### [实体类 (Entity)](./entity.md)
|
||||
了解 ECS 架构的基础 - 实体类的使用方法、生命周期管理和最佳实践。
|
||||
|
||||
### [组件系统 (Component)](./component/)
|
||||
### [组件系统 (Component)](./component.md)
|
||||
学习如何创建和使用组件,实现游戏功能的模块化设计。
|
||||
|
||||
### [系统架构 (System)](./system/)
|
||||
### [系统架构 (System)](./system.md)
|
||||
掌握系统的编写方法,实现游戏逻辑的处理。
|
||||
|
||||
### [实体查询与 Matcher](./entity-query/)
|
||||
### [实体查询与 Matcher](./entity-query.md)
|
||||
学习使用 Matcher 进行实体筛选和查询,掌握 `all`、`any`、`none`、`nothing` 等匹配条件。
|
||||
|
||||
### [场景管理 (Scene)](./scene/)
|
||||
### [场景管理 (Scene)](./scene.md)
|
||||
了解场景的生命周期、系统管理和实体容器功能。
|
||||
|
||||
### [事件系统 (Event)](./event-system/)
|
||||
### [事件系统 (Event)](./event-system.md)
|
||||
掌握类型安全的事件系统,实现组件间通信和系统协作。
|
||||
|
||||
### [序列化系统 (Serialization)](./serialization/)
|
||||
### [序列化系统 (Serialization)](./serialization.md)
|
||||
掌握场景、实体和组件的序列化方案,支持全量序列化和增量序列化,实现游戏存档、网络同步等功能。
|
||||
|
||||
### [时间和定时器 (Time)](./time-and-timers/)
|
||||
### [时间和定时器 (Time)](./time-and-timers.md)
|
||||
学习时间管理和定时器系统,实现游戏逻辑的精确时间控制。
|
||||
|
||||
### [日志系统 (Logger)](./logging/)
|
||||
### [日志系统 (Logger)](./logging.md)
|
||||
掌握分级日志系统,用于调试、监控和错误追踪。
|
||||
|
||||
### [平台适配器 (Platform Adapter)](./platform-adapter/)
|
||||
### [平台适配器 (Platform Adapter)](./platform-adapter.md)
|
||||
了解如何为不同平台实现和注册平台适配器,支持浏览器、小游戏、Node.js等环境。
|
||||
|
||||
## 高级特性
|
||||
|
||||
### [服务容器 (Service Container)](./service-container/)
|
||||
### [服务容器 (Service Container)](./service-container.md)
|
||||
掌握依赖注入和服务管理,实现松耦合的架构设计。
|
||||
|
||||
### [插件系统 (Plugin System)](./plugin-system/)
|
||||
### [插件系统 (Plugin System)](./plugin-system.md)
|
||||
学习如何开发和使用插件,扩展框架功能,实现功能模块化。
|
||||
@@ -1,6 +1,4 @@
|
||||
---
|
||||
title: "日志系统"
|
||||
---
|
||||
# 日志系统
|
||||
|
||||
ECS 框架提供了功能强大的分级日志系统,支持多种日志级别、颜色输出、自定义前缀和灵活的配置选项。日志系统可以帮助开发者调试代码和监控应用运行状态。
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
---
|
||||
title: "平台适配器"
|
||||
---
|
||||
# 平台适配器
|
||||
|
||||
## 概述
|
||||
|
||||
@@ -16,7 +14,7 @@ ECS框架提供了平台适配器接口,允许用户为不同的运行环境
|
||||
|
||||
## 支持的平台
|
||||
|
||||
### 🌐 [浏览器适配器](./platform-adapter/browser/)
|
||||
### 🌐 [浏览器适配器](./platform-adapter/browser.md)
|
||||
|
||||
支持所有现代浏览器环境,包括 Chrome、Firefox、Safari、Edge 等。
|
||||
|
||||
@@ -30,7 +28,7 @@ ECS框架提供了平台适配器接口,允许用户为不同的运行环境
|
||||
|
||||
---
|
||||
|
||||
### 📱 [微信小游戏适配器](./platform-adapter/wechat-minigame/)
|
||||
### 📱 [微信小游戏适配器](./platform-adapter/wechat-minigame.md)
|
||||
|
||||
专为微信小游戏环境设计,处理微信小游戏的特殊限制和API。
|
||||
|
||||
@@ -44,7 +42,7 @@ ECS框架提供了平台适配器接口,允许用户为不同的运行环境
|
||||
|
||||
---
|
||||
|
||||
### 🖥️ [Node.js适配器](./platform-adapter/nodejs/)
|
||||
### 🖥️ [Node.js适配器](./platform-adapter/nodejs.md)
|
||||
|
||||
为 Node.js 服务器环境提供支持,适用于游戏服务器和计算服务器。
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
---
|
||||
title: "浏览器适配器"
|
||||
---
|
||||
# 浏览器适配器
|
||||
|
||||
## 概述
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
---
|
||||
title: "Node.js 适配器"
|
||||
---
|
||||
# Node.js 适配器
|
||||
|
||||
## 概述
|
||||
|
||||
677
docs/guide/platform-adapter/wechat-minigame.md
Normal file
677
docs/guide/platform-adapter/wechat-minigame.md
Normal file
@@ -0,0 +1,677 @@
|
||||
# 微信小游戏适配器
|
||||
|
||||
## 概述
|
||||
|
||||
微信小游戏平台适配器专为微信小游戏环境设计,处理微信小游戏的特殊限制和API。
|
||||
|
||||
## 特性支持
|
||||
|
||||
- ✅ **Worker**: 支持(通过 `wx.createWorker` 创建,需要配置 game.json)
|
||||
- ❌ **SharedArrayBuffer**: 不支持
|
||||
- ❌ **Transferable Objects**: 不支持(只支持可序列化对象)
|
||||
- ✅ **高精度时间**: 使用 `Date.now()` 或 `wx.getPerformance()`
|
||||
- ✅ **设备信息**: 完整的微信小游戏设备信息
|
||||
|
||||
## 完整实现
|
||||
|
||||
```typescript
|
||||
import type {
|
||||
IPlatformAdapter,
|
||||
PlatformWorker,
|
||||
WorkerCreationOptions,
|
||||
PlatformConfig,
|
||||
WeChatDeviceInfo
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* 微信小游戏平台适配器
|
||||
* 支持微信小游戏环境
|
||||
*/
|
||||
export class WeChatMiniGameAdapter implements IPlatformAdapter {
|
||||
public readonly name = 'wechat-minigame';
|
||||
public readonly version: string;
|
||||
private systemInfo: any;
|
||||
|
||||
constructor() {
|
||||
// 获取微信小游戏版本信息
|
||||
this.systemInfo = this.getSystemInfo();
|
||||
this.version = this.systemInfo.version || 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否支持Worker
|
||||
*/
|
||||
public isWorkerSupported(): boolean {
|
||||
// 微信小游戏支持Worker,通过wx.createWorker创建
|
||||
return typeof wx !== 'undefined' && typeof wx.createWorker === 'function';
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否支持SharedArrayBuffer(不支持)
|
||||
*/
|
||||
public isSharedArrayBufferSupported(): boolean {
|
||||
return false; // 微信小游戏不支持SharedArrayBuffer
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取硬件并发数
|
||||
*/
|
||||
public getHardwareConcurrency(): number {
|
||||
// 微信小游戏官方限制:最多只能创建 1 个 Worker
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建Worker
|
||||
* @param script 脚本内容或文件路径
|
||||
* @param options Worker创建选项
|
||||
*/
|
||||
public createWorker(script: string, options: WorkerCreationOptions = {}): PlatformWorker {
|
||||
if (!this.isWorkerSupported()) {
|
||||
throw new Error('微信小游戏不支持Worker');
|
||||
}
|
||||
|
||||
try {
|
||||
return new WeChatWorker(script, options);
|
||||
} catch (error) {
|
||||
throw new Error(`创建微信Worker失败: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建SharedArrayBuffer(不支持)
|
||||
*/
|
||||
public createSharedArrayBuffer(length: number): SharedArrayBuffer | null {
|
||||
return null; // 微信小游戏不支持SharedArrayBuffer
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取高精度时间戳
|
||||
*/
|
||||
public getHighResTimestamp(): number {
|
||||
// 尝试使用微信的性能API,否则使用Date.now()
|
||||
if (typeof wx !== 'undefined' && wx.getPerformance) {
|
||||
const performance = wx.getPerformance();
|
||||
return performance.now();
|
||||
}
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取平台配置
|
||||
*/
|
||||
public getPlatformConfig(): PlatformConfig {
|
||||
return {
|
||||
maxWorkerCount: 1, // 微信小游戏最多支持 1 个 Worker
|
||||
supportsModuleWorker: false, // 不支持模块Worker
|
||||
supportsTransferableObjects: this.checkTransferableObjectsSupport(),
|
||||
maxSharedArrayBufferSize: 0,
|
||||
workerScriptPrefix: '',
|
||||
limitations: {
|
||||
noEval: true, // 微信小游戏限制eval使用
|
||||
requiresWorkerInit: false,
|
||||
memoryLimit: this.getMemoryLimit(),
|
||||
workerNotSupported: false,
|
||||
workerLimitations: [
|
||||
'最多只能创建 1 个 Worker',
|
||||
'创建新Worker前必须先调用 Worker.terminate()',
|
||||
'Worker脚本必须为项目内相对路径',
|
||||
'需要在 game.json 中配置 workers 路径',
|
||||
'使用 worker.onMessage() 而不是 self.onmessage',
|
||||
'需要基础库 1.9.90 及以上版本'
|
||||
]
|
||||
},
|
||||
extensions: {
|
||||
platform: 'wechat-minigame',
|
||||
systemInfo: this.systemInfo,
|
||||
appId: this.systemInfo.host?.appId || 'unknown'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取微信小游戏设备信息
|
||||
*/
|
||||
public getDeviceInfo(): WeChatDeviceInfo {
|
||||
return {
|
||||
// 设备基础信息
|
||||
brand: this.systemInfo.brand,
|
||||
model: this.systemInfo.model,
|
||||
platform: this.systemInfo.platform,
|
||||
system: this.systemInfo.system,
|
||||
benchmarkLevel: this.systemInfo.benchmarkLevel,
|
||||
cpuType: this.systemInfo.cpuType,
|
||||
memorySize: this.systemInfo.memorySize,
|
||||
deviceAbi: this.systemInfo.deviceAbi,
|
||||
abi: this.systemInfo.abi,
|
||||
|
||||
// 窗口信息
|
||||
screenWidth: this.systemInfo.screenWidth,
|
||||
screenHeight: this.systemInfo.screenHeight,
|
||||
screenTop: this.systemInfo.screenTop,
|
||||
windowWidth: this.systemInfo.windowWidth,
|
||||
windowHeight: this.systemInfo.windowHeight,
|
||||
pixelRatio: this.systemInfo.pixelRatio,
|
||||
statusBarHeight: this.systemInfo.statusBarHeight,
|
||||
safeArea: this.systemInfo.safeArea,
|
||||
|
||||
// 应用信息
|
||||
version: this.systemInfo.version,
|
||||
language: this.systemInfo.language,
|
||||
theme: this.systemInfo.theme,
|
||||
SDKVersion: this.systemInfo.SDKVersion,
|
||||
enableDebug: this.systemInfo.enableDebug,
|
||||
fontSizeSetting: this.systemInfo.fontSizeSetting,
|
||||
host: this.systemInfo.host
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步获取完整的平台配置
|
||||
*/
|
||||
public async getPlatformConfigAsync(): Promise<PlatformConfig> {
|
||||
// 可以在这里添加异步获取设备性能信息的逻辑
|
||||
const baseConfig = this.getPlatformConfig();
|
||||
|
||||
// 尝试获取设备性能信息
|
||||
try {
|
||||
const benchmarkLevel = await this.getBenchmarkLevel();
|
||||
baseConfig.extensions = {
|
||||
...baseConfig.extensions,
|
||||
benchmarkLevel
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn('获取性能基准失败:', error);
|
||||
}
|
||||
|
||||
return baseConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否支持Transferable Objects
|
||||
*/
|
||||
private checkTransferableObjectsSupport(): boolean {
|
||||
// 微信小游戏不支持 Transferable Objects
|
||||
// 基础库 2.20.2 之前只支持可序列化的 key-value 对象
|
||||
// 2.20.2 之后支持任意类型数据,但仍然不支持 Transferable Objects
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统信息
|
||||
*/
|
||||
private getSystemInfo(): any {
|
||||
if (typeof wx !== 'undefined' && wx.getSystemInfoSync) {
|
||||
try {
|
||||
return wx.getSystemInfoSync();
|
||||
} catch (error) {
|
||||
console.warn('获取微信系统信息失败:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取内存限制
|
||||
*/
|
||||
private getMemoryLimit(): number {
|
||||
// 微信小游戏通常有内存限制
|
||||
const memorySize = this.systemInfo.memorySize;
|
||||
if (memorySize) {
|
||||
// 解析内存大小字符串(如 "4GB")
|
||||
const match = memorySize.match(/(\d+)([GM]B)?/i);
|
||||
if (match) {
|
||||
const value = parseInt(match[1], 10);
|
||||
const unit = match[2]?.toUpperCase();
|
||||
|
||||
if (unit === 'GB') {
|
||||
return value * 1024 * 1024 * 1024;
|
||||
} else if (unit === 'MB') {
|
||||
return value * 1024 * 1024;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 默认限制为512MB
|
||||
return 512 * 1024 * 1024;
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步获取设备性能基准
|
||||
*/
|
||||
private async getBenchmarkLevel(): Promise<number> {
|
||||
return new Promise((resolve) => {
|
||||
if (typeof wx !== 'undefined' && wx.getDeviceInfo) {
|
||||
wx.getDeviceInfo({
|
||||
success: (res: any) => {
|
||||
resolve(res.benchmarkLevel || 0);
|
||||
},
|
||||
fail: () => {
|
||||
resolve(0);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resolve(this.systemInfo.benchmarkLevel || 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信Worker封装
|
||||
*/
|
||||
class WeChatWorker implements PlatformWorker {
|
||||
private _state: 'running' | 'terminated' = 'running';
|
||||
private worker: any;
|
||||
private scriptPath: string;
|
||||
private isTemporaryFile: boolean = false;
|
||||
|
||||
constructor(script: string, options: WorkerCreationOptions = {}) {
|
||||
if (typeof wx === 'undefined' || typeof wx.createWorker !== 'function') {
|
||||
throw new Error('微信小游戏不支持Worker');
|
||||
}
|
||||
|
||||
try {
|
||||
// 判断 script 是文件路径还是脚本内容
|
||||
if (this.isFilePath(script)) {
|
||||
// 直接使用文件路径
|
||||
this.scriptPath = script;
|
||||
this.isTemporaryFile = false;
|
||||
this.worker = wx.createWorker(this.scriptPath, {
|
||||
useExperimentalWorker: true // 启用实验性Worker获得更好性能
|
||||
});
|
||||
} else {
|
||||
// 微信小游戏不支持动态脚本内容,只能使用文件路径
|
||||
// 将脚本内容写入文件系统
|
||||
this.scriptPath = this.writeScriptToFile(script, options.name);
|
||||
this.isTemporaryFile = true;
|
||||
this.worker = wx.createWorker(this.scriptPath, {
|
||||
useExperimentalWorker: true
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`创建微信Worker失败: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为文件路径
|
||||
*/
|
||||
private isFilePath(script: string): boolean {
|
||||
// 简单判断:如果包含 .js 后缀且不包含换行符或分号,认为是文件路径
|
||||
return script.endsWith('.js') &&
|
||||
!script.includes('\n') &&
|
||||
!script.includes(';') &&
|
||||
script.length < 200; // 文件路径通常不会太长
|
||||
}
|
||||
|
||||
/**
|
||||
* 将脚本内容写入文件系统
|
||||
* 注意:微信小游戏不支持blob URL,只能使用文件系统
|
||||
*/
|
||||
private writeScriptToFile(script: string, name?: string): string {
|
||||
const fs = wx.getFileSystemManager();
|
||||
const fileName = name ? `worker-${name}.js` : `worker-${Date.now()}.js`;
|
||||
const filePath = `${wx.env.USER_DATA_PATH}/${fileName}`;
|
||||
|
||||
try {
|
||||
fs.writeFileSync(filePath, script, 'utf8');
|
||||
return filePath;
|
||||
} catch (error) {
|
||||
throw new Error(`写入Worker脚本文件失败: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public get state(): 'running' | 'terminated' {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
public postMessage(message: any, transfer?: Transferable[]): void {
|
||||
if (this._state === 'terminated') {
|
||||
throw new Error('Worker已被终止');
|
||||
}
|
||||
|
||||
try {
|
||||
// 微信小游戏 Worker 只支持可序列化对象,忽略 transfer 参数
|
||||
this.worker.postMessage(message);
|
||||
} catch (error) {
|
||||
throw new Error(`发送消息到微信Worker失败: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public onMessage(handler: (event: { data: any }) => void): void {
|
||||
// 微信小游戏使用 onMessage 方法,不是 onmessage 属性
|
||||
this.worker.onMessage((res: any) => {
|
||||
handler({ data: res });
|
||||
});
|
||||
}
|
||||
|
||||
public onError(handler: (error: ErrorEvent) => void): void {
|
||||
// 注意:微信小游戏 Worker 的错误处理可能与标准不同
|
||||
if (this.worker.onError) {
|
||||
this.worker.onError(handler);
|
||||
}
|
||||
}
|
||||
|
||||
public terminate(): void {
|
||||
if (this._state === 'running') {
|
||||
try {
|
||||
this.worker.terminate();
|
||||
this._state = 'terminated';
|
||||
|
||||
// 清理临时脚本文件
|
||||
this.cleanupScriptFile();
|
||||
} catch (error) {
|
||||
console.error('终止微信Worker失败:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理临时脚本文件
|
||||
*/
|
||||
private cleanupScriptFile(): void {
|
||||
// 只清理临时创建的文件,不清理用户提供的文件路径
|
||||
if (this.scriptPath && this.isTemporaryFile) {
|
||||
try {
|
||||
const fs = wx.getFileSystemManager();
|
||||
fs.unlinkSync(this.scriptPath);
|
||||
} catch (error) {
|
||||
console.warn('清理Worker脚本文件失败:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 复制代码
|
||||
|
||||
将上述代码复制到你的项目中,例如 `src/platform/WeChatMiniGameAdapter.ts`。
|
||||
|
||||
### 2. 注册适配器
|
||||
|
||||
```typescript
|
||||
import { PlatformManager } from '@esengine/ecs-framework';
|
||||
import { WeChatMiniGameAdapter } from './platform/WeChatMiniGameAdapter';
|
||||
|
||||
// 检查是否在微信小游戏环境
|
||||
if (typeof wx !== 'undefined') {
|
||||
const wechatAdapter = new WeChatMiniGameAdapter();
|
||||
PlatformManager.getInstance().registerAdapter(wechatAdapter);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. WorkerEntitySystem 使用方式
|
||||
|
||||
微信小游戏适配器与 WorkerEntitySystem 配合使用,自动处理 Worker 脚本创建:
|
||||
|
||||
#### 基本使用方式(推荐)
|
||||
|
||||
```typescript
|
||||
import { WorkerEntitySystem, Matcher, Entity } from '@esengine/ecs-framework';
|
||||
|
||||
interface PhysicsData {
|
||||
id: number;
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
mass: number;
|
||||
}
|
||||
|
||||
class PhysicsSystem extends WorkerEntitySystem<PhysicsData> {
|
||||
constructor() {
|
||||
super(Matcher.all(Transform, Velocity), {
|
||||
enableWorker: true,
|
||||
workerCount: 1, // 微信小游戏限制只能创建1个Worker
|
||||
systemConfig: { gravity: 100, friction: 0.95 }
|
||||
});
|
||||
}
|
||||
|
||||
protected getDefaultEntityDataSize(): number {
|
||||
return 6; // id, x, y, vx, vy, mass
|
||||
}
|
||||
|
||||
protected extractEntityData(entity: Entity): PhysicsData {
|
||||
const transform = entity.getComponent(Transform);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
const physics = entity.getComponent(Physics);
|
||||
|
||||
return {
|
||||
id: entity.id,
|
||||
x: transform.x,
|
||||
y: transform.y,
|
||||
vx: velocity.x,
|
||||
vy: velocity.y,
|
||||
mass: physics.mass
|
||||
};
|
||||
}
|
||||
|
||||
// WorkerEntitySystem 会自动将此函数序列化并写入临时文件
|
||||
protected workerProcess(entities: PhysicsData[], deltaTime: number, config: any): PhysicsData[] {
|
||||
return entities.map(entity => {
|
||||
// 应用重力
|
||||
entity.vy += config.gravity * deltaTime;
|
||||
|
||||
// 更新位置
|
||||
entity.x += entity.vx * deltaTime;
|
||||
entity.y += entity.vy * deltaTime;
|
||||
|
||||
// 应用摩擦力
|
||||
entity.vx *= config.friction;
|
||||
entity.vy *= config.friction;
|
||||
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
protected applyResult(entity: Entity, result: PhysicsData): void {
|
||||
const transform = entity.getComponent(Transform);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
transform.x = result.x;
|
||||
transform.y = result.y;
|
||||
velocity.x = result.vx;
|
||||
velocity.y = result.vy;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 使用预先创建的 Worker 文件(可选)
|
||||
|
||||
如果你希望使用预先创建的 Worker 文件:
|
||||
|
||||
```typescript
|
||||
// 1. 在 game.json 中配置 Worker 路径
|
||||
/*
|
||||
{
|
||||
"workers": "workers"
|
||||
}
|
||||
*/
|
||||
|
||||
// 2. 创建 workers/physics.js 文件
|
||||
// workers/physics.js 内容:
|
||||
/*
|
||||
// 微信小游戏 Worker 使用标准的 self.onmessage
|
||||
self.onmessage = function(e) {
|
||||
const { type, id, entities, deltaTime, systemConfig } = e.data;
|
||||
|
||||
if (entities) {
|
||||
// 处理物理计算
|
||||
const results = entities.map(entity => {
|
||||
entity.vy += systemConfig.gravity * deltaTime;
|
||||
entity.x += entity.vx * deltaTime;
|
||||
entity.y += entity.vy * deltaTime;
|
||||
return entity;
|
||||
});
|
||||
|
||||
self.postMessage({ id, result: results });
|
||||
}
|
||||
};
|
||||
*/
|
||||
|
||||
// 3. 通过平台适配器直接创建(不推荐,WorkerEntitySystem会自动处理)
|
||||
const adapter = PlatformManager.getInstance().getAdapter();
|
||||
const worker = adapter.createWorker('workers/physics.js');
|
||||
```
|
||||
|
||||
### 4. 获取设备信息
|
||||
|
||||
```typescript
|
||||
const manager = PlatformManager.getInstance();
|
||||
if (manager.hasAdapter()) {
|
||||
const adapter = manager.getAdapter();
|
||||
console.log('微信设备信息:', adapter.getDeviceInfo());
|
||||
}
|
||||
```
|
||||
|
||||
## 官方文档参考
|
||||
|
||||
在使用微信小游戏 Worker 之前,建议先阅读官方文档:
|
||||
|
||||
- [wx.createWorker API](https://developers.weixin.qq.com/minigame/dev/api/worker/wx.createWorker.html)
|
||||
- [Worker.postMessage API](https://developers.weixin.qq.com/minigame/dev/api/worker/Worker.postMessage.html)
|
||||
- [Worker.onMessage API](https://developers.weixin.qq.com/minigame/dev/api/worker/Worker.onMessage.html)
|
||||
- [Worker.terminate API](https://developers.weixin.qq.com/minigame/dev/api/worker/Worker.terminate.html)
|
||||
|
||||
## 重要注意事项
|
||||
|
||||
### Worker 限制和配置
|
||||
|
||||
微信小游戏的 Worker 有以下限制:
|
||||
|
||||
- **数量限制**: 最多只能创建 1 个 Worker
|
||||
- **版本要求**: 需要基础库 1.9.90 及以上版本
|
||||
- **脚本支持**: 不支持 blob URL,只能使用文件路径或写入文件系统
|
||||
- **文件路径**: Worker 脚本路径必须为绝对路径,但不能以 "/" 开头
|
||||
- **生命周期**: 创建新 Worker 前必须先调用 `Worker.terminate()` 终止当前 Worker
|
||||
- **消息处理**: Worker 内使用标准的 `self.onmessage`,主线程使用 `worker.onMessage()`
|
||||
- **实验性功能**: 支持 `useExperimentalWorker` 选项获得更好的 iOS 性能
|
||||
|
||||
#### Worker 配置(可选)
|
||||
|
||||
如果使用预先创建的 Worker 文件,需要在 `game.json` 中添加 workers 配置:
|
||||
|
||||
```json
|
||||
{
|
||||
"deviceOrientation": "portrait",
|
||||
"showStatusBar": false,
|
||||
"workers": "workers",
|
||||
"subpackages": []
|
||||
}
|
||||
```
|
||||
|
||||
**注意**: 使用 WorkerEntitySystem 时无需此配置,框架会自动将脚本写入临时文件。
|
||||
|
||||
### 内存限制
|
||||
|
||||
微信小游戏有严格的内存限制:
|
||||
|
||||
- 通常限制在 256MB - 512MB
|
||||
- 需要及时释放不用的资源
|
||||
- 避免内存泄漏
|
||||
|
||||
### API 限制
|
||||
|
||||
- 不支持 `eval()` 函数
|
||||
- 不支持 `Function` 构造器
|
||||
- DOM API 受限
|
||||
- 文件系统 API 受限
|
||||
|
||||
## 性能优化建议
|
||||
|
||||
### 1. 分帧处理
|
||||
|
||||
```typescript
|
||||
class FramedProcessor {
|
||||
private tasks: (() => void)[] = [];
|
||||
private isProcessing = false;
|
||||
|
||||
public addTask(task: () => void): void {
|
||||
this.tasks.push(task);
|
||||
if (!this.isProcessing) {
|
||||
this.processNextFrame();
|
||||
}
|
||||
}
|
||||
|
||||
private processNextFrame(): void {
|
||||
this.isProcessing = true;
|
||||
const startTime = Date.now();
|
||||
const frameTime = 16; // 16ms per frame
|
||||
|
||||
while (this.tasks.length > 0 && Date.now() - startTime < frameTime) {
|
||||
const task = this.tasks.shift();
|
||||
if (task) task();
|
||||
}
|
||||
|
||||
if (this.tasks.length > 0) {
|
||||
setTimeout(() => this.processNextFrame(), 0);
|
||||
} else {
|
||||
this.isProcessing = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 内存管理
|
||||
|
||||
```typescript
|
||||
class MemoryManager {
|
||||
private static readonly MAX_MEMORY = 256 * 1024 * 1024; // 256MB
|
||||
|
||||
public static checkMemoryUsage(): void {
|
||||
if (typeof wx !== 'undefined' && wx.getPerformance) {
|
||||
const performance = wx.getPerformance();
|
||||
const memoryInfo = performance.getEntries().find(
|
||||
(entry: any) => entry.entryType === 'memory'
|
||||
);
|
||||
|
||||
if (memoryInfo && memoryInfo.usedJSHeapSize > this.MAX_MEMORY * 0.8) {
|
||||
console.warn('内存使用率过高,建议清理资源');
|
||||
// 触发垃圾回收或资源清理
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 调试技巧
|
||||
|
||||
```typescript
|
||||
// 检查微信小游戏环境
|
||||
if (typeof wx !== 'undefined') {
|
||||
const adapter = new WeChatMiniGameAdapter();
|
||||
|
||||
console.log('微信版本:', adapter.version);
|
||||
console.log('设备信息:', adapter.getDeviceInfo());
|
||||
console.log('平台配置:', adapter.getPlatformConfig());
|
||||
|
||||
// 检查功能支持
|
||||
console.log('Worker支持:', adapter.isWorkerSupported());
|
||||
console.log('SharedArrayBuffer支持:', adapter.isSharedArrayBufferSupported());
|
||||
}
|
||||
```
|
||||
|
||||
## 微信小游戏特殊API
|
||||
|
||||
```typescript
|
||||
// 获取设备性能等级
|
||||
if (typeof wx !== 'undefined' && wx.getDeviceInfo) {
|
||||
wx.getDeviceInfo({
|
||||
success: (res) => {
|
||||
console.log('设备性能等级:', res.benchmarkLevel);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 监听内存警告
|
||||
if (typeof wx !== 'undefined' && wx.onMemoryWarning) {
|
||||
wx.onMemoryWarning(() => {
|
||||
console.warn('收到内存警告,开始清理资源');
|
||||
// 清理不必要的资源
|
||||
});
|
||||
}
|
||||
```
|
||||
643
docs/guide/plugin-system.md
Normal file
643
docs/guide/plugin-system.md
Normal file
@@ -0,0 +1,643 @@
|
||||
# 插件系统
|
||||
|
||||
插件系统允许你以模块化的方式扩展 ECS Framework 的功能。通过插件,你可以封装特定功能(如网络同步、物理引擎、调试工具等),并在多个项目中复用。
|
||||
|
||||
## 概述
|
||||
|
||||
### 什么是插件
|
||||
|
||||
插件是实现了 `IPlugin` 接口的类,可以在运行时动态安装到框架中。插件可以:
|
||||
|
||||
- 注册自定义服务到服务容器
|
||||
- 添加系统到场景
|
||||
- 注册自定义组件
|
||||
- 扩展框架功能
|
||||
|
||||
### 插件的优势
|
||||
|
||||
- **模块化**: 将功能封装为独立模块,提高代码可维护性
|
||||
- **可复用**: 同一个插件可以在多个项目中使用
|
||||
- **解耦**: 核心框架与扩展功能分离
|
||||
- **热插拔**: 运行时动态安装和卸载插件
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 创建第一个插件
|
||||
|
||||
创建一个简单的调试插件:
|
||||
|
||||
```typescript
|
||||
import { IPlugin, Core, ServiceContainer } from '@esengine/ecs-framework';
|
||||
|
||||
class DebugPlugin implements IPlugin {
|
||||
readonly name = 'debug-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
console.log('Debug plugin installed');
|
||||
|
||||
// 可以在这里注册服务、添加系统等
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
console.log('Debug plugin uninstalled');
|
||||
// 清理资源
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 安装插件
|
||||
|
||||
使用 `Core.installPlugin()` 安装插件:
|
||||
|
||||
```typescript
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始化Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 安装插件
|
||||
await Core.installPlugin(new DebugPlugin());
|
||||
|
||||
// 检查插件是否已安装
|
||||
if (Core.isPluginInstalled('debug-plugin')) {
|
||||
console.log('Debug plugin is running');
|
||||
}
|
||||
```
|
||||
|
||||
### 卸载插件
|
||||
|
||||
```typescript
|
||||
// 卸载插件
|
||||
await Core.uninstallPlugin('debug-plugin');
|
||||
```
|
||||
|
||||
### 获取插件实例
|
||||
|
||||
```typescript
|
||||
// 获取已安装的插件
|
||||
const plugin = Core.getPlugin('debug-plugin');
|
||||
if (plugin) {
|
||||
console.log(`Plugin version: ${plugin.version}`);
|
||||
}
|
||||
```
|
||||
|
||||
## 插件开发
|
||||
|
||||
### IPlugin 接口
|
||||
|
||||
所有插件必须实现 `IPlugin` 接口:
|
||||
|
||||
```typescript
|
||||
export interface IPlugin {
|
||||
// 插件唯一名称
|
||||
readonly name: string;
|
||||
|
||||
// 插件版本(建议遵循semver规范)
|
||||
readonly version: string;
|
||||
|
||||
// 依赖的其他插件(可选)
|
||||
readonly dependencies?: readonly string[];
|
||||
|
||||
// 安装插件时调用
|
||||
install(core: Core, services: ServiceContainer): void | Promise<void>;
|
||||
|
||||
// 卸载插件时调用
|
||||
uninstall(): void | Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
### 插件生命周期
|
||||
|
||||
#### install 方法
|
||||
|
||||
在插件安装时调用,用于初始化插件:
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// 1. 注册服务
|
||||
services.registerSingleton(MyService);
|
||||
|
||||
// 2. 访问当前场景
|
||||
const scene = core.scene;
|
||||
if (scene) {
|
||||
// 3. 添加系统
|
||||
scene.addSystem(new MySystem());
|
||||
}
|
||||
|
||||
// 4. 其他初始化逻辑
|
||||
console.log('Plugin initialized');
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 清理逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### uninstall 方法
|
||||
|
||||
在插件卸载时调用,用于清理资源:
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-plugin';
|
||||
readonly version = '1.0.0';
|
||||
private myService?: MyService;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
this.myService = new MyService();
|
||||
services.registerInstance(MyService, this.myService);
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 清理服务
|
||||
if (this.myService) {
|
||||
this.myService.dispose();
|
||||
this.myService = undefined;
|
||||
}
|
||||
|
||||
// 移除事件监听器
|
||||
// 释放其他资源
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 异步插件
|
||||
|
||||
插件的 `install` 和 `uninstall` 方法都支持异步:
|
||||
|
||||
```typescript
|
||||
class AsyncPlugin implements IPlugin {
|
||||
readonly name = 'async-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
async install(core: Core, services: ServiceContainer): Promise<void> {
|
||||
// 异步加载资源
|
||||
const config = await fetch('/plugin-config.json').then(r => r.json());
|
||||
|
||||
// 使用加载的配置初始化服务
|
||||
const service = new MyService(config);
|
||||
services.registerInstance(MyService, service);
|
||||
}
|
||||
|
||||
async uninstall(): Promise<void> {
|
||||
// 异步清理
|
||||
await this.saveState();
|
||||
}
|
||||
|
||||
private async saveState() {
|
||||
// 保存插件状态
|
||||
}
|
||||
}
|
||||
|
||||
// 使用
|
||||
await Core.installPlugin(new AsyncPlugin());
|
||||
```
|
||||
|
||||
### 注册服务
|
||||
|
||||
插件可以向服务容器注册自己的服务:
|
||||
|
||||
```typescript
|
||||
import { IService } from '@esengine/ecs-framework';
|
||||
|
||||
class NetworkService implements IService {
|
||||
connect(url: string) {
|
||||
console.log(`Connecting to ${url}`);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
console.log('Network service disposed');
|
||||
}
|
||||
}
|
||||
|
||||
class NetworkPlugin implements IPlugin {
|
||||
readonly name = 'network-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// 注册网络服务
|
||||
services.registerSingleton(NetworkService);
|
||||
|
||||
// 解析并使用服务
|
||||
const network = services.resolve(NetworkService);
|
||||
network.connect('ws://localhost:8080');
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 服务容器会自动调用服务的dispose方法
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 添加系统
|
||||
|
||||
插件可以向场景添加自定义系统:
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, Matcher } from '@esengine/ecs-framework';
|
||||
|
||||
class PhysicsSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(PhysicsBody));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 物理模拟逻辑
|
||||
}
|
||||
}
|
||||
|
||||
class PhysicsPlugin implements IPlugin {
|
||||
readonly name = 'physics-plugin';
|
||||
readonly version = '1.0.0';
|
||||
private physicsSystem?: PhysicsSystem;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
const scene = core.scene;
|
||||
if (scene) {
|
||||
this.physicsSystem = new PhysicsSystem();
|
||||
scene.addSystem(this.physicsSystem);
|
||||
}
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 移除系统
|
||||
if (this.physicsSystem) {
|
||||
const scene = Core.scene;
|
||||
if (scene) {
|
||||
scene.removeSystem(this.physicsSystem);
|
||||
}
|
||||
this.physicsSystem = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 依赖管理
|
||||
|
||||
### 声明依赖
|
||||
|
||||
插件可以声明对其他插件的依赖:
|
||||
|
||||
```typescript
|
||||
class AdvancedPhysicsPlugin implements IPlugin {
|
||||
readonly name = 'advanced-physics';
|
||||
readonly version = '2.0.0';
|
||||
|
||||
// 声明依赖基础物理插件
|
||||
readonly dependencies = ['physics-plugin'] as const;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// 可以安全地使用physics-plugin提供的服务
|
||||
const physicsService = services.resolve(PhysicsService);
|
||||
// ...
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 清理
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 依赖检查
|
||||
|
||||
框架会自动检查依赖关系,如果依赖未满足会抛出错误:
|
||||
|
||||
```typescript
|
||||
// 错误:physics-plugin 未安装
|
||||
try {
|
||||
await Core.installPlugin(new AdvancedPhysicsPlugin());
|
||||
} catch (error) {
|
||||
console.error(error); // Plugin advanced-physics has unmet dependencies: physics-plugin
|
||||
}
|
||||
|
||||
// 正确:先安装依赖
|
||||
await Core.installPlugin(new PhysicsPlugin());
|
||||
await Core.installPlugin(new AdvancedPhysicsPlugin());
|
||||
```
|
||||
|
||||
### 卸载顺序
|
||||
|
||||
框架会检查依赖关系,防止卸载被其他插件依赖的插件:
|
||||
|
||||
```typescript
|
||||
await Core.installPlugin(new PhysicsPlugin());
|
||||
await Core.installPlugin(new AdvancedPhysicsPlugin());
|
||||
|
||||
// 错误:physics-plugin 被 advanced-physics 依赖
|
||||
try {
|
||||
await Core.uninstallPlugin('physics-plugin');
|
||||
} catch (error) {
|
||||
console.error(error); // Cannot uninstall plugin physics-plugin: it is required by advanced-physics
|
||||
}
|
||||
|
||||
// 正确:先卸载依赖它的插件
|
||||
await Core.uninstallPlugin('advanced-physics');
|
||||
await Core.uninstallPlugin('physics-plugin');
|
||||
```
|
||||
|
||||
## 插件管理
|
||||
|
||||
### 通过 Core 管理
|
||||
|
||||
Core 类提供了便捷的插件管理方法:
|
||||
|
||||
```typescript
|
||||
// 安装插件
|
||||
await Core.installPlugin(myPlugin);
|
||||
|
||||
// 卸载插件
|
||||
await Core.uninstallPlugin('plugin-name');
|
||||
|
||||
// 检查插件是否已安装
|
||||
if (Core.isPluginInstalled('plugin-name')) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// 获取插件实例
|
||||
const plugin = Core.getPlugin('plugin-name');
|
||||
```
|
||||
|
||||
### 通过 PluginManager 管理
|
||||
|
||||
也可以直接使用 PluginManager 服务:
|
||||
|
||||
```typescript
|
||||
const pluginManager = Core.services.resolve(PluginManager);
|
||||
|
||||
// 获取所有插件
|
||||
const allPlugins = pluginManager.getAllPlugins();
|
||||
console.log(`Total plugins: ${allPlugins.length}`);
|
||||
|
||||
// 获取插件元数据
|
||||
const metadata = pluginManager.getMetadata('my-plugin');
|
||||
if (metadata) {
|
||||
console.log(`State: ${metadata.state}`);
|
||||
console.log(`Installed at: ${new Date(metadata.installedAt!)}`);
|
||||
}
|
||||
|
||||
// 获取所有插件元数据
|
||||
const allMetadata = pluginManager.getAllMetadata();
|
||||
for (const meta of allMetadata) {
|
||||
console.log(`${meta.name} v${meta.version} - ${meta.state}`);
|
||||
}
|
||||
```
|
||||
|
||||
## 实用插件示例
|
||||
|
||||
### 网络同步插件
|
||||
|
||||
```typescript
|
||||
import { IPlugin, IService, Core, ServiceContainer } from '@esengine/ecs-framework';
|
||||
|
||||
class NetworkSyncService implements IService {
|
||||
private ws?: WebSocket;
|
||||
|
||||
connect(url: string) {
|
||||
this.ws = new WebSocket(url);
|
||||
this.ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
this.handleMessage(data);
|
||||
};
|
||||
}
|
||||
|
||||
private handleMessage(data: any) {
|
||||
// 处理网络消息
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
this.ws = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NetworkSyncPlugin implements IPlugin {
|
||||
readonly name = 'network-sync';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// 注册网络服务
|
||||
services.registerSingleton(NetworkSyncService);
|
||||
|
||||
// 自动连接
|
||||
const network = services.resolve(NetworkSyncService);
|
||||
network.connect('ws://localhost:8080');
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 服务会自动dispose
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 性能分析插件
|
||||
|
||||
```typescript
|
||||
class PerformanceAnalysisPlugin implements IPlugin {
|
||||
readonly name = 'performance-analysis';
|
||||
readonly version = '1.0.0';
|
||||
private frameCount = 0;
|
||||
private totalTime = 0;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
const monitor = services.resolve(PerformanceMonitor);
|
||||
monitor.enable();
|
||||
|
||||
// 定期输出性能报告
|
||||
const timer = services.resolve(TimerManager);
|
||||
timer.schedule(5.0, true, null, () => {
|
||||
this.printReport(monitor);
|
||||
});
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 清理
|
||||
}
|
||||
|
||||
private printReport(monitor: PerformanceMonitor) {
|
||||
console.log('=== Performance Report ===');
|
||||
console.log(`FPS: ${monitor.getFPS()}`);
|
||||
console.log(`Memory: ${monitor.getMemoryUsage()} MB`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 命名规范
|
||||
|
||||
- 插件名称使用小写字母和连字符:`my-awesome-plugin`
|
||||
- 版本号遵循语义化版本规范:`1.0.0`
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-awesome-plugin'; // 好
|
||||
readonly version = '1.0.0'; // 好
|
||||
}
|
||||
```
|
||||
|
||||
### 清理资源
|
||||
|
||||
始终在 `uninstall` 中清理插件创建的所有资源:
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-plugin';
|
||||
readonly version = '1.0.0';
|
||||
private timerId?: number;
|
||||
private listener?: () => void;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// 添加定时器
|
||||
this.timerId = setInterval(() => {
|
||||
// ...
|
||||
}, 1000);
|
||||
|
||||
// 添加事件监听
|
||||
this.listener = () => {};
|
||||
window.addEventListener('resize', this.listener);
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 清理定时器
|
||||
if (this.timerId) {
|
||||
clearInterval(this.timerId);
|
||||
this.timerId = undefined;
|
||||
}
|
||||
|
||||
// 移除事件监听
|
||||
if (this.listener) {
|
||||
window.removeEventListener('resize', this.listener);
|
||||
this.listener = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 错误处理
|
||||
|
||||
在插件中妥善处理错误,避免影响整个应用:
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
async install(core: Core, services: ServiceContainer): Promise<void> {
|
||||
try {
|
||||
// 可能失败的操作
|
||||
await this.loadConfig();
|
||||
} catch (error) {
|
||||
console.error('Failed to load plugin config:', error);
|
||||
throw error; // 重新抛出,让框架知道安装失败
|
||||
}
|
||||
}
|
||||
|
||||
async uninstall(): Promise<void> {
|
||||
try {
|
||||
await this.cleanup();
|
||||
} catch (error) {
|
||||
console.error('Failed to cleanup plugin:', error);
|
||||
// 即使清理失败也不应该阻止卸载
|
||||
}
|
||||
}
|
||||
|
||||
private async loadConfig() {
|
||||
// 加载配置
|
||||
}
|
||||
|
||||
private async cleanup() {
|
||||
// 清理
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 配置化
|
||||
|
||||
允许用户配置插件行为:
|
||||
|
||||
```typescript
|
||||
interface NetworkPluginConfig {
|
||||
serverUrl: string;
|
||||
autoReconnect: boolean;
|
||||
timeout: number;
|
||||
}
|
||||
|
||||
class NetworkPlugin implements IPlugin {
|
||||
readonly name = 'network-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
constructor(private config: NetworkPluginConfig) {}
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
const network = new NetworkService(this.config);
|
||||
services.registerInstance(NetworkService, network);
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 清理
|
||||
}
|
||||
}
|
||||
|
||||
// 使用
|
||||
const plugin = new NetworkPlugin({
|
||||
serverUrl: 'ws://localhost:8080',
|
||||
autoReconnect: true,
|
||||
timeout: 5000
|
||||
});
|
||||
|
||||
await Core.installPlugin(plugin);
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 插件安装失败
|
||||
|
||||
**问题**: 插件安装时抛出错误
|
||||
|
||||
**原因**:
|
||||
- 依赖未满足
|
||||
- install 方法中有异常
|
||||
- 服务注册冲突
|
||||
|
||||
**解决**:
|
||||
1. 检查依赖是否已安装
|
||||
2. 查看错误日志
|
||||
3. 确保服务名称不冲突
|
||||
|
||||
### 插件卸载后仍有副作用
|
||||
|
||||
**问题**: 卸载插件后,插件的功能仍在运行
|
||||
|
||||
**原因**: uninstall 方法中未正确清理资源
|
||||
|
||||
**解决**: 确保在 uninstall 中清理:
|
||||
- 定时器
|
||||
- 事件监听器
|
||||
- WebSocket连接
|
||||
- 系统引用
|
||||
|
||||
### 何时使用插件
|
||||
|
||||
**适合使用插件**:
|
||||
- 可选功能(调试工具、性能分析)
|
||||
- 第三方集成(网络库、物理引擎)
|
||||
- 跨项目复用的功能模块
|
||||
|
||||
**不适合使用插件**:
|
||||
- 核心游戏逻辑
|
||||
- 简单的工具类
|
||||
- 项目特定的功能
|
||||
|
||||
## 相关链接
|
||||
|
||||
- [服务容器](./service-container.md) - 在插件中使用服务容器
|
||||
- [系统架构](./system.md) - 在插件中添加系统
|
||||
- [快速开始](./getting-started.md) - Core 初始化和基础使用
|
||||
675
docs/guide/scene-manager.md
Normal file
675
docs/guide/scene-manager.md
Normal file
@@ -0,0 +1,675 @@
|
||||
# SceneManager
|
||||
|
||||
SceneManager 是 ECS Framework 提供的轻量级场景管理器,适用于 95% 的游戏应用。它提供简单直观的 API,支持场景切换和延迟加载。
|
||||
|
||||
## 适用场景
|
||||
|
||||
SceneManager 适合以下场景:
|
||||
- 单人游戏
|
||||
- 简单多人游戏
|
||||
- 移动游戏
|
||||
- 需要场景切换的游戏(菜单、游戏、暂停等)
|
||||
- 不需要多 World 隔离的项目
|
||||
|
||||
## 特点
|
||||
|
||||
- 轻量级,零额外开销
|
||||
- 简单直观的 API
|
||||
- 支持延迟场景切换(避免在当前帧中途切换)
|
||||
- 自动管理 ECS 流式 API
|
||||
- 自动处理场景生命周期
|
||||
- 集成在 Core 中,自动更新
|
||||
|
||||
## 基本使用
|
||||
|
||||
### 推荐方式:使用 Core 的静态方法
|
||||
|
||||
这是最简单和推荐的方式,适合大多数应用:
|
||||
|
||||
```typescript
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// 1. 初始化 Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 2. 创建并设置场景
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "GameScene";
|
||||
|
||||
// 添加系统
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
|
||||
// 创建初始实体
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Transform(400, 300));
|
||||
player.addComponent(new Health(100));
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log("游戏场景已启动");
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 设置场景
|
||||
Core.setScene(new GameScene());
|
||||
|
||||
// 4. 游戏循环(Core.update 会自动更新场景)
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // 自动更新所有服务和场景
|
||||
}
|
||||
|
||||
// Laya 引擎集成
|
||||
Laya.timer.frameLoop(1, this, () => {
|
||||
const deltaTime = Laya.timer.delta / 1000;
|
||||
Core.update(deltaTime);
|
||||
});
|
||||
|
||||
// Cocos Creator 集成
|
||||
update(deltaTime: number) {
|
||||
Core.update(deltaTime);
|
||||
}
|
||||
```
|
||||
|
||||
### 高级方式:直接使用 SceneManager
|
||||
|
||||
如果需要更多控制,可以直接使用 SceneManager:
|
||||
|
||||
```typescript
|
||||
import { Core, SceneManager, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始化 Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 获取 SceneManager(Core 已自动创建并注册)
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
|
||||
// 设置场景
|
||||
const gameScene = new GameScene();
|
||||
sceneManager.setScene(gameScene);
|
||||
|
||||
// 游戏循环(仍然使用 Core.update)
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // Core会自动调用sceneManager.update()
|
||||
}
|
||||
```
|
||||
|
||||
**重要**:无论使用哪种方式,游戏循环中都应该只调用 `Core.update()`,它会自动更新 SceneManager 和场景。不需要手动调用 `sceneManager.update()`。
|
||||
|
||||
## 场景切换
|
||||
|
||||
### 立即切换
|
||||
|
||||
使用 `Core.setScene()` 或 `sceneManager.setScene()` 立即切换场景:
|
||||
|
||||
```typescript
|
||||
// 方式1:使用 Core(推荐)
|
||||
Core.setScene(new MenuScene());
|
||||
|
||||
// 方式2:使用 SceneManager
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
sceneManager.setScene(new MenuScene());
|
||||
```
|
||||
|
||||
### 延迟切换
|
||||
|
||||
使用 `Core.loadScene()` 或 `sceneManager.loadScene()` 延迟切换场景,场景会在下一帧切换:
|
||||
|
||||
```typescript
|
||||
// 方式1:使用 Core(推荐)
|
||||
Core.loadScene(new GameOverScene());
|
||||
|
||||
// 方式2:使用 SceneManager
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
sceneManager.loadScene(new GameOverScene());
|
||||
```
|
||||
|
||||
在 System 中切换场景时,应该使用延迟切换:
|
||||
|
||||
```typescript
|
||||
class GameOverSystem extends EntitySystem {
|
||||
process(entities: readonly Entity[]): void {
|
||||
const player = entities.find(e => e.name === 'Player');
|
||||
const health = player?.getComponent(Health);
|
||||
|
||||
if (health && health.value <= 0) {
|
||||
// 延迟切换到游戏结束场景(下一帧生效)
|
||||
Core.loadScene(new GameOverScene());
|
||||
// 当前帧继续执行,不会中断当前系统的处理
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 完整的场景切换示例
|
||||
|
||||
```typescript
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始化
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 菜单场景
|
||||
class MenuScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "MenuScene";
|
||||
|
||||
// 监听开始游戏事件
|
||||
this.eventSystem.on('start_game', () => {
|
||||
Core.loadScene(new GameScene());
|
||||
});
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log("显示菜单界面");
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
console.log("菜单场景卸载");
|
||||
}
|
||||
}
|
||||
|
||||
// 游戏场景
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "GameScene";
|
||||
|
||||
// 创建游戏实体
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Transform(400, 300));
|
||||
player.addComponent(new Health(100));
|
||||
|
||||
// 监听游戏结束事件
|
||||
this.eventSystem.on('game_over', () => {
|
||||
Core.loadScene(new GameOverScene());
|
||||
});
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log("游戏开始");
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
console.log("游戏场景卸载");
|
||||
}
|
||||
}
|
||||
|
||||
// 游戏结束场景
|
||||
class GameOverScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "GameOverScene";
|
||||
|
||||
// 监听返回菜单事件
|
||||
this.eventSystem.on('back_to_menu', () => {
|
||||
Core.loadScene(new MenuScene());
|
||||
});
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log("显示游戏结束界面");
|
||||
}
|
||||
}
|
||||
|
||||
// 开始游戏
|
||||
Core.setScene(new MenuScene());
|
||||
|
||||
// 游戏循环
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // 自动更新场景
|
||||
}
|
||||
```
|
||||
|
||||
## API 参考
|
||||
|
||||
### Core 静态方法(推荐)
|
||||
|
||||
#### Core.setScene()
|
||||
|
||||
立即切换场景。
|
||||
|
||||
```typescript
|
||||
public static setScene<T extends IScene>(scene: T): T
|
||||
```
|
||||
|
||||
**参数**:
|
||||
- `scene` - 要设置的场景实例
|
||||
|
||||
**返回**:
|
||||
- 返回设置的场景实例
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
const gameScene = Core.setScene(new GameScene());
|
||||
console.log(gameScene.name);
|
||||
```
|
||||
|
||||
#### Core.loadScene()
|
||||
|
||||
延迟加载场景(下一帧切换)。
|
||||
|
||||
```typescript
|
||||
public static loadScene<T extends IScene>(scene: T): void
|
||||
```
|
||||
|
||||
**参数**:
|
||||
- `scene` - 要加载的场景实例
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
Core.loadScene(new GameOverScene());
|
||||
```
|
||||
|
||||
#### Core.scene
|
||||
|
||||
获取当前活跃的场景。
|
||||
|
||||
```typescript
|
||||
public static get scene(): IScene | null
|
||||
```
|
||||
|
||||
**返回**:
|
||||
- 当前场景实例,如果没有场景则返回 null
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
const currentScene = Core.scene;
|
||||
if (currentScene) {
|
||||
console.log(`当前场景: ${currentScene.name}`);
|
||||
}
|
||||
```
|
||||
|
||||
#### Core.ecsAPI
|
||||
|
||||
获取 ECS 流式 API。
|
||||
|
||||
```typescript
|
||||
public static get ecsAPI(): ECSFluentAPI | null
|
||||
```
|
||||
|
||||
**返回**:
|
||||
- ECS API 实例,如果当前没有场景则返回 null
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
const api = Core.ecsAPI;
|
||||
if (api) {
|
||||
// 查询实体
|
||||
const enemies = api.find(Enemy, Transform);
|
||||
|
||||
// 发射事件
|
||||
api.emit('game:start', { level: 1 });
|
||||
}
|
||||
```
|
||||
|
||||
### SceneManager 方法(高级)
|
||||
|
||||
如果需要直接使用 SceneManager,可以通过服务容器获取:
|
||||
|
||||
```typescript
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
```
|
||||
|
||||
#### setScene()
|
||||
|
||||
立即切换场景。
|
||||
|
||||
```typescript
|
||||
public setScene<T extends IScene>(scene: T): T
|
||||
```
|
||||
|
||||
#### loadScene()
|
||||
|
||||
延迟加载场景。
|
||||
|
||||
```typescript
|
||||
public loadScene<T extends IScene>(scene: T): void
|
||||
```
|
||||
|
||||
#### currentScene
|
||||
|
||||
获取当前场景。
|
||||
|
||||
```typescript
|
||||
public get currentScene(): IScene | null
|
||||
```
|
||||
|
||||
#### api
|
||||
|
||||
获取 ECS 流式 API。
|
||||
|
||||
```typescript
|
||||
public get api(): ECSFluentAPI | null
|
||||
```
|
||||
|
||||
#### hasScene
|
||||
|
||||
检查是否有活跃场景。
|
||||
|
||||
```typescript
|
||||
public get hasScene(): boolean
|
||||
```
|
||||
|
||||
#### hasPendingScene
|
||||
|
||||
检查是否有待切换的场景。
|
||||
|
||||
```typescript
|
||||
public get hasPendingScene(): boolean
|
||||
```
|
||||
|
||||
## 使用 ECS 流式 API
|
||||
|
||||
通过 `Core.ecsAPI` 可以方便地访问场景的 ECS 功能:
|
||||
|
||||
```typescript
|
||||
const api = Core.ecsAPI;
|
||||
if (!api) {
|
||||
console.error('没有活跃场景');
|
||||
return;
|
||||
}
|
||||
|
||||
// 查询实体
|
||||
const players = api.find(Player, Transform);
|
||||
const enemies = api.find(Enemy, Health, Transform);
|
||||
|
||||
// 发射事件
|
||||
api.emit('player:scored', { points: 100 });
|
||||
|
||||
// 监听事件
|
||||
api.on('enemy:died', (data) => {
|
||||
console.log('敌人死亡:', data);
|
||||
});
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 使用 Core 的静态方法
|
||||
|
||||
```typescript
|
||||
// 推荐:使用 Core 的静态方法
|
||||
Core.setScene(new GameScene());
|
||||
Core.loadScene(new MenuScene());
|
||||
const currentScene = Core.scene;
|
||||
|
||||
// 不推荐:除非有特殊需求,否则不需要直接使用 SceneManager
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
sceneManager.setScene(new GameScene());
|
||||
```
|
||||
|
||||
### 2. 只调用 Core.update()
|
||||
|
||||
```typescript
|
||||
// 正确:只调用 Core.update()
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // 自动更新所有服务和场景
|
||||
}
|
||||
|
||||
// 错误:不要手动调用 sceneManager.update()
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime);
|
||||
sceneManager.update(); // 重复更新,会导致问题!
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 使用延迟切换避免问题
|
||||
|
||||
在 System 中切换场景时,应该使用 `loadScene()` 而不是 `setScene()`:
|
||||
|
||||
```typescript
|
||||
// 推荐:延迟切换
|
||||
class HealthSystem extends EntitySystem {
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health);
|
||||
if (health.value <= 0) {
|
||||
Core.loadScene(new GameOverScene());
|
||||
// 当前帧继续处理其他实体
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 不推荐:立即切换可能导致问题
|
||||
class HealthSystem extends EntitySystem {
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health);
|
||||
if (health.value <= 0) {
|
||||
Core.setScene(new GameOverScene());
|
||||
// 场景立即切换,当前帧的其他实体可能无法正常处理
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 场景职责分离
|
||||
|
||||
每个场景应该只负责一个特定的游戏状态:
|
||||
|
||||
```typescript
|
||||
// 好的设计 - 职责清晰
|
||||
class MenuScene extends Scene {
|
||||
// 只处理菜单相关逻辑
|
||||
}
|
||||
|
||||
class GameScene extends Scene {
|
||||
// 只处理游戏玩法逻辑
|
||||
}
|
||||
|
||||
class PauseScene extends Scene {
|
||||
// 只处理暂停界面逻辑
|
||||
}
|
||||
|
||||
// 避免的设计 - 职责混乱
|
||||
class MegaScene extends Scene {
|
||||
// 包含菜单、游戏、暂停等所有逻辑
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 资源管理
|
||||
|
||||
在场景的 `unload()` 方法中清理资源:
|
||||
|
||||
```typescript
|
||||
class GameScene extends Scene {
|
||||
private textures: Map<string, any> = new Map();
|
||||
private sounds: Map<string, any> = new Map();
|
||||
|
||||
protected initialize(): void {
|
||||
this.loadResources();
|
||||
}
|
||||
|
||||
private loadResources(): void {
|
||||
this.textures.set('player', loadTexture('player.png'));
|
||||
this.sounds.set('bgm', loadSound('bgm.mp3'));
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// 清理资源
|
||||
this.textures.clear();
|
||||
this.sounds.clear();
|
||||
console.log('场景资源已清理');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. 事件驱动的场景切换
|
||||
|
||||
使用事件系统来触发场景切换,保持代码解耦:
|
||||
|
||||
```typescript
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 监听场景切换事件
|
||||
this.eventSystem.on('goto:menu', () => {
|
||||
Core.loadScene(new MenuScene());
|
||||
});
|
||||
|
||||
this.eventSystem.on('goto:gameover', (data) => {
|
||||
Core.loadScene(new GameOverScene());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 在 System 中触发事件
|
||||
class GameLogicSystem extends EntitySystem {
|
||||
process(entities: readonly Entity[]): void {
|
||||
if (levelComplete) {
|
||||
this.scene.eventSystem.emitSync('goto:gameover', {
|
||||
score: 1000,
|
||||
level: 5
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 架构层次
|
||||
|
||||
SceneManager 在 ECS Framework 中的位置:
|
||||
|
||||
```
|
||||
Core (全局服务)
|
||||
└── SceneManager (场景管理,自动更新)
|
||||
└── Scene (当前场景)
|
||||
├── EntitySystem (系统)
|
||||
├── Entity (实体)
|
||||
└── Component (组件)
|
||||
```
|
||||
|
||||
## 与 WorldManager 的对比
|
||||
|
||||
| 特性 | SceneManager | WorldManager |
|
||||
|------|--------------|--------------|
|
||||
| 适用场景 | 95% 的游戏应用 | 高级多世界隔离场景 |
|
||||
| 复杂度 | 简单 | 复杂 |
|
||||
| 场景数量 | 单场景(可切换) | 多 World,每个 World 多场景 |
|
||||
| 性能开销 | 最小 | 较高 |
|
||||
| 使用方式 | `Core.setScene()` | `worldManager.createWorld()` |
|
||||
|
||||
**何时使用 SceneManager**:
|
||||
- 单人游戏
|
||||
- 简单的多人游戏
|
||||
- 移动游戏
|
||||
- 场景之间需要切换但不需要同时运行
|
||||
|
||||
**何时使用 WorldManager**:
|
||||
- MMO 游戏服务器(每个房间一个 World)
|
||||
- 游戏大厅系统(每个游戏房间完全隔离)
|
||||
- 需要运行多个完全独立的游戏实例
|
||||
|
||||
## 完整示例
|
||||
|
||||
```typescript
|
||||
import { Core, Scene, EntitySystem, Entity, Matcher } from '@esengine/ecs-framework';
|
||||
|
||||
// 定义组件
|
||||
class Transform {
|
||||
constructor(public x: number, public y: number) {}
|
||||
}
|
||||
|
||||
class Velocity {
|
||||
constructor(public vx: number, public vy: number) {}
|
||||
}
|
||||
|
||||
class Health {
|
||||
constructor(public value: number) {}
|
||||
}
|
||||
|
||||
// 定义系统
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Transform, Velocity));
|
||||
}
|
||||
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const transform = entity.getComponent(Transform);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
if (transform && velocity) {
|
||||
transform.x += velocity.vx;
|
||||
transform.y += velocity.vy;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 定义场景
|
||||
class MenuScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "MenuScene";
|
||||
console.log("菜单场景初始化");
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log("菜单场景启动");
|
||||
}
|
||||
}
|
||||
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "GameScene";
|
||||
|
||||
// 添加系统
|
||||
this.addSystem(new MovementSystem());
|
||||
|
||||
// 创建玩家
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Transform(400, 300));
|
||||
player.addComponent(new Velocity(0, 0));
|
||||
player.addComponent(new Health(100));
|
||||
|
||||
// 创建敌人
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const enemy = this.createEntity(`Enemy_${i}`);
|
||||
enemy.addComponent(new Transform(
|
||||
Math.random() * 800,
|
||||
Math.random() * 600
|
||||
));
|
||||
enemy.addComponent(new Velocity(
|
||||
Math.random() * 100 - 50,
|
||||
Math.random() * 100 - 50
|
||||
));
|
||||
enemy.addComponent(new Health(50));
|
||||
}
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log('游戏场景启动');
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
console.log('游戏场景卸载');
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 设置初始场景
|
||||
Core.setScene(new MenuScene());
|
||||
|
||||
// 游戏循环
|
||||
let lastTime = 0;
|
||||
function gameLoop(currentTime: number) {
|
||||
const deltaTime = (currentTime - lastTime) / 1000;
|
||||
lastTime = currentTime;
|
||||
|
||||
// 只需要调用 Core.update,它会自动更新场景
|
||||
Core.update(deltaTime);
|
||||
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
requestAnimationFrame(gameLoop);
|
||||
|
||||
// 切换到游戏场景
|
||||
setTimeout(() => {
|
||||
Core.loadScene(new GameScene());
|
||||
}, 3000);
|
||||
```
|
||||
|
||||
SceneManager 为大多数游戏提供了简单而强大的场景管理能力。通过 Core 的静态方法,你可以轻松地管理场景切换。如果你需要更高级的多世界隔离功能,请参考 [WorldManager](./world-manager.md) 文档。
|
||||
661
docs/guide/scene.md
Normal file
661
docs/guide/scene.md
Normal file
@@ -0,0 +1,661 @@
|
||||
# 场景管理
|
||||
|
||||
在 ECS 架构中,场景(Scene)是游戏世界的容器,负责管理实体、系统和组件的生命周期。场景提供了完整的 ECS 运行环境。
|
||||
|
||||
## 基本概念
|
||||
|
||||
场景是 ECS 框架的核心容器,提供:
|
||||
- 实体的创建、管理和销毁
|
||||
- 系统的注册和执行调度
|
||||
- 组件的存储和查询
|
||||
- 事件系统支持
|
||||
- 性能监控和调试信息
|
||||
|
||||
## 场景管理方式
|
||||
|
||||
ECS Framework 提供了两种场景管理方式:
|
||||
|
||||
1. **[SceneManager](./scene-manager.md)** - 适用于 95% 的游戏应用
|
||||
- 单人游戏、简单多人游戏、移动游戏
|
||||
- 轻量级,简单直观的 API
|
||||
- 支持场景切换
|
||||
|
||||
2. **[WorldManager](./world-manager.md)** - 适用于高级多世界隔离场景
|
||||
- MMO 游戏服务器、游戏房间系统
|
||||
- 多 World 管理,每个 World 可包含多个场景
|
||||
- 完全隔离的独立环境
|
||||
|
||||
本文档重点介绍 Scene 类本身的使用方法。关于场景管理器的详细信息,请查看对应的文档。
|
||||
|
||||
## 创建场景
|
||||
|
||||
### 继承 Scene 类
|
||||
|
||||
**推荐做法:继承 Scene 类来创建自定义场景**
|
||||
|
||||
```typescript
|
||||
import { Scene, EntitySystem } from '@esengine/ecs-framework';
|
||||
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 设置场景名称
|
||||
this.name = "GameScene";
|
||||
|
||||
// 添加系统
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
this.addSystem(new PhysicsSystem());
|
||||
|
||||
// 创建初始实体
|
||||
this.createInitialEntities();
|
||||
}
|
||||
|
||||
private createInitialEntities(): void {
|
||||
// 创建玩家
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Position(400, 300));
|
||||
player.addComponent(new Health(100));
|
||||
player.addComponent(new PlayerController());
|
||||
|
||||
// 创建敌人
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const enemy = this.createEntity(`Enemy_${i}`);
|
||||
enemy.addComponent(new Position(Math.random() * 800, Math.random() * 600));
|
||||
enemy.addComponent(new Health(50));
|
||||
enemy.addComponent(new EnemyAI());
|
||||
}
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log("游戏场景已启动");
|
||||
// 场景启动时的逻辑
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
console.log("游戏场景已卸载");
|
||||
// 场景卸载时的清理逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用场景配置
|
||||
|
||||
```typescript
|
||||
import { ISceneConfig } from '@esengine/ecs-framework';
|
||||
|
||||
const config: ISceneConfig = {
|
||||
name: "MainGame",
|
||||
enableEntityDirectUpdate: false
|
||||
};
|
||||
|
||||
class ConfiguredScene extends Scene {
|
||||
constructor() {
|
||||
super(config);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 场景生命周期
|
||||
|
||||
场景提供了完整的生命周期管理:
|
||||
|
||||
```typescript
|
||||
class ExampleScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 场景初始化:设置系统和初始实体
|
||||
console.log("场景初始化");
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
// 场景开始运行:游戏逻辑开始执行
|
||||
console.log("场景开始运行");
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// 场景卸载:清理资源
|
||||
console.log("场景卸载");
|
||||
}
|
||||
}
|
||||
|
||||
// 使用场景(由框架自动管理生命周期)
|
||||
const scene = new ExampleScene();
|
||||
// 场景的 initialize(), begin(), update(), end() 由框架自动调用
|
||||
```
|
||||
|
||||
**生命周期方法**:
|
||||
|
||||
1. `initialize()` - 场景初始化,设置系统和初始实体
|
||||
2. `begin()` / `onStart()` - 场景开始运行
|
||||
3. `update()` - 每帧更新(由场景管理器调用)
|
||||
4. `end()` / `unload()` - 场景卸载,清理资源
|
||||
|
||||
## 实体管理
|
||||
|
||||
### 创建实体
|
||||
|
||||
```typescript
|
||||
class EntityScene extends Scene {
|
||||
createGameEntities(): void {
|
||||
// 创建单个实体
|
||||
const player = this.createEntity("Player");
|
||||
|
||||
// 批量创建实体(高性能)
|
||||
const bullets = this.createEntities(100, "Bullet");
|
||||
|
||||
// 为批量创建的实体添加组件
|
||||
bullets.forEach((bullet, index) => {
|
||||
bullet.addComponent(new Position(index * 10, 100));
|
||||
bullet.addComponent(new Velocity(Math.random() * 200 - 100, -300));
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 查找实体
|
||||
|
||||
```typescript
|
||||
class SearchScene extends Scene {
|
||||
findEntities(): void {
|
||||
// 按名称查找
|
||||
const player = this.findEntity("Player");
|
||||
const player2 = this.getEntityByName("Player"); // 别名方法
|
||||
|
||||
// 按 ID 查找
|
||||
const entity = this.findEntityById(123);
|
||||
|
||||
// 按标签查找
|
||||
const enemies = this.findEntitiesByTag(2);
|
||||
const enemies2 = this.getEntitiesByTag(2); // 别名方法
|
||||
|
||||
if (player) {
|
||||
console.log(`找到玩家: ${player.name}`);
|
||||
}
|
||||
|
||||
console.log(`找到 ${enemies.length} 个敌人`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 销毁实体
|
||||
|
||||
```typescript
|
||||
class DestroyScene extends Scene {
|
||||
cleanupEntities(): void {
|
||||
// 销毁所有实体
|
||||
this.destroyAllEntities();
|
||||
|
||||
// 单个实体的销毁通过实体本身
|
||||
const enemy = this.findEntity("Enemy_1");
|
||||
if (enemy) {
|
||||
enemy.destroy(); // 实体会自动从场景中移除
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 系统管理
|
||||
|
||||
### 添加和移除系统
|
||||
|
||||
```typescript
|
||||
class SystemScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 添加系统
|
||||
const movementSystem = new MovementSystem();
|
||||
this.addSystem(movementSystem);
|
||||
|
||||
// 设置系统更新顺序
|
||||
movementSystem.updateOrder = 1;
|
||||
|
||||
// 添加更多系统
|
||||
this.addSystem(new PhysicsSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
}
|
||||
|
||||
public removeUnnecessarySystems(): void {
|
||||
// 获取系统
|
||||
const physicsSystem = this.getEntityProcessor(PhysicsSystem);
|
||||
|
||||
// 移除系统
|
||||
if (physicsSystem) {
|
||||
this.removeSystem(physicsSystem);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 系统访问
|
||||
|
||||
```typescript
|
||||
class SystemAccessScene extends Scene {
|
||||
public pausePhysics(): void {
|
||||
const physicsSystem = this.getEntityProcessor(PhysicsSystem);
|
||||
if (physicsSystem) {
|
||||
physicsSystem.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
public getAllSystems(): EntitySystem[] {
|
||||
return this.systems; // 获取所有系统
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 事件系统
|
||||
|
||||
场景内置了类型安全的事件系统:
|
||||
|
||||
```typescript
|
||||
class EventScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 监听事件
|
||||
this.eventSystem.on('player_died', this.onPlayerDied.bind(this));
|
||||
this.eventSystem.on('enemy_spawned', this.onEnemySpawned.bind(this));
|
||||
this.eventSystem.on('level_complete', this.onLevelComplete.bind(this));
|
||||
}
|
||||
|
||||
private onPlayerDied(data: any): void {
|
||||
console.log('玩家死亡事件');
|
||||
// 处理玩家死亡
|
||||
}
|
||||
|
||||
private onEnemySpawned(data: any): void {
|
||||
console.log('敌人生成事件');
|
||||
// 处理敌人生成
|
||||
}
|
||||
|
||||
private onLevelComplete(data: any): void {
|
||||
console.log('关卡完成事件');
|
||||
// 处理关卡完成
|
||||
}
|
||||
|
||||
public triggerGameEvent(): void {
|
||||
// 发送事件(同步)
|
||||
this.eventSystem.emitSync('custom_event', {
|
||||
message: "这是自定义事件",
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
// 发送事件(异步)
|
||||
this.eventSystem.emit('async_event', {
|
||||
data: "异步事件数据"
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 事件系统 API
|
||||
|
||||
```typescript
|
||||
// 监听事件
|
||||
this.eventSystem.on('event_name', callback);
|
||||
|
||||
// 监听一次(自动取消订阅)
|
||||
this.eventSystem.once('event_name', callback);
|
||||
|
||||
// 取消监听
|
||||
this.eventSystem.off('event_name', callback);
|
||||
|
||||
// 同步发送事件
|
||||
this.eventSystem.emitSync('event_name', data);
|
||||
|
||||
// 异步发送事件
|
||||
this.eventSystem.emit('event_name', data);
|
||||
|
||||
// 清除所有事件监听
|
||||
this.eventSystem.clear();
|
||||
```
|
||||
|
||||
## 场景统计和调试
|
||||
|
||||
### 获取场景统计
|
||||
|
||||
```typescript
|
||||
class StatsScene extends Scene {
|
||||
public showStats(): void {
|
||||
const stats = this.getStats();
|
||||
console.log(`实体数量: ${stats.entityCount}`);
|
||||
console.log(`系统数量: ${stats.processorCount}`);
|
||||
console.log('组件存储统计:', stats.componentStorageStats);
|
||||
}
|
||||
|
||||
public showDebugInfo(): void {
|
||||
const debugInfo = this.getDebugInfo();
|
||||
console.log('场景调试信息:', debugInfo);
|
||||
|
||||
// 显示所有实体信息
|
||||
debugInfo.entities.forEach(entity => {
|
||||
console.log(`实体 ${entity.name}(${entity.id}): ${entity.componentCount} 个组件`);
|
||||
console.log('组件类型:', entity.componentTypes);
|
||||
});
|
||||
|
||||
// 显示所有系统信息
|
||||
debugInfo.processors.forEach(processor => {
|
||||
console.log(`系统 ${processor.name}: 处理 ${processor.entityCount} 个实体`);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 组件查询
|
||||
|
||||
Scene 提供了强大的组件查询系统:
|
||||
|
||||
```typescript
|
||||
class QueryScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 创建一些实体
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const entity = this.createEntity(`Entity_${i}`);
|
||||
entity.addComponent(new Transform(i * 10, 0));
|
||||
entity.addComponent(new Velocity(1, 0));
|
||||
if (i % 2 === 0) {
|
||||
entity.addComponent(new Renderer());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public queryEntities(): void {
|
||||
// 通过 QuerySystem 查询
|
||||
const entities = this.querySystem.query([Transform, Velocity]);
|
||||
console.log(`找到 ${entities.length} 个有 Transform 和 Velocity 的实体`);
|
||||
|
||||
// 使用 ECS 流式 API(如果通过 SceneManager)
|
||||
// const api = sceneManager.api;
|
||||
// const entities = api?.find(Transform, Velocity);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 性能监控
|
||||
|
||||
Scene 内置了性能监控功能:
|
||||
|
||||
```typescript
|
||||
class PerformanceScene extends Scene {
|
||||
public showPerformance(): void {
|
||||
// 获取性能数据
|
||||
const perfData = this.performanceMonitor?.getPerformanceData();
|
||||
if (perfData) {
|
||||
console.log('FPS:', perfData.fps);
|
||||
console.log('帧时间:', perfData.frameTime);
|
||||
console.log('实体更新时间:', perfData.entityUpdateTime);
|
||||
console.log('系统更新时间:', perfData.systemUpdateTime);
|
||||
}
|
||||
|
||||
// 获取性能报告
|
||||
const report = this.performanceMonitor?.generateReport();
|
||||
if (report) {
|
||||
console.log('性能报告:', report);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 场景职责分离
|
||||
|
||||
```typescript
|
||||
// 好的场景设计 - 职责清晰
|
||||
class MenuScene extends Scene {
|
||||
// 只处理菜单相关逻辑
|
||||
}
|
||||
|
||||
class GameScene extends Scene {
|
||||
// 只处理游戏玩法逻辑
|
||||
}
|
||||
|
||||
class InventoryScene extends Scene {
|
||||
// 只处理物品栏逻辑
|
||||
}
|
||||
|
||||
// 避免的场景设计 - 职责混乱
|
||||
class MegaScene extends Scene {
|
||||
// 包含菜单、游戏、物品栏等所有逻辑
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 合理的系统组织
|
||||
|
||||
```typescript
|
||||
class OrganizedScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 按功能和依赖关系添加系统
|
||||
this.addInputSystems();
|
||||
this.addLogicSystems();
|
||||
this.addRenderSystems();
|
||||
}
|
||||
|
||||
private addInputSystems(): void {
|
||||
this.addSystem(new InputSystem());
|
||||
}
|
||||
|
||||
private addLogicSystems(): void {
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new PhysicsSystem());
|
||||
this.addSystem(new CollisionSystem());
|
||||
}
|
||||
|
||||
private addRenderSystems(): void {
|
||||
this.addSystem(new RenderSystem());
|
||||
this.addSystem(new UISystem());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 资源管理
|
||||
|
||||
```typescript
|
||||
class ResourceScene extends Scene {
|
||||
private textures: Map<string, any> = new Map();
|
||||
private sounds: Map<string, any> = new Map();
|
||||
|
||||
protected initialize(): void {
|
||||
this.loadResources();
|
||||
}
|
||||
|
||||
private loadResources(): void {
|
||||
// 加载场景所需资源
|
||||
this.textures.set('player', this.loadTexture('player.png'));
|
||||
this.sounds.set('bgm', this.loadSound('bgm.mp3'));
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// 清理资源
|
||||
this.textures.clear();
|
||||
this.sounds.clear();
|
||||
console.log('场景资源已清理');
|
||||
}
|
||||
|
||||
private loadTexture(path: string): any {
|
||||
// 加载纹理
|
||||
return null;
|
||||
}
|
||||
|
||||
private loadSound(path: string): any {
|
||||
// 加载音效
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 事件处理规范
|
||||
|
||||
```typescript
|
||||
class EventHandlingScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 集中管理事件监听
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
private setupEventListeners(): void {
|
||||
this.eventSystem.on('game_pause', this.onGamePause.bind(this));
|
||||
this.eventSystem.on('game_resume', this.onGameResume.bind(this));
|
||||
this.eventSystem.on('player_input', this.onPlayerInput.bind(this));
|
||||
}
|
||||
|
||||
private onGamePause(): void {
|
||||
// 暂停游戏逻辑
|
||||
this.systems.forEach(system => {
|
||||
if (system instanceof GameLogicSystem) {
|
||||
system.enabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private onGameResume(): void {
|
||||
// 恢复游戏逻辑
|
||||
this.systems.forEach(system => {
|
||||
if (system instanceof GameLogicSystem) {
|
||||
system.enabled = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private onPlayerInput(data: any): void {
|
||||
// 处理玩家输入
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// 清理事件监听
|
||||
this.eventSystem.clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 初始化顺序
|
||||
|
||||
```typescript
|
||||
class ProperInitScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 1. 首先设置场景配置
|
||||
this.name = "GameScene";
|
||||
|
||||
// 2. 然后添加系统(按依赖顺序)
|
||||
this.addSystem(new InputSystem());
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new PhysicsSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
|
||||
// 3. 最后创建实体
|
||||
this.createEntities();
|
||||
|
||||
// 4. 设置事件监听
|
||||
this.setupEvents();
|
||||
}
|
||||
|
||||
private createEntities(): void {
|
||||
// 创建实体
|
||||
}
|
||||
|
||||
private setupEvents(): void {
|
||||
// 设置事件监听
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 完整示例
|
||||
|
||||
```typescript
|
||||
import { Scene, EntitySystem, Entity, Matcher } from '@esengine/ecs-framework';
|
||||
|
||||
// 定义组件
|
||||
class Transform {
|
||||
constructor(public x: number, public y: number) {}
|
||||
}
|
||||
|
||||
class Velocity {
|
||||
constructor(public vx: number, public vy: number) {}
|
||||
}
|
||||
|
||||
class Health {
|
||||
constructor(public value: number) {}
|
||||
}
|
||||
|
||||
// 定义系统
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Transform, Velocity));
|
||||
}
|
||||
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const transform = entity.getComponent(Transform);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
if (transform && velocity) {
|
||||
transform.x += velocity.vx;
|
||||
transform.y += velocity.vy;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 定义场景
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "GameScene";
|
||||
|
||||
// 添加系统
|
||||
this.addSystem(new MovementSystem());
|
||||
|
||||
// 创建玩家
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Transform(400, 300));
|
||||
player.addComponent(new Velocity(0, 0));
|
||||
player.addComponent(new Health(100));
|
||||
|
||||
// 创建敌人
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const enemy = this.createEntity(`Enemy_${i}`);
|
||||
enemy.addComponent(new Transform(
|
||||
Math.random() * 800,
|
||||
Math.random() * 600
|
||||
));
|
||||
enemy.addComponent(new Velocity(
|
||||
Math.random() * 100 - 50,
|
||||
Math.random() * 100 - 50
|
||||
));
|
||||
enemy.addComponent(new Health(50));
|
||||
}
|
||||
|
||||
// 设置事件监听
|
||||
this.eventSystem.on('player_died', () => {
|
||||
console.log('玩家死亡!');
|
||||
});
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log('游戏场景启动');
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
console.log('游戏场景卸载');
|
||||
this.eventSystem.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// 使用场景
|
||||
// 方式1:通过 SceneManager(推荐)
|
||||
import { Core, SceneManager } from '@esengine/ecs-framework';
|
||||
|
||||
Core.create({ debug: true });
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
sceneManager.setScene(new GameScene());
|
||||
|
||||
// 方式2:通过 WorldManager(高级用例)
|
||||
import { WorldManager } from '@esengine/ecs-framework';
|
||||
|
||||
const worldManager = Core.services.resolve(WorldManager);
|
||||
const world = worldManager.createWorld('game');
|
||||
world.createScene('main', new GameScene());
|
||||
world.setSceneActive('main', true);
|
||||
```
|
||||
|
||||
## 下一步
|
||||
|
||||
- 了解 [SceneManager](./scene-manager.md) - 适用于大多数游戏的简单场景管理
|
||||
- 了解 [WorldManager](./world-manager.md) - 适用于需要多世界隔离的高级场景
|
||||
|
||||
场景是 ECS 框架的核心容器,正确使用场景管理能让你的游戏架构更加清晰、模块化和易于维护。
|
||||
923
docs/guide/serialization.md
Normal file
923
docs/guide/serialization.md
Normal file
@@ -0,0 +1,923 @@
|
||||
# 序列化系统
|
||||
|
||||
序列化系统提供了完整的场景、实体和组件数据持久化方案,支持全量序列化和增量序列化两种模式,适用于游戏存档、网络同步、场景编辑器、时间回溯等场景。
|
||||
|
||||
## 基本概念
|
||||
|
||||
序列化系统分为两个层次:
|
||||
|
||||
- **全量序列化**:序列化完整的场景状态,包括所有实体、组件和场景数据
|
||||
- **增量序列化**:只序列化相对于基础快照的变更部分,大幅减少数据量
|
||||
|
||||
### 支持的数据格式
|
||||
|
||||
- **JSON格式**:人类可读,便于调试和编辑
|
||||
- **Binary格式**:使用MessagePack,体积更小,性能更高
|
||||
|
||||
> **📢 v2.2.2 重要变更**
|
||||
>
|
||||
> 从 v2.2.2 开始,二进制序列化格式返回 `Uint8Array` 而非 Node.js 的 `Buffer`,以确保浏览器兼容性:
|
||||
> - `serialize({ format: 'binary' })` 返回 `string | Uint8Array`(原为 `string | Buffer`)
|
||||
> - `deserialize(data)` 接收 `string | Uint8Array`(原为 `string | Buffer`)
|
||||
> - `applyIncremental(data)` 接收 `IncrementalSnapshot | string | Uint8Array`(原为包含 `Buffer`)
|
||||
>
|
||||
> **迁移影响**:
|
||||
> - ✅ **运行时兼容**:Node.js 的 `Buffer` 继承自 `Uint8Array`,现有代码可直接运行
|
||||
> - ⚠️ **类型检查**:如果你的 TypeScript 代码中显式使用了 `Buffer` 类型,需要改为 `Uint8Array`
|
||||
> - ✅ **浏览器支持**:`Uint8Array` 是标准 JavaScript 类型,所有现代浏览器都支持
|
||||
|
||||
## 全量序列化
|
||||
|
||||
### 基础用法
|
||||
|
||||
#### 1. 标记可序列化组件
|
||||
|
||||
使用 `@Serializable` 和 `@Serialize` 装饰器标记需要序列化的组件和字段:
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent, Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSComponent('Player')
|
||||
@Serializable({ version: 1 })
|
||||
class PlayerComponent extends Component {
|
||||
@Serialize()
|
||||
public name: string = '';
|
||||
|
||||
@Serialize()
|
||||
public level: number = 1;
|
||||
|
||||
@Serialize()
|
||||
public experience: number = 0;
|
||||
|
||||
@Serialize()
|
||||
public position: { x: number; y: number } = { x: 0, y: 0 };
|
||||
|
||||
// 不使用 @Serialize() 的字段不会被序列化
|
||||
private tempData: any = null;
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 序列化场景
|
||||
|
||||
```typescript
|
||||
// JSON格式序列化
|
||||
const jsonData = scene.serialize({
|
||||
format: 'json',
|
||||
pretty: true // 美化输出
|
||||
});
|
||||
|
||||
// 保存到本地存储
|
||||
localStorage.setItem('gameSave', jsonData);
|
||||
|
||||
// Binary格式序列化(更小的体积)
|
||||
const binaryData = scene.serialize({
|
||||
format: 'binary'
|
||||
});
|
||||
|
||||
// 保存为文件(Node.js环境)
|
||||
// 注意:binaryData 是 Uint8Array 类型,Node.js 的 fs 可以直接写入
|
||||
fs.writeFileSync('save.bin', binaryData);
|
||||
```
|
||||
|
||||
#### 3. 反序列化场景
|
||||
|
||||
```typescript
|
||||
// 从JSON恢复
|
||||
const saveData = localStorage.getItem('gameSave');
|
||||
if (saveData) {
|
||||
scene.deserialize(saveData, {
|
||||
strategy: 'replace' // 替换当前场景内容
|
||||
});
|
||||
}
|
||||
|
||||
// 从Binary恢复
|
||||
const binaryData = fs.readFileSync('save.bin');
|
||||
scene.deserialize(binaryData, {
|
||||
strategy: 'merge' // 合并到现有场景
|
||||
});
|
||||
```
|
||||
|
||||
### 序列化选项
|
||||
|
||||
#### SerializationOptions
|
||||
|
||||
```typescript
|
||||
interface SceneSerializationOptions {
|
||||
// 指定要序列化的组件类型(可选)
|
||||
components?: ComponentType[];
|
||||
|
||||
// 序列化格式:'json' 或 'binary'
|
||||
format?: 'json' | 'binary';
|
||||
|
||||
// JSON美化输出
|
||||
pretty?: boolean;
|
||||
|
||||
// 包含元数据
|
||||
includeMetadata?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
示例:
|
||||
|
||||
```typescript
|
||||
// 只序列化特定组件类型
|
||||
const saveData = scene.serialize({
|
||||
format: 'json',
|
||||
components: [PlayerComponent, InventoryComponent],
|
||||
pretty: true,
|
||||
includeMetadata: true
|
||||
});
|
||||
```
|
||||
|
||||
#### DeserializationOptions
|
||||
|
||||
```typescript
|
||||
interface SceneDeserializationOptions {
|
||||
// 反序列化策略
|
||||
strategy?: 'merge' | 'replace';
|
||||
|
||||
// 组件类型注册表(可选,默认使用全局注册表)
|
||||
componentRegistry?: Map<string, ComponentType>;
|
||||
}
|
||||
```
|
||||
|
||||
### 高级装饰器
|
||||
|
||||
#### 字段序列化选项
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Advanced')
|
||||
@Serializable({ version: 1 })
|
||||
class AdvancedComponent extends Component {
|
||||
// 使用别名
|
||||
@Serialize({ alias: 'playerName' })
|
||||
public name: string = '';
|
||||
|
||||
// 自定义序列化器
|
||||
@Serialize({
|
||||
serializer: (value: Date) => value.toISOString(),
|
||||
deserializer: (value: string) => new Date(value)
|
||||
})
|
||||
public createdAt: Date = new Date();
|
||||
|
||||
// 忽略序列化
|
||||
@IgnoreSerialization()
|
||||
public cachedData: any = null;
|
||||
}
|
||||
```
|
||||
|
||||
#### 集合类型序列化
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Collections')
|
||||
@Serializable({ version: 1 })
|
||||
class CollectionsComponent extends Component {
|
||||
// Map序列化
|
||||
@SerializeAsMap()
|
||||
public inventory: Map<string, number> = new Map();
|
||||
|
||||
// Set序列化
|
||||
@SerializeAsSet()
|
||||
public acquiredSkills: Set<string> = new Set();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.inventory.set('gold', 100);
|
||||
this.inventory.set('silver', 50);
|
||||
this.acquiredSkills.add('attack');
|
||||
this.acquiredSkills.add('defense');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 组件继承与序列化
|
||||
|
||||
框架完整支持组件类的继承,子类会自动继承父类的序列化字段,同时可以添加自己的字段。
|
||||
|
||||
#### 基础继承
|
||||
|
||||
```typescript
|
||||
// 基类组件
|
||||
@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;
|
||||
}
|
||||
```
|
||||
|
||||
### 场景自定义数据
|
||||
|
||||
除了实体和组件,还可以序列化场景级别的配置数据:
|
||||
|
||||
```typescript
|
||||
// 设置场景数据
|
||||
scene.sceneData.set('weather', 'rainy');
|
||||
scene.sceneData.set('difficulty', 'hard');
|
||||
scene.sceneData.set('checkpoint', { x: 100, y: 200 });
|
||||
|
||||
// 序列化时会自动包含场景数据
|
||||
const saveData = scene.serialize({ format: 'json' });
|
||||
|
||||
// 反序列化后场景数据会恢复
|
||||
scene.deserialize(saveData);
|
||||
console.log(scene.sceneData.get('weather')); // 'rainy'
|
||||
```
|
||||
|
||||
## 增量序列化
|
||||
|
||||
增量序列化只保存场景的变更部分,适用于网络同步、撤销/重做、时间回溯等需要频繁保存状态的场景。
|
||||
|
||||
### 基础用法
|
||||
|
||||
#### 1. 创建基础快照
|
||||
|
||||
```typescript
|
||||
// 在需要开始记录变更前创建基础快照
|
||||
scene.createIncrementalSnapshot();
|
||||
```
|
||||
|
||||
#### 2. 修改场景
|
||||
|
||||
```typescript
|
||||
// 添加实体
|
||||
const enemy = scene.createEntity('Enemy');
|
||||
enemy.addComponent(new PositionComponent(100, 200));
|
||||
enemy.addComponent(new HealthComponent(50));
|
||||
|
||||
// 修改组件
|
||||
const player = scene.findEntity('Player');
|
||||
const pos = player.getComponent(PositionComponent);
|
||||
pos.x = 300;
|
||||
pos.y = 400;
|
||||
|
||||
// 删除组件
|
||||
player.removeComponentByType(BuffComponent);
|
||||
|
||||
// 删除实体
|
||||
const oldEntity = scene.findEntity('ToDelete');
|
||||
oldEntity.destroy();
|
||||
|
||||
// 修改场景数据
|
||||
scene.sceneData.set('score', 1000);
|
||||
```
|
||||
|
||||
#### 3. 获取增量变更
|
||||
|
||||
```typescript
|
||||
// 获取相对于基础快照的所有变更
|
||||
const incremental = scene.serializeIncremental();
|
||||
|
||||
// 查看变更统计
|
||||
const stats = IncrementalSerializer.getIncrementalStats(incremental);
|
||||
console.log('总变更数:', stats.totalChanges);
|
||||
console.log('新增实体:', stats.addedEntities);
|
||||
console.log('删除实体:', stats.removedEntities);
|
||||
console.log('新增组件:', stats.addedComponents);
|
||||
console.log('更新组件:', stats.updatedComponents);
|
||||
```
|
||||
|
||||
#### 4. 序列化增量数据
|
||||
|
||||
```typescript
|
||||
// JSON格式(默认)
|
||||
const jsonData = IncrementalSerializer.serializeIncremental(incremental, {
|
||||
format: 'json'
|
||||
});
|
||||
|
||||
// 二进制格式(更小的体积,更高性能)
|
||||
const binaryData = IncrementalSerializer.serializeIncremental(incremental, {
|
||||
format: 'binary'
|
||||
});
|
||||
|
||||
// 美化JSON输出(便于调试)
|
||||
const prettyJson = IncrementalSerializer.serializeIncremental(incremental, {
|
||||
format: 'json',
|
||||
pretty: true
|
||||
});
|
||||
|
||||
// 发送或保存
|
||||
socket.send(binaryData); // 网络传输使用二进制
|
||||
localStorage.setItem('changes', jsonData); // 本地存储可用JSON
|
||||
```
|
||||
|
||||
#### 5. 应用增量变更
|
||||
|
||||
```typescript
|
||||
// 在另一个场景应用变更
|
||||
const otherScene = new Scene();
|
||||
|
||||
// 直接应用增量对象
|
||||
otherScene.applyIncremental(incremental);
|
||||
|
||||
// 从JSON字符串应用
|
||||
const jsonData = IncrementalSerializer.serializeIncremental(incremental, { format: 'json' });
|
||||
otherScene.applyIncremental(jsonData);
|
||||
|
||||
// 从二进制Uint8Array应用
|
||||
const binaryData = IncrementalSerializer.serializeIncremental(incremental, { format: 'binary' });
|
||||
otherScene.applyIncremental(binaryData);
|
||||
```
|
||||
|
||||
### 增量快照管理
|
||||
|
||||
#### 更新快照基准
|
||||
|
||||
在应用增量变更后,可以更新快照基准:
|
||||
|
||||
```typescript
|
||||
// 创建初始快照
|
||||
scene.createIncrementalSnapshot();
|
||||
|
||||
// 第一次修改
|
||||
entity.addComponent(new VelocityComponent(5, 0));
|
||||
const incremental1 = scene.serializeIncremental();
|
||||
|
||||
// 更新基准(将当前状态设为新的基准)
|
||||
scene.updateIncrementalSnapshot();
|
||||
|
||||
// 第二次修改(增量将基于更新后的基准)
|
||||
entity.getComponent(VelocityComponent).dx = 10;
|
||||
const incremental2 = scene.serializeIncremental();
|
||||
```
|
||||
|
||||
#### 清除快照
|
||||
|
||||
```typescript
|
||||
// 释放快照占用的内存
|
||||
scene.clearIncrementalSnapshot();
|
||||
|
||||
// 检查是否有快照
|
||||
if (scene.hasIncrementalSnapshot()) {
|
||||
console.log('存在增量快照');
|
||||
}
|
||||
```
|
||||
|
||||
### 增量序列化选项
|
||||
|
||||
```typescript
|
||||
interface IncrementalSerializationOptions {
|
||||
// 是否进行组件数据的深度对比
|
||||
// 默认true,设为false可提升性能但可能漏掉组件内部字段变更
|
||||
deepComponentComparison?: boolean;
|
||||
|
||||
// 是否跟踪场景数据变更
|
||||
// 默认true
|
||||
trackSceneData?: boolean;
|
||||
|
||||
// 是否压缩快照(使用JSON序列化)
|
||||
// 默认false
|
||||
compressSnapshot?: boolean;
|
||||
|
||||
// 序列化格式
|
||||
// 'json': JSON格式(可读性好,方便调试)
|
||||
// 'binary': MessagePack二进制格式(体积小,性能高)
|
||||
// 默认 'json'
|
||||
format?: 'json' | 'binary';
|
||||
|
||||
// 是否美化JSON输出(仅在format='json'时有效)
|
||||
// 默认false
|
||||
pretty?: boolean;
|
||||
}
|
||||
|
||||
// 使用选项
|
||||
scene.createIncrementalSnapshot({
|
||||
deepComponentComparison: true,
|
||||
trackSceneData: true
|
||||
});
|
||||
```
|
||||
|
||||
### 增量数据结构
|
||||
|
||||
增量快照包含以下变更类型:
|
||||
|
||||
```typescript
|
||||
interface IncrementalSnapshot {
|
||||
version: number; // 快照版本号
|
||||
timestamp: number; // 时间戳
|
||||
sceneName: string; // 场景名称
|
||||
baseVersion: number; // 基础版本号
|
||||
entityChanges: EntityChange[]; // 实体变更
|
||||
componentChanges: ComponentChange[]; // 组件变更
|
||||
sceneDataChanges: SceneDataChange[]; // 场景数据变更
|
||||
}
|
||||
|
||||
// 变更操作类型
|
||||
enum ChangeOperation {
|
||||
EntityAdded = 'entity_added',
|
||||
EntityRemoved = 'entity_removed',
|
||||
EntityUpdated = 'entity_updated',
|
||||
ComponentAdded = 'component_added',
|
||||
ComponentRemoved = 'component_removed',
|
||||
ComponentUpdated = 'component_updated',
|
||||
SceneDataUpdated = 'scene_data_updated'
|
||||
}
|
||||
```
|
||||
|
||||
## 版本迁移
|
||||
|
||||
当组件结构发生变化时,版本迁移系统可以自动升级旧版本的存档数据。
|
||||
|
||||
### 注册迁移函数
|
||||
|
||||
```typescript
|
||||
import { VersionMigrationManager } from '@esengine/ecs-framework';
|
||||
|
||||
// 假设 PlayerComponent v1 有 hp 字段
|
||||
// v2 改为 health 和 maxHealth 字段
|
||||
|
||||
// 注册从版本1到版本2的迁移
|
||||
VersionMigrationManager.registerComponentMigration(
|
||||
'Player',
|
||||
1, // 从版本
|
||||
2, // 到版本
|
||||
(data) => {
|
||||
// 迁移逻辑
|
||||
const newData = {
|
||||
...data,
|
||||
health: data.hp,
|
||||
maxHealth: data.hp,
|
||||
};
|
||||
delete newData.hp;
|
||||
return newData;
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### 使用迁移构建器
|
||||
|
||||
```typescript
|
||||
import { MigrationBuilder } from '@esengine/ecs-framework';
|
||||
|
||||
new MigrationBuilder()
|
||||
.forComponent('Player')
|
||||
.fromVersionToVersion(2, 3)
|
||||
.migrate((data) => {
|
||||
// 从版本2迁移到版本3
|
||||
data.experience = data.exp || 0;
|
||||
delete data.exp;
|
||||
return data;
|
||||
});
|
||||
```
|
||||
|
||||
### 场景级迁移
|
||||
|
||||
```typescript
|
||||
// 注册场景级迁移
|
||||
VersionMigrationManager.registerSceneMigration(
|
||||
1, // 从版本
|
||||
2, // 到版本
|
||||
(scene) => {
|
||||
// 迁移场景结构
|
||||
scene.metadata = {
|
||||
...scene.metadata,
|
||||
migratedFrom: 1
|
||||
};
|
||||
return scene;
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### 检查迁移路径
|
||||
|
||||
```typescript
|
||||
// 检查是否可以迁移
|
||||
const canMigrate = VersionMigrationManager.canMigrateComponent(
|
||||
'Player',
|
||||
1, // 从版本
|
||||
3 // 到版本
|
||||
);
|
||||
|
||||
if (canMigrate) {
|
||||
// 可以安全迁移
|
||||
scene.deserialize(oldSaveData);
|
||||
}
|
||||
|
||||
// 获取迁移路径
|
||||
const path = VersionMigrationManager.getComponentMigrationPath('Player');
|
||||
console.log('可用迁移版本:', path); // [1, 2, 3]
|
||||
```
|
||||
|
||||
## 使用场景
|
||||
|
||||
### 游戏存档系统
|
||||
|
||||
```typescript
|
||||
class SaveSystem {
|
||||
private static SAVE_KEY = 'game_save';
|
||||
|
||||
// 保存游戏
|
||||
public static saveGame(scene: Scene): void {
|
||||
const saveData = scene.serialize({
|
||||
format: 'json',
|
||||
pretty: false
|
||||
});
|
||||
|
||||
localStorage.setItem(this.SAVE_KEY, saveData);
|
||||
console.log('游戏已保存');
|
||||
}
|
||||
|
||||
// 加载游戏
|
||||
public static loadGame(scene: Scene): boolean {
|
||||
const saveData = localStorage.getItem(this.SAVE_KEY);
|
||||
if (saveData) {
|
||||
scene.deserialize(saveData, {
|
||||
strategy: 'replace'
|
||||
});
|
||||
console.log('游戏已加载');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否有存档
|
||||
public static hasSave(): boolean {
|
||||
return localStorage.getItem(this.SAVE_KEY) !== null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 网络同步
|
||||
|
||||
```typescript
|
||||
class NetworkSync {
|
||||
private baseSnapshot?: any;
|
||||
private syncInterval: number = 100; // 100ms同步一次
|
||||
|
||||
constructor(private scene: Scene, private socket: WebSocket) {
|
||||
this.setupSync();
|
||||
}
|
||||
|
||||
private setupSync(): void {
|
||||
// 创建基础快照
|
||||
this.scene.createIncrementalSnapshot();
|
||||
|
||||
// 定期发送增量
|
||||
setInterval(() => {
|
||||
this.sendIncremental();
|
||||
}, this.syncInterval);
|
||||
|
||||
// 接收远程增量
|
||||
this.socket.onmessage = (event) => {
|
||||
this.receiveIncremental(event.data);
|
||||
};
|
||||
}
|
||||
|
||||
private sendIncremental(): void {
|
||||
const incremental = this.scene.serializeIncremental();
|
||||
const stats = IncrementalSerializer.getIncrementalStats(incremental);
|
||||
|
||||
// 只在有变更时发送
|
||||
if (stats.totalChanges > 0) {
|
||||
// 使用二进制格式减少网络传输量
|
||||
const binaryData = IncrementalSerializer.serializeIncremental(incremental, {
|
||||
format: 'binary'
|
||||
});
|
||||
this.socket.send(binaryData);
|
||||
|
||||
// 更新基准
|
||||
this.scene.updateIncrementalSnapshot();
|
||||
}
|
||||
}
|
||||
|
||||
private receiveIncremental(data: ArrayBuffer): void {
|
||||
// 直接应用二进制数据(ArrayBuffer 转 Uint8Array)
|
||||
const uint8Array = new Uint8Array(data);
|
||||
this.scene.applyIncremental(uint8Array);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 撤销/重做系统
|
||||
|
||||
```typescript
|
||||
class UndoRedoSystem {
|
||||
private history: IncrementalSnapshot[] = [];
|
||||
private currentIndex: number = -1;
|
||||
private maxHistory: number = 50;
|
||||
|
||||
constructor(private scene: Scene) {
|
||||
// 创建初始快照
|
||||
this.scene.createIncrementalSnapshot();
|
||||
this.saveState('Initial');
|
||||
}
|
||||
|
||||
// 保存当前状态
|
||||
public saveState(label: string): void {
|
||||
const incremental = this.scene.serializeIncremental();
|
||||
|
||||
// 删除当前位置之后的历史
|
||||
this.history = this.history.slice(0, this.currentIndex + 1);
|
||||
|
||||
// 添加新状态
|
||||
this.history.push(incremental);
|
||||
this.currentIndex++;
|
||||
|
||||
// 限制历史记录数量
|
||||
if (this.history.length > this.maxHistory) {
|
||||
this.history.shift();
|
||||
this.currentIndex--;
|
||||
}
|
||||
|
||||
// 更新快照基准
|
||||
this.scene.updateIncrementalSnapshot();
|
||||
}
|
||||
|
||||
// 撤销
|
||||
public undo(): boolean {
|
||||
if (this.currentIndex > 0) {
|
||||
this.currentIndex--;
|
||||
const incremental = this.history[this.currentIndex];
|
||||
this.scene.applyIncremental(incremental);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 重做
|
||||
public redo(): boolean {
|
||||
if (this.currentIndex < this.history.length - 1) {
|
||||
this.currentIndex++;
|
||||
const incremental = this.history[this.currentIndex];
|
||||
this.scene.applyIncremental(incremental);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public canUndo(): boolean {
|
||||
return this.currentIndex > 0;
|
||||
}
|
||||
|
||||
public canRedo(): boolean {
|
||||
return this.currentIndex < this.history.length - 1;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 关卡编辑器
|
||||
|
||||
```typescript
|
||||
class LevelEditor {
|
||||
// 导出关卡
|
||||
public exportLevel(scene: Scene, filename: string): void {
|
||||
const levelData = scene.serialize({
|
||||
format: 'json',
|
||||
pretty: true,
|
||||
includeMetadata: true
|
||||
});
|
||||
|
||||
// 浏览器环境
|
||||
const blob = new Blob([levelData], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// 导入关卡
|
||||
public importLevel(scene: Scene, fileContent: string): void {
|
||||
scene.deserialize(fileContent, {
|
||||
strategy: 'replace'
|
||||
});
|
||||
}
|
||||
|
||||
// 验证关卡数据
|
||||
public validateLevel(saveData: string): boolean {
|
||||
const validation = SceneSerializer.validate(saveData);
|
||||
if (!validation.valid) {
|
||||
console.error('关卡数据无效:', validation.errors);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 获取关卡信息(不完全反序列化)
|
||||
public getLevelInfo(saveData: string): any {
|
||||
const info = SceneSerializer.getInfo(saveData);
|
||||
return info;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 性能优化建议
|
||||
|
||||
### 1. 选择合适的格式
|
||||
|
||||
- **开发阶段**:使用JSON格式,便于调试和查看
|
||||
- **生产环境**:使用Binary格式,减少30-50%的数据大小
|
||||
|
||||
### 2. 按需序列化
|
||||
|
||||
```typescript
|
||||
// 只序列化需要持久化的组件
|
||||
const saveData = scene.serialize({
|
||||
format: 'binary',
|
||||
components: [PlayerComponent, InventoryComponent, QuestComponent]
|
||||
});
|
||||
```
|
||||
|
||||
### 3. 增量序列化优化
|
||||
|
||||
```typescript
|
||||
// 对于高频同步,关闭深度对比以提升性能
|
||||
scene.createIncrementalSnapshot({
|
||||
deepComponentComparison: false // 只检测组件的添加/删除
|
||||
});
|
||||
```
|
||||
|
||||
### 4. 批量操作
|
||||
|
||||
```typescript
|
||||
// 批量修改后再序列化
|
||||
scene.entities.buffer.forEach(entity => {
|
||||
// 批量修改
|
||||
});
|
||||
|
||||
// 一次性序列化所有变更
|
||||
const incremental = scene.serializeIncremental();
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 明确序列化字段
|
||||
|
||||
```typescript
|
||||
// 明确标记需要序列化的字段
|
||||
@ECSComponent('Player')
|
||||
@Serializable({ version: 1 })
|
||||
class PlayerComponent extends Component {
|
||||
@Serialize()
|
||||
public name: string = '';
|
||||
|
||||
@Serialize()
|
||||
public level: number = 1;
|
||||
|
||||
// 运行时数据不序列化
|
||||
private _cachedSprite: any = null;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 使用版本控制
|
||||
|
||||
```typescript
|
||||
// 为组件指定版本
|
||||
@Serializable({ version: 2 })
|
||||
class PlayerComponent extends Component {
|
||||
// 版本2的字段
|
||||
}
|
||||
|
||||
// 注册迁移函数确保兼容性
|
||||
VersionMigrationManager.registerComponentMigration('Player', 1, 2, migrateV1ToV2);
|
||||
```
|
||||
|
||||
### 3. 避免循环引用
|
||||
|
||||
```typescript
|
||||
// 不要在组件中直接引用其他实体
|
||||
@ECSComponent('Follower')
|
||||
@Serializable({ version: 1 })
|
||||
class FollowerComponent extends Component {
|
||||
// 存储实体ID而不是实体引用
|
||||
@Serialize()
|
||||
public targetId: number = 0;
|
||||
|
||||
// 通过场景查找目标实体
|
||||
public getTarget(scene: Scene): Entity | null {
|
||||
return scene.entities.findEntityById(this.targetId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 压缩大数据
|
||||
|
||||
```typescript
|
||||
// 对于大型数据结构,使用自定义序列化
|
||||
@ECSComponent('LargeData')
|
||||
@Serializable({ version: 1 })
|
||||
class LargeDataComponent extends Component {
|
||||
@Serialize({
|
||||
serializer: (data: LargeObject) => compressData(data),
|
||||
deserializer: (data: CompressedData) => decompressData(data)
|
||||
})
|
||||
public data: LargeObject;
|
||||
}
|
||||
```
|
||||
|
||||
## API参考
|
||||
|
||||
### 全量序列化API
|
||||
|
||||
- [`Scene.serialize()`](/api/classes/Scene#serialize) - 序列化场景
|
||||
- [`Scene.deserialize()`](/api/classes/Scene#deserialize) - 反序列化场景
|
||||
- [`SceneSerializer`](/api/classes/SceneSerializer) - 场景序列化器
|
||||
- [`ComponentSerializer`](/api/classes/ComponentSerializer) - 组件序列化器
|
||||
|
||||
### 增量序列化API
|
||||
|
||||
- [`Scene.createIncrementalSnapshot()`](/api/classes/Scene#createincrementalsnapshot) - 创建基础快照
|
||||
- [`Scene.serializeIncremental()`](/api/classes/Scene#serializeincremental) - 获取增量变更
|
||||
- [`Scene.applyIncremental()`](/api/classes/Scene#applyincremental) - 应用增量变更(支持IncrementalSnapshot对象、JSON字符串或二进制Uint8Array)
|
||||
- [`Scene.updateIncrementalSnapshot()`](/api/classes/Scene#updateincrementalsnapshot) - 更新快照基准
|
||||
- [`Scene.clearIncrementalSnapshot()`](/api/classes/Scene#clearincrementalsnapshot) - 清除快照
|
||||
- [`Scene.hasIncrementalSnapshot()`](/api/classes/Scene#hasincrementalsnapshot) - 检查是否有快照
|
||||
- [`IncrementalSerializer`](/api/classes/IncrementalSerializer) - 增量序列化器
|
||||
- [`IncrementalSnapshot`](/api/interfaces/IncrementalSnapshot) - 增量快照接口
|
||||
- [`IncrementalSerializationOptions`](/api/interfaces/IncrementalSerializationOptions) - 增量序列化选项
|
||||
- [`IncrementalSerializationFormat`](/api/type-aliases/IncrementalSerializationFormat) - 序列化格式类型
|
||||
|
||||
### 版本迁移API
|
||||
|
||||
- [`VersionMigrationManager`](/api/classes/VersionMigrationManager) - 版本迁移管理器
|
||||
- `VersionMigrationManager.registerComponentMigration()` - 注册组件迁移
|
||||
- `VersionMigrationManager.registerSceneMigration()` - 注册场景迁移
|
||||
- `VersionMigrationManager.canMigrateComponent()` - 检查是否可以迁移
|
||||
- `VersionMigrationManager.getComponentMigrationPath()` - 获取迁移路径
|
||||
|
||||
序列化系统是构建完整游戏的重要基础设施,合理使用可以实现强大的功能,如存档系统、网络同步、关卡编辑器等。
|
||||
828
docs/guide/service-container.md
Normal file
828
docs/guide/service-container.md
Normal file
@@ -0,0 +1,828 @@
|
||||
# 服务容器
|
||||
|
||||
服务容器(ServiceContainer)是 ECS Framework 的依赖注入容器,负责管理框架中所有服务的注册、解析和生命周期。通过服务容器,你可以实现松耦合的架构设计,提高代码的可测试性和可维护性。
|
||||
|
||||
## 概述
|
||||
|
||||
### 什么是服务容器
|
||||
|
||||
服务容器是一个轻量级的依赖注入(DI)容器,它提供了:
|
||||
|
||||
- **服务注册**: 将服务类型注册到容器中
|
||||
- **服务解析**: 从容器中获取服务实例
|
||||
- **生命周期管理**: 自动管理服务实例的创建和销毁
|
||||
- **依赖注入**: 自动解析服务之间的依赖关系
|
||||
|
||||
### 核心概念
|
||||
|
||||
#### 服务(Service)
|
||||
|
||||
服务是实现了 `IService` 接口的类,必须提供 `dispose()` 方法用于资源清理:
|
||||
|
||||
```typescript
|
||||
import { IService } from '@esengine/ecs-framework';
|
||||
|
||||
class MyService implements IService {
|
||||
constructor() {
|
||||
// 初始化逻辑
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
// 清理资源
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 服务标识符(ServiceIdentifier)
|
||||
|
||||
服务标识符用于在容器中唯一标识一个服务,支持两种类型:
|
||||
|
||||
- **类构造函数**: 直接使用服务类作为标识符,适用于具体实现类
|
||||
- **Symbol**: 使用 Symbol 作为标识符,适用于接口抽象(推荐用于插件和跨包场景)
|
||||
|
||||
```typescript
|
||||
// 方式1: 使用类作为标识符
|
||||
Core.services.registerSingleton(DataService);
|
||||
const data = Core.services.resolve(DataService);
|
||||
|
||||
// 方式2: 使用 Symbol 作为标识符(推荐用于接口)
|
||||
const IFileSystem = Symbol.for('IFileSystem');
|
||||
Core.services.registerInstance(IFileSystem, new TauriFileSystem());
|
||||
const fs = Core.services.resolve<IFileSystem>(IFileSystem);
|
||||
```
|
||||
|
||||
> **提示**: 使用 `Symbol.for()` 而非 `Symbol()` 可确保跨包/跨模块共享同一个标识符。详见[高级用法 - 接口与 Symbol 标识符模式](#接口与-symbol-标识符模式)。
|
||||
|
||||
#### 生命周期
|
||||
|
||||
服务容器支持两种生命周期:
|
||||
|
||||
- **Singleton(单例)**: 整个应用程序生命周期内只有一个实例,所有解析请求返回同一个实例
|
||||
- **Transient(瞬时)**: 每次解析都创建新的实例
|
||||
|
||||
## 基础使用
|
||||
|
||||
### 访问服务容器
|
||||
|
||||
ECS Framework 提供了三级服务容器:
|
||||
|
||||
> **版本说明**:World 服务容器功能在 v2.2.13+ 版本中可用
|
||||
|
||||
#### Core 级别服务容器
|
||||
|
||||
应用程序全局服务容器,可以通过 `Core.services` 访问:
|
||||
|
||||
```typescript
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始化Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 访问全局服务容器
|
||||
const container = Core.services;
|
||||
```
|
||||
|
||||
#### World 级别服务容器
|
||||
|
||||
每个 World 拥有独立的服务容器,用于管理 World 范围内的服务:
|
||||
|
||||
```typescript
|
||||
import { World } from '@esengine/ecs-framework';
|
||||
|
||||
// 创建 World
|
||||
const world = new World({ name: 'GameWorld' });
|
||||
|
||||
// 访问 World 级别的服务容器
|
||||
const worldContainer = world.services;
|
||||
|
||||
// 注册 World 级别的服务
|
||||
world.services.registerSingleton(RoomManager);
|
||||
```
|
||||
|
||||
#### Scene 级别服务容器
|
||||
|
||||
每个 Scene 拥有独立的服务容器,用于管理 Scene 范围内的服务:
|
||||
|
||||
```typescript
|
||||
// 访问 Scene 级别的服务容器
|
||||
const sceneContainer = scene.services;
|
||||
|
||||
// 注册 Scene 级别的服务
|
||||
scene.services.registerSingleton(PhysicsSystem);
|
||||
```
|
||||
|
||||
#### 服务容器层级
|
||||
|
||||
```
|
||||
Core.services (应用程序全局)
|
||||
└─ World.services (World 级别)
|
||||
└─ Scene.services (Scene 级别)
|
||||
```
|
||||
|
||||
不同级别的服务容器是独立的,服务不会自动向上或向下查找。选择合适的容器级别:
|
||||
|
||||
- **Core.services**: 应用程序级别的全局服务(配置、插件管理器等)
|
||||
- **World.services**: World 级别的服务(房间管理器、多人游戏状态等)
|
||||
- **Scene.services**: Scene 级别的服务(ECS 系统、场景特定逻辑等)
|
||||
|
||||
### 注册服务
|
||||
|
||||
#### 注册单例服务
|
||||
|
||||
单例服务在首次解析时创建,之后所有解析请求都返回同一个实例:
|
||||
|
||||
```typescript
|
||||
class DataService implements IService {
|
||||
private data: Map<string, any> = new Map();
|
||||
|
||||
getData(key: string) {
|
||||
return this.data.get(key);
|
||||
}
|
||||
|
||||
setData(key: string, value: any) {
|
||||
this.data.set(key, value);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.data.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// 注册单例服务
|
||||
Core.services.registerSingleton(DataService);
|
||||
```
|
||||
|
||||
#### 注册瞬时服务
|
||||
|
||||
瞬时服务每次解析都创建新实例,适用于无状态或短生命周期的服务:
|
||||
|
||||
```typescript
|
||||
class CommandService implements IService {
|
||||
execute(command: string) {
|
||||
console.log(`Executing: ${command}`);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
// 清理资源
|
||||
}
|
||||
}
|
||||
|
||||
// 注册瞬时服务
|
||||
Core.services.registerTransient(CommandService);
|
||||
```
|
||||
|
||||
#### 注册服务实例
|
||||
|
||||
直接注册已创建的实例,自动视为单例:
|
||||
|
||||
```typescript
|
||||
const config = new ConfigService();
|
||||
config.load('./config.json');
|
||||
|
||||
// 注册实例
|
||||
Core.services.registerInstance(ConfigService, config);
|
||||
```
|
||||
|
||||
#### 使用工厂函数注册
|
||||
|
||||
工厂函数允许你在创建服务时执行自定义逻辑:
|
||||
|
||||
```typescript
|
||||
Core.services.registerSingleton(LoggerService, (container) => {
|
||||
const logger = new LoggerService();
|
||||
logger.setLevel('debug');
|
||||
return logger;
|
||||
});
|
||||
```
|
||||
|
||||
### 解析服务
|
||||
|
||||
#### resolve 方法
|
||||
|
||||
解析服务实例,如果服务未注册会抛出异常:
|
||||
|
||||
```typescript
|
||||
// 解析服务
|
||||
const dataService = Core.services.resolve(DataService);
|
||||
dataService.setData('player', { name: 'Alice', score: 100 });
|
||||
|
||||
// 单例服务,多次解析返回同一个实例
|
||||
const same = Core.services.resolve(DataService);
|
||||
console.log(same === dataService); // true
|
||||
```
|
||||
|
||||
#### tryResolve 方法
|
||||
|
||||
尝试解析服务,如果未注册返回 null 而不抛出异常:
|
||||
|
||||
```typescript
|
||||
const optional = Core.services.tryResolve(OptionalService);
|
||||
if (optional) {
|
||||
optional.doSomething();
|
||||
}
|
||||
```
|
||||
|
||||
#### isRegistered 方法
|
||||
|
||||
检查服务是否已注册:
|
||||
|
||||
```typescript
|
||||
if (Core.services.isRegistered(DataService)) {
|
||||
const service = Core.services.resolve(DataService);
|
||||
}
|
||||
```
|
||||
|
||||
## 内置服务
|
||||
|
||||
Core 在初始化时自动注册了以下内置服务:
|
||||
|
||||
### TimerManager
|
||||
|
||||
定时器管理器,负责管理所有游戏定时器:
|
||||
|
||||
```typescript
|
||||
const timerManager = Core.services.resolve(TimerManager);
|
||||
|
||||
// 创建定时器
|
||||
timerManager.schedule(1.0, false, null, (timer) => {
|
||||
console.log('1秒后执行');
|
||||
});
|
||||
```
|
||||
|
||||
### PerformanceMonitor
|
||||
|
||||
性能监控器,监控游戏性能并提供优化建议:
|
||||
|
||||
```typescript
|
||||
const monitor = Core.services.resolve(PerformanceMonitor);
|
||||
|
||||
// 启用性能监控
|
||||
monitor.enable();
|
||||
|
||||
// 获取性能数据
|
||||
const fps = monitor.getFPS();
|
||||
```
|
||||
|
||||
### SceneManager
|
||||
|
||||
场景管理器,管理单场景应用的场景生命周期:
|
||||
|
||||
```typescript
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
|
||||
// 设置当前场景
|
||||
sceneManager.setScene(new GameScene());
|
||||
|
||||
// 获取当前场景
|
||||
const currentScene = sceneManager.currentScene;
|
||||
|
||||
// 延迟切换场景
|
||||
sceneManager.loadScene(new MenuScene());
|
||||
|
||||
// 更新场景
|
||||
sceneManager.update();
|
||||
```
|
||||
|
||||
### WorldManager
|
||||
|
||||
世界管理器,管理多个独立的 World 实例(高级用例):
|
||||
|
||||
```typescript
|
||||
const worldManager = Core.services.resolve(WorldManager);
|
||||
|
||||
// 创建独立的游戏世界
|
||||
const gameWorld = worldManager.createWorld('game_room_001', {
|
||||
name: 'GameRoom',
|
||||
maxScenes: 5
|
||||
});
|
||||
|
||||
// 在World中创建场景
|
||||
const scene = gameWorld.createScene('battle', new BattleScene());
|
||||
gameWorld.setSceneActive('battle', true);
|
||||
|
||||
// 更新所有World
|
||||
worldManager.updateAll();
|
||||
```
|
||||
|
||||
**适用场景**:
|
||||
- SceneManager: 适用于 95% 的游戏(单人游戏、简单多人游戏)
|
||||
- WorldManager: 适用于 MMO 服务器、游戏房间系统等需要完全隔离的多世界应用
|
||||
|
||||
### PoolManager
|
||||
|
||||
对象池管理器,管理所有对象池:
|
||||
|
||||
```typescript
|
||||
const poolManager = Core.services.resolve(PoolManager);
|
||||
|
||||
// 创建对象池
|
||||
const bulletPool = poolManager.createPool('bullets', () => new Bullet(), 100);
|
||||
```
|
||||
|
||||
### PluginManager
|
||||
|
||||
插件管理器,管理插件的安装和卸载:
|
||||
|
||||
```typescript
|
||||
const pluginManager = Core.services.resolve(PluginManager);
|
||||
|
||||
// 获取所有已安装的插件
|
||||
const plugins = pluginManager.getAllPlugins();
|
||||
```
|
||||
|
||||
## 依赖注入
|
||||
|
||||
ECS Framework 提供了装饰器来简化依赖注入。
|
||||
|
||||
### @Injectable 装饰器
|
||||
|
||||
标记类为可注入的服务:
|
||||
|
||||
```typescript
|
||||
import { Injectable, IService } from '@esengine/ecs-framework';
|
||||
|
||||
@Injectable()
|
||||
class GameService implements IService {
|
||||
constructor() {
|
||||
console.log('GameService created');
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
console.log('GameService disposed');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### @InjectProperty 装饰器
|
||||
|
||||
通过属性装饰器注入依赖。注入时机是在构造函数执行后、`onInitialize()` 调用前完成:
|
||||
|
||||
```typescript
|
||||
import { Injectable, InjectProperty, IService } from '@esengine/ecs-framework';
|
||||
|
||||
@Injectable()
|
||||
class PlayerService implements IService {
|
||||
@InjectProperty(DataService)
|
||||
private data!: DataService;
|
||||
|
||||
@InjectProperty(GameService)
|
||||
private game!: GameService;
|
||||
|
||||
dispose(): void {
|
||||
// 清理资源
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在 EntitySystem 中使用属性注入:
|
||||
|
||||
```typescript
|
||||
@Injectable()
|
||||
class CombatSystem extends EntitySystem {
|
||||
@InjectProperty(TimeService)
|
||||
private timeService!: TimeService;
|
||||
|
||||
@InjectProperty(AudioService)
|
||||
private audio!: AudioService;
|
||||
|
||||
constructor() {
|
||||
super(Matcher.all(Health, Attack));
|
||||
}
|
||||
|
||||
onInitialize(): void {
|
||||
// 此时属性已注入完成,可以安全使用
|
||||
console.log('Delta time:', this.timeService.getDeltaTime());
|
||||
}
|
||||
|
||||
processEntity(entity: Entity): void {
|
||||
// 使用注入的服务
|
||||
this.audio.playSound('attack');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **注意**: 属性声明时使用 `!` 断言(如 `private data!: DataService`),表示该属性会在使用前被注入。
|
||||
|
||||
### 注册可注入服务
|
||||
|
||||
使用 `registerInjectable` 自动处理依赖注入:
|
||||
|
||||
```typescript
|
||||
import { registerInjectable } from '@esengine/ecs-framework';
|
||||
|
||||
// 注册服务(会自动解析 @InjectProperty 依赖)
|
||||
registerInjectable(Core.services, PlayerService);
|
||||
|
||||
// 解析时会自动注入属性依赖
|
||||
const player = Core.services.resolve(PlayerService);
|
||||
```
|
||||
|
||||
### @Updatable 装饰器
|
||||
|
||||
标记服务为可更新的,使其在每帧自动被调用:
|
||||
|
||||
```typescript
|
||||
import { Injectable, Updatable, IService, IUpdatable } from '@esengine/ecs-framework';
|
||||
|
||||
@Injectable()
|
||||
@Updatable() // 默认优先级为0
|
||||
class PhysicsService implements IService, IUpdatable {
|
||||
update(deltaTime?: number): void {
|
||||
// 每帧更新物理模拟
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
// 清理资源
|
||||
}
|
||||
}
|
||||
|
||||
// 指定更新优先级(数值越小越先执行)
|
||||
@Injectable()
|
||||
@Updatable(10)
|
||||
class RenderService implements IService, IUpdatable {
|
||||
update(deltaTime?: number): void {
|
||||
// 每帧渲染
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
// 清理资源
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
使用 `@Updatable` 装饰器的服务会被 Core 自动调用,无需手动管理:
|
||||
|
||||
```typescript
|
||||
// Core.update() 会自动调用所有@Updatable服务的update方法
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // 自动更新所有可更新服务
|
||||
}
|
||||
```
|
||||
|
||||
## 自定义服务
|
||||
|
||||
### 创建自定义服务
|
||||
|
||||
实现 `IService` 接口并注册到容器:
|
||||
|
||||
```typescript
|
||||
import { IService } from '@esengine/ecs-framework';
|
||||
|
||||
class AudioService implements IService {
|
||||
private sounds: Map<string, HTMLAudioElement> = new Map();
|
||||
|
||||
play(soundId: string) {
|
||||
const sound = this.sounds.get(soundId);
|
||||
if (sound) {
|
||||
sound.play();
|
||||
}
|
||||
}
|
||||
|
||||
load(soundId: string, url: string) {
|
||||
const audio = new Audio(url);
|
||||
this.sounds.set(soundId, audio);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
// 停止所有音效并清理
|
||||
for (const sound of this.sounds.values()) {
|
||||
sound.pause();
|
||||
sound.src = '';
|
||||
}
|
||||
this.sounds.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// 注册自定义服务
|
||||
Core.services.registerSingleton(AudioService);
|
||||
|
||||
// 使用服务
|
||||
const audio = Core.services.resolve(AudioService);
|
||||
audio.load('jump', '/sounds/jump.mp3');
|
||||
audio.play('jump');
|
||||
```
|
||||
|
||||
### 服务间依赖
|
||||
|
||||
服务可以依赖其他服务:
|
||||
|
||||
```typescript
|
||||
@Injectable()
|
||||
class ConfigService implements IService {
|
||||
private config: any = {};
|
||||
|
||||
get(key: string) {
|
||||
return this.config[key];
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.config = {};
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
class NetworkService implements IService {
|
||||
constructor(
|
||||
@Inject(ConfigService) private config: ConfigService
|
||||
) {
|
||||
// 使用配置服务
|
||||
const apiUrl = this.config.get('apiUrl');
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
// 清理网络连接
|
||||
}
|
||||
}
|
||||
|
||||
// 注册服务(按依赖顺序)
|
||||
registerInjectable(Core.services, ConfigService);
|
||||
registerInjectable(Core.services, NetworkService);
|
||||
```
|
||||
|
||||
## 高级用法
|
||||
|
||||
### 接口与 Symbol 标识符模式
|
||||
|
||||
在大型项目或需要跨平台适配的游戏中,推荐使用"接口 + Symbol.for 标识符"模式。这种模式实现了真正的依赖倒置,让代码依赖于抽象而非具体实现。
|
||||
|
||||
#### 为什么使用 Symbol.for
|
||||
|
||||
- **跨包共享**: `Symbol.for('key')` 在全局 Symbol 注册表中创建/获取 Symbol,确保不同包中使用相同的标识符
|
||||
- **接口解耦**: 消费者只依赖接口定义,不依赖具体实现类
|
||||
- **可替换实现**: 可以在运行时注入不同的实现(如测试 Mock、不同平台适配)
|
||||
|
||||
#### 定义接口和标识符
|
||||
|
||||
以音频服务为例,游戏需要在不同平台(Web、微信小游戏、原生App)使用不同的音频实现:
|
||||
|
||||
```typescript
|
||||
// IAudioService.ts - 定义接口和标识符
|
||||
export interface IAudioService {
|
||||
dispose(): void;
|
||||
playSound(id: string): void;
|
||||
playMusic(id: string, loop?: boolean): void;
|
||||
stopMusic(): void;
|
||||
setVolume(volume: number): void;
|
||||
preload(id: string, url: string): Promise<void>;
|
||||
}
|
||||
|
||||
// 使用 Symbol.for 确保跨包共享同一个 Symbol
|
||||
export const IAudioService = Symbol.for('IAudioService');
|
||||
```
|
||||
|
||||
#### 实现接口
|
||||
|
||||
```typescript
|
||||
// WebAudioService.ts - Web 平台实现
|
||||
import { IAudioService } from './IAudioService';
|
||||
|
||||
export class WebAudioService implements IAudioService {
|
||||
private audioContext: AudioContext;
|
||||
private sounds: Map<string, AudioBuffer> = new Map();
|
||||
|
||||
constructor() {
|
||||
this.audioContext = new AudioContext();
|
||||
}
|
||||
|
||||
playSound(id: string): void {
|
||||
const buffer = this.sounds.get(id);
|
||||
if (buffer) {
|
||||
const source = this.audioContext.createBufferSource();
|
||||
source.buffer = buffer;
|
||||
source.connect(this.audioContext.destination);
|
||||
source.start();
|
||||
}
|
||||
}
|
||||
|
||||
async preload(id: string, url: string): Promise<void> {
|
||||
const response = await fetch(url);
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
|
||||
this.sounds.set(id, audioBuffer);
|
||||
}
|
||||
|
||||
// ... 其他方法实现
|
||||
|
||||
dispose(): void {
|
||||
this.audioContext.close();
|
||||
this.sounds.clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// WechatAudioService.ts - 微信小游戏平台实现
|
||||
export class WechatAudioService implements IAudioService {
|
||||
private innerAudioContexts: Map<string, WechatMinigame.InnerAudioContext> = new Map();
|
||||
|
||||
playSound(id: string): void {
|
||||
const ctx = this.innerAudioContexts.get(id);
|
||||
if (ctx) {
|
||||
ctx.play();
|
||||
}
|
||||
}
|
||||
|
||||
async preload(id: string, url: string): Promise<void> {
|
||||
const ctx = wx.createInnerAudioContext();
|
||||
ctx.src = url;
|
||||
this.innerAudioContexts.set(id, ctx);
|
||||
}
|
||||
|
||||
// ... 其他方法实现
|
||||
|
||||
dispose(): void {
|
||||
for (const ctx of this.innerAudioContexts.values()) {
|
||||
ctx.destroy();
|
||||
}
|
||||
this.innerAudioContexts.clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 注册和使用
|
||||
|
||||
```typescript
|
||||
import { IAudioService } from './IAudioService';
|
||||
import { WebAudioService } from './WebAudioService';
|
||||
import { WechatAudioService } from './WechatAudioService';
|
||||
|
||||
// 根据平台注册不同实现
|
||||
if (typeof wx !== 'undefined') {
|
||||
Core.services.registerInstance(IAudioService, new WechatAudioService());
|
||||
} else {
|
||||
Core.services.registerInstance(IAudioService, new WebAudioService());
|
||||
}
|
||||
|
||||
// 业务代码中使用 - 不关心具体实现
|
||||
const audio = Core.services.resolve<IAudioService>(IAudioService);
|
||||
await audio.preload('explosion', '/sounds/explosion.mp3');
|
||||
audio.playSound('explosion');
|
||||
```
|
||||
|
||||
#### 跨模块使用
|
||||
|
||||
```typescript
|
||||
// 在游戏系统中使用
|
||||
import { IAudioService } from '@mygame/core';
|
||||
|
||||
class CombatSystem extends EntitySystem {
|
||||
private audio: IAudioService;
|
||||
|
||||
initialize(): void {
|
||||
// 获取音频服务,不需要知道具体实现
|
||||
this.audio = this.scene.services.resolve<IAudioService>(IAudioService);
|
||||
}
|
||||
|
||||
onEntityDeath(entity: Entity): void {
|
||||
this.audio.playSound('death');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Symbol vs Symbol.for
|
||||
|
||||
```typescript
|
||||
// Symbol() - 每次创建唯一的 Symbol
|
||||
const sym1 = Symbol('test');
|
||||
const sym2 = Symbol('test');
|
||||
console.log(sym1 === sym2); // false - 不同的 Symbol
|
||||
|
||||
// Symbol.for() - 在全局注册表中共享
|
||||
const sym3 = Symbol.for('test');
|
||||
const sym4 = Symbol.for('test');
|
||||
console.log(sym3 === sym4); // true - 同一个 Symbol
|
||||
|
||||
// 跨包场景
|
||||
// package-a/index.ts
|
||||
export const IMyService = Symbol.for('IMyService');
|
||||
|
||||
// package-b/index.ts (不同的包)
|
||||
const IMyService = Symbol.for('IMyService');
|
||||
// 与 package-a 中的是同一个 Symbol!
|
||||
```
|
||||
|
||||
### 循环依赖检测
|
||||
|
||||
服务容器会自动检测循环依赖:
|
||||
|
||||
```typescript
|
||||
// A 依赖 B,B 依赖 A
|
||||
@Injectable()
|
||||
class ServiceA implements IService {
|
||||
constructor(@Inject(ServiceB) b: ServiceB) {}
|
||||
dispose(): void {}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
class ServiceB implements IService {
|
||||
constructor(@Inject(ServiceA) a: ServiceA) {}
|
||||
dispose(): void {}
|
||||
}
|
||||
|
||||
// 解析时会抛出错误: Circular dependency detected: ServiceA -> ServiceB -> ServiceA
|
||||
```
|
||||
|
||||
### 获取所有服务
|
||||
|
||||
```typescript
|
||||
// 获取所有已注册的服务类型
|
||||
const types = Core.services.getRegisteredServices();
|
||||
|
||||
// 获取所有已实例化的服务实例
|
||||
const instances = Core.services.getAll();
|
||||
```
|
||||
|
||||
### 服务清理
|
||||
|
||||
```typescript
|
||||
// 注销单个服务
|
||||
Core.services.unregister(MyService);
|
||||
|
||||
// 清空所有服务(会调用每个服务的dispose方法)
|
||||
Core.services.clear();
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 服务命名
|
||||
|
||||
服务类名应该以 `Service` 或 `Manager` 结尾,清晰表达其职责:
|
||||
|
||||
```typescript
|
||||
class PlayerService implements IService {}
|
||||
class AudioManager implements IService {}
|
||||
class NetworkService implements IService {}
|
||||
```
|
||||
|
||||
### 资源清理
|
||||
|
||||
始终在 `dispose()` 方法中清理资源:
|
||||
|
||||
```typescript
|
||||
class ResourceService implements IService {
|
||||
private resources: Map<string, Resource> = new Map();
|
||||
|
||||
dispose(): void {
|
||||
// 释放所有资源
|
||||
for (const resource of this.resources.values()) {
|
||||
resource.release();
|
||||
}
|
||||
this.resources.clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 避免过度使用
|
||||
|
||||
不要把所有类都注册为服务,服务应该是:
|
||||
|
||||
- 全局单例或需要共享状态
|
||||
- 需要在多处使用
|
||||
- 生命周期需要管理
|
||||
- 需要依赖注入
|
||||
|
||||
对于简单的工具类或数据类,直接创建实例即可。
|
||||
|
||||
### 依赖方向
|
||||
|
||||
保持清晰的依赖方向,避免循环依赖:
|
||||
|
||||
```
|
||||
高层服务 -> 中层服务 -> 底层服务
|
||||
GameLogic -> DataService -> ConfigService
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 服务未注册错误
|
||||
|
||||
**问题**: `Error: Service MyService is not registered`
|
||||
|
||||
**解决**:
|
||||
```typescript
|
||||
// 确保服务已注册
|
||||
Core.services.registerSingleton(MyService);
|
||||
|
||||
// 或者使用tryResolve
|
||||
const service = Core.services.tryResolve(MyService);
|
||||
if (!service) {
|
||||
console.log('Service not found');
|
||||
}
|
||||
```
|
||||
|
||||
### 循环依赖错误
|
||||
|
||||
**问题**: `Circular dependency detected`
|
||||
|
||||
**解决**: 重新设计服务依赖关系,引入中间服务或使用事件系统解耦。
|
||||
|
||||
### 何时使用单例 vs 瞬时
|
||||
|
||||
- **单例**: 管理器类、配置、缓存、状态管理
|
||||
- **瞬时**: 命令对象、请求处理器、临时任务
|
||||
|
||||
## 相关链接
|
||||
|
||||
- [插件系统](./plugin-system.md) - 使用服务容器注册插件服务
|
||||
- [快速开始](./getting-started.md) - Core 初始化和基础使用
|
||||
- [系统架构](./system.md) - 在系统中使用服务
|
||||
735
docs/guide/system.md
Normal file
735
docs/guide/system.md
Normal file
@@ -0,0 +1,735 @@
|
||||
# 系统架构
|
||||
|
||||
在 ECS 架构中,系统(System)是处理业务逻辑的地方。系统负责对拥有特定组件组合的实体执行操作,是 ECS 架构的逻辑处理单元。
|
||||
|
||||
## 基本概念
|
||||
|
||||
系统是继承自 `EntitySystem` 抽象基类的具体类,用于:
|
||||
- 定义实体的处理逻辑(如移动、碰撞检测、渲染等)
|
||||
- 根据组件组合筛选需要处理的实体
|
||||
- 提供生命周期管理和性能监控
|
||||
- 管理实体的添加、移除事件
|
||||
|
||||
## 系统类型
|
||||
|
||||
框架提供了几种不同类型的系统基类:
|
||||
|
||||
### EntitySystem - 基础系统
|
||||
|
||||
最基础的系统类,所有其他系统都继承自它:
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, ECSSystem, Matcher } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSSystem('Movement')
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// 使用 Matcher 定义需要处理的实体条件
|
||||
super(Matcher.all(Position, Velocity));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const position = entity.getComponent(Position);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
if (position && velocity) {
|
||||
position.x += velocity.dx * Time.deltaTime;
|
||||
position.y += velocity.dy * Time.deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ProcessingSystem - 处理系统
|
||||
|
||||
适用于不需要逐个处理实体的系统:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Physics')
|
||||
class PhysicsSystem extends ProcessingSystem {
|
||||
constructor() {
|
||||
super(); // 不需要指定 Matcher
|
||||
}
|
||||
|
||||
public processSystem(): void {
|
||||
// 执行物理世界步进
|
||||
this.physicsWorld.step(Time.deltaTime);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### PassiveSystem - 被动系统
|
||||
|
||||
被动系统不进行主动处理,主要用于监听实体的添加和移除事件:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('EntityTracker')
|
||||
class EntityTrackerSystem extends PassiveSystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Health));
|
||||
}
|
||||
|
||||
protected onAdded(entity: Entity): void {
|
||||
console.log(`生命值实体被添加: ${entity.name}`);
|
||||
}
|
||||
|
||||
protected onRemoved(entity: Entity): void {
|
||||
console.log(`生命值实体被移除: ${entity.name}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### IntervalSystem - 间隔系统
|
||||
|
||||
按固定时间间隔执行的系统:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('AutoSave')
|
||||
class AutoSaveSystem extends IntervalSystem {
|
||||
constructor() {
|
||||
// 每 5 秒执行一次
|
||||
super(5.0, Matcher.all(SaveData));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
console.log('执行自动保存...');
|
||||
// 保存游戏数据
|
||||
this.saveGameData(entities);
|
||||
}
|
||||
|
||||
private saveGameData(entities: readonly Entity[]): void {
|
||||
// 保存逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### WorkerEntitySystem - 多线程系统
|
||||
|
||||
基于Web Worker的多线程处理系统,适用于计算密集型任务,能够充分利用多核CPU性能。
|
||||
|
||||
Worker系统提供了真正的并行计算能力,支持SharedArrayBuffer优化,并具有自动降级支持。特别适合物理模拟、粒子系统、AI计算等场景。
|
||||
|
||||
**详细内容请参考:[Worker系统](/guide/worker-system)**
|
||||
|
||||
## 实体匹配器 (Matcher)
|
||||
|
||||
Matcher 用于定义系统需要处理哪些实体。它提供了灵活的条件组合:
|
||||
|
||||
### 基本匹配条件
|
||||
|
||||
```typescript
|
||||
// 必须同时拥有 Position 和 Velocity 组件
|
||||
const matcher1 = Matcher.all(Position, Velocity);
|
||||
|
||||
// 至少拥有 Health 或 Shield 组件之一
|
||||
const matcher2 = Matcher.any(Health, Shield);
|
||||
|
||||
// 不能拥有 Dead 组件
|
||||
const matcher3 = Matcher.none(Dead);
|
||||
```
|
||||
|
||||
### 复合匹配条件
|
||||
|
||||
```typescript
|
||||
// 复杂的组合条件
|
||||
const complexMatcher = Matcher.all(Position, Velocity)
|
||||
.any(Player, Enemy)
|
||||
.none(Dead, Disabled);
|
||||
|
||||
@ECSSystem('Combat')
|
||||
class CombatSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(complexMatcher);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 特殊匹配条件
|
||||
|
||||
```typescript
|
||||
// 按标签匹配
|
||||
const tagMatcher = Matcher.byTag(1); // 匹配标签为 1 的实体
|
||||
|
||||
// 按名称匹配
|
||||
const nameMatcher = Matcher.byName("Player"); // 匹配名称为 "Player" 的实体
|
||||
|
||||
// 单组件匹配
|
||||
const componentMatcher = Matcher.byComponent(Health); // 匹配拥有 Health 组件的实体
|
||||
|
||||
// 不匹配任何实体
|
||||
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) 文档。
|
||||
|
||||
## 系统生命周期
|
||||
|
||||
系统提供了完整的生命周期回调:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Example')
|
||||
class ExampleSystem extends EntitySystem {
|
||||
protected onInitialize(): void {
|
||||
console.log('系统初始化');
|
||||
// 系统被添加到场景时调用,用于初始化资源
|
||||
}
|
||||
|
||||
protected onBegin(): void {
|
||||
// 每帧处理开始前调用
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 主要的处理逻辑
|
||||
for (const entity of entities) {
|
||||
// 处理每个实体
|
||||
}
|
||||
}
|
||||
|
||||
protected lateProcess(entities: readonly Entity[]): void {
|
||||
// 主处理之后的后期处理
|
||||
}
|
||||
|
||||
protected onEnd(): void {
|
||||
// 每帧处理结束后调用
|
||||
}
|
||||
|
||||
protected onDestroy(): void {
|
||||
console.log('系统销毁');
|
||||
// 系统从场景移除时调用,用于清理资源
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 实体事件监听
|
||||
|
||||
系统可以监听实体的添加和移除事件:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('EnemyManager')
|
||||
class EnemyManagerSystem extends EntitySystem {
|
||||
private enemyCount = 0;
|
||||
|
||||
constructor() {
|
||||
super(Matcher.all(Enemy, Health));
|
||||
}
|
||||
|
||||
protected onAdded(entity: Entity): void {
|
||||
this.enemyCount++;
|
||||
console.log(`敌人加入战斗,当前敌人数量: ${this.enemyCount}`);
|
||||
|
||||
// 可以在这里为新敌人设置初始状态
|
||||
const health = entity.getComponent(Health);
|
||||
if (health) {
|
||||
health.current = health.max;
|
||||
}
|
||||
}
|
||||
|
||||
protected onRemoved(entity: Entity): void {
|
||||
this.enemyCount--;
|
||||
console.log(`敌人被移除,剩余敌人数量: ${this.enemyCount}`);
|
||||
|
||||
// 检查是否所有敌人都被消灭
|
||||
if (this.enemyCount === 0) {
|
||||
this.scene?.eventSystem.emitSync('all_enemies_defeated');
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 系统属性和方法
|
||||
|
||||
### 重要属性
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Example')
|
||||
class ExampleSystem extends EntitySystem {
|
||||
showSystemInfo(): void {
|
||||
console.log(`系统名称: ${this.systemName}`); // 系统名称
|
||||
console.log(`更新顺序: ${this.updateOrder}`); // 更新时序
|
||||
console.log(`是否启用: ${this.enabled}`); // 启用状态
|
||||
console.log(`实体数量: ${this.entities.length}`); // 匹配的实体数量
|
||||
console.log(`所属场景: ${this.scene?.name}`); // 所属场景
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 实体访问
|
||||
|
||||
```typescript
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 方式1:使用参数中的实体列表
|
||||
for (const entity of entities) {
|
||||
// 处理实体
|
||||
}
|
||||
|
||||
// 方式2:使用 this.entities 属性(与参数相同)
|
||||
for (const entity of this.entities) {
|
||||
// 处理实体
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 控制系统执行
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Conditional')
|
||||
class ConditionalSystem extends EntitySystem {
|
||||
private shouldProcess = true;
|
||||
|
||||
protected onCheckProcessing(): boolean {
|
||||
// 返回 false 时跳过本次处理
|
||||
return this.shouldProcess && this.entities.length > 0;
|
||||
}
|
||||
|
||||
public pause(): void {
|
||||
this.shouldProcess = false;
|
||||
}
|
||||
|
||||
public resume(): void {
|
||||
this.shouldProcess = true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 事件系统集成
|
||||
|
||||
系统可以方便地监听和发送事件:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('GameLogic')
|
||||
class GameLogicSystem extends EntitySystem {
|
||||
protected onInitialize(): void {
|
||||
// 添加事件监听器(系统销毁时自动清理)
|
||||
this.addEventListener('player_died', this.onPlayerDied.bind(this));
|
||||
this.addEventListener('level_complete', this.onLevelComplete.bind(this));
|
||||
}
|
||||
|
||||
private onPlayerDied(data: any): void {
|
||||
console.log('玩家死亡,重新开始游戏');
|
||||
// 处理玩家死亡逻辑
|
||||
}
|
||||
|
||||
private onLevelComplete(data: any): void {
|
||||
console.log('关卡完成,加载下一关');
|
||||
// 处理关卡完成逻辑
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 在处理过程中发送事件
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health);
|
||||
if (health && health.current <= 0) {
|
||||
this.scene?.eventSystem.emitSync('entity_died', { entity });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 性能监控
|
||||
|
||||
系统内置了性能监控功能:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Performance')
|
||||
class PerformanceSystem extends EntitySystem {
|
||||
protected onEnd(): void {
|
||||
// 获取性能数据
|
||||
const perfData = this.getPerformanceData();
|
||||
if (perfData) {
|
||||
console.log(`执行时间: ${perfData.executionTime.toFixed(2)}ms`);
|
||||
}
|
||||
|
||||
// 获取性能统计
|
||||
const stats = this.getPerformanceStats();
|
||||
if (stats) {
|
||||
console.log(`平均执行时间: ${stats.averageTime.toFixed(2)}ms`);
|
||||
}
|
||||
}
|
||||
|
||||
public resetPerformance(): void {
|
||||
this.resetPerformanceData();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 系统管理
|
||||
|
||||
### 添加系统到场景
|
||||
|
||||
框架提供了两种方式添加系统:传入实例或传入类型(自动依赖注入)。
|
||||
|
||||
```typescript
|
||||
// 在场景子类中添加系统
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 方式1:传入实例
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
|
||||
// 方式2:传入类型(自动依赖注入)
|
||||
this.addEntityProcessor(PhysicsSystem);
|
||||
|
||||
// 设置系统更新顺序
|
||||
const movementSystem = this.getSystem(MovementSystem);
|
||||
if (movementSystem) {
|
||||
movementSystem.updateOrder = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 系统依赖注入
|
||||
|
||||
系统实现了 `IService` 接口,支持通过依赖注入获取其他服务或系统:
|
||||
|
||||
```typescript
|
||||
import { ECSSystem, Injectable, Inject } from '@esengine/ecs-framework';
|
||||
|
||||
@Injectable()
|
||||
@ECSSystem('Physics')
|
||||
class PhysicsSystem extends EntitySystem {
|
||||
constructor(
|
||||
@Inject(CollisionService) private collision: CollisionService
|
||||
) {
|
||||
super(Matcher.all(Transform, RigidBody));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 使用注入的服务
|
||||
this.collision.detectCollisions(entities);
|
||||
}
|
||||
|
||||
// 实现 IService 接口的 dispose 方法
|
||||
public dispose(): void {
|
||||
// 清理资源
|
||||
}
|
||||
}
|
||||
|
||||
// 使用时传入类型即可,框架会自动注入依赖
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 自动依赖注入
|
||||
this.addEntityProcessor(PhysicsSystem);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
注意事项:
|
||||
- 使用 `@Injectable()` 装饰器标记需要依赖注入的系统
|
||||
- 在构造函数参数中使用 `@Inject()` 装饰器声明依赖
|
||||
- 系统必须实现 `dispose()` 方法(IService 接口要求)
|
||||
- 使用 `addEntityProcessor(类型)` 而不是 `addSystem(new 类型())` 来启用依赖注入
|
||||
|
||||
### 系统更新顺序
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Input')
|
||||
class InputSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(InputComponent));
|
||||
this.updateOrder = -100; // 输入系统优先执行
|
||||
}
|
||||
}
|
||||
|
||||
@ECSSystem('Physics')
|
||||
class PhysicsSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(RigidBody));
|
||||
this.updateOrder = 0; // 默认顺序
|
||||
}
|
||||
}
|
||||
|
||||
@ECSSystem('Render')
|
||||
class RenderSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Sprite, Transform));
|
||||
this.updateOrder = 100; // 渲染系统最后执行
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 复杂系统示例
|
||||
|
||||
### 碰撞检测系统
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Collision')
|
||||
class CollisionSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Transform, Collider));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 简单的 n² 碰撞检测
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
for (let j = i + 1; j < entities.length; j++) {
|
||||
this.checkCollision(entities[i], entities[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private checkCollision(entityA: Entity, entityB: Entity): void {
|
||||
const transformA = entityA.getComponent(Transform);
|
||||
const transformB = entityB.getComponent(Transform);
|
||||
const colliderA = entityA.getComponent(Collider);
|
||||
const colliderB = entityB.getComponent(Collider);
|
||||
|
||||
if (this.isColliding(transformA, colliderA, transformB, colliderB)) {
|
||||
// 发送碰撞事件
|
||||
this.scene?.eventSystem.emitSync('collision', {
|
||||
entityA,
|
||||
entityB
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private isColliding(transformA: Transform, colliderA: Collider,
|
||||
transformB: Transform, colliderB: Collider): boolean {
|
||||
// 碰撞检测逻辑
|
||||
return false; // 简化示例
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 状态机系统
|
||||
|
||||
```typescript
|
||||
@ECSSystem('StateMachine')
|
||||
class StateMachineSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(StateMachine));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const stateMachine = entity.getComponent(StateMachine);
|
||||
if (stateMachine) {
|
||||
stateMachine.updateTimer(Time.deltaTime);
|
||||
this.updateState(entity, stateMachine);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateState(entity: Entity, stateMachine: StateMachine): void {
|
||||
switch (stateMachine.currentState) {
|
||||
case EntityState.Idle:
|
||||
this.handleIdleState(entity, stateMachine);
|
||||
break;
|
||||
case EntityState.Moving:
|
||||
this.handleMovingState(entity, stateMachine);
|
||||
break;
|
||||
case EntityState.Attacking:
|
||||
this.handleAttackingState(entity, stateMachine);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private handleIdleState(entity: Entity, stateMachine: StateMachine): void {
|
||||
// 空闲状态逻辑
|
||||
}
|
||||
|
||||
private handleMovingState(entity: Entity, stateMachine: StateMachine): void {
|
||||
// 移动状态逻辑
|
||||
}
|
||||
|
||||
private handleAttackingState(entity: Entity, stateMachine: StateMachine): void {
|
||||
// 攻击状态逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 系统单一职责
|
||||
|
||||
```typescript
|
||||
// ✅ 好的系统设计 - 职责单一
|
||||
@ECSSystem('Movement')
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Position, Velocity));
|
||||
}
|
||||
}
|
||||
|
||||
@ECSSystem('Rendering')
|
||||
class RenderingSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Sprite, Transform));
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ 避免的系统设计 - 职责过多
|
||||
@ECSSystem('GameSystem')
|
||||
class GameSystem extends EntitySystem {
|
||||
// 一个系统处理移动、渲染、音效等多种逻辑
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 使用 @ECSSystem 装饰器
|
||||
|
||||
`@ECSSystem` 是系统类必须使用的装饰器,它为系统提供类型标识和元数据管理。
|
||||
|
||||
#### 为什么必须使用
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| **类型识别** | 提供稳定的系统名称,代码混淆后仍能正确识别 |
|
||||
| **调试支持** | 在性能监控、日志和调试工具中显示可读的系统名称 |
|
||||
| **系统管理** | 通过名称查找和管理系统 |
|
||||
| **序列化支持** | 场景序列化时可以记录系统配置 |
|
||||
|
||||
#### 基本语法
|
||||
|
||||
```typescript
|
||||
@ECSSystem(systemName: string)
|
||||
```
|
||||
|
||||
- `systemName`: 系统的名称,建议使用描述性的名称
|
||||
|
||||
#### 使用示例
|
||||
|
||||
```typescript
|
||||
// ✅ 正确的用法
|
||||
@ECSSystem('Physics')
|
||||
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
|
||||
// 按逻辑顺序设置系统的更新时序
|
||||
@ECSSystem('Input')
|
||||
class InputSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super();
|
||||
this.updateOrder = -100; // 最先处理输入
|
||||
}
|
||||
}
|
||||
|
||||
@ECSSystem('Logic')
|
||||
class GameLogicSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super();
|
||||
this.updateOrder = 0; // 处理游戏逻辑
|
||||
}
|
||||
}
|
||||
|
||||
@ECSSystem('Render')
|
||||
class RenderSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super();
|
||||
this.updateOrder = 100; // 最后进行渲染
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 避免在系统间直接引用
|
||||
|
||||
```typescript
|
||||
// ❌ 避免:系统间直接引用
|
||||
@ECSSystem('Bad')
|
||||
class BadSystem extends EntitySystem {
|
||||
private otherSystem: SomeOtherSystem; // 避免直接引用其他系统
|
||||
}
|
||||
|
||||
// ✅ 推荐:通过事件系统通信
|
||||
@ECSSystem('Good')
|
||||
class GoodSystem extends EntitySystem {
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 通过事件系统与其他系统通信
|
||||
this.scene?.eventSystem.emitSync('data_updated', { entities });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 及时清理资源
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Resource')
|
||||
class ResourceSystem extends EntitySystem {
|
||||
private resources: Map<string, any> = new Map();
|
||||
|
||||
protected onDestroy(): void {
|
||||
// 清理资源
|
||||
for (const [key, resource] of this.resources) {
|
||||
if (resource.dispose) {
|
||||
resource.dispose();
|
||||
}
|
||||
}
|
||||
this.resources.clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
系统是 ECS 架构的逻辑处理核心,正确设计和使用系统能让你的游戏代码更加模块化、高效和易于维护。
|
||||
@@ -1,6 +1,4 @@
|
||||
---
|
||||
title: "时间和定时器系统"
|
||||
---
|
||||
# 时间和定时器系统
|
||||
|
||||
ECS 框架提供了完整的时间管理和定时器系统,包括时间缩放、帧时间计算和灵活的定时器调度功能。
|
||||
|
||||
@@ -32,64 +30,6 @@ 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 类支持时间缩放功能,可以实现慢动作、快进等效果:
|
||||
@@ -108,10 +48,10 @@ class TimeControlSystem extends EntitySystem {
|
||||
console.log('快进模式启用');
|
||||
}
|
||||
|
||||
public enableBulletTime(): void {
|
||||
// 子弹时间效果(10%速度)
|
||||
Time.timeScale = 0.1;
|
||||
console.log('子弹时间启用');
|
||||
public pauseGame(): void {
|
||||
// 暂停游戏(时间静止)
|
||||
Time.timeScale = 0;
|
||||
console.log('游戏暂停');
|
||||
}
|
||||
|
||||
public resumeNormalSpeed(): void {
|
||||
608
docs/guide/worker-system.md
Normal file
608
docs/guide/worker-system.md
Normal file
@@ -0,0 +1,608 @@
|
||||
# Worker系统
|
||||
|
||||
Worker系统(WorkerEntitySystem)是ECS框架中基于Web Worker的多线程处理系统,专为计算密集型任务设计,能够充分利用多核CPU性能,实现真正的并行计算。
|
||||
|
||||
## 核心特性
|
||||
|
||||
- **真正的并行计算**:利用Web Worker在后台线程执行计算密集型任务
|
||||
- **自动负载均衡**:根据CPU核心数自动分配工作负载
|
||||
- **SharedArrayBuffer优化**:零拷贝数据共享,提升大规模计算性能
|
||||
- **降级支持**:不支持Worker时自动回退到主线程处理
|
||||
- **类型安全**:完整的TypeScript支持和类型检查
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 简单的物理系统示例
|
||||
|
||||
```typescript
|
||||
interface PhysicsData {
|
||||
id: number;
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
mass: number;
|
||||
radius: number;
|
||||
}
|
||||
|
||||
@ECSSystem('Physics')
|
||||
class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsData> {
|
||||
constructor() {
|
||||
super(Matcher.all(Position, Velocity, Physics), {
|
||||
enableWorker: true, // 启用Worker并行处理
|
||||
workerCount: 8, // Worker数量,系统会自动限制在硬件支持范围内
|
||||
entitiesPerWorker: 100, // 每个Worker处理的实体数量
|
||||
useSharedArrayBuffer: true, // 启用SharedArrayBuffer优化
|
||||
entityDataSize: 7, // 每个实体数据大小
|
||||
maxEntities: 10000, // 最大实体数量
|
||||
systemConfig: { // 传递给Worker的配置
|
||||
gravity: 100,
|
||||
friction: 0.95
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 数据提取:将Entity转换为可序列化的数据
|
||||
protected extractEntityData(entity: Entity): PhysicsData {
|
||||
const position = entity.getComponent(Position);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
const physics = entity.getComponent(Physics);
|
||||
|
||||
return {
|
||||
id: entity.id,
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
vx: velocity.x,
|
||||
vy: velocity.y,
|
||||
mass: physics.mass,
|
||||
radius: physics.radius
|
||||
};
|
||||
}
|
||||
|
||||
// Worker处理函数:纯函数,在Worker中执行
|
||||
protected workerProcess(
|
||||
entities: PhysicsData[],
|
||||
deltaTime: number,
|
||||
config: any
|
||||
): PhysicsData[] {
|
||||
return entities.map(entity => {
|
||||
// 应用重力
|
||||
entity.vy += config.gravity * deltaTime;
|
||||
|
||||
// 更新位置
|
||||
entity.x += entity.vx * deltaTime;
|
||||
entity.y += entity.vy * deltaTime;
|
||||
|
||||
// 应用摩擦力
|
||||
entity.vx *= config.friction;
|
||||
entity.vy *= config.friction;
|
||||
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
// 结果应用:将Worker处理结果应用回Entity
|
||||
protected applyResult(entity: Entity, result: PhysicsData): void {
|
||||
const position = entity.getComponent(Position);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
position.x = result.x;
|
||||
position.y = result.y;
|
||||
velocity.x = result.vx;
|
||||
velocity.y = result.vy;
|
||||
}
|
||||
|
||||
// SharedArrayBuffer优化支持
|
||||
protected getDefaultEntityDataSize(): number {
|
||||
return 7; // id, x, y, vx, vy, mass, radius
|
||||
}
|
||||
|
||||
protected writeEntityToBuffer(entityData: PhysicsData, offset: number): void {
|
||||
if (!this.sharedFloatArray) return;
|
||||
|
||||
this.sharedFloatArray[offset + 0] = entityData.id;
|
||||
this.sharedFloatArray[offset + 1] = entityData.x;
|
||||
this.sharedFloatArray[offset + 2] = entityData.y;
|
||||
this.sharedFloatArray[offset + 3] = entityData.vx;
|
||||
this.sharedFloatArray[offset + 4] = entityData.vy;
|
||||
this.sharedFloatArray[offset + 5] = entityData.mass;
|
||||
this.sharedFloatArray[offset + 6] = entityData.radius;
|
||||
}
|
||||
|
||||
protected readEntityFromBuffer(offset: number): PhysicsData | null {
|
||||
if (!this.sharedFloatArray) return null;
|
||||
|
||||
return {
|
||||
id: this.sharedFloatArray[offset + 0],
|
||||
x: this.sharedFloatArray[offset + 1],
|
||||
y: this.sharedFloatArray[offset + 2],
|
||||
vx: this.sharedFloatArray[offset + 3],
|
||||
vy: this.sharedFloatArray[offset + 4],
|
||||
mass: this.sharedFloatArray[offset + 5],
|
||||
radius: this.sharedFloatArray[offset + 6]
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 配置选项
|
||||
|
||||
Worker系统支持丰富的配置选项:
|
||||
|
||||
```typescript
|
||||
interface WorkerSystemConfig {
|
||||
/** 是否启用Worker并行处理 */
|
||||
enableWorker?: boolean;
|
||||
/** Worker数量,默认为CPU核心数,自动限制在系统最大值内 */
|
||||
workerCount?: number;
|
||||
/** 每个Worker处理的实体数量,用于控制负载分布 */
|
||||
entitiesPerWorker?: number;
|
||||
/** 系统配置数据,会传递给Worker */
|
||||
systemConfig?: any;
|
||||
/** 是否使用SharedArrayBuffer优化 */
|
||||
useSharedArrayBuffer?: boolean;
|
||||
/** 每个实体在SharedArrayBuffer中占用的Float32数量 */
|
||||
entityDataSize?: number;
|
||||
/** 最大实体数量(用于预分配SharedArrayBuffer) */
|
||||
maxEntities?: number;
|
||||
}
|
||||
```
|
||||
|
||||
### 配置建议
|
||||
|
||||
```typescript
|
||||
constructor() {
|
||||
super(matcher, {
|
||||
// 根据任务复杂度决定是否启用
|
||||
enableWorker: this.shouldUseWorker(),
|
||||
|
||||
// Worker数量:系统会自动限制在硬件支持范围内
|
||||
workerCount: 8, // 请求8个Worker,实际数量受CPU核心数限制
|
||||
|
||||
// 每个Worker处理的实体数量(可选)
|
||||
entitiesPerWorker: 200, // 精确控制负载分布
|
||||
|
||||
// 大量简单计算时启用SharedArrayBuffer
|
||||
useSharedArrayBuffer: this.entityCount > 1000,
|
||||
|
||||
// 根据实际数据结构设置
|
||||
entityDataSize: 8, // 确保与数据结构匹配
|
||||
|
||||
// 预估最大实体数量
|
||||
maxEntities: 10000,
|
||||
|
||||
// 传递给Worker的全局配置
|
||||
systemConfig: {
|
||||
gravity: 9.8,
|
||||
friction: 0.95,
|
||||
worldBounds: { width: 1920, height: 1080 }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private shouldUseWorker(): boolean {
|
||||
// 根据实体数量和计算复杂度决定
|
||||
return this.expectedEntityCount > 100;
|
||||
}
|
||||
|
||||
// 获取系统信息
|
||||
getSystemInfo() {
|
||||
const info = this.getWorkerInfo();
|
||||
console.log(`Worker数量: ${info.workerCount}/${info.maxSystemWorkerCount}`);
|
||||
console.log(`每Worker实体数: ${info.entitiesPerWorker || '自动分配'}`);
|
||||
console.log(`当前模式: ${info.currentMode}`);
|
||||
}
|
||||
```
|
||||
|
||||
## 处理模式
|
||||
|
||||
Worker系统支持两种处理模式:
|
||||
|
||||
### 1. 传统Worker模式
|
||||
|
||||
数据通过序列化在主线程和Worker间传递:
|
||||
|
||||
```typescript
|
||||
// 适用于:复杂计算逻辑,实体数量适中
|
||||
constructor() {
|
||||
super(matcher, {
|
||||
enableWorker: true,
|
||||
useSharedArrayBuffer: false, // 使用传统模式
|
||||
workerCount: 2
|
||||
});
|
||||
}
|
||||
|
||||
protected workerProcess(entities: EntityData[], deltaTime: number): EntityData[] {
|
||||
// 复杂的算法逻辑
|
||||
return entities.map(entity => {
|
||||
// AI决策、路径规划等复杂计算
|
||||
return this.complexAILogic(entity, deltaTime);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 2. SharedArrayBuffer模式
|
||||
|
||||
零拷贝数据共享,适合大量简单计算:
|
||||
|
||||
```typescript
|
||||
// 适用于:大量实体的简单计算
|
||||
constructor() {
|
||||
super(matcher, {
|
||||
enableWorker: true,
|
||||
useSharedArrayBuffer: true, // 启用共享内存
|
||||
entityDataSize: 6,
|
||||
maxEntities: 10000
|
||||
});
|
||||
}
|
||||
|
||||
protected getSharedArrayBufferProcessFunction(): SharedArrayBufferProcessFunction {
|
||||
return function(sharedFloatArray: Float32Array, startIndex: number, endIndex: number, deltaTime: number, config: any) {
|
||||
const entitySize = 6;
|
||||
for (let i = startIndex; i < endIndex; i++) {
|
||||
const offset = i * entitySize;
|
||||
|
||||
// 读取数据
|
||||
let x = sharedFloatArray[offset];
|
||||
let y = sharedFloatArray[offset + 1];
|
||||
let vx = sharedFloatArray[offset + 2];
|
||||
let vy = sharedFloatArray[offset + 3];
|
||||
|
||||
// 物理计算
|
||||
vy += config.gravity * deltaTime;
|
||||
x += vx * deltaTime;
|
||||
y += vy * deltaTime;
|
||||
|
||||
// 写回数据
|
||||
sharedFloatArray[offset] = x;
|
||||
sharedFloatArray[offset + 1] = y;
|
||||
sharedFloatArray[offset + 2] = vx;
|
||||
sharedFloatArray[offset + 3] = vy;
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## 完整示例:粒子物理系统
|
||||
|
||||
一个包含碰撞检测的完整粒子物理系统:
|
||||
|
||||
```typescript
|
||||
interface ParticleData {
|
||||
id: number;
|
||||
x: number;
|
||||
y: number;
|
||||
dx: number;
|
||||
dy: number;
|
||||
mass: number;
|
||||
radius: number;
|
||||
bounce: number;
|
||||
friction: number;
|
||||
}
|
||||
|
||||
@ECSSystem('ParticlePhysics')
|
||||
class ParticlePhysicsWorkerSystem extends WorkerEntitySystem<ParticleData> {
|
||||
constructor() {
|
||||
super(Matcher.all(Position, Velocity, Physics, Renderable), {
|
||||
enableWorker: true,
|
||||
workerCount: 6, // 请求6个Worker,自动限制在CPU核心数内
|
||||
entitiesPerWorker: 150, // 每个Worker处理150个粒子
|
||||
useSharedArrayBuffer: true,
|
||||
entityDataSize: 9,
|
||||
maxEntities: 5000,
|
||||
systemConfig: {
|
||||
gravity: 100,
|
||||
canvasWidth: 800,
|
||||
canvasHeight: 600,
|
||||
groundFriction: 0.98
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected extractEntityData(entity: Entity): ParticleData {
|
||||
const position = entity.getComponent(Position);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
const physics = entity.getComponent(Physics);
|
||||
const renderable = entity.getComponent(Renderable);
|
||||
|
||||
return {
|
||||
id: entity.id,
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
dx: velocity.dx,
|
||||
dy: velocity.dy,
|
||||
mass: physics.mass,
|
||||
radius: renderable.size,
|
||||
bounce: physics.bounce,
|
||||
friction: physics.friction
|
||||
};
|
||||
}
|
||||
|
||||
protected workerProcess(
|
||||
entities: ParticleData[],
|
||||
deltaTime: number,
|
||||
config: any
|
||||
): ParticleData[] {
|
||||
const result = entities.map(e => ({ ...e }));
|
||||
|
||||
// 基础物理更新
|
||||
for (const particle of result) {
|
||||
// 应用重力
|
||||
particle.dy += config.gravity * deltaTime;
|
||||
|
||||
// 更新位置
|
||||
particle.x += particle.dx * deltaTime;
|
||||
particle.y += particle.dy * deltaTime;
|
||||
|
||||
// 边界碰撞
|
||||
if (particle.x <= particle.radius) {
|
||||
particle.x = particle.radius;
|
||||
particle.dx = -particle.dx * particle.bounce;
|
||||
} else if (particle.x >= config.canvasWidth - particle.radius) {
|
||||
particle.x = config.canvasWidth - particle.radius;
|
||||
particle.dx = -particle.dx * particle.bounce;
|
||||
}
|
||||
|
||||
if (particle.y <= particle.radius) {
|
||||
particle.y = particle.radius;
|
||||
particle.dy = -particle.dy * particle.bounce;
|
||||
} else if (particle.y >= config.canvasHeight - particle.radius) {
|
||||
particle.y = config.canvasHeight - particle.radius;
|
||||
particle.dy = -particle.dy * particle.bounce;
|
||||
particle.dx *= config.groundFriction;
|
||||
}
|
||||
|
||||
// 空气阻力
|
||||
particle.dx *= particle.friction;
|
||||
particle.dy *= particle.friction;
|
||||
}
|
||||
|
||||
// 粒子间碰撞检测(O(n²)算法)
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
for (let j = i + 1; j < result.length; j++) {
|
||||
const p1 = result[i];
|
||||
const p2 = result[j];
|
||||
|
||||
const dx = p2.x - p1.x;
|
||||
const dy = p2.y - p1.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
const minDistance = p1.radius + p2.radius;
|
||||
|
||||
if (distance < minDistance && distance > 0) {
|
||||
// 分离粒子
|
||||
const nx = dx / distance;
|
||||
const ny = dy / distance;
|
||||
const overlap = minDistance - distance;
|
||||
|
||||
p1.x -= nx * overlap * 0.5;
|
||||
p1.y -= ny * overlap * 0.5;
|
||||
p2.x += nx * overlap * 0.5;
|
||||
p2.y += ny * overlap * 0.5;
|
||||
|
||||
// 弹性碰撞
|
||||
const relativeVelocityX = p2.dx - p1.dx;
|
||||
const relativeVelocityY = p2.dy - p1.dy;
|
||||
const velocityAlongNormal = relativeVelocityX * nx + relativeVelocityY * ny;
|
||||
|
||||
if (velocityAlongNormal > 0) continue;
|
||||
|
||||
const restitution = (p1.bounce + p2.bounce) * 0.5;
|
||||
const impulseScalar = -(1 + restitution) * velocityAlongNormal / (1/p1.mass + 1/p2.mass);
|
||||
|
||||
p1.dx -= impulseScalar * nx / p1.mass;
|
||||
p1.dy -= impulseScalar * ny / p1.mass;
|
||||
p2.dx += impulseScalar * nx / p2.mass;
|
||||
p2.dy += impulseScalar * ny / p2.mass;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected applyResult(entity: Entity, result: ParticleData): void {
|
||||
if (!entity?.enabled) return;
|
||||
|
||||
const position = entity.getComponent(Position);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
if (position && velocity) {
|
||||
position.set(result.x, result.y);
|
||||
velocity.set(result.dx, result.dy);
|
||||
}
|
||||
}
|
||||
|
||||
protected getDefaultEntityDataSize(): number {
|
||||
return 9;
|
||||
}
|
||||
|
||||
protected writeEntityToBuffer(data: ParticleData, offset: number): void {
|
||||
if (!this.sharedFloatArray) return;
|
||||
|
||||
this.sharedFloatArray[offset + 0] = data.id;
|
||||
this.sharedFloatArray[offset + 1] = data.x;
|
||||
this.sharedFloatArray[offset + 2] = data.y;
|
||||
this.sharedFloatArray[offset + 3] = data.dx;
|
||||
this.sharedFloatArray[offset + 4] = data.dy;
|
||||
this.sharedFloatArray[offset + 5] = data.mass;
|
||||
this.sharedFloatArray[offset + 6] = data.radius;
|
||||
this.sharedFloatArray[offset + 7] = data.bounce;
|
||||
this.sharedFloatArray[offset + 8] = data.friction;
|
||||
}
|
||||
|
||||
protected readEntityFromBuffer(offset: number): ParticleData | null {
|
||||
if (!this.sharedFloatArray) return null;
|
||||
|
||||
return {
|
||||
id: this.sharedFloatArray[offset + 0],
|
||||
x: this.sharedFloatArray[offset + 1],
|
||||
y: this.sharedFloatArray[offset + 2],
|
||||
dx: this.sharedFloatArray[offset + 3],
|
||||
dy: this.sharedFloatArray[offset + 4],
|
||||
mass: this.sharedFloatArray[offset + 5],
|
||||
radius: this.sharedFloatArray[offset + 6],
|
||||
bounce: this.sharedFloatArray[offset + 7],
|
||||
friction: this.sharedFloatArray[offset + 8]
|
||||
};
|
||||
}
|
||||
|
||||
// 性能监控
|
||||
public getPerformanceInfo(): {
|
||||
enabled: boolean;
|
||||
workerCount: number;
|
||||
entitiesPerWorker?: number;
|
||||
maxSystemWorkerCount: number;
|
||||
entityCount: number;
|
||||
isProcessing: boolean;
|
||||
currentMode: string;
|
||||
} {
|
||||
const workerInfo = this.getWorkerInfo();
|
||||
return {
|
||||
...workerInfo,
|
||||
entityCount: this.entities.length
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 适用场景
|
||||
|
||||
Worker系统特别适合以下场景:
|
||||
|
||||
### 1. 物理模拟
|
||||
- **重力系统**:大量实体的重力计算
|
||||
- **碰撞检测**:复杂的碰撞算法
|
||||
- **流体模拟**:粒子流体系统
|
||||
- **布料模拟**:顶点物理计算
|
||||
|
||||
### 2. AI计算
|
||||
- **路径寻找**:A*、Dijkstra等算法
|
||||
- **行为树**:复杂的AI决策逻辑
|
||||
- **群体智能**:鸟群、鱼群算法
|
||||
- **神经网络**:简单的AI推理
|
||||
|
||||
### 3. 数据处理
|
||||
- **大量实体更新**:状态机、生命周期管理
|
||||
- **统计计算**:游戏数据分析
|
||||
- **图像处理**:纹理生成、效果计算
|
||||
- **音频处理**:音效合成、频谱分析
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. Worker函数要求
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:Worker处理函数是纯函数
|
||||
protected workerProcess(entities: PhysicsData[], deltaTime: number, config: any): PhysicsData[] {
|
||||
// 只使用参数和标准JavaScript API
|
||||
return entities.map(entity => {
|
||||
// 纯计算逻辑,不依赖外部状态
|
||||
entity.y += entity.velocity * deltaTime;
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
// ❌ 避免:在Worker函数中使用外部引用
|
||||
protected workerProcess(entities: PhysicsData[], deltaTime: number): PhysicsData[] {
|
||||
// this 和外部变量在Worker中不可用
|
||||
return entities.map(entity => {
|
||||
entity.y += this.someProperty; // ❌ 错误
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 数据设计
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:合理的数据设计
|
||||
interface SimplePhysicsData {
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
// 保持数据结构简单,便于序列化
|
||||
}
|
||||
|
||||
// ❌ 避免:复杂的嵌套对象
|
||||
interface ComplexData {
|
||||
transform: {
|
||||
position: { x: number; y: number };
|
||||
rotation: { angle: number };
|
||||
};
|
||||
// 复杂嵌套结构增加序列化开销
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Worker数量控制
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:灵活的Worker配置
|
||||
constructor() {
|
||||
super(matcher, {
|
||||
// 直接指定需要的Worker数量,系统会自动限制在硬件支持范围内
|
||||
workerCount: 8, // 请求8个Worker
|
||||
entitiesPerWorker: 100, // 每个Worker处理100个实体
|
||||
enableWorker: this.shouldUseWorker(), // 条件启用
|
||||
});
|
||||
}
|
||||
|
||||
private shouldUseWorker(): boolean {
|
||||
// 根据实体数量和复杂度决定是否使用Worker
|
||||
return this.expectedEntityCount > 100;
|
||||
}
|
||||
|
||||
// 获取实际使用的Worker信息
|
||||
checkWorkerConfiguration() {
|
||||
const info = this.getWorkerInfo();
|
||||
console.log(`请求Worker数量: 8`);
|
||||
console.log(`实际Worker数量: ${info.workerCount}`);
|
||||
console.log(`系统最大支持: ${info.maxSystemWorkerCount}`);
|
||||
console.log(`每Worker实体数: ${info.entitiesPerWorker || '自动分配'}`);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 性能监控
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:性能监控
|
||||
public getPerformanceMetrics(): WorkerPerformanceMetrics {
|
||||
return {
|
||||
...this.getWorkerInfo(),
|
||||
entityCount: this.entities.length,
|
||||
averageProcessTime: this.getAverageProcessTime(),
|
||||
workerUtilization: this.getWorkerUtilization()
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## 性能优化建议
|
||||
|
||||
### 1. 计算密集度评估
|
||||
只对计算密集型任务使用Worker,避免在简单计算上增加线程开销。
|
||||
|
||||
### 2. 数据传输优化
|
||||
- 使用SharedArrayBuffer减少序列化开销
|
||||
- 保持数据结构简单和扁平
|
||||
- 避免频繁的大数据传输
|
||||
|
||||
### 3. 降级策略
|
||||
始终提供主线程回退方案,确保在不支持Worker的环境中正常运行。
|
||||
|
||||
### 4. 内存管理
|
||||
及时清理Worker池和共享缓冲区,避免内存泄漏。
|
||||
|
||||
### 5. 负载均衡
|
||||
使用 `entitiesPerWorker` 参数精确控制负载分布,避免某些Worker空闲而其他Worker过载。
|
||||
|
||||
## 在线演示
|
||||
|
||||
查看完整的Worker系统演示:[Worker系统演示](https://esengine.github.io/ecs-framework/demos/worker-system/)
|
||||
|
||||
该演示展示了:
|
||||
- 多线程物理计算
|
||||
- 实时性能对比
|
||||
- SharedArrayBuffer优化
|
||||
- 大量实体的并行处理
|
||||
|
||||
Worker系统为ECS框架提供了强大的并行计算能力,让你能够充分利用现代多核处理器的性能,为复杂的游戏逻辑和计算密集型任务提供了高效的解决方案。
|
||||
761
docs/guide/world-manager.md
Normal file
761
docs/guide/world-manager.md
Normal file
@@ -0,0 +1,761 @@
|
||||
# WorldManager
|
||||
|
||||
WorldManager 是 ECS Framework 提供的高级世界管理器,用于管理多个完全隔离的游戏世界(World)。每个 World 都是独立的 ECS 环境,可以包含多个场景。
|
||||
|
||||
## 适用场景
|
||||
|
||||
WorldManager 适合以下高级场景:
|
||||
- MMO 游戏服务器的多房间管理
|
||||
- 游戏大厅系统(每个游戏房间完全隔离)
|
||||
- 服务器端的多游戏实例
|
||||
- 需要完全隔离的多个游戏环境
|
||||
- 需要同时运行多个独立世界的应用
|
||||
|
||||
## 特点
|
||||
|
||||
- 多 World 管理,每个 World 完全独立
|
||||
- 每个 World 可以包含多个 Scene
|
||||
- 支持 World 的激活/停用
|
||||
- 自动清理空 World
|
||||
- World 级别的全局系统
|
||||
- 批量操作和查询
|
||||
|
||||
## 基本使用
|
||||
|
||||
### 初始化
|
||||
|
||||
WorldManager 是 Core 的内置服务,通过服务容器获取:
|
||||
|
||||
```typescript
|
||||
import { Core, WorldManager } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始化 Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 从服务容器获取 WorldManager(Core 已自动创建并注册)
|
||||
const worldManager = Core.services.resolve(WorldManager);
|
||||
```
|
||||
|
||||
### 创建 World
|
||||
|
||||
```typescript
|
||||
// 创建游戏房间 World
|
||||
const room1 = worldManager.createWorld('room_001', {
|
||||
name: 'GameRoom_001',
|
||||
maxScenes: 5,
|
||||
debug: true
|
||||
});
|
||||
|
||||
// 激活 World
|
||||
worldManager.setWorldActive('room_001', true);
|
||||
|
||||
// 创建更多房间
|
||||
const room2 = worldManager.createWorld('room_002', {
|
||||
name: 'GameRoom_002',
|
||||
maxScenes: 5
|
||||
});
|
||||
|
||||
worldManager.setWorldActive('room_002', true);
|
||||
```
|
||||
|
||||
### 游戏循环
|
||||
|
||||
在游戏循环中更新所有活跃的 World:
|
||||
|
||||
```typescript
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // 更新全局服务
|
||||
worldManager.updateAll(); // 更新所有活跃的 World
|
||||
}
|
||||
|
||||
// 启动游戏循环
|
||||
let lastTime = 0;
|
||||
setInterval(() => {
|
||||
const currentTime = Date.now();
|
||||
const deltaTime = (currentTime - lastTime) / 1000;
|
||||
lastTime = currentTime;
|
||||
|
||||
gameLoop(deltaTime);
|
||||
}, 16); // 60 FPS
|
||||
```
|
||||
|
||||
## World 管理
|
||||
|
||||
### 创建 World
|
||||
|
||||
```typescript
|
||||
// 基本创建
|
||||
const world = worldManager.createWorld('worldId');
|
||||
|
||||
// 带配置创建
|
||||
const world = worldManager.createWorld('worldId', {
|
||||
name: 'MyWorld',
|
||||
maxScenes: 10,
|
||||
autoCleanup: true,
|
||||
debug: true
|
||||
});
|
||||
```
|
||||
|
||||
**配置选项(IWorldConfig)**:
|
||||
- `name?: string` - World 名称
|
||||
- `maxScenes?: number` - 最大场景数量限制(默认 10)
|
||||
- `autoCleanup?: boolean` - 是否自动清理空场景(默认 true)
|
||||
- `debug?: boolean` - 是否启用调试模式(默认 false)
|
||||
|
||||
### 获取 World
|
||||
|
||||
```typescript
|
||||
// 通过 ID 获取
|
||||
const world = worldManager.getWorld('room_001');
|
||||
if (world) {
|
||||
console.log(`World: ${world.name}`);
|
||||
}
|
||||
|
||||
// 获取所有 World
|
||||
const allWorlds = worldManager.getAllWorlds();
|
||||
console.log(`共有 ${allWorlds.length} 个 World`);
|
||||
|
||||
// 获取所有 World ID
|
||||
const worldIds = worldManager.getWorldIds();
|
||||
console.log('World 列表:', worldIds);
|
||||
|
||||
// 通过名称查找
|
||||
const world = worldManager.findWorldByName('GameRoom_001');
|
||||
```
|
||||
|
||||
### 激活和停用 World
|
||||
|
||||
```typescript
|
||||
// 激活 World(开始运行和更新)
|
||||
worldManager.setWorldActive('room_001', true);
|
||||
|
||||
// 停用 World(停止更新但保留数据)
|
||||
worldManager.setWorldActive('room_001', false);
|
||||
|
||||
// 检查 World 是否激活
|
||||
if (worldManager.isWorldActive('room_001')) {
|
||||
console.log('房间正在运行');
|
||||
}
|
||||
|
||||
// 获取所有活跃的 World
|
||||
const activeWorlds = worldManager.getActiveWorlds();
|
||||
console.log(`当前有 ${activeWorlds.length} 个活跃 World`);
|
||||
```
|
||||
|
||||
### 移除 World
|
||||
|
||||
```typescript
|
||||
// 移除 World(会自动停用并销毁)
|
||||
const removed = worldManager.removeWorld('room_001');
|
||||
if (removed) {
|
||||
console.log('World 已移除');
|
||||
}
|
||||
```
|
||||
|
||||
## World 中的场景管理
|
||||
|
||||
每个 World 可以包含多个 Scene 并独立管理它们的生命周期。
|
||||
|
||||
### 创建场景
|
||||
|
||||
```typescript
|
||||
const world = worldManager.getWorld('room_001');
|
||||
if (!world) return;
|
||||
|
||||
// 创建场景
|
||||
const mainScene = world.createScene('main', new MainScene());
|
||||
const uiScene = world.createScene('ui', new UIScene());
|
||||
const hudScene = world.createScene('hud', new HUDScene());
|
||||
|
||||
// 激活场景
|
||||
world.setSceneActive('main', true);
|
||||
world.setSceneActive('ui', true);
|
||||
world.setSceneActive('hud', false);
|
||||
```
|
||||
|
||||
### 查询场景
|
||||
|
||||
```typescript
|
||||
// 获取特定场景
|
||||
const mainScene = world.getScene<MainScene>('main');
|
||||
if (mainScene) {
|
||||
console.log(`场景名称: ${mainScene.name}`);
|
||||
}
|
||||
|
||||
// 获取所有场景
|
||||
const allScenes = world.getAllScenes();
|
||||
console.log(`World 中共有 ${allScenes.length} 个场景`);
|
||||
|
||||
// 获取所有场景 ID
|
||||
const sceneIds = world.getSceneIds();
|
||||
console.log('场景列表:', sceneIds);
|
||||
|
||||
// 获取活跃场景数量
|
||||
const activeCount = world.getActiveSceneCount();
|
||||
console.log(`当前有 ${activeCount} 个活跃场景`);
|
||||
|
||||
// 检查场景是否激活
|
||||
if (world.isSceneActive('main')) {
|
||||
console.log('主场景正在运行');
|
||||
}
|
||||
```
|
||||
|
||||
### 场景切换
|
||||
|
||||
World 支持多场景同时运行,也支持场景切换:
|
||||
|
||||
```typescript
|
||||
class GameWorld {
|
||||
private world: World;
|
||||
|
||||
constructor(worldManager: WorldManager) {
|
||||
this.world = worldManager.createWorld('game', {
|
||||
name: 'GameWorld',
|
||||
maxScenes: 5
|
||||
});
|
||||
|
||||
// 创建所有场景
|
||||
this.world.createScene('menu', new MenuScene());
|
||||
this.world.createScene('game', new GameScene());
|
||||
this.world.createScene('pause', new PauseScene());
|
||||
this.world.createScene('gameover', new GameOverScene());
|
||||
|
||||
// 激活 World
|
||||
worldManager.setWorldActive('game', true);
|
||||
}
|
||||
|
||||
public showMenu(): void {
|
||||
this.deactivateAllScenes();
|
||||
this.world.setSceneActive('menu', true);
|
||||
}
|
||||
|
||||
public startGame(): void {
|
||||
this.deactivateAllScenes();
|
||||
this.world.setSceneActive('game', true);
|
||||
}
|
||||
|
||||
public pauseGame(): void {
|
||||
// 游戏场景继续存在但停止更新
|
||||
this.world.setSceneActive('game', false);
|
||||
// 显示暂停界面
|
||||
this.world.setSceneActive('pause', true);
|
||||
}
|
||||
|
||||
public resumeGame(): void {
|
||||
this.world.setSceneActive('pause', false);
|
||||
this.world.setSceneActive('game', true);
|
||||
}
|
||||
|
||||
public showGameOver(): void {
|
||||
this.deactivateAllScenes();
|
||||
this.world.setSceneActive('gameover', true);
|
||||
}
|
||||
|
||||
private deactivateAllScenes(): void {
|
||||
const sceneIds = this.world.getSceneIds();
|
||||
sceneIds.forEach(id => this.world.setSceneActive(id, false));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 移除场景
|
||||
|
||||
```typescript
|
||||
// 移除不再需要的场景
|
||||
const removed = world.removeScene('oldScene');
|
||||
if (removed) {
|
||||
console.log('场景已移除');
|
||||
}
|
||||
|
||||
// 场景会自动调用 end() 方法进行清理
|
||||
```
|
||||
|
||||
## 全局系统
|
||||
|
||||
World 支持全局系统,这些系统在 World 级别运行,不依赖特定 Scene。
|
||||
|
||||
### 定义全局系统
|
||||
|
||||
```typescript
|
||||
import { IGlobalSystem } from '@esengine/ecs-framework';
|
||||
|
||||
// 网络系统(World 级别)
|
||||
class NetworkSystem implements IGlobalSystem {
|
||||
readonly name = 'NetworkSystem';
|
||||
|
||||
private connectionId: string;
|
||||
|
||||
constructor(connectionId: string) {
|
||||
this.connectionId = connectionId;
|
||||
}
|
||||
|
||||
initialize(): void {
|
||||
console.log(`网络系统初始化: ${this.connectionId}`);
|
||||
// 建立网络连接
|
||||
}
|
||||
|
||||
update(deltaTime?: number): void {
|
||||
// 处理网络消息,不依赖任何 Scene
|
||||
// 接收和发送网络包
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
console.log(`网络系统销毁: ${this.connectionId}`);
|
||||
// 关闭网络连接
|
||||
}
|
||||
}
|
||||
|
||||
// 物理系统(World 级别)
|
||||
class PhysicsSystem implements IGlobalSystem {
|
||||
readonly name = 'PhysicsSystem';
|
||||
|
||||
initialize(): void {
|
||||
console.log('物理系统初始化');
|
||||
}
|
||||
|
||||
update(deltaTime?: number): void {
|
||||
// 物理模拟,作用于 World 中所有场景
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
console.log('物理系统销毁');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用全局系统
|
||||
|
||||
```typescript
|
||||
const world = worldManager.getWorld('room_001');
|
||||
if (!world) return;
|
||||
|
||||
// 添加全局系统
|
||||
const networkSystem = world.addGlobalSystem(new NetworkSystem('conn_001'));
|
||||
const physicsSystem = world.addGlobalSystem(new PhysicsSystem());
|
||||
|
||||
// 获取全局系统
|
||||
const network = world.getGlobalSystem(NetworkSystem);
|
||||
if (network) {
|
||||
console.log('找到网络系统');
|
||||
}
|
||||
|
||||
// 移除全局系统
|
||||
world.removeGlobalSystem(networkSystem);
|
||||
```
|
||||
|
||||
## 批量操作
|
||||
|
||||
### 更新所有 World
|
||||
|
||||
```typescript
|
||||
// 更新所有活跃的 World(应该在游戏循环中调用)
|
||||
worldManager.updateAll();
|
||||
|
||||
// 这会自动更新每个 World 的:
|
||||
// 1. 全局系统
|
||||
// 2. 所有活跃场景
|
||||
```
|
||||
|
||||
### 启动和停止
|
||||
|
||||
```typescript
|
||||
// 启动所有 World
|
||||
worldManager.startAll();
|
||||
|
||||
// 停止所有 World
|
||||
worldManager.stopAll();
|
||||
|
||||
// 检查是否正在运行
|
||||
if (worldManager.isRunning) {
|
||||
console.log('WorldManager 正在运行');
|
||||
}
|
||||
```
|
||||
|
||||
### 查找 World
|
||||
|
||||
```typescript
|
||||
// 使用条件查找
|
||||
const emptyWorlds = worldManager.findWorlds(world => {
|
||||
return world.sceneCount === 0;
|
||||
});
|
||||
|
||||
// 查找活跃的 World
|
||||
const activeWorlds = worldManager.findWorlds(world => {
|
||||
return world.isActive;
|
||||
});
|
||||
|
||||
// 查找特定名称的 World
|
||||
const world = worldManager.findWorldByName('GameRoom_001');
|
||||
```
|
||||
|
||||
## 统计和监控
|
||||
|
||||
### 获取统计信息
|
||||
|
||||
```typescript
|
||||
const stats = worldManager.getStats();
|
||||
|
||||
console.log(`总 World 数: ${stats.totalWorlds}`);
|
||||
console.log(`活跃 World 数: ${stats.activeWorlds}`);
|
||||
console.log(`总场景数: ${stats.totalScenes}`);
|
||||
console.log(`总实体数: ${stats.totalEntities}`);
|
||||
console.log(`总系统数: ${stats.totalSystems}`);
|
||||
|
||||
// 查看每个 World 的详细信息
|
||||
stats.worlds.forEach(worldInfo => {
|
||||
console.log(`World: ${worldInfo.name}`);
|
||||
console.log(` 场景数: ${worldInfo.sceneCount}`);
|
||||
console.log(` 是否活跃: ${worldInfo.isActive}`);
|
||||
});
|
||||
```
|
||||
|
||||
### 获取详细状态
|
||||
|
||||
```typescript
|
||||
const status = worldManager.getDetailedStatus();
|
||||
|
||||
// 包含所有 World 的详细状态
|
||||
status.worlds.forEach(worldStatus => {
|
||||
console.log(`World ID: ${worldStatus.id}`);
|
||||
console.log(`状态:`, worldStatus.status);
|
||||
});
|
||||
```
|
||||
|
||||
## 自动清理
|
||||
|
||||
WorldManager 支持自动清理空的 World。
|
||||
|
||||
### 配置清理
|
||||
|
||||
```typescript
|
||||
// 创建带清理配置的 WorldManager
|
||||
const worldManager = Core.services.resolve(WorldManager);
|
||||
|
||||
// WorldManager 的配置在 Core 中设置:
|
||||
// {
|
||||
// maxWorlds: 50,
|
||||
// autoCleanup: true,
|
||||
// cleanupFrameInterval: 1800 // 间隔多少帧清理闲置 World
|
||||
// }
|
||||
```
|
||||
|
||||
### 手动清理
|
||||
|
||||
```typescript
|
||||
// 手动触发清理
|
||||
const cleanedCount = worldManager.cleanup();
|
||||
console.log(`清理了 ${cleanedCount} 个 World`);
|
||||
```
|
||||
|
||||
**清理条件**:
|
||||
- World 未激活
|
||||
- 没有 Scene 或所有 Scene 都是空的
|
||||
- 创建时间超过 10 分钟
|
||||
|
||||
## API 参考
|
||||
|
||||
### WorldManager API
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `createWorld(worldId, config?)` | 创建新 World |
|
||||
| `removeWorld(worldId)` | 移除 World |
|
||||
| `getWorld(worldId)` | 获取 World |
|
||||
| `getAllWorlds()` | 获取所有 World |
|
||||
| `getWorldIds()` | 获取所有 World ID |
|
||||
| `setWorldActive(worldId, active)` | 设置 World 激活状态 |
|
||||
| `isWorldActive(worldId)` | 检查 World 是否激活 |
|
||||
| `getActiveWorlds()` | 获取所有活跃的 World |
|
||||
| `updateAll()` | 更新所有活跃 World |
|
||||
| `startAll()` | 启动所有 World |
|
||||
| `stopAll()` | 停止所有 World |
|
||||
| `findWorlds(predicate)` | 查找满足条件的 World |
|
||||
| `findWorldByName(name)` | 根据名称查找 World |
|
||||
| `getStats()` | 获取统计信息 |
|
||||
| `getDetailedStatus()` | 获取详细状态信息 |
|
||||
| `cleanup()` | 清理空 World |
|
||||
| `destroy()` | 销毁 WorldManager |
|
||||
|
||||
### World API
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `createScene(sceneId, sceneInstance?)` | 创建并添加 Scene |
|
||||
| `removeScene(sceneId)` | 移除 Scene |
|
||||
| `getScene(sceneId)` | 获取 Scene |
|
||||
| `getAllScenes()` | 获取所有 Scene |
|
||||
| `getSceneIds()` | 获取所有 Scene ID |
|
||||
| `setSceneActive(sceneId, active)` | 设置 Scene 激活状态 |
|
||||
| `isSceneActive(sceneId)` | 检查 Scene 是否激活 |
|
||||
| `getActiveSceneCount()` | 获取活跃 Scene 数量 |
|
||||
| `addGlobalSystem(system)` | 添加全局系统 |
|
||||
| `removeGlobalSystem(system)` | 移除全局系统 |
|
||||
| `getGlobalSystem(type)` | 获取全局系统 |
|
||||
| `start()` | 启动 World |
|
||||
| `stop()` | 停止 World |
|
||||
| `updateGlobalSystems()` | 更新全局系统 |
|
||||
| `updateScenes()` | 更新所有激活 Scene |
|
||||
| `destroy()` | 销毁 World |
|
||||
| `getStatus()` | 获取 World 状态 |
|
||||
| `getStats()` | 获取统计信息 |
|
||||
|
||||
### 属性
|
||||
|
||||
| 属性 | 说明 |
|
||||
|------|------|
|
||||
| `worldCount` | World 总数 |
|
||||
| `activeWorldCount` | 活跃 World 数量 |
|
||||
| `isRunning` | 是否正在运行 |
|
||||
| `config` | 配置信息 |
|
||||
|
||||
## 完整示例
|
||||
|
||||
### MMO 游戏房间系统
|
||||
|
||||
```typescript
|
||||
import { Core, WorldManager, Scene, World } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始化
|
||||
Core.create({ debug: true });
|
||||
const worldManager = Core.services.resolve(WorldManager);
|
||||
|
||||
// 房间管理器
|
||||
class RoomManager {
|
||||
private worldManager: WorldManager;
|
||||
private rooms: Map<string, World> = new Map();
|
||||
|
||||
constructor(worldManager: WorldManager) {
|
||||
this.worldManager = worldManager;
|
||||
}
|
||||
|
||||
// 创建游戏房间
|
||||
public createRoom(roomId: string, maxPlayers: number): World {
|
||||
const world = this.worldManager.createWorld(roomId, {
|
||||
name: `Room_${roomId}`,
|
||||
maxScenes: 3,
|
||||
debug: true
|
||||
});
|
||||
|
||||
// 创建房间场景
|
||||
world.createScene('lobby', new LobbyScene());
|
||||
world.createScene('game', new GameScene());
|
||||
world.createScene('result', new ResultScene());
|
||||
|
||||
// 添加房间级别的系统
|
||||
world.addGlobalSystem(new NetworkSystem(roomId));
|
||||
world.addGlobalSystem(new RoomLogicSystem(maxPlayers));
|
||||
|
||||
// 激活 World 和初始场景
|
||||
this.worldManager.setWorldActive(roomId, true);
|
||||
world.setSceneActive('lobby', true);
|
||||
|
||||
this.rooms.set(roomId, world);
|
||||
console.log(`房间 ${roomId} 已创建`);
|
||||
|
||||
return world;
|
||||
}
|
||||
|
||||
// 玩家加入房间
|
||||
public joinRoom(roomId: string, playerId: string): boolean {
|
||||
const world = this.rooms.get(roomId);
|
||||
if (!world) {
|
||||
console.log(`房间 ${roomId} 不存在`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 在大厅场景中创建玩家实体
|
||||
const lobbyScene = world.getScene('lobby');
|
||||
if (lobbyScene) {
|
||||
const player = lobbyScene.createEntity(`Player_${playerId}`);
|
||||
// 添加玩家组件...
|
||||
console.log(`玩家 ${playerId} 加入房间 ${roomId}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// 开始游戏
|
||||
public startGame(roomId: string): void {
|
||||
const world = this.rooms.get(roomId);
|
||||
if (!world) return;
|
||||
|
||||
// 切换到游戏场景
|
||||
world.setSceneActive('lobby', false);
|
||||
world.setSceneActive('game', true);
|
||||
|
||||
console.log(`房间 ${roomId} 游戏开始`);
|
||||
}
|
||||
|
||||
// 结束游戏
|
||||
public endGame(roomId: string): void {
|
||||
const world = this.rooms.get(roomId);
|
||||
if (!world) return;
|
||||
|
||||
// 切换到结果场景
|
||||
world.setSceneActive('game', false);
|
||||
world.setSceneActive('result', true);
|
||||
|
||||
console.log(`房间 ${roomId} 游戏结束`);
|
||||
}
|
||||
|
||||
// 关闭房间
|
||||
public closeRoom(roomId: string): void {
|
||||
this.worldManager.removeWorld(roomId);
|
||||
this.rooms.delete(roomId);
|
||||
console.log(`房间 ${roomId} 已关闭`);
|
||||
}
|
||||
|
||||
// 获取房间列表
|
||||
public getRoomList(): string[] {
|
||||
return Array.from(this.rooms.keys());
|
||||
}
|
||||
|
||||
// 获取房间统计
|
||||
public getRoomStats(roomId: string) {
|
||||
const world = this.rooms.get(roomId);
|
||||
return world?.getStats();
|
||||
}
|
||||
}
|
||||
|
||||
// 使用房间管理器
|
||||
const roomManager = new RoomManager(worldManager);
|
||||
|
||||
// 创建多个游戏房间
|
||||
roomManager.createRoom('room_001', 4);
|
||||
roomManager.createRoom('room_002', 4);
|
||||
roomManager.createRoom('room_003', 2);
|
||||
|
||||
// 玩家加入
|
||||
roomManager.joinRoom('room_001', 'player_1');
|
||||
roomManager.joinRoom('room_001', 'player_2');
|
||||
|
||||
// 开始游戏
|
||||
roomManager.startGame('room_001');
|
||||
|
||||
// 游戏循环
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime);
|
||||
worldManager.updateAll(); // 更新所有房间
|
||||
}
|
||||
|
||||
// 定期清理空房间
|
||||
setInterval(() => {
|
||||
const stats = worldManager.getStats();
|
||||
console.log(`当前房间数: ${stats.totalWorlds}`);
|
||||
console.log(`活跃房间数: ${stats.activeWorlds}`);
|
||||
|
||||
worldManager.cleanup();
|
||||
}, 60000); // 每分钟清理一次
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 合理的 World 粒度
|
||||
|
||||
```typescript
|
||||
// 推荐:每个独立环境一个 World
|
||||
const room1 = worldManager.createWorld('room_1'); // 游戏房间1
|
||||
const room2 = worldManager.createWorld('room_2'); // 游戏房间2
|
||||
|
||||
// 不推荐:过度使用 World
|
||||
const world1 = worldManager.createWorld('ui'); // UI 不需要独立 World
|
||||
const world2 = worldManager.createWorld('menu'); // 菜单不需要独立 World
|
||||
```
|
||||
|
||||
### 2. 使用全局系统处理跨场景逻辑
|
||||
|
||||
```typescript
|
||||
// 推荐:World 级别的系统
|
||||
class NetworkSystem implements IGlobalSystem {
|
||||
update() {
|
||||
// 网络处理不依赖场景
|
||||
}
|
||||
}
|
||||
|
||||
// 不推荐:在每个场景中重复创建
|
||||
class GameScene extends Scene {
|
||||
initialize() {
|
||||
this.addSystem(new NetworkSystem()); // 不应该在场景级别
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 及时清理不用的 World
|
||||
|
||||
```typescript
|
||||
// 推荐:玩家离开时清理房间
|
||||
function onPlayerLeave(roomId: string) {
|
||||
const world = worldManager.getWorld(roomId);
|
||||
if (world && world.sceneCount === 0) {
|
||||
worldManager.removeWorld(roomId);
|
||||
}
|
||||
}
|
||||
|
||||
// 或使用自动清理
|
||||
worldManager.cleanup();
|
||||
```
|
||||
|
||||
### 4. 监控资源使用
|
||||
|
||||
```typescript
|
||||
// 定期检查资源使用情况
|
||||
setInterval(() => {
|
||||
const stats = worldManager.getStats();
|
||||
|
||||
if (stats.totalWorlds > 100) {
|
||||
console.warn('World 数量过多,考虑清理');
|
||||
worldManager.cleanup();
|
||||
}
|
||||
|
||||
if (stats.totalEntities > 10000) {
|
||||
console.warn('实体数量过多,检查是否有泄漏');
|
||||
}
|
||||
}, 30000);
|
||||
```
|
||||
|
||||
## 与 SceneManager 的对比
|
||||
|
||||
| 特性 | SceneManager | WorldManager |
|
||||
|------|--------------|--------------|
|
||||
| 适用场景 | 95% 的游戏应用 | 高级多世界隔离场景 |
|
||||
| 复杂度 | 简单 | 复杂 |
|
||||
| 场景数量 | 单场景(可切换) | 多 World,每个 World 多场景 |
|
||||
| 场景隔离 | 无(场景切换) | 完全隔离(每个 World 独立) |
|
||||
| 性能开销 | 最小 | 较高 |
|
||||
| 全局系统 | 无 | 支持(World 级别) |
|
||||
| 使用示例 | 单人游戏、移动游戏 | MMO 服务器、游戏房间系统 |
|
||||
|
||||
**何时使用 WorldManager**:
|
||||
- MMO 游戏服务器(每个房间一个 World)
|
||||
- 游戏大厅系统(每个游戏房间完全隔离)
|
||||
- 需要运行多个完全独立的游戏实例
|
||||
- 服务器端模拟多个游戏世界
|
||||
|
||||
**何时使用 SceneManager**:
|
||||
- 单人游戏
|
||||
- 简单的多人游戏
|
||||
- 移动游戏
|
||||
- 场景之间需要切换但不需要同时运行
|
||||
|
||||
## 架构层次
|
||||
|
||||
WorldManager 在 ECS Framework 中的位置:
|
||||
|
||||
```
|
||||
Core (全局服务)
|
||||
└── WorldManager (世界管理)
|
||||
├── World 1 (游戏房间1)
|
||||
│ ├── GlobalSystem (全局系统)
|
||||
│ ├── Scene 1 (场景1)
|
||||
│ │ ├── EntitySystem
|
||||
│ │ ├── Entity
|
||||
│ │ └── Component
|
||||
│ └── Scene 2 (场景2)
|
||||
├── World 2 (游戏房间2)
|
||||
│ ├── GlobalSystem
|
||||
│ └── Scene 1
|
||||
└── World 3 (游戏房间3)
|
||||
```
|
||||
|
||||
WorldManager 为需要多世界隔离的高级应用提供了强大的管理能力。如果你的应用不需要多世界隔离,建议使用更简单的 [SceneManager](./scene-manager.md)。
|
||||
317
docs/index.md
Normal file
317
docs/index.md
Normal file
@@ -0,0 +1,317 @@
|
||||
---
|
||||
layout: page
|
||||
title: ESEngine - 高性能 TypeScript ECS 框架
|
||||
---
|
||||
|
||||
<ParticleHero />
|
||||
|
||||
<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,22 +0,0 @@
|
||||
{
|
||||
"name": "@esengine/docs",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/starlight": "^0.37.1",
|
||||
"@astrojs/vue": "^5.1.3",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"astro": "^5.6.1",
|
||||
"sharp": "^0.34.2",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"vue": "^3.5.26"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill-rule="evenodd" d="M81 36 64 0 47 36l-1 2-9-10a6 6 0 0 0-9 9l10 10h-2L0 64l36 17h2L28 91a6 6 0 1 0 9 9l9-10 1 2 17 36 17-36v-2l9 10a6 6 0 1 0 9-9l-9-9 2-1 36-17-36-17-2-1 9-9a6 6 0 1 0-9-9l-9 10v-2Zm-17 2-2 5c-4 8-11 15-19 19l-5 2 5 2c8 4 15 11 19 19l2 5 2-5c4-8 11-15 19-19l5-2-5-2c-8-4-15-11-19-19l-2-5Z" clip-rule="evenodd"/><path d="M118 19a6 6 0 0 0-9-9l-3 3a6 6 0 1 0 9 9l3-3Zm-96 4c-2 2-6 2-9 0l-3-3a6 6 0 1 1 9-9l3 3c3 2 3 6 0 9Zm0 82c-2-2-6-2-9 0l-3 3a6 6 0 1 0 9 9l3-3c3-2 3-6 0-9Zm96 4a6 6 0 0 1-9 9l-3-3a6 6 0 1 1 9-9l3 3Z"/><style>path{fill:#000}@media (prefers-color-scheme:dark){path{fill:#fff}}</style></svg>
|
||||
|
Before Width: | Height: | Size: 696 B |
@@ -1,45 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 96 KiB |
@@ -1,45 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1,20 +0,0 @@
|
||||
---
|
||||
import type { Props } from '@astrojs/starlight/props';
|
||||
import Default from '@astrojs/starlight/components/Head.astro';
|
||||
---
|
||||
|
||||
<Default {...Astro.props}><slot /></Default>
|
||||
|
||||
<!-- Preload fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<!-- Force dark mode -->
|
||||
<script is:inline>
|
||||
document.documentElement.dataset.theme = 'dark';
|
||||
localStorage.setItem('starlight-theme', 'dark');
|
||||
</script>
|
||||
@@ -1,3 +0,0 @@
|
||||
---
|
||||
// Empty component to disable theme selection (dark mode only)
|
||||
---
|
||||
@@ -1,7 +0,0 @@
|
||||
import { defineCollection } from 'astro:content';
|
||||
import { docsLoader } from '@astrojs/starlight/loaders';
|
||||
import { docsSchema } from '@astrojs/starlight/schema';
|
||||
|
||||
export const collections = {
|
||||
docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
|
||||
};
|
||||
@@ -1,64 +0,0 @@
|
||||
---
|
||||
title: API 参考
|
||||
description: ESEngine 完整 API 文档
|
||||
---
|
||||
|
||||
# API 参考
|
||||
|
||||
ESEngine 提供完整的 TypeScript API 文档,涵盖所有核心类、接口和方法。
|
||||
|
||||
## 核心模块
|
||||
|
||||
### 基础类
|
||||
|
||||
| 类名 | 描述 |
|
||||
|------|------|
|
||||
| [Core](/api/classes/Core) | 框架核心单例,管理整个 ECS 生命周期 |
|
||||
| [Scene](/api/classes/Scene) | 场景类,包含实体和系统 |
|
||||
| [World](/api/classes/World) | 游戏世界,可包含多个场景 |
|
||||
| [Entity](/api/classes/Entity) | 实体类,组件的容器 |
|
||||
| [Component](/api/classes/Component) | 组件基类,纯数据容器 |
|
||||
|
||||
### 系统类
|
||||
|
||||
| 类名 | 描述 |
|
||||
|------|------|
|
||||
| [EntitySystem](/api/classes/EntitySystem) | 实体系统基类 |
|
||||
| [ProcessingSystem](/api/classes/ProcessingSystem) | 处理系统,逐个处理实体 |
|
||||
| [IntervalSystem](/api/classes/IntervalSystem) | 间隔执行系统 |
|
||||
| [PassiveSystem](/api/classes/PassiveSystem) | 被动系统,不自动执行 |
|
||||
|
||||
### 工具类
|
||||
|
||||
| 类名 | 描述 |
|
||||
|------|------|
|
||||
| [Matcher](/api/classes/Matcher) | 实体匹配器,用于过滤实体 |
|
||||
| [Time](/api/classes/Time) | 时间管理器 |
|
||||
| [PerformanceMonitor](/api/classes/PerformanceMonitor) | 性能监控 |
|
||||
|
||||
## 装饰器
|
||||
|
||||
| 装饰器 | 描述 |
|
||||
|--------|------|
|
||||
| [@ECSComponent](/api/functions/ECSComponent) | 组件装饰器,用于注册组件 |
|
||||
| [@ECSSystem](/api/functions/ECSSystem) | 系统装饰器,用于注册系统 |
|
||||
|
||||
## 枚举
|
||||
|
||||
| 枚举 | 描述 |
|
||||
|------|------|
|
||||
| [ECSEventType](/api/enumerations/ECSEventType) | ECS 事件类型 |
|
||||
| [LogLevel](/api/enumerations/LogLevel) | 日志级别 |
|
||||
|
||||
## 接口
|
||||
|
||||
| 接口 | 描述 |
|
||||
|------|------|
|
||||
| [IScene](/api/interfaces/IScene) | 场景接口 |
|
||||
| [IComponent](/api/interfaces/IComponent) | 组件接口 |
|
||||
| [ISystemBase](/api/interfaces/ISystemBase) | 系统基础接口 |
|
||||
| [ICoreConfig](/api/interfaces/ICoreConfig) | Core 配置接口 |
|
||||
|
||||
:::tip[API 文档生成]
|
||||
完整 API 文档由 TypeDoc 自动生成,详见 GitHub 仓库中的 `/docs/api` 目录。
|
||||
:::
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
title: "Examples"
|
||||
description: "ESEngine example projects and demos"
|
||||
---
|
||||
|
||||
Explore example projects to learn ESEngine best practices.
|
||||
|
||||
## Available Examples
|
||||
|
||||
### [Worker System Demo](/en/examples/worker-system-demo/)
|
||||
Demonstrates how to use Web Workers for parallel processing, offloading heavy computations from the main thread.
|
||||
|
||||
## External Examples
|
||||
|
||||
### [Lawn Mower Demo](https://github.com/esengine/lawn-mower-demo)
|
||||
A complete game demo showcasing ESEngine features including:
|
||||
- Entity-Component-System architecture
|
||||
- Behavior tree AI
|
||||
- Scene management
|
||||
- Platform adaptation
|
||||
@@ -1,312 +0,0 @@
|
||||
---
|
||||
title: "Best Practices"
|
||||
description: "Component design patterns and complex examples"
|
||||
---
|
||||
|
||||
## Design Principles
|
||||
|
||||
### 1. Keep Components Simple
|
||||
|
||||
```typescript
|
||||
// ✅ Good component design - single responsibility
|
||||
@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 this design - too many responsibilities
|
||||
@ECSComponent('GameObject')
|
||||
class GameObject extends Component {
|
||||
x: number;
|
||||
y: number;
|
||||
dx: number;
|
||||
dy: number;
|
||||
health: number;
|
||||
damage: number;
|
||||
sprite: string;
|
||||
// Too many unrelated properties
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Use Constructor for Initialization
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Transform')
|
||||
class Transform extends Component {
|
||||
x: number;
|
||||
y: number;
|
||||
rotation: number;
|
||||
scale: number;
|
||||
|
||||
constructor(x = 0, y = 0, rotation = 0, scale = 1) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.rotation = rotation;
|
||||
this.scale = scale;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Clear Type Definitions
|
||||
|
||||
```typescript
|
||||
interface InventoryItem {
|
||||
id: string;
|
||||
name: string;
|
||||
quantity: number;
|
||||
type: 'weapon' | 'consumable' | 'misc';
|
||||
}
|
||||
|
||||
@ECSComponent('Inventory')
|
||||
class Inventory extends Component {
|
||||
items: InventoryItem[] = [];
|
||||
maxSlots: number;
|
||||
|
||||
constructor(maxSlots: number = 20) {
|
||||
super();
|
||||
this.maxSlots = maxSlots;
|
||||
}
|
||||
|
||||
addItem(item: InventoryItem): boolean {
|
||||
if (this.items.length < this.maxSlots) {
|
||||
this.items.push(item);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
removeItem(itemId: string): InventoryItem | null {
|
||||
const index = this.items.findIndex(item => item.id === itemId);
|
||||
if (index !== -1) {
|
||||
return this.items.splice(index, 1)[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Referencing Other Entities
|
||||
|
||||
When components need to reference other entities (like parent-child relationships, follow targets), **the recommended approach is to store entity IDs**, then look up in System:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Follower')
|
||||
class Follower extends Component {
|
||||
targetId: number;
|
||||
followDistance: number = 50;
|
||||
|
||||
constructor(targetId: number) {
|
||||
super();
|
||||
this.targetId = targetId;
|
||||
}
|
||||
}
|
||||
|
||||
// Look up target entity and handle logic in System
|
||||
class FollowerSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(new Matcher().all(Follower, Position));
|
||||
}
|
||||
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const follower = entity.getComponent(Follower)!;
|
||||
const position = entity.getComponent(Position)!;
|
||||
|
||||
// Look up target entity through scene
|
||||
const target = entity.scene?.findEntityById(follower.targetId);
|
||||
if (target) {
|
||||
const targetPos = target.getComponent(Position);
|
||||
if (targetPos) {
|
||||
// Follow logic
|
||||
const dx = targetPos.x - position.x;
|
||||
const dy = targetPos.y - position.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance > follower.followDistance) {
|
||||
// Move closer to target
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Advantages of this approach:
|
||||
- Components stay simple, only store basic data types
|
||||
- Follows data-oriented design
|
||||
- Unified lookup and logic handling in System
|
||||
- Easy to understand and maintain
|
||||
|
||||
**Avoid storing entity references directly in components**:
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong example: Storing entity reference directly
|
||||
@ECSComponent('BadFollower')
|
||||
class BadFollower extends Component {
|
||||
target: Entity; // Still holds reference after entity destroyed, may cause memory leak
|
||||
}
|
||||
```
|
||||
|
||||
## Complex Component Examples
|
||||
|
||||
### State Machine Component
|
||||
|
||||
```typescript
|
||||
enum EntityState {
|
||||
Idle,
|
||||
Moving,
|
||||
Attacking,
|
||||
Dead
|
||||
}
|
||||
|
||||
@ECSComponent('StateMachine')
|
||||
class StateMachine extends Component {
|
||||
private _currentState: EntityState = EntityState.Idle;
|
||||
private _previousState: EntityState = EntityState.Idle;
|
||||
private _stateTimer: number = 0;
|
||||
|
||||
get currentState(): EntityState {
|
||||
return this._currentState;
|
||||
}
|
||||
|
||||
get previousState(): EntityState {
|
||||
return this._previousState;
|
||||
}
|
||||
|
||||
get stateTimer(): number {
|
||||
return this._stateTimer;
|
||||
}
|
||||
|
||||
changeState(newState: EntityState): void {
|
||||
if (this._currentState !== newState) {
|
||||
this._previousState = this._currentState;
|
||||
this._currentState = newState;
|
||||
this._stateTimer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
updateTimer(deltaTime: number): void {
|
||||
this._stateTimer += deltaTime;
|
||||
}
|
||||
|
||||
isInState(state: EntityState): boolean {
|
||||
return this._currentState === state;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration Data Component
|
||||
|
||||
```typescript
|
||||
interface WeaponData {
|
||||
damage: number;
|
||||
range: number;
|
||||
fireRate: number;
|
||||
ammo: number;
|
||||
}
|
||||
|
||||
@ECSComponent('WeaponConfig')
|
||||
class WeaponConfig extends Component {
|
||||
data: WeaponData;
|
||||
|
||||
constructor(weaponData: WeaponData) {
|
||||
super();
|
||||
this.data = { ...weaponData }; // Deep copy to avoid shared reference
|
||||
}
|
||||
|
||||
// Provide convenience methods
|
||||
getDamage(): number {
|
||||
return this.data.damage;
|
||||
}
|
||||
|
||||
canFire(): boolean {
|
||||
return this.data.ammo > 0;
|
||||
}
|
||||
|
||||
consumeAmmo(): boolean {
|
||||
if (this.data.ammo > 0) {
|
||||
this.data.ammo--;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Tag Components
|
||||
|
||||
```typescript
|
||||
// Tag components: No data, only for identification
|
||||
@ECSComponent('Player')
|
||||
class PlayerTag extends Component {}
|
||||
|
||||
@ECSComponent('Enemy')
|
||||
class EnemyTag extends Component {}
|
||||
|
||||
@ECSComponent('Dead')
|
||||
class DeadTag extends Component {}
|
||||
|
||||
// Use tags for querying
|
||||
class EnemySystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(EnemyTag, Health).none(DeadTag));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Buffer Components
|
||||
|
||||
```typescript
|
||||
// Event/command buffer component
|
||||
@ECSComponent('DamageBuffer')
|
||||
class DamageBuffer extends Component {
|
||||
damages: { amount: number; source: number; timestamp: number }[] = [];
|
||||
|
||||
addDamage(amount: number, sourceId: number): void {
|
||||
this.damages.push({
|
||||
amount,
|
||||
source: sourceId,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.damages.length = 0;
|
||||
}
|
||||
|
||||
getTotalDamage(): number {
|
||||
return this.damages.reduce((sum, d) => sum + d.amount, 0);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## FAQ
|
||||
|
||||
### Q: How large should a component be?
|
||||
|
||||
**A**: Follow single responsibility principle. If a component contains unrelated data, split into multiple components.
|
||||
|
||||
### Q: Can components have methods?
|
||||
|
||||
**A**: Yes, but they should be data-related helper methods (like `isDead()`), not business logic. Business logic goes in Systems.
|
||||
|
||||
### Q: How to handle dependencies between components?
|
||||
|
||||
**A**: Handle inter-component interactions in Systems, don't directly access other components within a component.
|
||||
|
||||
### Q: When to use EntityRef?
|
||||
|
||||
**A**: Only when you need frequent access to referenced entity and the reference relationship is stable (like parent-child). Storing IDs is better for most cases.
|
||||
|
||||
---
|
||||
|
||||
Components are the data carriers of ECS architecture. Properly designing components makes your game code more modular, maintainable, and performant.
|
||||
@@ -1,228 +0,0 @@
|
||||
---
|
||||
title: "EntityRef Decorator"
|
||||
description: "Safe entity reference tracking mechanism"
|
||||
---
|
||||
|
||||
The framework provides the `@EntityRef` decorator for **special scenarios** to safely store entity references. This is an advanced feature; storing IDs is recommended for most cases.
|
||||
|
||||
## When Do You Need EntityRef?
|
||||
|
||||
`@EntityRef` can simplify code in these scenarios:
|
||||
|
||||
1. **Parent-Child Relationships**: Need to directly access parent or child entities in components
|
||||
2. **Complex Associations**: Multiple reference relationships between entities
|
||||
3. **Frequent Access**: Need to access referenced entity in multiple places, ID lookup has performance overhead
|
||||
|
||||
## Core Features
|
||||
|
||||
The `@EntityRef` decorator automatically tracks references through **ReferenceTracker**:
|
||||
|
||||
- When the referenced entity is destroyed, all `@EntityRef` properties pointing to it are automatically set to `null`
|
||||
- Prevents cross-scene references (outputs warning and refuses to set)
|
||||
- Prevents references to destroyed entities (outputs warning and sets to `null`)
|
||||
- Uses WeakRef to avoid memory leaks (automatic GC support)
|
||||
- Automatically cleans up reference registration when component is removed
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent, EntityRef, Entity } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSComponent('Parent')
|
||||
class ParentComponent extends Component {
|
||||
@EntityRef()
|
||||
parent: Entity | null = null;
|
||||
}
|
||||
|
||||
// Usage example
|
||||
const scene = new Scene();
|
||||
const parent = scene.createEntity('Parent');
|
||||
const child = scene.createEntity('Child');
|
||||
|
||||
const comp = child.addComponent(new ParentComponent());
|
||||
comp.parent = parent;
|
||||
|
||||
console.log(comp.parent); // Entity { name: 'Parent' }
|
||||
|
||||
// When parent is destroyed, comp.parent automatically becomes null
|
||||
parent.destroy();
|
||||
console.log(comp.parent); // null
|
||||
```
|
||||
|
||||
## Multiple Reference Properties
|
||||
|
||||
A component can have multiple `@EntityRef` properties:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Combat')
|
||||
class CombatComponent extends Component {
|
||||
@EntityRef()
|
||||
target: Entity | null = null;
|
||||
|
||||
@EntityRef()
|
||||
ally: Entity | null = null;
|
||||
|
||||
@EntityRef()
|
||||
lastAttacker: Entity | null = null;
|
||||
}
|
||||
|
||||
// Usage example
|
||||
const player = scene.createEntity('Player');
|
||||
const enemy = scene.createEntity('Enemy');
|
||||
const npc = scene.createEntity('NPC');
|
||||
|
||||
const combat = player.addComponent(new CombatComponent());
|
||||
combat.target = enemy;
|
||||
combat.ally = npc;
|
||||
|
||||
// After enemy is destroyed, only target becomes null, ally remains valid
|
||||
enemy.destroy();
|
||||
console.log(combat.target); // null
|
||||
console.log(combat.ally); // Entity { name: 'NPC' }
|
||||
```
|
||||
|
||||
## Safety Checks
|
||||
|
||||
`@EntityRef` provides multiple safety checks:
|
||||
|
||||
```typescript
|
||||
const scene1 = new Scene();
|
||||
const scene2 = new Scene();
|
||||
|
||||
const entity1 = scene1.createEntity('Entity1');
|
||||
const entity2 = scene2.createEntity('Entity2');
|
||||
|
||||
const comp = entity1.addComponent(new ParentComponent());
|
||||
|
||||
// Cross-scene reference fails
|
||||
comp.parent = entity2; // Outputs error log, comp.parent is null
|
||||
console.log(comp.parent); // null
|
||||
|
||||
// Reference to destroyed entity fails
|
||||
const entity3 = scene1.createEntity('Entity3');
|
||||
entity3.destroy();
|
||||
comp.parent = entity3; // Outputs warning log, comp.parent is null
|
||||
console.log(comp.parent); // null
|
||||
```
|
||||
|
||||
## Implementation Principle
|
||||
|
||||
`@EntityRef` uses the following mechanisms for automatic reference tracking:
|
||||
|
||||
1. **ReferenceTracker**: Scene holds a reference tracker that records all entity reference relationships
|
||||
2. **WeakRef**: Uses weak references to store components, avoiding memory leaks from circular references
|
||||
3. **Property Interception**: Intercepts getter/setter through `Object.defineProperty`
|
||||
4. **Automatic Cleanup**: When entity is destroyed, ReferenceTracker traverses all references and sets them to null
|
||||
|
||||
```typescript
|
||||
// Simplified implementation principle
|
||||
class ReferenceTracker {
|
||||
// entityId -> all component records referencing this entity
|
||||
private _references: Map<number, Set<{ component: WeakRef<Component>, propertyKey: string }>>;
|
||||
|
||||
// Called when entity is destroyed
|
||||
clearReferencesTo(entityId: number): void {
|
||||
const records = this._references.get(entityId);
|
||||
if (records) {
|
||||
for (const record of records) {
|
||||
const component = record.component.deref();
|
||||
if (component) {
|
||||
// Set component's reference property to null
|
||||
(component as any)[record.propertyKey] = null;
|
||||
}
|
||||
}
|
||||
this._references.delete(entityId);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
`@EntityRef` introduces some performance overhead:
|
||||
|
||||
- **Write Overhead**: Need to update ReferenceTracker each time a reference is set
|
||||
- **Memory Overhead**: ReferenceTracker needs to maintain reference mapping table
|
||||
- **Destroy Overhead**: Need to traverse all references and clean up when entity is destroyed
|
||||
|
||||
For most scenarios, this overhead is acceptable. But with **many entities and frequent reference changes**, storing IDs may be more efficient.
|
||||
|
||||
## Debug Support
|
||||
|
||||
ReferenceTracker provides debug interfaces:
|
||||
|
||||
```typescript
|
||||
// View which components reference an entity
|
||||
const references = scene.referenceTracker.getReferencesTo(entity.id);
|
||||
console.log(`Entity ${entity.name} is referenced by ${references.length} components`);
|
||||
|
||||
// Get complete debug info
|
||||
const debugInfo = scene.referenceTracker.getDebugInfo();
|
||||
console.log(debugInfo);
|
||||
```
|
||||
|
||||
## Comparison with Storing IDs
|
||||
|
||||
### Storing IDs (Recommended for Most Cases)
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Follower')
|
||||
class Follower extends Component {
|
||||
targetId: number | null = null;
|
||||
}
|
||||
|
||||
// Look up in System
|
||||
class FollowerSystem extends EntitySystem {
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const follower = entity.getComponent(Follower)!;
|
||||
const target = entity.scene?.findEntityById(follower.targetId);
|
||||
if (target) {
|
||||
// Follow logic
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using EntityRef (For Complex Associations)
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Transform')
|
||||
class Transform extends Component {
|
||||
@EntityRef()
|
||||
parent: Entity | null = null;
|
||||
|
||||
position: { x: number, y: number } = { x: 0, y: 0 };
|
||||
|
||||
// Can directly access parent entity's component
|
||||
getWorldPosition(): { x: number, y: number } {
|
||||
if (!this.parent) {
|
||||
return { ...this.position };
|
||||
}
|
||||
|
||||
const parentTransform = this.parent.getComponent(Transform);
|
||||
if (parentTransform) {
|
||||
const parentPos = parentTransform.getWorldPosition();
|
||||
return {
|
||||
x: parentPos.x + this.position.x,
|
||||
y: parentPos.y + this.position.y
|
||||
};
|
||||
}
|
||||
|
||||
return { ...this.position };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
| Approach | Use Case | Pros | Cons |
|
||||
|----------|----------|------|------|
|
||||
| Store ID | Most cases | Simple, no extra overhead | Need to lookup in System |
|
||||
| @EntityRef | Parent-child, complex associations | Auto-cleanup, cleaner code | Has performance overhead |
|
||||
|
||||
- **Recommended**: Use store ID + System lookup for most cases
|
||||
- **EntityRef Use Cases**: Parent-child relationships, complex associations, when component needs direct access to referenced entity
|
||||
- **Core Advantage**: Automatic cleanup, prevents dangling references, cleaner code
|
||||
- **Considerations**: Has performance overhead, not suitable for many dynamic references
|
||||
@@ -1,226 +0,0 @@
|
||||
---
|
||||
title: "Component System"
|
||||
description: "ECS component basics and creation methods"
|
||||
---
|
||||
|
||||
In ECS architecture, Components are carriers of data and behavior. Components define the properties and functionality that entities possess, and are the core building blocks of ECS architecture.
|
||||
|
||||
## Basic Concepts
|
||||
|
||||
Components are concrete classes that inherit from the `Component` abstract base class, used for:
|
||||
- Storing entity data (such as position, velocity, health, etc.)
|
||||
- Defining behavior methods related to the data
|
||||
- Providing lifecycle callback hooks
|
||||
- Supporting serialization and debugging
|
||||
|
||||
## Creating Components
|
||||
|
||||
### Basic Component Definition
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
x: number = 0;
|
||||
y: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('Health')
|
||||
class Health extends Component {
|
||||
current: number;
|
||||
max: number;
|
||||
|
||||
constructor(max: number = 100) {
|
||||
super();
|
||||
this.max = max;
|
||||
this.current = max;
|
||||
}
|
||||
|
||||
// Components can contain behavior methods
|
||||
takeDamage(damage: number): void {
|
||||
this.current = Math.max(0, this.current - damage);
|
||||
}
|
||||
|
||||
heal(amount: number): void {
|
||||
this.current = Math.min(this.max, this.current + amount);
|
||||
}
|
||||
|
||||
isDead(): boolean {
|
||||
return this.current <= 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## @ECSComponent Decorator
|
||||
|
||||
`@ECSComponent` is a required decorator for component classes, providing type identification and metadata management.
|
||||
|
||||
### Why It's Required
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| **Type Identification** | Provides stable type name that remains correct after code obfuscation |
|
||||
| **Serialization Support** | Uses this name as type identifier during serialization/deserialization |
|
||||
| **Component Registration** | Auto-registers to ComponentRegistry, assigns unique bitmask |
|
||||
| **Debug Support** | Shows readable component names in debug tools and logs |
|
||||
|
||||
### Basic Syntax
|
||||
|
||||
```typescript
|
||||
@ECSComponent(typeName: string)
|
||||
```
|
||||
|
||||
- `typeName`: Component's type name, recommended to use same or similar name as class name
|
||||
|
||||
### Usage Examples
|
||||
|
||||
```typescript
|
||||
// ✅ Correct usage
|
||||
@ECSComponent('Velocity')
|
||||
class Velocity extends Component {
|
||||
dx: number = 0;
|
||||
dy: number = 0;
|
||||
}
|
||||
|
||||
// ✅ Recommended: Keep type name consistent with class name
|
||||
@ECSComponent('PlayerController')
|
||||
class PlayerController extends Component {
|
||||
speed: number = 5;
|
||||
}
|
||||
|
||||
// ❌ Wrong usage - no decorator
|
||||
class BadComponent extends Component {
|
||||
// Components defined this way may have issues in production:
|
||||
// 1. Class name changes after minification, can't serialize correctly
|
||||
// 2. Component not registered to framework, queries may fail
|
||||
}
|
||||
```
|
||||
|
||||
### Using with @Serializable
|
||||
|
||||
When components need serialization support, use `@ECSComponent` and `@Serializable` together:
|
||||
|
||||
```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;
|
||||
|
||||
// Fields without @Serialize() won't be serialized
|
||||
private _cachedData: any = null;
|
||||
}
|
||||
```
|
||||
|
||||
> **Note**: `@ECSComponent`'s `typeName` and `@Serializable`'s `typeId` can differ. If `@Serializable` doesn't specify `typeId`, it defaults to `@ECSComponent`'s `typeName`.
|
||||
|
||||
### Type Name Uniqueness
|
||||
|
||||
Each component's type name should be unique:
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong: Two components using the same type name
|
||||
@ECSComponent('Health')
|
||||
class HealthComponent extends Component { }
|
||||
|
||||
@ECSComponent('Health') // Conflict!
|
||||
class EnemyHealthComponent extends Component { }
|
||||
|
||||
// ✅ Correct: Use different type names
|
||||
@ECSComponent('PlayerHealth')
|
||||
class PlayerHealthComponent extends Component { }
|
||||
|
||||
@ECSComponent('EnemyHealth')
|
||||
class EnemyHealthComponent extends Component { }
|
||||
```
|
||||
|
||||
## Component Properties
|
||||
|
||||
Each component has some built-in properties:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('ExampleComponent')
|
||||
class ExampleComponent extends Component {
|
||||
someData: string = "example";
|
||||
|
||||
onAddedToEntity(): void {
|
||||
console.log(`Component ID: ${this.id}`); // Unique component ID
|
||||
console.log(`Entity ID: ${this.entityId}`); // Owning entity's ID
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Component and Entity Relationship
|
||||
|
||||
Components store the owning entity's ID (`entityId`), not a direct entity reference. This is a reflection of ECS's data-oriented design, avoiding circular references.
|
||||
|
||||
In practice, **entity and component interactions should be handled in Systems**, not within components:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Health')
|
||||
class Health extends Component {
|
||||
current: number;
|
||||
max: number;
|
||||
|
||||
constructor(max: number = 100) {
|
||||
super();
|
||||
this.max = max;
|
||||
this.current = max;
|
||||
}
|
||||
|
||||
isDead(): boolean {
|
||||
return this.current <= 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('Damage')
|
||||
class Damage extends Component {
|
||||
value: number;
|
||||
|
||||
constructor(value: number) {
|
||||
super();
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Recommended: Handle logic in System
|
||||
class DamageSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(new Matcher().all(Health, Damage));
|
||||
}
|
||||
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health)!;
|
||||
const damage = entity.getComponent(Damage)!;
|
||||
|
||||
health.current -= damage.value;
|
||||
|
||||
if (health.isDead()) {
|
||||
entity.destroy();
|
||||
}
|
||||
|
||||
// Remove Damage component after applying damage
|
||||
entity.removeComponent(damage);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## More Topics
|
||||
|
||||
- [Lifecycle](/en/guide/component/lifecycle) - Component lifecycle hooks
|
||||
- [EntityRef Decorator](/en/guide/component/entity-ref) - Safe entity references
|
||||
- [Best Practices](/en/guide/component/best-practices) - Component design patterns and examples
|
||||
@@ -1,182 +0,0 @@
|
||||
---
|
||||
title: "Component Lifecycle"
|
||||
description: "Component lifecycle hooks and events"
|
||||
---
|
||||
|
||||
Components provide lifecycle hooks that can be overridden to execute specific logic.
|
||||
|
||||
## Lifecycle Methods
|
||||
|
||||
```typescript
|
||||
@ECSComponent('ExampleComponent')
|
||||
class ExampleComponent extends Component {
|
||||
private resource: SomeResource | null = null;
|
||||
|
||||
/**
|
||||
* Called when component is added to entity
|
||||
* Use for initializing resources, establishing references, etc.
|
||||
*/
|
||||
onAddedToEntity(): void {
|
||||
console.log(`Component ${this.constructor.name} added, Entity ID: ${this.entityId}`);
|
||||
this.resource = new SomeResource();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when component is removed from entity
|
||||
* Use for cleaning up resources, breaking references, etc.
|
||||
*/
|
||||
onRemovedFromEntity(): void {
|
||||
console.log(`Component ${this.constructor.name} removed`);
|
||||
if (this.resource) {
|
||||
this.resource.cleanup();
|
||||
this.resource = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Lifecycle Order
|
||||
|
||||
```
|
||||
Entity created
|
||||
↓
|
||||
addComponent() called
|
||||
↓
|
||||
onAddedToEntity() triggered
|
||||
↓
|
||||
Component in normal use...
|
||||
↓
|
||||
removeComponent() or entity.destroy() called
|
||||
↓
|
||||
onRemovedFromEntity() triggered
|
||||
↓
|
||||
Component removed/destroyed
|
||||
```
|
||||
|
||||
## Practical Use Cases
|
||||
|
||||
### Resource Management
|
||||
|
||||
```typescript
|
||||
@ECSComponent('TextureComponent')
|
||||
class TextureComponent extends Component {
|
||||
private _texture: Texture | null = null;
|
||||
texturePath: string = '';
|
||||
|
||||
onAddedToEntity(): void {
|
||||
// Load texture resource
|
||||
this._texture = TextureManager.load(this.texturePath);
|
||||
}
|
||||
|
||||
onRemovedFromEntity(): void {
|
||||
// Release texture resource
|
||||
if (this._texture) {
|
||||
TextureManager.release(this._texture);
|
||||
this._texture = null;
|
||||
}
|
||||
}
|
||||
|
||||
get texture(): Texture | null {
|
||||
return this._texture;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Event Listening
|
||||
|
||||
```typescript
|
||||
@ECSComponent('InputListener')
|
||||
class InputListener extends Component {
|
||||
private _boundHandler: ((e: KeyboardEvent) => void) | null = null;
|
||||
|
||||
onAddedToEntity(): void {
|
||||
this._boundHandler = this.handleKeyDown.bind(this);
|
||||
window.addEventListener('keydown', this._boundHandler);
|
||||
}
|
||||
|
||||
onRemovedFromEntity(): void {
|
||||
if (this._boundHandler) {
|
||||
window.removeEventListener('keydown', this._boundHandler);
|
||||
this._boundHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
private handleKeyDown(e: KeyboardEvent): void {
|
||||
// Handle keyboard input
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Registering with External Systems
|
||||
|
||||
```typescript
|
||||
@ECSComponent('PhysicsBody')
|
||||
class PhysicsBody extends Component {
|
||||
private _body: PhysicsWorld.Body | null = null;
|
||||
|
||||
onAddedToEntity(): void {
|
||||
// Create rigid body in physics world
|
||||
this._body = PhysicsWorld.createBody({
|
||||
entityId: this.entityId,
|
||||
type: 'dynamic'
|
||||
});
|
||||
}
|
||||
|
||||
onRemovedFromEntity(): void {
|
||||
// Remove rigid body from physics world
|
||||
if (this._body) {
|
||||
PhysicsWorld.removeBody(this._body);
|
||||
this._body = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
### Avoid Accessing Other Components in Lifecycle
|
||||
|
||||
```typescript
|
||||
@ECSComponent('BadComponent')
|
||||
class BadComponent extends Component {
|
||||
onAddedToEntity(): void {
|
||||
// ⚠️ Not recommended: Other components may not be added yet
|
||||
const other = this.entity?.getComponent(OtherComponent);
|
||||
if (other) {
|
||||
// May be null
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Recommended: Use System to Handle Inter-Component Interactions
|
||||
|
||||
```typescript
|
||||
@ECSSystem('InitializationSystem')
|
||||
class InitializationSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(ComponentA, ComponentB));
|
||||
}
|
||||
|
||||
// Use onAdded event to ensure both components exist
|
||||
onAdded(entity: Entity): void {
|
||||
const a = entity.getComponent(ComponentA)!;
|
||||
const b = entity.getComponent(ComponentB)!;
|
||||
// Safely initialize interaction
|
||||
a.linkTo(b);
|
||||
}
|
||||
|
||||
onRemoved(entity: Entity): void {
|
||||
// Cleanup
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Comparison with System Lifecycle
|
||||
|
||||
| Feature | Component Lifecycle | System Lifecycle |
|
||||
|---------|---------------------|------------------|
|
||||
| Trigger Timing | When component added/removed | When match conditions met |
|
||||
| Use Case | Resource init/cleanup | Business logic processing |
|
||||
| Access Other Components | Not recommended | Safe |
|
||||
| Access Scene | Limited | Full |
|
||||
@@ -1,225 +0,0 @@
|
||||
---
|
||||
title: "Best Practices"
|
||||
description: "Entity query optimization and practical applications"
|
||||
---
|
||||
|
||||
## Design Principles
|
||||
|
||||
### 1. Prefer EntitySystem
|
||||
|
||||
```typescript
|
||||
// ✅ Recommended: Use EntitySystem
|
||||
class GoodSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(HealthComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Automatically get matching entities, updated each frame
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ Not recommended: Manual query in update
|
||||
class BadSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty());
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Manual query each frame wastes performance
|
||||
const result = this.scene!.querySystem.queryAll(HealthComponent);
|
||||
for (const entity of result.entities) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Use none() for Exclusions
|
||||
|
||||
```typescript
|
||||
// Exclude dead enemies
|
||||
class EnemyAISystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(
|
||||
Matcher.empty()
|
||||
.all(EnemyTag, AIComponent)
|
||||
.none(DeadTag) // Don't process dead enemies
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Use Tags for Query Optimization
|
||||
|
||||
```typescript
|
||||
// ❌ Bad: Query all entities then filter
|
||||
const allEntities = scene.querySystem.getAllEntities();
|
||||
const players = allEntities.filter(e => e.hasComponent(PlayerTag));
|
||||
|
||||
// ✅ Good: Query directly by tag
|
||||
const players = scene.querySystem.queryByTag(Tags.PLAYER).entities;
|
||||
```
|
||||
|
||||
### 4. Avoid Overly Complex Query Conditions
|
||||
|
||||
```typescript
|
||||
// ❌ Not recommended: Too complex
|
||||
super(
|
||||
Matcher.empty()
|
||||
.all(A, B, C, D)
|
||||
.any(E, F, G)
|
||||
.none(H, I, J)
|
||||
);
|
||||
|
||||
// ✅ Recommended: Split into multiple simple systems
|
||||
class SystemAB extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(A, B));
|
||||
}
|
||||
}
|
||||
|
||||
class SystemCD extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(C, D));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Practical Applications
|
||||
|
||||
### Scenario 1: Physics System
|
||||
|
||||
```typescript
|
||||
class PhysicsSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(TransformComponent, RigidbodyComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const transform = entity.getComponent(TransformComponent)!;
|
||||
const rigidbody = entity.getComponent(RigidbodyComponent)!;
|
||||
|
||||
// Apply gravity
|
||||
rigidbody.velocity.y -= 9.8 * Time.deltaTime;
|
||||
|
||||
// Update position
|
||||
transform.position.x += rigidbody.velocity.x * Time.deltaTime;
|
||||
transform.position.y += rigidbody.velocity.y * Time.deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Scenario 2: Render System
|
||||
|
||||
```typescript
|
||||
class RenderSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(
|
||||
Matcher.empty()
|
||||
.all(TransformComponent, SpriteComponent)
|
||||
.none(InvisibleTag) // Exclude invisible entities
|
||||
);
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Sort by z-order
|
||||
const sorted = entities.slice().sort((a, b) => {
|
||||
const zA = a.getComponent(TransformComponent)!.z;
|
||||
const zB = b.getComponent(TransformComponent)!.z;
|
||||
return zA - zB;
|
||||
});
|
||||
|
||||
// Render entities
|
||||
for (const entity of sorted) {
|
||||
const transform = entity.getComponent(TransformComponent)!;
|
||||
const sprite = entity.getComponent(SpriteComponent)!;
|
||||
|
||||
renderer.drawSprite(sprite.texture, transform.position);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Scenario 3: Collision Detection
|
||||
|
||||
```typescript
|
||||
class CollisionSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(TransformComponent, ColliderComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Simple O(n²) collision detection
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
for (let j = i + 1; j < entities.length; j++) {
|
||||
this.checkCollision(entities[i], entities[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private checkCollision(a: Entity, b: Entity): void {
|
||||
const transA = a.getComponent(TransformComponent)!;
|
||||
const transB = b.getComponent(TransformComponent)!;
|
||||
const colliderA = a.getComponent(ColliderComponent)!;
|
||||
const colliderB = b.getComponent(ColliderComponent)!;
|
||||
|
||||
if (this.isOverlapping(transA, colliderA, transB, colliderB)) {
|
||||
// Trigger collision event
|
||||
scene.eventSystem.emit('collision', { entityA: a, entityB: b });
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Scenario 4: One-Time Query
|
||||
|
||||
```typescript
|
||||
// Execute one-time query outside of systems
|
||||
class GameManager {
|
||||
private scene: Scene;
|
||||
|
||||
public countEnemies(): number {
|
||||
const result = this.scene.querySystem.queryByTag(Tags.ENEMY);
|
||||
return result.count;
|
||||
}
|
||||
|
||||
public findNearestEnemy(playerPos: Vector2): Entity | null {
|
||||
const enemies = this.scene.querySystem.queryByTag(Tags.ENEMY);
|
||||
|
||||
let nearest: Entity | null = null;
|
||||
let minDistance = Infinity;
|
||||
|
||||
for (const enemy of enemies.entities) {
|
||||
const transform = enemy.getComponent(TransformComponent);
|
||||
if (!transform) continue;
|
||||
|
||||
const distance = Vector2.distance(playerPos, transform.position);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
nearest = enemy;
|
||||
}
|
||||
}
|
||||
|
||||
return nearest;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Statistics
|
||||
|
||||
```typescript
|
||||
const stats = querySystem.getStats();
|
||||
console.log('Total queries:', stats.queryStats.totalQueries);
|
||||
console.log('Cache hit rate:', stats.queryStats.cacheHitRate);
|
||||
console.log('Cache size:', stats.cacheStats.size);
|
||||
```
|
||||
|
||||
## Related APIs
|
||||
|
||||
- [Matcher](/api/classes/Matcher/) - Query condition descriptor API reference
|
||||
- [QuerySystem](/api/classes/QuerySystem/) - Query system API reference
|
||||
- [EntitySystem](/api/classes/EntitySystem/) - Entity system API reference
|
||||
- [Entity](/api/classes/Entity/) - Entity API reference
|
||||
@@ -1,133 +0,0 @@
|
||||
---
|
||||
title: "Compiled Query"
|
||||
description: "CompiledQuery type-safe query tool"
|
||||
---
|
||||
|
||||
> **v2.4.0+**
|
||||
|
||||
CompiledQuery is a lightweight query tool providing type-safe component access and change detection support. Suitable for ad-hoc queries, tool development, and simple iteration scenarios.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```typescript
|
||||
// Create compiled query
|
||||
const query = scene.querySystem.compile(Position, Velocity);
|
||||
|
||||
// Type-safe iteration - component parameters auto-infer types
|
||||
query.forEach((entity, pos, vel) => {
|
||||
pos.x += vel.vx * deltaTime;
|
||||
pos.y += vel.vy * deltaTime;
|
||||
});
|
||||
|
||||
// Get entity count
|
||||
console.log(`Matched entities: ${query.count}`);
|
||||
|
||||
// Get first matched entity
|
||||
const first = query.first();
|
||||
if (first) {
|
||||
const [entity, pos, vel] = first;
|
||||
console.log(`First entity: ${entity.name}`);
|
||||
}
|
||||
```
|
||||
|
||||
## Change Detection
|
||||
|
||||
CompiledQuery supports epoch-based change detection:
|
||||
|
||||
```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 {
|
||||
// Only process entities where Transform or Sprite changed
|
||||
this._query.forEachChanged(this._lastEpoch, (entity, transform, sprite) => {
|
||||
this.updateRenderData(entity, transform, sprite);
|
||||
});
|
||||
|
||||
// Save current epoch as next check starting point
|
||||
this._lastEpoch = this.scene!.epochManager.current;
|
||||
}
|
||||
|
||||
private updateRenderData(entity: Entity, transform: Transform, sprite: Sprite): void {
|
||||
// Update render data
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Functional API
|
||||
|
||||
CompiledQuery provides rich functional APIs:
|
||||
|
||||
```typescript
|
||||
const query = scene.querySystem.compile(Position, Health);
|
||||
|
||||
// map - Transform entity data
|
||||
const positions = query.map((entity, pos, health) => ({
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
healthPercent: health.current / health.max
|
||||
}));
|
||||
|
||||
// filter - Filter entities
|
||||
const lowHealthEntities = query.filter((entity, pos, health) => {
|
||||
return health.current < health.max * 0.2;
|
||||
});
|
||||
|
||||
// find - Find first matching entity
|
||||
const target = query.find((entity, pos, health) => {
|
||||
return health.current > 0 && pos.x > 100;
|
||||
});
|
||||
|
||||
// toArray - Convert to array
|
||||
const allData = query.toArray();
|
||||
for (const [entity, pos, health] of allData) {
|
||||
console.log(`${entity.name}: ${pos.x}, ${pos.y}`);
|
||||
}
|
||||
|
||||
// any/empty - Check for matches
|
||||
if (query.any()) {
|
||||
console.log('Has matching entities');
|
||||
}
|
||||
if (query.empty()) {
|
||||
console.log('No matching entities');
|
||||
}
|
||||
```
|
||||
|
||||
## CompiledQuery vs EntitySystem
|
||||
|
||||
| Feature | CompiledQuery | EntitySystem |
|
||||
|---------|---------------|--------------|
|
||||
| **Purpose** | Lightweight query tool | Complete system logic |
|
||||
| **Lifecycle** | None | Full (onInitialize, onDestroy, etc.) |
|
||||
| **Scheduling Integration** | None | Supports @Stage, @Before, @After |
|
||||
| **Change Detection** | forEachChanged | forEachChanged |
|
||||
| **Event Listening** | None | addEventListener |
|
||||
| **Command Buffer** | None | this.commands |
|
||||
| **Type-Safe Components** | forEach params auto-infer | Need manual getComponent |
|
||||
| **Use Cases** | Ad-hoc queries, tools, prototypes | Core game logic |
|
||||
|
||||
**Selection Advice**:
|
||||
|
||||
- Use **EntitySystem** for core game logic (movement, combat, AI, etc.)
|
||||
- Use **CompiledQuery** for one-time queries, tool development, or simple iteration
|
||||
|
||||
## API Reference
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `forEach(callback)` | Iterate all matched entities, type-safe component params |
|
||||
| `forEachChanged(sinceEpoch, callback)` | Only iterate changed entities |
|
||||
| `first()` | Get first matched entity and components |
|
||||
| `toArray()` | Convert to [entity, ...components] array |
|
||||
| `map(callback)` | Map transformation |
|
||||
| `filter(predicate)` | Filter entities |
|
||||
| `find(predicate)` | Find first entity meeting condition |
|
||||
| `any()` | Whether any matches exist |
|
||||
| `empty()` | Whether no matches exist |
|
||||
| `count` | Number of matched entities |
|
||||
| `entities` | Matched entity list (read-only) |
|
||||
@@ -1,244 +0,0 @@
|
||||
---
|
||||
title: "Entity Query System"
|
||||
description: "ECS entity query core concepts and basic usage"
|
||||
---
|
||||
|
||||
Entity querying is one of the core features of ECS architecture. This guide introduces how to use Matcher and QuerySystem to query and filter entities.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Matcher - Query Condition Descriptor
|
||||
|
||||
Matcher is a chainable API used to describe entity query conditions. It doesn't execute queries itself but passes conditions to EntitySystem or QuerySystem.
|
||||
|
||||
### QuerySystem - Query Execution Engine
|
||||
|
||||
QuerySystem is responsible for actually executing queries, using reactive query mechanisms internally for automatic performance optimization.
|
||||
|
||||
## Using Matcher in EntitySystem
|
||||
|
||||
This is the most common usage. EntitySystem automatically filters and processes entities matching conditions through Matcher.
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, Matcher, Entity, Component } from '@esengine/ecs-framework';
|
||||
|
||||
class PositionComponent extends Component {
|
||||
public x: number = 0;
|
||||
public y: number = 0;
|
||||
}
|
||||
|
||||
class VelocityComponent extends Component {
|
||||
public vx: number = 0;
|
||||
public vy: number = 0;
|
||||
}
|
||||
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// Method 1: Use Matcher.empty().all()
|
||||
super(Matcher.empty().all(PositionComponent, VelocityComponent));
|
||||
|
||||
// Method 2: Use Matcher.all() directly (equivalent)
|
||||
// super(Matcher.all(PositionComponent, VelocityComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const pos = entity.getComponent(PositionComponent)!;
|
||||
const vel = entity.getComponent(VelocityComponent)!;
|
||||
|
||||
pos.x += vel.vx;
|
||||
pos.y += vel.vy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add to scene
|
||||
scene.addEntityProcessor(new MovementSystem());
|
||||
```
|
||||
|
||||
### Matcher Chainable API
|
||||
|
||||
#### all() - Must Include All Components
|
||||
|
||||
```typescript
|
||||
class HealthSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// Entity must have both Health and Position components
|
||||
super(Matcher.empty().all(HealthComponent, PositionComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Only process entities with both components
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### any() - Include At Least One Component
|
||||
|
||||
```typescript
|
||||
class DamageableSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// Entity must have at least Health or Shield
|
||||
super(Matcher.any(HealthComponent, ShieldComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Process entities with health or shield
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### none() - Must Not Include Components
|
||||
|
||||
```typescript
|
||||
class AliveEntitySystem extends EntitySystem {
|
||||
constructor() {
|
||||
// Entity must not have DeadTag component
|
||||
super(Matcher.all(HealthComponent).none(DeadTag));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Only process living entities
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Combined Conditions
|
||||
|
||||
```typescript
|
||||
class CombatSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(
|
||||
Matcher.empty()
|
||||
.all(PositionComponent, HealthComponent) // Must have position and health
|
||||
.any(WeaponComponent, MagicComponent) // At least weapon or magic
|
||||
.none(DeadTag, FrozenTag) // Not dead or frozen
|
||||
);
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Process living entities that can fight
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### nothing() - Match No Entities
|
||||
|
||||
Used for systems that only need lifecycle methods (`onBegin`, `onEnd`) but don't process entities.
|
||||
|
||||
```typescript
|
||||
class FrameTimerSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// Match no entities
|
||||
super(Matcher.nothing());
|
||||
}
|
||||
|
||||
protected onBegin(): void {
|
||||
// Execute at frame start
|
||||
Performance.markFrameStart();
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Never called because no matching entities
|
||||
}
|
||||
|
||||
protected onEnd(): void {
|
||||
// Execute at frame end
|
||||
Performance.markFrameEnd();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### empty() vs nothing()
|
||||
|
||||
| Method | Behavior | Use Case |
|
||||
|--------|----------|----------|
|
||||
| `Matcher.empty()` | Match **all** entities | Process all entities in scene |
|
||||
| `Matcher.nothing()` | Match **no** entities | Only need lifecycle callbacks |
|
||||
|
||||
## Using QuerySystem Directly
|
||||
|
||||
If you don't need to create a system, you can use Scene's querySystem directly.
|
||||
|
||||
### Basic Query Methods
|
||||
|
||||
```typescript
|
||||
// Get scene's query system
|
||||
const querySystem = scene.querySystem;
|
||||
|
||||
// Query entities with all specified components
|
||||
const result1 = querySystem.queryAll(PositionComponent, VelocityComponent);
|
||||
console.log(`Found ${result1.count} moving entities`);
|
||||
console.log(`Query time: ${result1.executionTime.toFixed(2)}ms`);
|
||||
|
||||
// Query entities with any specified component
|
||||
const result2 = querySystem.queryAny(WeaponComponent, MagicComponent);
|
||||
console.log(`Found ${result2.count} combat units`);
|
||||
|
||||
// Query entities without specified components
|
||||
const result3 = querySystem.queryNone(DeadTag);
|
||||
console.log(`Found ${result3.count} living entities`);
|
||||
```
|
||||
|
||||
### Query by Tag and Name
|
||||
|
||||
```typescript
|
||||
// Query by tag
|
||||
const playerResult = querySystem.queryByTag(Tags.PLAYER);
|
||||
for (const player of playerResult.entities) {
|
||||
console.log('Player:', player.name);
|
||||
}
|
||||
|
||||
// Query by name
|
||||
const bossResult = querySystem.queryByName('Boss');
|
||||
if (bossResult.count > 0) {
|
||||
const boss = bossResult.entities[0];
|
||||
console.log('Found Boss:', boss);
|
||||
}
|
||||
|
||||
// Query by single component
|
||||
const healthResult = querySystem.queryByComponent(HealthComponent);
|
||||
console.log(`${healthResult.count} entities have health`);
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Automatic Caching
|
||||
|
||||
QuerySystem uses reactive queries internally with automatic caching:
|
||||
|
||||
```typescript
|
||||
// First query, executes actual query
|
||||
const result1 = querySystem.queryAll(PositionComponent);
|
||||
console.log('fromCache:', result1.fromCache); // false
|
||||
|
||||
// Second same query, uses cache
|
||||
const result2 = querySystem.queryAll(PositionComponent);
|
||||
console.log('fromCache:', result2.fromCache); // true
|
||||
```
|
||||
|
||||
### Automatic Updates on Entity Changes
|
||||
|
||||
Query cache updates automatically when entities add/remove components:
|
||||
|
||||
```typescript
|
||||
// Query entities with weapons
|
||||
const before = querySystem.queryAll(WeaponComponent);
|
||||
console.log('Before:', before.count); // Assume 5
|
||||
|
||||
// Add weapon to entity
|
||||
const enemy = scene.createEntity('Enemy');
|
||||
enemy.addComponent(new WeaponComponent());
|
||||
|
||||
// Query again, automatically includes new entity
|
||||
const after = querySystem.queryAll(WeaponComponent);
|
||||
console.log('After:', after.count); // Now 6
|
||||
```
|
||||
|
||||
## More Topics
|
||||
|
||||
- [Matcher API](/en/guide/entity-query/matcher-api) - Complete Matcher API reference
|
||||
- [Compiled Query](/en/guide/entity-query/compiled-query) - CompiledQuery advanced usage
|
||||
- [Best Practices](/en/guide/entity-query/best-practices) - Query optimization and practical applications
|
||||
@@ -1,118 +0,0 @@
|
||||
---
|
||||
title: "Matcher API"
|
||||
description: "Complete Matcher API reference"
|
||||
---
|
||||
|
||||
## Static Creation Methods
|
||||
|
||||
| Method | Description | Example |
|
||||
|--------|-------------|---------|
|
||||
| `Matcher.all(...types)` | Must include all specified components | `Matcher.all(Position, Velocity)` |
|
||||
| `Matcher.any(...types)` | Include at least one specified component | `Matcher.any(Health, Shield)` |
|
||||
| `Matcher.none(...types)` | Must not include any specified components | `Matcher.none(Dead)` |
|
||||
| `Matcher.byTag(tag)` | Query by tag | `Matcher.byTag(1)` |
|
||||
| `Matcher.byName(name)` | Query by name | `Matcher.byName("Player")` |
|
||||
| `Matcher.byComponent(type)` | Query by single component | `Matcher.byComponent(Health)` |
|
||||
| `Matcher.empty()` | Create empty matcher (matches all entities) | `Matcher.empty()` |
|
||||
| `Matcher.nothing()` | Match no entities | `Matcher.nothing()` |
|
||||
| `Matcher.complex()` | Create complex query builder | `Matcher.complex()` |
|
||||
|
||||
## Chainable Methods
|
||||
|
||||
| Method | Description | Example |
|
||||
|--------|-------------|---------|
|
||||
| `.all(...types)` | Add required components | `.all(Position)` |
|
||||
| `.any(...types)` | Add optional components (at least one) | `.any(Weapon, Magic)` |
|
||||
| `.none(...types)` | Add excluded components | `.none(Dead)` |
|
||||
| `.exclude(...types)` | Alias for `.none()` | `.exclude(Disabled)` |
|
||||
| `.one(...types)` | Alias for `.any()` | `.one(Player, Enemy)` |
|
||||
| `.withTag(tag)` | Add tag condition | `.withTag(1)` |
|
||||
| `.withName(name)` | Add name condition | `.withName("Boss")` |
|
||||
| `.withComponent(type)` | Add single component condition | `.withComponent(Health)` |
|
||||
|
||||
## Utility Methods
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `.getCondition()` | Get query condition (read-only) |
|
||||
| `.isEmpty()` | Check if empty condition |
|
||||
| `.isNothing()` | Check if nothing matcher |
|
||||
| `.clone()` | Clone matcher |
|
||||
| `.reset()` | Reset all conditions |
|
||||
| `.toString()` | Get string representation |
|
||||
|
||||
## Common Combination Examples
|
||||
|
||||
```typescript
|
||||
// Basic movement system
|
||||
Matcher.all(Position, Velocity)
|
||||
|
||||
// Attackable living entities
|
||||
Matcher.all(Position, Health)
|
||||
.any(Weapon, Magic)
|
||||
.none(Dead, Disabled)
|
||||
|
||||
// All tagged enemies
|
||||
Matcher.byTag(Tags.ENEMY)
|
||||
.all(AIComponent)
|
||||
|
||||
// System only needing lifecycle
|
||||
Matcher.nothing()
|
||||
```
|
||||
|
||||
## Query by Tag
|
||||
|
||||
```typescript
|
||||
class PlayerSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// Query entities with specific tag
|
||||
super(Matcher.empty().withTag(Tags.PLAYER));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Only process player entities
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Query by Name
|
||||
|
||||
```typescript
|
||||
class BossSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// Query entities with specific name
|
||||
super(Matcher.empty().withName('Boss'));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Only process entities named 'Boss'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
### Matcher is Immutable
|
||||
|
||||
```typescript
|
||||
const matcher = Matcher.empty().all(PositionComponent);
|
||||
|
||||
// Chain calls return new Matcher instances
|
||||
const matcher2 = matcher.any(VelocityComponent);
|
||||
|
||||
// Original matcher unchanged
|
||||
console.log(matcher === matcher2); // false
|
||||
```
|
||||
|
||||
### Query Results are Read-Only
|
||||
|
||||
```typescript
|
||||
const result = querySystem.queryAll(PositionComponent);
|
||||
|
||||
// Don't modify returned array
|
||||
result.entities.push(someEntity); // Wrong!
|
||||
|
||||
// If modification needed, copy first
|
||||
const mutableArray = [...result.entities];
|
||||
mutableArray.push(someEntity); // Correct
|
||||
```
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user