Compare commits
24 Commits
@esengine/
...
feat/textu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8662449dcf | ||
|
|
1834bc2068 | ||
|
|
c23c6c21db | ||
|
|
b494283e9c | ||
|
|
9b334f36e1 | ||
|
|
7f8d2eb142 | ||
|
|
9d3eeb1980 | ||
|
|
0bcb675c3b | ||
|
|
574b4d08a3 | ||
|
|
d64e463a71 | ||
|
|
792fd05c85 | ||
|
|
7814b97ace | ||
|
|
75be905f14 | ||
|
|
01293590e8 | ||
|
|
b236b729b4 | ||
|
|
0170dc6e9c | ||
|
|
7834328ae0 | ||
|
|
39fa797299 | ||
|
|
03229ffb59 | ||
|
|
844a770335 | ||
|
|
c8dc9869a3 | ||
|
|
38755c9014 | ||
|
|
5d5537e4c7 | ||
|
|
9da9f5f068 |
@@ -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,58 +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/network-server",
|
||||
"@esengine/editor-core",
|
||||
"@esengine/editor-runtime",
|
||||
"@esengine/editor-app",
|
||||
"@esengine/sprite-editor",
|
||||
"@esengine/camera-editor",
|
||||
"@esengine/particle-editor",
|
||||
"@esengine/tilemap-editor",
|
||||
"@esengine/mesh-3d-editor",
|
||||
"@esengine/fairygui-editor",
|
||||
"@esengine/physics-rapier2d-editor",
|
||||
"@esengine/behavior-tree-editor",
|
||||
"@esengine/blueprint-editor",
|
||||
"@esengine/asset-system-editor",
|
||||
"@esengine/material-editor",
|
||||
"@esengine/shader-editor",
|
||||
"@esengine/world-streaming-editor",
|
||||
"@esengine/node-editor",
|
||||
"@esengine/sdk",
|
||||
"@esengine/worker-generator",
|
||||
"@esengine/engine"
|
||||
]
|
||||
}
|
||||
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"
|
||||
117
.github/workflows/ci.yml
vendored
117
.github/workflows/ci.yml
vendored
@@ -13,35 +13,18 @@ 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
|
||||
@@ -56,35 +39,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/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
|
||||
@@ -95,11 +110,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
|
||||
@@ -107,6 +120,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`);
|
||||
}
|
||||
6
.github/workflows/codecov.yml
vendored
6
.github/workflows/codecov.yml
vendored
@@ -28,7 +28,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 +36,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 +46,4 @@ jobs:
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-report
|
||||
path: packages/framework/core/coverage/
|
||||
path: packages/core/coverage/
|
||||
|
||||
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>
|
||||
70
.github/workflows/release-changesets.yml
vendored
70
.github/workflows/release-changesets.yml
vendored
@@ -1,70 +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/network" build
|
||||
pnpm --filter "@esengine/cli" build
|
||||
|
||||
- name: Create Release Pull Request or Publish
|
||||
id: changesets
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
version: pnpm changeset:version
|
||||
publish: pnpm changeset:publish
|
||||
title: 'chore: release packages'
|
||||
commit: 'chore: release packages'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
62
.github/workflows/release-editor.yml
vendored
62
.github/workflows/release-editor.yml
vendored
@@ -50,7 +50,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 +65,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,15 +80,15 @@ 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
|
||||
@@ -99,7 +99,7 @@ jobs:
|
||||
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.'
|
||||
@@ -116,8 +116,8 @@ jobs:
|
||||
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
|
||||
packages/editor-app/src-tauri/target/release/bundle/nsis/*.exe
|
||||
packages/editor-app/src-tauri/target/release/bundle/msi/*.msi
|
||||
retention-days: 1
|
||||
|
||||
# SignPath 代码签名(Windows)
|
||||
@@ -156,25 +156,18 @@ jobs:
|
||||
if: steps.check-signpath.outputs.enabled == 'true'
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get artifact ID
|
||||
- name: Download Windows artifact
|
||||
if: steps.check-signpath.outputs.enabled == 'true'
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: windows-unsigned
|
||||
path: ./artifacts
|
||||
|
||||
- name: List artifacts for signing
|
||||
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"
|
||||
echo "Files to be signed:"
|
||||
find ./artifacts -type f \( -name "*.exe" -o -name "*.msi" \) | head -20
|
||||
|
||||
- name: Submit to SignPath for code signing
|
||||
if: steps.check-signpath.outputs.enabled == 'true'
|
||||
@@ -185,8 +178,8 @@ jobs:
|
||||
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 }}
|
||||
artifact-configuration-slug: 'default'
|
||||
github-artifact-name: 'windows-unsigned'
|
||||
wait-for-completion: true
|
||||
wait-for-completion-timeout-in-seconds: 600
|
||||
output-artifact-directory: './signed'
|
||||
@@ -197,8 +190,7 @@ jobs:
|
||||
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
|
||||
draft: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -221,7 +213,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 +231,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 }})
|
||||
|
||||
42
.github/workflows/release.yml
vendored
42
.github/workflows/release.yml
vendored
@@ -1,6 +1,7 @@
|
||||
name: Release NPM Packages
|
||||
|
||||
on:
|
||||
# 标签触发:支持 v* 和 {package}-v* 格式
|
||||
# Tag trigger: supports v* and {package}-v* formats
|
||||
push:
|
||||
tags:
|
||||
@@ -14,11 +15,12 @@ on:
|
||||
- 'physics-rapier2d-v*'
|
||||
- 'worker-generator-v*'
|
||||
|
||||
# Manual trigger option
|
||||
# 保留手动触发选项
|
||||
# Keep manual trigger option
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
package:
|
||||
description: 'Select package to publish'
|
||||
description: '选择要发布的包 | Select package to publish'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
@@ -31,7 +33,7 @@ on:
|
||||
- physics-rapier2d
|
||||
- worker-generator
|
||||
version_type:
|
||||
description: 'Version bump type'
|
||||
description: '版本更新类型 | Version bump type'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
@@ -59,10 +61,11 @@ jobs:
|
||||
id: parse
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "push" ]; then
|
||||
# Parse package and version from tag
|
||||
# 从标签解析包名和版本 | Parse package and version from tag
|
||||
TAG="${GITHUB_REF#refs/tags/}"
|
||||
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
# 解析格式:v1.0.0 或 package-v1.0.0
|
||||
# Parse format: v1.0.0 or package-v1.0.0
|
||||
if [[ "$TAG" =~ ^v([0-9]+\.[0-9]+\.[0-9]+.*)$ ]]; then
|
||||
PACKAGE="core"
|
||||
@@ -79,9 +82,10 @@ jobs:
|
||||
echo "package=$PACKAGE" >> $GITHUB_OUTPUT
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "mode=tag" >> $GITHUB_OUTPUT
|
||||
echo "Package: $PACKAGE"
|
||||
echo "Version: $VERSION"
|
||||
echo "📦 Package: $PACKAGE"
|
||||
echo "📌 Version: $VERSION"
|
||||
else
|
||||
# 手动触发:从 package.json 读取并 bump 版本
|
||||
# Manual trigger: read from package.json and bump version
|
||||
PACKAGE="${{ github.event.inputs.package }}"
|
||||
echo "package=$PACKAGE" >> $GITHUB_OUTPUT
|
||||
@@ -108,6 +112,7 @@ jobs:
|
||||
PACKAGE="${{ steps.parse.outputs.package }}"
|
||||
EXPECTED_VERSION="${{ steps.parse.outputs.version }}"
|
||||
|
||||
# 获取 package.json 中的版本
|
||||
# Get version from package.json
|
||||
ACTUAL_VERSION=$(node -p "require('./packages/$PACKAGE/package.json').version")
|
||||
|
||||
@@ -120,7 +125,7 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Version verified: $EXPECTED_VERSION"
|
||||
echo "✅ Version verified: $EXPECTED_VERSION"
|
||||
|
||||
- name: Bump version (manual mode)
|
||||
if: steps.parse.outputs.mode == 'manual'
|
||||
@@ -142,7 +147,7 @@ jobs:
|
||||
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"
|
||||
echo "📌 Bumped version: $CURRENT → $NEW_VERSION"
|
||||
|
||||
- name: Set final version
|
||||
id: version
|
||||
@@ -156,7 +161,7 @@ jobs:
|
||||
- name: Build core package (if needed)
|
||||
if: ${{ steps.parse.outputs.package != 'core' && steps.parse.outputs.package != 'node-editor' && steps.parse.outputs.package != 'worker-generator' }}
|
||||
run: |
|
||||
cd packages/framework/core
|
||||
cd packages/core
|
||||
pnpm run build
|
||||
|
||||
- name: Build node-editor package (if needed for blueprint)
|
||||
@@ -183,18 +188,17 @@ jobs:
|
||||
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 }}
|
||||
## 🚀 @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 }})
|
||||
📦 **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*
|
||||
*自动发布 | Auto-released by GitHub Actions*
|
||||
generate_release_notes: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -209,16 +213,16 @@ jobs:
|
||||
delete-branch: true
|
||||
title: "chore(${{ steps.parse.outputs.package }}): Release v${{ steps.version.outputs.value }}"
|
||||
body: |
|
||||
## Release v${{ steps.version.outputs.value }}
|
||||
## 🚀 Release v${{ steps.version.outputs.value }}
|
||||
|
||||
This PR updates `@esengine/${{ steps.parse.outputs.package }}` package version.
|
||||
此 PR 更新 `@esengine/${{ steps.parse.outputs.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/${{ steps.parse.outputs.package }}@${{ steps.version.outputs.value }}](https://www.npmjs.com/package/@esengine/${{ steps.parse.outputs.package }}/v/${{ steps.version.outputs.value }})
|
||||
- ✅ 更新 `packages/${{ steps.parse.outputs.package }}/package.json` → `${{ steps.version.outputs.value }}`
|
||||
|
||||
---
|
||||
*This PR was automatically created by the release workflow*
|
||||
*此 PR 由发布工作流自动创建*
|
||||
labels: |
|
||||
release
|
||||
${{ steps.parse.outputs.package }}
|
||||
|
||||
46
.github/workflows/size-limit.yml
vendored
Normal file
46
.github/workflows/size-limit.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
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@v4
|
||||
|
||||
- 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.3.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-message: |
|
||||
👋 你好!感谢你提交第一个 issue!
|
||||
|
||||
我们会尽快查看并回复。同时,建议你:
|
||||
- 📚 查看[文档](https://esengine.github.io/ecs-framework/)
|
||||
- 🤖 使用 [AI 文档助手](https://deepwiki.com/esengine/esengine)
|
||||
- 💬 加入 [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/esengine)
|
||||
|
||||
pr-message: |
|
||||
👋 你好!感谢你提交第一个 Pull Request!
|
||||
|
||||
在我们 Review 之前,请确保:
|
||||
- ✅ 代码遵循项目规范
|
||||
- ✅ 通过所有测试
|
||||
- ✅ 更新了相关文档
|
||||
- ✅ Commit 遵循 [Conventional Commits](https://www.conventionalcommits.org/) 规范
|
||||
|
||||
查看完整的[贡献指南](https://github.com/esengine/esengine/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/esengine/blob/master/CONTRIBUTING.md).
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -90,7 +90,3 @@ docs/.vitepress/dist/
|
||||
# Tauri 捆绑输出
|
||||
**/src-tauri/target/release/bundle/
|
||||
**/src-tauri/target/debug/bundle/
|
||||
|
||||
# Rust 构建产物
|
||||
**/engine-shared/target/
|
||||
external/
|
||||
|
||||
289
README.md
289
README.md
@@ -5,7 +5,7 @@
|
||||
</h1>
|
||||
|
||||
<p align="center">
|
||||
<strong>Modular Game Framework for TypeScript</strong>
|
||||
<strong>Cross-platform 2D Game Engine</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -23,58 +23,62 @@
|
||||
<p align="center">
|
||||
<a href="https://esengine.cn/">Documentation</a> ·
|
||||
<a href="https://esengine.cn/api/README">API Reference</a> ·
|
||||
<a href="https://github.com/esengine/esengine/releases">Download Editor</a> ·
|
||||
<a href="./examples/">Examples</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## What is ESEngine?
|
||||
## Overview
|
||||
|
||||
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.
|
||||
ESEngine is a cross-platform 2D game engine built from the ground up with modern web technologies. It provides a comprehensive toolset that enables developers to focus on creating games rather than building infrastructure.
|
||||
|
||||
The core is a high-performance **ECS (Entity-Component-System)** framework, accompanied by optional modules for AI, networking, physics, and more.
|
||||
Export your games to multiple platforms including web browsers, WeChat Mini Games, and other mini-game platforms from a single codebase.
|
||||
|
||||
## Key Features
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| **ECS Architecture** | Data-driven Entity-Component-System pattern for flexible and cache-friendly game logic |
|
||||
| **High-Performance Rendering** | Rust/WebAssembly 2D renderer with automatic sprite batching and WebGL 2.0 backend |
|
||||
| **Visual Editor** | Cross-platform desktop editor built with Tauri for scene management and asset workflows |
|
||||
| **Modular Design** | Import only what you need - each feature is a standalone package |
|
||||
| **Multi-Platform Export** | Deploy to Web, WeChat Mini Games, and more from one codebase |
|
||||
| **Physics Integration** | 2D physics powered by Rapier with editor visualization |
|
||||
| **Visual Scripting** | Behavior trees and blueprint system for designers |
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Runtime**: TypeScript, Rust, WebAssembly
|
||||
- **Renderer**: WebGL 2.0, WGPU (planned)
|
||||
- **Editor**: Tauri, React, Zustand
|
||||
- **Physics**: Rapier2D
|
||||
- **Build**: pnpm, Turborepo, Rollup
|
||||
|
||||
## License
|
||||
|
||||
ESEngine is **free and open source** under the [MIT License](LICENSE). No royalties, no strings attached.
|
||||
|
||||
## Installation
|
||||
|
||||
### npm
|
||||
|
||||
```bash
|
||||
npm install @esengine/ecs-framework
|
||||
```
|
||||
|
||||
## Features
|
||||
### Editor
|
||||
|
||||
| Module | Description | Engine Required |
|
||||
|--------|-------------|:---------------:|
|
||||
| **ECS Core** | Entity-Component-System framework with reactive queries | No |
|
||||
| **Behavior Tree** | AI behavior trees with visual editor support | No |
|
||||
| **Blueprint** | Visual scripting system | No |
|
||||
| **FSM** | Finite state machine | No |
|
||||
| **Timer** | Timer and cooldown systems | No |
|
||||
| **Spatial** | Spatial indexing and queries (QuadTree, Grid) | No |
|
||||
| **Pathfinding** | A* and navigation mesh pathfinding | No |
|
||||
| **Network** | Client/server networking with TSRPC | No |
|
||||
|
||||
> All framework modules can be used standalone with any rendering engine.
|
||||
Download pre-built binaries from the [Releases](https://github.com/esengine/esengine/releases) page (Windows, 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;
|
||||
@@ -87,7 +91,6 @@ class Velocity extends Component {
|
||||
dy = 0;
|
||||
}
|
||||
|
||||
// Define system (logic)
|
||||
@ECSSystem('Movement')
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
@@ -115,7 +118,7 @@ player.addComponent(new Velocity());
|
||||
|
||||
Core.setScene(scene);
|
||||
|
||||
// Integrate with your game loop
|
||||
// Game loop
|
||||
function gameLoop(currentTime: number) {
|
||||
Core.update(currentTime / 1000);
|
||||
requestAnimationFrame(gameLoop);
|
||||
@@ -123,132 +126,96 @@ function gameLoop(currentTime: number) {
|
||||
requestAnimationFrame(gameLoop);
|
||||
```
|
||||
|
||||
## Using with Other Engines
|
||||
|
||||
ESEngine's framework modules are designed to work alongside your preferred rendering engine:
|
||||
|
||||
### With Cocos Creator
|
||||
|
||||
```typescript
|
||||
import { Component as CCComponent, _decorator } from 'cc';
|
||||
import { Core, Scene, Matcher, EntitySystem } from '@esengine/ecs-framework';
|
||||
import { BehaviorTreeExecutionSystem } from '@esengine/behavior-tree';
|
||||
|
||||
const { ccclass } = _decorator;
|
||||
|
||||
@ccclass('GameManager')
|
||||
export class GameManager extends CCComponent {
|
||||
private ecsScene!: Scene;
|
||||
|
||||
start() {
|
||||
Core.create();
|
||||
this.ecsScene = new Scene();
|
||||
|
||||
// Add ECS systems
|
||||
this.ecsScene.addSystem(new BehaviorTreeExecutionSystem());
|
||||
this.ecsScene.addSystem(new MyGameSystem());
|
||||
|
||||
Core.setScene(this.ecsScene);
|
||||
}
|
||||
|
||||
update(dt: number) {
|
||||
Core.update(dt);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### With 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();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Packages
|
||||
|
||||
### Framework (Engine-Agnostic)
|
||||
ESEngine is organized as a monorepo with modular packages.
|
||||
|
||||
These packages have **zero rendering dependencies** and work with any engine:
|
||||
### Core
|
||||
|
||||
```bash
|
||||
npm install @esengine/ecs-framework # Core ECS
|
||||
npm install @esengine/behavior-tree # AI behavior trees
|
||||
npm install @esengine/blueprint # Visual scripting
|
||||
npm install @esengine/fsm # State machines
|
||||
npm install @esengine/timer # Timers & cooldowns
|
||||
npm install @esengine/spatial # Spatial indexing
|
||||
npm install @esengine/pathfinding # Pathfinding
|
||||
npm install @esengine/network # Networking
|
||||
```
|
||||
| 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 |
|
||||
|
||||
### ESEngine Runtime (Optional)
|
||||
### Runtime
|
||||
|
||||
If you want a complete engine solution with rendering:
|
||||
| Package | Description |
|
||||
|---------|-------------|
|
||||
| `@esengine/sprite` | 2D sprite rendering and animation |
|
||||
| `@esengine/tilemap` | Tile-based map rendering |
|
||||
| `@esengine/physics-rapier2d` | 2D physics simulation (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 |
|
||||
|
||||
| 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 Extensions
|
||||
|
||||
### Editor (Optional)
|
||||
| Package | Description |
|
||||
|---------|-------------|
|
||||
| `@esengine/sprite-editor` | Sprite inspector and tools |
|
||||
| `@esengine/tilemap-editor` | Visual tilemap editor |
|
||||
| `@esengine/physics-rapier2d-editor` | Physics collider visualization |
|
||||
| `@esengine/behavior-tree-editor` | Visual behavior tree editor |
|
||||
| `@esengine/blueprint-editor` | Visual scripting editor |
|
||||
| `@esengine/material-editor` | Material editor |
|
||||
|
||||
A visual editor built with Tauri for scene management:
|
||||
### Platform
|
||||
|
||||
- Download from [Releases](https://github.com/esengine/esengine/releases)
|
||||
- Supports behavior tree editing, tilemap painting, visual scripting
|
||||
| Package | Description |
|
||||
|---------|-------------|
|
||||
| `@esengine/platform-common` | Platform abstraction interfaces |
|
||||
| `@esengine/platform-web` | Web browser runtime |
|
||||
| `@esengine/platform-wechat` | WeChat Mini Game runtime |
|
||||
|
||||
## Project Structure
|
||||
## Editor
|
||||
|
||||
```
|
||||
esengine/
|
||||
├── packages/
|
||||
│ ├── framework/ # Engine-agnostic modules (NPM publishable)
|
||||
│ │ ├── core/ # ECS Framework
|
||||
│ │ ├── math/ # Math utilities
|
||||
│ │ ├── behavior-tree/ # AI behavior trees
|
||||
│ │ ├── blueprint/ # Visual scripting
|
||||
│ │ ├── fsm/ # Finite state machine
|
||||
│ │ ├── timer/ # Timer system
|
||||
│ │ ├── spatial/ # Spatial queries
|
||||
│ │ ├── pathfinding/ # Pathfinding
|
||||
│ │ ├── procgen/ # Procedural generation
|
||||
│ │ └── network/ # Networking
|
||||
│ │
|
||||
│ ├── engine/ # ESEngine runtime
|
||||
│ ├── rendering/ # Rendering modules
|
||||
│ ├── physics/ # Physics modules
|
||||
│ ├── editor/ # Visual editor
|
||||
│ └── rust/ # WASM renderer
|
||||
│
|
||||
├── docs/ # Documentation
|
||||
└── examples/ # Examples
|
||||
```
|
||||
The ESEngine Editor is a cross-platform desktop application built with Tauri and React.
|
||||
|
||||
### Features
|
||||
|
||||
- Scene hierarchy and entity management
|
||||
- Component inspector with custom property editors
|
||||
- Asset browser with drag-and-drop
|
||||
- Tilemap editor with paint and fill tools
|
||||
- Behavior tree visual editor
|
||||
- Blueprint visual scripting
|
||||
- Material and shader editing
|
||||
- Built-in performance profiler
|
||||
- Localization (English, Chinese)
|
||||
|
||||
### Screenshot
|
||||
|
||||

|
||||
|
||||
## Platform Support
|
||||
|
||||
| Platform | Runtime | Editor |
|
||||
|----------|:-------:|:------:|
|
||||
| Web Browser | ✓ | - |
|
||||
| Windows | - | ✓ |
|
||||
| macOS | - | ✓ |
|
||||
| WeChat Mini Game | In Progress | - |
|
||||
| Playable Ads | Planned | - |
|
||||
| Android | Planned | - |
|
||||
| iOS | Planned | - |
|
||||
|
||||
## Building from Source
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 18+
|
||||
- pnpm 10+
|
||||
- Rust toolchain (for WASM renderer)
|
||||
- wasm-pack
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
git clone https://github.com/esengine/esengine.git
|
||||
cd esengine
|
||||
@@ -256,28 +223,42 @@ cd esengine
|
||||
pnpm install
|
||||
pnpm build
|
||||
|
||||
# Type check framework packages
|
||||
pnpm type-check:framework
|
||||
# Optional: Build WASM renderer
|
||||
pnpm build:wasm
|
||||
```
|
||||
|
||||
# Run tests
|
||||
pnpm test
|
||||
### Run Editor
|
||||
|
||||
```bash
|
||||
cd packages/editor-app
|
||||
pnpm tauri:dev
|
||||
```
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
esengine/
|
||||
├── 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)
|
||||
- [Getting Started](https://esengine.cn/guide/getting-started.html)
|
||||
- [Architecture Guide](https://esengine.cn/guide/)
|
||||
- [API Reference](https://esengine.cn/api/README)
|
||||
|
||||
## 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
|
||||
|
||||
## 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`)
|
||||
@@ -287,10 +268,10 @@ Contributions are welcome! Please read our contributing guidelines before submit
|
||||
|
||||
## License
|
||||
|
||||
ESEngine is licensed under the [MIT License](LICENSE). Free for personal and commercial use.
|
||||
ESEngine is licensed under the [MIT License](LICENSE).
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
Made with care by the ESEngine community
|
||||
Made with ❤️ by the ESEngine team
|
||||
</p>
|
||||
|
||||
297
README_CN.md
297
README_CN.md
@@ -5,7 +5,7 @@
|
||||
</h1>
|
||||
|
||||
<p align="center">
|
||||
<strong>TypeScript 模块化游戏框架</strong>
|
||||
<strong>跨平台 2D 游戏引擎</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -23,58 +23,62 @@
|
||||
<p align="center">
|
||||
<a href="https://esengine.cn/">文档</a> ·
|
||||
<a href="https://esengine.cn/api/README">API 参考</a> ·
|
||||
<a href="https://github.com/esengine/esengine/releases">下载编辑器</a> ·
|
||||
<a href="./examples/">示例</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## ESEngine 是什么?
|
||||
## 概述
|
||||
|
||||
ESEngine 是一套**引擎无关的游戏开发模块**,可与 Cocos Creator、Laya、Phaser、PixiJS 等任何 JavaScript 游戏引擎配合使用。
|
||||
ESEngine 是一款基于现代 Web 技术从零构建的跨平台 2D 游戏引擎。它提供完整的工具集,让开发者专注于游戏创作而非基础设施搭建。
|
||||
|
||||
核心是一个高性能的 **ECS(实体-组件-系统)** 框架,配套 AI、网络、物理等可选模块。
|
||||
一套代码即可导出到 Web 浏览器、微信小游戏等多个平台。
|
||||
|
||||
## 核心特性
|
||||
|
||||
| 特性 | 描述 |
|
||||
|-----|------|
|
||||
| **ECS 架构** | 数据驱动的实体-组件-系统模式,提供灵活且缓存友好的游戏逻辑 |
|
||||
| **高性能渲染** | Rust/WebAssembly 2D 渲染器,支持自动精灵批处理和 WebGL 2.0 |
|
||||
| **可视化编辑器** | 基于 Tauri 的跨平台桌面编辑器,支持场景管理和资源工作流 |
|
||||
| **模块化设计** | 按需引入,每个功能都是独立的包 |
|
||||
| **多平台导出** | 一套代码部署到 Web、微信小游戏等平台 |
|
||||
| **物理集成** | 基于 Rapier 的 2D 物理,支持编辑器可视化 |
|
||||
| **可视化脚本** | 行为树和蓝图系统,适合策划使用 |
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **运行时**: TypeScript, Rust, WebAssembly
|
||||
- **渲染器**: WebGL 2.0, WGPU (计划中)
|
||||
- **编辑器**: Tauri, React, Zustand
|
||||
- **物理**: Rapier2D
|
||||
- **构建**: pnpm, Turborepo, Rollup
|
||||
|
||||
## 许可证
|
||||
|
||||
ESEngine **完全免费开源**,采用 [MIT 协议](LICENSE)。无版税,无附加条件。
|
||||
|
||||
## 安装
|
||||
|
||||
### npm
|
||||
|
||||
```bash
|
||||
npm install @esengine/ecs-framework
|
||||
```
|
||||
|
||||
## 功能模块
|
||||
### 编辑器
|
||||
|
||||
| 模块 | 描述 | 需要渲染引擎 |
|
||||
|------|------|:----------:|
|
||||
| **ECS 核心** | 实体-组件-系统框架,支持响应式查询 | 否 |
|
||||
| **行为树** | AI 行为树,支持可视化编辑 | 否 |
|
||||
| **蓝图** | 可视化脚本系统 | 否 |
|
||||
| **状态机** | 有限状态机 | 否 |
|
||||
| **定时器** | 定时器和冷却系统 | 否 |
|
||||
| **空间索引** | 空间查询(四叉树、网格) | 否 |
|
||||
| **寻路** | A* 和导航网格寻路 | 否 |
|
||||
| **网络** | 客户端/服务端网络通信 (TSRPC) | 否 |
|
||||
|
||||
> 所有框架模块都可以独立使用,无需依赖特定渲染引擎。
|
||||
从 [Releases](https://github.com/esengine/esengine/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;
|
||||
@@ -87,7 +91,6 @@ class Velocity extends Component {
|
||||
dy = 0;
|
||||
}
|
||||
|
||||
// 定义系统(逻辑)
|
||||
@ECSSystem('Movement')
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
@@ -115,7 +118,7 @@ player.addComponent(new Velocity());
|
||||
|
||||
Core.setScene(scene);
|
||||
|
||||
// 集成到你的游戏循环
|
||||
// 游戏循环
|
||||
function gameLoop(currentTime: number) {
|
||||
Core.update(currentTime / 1000);
|
||||
requestAnimationFrame(gameLoop);
|
||||
@@ -123,132 +126,96 @@ function gameLoop(currentTime: number) {
|
||||
requestAnimationFrame(gameLoop);
|
||||
```
|
||||
|
||||
## 与其他引擎配合使用
|
||||
## 模块
|
||||
|
||||
ESEngine 的框架模块设计为可与你喜欢的渲染引擎配合使用:
|
||||
ESEngine 采用 Monorepo 组织,包含多个模块化包。
|
||||
|
||||
### 与 Cocos Creator 配合
|
||||
### 核心
|
||||
|
||||
```typescript
|
||||
import { Component as CCComponent, _decorator } from 'cc';
|
||||
import { Core, Scene, Matcher, EntitySystem } from '@esengine/ecs-framework';
|
||||
import { BehaviorTreeExecutionSystem } from '@esengine/behavior-tree';
|
||||
|
||||
const { ccclass } = _decorator;
|
||||
|
||||
@ccclass('GameManager')
|
||||
export class GameManager extends CCComponent {
|
||||
private ecsScene!: Scene;
|
||||
|
||||
start() {
|
||||
Core.create();
|
||||
this.ecsScene = new Scene();
|
||||
|
||||
// 添加 ECS 系统
|
||||
this.ecsScene.addSystem(new BehaviorTreeExecutionSystem());
|
||||
this.ecsScene.addSystem(new MyGameSystem());
|
||||
|
||||
Core.setScene(this.ecsScene);
|
||||
}
|
||||
|
||||
update(dt: number) {
|
||||
Core.update(dt);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 与 Laya 3.x 配合
|
||||
|
||||
```typescript
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
import { FSMSystem } from '@esengine/fsm';
|
||||
|
||||
const { regClass } = Laya;
|
||||
|
||||
@regClass()
|
||||
export class ECSManager extends Laya.Script {
|
||||
private ecsScene = new Scene();
|
||||
|
||||
onAwake(): void {
|
||||
Core.create();
|
||||
this.ecsScene.addSystem(new FSMSystem());
|
||||
Core.setScene(this.ecsScene);
|
||||
}
|
||||
|
||||
onUpdate(): void {
|
||||
Core.update(Laya.timer.delta / 1000);
|
||||
}
|
||||
|
||||
onDestroy(): void {
|
||||
Core.destroy();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 包列表
|
||||
|
||||
### 框架包(引擎无关)
|
||||
|
||||
这些包**零渲染依赖**,可与任何引擎配合使用:
|
||||
|
||||
```bash
|
||||
npm install @esengine/ecs-framework # ECS 核心
|
||||
npm install @esengine/behavior-tree # AI 行为树
|
||||
npm install @esengine/blueprint # 可视化脚本
|
||||
npm install @esengine/fsm # 状态机
|
||||
npm install @esengine/timer # 定时器和冷却
|
||||
npm install @esengine/spatial # 空间索引
|
||||
npm install @esengine/pathfinding # 寻路
|
||||
npm install @esengine/network # 网络
|
||||
```
|
||||
|
||||
### ESEngine 运行时(可选)
|
||||
|
||||
如果你需要完整的引擎解决方案:
|
||||
|
||||
| 分类 | 包名 |
|
||||
| 包名 | 描述 |
|
||||
|------|------|
|
||||
| **核心** | `engine-core`, `asset-system`, `material-system` |
|
||||
| **渲染** | `sprite`, `tilemap`, `particle`, `camera`, `mesh-3d` |
|
||||
| **物理** | `physics-rapier2d` |
|
||||
| **平台** | `platform-web`, `platform-wechat` |
|
||||
| `@esengine/ecs-framework` | ECS 框架核心,包含实体管理、组件系统和查询 |
|
||||
| `@esengine/math` | 向量、矩阵和数学工具 |
|
||||
| `@esengine/engine` | Rust/WASM 2D 渲染器 |
|
||||
| `@esengine/engine-core` | 引擎模块系统和生命周期管理 |
|
||||
|
||||
### 编辑器(可选)
|
||||
### 运行时
|
||||
|
||||
基于 Tauri 构建的可视化编辑器:
|
||||
| 包名 | 描述 |
|
||||
|------|------|
|
||||
| `@esengine/sprite` | 2D 精灵渲染和动画 |
|
||||
| `@esengine/tilemap` | Tilemap 渲染 |
|
||||
| `@esengine/physics-rapier2d` | 2D 物理模拟 (Rapier) |
|
||||
| `@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) 下载
|
||||
- 支持行为树编辑、Tilemap 绘制、可视化脚本
|
||||
### 编辑器扩展
|
||||
|
||||
## 项目结构
|
||||
| 包名 | 描述 |
|
||||
|------|------|
|
||||
| `@esengine/sprite-editor` | 精灵检视器和工具 |
|
||||
| `@esengine/tilemap-editor` | 可视化 Tilemap 编辑器 |
|
||||
| `@esengine/physics-rapier2d-editor` | 物理碰撞体可视化 |
|
||||
| `@esengine/behavior-tree-editor` | 可视化行为树编辑器 |
|
||||
| `@esengine/blueprint-editor` | 可视化脚本编辑器 |
|
||||
| `@esengine/material-editor` | 材质编辑器 |
|
||||
|
||||
```
|
||||
esengine/
|
||||
├── packages/
|
||||
│ ├── framework/ # 引擎无关模块(可发布到 NPM)
|
||||
│ │ ├── core/ # ECS 框架
|
||||
│ │ ├── math/ # 数学工具
|
||||
│ │ ├── behavior-tree/ # AI 行为树
|
||||
│ │ ├── blueprint/ # 可视化脚本
|
||||
│ │ ├── fsm/ # 有限状态机
|
||||
│ │ ├── timer/ # 定时器系统
|
||||
│ │ ├── spatial/ # 空间查询
|
||||
│ │ ├── pathfinding/ # 寻路
|
||||
│ │ ├── procgen/ # 程序化生成
|
||||
│ │ └── network/ # 网络
|
||||
│ │
|
||||
│ ├── engine/ # ESEngine 运行时
|
||||
│ ├── rendering/ # 渲染模块
|
||||
│ ├── physics/ # 物理模块
|
||||
│ ├── editor/ # 可视化编辑器
|
||||
│ └── rust/ # WASM 渲染器
|
||||
│
|
||||
├── docs/ # 文档
|
||||
└── examples/ # 示例
|
||||
```
|
||||
### 平台
|
||||
|
||||
| 包名 | 描述 |
|
||||
|------|------|
|
||||
| `@esengine/platform-common` | 平台抽象接口 |
|
||||
| `@esengine/platform-web` | Web 浏览器运行时 |
|
||||
| `@esengine/platform-wechat` | 微信小游戏运行时 |
|
||||
|
||||
## 编辑器
|
||||
|
||||
ESEngine 编辑器是基于 Tauri 和 React 构建的跨平台桌面应用。
|
||||
|
||||
### 功能
|
||||
|
||||
- 场景层级和实体管理
|
||||
- 组件检视器,支持自定义属性编辑器
|
||||
- 资源浏览器,支持拖放
|
||||
- Tilemap 编辑器,支持绘制和填充工具
|
||||
- 行为树可视化编辑器
|
||||
- 蓝图可视化脚本
|
||||
- 材质和着色器编辑
|
||||
- 内置性能分析器
|
||||
- 多语言支持(英文、中文)
|
||||
|
||||
### 截图
|
||||
|
||||

|
||||
|
||||
## 平台支持
|
||||
|
||||
| 平台 | 运行时 | 编辑器 |
|
||||
|------|:------:|:------:|
|
||||
| Web 浏览器 | ✓ | - |
|
||||
| Windows | - | ✓ |
|
||||
| macOS | - | ✓ |
|
||||
| 微信小游戏 | 开发中 | - |
|
||||
| Playable 可玩广告 | 计划中 | - |
|
||||
| Android | 计划中 | - |
|
||||
| iOS | 计划中 | - |
|
||||
|
||||
## 从源码构建
|
||||
|
||||
### 前置要求
|
||||
|
||||
- Node.js 18+
|
||||
- pnpm 10+
|
||||
- Rust 工具链(用于 WASM 渲染器)
|
||||
- wasm-pack
|
||||
|
||||
### 安装
|
||||
|
||||
```bash
|
||||
git clone https://github.com/esengine/esengine.git
|
||||
cd esengine
|
||||
@@ -256,29 +223,43 @@ cd esengine
|
||||
pnpm install
|
||||
pnpm build
|
||||
|
||||
# 框架包类型检查
|
||||
pnpm type-check:framework
|
||||
# 可选:构建 WASM 渲染器
|
||||
pnpm build:wasm
|
||||
```
|
||||
|
||||
# 运行测试
|
||||
pnpm test
|
||||
### 运行编辑器
|
||||
|
||||
```bash
|
||||
cd packages/editor-app
|
||||
pnpm tauri:dev
|
||||
```
|
||||
|
||||
### 项目结构
|
||||
|
||||
```
|
||||
esengine/
|
||||
├── packages/ # 引擎包(运行时、编辑器、平台)
|
||||
├── docs/ # 文档源码
|
||||
├── examples/ # 示例项目
|
||||
├── scripts/ # 构建工具
|
||||
└── thirdparty/ # 第三方依赖
|
||||
```
|
||||
|
||||
## 文档
|
||||
|
||||
- [ECS 框架指南](./packages/framework/core/README.md)
|
||||
- [行为树指南](./packages/framework/behavior-tree/README.md)
|
||||
- [快速入门](https://esengine.cn/guide/getting-started.html)
|
||||
- [架构指南](https://esengine.cn/guide/)
|
||||
- [API 参考](https://esengine.cn/api/README)
|
||||
|
||||
## 社区
|
||||
|
||||
- [QQ 交流群](https://jq.qq.com/?_wv=1027&k=29w1Nud6) - 中文社区
|
||||
- [Discord](https://discord.gg/gCAgzXFW) - 国际社区
|
||||
- [GitHub Issues](https://github.com/esengine/esengine/issues) - Bug 反馈和功能建议
|
||||
- [GitHub Discussions](https://github.com/esengine/esengine/discussions) - 问题和想法
|
||||
- [QQ 交流群](https://jq.qq.com/?_wv=1027&k=29w1Nud6) - 中文社区
|
||||
|
||||
## 贡献
|
||||
|
||||
欢迎贡献代码!提交 PR 前请阅读贡献指南。
|
||||
欢迎贡献代码。提交 PR 前请阅读贡献指南。
|
||||
|
||||
1. Fork 仓库
|
||||
2. 创建功能分支 (`git checkout -b feature/amazing-feature`)
|
||||
@@ -288,10 +269,10 @@ pnpm test
|
||||
|
||||
## 许可证
|
||||
|
||||
ESEngine 基于 [MIT 协议](LICENSE) 开源,个人和商业使用均免费。
|
||||
ESEngine 基于 [MIT 协议](LICENSE) 开源。
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
由 ESEngine 社区用心打造
|
||||
由 ESEngine 团队用 ❤️ 打造
|
||||
</p>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { fileURLToPath } from 'url'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const corePackageJson = JSON.parse(
|
||||
readFileSync(join(__dirname, '../../packages/framework/core/package.json'), 'utf-8')
|
||||
readFileSync(join(__dirname, '../../packages/core/package.json'), 'utf-8')
|
||||
)
|
||||
|
||||
// Import i18n messages
|
||||
|
||||
@@ -1,663 +0,0 @@
|
||||
# ESEngine 材质系统统一架构重构方案
|
||||
|
||||
## 问题概述
|
||||
|
||||
当前 UI 和 Scene (Sprite) 两套渲染系统存在大量代码重复:
|
||||
|
||||
| 重复项 | Sprite | UI | 重复度 |
|
||||
|--------|--------|----|----|
|
||||
| 材质属性覆盖接口 | `MaterialPropertyOverride` | `UIMaterialPropertyOverride` | 100% |
|
||||
| 材质方法 (12个) | `SpriteComponent` | `UIRenderComponent` | 100% |
|
||||
| ShinyEffect 组件 | `ShinyEffectComponent` | `UIShinyEffectComponent` | 99% |
|
||||
| ShinyEffect 系统 | `ShinyEffectSystem` | `UIShinyEffectSystem` | 98% |
|
||||
|
||||
**根本原因**:缺乏统一的材质覆盖接口抽象层。
|
||||
|
||||
---
|
||||
|
||||
## 一、统一材质覆盖接口
|
||||
|
||||
### 1.1 定义通用接口
|
||||
|
||||
在 `@esengine/material-system` 包中定义统一接口:
|
||||
|
||||
```typescript
|
||||
// packages/material-system/src/interfaces/IMaterialOverridable.ts
|
||||
|
||||
/**
|
||||
* Material property override definition.
|
||||
* 材质属性覆盖定义。
|
||||
*/
|
||||
export interface MaterialPropertyOverride {
|
||||
type: 'float' | 'vec2' | 'vec3' | 'vec4' | 'color' | 'int';
|
||||
value: number | number[];
|
||||
}
|
||||
|
||||
export type MaterialOverrides = Record<string, MaterialPropertyOverride>;
|
||||
|
||||
/**
|
||||
* Interface for components that support material property overrides.
|
||||
* 支持材质属性覆盖的组件接口。
|
||||
*/
|
||||
export interface IMaterialOverridable {
|
||||
/** Material GUID for asset reference | 材质资产引用的 GUID */
|
||||
materialGuid: string;
|
||||
|
||||
/** Current material overrides | 当前材质覆盖 */
|
||||
readonly materialOverrides: MaterialOverrides;
|
||||
|
||||
/** Get current material ID | 获取当前材质 ID */
|
||||
getMaterialId(): number;
|
||||
|
||||
/** Set material ID | 设置材质 ID */
|
||||
setMaterialId(id: number): void;
|
||||
|
||||
// Uniform setters
|
||||
setOverrideFloat(name: string, value: number): this;
|
||||
setOverrideVec2(name: string, x: number, y: number): this;
|
||||
setOverrideVec3(name: string, x: number, y: number, z: number): this;
|
||||
setOverrideVec4(name: string, x: number, y: number, z: number, w: number): this;
|
||||
setOverrideColor(name: string, r: number, g: number, b: number, a?: number): this;
|
||||
setOverrideInt(name: string, value: number): this;
|
||||
|
||||
// Uniform getters
|
||||
getOverride(name: string): MaterialPropertyOverride | undefined;
|
||||
removeOverride(name: string): this;
|
||||
clearOverrides(): this;
|
||||
hasOverrides(): boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 创建 Mixin 实现
|
||||
|
||||
使用 Mixin 模式避免代码重复:
|
||||
|
||||
```typescript
|
||||
// packages/material-system/src/mixins/MaterialOverridableMixin.ts
|
||||
|
||||
import type { MaterialPropertyOverride, MaterialOverrides } from '../interfaces/IMaterialOverridable';
|
||||
|
||||
/**
|
||||
* Mixin that provides material override functionality.
|
||||
* 提供材质覆盖功能的 Mixin。
|
||||
*/
|
||||
export function MaterialOverridableMixin<TBase extends new (...args: any[]) => {}>(Base: TBase) {
|
||||
return class extends Base {
|
||||
materialGuid: string = '';
|
||||
private _materialId: number = 0;
|
||||
private _materialOverrides: MaterialOverrides = {};
|
||||
|
||||
get materialOverrides(): MaterialOverrides {
|
||||
return this._materialOverrides;
|
||||
}
|
||||
|
||||
getMaterialId(): number {
|
||||
return this._materialId;
|
||||
}
|
||||
|
||||
setMaterialId(id: number): void {
|
||||
this._materialId = id;
|
||||
}
|
||||
|
||||
setOverrideFloat(name: string, value: number): this {
|
||||
this._materialOverrides[name] = { type: 'float', value };
|
||||
return this;
|
||||
}
|
||||
|
||||
setOverrideVec2(name: string, x: number, y: number): this {
|
||||
this._materialOverrides[name] = { type: 'vec2', value: [x, y] };
|
||||
return this;
|
||||
}
|
||||
|
||||
setOverrideVec3(name: string, x: number, y: number, z: number): this {
|
||||
this._materialOverrides[name] = { type: 'vec3', value: [x, y, z] };
|
||||
return this;
|
||||
}
|
||||
|
||||
setOverrideVec4(name: string, x: number, y: number, z: number, w: number): this {
|
||||
this._materialOverrides[name] = { type: 'vec4', value: [x, y, z, w] };
|
||||
return this;
|
||||
}
|
||||
|
||||
setOverrideColor(name: string, r: number, g: number, b: number, a: number = 1.0): this {
|
||||
this._materialOverrides[name] = { type: 'color', value: [r, g, b, a] };
|
||||
return this;
|
||||
}
|
||||
|
||||
setOverrideInt(name: string, value: number): this {
|
||||
this._materialOverrides[name] = { type: 'int', value: Math.floor(value) };
|
||||
return this;
|
||||
}
|
||||
|
||||
getOverride(name: string): MaterialPropertyOverride | undefined {
|
||||
return this._materialOverrides[name];
|
||||
}
|
||||
|
||||
removeOverride(name: string): this {
|
||||
delete this._materialOverrides[name];
|
||||
return this;
|
||||
}
|
||||
|
||||
clearOverrides(): this {
|
||||
this._materialOverrides = {};
|
||||
return this;
|
||||
}
|
||||
|
||||
hasOverrides(): boolean {
|
||||
return Object.keys(this._materialOverrides).length > 0;
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、Shader Property 元数据系统
|
||||
|
||||
### 2.1 定义属性元数据接口
|
||||
|
||||
```typescript
|
||||
// packages/material-system/src/interfaces/IShaderProperty.ts
|
||||
|
||||
/**
|
||||
* Shader property UI metadata.
|
||||
* 着色器属性 UI 元数据。
|
||||
*/
|
||||
export interface ShaderPropertyMeta {
|
||||
/** Property type | 属性类型 */
|
||||
type: 'float' | 'vec2' | 'vec3' | 'vec4' | 'color' | 'int' | 'texture';
|
||||
|
||||
/** Display label (supports i18n key) | 显示标签(支持 i18n 键) */
|
||||
label: string;
|
||||
|
||||
/** Property group for organization | 属性分组 */
|
||||
group?: string;
|
||||
|
||||
/** Default value | 默认值 */
|
||||
default?: number | number[] | string;
|
||||
|
||||
// Numeric constraints
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
|
||||
/** UI hints | UI 提示 */
|
||||
hint?: 'range' | 'angle' | 'hdr' | 'normal';
|
||||
|
||||
/** Tooltip description | 工具提示描述 */
|
||||
tooltip?: string;
|
||||
|
||||
/** Whether to hide in inspector | 是否在检查器中隐藏 */
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extended shader definition with property metadata.
|
||||
* 带属性元数据的扩展着色器定义。
|
||||
*/
|
||||
export interface ShaderAssetDefinition {
|
||||
/** Shader name | 着色器名称 */
|
||||
name: string;
|
||||
|
||||
/** Display name for UI | UI 显示名称 */
|
||||
displayName?: string;
|
||||
|
||||
/** Shader description | 着色器描述 */
|
||||
description?: string;
|
||||
|
||||
/** Vertex shader source (inline or path) | 顶点着色器源(内联或路径)*/
|
||||
vertexSource: string;
|
||||
|
||||
/** Fragment shader source (inline or path) | 片段着色器源(内联或路径)*/
|
||||
fragmentSource: string;
|
||||
|
||||
/** Property metadata for inspector | 检查器属性元数据 */
|
||||
properties?: Record<string, ShaderPropertyMeta>;
|
||||
|
||||
/** Render queue / order | 渲染队列/顺序 */
|
||||
renderQueue?: number;
|
||||
|
||||
/** Preset blend mode | 预设混合模式 */
|
||||
blendMode?: 'alpha' | 'additive' | 'multiply' | 'opaque';
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 .shader 资产文件格式
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "esengine://schemas/shader.json",
|
||||
"version": 1,
|
||||
"name": "Shiny",
|
||||
"displayName": "闪光效果 | Shiny Effect",
|
||||
"description": "扫光高亮动画着色器 | Sweeping highlight animation shader",
|
||||
|
||||
"vertexSource": "./shaders/sprite.vert",
|
||||
"fragmentSource": "./shaders/shiny.frag",
|
||||
|
||||
"blendMode": "alpha",
|
||||
"renderQueue": 2000,
|
||||
|
||||
"properties": {
|
||||
"u_shinyProgress": {
|
||||
"type": "float",
|
||||
"label": "进度 | Progress",
|
||||
"group": "Animation",
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"hidden": true
|
||||
},
|
||||
"u_shinyWidth": {
|
||||
"type": "float",
|
||||
"label": "宽度 | Width",
|
||||
"group": "Effect",
|
||||
"default": 0.25,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"tooltip": "闪光带宽度 | Width of the shiny band"
|
||||
},
|
||||
"u_shinyRotation": {
|
||||
"type": "float",
|
||||
"label": "角度 | Rotation",
|
||||
"group": "Effect",
|
||||
"default": 2.25,
|
||||
"min": 0,
|
||||
"max": 6.28,
|
||||
"step": 0.01,
|
||||
"hint": "angle"
|
||||
},
|
||||
"u_shinySoftness": {
|
||||
"type": "float",
|
||||
"label": "柔和度 | Softness",
|
||||
"group": "Effect",
|
||||
"default": 1.0,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01
|
||||
},
|
||||
"u_shinyBrightness": {
|
||||
"type": "float",
|
||||
"label": "亮度 | Brightness",
|
||||
"group": "Effect",
|
||||
"default": 1.0,
|
||||
"min": 0,
|
||||
"max": 2,
|
||||
"step": 0.01
|
||||
},
|
||||
"u_shinyGloss": {
|
||||
"type": "float",
|
||||
"label": "光泽度 | Gloss",
|
||||
"group": "Effect",
|
||||
"default": 1.0,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"tooltip": "0=白色高光, 1=带颜色 | 0=white shine, 1=color-tinted"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、统一效果组件/系统架构
|
||||
|
||||
### 3.1 抽取通用 ShinyEffect 基类
|
||||
|
||||
```typescript
|
||||
// packages/material-system/src/effects/BaseShinyEffect.ts
|
||||
|
||||
import { Component, Property, Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* Base shiny effect configuration (shared between UI and Sprite).
|
||||
* 基础闪光效果配置(UI 和 Sprite 共享)。
|
||||
*/
|
||||
export abstract class BaseShinyEffect extends Component {
|
||||
// ============= Effect Parameters =============
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Width', min: 0, max: 1, step: 0.01 })
|
||||
public width: number = 0.25;
|
||||
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Rotation', min: 0, max: 360, step: 1 })
|
||||
public rotation: number = 129;
|
||||
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Softness', min: 0, max: 1, step: 0.01 })
|
||||
public softness: number = 1.0;
|
||||
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Brightness', min: 0, max: 2, step: 0.01 })
|
||||
public brightness: number = 1.0;
|
||||
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Gloss', min: 0, max: 2, step: 0.01 })
|
||||
public gloss: number = 1.0;
|
||||
|
||||
// ============= Animation Settings =============
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Play' })
|
||||
public play: boolean = true;
|
||||
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Loop' })
|
||||
public loop: boolean = true;
|
||||
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Duration', min: 0.1, step: 0.1 })
|
||||
public duration: number = 2.0;
|
||||
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Loop Delay', min: 0, step: 0.1 })
|
||||
public loopDelay: number = 2.0;
|
||||
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Initial Delay', min: 0, step: 0.1 })
|
||||
public initialDelay: number = 0;
|
||||
|
||||
// ============= Runtime State =============
|
||||
public progress: number = 0;
|
||||
public elapsedTime: number = 0;
|
||||
public inDelay: boolean = false;
|
||||
public delayRemaining: number = 0;
|
||||
public initialDelayProcessed: boolean = false;
|
||||
|
||||
reset(): void {
|
||||
this.progress = 0;
|
||||
this.elapsedTime = 0;
|
||||
this.inDelay = false;
|
||||
this.delayRemaining = 0;
|
||||
this.initialDelayProcessed = false;
|
||||
}
|
||||
|
||||
start(): void {
|
||||
this.reset();
|
||||
this.play = true;
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
this.play = false;
|
||||
}
|
||||
|
||||
getRotationRadians(): number {
|
||||
return this.rotation * Math.PI / 180;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 通用动画更新逻辑
|
||||
|
||||
```typescript
|
||||
// packages/material-system/src/effects/ShinyEffectAnimator.ts
|
||||
|
||||
import type { BaseShinyEffect } from './BaseShinyEffect';
|
||||
import type { IMaterialOverridable } from '../interfaces/IMaterialOverridable';
|
||||
import { BuiltInShaders } from '../types';
|
||||
|
||||
/**
|
||||
* Shared animator logic for shiny effect.
|
||||
* 闪光效果共享的动画逻辑。
|
||||
*/
|
||||
export class ShinyEffectAnimator {
|
||||
/**
|
||||
* Update animation state.
|
||||
* 更新动画状态。
|
||||
*/
|
||||
static updateAnimation(shiny: BaseShinyEffect, deltaTime: number): void {
|
||||
if (!shiny.initialDelayProcessed && shiny.initialDelay > 0) {
|
||||
shiny.delayRemaining = shiny.initialDelay;
|
||||
shiny.inDelay = true;
|
||||
shiny.initialDelayProcessed = true;
|
||||
}
|
||||
|
||||
if (shiny.inDelay) {
|
||||
shiny.delayRemaining -= deltaTime;
|
||||
if (shiny.delayRemaining <= 0) {
|
||||
shiny.inDelay = false;
|
||||
shiny.elapsedTime = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
shiny.elapsedTime += deltaTime;
|
||||
shiny.progress = Math.min(shiny.elapsedTime / shiny.duration, 1.0);
|
||||
|
||||
if (shiny.progress >= 1.0) {
|
||||
if (shiny.loop) {
|
||||
shiny.inDelay = true;
|
||||
shiny.delayRemaining = shiny.loopDelay;
|
||||
shiny.progress = 0;
|
||||
shiny.elapsedTime = 0;
|
||||
} else {
|
||||
shiny.play = false;
|
||||
shiny.progress = 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply material overrides.
|
||||
* 应用材质覆盖。
|
||||
*/
|
||||
static applyMaterialOverrides(shiny: BaseShinyEffect, target: IMaterialOverridable): void {
|
||||
if (target.getMaterialId() === 0) {
|
||||
target.setMaterialId(BuiltInShaders.Shiny);
|
||||
}
|
||||
|
||||
target.setOverrideFloat('u_shinyProgress', shiny.progress);
|
||||
target.setOverrideFloat('u_shinyWidth', shiny.width);
|
||||
target.setOverrideFloat('u_shinyRotation', shiny.getRotationRadians());
|
||||
target.setOverrideFloat('u_shinySoftness', shiny.softness);
|
||||
target.setOverrideFloat('u_shinyBrightness', shiny.brightness);
|
||||
target.setOverrideFloat('u_shinyGloss', shiny.gloss);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、Material Inspector 设计
|
||||
|
||||
### 4.1 组件架构
|
||||
|
||||
```
|
||||
MaterialPropertiesEditor (容器组件)
|
||||
├── ShaderSelector (着色器选择器)
|
||||
├── PropertyGroup (属性分组)
|
||||
│ ├── FloatProperty (浮点属性)
|
||||
│ ├── VectorProperty (向量属性)
|
||||
│ ├── ColorProperty (颜色属性)
|
||||
│ └── TextureProperty (纹理属性)
|
||||
└── OverrideIndicator (覆盖指示器)
|
||||
```
|
||||
|
||||
### 4.2 核心组件
|
||||
|
||||
```typescript
|
||||
// packages/editor-app/src/components/inspectors/material/MaterialPropertiesEditor.tsx
|
||||
|
||||
interface MaterialPropertiesEditorProps {
|
||||
/** Target component implementing IMaterialOverridable */
|
||||
target: IMaterialOverridable;
|
||||
/** Current shader definition with property metadata */
|
||||
shaderDef?: ShaderAssetDefinition;
|
||||
/** Callback when property changes */
|
||||
onChange?: (name: string, value: MaterialPropertyOverride) => void;
|
||||
}
|
||||
|
||||
export const MaterialPropertiesEditor: React.FC<MaterialPropertiesEditorProps> = ({
|
||||
target,
|
||||
shaderDef,
|
||||
onChange
|
||||
}) => {
|
||||
// Group properties by their group field
|
||||
const groupedProps = useMemo(() => {
|
||||
if (!shaderDef?.properties) return {};
|
||||
|
||||
const groups: Record<string, Array<[string, ShaderPropertyMeta]>> = {};
|
||||
for (const [name, meta] of Object.entries(shaderDef.properties)) {
|
||||
if (meta.hidden) continue;
|
||||
const group = meta.group || 'Default';
|
||||
if (!groups[group]) groups[group] = [];
|
||||
groups[group].push([name, meta]);
|
||||
}
|
||||
return groups;
|
||||
}, [shaderDef]);
|
||||
|
||||
return (
|
||||
<div className="material-properties-editor">
|
||||
<ShaderSelector
|
||||
currentShaderId={target.getMaterialId()}
|
||||
onSelect={(id) => target.setMaterialId(id)}
|
||||
/>
|
||||
|
||||
{Object.entries(groupedProps).map(([group, props]) => (
|
||||
<PropertyGroup key={group} title={group}>
|
||||
{props.map(([name, meta]) => (
|
||||
<PropertyField
|
||||
key={name}
|
||||
name={name}
|
||||
meta={meta}
|
||||
value={target.getOverride(name)?.value ?? meta.default}
|
||||
onChange={(value) => {
|
||||
applyOverride(target, name, meta.type, value);
|
||||
onChange?.(name, target.getOverride(name)!);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</PropertyGroup>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、实施计划
|
||||
|
||||
### Phase 1: 接口层 (1-2 天)
|
||||
|
||||
1. **创建 IMaterialOverridable 接口** (`packages/material-system/src/interfaces/`)
|
||||
2. **创建 MaterialOverridableMixin** (`packages/material-system/src/mixins/`)
|
||||
3. **导出新接口** (`packages/material-system/src/index.ts`)
|
||||
|
||||
### Phase 2: 重构现有组件 (2-3 天)
|
||||
|
||||
1. **修改 SpriteComponent**:实现 `IMaterialOverridable`,使用 Mixin
|
||||
2. **修改 UIRenderComponent**:实现 `IMaterialOverridable`,使用 Mixin
|
||||
3. **删除重复代码**:移除各组件中的重复材质方法
|
||||
|
||||
### Phase 3: 统一效果系统 (2-3 天)
|
||||
|
||||
1. **创建 BaseShinyEffect** (`packages/material-system/src/effects/`)
|
||||
2. **创建 ShinyEffectAnimator** (`packages/material-system/src/effects/`)
|
||||
3. **重构 ShinyEffectComponent**:继承 BaseShinyEffect
|
||||
4. **重构 UIShinyEffectComponent**:继承 BaseShinyEffect
|
||||
5. **重构系统**:使用 ShinyEffectAnimator
|
||||
|
||||
### Phase 4: Shader Property 系统 (2-3 天)
|
||||
|
||||
1. **定义 ShaderPropertyMeta 接口**
|
||||
2. **扩展 ShaderDefinition** 添加 properties 字段
|
||||
3. **创建 ShaderLoader** 支持 .shader 文件
|
||||
4. **注册内置着色器属性元数据**
|
||||
|
||||
### Phase 5: Material Inspector (3-4 天)
|
||||
|
||||
1. **创建 MaterialPropertiesEditor 组件**
|
||||
2. **创建 PropertyField 组件** (Float, Vector, Color, Texture)
|
||||
3. **集成到现有 Inspector 系统**
|
||||
4. **支持实时预览**
|
||||
|
||||
---
|
||||
|
||||
## 六、文件修改清单
|
||||
|
||||
| 优先级 | 包 | 文件 | 操作 |
|
||||
|--------|-----|------|------|
|
||||
| P0 | material-system | `src/interfaces/IMaterialOverridable.ts` | 新建 |
|
||||
| P0 | material-system | `src/mixins/MaterialOverridableMixin.ts` | 新建 |
|
||||
| P0 | material-system | `src/interfaces/IShaderProperty.ts` | 新建 |
|
||||
| P1 | material-system | `src/effects/BaseShinyEffect.ts` | 新建 |
|
||||
| P1 | material-system | `src/effects/ShinyEffectAnimator.ts` | 新建 |
|
||||
| P1 | sprite | `src/SpriteComponent.ts` | 重构 |
|
||||
| P1 | ui | `src/components/UIRenderComponent.ts` | 重构 |
|
||||
| P2 | sprite | `src/ShinyEffectComponent.ts` | 重构 |
|
||||
| P2 | ui | `src/components/UIShinyEffectComponent.ts` | 重构 |
|
||||
| P2 | sprite | `src/systems/ShinyEffectSystem.ts` | 重构 |
|
||||
| P2 | ui | `src/systems/render/UIShinyEffectSystem.ts` | 重构 |
|
||||
| P3 | material-system | `src/loaders/ShaderLoader.ts` | 扩展 |
|
||||
| P3 | editor-app | `src/components/inspectors/material/*` | 新建 |
|
||||
|
||||
---
|
||||
|
||||
## 七、Transform 组件统一(可选)
|
||||
|
||||
### 7.1 现状分析
|
||||
|
||||
| 特性 | TransformComponent | UITransformComponent |
|
||||
|------|-------------------|---------------------|
|
||||
| **坐标系** | 绝对坐标 (position.x/y/z) | 相对锚点坐标 (x/y + anchor) |
|
||||
| **尺寸** | ❌ 无 | ✅ width/height + 约束 |
|
||||
| **锚点系统** | ❌ 无 | ✅ anchorMin/Max |
|
||||
| **3D 支持** | ✅ IVector3 | ❌ 纯 2D |
|
||||
| **可见性** | ❌ 无 | ✅ visible, alpha |
|
||||
|
||||
### 7.2 结论
|
||||
|
||||
**不建议完全合并**,但可提取公共基类:
|
||||
|
||||
```typescript
|
||||
// packages/engine-core/src/interfaces/ITransformBase.ts
|
||||
|
||||
export interface ITransformBase {
|
||||
/** 旋转角度(度) | Rotation in degrees */
|
||||
rotation: number;
|
||||
|
||||
/** X 缩放 | Scale X */
|
||||
scaleX: number;
|
||||
|
||||
/** Y 缩放 | Scale Y */
|
||||
scaleY: number;
|
||||
|
||||
/** 本地到世界矩阵 | Local to world matrix */
|
||||
readonly localToWorldMatrix: Matrix2D;
|
||||
|
||||
/** 是否需要更新 | Dirty flag */
|
||||
isDirty: boolean;
|
||||
|
||||
/** 世界坐标 X | World position X */
|
||||
readonly worldX: number;
|
||||
|
||||
/** 世界坐标 Y | World position Y */
|
||||
readonly worldY: number;
|
||||
|
||||
/** 世界旋转 | World rotation */
|
||||
readonly worldRotation: number;
|
||||
|
||||
/** 世界缩放 X | World scale X */
|
||||
readonly worldScaleX: number;
|
||||
|
||||
/** 世界缩放 Y | World scale Y */
|
||||
readonly worldScaleY: number;
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3 收益
|
||||
|
||||
- 渲染系统可以统一处理 `ITransformBase`
|
||||
- 减少 SpriteRenderSystem 和 UIRenderSystem 的重复
|
||||
- Gizmo 系统可以共享变换操作逻辑
|
||||
|
||||
---
|
||||
|
||||
## 八、向后兼容性
|
||||
|
||||
1. **接口兼容**:现有组件的 API 保持不变
|
||||
2. **序列化兼容**:不改变现有序列化格式
|
||||
3. **渐进迁移**:可分阶段进行,不影响现有功能
|
||||
@@ -4,49 +4,6 @@
|
||||
|
||||
---
|
||||
|
||||
## v2.4.2 (2025-12-25)
|
||||
|
||||
### Features
|
||||
|
||||
- **IncrementalSerializer 实体过滤**: 增量序列化支持 `entityFilter` 选项 (#335)
|
||||
- 创建快照时可按条件过滤实体
|
||||
- 支持按标签、组件类型等自定义过滤逻辑
|
||||
- 适用于只同步部分实体的场景(如只同步玩家)
|
||||
|
||||
```typescript
|
||||
// 只快照玩家实体
|
||||
const snapshot = IncrementalSerializer.createSnapshot(scene, {
|
||||
entityFilter: (entity) => entity.tag === PLAYER_TAG
|
||||
});
|
||||
|
||||
// 只快照有特定组件的实体
|
||||
const snapshot = IncrementalSerializer.createSnapshot(scene, {
|
||||
entityFilter: (entity) => entity.hasComponent(PlayerMarker)
|
||||
});
|
||||
```
|
||||
|
||||
### Refactor
|
||||
|
||||
- 优化 `PlatformWorkerPool` 代码规范,提取为独立模块 (#335)
|
||||
- 优化 `WorkerEntitySystem` 实现,改进代码结构 (#334)
|
||||
- 代码规范化与依赖清理 (#317)
|
||||
- 代码结构优化,添加 `GlobalTypes.ts` 统一类型定义 (#316)
|
||||
|
||||
---
|
||||
|
||||
## v2.4.1 (2025-12-23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复 `IntervalSystem` 时间累加 bug,间隔计时更加准确
|
||||
- 修复 Cocos Creator 兼容性问题,类型导出更完整
|
||||
|
||||
### Documentation
|
||||
|
||||
- 新增 `Core.paused` 属性文档说明
|
||||
|
||||
---
|
||||
|
||||
## v2.4.0 (2025-12-15)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -4,49 +4,6 @@ This document records the version update history of the `@esengine/ecs-framework
|
||||
|
||||
---
|
||||
|
||||
## v2.4.2 (2025-12-25)
|
||||
|
||||
### Features
|
||||
|
||||
- **IncrementalSerializer Entity Filter**: Incremental serialization supports `entityFilter` option (#335)
|
||||
- Filter entities by condition when creating snapshots
|
||||
- Support custom filter logic by tag, component type, etc.
|
||||
- Suitable for scenarios that only sync partial entities (e.g., only sync players)
|
||||
|
||||
```typescript
|
||||
// Only snapshot player entities
|
||||
const snapshot = IncrementalSerializer.createSnapshot(scene, {
|
||||
entityFilter: (entity) => entity.tag === PLAYER_TAG
|
||||
});
|
||||
|
||||
// Only snapshot entities with specific component
|
||||
const snapshot = IncrementalSerializer.createSnapshot(scene, {
|
||||
entityFilter: (entity) => entity.hasComponent(PlayerMarker)
|
||||
});
|
||||
```
|
||||
|
||||
### Refactor
|
||||
|
||||
- Optimize `PlatformWorkerPool` code style, extract as standalone module (#335)
|
||||
- Optimize `WorkerEntitySystem` implementation, improve code structure (#334)
|
||||
- Code standardization and dependency cleanup (#317)
|
||||
- Code structure optimization, add `GlobalTypes.ts` for unified type definitions (#316)
|
||||
|
||||
---
|
||||
|
||||
## v2.4.1 (2025-12-23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix `IntervalSystem` time accumulation bug, interval timing is now more accurate
|
||||
- Fix Cocos Creator compatibility issue, more complete type exports
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add `Core.paused` property documentation
|
||||
|
||||
---
|
||||
|
||||
## v2.4.0 (2025-12-15)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -4,24 +4,7 @@ This guide will help you get started with ECS Framework, from installation to cr
|
||||
|
||||
## 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
|
||||
@@ -350,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
|
||||
|
||||
@@ -1,402 +0,0 @@
|
||||
# Time and Timer System
|
||||
|
||||
The ECS framework provides a complete time management and timer system, including time scaling, frame time calculation, and flexible timer scheduling.
|
||||
|
||||
## Time Class
|
||||
|
||||
The Time class is the core of the framework's time management, providing all game time-related functionality.
|
||||
|
||||
### Basic Time Properties
|
||||
|
||||
```typescript
|
||||
import { Time } from '@esengine/ecs-framework';
|
||||
|
||||
class GameSystem extends EntitySystem {
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Get frame time (seconds)
|
||||
const deltaTime = Time.deltaTime;
|
||||
|
||||
// Get unscaled frame time
|
||||
const unscaledDelta = Time.unscaledDeltaTime;
|
||||
|
||||
// Get total game time
|
||||
const totalTime = Time.totalTime;
|
||||
|
||||
// Get current frame count
|
||||
const frameCount = Time.frameCount;
|
||||
|
||||
console.log(`Frame ${frameCount}, delta: ${deltaTime}s, total: ${totalTime}s`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Game Pause
|
||||
|
||||
The framework provides two pause methods for different scenarios:
|
||||
|
||||
#### Core.paused (Recommended)
|
||||
|
||||
`Core.paused` is a **true pause** - when set, the entire game loop stops:
|
||||
|
||||
```typescript
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
class PauseMenuSystem extends EntitySystem {
|
||||
public pauseGame(): void {
|
||||
// True pause - all systems stop executing
|
||||
Core.paused = true;
|
||||
console.log('Game paused');
|
||||
}
|
||||
|
||||
public resumeGame(): void {
|
||||
// Resume game
|
||||
Core.paused = false;
|
||||
console.log('Game resumed');
|
||||
}
|
||||
|
||||
public togglePause(): void {
|
||||
Core.paused = !Core.paused;
|
||||
console.log(Core.paused ? 'Game paused' : 'Game resumed');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Time.timeScale = 0
|
||||
|
||||
`Time.timeScale = 0` only makes `deltaTime` become 0, **systems still execute**:
|
||||
|
||||
```typescript
|
||||
class SlowMotionSystem extends EntitySystem {
|
||||
public freezeTime(): void {
|
||||
// Time freeze - systems still execute, just deltaTime = 0
|
||||
Time.timeScale = 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Comparison
|
||||
|
||||
| Feature | `Core.paused = true` | `Time.timeScale = 0` |
|
||||
|---------|---------------------|---------------------|
|
||||
| System Execution | Completely stopped | Still running |
|
||||
| CPU Overhead | Zero | Normal overhead |
|
||||
| Time Updates | Stopped | Continues (deltaTime=0) |
|
||||
| Timers | Stopped | Continues (but time doesn't advance) |
|
||||
| Use Cases | Pause menu, game pause | Slow motion, bullet time effects |
|
||||
|
||||
**Recommendations**:
|
||||
- Pause menu, true game pause → Use `Core.paused = true`
|
||||
- Slow motion, bullet time effects → Use `Time.timeScale`
|
||||
|
||||
### Time Scaling
|
||||
|
||||
The Time class supports time scaling for slow motion, fast forward, and other effects:
|
||||
|
||||
```typescript
|
||||
class TimeControlSystem extends EntitySystem {
|
||||
public enableSlowMotion(): void {
|
||||
// Set to slow motion (50% speed)
|
||||
Time.timeScale = 0.5;
|
||||
console.log('Slow motion enabled');
|
||||
}
|
||||
|
||||
public enableFastForward(): void {
|
||||
// Set to fast forward (200% speed)
|
||||
Time.timeScale = 2.0;
|
||||
console.log('Fast forward enabled');
|
||||
}
|
||||
|
||||
public enableBulletTime(): void {
|
||||
// Bullet time effect (10% speed)
|
||||
Time.timeScale = 0.1;
|
||||
console.log('Bullet time enabled');
|
||||
}
|
||||
|
||||
public resumeNormalSpeed(): void {
|
||||
// Resume normal speed
|
||||
Time.timeScale = 1.0;
|
||||
console.log('Normal speed resumed');
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// deltaTime is affected by timeScale
|
||||
const scaledDelta = Time.deltaTime; // Affected by time scale
|
||||
const realDelta = Time.unscaledDeltaTime; // Not affected by time scale
|
||||
|
||||
for (const entity of entities) {
|
||||
const movement = entity.getComponent(Movement);
|
||||
if (movement) {
|
||||
// Use scaled time for game logic updates
|
||||
movement.update(scaledDelta);
|
||||
}
|
||||
|
||||
const ui = entity.getComponent(UIComponent);
|
||||
if (ui) {
|
||||
// UI animations use real time, not affected by game time scale
|
||||
ui.update(realDelta);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Time Check Utilities
|
||||
|
||||
```typescript
|
||||
class CooldownSystem extends EntitySystem {
|
||||
private lastAttackTime = 0;
|
||||
private lastSpawnTime = 0;
|
||||
|
||||
constructor() {
|
||||
super(Matcher.all(Weapon));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Check attack cooldown
|
||||
if (Time.checkEvery(1.5, this.lastAttackTime)) {
|
||||
this.performAttack();
|
||||
this.lastAttackTime = Time.totalTime;
|
||||
}
|
||||
|
||||
// Check spawn interval
|
||||
if (Time.checkEvery(3.0, this.lastSpawnTime)) {
|
||||
this.spawnEnemy();
|
||||
this.lastSpawnTime = Time.totalTime;
|
||||
}
|
||||
}
|
||||
|
||||
private performAttack(): void {
|
||||
console.log('Performing attack!');
|
||||
}
|
||||
|
||||
private spawnEnemy(): void {
|
||||
console.log('Spawning enemy!');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Core.schedule Timer System
|
||||
|
||||
Core provides powerful timer scheduling functionality for creating one-time or repeating timers.
|
||||
|
||||
### Basic Timer Usage
|
||||
|
||||
```typescript
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Create one-time timers
|
||||
this.createOneTimeTimers();
|
||||
|
||||
// Create repeating timers
|
||||
this.createRepeatingTimers();
|
||||
|
||||
// Create timers with context
|
||||
this.createContextTimers();
|
||||
}
|
||||
|
||||
private createOneTimeTimers(): void {
|
||||
// Execute once after 2 seconds
|
||||
Core.schedule(2.0, false, null, (timer) => {
|
||||
console.log('Executed after 2 second delay');
|
||||
});
|
||||
|
||||
// Show tip after 5 seconds
|
||||
Core.schedule(5.0, false, this, (timer) => {
|
||||
const scene = timer.getContext<GameScene>();
|
||||
scene.showTip('Game tip: 5 seconds have passed!');
|
||||
});
|
||||
}
|
||||
|
||||
private createRepeatingTimers(): void {
|
||||
// Execute every second
|
||||
const heartbeatTimer = Core.schedule(1.0, true, null, (timer) => {
|
||||
console.log(`Game heartbeat - Total time: ${Time.totalTime.toFixed(1)}s`);
|
||||
});
|
||||
|
||||
// Save timer reference for later control
|
||||
this.saveTimerReference(heartbeatTimer);
|
||||
}
|
||||
|
||||
private createContextTimers(): void {
|
||||
const gameData = { score: 0, level: 1 };
|
||||
|
||||
// Add score every 2 seconds
|
||||
Core.schedule(2.0, true, gameData, (timer) => {
|
||||
const data = timer.getContext<typeof gameData>();
|
||||
data.score += 10;
|
||||
console.log(`Score increased! Current score: ${data.score}`);
|
||||
});
|
||||
}
|
||||
|
||||
private saveTimerReference(timer: any): void {
|
||||
// Can stop timer later
|
||||
setTimeout(() => {
|
||||
timer.stop();
|
||||
console.log('Timer stopped');
|
||||
}, 10000); // Stop after 10 seconds
|
||||
}
|
||||
|
||||
private showTip(message: string): void {
|
||||
console.log('Tip:', message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Timer Control
|
||||
|
||||
```typescript
|
||||
class TimerControlExample {
|
||||
private attackTimer: any;
|
||||
private spawnerTimer: any;
|
||||
|
||||
public startCombat(): void {
|
||||
// Start attack timer
|
||||
this.attackTimer = Core.schedule(0.5, true, this, (timer) => {
|
||||
const self = timer.getContext<TimerControlExample>();
|
||||
self.performAttack();
|
||||
});
|
||||
|
||||
// Start enemy spawn timer
|
||||
this.spawnerTimer = Core.schedule(3.0, true, null, (timer) => {
|
||||
this.spawnEnemy();
|
||||
});
|
||||
}
|
||||
|
||||
public stopCombat(): void {
|
||||
// Stop all combat-related timers
|
||||
if (this.attackTimer) {
|
||||
this.attackTimer.stop();
|
||||
console.log('Attack timer stopped');
|
||||
}
|
||||
|
||||
if (this.spawnerTimer) {
|
||||
this.spawnerTimer.stop();
|
||||
console.log('Spawn timer stopped');
|
||||
}
|
||||
}
|
||||
|
||||
public resetAttackTimer(): void {
|
||||
// Reset attack timer
|
||||
if (this.attackTimer) {
|
||||
this.attackTimer.reset();
|
||||
console.log('Attack timer reset');
|
||||
}
|
||||
}
|
||||
|
||||
private performAttack(): void {
|
||||
console.log('Performing attack');
|
||||
}
|
||||
|
||||
private spawnEnemy(): void {
|
||||
console.log('Spawning enemy');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Use Appropriate Time Types
|
||||
|
||||
```typescript
|
||||
class MovementSystem extends EntitySystem {
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const movement = entity.getComponent(Movement);
|
||||
|
||||
// Use scaled time for game logic
|
||||
movement.position.x += movement.velocity.x * Time.deltaTime;
|
||||
|
||||
// Use real time for UI animations (not affected by game pause)
|
||||
const ui = entity.getComponent(UIAnimation);
|
||||
if (ui) {
|
||||
ui.update(Time.unscaledDeltaTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Timer Management
|
||||
|
||||
```typescript
|
||||
class TimerManager {
|
||||
private timers: any[] = [];
|
||||
|
||||
public createManagedTimer(duration: number, repeats: boolean, callback: () => void): any {
|
||||
const timer = Core.schedule(duration, repeats, null, callback);
|
||||
this.timers.push(timer);
|
||||
return timer;
|
||||
}
|
||||
|
||||
public stopAllTimers(): void {
|
||||
for (const timer of this.timers) {
|
||||
timer.stop();
|
||||
}
|
||||
this.timers = [];
|
||||
}
|
||||
|
||||
public cleanupCompletedTimers(): void {
|
||||
this.timers = this.timers.filter(timer => !timer.isDone);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Avoid Too Many Timers
|
||||
|
||||
```typescript
|
||||
// Avoid: Creating a timer for each entity
|
||||
class BadExample extends EntitySystem {
|
||||
protected onAdded(entity: Entity): void {
|
||||
Core.schedule(1.0, true, entity, (timer) => {
|
||||
// One timer per entity - poor performance
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Recommended: Manage time uniformly in the system
|
||||
class GoodExample extends EntitySystem {
|
||||
private lastUpdateTime = 0;
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Execute logic once per second
|
||||
if (Time.checkEvery(1.0, this.lastUpdateTime)) {
|
||||
this.processAllEntities(entities);
|
||||
this.lastUpdateTime = Time.totalTime;
|
||||
}
|
||||
}
|
||||
|
||||
private processAllEntities(entities: readonly Entity[]): void {
|
||||
// Batch process all entities
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Timer Context Usage
|
||||
|
||||
```typescript
|
||||
interface TimerContext {
|
||||
entityId: number;
|
||||
duration: number;
|
||||
onComplete: () => void;
|
||||
}
|
||||
|
||||
class ContextualTimerExample {
|
||||
public createEntityTimer(entityId: number, duration: number, onComplete: () => void): void {
|
||||
const context: TimerContext = {
|
||||
entityId,
|
||||
duration,
|
||||
onComplete
|
||||
};
|
||||
|
||||
Core.schedule(duration, false, context, (timer) => {
|
||||
const ctx = timer.getContext<TimerContext>();
|
||||
console.log(`Timer for entity ${ctx.entityId} completed`);
|
||||
ctx.onComplete();
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The time and timer system is an essential tool in game development. Using these features correctly will make your game logic more precise and controllable.
|
||||
@@ -4,24 +4,7 @@
|
||||
|
||||
## 安装
|
||||
|
||||
### 使用 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
|
||||
@@ -350,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
|
||||
|
||||
@@ -30,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 类支持时间缩放功能,可以实现慢动作、快进等效果:
|
||||
@@ -106,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 {
|
||||
|
||||
38
package.json
38
package.json
@@ -5,16 +5,7 @@
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.22.0",
|
||||
"workspaces": [
|
||||
"packages/framework/*",
|
||||
"packages/engine/*",
|
||||
"packages/rendering/*",
|
||||
"packages/physics/*",
|
||||
"packages/streaming/*",
|
||||
"packages/network-ext/*",
|
||||
"packages/editor/*",
|
||||
"packages/editor/plugins/*",
|
||||
"packages/rust/*",
|
||||
"packages/tools/*"
|
||||
"packages/*"
|
||||
],
|
||||
"keywords": [
|
||||
"ecs",
|
||||
@@ -26,9 +17,6 @@
|
||||
"egret"
|
||||
],
|
||||
"scripts": {
|
||||
"changeset": "changeset",
|
||||
"changeset:version": "changeset version",
|
||||
"changeset:publish": "changeset publish",
|
||||
"bootstrap": "lerna bootstrap",
|
||||
"clean": "turbo run clean",
|
||||
"build": "turbo run build",
|
||||
@@ -37,24 +25,23 @@
|
||||
"build:math": "turbo run build --filter=@esengine/ecs-framework-math",
|
||||
"build:editor": "turbo run build --filter=@esengine/editor-app...",
|
||||
"build:npm": "turbo run build:npm",
|
||||
"build:npm:core": "cd packages/framework/core && npm run build:npm",
|
||||
"build:npm:math": "cd packages/framework/math && npm run build:npm",
|
||||
"build:npm:core": "cd packages/core && npm run build:npm",
|
||||
"build:npm:math": "cd packages/math && npm run build:npm",
|
||||
"test": "turbo run test",
|
||||
"test:coverage": "turbo run test:coverage",
|
||||
"test:ci": "turbo run test:ci",
|
||||
"test:ci:framework": "turbo run test:ci --filter=@esengine/ecs-framework --filter=@esengine/ecs-framework-math --filter=@esengine/behavior-tree --filter=@esengine/blueprint --filter=@esengine/fsm --filter=@esengine/timer --filter=@esengine/spatial --filter=@esengine/procgen --filter=@esengine/pathfinding --filter=@esengine/network-protocols --filter=@esengine/network",
|
||||
"prepare:publish": "npm run build:npm && node scripts/pre-publish-check.cjs",
|
||||
"sync:versions": "node scripts/sync-versions.cjs",
|
||||
"publish:all": "npm run prepare:publish && npm run publish:all:dist",
|
||||
"publish:all:dist": "npm run publish:core && npm run publish:math",
|
||||
"publish:core": "cd packages/framework/core && npm run publish:npm",
|
||||
"publish:core:patch": "cd packages/framework/core && npm run publish:patch",
|
||||
"publish:math": "cd packages/framework/math && npm run publish:npm",
|
||||
"publish:math:patch": "cd packages/framework/math && npm run publish:patch",
|
||||
"publish:core": "cd packages/core && npm run publish:npm",
|
||||
"publish:core:patch": "cd packages/core && npm run publish:patch",
|
||||
"publish:math": "cd packages/math && npm run publish:npm",
|
||||
"publish:math:patch": "cd packages/math && npm run publish:patch",
|
||||
"publish": "lerna publish",
|
||||
"version": "lerna version",
|
||||
"release": "semantic-release",
|
||||
"release:core": "cd packages/framework/core && semantic-release",
|
||||
"release:core": "cd packages/core && semantic-release",
|
||||
"contributors:add": "all-contributors add",
|
||||
"contributors:generate": "all-contributors generate",
|
||||
"contributors:check": "all-contributors check",
|
||||
@@ -68,19 +55,15 @@
|
||||
"format": "prettier --write \"packages/**/src/**/*.{ts,tsx,js,jsx}\"",
|
||||
"format:check": "prettier --check \"packages/**/src/**/*.{ts,tsx,js,jsx}\"",
|
||||
"type-check": "turbo run type-check",
|
||||
"type-check:framework": "turbo run type-check --filter=@esengine/ecs-framework --filter=@esengine/ecs-framework-math --filter=@esengine/behavior-tree --filter=@esengine/blueprint --filter=@esengine/fsm --filter=@esengine/timer --filter=@esengine/spatial --filter=@esengine/procgen --filter=@esengine/pathfinding --filter=@esengine/network-protocols --filter=@esengine/network",
|
||||
"lint": "turbo run lint",
|
||||
"lint:framework": "turbo run lint --filter=@esengine/ecs-framework --filter=@esengine/ecs-framework-math --filter=@esengine/behavior-tree --filter=@esengine/blueprint --filter=@esengine/fsm --filter=@esengine/timer --filter=@esengine/spatial --filter=@esengine/procgen --filter=@esengine/pathfinding --filter=@esengine/network-protocols --filter=@esengine/network",
|
||||
"lint:fix": "turbo run lint:fix",
|
||||
"build:wasm": "cd packages/rust/engine && wasm-pack build --dev --out-dir pkg",
|
||||
"build:wasm:release": "cd packages/rust/engine && wasm-pack build --release --out-dir pkg",
|
||||
"build:wasm": "cd packages/engine && wasm-pack build --dev --out-dir pkg",
|
||||
"build:wasm:release": "cd packages/engine && wasm-pack build --release --out-dir pkg",
|
||||
"copy-modules": "node scripts/copy-engine-modules.mjs"
|
||||
},
|
||||
"author": "yhh",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@changesets/changelog-github": "^0.5.2",
|
||||
"@changesets/cli": "^2.29.8",
|
||||
"@commitlint/cli": "^18.6.0",
|
||||
"@commitlint/config-conventional": "^18.6.0",
|
||||
"@eslint/js": "^9.39.1",
|
||||
@@ -138,3 +121,4 @@
|
||||
"ws": "^8.18.2"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,5 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/esengine/esengine.git",
|
||||
"directory": "packages/asset-system-editor"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
}
|
||||
@@ -165,10 +165,7 @@ export function inferAssetType(path: string): AssetType {
|
||||
btree: 'behavior-tree',
|
||||
bp: 'blueprint',
|
||||
mat: 'material',
|
||||
particle: 'particle',
|
||||
|
||||
// FairyGUI
|
||||
fui: 'fui'
|
||||
particle: 'particle'
|
||||
};
|
||||
|
||||
return typeMap[ext] || 'binary';
|
||||
@@ -30,9 +30,9 @@
|
||||
"author": "yhh",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@esengine/build-config": "workspace:*",
|
||||
"@esengine/ecs-framework": "workspace:*",
|
||||
"@esengine/engine-core": "workspace:*",
|
||||
"@esengine/build-config": "workspace:*",
|
||||
"rimraf": "^5.0.0",
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.8.3"
|
||||
@@ -44,10 +44,5 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/esengine/esengine.git",
|
||||
"directory": "packages/asset-system"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/pako": "^2.0.4",
|
||||
"pako": "^2.1.0"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
}
|
||||
@@ -10,47 +10,6 @@ import {
|
||||
IAssetCatalogEntry
|
||||
} from '../types/AssetTypes';
|
||||
|
||||
/**
|
||||
* 纹理 Sprite 信息(从 meta 文件的 importSettings 读取)
|
||||
* Texture sprite info (read from meta file's importSettings)
|
||||
*/
|
||||
export interface ITextureSpriteInfo {
|
||||
/**
|
||||
* 九宫格切片边距 [top, right, bottom, left]
|
||||
* Nine-patch slice border
|
||||
*/
|
||||
sliceBorder?: [number, number, number, number];
|
||||
/**
|
||||
* Sprite 锚点 [x, y](0-1 归一化)
|
||||
* Sprite pivot point (0-1 normalized)
|
||||
*/
|
||||
pivot?: [number, number];
|
||||
/**
|
||||
* 纹理宽度(可选,需要纹理已加载)
|
||||
* Texture width (optional, requires texture to be loaded)
|
||||
*/
|
||||
width?: number;
|
||||
/**
|
||||
* 纹理高度(可选,需要纹理已加载)
|
||||
* Texture height (optional, requires texture to be loaded)
|
||||
*/
|
||||
height?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sprite settings in import settings
|
||||
* 导入设置中的 Sprite 设置
|
||||
*/
|
||||
interface ISpriteSettings {
|
||||
sliceBorder?: [number, number, number, number];
|
||||
pivot?: [number, number];
|
||||
pixelsPerUnit?: number;
|
||||
/** Texture width (from import settings) | 纹理宽度(来自导入设置) */
|
||||
width?: number;
|
||||
/** Texture height (from import settings) | 纹理高度(来自导入设置) */
|
||||
height?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset database implementation
|
||||
* 资产数据库实现
|
||||
@@ -253,41 +212,6 @@ export class AssetDatabase {
|
||||
return guid ? this._metadata.get(guid) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get texture sprite info from metadata
|
||||
* 从元数据获取纹理 Sprite 信息
|
||||
*
|
||||
* Extracts spriteSettings from importSettings if available.
|
||||
* 如果可用,从 importSettings 提取 spriteSettings。
|
||||
*
|
||||
* @param guid - Texture asset GUID | 纹理资产 GUID
|
||||
* @returns Sprite info or undefined if not found/not a texture | Sprite 信息或未找到/非纹理则为 undefined
|
||||
*/
|
||||
getTextureSpriteInfo(guid: AssetGUID): ITextureSpriteInfo | undefined {
|
||||
const metadata = this._metadata.get(guid);
|
||||
if (!metadata) return undefined;
|
||||
|
||||
// Check if it's a texture asset
|
||||
// 检查是否是纹理资产
|
||||
if (metadata.type !== AssetType.Texture) return undefined;
|
||||
|
||||
// Extract spriteSettings from importSettings
|
||||
// 从 importSettings 提取 spriteSettings
|
||||
const importSettings = metadata.importSettings as Record<string, unknown> | undefined;
|
||||
const spriteSettings = importSettings?.spriteSettings as ISpriteSettings | undefined;
|
||||
|
||||
if (!spriteSettings) return undefined;
|
||||
|
||||
return {
|
||||
sliceBorder: spriteSettings.sliceBorder,
|
||||
pivot: spriteSettings.pivot,
|
||||
// Include dimensions from import settings if available
|
||||
// 如果可用,包含来自导入设置的尺寸
|
||||
width: spriteSettings.width,
|
||||
height: spriteSettings.height
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Find assets by type
|
||||
* 按类型查找资产
|
||||
@@ -132,10 +132,7 @@ export class AssetManager implements IAssetManager {
|
||||
labels: [],
|
||||
tags: new Map(),
|
||||
lastModified: Date.now(),
|
||||
version: 1,
|
||||
// Include importSettings for sprite slicing (nine-patch), etc.
|
||||
// 包含 importSettings 以支持精灵切片(九宫格)等功能
|
||||
importSettings: entry.importSettings
|
||||
version: 1
|
||||
};
|
||||
|
||||
this._database.addAsset(metadata);
|
||||
@@ -176,11 +173,7 @@ export class AssetManager implements IAssetManager {
|
||||
}
|
||||
|
||||
// 创建加载器 / Create loader
|
||||
// 优先使用基于路径的加载器选择,支持多个加载器对应同一资产类型
|
||||
// 例如 Model3D 类型支持 GLTF/FBX/OBJ,根据扩展名选择正确的加载器
|
||||
// Prefer path-based loader selection, supports multiple loaders for same asset type
|
||||
// e.g., Model3D type supports GLTF/FBX/OBJ, selects correct loader by extension
|
||||
let loader = this._loaderFactory.createLoaderForPath(metadata.path);
|
||||
let loader = this._loaderFactory.createLoader(metadata.type);
|
||||
|
||||
// 如果没有找到 loader 且类型是 Custom,尝试重新解析类型
|
||||
// If no loader found and type is Custom, try to re-resolve the type
|
||||
@@ -36,7 +36,6 @@ export { RuntimeCatalog, runtimeCatalog } from './runtime/RuntimeCatalog';
|
||||
export * from './interfaces/IAssetLoader';
|
||||
export * from './interfaces/IAssetManager';
|
||||
export * from './interfaces/IAssetReader';
|
||||
export * from './interfaces/IAssetFileLoader';
|
||||
export * from './interfaces/IResourceComponent';
|
||||
|
||||
// Core
|
||||
@@ -57,31 +56,15 @@ export { BinaryLoader } from './loaders/BinaryLoader';
|
||||
export { AudioLoader } from './loaders/AudioLoader';
|
||||
export { PrefabLoader } from './loaders/PrefabLoader';
|
||||
|
||||
// 3D Model Loaders | 3D 模型加载器
|
||||
export { GLTFLoader } from './loaders/GLTFLoader';
|
||||
export { OBJLoader } from './loaders/OBJLoader';
|
||||
export { FBXLoader } from './loaders/FBXLoader';
|
||||
|
||||
// Integration
|
||||
export { EngineIntegration } from './integration/EngineIntegration';
|
||||
export type { ITextureEngineBridge, TextureLoadCallback } from './integration/EngineIntegration';
|
||||
export type { ITextureEngineBridge } from './integration/EngineIntegration';
|
||||
|
||||
// Services
|
||||
export { SceneResourceManager } from './services/SceneResourceManager';
|
||||
export type { IResourceLoader } from './services/SceneResourceManager';
|
||||
export { PathResolutionService } from './services/PathResolutionService';
|
||||
|
||||
// Asset Metadata Service (primary API for sprite info)
|
||||
// 资产元数据服务(sprite 信息的主要 API)
|
||||
export {
|
||||
setGlobalAssetDatabase,
|
||||
getGlobalAssetDatabase,
|
||||
setGlobalEngineBridge,
|
||||
getGlobalEngineBridge,
|
||||
getTextureSpriteInfo
|
||||
} from './services/AssetMetadataService';
|
||||
export type { ITextureSpriteInfo } from './core/AssetDatabase';
|
||||
|
||||
// Utils
|
||||
export { UVHelper } from './utils/UVHelper';
|
||||
export {
|
||||
@@ -31,6 +31,12 @@ export interface ITextureEngineBridge {
|
||||
*/
|
||||
unloadTexture(id: number): void;
|
||||
|
||||
/**
|
||||
* Get texture info
|
||||
* 获取纹理信息
|
||||
*/
|
||||
getTextureInfo(id: number): { width: number; height: number } | null;
|
||||
|
||||
/**
|
||||
* Get or load texture by path.
|
||||
* 按路径获取或加载纹理。
|
||||
@@ -103,20 +109,6 @@ export interface ITextureEngineBridge {
|
||||
* @returns Promise that resolves when texture is ready | 纹理就绪时解析的 Promise
|
||||
*/
|
||||
loadTextureAsync?(id: number, url: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Get texture info by path.
|
||||
* 通过路径获取纹理信息。
|
||||
*
|
||||
* This is the primary API for getting texture dimensions.
|
||||
* The Rust engine is the single source of truth for texture dimensions.
|
||||
* 这是获取纹理尺寸的主要 API。
|
||||
* Rust 引擎是纹理尺寸的唯一事实来源。
|
||||
*
|
||||
* @param path Image path/URL | 图片路径/URL
|
||||
* @returns Texture info or null if not loaded | 纹理信息或未加载则为 null
|
||||
*/
|
||||
getTextureInfoByPath?(path: string): { width: number; height: number } | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -139,43 +131,10 @@ interface DataAssetEntry {
|
||||
path: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Texture load callback type
|
||||
* 纹理加载回调类型
|
||||
*/
|
||||
export type TextureLoadCallback = (guid: string, path: string, textureId: number) => void;
|
||||
|
||||
/**
|
||||
* Asset system engine integration
|
||||
* 资产系统引擎集成
|
||||
*/
|
||||
/**
|
||||
* Texture sprite info (nine-patch border, pivot, etc.)
|
||||
* 纹理 Sprite 信息(九宫格边距、锚点等)
|
||||
*/
|
||||
export interface ITextureSpriteInfo {
|
||||
/**
|
||||
* 九宫格切片边距 [top, right, bottom, left]
|
||||
* Nine-patch slice border
|
||||
*/
|
||||
sliceBorder?: [number, number, number, number];
|
||||
/**
|
||||
* Sprite 锚点 [x, y](0-1 归一化)
|
||||
* Sprite pivot point (0-1 normalized)
|
||||
*/
|
||||
pivot?: [number, number];
|
||||
/**
|
||||
* 纹理宽度
|
||||
* Texture width
|
||||
*/
|
||||
width: number;
|
||||
/**
|
||||
* 纹理高度
|
||||
* Texture height
|
||||
*/
|
||||
height: number;
|
||||
}
|
||||
|
||||
export class EngineIntegration {
|
||||
private _assetManager: AssetManager;
|
||||
private _engineBridge?: ITextureEngineBridge;
|
||||
@@ -187,54 +146,6 @@ export class EngineIntegration {
|
||||
// Path-stable ID cache (persists across Play/Stop cycles)
|
||||
private static _pathIdCache = new Map<string, number>();
|
||||
|
||||
// 纹理 Sprite 信息缓存(全局静态,可供渲染系统访问)
|
||||
// Texture sprite info cache (global static, accessible by render systems)
|
||||
private static _textureSpriteInfoCache = new Map<AssetGUID, ITextureSpriteInfo>();
|
||||
|
||||
// 纹理加载回调(用于动态图集集成等)
|
||||
// Texture load callback (for dynamic atlas integration, etc.)
|
||||
private static _textureLoadCallbacks: TextureLoadCallback[] = [];
|
||||
|
||||
/**
|
||||
* Register a callback to be called when textures are loaded
|
||||
* 注册纹理加载时调用的回调
|
||||
*
|
||||
* This can be used for dynamic atlas integration.
|
||||
* 可用于动态图集集成。
|
||||
*
|
||||
* @param callback - Callback function | 回调函数
|
||||
*/
|
||||
static onTextureLoad(callback: TextureLoadCallback): void {
|
||||
if (!EngineIntegration._textureLoadCallbacks.includes(callback)) {
|
||||
EngineIntegration._textureLoadCallbacks.push(callback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a texture load callback
|
||||
* 移除纹理加载回调
|
||||
*/
|
||||
static removeTextureLoadCallback(callback: TextureLoadCallback): void {
|
||||
const index = EngineIntegration._textureLoadCallbacks.indexOf(callback);
|
||||
if (index >= 0) {
|
||||
EngineIntegration._textureLoadCallbacks.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify all callbacks of a texture load
|
||||
* 通知所有回调纹理已加载
|
||||
*/
|
||||
private static notifyTextureLoad(guid: string, path: string, textureId: number): void {
|
||||
for (const callback of EngineIntegration._textureLoadCallbacks) {
|
||||
try {
|
||||
callback(guid, path, textureId);
|
||||
} catch (e) {
|
||||
console.error('[EngineIntegration] Error in texture load callback:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Audio resource mappings | 音频资源映射
|
||||
private _audioIdMap = new Map<AssetGUID, number>();
|
||||
private _pathToAudioId = new Map<string, number>();
|
||||
@@ -368,16 +279,6 @@ export class EngineIntegration {
|
||||
const result = await this._assetManager.loadAsset<ITextureAsset>(guid);
|
||||
const metadata = result.metadata;
|
||||
const assetPath = metadata.path;
|
||||
const textureAsset = result.asset;
|
||||
|
||||
// 缓存 sprite 信息(九宫格边距等)到静态缓存
|
||||
// Cache sprite info (slice border, etc.) to static cache
|
||||
EngineIntegration._textureSpriteInfoCache.set(guid, {
|
||||
sliceBorder: textureAsset.sliceBorder,
|
||||
pivot: textureAsset.pivot,
|
||||
width: textureAsset.width,
|
||||
height: textureAsset.height
|
||||
});
|
||||
|
||||
// 生成路径稳定 ID
|
||||
// Generate path-stable ID
|
||||
@@ -408,37 +309,9 @@ export class EngineIntegration {
|
||||
this._textureIdMap.set(guid, stableId);
|
||||
this._pathToTextureId.set(assetPath, stableId);
|
||||
|
||||
// 通知回调(用于动态图集等)
|
||||
// Notify callbacks (for dynamic atlas, etc.)
|
||||
EngineIntegration.notifyTextureLoad(guid, engineUrl, stableId);
|
||||
|
||||
return stableId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get texture sprite info by GUID (static method for render system access)
|
||||
* 通过 GUID 获取纹理 Sprite 信息(静态方法,供渲染系统访问)
|
||||
*
|
||||
* Returns cached sprite info including nine-patch slice border.
|
||||
* Must call loadTextureByGuid first to populate the cache.
|
||||
* 返回缓存的 sprite 信息,包括九宫格边距。
|
||||
* 必须先调用 loadTextureByGuid 来填充缓存。
|
||||
*
|
||||
* @param guid - Texture asset GUID | 纹理资产 GUID
|
||||
* @returns Sprite info or undefined if not loaded | Sprite 信息或未加载则为 undefined
|
||||
*/
|
||||
static getTextureSpriteInfo(guid: AssetGUID): ITextureSpriteInfo | undefined {
|
||||
return EngineIntegration._textureSpriteInfoCache.get(guid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear texture sprite info cache
|
||||
* 清除纹理 Sprite 信息缓存
|
||||
*/
|
||||
static clearTextureSpriteInfoCache(): void {
|
||||
EngineIntegration._textureSpriteInfoCache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch load textures
|
||||
* 批量加载纹理
|
||||
264
packages/asset-system/src/interfaces/IAssetLoader.ts
Normal file
264
packages/asset-system/src/interfaces/IAssetLoader.ts
Normal file
@@ -0,0 +1,264 @@
|
||||
/**
|
||||
* Asset loader interfaces
|
||||
* 资产加载器接口
|
||||
*/
|
||||
|
||||
import {
|
||||
AssetType,
|
||||
AssetGUID,
|
||||
IAssetLoadOptions,
|
||||
IAssetMetadata
|
||||
} from '../types/AssetTypes';
|
||||
import type { IAssetContent, AssetContentType } from './IAssetReader';
|
||||
|
||||
/**
|
||||
* Parse context provided to loaders.
|
||||
* 提供给加载器的解析上下文。
|
||||
*/
|
||||
export interface IAssetParseContext {
|
||||
/** Asset metadata. | 资产元数据。 */
|
||||
metadata: IAssetMetadata;
|
||||
/** Load options. | 加载选项。 */
|
||||
options?: IAssetLoadOptions;
|
||||
/**
|
||||
* Load a dependency asset by relative path.
|
||||
* 通过相对路径加载依赖资产。
|
||||
*/
|
||||
loadDependency<D = unknown>(relativePath: string): Promise<D>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset loader interface.
|
||||
* 资产加载器接口。
|
||||
*
|
||||
* Loaders only parse content, file reading is handled by AssetManager.
|
||||
* 加载器只负责解析内容,文件读取由 AssetManager 处理。
|
||||
*/
|
||||
export interface IAssetLoader<T = unknown> {
|
||||
/** Supported asset type. | 支持的资产类型。 */
|
||||
readonly supportedType: AssetType;
|
||||
|
||||
/** Supported file extensions. | 支持的文件扩展名。 */
|
||||
readonly supportedExtensions: string[];
|
||||
|
||||
/**
|
||||
* Required content type for this loader.
|
||||
* 此加载器需要的内容类型。
|
||||
*
|
||||
* - 'text': For JSON, shader, material files
|
||||
* - 'binary': For binary formats
|
||||
* - 'image': For textures
|
||||
* - 'audio': For audio files
|
||||
*/
|
||||
readonly contentType: AssetContentType;
|
||||
|
||||
/**
|
||||
* Parse asset from content.
|
||||
* 从内容解析资产。
|
||||
*
|
||||
* @param content - File content. | 文件内容。
|
||||
* @param context - Parse context. | 解析上下文。
|
||||
* @returns Parsed asset. | 解析后的资产。
|
||||
*/
|
||||
parse(content: IAssetContent, context: IAssetParseContext): Promise<T>;
|
||||
|
||||
/**
|
||||
* Dispose loaded asset and free resources.
|
||||
* 释放已加载的资产。
|
||||
*/
|
||||
dispose(asset: T): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset loader factory interface
|
||||
* 资产加载器工厂接口
|
||||
*/
|
||||
export interface IAssetLoaderFactory {
|
||||
/**
|
||||
* Create loader for specific asset type
|
||||
* 为特定资产类型创建加载器
|
||||
*/
|
||||
createLoader(type: AssetType): IAssetLoader | null;
|
||||
|
||||
/**
|
||||
* Register custom loader
|
||||
* 注册自定义加载器
|
||||
*/
|
||||
registerLoader(type: AssetType, loader: IAssetLoader): void;
|
||||
|
||||
/**
|
||||
* Unregister loader
|
||||
* 注销加载器
|
||||
*/
|
||||
unregisterLoader(type: AssetType): void;
|
||||
|
||||
/**
|
||||
* Check if loader exists for type
|
||||
* 检查类型是否有加载器
|
||||
*/
|
||||
hasLoader(type: AssetType): boolean;
|
||||
|
||||
/**
|
||||
* Get asset type by file extension
|
||||
* 根据文件扩展名获取资产类型
|
||||
*/
|
||||
getAssetTypeByExtension(extension: string): AssetType | null;
|
||||
|
||||
/**
|
||||
* Get asset type by file path
|
||||
* 根据文件路径获取资产类型
|
||||
*/
|
||||
getAssetTypeByPath(path: string): AssetType | null;
|
||||
|
||||
/**
|
||||
* Get all supported file extensions from all registered loaders.
|
||||
* 获取所有注册加载器支持的文件扩展名。
|
||||
*
|
||||
* @returns Array of extension patterns (e.g., ['*.png', '*.jpg', '*.particle'])
|
||||
*/
|
||||
getAllSupportedExtensions(): string[];
|
||||
|
||||
/**
|
||||
* Get extension to type mapping for all registered loaders.
|
||||
* 获取所有注册加载器的扩展名到类型的映射。
|
||||
*
|
||||
* @returns Map of extension (without dot) to asset type string
|
||||
*/
|
||||
getExtensionTypeMap(): Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Texture asset interface
|
||||
* 纹理资产接口
|
||||
*/
|
||||
export interface ITextureAsset {
|
||||
/** WebGL纹理ID / WebGL texture ID */
|
||||
textureId: number;
|
||||
/** 宽度 / Width */
|
||||
width: number;
|
||||
/** 高度 / Height */
|
||||
height: number;
|
||||
/** 格式 / Format */
|
||||
format: 'rgba' | 'rgb' | 'alpha';
|
||||
/** 是否有Mipmap / Has mipmaps */
|
||||
hasMipmaps: boolean;
|
||||
/** 原始数据(如果可用) / Raw image data if available */
|
||||
data?: ImageData | HTMLImageElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mesh asset interface
|
||||
* 网格资产接口
|
||||
*/
|
||||
export interface IMeshAsset {
|
||||
/** 顶点数据 / Vertex data */
|
||||
vertices: Float32Array;
|
||||
/** 索引数据 / Index data */
|
||||
indices: Uint16Array | Uint32Array;
|
||||
/** 法线数据 / Normal data */
|
||||
normals?: Float32Array;
|
||||
/** UV坐标 / UV coordinates */
|
||||
uvs?: Float32Array;
|
||||
/** 切线数据 / Tangent data */
|
||||
tangents?: Float32Array;
|
||||
/** 边界盒 / Axis-aligned bounding box */
|
||||
bounds: {
|
||||
min: [number, number, number];
|
||||
max: [number, number, number];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Audio asset interface
|
||||
* 音频资产接口
|
||||
*/
|
||||
export interface IAudioAsset {
|
||||
/** 音频缓冲区 / Audio buffer */
|
||||
buffer: AudioBuffer;
|
||||
/** 时长(秒) / Duration in seconds */
|
||||
duration: number;
|
||||
/** 采样率 / Sample rate */
|
||||
sampleRate: number;
|
||||
/** 声道数 / Number of channels */
|
||||
channels: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Material asset interface
|
||||
* 材质资产接口
|
||||
*/
|
||||
export interface IMaterialAsset {
|
||||
/** 着色器名称 / Shader name */
|
||||
shader: string;
|
||||
/** 材质属性 / Material properties */
|
||||
properties: Map<string, unknown>;
|
||||
/** 纹理映射 / Texture slot mappings */
|
||||
textures: Map<string, AssetGUID>;
|
||||
/** 渲染状态 / Render states */
|
||||
renderStates: {
|
||||
cullMode?: 'none' | 'front' | 'back';
|
||||
blendMode?: 'none' | 'alpha' | 'additive' | 'multiply';
|
||||
depthTest?: boolean;
|
||||
depthWrite?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
// 预制体资产接口从专用文件导出 | Prefab asset interface exported from dedicated file
|
||||
export type { IPrefabAsset, IPrefabData, IPrefabMetadata, IPrefabService } from './IPrefabAsset';
|
||||
|
||||
/**
|
||||
* Scene asset interface
|
||||
* 场景资产接口
|
||||
*/
|
||||
export interface ISceneAsset {
|
||||
/** 场景名称 / Scene name */
|
||||
name: string;
|
||||
/** 实体列表 / Serialized entity list */
|
||||
entities: unknown[];
|
||||
/** 场景设置 / Scene settings */
|
||||
settings: {
|
||||
/** 环境光 / Ambient light */
|
||||
ambientLight?: [number, number, number];
|
||||
/** 雾效 / Fog settings */
|
||||
fog?: {
|
||||
enabled: boolean;
|
||||
color: [number, number, number];
|
||||
density: number;
|
||||
};
|
||||
/** 天空盒 / Skybox asset */
|
||||
skybox?: AssetGUID;
|
||||
};
|
||||
/** 引用的资产 / All referenced assets */
|
||||
referencedAssets: AssetGUID[];
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON asset interface
|
||||
* JSON资产接口
|
||||
*/
|
||||
export interface IJsonAsset {
|
||||
/** JSON数据 / JSON data */
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Text asset interface
|
||||
* 文本资产接口
|
||||
*/
|
||||
export interface ITextAsset {
|
||||
/** 文本内容 / Text content */
|
||||
content: string;
|
||||
/** 编码格式 / Encoding */
|
||||
encoding: 'utf8' | 'utf16' | 'ascii';
|
||||
}
|
||||
|
||||
/**
|
||||
* Binary asset interface
|
||||
* 二进制资产接口
|
||||
*/
|
||||
export interface IBinaryAsset {
|
||||
/** 二进制数据 / Binary data */
|
||||
data: ArrayBuffer;
|
||||
/** MIME类型 / MIME type */
|
||||
mimeType?: string;
|
||||
}
|
||||
@@ -11,24 +11,14 @@ import { TextLoader } from './TextLoader';
|
||||
import { BinaryLoader } from './BinaryLoader';
|
||||
import { AudioLoader } from './AudioLoader';
|
||||
import { PrefabLoader } from './PrefabLoader';
|
||||
import { GLTFLoader } from './GLTFLoader';
|
||||
import { OBJLoader } from './OBJLoader';
|
||||
import { FBXLoader } from './FBXLoader';
|
||||
|
||||
/**
|
||||
* Asset loader factory
|
||||
* 资产加载器工厂
|
||||
*
|
||||
* Supports multiple loaders per asset type (selected by file extension).
|
||||
* 支持每种资产类型的多个加载器(按文件扩展名选择)。
|
||||
*/
|
||||
export class AssetLoaderFactory implements IAssetLoaderFactory {
|
||||
private readonly _loaders = new Map<AssetType, IAssetLoader>();
|
||||
|
||||
/** Extension -> Loader map for precise loader selection */
|
||||
/** 扩展名 -> 加载器映射,用于精确选择加载器 */
|
||||
private readonly _extensionLoaders = new Map<string, IAssetLoader>();
|
||||
|
||||
constructor() {
|
||||
// 注册默认加载器 / Register default loaders
|
||||
this.registerDefaultLoaders();
|
||||
@@ -56,34 +46,6 @@ export class AssetLoaderFactory implements IAssetLoaderFactory {
|
||||
|
||||
// 预制体加载器 / Prefab loader
|
||||
this._loaders.set(AssetType.Prefab, new PrefabLoader());
|
||||
|
||||
// 3D模型加载器 / 3D Model loaders
|
||||
// Default is GLTF, but OBJ and FBX are also supported
|
||||
// 默认是 GLTF,但也支持 OBJ 和 FBX
|
||||
const gltfLoader = new GLTFLoader();
|
||||
const objLoader = new OBJLoader();
|
||||
const fbxLoader = new FBXLoader();
|
||||
|
||||
this._loaders.set(AssetType.Model3D, gltfLoader);
|
||||
|
||||
// Register extension-specific loaders
|
||||
// 注册特定扩展名的加载器
|
||||
this.registerExtensionLoader('.gltf', gltfLoader);
|
||||
this.registerExtensionLoader('.glb', gltfLoader);
|
||||
this.registerExtensionLoader('.obj', objLoader);
|
||||
this.registerExtensionLoader('.fbx', fbxLoader);
|
||||
|
||||
// 注:Shader 和 Material 加载器由 material-system 模块注册
|
||||
// Note: Shader and Material loaders are registered by material-system module
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a loader for a specific file extension
|
||||
* 为特定文件扩展名注册加载器
|
||||
*/
|
||||
registerExtensionLoader(extension: string, loader: IAssetLoader): void {
|
||||
const ext = extension.toLowerCase().startsWith('.') ? extension.toLowerCase() : `.${extension.toLowerCase()}`;
|
||||
this._extensionLoaders.set(ext, loader);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,38 +56,6 @@ export class AssetLoaderFactory implements IAssetLoaderFactory {
|
||||
return this._loaders.get(type) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create loader for a specific file path (selects by extension)
|
||||
* 为特定文件路径创建加载器(按扩展名选择)
|
||||
*
|
||||
* This method is preferred over createLoader() when multiple loaders
|
||||
* support the same asset type (e.g., Model3D with GLTF/OBJ/FBX).
|
||||
* 当多个加载器支持相同资产类型时(如 Model3D 的 GLTF/OBJ/FBX),
|
||||
* 优先使用此方法而非 createLoader()。
|
||||
*/
|
||||
createLoaderForPath(path: string): IAssetLoader | null {
|
||||
const lastDot = path.lastIndexOf('.');
|
||||
if (lastDot !== -1) {
|
||||
const ext = path.substring(lastDot).toLowerCase();
|
||||
|
||||
// First try extension-specific loader
|
||||
// 首先尝试特定扩展名的加载器
|
||||
const extLoader = this._extensionLoaders.get(ext);
|
||||
if (extLoader) {
|
||||
return extLoader;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to type-based lookup
|
||||
// 回退到基于类型的查找
|
||||
const type = this.getAssetTypeByPath(path);
|
||||
if (type) {
|
||||
return this.createLoader(type);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register custom loader
|
||||
* 注册自定义加载器
|
||||
@@ -159,16 +89,6 @@ export class AssetLoaderFactory implements IAssetLoaderFactory {
|
||||
*/
|
||||
getAssetTypeByExtension(extension: string): AssetType | null {
|
||||
const ext = extension.toLowerCase();
|
||||
|
||||
// Check extension-specific loaders first
|
||||
// 首先检查特定扩展名的加载器
|
||||
const extLoader = this._extensionLoaders.get(ext);
|
||||
if (extLoader) {
|
||||
return extLoader.supportedType;
|
||||
}
|
||||
|
||||
// Fall back to type-based loaders
|
||||
// 回退到基于类型的加载器
|
||||
for (const [type, loader] of this._loaders) {
|
||||
if (loader.supportedExtensions.some(e => e.toLowerCase() === ext)) {
|
||||
return type;
|
||||
@@ -236,22 +156,14 @@ export class AssetLoaderFactory implements IAssetLoaderFactory {
|
||||
getAllSupportedExtensions(): string[] {
|
||||
const extensions = new Set<string>();
|
||||
|
||||
// From type-based loaders
|
||||
// 从基于类型的加载器
|
||||
for (const loader of this._loaders.values()) {
|
||||
for (const ext of loader.supportedExtensions) {
|
||||
// 转换为 glob 模式 | Convert to glob pattern
|
||||
const cleanExt = ext.startsWith('.') ? ext.substring(1) : ext;
|
||||
extensions.add(`*.${cleanExt}`);
|
||||
}
|
||||
}
|
||||
|
||||
// From extension-specific loaders
|
||||
// 从特定扩展名的加载器
|
||||
for (const ext of this._extensionLoaders.keys()) {
|
||||
const cleanExt = ext.startsWith('.') ? ext.substring(1) : ext;
|
||||
extensions.add(`*.${cleanExt}`);
|
||||
}
|
||||
|
||||
return Array.from(extensions);
|
||||
}
|
||||
|
||||
@@ -264,8 +176,6 @@ export class AssetLoaderFactory implements IAssetLoaderFactory {
|
||||
getExtensionTypeMap(): Record<string, string> {
|
||||
const map: Record<string, string> = {};
|
||||
|
||||
// From type-based loaders
|
||||
// 从基于类型的加载器
|
||||
for (const [type, loader] of this._loaders) {
|
||||
for (const ext of loader.supportedExtensions) {
|
||||
const cleanExt = ext.startsWith('.') ? ext.substring(1) : ext;
|
||||
@@ -273,13 +183,6 @@ export class AssetLoaderFactory implements IAssetLoaderFactory {
|
||||
}
|
||||
}
|
||||
|
||||
// From extension-specific loaders
|
||||
// 从特定扩展名的加载器
|
||||
for (const [ext, loader] of this._extensionLoaders) {
|
||||
const cleanExt = ext.startsWith('.') ? ext.substring(1) : ext;
|
||||
map[cleanExt.toLowerCase()] = loader.supportedType;
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
}
|
||||
@@ -16,16 +16,6 @@ interface IEngineBridgeGlobal {
|
||||
unloadTexture?(textureId: number): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sprite settings from texture meta
|
||||
* 纹理 meta 中的 Sprite 设置
|
||||
*/
|
||||
interface ISpriteSettings {
|
||||
sliceBorder?: [number, number, number, number];
|
||||
pivot?: [number, number];
|
||||
pixelsPerUnit?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取全局引擎桥接
|
||||
* Get global engine bridge
|
||||
@@ -71,22 +61,13 @@ export class TextureLoader implements IAssetLoader<ITextureAsset> {
|
||||
|
||||
const image = content.image;
|
||||
|
||||
// Read sprite settings from import settings
|
||||
// 从导入设置读取 sprite 设置
|
||||
const importSettings = context.metadata.importSettings as Record<string, unknown> | undefined;
|
||||
const spriteSettings = importSettings?.spriteSettings as ISpriteSettings | undefined;
|
||||
|
||||
const textureAsset: ITextureAsset = {
|
||||
textureId: TextureLoader._nextTextureId++,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
format: 'rgba',
|
||||
hasMipmaps: false,
|
||||
data: image,
|
||||
// Include sprite settings if available
|
||||
// 如果有则包含 sprite 设置
|
||||
sliceBorder: spriteSettings?.sliceBorder,
|
||||
pivot: spriteSettings?.pivot
|
||||
data: image
|
||||
};
|
||||
|
||||
// Upload to GPU if bridge exists.
|
||||
@@ -52,8 +52,6 @@ export const AssetType = {
|
||||
Texture: 'texture',
|
||||
/** 网格 */
|
||||
Mesh: 'mesh',
|
||||
/** 3D模型 (GLTF/GLB) | 3D Model */
|
||||
Model3D: 'model3d',
|
||||
/** 材质 */
|
||||
Material: 'material',
|
||||
/** 着色器 */
|
||||
@@ -408,12 +406,6 @@ export interface IAssetCatalogEntry {
|
||||
|
||||
/** 可用变体 / Available variants (platform/quality specific) */
|
||||
variants?: IAssetVariant[];
|
||||
|
||||
/**
|
||||
* Import settings (e.g., sprite slicing for nine-patch)
|
||||
* 导入设置(如九宫格切片信息)
|
||||
*/
|
||||
importSettings?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
import { runtimeOnlyPreset } from '../../tools/build-config/src/presets/plugin-tsup';
|
||||
import { runtimeOnlyPreset } from '../build-config/src/presets/plugin-tsup';
|
||||
|
||||
export default defineConfig({
|
||||
...runtimeOnlyPreset(),
|
||||
@@ -1,43 +1,46 @@
|
||||
{
|
||||
"name": "@esengine/network-protocols",
|
||||
"name": "@esengine/audio",
|
||||
"version": "1.0.0",
|
||||
"description": "Shared network protocols for client and server",
|
||||
"description": "ECS-based audio system",
|
||||
"esengine": {
|
||||
"plugin": true,
|
||||
"pluginExport": "AudioPlugin",
|
||||
"category": "audio",
|
||||
"isEnginePlugin": true
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.mjs",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.mjs",
|
||||
"require": "./dist/index.js"
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"proto": "tsrpc proto --input src/shared/protocols --output src/shared/protocols/serviceProto.ts",
|
||||
"build": "tsup",
|
||||
"build:watch": "tsup --watch",
|
||||
"type-check": "tsc --noEmit",
|
||||
"clean": "rimraf dist"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@esengine/ecs-framework": "workspace:*",
|
||||
"@esengine/engine-core": "workspace:*",
|
||||
"@esengine/build-config": "workspace:*",
|
||||
"rimraf": "^5.0.5",
|
||||
"tsrpc-cli": "^2.4.5",
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"keywords": [
|
||||
"ecs",
|
||||
"network",
|
||||
"protocols",
|
||||
"tsrpc"
|
||||
"audio",
|
||||
"sound",
|
||||
"music"
|
||||
],
|
||||
"author": "yhh",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tsrpc-proto": "^1.4.3"
|
||||
}
|
||||
"license": "MIT"
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": false,
|
||||
"declaration": true,
|
||||
13
packages/audio/tsconfig.json
Normal file
13
packages/audio/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"],
|
||||
"references": [
|
||||
{ "path": "../core" }
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
import { runtimeOnlyPreset } from '../../tools/build-config/src/presets/plugin-tsup';
|
||||
import { runtimeOnlyPreset } from '../build-config/src/presets/plugin-tsup';
|
||||
|
||||
export default defineConfig({
|
||||
...runtimeOnlyPreset(),
|
||||
49
packages/behavior-tree-editor/package.json
Normal file
49
packages/behavior-tree-editor/package.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "@esengine/behavior-tree-editor",
|
||||
"version": "1.0.0",
|
||||
"description": "Editor support for @esengine/behavior-tree - visual editor, inspectors, and tools",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"build:watch": "tsup --watch",
|
||||
"type-check": "tsc --noEmit",
|
||||
"clean": "rimraf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@esengine/behavior-tree": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@esengine/ecs-framework": "workspace:*",
|
||||
"@esengine/engine-core": "workspace:*",
|
||||
"@esengine/editor-core": "workspace:*",
|
||||
"@esengine/editor-runtime": "workspace:*",
|
||||
"@esengine/node-editor": "workspace:*",
|
||||
"@esengine/build-config": "workspace:*",
|
||||
"lucide-react": "^0.545.0",
|
||||
"react": "^18.3.1",
|
||||
"zustand": "^5.0.8",
|
||||
"@types/react": "^18.3.12",
|
||||
"rimraf": "^5.0.5",
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"keywords": [
|
||||
"ecs",
|
||||
"behavior-tree",
|
||||
"editor"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT"
|
||||
}
|
||||
@@ -24,64 +24,64 @@ export interface GlobalBlackboardVariable {
|
||||
* 管理跨行为树共享的全局变量
|
||||
*/
|
||||
export class GlobalBlackboardService {
|
||||
private static _instance: GlobalBlackboardService;
|
||||
private _variables: Map<string, GlobalBlackboardVariable> = new Map();
|
||||
private _changeCallbacks: Array<() => void> = [];
|
||||
private _projectPath: string | null = null;
|
||||
private static instance: GlobalBlackboardService;
|
||||
private variables: Map<string, GlobalBlackboardVariable> = new Map();
|
||||
private changeCallbacks: Array<() => void> = [];
|
||||
private projectPath: string | null = null;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
static getInstance(): GlobalBlackboardService {
|
||||
if (!this._instance) {
|
||||
this._instance = new GlobalBlackboardService();
|
||||
if (!this.instance) {
|
||||
this.instance = new GlobalBlackboardService();
|
||||
}
|
||||
return this._instance;
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置项目路径
|
||||
*/
|
||||
setProjectPath(path: string | null): void {
|
||||
this._projectPath = path;
|
||||
this.projectPath = path;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目路径
|
||||
*/
|
||||
getProjectPath(): string | null {
|
||||
return this._projectPath;
|
||||
return this.projectPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加全局变量
|
||||
*/
|
||||
addVariable(variable: GlobalBlackboardVariable): void {
|
||||
if (this._variables.has(variable.key)) {
|
||||
if (this.variables.has(variable.key)) {
|
||||
throw new Error(`全局变量 "${variable.key}" 已存在`);
|
||||
}
|
||||
this._variables.set(variable.key, variable);
|
||||
this._notifyChange();
|
||||
this.variables.set(variable.key, variable);
|
||||
this.notifyChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新全局变量
|
||||
*/
|
||||
updateVariable(key: string, updates: Partial<Omit<GlobalBlackboardVariable, 'key'>>): void {
|
||||
const variable = this._variables.get(key);
|
||||
const variable = this.variables.get(key);
|
||||
if (!variable) {
|
||||
throw new Error(`全局变量 "${key}" 不存在`);
|
||||
}
|
||||
this._variables.set(key, { ...variable, ...updates });
|
||||
this._notifyChange();
|
||||
this.variables.set(key, { ...variable, ...updates });
|
||||
this.notifyChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除全局变量
|
||||
*/
|
||||
deleteVariable(key: string): boolean {
|
||||
const result = this._variables.delete(key);
|
||||
const result = this.variables.delete(key);
|
||||
if (result) {
|
||||
this._notifyChange();
|
||||
this.notifyChange();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -90,36 +90,36 @@ export class GlobalBlackboardService {
|
||||
* 重命名全局变量
|
||||
*/
|
||||
renameVariable(oldKey: string, newKey: string): void {
|
||||
if (!this._variables.has(oldKey)) {
|
||||
if (!this.variables.has(oldKey)) {
|
||||
throw new Error(`全局变量 "${oldKey}" 不存在`);
|
||||
}
|
||||
if (this._variables.has(newKey)) {
|
||||
if (this.variables.has(newKey)) {
|
||||
throw new Error(`全局变量 "${newKey}" 已存在`);
|
||||
}
|
||||
|
||||
const variable = this._variables.get(oldKey)!;
|
||||
this._variables.delete(oldKey);
|
||||
this._variables.set(newKey, { ...variable, key: newKey });
|
||||
this._notifyChange();
|
||||
const variable = this.variables.get(oldKey)!;
|
||||
this.variables.delete(oldKey);
|
||||
this.variables.set(newKey, { ...variable, key: newKey });
|
||||
this.notifyChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取全局变量
|
||||
*/
|
||||
getVariable(key: string): GlobalBlackboardVariable | undefined {
|
||||
return this._variables.get(key);
|
||||
return this.variables.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有全局变量
|
||||
*/
|
||||
getAllVariables(): GlobalBlackboardVariable[] {
|
||||
return Array.from(this._variables.values());
|
||||
return Array.from(this.variables.values());
|
||||
}
|
||||
|
||||
getVariablesMap(): Record<string, GlobalBlackboardValue> {
|
||||
const map: Record<string, GlobalBlackboardValue> = {};
|
||||
for (const [, variable] of this._variables) {
|
||||
for (const [, variable] of this.variables) {
|
||||
map[variable.key] = variable.defaultValue;
|
||||
}
|
||||
return map;
|
||||
@@ -129,15 +129,15 @@ export class GlobalBlackboardService {
|
||||
* 检查变量是否存在
|
||||
*/
|
||||
hasVariable(key: string): boolean {
|
||||
return this._variables.has(key);
|
||||
return this.variables.has(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有变量
|
||||
*/
|
||||
clear(): void {
|
||||
this._variables.clear();
|
||||
this._notifyChange();
|
||||
this.variables.clear();
|
||||
this.notifyChange();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -146,7 +146,7 @@ export class GlobalBlackboardService {
|
||||
toConfig(): GlobalBlackboardConfig {
|
||||
const variables: BlackboardVariable[] = [];
|
||||
|
||||
for (const variable of this._variables.values()) {
|
||||
for (const variable of this.variables.values()) {
|
||||
variables.push({
|
||||
name: variable.key,
|
||||
type: variable.type,
|
||||
@@ -162,11 +162,11 @@ export class GlobalBlackboardService {
|
||||
* 从配置导入
|
||||
*/
|
||||
fromConfig(config: GlobalBlackboardConfig): void {
|
||||
this._variables.clear();
|
||||
this.variables.clear();
|
||||
|
||||
if (config.variables && Array.isArray(config.variables)) {
|
||||
for (const variable of config.variables) {
|
||||
this._variables.set(variable.name, {
|
||||
this.variables.set(variable.name, {
|
||||
key: variable.name,
|
||||
type: variable.type,
|
||||
defaultValue: variable.value as GlobalBlackboardValue,
|
||||
@@ -175,7 +175,7 @@ export class GlobalBlackboardService {
|
||||
}
|
||||
}
|
||||
|
||||
this._notifyChange();
|
||||
this.notifyChange();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -202,17 +202,17 @@ export class GlobalBlackboardService {
|
||||
* 监听变化
|
||||
*/
|
||||
onChange(callback: () => void): () => void {
|
||||
this._changeCallbacks.push(callback);
|
||||
this.changeCallbacks.push(callback);
|
||||
return () => {
|
||||
const index = this._changeCallbacks.indexOf(callback);
|
||||
const index = this.changeCallbacks.indexOf(callback);
|
||||
if (index > -1) {
|
||||
this._changeCallbacks.splice(index, 1);
|
||||
this.changeCallbacks.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private _notifyChange(): void {
|
||||
this._changeCallbacks.forEach((cb) => {
|
||||
private notifyChange(): void {
|
||||
this.changeCallbacks.forEach((cb) => {
|
||||
try {
|
||||
cb();
|
||||
} catch (error) {
|
||||
@@ -536,15 +536,15 @@ export const useBehaviorTreeDataStore = create<BehaviorTreeDataState>((set, get)
|
||||
* 将 Zustand Store 适配为 ITreeState 接口
|
||||
*/
|
||||
export class TreeStateAdapter implements ITreeState {
|
||||
private static _instance: TreeStateAdapter | null = null;
|
||||
private static instance: TreeStateAdapter | null = null;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
static getInstance(): TreeStateAdapter {
|
||||
if (!TreeStateAdapter._instance) {
|
||||
TreeStateAdapter._instance = new TreeStateAdapter();
|
||||
if (!TreeStateAdapter.instance) {
|
||||
TreeStateAdapter.instance = new TreeStateAdapter();
|
||||
}
|
||||
return TreeStateAdapter._instance;
|
||||
return TreeStateAdapter.instance;
|
||||
}
|
||||
|
||||
getTree(): BehaviorTree {
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user