Compare commits

..

5 Commits

Author SHA1 Message Date
yhh
566e1977fd refactor: 改用 Imgur 图床上传图片 2025-12-04 09:56:10 +08:00
yhh
17f6259f43 chore: 删除测试图片 2025-12-04 09:54:46 +08:00
yhh
5d3483fc65 chore: 更新 pnpm-lock.yaml 2025-12-04 09:47:12 +08:00
yhh
d07a5d81fc Merge branch 'master' into feat/github-forum 2025-12-04 09:46:22 +08:00
yhh
6a4e6fbc04 feat(editor): 添加 GitHub Discussions 社区论坛功能 2025-12-04 09:45:16 +08:00
883 changed files with 20557 additions and 125999 deletions

2
.github/FUNDING.yml vendored
View File

@@ -9,4 +9,4 @@ community_bridge: # Replace with a single Community Bridge project-name e.g., cl
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: ['https://github.com/esengine/esengine/blob/master/sponsor/alipay.jpg', 'https://github.com/esengine/esengine/blob/master/sponsor/wechatpay.png'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
custom: ['https://github.com/esengine/ecs-framework/blob/master/sponsor/alipay.jpg', 'https://github.com/esengine/ecs-framework/blob/master/sponsor/wechatpay.png'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View File

@@ -5,7 +5,7 @@ contact_links:
about: 查看完整文档和教程 / View full documentation and tutorials
- name: 🤖 AI 文档助手 / AI Documentation Assistant
url: https://deepwiki.com/esengine/esengine
url: https://deepwiki.com/esengine/ecs-framework
about: 使用 AI 助手快速找到答案 / Use AI assistant to quickly find answers
- name: 💬 QQ 交流群 / QQ Group
@@ -13,5 +13,5 @@ contact_links:
about: 加入社区交流群 / Join the community group
- name: 🌟 GitHub Discussions
url: https://github.com/esengine/esengine/discussions
url: https://github.com/esengine/ecs-framework/discussions
about: 参与社区讨论 / Join community discussions

View File

@@ -8,12 +8,12 @@ body:
value: |
💡 提示:如果是简单问题,可以先查看:
- [📚 文档](https://esengine.github.io/ecs-framework/)
- [📖 AI 文档助手](https://deepwiki.com/esengine/esengine)
- [📖 AI 文档助手](https://deepwiki.com/esengine/ecs-framework)
- [💬 QQ 交流群](https://jq.qq.com/?_wv=1027&k=29w1Nud6)
💡 Tip: For simple questions, please check first:
- [📚 Documentation](https://esengine.github.io/ecs-framework/)
- [📖 AI Documentation](https://deepwiki.com/esengine/esengine)
- [📖 AI Documentation](https://deepwiki.com/esengine/ecs-framework)
- type: textarea
id: question

View File

@@ -31,7 +31,9 @@ jobs:
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v2
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4

View File

@@ -15,7 +15,9 @@ jobs:
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v2
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4

View File

@@ -18,7 +18,9 @@ jobs:
fetch-depth: 0
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v2
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4

View File

@@ -30,7 +30,9 @@ jobs:
fetch-depth: 0
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v2
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4

View File

@@ -34,7 +34,9 @@ jobs:
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v2
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
@@ -92,7 +94,6 @@ jobs:
node scripts/bundle-runtime.mjs
- name: Build Tauri app
id: tauri
uses: tauri-apps/tauri-action@v0.5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -103,111 +104,16 @@ jobs:
tagName: ${{ github.event_name == 'workflow_dispatch' && format('editor-v{0}', github.event.inputs.version) || github.ref_name }}
releaseName: 'ECS Editor v${{ github.event.inputs.version || github.ref_name }}'
releaseBody: 'See the assets to download this version and install.'
releaseDraft: true
releaseDraft: false
prerelease: false
includeUpdaterJson: true
updaterJsonKeepUniversal: false
args: ${{ matrix.platform == 'macos-latest' && format('--target {0}', matrix.target) || '' }}
# Windows 构建上传 artifact 供 SignPath 签名
- name: Upload Windows artifacts for signing
if: matrix.platform == 'windows-latest'
uses: actions/upload-artifact@v4
with:
name: windows-unsigned
path: |
packages/editor-app/src-tauri/target/release/bundle/nsis/*.exe
packages/editor-app/src-tauri/target/release/bundle/msi/*.msi
retention-days: 1
# SignPath 代码签名Windows
# SignPath OSS code signing for Windows
#
# 配置步骤 | Setup Steps:
# 1. 在 SignPath 门户创建项目 | Create project in SignPath portal
# 2. 导入 .signpath/artifact-configuration.xml | Import artifact configuration
# 3. 使用 'test-signing' 策略测试 | Use 'test-signing' policy for testing
# 生产环境改为 'release-signing' | Change to 'release-signing' for production
# 4. 配置 GitHub Secrets | Configure GitHub Secrets:
# - SIGNPATH_API_TOKEN: API token from SignPath
# - SIGNPATH_ORGANIZATION_ID: Your organization ID
#
# 文档 | Documentation: https://about.signpath.io/documentation/trusted-build-systems/github
sign-windows:
needs: build-tauri
runs-on: ubuntu-latest
# 只有在构建成功时才运行 | Only run on successful build
if: success()
steps:
- name: Check SignPath configuration
id: check-signpath
run: |
if [ -n "${{ secrets.SIGNPATH_API_TOKEN }}" ] && [ -n "${{ secrets.SIGNPATH_ORGANIZATION_ID }}" ]; then
echo "enabled=true" >> $GITHUB_OUTPUT
echo "SignPath is configured, proceeding with code signing"
else
echo "enabled=false" >> $GITHUB_OUTPUT
echo "SignPath secrets not configured, skipping code signing"
echo "To enable: add SIGNPATH_API_TOKEN and SIGNPATH_ORGANIZATION_ID secrets"
fi
- name: Checkout
if: steps.check-signpath.outputs.enabled == 'true'
uses: actions/checkout@v4
- name: Get artifact ID
if: steps.check-signpath.outputs.enabled == 'true'
id: get-artifact
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# 获取 windows-unsigned artifact 的 ID
ARTIFACT_ID=$(gh api \
-H "Accept: application/vnd.github+json" \
"/repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts" \
--jq '.artifacts[] | select(.name == "windows-unsigned") | .id')
if [ -z "$ARTIFACT_ID" ]; then
echo "Error: Could not find artifact 'windows-unsigned'"
exit 1
fi
echo "artifact-id=$ARTIFACT_ID" >> $GITHUB_OUTPUT
echo "Found artifact ID: $ARTIFACT_ID"
- name: Submit to SignPath for code signing
if: steps.check-signpath.outputs.enabled == 'true'
id: signpath
uses: signpath/github-action-submit-signing-request@v1
with:
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }}
project-slug: 'ecs-framework'
signing-policy-slug: 'test-signing'
artifact-configuration-slug: 'initial'
github-artifact-id: ${{ steps.get-artifact.outputs.artifact-id }}
wait-for-completion: true
wait-for-completion-timeout-in-seconds: 600
output-artifact-directory: './signed'
- name: Upload signed artifacts to release
if: steps.check-signpath.outputs.enabled == 'true'
uses: softprops/action-gh-release@v1
with:
files: ./signed/*
tag_name: ${{ github.event_name == 'workflow_dispatch' && format('editor-v{0}', github.event.inputs.version) || github.ref_name }}
# 保持 Draft 状态,需要手动发布 | Keep as draft, require manual publish
draft: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# 构建成功后,创建 PR 更新版本号
# Create PR to update version after successful build
update-version-pr:
needs: [build-tauri, sign-windows]
# 即使签名跳过也要运行 | Run even if signing is skipped
if: github.event_name == 'workflow_dispatch' && !failure()
needs: build-tauri
if: github.event_name == 'workflow_dispatch' && success()
runs-on: ubuntu-latest
steps:

View File

@@ -1,26 +1,10 @@
name: Release NPM Packages
on:
# 标签触发:支持 v* 和 {package}-v* 格式
# Tag trigger: supports v* and {package}-v* formats
push:
tags:
- 'v*'
- 'core-v*'
- 'behavior-tree-v*'
- 'editor-core-v*'
- 'node-editor-v*'
- 'blueprint-v*'
- 'tilemap-v*'
- 'physics-rapier2d-v*'
- 'worker-generator-v*'
# 保留手动触发选项
# Keep manual trigger option
workflow_dispatch:
inputs:
package:
description: '选择要发布的包 | Select package to publish'
description: '选择要发布的包'
required: true
type: choice
options:
@@ -31,15 +15,19 @@ on:
- blueprint
- tilemap
- physics-rapier2d
- worker-generator
version_type:
description: '版本更新类型 | Version bump type'
description: '版本更新类型'
required: true
type: choice
options:
- patch
- minor
- major
- custom
custom_version:
description: '自定义版本号 (仅当选择 custom 时使用,例如: 2.2.9)'
required: false
type: string
permissions:
contents: write
@@ -48,7 +36,7 @@ permissions:
jobs:
release-package:
name: Release Package
name: Release ${{ github.event.inputs.package }} Package
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -57,44 +45,10 @@ jobs:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Parse tag or input
id: parse
run: |
if [ "${{ github.event_name }}" = "push" ]; then
# 从标签解析包名和版本 | Parse package and version from tag
TAG="${GITHUB_REF#refs/tags/}"
echo "tag=$TAG" >> $GITHUB_OUTPUT
# 解析格式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"
VERSION="${BASH_REMATCH[1]}"
elif [[ "$TAG" =~ ^([a-z-]+)-v([0-9]+\.[0-9]+\.[0-9]+.*)$ ]]; then
PACKAGE="${BASH_REMATCH[1]}"
VERSION="${BASH_REMATCH[2]}"
else
echo "::error::Invalid tag format: $TAG"
echo "Expected: v1.0.0 or package-v1.0.0"
exit 1
fi
echo "package=$PACKAGE" >> $GITHUB_OUTPUT
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "mode=tag" >> $GITHUB_OUTPUT
echo "📦 Package: $PACKAGE"
echo "📌 Version: $VERSION"
else
# 手动触发:从 package.json 读取并 bump 版本
# Manual trigger: read from package.json and bump version
PACKAGE="${{ github.event.inputs.package }}"
echo "package=$PACKAGE" >> $GITHUB_OUTPUT
echo "mode=manual" >> $GITHUB_OUTPUT
echo "version_type=${{ github.event.inputs.version_type }}" >> $GITHUB_OUTPUT
fi
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v2
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
@@ -106,124 +60,76 @@ jobs:
- name: Install dependencies
run: pnpm install
- name: Verify version (tag mode)
if: steps.parse.outputs.mode == 'tag'
run: |
PACKAGE="${{ steps.parse.outputs.package }}"
EXPECTED_VERSION="${{ steps.parse.outputs.version }}"
# 获取 package.json 中的版本
# Get version from package.json
ACTUAL_VERSION=$(node -p "require('./packages/$PACKAGE/package.json').version")
if [ "$EXPECTED_VERSION" != "$ACTUAL_VERSION" ]; then
echo "::error::Version mismatch!"
echo "Tag version: $EXPECTED_VERSION"
echo "package.json version: $ACTUAL_VERSION"
echo ""
echo "Please update packages/$PACKAGE/package.json to version $EXPECTED_VERSION before tagging."
exit 1
fi
echo "✅ Version verified: $EXPECTED_VERSION"
- name: Bump version (manual mode)
if: steps.parse.outputs.mode == 'manual'
id: bump
run: |
PACKAGE="${{ steps.parse.outputs.package }}"
cd packages/$PACKAGE
CURRENT=$(node -p "require('./package.json').version")
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT"
case "${{ steps.parse.outputs.version_type }}" in
major) NEW_VERSION="$((MAJOR+1)).0.0" ;;
minor) NEW_VERSION="$MAJOR.$((MINOR+1)).0" ;;
patch) NEW_VERSION="$MAJOR.$MINOR.$((PATCH+1))" ;;
esac
# Update package.json
node -e "const fs=require('fs'); const pkg=JSON.parse(fs.readFileSync('package.json')); pkg.version='$NEW_VERSION'; fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2)+'\n')"
echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT
echo "📌 Bumped version: $CURRENT → $NEW_VERSION"
- name: Set final version
id: version
run: |
if [ "${{ steps.parse.outputs.mode }}" = "tag" ]; then
echo "value=${{ steps.parse.outputs.version }}" >> $GITHUB_OUTPUT
else
echo "value=${{ steps.bump.outputs.version }}" >> $GITHUB_OUTPUT
fi
- name: Build core package (if needed)
if: ${{ steps.parse.outputs.package != 'core' && steps.parse.outputs.package != 'node-editor' && steps.parse.outputs.package != 'worker-generator' }}
if: ${{ github.event.inputs.package != 'core' && github.event.inputs.package != 'node-editor' }}
run: |
cd packages/core
pnpm run build
- name: Build node-editor package (if needed for blueprint)
if: ${{ steps.parse.outputs.package == 'blueprint' }}
if: ${{ github.event.inputs.package == 'blueprint' }}
run: |
cd packages/node-editor
pnpm run build
# - name: Run tests
# run: |
# cd packages/${{ github.event.inputs.package }}
# npm run test:ci
- name: Update version
id: version
run: |
cd packages/${{ github.event.inputs.package }}
if [ "${{ github.event.inputs.version_type }}" = "custom" ]; then
NEW_VERSION=${{ github.event.inputs.custom_version }}
else
# Get current version and bump it
CURRENT=$(node -p "require('./package.json').version")
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT"
case "${{ github.event.inputs.version_type }}" in
major) NEW_VERSION="$((MAJOR+1)).0.0" ;;
minor) NEW_VERSION="$MAJOR.$((MINOR+1)).0" ;;
patch) NEW_VERSION="$MAJOR.$MINOR.$((PATCH+1))" ;;
esac
fi
# Update package.json using node
node -e "const fs=require('fs'); const pkg=JSON.parse(fs.readFileSync('package.json')); pkg.version='$NEW_VERSION'; fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2)+'\n')"
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
echo "发布版本: $NEW_VERSION"
- name: Build package
run: |
cd packages/${{ steps.parse.outputs.package }}
cd packages/${{ github.event.inputs.package }}
pnpm run build:npm
- name: Publish to npm
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
cd packages/${{ steps.parse.outputs.package }}/dist
cd packages/${{ github.event.inputs.package }}/dist
pnpm publish --access public --no-git-checks
- name: Create GitHub Release (tag mode)
if: steps.parse.outputs.mode == 'tag'
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ steps.parse.outputs.tag }}
name: "${{ steps.parse.outputs.package }} v${{ steps.version.outputs.value }}"
body: |
## 🚀 @esengine/${{ steps.parse.outputs.package }} v${{ steps.version.outputs.value }}
📦 **NPM**: [@esengine/${{ steps.parse.outputs.package }}@${{ steps.version.outputs.value }}](https://www.npmjs.com/package/@esengine/${{ steps.parse.outputs.package }}/v/${{ steps.version.outputs.value }})
```bash
npm install @esengine/${{ steps.parse.outputs.package }}@${{ steps.version.outputs.value }}
```
---
*自动发布 | Auto-released by GitHub Actions*
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create Pull Request (manual mode)
if: steps.parse.outputs.mode == 'manual'
- name: Create Pull Request
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "chore(${{ steps.parse.outputs.package }}): release v${{ steps.version.outputs.value }}"
branch: release/${{ steps.parse.outputs.package }}-v${{ steps.version.outputs.value }}
commit-message: "chore(${{ github.event.inputs.package }}): release v${{ steps.version.outputs.new_version }}"
branch: release/${{ github.event.inputs.package }}-v${{ steps.version.outputs.new_version }}
delete-branch: true
title: "chore(${{ steps.parse.outputs.package }}): Release v${{ steps.version.outputs.value }}"
title: "chore(${{ github.event.inputs.package }}): Release v${{ steps.version.outputs.new_version }}"
body: |
## 🚀 Release v${{ steps.version.outputs.value }}
## 🚀 Release v${{ steps.version.outputs.new_version }}
此 PR 更新 `@esengine/${{ steps.parse.outputs.package }}` 包的版本号
此 PR 更新 `@esengine/${{ github.event.inputs.package }}` 包的版本号
### 变更
- ✅ 已发布到 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 }}`
- ✅ 已发布到 npm: [@esengine/${{ github.event.inputs.package }}@${{ steps.version.outputs.new_version }}](https://www.npmjs.com/package/@esengine/${{ github.event.inputs.package }}/v/${{ steps.version.outputs.new_version }})
- ✅ 更新 `packages/${{ github.event.inputs.package }}/package.json` → `${{ steps.version.outputs.new_version }}`
---
*此 PR 由发布工作流自动创建*
labels: |
release
${{ steps.parse.outputs.package }}
${{ github.event.inputs.package }}
automated pr

View File

@@ -23,7 +23,9 @@ jobs:
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v2
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4

View File

@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Welcome new contributors
uses: actions/first-interaction@v1.3.0
uses: actions/first-interaction@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
issue-message: |
@@ -23,7 +23,7 @@ jobs:
我们会尽快查看并回复。同时,建议你:
- 📚 查看[文档](https://esengine.github.io/ecs-framework/)
- 🤖 使用 [AI 文档助手](https://deepwiki.com/esengine/esengine)
- 🤖 使用 [AI 文档助手](https://deepwiki.com/esengine/ecs-framework)
- 💬 加入 [QQ 交流群](https://jq.qq.com/?_wv=1027&k=29w1Nud6)
---
@@ -32,7 +32,7 @@ jobs:
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)
- 🤖 Use [AI documentation assistant](https://deepwiki.com/esengine/ecs-framework)
pr-message: |
👋 你好!感谢你提交第一个 Pull Request
@@ -43,7 +43,7 @@ jobs:
- ✅ 更新了相关文档
- ✅ Commit 遵循 [Conventional Commits](https://www.conventionalcommits.org/) 规范
查看完整的[贡献指南](https://github.com/esengine/esengine/blob/master/CONTRIBUTING.md)。
查看完整的[贡献指南](https://github.com/esengine/ecs-framework/blob/master/CONTRIBUTING.md)。
---
@@ -55,4 +55,4 @@ jobs:
- ✅ 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).
See the full [Contributing Guide](https://github.com/esengine/ecs-framework/blob/master/CONTRIBUTING.md).

12
.gitignore vendored
View File

@@ -48,14 +48,6 @@ logs/
.env.test.local
.env.production.local
# 代码签名证书(敏感文件)
certs/
*.pfx
*.p12
*.cer
*.pem
*.key
# 测试覆盖率
coverage/
*.lcov
@@ -90,7 +82,3 @@ docs/.vitepress/dist/
# Tauri 捆绑输出
**/src-tauri/target/release/bundle/
**/src-tauri/target/debug/bundle/
# Rust 构建产物
**/engine-shared/target/
external/

View File

@@ -119,9 +119,9 @@ npm run format
## 问题反馈 / Issue Reporting
如果你发现了 bug 或有新功能建议,请[创建 Issue](https://github.com/esengine/esengine/issues/new)。
如果你发现了 bug 或有新功能建议,请[创建 Issue](https://github.com/esengine/ecs-framework/issues/new)。
If you find a bug or have a feature request, please [create an issue](https://github.com/esengine/esengine/issues/new).
If you find a bug or have a feature request, please [create an issue](https://github.com/esengine/ecs-framework/issues/new).
## 许可证 / License

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2025 ESEngine Contributors
Copyright (c) 2025 ECS Framework
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

174
README.md
View File

@@ -1,75 +1,40 @@
<h1 align="center">
<img src="https://raw.githubusercontent.com/esengine/esengine/master/docs/public/logo.svg" alt="ESEngine" width="180">
<br>
ESEngine
</h1>
# ESEngine
<p align="center">
<strong>Cross-platform 2D Game Engine</strong>
</p>
**English** | [中文](./README_CN.md)
<p align="center">
<a href="https://www.npmjs.com/package/@esengine/ecs-framework"><img src="https://img.shields.io/npm/v/@esengine/ecs-framework?style=flat-square&color=blue" alt="npm"></a>
<a href="https://github.com/esengine/esengine/actions"><img src="https://img.shields.io/github/actions/workflow/status/esengine/esengine/ci.yml?branch=master&style=flat-square" alt="build"></a>
<a href="https://github.com/esengine/esengine/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="license"></a>
<a href="https://github.com/esengine/esengine/stargazers"><img src="https://img.shields.io/github/stars/esengine/esengine?style=flat-square" alt="stars"></a>
<img src="https://img.shields.io/badge/TypeScript-5.0+-blue?style=flat-square&logo=typescript&logoColor=white" alt="TypeScript">
</p>
**[Documentation](https://esengine.github.io/ecs-framework/) | [API Reference](https://esengine.github.io/ecs-framework/api/) | [Examples](./examples/)**
<p align="center">
<b>English</b> | <a href="./README_CN.md">中文</a>
</p>
ESEngine is a cross-platform 2D game engine for creating games from a unified interface. It provides a comprehensive set of common tools so that developers can focus on making games without having to reinvent the wheel.
<p align="center">
<a href="https://esengine.cn/">Documentation</a> ·
<a href="https://esengine.cn/api/README">API Reference</a> ·
<a href="https://github.com/esengine/esengine/releases">Download Editor</a> ·
<a href="./examples/">Examples</a>
</p>
Games can be exported to multiple platforms including Web browsers, WeChat Mini Games, and other mini-game platforms.
---
## Free and Open Source
## Overview
ESEngine is completely free and open source under the MIT license. No strings attached, no royalties. Your games are yours.
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.
## Features
Export your games to multiple platforms including web browsers, WeChat Mini Games, and other mini-game platforms from a single codebase.
- **Data-Driven Architecture**: Built on Entity-Component-System (ECS) pattern for flexible and performant game logic
- **High-Performance Rendering**: Rust/WebAssembly 2D renderer with sprite batching and WebGL 2.0 backend
- **Visual Editor**: Cross-platform desktop editor with scene management, asset browser, and visual tools
- **Modular Design**: Use only what you need. Each feature is a separate module that can be included independently
- **Multi-Platform**: Deploy to Web, WeChat Mini Games, and more from a single codebase
## Key Features
## Getting the Engine
| 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
### Using npm
```bash
npm install @esengine/ecs-framework
```
### Editor
### Building from Source
Download pre-built binaries from the [Releases](https://github.com/esengine/esengine/releases) page (Windows, macOS).
See [Building from Source](#building-from-source) for detailed instructions.
### Editor Download
Pre-built editor binaries are available on the [Releases](https://github.com/esengine/ecs-framework/releases) page for Windows and macOS.
## Quick Start
@@ -107,7 +72,6 @@ class MovementSystem extends EntitySystem {
}
}
// Initialize
Core.create();
const scene = new Scene();
scene.addSystem(new MovementSystem());
@@ -119,16 +83,20 @@ player.addComponent(new Velocity());
Core.setScene(scene);
// Game loop
let lastTime = 0;
function gameLoop(currentTime: number) {
Core.update(currentTime / 1000);
const deltaTime = (currentTime - lastTime) / 1000;
lastTime = currentTime;
Core.update(deltaTime);
requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);
```
## Packages
## Modules
ESEngine is organized as a monorepo with modular packages.
ESEngine is organized into modular packages. Each feature has a runtime module and an optional editor extension.
### Core
@@ -139,13 +107,13 @@ ESEngine is organized as a monorepo with modular packages.
| `@esengine/engine` | Rust/WASM 2D renderer |
| `@esengine/engine-core` | Engine module system and lifecycle management |
### Runtime
### Runtime Modules
| Package | Description |
|---------|-------------|
| `@esengine/sprite` | 2D sprite rendering and animation |
| `@esengine/tilemap` | Tile-based map rendering |
| `@esengine/physics-rapier2d` | 2D physics simulation (Rapier) |
| `@esengine/tilemap` | Tile-based map rendering with animation support |
| `@esengine/physics-rapier2d` | 2D physics simulation powered by Rapier |
| `@esengine/behavior-tree` | Behavior tree AI system |
| `@esengine/blueprint` | Visual scripting runtime |
| `@esengine/camera` | Camera control and management |
@@ -159,11 +127,12 @@ ESEngine is organized as a monorepo with modular packages.
| Package | Description |
|---------|-------------|
| `@esengine/sprite-editor` | Sprite inspector and tools |
| `@esengine/tilemap-editor` | Visual tilemap editor |
| `@esengine/physics-rapier2d-editor` | Physics collider visualization |
| `@esengine/tilemap-editor` | Visual tilemap editor with brush tools |
| `@esengine/physics-rapier2d-editor` | Physics collider visualization and editing |
| `@esengine/behavior-tree-editor` | Visual behavior tree editor |
| `@esengine/blueprint-editor` | Visual scripting editor |
| `@esengine/material-editor` | Material editor |
| `@esengine/material-editor` | Material and shader editor |
| `@esengine/shader-editor` | Shader code editor |
### Platform
@@ -175,59 +144,65 @@ ESEngine is organized as a monorepo with modular packages.
## Editor
The ESEngine Editor is a cross-platform desktop application built with Tauri and React.
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
- Component inspector with custom editors
- Asset browser with drag-and-drop support
- Tilemap editor with paint, fill, and selection tools
- Behavior tree visual editor
- Blueprint visual scripting
- Material and shader editing
- Built-in performance profiler
- Localization (English, Chinese)
- Localization support (English, Chinese)
### Screenshot
![ESEngine Editor](screenshots/main_screetshot.png)
## Platform Support
## Supported Platforms
| Platform | Runtime | Editor |
|----------|:-------:|:------:|
| Web Browser | | - |
| Windows | - | |
| macOS | - | |
|----------|---------|--------|
| Web Browser | Yes | - |
| Windows | - | Yes |
| macOS | - | Yes |
| WeChat Mini Game | In Progress | - |
| Playable Ads | Planned | - |
| Android | Planned | - |
| iOS | Planned | - |
| Windows Native | Planned | - |
| Other Platforms | Planned | - |
## Building from Source
### Prerequisites
- Node.js 18+
- pnpm 10+
- Node.js 18 or later
- pnpm 10 or later
- Rust toolchain (for WASM renderer)
- wasm-pack
### Setup
```bash
git clone https://github.com/esengine/esengine.git
cd esengine
# Clone repository
git clone https://github.com/esengine/ecs-framework.git
cd ecs-framework
# Install dependencies
pnpm install
# Build all packages
pnpm build
# Optional: Build WASM renderer
# Build WASM renderer (optional)
pnpm build:wasm
```
### Run Editor
### Running the Editor
```bash
cd packages/editor-app
@@ -237,41 +212,34 @@ 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
ecs-framework/
├── packages/ Engine packages (runtime, editor, platform)
├── docs/ Documentation source
├── examples/ Example projects
├── scripts/ Build utilities
└── thirdparty/ Third-party dependencies
```
## Documentation
- [Getting Started](https://esengine.cn/guide/getting-started.html)
- [Architecture Guide](https://esengine.cn/guide/)
- [API Reference](https://esengine.cn/api/README)
- [Getting Started](https://esengine.github.io/ecs-framework/guide/getting-started.html)
- [Architecture Guide](https://esengine.github.io/ecs-framework/guide/)
- [API Reference](https://esengine.github.io/ecs-framework/api/)
## Community
- [GitHub Issues](https://github.com/esengine/esengine/issues) - Bug reports and feature requests
- [GitHub Discussions](https://github.com/esengine/esengine/discussions) - Questions and ideas
- [GitHub Issues](https://github.com/esengine/ecs-framework/issues) - Bug reports and feature requests
- [GitHub Discussions](https://github.com/esengine/ecs-framework/discussions) - Questions and ideas
## Contributing
Contributions are welcome. Please read the contributing guidelines before submitting a pull request.
1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
2. Create a feature branch
3. Make changes with tests
4. Submit a pull request
## License
ESEngine is licensed under the [MIT License](LICENSE).
---
<p align="center">
Made with ❤️ by the ESEngine team
</p>

View File

@@ -1,75 +1,40 @@
<h1 align="center">
<img src="https://raw.githubusercontent.com/esengine/esengine/master/docs/public/logo.svg" alt="ESEngine" width="180">
<br>
ESEngine
</h1>
# ESEngine
<p align="center">
<strong>跨平台 2D 游戏引擎</strong>
</p>
[English](./README.md) | **中文**
<p align="center">
<a href="https://www.npmjs.com/package/@esengine/ecs-framework"><img src="https://img.shields.io/npm/v/@esengine/ecs-framework?style=flat-square&color=blue" alt="npm"></a>
<a href="https://github.com/esengine/esengine/actions"><img src="https://img.shields.io/github/actions/workflow/status/esengine/esengine/ci.yml?branch=master&style=flat-square" alt="build"></a>
<a href="https://github.com/esengine/esengine/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="license"></a>
<a href="https://github.com/esengine/esengine/stargazers"><img src="https://img.shields.io/github/stars/esengine/esengine?style=flat-square" alt="stars"></a>
<img src="https://img.shields.io/badge/TypeScript-5.0+-blue?style=flat-square&logo=typescript&logoColor=white" alt="TypeScript">
</p>
**[文档](https://esengine.github.io/ecs-framework/) | [API 参考](https://esengine.github.io/ecs-framework/api/) | [示例](./examples/)**
<p align="center">
<a href="./README.md">English</a> | <b>中文</b>
</p>
ESEngine 是一个跨平台 2D 游戏引擎,提供统一的开发界面。它包含完整的常用工具集,让开发者专注于游戏创作本身。
<p align="center">
<a href="https://esengine.cn/">文档</a> ·
<a href="https://esengine.cn/api/README">API 参考</a> ·
<a href="https://github.com/esengine/esengine/releases">下载编辑器</a> ·
<a href="./examples/">示例</a>
</p>
游戏可以导出到多个平台,包括 Web 浏览器、微信小游戏等小游戏平台。
---
## 免费开源
## 概述
ESEngine 基于 MIT 协议完全免费开源。无附加条件,无版税。你的游戏完全属于你。
ESEngine 是一款基于现代 Web 技术从零构建的跨平台 2D 游戏引擎。它提供完整的工具集,让开发者专注于游戏创作而非基础设施搭建。
## 特性
一套代码即可导出到 Web 浏览器、微信小游戏等多个平台。
- **数据驱动架构**:基于 ECS实体-组件-系统)模式构建,提供灵活高效的游戏逻辑
- **高性能渲染**Rust/WebAssembly 2D 渲染器,支持精灵批处理和 WebGL 2.0
- **可视化编辑器**:跨平台桌面编辑器,包含场景管理、资源浏览器和可视化工具
- **模块化设计**:按需使用,每个功能都是独立模块,可单独引入
- **多平台支持**:一套代码部署到 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
### 通过 npm 安装
```bash
npm install @esengine/ecs-framework
```
### 编辑器
### 从源码构建
[Releases](https://github.com/esengine/esengine/releases) 页面下载预编译版本(支持 Windows、macOS
详见 [从源码构建](#从源码构建) 章节
### 编辑器下载
预编译的编辑器可在 [Releases](https://github.com/esengine/ecs-framework/releases) 页面下载,支持 Windows 和 macOS。
## 快速开始
@@ -107,7 +72,6 @@ class MovementSystem extends EntitySystem {
}
}
// 初始化
Core.create();
const scene = new Scene();
scene.addSystem(new MovementSystem());
@@ -119,8 +83,12 @@ player.addComponent(new Velocity());
Core.setScene(scene);
// 游戏循环
let lastTime = 0;
function gameLoop(currentTime: number) {
Core.update(currentTime / 1000);
const deltaTime = (currentTime - lastTime) / 1000;
lastTime = currentTime;
Core.update(deltaTime);
requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);
@@ -128,7 +96,7 @@ requestAnimationFrame(gameLoop);
## 模块
ESEngine 采用 Monorepo 组织,包含多个模块化包
ESEngine 采用模块化组织。每个功能都有运行时模块和可选的编辑器扩展
### 核心
@@ -139,13 +107,13 @@ ESEngine 采用 Monorepo 组织,包含多个模块化包。
| `@esengine/engine` | Rust/WASM 2D 渲染器 |
| `@esengine/engine-core` | 引擎模块系统和生命周期管理 |
### 运行时
### 运行时模块
| 包名 | 描述 |
|------|------|
| `@esengine/sprite` | 2D 精灵渲染和动画 |
| `@esengine/tilemap` | Tilemap 渲染 |
| `@esengine/physics-rapier2d` | 2D 物理模拟 (Rapier) |
| `@esengine/tilemap` | Tilemap 渲染,支持动画 |
| `@esengine/physics-rapier2d` | 基于 Rapier 的 2D 物理模拟 |
| `@esengine/behavior-tree` | 行为树 AI 系统 |
| `@esengine/blueprint` | 可视化脚本运行时 |
| `@esengine/camera` | 相机控制和管理 |
@@ -159,11 +127,12 @@ ESEngine 采用 Monorepo 组织,包含多个模块化包。
| 包名 | 描述 |
|------|------|
| `@esengine/sprite-editor` | 精灵检视器和工具 |
| `@esengine/tilemap-editor` | 可视化 Tilemap 编辑器 |
| `@esengine/physics-rapier2d-editor` | 物理碰撞体可视化 |
| `@esengine/tilemap-editor` | 可视化 Tilemap 编辑器,支持笔刷工具 |
| `@esengine/physics-rapier2d-editor` | 物理碰撞体可视化和编辑 |
| `@esengine/behavior-tree-editor` | 可视化行为树编辑器 |
| `@esengine/blueprint-editor` | 可视化脚本编辑器 |
| `@esengine/material-editor` | 材质编辑器 |
| `@esengine/material-editor` | 材质和着色器编辑器 |
| `@esengine/shader-editor` | 着色器代码编辑器 |
### 平台
@@ -180,9 +149,9 @@ ESEngine 编辑器是基于 Tauri 和 React 构建的跨平台桌面应用。
### 功能
- 场景层级和实体管理
- 组件检视器,支持自定义属性编辑器
- 组件检视器,支持自定义编辑器
- 资源浏览器,支持拖放
- Tilemap 编辑器,支持绘制填充工具
- Tilemap 编辑器,支持绘制填充、选择工具
- 行为树可视化编辑器
- 蓝图可视化脚本
- 材质和着色器编辑
@@ -193,37 +162,43 @@ ESEngine 编辑器是基于 Tauri 和 React 构建的跨平台桌面应用。
![ESEngine Editor](screenshots/main_screetshot.png)
## 平台支持
## 支持的平台
| 平台 | 运行时 | 编辑器 |
|------|:------:|:------:|
| Web 浏览器 | | - |
| Windows | - | |
| macOS | - | |
|------|--------|--------|
| Web 浏览器 | 支持 | - |
| Windows | - | 支持 |
| macOS | - | 支持 |
| 微信小游戏 | 开发中 | - |
| Playable 可玩广告 | 计划中 | - |
| Android | 计划中 | - |
| iOS | 计划中 | - |
| Windows 原生 | 计划中 | - |
| 其他平台 | 计划中 | - |
## 从源码构建
### 前置要求
- Node.js 18+
- pnpm 10+
- Node.js 18 或更高版本
- pnpm 10 或更高版本
- Rust 工具链(用于 WASM 渲染器)
- wasm-pack
### 安装
```bash
git clone https://github.com/esengine/esengine.git
cd esengine
# 克隆仓库
git clone https://github.com/esengine/ecs-framework.git
cd ecs-framework
# 安装依赖
pnpm install
# 构建所有包
pnpm build
# 可选:构建 WASM 渲染器
# 构建 WASM 渲染器(可选)
pnpm build:wasm
```
@@ -237,24 +212,24 @@ pnpm tauri:dev
### 项目结构
```
esengine/
├── packages/ # 引擎包(运行时、编辑器、平台)
├── docs/ # 文档源码
├── examples/ # 示例项目
├── scripts/ # 构建工具
└── thirdparty/ # 第三方依赖
ecs-framework/
├── packages/ 引擎包(运行时、编辑器、平台)
├── docs/ 文档源码
├── examples/ 示例项目
├── scripts/ 构建工具
└── thirdparty/ 第三方依赖
```
## 文档
- [快速入门](https://esengine.cn/guide/getting-started.html)
- [架构指南](https://esengine.cn/guide/)
- [API 参考](https://esengine.cn/api/README)
- [快速入门](https://esengine.github.io/ecs-framework/guide/getting-started.html)
- [架构指南](https://esengine.github.io/ecs-framework/guide/)
- [API 参考](https://esengine.github.io/ecs-framework/api/)
## 社区
- [GitHub Issues](https://github.com/esengine/esengine/issues) - Bug 反馈和功能建议
- [GitHub Discussions](https://github.com/esengine/esengine/discussions) - 问题和想法
- [GitHub Issues](https://github.com/esengine/ecs-framework/issues) - Bug 反馈和功能建议
- [GitHub Discussions](https://github.com/esengine/ecs-framework/discussions) - 问题和想法
- [QQ 交流群](https://jq.qq.com/?_wv=1027&k=29w1Nud6) - 中文社区
## 贡献
@@ -262,17 +237,10 @@ esengine/
欢迎贡献代码。提交 PR 前请阅读贡献指南。
1. Fork 仓库
2. 创建功能分支 (`git checkout -b feature/amazing-feature`)
3. 提交修改 (`git commit -m 'Add amazing feature'`)
4. 推送分支 (`git push origin feature/amazing-feature`)
5. 发起 Pull Request
2. 创建功能分支
3. 修改代码并测试
4. 提交 PR
## 许可证
ESEngine 基于 [MIT 协议](LICENSE) 开源。
---
<p align="center">
由 ESEngine 团队用 ❤️ 打造
</p>

View File

@@ -1,71 +1,13 @@
# Security Policy / 安全政策
**English** | [中文](#安全政策-1)
## Supported Versions
We provide security updates for the following versions:
| Version | Supported |
| ------- | ------------------ |
| 2.x.x | :white_check_mark: |
| 1.x.x | :x: |
## Reporting a Vulnerability
If you discover a security vulnerability, please report it through the following channels:
### Reporting Channels
- **GitHub Security Advisories**: [Report a vulnerability](https://github.com/esengine/esengine/security/advisories/new) (Recommended)
- **Email**: security@esengine.dev
### Reporting Guidelines
1. **Do NOT** report security vulnerabilities in public issues
2. Provide a detailed description of the vulnerability, including:
- Affected versions
- Steps to reproduce
- Potential impact
- Suggested fix (if available)
### Response Timeline
- **Acknowledgment**: Within 72 hours
- **Initial Assessment**: Within 1 week
- **Fix Release**: Typically within 2-4 weeks, depending on severity
### Process
1. We will confirm the existence and severity of the vulnerability
2. Develop and test a fix
3. Release a security update
4. Publicly disclose the vulnerability details after the fix is released
## Security Best Practices
When using ESEngine, please follow these security recommendations:
- Always use the latest stable version
- Regularly update dependencies
- Disable debug mode in production
- Validate all external input data
- Do not store sensitive information on the client side
---
# 安全政策
[English](#security-policy--安全政策) | **中文**
## 支持的版本
我们为以下版本提供安全更新:
| 版本 | 支持状态 |
| ------- | ------------------ |
| 2.x.x | :white_check_mark: |
| 1.x.x | :x: |
| 2.0.x | :white_check_mark: |
| 1.0.x | :x: |
## 报告漏洞
@@ -73,10 +15,10 @@ When using ESEngine, please follow these security recommendations:
### 报告渠道
- **GitHub 安全公告**: [报告漏洞](https://github.com/esengine/esengine/security/advisories/new)(推荐)
- **邮箱**: security@esengine.dev
- **邮箱**: [安全邮箱将在实际部署时提供]
- **GitHub**: 创建私有安全报告(推荐)
### 报告指南
### 报告流程
1. **不要**在公开的 issue 中报告安全漏洞
2. 提供详细的漏洞描述,包括:
@@ -98,9 +40,9 @@ When using ESEngine, please follow these security recommendations:
3. 发布安全更新
4. 在修复发布后,会在相关渠道公布漏洞详情
## 安全最佳实践
### 安全最佳实践
使用 ESEngine 时,请遵循以下安全建议:
使用 ECS Framework 时,请遵循以下安全建议:
- 始终使用最新的稳定版本
- 定期更新依赖项
@@ -108,6 +50,4 @@ When using ESEngine, please follow these security recommendations:
- 验证所有外部输入数据
- 不要在客户端存储敏感信息
感谢您帮助保持 ESEngine 的安全性!
Thank you for helping keep ESEngine secure!
感谢您帮助保持 ECS Framework 的安全性!

View File

@@ -45,8 +45,7 @@ function createSidebar(t, prefix = '') {
link: `${prefix}/guide/scene`,
items: [
{ text: t.sidebar.sceneManager, link: `${prefix}/guide/scene-manager` },
{ text: t.sidebar.worldManager, link: `${prefix}/guide/world-manager` },
{ text: t.sidebar.persistentEntity, link: `${prefix}/guide/persistent-entity` }
{ text: t.sidebar.worldManager, link: `${prefix}/guide/world-manager` }
]
},
{
@@ -181,10 +180,9 @@ function createNav(t, prefix = '') {
{ text: t.nav.lawnMowerDemo, link: 'https://github.com/esengine/lawn-mower-demo' }
]
},
{ text: t.nav.changelog, link: `${prefix}/changelog` },
{
text: `v${corePackageJson.version}`,
link: 'https://github.com/esengine/esengine/releases'
link: 'https://github.com/esengine/ecs-framework/releases'
}
]
}
@@ -220,7 +218,7 @@ export default defineConfig({
nav: createNav(zh, ''),
sidebar: createSidebar(zh, ''),
editLink: {
pattern: 'https://github.com/esengine/esengine/edit/master/docs/:path',
pattern: 'https://github.com/esengine/ecs-framework/edit/master/docs/:path',
text: zh.common.editOnGithub
},
outline: {
@@ -238,7 +236,7 @@ export default defineConfig({
nav: createNav(en, '/en'),
sidebar: createSidebar(en, '/en'),
editLink: {
pattern: 'https://github.com/esengine/esengine/edit/master/docs/:path',
pattern: 'https://github.com/esengine/ecs-framework/edit/master/docs/:path',
text: en.common.editOnGithub
},
outline: {
@@ -253,7 +251,7 @@ export default defineConfig({
siteTitle: 'ESEngine',
socialLinks: [
{ icon: 'github', link: 'https://github.com/esengine/esengine' }
{ icon: 'github', link: 'https://github.com/esengine/ecs-framework' }
],
footer: {
@@ -273,7 +271,6 @@ export default defineConfig({
base: '/',
cleanUrls: true,
ignoreDeadLinks: true,
markdown: {
lineNumbers: true,

View File

@@ -6,8 +6,7 @@
"api": "API",
"examples": "Examples",
"workerDemo": "Worker System Demo",
"lawnMowerDemo": "Lawn Mower Demo",
"changelog": "Changelog"
"lawnMowerDemo": "Lawn Mower Demo"
},
"sidebar": {
"gettingStarted": "Getting Started",
@@ -23,7 +22,6 @@
"scene": "Scene",
"sceneManager": "SceneManager",
"worldManager": "WorldManager",
"persistentEntity": "Persistent Entity",
"behaviorTree": "Behavior Tree",
"btGettingStarted": "Getting Started",
"btCoreConcepts": "Core Concepts",

View File

@@ -6,8 +6,7 @@
"api": "API",
"examples": "示例",
"workerDemo": "Worker系统演示",
"lawnMowerDemo": "割草机演示",
"changelog": "更新日志"
"lawnMowerDemo": "割草机演示"
},
"sidebar": {
"gettingStarted": "开始使用",
@@ -23,7 +22,6 @@
"scene": "场景管理 (Scene)",
"sceneManager": "SceneManager",
"worldManager": "WorldManager",
"persistentEntity": "持久化实体 (Persistent Entity)",
"behaviorTree": "行为树系统 (Behavior Tree)",
"btGettingStarted": "快速开始",
"btCoreConcepts": "核心概念",

View File

@@ -219,7 +219,7 @@ onUnmounted(() => {
</p>
<div class="hero-actions">
<a href="/guide/getting-started" class="btn-primary">开始使用</a>
<a href="https://github.com/esengine/esengine" class="btn-secondary" target="_blank">了解更多</a>
<a href="https://github.com/esengine/ecs-framework" class="btn-secondary" target="_blank">了解更多</a>
</div>
</div>

View File

@@ -219,7 +219,7 @@ onUnmounted(() => {
</p>
<div class="hero-actions">
<a href="/en/guide/getting-started" class="btn-primary">Get Started</a>
<a href="https://github.com/esengine/esengine" class="btn-secondary" target="_blank">Learn More</a>
<a href="https://github.com/esengine/ecs-framework" class="btn-secondary" target="_blank">Learn More</a>
</div>
</div>

View File

@@ -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. **渐进迁移**:可分阶段进行,不影响现有功能

View File

@@ -1,250 +0,0 @@
# Changelog
本文档记录 `@esengine/ecs-framework` 核心库的版本更新历史。
---
## v2.4.0 (2025-12-15)
### Features
- **EntityHandle 实体句柄**: 轻量级实体引用抽象 (#304)
- 28位索引 + 20位代数generation设计高效复用已销毁实体槽位
- `EntityHandleManager` 管理句柄生命周期和有效性验证
- 支持句柄转换为实体引用,检测悬空引用
- **SystemScheduler 系统调度器**: 声明式系统调度 (#304)
- 新增 `@Stage(name)` 装饰器指定系统执行阶段
- 新增 `@Before(SystemClass)` / `@After(SystemClass)` 装饰器声明系统依赖
- 新增 `@InSet(setName)` 装饰器将系统归入逻辑分组
- 基于拓扑排序自动解析执行顺序,检测循环依赖
- **EpochManager 变更检测**: 帧级变更追踪机制 (#304)
- 跟踪组件添加/修改时间戳epoch
- 支持查询"自上次检查以来变化的组件"
- 适用于脏检测、增量更新等优化场景
- **CompiledQuery 编译查询**: 预编译类型安全查询 (#304)
- 编译时生成优化的查询逻辑,减少运行时开销
- 完整的 TypeScript 类型推断支持
- 支持 `With``Without``Changed` 等查询条件组合
- **PluginServiceRegistry**: 类型安全的插件服务注册表 (#300)
- 通过 `Core.pluginServices` 访问
- 支持 `ServiceToken<T>` 模式获取服务
- **组件自动注册**: `@ECSComponent` 装饰器增强 (#302)
- 装饰器现在自动注册到 `ComponentRegistry`
- 解决 `Decorators ↔ ComponentRegistry` 循环依赖
- 新建 `ComponentTypeUtils.ts` 作为底层无依赖模块
### API Changes
- `EntitySystem` 添加 `getBefore()` / `getAfter()` / `getSets()` getter 方法
- `Entity` 添加 `markDirty()` 辅助方法用于手动触发变更检测
- `IScene` 添加 `epochManager` 属性
- `CommandBuffer.pendingCount` 修正为返回实际操作数(而非实体数)
### Documentation
- 更新系统调度文档,添加声明式依赖配置章节
- 更新实体查询文档,添加编译查询使用说明
---
## v2.3.2 (2025-12-08)
### Features
- **微信小游戏 Worker 支持**: 添加对微信小游戏平台 Worker 的完整支持 (#297)
- 新增 `workerScriptPath` 配置项,支持预编译 Worker 脚本路径
- 修复微信小游戏 Worker 消息格式差异(`res` 直接是数据,无需 `.data`
- 适用于微信小游戏等不支持动态脚本的平台
### New Package
- **@esengine/worker-generator** `v1.0.2`: CLI 工具,从 `WorkerEntitySystem` 子类自动生成 Worker 文件
- 自动扫描并提取 `workerProcess` 方法体
- 支持 `--wechat` 模式,使用 TypeScript 编译器转换为 ES5 语法
- 读取代码中的 `workerScriptPath` 配置,生成到指定路径
- 生成 `worker-mapping.json` 映射文件
### Documentation
- 更新 Worker 系统文档,添加微信小游戏支持章节
- 新增英文版 Worker 系统文档 (`docs/en/guide/worker-system.md`)
---
## v2.3.1 (2025-12-07)
### Bug Fixes
- **类型导出修复**: 修复 v2.3.0 中的类型导出问题
- 解决 `ServiceToken` 跨包类型兼容性问题
- 修复 `editor-app``behavior-tree-editor` 中的类型引用
---
## v2.3.0 (2025-12-06) ⚠️ DEPRECATED
> **警告**: 此版本存在类型导出问题,请升级到 v2.3.1 或更高版本。
>
> **Warning**: This version has type export issues. Please upgrade to v2.3.1 or later.
### Features
- **持久化实体**: 添加实体跨场景迁移支持 (#285)
- 新增 `EEntityLifecyclePolicy` 枚举(`SceneLocal`/`Persistent`
- Entity 添加 `setPersistent()``setSceneLocal()``isPersistent` API
- Scene 添加 `findPersistentEntities()``extractPersistentEntities()``receiveMigratedEntities()`
- `SceneManager.setScene()` 自动处理持久化实体迁移
- 适用场景:全局管理器、玩家角色、跨场景状态保持
- **CommandBuffer 延迟命令系统**: 在帧末统一执行实体操作 (#281)
- 支持延迟添加/移除组件、销毁实体、设置实体激活状态
- 每个系统拥有独立的 `commands` 属性
- 避免在迭代过程中修改实体列表,防止迭代问题
- Scene 在 `lateUpdate` 后自动刷新所有命令缓冲区
### Performance
- **ReactiveQuery 快照优化**: 优化实体查询迭代性能 (#281)
- 添加快照机制,避免每帧拷贝数组
- 只在实体列表变化时创建新快照
- 静态场景下多个系统共享同一快照
---
## v2.2.21 (2025-12-05)
### Bug Fixes
- **迭代安全修复**: 修复 `process`/`lateProcess` 迭代时组件变化导致跳过实体的问题 (#272)
- 在系统处理过程中添加/移除组件不再导致实体被意外跳过
### Performance
- **HierarchySystem 性能优化**: 优化层级系统避免每帧遍历所有实体 (#279)
- 使用脏实体集合代替每帧遍历所有实体
- 静态场景下 `process()` 从 O(n) 优化为 O(1)
- 1000 实体静态场景: 81.79μs → 0.07μs (快 1168 倍)
- 10000 实体静态场景: 939.43μs → 0.56μs (快 1677 倍)
- 服务端模拟 (100房间 x 100实体): 2.7ms → 1.4ms 每 tick
---
## v2.2.20 (2025-12-04)
### Bug Fixes
- **系统 onAdded 回调修复**: 修复系统 `onAdded` 回调受注册顺序影响的问题 (#270)
- 系统初始化时会对已存在的匹配实体触发 `onAdded` 回调
- 新增 `matchesEntity(entity)` 方法,用于检查实体是否匹配系统的查询条件
- Scene 新增 `notifySystemsEntityAdded/Removed` 方法,确保所有系统都能收到实体变更通知
---
## v2.2.19 (2025-12-03)
### Features
- **系统稳定排序**: 添加系统稳定排序支持 (#257)
- 新增 `addOrder` 属性,用于 `updateOrder` 相同时的稳定排序
- 确保相同优先级的系统按添加顺序执行
- **模块配置**: 添加 `module.json` 配置文件 (#256)
- 定义模块 ID、名称、版本等元信息
- 支持模块依赖声明和导出配置
---
## v2.2.18 (2025-11-30)
### Features
- **高级性能分析器**: 实现全新的性能分析 SDK (#248)
- `ProfilerSDK`: 统一的性能分析接口
- 手动采样标记 (`beginSample`/`endSample`)
- 自动作用域测量 (`measure`/`measureAsync`)
- 调用层级追踪和调用图生成
- 计数器和仪表支持
- `AdvancedProfilerCollector`: 高级性能数据收集器
- 帧时间统计和历史记录
- 内存快照和 GC 检测
- 长任务检测 (Long Task API)
- 性能报告生成
- `DebugManager`: 调试管理器
- 统一的调试工具入口
- 性能分析器集成
- **属性装饰器增强**: 扩展 `@serialize` 装饰器功能 (#247)
- 支持更多序列化选项配置
### Improvements
- **EntitySystem 测试覆盖**: 添加完整的系统测试用例 (#240)
- 覆盖实体查询、缓存、生命周期等场景
- **Matcher 增强**: 优化匹配器功能 (#240)
- 改进匹配逻辑和性能
---
## v2.2.17 (2025-11-28)
### Features
- **ComponentRegistry 增强**: 添加组件注册表新功能 (#244)
- 支持通过名称注册和查询组件类型
- 添加组件掩码缓存优化性能
- **序列化装饰器改进**: 增强 `@serialize` 装饰器 (#244)
- 支持更灵活的序列化配置
- 改进嵌套对象序列化
- **EntitySystem 生命周期**: 新增系统生命周期方法 (#244)
- `onSceneStart()`: 场景开始时调用
- `onSceneStop()`: 场景停止时调用
---
## v2.2.16 (2025-11-27)
### Features
- **组件生命周期**: 添加组件生命周期回调支持 (#237)
- `onDeserialized()`: 组件从场景文件加载或快照恢复后调用,用于恢复运行时数据
- **ServiceContainer 增强**: 改进服务容器功能 (#237)
- 支持 `Symbol.for()` 模式的服务标识
- 新增 `@InjectProperty` 属性注入装饰器
- 改进服务解析和生命周期管理
- **SceneSerializer 增强**: 场景序列化器新功能 (#237)
- 支持更多组件类型的序列化
- 改进反序列化错误处理
- **属性装饰器扩展**: 扩展 `@serialize` 装饰器 (#238)
- 支持 `@range``@slider` 等编辑器提示
- 支持 `@dropdown``@color` 等 UI 类型
- 支持 `@asset` 资源引用类型
### Improvements
- **Matcher 测试**: 添加 Matcher 匹配器测试用例 (#240)
- **EntitySystem 测试**: 添加实体系统完整测试覆盖 (#240)
---
## 版本说明
- **主版本号**: 重大不兼容更新
- **次版本号**: 新功能添加(向后兼容)
- **修订版本号**: Bug 修复和小改进
## 相关链接
- [GitHub Releases](https://github.com/esengine/esengine/releases)
- [NPM Package](https://www.npmjs.com/package/@esengine/ecs-framework)
- [文档首页](./index.md)

View File

@@ -1,248 +0,0 @@
# Changelog
This document records the version update history of the `@esengine/ecs-framework` core library.
---
## v2.4.0 (2025-12-15)
### Features
- **EntityHandle**: Lightweight entity reference abstraction (#304)
- 28-bit index + 20-bit generation design for efficient reuse of destroyed entity slots
- `EntityHandleManager` manages handle lifecycle and validity verification
- Support handle-to-entity conversion with dangling reference detection
- **SystemScheduler**: Declarative system scheduling (#304)
- New `@Stage(name)` decorator to specify system execution stage
- New `@Before(SystemClass)` / `@After(SystemClass)` decorators to declare dependencies
- New `@InSet(setName)` decorator to group systems logically
- Automatic execution order resolution via topological sort with cycle detection
- **EpochManager**: Frame-level change detection mechanism (#304)
- Track component add/modify timestamps (epochs)
- Support querying "components changed since last check"
- Suitable for dirty checking, incremental updates, and other optimization scenarios
- **CompiledQuery**: Pre-compiled type-safe queries (#304)
- Compile-time generated optimized query logic, reducing runtime overhead
- Full TypeScript type inference support
- Support `With`, `Without`, `Changed` and other query condition combinations
- **PluginServiceRegistry**: Type-safe plugin service registry (#300)
- Accessible via `Core.pluginServices`
- Support `ServiceToken<T>` pattern for service retrieval
- **Component Auto-Registration**: `@ECSComponent` decorator enhancement (#302)
- Decorator now automatically registers to `ComponentRegistry`
- Resolved `Decorators ↔ ComponentRegistry` circular dependency
- New `ComponentTypeUtils.ts` as low-level dependency-free module
### API Changes
- `EntitySystem` adds `getBefore()` / `getAfter()` / `getSets()` getter methods
- `Entity` adds `markDirty()` helper method for manual change detection triggering
- `IScene` adds `epochManager` property
- `CommandBuffer.pendingCount` corrected to return actual operation count (not entity count)
### Documentation
- Updated system scheduling documentation with declarative dependency configuration
- Updated entity query documentation with compiled query usage
---
## v2.3.2 (2025-12-08)
### Features
- **WeChat Mini Game Worker Support**: Add complete Worker support for WeChat Mini Game platform (#297)
- New `workerScriptPath` config option for pre-compiled Worker script path
- Fix WeChat Mini Game Worker message format difference (`res` is data directly, no `.data` wrapper)
- Applicable to WeChat Mini Game and other platforms that don't support dynamic scripts
### New Package
- **@esengine/worker-generator** `v1.0.2`: CLI tool to auto-generate Worker files from `WorkerEntitySystem` subclasses
- Automatically scan and extract `workerProcess` method body
- Support `--wechat` mode, use TypeScript compiler to convert to ES5 syntax
- Read `workerScriptPath` config from code, generate to specified path
- Generate `worker-mapping.json` mapping file
### Documentation
- Updated Worker system documentation with WeChat Mini Game support section
- Added English Worker system documentation (`docs/en/guide/worker-system.md`)
---
## v2.3.1 (2025-12-07)
### Bug Fixes
- **Type export fix**: Fix type export issues in v2.3.0
- Resolve `ServiceToken` cross-package type compatibility issues
- Fix type references in `editor-app` and `behavior-tree-editor`
---
## v2.3.0 (2025-12-06) ⚠️ DEPRECATED
> **Warning**: This version has type export issues. Please upgrade to v2.3.1 or later.
### Features
- **Persistent Entity**: Add entity cross-scene migration support (#285)
- New `EEntityLifecyclePolicy` enum (`SceneLocal`/`Persistent`)
- Entity adds `setPersistent()`, `setSceneLocal()`, `isPersistent` API
- Scene adds `findPersistentEntities()`, `extractPersistentEntities()`, `receiveMigratedEntities()`
- `SceneManager.setScene()` automatically handles persistent entity migration
- Use cases: global managers, player characters, cross-scene state persistence
- **CommandBuffer Deferred Command System**: Execute entity operations uniformly at end of frame (#281)
- Support deferred add/remove components, destroy entities, set entity active state
- Each system has its own `commands` property
- Avoid modifying entity list during iteration, preventing iteration issues
- Scene automatically flushes all command buffers after `lateUpdate`
### Performance
- **ReactiveQuery Snapshot Optimization**: Optimize entity query iteration performance (#281)
- Add snapshot mechanism to avoid copying arrays every frame
- Only create new snapshots when entity list changes
- Multiple systems share the same snapshot in static scenes
---
## v2.2.21 (2025-12-05)
### Bug Fixes
- **Iteration safety fix**: Fix issue where component changes during `process`/`lateProcess` iteration caused entities to be skipped (#272)
- Adding/removing components during system processing no longer causes entities to be unexpectedly skipped
### Performance
- **HierarchySystem optimization**: Optimize hierarchy system to avoid iterating all entities every frame (#279)
- Use dirty entity set instead of iterating all entities
- Static scene `process()` optimized from O(n) to O(1)
- 1000 entities static scene: 81.79μs → 0.07μs (1168x faster)
- 10000 entities static scene: 939.43μs → 0.56μs (1677x faster)
- Server simulation (100 rooms x 100 entities): 2.7ms → 1.4ms per tick
---
## v2.2.20 (2025-12-04)
### Bug Fixes
- **System onAdded callback fix**: Fix issue where system `onAdded` callback was affected by registration order (#270)
- System initialization now triggers `onAdded` callback for existing matching entities
- Added `matchesEntity(entity)` method to check if an entity matches the system's query condition
- Scene added `notifySystemsEntityAdded/Removed` methods to ensure all systems receive entity change notifications
---
## v2.2.19 (2025-12-03)
### Features
- **System stable sorting**: Add stable sorting support for systems (#257)
- Added `addOrder` property for stable sorting when `updateOrder` is the same
- Ensures systems with same priority execute in add order
- **Module configuration**: Add `module.json` configuration file (#256)
- Define module ID, name, version and other metadata
- Support module dependency declaration and export configuration
---
## v2.2.18 (2025-11-30)
### Features
- **Advanced Performance Profiler**: Implement new performance analysis SDK (#248)
- `ProfilerSDK`: Unified performance analysis interface
- Manual sampling markers (`beginSample`/`endSample`)
- Automatic scope measurement (`measure`/`measureAsync`)
- Call hierarchy tracking and call graph generation
- Counter and gauge support
- `AdvancedProfilerCollector`: Advanced performance data collector
- Frame time statistics and history
- Memory snapshots and GC detection
- Long task detection (Long Task API)
- Performance report generation
- `DebugManager`: Debug manager
- Unified debug tool entry point
- Profiler integration
- **Property decorator enhancement**: Extend `@serialize` decorator functionality (#247)
- Support more serialization option configurations
### Improvements
- **EntitySystem test coverage**: Add complete system test cases (#240)
- Cover entity query, cache, lifecycle scenarios
- **Matcher enhancement**: Optimize matcher functionality (#240)
- Improved matching logic and performance
---
## v2.2.17 (2025-11-28)
### Features
- **ComponentRegistry enhancement**: Add new component registry features (#244)
- Support registering and querying component types by name
- Add component mask caching for performance optimization
- **Serialization decorator improvements**: Enhance `@serialize` decorator (#244)
- Support more flexible serialization configuration
- Improved nested object serialization
- **EntitySystem lifecycle**: Add new system lifecycle methods (#244)
- `onSceneStart()`: Called when scene starts
- `onSceneStop()`: Called when scene stops
---
## v2.2.16 (2025-11-27)
### Features
- **Component lifecycle**: Add component lifecycle callback support (#237)
- `onDeserialized()`: Called after component is loaded from scene file or snapshot restore, used to restore runtime data
- **ServiceContainer enhancement**: Improve service container functionality (#237)
- Support `Symbol.for()` pattern for service identifiers
- Added `@InjectProperty` property injection decorator
- Improved service resolution and lifecycle management
- **SceneSerializer enhancement**: New scene serializer features (#237)
- Support serialization of more component types
- Improved deserialization error handling
- **Property decorator extension**: Extend `@serialize` decorator (#238)
- Support `@range`, `@slider` and other editor hints
- Support `@dropdown`, `@color` and other UI types
- Support `@asset` resource reference type
### Improvements
- **Matcher tests**: Add Matcher test cases (#240)
- **EntitySystem tests**: Add complete entity system test coverage (#240)
---
## Version Notes
- **Major version**: Breaking changes
- **Minor version**: New features (backward compatible)
- **Patch version**: Bug fixes and improvements
## Related Links
- [GitHub Releases](https://github.com/esengine/esengine/releases)
- [NPM Package](https://www.npmjs.com/package/@esengine/ecs-framework)
- [Documentation Home](./index.md)

View File

@@ -1,444 +0,0 @@
# Entity
In ECS architecture, an Entity is the basic object in the game world. An entity itself does not contain game logic or data - it's just a container that combines different components to achieve various functionalities.
## Basic Concepts
An entity is a lightweight object mainly used for:
- Serving as a container for components
- Providing a unique identifier (ID)
- Managing component lifecycle
::: tip About Parent-Child Hierarchy
Parent-child hierarchy relationships between entities are managed through `HierarchyComponent` and `HierarchySystem`, not built-in Entity properties. This design follows ECS composition principles - only entities that need hierarchy relationships add this component.
See [Hierarchy System](./hierarchy.md) documentation.
:::
## Creating Entities
**Important: Entities must be created through Scene, manual creation is not supported!**
Entities must be created through the scene's `createEntity()` method to ensure:
- Entity is properly added to the scene's entity management system
- Entity is added to the query system for system use
- Entity gets the correct scene reference
- Related lifecycle events are triggered
```typescript
// Correct way: create entity through scene
const player = scene.createEntity("Player");
// Wrong way: manually create entity
// const entity = new Entity("MyEntity", 1); // System cannot manage such entities
```
## Adding Components
Entities gain functionality by adding components:
```typescript
import { Component, ECSComponent } from '@esengine/ecs-framework';
// Define position component
@ECSComponent('Position')
class Position extends Component {
x: number = 0;
y: number = 0;
constructor(x: number = 0, y: number = 0) {
super();
this.x = x;
this.y = y;
}
}
// Define health component
@ECSComponent('Health')
class Health extends Component {
current: number = 100;
max: number = 100;
constructor(max: number = 100) {
super();
this.max = max;
this.current = max;
}
}
// Add components to entity
const player = scene.createEntity("Player");
player.addComponent(new Position(100, 200));
player.addComponent(new Health(150));
```
## Getting Components
```typescript
// Get component (pass component class, not instance)
const position = player.getComponent(Position); // Returns Position | null
const health = player.getComponent(Health); // Returns Health | null
// Check if component exists
if (position) {
console.log(`Player position: x=${position.x}, y=${position.y}`);
}
// Check if entity has a component
if (player.hasComponent(Position)) {
console.log("Player has position component");
}
// Get all component instances (read-only property)
const allComponents = player.components; // readonly Component[]
// Get all components of specified type (supports multiple components of same type)
const allHealthComponents = player.getComponents(Health); // Health[]
// Get or create component (creates automatically if not exists)
const position = player.getOrCreateComponent(Position, 0, 0); // Pass constructor arguments
const health = player.getOrCreateComponent(Health, 100); // Returns existing if present, creates new if not
```
## Removing Components
```typescript
// Method 1: Remove by component type
const removedHealth = player.removeComponentByType(Health);
if (removedHealth) {
console.log("Health component removed");
}
// Method 2: Remove by component instance
const healthComponent = player.getComponent(Health);
if (healthComponent) {
player.removeComponent(healthComponent);
}
// Batch remove multiple component types
const removedComponents = player.removeComponentsByTypes([Position, Health]);
// Check if component was removed
if (!player.hasComponent(Health)) {
console.log("Health component has been removed");
}
```
## Finding Entities
Scene provides multiple ways to find entities:
### Find by Name
```typescript
// Find single entity
const player = scene.findEntity("Player");
// Or use alias method
const player2 = scene.getEntityByName("Player");
if (player) {
console.log("Found player entity");
}
```
### Find by ID
```typescript
// Find by entity ID
const entity = scene.findEntityById(123);
```
### Find by Tag
Entities support a tag system for quick categorization and lookup:
```typescript
// Set tags
player.tag = 1; // Player tag
enemy.tag = 2; // Enemy tag
// Find all entities by tag
const players = scene.findEntitiesByTag(1);
const enemies = scene.findEntitiesByTag(2);
// Or use alias method
const allPlayers = scene.getEntitiesByTag(1);
```
## Entity Lifecycle
```typescript
// Destroy entity
player.destroy();
// Check if entity is destroyed
if (player.isDestroyed) {
console.log("Entity has been destroyed");
}
```
## Entity Events
Component changes on entities trigger events:
```typescript
// Listen for component added event
scene.eventSystem.on('component:added', (data) => {
console.log('Component added:', data);
});
// Listen for entity created event
scene.eventSystem.on('entity:created', (data) => {
console.log('Entity created:', data.entityName);
});
```
## Performance Optimization
### Batch Entity Creation
The framework provides high-performance batch creation methods:
```typescript
// Batch create 100 bullet entities (high-performance version)
const bullets = scene.createEntities(100, "Bullet");
// Add components to each bullet
bullets.forEach((bullet, index) => {
bullet.addComponent(new Position(Math.random() * 800, Math.random() * 600));
bullet.addComponent(new Velocity(Math.random() * 100 - 50, Math.random() * 100 - 50));
});
```
`createEntities()` method will:
- Batch allocate entity IDs
- Batch add to entity list
- Optimize query system updates
- Reduce system cache clearing times
## Best Practices
### 1. Appropriate Component Granularity
```typescript
// Good practice: single-purpose components
@ECSComponent('Position')
class Position extends Component {
x: number = 0;
y: number = 0;
}
@ECSComponent('Velocity')
class Velocity extends Component {
dx: number = 0;
dy: number = 0;
}
// Avoid: overly complex components
@ECSComponent('Player')
class Player extends Component {
// Avoid including too many unrelated properties in one component
x: number;
y: number;
health: number;
inventory: Item[];
skills: Skill[];
}
```
### 2. Use Decorators
Always use `@ECSComponent` decorator:
```typescript
@ECSComponent('Transform')
class Transform extends Component {
// Component implementation
}
```
### 3. Proper Naming
```typescript
// Clear entity naming
const mainCharacter = scene.createEntity("MainCharacter");
const enemy1 = scene.createEntity("Goblin_001");
const collectible = scene.createEntity("HealthPotion");
```
### 4. Timely Cleanup
```typescript
// Destroy entities that are no longer needed
if (enemy.getComponent(Health).current <= 0) {
enemy.destroy();
}
```
## Debugging Entities
The framework provides debugging features to help development:
```typescript
// Get entity debug info
const debugInfo = entity.getDebugInfo();
console.log('Entity info:', debugInfo);
// List all components of entity
entity.components.forEach(component => {
console.log('Component:', component.constructor.name);
});
```
Entities are one of the core concepts in ECS architecture. Understanding how to use entities correctly will help you build efficient, maintainable game code.
## Entity Handle (EntityHandle)
Entity handles provide a safe way to reference entities, solving the "referencing destroyed entity" problem.
### Problem Scenario
Suppose your AI system needs to track a target enemy:
```typescript
// Wrong approach: directly store entity reference
class AISystem extends EntitySystem {
private targetEnemy: Entity | null = null;
setTarget(enemy: Entity) {
this.targetEnemy = enemy;
}
process() {
if (this.targetEnemy) {
// Dangerous! Enemy might be destroyed, but reference still exists
// Worse: this memory location might be reused by a new entity
const health = this.targetEnemy.getComponent(Health);
// Might operate on the wrong entity!
}
}
}
```
### Correct Approach Using Handles
Each entity is automatically assigned a handle when created, accessible via `entity.handle`:
```typescript
import { EntityHandle, NULL_HANDLE, isValidHandle } from '@esengine/ecs-framework';
class AISystem extends EntitySystem {
// Store handle instead of entity reference
private targetHandle: EntityHandle = NULL_HANDLE;
setTarget(enemy: Entity) {
// Save enemy's handle
this.targetHandle = enemy.handle;
}
process() {
if (!isValidHandle(this.targetHandle)) {
return; // No target
}
// Get entity through handle (automatically checks validity)
const enemy = this.scene.findEntityByHandle(this.targetHandle);
if (!enemy) {
// Enemy was destroyed, clear reference
this.targetHandle = NULL_HANDLE;
return;
}
// Safe operation
const health = enemy.getComponent(Health);
}
}
```
### Complete Example: Skill Target Locking
```typescript
import {
EntitySystem, Entity, EntityHandle, NULL_HANDLE, isValidHandle
} from '@esengine/ecs-framework';
@ECSSystem('SkillTargeting')
class SkillTargetingSystem extends EntitySystem {
// Store handles for multiple targets
private lockedTargets: Map<Entity, EntityHandle> = new Map();
// Lock target
lockTarget(caster: Entity, target: Entity) {
this.lockedTargets.set(caster, target.handle);
}
// Get locked target
getLockedTarget(caster: Entity): Entity | null {
const handle = this.lockedTargets.get(caster);
if (!handle || !isValidHandle(handle)) {
return null;
}
// findEntityByHandle checks if handle is valid
const target = this.scene.findEntityByHandle(handle);
if (!target) {
// Target died, clear lock
this.lockedTargets.delete(caster);
}
return target;
}
// Cast skill
castSkill(caster: Entity) {
const target = this.getLockedTarget(caster);
if (!target) {
console.log('Target lost, skill cancelled');
return;
}
// Safely deal damage to target
const health = target.getComponent(Health);
if (health) {
health.current -= 10;
}
}
}
```
### Handle vs Entity Reference
| Scenario | Recommended Approach |
|----------|---------------------|
| Temporary use within same frame | Use `Entity` reference directly |
| Cross-frame storage (e.g., AI target, skill target) | Use `EntityHandle` |
| Needs serialization | Use `EntityHandle` (numeric type) |
| Network synchronization | Use `EntityHandle` (can be transmitted directly) |
### API Quick Reference
```typescript
// Get entity's handle
const handle = entity.handle;
// Check if handle is non-null
if (isValidHandle(handle)) { ... }
// Get entity through handle (automatically checks validity)
const entity = scene.findEntityByHandle(handle);
// Check if entity corresponding to handle is alive
const alive = scene.handleManager.isAlive(handle);
// Null handle constant
const emptyHandle = NULL_HANDLE;
```
## Next Steps
- Learn about [Hierarchy System](./hierarchy.md) to establish parent-child relationships
- Learn about [Component System](./component.md) to add functionality to entities
- Learn about [Scene Management](./scene.md) to organize and manage entities

View File

@@ -1,360 +0,0 @@
# Persistent Entity
> **Version**: v2.3.0+
Persistent Entity is a special type of entity that automatically migrates to the new scene during scene transitions. It is suitable for game objects that need to maintain state across scenes, such as players, game managers, audio managers, etc.
## Basic Concepts
In the ECS framework, entities have two lifecycle policies:
| Policy | Description | Default |
|--------|-------------|---------|
| `SceneLocal` | Scene-local entity, destroyed when scene changes | ✓ |
| `Persistent` | Persistent entity, automatically migrates during scene transitions | |
## Quick Start
### Creating a Persistent Entity
```typescript
import { Scene } from '@esengine/ecs-framework';
class GameScene extends Scene {
protected initialize(): void {
// Create a persistent player entity
const player = this.createEntity('Player').setPersistent();
player.addComponent(new Position(100, 200));
player.addComponent(new PlayerData('Hero', 500));
// Create a normal enemy entity (destroyed when scene changes)
const enemy = this.createEntity('Enemy');
enemy.addComponent(new Position(300, 200));
enemy.addComponent(new EnemyAI());
}
}
```
### Behavior During Scene Transitions
```typescript
import { Core, Scene } from '@esengine/ecs-framework';
// Initial scene
class Level1Scene extends Scene {
protected initialize(): void {
// Player - persistent, will migrate to the next scene
const player = this.createEntity('Player').setPersistent();
player.addComponent(new Position(0, 0));
player.addComponent(new Health(100));
// Enemy - scene-local, destroyed when scene changes
const enemy = this.createEntity('Enemy');
enemy.addComponent(new Position(100, 100));
}
}
// Target scene
class Level2Scene extends Scene {
protected initialize(): void {
// New enemy
const enemy = this.createEntity('Boss');
enemy.addComponent(new Position(200, 200));
}
public onStart(): void {
// Player has automatically migrated to this scene
const player = this.findEntity('Player');
console.log(player !== null); // true
// Position and health data are fully preserved
const position = player?.getComponent(Position);
const health = player?.getComponent(Health);
console.log(position?.x, position?.y); // 0, 0
console.log(health?.value); // 100
}
}
// Switch scenes
Core.create({ debug: true });
Core.setScene(new Level1Scene());
// Later switch to Level2
Core.loadScene(new Level2Scene());
// Player entity migrates automatically, Enemy entity is destroyed
```
## API Reference
### Entity Methods
#### setPersistent()
Marks the entity as persistent, preventing destruction during scene transitions.
```typescript
public setPersistent(): this
```
**Returns**: Returns the entity itself for method chaining
**Example**:
```typescript
const player = scene.createEntity('Player')
.setPersistent();
player.addComponent(new Position(100, 200));
```
#### setSceneLocal()
Restores the entity to scene-local policy (default).
```typescript
public setSceneLocal(): this
```
**Returns**: Returns the entity itself for method chaining
**Example**:
```typescript
// Dynamically cancel persistence
player.setSceneLocal();
```
#### isPersistent
Checks if the entity is persistent.
```typescript
public get isPersistent(): boolean
```
**Example**:
```typescript
if (entity.isPersistent) {
console.log('This is a persistent entity');
}
```
#### lifecyclePolicy
Gets the entity's lifecycle policy.
```typescript
public get lifecyclePolicy(): EEntityLifecyclePolicy
```
**Example**:
```typescript
import { EEntityLifecyclePolicy } from '@esengine/ecs-framework';
if (entity.lifecyclePolicy === EEntityLifecyclePolicy.Persistent) {
console.log('Persistent entity');
}
```
### Scene Methods
#### findPersistentEntities()
Finds all persistent entities in the scene.
```typescript
public findPersistentEntities(): Entity[]
```
**Returns**: Array of persistent entities
**Example**:
```typescript
const persistentEntities = scene.findPersistentEntities();
console.log(`Scene has ${persistentEntities.length} persistent entities`);
```
#### extractPersistentEntities()
Extracts and removes all persistent entities from the scene (typically called internally by the framework).
```typescript
public extractPersistentEntities(): Entity[]
```
**Returns**: Array of extracted persistent entities
#### receiveMigratedEntities()
Receives migrated entities (typically called internally by the framework).
```typescript
public receiveMigratedEntities(entities: Entity[]): void
```
**Parameters**:
- `entities` - Array of entities to receive
## Use Cases
### 1. Player Entity Across Levels
```typescript
class PlayerSetupScene extends Scene {
protected initialize(): void {
// Player maintains state across all levels
const player = this.createEntity('Player').setPersistent();
player.addComponent(new Transform(0, 0));
player.addComponent(new Health(100));
player.addComponent(new Inventory());
player.addComponent(new PlayerStats());
}
}
class Level1 extends Scene { /* ... */ }
class Level2 extends Scene { /* ... */ }
class Level3 extends Scene { /* ... */ }
// Player entity automatically migrates between all levels
Core.setScene(new PlayerSetupScene());
// ... game progresses
Core.loadScene(new Level1());
// ... level complete
Core.loadScene(new Level2());
// Player data (health, inventory, stats) fully preserved
```
### 2. Global Managers
```typescript
class BootstrapScene extends Scene {
protected initialize(): void {
// Audio manager - persists across scenes
const audioManager = this.createEntity('AudioManager').setPersistent();
audioManager.addComponent(new AudioController());
// Achievement manager - persists across scenes
const achievementManager = this.createEntity('AchievementManager').setPersistent();
achievementManager.addComponent(new AchievementTracker());
// Game settings - persists across scenes
const settings = this.createEntity('GameSettings').setPersistent();
settings.addComponent(new SettingsData());
}
}
```
### 3. Dynamically Toggling Persistence
```typescript
class GameScene extends Scene {
protected initialize(): void {
// Initially created as a normal entity
const companion = this.createEntity('Companion');
companion.addComponent(new Transform(0, 0));
companion.addComponent(new CompanionAI());
// Listen for recruitment event
this.eventSystem.on('companion:recruited', () => {
// After recruitment, become persistent
companion.setPersistent();
console.log('Companion joined the party, will follow player across scenes');
});
// Listen for dismissal event
this.eventSystem.on('companion:dismissed', () => {
// After dismissal, restore to scene-local
companion.setSceneLocal();
console.log('Companion left the party, will no longer persist across scenes');
});
}
}
```
## Best Practices
### 1. Clearly Identify Persistent Entities
```typescript
// Recommended: Mark immediately when creating
const player = this.createEntity('Player').setPersistent();
// Not recommended: Marking after creation (easy to forget)
const player = this.createEntity('Player');
// ... lots of code ...
player.setPersistent(); // Easy to forget
```
### 2. Use Persistence Appropriately
```typescript
// ✓ Entities suitable for persistence
const player = this.createEntity('Player').setPersistent(); // Player
const gameManager = this.createEntity('GameManager').setPersistent(); // Global manager
const audioManager = this.createEntity('AudioManager').setPersistent(); // Audio system
// ✗ Entities that should NOT be persistent
const bullet = this.createEntity('Bullet'); // Temporary objects
const enemy = this.createEntity('Enemy'); // Level-specific enemies
const particle = this.createEntity('Particle'); // Effect particles
```
### 3. Check Migrated Entities
```typescript
class NewScene extends Scene {
public onStart(): void {
// Check if expected persistent entities exist
const player = this.findEntity('Player');
if (!player) {
console.error('Player entity did not migrate correctly!');
// Handle error case
}
}
}
```
### 4. Avoid Circular References
```typescript
// ✗ Avoid: Persistent entity referencing scene-local entity
class BadScene extends Scene {
protected initialize(): void {
const player = this.createEntity('Player').setPersistent();
const enemy = this.createEntity('Enemy');
// Dangerous: player is persistent but enemy is not
// After scene change, enemy is destroyed, reference becomes invalid
player.addComponent(new TargetComponent(enemy));
}
}
// ✓ Recommended: Use ID references or event system
class GoodScene extends Scene {
protected initialize(): void {
const player = this.createEntity('Player').setPersistent();
const enemy = this.createEntity('Enemy');
// Store ID instead of direct reference
player.addComponent(new TargetComponent(enemy.id));
// Or use event system for communication
}
}
```
## Important Notes
1. **Destroyed entities will not migrate**: If an entity is destroyed before scene transition, it will not migrate even if marked as persistent.
2. **Component data is fully preserved**: All components and their state are preserved during migration.
3. **Scene reference is updated**: After migration, the entity's `scene` property will point to the new scene.
4. **Query system is updated**: Migrated entities are automatically registered in the new scene's query system.
5. **Delayed transitions also work**: Persistent entities migrate when using `Core.loadScene()` for delayed transitions as well.
## Related Documentation
- [Scene](./scene) - Learn the basics of scenes
- [SceneManager](./scene-manager) - Learn about scene transitions
- [WorldManager](./world-manager) - Learn about multi-world management

View File

@@ -1,436 +0,0 @@
# SceneManager
SceneManager is a lightweight scene manager provided by ECS Framework, suitable for 95% of game applications. It provides a simple and intuitive API with support for scene transitions and delayed loading.
## Use Cases
SceneManager is suitable for:
- Single-player games
- Simple multiplayer games
- Mobile games
- Games requiring scene transitions (menu, game, pause, etc.)
- Projects that don't need multi-World isolation
## Features
- Lightweight, zero extra overhead
- Simple and intuitive API
- Supports delayed scene transitions (avoids switching mid-frame)
- Automatic ECS fluent API management
- Automatic scene lifecycle handling
- Integrated with Core, auto-updated
- Supports [Persistent Entity](./persistent-entity) migration across scenes (v2.3.0+)
## Basic Usage
### Recommended: Using Core's Static Methods
This is the simplest and recommended approach, suitable for most applications:
```typescript
import { Core, Scene } from '@esengine/ecs-framework';
// 1. Initialize Core
Core.create({ debug: true });
// 2. Create and set scene
class GameScene extends Scene {
protected initialize(): void {
this.name = "GameScene";
// Add systems
this.addSystem(new MovementSystem());
this.addSystem(new RenderSystem());
// Create initial entities
const player = this.createEntity("Player");
player.addComponent(new Transform(400, 300));
player.addComponent(new Health(100));
}
public onStart(): void {
console.log("Game scene started");
}
}
// 3. Set scene
Core.setScene(new GameScene());
// 4. Game loop (Core.update automatically updates the scene)
function gameLoop(deltaTime: number) {
Core.update(deltaTime); // Automatically updates all services and scenes
}
// Laya engine integration
Laya.timer.frameLoop(1, this, () => {
const deltaTime = Laya.timer.delta / 1000;
Core.update(deltaTime);
});
// Cocos Creator integration
update(deltaTime: number) {
Core.update(deltaTime);
}
```
### Advanced: Using SceneManager Directly
If you need more control, you can use SceneManager directly:
```typescript
import { Core, SceneManager, Scene } from '@esengine/ecs-framework';
// Initialize Core
Core.create({ debug: true });
// Get SceneManager (already auto-created and registered by Core)
const sceneManager = Core.services.resolve(SceneManager);
// Set scene
const gameScene = new GameScene();
sceneManager.setScene(gameScene);
// Game loop (still use Core.update)
function gameLoop(deltaTime: number) {
Core.update(deltaTime); // Core automatically calls sceneManager.update()
}
```
**Important**: Regardless of which approach you use, you should only call `Core.update()` in the game loop. It automatically updates SceneManager and scenes. You don't need to manually call `sceneManager.update()`.
## Scene Transitions
### Immediate Transition
Use `Core.setScene()` or `sceneManager.setScene()` to immediately switch scenes:
```typescript
// Method 1: Using Core (recommended)
Core.setScene(new MenuScene());
// Method 2: Using SceneManager
const sceneManager = Core.services.resolve(SceneManager);
sceneManager.setScene(new MenuScene());
```
### Delayed Transition
Use `Core.loadScene()` or `sceneManager.loadScene()` for delayed scene transition, which takes effect on the next frame:
```typescript
// Method 1: Using Core (recommended)
Core.loadScene(new GameOverScene());
// Method 2: Using SceneManager
const sceneManager = Core.services.resolve(SceneManager);
sceneManager.loadScene(new GameOverScene());
```
When switching scenes from within a System, use delayed transitions:
```typescript
class GameOverSystem extends EntitySystem {
process(entities: readonly Entity[]): void {
const player = entities.find(e => e.name === 'Player');
const health = player?.getComponent(Health);
if (health && health.value <= 0) {
// Delayed transition to game over scene (takes effect next frame)
Core.loadScene(new GameOverScene());
// Current frame continues execution, won't interrupt current system processing
}
}
}
```
## API Reference
### Core Static Methods (Recommended)
#### Core.setScene()
Immediately switch scenes.
```typescript
public static setScene<T extends IScene>(scene: T): T
```
**Parameters**:
- `scene` - The scene instance to set
**Returns**:
- Returns the set scene instance
**Example**:
```typescript
const gameScene = Core.setScene(new GameScene());
console.log(gameScene.name);
```
#### Core.loadScene()
Delayed scene loading (switches on next frame).
```typescript
public static loadScene<T extends IScene>(scene: T): void
```
**Parameters**:
- `scene` - The scene instance to load
**Example**:
```typescript
Core.loadScene(new GameOverScene());
```
#### Core.scene
Get the currently active scene.
```typescript
public static get scene(): IScene | null
```
**Returns**:
- Current scene instance, or null if no scene
**Example**:
```typescript
const currentScene = Core.scene;
if (currentScene) {
console.log(`Current scene: ${currentScene.name}`);
}
```
### SceneManager Methods (Advanced)
If you need to use SceneManager directly, get it through the service container:
```typescript
const sceneManager = Core.services.resolve(SceneManager);
```
#### setScene()
Immediately switch scenes.
```typescript
public setScene<T extends IScene>(scene: T): T
```
#### loadScene()
Delayed scene loading.
```typescript
public loadScene<T extends IScene>(scene: T): void
```
#### currentScene
Get the current scene.
```typescript
public get currentScene(): IScene | null
```
#### hasScene
Check if there's an active scene.
```typescript
public get hasScene(): boolean
```
#### hasPendingScene
Check if there's a pending scene transition.
```typescript
public get hasPendingScene(): boolean
```
## Best Practices
### 1. Use Core's Static Methods
```typescript
// Recommended: Use Core's static methods
Core.setScene(new GameScene());
Core.loadScene(new MenuScene());
const currentScene = Core.scene;
// Not recommended: Don't directly use SceneManager unless you have special needs
const sceneManager = Core.services.resolve(SceneManager);
sceneManager.setScene(new GameScene());
```
### 2. Only Call Core.update()
```typescript
// Correct: Only call Core.update()
function gameLoop(deltaTime: number) {
Core.update(deltaTime); // Automatically updates all services and scenes
}
// Incorrect: Don't manually call sceneManager.update()
function gameLoop(deltaTime: number) {
Core.update(deltaTime);
sceneManager.update(); // Duplicate update, will cause issues!
}
```
### 3. Use Delayed Transitions to Avoid Issues
When switching scenes from within a System, use `loadScene()` instead of `setScene()`:
```typescript
// Recommended: Delayed transition
class HealthSystem extends EntitySystem {
process(entities: readonly Entity[]): void {
for (const entity of entities) {
const health = entity.getComponent(Health);
if (health.value <= 0) {
Core.loadScene(new GameOverScene());
// Current frame continues processing other entities
}
}
}
}
// Not recommended: Immediate transition may cause issues
class HealthSystem extends EntitySystem {
process(entities: readonly Entity[]): void {
for (const entity of entities) {
const health = entity.getComponent(Health);
if (health.value <= 0) {
Core.setScene(new GameOverScene());
// Scene switches immediately, other entities in current frame may not process correctly
}
}
}
}
```
### 4. Scene Responsibility Separation
Each scene should be responsible for only one specific game state:
```typescript
// Good design - clear responsibilities
class MenuScene extends Scene {
// Only handles menu-related logic
}
class GameScene extends Scene {
// Only handles gameplay logic
}
class PauseScene extends Scene {
// Only handles pause screen logic
}
// Avoid this design - mixed responsibilities
class MegaScene extends Scene {
// Contains menu, game, pause, and all other logic
}
```
### 5. Resource Management
Clean up resources in the scene's `unload()` method:
```typescript
class GameScene extends Scene {
private textures: Map<string, any> = new Map();
private sounds: Map<string, any> = new Map();
protected initialize(): void {
this.loadResources();
}
private loadResources(): void {
this.textures.set('player', loadTexture('player.png'));
this.sounds.set('bgm', loadSound('bgm.mp3'));
}
public unload(): void {
// Cleanup resources
this.textures.clear();
this.sounds.clear();
console.log('Scene resources cleaned up');
}
}
```
### 6. Event-Driven Scene Transitions
Use the event system to trigger scene transitions, keeping code decoupled:
```typescript
class GameScene extends Scene {
protected initialize(): void {
// Listen to scene transition events
this.eventSystem.on('goto:menu', () => {
Core.loadScene(new MenuScene());
});
this.eventSystem.on('goto:gameover', (data) => {
Core.loadScene(new GameOverScene());
});
}
}
// Trigger events in System
class GameLogicSystem extends EntitySystem {
process(entities: readonly Entity[]): void {
if (levelComplete) {
this.scene.eventSystem.emitSync('goto:gameover', {
score: 1000,
level: 5
});
}
}
}
```
## Architecture Overview
SceneManager's position in ECS Framework:
```
Core (Global Services)
└── SceneManager (Scene Management, auto-updated)
└── Scene (Current Scene)
├── EntitySystem (Systems)
├── Entity (Entities)
└── Component (Components)
```
## Comparison with WorldManager
| Feature | SceneManager | WorldManager |
|---------|--------------|--------------|
| Use Case | 95% of game applications | Advanced multi-world isolation scenarios |
| Complexity | Simple | Complex |
| Scene Count | Single scene (switchable) | Multiple Worlds, each with multiple scenes |
| Performance Overhead | Minimal | Higher |
| Usage | `Core.setScene()` | `worldManager.createWorld()` |
**When to use SceneManager**:
- Single-player games
- Simple multiplayer games
- Mobile games
- Scenes that need transitions but don't need to run simultaneously
**When to use WorldManager**:
- MMO game servers (one World per room)
- Game lobby systems (complete isolation per game room)
- Need to run multiple completely independent game instances
## Related Documentation
- [Persistent Entity](./persistent-entity) - Learn how to keep entities across scene transitions
- [WorldManager](./world-manager) - Learn about advanced multi-world isolation features
SceneManager provides simple yet powerful scene management capabilities for most games. Through Core's static methods, you can easily manage scene transitions.

View File

@@ -1,364 +0,0 @@
# Scene Management
In the ECS architecture, a Scene is a container for the game world, responsible for managing the lifecycle of entities, systems, and components. Scenes provide a complete ECS runtime environment.
## Basic Concepts
Scene is the core container of the ECS framework, providing:
- Entity creation, management, and destruction
- System registration and execution scheduling
- Component storage and querying
- Event system support
- Performance monitoring and debugging information
## Scene Management Options
ECS Framework provides two scene management approaches:
1. **[SceneManager](./scene-manager)** - Suitable for 95% of game applications
- Single-player games, simple multiplayer games, mobile games
- Lightweight, simple and intuitive API
- Supports scene transitions
2. **[WorldManager](./world-manager)** - Suitable for advanced multi-world isolation scenarios
- MMO game servers, game room systems
- Multi-World management, each World can contain multiple scenes
- Completely isolated independent environments
This document focuses on the usage of the Scene class itself. For detailed information about scene managers, please refer to the corresponding documentation.
## Creating a Scene
### Inheriting the Scene Class
**Recommended: Inherit the Scene class to create custom scenes**
```typescript
import { Scene, EntitySystem } from '@esengine/ecs-framework';
class GameScene extends Scene {
protected initialize(): void {
// Set scene name
this.name = "GameScene";
// Add systems
this.addSystem(new MovementSystem());
this.addSystem(new RenderSystem());
this.addSystem(new PhysicsSystem());
// Create initial entities
this.createInitialEntities();
}
private createInitialEntities(): void {
// Create player
const player = this.createEntity("Player");
player.addComponent(new Position(400, 300));
player.addComponent(new Health(100));
player.addComponent(new PlayerController());
// Create enemies
for (let i = 0; i < 5; i++) {
const enemy = this.createEntity(`Enemy_${i}`);
enemy.addComponent(new Position(Math.random() * 800, Math.random() * 600));
enemy.addComponent(new Health(50));
enemy.addComponent(new EnemyAI());
}
}
public onStart(): void {
console.log("Game scene started");
// Logic when scene starts
}
public unload(): void {
console.log("Game scene unloaded");
// Cleanup logic when scene unloads
}
}
```
### Using Scene Configuration
```typescript
import { ISceneConfig } from '@esengine/ecs-framework';
const config: ISceneConfig = {
name: "MainGame",
enableEntityDirectUpdate: false
};
class ConfiguredScene extends Scene {
constructor() {
super(config);
}
}
```
## Scene Lifecycle
Scene provides complete lifecycle management:
```typescript
class ExampleScene extends Scene {
protected initialize(): void {
// Scene initialization: setup systems and initial entities
console.log("Scene initializing");
}
public onStart(): void {
// Scene starts running: game logic begins execution
console.log("Scene starting");
}
public unload(): void {
// Scene unloading: cleanup resources
console.log("Scene unloading");
}
}
// Using scenes (lifecycle automatically managed by framework)
const scene = new ExampleScene();
// Scene's initialize(), begin(), update(), end() are automatically called by the framework
```
**Lifecycle Methods**:
1. `initialize()` - Scene initialization, setup systems and initial entities
2. `begin()` / `onStart()` - Scene starts running
3. `update()` - Per-frame update (called by scene manager)
4. `end()` / `unload()` - Scene unloading, cleanup resources
## Entity Management
### Creating Entities
```typescript
class EntityScene extends Scene {
createGameEntities(): void {
// Create single entity
const player = this.createEntity("Player");
// Batch create entities (high performance)
const bullets = this.createEntities(100, "Bullet");
// Add components to batch-created entities
bullets.forEach((bullet, index) => {
bullet.addComponent(new Position(index * 10, 100));
bullet.addComponent(new Velocity(Math.random() * 200 - 100, -300));
});
}
}
```
### Finding Entities
```typescript
class SearchScene extends Scene {
findEntities(): void {
// Find by name
const player = this.findEntity("Player");
const player2 = this.getEntityByName("Player"); // Alias method
// Find by ID
const entity = this.findEntityById(123);
// Find by tag
const enemies = this.findEntitiesByTag(2);
const enemies2 = this.getEntitiesByTag(2); // Alias method
if (player) {
console.log(`Found player: ${player.name}`);
}
console.log(`Found ${enemies.length} enemies`);
}
}
```
### Destroying Entities
```typescript
class DestroyScene extends Scene {
cleanupEntities(): void {
// Destroy all entities
this.destroyAllEntities();
// Single entity destruction through the entity itself
const enemy = this.findEntity("Enemy_1");
if (enemy) {
enemy.destroy(); // Entity is automatically removed from the scene
}
}
}
```
## System Management
### Adding and Removing Systems
```typescript
class SystemScene extends Scene {
protected initialize(): void {
// Add systems
const movementSystem = new MovementSystem();
this.addSystem(movementSystem);
// Set system update order
movementSystem.updateOrder = 1;
// Add more systems
this.addSystem(new PhysicsSystem());
this.addSystem(new RenderSystem());
}
public removeUnnecessarySystems(): void {
// Get system
const physicsSystem = this.getEntityProcessor(PhysicsSystem);
// Remove system
if (physicsSystem) {
this.removeSystem(physicsSystem);
}
}
}
```
## Event System
Scene has a built-in type-safe event system:
```typescript
class EventScene extends Scene {
protected initialize(): void {
// Listen to events
this.eventSystem.on('player_died', this.onPlayerDied.bind(this));
this.eventSystem.on('enemy_spawned', this.onEnemySpawned.bind(this));
this.eventSystem.on('level_complete', this.onLevelComplete.bind(this));
}
private onPlayerDied(data: any): void {
console.log('Player died event');
// Handle player death
}
private onEnemySpawned(data: any): void {
console.log('Enemy spawned event');
// Handle enemy spawn
}
private onLevelComplete(data: any): void {
console.log('Level complete event');
// Handle level completion
}
public triggerGameEvent(): void {
// Send event (synchronous)
this.eventSystem.emitSync('custom_event', {
message: "This is a custom event",
timestamp: Date.now()
});
// Send event (asynchronous)
this.eventSystem.emit('async_event', {
data: "Async event data"
});
}
}
```
## Best Practices
### 1. Scene Responsibility Separation
```typescript
// Good scene design - clear responsibilities
class MenuScene extends Scene {
// Only handles menu-related logic
}
class GameScene extends Scene {
// Only handles gameplay logic
}
class InventoryScene extends Scene {
// Only handles inventory logic
}
// Avoid this design - mixed responsibilities
class MegaScene extends Scene {
// Contains menu, game, inventory, and all other logic
}
```
### 2. Proper System Organization
```typescript
class OrganizedScene extends Scene {
protected initialize(): void {
// Add systems by function and dependencies
this.addInputSystems();
this.addLogicSystems();
this.addRenderSystems();
}
private addInputSystems(): void {
this.addSystem(new InputSystem());
}
private addLogicSystems(): void {
this.addSystem(new MovementSystem());
this.addSystem(new PhysicsSystem());
this.addSystem(new CollisionSystem());
}
private addRenderSystems(): void {
this.addSystem(new RenderSystem());
this.addSystem(new UISystem());
}
}
```
### 3. Resource Management
```typescript
class ResourceScene extends Scene {
private textures: Map<string, any> = new Map();
private sounds: Map<string, any> = new Map();
protected initialize(): void {
this.loadResources();
}
private loadResources(): void {
// Load resources needed by the scene
this.textures.set('player', this.loadTexture('player.png'));
this.sounds.set('bgm', this.loadSound('bgm.mp3'));
}
public unload(): void {
// Cleanup resources
this.textures.clear();
this.sounds.clear();
console.log('Scene resources cleaned up');
}
private loadTexture(path: string): any {
// Load texture
return null;
}
private loadSound(path: string): any {
// Load sound
return null;
}
}
```
## Next Steps
- Learn about [SceneManager](./scene-manager) - Simple scene management for most games
- Learn about [WorldManager](./world-manager) - For scenarios requiring multi-world isolation
- Learn about [Persistent Entity](./persistent-entity) - Keep entities across scene transitions (v2.3.0+)
Scene is the core container of the ECS framework. Proper scene management makes your game architecture clearer, more modular, and easier to maintain.

File diff suppressed because it is too large Load Diff

View File

@@ -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.

View File

@@ -1,570 +0,0 @@
# Worker System
The Worker System (WorkerEntitySystem) is a multi-threaded processing system based on Web Workers in the ECS framework. It's designed for compute-intensive tasks, fully utilizing multi-core CPU performance for true parallel computing.
## Core Features
- **True Parallel Computing**: Execute compute-intensive tasks in background threads using Web Workers
- **Automatic Load Balancing**: Automatically distribute workload based on CPU core count
- **SharedArrayBuffer Optimization**: Zero-copy data sharing for improved large-scale computation performance
- **Graceful Degradation**: Automatic fallback to main thread processing when Workers are not supported
- **Type Safety**: Full TypeScript support and type checking
## Basic Usage
### Simple Physics System Example
```typescript
interface PhysicsData {
id: number;
x: number;
y: number;
vx: number;
vy: number;
mass: number;
radius: number;
}
@ECSSystem('Physics')
class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsData> {
constructor() {
super(Matcher.all(Position, Velocity, Physics), {
enableWorker: true, // Enable Worker parallel processing
workerCount: 8, // Worker count, auto-limited to hardware capacity
entitiesPerWorker: 100, // Entities per Worker
useSharedArrayBuffer: true, // Enable SharedArrayBuffer optimization
entityDataSize: 7, // Data size per entity
maxEntities: 10000, // Maximum entity count
systemConfig: { // Configuration passed to Worker
gravity: 100,
friction: 0.95
}
});
}
// Data extraction: Convert Entity to serializable data
protected extractEntityData(entity: Entity): PhysicsData {
const position = entity.getComponent(Position);
const velocity = entity.getComponent(Velocity);
const physics = entity.getComponent(Physics);
return {
id: entity.id,
x: position.x,
y: position.y,
vx: velocity.x,
vy: velocity.y,
mass: physics.mass,
radius: physics.radius
};
}
// Worker processing function: Pure function executed in Worker
protected workerProcess(
entities: PhysicsData[],
deltaTime: number,
config: any
): PhysicsData[] {
return entities.map(entity => {
// Apply gravity
entity.vy += config.gravity * deltaTime;
// Update position
entity.x += entity.vx * deltaTime;
entity.y += entity.vy * deltaTime;
// Apply friction
entity.vx *= config.friction;
entity.vy *= config.friction;
return entity;
});
}
// Apply results: Apply Worker processing results back to Entity
protected applyResult(entity: Entity, result: PhysicsData): void {
const position = entity.getComponent(Position);
const velocity = entity.getComponent(Velocity);
position.x = result.x;
position.y = result.y;
velocity.x = result.vx;
velocity.y = result.vy;
}
// SharedArrayBuffer optimization support
protected getDefaultEntityDataSize(): number {
return 7; // id, x, y, vx, vy, mass, radius
}
protected writeEntityToBuffer(entityData: PhysicsData, offset: number): void {
if (!this.sharedFloatArray) return;
this.sharedFloatArray[offset + 0] = entityData.id;
this.sharedFloatArray[offset + 1] = entityData.x;
this.sharedFloatArray[offset + 2] = entityData.y;
this.sharedFloatArray[offset + 3] = entityData.vx;
this.sharedFloatArray[offset + 4] = entityData.vy;
this.sharedFloatArray[offset + 5] = entityData.mass;
this.sharedFloatArray[offset + 6] = entityData.radius;
}
protected readEntityFromBuffer(offset: number): PhysicsData | null {
if (!this.sharedFloatArray) return null;
return {
id: this.sharedFloatArray[offset + 0],
x: this.sharedFloatArray[offset + 1],
y: this.sharedFloatArray[offset + 2],
vx: this.sharedFloatArray[offset + 3],
vy: this.sharedFloatArray[offset + 4],
mass: this.sharedFloatArray[offset + 5],
radius: this.sharedFloatArray[offset + 6]
};
}
}
```
## Configuration Options
The Worker system supports rich configuration options:
```typescript
interface WorkerSystemConfig {
/** Enable Worker parallel processing */
enableWorker?: boolean;
/** Worker count, defaults to CPU core count, auto-limited to system maximum */
workerCount?: number;
/** Entities per Worker for load distribution control */
entitiesPerWorker?: number;
/** System configuration data passed to Worker */
systemConfig?: any;
/** Enable SharedArrayBuffer optimization */
useSharedArrayBuffer?: boolean;
/** Float32 count per entity in SharedArrayBuffer */
entityDataSize?: number;
/** Maximum entity count (for SharedArrayBuffer pre-allocation) */
maxEntities?: number;
/** Pre-compiled Worker script path (for platforms like WeChat Mini Game that don't support dynamic scripts) */
workerScriptPath?: string;
}
```
### Configuration Recommendations
```typescript
constructor() {
super(matcher, {
// Decide based on task complexity
enableWorker: this.shouldUseWorker(),
// Worker count: System auto-limits to hardware capacity
workerCount: 8, // Request 8 Workers, actual count limited by CPU cores
// Entities per Worker (optional)
entitiesPerWorker: 200, // Precise load distribution control
// Enable SharedArrayBuffer for many simple calculations
useSharedArrayBuffer: this.entityCount > 1000,
// Set according to actual data structure
entityDataSize: 8, // Ensure it matches data structure
// Estimated maximum entity count
maxEntities: 10000,
// Global configuration passed to Worker
systemConfig: {
gravity: 9.8,
friction: 0.95,
worldBounds: { width: 1920, height: 1080 }
}
});
}
private shouldUseWorker(): boolean {
// Decide based on entity count and complexity
return this.expectedEntityCount > 100;
}
// Get system info
getSystemInfo() {
const info = this.getWorkerInfo();
console.log(`Worker count: ${info.workerCount}/${info.maxSystemWorkerCount}`);
console.log(`Entities per Worker: ${info.entitiesPerWorker || 'auto'}`);
console.log(`Current mode: ${info.currentMode}`);
}
```
## Processing Modes
The Worker system supports two processing modes:
### 1. Traditional Worker Mode
Data is serialized and passed between main thread and Workers:
```typescript
// Suitable for: Complex computation logic, moderate entity count
constructor() {
super(matcher, {
enableWorker: true,
useSharedArrayBuffer: false, // Use traditional mode
workerCount: 2
});
}
protected workerProcess(entities: EntityData[], deltaTime: number): EntityData[] {
// Complex algorithm logic
return entities.map(entity => {
// AI decisions, pathfinding, etc.
return this.complexAILogic(entity, deltaTime);
});
}
```
### 2. SharedArrayBuffer Mode
Zero-copy data sharing, suitable for many simple calculations:
```typescript
// Suitable for: Many entities with simple calculations
constructor() {
super(matcher, {
enableWorker: true,
useSharedArrayBuffer: true, // Enable shared memory
entityDataSize: 6,
maxEntities: 10000
});
}
protected getSharedArrayBufferProcessFunction(): SharedArrayBufferProcessFunction {
return function(sharedFloatArray: Float32Array, startIndex: number, endIndex: number, deltaTime: number, config: any) {
const entitySize = 6;
for (let i = startIndex; i < endIndex; i++) {
const offset = i * entitySize;
// Read data
let x = sharedFloatArray[offset];
let y = sharedFloatArray[offset + 1];
let vx = sharedFloatArray[offset + 2];
let vy = sharedFloatArray[offset + 3];
// Physics calculations
vy += config.gravity * deltaTime;
x += vx * deltaTime;
y += vy * deltaTime;
// Write back data
sharedFloatArray[offset] = x;
sharedFloatArray[offset + 1] = y;
sharedFloatArray[offset + 2] = vx;
sharedFloatArray[offset + 3] = vy;
}
};
}
```
## Use Cases
The Worker system is particularly suitable for:
### 1. Physics Simulation
- **Gravity systems**: Gravity calculations for many entities
- **Collision detection**: Complex collision algorithms
- **Fluid simulation**: Particle fluid systems
- **Cloth simulation**: Vertex physics calculations
### 2. AI Computation
- **Pathfinding**: A*, Dijkstra algorithms
- **Behavior trees**: Complex AI decision logic
- **Swarm intelligence**: Boid, fish school algorithms
- **Neural networks**: Simple AI inference
### 3. Data Processing
- **Bulk entity updates**: State machines, lifecycle management
- **Statistical calculations**: Game data analysis
- **Image processing**: Texture generation, effect calculations
- **Audio processing**: Sound synthesis, spectrum analysis
## Best Practices
### 1. Worker Function Requirements
```typescript
// Recommended: Worker processing function is a pure function
protected workerProcess(entities: PhysicsData[], deltaTime: number, config: any): PhysicsData[] {
// Only use parameters and standard JavaScript APIs
return entities.map(entity => {
// Pure computation logic, no external state dependencies
entity.y += entity.velocity * deltaTime;
return entity;
});
}
// Avoid: Using external references in Worker function
protected workerProcess(entities: PhysicsData[], deltaTime: number): PhysicsData[] {
// this and external variables are not available in Worker
return entities.map(entity => {
entity.y += this.someProperty; // Error
return entity;
});
}
```
### 2. Data Design
```typescript
// Recommended: Reasonable data design
interface SimplePhysicsData {
x: number;
y: number;
vx: number;
vy: number;
// Keep data structure simple for easy serialization
}
// Avoid: Complex nested objects
interface ComplexData {
transform: {
position: { x: number; y: number };
rotation: { angle: number };
};
// Complex nested structures increase serialization overhead
}
```
### 3. Worker Count Control
```typescript
// Recommended: Flexible Worker configuration
constructor() {
super(matcher, {
// Specify needed Worker count, system auto-limits to hardware capacity
workerCount: 8, // Request 8 Workers
entitiesPerWorker: 100, // 100 entities per Worker
enableWorker: this.shouldUseWorker(), // Conditional enable
});
}
private shouldUseWorker(): boolean {
// Decide based on entity count and complexity
return this.expectedEntityCount > 100;
}
// Get actual Worker info
checkWorkerConfiguration() {
const info = this.getWorkerInfo();
console.log(`Requested Workers: 8`);
console.log(`Actual Workers: ${info.workerCount}`);
console.log(`System maximum: ${info.maxSystemWorkerCount}`);
console.log(`Entities per Worker: ${info.entitiesPerWorker || 'auto'}`);
}
```
### 4. Performance Monitoring
```typescript
// Recommended: Performance monitoring
public getPerformanceMetrics(): WorkerPerformanceMetrics {
return {
...this.getWorkerInfo(),
entityCount: this.entities.length,
averageProcessTime: this.getAverageProcessTime(),
workerUtilization: this.getWorkerUtilization()
};
}
```
## Performance Optimization Tips
### 1. Compute Intensity Assessment
Only use Workers for compute-intensive tasks to avoid thread overhead for simple calculations.
### 2. Data Transfer Optimization
- Use SharedArrayBuffer to reduce serialization overhead
- Keep data structures simple and flat
- Avoid frequent large data transfers
### 3. Degradation Strategy
Always provide main thread fallback to ensure normal operation in environments without Worker support.
### 4. Memory Management
Clean up Worker pools and shared buffers promptly to avoid memory leaks.
### 5. Load Balancing
Use `entitiesPerWorker` parameter to precisely control load distribution, avoiding idle Workers while others are overloaded.
## WeChat Mini Game Support
WeChat Mini Game has special Worker limitations and doesn't support dynamic Worker script creation. ESEngine provides the `@esengine/worker-generator` CLI tool to solve this problem.
### WeChat Mini Game Worker Limitations
| Feature | Browser | WeChat Mini Game |
|---------|---------|------------------|
| Dynamic scripts (Blob URL) | Supported | Not supported |
| Worker count | Multiple | Maximum 1 |
| Script source | Any | Must be in code package |
| SharedArrayBuffer | Requires COOP/COEP | Limited support |
### Using Worker Generator CLI
#### 1. Install the Tool
```bash
pnpm add -D @esengine/worker-generator
```
#### 2. Configure workerScriptPath
Configure `workerScriptPath` in your WorkerEntitySystem subclass:
```typescript
@ECSSystem('Physics')
class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsData> {
constructor() {
super(Matcher.all(Position, Velocity, Physics), {
enableWorker: true,
workerScriptPath: 'workers/physics-worker.js', // Specify Worker file path
systemConfig: {
gravity: 100,
friction: 0.95
}
});
}
protected workerProcess(
entities: PhysicsData[],
deltaTime: number,
config: any
): PhysicsData[] {
// Physics calculation logic
return entities.map(entity => {
entity.vy += config.gravity * deltaTime;
entity.x += entity.vx * deltaTime;
entity.y += entity.vy * deltaTime;
return entity;
});
}
// ... other methods
}
```
#### 3. Generate Worker Files
Run the CLI tool to automatically extract `workerProcess` functions and generate WeChat Mini Game compatible Worker files:
```bash
# Basic usage
npx esengine-worker-gen --src ./src --wechat
# Full options
npx esengine-worker-gen \
--src ./src \ # Source directory
--wechat \ # Generate WeChat Mini Game compatible code
--mapping \ # Generate worker-mapping.json
--verbose # Verbose output
```
The CLI tool will:
1. Scan source directory for all `WorkerEntitySystem` subclasses
2. Read each class's `workerScriptPath` configuration
3. Extract `workerProcess` method body
4. Convert to ES5 syntax (WeChat Mini Game compatible)
5. Generate to configured path
#### 4. Configure game.json
Configure workers directory in WeChat Mini Game's `game.json`:
```json
{
"deviceOrientation": "portrait",
"workers": "workers"
}
```
#### 5. Project Structure
```
your-game/
├── game.js
├── game.json # Configure "workers": "workers"
├── src/
│ └── systems/
│ └── PhysicsSystem.ts # workerScriptPath: 'workers/physics-worker.js'
└── workers/
├── physics-worker.js # Auto-generated
└── worker-mapping.json # Auto-generated
```
### Temporarily Disabling Workers
If you need to temporarily disable Workers (e.g., for debugging), there are two ways:
#### Method 1: Configuration Disable
```typescript
constructor() {
super(matcher, {
enableWorker: false, // Disable Worker, use main thread processing
// ...
});
}
```
#### Method 2: Platform Adapter Disable
Return Worker not supported in custom platform adapter:
```typescript
class MyPlatformAdapter implements IPlatformAdapter {
isWorkerSupported(): boolean {
return false; // Return false to disable Worker
}
// ...
}
```
### Important Notes
1. **Re-run CLI tool after each `workerProcess` modification** to generate new Worker files
2. **Worker functions must be pure functions**, cannot depend on `this` or external variables:
```typescript
// Correct: Only use parameters
protected workerProcess(entities, deltaTime, config) {
return entities.map(e => {
e.y += config.gravity * deltaTime;
return e;
});
}
// Wrong: Using this
protected workerProcess(entities, deltaTime, config) {
return entities.map(e => {
e.y += this.gravity * deltaTime; // Cannot access this in Worker
return e;
});
}
```
3. **Pass configuration data via `systemConfig`**, not class properties
4. **Developer tool warnings can be ignored**:
- `getNetworkType:fail not support` - WeChat DevTools internal behavior
- `SharedArrayBuffer will require cross-origin isolation` - Development environment warning, won't appear on real devices
## Online Demo
See the complete Worker system demo: [Worker System Demo](https://esengine.github.io/ecs-framework/demos/worker-system/)
The demo showcases:
- Multi-threaded physics computation
- Real-time performance comparison
- SharedArrayBuffer optimization
- Parallel processing of many entities
The Worker system provides powerful parallel computing capabilities for the ECS framework, allowing you to fully utilize modern multi-core processor performance, offering efficient solutions for complex game logic and compute-intensive tasks.

View File

@@ -192,6 +192,6 @@ export class AttackAction implements INodeExecutor {
## 获取帮助
- 提交 [Issue](https://github.com/esengine/esengine/issues)
- 提交 [Issue](https://github.com/esengine/ecs-framework/issues)
- 加入社区讨论
- 参考文档中的完整代码示例

View File

@@ -430,137 +430,6 @@ class GameManager {
}
```
## 编译查询 (CompiledQuery)
> **v2.4.0+**
CompiledQuery 是一个轻量级的查询工具,提供类型安全的组件访问和变更检测支持。适合临时查询、工具开发和简单的迭代场景。
### 基本用法
```typescript
// 创建编译查询
const query = scene.querySystem.compile(Position, Velocity);
// 类型安全的遍历 - 组件参数自动推断类型
query.forEach((entity, pos, vel) => {
pos.x += vel.vx * deltaTime;
pos.y += vel.vy * deltaTime;
});
// 获取实体数量
console.log(`匹配实体数: ${query.count}`);
// 获取第一个匹配的实体
const first = query.first();
if (first) {
const [entity, pos, vel] = first;
console.log(`第一个实体: ${entity.name}`);
}
```
### 变更检测
CompiledQuery 支持基于 epoch 的变更检测:
```typescript
class RenderSystem extends EntitySystem {
private _query: CompiledQuery<[typeof Transform, typeof Sprite]>;
private _lastEpoch = 0;
protected onInitialize(): void {
this._query = this.scene!.querySystem.compile(Transform, Sprite);
}
protected process(entities: readonly Entity[]): void {
// 只处理 Transform 或 Sprite 发生变化的实体
this._query.forEachChanged(this._lastEpoch, (entity, transform, sprite) => {
this.updateRenderData(entity, transform, sprite);
});
// 保存当前 epoch 作为下次检查的起点
this._lastEpoch = this.scene!.epochManager.current;
}
private updateRenderData(entity: Entity, transform: Transform, sprite: Sprite): void {
// 更新渲染数据
}
}
```
### 函数式 API
CompiledQuery 提供了丰富的函数式 API
```typescript
const query = scene.querySystem.compile(Position, Health);
// map - 转换实体数据
const positions = query.map((entity, pos, health) => ({
x: pos.x,
y: pos.y,
healthPercent: health.current / health.max
}));
// filter - 过滤实体
const lowHealthEntities = query.filter((entity, pos, health) => {
return health.current < health.max * 0.2;
});
// find - 查找第一个匹配的实体
const target = query.find((entity, pos, health) => {
return health.current > 0 && pos.x > 100;
});
// toArray - 转换为数组
const allData = query.toArray();
for (const [entity, pos, health] of allData) {
console.log(`${entity.name}: ${pos.x}, ${pos.y}`);
}
// any/empty - 检查是否有匹配
if (query.any()) {
console.log('有匹配的实体');
}
if (query.empty()) {
console.log('没有匹配的实体');
}
```
### CompiledQuery vs EntitySystem
| 特性 | CompiledQuery | EntitySystem |
|------|---------------|--------------|
| **用途** | 轻量级查询工具 | 完整的系统逻辑 |
| **生命周期** | 无 | 完整 (onInitialize, onDestroy 等) |
| **调度集成** | 无 | 支持 @Stage, @Before, @After |
| **变更检测** | forEachChanged | forEachChanged |
| **事件监听** | 无 | addEventListener |
| **命令缓冲** | 无 | this.commands |
| **类型安全组件** | forEach 参数自动推断 | 需要手动 getComponent |
| **适用场景** | 临时查询、工具、原型 | 核心游戏逻辑 |
**选择建议**
- 使用 **EntitySystem** 处理核心游戏逻辑移动、战斗、AI 等)
- 使用 **CompiledQuery** 进行一次性查询、工具开发或简单迭代
### CompiledQuery API 参考
| 方法 | 说明 |
|------|------|
| `forEach(callback)` | 遍历所有匹配实体,类型安全的组件参数 |
| `forEachChanged(sinceEpoch, callback)` | 只遍历变更的实体 |
| `first()` | 获取第一个匹配的实体和组件 |
| `toArray()` | 转换为 [entity, ...components] 数组 |
| `map(callback)` | 映射转换 |
| `filter(predicate)` | 过滤实体 |
| `find(predicate)` | 查找第一个满足条件的实体 |
| `any()` | 是否有任何匹配 |
| `empty()` | 是否没有匹配 |
| `count` | 匹配的实体数量 |
| `entities` | 匹配的实体列表(只读) |
## 最佳实践
### 1. 优先使用 EntitySystem

View File

@@ -293,152 +293,6 @@ entity.components.forEach(component => {
实体是 ECS 架构的核心概念之一,理解如何正确使用实体将帮助你构建高效、可维护的游戏代码。
## 实体句柄 (EntityHandle)
实体句柄是一种安全的实体引用方式,用于解决"引用已销毁实体"的问题。
### 问题场景
假设你的 AI 系统需要追踪一个目标敌人:
```typescript
// 错误做法:直接存储实体引用
class AISystem extends EntitySystem {
private targetEnemy: Entity | null = null;
setTarget(enemy: Entity) {
this.targetEnemy = enemy;
}
process() {
if (this.targetEnemy) {
// 危险!敌人可能已被销毁,但引用还在
// 更糟糕:这个内存位置可能被新实体复用了
const health = this.targetEnemy.getComponent(Health);
// 可能操作了错误的实体!
}
}
}
```
### 使用句柄的正确做法
每个实体创建时会自动分配一个句柄,通过 `entity.handle` 获取:
```typescript
import { EntityHandle, NULL_HANDLE, isValidHandle } from '@esengine/ecs-framework';
class AISystem extends EntitySystem {
// 存储句柄而非实体引用
private targetHandle: EntityHandle = NULL_HANDLE;
setTarget(enemy: Entity) {
// 保存敌人的句柄
this.targetHandle = enemy.handle;
}
process() {
if (!isValidHandle(this.targetHandle)) {
return; // 没有目标
}
// 通过句柄获取实体(自动检测是否有效)
const enemy = this.scene.findEntityByHandle(this.targetHandle);
if (!enemy) {
// 敌人已被销毁,清空引用
this.targetHandle = NULL_HANDLE;
return;
}
// 安全操作
const health = enemy.getComponent(Health);
}
}
```
### 完整示例:技能目标锁定
```typescript
import {
EntitySystem, Entity, EntityHandle, NULL_HANDLE, isValidHandle
} from '@esengine/ecs-framework';
@ECSSystem('SkillTargeting')
class SkillTargetingSystem extends EntitySystem {
// 存储多个目标的句柄
private lockedTargets: Map<Entity, EntityHandle> = new Map();
// 锁定目标
lockTarget(caster: Entity, target: Entity) {
this.lockedTargets.set(caster, target.handle);
}
// 获取锁定的目标
getLockedTarget(caster: Entity): Entity | null {
const handle = this.lockedTargets.get(caster);
if (!handle || !isValidHandle(handle)) {
return null;
}
// findEntityByHandle 会检查句柄是否有效
const target = this.scene.findEntityByHandle(handle);
if (!target) {
// 目标已死亡,清除锁定
this.lockedTargets.delete(caster);
}
return target;
}
// 释放技能
castSkill(caster: Entity) {
const target = this.getLockedTarget(caster);
if (!target) {
console.log('目标丢失,技能取消');
return;
}
// 安全地对目标造成伤害
const health = target.getComponent(Health);
if (health) {
health.current -= 10;
}
}
}
```
### 句柄 vs 实体引用
| 场景 | 推荐方式 |
|-----|---------|
| 同一帧内临时使用 | 直接用 `Entity` 引用 |
| 跨帧存储(如 AI 目标、技能目标) | 使用 `EntityHandle` |
| 需要序列化保存 | 使用 `EntityHandle`(数字类型) |
| 网络同步 | 使用 `EntityHandle`(可直接传输) |
### API 速查
```typescript
// 获取实体的句柄
const handle = entity.handle;
// 检查句柄是否非空
if (isValidHandle(handle)) { ... }
// 通过句柄获取实体(自动检测有效性)
const entity = scene.findEntityByHandle(handle);
// 检查句柄对应的实体是否存活
const alive = scene.handleManager.isAlive(handle);
// 空句柄常量
const emptyHandle = NULL_HANDLE;
```
## 下一步
- 了解 [层级系统](./hierarchy.md) 建立实体间的父子关系

View File

@@ -6,7 +6,7 @@
### 为什么不在 Entity 中内置层级?
传统的游戏对象模型将层级关系内置于实体中。ECS Framework 选择组件化方案的原因:
传统的游戏对象模型(如 Unity 的 GameObject将层级关系内置于实体中。ECS Framework 选择组件化方案的原因:
1. **ECS 组合原则**:层级是一种"功能",应该通过组件添加,而非所有实体都具备
2. **按需使用**:只有需要层级关系的实体才添加 `HierarchyComponent`

View File

@@ -1,360 +0,0 @@
# 持久化实体
> **版本**: v2.3.0+
持久化实体Persistent Entity是一种可以在场景切换时自动迁移到新场景的特殊实体。适用于需要跨场景保持状态的游戏对象如玩家、游戏管理器、音频管理器等。
## 基本概念
在 ECS 框架中,实体有两种生命周期策略:
| 策略 | 说明 | 默认 |
|-----|------|------|
| `SceneLocal` | 场景本地实体,场景切换时销毁 | ✓ |
| `Persistent` | 持久化实体,场景切换时自动迁移 | |
## 快速开始
### 创建持久化实体
```typescript
import { Scene } from '@esengine/ecs-framework';
class GameScene extends Scene {
protected initialize(): void {
// 创建持久化玩家实体
const player = this.createEntity('Player').setPersistent();
player.addComponent(new Position(100, 200));
player.addComponent(new PlayerData('Hero', 500));
// 创建普通敌人实体(场景切换时销毁)
const enemy = this.createEntity('Enemy');
enemy.addComponent(new Position(300, 200));
enemy.addComponent(new EnemyAI());
}
}
```
### 场景切换时的行为
```typescript
import { Core, Scene } from '@esengine/ecs-framework';
// 初始场景
class Level1Scene extends Scene {
protected initialize(): void {
// 玩家 - 持久化,会迁移到下一个场景
const player = this.createEntity('Player').setPersistent();
player.addComponent(new Position(0, 0));
player.addComponent(new Health(100));
// 敌人 - 场景本地,切换时销毁
const enemy = this.createEntity('Enemy');
enemy.addComponent(new Position(100, 100));
}
}
// 目标场景
class Level2Scene extends Scene {
protected initialize(): void {
// 新的敌人
const enemy = this.createEntity('Boss');
enemy.addComponent(new Position(200, 200));
}
public onStart(): void {
// 玩家已自动迁移到此场景
const player = this.findEntity('Player');
console.log(player !== null); // true
// 位置和血量数据完整保留
const position = player?.getComponent(Position);
const health = player?.getComponent(Health);
console.log(position?.x, position?.y); // 0, 0
console.log(health?.value); // 100
}
}
// 切换场景
Core.create({ debug: true });
Core.setScene(new Level1Scene());
// 稍后切换到 Level2
Core.loadScene(new Level2Scene());
// Player 实体自动迁移Enemy 实体被销毁
```
## API 参考
### Entity 方法
#### setPersistent()
将实体标记为持久化,场景切换时不会被销毁。
```typescript
public setPersistent(): this
```
**返回**: 返回实体本身,支持链式调用
**示例**:
```typescript
const player = scene.createEntity('Player')
.setPersistent();
player.addComponent(new Position(100, 200));
```
#### setSceneLocal()
将实体恢复为场景本地策略(默认)。
```typescript
public setSceneLocal(): this
```
**返回**: 返回实体本身,支持链式调用
**示例**:
```typescript
// 动态取消持久化
player.setSceneLocal();
```
#### isPersistent
检查实体是否为持久化实体。
```typescript
public get isPersistent(): boolean
```
**示例**:
```typescript
if (entity.isPersistent) {
console.log('这是持久化实体');
}
```
#### lifecyclePolicy
获取实体的生命周期策略。
```typescript
public get lifecyclePolicy(): EEntityLifecyclePolicy
```
**示例**:
```typescript
import { EEntityLifecyclePolicy } from '@esengine/ecs-framework';
if (entity.lifecyclePolicy === EEntityLifecyclePolicy.Persistent) {
console.log('持久化实体');
}
```
### Scene 方法
#### findPersistentEntities()
查找场景中所有持久化实体。
```typescript
public findPersistentEntities(): Entity[]
```
**返回**: 持久化实体数组
**示例**:
```typescript
const persistentEntities = scene.findPersistentEntities();
console.log(`场景中有 ${persistentEntities.length} 个持久化实体`);
```
#### extractPersistentEntities()
提取并从场景中移除所有持久化实体(通常由框架内部调用)。
```typescript
public extractPersistentEntities(): Entity[]
```
**返回**: 被提取的持久化实体数组
#### receiveMigratedEntities()
接收迁移过来的实体(通常由框架内部调用)。
```typescript
public receiveMigratedEntities(entities: Entity[]): void
```
**参数**:
- `entities` - 要接收的实体数组
## 使用场景
### 1. 玩家实体跨关卡
```typescript
class PlayerSetupScene extends Scene {
protected initialize(): void {
// 玩家在所有关卡中保持状态
const player = this.createEntity('Player').setPersistent();
player.addComponent(new Transform(0, 0));
player.addComponent(new Health(100));
player.addComponent(new Inventory());
player.addComponent(new PlayerStats());
}
}
class Level1 extends Scene { /* ... */ }
class Level2 extends Scene { /* ... */ }
class Level3 extends Scene { /* ... */ }
// 玩家实体会自动在所有关卡间迁移
Core.setScene(new PlayerSetupScene());
// ... 游戏进行
Core.loadScene(new Level1());
// ... 关卡完成
Core.loadScene(new Level2());
// 玩家数据(血量、物品栏、属性)完整保留
```
### 2. 全局管理器
```typescript
class BootstrapScene extends Scene {
protected initialize(): void {
// 音频管理器 - 跨场景保持
const audioManager = this.createEntity('AudioManager').setPersistent();
audioManager.addComponent(new AudioController());
// 成就管理器 - 跨场景保持
const achievementManager = this.createEntity('AchievementManager').setPersistent();
achievementManager.addComponent(new AchievementTracker());
// 游戏设置 - 跨场景保持
const settings = this.createEntity('GameSettings').setPersistent();
settings.addComponent(new SettingsData());
}
}
```
### 3. 动态切换持久化状态
```typescript
class GameScene extends Scene {
protected initialize(): void {
// 初始创建为普通实体
const companion = this.createEntity('Companion');
companion.addComponent(new Transform(0, 0));
companion.addComponent(new CompanionAI());
// 监听招募事件
this.eventSystem.on('companion:recruited', () => {
// 招募后变为持久化实体
companion.setPersistent();
console.log('同伴已加入队伍,将跟随玩家跨场景');
});
// 监听解散事件
this.eventSystem.on('companion:dismissed', () => {
// 解散后恢复为场景本地实体
companion.setSceneLocal();
console.log('同伴已离队,不再跨场景');
});
}
}
```
## 最佳实践
### 1. 明确标识持久化实体
```typescript
// 推荐:在创建时立即标记
const player = this.createEntity('Player').setPersistent();
// 不推荐:创建后再标记(容易遗漏)
const player = this.createEntity('Player');
// ... 很多代码 ...
player.setPersistent(); // 容易忘记
```
### 2. 合理使用持久化
```typescript
// ✓ 适合持久化的实体
const player = this.createEntity('Player').setPersistent(); // 玩家
const gameManager = this.createEntity('GameManager').setPersistent(); // 全局管理器
const audioManager = this.createEntity('AudioManager').setPersistent(); // 音频系统
// ✗ 不应持久化的实体
const bullet = this.createEntity('Bullet'); // 临时对象
const enemy = this.createEntity('Enemy'); // 关卡特定敌人
const particle = this.createEntity('Particle'); // 特效粒子
```
### 3. 检查迁移后的实体
```typescript
class NewScene extends Scene {
public onStart(): void {
// 检查预期的持久化实体是否存在
const player = this.findEntity('Player');
if (!player) {
console.error('玩家实体未正确迁移!');
// 处理错误情况
}
}
}
```
### 4. 避免循环引用
```typescript
// ✗ 避免:持久化实体引用场景本地实体
class BadScene extends Scene {
protected initialize(): void {
const player = this.createEntity('Player').setPersistent();
const enemy = this.createEntity('Enemy');
// 危险player 持久化但 enemy 不是
// 场景切换后 enemy 被销毁,引用失效
player.addComponent(new TargetComponent(enemy));
}
}
// ✓ 推荐:使用 ID 引用或事件系统
class GoodScene extends Scene {
protected initialize(): void {
const player = this.createEntity('Player').setPersistent();
const enemy = this.createEntity('Enemy');
// 存储 ID 而非直接引用
player.addComponent(new TargetComponent(enemy.id));
// 或使用事件系统通信
}
}
```
## 注意事项
1. **已销毁的实体不会迁移**:如果实体在场景切换前被销毁,即使标记为持久化也不会迁移。
2. **组件数据完整保留**:迁移时所有组件及其状态都会保留。
3. **场景引用会更新**:迁移后实体的 `scene` 属性会指向新场景。
4. **查询系统会更新**:迁移的实体会自动注册到新场景的查询系统中。
5. **延迟切换同样生效**:使用 `Core.loadScene()` 延迟切换时,持久化实体同样会迁移。
## 相关文档
- [场景管理](./scene.md) - 了解场景的基本使用
- [SceneManager](./scene-manager.md) - 了解场景切换
- [WorldManager](./world-manager.md) - 了解多世界管理

View File

@@ -6,198 +6,409 @@
## 特性支持
| 特性 | 支持情况 | 说明 |
|------|----------|------|
| **Worker** | ✅ 支持 | 需要使用预编译文件,配置 `workerScriptPath` |
| **SharedArrayBuffer** | ❌ 不支持 | 微信小游戏环境不支持 |
| **Transferable Objects** | ❌ 不支持 | 只支持可序列化对象 |
| **高精度时间** | ✅ 支持 | 使用 `wx.getPerformance()` |
| **设备信息** | ✅ 支持 | 完整的微信小游戏设备信息 |
-**Worker**: 支持(通过 `wx.createWorker` 创建,需要配置 game.json
-**SharedArrayBuffer**: 不支持
-**Transferable Objects**: 不支持(只支持可序列化对象)
-**高精度时间**: 使用 `Date.now()``wx.getPerformance()`
-**设备信息**: 完整的微信小游戏设备信息
## WorkerEntitySystem 使用方式
## 完整实现
### 重要:微信小游戏 Worker 限制
```typescript
import type {
IPlatformAdapter,
PlatformWorker,
WorkerCreationOptions,
PlatformConfig,
WeChatDeviceInfo
} from '@esengine/ecs-framework';
微信小游戏的 Worker 有以下限制:
- **Worker 脚本必须在代码包内**,不能动态生成
- **必须在 `game.json` 中配置** `workers` 目录
- **最多只能创建 1 个 Worker**
/**
* 微信小游戏平台适配器
* 支持微信小游戏环境
*/
export class WeChatMiniGameAdapter implements IPlatformAdapter {
public readonly name = 'wechat-minigame';
public readonly version: string;
private systemInfo: any;
因此,使用 `WorkerEntitySystem` 时有两种方式:
1. **推荐:使用 CLI 工具自动生成** Worker 文件
2. 手动创建 Worker 文件
### 方式一:使用 CLI 工具自动生成(推荐)
我们提供了 `@esengine/worker-generator` 工具,可以自动从你的 TypeScript 代码中提取 `workerProcess` 函数并生成微信小游戏兼容的 Worker 文件。
#### 安装
```bash
pnpm add -D @esengine/worker-generator
# 或
npm install --save-dev @esengine/worker-generator
```
#### 使用
```bash
# 扫描 src 目录,生成 Worker 文件到 workers 目录
npx esengine-worker-gen --src ./src --out ./workers --wechat
# 查看帮助
npx esengine-worker-gen --help
```
#### 参数说明
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `-s, --src <dir>` | 源代码目录 | `./src` |
| `-o, --out <dir>` | 输出目录 | `./workers` |
| `-w, --wechat` | 生成微信小游戏兼容代码 | `false` |
| `-m, --mapping` | 生成 worker-mapping.json | `true` |
| `-t, --tsconfig <path>` | TypeScript 配置文件路径 | 自动查找 |
| `-v, --verbose` | 详细输出 | `false` |
#### 示例输出
```
🔧 ESEngine Worker Generator
Source directory: /project/src
Output directory: /project/workers
WeChat mode: Yes
Scanning for WorkerEntitySystem classes...
✓ Found 1 WorkerEntitySystem class(es):
- PhysicsSystem (src/systems/PhysicsSystem.ts)
Generating Worker files...
✓ Successfully generated 1 Worker file(s):
- PhysicsSystem -> workers/physics-system-worker.js
📝 Usage:
1. Copy the generated files to your project's workers/ directory
2. Configure game.json (WeChat): { "workers": "workers" }
3. In your System constructor, add:
workerScriptPath: 'workers/physics-system-worker.js'
```
#### 在构建流程中集成
```json
// package.json
{
"scripts": {
"build:workers": "esengine-worker-gen --src ./src --out ./workers --wechat",
"build": "pnpm build:workers && your-build-command"
constructor() {
// 获取微信小游戏版本信息
this.systemInfo = this.getSystemInfo();
this.version = this.systemInfo.version || 'unknown';
}
/**
* 检查是否支持Worker
*/
public isWorkerSupported(): boolean {
// 微信小游戏支持Worker通过wx.createWorker创建
return typeof wx !== 'undefined' && typeof wx.createWorker === 'function';
}
```
### 方式二:手动创建 Worker 文件
/**
* 检查是否支持SharedArrayBuffer不支持
*/
public isSharedArrayBufferSupported(): boolean {
return false; // 微信小游戏不支持SharedArrayBuffer
}
如果你不想使用 CLI 工具,也可以手动创建 Worker 文件。
/**
* 获取硬件并发数
*/
public getHardwareConcurrency(): number {
// 微信小游戏官方限制:最多只能创建 1 个 Worker
return 1;
}
在项目中创建 `workers/entity-worker.js`
```javascript
// workers/entity-worker.js
// 微信小游戏 WorkerEntitySystem 通用 Worker 模板
let sharedFloatArray = null;
worker.onMessage(function(e) {
const { type, id, entities, deltaTime, systemConfig, startIndex, endIndex, sharedBuffer } = e.data;
/**
* 创建Worker
* @param script 脚本内容或文件路径
* @param options Worker创建选项
*/
public createWorker(script: string, options: WorkerCreationOptions = {}): PlatformWorker {
if (!this.isWorkerSupported()) {
throw new Error('微信小游戏不支持Worker');
}
try {
// 处理 SharedArrayBuffer 初始化
if (type === 'init' && sharedBuffer) {
sharedFloatArray = new Float32Array(sharedBuffer);
worker.postMessage({ type: 'init', success: true });
return;
return new WeChatWorker(script, options);
} catch (error) {
throw new Error(`创建微信Worker失败: ${(error as Error).message}`);
}
}
// 处理 SharedArrayBuffer 数据
if (type === 'shared' && sharedFloatArray) {
processSharedArrayBuffer(startIndex, endIndex, deltaTime, systemConfig);
worker.postMessage({ id, result: null });
return;
/**
* 创建SharedArrayBuffer不支持
*/
public createSharedArrayBuffer(length: number): SharedArrayBuffer | null {
return null; // 微信小游戏不支持SharedArrayBuffer
}
// 传统处理方式
if (entities) {
const result = workerProcess(entities, deltaTime, systemConfig);
/**
* 获取高精度时间戳
*/
public getHighResTimestamp(): number {
// 尝试使用微信的性能API否则使用Date.now()
if (typeof wx !== 'undefined' && wx.getPerformance) {
const performance = wx.getPerformance();
return performance.now();
}
return Date.now();
}
// 处理 Promise 返回值
if (result && typeof result.then === 'function') {
result.then(function(finalResult) {
worker.postMessage({ id, result: finalResult });
}).catch(function(error) {
worker.postMessage({ id, error: error.message });
/**
* 获取平台配置
*/
public getPlatformConfig(): PlatformConfig {
return {
maxWorkerCount: 1, // 微信小游戏最多支持 1 个 Worker
supportsModuleWorker: false, // 不支持模块Worker
supportsTransferableObjects: this.checkTransferableObjectsSupport(),
maxSharedArrayBufferSize: 0,
workerScriptPrefix: '',
limitations: {
noEval: true, // 微信小游戏限制eval使用
requiresWorkerInit: false,
memoryLimit: this.getMemoryLimit(),
workerNotSupported: false,
workerLimitations: [
'最多只能创建 1 个 Worker',
'创建新Worker前必须先调用 Worker.terminate()',
'Worker脚本必须为项目内相对路径',
'需要在 game.json 中配置 workers 路径',
'使用 worker.onMessage() 而不是 self.onmessage',
'需要基础库 1.9.90 及以上版本'
]
},
extensions: {
platform: 'wechat-minigame',
systemInfo: this.systemInfo,
appId: this.systemInfo.host?.appId || 'unknown'
}
};
}
/**
* 获取微信小游戏设备信息
*/
public getDeviceInfo(): WeChatDeviceInfo {
return {
// 设备基础信息
brand: this.systemInfo.brand,
model: this.systemInfo.model,
platform: this.systemInfo.platform,
system: this.systemInfo.system,
benchmarkLevel: this.systemInfo.benchmarkLevel,
cpuType: this.systemInfo.cpuType,
memorySize: this.systemInfo.memorySize,
deviceAbi: this.systemInfo.deviceAbi,
abi: this.systemInfo.abi,
// 窗口信息
screenWidth: this.systemInfo.screenWidth,
screenHeight: this.systemInfo.screenHeight,
screenTop: this.systemInfo.screenTop,
windowWidth: this.systemInfo.windowWidth,
windowHeight: this.systemInfo.windowHeight,
pixelRatio: this.systemInfo.pixelRatio,
statusBarHeight: this.systemInfo.statusBarHeight,
safeArea: this.systemInfo.safeArea,
// 应用信息
version: this.systemInfo.version,
language: this.systemInfo.language,
theme: this.systemInfo.theme,
SDKVersion: this.systemInfo.SDKVersion,
enableDebug: this.systemInfo.enableDebug,
fontSizeSetting: this.systemInfo.fontSizeSetting,
host: this.systemInfo.host
};
}
/**
* 异步获取完整的平台配置
*/
public async getPlatformConfigAsync(): Promise<PlatformConfig> {
// 可以在这里添加异步获取设备性能信息的逻辑
const baseConfig = this.getPlatformConfig();
// 尝试获取设备性能信息
try {
const benchmarkLevel = await this.getBenchmarkLevel();
baseConfig.extensions = {
...baseConfig.extensions,
benchmarkLevel
};
} catch (error) {
console.warn('获取性能基准失败:', error);
}
return baseConfig;
}
/**
* 检查是否支持Transferable Objects
*/
private checkTransferableObjectsSupport(): boolean {
// 微信小游戏不支持 Transferable Objects
// 基础库 2.20.2 之前只支持可序列化的 key-value 对象
// 2.20.2 之后支持任意类型数据,但仍然不支持 Transferable Objects
return false;
}
/**
* 获取系统信息
*/
private getSystemInfo(): any {
if (typeof wx !== 'undefined' && wx.getSystemInfoSync) {
try {
return wx.getSystemInfoSync();
} catch (error) {
console.warn('获取微信系统信息失败:', error);
return {};
}
}
return {};
}
/**
* 获取内存限制
*/
private getMemoryLimit(): number {
// 微信小游戏通常有内存限制
const memorySize = this.systemInfo.memorySize;
if (memorySize) {
// 解析内存大小字符串(如 "4GB"
const match = memorySize.match(/(\d+)([GM]B)?/i);
if (match) {
const value = parseInt(match[1], 10);
const unit = match[2]?.toUpperCase();
if (unit === 'GB') {
return value * 1024 * 1024 * 1024;
} else if (unit === 'MB') {
return value * 1024 * 1024;
}
}
}
// 默认限制为512MB
return 512 * 1024 * 1024;
}
/**
* 异步获取设备性能基准
*/
private async getBenchmarkLevel(): Promise<number> {
return new Promise((resolve) => {
if (typeof wx !== 'undefined' && wx.getDeviceInfo) {
wx.getDeviceInfo({
success: (res: any) => {
resolve(res.benchmarkLevel || 0);
},
fail: () => {
resolve(0);
}
});
} else {
worker.postMessage({ id, result: result });
resolve(this.systemInfo.benchmarkLevel || 0);
}
});
}
}
/**
* 微信Worker封装
*/
class WeChatWorker implements PlatformWorker {
private _state: 'running' | 'terminated' = 'running';
private worker: any;
private scriptPath: string;
private isTemporaryFile: boolean = false;
constructor(script: string, options: WorkerCreationOptions = {}) {
if (typeof wx === 'undefined' || typeof wx.createWorker !== 'function') {
throw new Error('微信小游戏不支持Worker');
}
try {
// 判断 script 是文件路径还是脚本内容
if (this.isFilePath(script)) {
// 直接使用文件路径
this.scriptPath = script;
this.isTemporaryFile = false;
this.worker = wx.createWorker(this.scriptPath, {
useExperimentalWorker: true // 启用实验性Worker获得更好性能
});
} else {
// 微信小游戏不支持动态脚本内容,只能使用文件路径
// 将脚本内容写入文件系统
this.scriptPath = this.writeScriptToFile(script, options.name);
this.isTemporaryFile = true;
this.worker = wx.createWorker(this.scriptPath, {
useExperimentalWorker: true
});
}
} catch (error) {
worker.postMessage({ id, error: error.message });
throw new Error(`创建微信Worker失败: ${(error as Error).message}`);
}
});
/**
* 实体处理函数 - 根据你的业务逻辑修改此函数
* @param {Array} entities - 实体数据数组
* @param {number} deltaTime - 帧间隔时间
* @param {Object} systemConfig - 系统配置
* @returns {Array} 处理后的实体数据
*/
function workerProcess(entities, deltaTime, systemConfig) {
// ====== 在这里编写你的处理逻辑 ======
// 示例:物理计算
return entities.map(function(entity) {
// 应用重力
entity.vy += (systemConfig.gravity || 100) * deltaTime;
// 更新位置
entity.x += entity.vx * deltaTime;
entity.y += entity.vy * deltaTime;
// 应用摩擦力
entity.vx *= (systemConfig.friction || 0.95);
entity.vy *= (systemConfig.friction || 0.95);
return entity;
});
}
/**
* SharedArrayBuffer 处理函数(可选)
* 判断是否为文件路径
*/
function processSharedArrayBuffer(startIndex, endIndex, deltaTime, systemConfig) {
if (!sharedFloatArray) return;
private isFilePath(script: string): boolean {
// 简单判断:如果包含 .js 后缀且不包含换行符或分号,认为是文件路径
return script.endsWith('.js') &&
!script.includes('\n') &&
!script.includes(';') &&
script.length < 200; // 文件路径通常不会太长
}
// ====== 根据需要实现 SharedArrayBuffer 处理逻辑 ======
// 注意:微信小游戏不支持 SharedArrayBuffer此函数通常不会被调用
/**
* 将脚本内容写入文件系统
* 注意微信小游戏不支持blob URL只能使用文件系统
*/
private writeScriptToFile(script: string, name?: string): string {
const fs = wx.getFileSystemManager();
const fileName = name ? `worker-${name}.js` : `worker-${Date.now()}.js`;
const filePath = `${wx.env.USER_DATA_PATH}/${fileName}`;
try {
fs.writeFileSync(filePath, script, 'utf8');
return filePath;
} catch (error) {
throw new Error(`写入Worker脚本文件失败: ${(error as Error).message}`);
}
}
public get state(): 'running' | 'terminated' {
return this._state;
}
public postMessage(message: any, transfer?: Transferable[]): void {
if (this._state === 'terminated') {
throw new Error('Worker已被终止');
}
try {
// 微信小游戏 Worker 只支持可序列化对象,忽略 transfer 参数
this.worker.postMessage(message);
} catch (error) {
throw new Error(`发送消息到微信Worker失败: ${(error as Error).message}`);
}
}
public onMessage(handler: (event: { data: any }) => void): void {
// 微信小游戏使用 onMessage 方法,不是 onmessage 属性
this.worker.onMessage((res: any) => {
handler({ data: res });
});
}
public onError(handler: (error: ErrorEvent) => void): void {
// 注意:微信小游戏 Worker 的错误处理可能与标准不同
if (this.worker.onError) {
this.worker.onError(handler);
}
}
public terminate(): void {
if (this._state === 'running') {
try {
this.worker.terminate();
this._state = 'terminated';
// 清理临时脚本文件
this.cleanupScriptFile();
} catch (error) {
console.error('终止微信Worker失败:', error);
}
}
}
/**
* 清理临时脚本文件
*/
private cleanupScriptFile(): void {
// 只清理临时创建的文件,不清理用户提供的文件路径
if (this.scriptPath && this.isTemporaryFile) {
try {
const fs = wx.getFileSystemManager();
fs.unlinkSync(this.scriptPath);
} catch (error) {
console.warn('清理Worker脚本文件失败:', error);
}
}
}
}
```
### 步骤 2配置 game.json
## 使用方法
`game.json` 中添加 workers 配置:
### 1. 复制代码
```json
{
"deviceOrientation": "portrait",
"showStatusBar": false,
"workers": "workers"
将上述代码复制到你的项目中,例如 `src/platform/WeChatMiniGameAdapter.ts`
### 2. 注册适配器
```typescript
import { PlatformManager } from '@esengine/ecs-framework';
import { WeChatMiniGameAdapter } from './platform/WeChatMiniGameAdapter';
// 检查是否在微信小游戏环境
if (typeof wx !== 'undefined') {
const wechatAdapter = new WeChatMiniGameAdapter();
PlatformManager.getInstance().registerAdapter(wechatAdapter);
}
```
### 步骤 3使用 WorkerEntitySystem
### 3. WorkerEntitySystem 使用方式
微信小游戏适配器与 WorkerEntitySystem 配合使用,自动处理 Worker 脚本创建:
#### 基本使用方式(推荐)
```typescript
import { WorkerEntitySystem, Matcher, Entity } from '@esengine/ecs-framework';
@@ -216,16 +427,12 @@ class PhysicsSystem extends WorkerEntitySystem<PhysicsData> {
super(Matcher.all(Transform, Velocity), {
enableWorker: true,
workerCount: 1, // 微信小游戏限制只能创建1个Worker
workerScriptPath: 'workers/entity-worker.js', // 指定预编译的 Worker 文件
systemConfig: {
gravity: 100,
friction: 0.95
}
systemConfig: { gravity: 100, friction: 0.95 }
});
}
protected getDefaultEntityDataSize(): number {
return 6;
return 6; // id, x, y, vx, vy, mass
}
protected extractEntityData(entity: Entity): PhysicsData {
@@ -243,15 +450,20 @@ class PhysicsSystem extends WorkerEntitySystem<PhysicsData> {
};
}
// 注意:在微信小游戏中,此方法不会被使用
// Worker 的处理逻辑在 workers/entity-worker.js 中的 workerProcess 函数里
// WorkerEntitySystem 会自动将此函数序列化并写入临时文件
protected workerProcess(entities: PhysicsData[], deltaTime: number, config: any): PhysicsData[] {
return entities.map(entity => {
// 应用重力
entity.vy += config.gravity * deltaTime;
// 更新位置
entity.x += entity.vx * deltaTime;
entity.y += entity.vy * deltaTime;
// 应用摩擦力
entity.vx *= config.friction;
entity.vy *= config.friction;
return entity;
});
}
@@ -265,219 +477,201 @@ class PhysicsSystem extends WorkerEntitySystem<PhysicsData> {
velocity.x = result.vx;
velocity.y = result.vy;
}
// SharedArrayBuffer 相关方法(微信小游戏不支持,可省略)
protected writeEntityToBuffer(data: PhysicsData, offset: number): void {}
protected readEntityFromBuffer(offset: number): PhysicsData | null { return null; }
}
```
### 临时禁用 Worker降级到同步模式
#### 使用预先创建的 Worker 文件(可选
如果遇到问题,可以临时禁用 Worker
如果你希望使用预先创建的 Worker 文件
```typescript
class PhysicsSystem extends WorkerEntitySystem<PhysicsData> {
constructor() {
super(Matcher.all(Transform, Velocity), {
enableWorker: false, // 禁用 Worker使用主线程同步处理
// ... 其他配置
});
// 1. 在 game.json 中配置 Worker 路径
/*
{
"workers": "workers"
}
}
```
## 完整适配器实现
```typescript
import type {
IPlatformAdapter,
PlatformWorker,
WorkerCreationOptions,
PlatformConfig
} from '@esengine/ecs-framework';
/**
* 微信小游戏平台适配器
*/
export class WeChatMiniGameAdapter implements IPlatformAdapter {
public readonly name = 'wechat-minigame';
public readonly version: string;
private systemInfo: any;
constructor() {
this.systemInfo = this.getSystemInfo();
this.version = this.systemInfo.SDKVersion || 'unknown';
}
// 2. 创建 workers/physics.js 文件
// workers/physics.js 内容:
/*
// 微信小游戏 Worker 使用标准的 self.onmessage
self.onmessage = function(e) {
const { type, id, entities, deltaTime, systemConfig } = e.data;
public isWorkerSupported(): boolean {
return typeof wx !== 'undefined' && typeof wx.createWorker === 'function';
}
public isSharedArrayBufferSupported(): boolean {
return false;
}
public getHardwareConcurrency(): number {
return 1; // 微信小游戏最多 1 个 Worker
}
public createWorker(scriptPath: string, options: WorkerCreationOptions = {}): PlatformWorker {
if (!this.isWorkerSupported()) {
throw new Error('微信小游戏环境不支持 Worker');
}
// scriptPath 必须是代码包内的文件路径
const worker = wx.createWorker(scriptPath, {
useExperimentalWorker: true
if (entities) {
// 处理物理计算
const results = entities.map(entity => {
entity.vy += systemConfig.gravity * deltaTime;
entity.x += entity.vx * deltaTime;
entity.y += entity.vy * deltaTime;
return entity;
});
return new WeChatWorker(worker);
}
public createSharedArrayBuffer(length: number): SharedArrayBuffer | null {
return null;
}
public getHighResTimestamp(): number {
if (typeof wx !== 'undefined' && wx.getPerformance) {
return wx.getPerformance().now();
}
return Date.now();
}
public getPlatformConfig(): PlatformConfig {
return {
maxWorkerCount: 1,
supportsModuleWorker: false,
supportsTransferableObjects: false,
maxSharedArrayBufferSize: 0,
workerScriptPrefix: '',
limitations: {
noEval: true, // 重要:标记不支持动态脚本
requiresWorkerInit: false,
memoryLimit: 512 * 1024 * 1024,
workerNotSupported: false,
workerLimitations: [
'最多只能创建 1 个 Worker',
'Worker 脚本必须在代码包内',
'需要在 game.json 中配置 workers 路径',
'需要使用 workerScriptPath 配置'
]
},
extensions: {
platform: 'wechat-minigame',
sdkVersion: this.systemInfo.SDKVersion
self.postMessage({ id, result: results });
}
};
}
private getSystemInfo(): any {
if (typeof wx !== 'undefined' && wx.getSystemInfoSync) {
try {
return wx.getSystemInfoSync();
} catch (error) {
console.warn('获取微信系统信息失败:', error);
}
}
return {};
}
}
/**
* 微信 Worker 封装
*/
class WeChatWorker implements PlatformWorker {
private _state: 'running' | 'terminated' = 'running';
private worker: any;
constructor(worker: any) {
this.worker = worker;
}
public get state(): 'running' | 'terminated' {
return this._state;
}
public postMessage(message: any, transfer?: Transferable[]): void {
if (this._state === 'terminated') {
throw new Error('Worker 已被终止');
}
this.worker.postMessage(message);
}
public onMessage(handler: (event: { data: any }) => void): void {
this.worker.onMessage((res: any) => {
handler({ data: res });
});
}
public onError(handler: (error: ErrorEvent) => void): void {
if (this.worker.onError) {
this.worker.onError(handler);
}
}
public terminate(): void {
if (this._state === 'running') {
this.worker.terminate();
this._state = 'terminated';
}
}
}
// 3. 通过平台适配器直接创建不推荐WorkerEntitySystem会自动处理
const adapter = PlatformManager.getInstance().getAdapter();
const worker = adapter.createWorker('workers/physics.js');
```
## 注册适配器
### 4. 获取设备信息
```typescript
import { PlatformManager } from '@esengine/ecs-framework';
import { WeChatMiniGameAdapter } from './platform/WeChatMiniGameAdapter';
// 在游戏启动时注册适配器
if (typeof wx !== 'undefined') {
const adapter = new WeChatMiniGameAdapter();
PlatformManager.getInstance().registerAdapter(adapter);
const manager = PlatformManager.getInstance();
if (manager.hasAdapter()) {
const adapter = manager.getAdapter();
console.log('微信设备信息:', adapter.getDeviceInfo());
}
```
## 官方文档参考
在使用微信小游戏 Worker 之前,建议先阅读官方文档:
- [wx.createWorker API](https://developers.weixin.qq.com/minigame/dev/api/worker/wx.createWorker.html)
- [Worker.postMessage API](https://developers.weixin.qq.com/minigame/dev/api/worker/Worker.postMessage.html)
- [Worker.onMessage API](https://developers.weixin.qq.com/minigame/dev/api/worker/Worker.onMessage.html)
- [Worker.terminate API](https://developers.weixin.qq.com/minigame/dev/api/worker/Worker.terminate.html)
## 重要注意事项
### Worker 限制
### Worker 限制和配置
| 限制项 | 说明 |
|--------|------|
| 数量限制 | 最多只能创建 1 个 Worker |
| 版本要求 | 需要基础库 1.9.90 及以上 |
| 脚本位置 | 必须在代码包内,不支持动态生成 |
| 生命周期 | 创建新 Worker 前必须先 terminate() |
微信小游戏的 Worker 有以下限制:
- **数量限制**: 最多只能创建 1 个 Worker
- **版本要求**: 需要基础库 1.9.90 及以上版本
- **脚本支持**: 不支持 blob URL只能使用文件路径或写入文件系统
- **文件路径**: Worker 脚本路径必须为绝对路径,但不能以 "/" 开头
- **生命周期**: 创建新 Worker 前必须先调用 `Worker.terminate()` 终止当前 Worker
- **消息处理**: Worker 内使用标准的 `self.onmessage`,主线程使用 `worker.onMessage()`
- **实验性功能**: 支持 `useExperimentalWorker` 选项获得更好的 iOS 性能
#### Worker 配置(可选)
如果使用预先创建的 Worker 文件,需要在 `game.json` 中添加 workers 配置:
```json
{
"deviceOrientation": "portrait",
"showStatusBar": false,
"workers": "workers",
"subpackages": []
}
```
**注意**: 使用 WorkerEntitySystem 时无需此配置,框架会自动将脚本写入临时文件。
### 内存限制
微信小游戏有严格的内存限制:
- 通常限制在 256MB - 512MB
- 需要及时释放不用的资源
- 建议监听内存警告:
- 避免内存泄漏
### API 限制
- 不支持 `eval()` 函数
- 不支持 `Function` 构造器
- DOM API 受限
- 文件系统 API 受限
## 性能优化建议
### 1. 分帧处理
```typescript
wx.onMemoryWarning(() => {
console.warn('收到内存警告,开始清理资源');
// 清理不必要的资源
});
class FramedProcessor {
private tasks: (() => void)[] = [];
private isProcessing = false;
public addTask(task: () => void): void {
this.tasks.push(task);
if (!this.isProcessing) {
this.processNextFrame();
}
}
private processNextFrame(): void {
this.isProcessing = true;
const startTime = Date.now();
const frameTime = 16; // 16ms per frame
while (this.tasks.length > 0 && Date.now() - startTime < frameTime) {
const task = this.tasks.shift();
if (task) task();
}
if (this.tasks.length > 0) {
setTimeout(() => this.processNextFrame(), 0);
} else {
this.isProcessing = false;
}
}
}
```
### 2. 内存管理
```typescript
class MemoryManager {
private static readonly MAX_MEMORY = 256 * 1024 * 1024; // 256MB
public static checkMemoryUsage(): void {
if (typeof wx !== 'undefined' && wx.getPerformance) {
const performance = wx.getPerformance();
const memoryInfo = performance.getEntries().find(
(entry: any) => entry.entryType === 'memory'
);
if (memoryInfo && memoryInfo.usedJSHeapSize > this.MAX_MEMORY * 0.8) {
console.warn('内存使用率过高,建议清理资源');
// 触发垃圾回收或资源清理
}
}
}
}
```
## 调试技巧
```typescript
// 检查 Worker 配置
const adapter = PlatformManager.getInstance().getAdapter();
const config = adapter.getPlatformConfig();
// 检查微信小游戏环境
if (typeof wx !== 'undefined') {
const adapter = new WeChatMiniGameAdapter();
console.log('微信版本:', adapter.version);
console.log('设备信息:', adapter.getDeviceInfo());
console.log('平台配置:', adapter.getPlatformConfig());
// 检查功能支持
console.log('Worker支持:', adapter.isWorkerSupported());
console.log('最大 Worker 数:', config.maxWorkerCount);
console.log('平台限制:', config.limitations);
console.log('SharedArrayBuffer支持:', adapter.isSharedArrayBufferSupported());
}
```
## 微信小游戏特殊API
```typescript
// 获取设备性能等级
if (typeof wx !== 'undefined' && wx.getDeviceInfo) {
wx.getDeviceInfo({
success: (res) => {
console.log('设备性能等级:', res.benchmarkLevel);
}
});
}
// 监听内存警告
if (typeof wx !== 'undefined' && wx.onMemoryWarning) {
wx.onMemoryWarning(() => {
console.warn('收到内存警告,开始清理资源');
// 清理不必要的资源
});
}
```

View File

@@ -1,4 +1,4 @@
# SceneManager
# SceneManager
SceneManager 是 ECS Framework 提供的轻量级场景管理器,适用于 95% 的游戏应用。它提供简单直观的 API支持场景切换和延迟加载。
@@ -19,7 +19,6 @@ SceneManager 适合以下场景:
- 自动管理 ECS 流式 API
- 自动处理场景生命周期
- 集成在 Core 中,自动更新
- 支持[持久化实体](./persistent-entity.md)跨场景迁移v2.3.0+
## 基本使用
@@ -673,9 +672,4 @@ setTimeout(() => {
}, 3000);
```
SceneManager 为大多数游戏提供了简单而强大的场景管理能力。通过 Core 的静态方法,你可以轻松地管理场景切换。
## 相关文档
- [持久化实体](./persistent-entity.md) - 了解如何让实体跨场景保持状态
- [WorldManager](./world-manager.md) - 了解更高级的多世界隔离功能
SceneManager 为大多数游戏提供了简单而强大的场景管理能力。通过 Core 的静态方法,你可以轻松地管理场景切换。如果你需要更高级的多世界隔离功能,请参考 [WorldManager](./world-manager.md) 文档。

View File

@@ -1,4 +1,4 @@
# 场景管理
# 场景管理
在 ECS 架构中场景Scene是游戏世界的容器负责管理实体、系统和组件的生命周期。场景提供了完整的 ECS 运行环境。
@@ -657,6 +657,5 @@ world.setSceneActive('main', true);
- 了解 [SceneManager](./scene-manager.md) - 适用于大多数游戏的简单场景管理
- 了解 [WorldManager](./world-manager.md) - 适用于需要多世界隔离的高级场景
- 了解 [持久化实体](./persistent-entity.md) - 让实体跨场景保持状态v2.3.0+
场景是 ECS 框架的核心容器,正确使用场景管理能让你的游戏架构更加清晰、模块化和易于维护。

View File

@@ -1,4 +1,4 @@
# 系统架构
# 系统架构
在 ECS 架构中系统System是处理业务逻辑的地方。系统负责对拥有特定组件组合的实体执行操作是 ECS 架构的逻辑处理单元。
@@ -216,13 +216,11 @@ class ExampleSystem extends EntitySystem {
// 主要的处理逻辑
for (const entity of entities) {
// 处理每个实体
// ✅ 可以安全地在这里添加/移除组件,不会影响当前迭代
}
}
protected lateProcess(entities: readonly Entity[]): void {
// 主处理之后的后期处理
// ✅ 可以安全地在这里添加/移除组件,不会影响当前迭代
}
protected onEnd(): void {
@@ -272,172 +270,6 @@ class EnemyManagerSystem extends EntitySystem {
}
```
### 重要onAdded/onRemoved 的调用时机
> ⚠️ **注意**`onAdded` 和 `onRemoved` 回调是**同步调用**的,会在 `addComponent`/`removeComponent` 返回**之前**立即执行。
这意味着:
```typescript
// ❌ 错误的用法:链式赋值在 onAdded 之后才执行
const comp = entity.addComponent(new ClickComponent());
comp.element = this._element; // 此时 onAdded 已经执行完了!
// ✅ 正确的用法:通过构造函数传入初始值
const comp = entity.addComponent(new ClickComponent(this._element));
// ✅ 或者使用 createComponent 方法
const comp = entity.createComponent(ClickComponent, this._element);
```
**为什么这样设计?**
事件驱动设计确保 `onAdded`/`onRemoved` 回调不受系统注册顺序的影响。当组件被添加时,所有监听该组件的系统都会立即收到通知,而不是等到下一帧。
**最佳实践:**
1. 组件的初始值应该通过**构造函数**传入
2. 不要依赖 `addComponent` 返回后再设置属性
3. 如果需要在 `onAdded` 中访问组件属性,确保这些属性在构造时已经设置
### 在 process/lateProcess 中安全地修改组件
`process``lateProcess` 中迭代实体时,可以安全地添加或移除组件,不会影响当前的迭代过程:
```typescript
@ECSSystem('Damage')
class DamageSystem extends EntitySystem {
constructor() {
super(Matcher.all(Health, DamageReceiver));
}
protected process(entities: readonly Entity[]): void {
for (const entity of entities) {
const health = entity.getComponent(Health);
const damage = entity.getComponent(DamageReceiver);
if (health && damage) {
health.current -= damage.amount;
// ✅ 安全:移除组件不会影响当前迭代
entity.removeComponent(damage);
if (health.current <= 0) {
// ✅ 安全:添加组件也不会影响当前迭代
entity.addComponent(new Dead());
}
}
}
}
}
```
框架会在每次 `process`/`lateProcess` 调用前创建实体列表的快照,确保迭代过程中的组件变化不会导致跳过实体或重复处理。
## 命令缓冲区 (CommandBuffer)
> **v2.3.0+**
CommandBuffer 提供了一种延迟执行实体操作的机制。当你需要在迭代过程中销毁实体或进行其他可能影响迭代的操作时,使用 CommandBuffer 可以将这些操作推迟到帧末统一执行。
### 基本用法
每个 EntitySystem 都内置了 `commands` 属性:
```typescript
@ECSSystem('Damage')
class DamageSystem extends EntitySystem {
constructor() {
super(Matcher.all(Health, DamageReceiver));
}
protected process(entities: readonly Entity[]): void {
for (const entity of entities) {
const health = entity.getComponent(Health);
const damage = entity.getComponent(DamageReceiver);
if (health && damage) {
health.current -= damage.amount;
// 使用命令缓冲区延迟移除组件
this.commands.removeComponent(entity, DamageReceiver);
if (health.current <= 0) {
// 延迟添加死亡标记
this.commands.addComponent(entity, new Dead());
// 延迟销毁实体
this.commands.destroyEntity(entity);
}
}
}
}
}
```
### 支持的命令
| 方法 | 说明 |
|------|------|
| `addComponent(entity, component)` | 延迟添加组件 |
| `removeComponent(entity, ComponentType)` | 延迟移除组件 |
| `destroyEntity(entity)` | 延迟销毁实体 |
| `setEntityActive(entity, active)` | 延迟设置实体激活状态 |
### 执行时机
命令缓冲区中的命令会在每帧的 `lateUpdate` 阶段之后自动执行。执行顺序与命令入队顺序一致。
```
场景更新流程:
1. onBegin()
2. process()
3. lateProcess()
4. onEnd()
5. flushCommandBuffers() <-- 命令在这里执行
```
### 使用场景
CommandBuffer 适用于以下场景:
1. **在迭代中销毁实体**:避免修改正在遍历的集合
2. **批量延迟操作**:将多个操作合并到帧末执行
3. **跨系统协调**:一个系统标记,另一个系统响应
```typescript
// 示例:敌人死亡系统
@ECSSystem('EnemyDeath')
class EnemyDeathSystem extends EntitySystem {
constructor() {
super(Matcher.all(Enemy, Health));
}
protected process(entities: readonly Entity[]): void {
for (const entity of entities) {
const health = entity.getComponent(Health);
if (health && health.current <= 0) {
// 播放死亡动画、掉落物品等
this.spawnLoot(entity);
// 延迟销毁,不影响当前迭代
this.commands.destroyEntity(entity);
}
}
}
private spawnLoot(entity: Entity): void {
// 掉落物品逻辑
}
}
```
### 注意事项
- 命令会跳过已销毁的实体(安全检查)
- 单个命令执行失败不会影响其他命令
- 命令按入队顺序执行
- 每次 `flush()` 后命令队列会清空
## 系统属性和方法
### 重要属性
@@ -625,8 +457,6 @@ class GameScene extends Scene {
### 系统更新顺序
系统的执行顺序由 `updateOrder` 属性决定,数值越小越先执行:
```typescript
@ECSSystem('Input')
class InputSystem extends EntitySystem {
@@ -653,262 +483,6 @@ class RenderSystem extends EntitySystem {
}
```
#### 稳定排序addOrder
当多个系统的 `updateOrder` 相同时,框架使用 `addOrder`(添加顺序)作为第二排序条件,确保排序结果稳定可预测:
```typescript
// 这两个系统 updateOrder 都是默认值 0
@ECSSystem('SystemA')
class SystemA extends EntitySystem { /* ... */ }
@ECSSystem('SystemB')
class SystemB extends EntitySystem { /* ... */ }
// 添加顺序决定了执行顺序
scene.addSystem(new SystemA()); // addOrder = 0先执行
scene.addSystem(new SystemB()); // addOrder = 1后执行
```
> **注意**`addOrder` 由框架在 `addSystem` 时自动设置,无需手动管理。这确保了相同 `updateOrder` 的系统按照添加顺序执行,避免了排序不稳定导致的随机行为。
## 声明式系统调度
> **v2.4.0+**
除了使用 `updateOrder` 手动控制执行顺序外,框架还提供了声明式的系统调度机制,让你可以通过依赖关系来定义系统的执行顺序。
### 调度装饰器
```typescript
import { EntitySystem, ECSSystem, Stage, Before, After, InSet } from '@esengine/ecs-framework';
// 使用装饰器声明系统调度
@ECSSystem('Movement')
@Stage('update') // 在 update 阶段执行
@After('InputSystem') // 在 InputSystem 之后执行
@Before('RenderSystem') // 在 RenderSystem 之前执行
class MovementSystem extends EntitySystem {
constructor() {
super(Matcher.all(Position, Velocity));
}
protected process(entities: readonly Entity[]): void {
// 移动逻辑
}
}
// 使用系统集合进行分组
@ECSSystem('Physics')
@Stage('update')
@InSet('CoreSystems') // 属于 CoreSystems 集合
class PhysicsSystem extends EntitySystem {
// ...
}
@ECSSystem('Collision')
@Stage('update')
@After('set:CoreSystems') // 在 CoreSystems 集合的所有系统之后执行
class CollisionSystem extends EntitySystem {
// ...
}
```
### 系统执行阶段
框架定义了以下系统执行阶段,按顺序执行:
| 阶段 | 说明 | 典型用途 |
|------|------|----------|
| `startup` | 启动阶段 | 一次性初始化 |
| `preUpdate` | 更新前阶段 | 输入处理、状态准备 |
| `update` | 主更新阶段(默认) | 核心游戏逻辑 |
| `postUpdate` | 更新后阶段 | 物理、碰撞检测 |
| `cleanup` | 清理阶段 | 资源清理、状态重置 |
### Fluent API 配置
如果不想使用装饰器,也可以使用 Fluent API 在运行时配置调度:
```typescript
@ECSSystem('Movement')
class MovementSystem extends EntitySystem {
constructor() {
super(Matcher.all(Position, Velocity));
// 使用 Fluent API 配置调度
this.stage('update')
.after('InputSystem')
.before('RenderSystem')
.inSet('CoreSystems');
}
}
```
### 循环依赖检测
框架会自动检测循环依赖并抛出明确的错误:
```typescript
// 这会导致循环依赖错误
@ECSSystem('SystemA')
@Before('SystemB')
class SystemA extends EntitySystem { }
@ECSSystem('SystemB')
@Before('SystemA') // 错误A -> B -> A 形成循环
class SystemB extends EntitySystem { }
// 错误信息Cyclic dependency detected: SystemA -> SystemB -> SystemA
```
## 帧级变更检测
> **v2.4.0+**
框架提供了基于 epoch 的帧级变更检测机制,让系统可以只处理发生变化的实体,大幅提升性能。
### 核心概念
- **Epoch**:全局帧计数器,每帧递增
- **lastWriteEpoch**:组件最后被修改时的 epoch
- **变更检测**:通过比较 epoch 判断组件是否在指定时间点后发生变化
### 标记组件为已修改
修改组件数据后,需要标记组件为已变更。有两种方式:
**方式 1通过 Entity 辅助方法(推荐)**
```typescript
// 修改组件后通过 entity.markDirty() 标记
const pos = entity.getComponent(Position)!;
pos.x = 100;
pos.y = 200;
entity.markDirty(pos);
// 可以同时标记多个组件
const vel = entity.getComponent(Velocity)!;
vel.vx = 10;
entity.markDirty(pos, vel);
```
**方式 2在组件内部封装**
```typescript
class VelocityComponent extends Component {
private _vx: number = 0;
private _vy: number = 0;
// 提供修改方法,接收 epoch 参数
public setVelocity(vx: number, vy: number, epoch: number): void {
this._vx = vx;
this._vy = vy;
this.markDirty(epoch);
}
public get vx(): number { return this._vx; }
public get vy(): number { return this._vy; }
}
// 在系统中使用
const vel = entity.getComponent(VelocityComponent)!;
vel.setVelocity(10, 20, this.currentEpoch);
```
### 在系统中使用变更检测
EntitySystem 提供了多个变更检测辅助方法:
```typescript
@ECSSystem('Physics')
class PhysicsSystem extends EntitySystem {
constructor() {
super(Matcher.all(Position, Velocity));
}
protected process(entities: readonly Entity[]): void {
// 方式1使用 forEachChanged 只处理变更的实体
// 自动保存 epoch 检查点
this.forEachChanged(entities, [Velocity], (entity) => {
const pos = this.requireComponent(entity, Position);
const vel = this.requireComponent(entity, Velocity);
// 只有 Velocity 变化时才更新位置
pos.x += vel.vx * Time.deltaTime;
pos.y += vel.vy * Time.deltaTime;
});
}
}
@ECSSystem('Transform')
class TransformSystem extends EntitySystem {
constructor() {
super(Matcher.all(Transform, RigidBody));
}
protected process(entities: readonly Entity[]): void {
// 方式2使用 filterChanged 获取变更的实体列表
const changedEntities = this.filterChanged(entities, [RigidBody]);
for (const entity of changedEntities) {
// 处理物理状态变化的实体
this.updatePhysics(entity);
}
// 手动保存 epoch 检查点
this.saveEpoch();
}
protected updatePhysics(entity: Entity): void {
// 物理更新逻辑
}
}
```
### 变更检测 API 参考
| 方法 | 说明 |
|------|------|
| `forEachChanged(entities, [Types], callback)` | 遍历指定组件发生变更的实体,自动保存检查点 |
| `filterChanged(entities, [Types])` | 返回指定组件发生变更的实体数组 |
| `hasChanged(entity, [Types])` | 检查单个实体的指定组件是否发生变更 |
| `saveEpoch()` | 手动保存当前 epoch 作为检查点 |
| `lastProcessEpoch` | 获取上次保存的 epoch 检查点 |
| `currentEpoch` | 获取当前场景的 epoch |
### 使用场景
变更检测特别适合以下场景:
1. **脏标记优化**:只在数据变化时更新渲染
2. **物理同步**:只同步位置/速度发生变化的实体
3. **网络同步**:只发送变化的组件数据
4. **缓存失效**:只在依赖数据变化时重新计算
```typescript
@ECSSystem('NetworkSync')
class NetworkSyncSystem extends EntitySystem {
constructor() {
super(Matcher.all(NetworkComponent, Transform));
}
protected process(entities: readonly Entity[]): void {
// 只同步变化的实体,大幅减少网络流量
this.forEachChanged(entities, [Transform], (entity) => {
const transform = this.requireComponent(entity, Transform);
const network = this.requireComponent(entity, NetworkComponent);
this.sendTransformUpdate(network.id, transform);
});
}
private sendTransformUpdate(id: string, transform: Transform): void {
// 发送网络更新
}
}
```
## 复杂系统示例
### 碰撞检测系统

View File

@@ -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 {

View File

@@ -145,8 +145,6 @@ interface WorkerSystemConfig {
entityDataSize?: number;
/** 最大实体数量用于预分配SharedArrayBuffer */
maxEntities?: number;
/** 预编译的Worker脚本路径用于微信小游戏等不支持动态脚本的平台 */
workerScriptPath?: string;
}
```
@@ -607,166 +605,4 @@ public getPerformanceMetrics(): WorkerPerformanceMetrics {
- SharedArrayBuffer优化
- 大量实体的并行处理
## 微信小游戏支持
微信小游戏对 Worker 有特殊限制,不支持动态创建 Worker 脚本。ESEngine 提供了 `@esengine/worker-generator` CLI 工具来解决这个问题。
### 微信小游戏 Worker 限制
| 特性 | 浏览器 | 微信小游戏 |
|------|--------|-----------|
| 动态脚本 (Blob URL) | ✅ 支持 | ❌ 不支持 |
| Worker 数量 | 多个 | 最多 1 个 |
| 脚本来源 | 任意 | 必须是代码包内文件 |
| SharedArrayBuffer | 需要 COOP/COEP | 有限支持 |
### 使用 Worker Generator CLI
#### 1. 安装工具
```bash
pnpm add -D @esengine/worker-generator
```
#### 2. 配置 workerScriptPath
在你的 WorkerEntitySystem 子类中配置 `workerScriptPath`
```typescript
@ECSSystem('Physics')
class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsData> {
constructor() {
super(Matcher.all(Position, Velocity, Physics), {
enableWorker: true,
workerScriptPath: 'workers/physics-worker.js', // 指定 Worker 文件路径
systemConfig: {
gravity: 100,
friction: 0.95
}
});
}
protected workerProcess(
entities: PhysicsData[],
deltaTime: number,
config: any
): PhysicsData[] {
// 物理计算逻辑
return entities.map(entity => {
entity.vy += config.gravity * deltaTime;
entity.x += entity.vx * deltaTime;
entity.y += entity.vy * deltaTime;
return entity;
});
}
// ... 其他方法
}
```
#### 3. 生成 Worker 文件
运行 CLI 工具自动提取 `workerProcess` 函数并生成兼容微信小游戏的 Worker 文件:
```bash
# 基本用法
npx esengine-worker-gen --src ./src --wechat
# 完整选项
npx esengine-worker-gen \
--src ./src \ # 源码目录
--wechat \ # 生成微信小游戏兼容代码
--mapping \ # 生成 worker-mapping.json
--verbose # 详细输出
```
CLI 工具会:
1. 扫描源码目录,找到所有 `WorkerEntitySystem` 子类
2. 读取每个类的 `workerScriptPath` 配置
3. 提取 `workerProcess` 方法体
4. 转换为 ES5 语法(微信小游戏兼容)
5. 生成到配置的路径
#### 4. 配置 game.json
在微信小游戏的 `game.json` 中配置 workers 目录:
```json
{
"deviceOrientation": "portrait",
"workers": "workers"
}
```
#### 5. 项目结构
```
your-game/
├── game.js
├── game.json # 配置 "workers": "workers"
├── src/
│ └── systems/
│ └── PhysicsSystem.ts # workerScriptPath: 'workers/physics-worker.js'
└── workers/
├── physics-worker.js # 自动生成
└── worker-mapping.json # 自动生成
```
### 临时禁用 Worker
如果需要临时禁用 Worker例如调试时有两种方式
#### 方式 1配置禁用
```typescript
constructor() {
super(matcher, {
enableWorker: false, // 禁用 Worker使用主线程处理
// ...
});
}
```
#### 方式 2平台适配器禁用
在自定义平台适配器中返回不支持 Worker
```typescript
class MyPlatformAdapter implements IPlatformAdapter {
isWorkerSupported(): boolean {
return false; // 返回 false 禁用 Worker
}
// ...
}
```
### 注意事项
1. **每次修改 `workerProcess` 后都需要重新运行 CLI 工具**生成新的 Worker 文件
2. **Worker 函数必须是纯函数**,不能依赖 `this` 或外部变量:
```typescript
// ✅ 正确:只使用参数
protected workerProcess(entities, deltaTime, config) {
return entities.map(e => {
e.y += config.gravity * deltaTime;
return e;
});
}
// ❌ 错误:使用 this
protected workerProcess(entities, deltaTime, config) {
return entities.map(e => {
e.y += this.gravity * deltaTime; // Worker 中无法访问 this
return e;
});
}
```
3. **配置数据通过 `systemConfig` 传递**,而不是类属性
4. **开发者工具中的警告可以忽略**
- `getNetworkType:fail not support` - 微信开发者工具内部行为
- `SharedArrayBuffer will require cross-origin isolation` - 开发环境警告,真机不会出现
Worker系统为ECS框架提供了强大的并行计算能力让你能够充分利用现代多核处理器的性能为复杂的游戏逻辑和计算密集型任务提供了高效的解决方案。

View File

@@ -1,45 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<defs>
<!-- Dark gradient background -->
<linearGradient id="bgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#2d2d2d"/>
<stop offset="100%" style="stop-color:#1a1a1a"/>
</linearGradient>
<!-- Clean white text -->
<linearGradient id="whiteGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#ffffff"/>
<stop offset="100%" style="stop-color:#e8e8e8"/>
</linearGradient>
<!-- Subtle inner shadow -->
<filter id="innerShadow">
<feOffset dx="0" dy="2"/>
<feGaussianBlur stdDeviation="1" result="offset-blur"/>
<feComposite operator="out" in="SourceGraphic" in2="offset-blur" result="inverse"/>
<feFlood flood-color="black" flood-opacity="0.2" result="color"/>
<feComposite operator="in" in="color" in2="inverse" result="shadow"/>
<feComposite operator="over" in="shadow" in2="SourceGraphic"/>
</filter>
</defs>
<!-- Background -->
<rect width="512" height="512" fill="url(#bgGrad)"/>
<!-- Subtle border -->
<rect x="1" y="1" width="510" height="510" fill="none" stroke="#3d3d3d" stroke-width="2"/>
<!-- ES Text -->
<g filter="url(#innerShadow)">
<!-- E -->
<polygon points="72,120 72,392 240,392 240,340 140,340 140,282 220,282 220,230 140,230 140,172 240,172 240,120"
fill="url(#whiteGrad)"/>
<!-- S -->
<path d="M 280 172 Q 280 120 340 120 L 420 120 Q 450 120 450 160 L 450 186 L 398 186 L 398 168 Q 398 158 384 158 L 350 158 Q 320 158 320 188 Q 320 218 350 218 L 400 218 Q 450 218 450 274 L 450 332 Q 450 392 390 392 L 310 392 Q 270 392 270 340 L 270 314 L 322 314 L 322 340 Q 322 354 340 354 L 384 354 Q 404 354 404 324 L 404 290 Q 404 260 380 260 L 330 260 Q 280 260 280 208 Z"
fill="url(#whiteGrad)"/>
</g>
<!-- Accent line -->
<rect x="72" y="424" width="368" height="4" fill="#ffffff" opacity="0.6"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -123,9 +123,7 @@ class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsEntityData> {
enableWorker,
workerCount: isSharedArrayBufferAvailable ? (navigator.hardwareConcurrency || 2) : 1,
systemConfig: defaultConfig,
useSharedArrayBuffer: true,
// 微信小游戏等平台需要配置此路径CLI 工具会根据此路径生成 Worker 文件
workerScriptPath: 'workers/physics-worker.js'
useSharedArrayBuffer: true
}
);

View File

@@ -1,277 +0,0 @@
/**
* Auto-generated Worker file for PhysicsWorkerSystem
* 自动生成的 Worker 文件
*
* Source: F:/ecs-framework/examples/core-demos/src/demos/WorkerSystemDemo.ts
* Generated by @esengine/worker-generator
*
* 使用方式 | Usage:
* 1. 将此文件放入 workers/ 目录
* 2. 在 game.json 中配置 "workers": "workers"
* 3. 在 System 中配置 workerScriptPath: 'workers/physics-worker-system-worker.js'
*/
// 微信小游戏 Worker 环境
// WeChat Mini Game Worker environment
let sharedFloatArray = null;
const ENTITY_DATA_SIZE = 9;
worker.onMessage(function(res) {
// 微信小游戏 Worker 消息直接传递数据,不需要 .data
// WeChat Mini Game Worker passes data directly, no .data wrapper
var type = res.type;
var id = res.id;
var entities = res.entities;
var deltaTime = res.deltaTime;
var systemConfig = res.systemConfig;
var startIndex = res.startIndex;
var endIndex = res.endIndex;
var sharedBuffer = res.sharedBuffer;
try {
// 处理 SharedArrayBuffer 初始化
// Handle SharedArrayBuffer initialization
if (type === 'init' && sharedBuffer) {
sharedFloatArray = new Float32Array(sharedBuffer);
worker.postMessage({ type: 'init', success: true });
return;
}
// 处理 SharedArrayBuffer 数据
// Handle SharedArrayBuffer data
if (type === 'shared' && sharedFloatArray) {
processSharedArrayBuffer(startIndex, endIndex, deltaTime, systemConfig);
worker.postMessage({ id: id, result: null });
return;
}
// 传统处理方式
// Traditional processing
if (entities) {
var result = workerProcess(entities, deltaTime, systemConfig);
// 处理 Promise 返回值
// Handle Promise return value
if (result && typeof result.then === 'function') {
result.then(function(finalResult) {
worker.postMessage({ id: id, result: finalResult });
}).catch(function(error) {
worker.postMessage({ id: id, error: error.message });
});
} else {
worker.postMessage({ id: id, result: result });
}
}
} catch (error) {
worker.postMessage({ id: id, error: error.message });
}
});
/**
* 实体处理函数 - 从 PhysicsWorkerSystem.workerProcess 提取
* Entity processing function - extracted from PhysicsWorkerSystem.workerProcess
*/
function workerProcess(entities, deltaTime, systemConfig) {
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var config = systemConfig || this.physicsConfig;
var result = entities.map(function (e) { return (__assign({}, e)); });
for (var i = 0; i < result.length; i++) {
var entity = result[i];
entity.dy += config.gravity * deltaTime;
entity.x += entity.dx * deltaTime;
entity.y += entity.dy * deltaTime;
if (entity.x <= entity.radius) {
entity.x = entity.radius;
entity.dx = -entity.dx * entity.bounce;
}
else if (entity.x >= config.canvasWidth - entity.radius) {
entity.x = config.canvasWidth - entity.radius;
entity.dx = -entity.dx * entity.bounce;
}
if (entity.y <= entity.radius) {
entity.y = entity.radius;
entity.dy = -entity.dy * entity.bounce;
}
else if (entity.y >= config.canvasHeight - entity.radius) {
entity.y = config.canvasHeight - entity.radius;
entity.dy = -entity.dy * entity.bounce;
entity.dx *= config.groundFriction;
}
entity.dx *= entity.friction;
entity.dy *= entity.friction;
}
for (var i = 0; i < result.length; i++) {
for (var j = i + 1; j < result.length; j++) {
var ball1 = result[i];
var ball2 = result[j];
var dx = ball2.x - ball1.x;
var dy = ball2.y - ball1.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var minDistance = ball1.radius + ball2.radius;
if (distance < minDistance && distance > 0) {
var nx = dx / distance;
var ny = dy / distance;
var overlap = minDistance - distance;
var separationX = nx * overlap * 0.5;
var separationY = ny * overlap * 0.5;
ball1.x -= separationX;
ball1.y -= separationY;
ball2.x += separationX;
ball2.y += separationY;
var relativeVelocityX = ball2.dx - ball1.dx;
var relativeVelocityY = ball2.dy - ball1.dy;
var velocityAlongNormal = relativeVelocityX * nx + relativeVelocityY * ny;
if (velocityAlongNormal > 0)
continue;
var restitution = (ball1.bounce + ball2.bounce) * 0.5;
var impulseScalar = -(1 + restitution) * velocityAlongNormal / (1 / ball1.mass + 1 / ball2.mass);
var impulseX = impulseScalar * nx;
var impulseY = impulseScalar * ny;
ball1.dx -= impulseX / ball1.mass;
ball1.dy -= impulseY / ball1.mass;
ball2.dx += impulseX / ball2.mass;
ball2.dy += impulseY / ball2.mass;
var energyLoss = 0.98;
ball1.dx *= energyLoss;
ball1.dy *= energyLoss;
ball2.dx *= energyLoss;
ball2.dy *= energyLoss;
}
}
}
return result;
}
/**
* SharedArrayBuffer 处理函数
* SharedArrayBuffer processing function
*/
function processSharedArrayBuffer(startIndex, endIndex, deltaTime, systemConfig) {
if (!sharedFloatArray) return;
var config = systemConfig || {
gravity: 100,
canvasWidth: 800,
canvasHeight: 600,
groundFriction: 0.98
};
var actualEntityCount = sharedFloatArray[0];
// 基础物理更新
for (var i = startIndex; i < endIndex && i < actualEntityCount; i++) {
var offset = i * 9 + 9;
var id = sharedFloatArray[offset + 0];
if (id === 0)
continue;
var x = sharedFloatArray[offset + 1];
var y = sharedFloatArray[offset + 2];
var dx = sharedFloatArray[offset + 3];
var dy = sharedFloatArray[offset + 4];
var bounce = sharedFloatArray[offset + 6];
var friction = sharedFloatArray[offset + 7];
var radius = sharedFloatArray[offset + 8];
// 应用重力
dy += config.gravity * deltaTime;
// 更新位置
x += dx * deltaTime;
y += dy * deltaTime;
// 边界碰撞
if (x <= radius) {
x = radius;
dx = -dx * bounce;
}
else if (x >= config.canvasWidth - radius) {
x = config.canvasWidth - radius;
dx = -dx * bounce;
}
if (y <= radius) {
y = radius;
dy = -dy * bounce;
}
else if (y >= config.canvasHeight - radius) {
y = config.canvasHeight - radius;
dy = -dy * bounce;
dx *= config.groundFriction;
}
// 空气阻力
dx *= friction;
dy *= friction;
// 写回数据
sharedFloatArray[offset + 1] = x;
sharedFloatArray[offset + 2] = y;
sharedFloatArray[offset + 3] = dx;
sharedFloatArray[offset + 4] = dy;
}
// 碰撞检测
for (var i = startIndex; i < endIndex && i < actualEntityCount; i++) {
var offset1 = i * 9 + 9;
var id1 = sharedFloatArray[offset1 + 0];
if (id1 === 0)
continue;
var x1 = sharedFloatArray[offset1 + 1];
var y1 = sharedFloatArray[offset1 + 2];
var dx1 = sharedFloatArray[offset1 + 3];
var dy1 = sharedFloatArray[offset1 + 4];
var mass1 = sharedFloatArray[offset1 + 5];
var bounce1 = sharedFloatArray[offset1 + 6];
var radius1 = sharedFloatArray[offset1 + 8];
for (var j = 0; j < actualEntityCount; j++) {
if (i === j)
continue;
var offset2 = j * 9 + 9;
var id2 = sharedFloatArray[offset2 + 0];
if (id2 === 0)
continue;
var x2 = sharedFloatArray[offset2 + 1];
var y2 = sharedFloatArray[offset2 + 2];
var dx2 = sharedFloatArray[offset2 + 3];
var dy2 = sharedFloatArray[offset2 + 4];
var mass2 = sharedFloatArray[offset2 + 5];
var bounce2 = sharedFloatArray[offset2 + 6];
var radius2 = sharedFloatArray[offset2 + 8];
if (isNaN(x2) || isNaN(y2) || isNaN(radius2) || radius2 <= 0)
continue;
var deltaX = x2 - x1;
var deltaY = y2 - y1;
var distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
var minDistance = radius1 + radius2;
if (distance < minDistance && distance > 0) {
var nx = deltaX / distance;
var ny = deltaY / distance;
var overlap = minDistance - distance;
var separationX = nx * overlap * 0.5;
var separationY = ny * overlap * 0.5;
x1 -= separationX;
y1 -= separationY;
var relativeVelocityX = dx2 - dx1;
var relativeVelocityY = dy2 - dy1;
var velocityAlongNormal = relativeVelocityX * nx + relativeVelocityY * ny;
if (velocityAlongNormal > 0)
continue;
var restitution = (bounce1 + bounce2) * 0.5;
var impulseScalar = -(1 + restitution) * velocityAlongNormal / (1 / mass1 + 1 / mass2);
var impulseX = impulseScalar * nx;
var impulseY = impulseScalar * ny;
dx1 -= impulseX / mass1;
dy1 -= impulseY / mass1;
var energyLoss = 0.98;
dx1 *= energyLoss;
dy1 *= energyLoss;
}
}
sharedFloatArray[offset1 + 1] = x1;
sharedFloatArray[offset1 + 2] = y1;
sharedFloatArray[offset1 + 3] = dx1;
sharedFloatArray[offset1 + 4] = dy1;
}
}

View File

@@ -1,6 +0,0 @@
{
"generatedAt": "2025-12-08T09:16:10.529Z",
"mappings": {
"PhysicsWorkerSystem": "physics-worker.js"
}
}

View File

@@ -1,30 +0,0 @@
/**
* 构建脚本 - 打包为微信小游戏可用的 JS 文件
* Build script - bundle for WeChat Mini Game
*/
const esbuild = require('esbuild');
const path = require('path');
const fs = require('fs');
const outDir = path.join(__dirname, 'dist');
// 确保输出目录存在
if (!fs.existsSync(outDir)) {
fs.mkdirSync(outDir, { recursive: true });
}
// 打包主程序
esbuild.buildSync({
entryPoints: ['src/index.ts'],
bundle: true,
outfile: 'dist/game-bundle.js',
format: 'iife',
globalName: 'GameDemo',
target: ['es2015'],
platform: 'browser',
external: [], // 不排除任何依赖,全部打包
minify: false,
sourcemap: true,
});
console.log('Build complete: dist/game-bundle.js');

View File

@@ -1,351 +0,0 @@
/**
* 部署脚本 - 复制文件到微信小游戏项目
* Deploy script - copy files to WeChat Mini Game project
*/
const fs = require('fs');
const path = require('path');
// 微信小游戏项目路径
const WECHAT_PROJECT = 'F:/MiniGame';
// 需要复制的文件
const filesToCopy = [
// Worker 文件
{ src: 'workers/physics-worker.js', dest: 'workers/physics-worker.js' },
// 注意worker-mapping.json 不要放在 workers 目录,微信会把它当 JS 编译
// Note: Don't put worker-mapping.json in workers dir, WeChat will try to compile it as JS
];
// ECS 框架库
const ecsFrameworkSrc = path.join(__dirname, '../../packages/core/dist/index.umd.js');
const ecsFrameworkDest = path.join(WECHAT_PROJECT, 'libs/ecs-framework.js');
// 确保目录存在
function ensureDir(filePath) {
const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
console.log('Deploying to WeChat Mini Game project:', WECHAT_PROJECT);
console.log('');
// 复制 ECS 框架
ensureDir(ecsFrameworkDest);
if (fs.existsSync(ecsFrameworkSrc)) {
fs.copyFileSync(ecsFrameworkSrc, ecsFrameworkDest);
console.log('Copied: ecs-framework.js');
} else {
console.warn('Warning: ECS framework not found at', ecsFrameworkSrc);
console.warn('Please run "pnpm build" in packages/core first');
}
// 复制 Worker 文件
for (const file of filesToCopy) {
const srcPath = path.join(__dirname, file.src);
const destPath = path.join(WECHAT_PROJECT, file.dest);
if (fs.existsSync(srcPath)) {
ensureDir(destPath);
fs.copyFileSync(srcPath, destPath);
console.log('Copied:', file.dest);
} else {
console.warn('Warning: File not found:', srcPath);
}
}
// 创建 game.js - 完整物理球可视化演示
// Create game.js - Full physics ball visualization demo
const gameJs = `/**
* ESEngine Worker System 微信小游戏物理演示
* ESEngine Worker System WeChat Mini Game Physics Demo
*
* 演示 Worker 线程处理物理计算,主线程渲染
* Demonstrates Worker thread physics + main thread rendering
*/
// ============ 配置 | Configuration ============
var CONFIG = {
BALL_COUNT: 20, // 球数量 | Number of balls
GRAVITY: 400, // 重力 | Gravity
GROUND_FRICTION: 0.98, // 地面摩擦 | Ground friction
BALL_BOUNCE: 0.85, // 弹性系数 | Bounce factor
MIN_RADIUS: 8, // 最小半径 | Min radius
MAX_RADIUS: 20, // 最大半径 | Max radius
COLORS: ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', '#98D8C8', '#F7DC6F']
};
// ============ 全局状态 | Global State ============
var canvas = wx.createCanvas();
var ctx = canvas.getContext('2d');
var worker = null;
var entities = [];
var lastTime = Date.now();
var frameCount = 0;
var fps = 0;
var workerReady = false;
var pendingRequest = false;
console.log('====================================');
console.log('ESEngine Worker 物理演示');
console.log('Canvas:', canvas.width, 'x', canvas.height);
console.log('====================================');
// ============ 初始化实体 | Initialize Entities ============
function initEntities() {
entities = [];
for (var i = 0; i < CONFIG.BALL_COUNT; i++) {
var radius = CONFIG.MIN_RADIUS + Math.random() * (CONFIG.MAX_RADIUS - CONFIG.MIN_RADIUS);
entities.push({
id: i + 1,
x: radius + Math.random() * (canvas.width - radius * 2),
y: radius + Math.random() * (canvas.height * 0.5), // 上半部分生成
dx: (Math.random() - 0.5) * 200,
dy: Math.random() * 100,
mass: radius * 0.1,
bounce: CONFIG.BALL_BOUNCE,
friction: CONFIG.GROUND_FRICTION,
radius: radius,
color: CONFIG.COLORS[i % CONFIG.COLORS.length]
});
}
console.log('Created', entities.length, 'balls');
}
// ============ 创建 Worker | Create Worker ============
function createWorker() {
try {
worker = wx.createWorker('workers/physics-worker.js', {
useExperimentalWorker: true
});
worker.onMessage(function(res) {
pendingRequest = false;
if (res.error) {
console.error('Worker error:', res.error);
return;
}
if (res.result && Array.isArray(res.result)) {
// 更新实体位置(保留颜色等渲染属性)
// Update entity positions (keep rendering properties like color)
for (var i = 0; i < res.result.length; i++) {
var updated = res.result[i];
var entity = entities[i];
if (entity && updated) {
entity.x = updated.x;
entity.y = updated.y;
entity.dx = updated.dx;
entity.dy = updated.dy;
}
}
}
});
workerReady = true;
console.log('Worker created successfully!');
} catch (error) {
console.error('Worker creation failed:', error.message);
workerReady = false;
}
}
// ============ 发送物理更新到 Worker | Send Physics Update to Worker ============
function sendToWorker(deltaTime) {
if (!worker || !workerReady || pendingRequest) return;
// 准备发送数据(只发送物理相关属性)
// Prepare data (only physics-related properties)
var physicsData = [];
for (var i = 0; i < entities.length; i++) {
var e = entities[i];
physicsData.push({
id: e.id,
x: e.x,
y: e.y,
dx: e.dx,
dy: e.dy,
mass: e.mass,
bounce: e.bounce,
friction: e.friction,
radius: e.radius
});
}
pendingRequest = true;
worker.postMessage({
id: Date.now(),
entities: physicsData,
deltaTime: deltaTime,
systemConfig: {
gravity: CONFIG.GRAVITY,
canvasWidth: canvas.width,
canvasHeight: canvas.height,
groundFriction: CONFIG.GROUND_FRICTION
}
});
}
// ============ 渲染 | Render ============
function render() {
// 清屏 - 深色背景
// Clear screen - dark background
ctx.fillStyle = '#1a1a2e';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 绘制地面
// Draw ground
ctx.fillStyle = '#2d3436';
ctx.fillRect(0, canvas.height - 10, canvas.width, 10);
// 绘制所有球
// Draw all balls
for (var i = 0; i < entities.length; i++) {
var e = entities[i];
// 球体渐变效果
// Ball gradient effect
var gradient = ctx.createRadialGradient(
e.x - e.radius * 0.3, e.y - e.radius * 0.3, 0,
e.x, e.y, e.radius
);
gradient.addColorStop(0, '#ffffff');
gradient.addColorStop(0.3, e.color);
gradient.addColorStop(1, shadeColor(e.color, -30));
ctx.beginPath();
ctx.arc(e.x, e.y, e.radius, 0, Math.PI * 2);
ctx.fillStyle = gradient;
ctx.fill();
// 球体边框
// Ball border
ctx.strokeStyle = shadeColor(e.color, -50);
ctx.lineWidth = 1;
ctx.stroke();
}
// 绘制 UI
// Draw UI
ctx.fillStyle = '#ffffff';
ctx.font = '14px Arial';
ctx.textAlign = 'left';
ctx.fillText('ESEngine Worker Physics Demo', 10, 25);
ctx.fillText('FPS: ' + fps + ' | Balls: ' + entities.length, 10, 45);
ctx.fillText('Worker: ' + (workerReady ? 'Active' : 'Failed'), 10, 65);
// 提示文字
// Hint text
ctx.textAlign = 'center';
ctx.fillStyle = '#888888';
ctx.font = '12px Arial';
ctx.fillText('Physics calculated in Worker thread', canvas.width / 2, canvas.height - 20);
}
// 颜色加深/减淡工具函数
// Color shade utility function
function shadeColor(color, percent) {
var num = parseInt(color.replace('#', ''), 16);
var amt = Math.round(2.55 * percent);
var R = (num >> 16) + amt;
var G = (num >> 8 & 0x00FF) + amt;
var B = (num & 0x0000FF) + amt;
R = Math.max(0, Math.min(255, R));
G = Math.max(0, Math.min(255, G));
B = Math.max(0, Math.min(255, B));
return '#' + (0x1000000 + R * 0x10000 + G * 0x100 + B).toString(16).slice(1);
}
// ============ 游戏循环 | Game Loop ============
function gameLoop() {
var now = Date.now();
var deltaTime = (now - lastTime) / 1000;
lastTime = now;
// 限制 deltaTime 防止跳帧
// Clamp deltaTime to prevent frame skip
if (deltaTime > 0.1) deltaTime = 0.1;
// FPS 计算
// FPS calculation
frameCount++;
if (frameCount >= 30) {
fps = Math.round(30 / ((now - (lastTime - deltaTime * 1000 * 30)) / 1000));
frameCount = 0;
}
// 发送物理计算到 Worker
// Send physics calculation to Worker
sendToWorker(deltaTime);
// 渲染
// Render
render();
// 下一帧
// Next frame
requestAnimationFrame(gameLoop);
}
// ============ 启动 | Start ============
initEntities();
createWorker();
// 简单的 FPS 计算变量
var fpsLastTime = Date.now();
var fpsFrameCount = 0;
// 覆盖 FPS 计算逻辑
setInterval(function() {
var now = Date.now();
fps = Math.round(fpsFrameCount * 1000 / (now - fpsLastTime));
fpsLastTime = now;
fpsFrameCount = 0;
}, 1000);
// 修改 gameLoop 中的帧计数
var originalGameLoop = gameLoop;
gameLoop = function() {
fpsFrameCount++;
var now = Date.now();
var deltaTime = (now - lastTime) / 1000;
lastTime = now;
if (deltaTime > 0.1) deltaTime = 0.1;
sendToWorker(deltaTime);
render();
requestAnimationFrame(gameLoop);
};
// 开始游戏循环
// Start game loop
console.log('Starting game loop...');
requestAnimationFrame(gameLoop);
// 触摸重置
// Touch to reset
wx.onTouchStart(function(e) {
console.log('Touch detected - resetting balls');
initEntities();
});
`;
const gameJsPath = path.join(WECHAT_PROJECT, 'game.js');
fs.writeFileSync(gameJsPath, gameJs);
console.log('Created: game.js');
// 确保 game.json 配置正确
const gameJsonPath = path.join(WECHAT_PROJECT, 'game.json');
const gameJson = {
deviceOrientation: 'portrait',
workers: 'workers'
};
fs.writeFileSync(gameJsonPath, JSON.stringify(gameJson, null, 2));
console.log('Updated: game.json');
console.log('\\nDeploy complete!');
console.log('Open WeChat DevTools and load:', WECHAT_PROJECT);

View File

@@ -1,15 +0,0 @@
{
"name": "wechat-worker-demo",
"version": "1.0.0",
"description": "ESEngine Worker System WeChat Mini Game Demo",
"scripts": {
"build": "node build.js",
"generate-worker": "npx esengine-worker-gen --src ./src --wechat --verbose",
"deploy": "node deploy.js"
},
"devDependencies": {
"@esengine/ecs-framework": "^2.3.2",
"@esengine/worker-generator": "^1.0.1",
"esbuild": "^0.19.0"
}
}

View File

@@ -1,65 +0,0 @@
/**
* 组件定义
* Component definitions
*/
import { Component, ECSComponent } from '@esengine/ecs-framework';
@ECSComponent('Position')
export class Position extends Component {
x: number = 0;
y: number = 0;
constructor(x: number = 0, y: number = 0) {
super();
this.x = x;
this.y = y;
}
set(x: number, y: number): void {
this.x = x;
this.y = y;
}
}
@ECSComponent('Velocity')
export class Velocity extends Component {
dx: number = 0;
dy: number = 0;
constructor(dx: number = 0, dy: number = 0) {
super();
this.dx = dx;
this.dy = dy;
}
set(dx: number, dy: number): void {
this.dx = dx;
this.dy = dy;
}
}
@ECSComponent('Physics')
export class Physics extends Component {
mass: number = 1;
bounce: number = 0.8;
friction: number = 0.95;
constructor(mass: number = 1, bounce: number = 0.8, friction: number = 0.95) {
super();
this.mass = mass;
this.bounce = bounce;
this.friction = friction;
}
}
@ECSComponent('Renderable')
export class Renderable extends Component {
color: string = '#ffffff';
size: number = 5;
constructor(color: string = '#ffffff', size: number = 5) {
super();
this.color = color;
this.size = size;
}
}

View File

@@ -1,6 +0,0 @@
/**
* ESEngine Worker System 微信小游戏示例
* ESEngine Worker System WeChat Mini Game Example
*/
export { Position, Velocity, Physics, Renderable } from './components';
export { PhysicsWorkerSystem } from './systems/PhysicsWorkerSystem';

View File

@@ -1,212 +0,0 @@
/**
* 物理 Worker 系统
* Physics Worker System
*
* 这个系统会被 worker-generator CLI 扫描,
* 自动提取 workerProcess 方法生成 Worker 文件
*/
import {
WorkerEntitySystem,
Matcher,
Entity,
ECSSystem
} from '@esengine/ecs-framework';
import { Position, Velocity, Physics, Renderable } from '../components';
/**
* 物理实体数据
* Physics entity data
*/
export interface PhysicsEntityData {
id: number;
x: number;
y: number;
dx: number;
dy: number;
mass: number;
bounce: number;
friction: number;
radius: number;
}
/**
* 物理系统配置
* Physics system config
*/
export interface PhysicsConfig {
gravity: number;
canvasWidth: number;
canvasHeight: number;
groundFriction: number;
}
@ECSSystem('PhysicsWorkerSystem')
export class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsEntityData> {
constructor(canvasWidth: number = 375, canvasHeight: number = 667) {
super(
Matcher.empty().all(Position, Velocity, Physics),
{
enableWorker: true,
workerCount: 1,
// 重要:这个路径会被 CLI 工具读取,生成 Worker 文件到此位置
// Important: CLI tool reads this path to generate Worker file
workerScriptPath: 'workers/physics-worker.js',
systemConfig: {
gravity: 200,
canvasWidth,
canvasHeight,
groundFriction: 0.98
} as PhysicsConfig
}
);
}
/**
* 提取实体数据
* Extract entity data
*/
protected extractEntityData(entity: Entity): PhysicsEntityData {
const position = entity.getComponent(Position)!;
const velocity = entity.getComponent(Velocity)!;
const physics = entity.getComponent(Physics)!;
const renderable = entity.getComponent(Renderable);
return {
id: entity.id,
x: position.x,
y: position.y,
dx: velocity.dx,
dy: velocity.dy,
mass: physics.mass,
bounce: physics.bounce,
friction: physics.friction,
radius: renderable?.size || 5
};
}
/**
* Worker 处理函数 - 会被 CLI 工具提取
* Worker process function - will be extracted by CLI tool
*
* 注意:这个函数必须是纯函数,不能使用 this 或外部变量
* Note: This function must be pure, cannot use this or external variables
*/
protected workerProcess(
entities: PhysicsEntityData[],
deltaTime: number,
config: PhysicsConfig
): PhysicsEntityData[] {
const gravity = config.gravity;
const canvasWidth = config.canvasWidth;
const canvasHeight = config.canvasHeight;
const groundFriction = config.groundFriction;
// 复制实体数组避免修改原数据
// Copy entity array to avoid modifying original data
const result = entities.map(e => ({ ...e }));
// 物理更新
// Physics update
for (let i = 0; i < result.length; i++) {
const entity = result[i];
// 应用重力
// Apply gravity
entity.dy += gravity * deltaTime;
// 更新位置
// Update position
entity.x += entity.dx * deltaTime;
entity.y += entity.dy * deltaTime;
// 边界碰撞
// Boundary collision
if (entity.x <= entity.radius) {
entity.x = entity.radius;
entity.dx = -entity.dx * entity.bounce;
} else if (entity.x >= canvasWidth - entity.radius) {
entity.x = canvasWidth - entity.radius;
entity.dx = -entity.dx * entity.bounce;
}
if (entity.y <= entity.radius) {
entity.y = entity.radius;
entity.dy = -entity.dy * entity.bounce;
} else if (entity.y >= canvasHeight - entity.radius) {
entity.y = canvasHeight - entity.radius;
entity.dy = -entity.dy * entity.bounce;
entity.dx *= groundFriction;
}
// 空气阻力
// Air friction
entity.dx *= entity.friction;
entity.dy *= entity.friction;
}
// 简单碰撞检测
// Simple collision detection
for (let i = 0; i < result.length; i++) {
for (let j = i + 1; j < result.length; j++) {
const ball1 = result[i];
const ball2 = result[j];
const dx = ball2.x - ball1.x;
const dy = ball2.y - ball1.y;
const distance = Math.sqrt(dx * dx + dy * dy);
const minDistance = ball1.radius + ball2.radius;
if (distance < minDistance && distance > 0) {
// 分离两个球
// Separate two balls
const nx = dx / distance;
const ny = dy / distance;
const overlap = minDistance - distance;
ball1.x -= nx * overlap * 0.5;
ball1.y -= ny * overlap * 0.5;
ball2.x += nx * overlap * 0.5;
ball2.y += ny * overlap * 0.5;
// 弹性碰撞
// Elastic collision
const relVx = ball2.dx - ball1.dx;
const relVy = ball2.dy - ball1.dy;
const velAlongNormal = relVx * nx + relVy * ny;
if (velAlongNormal > 0) continue;
const restitution = (ball1.bounce + ball2.bounce) * 0.5;
const impulse = -(1 + restitution) * velAlongNormal / (1/ball1.mass + 1/ball2.mass);
ball1.dx -= impulse * nx / ball1.mass;
ball1.dy -= impulse * ny / ball1.mass;
ball2.dx += impulse * nx / ball2.mass;
ball2.dy += impulse * ny / ball2.mass;
}
}
}
return result;
}
/**
* 应用处理结果
* Apply processing result
*/
protected applyResult(entity: Entity, result: PhysicsEntityData): void {
if (!entity?.enabled) return;
const position = entity.getComponent(Position);
const velocity = entity.getComponent(Velocity);
if (position && velocity) {
position.set(result.x, result.y);
velocity.set(result.dx, result.dy);
}
}
protected getDefaultEntityDataSize(): number {
return 9;
}
}

View File

@@ -1,17 +0,0 @@
{
"compilerOptions": {
"target": "ES2015",
"module": "ESNext",
"moduleResolution": "node",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": false,
"outDir": "./dist",
"rootDir": "./src",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -1,6 +0,0 @@
{
"generatedAt": "2025-12-08T10:33:50.647Z",
"mappings": {
"PhysicsWorkerSystem": "workers/physics-worker.js"
}
}

View File

@@ -1,175 +0,0 @@
/**
* Auto-generated Worker file for PhysicsWorkerSystem
* 自动生成的 Worker 文件
*
* Source: F:/ecs-framework/examples/wechat-worker-demo/src/systems/PhysicsWorkerSystem.ts
* Generated by @esengine/worker-generator
*
* 使用方式 | Usage:
* 1. 将此文件放入 workers/ 目录
* 2. 在 game.json 中配置 "workers": "workers"
* 3. 在 System 中配置 workerScriptPath: 'workers/physics-worker-system-worker.js'
*/
// 微信小游戏 Worker 环境
// WeChat Mini Game Worker environment
let sharedFloatArray = null;
const ENTITY_DATA_SIZE = 9;
worker.onMessage(function(res) {
// 微信小游戏 Worker 消息直接传递数据,不需要 .data
// WeChat Mini Game Worker passes data directly, no .data wrapper
var type = res.type;
var id = res.id;
var entities = res.entities;
var deltaTime = res.deltaTime;
var systemConfig = res.systemConfig;
var startIndex = res.startIndex;
var endIndex = res.endIndex;
var sharedBuffer = res.sharedBuffer;
try {
// 处理 SharedArrayBuffer 初始化
// Handle SharedArrayBuffer initialization
if (type === 'init' && sharedBuffer) {
sharedFloatArray = new Float32Array(sharedBuffer);
worker.postMessage({ type: 'init', success: true });
return;
}
// 处理 SharedArrayBuffer 数据
// Handle SharedArrayBuffer data
if (type === 'shared' && sharedFloatArray) {
processSharedArrayBuffer(startIndex, endIndex, deltaTime, systemConfig);
worker.postMessage({ id: id, result: null });
return;
}
// 传统处理方式
// Traditional processing
if (entities) {
var result = workerProcess(entities, deltaTime, systemConfig);
// 处理 Promise 返回值
// Handle Promise return value
if (result && typeof result.then === 'function') {
result.then(function(finalResult) {
worker.postMessage({ id: id, result: finalResult });
}).catch(function(error) {
worker.postMessage({ id: id, error: error.message });
});
} else {
worker.postMessage({ id: id, result: result });
}
}
} catch (error) {
worker.postMessage({ id: id, error: error.message });
}
});
/**
* 实体处理函数 - 从 PhysicsWorkerSystem.workerProcess 提取
* Entity processing function - extracted from PhysicsWorkerSystem.workerProcess
*/
function workerProcess(entities, deltaTime, config) {
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var gravity = config.gravity;
var canvasWidth = config.canvasWidth;
var canvasHeight = config.canvasHeight;
var groundFriction = config.groundFriction;
// 复制实体数组避免修改原数据
// Copy entity array to avoid modifying original data
var result = entities.map(function (e) { return (__assign({}, e)); });
// 物理更新
// Physics update
for (var i = 0; i < result.length; i++) {
var entity = result[i];
// 应用重力
// Apply gravity
entity.dy += gravity * deltaTime;
// 更新位置
// Update position
entity.x += entity.dx * deltaTime;
entity.y += entity.dy * deltaTime;
// 边界碰撞
// Boundary collision
if (entity.x <= entity.radius) {
entity.x = entity.radius;
entity.dx = -entity.dx * entity.bounce;
}
else if (entity.x >= canvasWidth - entity.radius) {
entity.x = canvasWidth - entity.radius;
entity.dx = -entity.dx * entity.bounce;
}
if (entity.y <= entity.radius) {
entity.y = entity.radius;
entity.dy = -entity.dy * entity.bounce;
}
else if (entity.y >= canvasHeight - entity.radius) {
entity.y = canvasHeight - entity.radius;
entity.dy = -entity.dy * entity.bounce;
entity.dx *= groundFriction;
}
// 空气阻力
// Air friction
entity.dx *= entity.friction;
entity.dy *= entity.friction;
}
// 简单碰撞检测
// Simple collision detection
for (var i = 0; i < result.length; i++) {
for (var j = i + 1; j < result.length; j++) {
var ball1 = result[i];
var ball2 = result[j];
var dx = ball2.x - ball1.x;
var dy = ball2.y - ball1.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var minDistance = ball1.radius + ball2.radius;
if (distance < minDistance && distance > 0) {
// 分离两个球
// Separate two balls
var nx = dx / distance;
var ny = dy / distance;
var overlap = minDistance - distance;
ball1.x -= nx * overlap * 0.5;
ball1.y -= ny * overlap * 0.5;
ball2.x += nx * overlap * 0.5;
ball2.y += ny * overlap * 0.5;
// 弹性碰撞
// Elastic collision
var relVx = ball2.dx - ball1.dx;
var relVy = ball2.dy - ball1.dy;
var velAlongNormal = relVx * nx + relVy * ny;
if (velAlongNormal > 0)
continue;
var restitution = (ball1.bounce + ball2.bounce) * 0.5;
var impulse = -(1 + restitution) * velAlongNormal / (1 / ball1.mass + 1 / ball2.mass);
ball1.dx -= impulse * nx / ball1.mass;
ball1.dy -= impulse * ny / ball1.mass;
ball2.dx += impulse * nx / ball2.mass;
ball2.dy += impulse * ny / ball2.mass;
}
}
}
return result;
}
/**
* SharedArrayBuffer 处理函数
* SharedArrayBuffer processing function
*/
function processSharedArrayBuffer(startIndex, endIndex, deltaTime, systemConfig) {
if (!sharedFloatArray) return;
// No SharedArrayBuffer processing defined
}

View File

@@ -1,6 +0,0 @@
{
"generatedAt": "2025-12-08T09:57:08.855Z",
"mappings": {
"PhysicsWorkerSystem": "physics-worker.js"
}
}

View File

@@ -109,7 +109,7 @@
},
"repository": {
"type": "git",
"url": "https://github.com/esengine/esengine.git"
"url": "https://github.com/esengine/ecs-framework.git"
},
"dependencies": {
"@types/multer": "^1.4.13",

View File

@@ -44,7 +44,7 @@
},
"repository": {
"type": "git",
"url": "https://github.com/esengine/esengine.git",
"url": "https://github.com/esengine/ecs-framework.git",
"directory": "packages/asset-system-editor"
}
}

View File

@@ -13,24 +13,22 @@
* - 导入设置
*/
// Meta file management | 元数据文件管理
// Meta file management
export {
AssetMetaManager,
type IAssetMeta,
type IImportSettings,
type ISpriteSettings,
type IMetaFileSystem,
generateGUID,
getMetaFilePath,
inferAssetType,
getDefaultImportSettings,
createAssetMeta,
serializeAssetMeta,
parseAssetMeta
parseAssetMeta,
isValidGUID
} from './meta/AssetMetaFile';
// Re-export utilities from asset-system | 从 asset-system 重导出工具函数
export { generateGUID, isValidGUID } from '@esengine/asset-system';
// Asset packing
export {
AssetPacker,

View File

@@ -13,11 +13,7 @@
* - 标签:用户定义的标签
*/
import {
AssetGUID,
AssetType,
generateGUID
} from '@esengine/asset-system';
import { AssetGUID, AssetType } from '@esengine/asset-system';
/**
* Meta file content structure
@@ -28,17 +24,6 @@ export interface IAssetMeta {
guid: AssetGUID;
/** Asset type | 资产类型 */
type: AssetType;
/**
* Explicit loader type override
* 显式指定的加载器类型覆盖
*
* When set, this type will be used instead of extension-based detection.
* Useful when file extension doesn't match the actual content type.
*
* 设置后,将使用此类型而非基于扩展名的检测。
* 适用于文件扩展名与实际内容类型不匹配的情况。
*/
loaderType?: string;
/** Import settings | 导入设置 */
importSettings?: IImportSettings;
/** User-defined labels | 用户定义的标签 */
@@ -49,36 +34,6 @@ export interface IAssetMeta {
lastModified?: number;
}
/**
* Sprite settings for textures
* 纹理的 Sprite 设置
*/
export interface ISpriteSettings {
/**
* Nine-patch slice border [top, right, bottom, left]
* 九宫格切片边距
*
* Defines the non-stretchable borders for nine-patch rendering.
* 定义九宫格渲染时不可拉伸的边框区域。
*/
sliceBorder?: [number, number, number, number];
/**
* Sprite pivot point (0-1 normalized)
* Sprite 锚点0-1 归一化)
*
* Default is [0.5, 0.5] (center)
* 默认为 [0.5, 0.5](中心)
*/
pivot?: [number, number];
/**
* Pixels per unit for world-space rendering
* 世界空间渲染的像素单位比
*/
pixelsPerUnit?: number;
}
/**
* Import settings for different asset types
* 不同资产类型的导入设置
@@ -92,9 +47,6 @@ export interface IImportSettings {
wrapMode?: 'clamp' | 'repeat' | 'mirror';
premultiplyAlpha?: boolean;
// Sprite settings | Sprite 设置
spriteSettings?: ISpriteSettings;
// Audio settings | 音频设置
audioFormat?: 'mp3' | 'ogg' | 'wav';
sampleRate?: number;
@@ -105,6 +57,23 @@ export interface IImportSettings {
[key: string]: unknown;
}
/**
* Generate a new UUID v4
* 生成新的 UUID v4
*/
export function generateGUID(): AssetGUID {
// Use crypto.randomUUID if available (modern browsers/Node 19+)
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
return crypto.randomUUID();
}
// Fallback implementation
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
/**
* Get meta file path for an asset
@@ -164,11 +133,7 @@ export function inferAssetType(path: string): AssetType {
tileset: 'tileset',
btree: 'behavior-tree',
bp: 'blueprint',
mat: 'material',
particle: 'particle',
// FairyGUI
fui: 'fui'
mat: 'material'
};
return typeMap[ext] || 'binary';
@@ -251,6 +216,14 @@ export function parseAssetMeta(json: string): IAssetMeta {
return meta;
}
/**
* Validate GUID format (UUID v4)
* 验证 GUID 格式 (UUID v4)
*/
export function isValidGUID(guid: string): boolean {
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
return uuidRegex.test(guid);
}
/**
* Asset Meta File Manager
@@ -421,21 +394,6 @@ export class AssetMetaManager {
}
}
/**
* Invalidate cache for a specific asset path
* 使特定资产路径的缓存失效
*
* Call this when a .meta file is modified externally.
* 当 .meta 文件被外部修改时调用此方法。
*/
invalidateCache(assetPath: string): void {
const meta = this._cache.get(assetPath);
if (meta) {
this._guidToPath.delete(meta.guid);
this._cache.delete(assetPath);
}
}
/**
* Clear cache
* 清除缓存

View File

@@ -15,8 +15,7 @@ import {
IRuntimeBundleInfo,
IRuntimeAssetLocation,
IAssetToPack,
IBundlePackOptions,
hashBuffer
IBundlePackOptions
} from '@esengine/asset-system';
import { IAssetMeta } from '../meta/AssetMetaFile';
@@ -130,10 +129,8 @@ export class AssetPacker {
catalogBundles[bundleName] = {
url: `assets/${bundleName}.bundle`,
size: packed.data.byteLength,
hash: await hashBuffer(packed.data),
// 预加载核心资产包(可通过配置扩展) | Preload core bundles (extensible via config)
preload: options.preloadBundles?.includes(bundleName) ??
(bundleName === 'core' || bundleName === 'main')
hash: await this._hashBuffer(packed.data),
preload: bundleName === 'core' || bundleName === 'main'
};
// Add asset locations
@@ -324,7 +321,7 @@ export class AssetPacker {
const manifest: IBundleManifest = {
name,
version: '1.0',
hash: await hashBuffer(bundleData.buffer),
hash: await this._hashBuffer(bundleData.buffer),
compression: 'none',
size: bundleData.byteLength,
assets: assetInfos,
@@ -339,6 +336,27 @@ export class AssetPacker {
};
}
/**
* Hash a buffer using SHA-256
* 使用 SHA-256 哈希缓冲区
*/
private async _hashBuffer(buffer: ArrayBuffer): Promise<string> {
// Use Web Crypto API if available
if (typeof crypto !== 'undefined' && crypto.subtle) {
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('').slice(0, 16);
}
// Fallback: simple hash
const view = new Uint8Array(buffer);
let hash = 0;
for (let i = 0; i < view.length; i++) {
hash = ((hash << 5) - hash) + view[i];
hash = hash & hash;
}
return Math.abs(hash).toString(16).padStart(16, '0');
}
}
/**

View File

@@ -1,7 +1,6 @@
{
"id": "asset-system",
"name": "@esengine/asset-system",
"globalKey": "assetSystem",
"displayName": "Asset System",
"description": "Asset loading, caching and management | 资源加载、缓存和管理",
"version": "1.0.0",
@@ -29,9 +28,7 @@
"TextureLoader",
"JsonLoader",
"TextLoader",
"BinaryLoader",
"AudioLoader",
"PrefabLoader"
"BinaryLoader"
],
"other": [
"AssetManager",
@@ -39,33 +36,6 @@
"AssetCache"
]
},
"assetExtensions": {
".png": "texture",
".jpg": "texture",
".jpeg": "texture",
".gif": "texture",
".webp": "texture",
".bmp": "texture",
".svg": "texture",
".mp3": "audio",
".ogg": "audio",
".wav": "audio",
".m4a": "audio",
".aac": "audio",
".flac": "audio",
".json": "data",
".xml": "data",
".yaml": "data",
".yml": "data",
".txt": "text",
".ttf": "font",
".woff": "font",
".woff2": "font",
".otf": "font",
".fnt": "font",
".atlas": "atlas",
".prefab": "prefab"
},
"requiresWasm": false,
"outputPath": "dist/index.js"
}

View File

@@ -31,7 +31,6 @@
"license": "MIT",
"devDependencies": {
"@esengine/ecs-framework": "workspace:*",
"@esengine/engine-core": "workspace:*",
"@esengine/build-config": "workspace:*",
"rimraf": "^5.0.0",
"tsup": "^8.0.0",
@@ -42,7 +41,7 @@
},
"repository": {
"type": "git",
"url": "https://github.com/esengine/esengine.git",
"url": "https://github.com/esengine/ecs-framework.git",
"directory": "packages/asset-system"
}
}

View File

@@ -204,12 +204,6 @@ export interface IBundlePackOptions {
groupByType?: boolean;
/** Include asset names in bundle | 在包中包含资产名称 */
includeNames?: boolean;
/**
* 需要预加载的包名列表 | List of bundle names to preload
* 如果未指定,默认预加载 'core' 和 'main' 包
* If not specified, defaults to preloading 'core' and 'main' bundles
*/
preloadBundles?: string[];
}
/**

View File

@@ -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
* 按类型查找资产

View File

@@ -104,23 +104,12 @@ export class AssetManager implements IAssetManager {
return this._database;
}
/**
* Get the loader factory.
* 获取加载器工厂。
*/
getLoaderFactory(): AssetLoaderFactory {
return this._loaderFactory as AssetLoaderFactory;
}
/**
* Initialize from catalog
* 从目录初始化
*
* Can be called after construction to load catalog entries.
* 可在构造后调用以加载目录条目。
*/
initializeFromCatalog(catalog: IAssetCatalog): void {
for (const [guid, entry] of Object.entries(catalog.entries)) {
private initializeFromCatalog(catalog: IAssetCatalog): void {
catalog.entries.forEach((entry, guid) => {
const metadata: IAssetMetadata = {
guid,
path: entry.path,
@@ -132,15 +121,12 @@ 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);
this._pathToGuid.set(entry.path, guid);
}
});
}
/**
@@ -611,42 +597,6 @@ export class AssetManager implements IAssetManager {
});
}
/**
* Unload assets by type
* 按类型卸载资产
*
* This is useful for clearing texture caches when restoring scene snapshots.
* 在恢复场景快照时清除纹理缓存时很有用。
*
* @param assetType 要卸载的资产类型 / Asset type to unload
* @param bForce 是否强制卸载(忽略引用计数)/ Whether to force unload (ignore reference count)
*/
unloadAssetsByType(assetType: AssetType, bForce: boolean = false): void {
const guids = Array.from(this._assets.keys());
guids.forEach((guid) => {
const entry = this._assets.get(guid);
if (entry && entry.metadata.type === assetType) {
if (bForce || entry.referenceCount === 0) {
// 获取加载器以释放资源 / Get loader to dispose resources
const loader = this._loaderFactory.createLoader(entry.metadata.type);
if (loader) {
loader.dispose(entry.asset);
}
// 清理条目 / Clean up entry
this._handleToGuid.delete(entry.handle);
this._assets.delete(guid);
this._cache.remove(guid);
// 更新统计 / Update statistics
this._statistics.loadedCount--;
entry.state = AssetState.Unloaded;
}
}
});
}
/**
* Add reference to asset
* 增加资产引用

View File

@@ -233,3 +233,9 @@ export class AssetPathResolver {
return path.replace(/\\/g, '/').replace(/\/+/g, '/');
}
}
/**
* Global asset path resolver instance
* 全局资产路径解析器实例
*/
export const globalPathResolver = new AssetPathResolver();

View File

@@ -10,19 +10,6 @@
* For editor-side functionality (meta files, packing), use @esengine/asset-system-editor
*/
// Service tokens (谁定义接口,谁导出 Token)
export {
AssetManagerToken,
PrefabServiceToken,
PathResolutionServiceToken,
type IAssetManager,
type IPrefabService,
type IPrefabAsset,
type IPrefabData,
type IPrefabMetadata,
type IPathResolutionService
} from './tokens';
// Types
export * from './types/AssetTypes';
@@ -36,7 +23,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
@@ -45,7 +31,7 @@ export { AssetCache } from './core/AssetCache';
export { AssetDatabase } from './core/AssetDatabase';
export { AssetLoadQueue } from './core/AssetLoadQueue';
export { AssetReference, WeakAssetReference, AssetReferenceArray } from './core/AssetReference';
export { AssetPathResolver } from './core/AssetPathResolver';
export { AssetPathResolver, globalPathResolver } from './core/AssetPathResolver';
export type { IAssetPathConfig } from './core/AssetPathResolver';
// Loaders
@@ -54,58 +40,37 @@ export { TextureLoader } from './loaders/TextureLoader';
export { JsonLoader } from './loaders/JsonLoader';
export { TextLoader } from './loaders/TextLoader';
export { BinaryLoader } from './loaders/BinaryLoader';
export { AudioLoader } from './loaders/AudioLoader';
export { PrefabLoader } from './loaders/PrefabLoader';
// Integration
export { EngineIntegration } from './integration/EngineIntegration';
export type { ITextureEngineBridge, TextureLoadCallback } from './integration/EngineIntegration';
export type { IEngineBridge } 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 {
isValidGUID,
generateGUID,
hashBuffer,
hashString,
hashFileInfo
} from './utils/AssetUtils';
export {
collectAssetReferences,
extractUniqueGuids,
groupByComponentType,
DEFAULT_ASSET_PATTERNS,
type SceneAssetRef,
type AssetFieldPattern
} from './utils/AssetCollector';
// Re-export for initializeAssetSystem
// Default instance
import { AssetManager } from './core/AssetManager';
import type { IAssetCatalog } from './types/AssetTypes';
/**
* Default asset manager instance
* 默认资产管理器实例
*/
export const assetManager = new AssetManager();
/**
* Initialize asset system with catalog
* 使用目录初始化资产系统
*
* @param catalog 资产目录 | Asset catalog
* @returns 新的 AssetManager 实例 | New AssetManager instance
*/
export function initializeAssetSystem(catalog?: IAssetCatalog): AssetManager {
if (catalog) {
return new AssetManager(catalog);
}
return assetManager;
}
// Re-export IAssetCatalog for initializeAssetSystem signature
import type { IAssetCatalog } from './types/AssetTypes';

View File

@@ -4,15 +4,15 @@
*/
import { AssetManager } from '../core/AssetManager';
import { AssetGUID, AssetType } from '../types/AssetTypes';
import { ITextureAsset, IAudioAsset, IJsonAsset } from '../interfaces/IAssetLoader';
import { PathResolutionService, type IPathResolutionService } from '../services/PathResolutionService';
import { AssetGUID } from '../types/AssetTypes';
import { ITextureAsset } from '../interfaces/IAssetLoader';
import { globalPathResolver } from '../core/AssetPathResolver';
/**
* Texture engine bridge interface (for asset system)
* 纹理引擎桥接接口(用于资产系统)
* Engine bridge interface
* 引擎桥接接口
*/
export interface ITextureEngineBridge {
export interface IEngineBridge {
/**
* Load texture to GPU
* 加载纹理到GPU
@@ -32,273 +32,32 @@ export interface ITextureEngineBridge {
unloadTexture(id: number): void;
/**
* Get or load texture by path.
* 按路径获取或加载纹理。
*
* This is the preferred method for getting texture IDs.
* The Rust engine is the single source of truth for texture ID allocation.
* 这是获取纹理 ID 的首选方法。
* Rust 引擎是纹理 ID 分配的唯一事实来源。
*
* @param path Image path/URL | 图片路径/URL
* @returns Texture ID allocated by Rust engine | Rust 引擎分配的纹理 ID
* Get texture info
* 获取纹理信息
*/
getOrLoadTextureByPath?(path: string): number;
/**
* Clear the texture path cache (optional).
* 清除纹理路径缓存(可选)。
*
* This should be called when restoring scene snapshots to ensure
* textures are reloaded with correct IDs.
* 在恢复场景快照时应调用此方法以确保纹理使用正确的ID重新加载。
*/
clearTexturePathCache?(): void;
/**
* Clear all textures and reset state (optional).
* 清除所有纹理并重置状态(可选)。
*/
clearAllTextures?(): void;
// ===== Texture State API =====
// ===== 纹理状态 API =====
/**
* Get texture loading state.
* 获取纹理加载状态。
*
* @param id Texture ID | 纹理 ID
* @returns State string: 'loading', 'ready', or 'failed:reason' | 状态字符串
*/
getTextureState?(id: number): string;
/**
* Check if texture is ready for rendering.
* 检查纹理是否已就绪可渲染。
*
* @param id Texture ID | 纹理 ID
* @returns true if texture data is loaded | 纹理数据已加载则返回 true
*/
isTextureReady?(id: number): boolean;
/**
* Get count of textures currently loading.
* 获取当前正在加载的纹理数量。
*
* @returns Number of textures in 'loading' state | 处于加载状态的纹理数量
*/
getTextureLoadingCount?(): number;
/**
* Load texture asynchronously with Promise.
* 使用 Promise 异步加载纹理。
*
* Unlike loadTexture which returns immediately, this method
* waits until the texture is actually loaded and ready.
* 与 loadTexture 立即返回不同,此方法会等待纹理实际加载完成。
*
* @param id Texture ID | 纹理 ID
* @param url Image URL | 图片 URL
* @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;
getTextureInfo(id: number): { width: number; height: number } | null;
}
/**
* Audio asset with runtime ID
* 带运行时 ID 的音频资产
*/
interface AudioAssetEntry {
id: number;
asset: IAudioAsset;
path: string;
}
/**
* Data asset with runtime ID
* 带运行时 ID 的数据资产
*/
interface DataAssetEntry {
id: number;
data: unknown;
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;
private _pathResolver: IPathResolutionService;
private _engineBridge?: IEngineBridge;
private _textureIdMap = new Map<AssetGUID, number>();
private _pathToTextureId = new Map<string, number>();
// 路径稳定 ID 缓存(跨 Play/Stop 循环保持稳定)
// 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>();
private _audioAssets = new Map<number, AudioAssetEntry>();
private static _nextAudioId = 1;
// Data resource mappings | 数据资源映射
private _dataIdMap = new Map<AssetGUID, number>();
private _pathToDataId = new Map<string, number>();
private _dataAssets = new Map<number, DataAssetEntry>();
private static _nextDataId = 1;
/**
* 根据路径生成稳定的 ID使用 FNV-1a hash
* Generate stable ID from path (using FNV-1a hash)
*
* 相同路径永远返回相同 ID即使在 clearTextureMappings 后
* Same path always returns same ID, even after clearTextureMappings
*
* @param path 资源路径 | Resource path
* @param type 资源类型 | Resource type
* @returns 稳定的运行时 ID | Stable runtime ID
*/
private static getStableIdForPath(path: string, type: 'texture' | 'audio'): number {
const cacheKey = `${type}:${path}`;
const cached = EngineIntegration._pathIdCache.get(cacheKey);
if (cached !== undefined) {
return cached;
}
// FNV-1a hash 算法 | FNV-1a hash algorithm
let hash = 2166136261; // FNV offset basis
for (let i = 0; i < path.length; i++) {
hash ^= path.charCodeAt(i);
hash = Math.imul(hash, 16777619); // FNV prime
hash = hash >>> 0; // Keep as uint32
}
// 确保 ID > 00 保留给默认纹理)
// Ensure ID > 0 (0 is reserved for default texture)
const id = (hash % 0x7FFFFFFF) + 1;
EngineIntegration._pathIdCache.set(cacheKey, id);
return id;
}
constructor(assetManager: AssetManager, engineBridge?: ITextureEngineBridge, pathResolver?: IPathResolutionService) {
constructor(assetManager: AssetManager, engineBridge?: IEngineBridge) {
this._assetManager = assetManager;
this._engineBridge = engineBridge;
this._pathResolver = pathResolver ?? new PathResolutionService();
}
/**
* Set path resolver
* 设置路径解析器
*/
setPathResolver(resolver: IPathResolutionService): void {
this._pathResolver = resolver;
}
/**
* Set engine bridge
* 设置引擎桥接
*/
setEngineBridge(bridge: ITextureEngineBridge): void {
setEngineBridge(bridge: IEngineBridge): void {
this._engineBridge = bridge;
}
@@ -306,56 +65,41 @@ export class EngineIntegration {
* Load texture for component
* 为组件加载纹理
*
* 使用路径稳定 ID 确保相同路径在 Play/Stop 循环后返回相同 ID。
* 这样组件保存的 textureId 在恢复场景后仍然有效。
*
* Uses path-stable ID to ensure same path returns same ID across Play/Stop cycles.
* This ensures component's saved textureId remains valid after scene restore.
*
* AssetManager 内部会处理路径解析,这里只需传入原始路径。
* AssetManager handles path resolution internally, just pass the original path here.
*/
async loadTextureForComponent(texturePath: string): Promise<number> {
// 生成路径稳定 ID相同路径永远返回相同 ID
// Generate path-stable ID (same path always returns same ID)
const stableId = EngineIntegration.getStableIdForPath(texturePath, 'texture');
// 检查是否已加载到 GPU
// Check if already loaded to GPU
// 检查缓存(使用原始路径作为键
// Check cache (using original path as key)
const existingId = this._pathToTextureId.get(texturePath);
if (existingId === stableId) {
return stableId; // 已加载,直接返回 | Already loaded, return directly
if (existingId) {
return existingId;
}
// 解析路径为引擎可用的 URL
// Resolve path to engine-compatible URL
const engineUrl = this._pathResolver.catalogToRuntime(texturePath);
// 通过资产系统加载AssetManager 内部会解析路径)
// Load through asset system (AssetManager resolves path internally)
const result = await this._assetManager.loadAssetByPath<ITextureAsset>(texturePath);
const textureAsset = result.asset;
// 使用稳定 ID 加载纹理到 GPU
// Load texture to GPU with stable ID
if (this._engineBridge) {
// 优先使用异步加载(支持加载状态追踪)
// Prefer async loading (supports loading state tracking)
if (this._engineBridge.loadTextureAsync) {
await this._engineBridge.loadTextureAsync(stableId, engineUrl);
} else {
await this._engineBridge.loadTexture(stableId, engineUrl);
}
// 如果有引擎桥接,上传到GPU
// Upload to GPU if bridge exists
// 使用 globalPathResolver 将路径转换为引擎可用的 URL
// Use globalPathResolver to convert path to engine-compatible URL
if (this._engineBridge && textureAsset.data) {
const engineUrl = globalPathResolver.resolve(texturePath);
await this._engineBridge.loadTexture(textureAsset.textureId, engineUrl);
}
// 缓存映射
// Cache mapping
this._pathToTextureId.set(texturePath, stableId);
// 缓存映射(使用原始路径作为键,避免重复解析)
// Cache mapping (using original path as key to avoid re-resolving)
this._pathToTextureId.set(texturePath, textureAsset.textureId);
return stableId;
return textureAsset.textureId;
}
/**
* Load texture by GUID
* 通过GUID加载纹理
*
* 使用路径稳定 ID 确保相同路径在 Play/Stop 循环后返回相同 ID。
* Uses path-stable ID to ensure same path returns same ID across Play/Stop cycles.
*/
async loadTextureByGuid(guid: AssetGUID): Promise<number> {
// 检查是否已有纹理ID / Check if texture ID exists
@@ -364,79 +108,20 @@ export class EngineIntegration {
return existingId;
}
// 通过资产系统加载获取元数据和路径 / Load through asset system to get metadata and path
// 通过资产系统加载 / Load through asset system
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
const stableId = EngineIntegration.getStableIdForPath(assetPath, 'texture');
// 检查是否已加载到 GPU
// Check if already loaded to GPU
if (this._pathToTextureId.get(assetPath) === stableId) {
this._textureIdMap.set(guid, stableId);
return stableId;
}
// 解析路径为引擎可用的 URL
// Resolve path to engine-compatible URL
const engineUrl = this._pathResolver.catalogToRuntime(assetPath);
// 使用稳定 ID 加载纹理到 GPU
// Load texture to GPU with stable ID
if (this._engineBridge) {
if (this._engineBridge.loadTextureAsync) {
await this._engineBridge.loadTextureAsync(stableId, engineUrl);
} else {
await this._engineBridge.loadTexture(stableId, engineUrl);
}
// 如果有引擎桥接上传到GPU / Upload to GPU if bridge exists
if (this._engineBridge && textureAsset.data) {
const metadata = result.metadata;
await this._engineBridge.loadTexture(textureAsset.textureId, metadata.path);
}
// 缓存映射 / Cache mapping
this._textureIdMap.set(guid, stableId);
this._pathToTextureId.set(assetPath, stableId);
this._textureIdMap.set(guid, textureAsset.textureId);
// 通知回调(用于动态图集等)
// 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();
return textureAsset.textureId;
}
/**
@@ -485,241 +170,14 @@ export class EngineIntegration {
* @returns 路径到运行时 ID 的映射 / Map of paths to runtime IDs
*/
async loadResourcesBatch(paths: string[], type: 'texture' | 'audio' | 'font' | 'data'): Promise<Map<string, number>> {
switch (type) {
case 'texture':
// 目前只支持纹理 / Currently only supports textures
if (type === 'texture') {
return this.loadTexturesBatch(paths);
case 'audio':
return this.loadAudioBatch(paths);
case 'data':
return this.loadDataBatch(paths);
case 'font':
// 字体资源暂未实现 / Font resources not yet implemented
console.warn('[EngineIntegration] Font resource loading not yet implemented');
}
// 其他资源类型暂未实现 / Other resource types not yet implemented
console.warn(`[EngineIntegration] Resource type '${type}' not yet supported`);
return new Map();
default:
console.warn(`[EngineIntegration] Unknown resource type '${type}'`);
return new Map();
}
}
// ============= Audio Resource Methods =============
// ============= 音频资源方法 =============
/**
* Load audio for component
* 为组件加载音频
*
* @param audioPath 音频文件路径 / Audio file path
* @returns 运行时音频 ID / Runtime audio ID
*/
async loadAudioForComponent(audioPath: string): Promise<number> {
// 检查缓存 / Check cache
const existingId = this._pathToAudioId.get(audioPath);
if (existingId) {
return existingId;
}
// 通过资产系统加载 / Load through asset system
const result = await this._assetManager.loadAssetByPath<IAudioAsset>(audioPath);
const audioAsset = result.asset;
// 分配运行时 ID / Assign runtime ID
const audioId = EngineIntegration._nextAudioId++;
// 缓存映射 / Cache mapping
this._pathToAudioId.set(audioPath, audioId);
this._audioAssets.set(audioId, {
id: audioId,
asset: audioAsset,
path: audioPath
});
return audioId;
}
/**
* Batch load audio files
* 批量加载音频文件
*/
async loadAudioBatch(paths: string[]): Promise<Map<string, number>> {
const results = new Map<string, number>();
// 收集需要加载的音频 / Collect audio to load
const toLoad: string[] = [];
for (const path of paths) {
const existingId = this._pathToAudioId.get(path);
if (existingId) {
results.set(path, existingId);
} else {
toLoad.push(path);
}
}
if (toLoad.length === 0) {
return results;
}
// 并行加载所有音频 / Load all audio in parallel
const loadPromises = toLoad.map(async (path) => {
try {
const id = await this.loadAudioForComponent(path);
results.set(path, id);
} catch (error) {
console.error(`Failed to load audio: ${path}`, error);
results.set(path, 0);
}
});
await Promise.all(loadPromises);
return results;
}
/**
* Get audio asset by ID
* 通过 ID 获取音频资产
*/
getAudioAsset(audioId: number): IAudioAsset | null {
const entry = this._audioAssets.get(audioId);
return entry?.asset || null;
}
/**
* Get audio ID for path
* 获取路径的音频 ID
*/
getAudioId(path: string): number | null {
return this._pathToAudioId.get(path) || null;
}
/**
* Unload audio
* 卸载音频
*/
unloadAudio(audioId: number): void {
const entry = this._audioAssets.get(audioId);
if (entry) {
this._pathToAudioId.delete(entry.path);
this._audioAssets.delete(audioId);
// 从 GUID 映射中清理 / Clean up GUID mapping
for (const [guid, id] of this._audioIdMap.entries()) {
if (id === audioId) {
this._audioIdMap.delete(guid);
this._assetManager.unloadAsset(guid);
break;
}
}
}
}
// ============= Data Resource Methods =============
// ============= 数据资源方法 =============
/**
* Load data (JSON) for component
* 为组件加载数据JSON
*
* @param dataPath 数据文件路径 / Data file path
* @returns 运行时数据 ID / Runtime data ID
*/
async loadDataForComponent(dataPath: string): Promise<number> {
// 检查缓存 / Check cache
const existingId = this._pathToDataId.get(dataPath);
if (existingId) {
return existingId;
}
// 通过资产系统加载 / Load through asset system
const result = await this._assetManager.loadAssetByPath<IJsonAsset>(dataPath);
const jsonAsset = result.asset;
// 分配运行时 ID / Assign runtime ID
const dataId = EngineIntegration._nextDataId++;
// 缓存映射 / Cache mapping
this._pathToDataId.set(dataPath, dataId);
this._dataAssets.set(dataId, {
id: dataId,
data: jsonAsset.data,
path: dataPath
});
return dataId;
}
/**
* Batch load data files
* 批量加载数据文件
*/
async loadDataBatch(paths: string[]): Promise<Map<string, number>> {
const results = new Map<string, number>();
// 收集需要加载的数据 / Collect data to load
const toLoad: string[] = [];
for (const path of paths) {
const existingId = this._pathToDataId.get(path);
if (existingId) {
results.set(path, existingId);
} else {
toLoad.push(path);
}
}
if (toLoad.length === 0) {
return results;
}
// 并行加载所有数据 / Load all data in parallel
const loadPromises = toLoad.map(async (path) => {
try {
const id = await this.loadDataForComponent(path);
results.set(path, id);
} catch (error) {
console.error(`Failed to load data: ${path}`, error);
results.set(path, 0);
}
});
await Promise.all(loadPromises);
return results;
}
/**
* Get data by ID
* 通过 ID 获取数据
*/
getData<T = unknown>(dataId: number): T | null {
const entry = this._dataAssets.get(dataId);
return (entry?.data as T) || null;
}
/**
* Get data ID for path
* 获取路径的数据 ID
*/
getDataId(path: string): number | null {
return this._pathToDataId.get(path) || null;
}
/**
* Unload data
* 卸载数据
*/
unloadData(dataId: number): void {
const entry = this._dataAssets.get(dataId);
if (entry) {
this._pathToDataId.delete(entry.path);
this._dataAssets.delete(dataId);
// 从 GUID 映射中清理 / Clean up GUID mapping
for (const [guid, id] of this._dataIdMap.entries()) {
if (id === dataId) {
this._dataIdMap.delete(guid);
this._assetManager.unloadAsset(guid);
break;
}
}
}
}
/**
@@ -767,66 +225,12 @@ export class EngineIntegration {
}
/**
* Clear all texture mappings (for scene switching)
* 清空所有纹理映射(用于场景切换)
*
* 注意:使用路径稳定 ID 后,不应在 Play/Stop 循环中调用此方法。
* 此方法仅用于场景切换时释放旧场景的纹理资源。
*
* NOTE: With path-stable IDs, this should NOT be called during Play/Stop cycle.
* This method is only for releasing old scene's texture resources during scene switching.
*
* _pathIdCache 不会被清除,确保相同路径始终返回相同 ID。
* _pathIdCache is NOT cleared, ensuring same path always returns same ID.
* Clear all texture mappings
* 清空所有纹理映射
*/
clearTextureMappings(): void {
// 1. 清除加载状态映射(不清除 _pathIdCache
// Clear load state mappings (NOT clearing _pathIdCache)
this._textureIdMap.clear();
this._pathToTextureId.clear();
// 2. 清除 Rust 引擎的 GPU 纹理资源
// Clear Rust engine's GPU texture resources
if (this._engineBridge?.clearAllTextures) {
this._engineBridge.clearAllTextures();
}
// 3. 清除 AssetManager 中的纹理资产缓存
// Clear texture asset cache in AssetManager
this._assetManager.unloadAssetsByType(AssetType.Texture, true);
// 注意:不再重置 TextureLoader 的 ID 计数器,因为现在使用路径稳定 ID
// NOTE: No longer reset TextureLoader's ID counter as we now use path-stable IDs
}
/**
* Clear all audio mappings
* 清空所有音频映射
*/
clearAudioMappings(): void {
this._audioIdMap.clear();
this._pathToAudioId.clear();
this._audioAssets.clear();
}
/**
* Clear all data mappings
* 清空所有数据映射
*/
clearDataMappings(): void {
this._dataIdMap.clear();
this._pathToDataId.clear();
this._dataAssets.clear();
}
/**
* Clear all resource mappings
* 清空所有资源映射
*/
clearAllMappings(): void {
this.clearTextureMappings();
this.clearAudioMappings();
this.clearDataMappings();
}
/**
@@ -835,13 +239,9 @@ export class EngineIntegration {
*/
getStatistics(): {
loadedTextures: number;
loadedAudio: number;
loadedData: number;
} {
return {
loadedTextures: this._pathToTextureId.size,
loadedAudio: this._audioAssets.size,
loadedData: this._dataAssets.size
loadedTextures: this._pathToTextureId.size
};
}
}

View File

@@ -1,103 +0,0 @@
/**
* Asset File Loader Interface
* 资产文件加载器接口
*
* High-level file loading abstraction that combines path resolution
* with platform-specific file reading.
* 高级文件加载抽象,结合路径解析和平台特定的文件读取。
*
* This is the unified entry point for all file loading in the engine.
* Different from IAssetLoader (which parses content), this interface
* handles the actual file fetching from asset paths.
* 这是引擎中所有文件加载的统一入口。
* 与 IAssetLoader解析内容不同此接口处理从资产路径获取文件。
*/
/**
* Asset file loader interface.
* 资产文件加载器接口。
*
* Provides a unified API for loading files from asset paths (relative to project).
* Different platforms provide their own implementations.
* 提供从资产路径(相对于项目)加载文件的统一 API。
* 不同平台提供各自的实现。
*
* @example
* ```typescript
* // Get global loader
* const loader = getGlobalAssetFileLoader();
*
* // Load image from asset path (relative to project)
* const image = await loader.loadImage('assets/demo/button.png');
*
* // Load text content
* const json = await loader.loadText('assets/config.json');
* ```
*/
export interface IAssetFileLoader {
/**
* Load image from asset path.
* 从资产路径加载图片。
*
* @param assetPath - Asset path relative to project (e.g., "assets/demo/button.png").
* 相对于项目的资产路径。
* @returns Promise resolving to HTMLImageElement. | 返回 HTMLImageElement 的 Promise。
*/
loadImage(assetPath: string): Promise<HTMLImageElement>;
/**
* Load text content from asset path.
* 从资产路径加载文本内容。
*
* @param assetPath - Asset path relative to project. | 相对于项目的资产路径。
* @returns Promise resolving to text content. | 返回文本内容的 Promise。
*/
loadText(assetPath: string): Promise<string>;
/**
* Load binary data from asset path.
* 从资产路径加载二进制数据。
*
* @param assetPath - Asset path relative to project. | 相对于项目的资产路径。
* @returns Promise resolving to ArrayBuffer. | 返回 ArrayBuffer 的 Promise。
*/
loadBinary(assetPath: string): Promise<ArrayBuffer>;
/**
* Check if asset file exists.
* 检查资产文件是否存在。
*
* @param assetPath - Asset path relative to project. | 相对于项目的资产路径。
* @returns Promise resolving to boolean. | 返回布尔值的 Promise。
*/
exists(assetPath: string): Promise<boolean>;
}
/**
* Global asset file loader instance.
* 全局资产文件加载器实例。
*/
let globalAssetFileLoader: IAssetFileLoader | null = null;
/**
* Set the global asset file loader.
* 设置全局资产文件加载器。
*
* Should be called during engine initialization with platform-specific implementation.
* 应在引擎初始化期间使用平台特定的实现调用。
*
* @param loader - Asset file loader instance or null. | 资产文件加载器实例或 null。
*/
export function setGlobalAssetFileLoader(loader: IAssetFileLoader | null): void {
globalAssetFileLoader = loader;
}
/**
* Get the global asset file loader.
* 获取全局资产文件加载器。
*
* @returns Asset file loader instance or null. | 资产文件加载器实例或 null。
*/
export function getGlobalAssetFileLoader(): IAssetFileLoader | null {
return globalAssetFileLoader;
}

View File

@@ -109,22 +109,6 @@ export interface IAssetLoaderFactory {
* 根据文件路径获取资产类型
*/
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>;
}
/**
@@ -144,24 +128,6 @@ export interface ITextureAsset {
hasMipmaps: boolean;
/** 原始数据(如果可用) / Raw image data if available */
data?: ImageData | HTMLImageElement;
// ===== Sprite Settings =====
// ===== Sprite 设置 =====
/**
* 九宫格切片边距 [top, right, bottom, left]
* Nine-patch slice border
*
* Defines the non-stretchable borders for nine-patch rendering.
* 定义九宫格渲染时不可拉伸的边框区域。
*/
sliceBorder?: [number, number, number, number];
/**
* Sprite 锚点 [x, y]0-1 归一化)
* Sprite pivot point (0-1 normalized)
*/
pivot?: [number, number];
}
/**
@@ -201,113 +167,38 @@ export interface IAudioAsset {
channels: number;
}
/**
* Shader property type
* 着色器属性类型
*/
export type ShaderPropertyType = 'float' | 'vec2' | 'vec3' | 'vec4' | 'int' | 'sampler2D' | 'mat3' | 'mat4';
/**
* Shader property definition
* 着色器属性定义
*/
export interface IShaderProperty {
/** 属性名称uniform 名) / Property name (uniform name) */
name: string;
/** 属性类型 / Property type */
type: ShaderPropertyType;
/** 默认值 / Default value */
default: number | number[];
/** 显示名称(编辑器用) / Display name for editor */
displayName?: string;
/** 值范围(用于 float/int / Value range for float/int */
range?: [number, number];
/** 是否隐藏(内部使用) / Hidden from inspector */
hidden?: boolean;
}
/**
* Shader asset interface
* 着色器资产接口
*
* Shader assets contain GLSL source code and property definitions.
* 着色器资产包含 GLSL 源代码和属性定义。
*/
export interface IShaderAsset {
/** 着色器名称 / Shader name (e.g., "UI/Shiny") */
name: string;
/** 顶点着色器源代码 / Vertex shader GLSL source */
vertex: string;
/** 片段着色器源代码 / Fragment shader GLSL source */
fragment: string;
/** 属性定义列表 / Property definitions */
properties: IShaderProperty[];
/** 编译后的着色器 ID运行时填充 / Compiled shader ID (runtime) */
shaderId?: number;
}
/**
* Material property value
* 材质属性值
*/
export type MaterialPropertyValue = number | number[] | string;
/**
* Material animator configuration
* 材质动画器配置
*/
export interface IMaterialAnimator {
/** 要动画的属性名 / Property to animate */
property: string;
/** 起始值 / Start value */
from: number;
/** 结束值 / End value */
to: number;
/** 持续时间(秒) / Duration in seconds */
duration: number;
/** 是否循环 / Loop animation */
loop?: boolean;
/** 循环间隔(秒) / Delay between loops */
loopDelay?: number;
/** 缓动函数 / Easing function */
easing?: 'linear' | 'easeIn' | 'easeOut' | 'easeInOut';
/** 是否自动播放 / Auto play on start */
autoPlay?: boolean;
}
/**
* Material asset interface
* 材质资产接口
*
* Material assets reference a shader and define property values.
* 材质资产引用着色器并定义属性值。
*/
export interface IMaterialAsset {
/** 材质名称 / Material name */
name: string;
/** 着色器 GUID 或内置路径 / Shader GUID or built-in path (e.g., "builtin://shaders/Shiny") */
/** 着色器名称 / Shader name */
shader: string;
/** 材质属性 / Material property values */
properties: Record<string, MaterialPropertyValue>;
/** 纹理映射 / Texture slot mappings (property name -> texture GUID) */
textures?: Record<string, AssetGUID>;
/** 材质属性 / Material properties */
properties: Map<string, unknown>;
/** 纹理映射 / Texture slot mappings */
textures: Map<string, AssetGUID>;
/** 渲染状态 / Render states */
renderStates?: {
renderStates: {
cullMode?: 'none' | 'front' | 'back';
blendMode?: 'none' | 'alpha' | 'additive' | 'multiply' | 'screen';
blendMode?: 'none' | 'alpha' | 'additive' | 'multiply';
depthTest?: boolean;
depthWrite?: boolean;
};
/** 动画器配置(可选) / Animator configuration (optional) */
animator?: IMaterialAnimator;
/** 运行时:编译后的着色器 ID / Runtime: compiled shader ID */
_shaderId?: number;
/** 运行时:引擎材质 ID / Runtime: engine material ID */
_materialId?: number;
}
// 预制体资产接口从专用文件导出 | Prefab asset interface exported from dedicated file
export type { IPrefabAsset, IPrefabData, IPrefabMetadata, IPrefabService } from './IPrefabAsset';
/**
* Prefab asset interface
* 预制体资产接口
*/
export interface IPrefabAsset {
/** 根实体数据 / Serialized entity hierarchy */
root: unknown;
/** 包含的组件类型 / Component types used in prefab */
componentTypes: string[];
/** 引用的资产 / All referenced assets */
referencedAssets: AssetGUID[];
}
/**
* Scene asset interface

View File

@@ -12,12 +12,9 @@ import {
IAssetLoadResult,
IAssetReferenceInfo,
IAssetPreloadGroup,
IAssetLoadProgress,
IAssetCatalog
IAssetLoadProgress
} from '../types/AssetTypes';
import { IAssetLoader, IAssetLoaderFactory } from './IAssetLoader';
import { IAssetReader } from './IAssetReader';
import type { AssetDatabase } from '../core/AssetDatabase';
import { IAssetLoader } from './IAssetLoader';
/**
* Asset manager interface
@@ -153,39 +150,6 @@ export interface IAssetManager {
* 释放管理器
*/
dispose(): void;
/**
* Set asset reader
* 设置资产读取器
*/
setReader(reader: IAssetReader): void;
/**
* Initialize from catalog
* 从目录初始化
*
* Loads asset metadata from a catalog for runtime asset resolution.
* 从目录加载资产元数据,用于运行时资产解析。
*/
initializeFromCatalog(catalog: IAssetCatalog): void;
/**
* Get the asset database
* 获取资产数据库
*/
getDatabase(): AssetDatabase;
/**
* Get the loader factory
* 获取加载器工厂
*/
getLoaderFactory(): IAssetLoaderFactory;
/**
* Set project root path
* 设置项目根路径
*/
setProjectRoot(path: string): void;
}
/**

View File

@@ -1,405 +0,0 @@
/**
* 预制体资产接口定义
* Prefab asset interface definitions
*
* 定义预制体系统的核心类型,包括预制体数据格式、元数据、实例化选项等。
* Defines core types for the prefab system including data format, metadata, instantiation options, etc.
*/
import type { AssetGUID } from '../types/AssetTypes';
import type { SerializedEntity } from '@esengine/ecs-framework';
/**
* 预制体序列化实体(扩展自 SerializedEntity
* Serialized prefab entity (extends SerializedEntity)
*
* 在标准 SerializedEntity 基础上添加预制体特定属性。
* Adds prefab-specific properties on top of standard SerializedEntity.
*/
export interface SerializedPrefabEntity extends SerializedEntity {
/**
* 是否为预制体根节点
* Whether this is the prefab root entity
*/
isPrefabRoot?: boolean;
/**
* 嵌套预制体的 GUID如果此实体是另一个预制体的实例
* GUID of nested prefab (if this entity is an instance of another prefab)
*/
nestedPrefabGuid?: AssetGUID;
}
/**
* 预制体元数据
* Prefab metadata
*/
export interface IPrefabMetadata {
/**
* 预制体名称
* Prefab name
*/
name: string;
/**
* 资产 GUID在保存为资产后填充
* Asset GUID (populated after saving as asset)
*/
guid?: AssetGUID;
/**
* 创建时间戳
* Creation timestamp
*/
createdAt: number;
/**
* 最后修改时间戳
* Last modification timestamp
*/
modifiedAt: number;
/**
* 使用的组件类型列表
* List of component types used
*/
componentTypes: string[];
/**
* 引用的资产 GUID 列表
* List of referenced asset GUIDs
*/
referencedAssets: AssetGUID[];
/**
* 预制体描述
* Prefab description
*/
description?: string;
/**
* 预制体标签(用于分类和搜索)
* Prefab tags (for categorization and search)
*/
tags?: string[];
/**
* 缩略图数据Base64 编码)
* Thumbnail data (Base64 encoded)
*/
thumbnail?: string;
}
/**
* 组件类型注册条目
* Component type registry entry
*/
export interface IPrefabComponentTypeEntry {
/**
* 组件类型名称
* Component type name
*/
typeName: string;
/**
* 组件版本号
* Component version number
*/
version: number;
}
/**
* 预制体文件数据格式
* Prefab file data format
*
* 这是 .prefab 文件的完整结构。
* This is the complete structure of a .prefab file.
*/
export interface IPrefabData {
/**
* 预制体格式版本号
* Prefab format version number
*/
version: number;
/**
* 预制体元数据
* Prefab metadata
*/
metadata: IPrefabMetadata;
/**
* 根实体数据(包含完整的实体层级)
* Root entity data (contains full entity hierarchy)
*/
root: SerializedPrefabEntity;
/**
* 组件类型注册表(用于版本管理和兼容性检查)
* Component type registry (for versioning and compatibility checks)
*/
componentTypeRegistry: IPrefabComponentTypeEntry[];
}
/**
* 预制体资产(加载后的内存表示)
* Prefab asset (in-memory representation after loading)
*/
export interface IPrefabAsset {
/**
* 预制体数据
* Prefab data
*/
data: IPrefabData;
/**
* 资产 GUID
* Asset GUID
*/
guid: AssetGUID;
/**
* 资产路径
* Asset path
*/
path: string;
/**
* 根实体数据(快捷访问)
* Root entity data (quick access)
*/
readonly root: SerializedPrefabEntity;
/**
* 包含的组件类型列表(快捷访问)
* List of component types used (quick access)
*/
readonly componentTypes: string[];
/**
* 引用的资产列表(快捷访问)
* List of referenced assets (quick access)
*/
readonly referencedAssets: AssetGUID[];
}
/**
* 预制体实例化选项
* Prefab instantiation options
*/
export interface IPrefabInstantiateOptions {
/**
* 父实体 ID可选
* Parent entity ID (optional)
*/
parentId?: number;
/**
* 位置覆盖
* Position override
*/
position?: { x: number; y: number };
/**
* 旋转覆盖(角度)
* Rotation override (in degrees)
*/
rotation?: number;
/**
* 缩放覆盖
* Scale override
*/
scale?: { x: number; y: number };
/**
* 实体名称覆盖
* Entity name override
*/
name?: string;
/**
* 是否保留原始实体 ID默认 false生成新 ID
* Whether to preserve original entity IDs (default false, generate new IDs)
*/
preserveIds?: boolean;
/**
* 是否标记为预制体实例(默认 true
* Whether to mark as prefab instance (default true)
*/
trackInstance?: boolean;
/**
* 属性覆盖(组件属性覆盖)
* Property overrides (component property overrides)
*/
propertyOverrides?: IPrefabPropertyOverride[];
}
/**
* 预制体属性覆盖
* Prefab property override
*
* 用于记录预制体实例对原始预制体属性的修改。
* Used to record modifications to prefab properties in instances.
*/
export interface IPrefabPropertyOverride {
/**
* 目标实体路径(从根节点的相对路径,如 "Root/Child/GrandChild"
* Target entity path (relative path from root, e.g., "Root/Child/GrandChild")
*/
entityPath: string;
/**
* 组件类型名称
* Component type name
*/
componentType: string;
/**
* 属性路径(支持嵌套,如 "position.x"
* Property path (supports nesting, e.g., "position.x")
*/
propertyPath: string;
/**
* 覆盖值
* Override value
*/
value: unknown;
}
/**
* 预制体创建选项
* Prefab creation options
*/
export interface IPrefabCreateOptions {
/**
* 预制体名称
* Prefab name
*/
name: string;
/**
* 预制体描述
* Prefab description
*/
description?: string;
/**
* 预制体标签
* Prefab tags
*/
tags?: string[];
/**
* 是否包含子实体
* Whether to include child entities
*/
includeChildren?: boolean;
/**
* 保存路径(可选,用于指定保存位置)
* Save path (optional, for specifying save location)
*/
savePath?: string;
}
/**
* 预制体服务接口
* Prefab service interface
*
* 提供预制体的创建、实例化、管理等功能。
* Provides prefab creation, instantiation, management, etc.
*/
export interface IPrefabService {
/**
* 从实体创建预制体数据
* Create prefab data from entity
*
* @param entity - 源实体 | Source entity
* @param options - 创建选项 | Creation options
* @returns 预制体数据 | Prefab data
*/
createPrefab(entity: unknown, options: IPrefabCreateOptions): IPrefabData;
/**
* 实例化预制体
* Instantiate prefab
*
* @param prefab - 预制体资产 | Prefab asset
* @param scene - 目标场景 | Target scene
* @param options - 实例化选项 | Instantiation options
* @returns 创建的根实体 | Created root entity
*/
instantiate(prefab: IPrefabAsset, scene: unknown, options?: IPrefabInstantiateOptions): unknown;
/**
* 通过 GUID 实例化预制体
* Instantiate prefab by GUID
*
* @param guid - 预制体资产 GUID | Prefab asset GUID
* @param scene - 目标场景 | Target scene
* @param options - 实例化选项 | Instantiation options
* @returns 创建的根实体 | Created root entity
*/
instantiateByGuid(guid: AssetGUID, scene: unknown, options?: IPrefabInstantiateOptions): Promise<unknown>;
/**
* 检查实体是否为预制体实例
* Check if entity is a prefab instance
*
* @param entity - 要检查的实体 | Entity to check
* @returns 是否为预制体实例 | Whether it's a prefab instance
*/
isPrefabInstance(entity: unknown): boolean;
/**
* 获取预制体实例的源预制体 GUID
* Get source prefab GUID of a prefab instance
*
* @param entity - 预制体实例 | Prefab instance
* @returns 源预制体 GUID如果不是实例则返回 null | Source prefab GUID, null if not an instance
*/
getSourcePrefabGuid(entity: unknown): AssetGUID | null;
/**
* 将实例的修改应用到源预制体
* Apply instance modifications to source prefab
*
* @param instance - 预制体实例 | Prefab instance
* @returns 是否成功应用 | Whether application was successful
*/
applyToPrefab?(instance: unknown): Promise<boolean>;
/**
* 将实例还原为源预制体的状态
* Revert instance to source prefab state
*
* @param instance - 预制体实例 | Prefab instance
* @returns 是否成功还原 | Whether revert was successful
*/
revertToPrefab?(instance: unknown): Promise<boolean>;
/**
* 获取实例相对于源预制体的属性覆盖
* Get property overrides of instance relative to source prefab
*
* @param instance - 预制体实例 | Prefab instance
* @returns 属性覆盖列表 | List of property overrides
*/
getPropertyOverrides?(instance: unknown): IPrefabPropertyOverride[];
}
/**
* 预制体文件格式版本
* Prefab file format version
*/
export const PREFAB_FORMAT_VERSION = 1;
/**
* 预制体文件扩展名
* Prefab file extension
*/
export const PREFAB_FILE_EXTENSION = '.prefab';

View File

@@ -9,8 +9,6 @@ import { TextureLoader } from './TextureLoader';
import { JsonLoader } from './JsonLoader';
import { TextLoader } from './TextLoader';
import { BinaryLoader } from './BinaryLoader';
import { AudioLoader } from './AudioLoader';
import { PrefabLoader } from './PrefabLoader';
/**
* Asset loader factory
@@ -40,15 +38,6 @@ export class AssetLoaderFactory implements IAssetLoaderFactory {
// 二进制加载器 / Binary loader
this._loaders.set(AssetType.Binary, new BinaryLoader());
// 音频加载器 / Audio loader
this._loaders.set(AssetType.Audio, new AudioLoader());
// 预制体加载器 / Prefab loader
this._loaders.set(AssetType.Prefab, new PrefabLoader());
// 注Shader 和 Material 加载器由 material-system 模块注册
// Note: Shader and Material loaders are registered by material-system module
}
/**
@@ -149,43 +138,4 @@ export class AssetLoaderFactory implements IAssetLoaderFactory {
clear(): void {
this._loaders.clear();
}
/**
* Get all supported file extensions from all registered loaders.
* 获取所有注册加载器支持的文件扩展名。
*
* @returns Array of extension patterns (e.g., ['*.png', '*.jpg', '*.particle'])
*/
getAllSupportedExtensions(): string[] {
const extensions = new Set<string>();
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}`);
}
}
return Array.from(extensions);
}
/**
* Get extension to type mapping for all registered loaders.
* 获取所有注册加载器的扩展名到类型的映射。
*
* @returns Map of extension (without dot) to asset type string
*/
getExtensionTypeMap(): Record<string, string> {
const map: Record<string, string> = {};
for (const [type, loader] of this._loaders) {
for (const ext of loader.supportedExtensions) {
const cleanExt = ext.startsWith('.') ? ext.substring(1) : ext;
map[cleanExt.toLowerCase()] = type;
}
}
return map;
}
}

View File

@@ -1,109 +0,0 @@
/**
* Audio asset loader
* 音频资产加载器
*/
import { AssetType } from '../types/AssetTypes';
import { IAssetLoader, IAudioAsset, IAssetParseContext } from '../interfaces/IAssetLoader';
import { IAssetContent, AssetContentType } from '../interfaces/IAssetReader';
/**
* Audio loader implementation
* 音频加载器实现
*
* Uses Web Audio API to decode audio data into AudioBuffer.
* 使用 Web Audio API 将音频数据解码为 AudioBuffer。
*/
export class AudioLoader implements IAssetLoader<IAudioAsset> {
readonly supportedType = AssetType.Audio;
readonly supportedExtensions = ['.mp3', '.wav', '.ogg', '.m4a', '.flac', '.aac'];
readonly contentType: AssetContentType = 'audio';
private static _audioContext: AudioContext | null = null;
/**
* Get or create shared AudioContext
* 获取或创建共享的 AudioContext
*/
private static getAudioContext(): AudioContext {
if (!AudioLoader._audioContext) {
// 兼容旧版 Safari 的 webkitAudioContext
// Support legacy Safari webkitAudioContext
const AudioContextClass = window.AudioContext ||
(window as Window & { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
if (!AudioContextClass) {
throw new Error('AudioContext is not supported in this browser');
}
AudioLoader._audioContext = new AudioContextClass();
}
return AudioLoader._audioContext;
}
/**
* Parse audio from content.
* 从内容解析音频。
*/
async parse(content: IAssetContent, _context: IAssetParseContext): Promise<IAudioAsset> {
if (!content.audioBuffer) {
throw new Error('Audio content is empty');
}
const audioBuffer = content.audioBuffer;
const audioAsset: IAudioAsset = {
buffer: audioBuffer,
duration: audioBuffer.duration,
sampleRate: audioBuffer.sampleRate,
channels: audioBuffer.numberOfChannels
};
return audioAsset;
}
/**
* Dispose loaded asset
* 释放已加载的资产
*/
dispose(_asset: IAudioAsset): void {
// AudioBuffer doesn't need explicit cleanup in most browsers
// AudioBuffer 在大多数浏览器中不需要显式清理
// The garbage collector will handle it when no references remain
// 当没有引用时,垃圾回收器会处理它
}
/**
* Close the shared AudioContext
* 关闭共享的 AudioContext
*
* Call this when completely shutting down audio system.
* 在完全关闭音频系统时调用。
*/
static closeAudioContext(): void {
if (AudioLoader._audioContext) {
AudioLoader._audioContext.close();
AudioLoader._audioContext = null;
}
}
/**
* Resume AudioContext after user interaction
* 用户交互后恢复 AudioContext
*
* Browsers require user interaction before audio can play.
* 浏览器要求用户交互后才能播放音频。
*/
static async resumeAudioContext(): Promise<void> {
const ctx = AudioLoader.getAudioContext();
if (ctx.state === 'suspended') {
await ctx.resume();
}
}
/**
* Get the shared AudioContext instance
* 获取共享的 AudioContext 实例
*/
static get audioContext(): AudioContext {
return AudioLoader.getAudioContext();
}
}

View File

@@ -38,8 +38,6 @@ export class BinaryLoader implements IAssetLoader<IBinaryAsset> {
* 释放已加载的资产
*/
dispose(asset: IBinaryAsset): void {
// 释放二进制数据引用以允许垃圾回收
// Release binary data reference to allow garbage collection
(asset as { data: ArrayBuffer | null }).data = null;
(asset as any).data = null;
}
}

View File

@@ -35,7 +35,6 @@ export class JsonLoader implements IAssetLoader<IJsonAsset> {
* 释放已加载的资产
*/
dispose(asset: IJsonAsset): void {
// 清空 JSON 数据 | Clear JSON data
asset.data = null;
(asset as any).data = null;
}
}

View File

@@ -1,156 +0,0 @@
/**
* 预制体资产加载器
* Prefab asset loader
*/
import { AssetType } from '../types/AssetTypes';
import type { IAssetLoader, IAssetParseContext } from '../interfaces/IAssetLoader';
import type { IAssetContent, AssetContentType } from '../interfaces/IAssetReader';
import type {
IPrefabAsset,
IPrefabData,
SerializedPrefabEntity
} from '../interfaces/IPrefabAsset';
import { PREFAB_FORMAT_VERSION } from '../interfaces/IPrefabAsset';
/**
* 预制体加载器实现
* Prefab loader implementation
*/
export class PrefabLoader implements IAssetLoader<IPrefabAsset> {
readonly supportedType = AssetType.Prefab;
readonly supportedExtensions = ['.prefab'];
readonly contentType: AssetContentType = 'text';
/**
* 从文本内容解析预制体
* Parse prefab from text content
*/
async parse(content: IAssetContent, context: IAssetParseContext): Promise<IPrefabAsset> {
if (!content.text) {
throw new Error('Prefab content is empty');
}
let prefabData: IPrefabData;
try {
prefabData = JSON.parse(content.text) as IPrefabData;
} catch (error) {
throw new Error(`Failed to parse prefab JSON: ${(error as Error).message}`);
}
// 验证预制体格式 | Validate prefab format
this.validatePrefabData(prefabData);
// 版本兼容性检查 | Version compatibility check
if (prefabData.version > PREFAB_FORMAT_VERSION) {
console.warn(
`Prefab version ${prefabData.version} is newer than supported version ${PREFAB_FORMAT_VERSION}. ` +
`Some features may not work correctly.`
);
}
// 构建资产对象 | Build asset object
const prefabAsset: IPrefabAsset = {
data: prefabData,
guid: context.metadata.guid,
path: context.metadata.path,
// 快捷访问属性 | Quick access properties
get root(): SerializedPrefabEntity {
return prefabData.root;
},
get componentTypes(): string[] {
return prefabData.metadata.componentTypes;
},
get referencedAssets(): string[] {
return prefabData.metadata.referencedAssets;
}
};
return prefabAsset;
}
/**
* 释放已加载的资产
* Dispose loaded asset
*/
dispose(asset: IPrefabAsset): void {
// 清空预制体数据 | Clear prefab data
(asset as { data: IPrefabData | null }).data = null;
}
/**
* 验证预制体数据格式
* Validate prefab data format
*/
private validatePrefabData(data: unknown): asserts data is IPrefabData {
if (!data || typeof data !== 'object') {
throw new Error('Invalid prefab data: expected object');
}
const prefab = data as Partial<IPrefabData>;
// 验证版本号 | Validate version
if (typeof prefab.version !== 'number') {
throw new Error('Invalid prefab data: missing or invalid version');
}
// 验证元数据 | Validate metadata
if (!prefab.metadata || typeof prefab.metadata !== 'object') {
throw new Error('Invalid prefab data: missing or invalid metadata');
}
const metadata = prefab.metadata;
if (typeof metadata.name !== 'string') {
throw new Error('Invalid prefab data: missing or invalid metadata.name');
}
if (!Array.isArray(metadata.componentTypes)) {
throw new Error('Invalid prefab data: missing or invalid metadata.componentTypes');
}
if (!Array.isArray(metadata.referencedAssets)) {
throw new Error('Invalid prefab data: missing or invalid metadata.referencedAssets');
}
// 验证根实体 | Validate root entity
if (!prefab.root || typeof prefab.root !== 'object') {
throw new Error('Invalid prefab data: missing or invalid root entity');
}
this.validateSerializedEntity(prefab.root);
// 验证组件类型注册表 | Validate component type registry
if (!Array.isArray(prefab.componentTypeRegistry)) {
throw new Error('Invalid prefab data: missing or invalid componentTypeRegistry');
}
}
/**
* 验证序列化实体格式
* Validate serialized entity format
*/
private validateSerializedEntity(entity: unknown): void {
if (!entity || typeof entity !== 'object') {
throw new Error('Invalid entity data: expected object');
}
const e = entity as Partial<SerializedPrefabEntity>;
if (typeof e.id !== 'number') {
throw new Error('Invalid entity data: missing or invalid id');
}
if (typeof e.name !== 'string') {
throw new Error('Invalid entity data: missing or invalid name');
}
if (!Array.isArray(e.components)) {
throw new Error('Invalid entity data: missing or invalid components array');
}
if (!Array.isArray(e.children)) {
throw new Error('Invalid entity data: missing or invalid children array');
}
// 递归验证子实体 | Recursively validate child entities
for (const child of e.children) {
this.validateSerializedEntity(child);
}
}
}

View File

@@ -50,7 +50,6 @@ export class TextLoader implements IAssetLoader<ITextAsset> {
* 释放已加载的资产
*/
dispose(asset: ITextAsset): void {
// 清空文本内容 | Clear text content
asset.content = '';
(asset as any).content = '';
}
}

View File

@@ -7,36 +7,6 @@ import { AssetType } from '../types/AssetTypes';
import { IAssetLoader, ITextureAsset, IAssetParseContext } from '../interfaces/IAssetLoader';
import { IAssetContent, AssetContentType } from '../interfaces/IAssetReader';
/**
* 全局引擎桥接接口(运行时挂载到 window
* Global engine bridge interface (mounted to window at runtime)
*/
interface IEngineBridgeGlobal {
loadTexture?(textureId: number, path: string): Promise<void>;
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
*/
function getEngineBridge(): IEngineBridgeGlobal | undefined {
if (typeof window !== 'undefined' && 'engineBridge' in window) {
return (window as Window & { engineBridge?: IEngineBridgeGlobal }).engineBridge;
}
return undefined;
}
/**
* Texture loader implementation
* 纹理加载器实现
@@ -48,18 +18,6 @@ export class TextureLoader implements IAssetLoader<ITextureAsset> {
private static _nextTextureId = 1;
/**
* Reset texture ID counter
* 重置纹理 ID 计数器
*
* This should be called when restoring scene snapshots to ensure
* textures start with fresh IDs.
* 在恢复场景快照时应调用此方法,以确保纹理从新 ID 开始。
*/
static resetTextureIdCounter(): void {
TextureLoader._nextTextureId = 1;
}
/**
* Parse texture from image content.
* 从图片内容解析纹理。
@@ -71,43 +29,46 @@ 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.
const bridge = getEngineBridge();
if (bridge?.loadTexture) {
await bridge.loadTexture(textureAsset.textureId, context.metadata.path);
if (typeof window !== 'undefined' && (window as any).engineBridge) {
await this.uploadToGPU(textureAsset, context.metadata.path);
}
return textureAsset;
}
/**
* Upload texture to GPU
* 上传纹理到GPU
*/
private async uploadToGPU(textureAsset: ITextureAsset, path: string): Promise<void> {
const bridge = (window as any).engineBridge;
if (bridge && bridge.loadTexture) {
await bridge.loadTexture(textureAsset.textureId, path);
}
}
/**
* Dispose loaded asset
* 释放已加载的资产
*/
dispose(asset: ITextureAsset): void {
// Release GPU resources.
const bridge = getEngineBridge();
if (bridge?.unloadTexture) {
if (typeof window !== 'undefined' && (window as any).engineBridge) {
const bridge = (window as any).engineBridge;
if (bridge.unloadTexture) {
bridge.unloadTexture(asset.textureId);
}
}
// Clean up image data.
if (asset.data instanceof HTMLImageElement) {

View File

@@ -1,139 +0,0 @@
/**
* Asset Metadata Service
* 资产元数据服务
*
* Provides global access to asset metadata without requiring asset loading.
* This service is independent of the texture loading path, allowing
* render systems to query sprite info regardless of how textures are loaded.
*
* 提供对资产元数据的全局访问,无需加载资产。
* 此服务独立于纹理加载路径,允许渲染系统查询 sprite 信息,
* 无论纹理是如何加载的。
*/
import { AssetDatabase, ITextureSpriteInfo } from '../core/AssetDatabase';
import type { AssetGUID } from '../types/AssetTypes';
import type { ITextureEngineBridge } from '../integration/EngineIntegration';
/**
* Global asset database instance
* 全局资产数据库实例
*/
let globalAssetDatabase: AssetDatabase | null = null;
/**
* Global engine bridge instance
* 全局引擎桥实例
*
* Used to query texture dimensions from Rust engine (single source of truth).
* 用于从 Rust 引擎查询纹理尺寸(唯一事实来源)。
*/
let globalEngineBridge: ITextureEngineBridge | null = null;
/**
* Set the global asset database
* 设置全局资产数据库
*
* Should be called during engine initialization.
* 应在引擎初始化期间调用。
*
* @param database - AssetDatabase instance | AssetDatabase 实例
*/
export function setGlobalAssetDatabase(database: AssetDatabase | null): void {
globalAssetDatabase = database;
}
/**
* Get the global asset database
* 获取全局资产数据库
*
* @returns AssetDatabase instance or null | AssetDatabase 实例或 null
*/
export function getGlobalAssetDatabase(): AssetDatabase | null {
return globalAssetDatabase;
}
/**
* Set the global engine bridge
* 设置全局引擎桥
*
* The engine bridge is used to query texture dimensions directly from Rust engine.
* This is the single source of truth for texture dimensions.
* 引擎桥用于直接从 Rust 引擎查询纹理尺寸。
* 这是纹理尺寸的唯一事实来源。
*
* @param bridge - ITextureEngineBridge instance | ITextureEngineBridge 实例
*/
export function setGlobalEngineBridge(bridge: ITextureEngineBridge | null): void {
globalEngineBridge = bridge;
}
/**
* Get the global engine bridge
* 获取全局引擎桥
*
* @returns ITextureEngineBridge instance or null | ITextureEngineBridge 实例或 null
*/
export function getGlobalEngineBridge(): ITextureEngineBridge | null {
return globalEngineBridge;
}
/**
* Get texture sprite info by GUID
* 通过 GUID 获取纹理 Sprite 信息
*
* This is the primary API for render systems to query nine-patch/sprite info.
* It combines data from:
* - Asset metadata (sliceBorder, pivot) from AssetDatabase
* - Texture dimensions (width, height) from Rust engine (single source of truth)
*
* 这是渲染系统查询九宫格/sprite 信息的主要 API。
* 它合并来自:
* - AssetDatabase 的资产元数据sliceBorder, pivot
* - Rust 引擎的纹理尺寸width, height唯一事实来源
*
* @param guid - Texture asset GUID | 纹理资产 GUID
* @returns Sprite info or undefined | Sprite 信息或 undefined
*/
export function getTextureSpriteInfo(guid: AssetGUID): ITextureSpriteInfo | undefined {
// Get sprite settings from metadata
// 从元数据获取 sprite 设置
const metadataInfo = globalAssetDatabase?.getTextureSpriteInfo(guid);
// Get texture dimensions from Rust engine (single source of truth)
// 从 Rust 引擎获取纹理尺寸(唯一事实来源)
let dimensions: { width: number; height: number } | undefined;
if (globalEngineBridge?.getTextureInfoByPath && globalAssetDatabase) {
// Get asset path from database
// 从数据库获取资产路径
const metadata = globalAssetDatabase.getMetadata(guid);
if (metadata?.path) {
const engineInfo = globalEngineBridge.getTextureInfoByPath(metadata.path);
if (engineInfo) {
dimensions = engineInfo;
}
}
}
// If no metadata and no dimensions, return undefined
// 如果没有元数据也没有尺寸,返回 undefined
if (!metadataInfo && !dimensions) {
return undefined;
}
// Merge the two sources
// 合并两个数据源
// Prefer engine dimensions (runtime loaded), fallback to metadata dimensions (catalog stored)
// 优先使用引擎尺寸(运行时加载),后备使用元数据尺寸(目录存储)
return {
sliceBorder: metadataInfo?.sliceBorder,
pivot: metadataInfo?.pivot,
width: dimensions?.width ?? metadataInfo?.width,
height: dimensions?.height ?? metadataInfo?.height
};
}
// Re-export type for convenience
// 为方便起见重新导出类型
export type { ITextureSpriteInfo };

View File

@@ -1,239 +0,0 @@
/**
* 路径解析服务
* Path Resolution Service
*
* 提供统一的路径解析接口处理编辑器、Catalog、运行时三层路径转换。
* Provides unified path resolution interface for editor, catalog, and runtime path conversion.
*
* 路径格式约定 | Path Format Convention:
* - 编辑器路径 (Editor Path): 绝对路径,如 `C:\Project\assets\textures\bg.png`
* - Catalog 路径 (Catalog Path): 相对于 assets 目录,不含 `assets/` 前缀,如 `textures/bg.png`
* - 运行时 URL (Runtime URL): 完整 URL如 `./assets/textures/bg.png` 或 `https://cdn.example.com/assets/textures/bg.png`
*
* @example
* ```typescript
* import { PathResolutionServiceToken, type IPathResolutionService } from '@esengine/asset-system';
*
* // 获取服务
* const pathService = context.services.get(PathResolutionServiceToken);
*
* // Catalog 路径转运行时 URL
* const url = pathService.catalogToRuntime('textures/bg.png');
* // => './assets/textures/bg.png'
*
* // 编辑器路径转 Catalog 路径
* const catalogPath = pathService.editorToCatalog('C:\\Project\\assets\\textures\\bg.png', 'C:\\Project');
* // => 'textures/bg.png'
* ```
*/
import { createServiceToken } from '@esengine/ecs-framework';
// ============================================================================
// 接口定义 | Interface Definitions
// ============================================================================
/**
* 路径解析服务接口
* Path resolution service interface
*/
export interface IPathResolutionService {
/**
* 将 Catalog 路径转换为运行时 URL
* Convert catalog path to runtime URL
*
* @param catalogPath Catalog 路径(相对于 assets 目录,不含 assets/ 前缀)
* @returns 运行时 URL
*
* @example
* ```typescript
* // 输入: 'textures/bg.png'
* // 输出: './assets/textures/bg.png' (取决于 baseUrl 配置)
* pathService.catalogToRuntime('textures/bg.png');
* ```
*/
catalogToRuntime(catalogPath: string): string;
/**
* 将编辑器绝对路径转换为 Catalog 路径
* Convert editor absolute path to catalog path
*
* @param editorPath 编辑器绝对路径
* @param projectRoot 项目根目录
* @returns Catalog 路径(相对于 assets 目录,不含 assets/ 前缀)
*
* @example
* ```typescript
* // 输入: 'C:\\Project\\assets\\textures\\bg.png', 'C:\\Project'
* // 输出: 'textures/bg.png'
* pathService.editorToCatalog('C:\\Project\\assets\\textures\\bg.png', 'C:\\Project');
* ```
*/
editorToCatalog(editorPath: string, projectRoot: string): string;
/**
* 设置运行时基础 URL
* Set runtime base URL
*
* @param url 基础 URL通常为 './assets' 或 CDN URL
*/
setBaseUrl(url: string): void;
/**
* 获取当前基础 URL
* Get current base URL
*/
getBaseUrl(): string;
/**
* 规范化路径(统一斜杠方向,移除重复斜杠)
* Normalize path (unify slash direction, remove duplicate slashes)
*
* @param path 输入路径
* @returns 规范化后的路径
*/
normalize(path: string): string;
/**
* 检查路径是否为绝对 URL
* Check if path is absolute URL
*
* @param path 输入路径
* @returns 是否为绝对 URL
*/
isAbsoluteUrl(path: string): boolean;
}
// ============================================================================
// 服务令牌 | Service Token
// ============================================================================
/**
* 路径解析服务令牌
* Path resolution service token
*/
export const PathResolutionServiceToken = createServiceToken<IPathResolutionService>('pathResolutionService');
// ============================================================================
// 默认实现 | Default Implementation
// ============================================================================
/**
* 路径解析服务默认实现
* Default path resolution service implementation
*/
export class PathResolutionService implements IPathResolutionService {
private _baseUrl: string = './assets';
private _assetsDir: string = 'assets';
/**
* 创建路径解析服务
* Create path resolution service
*
* @param baseUrl 基础 URL默认 './assets'
*/
constructor(baseUrl?: string) {
if (baseUrl !== undefined) {
this._baseUrl = baseUrl;
}
}
/**
* 将 Catalog 路径转换为运行时 URL
* Convert catalog path to runtime URL
*/
catalogToRuntime(catalogPath: string): string {
// 空路径直接返回
if (!catalogPath) {
return catalogPath;
}
// 已经是绝对 URL 则直接返回
if (this.isAbsoluteUrl(catalogPath)) {
return catalogPath;
}
// Data URL 直接返回
if (catalogPath.startsWith('data:')) {
return catalogPath;
}
// 规范化路径
let normalized = this.normalize(catalogPath);
// 移除开头的斜杠
normalized = normalized.replace(/^\/+/, '');
// 如果路径以 'assets/' 开头,移除它(避免重复)
// Catalog 路径不应包含 assets/ 前缀
if (normalized.startsWith('assets/')) {
normalized = normalized.substring(7);
}
// 构建完整 URL
const base = this._baseUrl.replace(/\/+$/, ''); // 移除尾部斜杠
return `${base}/${normalized}`;
}
/**
* 将编辑器绝对路径转换为 Catalog 路径
* Convert editor absolute path to catalog path
*/
editorToCatalog(editorPath: string, projectRoot: string): string {
// 规范化路径
let normalizedPath = this.normalize(editorPath);
let normalizedRoot = this.normalize(projectRoot);
// 确保根路径以斜杠结尾
if (!normalizedRoot.endsWith('/')) {
normalizedRoot += '/';
}
// 移除项目根路径前缀
if (normalizedPath.startsWith(normalizedRoot)) {
normalizedPath = normalizedPath.substring(normalizedRoot.length);
}
// 移除 assets/ 前缀(如果存在)
const assetsPrefix = `${this._assetsDir}/`;
if (normalizedPath.startsWith(assetsPrefix)) {
normalizedPath = normalizedPath.substring(assetsPrefix.length);
}
return normalizedPath;
}
/**
* 设置运行时基础 URL
* Set runtime base URL
*/
setBaseUrl(url: string): void {
this._baseUrl = url;
}
/**
* 获取当前基础 URL
* Get current base URL
*/
getBaseUrl(): string {
return this._baseUrl;
}
/**
* 规范化路径
* Normalize path
*/
normalize(path: string): string {
return path
.replace(/\\/g, '/') // 反斜杠转正斜杠
.replace(/\/+/g, '/'); // 移除重复斜杠
}
/**
* 检查路径是否为绝对 URL
* Check if path is absolute URL
*/
isAbsoluteUrl(path: string): boolean {
return /^(https?:\/\/|file:\/\/|asset:\/\/|blob:)/.test(path);
}
}

View File

@@ -22,58 +22,11 @@ export interface IResourceLoader {
* @returns 路径到运行时 ID 的映射 / Map of paths to runtime IDs
*/
loadResourcesBatch(paths: string[], type: ResourceReference['type']): Promise<Map<string, number>>;
/**
* 卸载纹理资源(可选)
* Unload texture resource (optional)
*/
unloadTexture?(textureId: number): void;
/**
* 卸载音频资源(可选)
* Unload audio resource (optional)
*/
unloadAudio?(audioId: number): void;
/**
* 卸载数据资源(可选)
* Unload data resource (optional)
*/
unloadData?(dataId: number): void;
}
/**
* 资源引用计数条目
* Resource reference count entry
*/
interface ResourceRefCountEntry {
/** 资源路径 / Resource path */
path: string;
/** 资源类型 / Resource type */
type: ResourceReference['type'];
/** 运行时 ID / Runtime ID */
runtimeId: number;
/** 使用此资源的场景名称集合 / Set of scene names using this resource */
sceneNames: Set<string>;
}
export class SceneResourceManager {
private resourceLoader: IResourceLoader | null = null;
/**
* 资源引用计数表
* Resource reference count table
*
* Key: resource path, Value: reference count entry
*/
private _resourceRefCounts = new Map<string, ResourceRefCountEntry>();
/**
* 场景到其使用的资源路径的映射
* Map of scene name to resource paths used by that scene
*/
private _sceneResources = new Map<string, Set<string>>();
/**
* 设置资源加载器实现
* Set the resource loader implementation
@@ -100,8 +53,6 @@ export class SceneResourceManager {
* Batch load each resource type
* 4. 将运行时 ID 分配回组件
* Assign runtime IDs back to components
* 5. 更新引用计数
* Update reference counts
*
* @param scene 要加载资源的场景 / The scene to load resources for
* @returns 当所有资源加载完成时解析的 Promise / Promise that resolves when all resources are loaded
@@ -112,8 +63,6 @@ export class SceneResourceManager {
return;
}
const sceneName = scene.name;
// 从组件收集所有资源引用 / Collect all resource references from components
const resourceRefs = this.collectResourceReferences(scene);
@@ -142,9 +91,6 @@ export class SceneResourceManager {
// 合并到总映射表 / Merge into combined map
for (const [path, id] of resourceIds) {
allResourceIds.set(path, id);
// 更新引用计数 / Update reference count
this.addResourceReference(path, type, id, sceneName);
}
} catch (error) {
console.error(`[SceneResourceManager] Failed to load ${type} resources:`, error);
@@ -153,58 +99,6 @@ export class SceneResourceManager {
// 将资源 ID 分配回组件 / Assign resource IDs back to components
this.assignResourceIds(scene, allResourceIds);
// 记录场景使用的资源 / Record resources used by scene
const scenePaths = new Set<string>();
for (const ref of resourceRefs) {
scenePaths.add(ref.path);
}
this._sceneResources.set(sceneName, scenePaths);
}
/**
* 添加资源引用
* Add resource reference
*/
private addResourceReference(
path: string,
type: ResourceReference['type'],
runtimeId: number,
sceneName: string
): void {
let entry = this._resourceRefCounts.get(path);
if (!entry) {
entry = {
path,
type,
runtimeId,
sceneNames: new Set()
};
this._resourceRefCounts.set(path, entry);
}
entry.sceneNames.add(sceneName);
}
/**
* 移除资源引用
* Remove resource reference
*
* @returns true 如果资源引用计数归零 / true if resource reference count reaches zero
*/
private removeResourceReference(path: string, sceneName: string): boolean {
const entry = this._resourceRefCounts.get(path);
if (!entry) {
return false;
}
entry.sceneNames.delete(sceneName);
if (entry.sceneNames.size === 0) {
this._resourceRefCounts.delete(path);
return true;
}
return false;
}
/**
@@ -247,112 +141,15 @@ export class SceneResourceManager {
* 卸载场景使用的所有资源
* Unload all resources used by a scene
*
* 在场景销毁时调用,只会卸载不再被其他场景引用的资源
* Called when a scene is being destroyed, only unloads resources not referenced by other scenes
* 在场景销毁时调用
* Called when a scene is being destroyed
*
* @param scene 要卸载资源的场景 / The scene to unload resources for
*/
async unloadSceneResources(scene: Scene): Promise<void> {
const sceneName = scene.name;
// 获取场景使用的资源路径 / Get resource paths used by scene
const scenePaths = this._sceneResources.get(sceneName);
if (!scenePaths) {
return;
}
// 要卸载的资源 / Resources to unload
const toUnload: ResourceRefCountEntry[] = [];
// 移除引用并收集需要卸载的资源 / Remove references and collect resources to unload
for (const path of scenePaths) {
const entry = this._resourceRefCounts.get(path);
if (entry) {
const shouldUnload = this.removeResourceReference(path, sceneName);
if (shouldUnload) {
toUnload.push(entry);
}
}
}
// 清理场景资源记录 / Clean up scene resource record
this._sceneResources.delete(sceneName);
// 卸载不再使用的资源 / Unload resources no longer in use
if (this.resourceLoader && toUnload.length > 0) {
for (const entry of toUnload) {
this.unloadResource(entry);
}
}
}
/**
* 卸载单个资源
* Unload a single resource
*/
private unloadResource(entry: ResourceRefCountEntry): void {
if (!this.resourceLoader) return;
switch (entry.type) {
case 'texture':
if (this.resourceLoader.unloadTexture) {
this.resourceLoader.unloadTexture(entry.runtimeId);
}
break;
case 'audio':
if (this.resourceLoader.unloadAudio) {
this.resourceLoader.unloadAudio(entry.runtimeId);
}
break;
case 'data':
if (this.resourceLoader.unloadData) {
this.resourceLoader.unloadData(entry.runtimeId);
}
break;
case 'font':
// 字体卸载暂未实现 / Font unloading not yet implemented
break;
}
}
/**
* 获取资源统计信息
* Get resource statistics
*/
getStatistics(): {
totalResources: number;
trackedScenes: number;
resourcesByType: Map<ResourceReference['type'], number>;
} {
const resourcesByType = new Map<ResourceReference['type'], number>();
for (const entry of this._resourceRefCounts.values()) {
const count = resourcesByType.get(entry.type) || 0;
resourcesByType.set(entry.type, count + 1);
}
return {
totalResources: this._resourceRefCounts.size,
trackedScenes: this._sceneResources.size,
resourcesByType
};
}
/**
* 获取资源的引用计数
* Get reference count for a resource
*/
getResourceRefCount(path: string): number {
const entry = this._resourceRefCounts.get(path);
return entry ? entry.sceneNames.size : 0;
}
/**
* 清空所有跟踪数据
* Clear all tracking data
*/
clearAll(): void {
this._resourceRefCounts.clear();
this._sceneResources.clear();
async unloadSceneResources(_scene: Scene): Promise<void> {
// TODO: 实现资源卸载 / Implement resource unloading
// 需要跟踪资源引用计数,仅在不再使用时卸载
// Need to track resource reference counts and only unload when no longer used
console.log('[SceneResourceManager] Scene resource unloading not yet implemented');
}
}

View File

@@ -1,54 +0,0 @@
/**
* Asset System 服务令牌
* Asset System service tokens
*
* 定义 asset-system 模块导出的服务令牌和接口。
* Defines service tokens and interfaces exported by asset-system module.
*
* @example
* ```typescript
* // 消费方导入 Token | Consumer imports Token
* import { AssetManagerToken, type IAssetManager } from '@esengine/asset-system';
*
* // 获取服务 | Get service
* const assetManager = context.services.get(AssetManagerToken);
* ```
*/
import { createServiceToken } from '@esengine/ecs-framework';
import type { IAssetManager } from './interfaces/IAssetManager';
import type { IPrefabService } from './interfaces/IPrefabAsset';
import type { IPathResolutionService } from './services/PathResolutionService';
// 重新导出接口方便使用 | Re-export interface for convenience
export type { IAssetManager } from './interfaces/IAssetManager';
export type { IAssetLoadResult } from './types/AssetTypes';
export type { IPrefabService, IPrefabAsset, IPrefabData, IPrefabMetadata } from './interfaces/IPrefabAsset';
export type { IPathResolutionService } from './services/PathResolutionService';
/**
* 资产管理器服务令牌
* Asset manager service token
*
* 用于注册和获取资产管理器服务。
* For registering and getting asset manager service.
*/
export const AssetManagerToken = createServiceToken<IAssetManager>('assetManager');
/**
* 预制体服务令牌
* Prefab service token
*
* 用于注册和获取预制体服务。
* For registering and getting prefab service.
*/
export const PrefabServiceToken = createServiceToken<IPrefabService>('prefabService');
/**
* 路径解析服务令牌
* Path resolution service token
*
* 用于注册和获取路径解析服务。
* For registering and getting path resolution service.
*/
export const PathResolutionServiceToken = createServiceToken<IPathResolutionService>('pathResolutionService');

View File

@@ -357,170 +357,40 @@ export interface IAssetLoadProgress {
progress: number;
}
/**
* Asset loading strategy
* 资产加载策略
*
* - 'file': Load assets directly via HTTP (development, simple builds)
* - 'bundle': Load assets from binary bundles (optimized production builds)
*
* - 'file': 通过 HTTP 直接加载资产(开发模式、简单构建)
* - 'bundle': 从二进制包加载资产(优化的生产构建)
*/
export type AssetLoadStrategy = 'file' | 'bundle';
/**
* Asset catalog entry for runtime lookups
* 运行时查找的资产目录条目
*
* This is a unified format supporting both file-based and bundle-based loading.
* 这是一个统一格式,同时支持基于文件和基于包的加载。
*/
export interface IAssetCatalogEntry {
/** 资产 GUID / Asset GUID */
/** 资产GUID */
guid: AssetGUID;
/** 资产相对路径 / Asset relative path (e.g., 'assets/textures/player.png') */
/** 资产路径 */
path: string;
/** 资产类型 / Asset type */
/** 资产类型 */
type: AssetType;
/** 文件大小(字节) / File size in bytes */
size: number;
/** 内容哈希(用于缓存校验) / Content hash for cache validation */
hash: string;
// ===== Bundle mode fields (optional) =====
// ===== Bundle 模式字段(可选)=====
/** 所在包名称(仅 bundle 模式) / Bundle name (bundle mode only) */
bundle?: string;
/** 包内偏移(仅 bundle 模式) / Offset within bundle (bundle mode only) */
offset?: number;
// ===== Optional metadata =====
// ===== 可选元数据 =====
/** 可用变体 / Available variants (platform/quality specific) */
/** 所在包名称 / Bundle containing this asset */
bundleName?: string;
/** 可用变体 / Available variants */
variants?: IAssetVariant[];
/**
* Import settings (e.g., sprite slicing for nine-patch)
* 导入设置(如九宫格切片信息)
*/
importSettings?: Record<string, unknown>;
}
/**
* Asset bundle info for runtime loading
* 运行时加载的资产包信息
*/
export interface IAssetBundleInfo {
/** 包 URL相对于 catalog / Bundle URL relative to catalog */
url: string;
/** 包大小(字节) / Bundle size in bytes */
/** 大小(字节) / Size in bytes */
size: number;
/** 内容哈希 / Content hash for integrity check */
/** 内容哈希 / Content hash */
hash: string;
/** 是否预加载 / Whether to preload this bundle */
preload?: boolean;
/** 压缩类型 / Compression type */
compression?: 'none' | 'gzip' | 'brotli';
/** 依赖的其他包 / Dependencies on other bundles */
dependencies?: string[];
}
/**
* Runtime asset catalog
* 运行时资产目录
*
* This is the canonical format for asset catalogs in ESEngine.
* Both WebBuildPipeline and AssetPacker generate this format.
* 这是 ESEngine 中资产目录的标准格式。
* WebBuildPipeline 和 AssetPacker 都生成此格式。
*
* @example File mode (development/simple builds)
* ```json
* {
* "version": "1.0.0",
* "createdAt": 1702185600000,
* "loadStrategy": "file",
* "entries": {
* "550e8400-e29b-41d4-a716-446655440000": {
* "guid": "550e8400-e29b-41d4-a716-446655440000",
* "path": "assets/textures/player.png",
* "type": "texture",
* "size": 12345,
* "hash": "abc123"
* }
* }
* }
* ```
*
* @example Bundle mode (optimized production)
* ```json
* {
* "version": "1.0.0",
* "createdAt": 1702185600000,
* "loadStrategy": "bundle",
* "entries": {
* "550e8400-e29b-41d4-a716-446655440000": {
* "guid": "550e8400-e29b-41d4-a716-446655440000",
* "path": "assets/textures/player.png",
* "type": "texture",
* "size": 12345,
* "hash": "abc123",
* "bundle": "textures",
* "offset": 1024
* }
* },
* "bundles": {
* "textures": {
* "url": "bundles/textures.bundle",
* "size": 1048576,
* "hash": "def456",
* "preload": true
* }
* }
* }
* ```
*/
export interface IAssetCatalog {
/** 目录版本号 / Catalog version */
/** 版本号 */
version: string;
/** 创建时间戳 / Creation timestamp (Unix ms) */
/** 创建时间戳 / Creation timestamp */
createdAt: number;
/**
* 加载策略 / Loading strategy
* - 'file': 直接 HTTP 加载
* - 'bundle': 从二进制包加载
*/
loadStrategy: AssetLoadStrategy;
/**
* 资产条目GUID 到条目的映射)
* Asset entries (GUID to entry mapping)
*
* Uses Record for JSON serialization compatibility.
* 使用 Record 以兼容 JSON 序列化。
*/
entries: Record<AssetGUID, IAssetCatalogEntry>;
/**
* 包信息(仅 bundle 模式)
* Bundle info (bundle mode only)
*/
bundles?: Record<string, IAssetBundleInfo>;
/** 所有目录条目 / All catalog entries */
entries: Map<AssetGUID, IAssetCatalogEntry>;
/** 此目录中的包 / Bundles in this catalog */
bundles: Map<string, IAssetBundleManifest>;
}
/**

View File

@@ -1,239 +0,0 @@
/**
* 通用资产收集器
* Generic Asset Collector
*
* 从序列化的场景数据中自动收集资产引用。
* 支持基于字段名模式和 Property 元数据两种识别方式。
*
* Automatically collects asset references from serialized scene data.
* Supports both field name pattern matching and Property metadata recognition.
*/
/**
* 场景资产引用信息(用于构建时收集)
* Scene asset reference info (for build-time collection)
*/
export interface SceneAssetRef {
/** 资产 GUID | Asset GUID */
guid: string;
/** 来源组件类型 | Source component type */
componentType: string;
/** 来源字段名 | Source field name */
fieldName: string;
/** 实体名称(可选)| Entity name (optional) */
entityName?: string;
}
/**
* 资产字段模式配置
* Asset field pattern configuration
*/
export interface AssetFieldPattern {
/** 字段名模式(正则表达式)| Field name pattern (regex) */
pattern: RegExp;
/** 字段类型(用于分类)| Field type (for categorization) */
type?: string;
}
/**
* 默认资产字段模式
* Default asset field patterns
*
* 这些模式用于识别常见的资产引用字段
* These patterns are used to identify common asset reference fields
*/
export const DEFAULT_ASSET_PATTERNS: AssetFieldPattern[] = [
// GUID 类字段 | GUID-like fields
{ pattern: /^.*[Gg]uid$/, type: 'guid' },
{ pattern: /^.*[Aa]sset[Ii]d$/, type: 'guid' },
{ pattern: /^.*[Aa]ssetGuid$/, type: 'guid' },
// 纹理/贴图字段 | Texture fields
{ pattern: /^texture$/, type: 'texture' },
{ pattern: /^.*[Tt]exture[Pp]ath$/, type: 'texture' },
// 音频字段 | Audio fields
{ pattern: /^clip$/, type: 'audio' },
{ pattern: /^.*[Aa]udio[Pp]ath$/, type: 'audio' },
// 通用路径字段 | Generic path fields
{ pattern: /^.*[Pp]ath$/, type: 'path' },
];
/**
* 检查值是否像 GUID
* Check if value looks like a GUID
*/
function isGuidLike(value: unknown): value is string {
if (typeof value !== 'string') return false;
// GUID 格式: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
// 或者简单的包含连字符的长字符串
return /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(value) ||
(value.includes('-') && value.length >= 30 && value.length <= 40);
}
/**
* 从组件数据中收集资产引用
* Collect asset references from component data
*/
function collectFromComponentData(
componentType: string,
data: Record<string, unknown>,
patterns: AssetFieldPattern[],
entityName?: string
): SceneAssetRef[] {
const references: SceneAssetRef[] = [];
for (const [fieldName, value] of Object.entries(data)) {
// 检查是否匹配任何资产字段模式
// Check if matches any asset field pattern
const matchesPattern = patterns.some(p => p.pattern.test(fieldName));
if (matchesPattern) {
// 处理单个值 | Handle single value
if (isGuidLike(value)) {
references.push({
guid: value,
componentType,
fieldName,
entityName
});
}
// 处理数组 | Handle array
else if (Array.isArray(value)) {
for (const item of value) {
if (isGuidLike(item)) {
references.push({
guid: item,
componentType,
fieldName,
entityName
});
}
}
}
}
// 特殊处理已知的数组字段(如 particleAssets
// Special handling for known array fields (like particleAssets)
if (fieldName === 'particleAssets' && Array.isArray(value)) {
for (const item of value) {
if (isGuidLike(item)) {
references.push({
guid: item,
componentType,
fieldName,
entityName
});
}
}
}
}
return references;
}
/**
* 实体类型定义(支持嵌套 children
* Entity type definition (supports nested children)
*/
interface EntityData {
name?: string;
components?: Array<{ type: string; data?: Record<string, unknown> }>;
children?: EntityData[];
}
/**
* 递归处理实体及其子实体
* Recursively process entity and its children
*/
function collectFromEntity(
entity: EntityData,
patterns: AssetFieldPattern[],
references: SceneAssetRef[]
): void {
const entityName = entity.name;
// 处理当前实体的组件 | Process current entity's components
if (entity.components) {
for (const component of entity.components) {
if (!component.data) continue;
const componentRefs = collectFromComponentData(
component.type,
component.data,
patterns,
entityName
);
references.push(...componentRefs);
}
}
// 递归处理子实体 | Recursively process children
if (entity.children && Array.isArray(entity.children)) {
for (const child of entity.children) {
collectFromEntity(child, patterns, references);
}
}
}
/**
* 从序列化的场景数据中收集所有资产引用
* Collect all asset references from serialized scene data
*
* @param sceneData 序列化的场景数据JSON 对象)| Serialized scene data (JSON object)
* @param patterns 资产字段模式(可选,默认使用内置模式)| Asset field patterns (optional, defaults to built-in patterns)
* @returns 资产引用列表 | List of asset references
*
* @example
* ```typescript
* const sceneData = JSON.parse(sceneJson);
* const references = collectAssetReferences(sceneData);
* for (const ref of references) {
* console.log(`Found asset ${ref.guid} in ${ref.componentType}.${ref.fieldName}`);
* }
* ```
*/
export function collectAssetReferences(
sceneData: { entities?: EntityData[] },
patterns: AssetFieldPattern[] = DEFAULT_ASSET_PATTERNS
): SceneAssetRef[] {
const references: SceneAssetRef[] = [];
if (!sceneData.entities) {
return references;
}
// 遍历顶层实体,递归处理嵌套的子实体
// Iterate top-level entities, recursively process nested children
for (const entity of sceneData.entities) {
collectFromEntity(entity, patterns, references);
}
return references;
}
/**
* 从资产引用列表中提取唯一的 GUID 集合
* Extract unique GUID set from asset references
*/
export function extractUniqueGuids(references: SceneAssetRef[]): Set<string> {
return new Set(references.map(ref => ref.guid));
}
/**
* 按组件类型分组资产引用
* Group asset references by component type
*/
export function groupByComponentType(references: SceneAssetRef[]): Map<string, SceneAssetRef[]> {
const groups = new Map<string, SceneAssetRef[]>();
for (const ref of references) {
const existing = groups.get(ref.componentType) || [];
existing.push(ref);
groups.set(ref.componentType, existing);
}
return groups;
}

View File

@@ -1,79 +0,0 @@
/**
* Asset Utilities
* 资产工具函数
*
* Provides common utilities for asset management:
* - GUID validation and generation (re-exported from core)
* - Content hashing
* 提供资产管理的通用工具:
* - GUID 验证和生成(从 core 重导出)
* - 内容哈希
*/
// Re-export GUID utilities from core (single source of truth)
// 从 core 重导出 GUID 工具(单一来源)
export { generateGUID, isValidGUID } from '@esengine/ecs-framework';
// ============================================================================
// Hash Utilities
// 哈希工具
// ============================================================================
/**
* Compute SHA-256 hash of an ArrayBuffer
* 计算 ArrayBuffer 的 SHA-256 哈希
*
* Returns first 16 hex characters of the hash.
* 返回哈希的前 16 个十六进制字符。
*
* @param buffer - The buffer to hash
* @returns Hash string (16 hex characters)
*/
export async function hashBuffer(buffer: ArrayBuffer): Promise<string> {
// Use Web Crypto API if available
// 如果可用则使用 Web Crypto API
if (typeof crypto !== 'undefined' && crypto.subtle) {
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('').slice(0, 16);
}
// Fallback: simple DJB2 hash
// 回退:简单的 DJB2 哈希
const view = new Uint8Array(buffer);
let hash = 5381;
for (let i = 0; i < view.length; i++) {
hash = ((hash << 5) + hash) ^ view[i];
}
return Math.abs(hash).toString(16).padStart(16, '0');
}
/**
* Compute hash of a string
* 计算字符串的哈希
*
* @param str - The string to hash
* @returns Hash string (8 hex characters)
*/
export function hashString(str: string): string {
let hash = 5381;
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) + hash) ^ str.charCodeAt(i);
}
return Math.abs(hash).toString(16).padStart(8, '0');
}
/**
* Compute content hash from file path and size
* 从文件路径和大小计算内容哈希
*
* This is a lightweight hash for quick comparison, not cryptographically secure.
* 这是一个用于快速比较的轻量级哈希,不具有加密安全性。
*
* @param path - File path
* @param size - File size in bytes
* @returns Hash string (8 hex characters)
*/
export function hashFileInfo(path: string, size: number): string {
return hashString(`${path}:${size}`);
}

View File

@@ -1,7 +1,6 @@
{
"id": "audio",
"name": "@esengine/audio",
"globalKey": "audio",
"displayName": "Audio",
"description": "Audio playback and sound effects | 音频播放和音效",
"version": "1.0.0",

View File

@@ -1,9 +1,9 @@
import type { IComponentRegistry } from '@esengine/ecs-framework';
import type { IRuntimeModule, IRuntimePlugin, ModuleManifest } from '@esengine/engine-core';
import type { ComponentRegistry as ComponentRegistryType } from '@esengine/ecs-framework';
import type { IRuntimeModule, IPlugin, ModuleManifest } from '@esengine/engine-core';
import { AudioSourceComponent } from './AudioSourceComponent';
class AudioRuntimeModule implements IRuntimeModule {
registerComponents(registry: IComponentRegistry): void {
registerComponents(registry: typeof ComponentRegistryType): void {
registry.register(AudioSourceComponent);
}
}
@@ -22,7 +22,7 @@ const manifest: ModuleManifest = {
exports: { components: ['AudioSourceComponent'] }
};
export const AudioPlugin: IRuntimePlugin = {
export const AudioPlugin: IPlugin = {
manifest,
runtimeModule: new AudioRuntimeModule()
};

View File

@@ -1,15 +1,11 @@
import { Component, ECSComponent, Serializable, Serialize, Property } from '@esengine/ecs-framework';
@ECSComponent('AudioSource')
@Serializable({ version: 2, typeId: 'AudioSource' })
@Serializable({ version: 1, typeId: 'AudioSource' })
export class AudioSourceComponent extends Component {
/**
* 音频资产 GUID
* Audio clip asset GUID
*/
@Serialize()
@Property({ type: 'asset', label: 'Audio Clip', assetType: 'audio' })
clipGuid: string = '';
clip: string = '';
/** 范围 [0, 1] */
@Serialize()

View File

@@ -1,6 +1,2 @@
export { AudioSourceComponent } from './AudioSourceComponent';
export { AudioPlugin } from './AudioPlugin';
// Service Tokens (reserved for future use)
// 服务令牌(预留用于未来扩展)
// export { AudioManagerToken, type IAudioManager } from './tokens';

View File

@@ -1,31 +0,0 @@
/**
* Audio Module Service Tokens
* 音频模块服务令牌
*
* 遵循"谁定义接口,谁导出 Token"原则。
* Following "who defines interface, who exports Token" principle.
*
* 当前模块仅提供组件,暂无服务定义。
* 此文件预留用于未来可能添加的 AudioManager 服务。
*
* Currently this module only provides components, no services defined yet.
* This file is reserved for potential future AudioManager service.
*/
// import { createServiceToken } from '@esengine/ecs-framework';
// ============================================================================
// Reserved for future service tokens
// 预留用于未来的服务令牌
// ============================================================================
// export interface IAudioManager {
// // 播放音效 | Play sound effect
// playSound(path: string): void;
// // 播放背景音乐 | Play background music
// playMusic(path: string): void;
// // 停止所有音频 | Stop all audio
// stopAll(): void;
// }
// export const AudioManagerToken = createServiceToken<IAudioManager>('audioManager');

View File

@@ -2,7 +2,6 @@ import { React, useRef, useEffect, useState, useMemo, Icons } from '@esengine/ed
import type { LucideIcon } from '@esengine/editor-runtime';
import type { NodeTemplate } from '@esengine/behavior-tree';
import { NodeFactory } from '../../infrastructure/factories/NodeFactory';
import { useBTLocale } from '../../hooks/useBTLocale';
const { Search, X, ChevronDown, ChevronRight } = Icons;
@@ -36,7 +35,6 @@ export const QuickCreateMenu: React.FC<QuickCreateMenuProps> = ({
onNodeSelect,
onClose
}) => {
const { t } = useBTLocale();
const selectedNodeRef = useRef<HTMLDivElement>(null);
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(new Set());
const [shouldAutoScroll, setShouldAutoScroll] = useState(false);
@@ -54,12 +52,11 @@ export const QuickCreateMenu: React.FC<QuickCreateMenuProps> = ({
})
: allTemplates;
const uncategorizedLabel = t('quickCreate.uncategorized');
const categoryGroups: CategoryGroup[] = React.useMemo(() => {
const groups = new Map<string, NodeTemplate[]>();
filteredTemplates.forEach((template: NodeTemplate) => {
const category = template.category || uncategorizedLabel;
const category = template.category || '未分类';
if (!groups.has(category)) {
groups.set(category, []);
}
@@ -71,7 +68,7 @@ export const QuickCreateMenu: React.FC<QuickCreateMenuProps> = ({
templates,
isExpanded: searchTextLower ? true : expandedCategories.has(category)
})).sort((a, b) => a.category.localeCompare(b.category));
}, [filteredTemplates, expandedCategories, searchTextLower, uncategorizedLabel]);
}, [filteredTemplates, expandedCategories, searchTextLower]);
const flattenedTemplates = React.useMemo(() => {
return categoryGroups.flatMap((group) =>
@@ -93,10 +90,10 @@ export const QuickCreateMenu: React.FC<QuickCreateMenuProps> = ({
useEffect(() => {
if (allTemplates.length > 0 && expandedCategories.size === 0) {
const categories = new Set(allTemplates.map((tmpl) => tmpl.category || uncategorizedLabel));
const categories = new Set(allTemplates.map((t) => t.category || '未分类'));
setExpandedCategories(categories);
}
}, [allTemplates, expandedCategories.size, uncategorizedLabel]);
}, [allTemplates, expandedCategories.size]);
useEffect(() => {
if (shouldAutoScroll && selectedNodeRef.current) {
@@ -164,7 +161,7 @@ export const QuickCreateMenu: React.FC<QuickCreateMenuProps> = ({
<Search size={16} style={{ color: '#999', flexShrink: 0 }} />
<input
type="text"
placeholder={t('quickCreate.searchPlaceholder')}
placeholder="搜索节点..."
autoFocus
value={searchText}
onChange={(e) => {
@@ -232,7 +229,7 @@ export const QuickCreateMenu: React.FC<QuickCreateMenuProps> = ({
color: '#666',
fontSize: '12px'
}}>
{t('quickCreate.noMatchingNodes')}
</div>
) : (
categoryGroups.map((group) => {

View File

@@ -11,7 +11,7 @@ import {
} from '@esengine/editor-runtime';
import { useBehaviorTreeDataStore } from '../../stores';
import { BehaviorTreeEditor } from '../BehaviorTreeEditor';
import { BehaviorTreeServiceToken } from '../../tokens';
import { BehaviorTreeService } from '../../services/BehaviorTreeService';
import { showToast } from '../../services/NotificationService';
import { Node as BehaviorTreeNode } from '../../domain/models/Node';
import { BehaviorTree } from '../../domain/models/BehaviorTree';
@@ -171,7 +171,7 @@ export const BehaviorTreeEditorPanel: React.FC<BehaviorTreeEditorPanelProps> = (
filePath = selected;
}
const service = PluginAPI.resolve(BehaviorTreeServiceToken);
const service = PluginAPI.resolve<BehaviorTreeService>(BehaviorTreeService);
await service.saveToFile(filePath);
setCurrentFilePath(filePath);
@@ -205,7 +205,7 @@ export const BehaviorTreeEditorPanel: React.FC<BehaviorTreeEditorPanelProps> = (
if (!selected) return;
const filePath = selected as string;
const service = PluginAPI.resolve(BehaviorTreeServiceToken);
const service = PluginAPI.resolve<BehaviorTreeService>(BehaviorTreeService);
await service.loadFromFile(filePath);
setCurrentFilePath(filePath);

View File

@@ -1,5 +1,4 @@
import { React, Icons } from '@esengine/editor-runtime';
import { useBTLocale } from '../../hooks/useBTLocale';
const { Play, Pause, Square, SkipForward, Undo, Redo, ZoomIn, Save, FolderOpen, Download, Clipboard, Home } = Icons;
@@ -44,8 +43,6 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
onCopyToClipboard,
onGoToRoot
}) => {
const { t } = useBTLocale();
return (
<div style={{
position: 'absolute',
@@ -84,7 +81,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
alignItems: 'center',
transition: 'all 0.15s'
}}
title={t('toolbar.openFile')}
title="打开文件 (Ctrl+O)"
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#4a4a4a'}
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#3c3c3c'}
>
@@ -107,7 +104,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
alignItems: 'center',
transition: 'all 0.15s'
}}
title={hasUnsavedChanges ? t('toolbar.saveUnsaved') : t('toolbar.save')}
title={`保存 (Ctrl+S)${hasUnsavedChanges ? ' - 有未保存的更改' : ''}`}
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = hasUnsavedChanges ? '#1d4ed8' : '#4a4a4a'}
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = hasUnsavedChanges ? '#2563eb' : '#3c3c3c'}
>
@@ -130,7 +127,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
alignItems: 'center',
transition: 'all 0.15s'
}}
title={t('toolbar.export')}
title="导出运行时配置"
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#4a4a4a'}
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#3c3c3c'}
>
@@ -153,7 +150,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
alignItems: 'center',
transition: 'all 0.15s'
}}
title={t('toolbar.copyToClipboard')}
title="复制JSON到剪贴板"
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#4a4a4a'}
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#3c3c3c'}
>
@@ -195,7 +192,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
gap: '4px',
transition: 'all 0.15s'
}}
title={t('toolbar.run')}
title="运行 (Play)"
onMouseEnter={(e) => {
if (executionMode !== 'running') {
e.currentTarget.style.backgroundColor = '#15803d';
@@ -226,7 +223,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
alignItems: 'center',
transition: 'all 0.15s'
}}
title={executionMode === 'paused' ? t('toolbar.resume') : t('toolbar.pause')}
title={executionMode === 'paused' ? '继续' : '暂停'}
onMouseEnter={(e) => {
if (executionMode !== 'idle') {
e.currentTarget.style.backgroundColor = '#d97706';
@@ -257,7 +254,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
alignItems: 'center',
transition: 'all 0.15s'
}}
title={t('toolbar.stop')}
title="停止"
onMouseEnter={(e) => {
if (executionMode !== 'idle') {
e.currentTarget.style.backgroundColor = '#b91c1c';
@@ -288,7 +285,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
alignItems: 'center',
transition: 'all 0.15s'
}}
title={t('toolbar.step')}
title="单步执行"
onMouseEnter={(e) => {
if (executionMode === 'idle' || executionMode === 'paused') {
e.currentTarget.style.backgroundColor = '#2563eb';
@@ -327,7 +324,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
gap: '4px',
transition: 'all 0.15s'
}}
title={t('toolbar.resetView')}
title="重置视图 (滚轮缩放, Alt+拖动平移)"
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#4a4a4a'}
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#3c3c3c'}
>
@@ -365,7 +362,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
alignItems: 'center',
transition: 'all 0.15s'
}}
title={t('toolbar.undo')}
title="撤销 (Ctrl+Z)"
onMouseEnter={(e) => {
if (canUndo) {
e.currentTarget.style.backgroundColor = '#4a4a4a';
@@ -395,7 +392,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
alignItems: 'center',
transition: 'all 0.15s'
}}
title={t('toolbar.redo')}
title="重做 (Ctrl+Shift+Z / Ctrl+Y)"
onMouseEnter={(e) => {
if (canRedo) {
e.currentTarget.style.backgroundColor = '#4a4a4a';
@@ -441,8 +438,8 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
color: executionMode === 'running' ? '#16a34a' :
executionMode === 'paused' ? '#f59e0b' : '#888'
}}>
{executionMode === 'idle' ? t('execution.idle') :
executionMode === 'running' ? t('execution.running') : t('execution.paused')}
{executionMode === 'idle' ? 'Idle' :
executionMode === 'running' ? 'Running' : 'Paused'}
</span>
</div>
@@ -468,7 +465,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
gap: '4px',
transition: 'all 0.15s'
}}
title={t('toolbar.goToRoot')}
title="回到根节点"
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#4a4a4a'}
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#3c3c3c'}
>

View File

@@ -1,12 +1,8 @@
import { DomainError } from './DomainError';
import { translateBT } from '../../hooks/useBTLocale';
/**
* 验证错误
* Validation Error
*
* 当业务规则验证失败时抛出
* Thrown when business rule validation fails
*/
export class ValidationError extends DomainError {
constructor(
@@ -19,28 +15,28 @@ export class ValidationError extends DomainError {
static rootNodeMaxChildren(): ValidationError {
return new ValidationError(
translateBT('validation.rootNodeMaxChildren'),
'根节点只能连接一个子节点',
'children'
);
}
static decoratorNodeMaxChildren(): ValidationError {
return new ValidationError(
translateBT('validation.decoratorNodeMaxChildren'),
'装饰节点只能连接一个子节点',
'children'
);
}
static leafNodeNoChildren(): ValidationError {
return new ValidationError(
translateBT('validation.leafNodeNoChildren'),
'叶子节点不能有子节点',
'children'
);
}
static circularReference(nodeId: string): ValidationError {
return new ValidationError(
translateBT('validation.circularReference', undefined, { nodeId }),
`检测到循环引用,节点 ${nodeId} 不能连接到自己或其子节点`,
'connection',
nodeId
);
@@ -48,7 +44,7 @@ export class ValidationError extends DomainError {
static invalidConnection(from: string, to: string, reason: string): ValidationError {
return new ValidationError(
translateBT('validation.invalidConnection', undefined, { reason }),
`无效的连接:${reason}`,
'connection',
{ from, to }
);

View File

@@ -2,4 +2,3 @@ export { useCommandHistory } from './useCommandHistory';
export { useNodeOperations } from './useNodeOperations';
export { useConnectionOperations } from './useConnectionOperations';
export { useCanvasInteraction } from './useCanvasInteraction';
export { useBTLocale, translateBT } from './useBTLocale';

View File

@@ -1,73 +0,0 @@
/**
* Behavior Tree Editor Locale Hook
* 行为树编辑器语言钩子
*
* Uses the unified plugin i18n infrastructure from editor-runtime.
* 使用 editor-runtime 的统一插件国际化基础设施。
*/
import {
createPluginLocale,
createPluginTranslator,
getCurrentLocale
} from '@esengine/editor-runtime';
import { en, zh, es } from '../locales';
import type { Locale, TranslationParams } from '@esengine/editor-core';
// Create translations bundle
// 创建翻译包
const translations = { en, zh, es };
/**
* Hook for accessing behavior tree editor translations
* 访问行为树编辑器翻译的 Hook
*
* Uses the unified createPluginLocale factory from editor-runtime.
* 使用 editor-runtime 的统一 createPluginLocale 工厂。
*
* @example
* ```tsx
* const { t, locale } = useBTLocale();
* return <button title={t('toolbar.save')}>{t('toolbar.saveUnsaved')}</button>;
* ```
*/
export const useBTLocale = createPluginLocale(translations);
// Create non-React translator using the unified infrastructure
// 使用统一基础设施创建非 React 翻译器
const btTranslator = createPluginTranslator(translations);
/**
* Non-React translation function for behavior tree editor
* 行为树编辑器的非 React 翻译函数
*
* Use this in services, utilities, and other non-React contexts.
* 在服务、工具类和其他非 React 上下文中使用。
*
* @param key - Translation key | 翻译键
* @param locale - Optional locale, defaults to current locale | 可选语言,默认使用当前语言
* @param params - Optional interpolation parameters | 可选插值参数
*
* @example
* ```typescript
* // With explicit locale
* translateBT('errors.notFound', 'zh');
*
* // With current locale (auto-detected)
* translateBT('errors.notFound');
*
* // With parameters
* translateBT('messages.saved', undefined, { name: 'MyTree' });
* ```
*/
export function translateBT(
key: string,
locale?: Locale,
params?: TranslationParams
): string {
const targetLocale = locale || getCurrentLocale();
return btTranslator(key, targetLocale, params);
}
// Re-export for external use
// 重新导出供外部使用
export { getCurrentLocale } from '@esengine/editor-runtime';

View File

@@ -7,7 +7,7 @@ import type { ServiceContainer } from '@esengine/ecs-framework';
import { TransformComponent } from '@esengine/engine-core';
import {
type IEditorModuleLoader,
type IEditorPlugin,
type IPluginLoader,
type PanelDescriptor,
type EntityCreationTemplate,
type FileCreationTemplate,
@@ -27,7 +27,6 @@ import {
type IFileSystem,
createLogger,
PluginAPI,
LocaleService,
} from '@esengine/editor-runtime';
// Runtime imports from @esengine/behavior-tree package
@@ -36,7 +35,6 @@ import { BehaviorTreeRuntimeComponent, BehaviorTreeRuntimeModule } from '@esengi
// Editor components and services
import { BehaviorTreeService } from './services/BehaviorTreeService';
import { FileSystemService } from './services/FileSystemService';
import { BehaviorTreeServiceToken, type IBehaviorTreeService } from './tokens';
import { BehaviorTreeCompiler } from './compiler/BehaviorTreeCompiler';
import { BehaviorTreeNodeInspectorProvider } from './providers/BehaviorTreeNodeInspectorProvider';
import { BehaviorTreeEditorPanel } from './components/panels/BehaviorTreeEditorPanel';
@@ -47,9 +45,6 @@ import { PluginContext } from './PluginContext';
// Import manifest from local file
import { manifest } from './BehaviorTreePlugin';
// Import locale translations
import { en, zh, es } from './locales';
// 导入编辑器 CSS 样式(会被 vite 自动处理并注入到 DOM
// Import editor CSS styles (automatically handled and injected by vite)
import './styles/BehaviorTreeNode.css';
@@ -86,9 +81,6 @@ export class BehaviorTreeEditorModule implements IEditorModuleLoader {
// 订阅创建资产消息
this.subscribeToMessages(services);
// 注册翻译 | Register translations
this.registerTranslations(services);
logger.info('BehaviorTree editor module installed');
}
@@ -181,7 +173,7 @@ export class BehaviorTreeEditorModule implements IEditorModuleLoader {
if (this.services) {
this.services.unregister(FileSystemService);
this.services.unregister(BehaviorTreeServiceToken.id);
this.services.unregister(BehaviorTreeService);
}
useBehaviorTreeDataStore.getState().reset();
@@ -198,16 +190,11 @@ export class BehaviorTreeEditorModule implements IEditorModuleLoader {
}
services.registerSingleton(FileSystemService);
// BehaviorTreeService - 使用 ServiceToken.id (symbol) 注册
// BehaviorTreeService - register with ServiceToken.id (symbol)
// ServiceContainer 支持 symbol 作为 ServiceIdentifier
// ServiceContainer supports symbol as ServiceIdentifier
const tokenId = BehaviorTreeServiceToken.id;
if (services.isRegistered(tokenId)) {
services.unregister(tokenId);
// BehaviorTreeService
if (services.isRegistered(BehaviorTreeService)) {
services.unregister(BehaviorTreeService);
}
const btService = new BehaviorTreeService();
services.registerInstance(tokenId, btService);
services.registerSingleton(BehaviorTreeService);
}
private registerCompilers(services: ServiceContainer): void {
@@ -221,22 +208,6 @@ export class BehaviorTreeEditorModule implements IEditorModuleLoader {
}
}
/**
* 注册插件翻译到 LocaleService
* Register plugin translations to LocaleService
*/
private registerTranslations(services: ServiceContainer): void {
try {
const localeService = services.tryResolve<LocaleService>(LocaleService);
if (localeService) {
localeService.extendTranslations('behaviorTree', { en, zh, es });
logger.info('BehaviorTree translations registered');
}
} catch (error) {
logger.warn('Failed to register translations:', error);
}
}
private registerInspectorProviders(services: ServiceContainer): void {
try {
const inspectorRegistry = services.resolve<InspectorRegistry>(IInspectorRegistry);
@@ -313,9 +284,7 @@ export class BehaviorTreeEditorModule implements IEditorModuleLoader {
extensions: ['btree'],
onDoubleClick: async (filePath: string) => {
if (this.services) {
// 使用 ServiceToken.id 解析服务
// Resolve service using ServiceToken.id
const service = this.services.resolve<IBehaviorTreeService>(BehaviorTreeServiceToken.id);
const service = this.services.resolve(BehaviorTreeService);
if (service) {
await service.loadFromFile(filePath);
}
@@ -370,7 +339,7 @@ export class BehaviorTreeEditorModule implements IEditorModuleLoader {
}
// Create the complete plugin with editor module
export const BehaviorTreePlugin: IEditorPlugin = {
export const BehaviorTreePlugin: IPluginLoader = {
manifest,
runtimeModule: new BehaviorTreeRuntimeModule(),
editorModule: new BehaviorTreeEditorModule(),
@@ -382,7 +351,6 @@ export { BehaviorTreeRuntimeModule };
export { PluginContext } from './PluginContext';
export { BehaviorTreeEditorPanel } from './components/panels/BehaviorTreeEditorPanel';
export * from './services/BehaviorTreeService';
export * from './tokens';
export * from './providers/BehaviorTreeNodeInspectorProvider';
export * from './domain';

Some files were not shown because too many files have changed in this diff Show More