Compare commits
140 Commits
v2.1.51
...
issue-151-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a801e4f50e | ||
|
|
a9f9ad9b94 | ||
|
|
3cf1dab5b9 | ||
|
|
63165bbbfc | ||
|
|
61caad2bef | ||
|
|
b826bbc4c7 | ||
|
|
2ce7dad8d8 | ||
|
|
dff400bf22 | ||
|
|
27ce902344 | ||
|
|
33ee0a04c6 | ||
|
|
d68f6922f8 | ||
|
|
f8539d7958 | ||
|
|
14dc911e0a | ||
|
|
deccb6bf84 | ||
|
|
dacbfcae95 | ||
|
|
1b69ed17b7 | ||
|
|
241acc9050 | ||
|
|
8fa921930c | ||
|
|
011e43811a | ||
|
|
9f16debd75 | ||
|
|
92c56c439b | ||
|
|
7de6a5af0f | ||
|
|
173a063781 | ||
|
|
e04ac7c909 | ||
|
|
a6e49e1d47 | ||
|
|
f0046c7dc2 | ||
|
|
2a17c47c25 | ||
|
|
8d741bf1b9 | ||
|
|
c676006632 | ||
|
|
5bcfd597b9 | ||
|
|
3cda3c2238 | ||
|
|
43bdd7e43b | ||
|
|
1ec7892338 | ||
|
|
6bcfd48a2f | ||
|
|
345ef70972 | ||
|
|
c876edca0c | ||
|
|
fcf3def284 | ||
|
|
6f1a2896dd | ||
|
|
62381f4160 | ||
|
|
171805debf | ||
|
|
619abcbfbc | ||
|
|
03909924c2 | ||
|
|
f4ea077114 | ||
|
|
956ccf9195 | ||
|
|
e880925e3f | ||
|
|
0a860920ad | ||
|
|
fb7a1b1282 | ||
|
|
59970ef7c3 | ||
|
|
a7750c2894 | ||
|
|
b69b81f63a | ||
|
|
00fc6dfd67 | ||
|
|
82451e9fd3 | ||
|
|
d0fcc0e447 | ||
|
|
285279629e | ||
|
|
cbfe09b5e9 | ||
|
|
b757c1d06c | ||
|
|
4550a6146a | ||
|
|
3224bb9696 | ||
|
|
3a5e73266e | ||
|
|
1cf5641c4c | ||
|
|
85dad41e60 | ||
|
|
bd839cf431 | ||
|
|
b20b2ae4ce | ||
|
|
cac6aedf78 | ||
|
|
a572c80967 | ||
|
|
a09e8261db | ||
|
|
62e8ebe926 | ||
|
|
96e0a9126f | ||
|
|
4afb195814 | ||
|
|
7da5366bca | ||
|
|
d979c38615 | ||
|
|
7def06126b | ||
|
|
9f600f88b0 | ||
|
|
e3c4d5f0c0 | ||
|
|
b97f3a8431 | ||
|
|
3b917a06af | ||
|
|
360106fb92 | ||
|
|
507ed5005f | ||
|
|
8bea5d5e68 | ||
|
|
99076adb72 | ||
|
|
d1a6230b23 | ||
|
|
cfe3916934 | ||
|
|
d798995876 | ||
|
|
43e6b7bf88 | ||
|
|
9253686de1 | ||
|
|
7e7eae2d1a | ||
|
|
1924d979d6 | ||
|
|
ed84394301 | ||
|
|
bb99cf5389 | ||
|
|
2d0700f441 | ||
|
|
e3ead8a695 | ||
|
|
942043f0b0 | ||
|
|
23d81bca35 | ||
|
|
701f538e57 | ||
|
|
bb3017ffc2 | ||
|
|
f2b9c5cc5a | ||
|
|
532a52acfc | ||
|
|
c19b5ae9a7 | ||
|
|
5f507532ed | ||
|
|
6e48f22540 | ||
|
|
66aa9f4f20 | ||
|
|
62f895efe0 | ||
|
|
4a060e1ce3 | ||
|
|
a0177c9163 | ||
|
|
f45af34614 | ||
|
|
14a8d755f0 | ||
|
|
b67ab80c75 | ||
|
|
ae71af856b | ||
|
|
279c1d9bc9 | ||
|
|
9068a109b0 | ||
|
|
7850fc610c | ||
|
|
536871d09b | ||
|
|
1af2cf5f99 | ||
|
|
b13132b259 | ||
|
|
a1a6970ea4 | ||
|
|
41bbe23404 | ||
|
|
1d2a3e283e | ||
|
|
62d7521384 | ||
|
|
bf14b59a28 | ||
|
|
0a0f64510f | ||
|
|
9445c735c3 | ||
|
|
7339e7ecec | ||
|
|
79f7c89e23 | ||
|
|
e724e5a1ba | ||
|
|
fdaa94a61d | ||
|
|
6af0074c36 | ||
|
|
97a69fed09 | ||
|
|
959879440d | ||
|
|
fd1bbb0e00 | ||
|
|
072e68cf43 | ||
|
|
610232e6b0 | ||
|
|
69c46f32eb | ||
|
|
06b3f92007 | ||
|
|
c631290049 | ||
|
|
f41c1a3ca3 | ||
|
|
bd6ba84087 | ||
|
|
1512409eb3 | ||
|
|
bcb5feeb1c | ||
|
|
da8b7cf601 | ||
|
|
316527c459 |
35
.editorconfig
Normal file
35
.editorconfig
Normal file
@@ -0,0 +1,35 @@
|
||||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# 所有文件的默认设置
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
# TypeScript/JavaScript 文件
|
||||
[*.{ts,tsx,js,jsx,mjs,cjs}]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
# JSON 文件
|
||||
[*.json]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
# YAML 文件
|
||||
[*.{yml,yaml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
# Markdown 文件
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
indent_size = 2
|
||||
|
||||
# 包管理文件
|
||||
[{package.json,package-lock.json,tsconfig.json}]
|
||||
indent_size = 2
|
||||
45
.eslintrc.json
Normal file
45
.eslintrc.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2020,
|
||||
"sourceType": "module",
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"semi": ["error", "always"],
|
||||
"quotes": ["error", "single", { "avoidEscape": true }],
|
||||
"indent": ["error", 4, { "SwitchCase": 1 }],
|
||||
"no-trailing-spaces": "error",
|
||||
"eol-last": ["error", "always"],
|
||||
"comma-dangle": ["error", "none"],
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"array-bracket-spacing": ["error", "never"],
|
||||
"arrow-parens": ["error", "always"],
|
||||
"no-multiple-empty-lines": ["error", { "max": 2, "maxEOF": 1 }],
|
||||
"no-console": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
|
||||
"@typescript-eslint/no-non-null-assertion": "off"
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"node_modules/",
|
||||
"dist/",
|
||||
"bin/",
|
||||
"build/",
|
||||
"coverage/",
|
||||
"thirdparty/",
|
||||
"examples/lawn-mower-demo/",
|
||||
"extensions/",
|
||||
"*.min.js",
|
||||
"*.d.ts"
|
||||
]
|
||||
}
|
||||
44
.gitattributes
vendored
Normal file
44
.gitattributes
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
# 自动检测文本文件并规范化换行符
|
||||
* text=auto
|
||||
|
||||
# 源代码文件强制使用 LF
|
||||
*.ts text eol=lf
|
||||
*.tsx text eol=lf
|
||||
*.js text eol=lf
|
||||
*.jsx text eol=lf
|
||||
*.mjs text eol=lf
|
||||
*.cjs text eol=lf
|
||||
*.json text eol=lf
|
||||
*.md text eol=lf
|
||||
*.yml text eol=lf
|
||||
*.yaml text eol=lf
|
||||
|
||||
# 配置文件强制使用 LF
|
||||
.gitignore text eol=lf
|
||||
.gitattributes text eol=lf
|
||||
.editorconfig text eol=lf
|
||||
.prettierrc text eol=lf
|
||||
.prettierignore text eol=lf
|
||||
.eslintrc.json text eol=lf
|
||||
tsconfig.json text eol=lf
|
||||
|
||||
# Shell 脚本强制使用 LF
|
||||
*.sh text eol=lf
|
||||
|
||||
# Windows 批处理文件使用 CRLF
|
||||
*.bat text eol=crlf
|
||||
*.cmd text eol=crlf
|
||||
*.ps1 text eol=crlf
|
||||
|
||||
# 二进制文件不转换
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.ico binary
|
||||
*.svg binary
|
||||
*.woff binary
|
||||
*.woff2 binary
|
||||
*.ttf binary
|
||||
*.eot binary
|
||||
*.otf binary
|
||||
151
.github/workflows/release-editor.yml
vendored
Normal file
151
.github/workflows/release-editor.yml
vendored
Normal file
@@ -0,0 +1,151 @@
|
||||
name: Release Editor App
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'editor-v*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Release version (e.g., 1.0.0)'
|
||||
required: true
|
||||
default: '1.0.0'
|
||||
|
||||
jobs:
|
||||
build-tauri:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
arch: x64
|
||||
- platform: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
arch: x64
|
||||
- platform: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
arch: arm64
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Rust cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: packages/editor-app/src-tauri
|
||||
cache-on-failure: true
|
||||
|
||||
- name: Install dependencies (Ubuntu)
|
||||
if: matrix.platform == 'ubuntu-latest'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Update version in config files (for manual trigger)
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |
|
||||
cd packages/editor-app
|
||||
# 临时更新版本号用于构建(不提交到仓库)
|
||||
npm version ${{ github.event.inputs.version }} --no-git-tag-version
|
||||
node scripts/sync-version.js
|
||||
|
||||
- name: Cache TypeScript build
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
packages/core/bin
|
||||
packages/editor-core/dist
|
||||
key: ${{ runner.os }}-ts-build-${{ hashFiles('packages/core/src/**', 'packages/editor-core/src/**') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-ts-build-
|
||||
|
||||
- name: Build core package
|
||||
run: npm run build:core
|
||||
|
||||
- name: Build editor-core package
|
||||
run: |
|
||||
cd packages/editor-core
|
||||
npm run build
|
||||
|
||||
- name: Build Tauri app
|
||||
uses: tauri-apps/tauri-action@v0.5
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
with:
|
||||
projectPath: packages/editor-app
|
||||
tagName: ${{ github.event_name == 'workflow_dispatch' && format('editor-v{0}', github.event.inputs.version) || github.ref_name }}
|
||||
releaseName: 'ECS Editor v${{ github.event.inputs.version || github.ref_name }}'
|
||||
releaseBody: 'See the assets to download this version and install.'
|
||||
releaseDraft: false
|
||||
prerelease: false
|
||||
includeUpdaterJson: true
|
||||
updaterJsonKeepUniversal: false
|
||||
args: ${{ matrix.platform == 'macos-latest' && format('--target {0}', matrix.target) || '' }}
|
||||
|
||||
# 构建成功后,创建 PR 更新版本号
|
||||
update-version-pr:
|
||||
needs: build-tauri
|
||||
if: github.event_name == 'workflow_dispatch' && success()
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
|
||||
- name: Update version files
|
||||
run: |
|
||||
cd packages/editor-app
|
||||
npm version ${{ github.event.inputs.version }} --no-git-tag-version
|
||||
node scripts/sync-version.js
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: "chore(editor): bump version to ${{ github.event.inputs.version }}"
|
||||
branch: release/editor-v${{ github.event.inputs.version }}
|
||||
delete-branch: true
|
||||
title: "chore(editor): Release v${{ github.event.inputs.version }}"
|
||||
body: |
|
||||
## 🚀 Release v${{ github.event.inputs.version }}
|
||||
|
||||
This PR updates the editor version after successful release build.
|
||||
|
||||
### Changes
|
||||
- ✅ Updated `packages/editor-app/package.json` → `${{ github.event.inputs.version }}`
|
||||
- ✅ Updated `packages/editor-app/src-tauri/tauri.conf.json` → `${{ github.event.inputs.version }}`
|
||||
|
||||
### Release
|
||||
- 📦 [GitHub Release](https://github.com/${{ github.repository }}/releases/tag/editor-v${{ github.event.inputs.version }})
|
||||
|
||||
---
|
||||
*This PR was automatically created by the release workflow.*
|
||||
labels: |
|
||||
release
|
||||
editor
|
||||
automated pr
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -69,3 +69,12 @@ docs/.vitepress/dist/
|
||||
/demo/.idea/
|
||||
/demo/.vscode/
|
||||
/demo_wxgame/
|
||||
|
||||
# Tauri 构建产物
|
||||
**/src-tauri/target/
|
||||
**/src-tauri/WixTools/
|
||||
**/src-tauri/gen/
|
||||
|
||||
# Tauri 捆绑输出
|
||||
**/src-tauri/target/release/bundle/
|
||||
**/src-tauri/target/debug/bundle/
|
||||
|
||||
49
.prettierignore
Normal file
49
.prettierignore
Normal file
@@ -0,0 +1,49 @@
|
||||
# 依赖和构建输出
|
||||
node_modules/
|
||||
dist/
|
||||
bin/
|
||||
build/
|
||||
coverage/
|
||||
*.min.js
|
||||
*.min.css
|
||||
|
||||
# 编译输出
|
||||
**/*.d.ts
|
||||
tsconfig.tsbuildinfo
|
||||
|
||||
# 日志
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# 第三方库
|
||||
thirdparty/
|
||||
examples/lawn-mower-demo/
|
||||
extensions/
|
||||
|
||||
# 文档生成
|
||||
docs/.vitepress/cache/
|
||||
docs/.vitepress/dist/
|
||||
docs/api/
|
||||
|
||||
# 临时文件
|
||||
*.tmp
|
||||
*.bak
|
||||
*.swp
|
||||
*~
|
||||
|
||||
# 系统文件
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# 编辑器
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# 其他
|
||||
*.backup
|
||||
CHANGELOG.md
|
||||
LICENSE
|
||||
README.md
|
||||
14
.prettierrc
Normal file
14
.prettierrc
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 4,
|
||||
"useTabs": false,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 120,
|
||||
"arrowParens": "always",
|
||||
"endOfLine": "lf",
|
||||
"bracketSpacing": true,
|
||||
"quoteProps": "as-needed",
|
||||
"jsxSingleQuote": false,
|
||||
"proseWrap": "preserve"
|
||||
}
|
||||
40
README.md
40
README.md
@@ -95,11 +95,47 @@ function gameLoop(deltaTime: number) {
|
||||
|
||||
支持主流游戏引擎和 Web 平台:
|
||||
|
||||
- **Cocos Creator** - 内置引擎集成支持,提供[专用调试插件](https://store.cocos.com/app/detail/7823)
|
||||
- **Laya 引擎** - 完整的生命周期管理
|
||||
- **Cocos Creator**
|
||||
- **Laya 引擎**
|
||||
- **原生 Web** - 浏览器环境直接运行
|
||||
- **小游戏平台** - 微信、支付宝等小游戏
|
||||
|
||||
## ECS Framework Editor
|
||||
|
||||
跨平台桌面编辑器,提供可视化开发和调试工具。
|
||||
|
||||
### 主要功能
|
||||
|
||||
- **场景管理** - 可视化场景层级和实体管理
|
||||
- **组件检视** - 实时查看和编辑实体组件
|
||||
- **性能分析** - 内置 Profiler 监控系统性能
|
||||
- **插件系统** - 可扩展的插件架构
|
||||
- **远程调试** - 连接运行中的游戏进行实时调试
|
||||
- **自动更新** - 支持热更新,自动获取最新版本
|
||||
|
||||
### 下载
|
||||
|
||||
[](https://github.com/esengine/ecs-framework/releases/latest)
|
||||
|
||||
支持 Windows、macOS (Intel & Apple Silicon)
|
||||
|
||||
### 截图
|
||||
|
||||
<img src="screenshots/main_screetshot.png" alt="ECS Framework Editor" width="800">
|
||||
|
||||
<details>
|
||||
<summary>查看更多截图</summary>
|
||||
|
||||
**性能分析器**
|
||||
<img src="screenshots/performance_profiler.png" alt="Performance Profiler" width="600">
|
||||
|
||||
**插件管理**
|
||||
<img src="screenshots/plugin_manager.png" alt="Plugin Manager" width="600">
|
||||
|
||||
**设置界面**
|
||||
<img src="screenshots/settings.png" alt="Settings" width="600">
|
||||
|
||||
</details>
|
||||
|
||||
## 示例项目
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ export default defineConfig({
|
||||
items: [
|
||||
{ text: '实体类 (Entity)', link: '/guide/entity' },
|
||||
{ text: '组件系统 (Component)', link: '/guide/component' },
|
||||
{ text: '实体查询系统', link: '/guide/entity-query' },
|
||||
{
|
||||
text: '系统架构 (System)',
|
||||
link: '/guide/system',
|
||||
@@ -73,12 +74,28 @@ export default defineConfig({
|
||||
{ text: 'Worker系统 (多线程)', link: '/guide/worker-system' }
|
||||
]
|
||||
},
|
||||
{ text: '场景管理 (Scene)', link: '/guide/scene' },
|
||||
{
|
||||
text: '场景管理 (Scene)',
|
||||
link: '/guide/scene',
|
||||
items: [
|
||||
{ text: 'SceneManager', link: '/guide/scene-manager' },
|
||||
{ text: 'WorldManager', link: '/guide/world-manager' }
|
||||
]
|
||||
},
|
||||
{ text: '序列化系统 (Serialization)', link: '/guide/serialization' },
|
||||
{ text: '事件系统 (Event)', link: '/guide/event-system' },
|
||||
{ text: '时间和定时器 (Time)', link: '/guide/time-and-timers' },
|
||||
{ text: '日志系统 (Logger)', link: '/guide/logging' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '高级特性',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: '服务容器 (Service Container)', link: '/guide/service-container' },
|
||||
{ text: '插件系统 (Plugin System)', link: '/guide/plugin-system' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '平台适配器',
|
||||
link: '/guide/platform-adapter',
|
||||
|
||||
@@ -63,14 +63,14 @@ class Health extends Component {
|
||||
- 框架能正确管理组件注册
|
||||
|
||||
```typescript
|
||||
// ✅ 正确的用法
|
||||
// 正确的用法
|
||||
@ECSComponent('Velocity')
|
||||
class Velocity extends Component {
|
||||
dx: number = 0;
|
||||
dy: number = 0;
|
||||
}
|
||||
|
||||
// ❌ 错误的用法 - 没有装饰器
|
||||
// 错误的用法 - 没有装饰器
|
||||
class BadComponent extends Component {
|
||||
// 这样定义的组件可能在生产环境出现问题
|
||||
}
|
||||
@@ -90,7 +90,7 @@ class ExampleComponent extends Component {
|
||||
* 用于初始化资源、建立引用等
|
||||
*/
|
||||
onAddedToEntity(): void {
|
||||
console.log(`组件 ${this.constructor.name} 被添加到实体 ${this.entity.name}`);
|
||||
console.log(`组件 ${this.constructor.name} 已添加,实体ID: ${this.entityId}`);
|
||||
this.resource = new SomeResource();
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ class ExampleComponent extends Component {
|
||||
* 用于清理资源、断开引用等
|
||||
*/
|
||||
onRemovedFromEntity(): void {
|
||||
console.log(`组件 ${this.constructor.name} 从实体 ${this.entity.name} 移除`);
|
||||
console.log(`组件 ${this.constructor.name} 已移除`);
|
||||
if (this.resource) {
|
||||
this.resource.cleanup();
|
||||
this.resource = null;
|
||||
@@ -108,30 +108,58 @@ class ExampleComponent extends Component {
|
||||
}
|
||||
```
|
||||
|
||||
## 访问实体
|
||||
## 组件与实体的关系
|
||||
|
||||
组件可以通过 `this.entity` 访问其所属的实体:
|
||||
组件存储了所属实体的ID (`entityId`),而不是直接引用实体对象。这是ECS数据导向设计的体现,避免了循环引用。
|
||||
|
||||
在实际使用中,**应该在 System 中处理实体和组件的交互**,而不是在组件内部:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Damage')
|
||||
class Damage extends Component {
|
||||
damage: number;
|
||||
@ECSComponent('Health')
|
||||
class Health extends Component {
|
||||
current: number;
|
||||
max: number;
|
||||
|
||||
constructor(damage: number) {
|
||||
constructor(max: number = 100) {
|
||||
super();
|
||||
this.damage = damage;
|
||||
this.max = max;
|
||||
this.current = max;
|
||||
}
|
||||
|
||||
// 在组件方法中访问实体和其他组件
|
||||
applyDamage(): void {
|
||||
const health = this.entity.getComponent(Health);
|
||||
if (health) {
|
||||
health.takeDamage(this.damage);
|
||||
isDead(): boolean {
|
||||
return this.current <= 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('Damage')
|
||||
class Damage extends Component {
|
||||
value: number;
|
||||
|
||||
constructor(value: number) {
|
||||
super();
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
// 推荐:在 System 中处理逻辑
|
||||
class DamageSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(new Matcher().all(Health, Damage));
|
||||
}
|
||||
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health)!;
|
||||
const damage = entity.getComponent(Damage)!;
|
||||
|
||||
health.current -= damage.value;
|
||||
|
||||
// 如果生命值为0,销毁实体
|
||||
if (health.isDead()) {
|
||||
this.entity.destroy();
|
||||
entity.destroy();
|
||||
}
|
||||
|
||||
// 应用伤害后移除 Damage 组件
|
||||
entity.removeComponent(damage);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,9 +174,27 @@ class Damage extends Component {
|
||||
class ExampleComponent extends Component {
|
||||
someData: string = "example";
|
||||
|
||||
showComponentInfo(): void {
|
||||
console.log(`组件ID: ${this.id}`); // 唯一的组件ID
|
||||
console.log(`所属实体: ${this.entity.name}`); // 所属实体引用
|
||||
onAddedToEntity(): void {
|
||||
console.log(`组件ID: ${this.id}`); // 唯一的组件ID
|
||||
console.log(`所属实体ID: ${this.entityId}`); // 所属实体的ID
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
如果需要访问实体对象,应该在 System 中进行:
|
||||
|
||||
```typescript
|
||||
class ExampleSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(new Matcher().all(ExampleComponent));
|
||||
}
|
||||
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const comp = entity.getComponent(ExampleComponent)!;
|
||||
console.log(`实体名称: ${entity.name}`);
|
||||
console.log(`组件数据: ${comp.someData}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -245,7 +291,7 @@ class WeaponConfig extends Component {
|
||||
### 1. 保持组件简单
|
||||
|
||||
```typescript
|
||||
// ✅ 好的组件设计 - 单一职责
|
||||
// 好的组件设计 - 单一职责
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
x: number = 0;
|
||||
@@ -258,7 +304,7 @@ class Velocity extends Component {
|
||||
dy: number = 0;
|
||||
}
|
||||
|
||||
// ❌ 避免的组件设计 - 职责过多
|
||||
// 避免的组件设计 - 职责过多
|
||||
@ECSComponent('GameObject')
|
||||
class GameObject extends Component {
|
||||
x: number;
|
||||
@@ -330,16 +376,11 @@ class Inventory extends Component {
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 避免在组件中存储实体引用
|
||||
### 4. 引用其他实体
|
||||
|
||||
当组件需要关联其他实体时(如父子关系、跟随目标等),**推荐方式是存储实体ID**,然后在 System 中查找:
|
||||
|
||||
```typescript
|
||||
// ❌ 避免:在组件中存储其他实体的引用
|
||||
@ECSComponent('BadFollower')
|
||||
class BadFollower extends Component {
|
||||
target: Entity; // 直接引用可能导致内存泄漏
|
||||
}
|
||||
|
||||
// ✅ 推荐:存储实体ID,通过场景查找
|
||||
@ECSComponent('Follower')
|
||||
class Follower extends Component {
|
||||
targetId: number;
|
||||
@@ -349,11 +390,269 @@ class Follower extends Component {
|
||||
super();
|
||||
this.targetId = targetId;
|
||||
}
|
||||
}
|
||||
|
||||
getTarget(): Entity | null {
|
||||
return this.entity.scene?.findEntityById(this.targetId) || null;
|
||||
// 在 System 中查找目标实体并处理逻辑
|
||||
class FollowerSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(new Matcher().all(Follower, Position));
|
||||
}
|
||||
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const follower = entity.getComponent(Follower)!;
|
||||
const position = entity.getComponent(Position)!;
|
||||
|
||||
// 通过场景查找目标实体
|
||||
const target = entity.scene?.findEntityById(follower.targetId);
|
||||
if (target) {
|
||||
const targetPos = target.getComponent(Position);
|
||||
if (targetPos) {
|
||||
// 跟随逻辑
|
||||
const dx = targetPos.x - position.x;
|
||||
const dy = targetPos.y - position.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance > follower.followDistance) {
|
||||
// 移动靠近目标
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这种方式的优势:
|
||||
- 组件保持简单,只存储基本数据类型
|
||||
- 符合数据导向设计
|
||||
- 在 System 中统一处理查找和逻辑
|
||||
- 易于理解和维护
|
||||
|
||||
**避免在组件中直接存储实体引用**:
|
||||
|
||||
```typescript
|
||||
// 错误示范:直接存储实体引用
|
||||
@ECSComponent('BadFollower')
|
||||
class BadFollower extends Component {
|
||||
target: Entity; // 实体销毁后仍持有引用,可能导致内存泄漏
|
||||
}
|
||||
```
|
||||
|
||||
## 高级特性
|
||||
|
||||
### EntityRef 装饰器 - 自动引用追踪
|
||||
|
||||
框架提供了 `@EntityRef` 装饰器用于**特殊场景**下安全地存储实体引用。这是一个高级特性,一般情况下推荐使用存储ID的方式。
|
||||
|
||||
#### 什么时候需要 EntityRef?
|
||||
|
||||
在以下场景中,`@EntityRef` 可以简化代码:
|
||||
|
||||
1. **父子关系**: 需要在组件中直接访问父实体或子实体
|
||||
2. **复杂关联**: 实体之间有多个引用关系
|
||||
3. **频繁访问**: 需要在多处访问引用的实体,使用ID查找会有性能开销
|
||||
|
||||
#### 核心特性
|
||||
|
||||
`@EntityRef` 装饰器通过 **ReferenceTracker** 自动追踪引用关系:
|
||||
|
||||
- 当被引用的实体销毁时,所有指向它的 `@EntityRef` 属性自动设为 `null`
|
||||
- 防止跨场景引用(会输出警告并拒绝设置)
|
||||
- 防止引用已销毁的实体(会输出警告并设为 `null`)
|
||||
- 使用 WeakRef 避免内存泄漏(自动GC支持)
|
||||
- 组件移除时自动清理引用注册
|
||||
|
||||
#### 基本用法
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent, EntityRef, Entity } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSComponent('Parent')
|
||||
class ParentComponent extends Component {
|
||||
@EntityRef()
|
||||
parent: Entity | null = null;
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
const scene = new Scene();
|
||||
const parent = scene.createEntity('Parent');
|
||||
const child = scene.createEntity('Child');
|
||||
|
||||
const comp = child.addComponent(new ParentComponent());
|
||||
comp.parent = parent;
|
||||
|
||||
console.log(comp.parent); // Entity { name: 'Parent' }
|
||||
|
||||
// 当 parent 被销毁时,comp.parent 自动变为 null
|
||||
parent.destroy();
|
||||
console.log(comp.parent); // null
|
||||
```
|
||||
|
||||
#### 多个引用属性
|
||||
|
||||
一个组件可以有多个 `@EntityRef` 属性:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Combat')
|
||||
class CombatComponent extends Component {
|
||||
@EntityRef()
|
||||
target: Entity | null = null;
|
||||
|
||||
@EntityRef()
|
||||
ally: Entity | null = null;
|
||||
|
||||
@EntityRef()
|
||||
lastAttacker: Entity | null = null;
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
const player = scene.createEntity('Player');
|
||||
const enemy = scene.createEntity('Enemy');
|
||||
const npc = scene.createEntity('NPC');
|
||||
|
||||
const combat = player.addComponent(new CombatComponent());
|
||||
combat.target = enemy;
|
||||
combat.ally = npc;
|
||||
|
||||
// enemy 销毁后,只有 target 变为 null,ally 仍然有效
|
||||
enemy.destroy();
|
||||
console.log(combat.target); // null
|
||||
console.log(combat.ally); // Entity { name: 'NPC' }
|
||||
```
|
||||
|
||||
#### 安全检查
|
||||
|
||||
`@EntityRef` 提供了多重安全检查:
|
||||
|
||||
```typescript
|
||||
const scene1 = new Scene();
|
||||
const scene2 = new Scene();
|
||||
|
||||
const entity1 = scene1.createEntity('Entity1');
|
||||
const entity2 = scene2.createEntity('Entity2');
|
||||
|
||||
const comp = entity1.addComponent(new ParentComponent());
|
||||
|
||||
// 跨场景引用会失败
|
||||
comp.parent = entity2; // 输出错误日志,comp.parent 为 null
|
||||
console.log(comp.parent); // null
|
||||
|
||||
// 引用已销毁的实体会失败
|
||||
const entity3 = scene1.createEntity('Entity3');
|
||||
entity3.destroy();
|
||||
comp.parent = entity3; // 输出警告日志,comp.parent 为 null
|
||||
console.log(comp.parent); // null
|
||||
```
|
||||
|
||||
#### 实现原理
|
||||
|
||||
`@EntityRef` 使用以下机制实现自动引用追踪:
|
||||
|
||||
1. **ReferenceTracker**: Scene 持有一个引用追踪器,记录所有实体引用关系
|
||||
2. **WeakRef**: 使用弱引用存储组件,避免循环引用导致内存泄漏
|
||||
3. **属性拦截**: 通过 `Object.defineProperty` 拦截 getter/setter
|
||||
4. **自动清理**: 实体销毁时,ReferenceTracker 遍历所有引用并设为 null
|
||||
|
||||
```typescript
|
||||
// 简化的实现原理
|
||||
class ReferenceTracker {
|
||||
// entityId -> 引用该实体的所有组件记录
|
||||
private _references: Map<number, Set<{ component: WeakRef<Component>, propertyKey: string }>>;
|
||||
|
||||
// 实体销毁时调用
|
||||
clearReferencesTo(entityId: number): void {
|
||||
const records = this._references.get(entityId);
|
||||
if (records) {
|
||||
for (const record of records) {
|
||||
const component = record.component.deref();
|
||||
if (component) {
|
||||
// 将组件的引用属性设为 null
|
||||
(component as any)[record.propertyKey] = null;
|
||||
}
|
||||
}
|
||||
this._references.delete(entityId);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 性能考虑
|
||||
|
||||
`@EntityRef` 会带来一些性能开销:
|
||||
|
||||
- **写入开销**: 每次设置引用时需要更新 ReferenceTracker
|
||||
- **内存开销**: ReferenceTracker 需要维护引用映射表
|
||||
- **销毁开销**: 实体销毁时需要遍历所有引用并清理
|
||||
|
||||
对于大多数场景,这些开销是可以接受的。但如果有**大量实体和频繁的引用变更**,存储ID可能更高效。
|
||||
|
||||
#### 最佳实践
|
||||
|
||||
```typescript
|
||||
// 推荐:适合使用 @EntityRef 的场景 - 父子关系
|
||||
@ECSComponent('Transform')
|
||||
class Transform extends Component {
|
||||
@EntityRef()
|
||||
parent: Entity | null = null;
|
||||
|
||||
position: { x: number, y: number } = { x: 0, y: 0 };
|
||||
|
||||
// 可以直接访问父实体的组件
|
||||
getWorldPosition(): { x: number, y: number } {
|
||||
if (!this.parent) {
|
||||
return { ...this.position };
|
||||
}
|
||||
|
||||
const parentTransform = this.parent.getComponent(Transform);
|
||||
if (parentTransform) {
|
||||
const parentPos = parentTransform.getWorldPosition();
|
||||
return {
|
||||
x: parentPos.x + this.position.x,
|
||||
y: parentPos.y + this.position.y
|
||||
};
|
||||
}
|
||||
|
||||
return { ...this.position };
|
||||
}
|
||||
}
|
||||
|
||||
// 不推荐:不适合使用 @EntityRef 的场景 - 大量动态目标
|
||||
@ECSComponent('AITarget')
|
||||
class AITarget extends Component {
|
||||
@EntityRef()
|
||||
target: Entity | null = null; // 如果目标频繁变化,用ID更好
|
||||
|
||||
updateCooldown: number = 0;
|
||||
}
|
||||
|
||||
// 推荐:这种场景用ID更好
|
||||
@ECSComponent('AITarget')
|
||||
class AITargetBetter extends Component {
|
||||
targetId: number | null = null; // 存储ID
|
||||
updateCooldown: number = 0;
|
||||
}
|
||||
```
|
||||
|
||||
#### 调试支持
|
||||
|
||||
ReferenceTracker 提供了调试接口:
|
||||
|
||||
```typescript
|
||||
// 查看某个实体被哪些组件引用
|
||||
const references = scene.referenceTracker.getReferencesTo(entity.id);
|
||||
console.log(`实体 ${entity.name} 被 ${references.length} 个组件引用`);
|
||||
|
||||
// 获取完整的调试信息
|
||||
const debugInfo = scene.referenceTracker.getDebugInfo();
|
||||
console.log(debugInfo);
|
||||
```
|
||||
|
||||
#### 总结
|
||||
|
||||
- **推荐做法**: 大部分情况使用存储ID + System查找的方式
|
||||
- **EntityRef 适用场景**: 父子关系、复杂关联、组件内需要直接访问引用实体的场景
|
||||
- **核心优势**: 自动清理、防止悬空引用、代码更简洁
|
||||
- **注意事项**: 有性能开销,不适合大量动态引用的场景
|
||||
|
||||
组件是 ECS 架构的数据载体,正确设计组件能让你的游戏代码更模块化、可维护和高性能。
|
||||
501
docs/guide/entity-query.md
Normal file
501
docs/guide/entity-query.md
Normal file
@@ -0,0 +1,501 @@
|
||||
# 实体查询系统
|
||||
|
||||
实体查询是 ECS 架构的核心功能之一。本指南将介绍如何使用 Matcher 和 QuerySystem 来查询和筛选实体。
|
||||
|
||||
## 核心概念
|
||||
|
||||
### Matcher - 查询条件描述符
|
||||
|
||||
Matcher 是一个链式 API,用于描述实体查询条件。它本身不执行查询,而是作为条件传递给 EntitySystem 或 QuerySystem。
|
||||
|
||||
### QuerySystem - 查询执行引擎
|
||||
|
||||
QuerySystem 负责实际执行查询,内部使用响应式查询机制自动优化性能。
|
||||
|
||||
## 在 EntitySystem 中使用 Matcher
|
||||
|
||||
这是最常见的使用方式。EntitySystem 通过 Matcher 自动筛选和处理符合条件的实体。
|
||||
|
||||
### 基础用法
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, Matcher, Entity, Component } from '@esengine/ecs-framework';
|
||||
|
||||
class PositionComponent extends Component {
|
||||
public x: number = 0;
|
||||
public y: number = 0;
|
||||
}
|
||||
|
||||
class VelocityComponent extends Component {
|
||||
public vx: number = 0;
|
||||
public vy: number = 0;
|
||||
}
|
||||
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// 方式1: 使用 Matcher.empty().all()
|
||||
super(Matcher.empty().all(PositionComponent, VelocityComponent));
|
||||
|
||||
// 方式2: 直接使用 Matcher.all() (等价)
|
||||
// super(Matcher.all(PositionComponent, VelocityComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const pos = entity.getComponent(PositionComponent)!;
|
||||
const vel = entity.getComponent(VelocityComponent)!;
|
||||
|
||||
pos.x += vel.vx;
|
||||
pos.y += vel.vy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加到场景
|
||||
scene.addEntityProcessor(new MovementSystem());
|
||||
```
|
||||
|
||||
### Matcher 链式 API
|
||||
|
||||
#### all() - 必须包含所有组件
|
||||
|
||||
```typescript
|
||||
class HealthSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// 实体必须同时拥有 Health 和 Position 组件
|
||||
super(Matcher.empty().all(HealthComponent, PositionComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 只处理同时拥有两个组件的实体
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### any() - 至少包含一个组件
|
||||
|
||||
```typescript
|
||||
class DamageableSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// 实体至少拥有 Health 或 Shield 其中之一
|
||||
super(Matcher.any(HealthComponent, ShieldComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 处理拥有生命值或护盾的实体
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### none() - 不能包含指定组件
|
||||
|
||||
```typescript
|
||||
class AliveEntitySystem extends EntitySystem {
|
||||
constructor() {
|
||||
// 实体不能拥有 DeadTag 组件
|
||||
super(Matcher.all(HealthComponent).none(DeadTag));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 只处理活着的实体
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 组合条件
|
||||
|
||||
```typescript
|
||||
class CombatSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(
|
||||
Matcher.empty()
|
||||
.all(PositionComponent, HealthComponent) // 必须有位置和生命
|
||||
.any(WeaponComponent, MagicComponent) // 至少有武器或魔法
|
||||
.none(DeadTag, FrozenTag) // 不能是死亡或冰冻状态
|
||||
);
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 处理可以战斗的活着的实体
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 按标签查询
|
||||
|
||||
```typescript
|
||||
class PlayerSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// 查询特定标签的实体
|
||||
super(Matcher.empty().withTag(Tags.PLAYER));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 只处理玩家实体
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 按名称查询
|
||||
|
||||
```typescript
|
||||
class BossSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// 查询特定名称的实体
|
||||
super(Matcher.empty().withName('Boss'));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 只处理名为 'Boss' 的实体
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 直接使用 QuerySystem
|
||||
|
||||
如果不需要创建系统,可以直接使用 Scene 的 querySystem 进行查询。
|
||||
|
||||
### 基础查询方法
|
||||
|
||||
```typescript
|
||||
// 获取场景的查询系统
|
||||
const querySystem = scene.querySystem;
|
||||
|
||||
// 查询拥有所有指定组件的实体
|
||||
const result1 = querySystem.queryAll(PositionComponent, VelocityComponent);
|
||||
console.log(`找到 ${result1.count} 个移动实体`);
|
||||
console.log(`查询耗时: ${result1.executionTime.toFixed(2)}ms`);
|
||||
|
||||
// 查询拥有任意指定组件的实体
|
||||
const result2 = querySystem.queryAny(WeaponComponent, MagicComponent);
|
||||
console.log(`找到 ${result2.count} 个战斗单位`);
|
||||
|
||||
// 查询不包含指定组件的实体
|
||||
const result3 = querySystem.queryNone(DeadTag);
|
||||
console.log(`找到 ${result3.count} 个活着的实体`);
|
||||
```
|
||||
|
||||
### 按标签查询
|
||||
|
||||
```typescript
|
||||
const playerResult = querySystem.queryByTag(Tags.PLAYER);
|
||||
for (const player of playerResult.entities) {
|
||||
console.log('玩家:', player.name);
|
||||
}
|
||||
```
|
||||
|
||||
### 按名称查询
|
||||
|
||||
```typescript
|
||||
const bossResult = querySystem.queryByName('Boss');
|
||||
if (bossResult.count > 0) {
|
||||
const boss = bossResult.entities[0];
|
||||
console.log('找到Boss:', boss);
|
||||
}
|
||||
```
|
||||
|
||||
### 按单个组件查询
|
||||
|
||||
```typescript
|
||||
const healthResult = querySystem.queryByComponent(HealthComponent);
|
||||
console.log(`有 ${healthResult.count} 个实体拥有生命值`);
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 自动缓存
|
||||
|
||||
QuerySystem 内部使用响应式查询自动缓存结果,相同的查询条件会直接使用缓存:
|
||||
|
||||
```typescript
|
||||
// 第一次查询,执行实际查询
|
||||
const result1 = querySystem.queryAll(PositionComponent);
|
||||
console.log('fromCache:', result1.fromCache); // false
|
||||
|
||||
// 第二次相同查询,使用缓存
|
||||
const result2 = querySystem.queryAll(PositionComponent);
|
||||
console.log('fromCache:', result2.fromCache); // true
|
||||
```
|
||||
|
||||
### 实体变化自动更新
|
||||
|
||||
当实体添加/移除组件时,查询缓存会自动更新:
|
||||
|
||||
```typescript
|
||||
// 查询拥有武器的实体
|
||||
const before = querySystem.queryAll(WeaponComponent);
|
||||
console.log('之前:', before.count); // 假设为 5
|
||||
|
||||
// 给实体添加武器
|
||||
const enemy = scene.createEntity('Enemy');
|
||||
enemy.addComponent(new WeaponComponent());
|
||||
|
||||
// 再次查询,自动包含新实体
|
||||
const after = querySystem.queryAll(WeaponComponent);
|
||||
console.log('之后:', after.count); // 现在是 6
|
||||
```
|
||||
|
||||
### 查询性能统计
|
||||
|
||||
```typescript
|
||||
const stats = querySystem.getStats();
|
||||
console.log('总查询次数:', stats.queryStats.totalQueries);
|
||||
console.log('缓存命中率:', stats.queryStats.cacheHitRate);
|
||||
console.log('缓存大小:', stats.cacheStats.size);
|
||||
```
|
||||
|
||||
## 实际应用场景
|
||||
|
||||
### 场景1: 物理系统
|
||||
|
||||
```typescript
|
||||
class PhysicsSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(TransformComponent, RigidbodyComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const transform = entity.getComponent(TransformComponent)!;
|
||||
const rigidbody = entity.getComponent(RigidbodyComponent)!;
|
||||
|
||||
// 应用重力
|
||||
rigidbody.velocity.y -= 9.8 * Time.deltaTime;
|
||||
|
||||
// 更新位置
|
||||
transform.position.x += rigidbody.velocity.x * Time.deltaTime;
|
||||
transform.position.y += rigidbody.velocity.y * Time.deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 场景2: 渲染系统
|
||||
|
||||
```typescript
|
||||
class RenderSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(
|
||||
Matcher.empty()
|
||||
.all(TransformComponent, SpriteComponent)
|
||||
.none(InvisibleTag) // 排除不可见实体
|
||||
);
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 按 z-order 排序
|
||||
const sorted = entities.slice().sort((a, b) => {
|
||||
const zA = a.getComponent(TransformComponent)!.z;
|
||||
const zB = b.getComponent(TransformComponent)!.z;
|
||||
return zA - zB;
|
||||
});
|
||||
|
||||
// 渲染实体
|
||||
for (const entity of sorted) {
|
||||
const transform = entity.getComponent(TransformComponent)!;
|
||||
const sprite = entity.getComponent(SpriteComponent)!;
|
||||
|
||||
renderer.drawSprite(sprite.texture, transform.position);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 场景3: 碰撞检测
|
||||
|
||||
```typescript
|
||||
class CollisionSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(TransformComponent, ColliderComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 简单的 O(n²) 碰撞检测
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
for (let j = i + 1; j < entities.length; j++) {
|
||||
this.checkCollision(entities[i], entities[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private checkCollision(a: Entity, b: Entity): void {
|
||||
const transA = a.getComponent(TransformComponent)!;
|
||||
const transB = b.getComponent(TransformComponent)!;
|
||||
const colliderA = a.getComponent(ColliderComponent)!;
|
||||
const colliderB = b.getComponent(ColliderComponent)!;
|
||||
|
||||
if (this.isOverlapping(transA, colliderA, transB, colliderB)) {
|
||||
// 触发碰撞事件
|
||||
scene.eventSystem.emit('collision', { entityA: a, entityB: b });
|
||||
}
|
||||
}
|
||||
|
||||
private isOverlapping(...args: any[]): boolean {
|
||||
// 碰撞检测逻辑
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 场景4: 一次性查询
|
||||
|
||||
```typescript
|
||||
// 在系统外部执行一次性查询
|
||||
class GameManager {
|
||||
private scene: Scene;
|
||||
|
||||
public countEnemies(): number {
|
||||
const result = this.scene.querySystem.queryByTag(Tags.ENEMY);
|
||||
return result.count;
|
||||
}
|
||||
|
||||
public findNearestEnemy(playerPos: Vector2): Entity | null {
|
||||
const enemies = this.scene.querySystem.queryByTag(Tags.ENEMY);
|
||||
|
||||
let nearest: Entity | null = null;
|
||||
let minDistance = Infinity;
|
||||
|
||||
for (const enemy of enemies.entities) {
|
||||
const transform = enemy.getComponent(TransformComponent);
|
||||
if (!transform) continue;
|
||||
|
||||
const distance = Vector2.distance(playerPos, transform.position);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
nearest = enemy;
|
||||
}
|
||||
}
|
||||
|
||||
return nearest;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 优先使用 EntitySystem
|
||||
|
||||
```typescript
|
||||
// 推荐: 使用 EntitySystem
|
||||
class GoodSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(HealthComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 自动获得符合条件的实体,每帧自动更新
|
||||
}
|
||||
}
|
||||
|
||||
// 不推荐: 在 update 中手动查询
|
||||
class BadSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty());
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 每帧手动查询,浪费性能
|
||||
const result = this.scene!.querySystem.queryAll(HealthComponent);
|
||||
for (const entity of result.entities) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 合理使用 none() 排除条件
|
||||
|
||||
```typescript
|
||||
// 排除已死亡的敌人
|
||||
class EnemyAISystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(
|
||||
Matcher.empty()
|
||||
.all(EnemyTag, AIComponent)
|
||||
.none(DeadTag) // 不处理死亡的敌人
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 使用标签优化查询
|
||||
|
||||
```typescript
|
||||
// 不好: 查询所有实体再过滤
|
||||
const allEntities = scene.querySystem.getAllEntities();
|
||||
const players = allEntities.filter(e => e.hasComponent(PlayerTag));
|
||||
|
||||
// 好: 直接按标签查询
|
||||
const players = scene.querySystem.queryByTag(Tags.PLAYER).entities;
|
||||
```
|
||||
|
||||
### 4. 避免过于复杂的查询条件
|
||||
|
||||
```typescript
|
||||
// 不推荐: 过于复杂
|
||||
super(
|
||||
Matcher.empty()
|
||||
.all(A, B, C, D)
|
||||
.any(E, F, G)
|
||||
.none(H, I, J)
|
||||
);
|
||||
|
||||
// 推荐: 拆分成多个简单系统
|
||||
class SystemAB extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(A, B));
|
||||
}
|
||||
}
|
||||
|
||||
class SystemCD extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(C, D));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
### 1. 查询结果是只读的
|
||||
|
||||
```typescript
|
||||
const result = querySystem.queryAll(PositionComponent);
|
||||
|
||||
// 不要修改返回的数组
|
||||
result.entities.push(someEntity); // 错误!
|
||||
|
||||
// 如果需要修改,先复制
|
||||
const mutableArray = [...result.entities];
|
||||
mutableArray.push(someEntity); // 正确
|
||||
```
|
||||
|
||||
### 2. 组件添加/移除后的查询时机
|
||||
|
||||
```typescript
|
||||
// 创建实体并添加组件
|
||||
const entity = scene.createEntity('Player');
|
||||
entity.addComponent(new PositionComponent());
|
||||
|
||||
// 立即查询可能获取到新实体
|
||||
const result = scene.querySystem.queryAll(PositionComponent);
|
||||
// result.entities 包含新创建的实体
|
||||
```
|
||||
|
||||
### 3. Matcher 是不可变的
|
||||
|
||||
```typescript
|
||||
const matcher = Matcher.empty().all(PositionComponent);
|
||||
|
||||
// 链式调用返回新的 Matcher 实例
|
||||
const matcher2 = matcher.any(VelocityComponent);
|
||||
|
||||
// matcher 本身不变
|
||||
console.log(matcher === matcher2); // false
|
||||
```
|
||||
|
||||
## 相关 API
|
||||
|
||||
- [Matcher](../api/classes/Matcher.md) - 查询条件描述符 API 参考
|
||||
- [QuerySystem](../api/classes/QuerySystem.md) - 查询系统 API 参考
|
||||
- [EntitySystem](../api/classes/EntitySystem.md) - 实体系统 API 参考
|
||||
- [Entity](../api/classes/Entity.md) - 实体 API 参考
|
||||
@@ -83,8 +83,8 @@ if (player.hasComponent(Position)) {
|
||||
console.log("玩家有位置组件");
|
||||
}
|
||||
|
||||
// 获取所有组件实例(直接访问 components 属性)
|
||||
const allComponents = player.components; // Component[]
|
||||
// 获取所有组件实例(只读属性)
|
||||
const allComponents = player.components; // readonly Component[]
|
||||
|
||||
// 获取指定类型的所有组件(支持同类型多组件)
|
||||
const allHealthComponents = player.getComponents(Health); // Health[]
|
||||
|
||||
@@ -264,22 +264,17 @@ player.addComponent(new Velocity(50, 30)); // 每秒移动 50 像素(x方向
|
||||
player.addComponent(new Sprite("player.png", 64, 64));
|
||||
```
|
||||
|
||||
## World 概念
|
||||
## 场景管理
|
||||
|
||||
World 是 Scene 的容器,用于管理多个独立的游戏世界。这种设计特别适用于:
|
||||
- 多人游戏房间(每个房间一个 World)
|
||||
- 不同的游戏模式
|
||||
- 独立的模拟环境
|
||||
|
||||
### 基本用法
|
||||
Core 内置了场景管理功能,使用非常简单:
|
||||
|
||||
```typescript
|
||||
import { World, Scene } from '@esengine/ecs-framework'
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// 创建游戏房间的World
|
||||
const roomWorld = new World({ name: 'Room_001' });
|
||||
// 初始化Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 在World中创建多个Scene
|
||||
// 创建并设置场景
|
||||
class GameScene extends Scene {
|
||||
initialize(): void {
|
||||
this.name = "GamePlay";
|
||||
@@ -288,78 +283,106 @@ class GameScene extends Scene {
|
||||
}
|
||||
}
|
||||
|
||||
class UIScene extends Scene {
|
||||
initialize(): void {
|
||||
this.name = "UI";
|
||||
// UI相关系统
|
||||
}
|
||||
const gameScene = new GameScene();
|
||||
Core.setScene(gameScene);
|
||||
|
||||
// 游戏循环(自动更新场景)
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // 自动更新全局服务和场景
|
||||
}
|
||||
|
||||
// 添加Scene到World
|
||||
const gameScene = roomWorld.createScene('game', new GameScene());
|
||||
const uiScene = roomWorld.createScene('ui', new UIScene());
|
||||
// 切换场景
|
||||
Core.loadScene(new MenuScene()); // 延迟切换(下一帧)
|
||||
Core.setScene(new GameScene()); // 立即切换
|
||||
|
||||
// 激活Scene
|
||||
roomWorld.setSceneActive('game', true);
|
||||
roomWorld.setSceneActive('ui', true);
|
||||
// 访问当前场景
|
||||
const currentScene = Core.scene;
|
||||
|
||||
// 启动World
|
||||
roomWorld.start();
|
||||
// 使用流式API
|
||||
const player = Core.ecsAPI?.createEntity('Player')
|
||||
.addComponent(Position, 100, 100)
|
||||
.addComponent(Velocity, 50, 0);
|
||||
```
|
||||
|
||||
### World 生命周期
|
||||
### 高级:使用 WorldManager 管理多世界
|
||||
|
||||
World 提供了完整的生命周期管理:
|
||||
- `start()`: 启动 World 和所有全局系统
|
||||
- `updateGlobalSystems()`: 更新全局系统(由 Core.update() 调用)
|
||||
- `updateScenes()`: 更新所有激活的 Scene(由 Core.update() 调用)
|
||||
- `stop()`: 停止 World
|
||||
- `destroy()`: 销毁 World 和所有资源
|
||||
仅适用于复杂的服务器端应用(MMO游戏服务器、游戏房间系统等):
|
||||
|
||||
```typescript
|
||||
import { Core, WorldManager } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始化Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 从服务容器获取 WorldManager(Core 已自动创建并注册)
|
||||
const worldManager = Core.services.resolve(WorldManager);
|
||||
|
||||
// 创建多个独立的游戏世界
|
||||
const room1 = worldManager.createWorld('room_001');
|
||||
const room2 = worldManager.createWorld('room_002');
|
||||
|
||||
// 在每个世界中创建场景
|
||||
const gameScene1 = room1.createScene('game', new GameScene());
|
||||
const gameScene2 = room2.createScene('game', new GameScene());
|
||||
|
||||
// 激活场景
|
||||
room1.setSceneActive('game', true);
|
||||
room2.setSceneActive('game', true);
|
||||
|
||||
// 游戏循环(需要手动更新世界)
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // 更新全局服务
|
||||
worldManager.updateAll(); // 手动更新所有世界
|
||||
}
|
||||
```
|
||||
|
||||
## 与游戏引擎集成
|
||||
|
||||
### Laya 引擎集成
|
||||
|
||||
```typescript
|
||||
import { Stage } from "laya/display/Stage"
|
||||
import { Stat } from "laya/utils/Stat"
|
||||
import { Laya } from "Laya"
|
||||
import { Stage } from "laya/display/Stage";
|
||||
import { Laya } from "Laya";
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始化 Laya
|
||||
Laya.init(800, 600).then(() => {
|
||||
// 初始化 ECS
|
||||
const core = Core.create(true)
|
||||
|
||||
// 设置场景...
|
||||
Core.create(true);
|
||||
Core.setScene(new GameScene());
|
||||
|
||||
// 启动游戏循环
|
||||
Laya.timer.frameLoop(1, this, () => {
|
||||
const deltaTime = Laya.timer.delta / 1000 // 转换为秒
|
||||
Core.update(deltaTime)
|
||||
})
|
||||
})
|
||||
const deltaTime = Laya.timer.delta / 1000;
|
||||
Core.update(deltaTime); // 自动更新全局服务和场景
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Cocos Creator 集成
|
||||
|
||||
```typescript
|
||||
import { Component, _decorator } from 'cc'
|
||||
import { Component, _decorator } from 'cc';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
const { ccclass } = _decorator
|
||||
const { ccclass } = _decorator;
|
||||
|
||||
@ccclass('ECSGameManager')
|
||||
export class ECSGameManager extends Component {
|
||||
|
||||
onLoad() {
|
||||
// 初始化 ECS
|
||||
const core = Core.create(true)
|
||||
|
||||
// 设置场景...
|
||||
Core.create(true);
|
||||
Core.setScene(new GameScene());
|
||||
}
|
||||
|
||||
update(deltaTime: number) {
|
||||
// 更新 ECS
|
||||
Core.update(deltaTime)
|
||||
// 自动更新全局服务和场景
|
||||
Core.update(deltaTime);
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
// 清理资源
|
||||
Core.destroy();
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -378,7 +401,7 @@ export class ECSGameManager extends Component {
|
||||
|
||||
确保:
|
||||
1. 系统已添加到场景:`this.addSystem(system)` (在 Scene 的 initialize 方法中)
|
||||
2. 场景已设置为当前场景:`Core.setScene(scene)`
|
||||
2. 场景已设置:`Core.setScene(scene)`
|
||||
3. 游戏循环在调用:`Core.update(deltaTime)`
|
||||
|
||||
### 如何调试 ECS 应用?
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
### [事件系统 (Event)](./event-system.md)
|
||||
掌握类型安全的事件系统,实现组件间通信和系统协作。
|
||||
|
||||
### [序列化系统 (Serialization)](./serialization.md)
|
||||
掌握场景、实体和组件的序列化方案,支持全量序列化和增量序列化,实现游戏存档、网络同步等功能。
|
||||
|
||||
### [时间和定时器 (Time)](./time-and-timers.md)
|
||||
学习时间管理和定时器系统,实现游戏逻辑的精确时间控制。
|
||||
|
||||
@@ -26,4 +29,12 @@
|
||||
掌握分级日志系统,用于调试、监控和错误追踪。
|
||||
|
||||
### [平台适配器 (Platform Adapter)](./platform-adapter.md)
|
||||
了解如何为不同平台实现和注册平台适配器,支持浏览器、小游戏、Node.js等环境。
|
||||
了解如何为不同平台实现和注册平台适配器,支持浏览器、小游戏、Node.js等环境。
|
||||
|
||||
## 高级特性
|
||||
|
||||
### [服务容器 (Service Container)](./service-container.md)
|
||||
掌握依赖注入和服务管理,实现松耦合的架构设计。
|
||||
|
||||
### [插件系统 (Plugin System)](./plugin-system.md)
|
||||
学习如何开发和使用插件,扩展框架功能,实现功能模块化。
|
||||
643
docs/guide/plugin-system.md
Normal file
643
docs/guide/plugin-system.md
Normal file
@@ -0,0 +1,643 @@
|
||||
# 插件系统
|
||||
|
||||
插件系统允许你以模块化的方式扩展 ECS Framework 的功能。通过插件,你可以封装特定功能(如网络同步、物理引擎、调试工具等),并在多个项目中复用。
|
||||
|
||||
## 概述
|
||||
|
||||
### 什么是插件
|
||||
|
||||
插件是实现了 `IPlugin` 接口的类,可以在运行时动态安装到框架中。插件可以:
|
||||
|
||||
- 注册自定义服务到服务容器
|
||||
- 添加系统到场景
|
||||
- 注册自定义组件
|
||||
- 扩展框架功能
|
||||
|
||||
### 插件的优势
|
||||
|
||||
- **模块化**: 将功能封装为独立模块,提高代码可维护性
|
||||
- **可复用**: 同一个插件可以在多个项目中使用
|
||||
- **解耦**: 核心框架与扩展功能分离
|
||||
- **热插拔**: 运行时动态安装和卸载插件
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 创建第一个插件
|
||||
|
||||
创建一个简单的调试插件:
|
||||
|
||||
```typescript
|
||||
import { IPlugin, Core, ServiceContainer } from '@esengine/ecs-framework';
|
||||
|
||||
class DebugPlugin implements IPlugin {
|
||||
readonly name = 'debug-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
console.log('Debug plugin installed');
|
||||
|
||||
// 可以在这里注册服务、添加系统等
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
console.log('Debug plugin uninstalled');
|
||||
// 清理资源
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 安装插件
|
||||
|
||||
使用 `Core.installPlugin()` 安装插件:
|
||||
|
||||
```typescript
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始化Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 安装插件
|
||||
await Core.installPlugin(new DebugPlugin());
|
||||
|
||||
// 检查插件是否已安装
|
||||
if (Core.isPluginInstalled('debug-plugin')) {
|
||||
console.log('Debug plugin is running');
|
||||
}
|
||||
```
|
||||
|
||||
### 卸载插件
|
||||
|
||||
```typescript
|
||||
// 卸载插件
|
||||
await Core.uninstallPlugin('debug-plugin');
|
||||
```
|
||||
|
||||
### 获取插件实例
|
||||
|
||||
```typescript
|
||||
// 获取已安装的插件
|
||||
const plugin = Core.getPlugin('debug-plugin');
|
||||
if (plugin) {
|
||||
console.log(`Plugin version: ${plugin.version}`);
|
||||
}
|
||||
```
|
||||
|
||||
## 插件开发
|
||||
|
||||
### IPlugin 接口
|
||||
|
||||
所有插件必须实现 `IPlugin` 接口:
|
||||
|
||||
```typescript
|
||||
export interface IPlugin {
|
||||
// 插件唯一名称
|
||||
readonly name: string;
|
||||
|
||||
// 插件版本(建议遵循semver规范)
|
||||
readonly version: string;
|
||||
|
||||
// 依赖的其他插件(可选)
|
||||
readonly dependencies?: readonly string[];
|
||||
|
||||
// 安装插件时调用
|
||||
install(core: Core, services: ServiceContainer): void | Promise<void>;
|
||||
|
||||
// 卸载插件时调用
|
||||
uninstall(): void | Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
### 插件生命周期
|
||||
|
||||
#### install 方法
|
||||
|
||||
在插件安装时调用,用于初始化插件:
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// 1. 注册服务
|
||||
services.registerSingleton(MyService);
|
||||
|
||||
// 2. 访问当前场景
|
||||
const scene = core.scene;
|
||||
if (scene) {
|
||||
// 3. 添加系统
|
||||
scene.addSystem(new MySystem());
|
||||
}
|
||||
|
||||
// 4. 其他初始化逻辑
|
||||
console.log('Plugin initialized');
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 清理逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### uninstall 方法
|
||||
|
||||
在插件卸载时调用,用于清理资源:
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-plugin';
|
||||
readonly version = '1.0.0';
|
||||
private myService?: MyService;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
this.myService = new MyService();
|
||||
services.registerInstance(MyService, this.myService);
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 清理服务
|
||||
if (this.myService) {
|
||||
this.myService.dispose();
|
||||
this.myService = undefined;
|
||||
}
|
||||
|
||||
// 移除事件监听器
|
||||
// 释放其他资源
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 异步插件
|
||||
|
||||
插件的 `install` 和 `uninstall` 方法都支持异步:
|
||||
|
||||
```typescript
|
||||
class AsyncPlugin implements IPlugin {
|
||||
readonly name = 'async-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
async install(core: Core, services: ServiceContainer): Promise<void> {
|
||||
// 异步加载资源
|
||||
const config = await fetch('/plugin-config.json').then(r => r.json());
|
||||
|
||||
// 使用加载的配置初始化服务
|
||||
const service = new MyService(config);
|
||||
services.registerInstance(MyService, service);
|
||||
}
|
||||
|
||||
async uninstall(): Promise<void> {
|
||||
// 异步清理
|
||||
await this.saveState();
|
||||
}
|
||||
|
||||
private async saveState() {
|
||||
// 保存插件状态
|
||||
}
|
||||
}
|
||||
|
||||
// 使用
|
||||
await Core.installPlugin(new AsyncPlugin());
|
||||
```
|
||||
|
||||
### 注册服务
|
||||
|
||||
插件可以向服务容器注册自己的服务:
|
||||
|
||||
```typescript
|
||||
import { IService } from '@esengine/ecs-framework';
|
||||
|
||||
class NetworkService implements IService {
|
||||
connect(url: string) {
|
||||
console.log(`Connecting to ${url}`);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
console.log('Network service disposed');
|
||||
}
|
||||
}
|
||||
|
||||
class NetworkPlugin implements IPlugin {
|
||||
readonly name = 'network-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// 注册网络服务
|
||||
services.registerSingleton(NetworkService);
|
||||
|
||||
// 解析并使用服务
|
||||
const network = services.resolve(NetworkService);
|
||||
network.connect('ws://localhost:8080');
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 服务容器会自动调用服务的dispose方法
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 添加系统
|
||||
|
||||
插件可以向场景添加自定义系统:
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, Matcher } from '@esengine/ecs-framework';
|
||||
|
||||
class PhysicsSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(PhysicsBody));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 物理模拟逻辑
|
||||
}
|
||||
}
|
||||
|
||||
class PhysicsPlugin implements IPlugin {
|
||||
readonly name = 'physics-plugin';
|
||||
readonly version = '1.0.0';
|
||||
private physicsSystem?: PhysicsSystem;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
const scene = core.scene;
|
||||
if (scene) {
|
||||
this.physicsSystem = new PhysicsSystem();
|
||||
scene.addSystem(this.physicsSystem);
|
||||
}
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 移除系统
|
||||
if (this.physicsSystem) {
|
||||
const scene = Core.scene;
|
||||
if (scene) {
|
||||
scene.removeSystem(this.physicsSystem);
|
||||
}
|
||||
this.physicsSystem = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 依赖管理
|
||||
|
||||
### 声明依赖
|
||||
|
||||
插件可以声明对其他插件的依赖:
|
||||
|
||||
```typescript
|
||||
class AdvancedPhysicsPlugin implements IPlugin {
|
||||
readonly name = 'advanced-physics';
|
||||
readonly version = '2.0.0';
|
||||
|
||||
// 声明依赖基础物理插件
|
||||
readonly dependencies = ['physics-plugin'] as const;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// 可以安全地使用physics-plugin提供的服务
|
||||
const physicsService = services.resolve(PhysicsService);
|
||||
// ...
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 清理
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 依赖检查
|
||||
|
||||
框架会自动检查依赖关系,如果依赖未满足会抛出错误:
|
||||
|
||||
```typescript
|
||||
// 错误:physics-plugin 未安装
|
||||
try {
|
||||
await Core.installPlugin(new AdvancedPhysicsPlugin());
|
||||
} catch (error) {
|
||||
console.error(error); // Plugin advanced-physics has unmet dependencies: physics-plugin
|
||||
}
|
||||
|
||||
// 正确:先安装依赖
|
||||
await Core.installPlugin(new PhysicsPlugin());
|
||||
await Core.installPlugin(new AdvancedPhysicsPlugin());
|
||||
```
|
||||
|
||||
### 卸载顺序
|
||||
|
||||
框架会检查依赖关系,防止卸载被其他插件依赖的插件:
|
||||
|
||||
```typescript
|
||||
await Core.installPlugin(new PhysicsPlugin());
|
||||
await Core.installPlugin(new AdvancedPhysicsPlugin());
|
||||
|
||||
// 错误:physics-plugin 被 advanced-physics 依赖
|
||||
try {
|
||||
await Core.uninstallPlugin('physics-plugin');
|
||||
} catch (error) {
|
||||
console.error(error); // Cannot uninstall plugin physics-plugin: it is required by advanced-physics
|
||||
}
|
||||
|
||||
// 正确:先卸载依赖它的插件
|
||||
await Core.uninstallPlugin('advanced-physics');
|
||||
await Core.uninstallPlugin('physics-plugin');
|
||||
```
|
||||
|
||||
## 插件管理
|
||||
|
||||
### 通过 Core 管理
|
||||
|
||||
Core 类提供了便捷的插件管理方法:
|
||||
|
||||
```typescript
|
||||
// 安装插件
|
||||
await Core.installPlugin(myPlugin);
|
||||
|
||||
// 卸载插件
|
||||
await Core.uninstallPlugin('plugin-name');
|
||||
|
||||
// 检查插件是否已安装
|
||||
if (Core.isPluginInstalled('plugin-name')) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// 获取插件实例
|
||||
const plugin = Core.getPlugin('plugin-name');
|
||||
```
|
||||
|
||||
### 通过 PluginManager 管理
|
||||
|
||||
也可以直接使用 PluginManager 服务:
|
||||
|
||||
```typescript
|
||||
const pluginManager = Core.services.resolve(PluginManager);
|
||||
|
||||
// 获取所有插件
|
||||
const allPlugins = pluginManager.getAllPlugins();
|
||||
console.log(`Total plugins: ${allPlugins.length}`);
|
||||
|
||||
// 获取插件元数据
|
||||
const metadata = pluginManager.getMetadata('my-plugin');
|
||||
if (metadata) {
|
||||
console.log(`State: ${metadata.state}`);
|
||||
console.log(`Installed at: ${new Date(metadata.installedAt!)}`);
|
||||
}
|
||||
|
||||
// 获取所有插件元数据
|
||||
const allMetadata = pluginManager.getAllMetadata();
|
||||
for (const meta of allMetadata) {
|
||||
console.log(`${meta.name} v${meta.version} - ${meta.state}`);
|
||||
}
|
||||
```
|
||||
|
||||
## 实用插件示例
|
||||
|
||||
### 网络同步插件
|
||||
|
||||
```typescript
|
||||
import { IPlugin, IService, Core, ServiceContainer } from '@esengine/ecs-framework';
|
||||
|
||||
class NetworkSyncService implements IService {
|
||||
private ws?: WebSocket;
|
||||
|
||||
connect(url: string) {
|
||||
this.ws = new WebSocket(url);
|
||||
this.ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
this.handleMessage(data);
|
||||
};
|
||||
}
|
||||
|
||||
private handleMessage(data: any) {
|
||||
// 处理网络消息
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
this.ws = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NetworkSyncPlugin implements IPlugin {
|
||||
readonly name = 'network-sync';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// 注册网络服务
|
||||
services.registerSingleton(NetworkSyncService);
|
||||
|
||||
// 自动连接
|
||||
const network = services.resolve(NetworkSyncService);
|
||||
network.connect('ws://localhost:8080');
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 服务会自动dispose
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 性能分析插件
|
||||
|
||||
```typescript
|
||||
class PerformanceAnalysisPlugin implements IPlugin {
|
||||
readonly name = 'performance-analysis';
|
||||
readonly version = '1.0.0';
|
||||
private frameCount = 0;
|
||||
private totalTime = 0;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
const monitor = services.resolve(PerformanceMonitor);
|
||||
monitor.enable();
|
||||
|
||||
// 定期输出性能报告
|
||||
const timer = services.resolve(TimerManager);
|
||||
timer.schedule(5.0, true, null, () => {
|
||||
this.printReport(monitor);
|
||||
});
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 清理
|
||||
}
|
||||
|
||||
private printReport(monitor: PerformanceMonitor) {
|
||||
console.log('=== Performance Report ===');
|
||||
console.log(`FPS: ${monitor.getFPS()}`);
|
||||
console.log(`Memory: ${monitor.getMemoryUsage()} MB`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 命名规范
|
||||
|
||||
- 插件名称使用小写字母和连字符:`my-awesome-plugin`
|
||||
- 版本号遵循语义化版本规范:`1.0.0`
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-awesome-plugin'; // 好
|
||||
readonly version = '1.0.0'; // 好
|
||||
}
|
||||
```
|
||||
|
||||
### 清理资源
|
||||
|
||||
始终在 `uninstall` 中清理插件创建的所有资源:
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-plugin';
|
||||
readonly version = '1.0.0';
|
||||
private timerId?: number;
|
||||
private listener?: () => void;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// 添加定时器
|
||||
this.timerId = setInterval(() => {
|
||||
// ...
|
||||
}, 1000);
|
||||
|
||||
// 添加事件监听
|
||||
this.listener = () => {};
|
||||
window.addEventListener('resize', this.listener);
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 清理定时器
|
||||
if (this.timerId) {
|
||||
clearInterval(this.timerId);
|
||||
this.timerId = undefined;
|
||||
}
|
||||
|
||||
// 移除事件监听
|
||||
if (this.listener) {
|
||||
window.removeEventListener('resize', this.listener);
|
||||
this.listener = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 错误处理
|
||||
|
||||
在插件中妥善处理错误,避免影响整个应用:
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
async install(core: Core, services: ServiceContainer): Promise<void> {
|
||||
try {
|
||||
// 可能失败的操作
|
||||
await this.loadConfig();
|
||||
} catch (error) {
|
||||
console.error('Failed to load plugin config:', error);
|
||||
throw error; // 重新抛出,让框架知道安装失败
|
||||
}
|
||||
}
|
||||
|
||||
async uninstall(): Promise<void> {
|
||||
try {
|
||||
await this.cleanup();
|
||||
} catch (error) {
|
||||
console.error('Failed to cleanup plugin:', error);
|
||||
// 即使清理失败也不应该阻止卸载
|
||||
}
|
||||
}
|
||||
|
||||
private async loadConfig() {
|
||||
// 加载配置
|
||||
}
|
||||
|
||||
private async cleanup() {
|
||||
// 清理
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 配置化
|
||||
|
||||
允许用户配置插件行为:
|
||||
|
||||
```typescript
|
||||
interface NetworkPluginConfig {
|
||||
serverUrl: string;
|
||||
autoReconnect: boolean;
|
||||
timeout: number;
|
||||
}
|
||||
|
||||
class NetworkPlugin implements IPlugin {
|
||||
readonly name = 'network-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
constructor(private config: NetworkPluginConfig) {}
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
const network = new NetworkService(this.config);
|
||||
services.registerInstance(NetworkService, network);
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 清理
|
||||
}
|
||||
}
|
||||
|
||||
// 使用
|
||||
const plugin = new NetworkPlugin({
|
||||
serverUrl: 'ws://localhost:8080',
|
||||
autoReconnect: true,
|
||||
timeout: 5000
|
||||
});
|
||||
|
||||
await Core.installPlugin(plugin);
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 插件安装失败
|
||||
|
||||
**问题**: 插件安装时抛出错误
|
||||
|
||||
**原因**:
|
||||
- 依赖未满足
|
||||
- install 方法中有异常
|
||||
- 服务注册冲突
|
||||
|
||||
**解决**:
|
||||
1. 检查依赖是否已安装
|
||||
2. 查看错误日志
|
||||
3. 确保服务名称不冲突
|
||||
|
||||
### 插件卸载后仍有副作用
|
||||
|
||||
**问题**: 卸载插件后,插件的功能仍在运行
|
||||
|
||||
**原因**: uninstall 方法中未正确清理资源
|
||||
|
||||
**解决**: 确保在 uninstall 中清理:
|
||||
- 定时器
|
||||
- 事件监听器
|
||||
- WebSocket连接
|
||||
- 系统引用
|
||||
|
||||
### 何时使用插件
|
||||
|
||||
**适合使用插件**:
|
||||
- 可选功能(调试工具、性能分析)
|
||||
- 第三方集成(网络库、物理引擎)
|
||||
- 跨项目复用的功能模块
|
||||
|
||||
**不适合使用插件**:
|
||||
- 核心游戏逻辑
|
||||
- 简单的工具类
|
||||
- 项目特定的功能
|
||||
|
||||
## 相关链接
|
||||
|
||||
- [服务容器](./service-container.md) - 在插件中使用服务容器
|
||||
- [系统架构](./system.md) - 在插件中添加系统
|
||||
- [快速开始](./getting-started.md) - Core 初始化和基础使用
|
||||
675
docs/guide/scene-manager.md
Normal file
675
docs/guide/scene-manager.md
Normal file
@@ -0,0 +1,675 @@
|
||||
# SceneManager
|
||||
|
||||
SceneManager 是 ECS Framework 提供的轻量级场景管理器,适用于 95% 的游戏应用。它提供简单直观的 API,支持场景切换和延迟加载。
|
||||
|
||||
## 适用场景
|
||||
|
||||
SceneManager 适合以下场景:
|
||||
- 单人游戏
|
||||
- 简单多人游戏
|
||||
- 移动游戏
|
||||
- 需要场景切换的游戏(菜单、游戏、暂停等)
|
||||
- 不需要多 World 隔离的项目
|
||||
|
||||
## 特点
|
||||
|
||||
- 轻量级,零额外开销
|
||||
- 简单直观的 API
|
||||
- 支持延迟场景切换(避免在当前帧中途切换)
|
||||
- 自动管理 ECS 流式 API
|
||||
- 自动处理场景生命周期
|
||||
- 集成在 Core 中,自动更新
|
||||
|
||||
## 基本使用
|
||||
|
||||
### 推荐方式:使用 Core 的静态方法
|
||||
|
||||
这是最简单和推荐的方式,适合大多数应用:
|
||||
|
||||
```typescript
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// 1. 初始化 Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 2. 创建并设置场景
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "GameScene";
|
||||
|
||||
// 添加系统
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
|
||||
// 创建初始实体
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Transform(400, 300));
|
||||
player.addComponent(new Health(100));
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log("游戏场景已启动");
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 设置场景
|
||||
Core.setScene(new GameScene());
|
||||
|
||||
// 4. 游戏循环(Core.update 会自动更新场景)
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // 自动更新所有服务和场景
|
||||
}
|
||||
|
||||
// Laya 引擎集成
|
||||
Laya.timer.frameLoop(1, this, () => {
|
||||
const deltaTime = Laya.timer.delta / 1000;
|
||||
Core.update(deltaTime);
|
||||
});
|
||||
|
||||
// Cocos Creator 集成
|
||||
update(deltaTime: number) {
|
||||
Core.update(deltaTime);
|
||||
}
|
||||
```
|
||||
|
||||
### 高级方式:直接使用 SceneManager
|
||||
|
||||
如果需要更多控制,可以直接使用 SceneManager:
|
||||
|
||||
```typescript
|
||||
import { Core, SceneManager, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始化 Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 获取 SceneManager(Core 已自动创建并注册)
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
|
||||
// 设置场景
|
||||
const gameScene = new GameScene();
|
||||
sceneManager.setScene(gameScene);
|
||||
|
||||
// 游戏循环(仍然使用 Core.update)
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // Core会自动调用sceneManager.update()
|
||||
}
|
||||
```
|
||||
|
||||
**重要**:无论使用哪种方式,游戏循环中都应该只调用 `Core.update()`,它会自动更新 SceneManager 和场景。不需要手动调用 `sceneManager.update()`。
|
||||
|
||||
## 场景切换
|
||||
|
||||
### 立即切换
|
||||
|
||||
使用 `Core.setScene()` 或 `sceneManager.setScene()` 立即切换场景:
|
||||
|
||||
```typescript
|
||||
// 方式1:使用 Core(推荐)
|
||||
Core.setScene(new MenuScene());
|
||||
|
||||
// 方式2:使用 SceneManager
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
sceneManager.setScene(new MenuScene());
|
||||
```
|
||||
|
||||
### 延迟切换
|
||||
|
||||
使用 `Core.loadScene()` 或 `sceneManager.loadScene()` 延迟切换场景,场景会在下一帧切换:
|
||||
|
||||
```typescript
|
||||
// 方式1:使用 Core(推荐)
|
||||
Core.loadScene(new GameOverScene());
|
||||
|
||||
// 方式2:使用 SceneManager
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
sceneManager.loadScene(new GameOverScene());
|
||||
```
|
||||
|
||||
在 System 中切换场景时,应该使用延迟切换:
|
||||
|
||||
```typescript
|
||||
class GameOverSystem extends EntitySystem {
|
||||
process(entities: readonly Entity[]): void {
|
||||
const player = entities.find(e => e.name === 'Player');
|
||||
const health = player?.getComponent(Health);
|
||||
|
||||
if (health && health.value <= 0) {
|
||||
// 延迟切换到游戏结束场景(下一帧生效)
|
||||
Core.loadScene(new GameOverScene());
|
||||
// 当前帧继续执行,不会中断当前系统的处理
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 完整的场景切换示例
|
||||
|
||||
```typescript
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始化
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 菜单场景
|
||||
class MenuScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "MenuScene";
|
||||
|
||||
// 监听开始游戏事件
|
||||
this.eventSystem.on('start_game', () => {
|
||||
Core.loadScene(new GameScene());
|
||||
});
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log("显示菜单界面");
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
console.log("菜单场景卸载");
|
||||
}
|
||||
}
|
||||
|
||||
// 游戏场景
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "GameScene";
|
||||
|
||||
// 创建游戏实体
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Transform(400, 300));
|
||||
player.addComponent(new Health(100));
|
||||
|
||||
// 监听游戏结束事件
|
||||
this.eventSystem.on('game_over', () => {
|
||||
Core.loadScene(new GameOverScene());
|
||||
});
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log("游戏开始");
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
console.log("游戏场景卸载");
|
||||
}
|
||||
}
|
||||
|
||||
// 游戏结束场景
|
||||
class GameOverScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "GameOverScene";
|
||||
|
||||
// 监听返回菜单事件
|
||||
this.eventSystem.on('back_to_menu', () => {
|
||||
Core.loadScene(new MenuScene());
|
||||
});
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log("显示游戏结束界面");
|
||||
}
|
||||
}
|
||||
|
||||
// 开始游戏
|
||||
Core.setScene(new MenuScene());
|
||||
|
||||
// 游戏循环
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // 自动更新场景
|
||||
}
|
||||
```
|
||||
|
||||
## API 参考
|
||||
|
||||
### Core 静态方法(推荐)
|
||||
|
||||
#### Core.setScene()
|
||||
|
||||
立即切换场景。
|
||||
|
||||
```typescript
|
||||
public static setScene<T extends IScene>(scene: T): T
|
||||
```
|
||||
|
||||
**参数**:
|
||||
- `scene` - 要设置的场景实例
|
||||
|
||||
**返回**:
|
||||
- 返回设置的场景实例
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
const gameScene = Core.setScene(new GameScene());
|
||||
console.log(gameScene.name);
|
||||
```
|
||||
|
||||
#### Core.loadScene()
|
||||
|
||||
延迟加载场景(下一帧切换)。
|
||||
|
||||
```typescript
|
||||
public static loadScene<T extends IScene>(scene: T): void
|
||||
```
|
||||
|
||||
**参数**:
|
||||
- `scene` - 要加载的场景实例
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
Core.loadScene(new GameOverScene());
|
||||
```
|
||||
|
||||
#### Core.scene
|
||||
|
||||
获取当前活跃的场景。
|
||||
|
||||
```typescript
|
||||
public static get scene(): IScene | null
|
||||
```
|
||||
|
||||
**返回**:
|
||||
- 当前场景实例,如果没有场景则返回 null
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
const currentScene = Core.scene;
|
||||
if (currentScene) {
|
||||
console.log(`当前场景: ${currentScene.name}`);
|
||||
}
|
||||
```
|
||||
|
||||
#### Core.ecsAPI
|
||||
|
||||
获取 ECS 流式 API。
|
||||
|
||||
```typescript
|
||||
public static get ecsAPI(): ECSFluentAPI | null
|
||||
```
|
||||
|
||||
**返回**:
|
||||
- ECS API 实例,如果当前没有场景则返回 null
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
const api = Core.ecsAPI;
|
||||
if (api) {
|
||||
// 查询实体
|
||||
const enemies = api.find(Enemy, Transform);
|
||||
|
||||
// 发射事件
|
||||
api.emit('game:start', { level: 1 });
|
||||
}
|
||||
```
|
||||
|
||||
### SceneManager 方法(高级)
|
||||
|
||||
如果需要直接使用 SceneManager,可以通过服务容器获取:
|
||||
|
||||
```typescript
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
```
|
||||
|
||||
#### setScene()
|
||||
|
||||
立即切换场景。
|
||||
|
||||
```typescript
|
||||
public setScene<T extends IScene>(scene: T): T
|
||||
```
|
||||
|
||||
#### loadScene()
|
||||
|
||||
延迟加载场景。
|
||||
|
||||
```typescript
|
||||
public loadScene<T extends IScene>(scene: T): void
|
||||
```
|
||||
|
||||
#### currentScene
|
||||
|
||||
获取当前场景。
|
||||
|
||||
```typescript
|
||||
public get currentScene(): IScene | null
|
||||
```
|
||||
|
||||
#### api
|
||||
|
||||
获取 ECS 流式 API。
|
||||
|
||||
```typescript
|
||||
public get api(): ECSFluentAPI | null
|
||||
```
|
||||
|
||||
#### hasScene
|
||||
|
||||
检查是否有活跃场景。
|
||||
|
||||
```typescript
|
||||
public get hasScene(): boolean
|
||||
```
|
||||
|
||||
#### hasPendingScene
|
||||
|
||||
检查是否有待切换的场景。
|
||||
|
||||
```typescript
|
||||
public get hasPendingScene(): boolean
|
||||
```
|
||||
|
||||
## 使用 ECS 流式 API
|
||||
|
||||
通过 `Core.ecsAPI` 可以方便地访问场景的 ECS 功能:
|
||||
|
||||
```typescript
|
||||
const api = Core.ecsAPI;
|
||||
if (!api) {
|
||||
console.error('没有活跃场景');
|
||||
return;
|
||||
}
|
||||
|
||||
// 查询实体
|
||||
const players = api.find(Player, Transform);
|
||||
const enemies = api.find(Enemy, Health, Transform);
|
||||
|
||||
// 发射事件
|
||||
api.emit('player:scored', { points: 100 });
|
||||
|
||||
// 监听事件
|
||||
api.on('enemy:died', (data) => {
|
||||
console.log('敌人死亡:', data);
|
||||
});
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 使用 Core 的静态方法
|
||||
|
||||
```typescript
|
||||
// 推荐:使用 Core 的静态方法
|
||||
Core.setScene(new GameScene());
|
||||
Core.loadScene(new MenuScene());
|
||||
const currentScene = Core.scene;
|
||||
|
||||
// 不推荐:除非有特殊需求,否则不需要直接使用 SceneManager
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
sceneManager.setScene(new GameScene());
|
||||
```
|
||||
|
||||
### 2. 只调用 Core.update()
|
||||
|
||||
```typescript
|
||||
// 正确:只调用 Core.update()
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // 自动更新所有服务和场景
|
||||
}
|
||||
|
||||
// 错误:不要手动调用 sceneManager.update()
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime);
|
||||
sceneManager.update(); // 重复更新,会导致问题!
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 使用延迟切换避免问题
|
||||
|
||||
在 System 中切换场景时,应该使用 `loadScene()` 而不是 `setScene()`:
|
||||
|
||||
```typescript
|
||||
// 推荐:延迟切换
|
||||
class HealthSystem extends EntitySystem {
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health);
|
||||
if (health.value <= 0) {
|
||||
Core.loadScene(new GameOverScene());
|
||||
// 当前帧继续处理其他实体
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 不推荐:立即切换可能导致问题
|
||||
class HealthSystem extends EntitySystem {
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health);
|
||||
if (health.value <= 0) {
|
||||
Core.setScene(new GameOverScene());
|
||||
// 场景立即切换,当前帧的其他实体可能无法正常处理
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 场景职责分离
|
||||
|
||||
每个场景应该只负责一个特定的游戏状态:
|
||||
|
||||
```typescript
|
||||
// 好的设计 - 职责清晰
|
||||
class MenuScene extends Scene {
|
||||
// 只处理菜单相关逻辑
|
||||
}
|
||||
|
||||
class GameScene extends Scene {
|
||||
// 只处理游戏玩法逻辑
|
||||
}
|
||||
|
||||
class PauseScene extends Scene {
|
||||
// 只处理暂停界面逻辑
|
||||
}
|
||||
|
||||
// 避免的设计 - 职责混乱
|
||||
class MegaScene extends Scene {
|
||||
// 包含菜单、游戏、暂停等所有逻辑
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 资源管理
|
||||
|
||||
在场景的 `unload()` 方法中清理资源:
|
||||
|
||||
```typescript
|
||||
class GameScene extends Scene {
|
||||
private textures: Map<string, any> = new Map();
|
||||
private sounds: Map<string, any> = new Map();
|
||||
|
||||
protected initialize(): void {
|
||||
this.loadResources();
|
||||
}
|
||||
|
||||
private loadResources(): void {
|
||||
this.textures.set('player', loadTexture('player.png'));
|
||||
this.sounds.set('bgm', loadSound('bgm.mp3'));
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// 清理资源
|
||||
this.textures.clear();
|
||||
this.sounds.clear();
|
||||
console.log('场景资源已清理');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. 事件驱动的场景切换
|
||||
|
||||
使用事件系统来触发场景切换,保持代码解耦:
|
||||
|
||||
```typescript
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 监听场景切换事件
|
||||
this.eventSystem.on('goto:menu', () => {
|
||||
Core.loadScene(new MenuScene());
|
||||
});
|
||||
|
||||
this.eventSystem.on('goto:gameover', (data) => {
|
||||
Core.loadScene(new GameOverScene());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 在 System 中触发事件
|
||||
class GameLogicSystem extends EntitySystem {
|
||||
process(entities: readonly Entity[]): void {
|
||||
if (levelComplete) {
|
||||
this.scene.eventSystem.emitSync('goto:gameover', {
|
||||
score: 1000,
|
||||
level: 5
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 架构层次
|
||||
|
||||
SceneManager 在 ECS Framework 中的位置:
|
||||
|
||||
```
|
||||
Core (全局服务)
|
||||
└── SceneManager (场景管理,自动更新)
|
||||
└── Scene (当前场景)
|
||||
├── EntitySystem (系统)
|
||||
├── Entity (实体)
|
||||
└── Component (组件)
|
||||
```
|
||||
|
||||
## 与 WorldManager 的对比
|
||||
|
||||
| 特性 | SceneManager | WorldManager |
|
||||
|------|--------------|--------------|
|
||||
| 适用场景 | 95% 的游戏应用 | 高级多世界隔离场景 |
|
||||
| 复杂度 | 简单 | 复杂 |
|
||||
| 场景数量 | 单场景(可切换) | 多 World,每个 World 多场景 |
|
||||
| 性能开销 | 最小 | 较高 |
|
||||
| 使用方式 | `Core.setScene()` | `worldManager.createWorld()` |
|
||||
|
||||
**何时使用 SceneManager**:
|
||||
- 单人游戏
|
||||
- 简单的多人游戏
|
||||
- 移动游戏
|
||||
- 场景之间需要切换但不需要同时运行
|
||||
|
||||
**何时使用 WorldManager**:
|
||||
- MMO 游戏服务器(每个房间一个 World)
|
||||
- 游戏大厅系统(每个游戏房间完全隔离)
|
||||
- 需要运行多个完全独立的游戏实例
|
||||
|
||||
## 完整示例
|
||||
|
||||
```typescript
|
||||
import { Core, Scene, EntitySystem, Entity, Matcher } from '@esengine/ecs-framework';
|
||||
|
||||
// 定义组件
|
||||
class Transform {
|
||||
constructor(public x: number, public y: number) {}
|
||||
}
|
||||
|
||||
class Velocity {
|
||||
constructor(public vx: number, public vy: number) {}
|
||||
}
|
||||
|
||||
class Health {
|
||||
constructor(public value: number) {}
|
||||
}
|
||||
|
||||
// 定义系统
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Transform, Velocity));
|
||||
}
|
||||
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const transform = entity.getComponent(Transform);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
if (transform && velocity) {
|
||||
transform.x += velocity.vx;
|
||||
transform.y += velocity.vy;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 定义场景
|
||||
class MenuScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "MenuScene";
|
||||
console.log("菜单场景初始化");
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log("菜单场景启动");
|
||||
}
|
||||
}
|
||||
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "GameScene";
|
||||
|
||||
// 添加系统
|
||||
this.addSystem(new MovementSystem());
|
||||
|
||||
// 创建玩家
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Transform(400, 300));
|
||||
player.addComponent(new Velocity(0, 0));
|
||||
player.addComponent(new Health(100));
|
||||
|
||||
// 创建敌人
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const enemy = this.createEntity(`Enemy_${i}`);
|
||||
enemy.addComponent(new Transform(
|
||||
Math.random() * 800,
|
||||
Math.random() * 600
|
||||
));
|
||||
enemy.addComponent(new Velocity(
|
||||
Math.random() * 100 - 50,
|
||||
Math.random() * 100 - 50
|
||||
));
|
||||
enemy.addComponent(new Health(50));
|
||||
}
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log('游戏场景启动');
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
console.log('游戏场景卸载');
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 设置初始场景
|
||||
Core.setScene(new MenuScene());
|
||||
|
||||
// 游戏循环
|
||||
let lastTime = 0;
|
||||
function gameLoop(currentTime: number) {
|
||||
const deltaTime = (currentTime - lastTime) / 1000;
|
||||
lastTime = currentTime;
|
||||
|
||||
// 只需要调用 Core.update,它会自动更新场景
|
||||
Core.update(deltaTime);
|
||||
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
requestAnimationFrame(gameLoop);
|
||||
|
||||
// 切换到游戏场景
|
||||
setTimeout(() => {
|
||||
Core.loadScene(new GameScene());
|
||||
}, 3000);
|
||||
```
|
||||
|
||||
SceneManager 为大多数游戏提供了简单而强大的场景管理能力。通过 Core 的静态方法,你可以轻松地管理场景切换。如果你需要更高级的多世界隔离功能,请参考 [WorldManager](./world-manager.md) 文档。
|
||||
@@ -11,6 +11,22 @@
|
||||
- 事件系统支持
|
||||
- 性能监控和调试信息
|
||||
|
||||
## 场景管理方式
|
||||
|
||||
ECS Framework 提供了两种场景管理方式:
|
||||
|
||||
1. **[SceneManager](./scene-manager.md)** - 适用于 95% 的游戏应用
|
||||
- 单人游戏、简单多人游戏、移动游戏
|
||||
- 轻量级,简单直观的 API
|
||||
- 支持场景切换
|
||||
|
||||
2. **[WorldManager](./world-manager.md)** - 适用于高级多世界隔离场景
|
||||
- MMO 游戏服务器、游戏房间系统
|
||||
- 多 World 管理,每个 World 可包含多个场景
|
||||
- 完全隔离的独立环境
|
||||
|
||||
本文档重点介绍 Scene 类本身的使用方法。关于场景管理器的详细信息,请查看对应的文档。
|
||||
|
||||
## 创建场景
|
||||
|
||||
### 继承 Scene 类
|
||||
@@ -106,6 +122,13 @@ const scene = new ExampleScene();
|
||||
// 场景的 initialize(), begin(), update(), end() 由框架自动调用
|
||||
```
|
||||
|
||||
**生命周期方法**:
|
||||
|
||||
1. `initialize()` - 场景初始化,设置系统和初始实体
|
||||
2. `begin()` / `onStart()` - 场景开始运行
|
||||
3. `update()` - 每帧更新(由场景管理器调用)
|
||||
4. `end()` / `unload()` - 场景卸载,清理资源
|
||||
|
||||
## 实体管理
|
||||
|
||||
### 创建实体
|
||||
@@ -247,15 +270,42 @@ class EventScene extends Scene {
|
||||
}
|
||||
|
||||
public triggerGameEvent(): void {
|
||||
// 发送事件
|
||||
// 发送事件(同步)
|
||||
this.eventSystem.emitSync('custom_event', {
|
||||
message: "这是自定义事件",
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
// 发送事件(异步)
|
||||
this.eventSystem.emit('async_event', {
|
||||
data: "异步事件数据"
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 事件系统 API
|
||||
|
||||
```typescript
|
||||
// 监听事件
|
||||
this.eventSystem.on('event_name', callback);
|
||||
|
||||
// 监听一次(自动取消订阅)
|
||||
this.eventSystem.once('event_name', callback);
|
||||
|
||||
// 取消监听
|
||||
this.eventSystem.off('event_name', callback);
|
||||
|
||||
// 同步发送事件
|
||||
this.eventSystem.emitSync('event_name', data);
|
||||
|
||||
// 异步发送事件
|
||||
this.eventSystem.emit('event_name', data);
|
||||
|
||||
// 清除所有事件监听
|
||||
this.eventSystem.clear();
|
||||
```
|
||||
|
||||
## 场景统计和调试
|
||||
|
||||
### 获取场景统计
|
||||
@@ -287,110 +337,59 @@ class StatsScene extends Scene {
|
||||
}
|
||||
```
|
||||
|
||||
## 场景集成到框架
|
||||
## 组件查询
|
||||
|
||||
场景可以通过两种方式运行:
|
||||
|
||||
### 1. 简单的单场景应用
|
||||
Scene 提供了强大的组件查询系统:
|
||||
|
||||
```typescript
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
// 创建游戏场景
|
||||
class GameScene extends Scene {
|
||||
class QueryScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "GameScene";
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
}
|
||||
}
|
||||
|
||||
// 启动游戏
|
||||
Core.create();
|
||||
const gameScene = new GameScene();
|
||||
Core.setScene(gameScene);
|
||||
```
|
||||
|
||||
### 2. 复杂的多场景应用
|
||||
|
||||
```typescript
|
||||
import { WorldManager } from '@esengine/ecs-framework';
|
||||
|
||||
// 获取WorldManager实例
|
||||
const worldManager = WorldManager.getInstance();
|
||||
|
||||
// 创建World
|
||||
const gameWorld = worldManager.createWorld('game', {
|
||||
name: 'MainGame',
|
||||
maxScenes: 5
|
||||
});
|
||||
|
||||
// 在World中创建场景
|
||||
const menuScene = gameWorld.createScene('menu', new MenuScene());
|
||||
const gameScene = gameWorld.createScene('game', new GameScene());
|
||||
|
||||
// 激活场景
|
||||
gameWorld.setSceneActive('menu', true);
|
||||
```
|
||||
|
||||
## 多场景管理
|
||||
|
||||
在World中可以管理多个场景,通过激活/停用来切换:
|
||||
|
||||
```typescript
|
||||
class GameWorld extends World {
|
||||
private menuScene: Scene;
|
||||
private gameScene: Scene;
|
||||
private gameOverScene: Scene;
|
||||
|
||||
public initialize(): void {
|
||||
// 创建多个场景
|
||||
this.menuScene = this.createScene('menu', new MenuScene());
|
||||
this.gameScene = this.createScene('game', new GameScene());
|
||||
this.gameOverScene = this.createScene('gameover', new GameOverScene());
|
||||
|
||||
// 设置初始场景
|
||||
this.showMenu();
|
||||
// 创建一些实体
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const entity = this.createEntity(`Entity_${i}`);
|
||||
entity.addComponent(new Transform(i * 10, 0));
|
||||
entity.addComponent(new Velocity(1, 0));
|
||||
if (i % 2 === 0) {
|
||||
entity.addComponent(new Renderer());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public showMenu(): void {
|
||||
this.deactivateAllScenes();
|
||||
this.setSceneActive('menu', true);
|
||||
}
|
||||
public queryEntities(): void {
|
||||
// 通过 QuerySystem 查询
|
||||
const entities = this.querySystem.query([Transform, Velocity]);
|
||||
console.log(`找到 ${entities.length} 个有 Transform 和 Velocity 的实体`);
|
||||
|
||||
public startGame(): void {
|
||||
this.deactivateAllScenes();
|
||||
this.setSceneActive('game', true);
|
||||
}
|
||||
|
||||
public showGameOver(): void {
|
||||
this.deactivateAllScenes();
|
||||
this.setSceneActive('gameover', true);
|
||||
}
|
||||
|
||||
private deactivateAllScenes(): void {
|
||||
this.setSceneActive('menu', false);
|
||||
this.setSceneActive('game', false);
|
||||
this.setSceneActive('gameover', false);
|
||||
// 使用 ECS 流式 API(如果通过 SceneManager)
|
||||
// const api = sceneManager.api;
|
||||
// const entities = api?.find(Transform, Velocity);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 与 World 的关系
|
||||
## 性能监控
|
||||
|
||||
Scene 的运行架构层次:
|
||||
Scene 内置了性能监控功能:
|
||||
|
||||
```typescript
|
||||
// Core -> WorldManager -> World -> Scene -> EntitySystem -> Entity -> Component
|
||||
class PerformanceScene extends Scene {
|
||||
public showPerformance(): void {
|
||||
// 获取性能数据
|
||||
const perfData = this.performanceMonitor?.getPerformanceData();
|
||||
if (perfData) {
|
||||
console.log('FPS:', perfData.fps);
|
||||
console.log('帧时间:', perfData.frameTime);
|
||||
console.log('实体更新时间:', perfData.entityUpdateTime);
|
||||
console.log('系统更新时间:', perfData.systemUpdateTime);
|
||||
}
|
||||
|
||||
// 1. 简单应用:Core直接管理单个Scene
|
||||
Core.setScene(new GameScene());
|
||||
|
||||
// 2. 复杂应用:WorldManager管理多个World,每个World管理多个Scene
|
||||
const worldManager = WorldManager.getInstance();
|
||||
const world = worldManager.createWorld('gameWorld');
|
||||
const scene = world.createScene('mainScene', new GameScene());
|
||||
world.setSceneActive('mainScene', true);
|
||||
// 获取性能报告
|
||||
const report = this.performanceMonitor?.generateReport();
|
||||
if (report) {
|
||||
console.log('性能报告:', report);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
@@ -398,7 +397,7 @@ world.setSceneActive('mainScene', true);
|
||||
### 1. 场景职责分离
|
||||
|
||||
```typescript
|
||||
// ✅ 好的场景设计 - 职责清晰
|
||||
// 好的场景设计 - 职责清晰
|
||||
class MenuScene extends Scene {
|
||||
// 只处理菜单相关逻辑
|
||||
}
|
||||
@@ -411,7 +410,7 @@ class InventoryScene extends Scene {
|
||||
// 只处理物品栏逻辑
|
||||
}
|
||||
|
||||
// ❌ 避免的场景设计 - 职责混乱
|
||||
// 避免的场景设计 - 职责混乱
|
||||
class MegaScene extends Scene {
|
||||
// 包含菜单、游戏、物品栏等所有逻辑
|
||||
}
|
||||
@@ -458,12 +457,25 @@ class ResourceScene extends Scene {
|
||||
|
||||
private loadResources(): void {
|
||||
// 加载场景所需资源
|
||||
this.textures.set('player', this.loadTexture('player.png'));
|
||||
this.sounds.set('bgm', this.loadSound('bgm.mp3'));
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// 清理资源
|
||||
this.textures.clear();
|
||||
this.sounds.clear();
|
||||
console.log('场景资源已清理');
|
||||
}
|
||||
|
||||
private loadTexture(path: string): any {
|
||||
// 加载纹理
|
||||
return null;
|
||||
}
|
||||
|
||||
private loadSound(path: string): any {
|
||||
// 加载音效
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -504,7 +516,146 @@ class EventHandlingScene extends Scene {
|
||||
private onPlayerInput(data: any): void {
|
||||
// 处理玩家输入
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// 清理事件监听
|
||||
this.eventSystem.clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
场景是 ECS 框架的核心容器,正确使用场景管理能让你的游戏架构更加清晰、模块化和易于维护。
|
||||
### 5. 初始化顺序
|
||||
|
||||
```typescript
|
||||
class ProperInitScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 1. 首先设置场景配置
|
||||
this.name = "GameScene";
|
||||
|
||||
// 2. 然后添加系统(按依赖顺序)
|
||||
this.addSystem(new InputSystem());
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new PhysicsSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
|
||||
// 3. 最后创建实体
|
||||
this.createEntities();
|
||||
|
||||
// 4. 设置事件监听
|
||||
this.setupEvents();
|
||||
}
|
||||
|
||||
private createEntities(): void {
|
||||
// 创建实体
|
||||
}
|
||||
|
||||
private setupEvents(): void {
|
||||
// 设置事件监听
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 完整示例
|
||||
|
||||
```typescript
|
||||
import { Scene, EntitySystem, Entity, Matcher } from '@esengine/ecs-framework';
|
||||
|
||||
// 定义组件
|
||||
class Transform {
|
||||
constructor(public x: number, public y: number) {}
|
||||
}
|
||||
|
||||
class Velocity {
|
||||
constructor(public vx: number, public vy: number) {}
|
||||
}
|
||||
|
||||
class Health {
|
||||
constructor(public value: number) {}
|
||||
}
|
||||
|
||||
// 定义系统
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Transform, Velocity));
|
||||
}
|
||||
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const transform = entity.getComponent(Transform);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
if (transform && velocity) {
|
||||
transform.x += velocity.vx;
|
||||
transform.y += velocity.vy;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 定义场景
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "GameScene";
|
||||
|
||||
// 添加系统
|
||||
this.addSystem(new MovementSystem());
|
||||
|
||||
// 创建玩家
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Transform(400, 300));
|
||||
player.addComponent(new Velocity(0, 0));
|
||||
player.addComponent(new Health(100));
|
||||
|
||||
// 创建敌人
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const enemy = this.createEntity(`Enemy_${i}`);
|
||||
enemy.addComponent(new Transform(
|
||||
Math.random() * 800,
|
||||
Math.random() * 600
|
||||
));
|
||||
enemy.addComponent(new Velocity(
|
||||
Math.random() * 100 - 50,
|
||||
Math.random() * 100 - 50
|
||||
));
|
||||
enemy.addComponent(new Health(50));
|
||||
}
|
||||
|
||||
// 设置事件监听
|
||||
this.eventSystem.on('player_died', () => {
|
||||
console.log('玩家死亡!');
|
||||
});
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log('游戏场景启动');
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
console.log('游戏场景卸载');
|
||||
this.eventSystem.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// 使用场景
|
||||
// 方式1:通过 SceneManager(推荐)
|
||||
import { Core, SceneManager } from '@esengine/ecs-framework';
|
||||
|
||||
Core.create({ debug: true });
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
sceneManager.setScene(new GameScene());
|
||||
|
||||
// 方式2:通过 WorldManager(高级用例)
|
||||
import { WorldManager } from '@esengine/ecs-framework';
|
||||
|
||||
const worldManager = Core.services.resolve(WorldManager);
|
||||
const world = worldManager.createWorld('game');
|
||||
world.createScene('main', new GameScene());
|
||||
world.setSceneActive('main', true);
|
||||
```
|
||||
|
||||
## 下一步
|
||||
|
||||
- 了解 [SceneManager](./scene-manager.md) - 适用于大多数游戏的简单场景管理
|
||||
- 了解 [WorldManager](./world-manager.md) - 适用于需要多世界隔离的高级场景
|
||||
|
||||
场景是 ECS 框架的核心容器,正确使用场景管理能让你的游戏架构更加清晰、模块化和易于维护。
|
||||
|
||||
823
docs/guide/serialization.md
Normal file
823
docs/guide/serialization.md
Normal file
@@ -0,0 +1,823 @@
|
||||
# 序列化系统
|
||||
|
||||
序列化系统提供了完整的场景、实体和组件数据持久化方案,支持全量序列化和增量序列化两种模式,适用于游戏存档、网络同步、场景编辑器、时间回溯等场景。
|
||||
|
||||
## 基本概念
|
||||
|
||||
序列化系统分为两个层次:
|
||||
|
||||
- **全量序列化**:序列化完整的场景状态,包括所有实体、组件和场景数据
|
||||
- **增量序列化**:只序列化相对于基础快照的变更部分,大幅减少数据量
|
||||
|
||||
### 支持的数据格式
|
||||
|
||||
- **JSON格式**:人类可读,便于调试和编辑
|
||||
- **Binary格式**:使用MessagePack,体积更小,性能更高
|
||||
|
||||
> **📢 v2.2.2 重要变更**
|
||||
>
|
||||
> 从 v2.2.2 开始,二进制序列化格式返回 `Uint8Array` 而非 Node.js 的 `Buffer`,以确保浏览器兼容性:
|
||||
> - `serialize({ format: 'binary' })` 返回 `string | Uint8Array`(原为 `string | Buffer`)
|
||||
> - `deserialize(data)` 接收 `string | Uint8Array`(原为 `string | Buffer`)
|
||||
> - `applyIncremental(data)` 接收 `IncrementalSnapshot | string | Uint8Array`(原为包含 `Buffer`)
|
||||
>
|
||||
> **迁移影响**:
|
||||
> - ✅ **运行时兼容**:Node.js 的 `Buffer` 继承自 `Uint8Array`,现有代码可直接运行
|
||||
> - ⚠️ **类型检查**:如果你的 TypeScript 代码中显式使用了 `Buffer` 类型,需要改为 `Uint8Array`
|
||||
> - ✅ **浏览器支持**:`Uint8Array` 是标准 JavaScript 类型,所有现代浏览器都支持
|
||||
|
||||
## 全量序列化
|
||||
|
||||
### 基础用法
|
||||
|
||||
#### 1. 标记可序列化组件
|
||||
|
||||
使用 `@Serializable` 和 `@Serialize` 装饰器标记需要序列化的组件和字段:
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent, Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSComponent('Player')
|
||||
@Serializable({ version: 1 })
|
||||
class PlayerComponent extends Component {
|
||||
@Serialize()
|
||||
public name: string = '';
|
||||
|
||||
@Serialize()
|
||||
public level: number = 1;
|
||||
|
||||
@Serialize()
|
||||
public experience: number = 0;
|
||||
|
||||
@Serialize()
|
||||
public position: { x: number; y: number } = { x: 0, y: 0 };
|
||||
|
||||
// 不使用 @Serialize() 的字段不会被序列化
|
||||
private tempData: any = null;
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 序列化场景
|
||||
|
||||
```typescript
|
||||
// JSON格式序列化
|
||||
const jsonData = scene.serialize({
|
||||
format: 'json',
|
||||
pretty: true // 美化输出
|
||||
});
|
||||
|
||||
// 保存到本地存储
|
||||
localStorage.setItem('gameSave', jsonData);
|
||||
|
||||
// Binary格式序列化(更小的体积)
|
||||
const binaryData = scene.serialize({
|
||||
format: 'binary'
|
||||
});
|
||||
|
||||
// 保存为文件(Node.js环境)
|
||||
// 注意:binaryData 是 Uint8Array 类型,Node.js 的 fs 可以直接写入
|
||||
fs.writeFileSync('save.bin', binaryData);
|
||||
```
|
||||
|
||||
#### 3. 反序列化场景
|
||||
|
||||
```typescript
|
||||
// 从JSON恢复
|
||||
const saveData = localStorage.getItem('gameSave');
|
||||
if (saveData) {
|
||||
scene.deserialize(saveData, {
|
||||
strategy: 'replace' // 替换当前场景内容
|
||||
});
|
||||
}
|
||||
|
||||
// 从Binary恢复
|
||||
const binaryData = fs.readFileSync('save.bin');
|
||||
scene.deserialize(binaryData, {
|
||||
strategy: 'merge' // 合并到现有场景
|
||||
});
|
||||
```
|
||||
|
||||
### 序列化选项
|
||||
|
||||
#### SerializationOptions
|
||||
|
||||
```typescript
|
||||
interface SceneSerializationOptions {
|
||||
// 指定要序列化的组件类型(可选)
|
||||
components?: ComponentType[];
|
||||
|
||||
// 序列化格式:'json' 或 'binary'
|
||||
format?: 'json' | 'binary';
|
||||
|
||||
// JSON美化输出
|
||||
pretty?: boolean;
|
||||
|
||||
// 包含元数据
|
||||
includeMetadata?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
示例:
|
||||
|
||||
```typescript
|
||||
// 只序列化特定组件类型
|
||||
const saveData = scene.serialize({
|
||||
format: 'json',
|
||||
components: [PlayerComponent, InventoryComponent],
|
||||
pretty: true,
|
||||
includeMetadata: true
|
||||
});
|
||||
```
|
||||
|
||||
#### DeserializationOptions
|
||||
|
||||
```typescript
|
||||
interface SceneDeserializationOptions {
|
||||
// 反序列化策略
|
||||
strategy?: 'merge' | 'replace';
|
||||
|
||||
// 组件类型注册表(可选,默认使用全局注册表)
|
||||
componentRegistry?: Map<string, ComponentType>;
|
||||
}
|
||||
```
|
||||
|
||||
### 高级装饰器
|
||||
|
||||
#### 字段序列化选项
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Advanced')
|
||||
@Serializable({ version: 1 })
|
||||
class AdvancedComponent extends Component {
|
||||
// 使用别名
|
||||
@Serialize({ alias: 'playerName' })
|
||||
public name: string = '';
|
||||
|
||||
// 自定义序列化器
|
||||
@Serialize({
|
||||
serializer: (value: Date) => value.toISOString(),
|
||||
deserializer: (value: string) => new Date(value)
|
||||
})
|
||||
public createdAt: Date = new Date();
|
||||
|
||||
// 忽略序列化
|
||||
@IgnoreSerialization()
|
||||
public cachedData: any = null;
|
||||
}
|
||||
```
|
||||
|
||||
#### 集合类型序列化
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Collections')
|
||||
@Serializable({ version: 1 })
|
||||
class CollectionsComponent extends Component {
|
||||
// Map序列化
|
||||
@SerializeAsMap()
|
||||
public inventory: Map<string, number> = new Map();
|
||||
|
||||
// Set序列化
|
||||
@SerializeAsSet()
|
||||
public acquiredSkills: Set<string> = new Set();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.inventory.set('gold', 100);
|
||||
this.inventory.set('silver', 50);
|
||||
this.acquiredSkills.add('attack');
|
||||
this.acquiredSkills.add('defense');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 场景自定义数据
|
||||
|
||||
除了实体和组件,还可以序列化场景级别的配置数据:
|
||||
|
||||
```typescript
|
||||
// 设置场景数据
|
||||
scene.sceneData.set('weather', 'rainy');
|
||||
scene.sceneData.set('difficulty', 'hard');
|
||||
scene.sceneData.set('checkpoint', { x: 100, y: 200 });
|
||||
|
||||
// 序列化时会自动包含场景数据
|
||||
const saveData = scene.serialize({ format: 'json' });
|
||||
|
||||
// 反序列化后场景数据会恢复
|
||||
scene.deserialize(saveData);
|
||||
console.log(scene.sceneData.get('weather')); // 'rainy'
|
||||
```
|
||||
|
||||
## 增量序列化
|
||||
|
||||
增量序列化只保存场景的变更部分,适用于网络同步、撤销/重做、时间回溯等需要频繁保存状态的场景。
|
||||
|
||||
### 基础用法
|
||||
|
||||
#### 1. 创建基础快照
|
||||
|
||||
```typescript
|
||||
// 在需要开始记录变更前创建基础快照
|
||||
scene.createIncrementalSnapshot();
|
||||
```
|
||||
|
||||
#### 2. 修改场景
|
||||
|
||||
```typescript
|
||||
// 添加实体
|
||||
const enemy = scene.createEntity('Enemy');
|
||||
enemy.addComponent(new PositionComponent(100, 200));
|
||||
enemy.addComponent(new HealthComponent(50));
|
||||
|
||||
// 修改组件
|
||||
const player = scene.findEntity('Player');
|
||||
const pos = player.getComponent(PositionComponent);
|
||||
pos.x = 300;
|
||||
pos.y = 400;
|
||||
|
||||
// 删除组件
|
||||
player.removeComponentByType(BuffComponent);
|
||||
|
||||
// 删除实体
|
||||
const oldEntity = scene.findEntity('ToDelete');
|
||||
oldEntity.destroy();
|
||||
|
||||
// 修改场景数据
|
||||
scene.sceneData.set('score', 1000);
|
||||
```
|
||||
|
||||
#### 3. 获取增量变更
|
||||
|
||||
```typescript
|
||||
// 获取相对于基础快照的所有变更
|
||||
const incremental = scene.serializeIncremental();
|
||||
|
||||
// 查看变更统计
|
||||
const stats = IncrementalSerializer.getIncrementalStats(incremental);
|
||||
console.log('总变更数:', stats.totalChanges);
|
||||
console.log('新增实体:', stats.addedEntities);
|
||||
console.log('删除实体:', stats.removedEntities);
|
||||
console.log('新增组件:', stats.addedComponents);
|
||||
console.log('更新组件:', stats.updatedComponents);
|
||||
```
|
||||
|
||||
#### 4. 序列化增量数据
|
||||
|
||||
```typescript
|
||||
// JSON格式(默认)
|
||||
const jsonData = IncrementalSerializer.serializeIncremental(incremental, {
|
||||
format: 'json'
|
||||
});
|
||||
|
||||
// 二进制格式(更小的体积,更高性能)
|
||||
const binaryData = IncrementalSerializer.serializeIncremental(incremental, {
|
||||
format: 'binary'
|
||||
});
|
||||
|
||||
// 美化JSON输出(便于调试)
|
||||
const prettyJson = IncrementalSerializer.serializeIncremental(incremental, {
|
||||
format: 'json',
|
||||
pretty: true
|
||||
});
|
||||
|
||||
// 发送或保存
|
||||
socket.send(binaryData); // 网络传输使用二进制
|
||||
localStorage.setItem('changes', jsonData); // 本地存储可用JSON
|
||||
```
|
||||
|
||||
#### 5. 应用增量变更
|
||||
|
||||
```typescript
|
||||
// 在另一个场景应用变更
|
||||
const otherScene = new Scene();
|
||||
|
||||
// 直接应用增量对象
|
||||
otherScene.applyIncremental(incremental);
|
||||
|
||||
// 从JSON字符串应用
|
||||
const jsonData = IncrementalSerializer.serializeIncremental(incremental, { format: 'json' });
|
||||
otherScene.applyIncremental(jsonData);
|
||||
|
||||
// 从二进制Uint8Array应用
|
||||
const binaryData = IncrementalSerializer.serializeIncremental(incremental, { format: 'binary' });
|
||||
otherScene.applyIncremental(binaryData);
|
||||
```
|
||||
|
||||
### 增量快照管理
|
||||
|
||||
#### 更新快照基准
|
||||
|
||||
在应用增量变更后,可以更新快照基准:
|
||||
|
||||
```typescript
|
||||
// 创建初始快照
|
||||
scene.createIncrementalSnapshot();
|
||||
|
||||
// 第一次修改
|
||||
entity.addComponent(new VelocityComponent(5, 0));
|
||||
const incremental1 = scene.serializeIncremental();
|
||||
|
||||
// 更新基准(将当前状态设为新的基准)
|
||||
scene.updateIncrementalSnapshot();
|
||||
|
||||
// 第二次修改(增量将基于更新后的基准)
|
||||
entity.getComponent(VelocityComponent).dx = 10;
|
||||
const incremental2 = scene.serializeIncremental();
|
||||
```
|
||||
|
||||
#### 清除快照
|
||||
|
||||
```typescript
|
||||
// 释放快照占用的内存
|
||||
scene.clearIncrementalSnapshot();
|
||||
|
||||
// 检查是否有快照
|
||||
if (scene.hasIncrementalSnapshot()) {
|
||||
console.log('存在增量快照');
|
||||
}
|
||||
```
|
||||
|
||||
### 增量序列化选项
|
||||
|
||||
```typescript
|
||||
interface IncrementalSerializationOptions {
|
||||
// 是否进行组件数据的深度对比
|
||||
// 默认true,设为false可提升性能但可能漏掉组件内部字段变更
|
||||
deepComponentComparison?: boolean;
|
||||
|
||||
// 是否跟踪场景数据变更
|
||||
// 默认true
|
||||
trackSceneData?: boolean;
|
||||
|
||||
// 是否压缩快照(使用JSON序列化)
|
||||
// 默认false
|
||||
compressSnapshot?: boolean;
|
||||
|
||||
// 序列化格式
|
||||
// 'json': JSON格式(可读性好,方便调试)
|
||||
// 'binary': MessagePack二进制格式(体积小,性能高)
|
||||
// 默认 'json'
|
||||
format?: 'json' | 'binary';
|
||||
|
||||
// 是否美化JSON输出(仅在format='json'时有效)
|
||||
// 默认false
|
||||
pretty?: boolean;
|
||||
}
|
||||
|
||||
// 使用选项
|
||||
scene.createIncrementalSnapshot({
|
||||
deepComponentComparison: true,
|
||||
trackSceneData: true
|
||||
});
|
||||
```
|
||||
|
||||
### 增量数据结构
|
||||
|
||||
增量快照包含以下变更类型:
|
||||
|
||||
```typescript
|
||||
interface IncrementalSnapshot {
|
||||
version: number; // 快照版本号
|
||||
timestamp: number; // 时间戳
|
||||
sceneName: string; // 场景名称
|
||||
baseVersion: number; // 基础版本号
|
||||
entityChanges: EntityChange[]; // 实体变更
|
||||
componentChanges: ComponentChange[]; // 组件变更
|
||||
sceneDataChanges: SceneDataChange[]; // 场景数据变更
|
||||
}
|
||||
|
||||
// 变更操作类型
|
||||
enum ChangeOperation {
|
||||
EntityAdded = 'entity_added',
|
||||
EntityRemoved = 'entity_removed',
|
||||
EntityUpdated = 'entity_updated',
|
||||
ComponentAdded = 'component_added',
|
||||
ComponentRemoved = 'component_removed',
|
||||
ComponentUpdated = 'component_updated',
|
||||
SceneDataUpdated = 'scene_data_updated'
|
||||
}
|
||||
```
|
||||
|
||||
## 版本迁移
|
||||
|
||||
当组件结构发生变化时,版本迁移系统可以自动升级旧版本的存档数据。
|
||||
|
||||
### 注册迁移函数
|
||||
|
||||
```typescript
|
||||
import { VersionMigrationManager } from '@esengine/ecs-framework';
|
||||
|
||||
// 假设 PlayerComponent v1 有 hp 字段
|
||||
// v2 改为 health 和 maxHealth 字段
|
||||
|
||||
// 注册从版本1到版本2的迁移
|
||||
VersionMigrationManager.registerComponentMigration(
|
||||
'Player',
|
||||
1, // 从版本
|
||||
2, // 到版本
|
||||
(data) => {
|
||||
// 迁移逻辑
|
||||
const newData = {
|
||||
...data,
|
||||
health: data.hp,
|
||||
maxHealth: data.hp,
|
||||
};
|
||||
delete newData.hp;
|
||||
return newData;
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### 使用迁移构建器
|
||||
|
||||
```typescript
|
||||
import { MigrationBuilder } from '@esengine/ecs-framework';
|
||||
|
||||
new MigrationBuilder()
|
||||
.forComponent('Player')
|
||||
.fromVersionToVersion(2, 3)
|
||||
.migrate((data) => {
|
||||
// 从版本2迁移到版本3
|
||||
data.experience = data.exp || 0;
|
||||
delete data.exp;
|
||||
return data;
|
||||
});
|
||||
```
|
||||
|
||||
### 场景级迁移
|
||||
|
||||
```typescript
|
||||
// 注册场景级迁移
|
||||
VersionMigrationManager.registerSceneMigration(
|
||||
1, // 从版本
|
||||
2, // 到版本
|
||||
(scene) => {
|
||||
// 迁移场景结构
|
||||
scene.metadata = {
|
||||
...scene.metadata,
|
||||
migratedFrom: 1
|
||||
};
|
||||
return scene;
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### 检查迁移路径
|
||||
|
||||
```typescript
|
||||
// 检查是否可以迁移
|
||||
const canMigrate = VersionMigrationManager.canMigrateComponent(
|
||||
'Player',
|
||||
1, // 从版本
|
||||
3 // 到版本
|
||||
);
|
||||
|
||||
if (canMigrate) {
|
||||
// 可以安全迁移
|
||||
scene.deserialize(oldSaveData);
|
||||
}
|
||||
|
||||
// 获取迁移路径
|
||||
const path = VersionMigrationManager.getComponentMigrationPath('Player');
|
||||
console.log('可用迁移版本:', path); // [1, 2, 3]
|
||||
```
|
||||
|
||||
## 使用场景
|
||||
|
||||
### 游戏存档系统
|
||||
|
||||
```typescript
|
||||
class SaveSystem {
|
||||
private static SAVE_KEY = 'game_save';
|
||||
|
||||
// 保存游戏
|
||||
public static saveGame(scene: Scene): void {
|
||||
const saveData = scene.serialize({
|
||||
format: 'json',
|
||||
pretty: false
|
||||
});
|
||||
|
||||
localStorage.setItem(this.SAVE_KEY, saveData);
|
||||
console.log('游戏已保存');
|
||||
}
|
||||
|
||||
// 加载游戏
|
||||
public static loadGame(scene: Scene): boolean {
|
||||
const saveData = localStorage.getItem(this.SAVE_KEY);
|
||||
if (saveData) {
|
||||
scene.deserialize(saveData, {
|
||||
strategy: 'replace'
|
||||
});
|
||||
console.log('游戏已加载');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否有存档
|
||||
public static hasSave(): boolean {
|
||||
return localStorage.getItem(this.SAVE_KEY) !== null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 网络同步
|
||||
|
||||
```typescript
|
||||
class NetworkSync {
|
||||
private baseSnapshot?: any;
|
||||
private syncInterval: number = 100; // 100ms同步一次
|
||||
|
||||
constructor(private scene: Scene, private socket: WebSocket) {
|
||||
this.setupSync();
|
||||
}
|
||||
|
||||
private setupSync(): void {
|
||||
// 创建基础快照
|
||||
this.scene.createIncrementalSnapshot();
|
||||
|
||||
// 定期发送增量
|
||||
setInterval(() => {
|
||||
this.sendIncremental();
|
||||
}, this.syncInterval);
|
||||
|
||||
// 接收远程增量
|
||||
this.socket.onmessage = (event) => {
|
||||
this.receiveIncremental(event.data);
|
||||
};
|
||||
}
|
||||
|
||||
private sendIncremental(): void {
|
||||
const incremental = this.scene.serializeIncremental();
|
||||
const stats = IncrementalSerializer.getIncrementalStats(incremental);
|
||||
|
||||
// 只在有变更时发送
|
||||
if (stats.totalChanges > 0) {
|
||||
// 使用二进制格式减少网络传输量
|
||||
const binaryData = IncrementalSerializer.serializeIncremental(incremental, {
|
||||
format: 'binary'
|
||||
});
|
||||
this.socket.send(binaryData);
|
||||
|
||||
// 更新基准
|
||||
this.scene.updateIncrementalSnapshot();
|
||||
}
|
||||
}
|
||||
|
||||
private receiveIncremental(data: ArrayBuffer): void {
|
||||
// 直接应用二进制数据(ArrayBuffer 转 Uint8Array)
|
||||
const uint8Array = new Uint8Array(data);
|
||||
this.scene.applyIncremental(uint8Array);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 撤销/重做系统
|
||||
|
||||
```typescript
|
||||
class UndoRedoSystem {
|
||||
private history: IncrementalSnapshot[] = [];
|
||||
private currentIndex: number = -1;
|
||||
private maxHistory: number = 50;
|
||||
|
||||
constructor(private scene: Scene) {
|
||||
// 创建初始快照
|
||||
this.scene.createIncrementalSnapshot();
|
||||
this.saveState('Initial');
|
||||
}
|
||||
|
||||
// 保存当前状态
|
||||
public saveState(label: string): void {
|
||||
const incremental = this.scene.serializeIncremental();
|
||||
|
||||
// 删除当前位置之后的历史
|
||||
this.history = this.history.slice(0, this.currentIndex + 1);
|
||||
|
||||
// 添加新状态
|
||||
this.history.push(incremental);
|
||||
this.currentIndex++;
|
||||
|
||||
// 限制历史记录数量
|
||||
if (this.history.length > this.maxHistory) {
|
||||
this.history.shift();
|
||||
this.currentIndex--;
|
||||
}
|
||||
|
||||
// 更新快照基准
|
||||
this.scene.updateIncrementalSnapshot();
|
||||
}
|
||||
|
||||
// 撤销
|
||||
public undo(): boolean {
|
||||
if (this.currentIndex > 0) {
|
||||
this.currentIndex--;
|
||||
const incremental = this.history[this.currentIndex];
|
||||
this.scene.applyIncremental(incremental);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 重做
|
||||
public redo(): boolean {
|
||||
if (this.currentIndex < this.history.length - 1) {
|
||||
this.currentIndex++;
|
||||
const incremental = this.history[this.currentIndex];
|
||||
this.scene.applyIncremental(incremental);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public canUndo(): boolean {
|
||||
return this.currentIndex > 0;
|
||||
}
|
||||
|
||||
public canRedo(): boolean {
|
||||
return this.currentIndex < this.history.length - 1;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 关卡编辑器
|
||||
|
||||
```typescript
|
||||
class LevelEditor {
|
||||
// 导出关卡
|
||||
public exportLevel(scene: Scene, filename: string): void {
|
||||
const levelData = scene.serialize({
|
||||
format: 'json',
|
||||
pretty: true,
|
||||
includeMetadata: true
|
||||
});
|
||||
|
||||
// 浏览器环境
|
||||
const blob = new Blob([levelData], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// 导入关卡
|
||||
public importLevel(scene: Scene, fileContent: string): void {
|
||||
scene.deserialize(fileContent, {
|
||||
strategy: 'replace'
|
||||
});
|
||||
}
|
||||
|
||||
// 验证关卡数据
|
||||
public validateLevel(saveData: string): boolean {
|
||||
const validation = SceneSerializer.validate(saveData);
|
||||
if (!validation.valid) {
|
||||
console.error('关卡数据无效:', validation.errors);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 获取关卡信息(不完全反序列化)
|
||||
public getLevelInfo(saveData: string): any {
|
||||
const info = SceneSerializer.getInfo(saveData);
|
||||
return info;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 性能优化建议
|
||||
|
||||
### 1. 选择合适的格式
|
||||
|
||||
- **开发阶段**:使用JSON格式,便于调试和查看
|
||||
- **生产环境**:使用Binary格式,减少30-50%的数据大小
|
||||
|
||||
### 2. 按需序列化
|
||||
|
||||
```typescript
|
||||
// 只序列化需要持久化的组件
|
||||
const saveData = scene.serialize({
|
||||
format: 'binary',
|
||||
components: [PlayerComponent, InventoryComponent, QuestComponent]
|
||||
});
|
||||
```
|
||||
|
||||
### 3. 增量序列化优化
|
||||
|
||||
```typescript
|
||||
// 对于高频同步,关闭深度对比以提升性能
|
||||
scene.createIncrementalSnapshot({
|
||||
deepComponentComparison: false // 只检测组件的添加/删除
|
||||
});
|
||||
```
|
||||
|
||||
### 4. 批量操作
|
||||
|
||||
```typescript
|
||||
// 批量修改后再序列化
|
||||
scene.entities.buffer.forEach(entity => {
|
||||
// 批量修改
|
||||
});
|
||||
|
||||
// 一次性序列化所有变更
|
||||
const incremental = scene.serializeIncremental();
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 明确序列化字段
|
||||
|
||||
```typescript
|
||||
// 明确标记需要序列化的字段
|
||||
@ECSComponent('Player')
|
||||
@Serializable({ version: 1 })
|
||||
class PlayerComponent extends Component {
|
||||
@Serialize()
|
||||
public name: string = '';
|
||||
|
||||
@Serialize()
|
||||
public level: number = 1;
|
||||
|
||||
// 运行时数据不序列化
|
||||
private _cachedSprite: any = null;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 使用版本控制
|
||||
|
||||
```typescript
|
||||
// 为组件指定版本
|
||||
@Serializable({ version: 2 })
|
||||
class PlayerComponent extends Component {
|
||||
// 版本2的字段
|
||||
}
|
||||
|
||||
// 注册迁移函数确保兼容性
|
||||
VersionMigrationManager.registerComponentMigration('Player', 1, 2, migrateV1ToV2);
|
||||
```
|
||||
|
||||
### 3. 避免循环引用
|
||||
|
||||
```typescript
|
||||
// 不要在组件中直接引用其他实体
|
||||
@ECSComponent('Follower')
|
||||
@Serializable({ version: 1 })
|
||||
class FollowerComponent extends Component {
|
||||
// 存储实体ID而不是实体引用
|
||||
@Serialize()
|
||||
public targetId: number = 0;
|
||||
|
||||
// 通过场景查找目标实体
|
||||
public getTarget(scene: Scene): Entity | null {
|
||||
return scene.entities.findEntityById(this.targetId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 压缩大数据
|
||||
|
||||
```typescript
|
||||
// 对于大型数据结构,使用自定义序列化
|
||||
@ECSComponent('LargeData')
|
||||
@Serializable({ version: 1 })
|
||||
class LargeDataComponent extends Component {
|
||||
@Serialize({
|
||||
serializer: (data: LargeObject) => compressData(data),
|
||||
deserializer: (data: CompressedData) => decompressData(data)
|
||||
})
|
||||
public data: LargeObject;
|
||||
}
|
||||
```
|
||||
|
||||
## API参考
|
||||
|
||||
### 全量序列化API
|
||||
|
||||
- [`Scene.serialize()`](/api/classes/Scene#serialize) - 序列化场景
|
||||
- [`Scene.deserialize()`](/api/classes/Scene#deserialize) - 反序列化场景
|
||||
- [`SceneSerializer`](/api/classes/SceneSerializer) - 场景序列化器
|
||||
- [`ComponentSerializer`](/api/classes/ComponentSerializer) - 组件序列化器
|
||||
|
||||
### 增量序列化API
|
||||
|
||||
- [`Scene.createIncrementalSnapshot()`](/api/classes/Scene#createincrementalsnapshot) - 创建基础快照
|
||||
- [`Scene.serializeIncremental()`](/api/classes/Scene#serializeincremental) - 获取增量变更
|
||||
- [`Scene.applyIncremental()`](/api/classes/Scene#applyincremental) - 应用增量变更(支持IncrementalSnapshot对象、JSON字符串或二进制Uint8Array)
|
||||
- [`Scene.updateIncrementalSnapshot()`](/api/classes/Scene#updateincrementalsnapshot) - 更新快照基准
|
||||
- [`Scene.clearIncrementalSnapshot()`](/api/classes/Scene#clearincrementalsnapshot) - 清除快照
|
||||
- [`Scene.hasIncrementalSnapshot()`](/api/classes/Scene#hasincrementalsnapshot) - 检查是否有快照
|
||||
- [`IncrementalSerializer`](/api/classes/IncrementalSerializer) - 增量序列化器
|
||||
- [`IncrementalSnapshot`](/api/interfaces/IncrementalSnapshot) - 增量快照接口
|
||||
- [`IncrementalSerializationOptions`](/api/interfaces/IncrementalSerializationOptions) - 增量序列化选项
|
||||
- [`IncrementalSerializationFormat`](/api/type-aliases/IncrementalSerializationFormat) - 序列化格式类型
|
||||
|
||||
### 版本迁移API
|
||||
|
||||
- [`VersionMigrationManager`](/api/classes/VersionMigrationManager) - 版本迁移管理器
|
||||
- `VersionMigrationManager.registerComponentMigration()` - 注册组件迁移
|
||||
- `VersionMigrationManager.registerSceneMigration()` - 注册场景迁移
|
||||
- `VersionMigrationManager.canMigrateComponent()` - 检查是否可以迁移
|
||||
- `VersionMigrationManager.getComponentMigrationPath()` - 获取迁移路径
|
||||
|
||||
序列化系统是构建完整游戏的重要基础设施,合理使用可以实现强大的功能,如存档系统、网络同步、关卡编辑器等。
|
||||
589
docs/guide/service-container.md
Normal file
589
docs/guide/service-container.md
Normal file
@@ -0,0 +1,589 @@
|
||||
# 服务容器
|
||||
|
||||
服务容器(ServiceContainer)是 ECS Framework 的依赖注入容器,负责管理框架中所有服务的注册、解析和生命周期。通过服务容器,你可以实现松耦合的架构设计,提高代码的可测试性和可维护性。
|
||||
|
||||
## 概述
|
||||
|
||||
### 什么是服务容器
|
||||
|
||||
服务容器是一个轻量级的依赖注入(DI)容器,它提供了:
|
||||
|
||||
- **服务注册**: 将服务类型注册到容器中
|
||||
- **服务解析**: 从容器中获取服务实例
|
||||
- **生命周期管理**: 自动管理服务实例的创建和销毁
|
||||
- **依赖注入**: 自动解析服务之间的依赖关系
|
||||
|
||||
### 核心概念
|
||||
|
||||
#### 服务(Service)
|
||||
|
||||
服务是实现了 `IService` 接口的类,必须提供 `dispose()` 方法用于资源清理:
|
||||
|
||||
```typescript
|
||||
import { IService } from '@esengine/ecs-framework';
|
||||
|
||||
class MyService implements IService {
|
||||
constructor() {
|
||||
// 初始化逻辑
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
// 清理资源
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 生命周期
|
||||
|
||||
服务容器支持两种生命周期:
|
||||
|
||||
- **Singleton(单例)**: 整个应用程序生命周期内只有一个实例,所有解析请求返回同一个实例
|
||||
- **Transient(瞬时)**: 每次解析都创建新的实例
|
||||
|
||||
## 基础使用
|
||||
|
||||
### 访问服务容器
|
||||
|
||||
Core 类内置了服务容器,可以通过 `Core.services` 访问:
|
||||
|
||||
```typescript
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始化Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 访问服务容器
|
||||
const container = Core.services;
|
||||
```
|
||||
|
||||
### 注册服务
|
||||
|
||||
#### 注册单例服务
|
||||
|
||||
单例服务在首次解析时创建,之后所有解析请求都返回同一个实例:
|
||||
|
||||
```typescript
|
||||
class DataService implements IService {
|
||||
private data: Map<string, any> = new Map();
|
||||
|
||||
getData(key: string) {
|
||||
return this.data.get(key);
|
||||
}
|
||||
|
||||
setData(key: string, value: any) {
|
||||
this.data.set(key, value);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.data.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// 注册单例服务
|
||||
Core.services.registerSingleton(DataService);
|
||||
```
|
||||
|
||||
#### 注册瞬时服务
|
||||
|
||||
瞬时服务每次解析都创建新实例,适用于无状态或短生命周期的服务:
|
||||
|
||||
```typescript
|
||||
class CommandService implements IService {
|
||||
execute(command: string) {
|
||||
console.log(`Executing: ${command}`);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
// 清理资源
|
||||
}
|
||||
}
|
||||
|
||||
// 注册瞬时服务
|
||||
Core.services.registerTransient(CommandService);
|
||||
```
|
||||
|
||||
#### 注册服务实例
|
||||
|
||||
直接注册已创建的实例,自动视为单例:
|
||||
|
||||
```typescript
|
||||
const config = new ConfigService();
|
||||
config.load('./config.json');
|
||||
|
||||
// 注册实例
|
||||
Core.services.registerInstance(ConfigService, config);
|
||||
```
|
||||
|
||||
#### 使用工厂函数注册
|
||||
|
||||
工厂函数允许你在创建服务时执行自定义逻辑:
|
||||
|
||||
```typescript
|
||||
Core.services.registerSingleton(LoggerService, (container) => {
|
||||
const logger = new LoggerService();
|
||||
logger.setLevel('debug');
|
||||
return logger;
|
||||
});
|
||||
```
|
||||
|
||||
### 解析服务
|
||||
|
||||
#### resolve 方法
|
||||
|
||||
解析服务实例,如果服务未注册会抛出异常:
|
||||
|
||||
```typescript
|
||||
// 解析服务
|
||||
const dataService = Core.services.resolve(DataService);
|
||||
dataService.setData('player', { name: 'Alice', score: 100 });
|
||||
|
||||
// 单例服务,多次解析返回同一个实例
|
||||
const same = Core.services.resolve(DataService);
|
||||
console.log(same === dataService); // true
|
||||
```
|
||||
|
||||
#### tryResolve 方法
|
||||
|
||||
尝试解析服务,如果未注册返回 null 而不抛出异常:
|
||||
|
||||
```typescript
|
||||
const optional = Core.services.tryResolve(OptionalService);
|
||||
if (optional) {
|
||||
optional.doSomething();
|
||||
}
|
||||
```
|
||||
|
||||
#### isRegistered 方法
|
||||
|
||||
检查服务是否已注册:
|
||||
|
||||
```typescript
|
||||
if (Core.services.isRegistered(DataService)) {
|
||||
const service = Core.services.resolve(DataService);
|
||||
}
|
||||
```
|
||||
|
||||
## 内置服务
|
||||
|
||||
Core 在初始化时自动注册了以下内置服务:
|
||||
|
||||
### TimerManager
|
||||
|
||||
定时器管理器,负责管理所有游戏定时器:
|
||||
|
||||
```typescript
|
||||
const timerManager = Core.services.resolve(TimerManager);
|
||||
|
||||
// 创建定时器
|
||||
timerManager.schedule(1.0, false, null, (timer) => {
|
||||
console.log('1秒后执行');
|
||||
});
|
||||
```
|
||||
|
||||
### PerformanceMonitor
|
||||
|
||||
性能监控器,监控游戏性能并提供优化建议:
|
||||
|
||||
```typescript
|
||||
const monitor = Core.services.resolve(PerformanceMonitor);
|
||||
|
||||
// 启用性能监控
|
||||
monitor.enable();
|
||||
|
||||
// 获取性能数据
|
||||
const fps = monitor.getFPS();
|
||||
```
|
||||
|
||||
### SceneManager
|
||||
|
||||
场景管理器,管理单场景应用的场景生命周期:
|
||||
|
||||
```typescript
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
|
||||
// 设置当前场景
|
||||
sceneManager.setScene(new GameScene());
|
||||
|
||||
// 获取当前场景
|
||||
const currentScene = sceneManager.currentScene;
|
||||
|
||||
// 延迟切换场景
|
||||
sceneManager.loadScene(new MenuScene());
|
||||
|
||||
// 更新场景
|
||||
sceneManager.update();
|
||||
```
|
||||
|
||||
### WorldManager
|
||||
|
||||
世界管理器,管理多个独立的 World 实例(高级用例):
|
||||
|
||||
```typescript
|
||||
const worldManager = Core.services.resolve(WorldManager);
|
||||
|
||||
// 创建独立的游戏世界
|
||||
const gameWorld = worldManager.createWorld('game_room_001', {
|
||||
name: 'GameRoom',
|
||||
maxScenes: 5
|
||||
});
|
||||
|
||||
// 在World中创建场景
|
||||
const scene = gameWorld.createScene('battle', new BattleScene());
|
||||
gameWorld.setSceneActive('battle', true);
|
||||
|
||||
// 更新所有World
|
||||
worldManager.updateAll();
|
||||
```
|
||||
|
||||
**适用场景**:
|
||||
- SceneManager: 适用于 95% 的游戏(单人游戏、简单多人游戏)
|
||||
- WorldManager: 适用于 MMO 服务器、游戏房间系统等需要完全隔离的多世界应用
|
||||
|
||||
### PoolManager
|
||||
|
||||
对象池管理器,管理所有对象池:
|
||||
|
||||
```typescript
|
||||
const poolManager = Core.services.resolve(PoolManager);
|
||||
|
||||
// 创建对象池
|
||||
const bulletPool = poolManager.createPool('bullets', () => new Bullet(), 100);
|
||||
```
|
||||
|
||||
### PluginManager
|
||||
|
||||
插件管理器,管理插件的安装和卸载:
|
||||
|
||||
```typescript
|
||||
const pluginManager = Core.services.resolve(PluginManager);
|
||||
|
||||
// 获取所有已安装的插件
|
||||
const plugins = pluginManager.getAllPlugins();
|
||||
```
|
||||
|
||||
## 依赖注入
|
||||
|
||||
ECS Framework 提供了装饰器来简化依赖注入。
|
||||
|
||||
### @Injectable 装饰器
|
||||
|
||||
标记类为可注入的服务:
|
||||
|
||||
```typescript
|
||||
import { Injectable, IService } from '@esengine/ecs-framework';
|
||||
|
||||
@Injectable()
|
||||
class GameService implements IService {
|
||||
constructor() {
|
||||
console.log('GameService created');
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
console.log('GameService disposed');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### @Inject 装饰器
|
||||
|
||||
在构造函数中注入依赖:
|
||||
|
||||
```typescript
|
||||
import { Injectable, Inject, IService } from '@esengine/ecs-framework';
|
||||
|
||||
@Injectable()
|
||||
class PlayerService implements IService {
|
||||
constructor(
|
||||
@Inject(DataService) private data: DataService,
|
||||
@Inject(GameService) private game: GameService
|
||||
) {
|
||||
// data 和 game 会自动从容器中解析
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
// 清理资源
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 注册可注入服务
|
||||
|
||||
使用 `registerInjectable` 自动处理依赖注入:
|
||||
|
||||
```typescript
|
||||
import { registerInjectable } from '@esengine/ecs-framework';
|
||||
|
||||
// 注册服务(会自动解析@Inject依赖)
|
||||
registerInjectable(Core.services, PlayerService);
|
||||
|
||||
// 解析时会自动注入依赖
|
||||
const player = Core.services.resolve(PlayerService);
|
||||
```
|
||||
|
||||
### @Updatable 装饰器
|
||||
|
||||
标记服务为可更新的,使其在每帧自动被调用:
|
||||
|
||||
```typescript
|
||||
import { Injectable, Updatable, IService, IUpdatable } from '@esengine/ecs-framework';
|
||||
|
||||
@Injectable()
|
||||
@Updatable() // 默认优先级为0
|
||||
class PhysicsService implements IService, IUpdatable {
|
||||
update(deltaTime?: number): void {
|
||||
// 每帧更新物理模拟
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
// 清理资源
|
||||
}
|
||||
}
|
||||
|
||||
// 指定更新优先级(数值越小越先执行)
|
||||
@Injectable()
|
||||
@Updatable(10)
|
||||
class RenderService implements IService, IUpdatable {
|
||||
update(deltaTime?: number): void {
|
||||
// 每帧渲染
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
// 清理资源
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
使用 `@Updatable` 装饰器的服务会被 Core 自动调用,无需手动管理:
|
||||
|
||||
```typescript
|
||||
// Core.update() 会自动调用所有@Updatable服务的update方法
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // 自动更新所有可更新服务
|
||||
}
|
||||
```
|
||||
|
||||
## 自定义服务
|
||||
|
||||
### 创建自定义服务
|
||||
|
||||
实现 `IService` 接口并注册到容器:
|
||||
|
||||
```typescript
|
||||
import { IService } from '@esengine/ecs-framework';
|
||||
|
||||
class AudioService implements IService {
|
||||
private sounds: Map<string, HTMLAudioElement> = new Map();
|
||||
|
||||
play(soundId: string) {
|
||||
const sound = this.sounds.get(soundId);
|
||||
if (sound) {
|
||||
sound.play();
|
||||
}
|
||||
}
|
||||
|
||||
load(soundId: string, url: string) {
|
||||
const audio = new Audio(url);
|
||||
this.sounds.set(soundId, audio);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
// 停止所有音效并清理
|
||||
for (const sound of this.sounds.values()) {
|
||||
sound.pause();
|
||||
sound.src = '';
|
||||
}
|
||||
this.sounds.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// 注册自定义服务
|
||||
Core.services.registerSingleton(AudioService);
|
||||
|
||||
// 使用服务
|
||||
const audio = Core.services.resolve(AudioService);
|
||||
audio.load('jump', '/sounds/jump.mp3');
|
||||
audio.play('jump');
|
||||
```
|
||||
|
||||
### 服务间依赖
|
||||
|
||||
服务可以依赖其他服务:
|
||||
|
||||
```typescript
|
||||
@Injectable()
|
||||
class ConfigService implements IService {
|
||||
private config: any = {};
|
||||
|
||||
get(key: string) {
|
||||
return this.config[key];
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.config = {};
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
class NetworkService implements IService {
|
||||
constructor(
|
||||
@Inject(ConfigService) private config: ConfigService
|
||||
) {
|
||||
// 使用配置服务
|
||||
const apiUrl = this.config.get('apiUrl');
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
// 清理网络连接
|
||||
}
|
||||
}
|
||||
|
||||
// 注册服务(按依赖顺序)
|
||||
registerInjectable(Core.services, ConfigService);
|
||||
registerInjectable(Core.services, NetworkService);
|
||||
```
|
||||
|
||||
## 高级用法
|
||||
|
||||
### 服务替换(测试)
|
||||
|
||||
在测试中替换真实服务为模拟服务:
|
||||
|
||||
```typescript
|
||||
// 测试代码
|
||||
class MockDataService implements IService {
|
||||
getData(key: string) {
|
||||
return 'mock data';
|
||||
}
|
||||
|
||||
dispose(): void {}
|
||||
}
|
||||
|
||||
// 注册模拟服务(用于测试)
|
||||
Core.services.registerInstance(DataService, new MockDataService());
|
||||
```
|
||||
|
||||
### 循环依赖检测
|
||||
|
||||
服务容器会自动检测循环依赖:
|
||||
|
||||
```typescript
|
||||
// A 依赖 B,B 依赖 A
|
||||
@Injectable()
|
||||
class ServiceA implements IService {
|
||||
constructor(@Inject(ServiceB) b: ServiceB) {}
|
||||
dispose(): void {}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
class ServiceB implements IService {
|
||||
constructor(@Inject(ServiceA) a: ServiceA) {}
|
||||
dispose(): void {}
|
||||
}
|
||||
|
||||
// 解析时会抛出错误: Circular dependency detected: ServiceA -> ServiceB -> ServiceA
|
||||
```
|
||||
|
||||
### 获取所有服务
|
||||
|
||||
```typescript
|
||||
// 获取所有已注册的服务类型
|
||||
const types = Core.services.getRegisteredServices();
|
||||
|
||||
// 获取所有已实例化的服务实例
|
||||
const instances = Core.services.getAll();
|
||||
```
|
||||
|
||||
### 服务清理
|
||||
|
||||
```typescript
|
||||
// 注销单个服务
|
||||
Core.services.unregister(MyService);
|
||||
|
||||
// 清空所有服务(会调用每个服务的dispose方法)
|
||||
Core.services.clear();
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 服务命名
|
||||
|
||||
服务类名应该以 `Service` 或 `Manager` 结尾,清晰表达其职责:
|
||||
|
||||
```typescript
|
||||
class PlayerService implements IService {}
|
||||
class AudioManager implements IService {}
|
||||
class NetworkService implements IService {}
|
||||
```
|
||||
|
||||
### 资源清理
|
||||
|
||||
始终在 `dispose()` 方法中清理资源:
|
||||
|
||||
```typescript
|
||||
class ResourceService implements IService {
|
||||
private resources: Map<string, Resource> = new Map();
|
||||
|
||||
dispose(): void {
|
||||
// 释放所有资源
|
||||
for (const resource of this.resources.values()) {
|
||||
resource.release();
|
||||
}
|
||||
this.resources.clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 避免过度使用
|
||||
|
||||
不要把所有类都注册为服务,服务应该是:
|
||||
|
||||
- 全局单例或需要共享状态
|
||||
- 需要在多处使用
|
||||
- 生命周期需要管理
|
||||
- 需要依赖注入
|
||||
|
||||
对于简单的工具类或数据类,直接创建实例即可。
|
||||
|
||||
### 依赖方向
|
||||
|
||||
保持清晰的依赖方向,避免循环依赖:
|
||||
|
||||
```
|
||||
高层服务 -> 中层服务 -> 底层服务
|
||||
GameLogic -> DataService -> ConfigService
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 服务未注册错误
|
||||
|
||||
**问题**: `Error: Service MyService is not registered`
|
||||
|
||||
**解决**:
|
||||
```typescript
|
||||
// 确保服务已注册
|
||||
Core.services.registerSingleton(MyService);
|
||||
|
||||
// 或者使用tryResolve
|
||||
const service = Core.services.tryResolve(MyService);
|
||||
if (!service) {
|
||||
console.log('Service not found');
|
||||
}
|
||||
```
|
||||
|
||||
### 循环依赖错误
|
||||
|
||||
**问题**: `Circular dependency detected`
|
||||
|
||||
**解决**: 重新设计服务依赖关系,引入中间服务或使用事件系统解耦。
|
||||
|
||||
### 何时使用单例 vs 瞬时
|
||||
|
||||
- **单例**: 管理器类、配置、缓存、状态管理
|
||||
- **瞬时**: 命令对象、请求处理器、临时任务
|
||||
|
||||
## 相关链接
|
||||
|
||||
- [插件系统](./plugin-system.md) - 使用服务容器注册插件服务
|
||||
- [快速开始](./getting-started.md) - Core 初始化和基础使用
|
||||
- [系统架构](./system.md) - 在系统中使用服务
|
||||
@@ -354,14 +354,18 @@ class PerformanceSystem extends EntitySystem {
|
||||
|
||||
### 添加系统到场景
|
||||
|
||||
框架提供了两种方式添加系统:传入实例或传入类型(自动依赖注入)。
|
||||
|
||||
```typescript
|
||||
// 在场景子类中添加系统
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 添加系统
|
||||
// 方式1:传入实例
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
this.addSystem(new PhysicsSystem());
|
||||
|
||||
// 方式2:传入类型(自动依赖注入)
|
||||
this.addEntityProcessor(PhysicsSystem);
|
||||
|
||||
// 设置系统更新顺序
|
||||
const movementSystem = this.getSystem(MovementSystem);
|
||||
@@ -372,6 +376,48 @@ class GameScene extends Scene {
|
||||
}
|
||||
```
|
||||
|
||||
### 系统依赖注入
|
||||
|
||||
系统实现了 `IService` 接口,支持通过依赖注入获取其他服务或系统:
|
||||
|
||||
```typescript
|
||||
import { ECSSystem, Injectable, Inject } from '@esengine/ecs-framework';
|
||||
|
||||
@Injectable()
|
||||
@ECSSystem('Physics')
|
||||
class PhysicsSystem extends EntitySystem {
|
||||
constructor(
|
||||
@Inject(CollisionService) private collision: CollisionService
|
||||
) {
|
||||
super(Matcher.all(Transform, RigidBody));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 使用注入的服务
|
||||
this.collision.detectCollisions(entities);
|
||||
}
|
||||
|
||||
// 实现 IService 接口的 dispose 方法
|
||||
public dispose(): void {
|
||||
// 清理资源
|
||||
}
|
||||
}
|
||||
|
||||
// 使用时传入类型即可,框架会自动注入依赖
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 自动依赖注入
|
||||
this.addEntityProcessor(PhysicsSystem);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
注意事项:
|
||||
- 使用 `@Injectable()` 装饰器标记需要依赖注入的系统
|
||||
- 在构造函数参数中使用 `@Inject()` 装饰器声明依赖
|
||||
- 系统必须实现 `dispose()` 方法(IService 接口要求)
|
||||
- 使用 `addEntityProcessor(类型)` 而不是 `addSystem(new 类型())` 来启用依赖注入
|
||||
|
||||
### 系统更新顺序
|
||||
|
||||
```typescript
|
||||
|
||||
761
docs/guide/world-manager.md
Normal file
761
docs/guide/world-manager.md
Normal file
@@ -0,0 +1,761 @@
|
||||
# WorldManager
|
||||
|
||||
WorldManager 是 ECS Framework 提供的高级世界管理器,用于管理多个完全隔离的游戏世界(World)。每个 World 都是独立的 ECS 环境,可以包含多个场景。
|
||||
|
||||
## 适用场景
|
||||
|
||||
WorldManager 适合以下高级场景:
|
||||
- MMO 游戏服务器的多房间管理
|
||||
- 游戏大厅系统(每个游戏房间完全隔离)
|
||||
- 服务器端的多游戏实例
|
||||
- 需要完全隔离的多个游戏环境
|
||||
- 需要同时运行多个独立世界的应用
|
||||
|
||||
## 特点
|
||||
|
||||
- 多 World 管理,每个 World 完全独立
|
||||
- 每个 World 可以包含多个 Scene
|
||||
- 支持 World 的激活/停用
|
||||
- 自动清理空 World
|
||||
- World 级别的全局系统
|
||||
- 批量操作和查询
|
||||
|
||||
## 基本使用
|
||||
|
||||
### 初始化
|
||||
|
||||
WorldManager 是 Core 的内置服务,通过服务容器获取:
|
||||
|
||||
```typescript
|
||||
import { Core, WorldManager } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始化 Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 从服务容器获取 WorldManager(Core 已自动创建并注册)
|
||||
const worldManager = Core.services.resolve(WorldManager);
|
||||
```
|
||||
|
||||
### 创建 World
|
||||
|
||||
```typescript
|
||||
// 创建游戏房间 World
|
||||
const room1 = worldManager.createWorld('room_001', {
|
||||
name: 'GameRoom_001',
|
||||
maxScenes: 5,
|
||||
debug: true
|
||||
});
|
||||
|
||||
// 激活 World
|
||||
worldManager.setWorldActive('room_001', true);
|
||||
|
||||
// 创建更多房间
|
||||
const room2 = worldManager.createWorld('room_002', {
|
||||
name: 'GameRoom_002',
|
||||
maxScenes: 5
|
||||
});
|
||||
|
||||
worldManager.setWorldActive('room_002', true);
|
||||
```
|
||||
|
||||
### 游戏循环
|
||||
|
||||
在游戏循环中更新所有活跃的 World:
|
||||
|
||||
```typescript
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // 更新全局服务
|
||||
worldManager.updateAll(); // 更新所有活跃的 World
|
||||
}
|
||||
|
||||
// 启动游戏循环
|
||||
let lastTime = 0;
|
||||
setInterval(() => {
|
||||
const currentTime = Date.now();
|
||||
const deltaTime = (currentTime - lastTime) / 1000;
|
||||
lastTime = currentTime;
|
||||
|
||||
gameLoop(deltaTime);
|
||||
}, 16); // 60 FPS
|
||||
```
|
||||
|
||||
## World 管理
|
||||
|
||||
### 创建 World
|
||||
|
||||
```typescript
|
||||
// 基本创建
|
||||
const world = worldManager.createWorld('worldId');
|
||||
|
||||
// 带配置创建
|
||||
const world = worldManager.createWorld('worldId', {
|
||||
name: 'MyWorld',
|
||||
maxScenes: 10,
|
||||
autoCleanup: true,
|
||||
debug: true
|
||||
});
|
||||
```
|
||||
|
||||
**配置选项(IWorldConfig)**:
|
||||
- `name?: string` - World 名称
|
||||
- `maxScenes?: number` - 最大场景数量限制(默认 10)
|
||||
- `autoCleanup?: boolean` - 是否自动清理空场景(默认 true)
|
||||
- `debug?: boolean` - 是否启用调试模式(默认 false)
|
||||
|
||||
### 获取 World
|
||||
|
||||
```typescript
|
||||
// 通过 ID 获取
|
||||
const world = worldManager.getWorld('room_001');
|
||||
if (world) {
|
||||
console.log(`World: ${world.name}`);
|
||||
}
|
||||
|
||||
// 获取所有 World
|
||||
const allWorlds = worldManager.getAllWorlds();
|
||||
console.log(`共有 ${allWorlds.length} 个 World`);
|
||||
|
||||
// 获取所有 World ID
|
||||
const worldIds = worldManager.getWorldIds();
|
||||
console.log('World 列表:', worldIds);
|
||||
|
||||
// 通过名称查找
|
||||
const world = worldManager.findWorldByName('GameRoom_001');
|
||||
```
|
||||
|
||||
### 激活和停用 World
|
||||
|
||||
```typescript
|
||||
// 激活 World(开始运行和更新)
|
||||
worldManager.setWorldActive('room_001', true);
|
||||
|
||||
// 停用 World(停止更新但保留数据)
|
||||
worldManager.setWorldActive('room_001', false);
|
||||
|
||||
// 检查 World 是否激活
|
||||
if (worldManager.isWorldActive('room_001')) {
|
||||
console.log('房间正在运行');
|
||||
}
|
||||
|
||||
// 获取所有活跃的 World
|
||||
const activeWorlds = worldManager.getActiveWorlds();
|
||||
console.log(`当前有 ${activeWorlds.length} 个活跃 World`);
|
||||
```
|
||||
|
||||
### 移除 World
|
||||
|
||||
```typescript
|
||||
// 移除 World(会自动停用并销毁)
|
||||
const removed = worldManager.removeWorld('room_001');
|
||||
if (removed) {
|
||||
console.log('World 已移除');
|
||||
}
|
||||
```
|
||||
|
||||
## World 中的场景管理
|
||||
|
||||
每个 World 可以包含多个 Scene 并独立管理它们的生命周期。
|
||||
|
||||
### 创建场景
|
||||
|
||||
```typescript
|
||||
const world = worldManager.getWorld('room_001');
|
||||
if (!world) return;
|
||||
|
||||
// 创建场景
|
||||
const mainScene = world.createScene('main', new MainScene());
|
||||
const uiScene = world.createScene('ui', new UIScene());
|
||||
const hudScene = world.createScene('hud', new HUDScene());
|
||||
|
||||
// 激活场景
|
||||
world.setSceneActive('main', true);
|
||||
world.setSceneActive('ui', true);
|
||||
world.setSceneActive('hud', false);
|
||||
```
|
||||
|
||||
### 查询场景
|
||||
|
||||
```typescript
|
||||
// 获取特定场景
|
||||
const mainScene = world.getScene<MainScene>('main');
|
||||
if (mainScene) {
|
||||
console.log(`场景名称: ${mainScene.name}`);
|
||||
}
|
||||
|
||||
// 获取所有场景
|
||||
const allScenes = world.getAllScenes();
|
||||
console.log(`World 中共有 ${allScenes.length} 个场景`);
|
||||
|
||||
// 获取所有场景 ID
|
||||
const sceneIds = world.getSceneIds();
|
||||
console.log('场景列表:', sceneIds);
|
||||
|
||||
// 获取活跃场景数量
|
||||
const activeCount = world.getActiveSceneCount();
|
||||
console.log(`当前有 ${activeCount} 个活跃场景`);
|
||||
|
||||
// 检查场景是否激活
|
||||
if (world.isSceneActive('main')) {
|
||||
console.log('主场景正在运行');
|
||||
}
|
||||
```
|
||||
|
||||
### 场景切换
|
||||
|
||||
World 支持多场景同时运行,也支持场景切换:
|
||||
|
||||
```typescript
|
||||
class GameWorld {
|
||||
private world: World;
|
||||
|
||||
constructor(worldManager: WorldManager) {
|
||||
this.world = worldManager.createWorld('game', {
|
||||
name: 'GameWorld',
|
||||
maxScenes: 5
|
||||
});
|
||||
|
||||
// 创建所有场景
|
||||
this.world.createScene('menu', new MenuScene());
|
||||
this.world.createScene('game', new GameScene());
|
||||
this.world.createScene('pause', new PauseScene());
|
||||
this.world.createScene('gameover', new GameOverScene());
|
||||
|
||||
// 激活 World
|
||||
worldManager.setWorldActive('game', true);
|
||||
}
|
||||
|
||||
public showMenu(): void {
|
||||
this.deactivateAllScenes();
|
||||
this.world.setSceneActive('menu', true);
|
||||
}
|
||||
|
||||
public startGame(): void {
|
||||
this.deactivateAllScenes();
|
||||
this.world.setSceneActive('game', true);
|
||||
}
|
||||
|
||||
public pauseGame(): void {
|
||||
// 游戏场景继续存在但停止更新
|
||||
this.world.setSceneActive('game', false);
|
||||
// 显示暂停界面
|
||||
this.world.setSceneActive('pause', true);
|
||||
}
|
||||
|
||||
public resumeGame(): void {
|
||||
this.world.setSceneActive('pause', false);
|
||||
this.world.setSceneActive('game', true);
|
||||
}
|
||||
|
||||
public showGameOver(): void {
|
||||
this.deactivateAllScenes();
|
||||
this.world.setSceneActive('gameover', true);
|
||||
}
|
||||
|
||||
private deactivateAllScenes(): void {
|
||||
const sceneIds = this.world.getSceneIds();
|
||||
sceneIds.forEach(id => this.world.setSceneActive(id, false));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 移除场景
|
||||
|
||||
```typescript
|
||||
// 移除不再需要的场景
|
||||
const removed = world.removeScene('oldScene');
|
||||
if (removed) {
|
||||
console.log('场景已移除');
|
||||
}
|
||||
|
||||
// 场景会自动调用 end() 方法进行清理
|
||||
```
|
||||
|
||||
## 全局系统
|
||||
|
||||
World 支持全局系统,这些系统在 World 级别运行,不依赖特定 Scene。
|
||||
|
||||
### 定义全局系统
|
||||
|
||||
```typescript
|
||||
import { IGlobalSystem } from '@esengine/ecs-framework';
|
||||
|
||||
// 网络系统(World 级别)
|
||||
class NetworkSystem implements IGlobalSystem {
|
||||
readonly name = 'NetworkSystem';
|
||||
|
||||
private connectionId: string;
|
||||
|
||||
constructor(connectionId: string) {
|
||||
this.connectionId = connectionId;
|
||||
}
|
||||
|
||||
initialize(): void {
|
||||
console.log(`网络系统初始化: ${this.connectionId}`);
|
||||
// 建立网络连接
|
||||
}
|
||||
|
||||
update(deltaTime?: number): void {
|
||||
// 处理网络消息,不依赖任何 Scene
|
||||
// 接收和发送网络包
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
console.log(`网络系统销毁: ${this.connectionId}`);
|
||||
// 关闭网络连接
|
||||
}
|
||||
}
|
||||
|
||||
// 物理系统(World 级别)
|
||||
class PhysicsSystem implements IGlobalSystem {
|
||||
readonly name = 'PhysicsSystem';
|
||||
|
||||
initialize(): void {
|
||||
console.log('物理系统初始化');
|
||||
}
|
||||
|
||||
update(deltaTime?: number): void {
|
||||
// 物理模拟,作用于 World 中所有场景
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
console.log('物理系统销毁');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用全局系统
|
||||
|
||||
```typescript
|
||||
const world = worldManager.getWorld('room_001');
|
||||
if (!world) return;
|
||||
|
||||
// 添加全局系统
|
||||
const networkSystem = world.addGlobalSystem(new NetworkSystem('conn_001'));
|
||||
const physicsSystem = world.addGlobalSystem(new PhysicsSystem());
|
||||
|
||||
// 获取全局系统
|
||||
const network = world.getGlobalSystem(NetworkSystem);
|
||||
if (network) {
|
||||
console.log('找到网络系统');
|
||||
}
|
||||
|
||||
// 移除全局系统
|
||||
world.removeGlobalSystem(networkSystem);
|
||||
```
|
||||
|
||||
## 批量操作
|
||||
|
||||
### 更新所有 World
|
||||
|
||||
```typescript
|
||||
// 更新所有活跃的 World(应该在游戏循环中调用)
|
||||
worldManager.updateAll();
|
||||
|
||||
// 这会自动更新每个 World 的:
|
||||
// 1. 全局系统
|
||||
// 2. 所有活跃场景
|
||||
```
|
||||
|
||||
### 启动和停止
|
||||
|
||||
```typescript
|
||||
// 启动所有 World
|
||||
worldManager.startAll();
|
||||
|
||||
// 停止所有 World
|
||||
worldManager.stopAll();
|
||||
|
||||
// 检查是否正在运行
|
||||
if (worldManager.isRunning) {
|
||||
console.log('WorldManager 正在运行');
|
||||
}
|
||||
```
|
||||
|
||||
### 查找 World
|
||||
|
||||
```typescript
|
||||
// 使用条件查找
|
||||
const emptyWorlds = worldManager.findWorlds(world => {
|
||||
return world.sceneCount === 0;
|
||||
});
|
||||
|
||||
// 查找活跃的 World
|
||||
const activeWorlds = worldManager.findWorlds(world => {
|
||||
return world.isActive;
|
||||
});
|
||||
|
||||
// 查找特定名称的 World
|
||||
const world = worldManager.findWorldByName('GameRoom_001');
|
||||
```
|
||||
|
||||
## 统计和监控
|
||||
|
||||
### 获取统计信息
|
||||
|
||||
```typescript
|
||||
const stats = worldManager.getStats();
|
||||
|
||||
console.log(`总 World 数: ${stats.totalWorlds}`);
|
||||
console.log(`活跃 World 数: ${stats.activeWorlds}`);
|
||||
console.log(`总场景数: ${stats.totalScenes}`);
|
||||
console.log(`总实体数: ${stats.totalEntities}`);
|
||||
console.log(`总系统数: ${stats.totalSystems}`);
|
||||
|
||||
// 查看每个 World 的详细信息
|
||||
stats.worlds.forEach(worldInfo => {
|
||||
console.log(`World: ${worldInfo.name}`);
|
||||
console.log(` 场景数: ${worldInfo.sceneCount}`);
|
||||
console.log(` 是否活跃: ${worldInfo.isActive}`);
|
||||
});
|
||||
```
|
||||
|
||||
### 获取详细状态
|
||||
|
||||
```typescript
|
||||
const status = worldManager.getDetailedStatus();
|
||||
|
||||
// 包含所有 World 的详细状态
|
||||
status.worlds.forEach(worldStatus => {
|
||||
console.log(`World ID: ${worldStatus.id}`);
|
||||
console.log(`状态:`, worldStatus.status);
|
||||
});
|
||||
```
|
||||
|
||||
## 自动清理
|
||||
|
||||
WorldManager 支持自动清理空的 World。
|
||||
|
||||
### 配置清理
|
||||
|
||||
```typescript
|
||||
// 创建带清理配置的 WorldManager
|
||||
const worldManager = Core.services.resolve(WorldManager);
|
||||
|
||||
// WorldManager 的配置在 Core 中设置:
|
||||
// {
|
||||
// maxWorlds: 50,
|
||||
// autoCleanup: true,
|
||||
// cleanupInterval: 30000 // 30 秒
|
||||
// }
|
||||
```
|
||||
|
||||
### 手动清理
|
||||
|
||||
```typescript
|
||||
// 手动触发清理
|
||||
const cleanedCount = worldManager.cleanup();
|
||||
console.log(`清理了 ${cleanedCount} 个 World`);
|
||||
```
|
||||
|
||||
**清理条件**:
|
||||
- World 未激活
|
||||
- 没有 Scene 或所有 Scene 都是空的
|
||||
- 创建时间超过 10 分钟
|
||||
|
||||
## API 参考
|
||||
|
||||
### WorldManager API
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `createWorld(worldId, config?)` | 创建新 World |
|
||||
| `removeWorld(worldId)` | 移除 World |
|
||||
| `getWorld(worldId)` | 获取 World |
|
||||
| `getAllWorlds()` | 获取所有 World |
|
||||
| `getWorldIds()` | 获取所有 World ID |
|
||||
| `setWorldActive(worldId, active)` | 设置 World 激活状态 |
|
||||
| `isWorldActive(worldId)` | 检查 World 是否激活 |
|
||||
| `getActiveWorlds()` | 获取所有活跃的 World |
|
||||
| `updateAll()` | 更新所有活跃 World |
|
||||
| `startAll()` | 启动所有 World |
|
||||
| `stopAll()` | 停止所有 World |
|
||||
| `findWorlds(predicate)` | 查找满足条件的 World |
|
||||
| `findWorldByName(name)` | 根据名称查找 World |
|
||||
| `getStats()` | 获取统计信息 |
|
||||
| `getDetailedStatus()` | 获取详细状态信息 |
|
||||
| `cleanup()` | 清理空 World |
|
||||
| `destroy()` | 销毁 WorldManager |
|
||||
|
||||
### World API
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `createScene(sceneId, sceneInstance?)` | 创建并添加 Scene |
|
||||
| `removeScene(sceneId)` | 移除 Scene |
|
||||
| `getScene(sceneId)` | 获取 Scene |
|
||||
| `getAllScenes()` | 获取所有 Scene |
|
||||
| `getSceneIds()` | 获取所有 Scene ID |
|
||||
| `setSceneActive(sceneId, active)` | 设置 Scene 激活状态 |
|
||||
| `isSceneActive(sceneId)` | 检查 Scene 是否激活 |
|
||||
| `getActiveSceneCount()` | 获取活跃 Scene 数量 |
|
||||
| `addGlobalSystem(system)` | 添加全局系统 |
|
||||
| `removeGlobalSystem(system)` | 移除全局系统 |
|
||||
| `getGlobalSystem(type)` | 获取全局系统 |
|
||||
| `start()` | 启动 World |
|
||||
| `stop()` | 停止 World |
|
||||
| `updateGlobalSystems()` | 更新全局系统 |
|
||||
| `updateScenes()` | 更新所有激活 Scene |
|
||||
| `destroy()` | 销毁 World |
|
||||
| `getStatus()` | 获取 World 状态 |
|
||||
| `getStats()` | 获取统计信息 |
|
||||
|
||||
### 属性
|
||||
|
||||
| 属性 | 说明 |
|
||||
|------|------|
|
||||
| `worldCount` | World 总数 |
|
||||
| `activeWorldCount` | 活跃 World 数量 |
|
||||
| `isRunning` | 是否正在运行 |
|
||||
| `config` | 配置信息 |
|
||||
|
||||
## 完整示例
|
||||
|
||||
### MMO 游戏房间系统
|
||||
|
||||
```typescript
|
||||
import { Core, WorldManager, Scene, World } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始化
|
||||
Core.create({ debug: true });
|
||||
const worldManager = Core.services.resolve(WorldManager);
|
||||
|
||||
// 房间管理器
|
||||
class RoomManager {
|
||||
private worldManager: WorldManager;
|
||||
private rooms: Map<string, World> = new Map();
|
||||
|
||||
constructor(worldManager: WorldManager) {
|
||||
this.worldManager = worldManager;
|
||||
}
|
||||
|
||||
// 创建游戏房间
|
||||
public createRoom(roomId: string, maxPlayers: number): World {
|
||||
const world = this.worldManager.createWorld(roomId, {
|
||||
name: `Room_${roomId}`,
|
||||
maxScenes: 3,
|
||||
debug: true
|
||||
});
|
||||
|
||||
// 创建房间场景
|
||||
world.createScene('lobby', new LobbyScene());
|
||||
world.createScene('game', new GameScene());
|
||||
world.createScene('result', new ResultScene());
|
||||
|
||||
// 添加房间级别的系统
|
||||
world.addGlobalSystem(new NetworkSystem(roomId));
|
||||
world.addGlobalSystem(new RoomLogicSystem(maxPlayers));
|
||||
|
||||
// 激活 World 和初始场景
|
||||
this.worldManager.setWorldActive(roomId, true);
|
||||
world.setSceneActive('lobby', true);
|
||||
|
||||
this.rooms.set(roomId, world);
|
||||
console.log(`房间 ${roomId} 已创建`);
|
||||
|
||||
return world;
|
||||
}
|
||||
|
||||
// 玩家加入房间
|
||||
public joinRoom(roomId: string, playerId: string): boolean {
|
||||
const world = this.rooms.get(roomId);
|
||||
if (!world) {
|
||||
console.log(`房间 ${roomId} 不存在`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 在大厅场景中创建玩家实体
|
||||
const lobbyScene = world.getScene('lobby');
|
||||
if (lobbyScene) {
|
||||
const player = lobbyScene.createEntity(`Player_${playerId}`);
|
||||
// 添加玩家组件...
|
||||
console.log(`玩家 ${playerId} 加入房间 ${roomId}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// 开始游戏
|
||||
public startGame(roomId: string): void {
|
||||
const world = this.rooms.get(roomId);
|
||||
if (!world) return;
|
||||
|
||||
// 切换到游戏场景
|
||||
world.setSceneActive('lobby', false);
|
||||
world.setSceneActive('game', true);
|
||||
|
||||
console.log(`房间 ${roomId} 游戏开始`);
|
||||
}
|
||||
|
||||
// 结束游戏
|
||||
public endGame(roomId: string): void {
|
||||
const world = this.rooms.get(roomId);
|
||||
if (!world) return;
|
||||
|
||||
// 切换到结果场景
|
||||
world.setSceneActive('game', false);
|
||||
world.setSceneActive('result', true);
|
||||
|
||||
console.log(`房间 ${roomId} 游戏结束`);
|
||||
}
|
||||
|
||||
// 关闭房间
|
||||
public closeRoom(roomId: string): void {
|
||||
this.worldManager.removeWorld(roomId);
|
||||
this.rooms.delete(roomId);
|
||||
console.log(`房间 ${roomId} 已关闭`);
|
||||
}
|
||||
|
||||
// 获取房间列表
|
||||
public getRoomList(): string[] {
|
||||
return Array.from(this.rooms.keys());
|
||||
}
|
||||
|
||||
// 获取房间统计
|
||||
public getRoomStats(roomId: string) {
|
||||
const world = this.rooms.get(roomId);
|
||||
return world?.getStats();
|
||||
}
|
||||
}
|
||||
|
||||
// 使用房间管理器
|
||||
const roomManager = new RoomManager(worldManager);
|
||||
|
||||
// 创建多个游戏房间
|
||||
roomManager.createRoom('room_001', 4);
|
||||
roomManager.createRoom('room_002', 4);
|
||||
roomManager.createRoom('room_003', 2);
|
||||
|
||||
// 玩家加入
|
||||
roomManager.joinRoom('room_001', 'player_1');
|
||||
roomManager.joinRoom('room_001', 'player_2');
|
||||
|
||||
// 开始游戏
|
||||
roomManager.startGame('room_001');
|
||||
|
||||
// 游戏循环
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime);
|
||||
worldManager.updateAll(); // 更新所有房间
|
||||
}
|
||||
|
||||
// 定期清理空房间
|
||||
setInterval(() => {
|
||||
const stats = worldManager.getStats();
|
||||
console.log(`当前房间数: ${stats.totalWorlds}`);
|
||||
console.log(`活跃房间数: ${stats.activeWorlds}`);
|
||||
|
||||
worldManager.cleanup();
|
||||
}, 60000); // 每分钟清理一次
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 合理的 World 粒度
|
||||
|
||||
```typescript
|
||||
// 推荐:每个独立环境一个 World
|
||||
const room1 = worldManager.createWorld('room_1'); // 游戏房间1
|
||||
const room2 = worldManager.createWorld('room_2'); // 游戏房间2
|
||||
|
||||
// 不推荐:过度使用 World
|
||||
const world1 = worldManager.createWorld('ui'); // UI 不需要独立 World
|
||||
const world2 = worldManager.createWorld('menu'); // 菜单不需要独立 World
|
||||
```
|
||||
|
||||
### 2. 使用全局系统处理跨场景逻辑
|
||||
|
||||
```typescript
|
||||
// 推荐:World 级别的系统
|
||||
class NetworkSystem implements IGlobalSystem {
|
||||
update() {
|
||||
// 网络处理不依赖场景
|
||||
}
|
||||
}
|
||||
|
||||
// 不推荐:在每个场景中重复创建
|
||||
class GameScene extends Scene {
|
||||
initialize() {
|
||||
this.addSystem(new NetworkSystem()); // 不应该在场景级别
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 及时清理不用的 World
|
||||
|
||||
```typescript
|
||||
// 推荐:玩家离开时清理房间
|
||||
function onPlayerLeave(roomId: string) {
|
||||
const world = worldManager.getWorld(roomId);
|
||||
if (world && world.sceneCount === 0) {
|
||||
worldManager.removeWorld(roomId);
|
||||
}
|
||||
}
|
||||
|
||||
// 或使用自动清理
|
||||
worldManager.cleanup();
|
||||
```
|
||||
|
||||
### 4. 监控资源使用
|
||||
|
||||
```typescript
|
||||
// 定期检查资源使用情况
|
||||
setInterval(() => {
|
||||
const stats = worldManager.getStats();
|
||||
|
||||
if (stats.totalWorlds > 100) {
|
||||
console.warn('World 数量过多,考虑清理');
|
||||
worldManager.cleanup();
|
||||
}
|
||||
|
||||
if (stats.totalEntities > 10000) {
|
||||
console.warn('实体数量过多,检查是否有泄漏');
|
||||
}
|
||||
}, 30000);
|
||||
```
|
||||
|
||||
## 与 SceneManager 的对比
|
||||
|
||||
| 特性 | SceneManager | WorldManager |
|
||||
|------|--------------|--------------|
|
||||
| 适用场景 | 95% 的游戏应用 | 高级多世界隔离场景 |
|
||||
| 复杂度 | 简单 | 复杂 |
|
||||
| 场景数量 | 单场景(可切换) | 多 World,每个 World 多场景 |
|
||||
| 场景隔离 | 无(场景切换) | 完全隔离(每个 World 独立) |
|
||||
| 性能开销 | 最小 | 较高 |
|
||||
| 全局系统 | 无 | 支持(World 级别) |
|
||||
| 使用示例 | 单人游戏、移动游戏 | MMO 服务器、游戏房间系统 |
|
||||
|
||||
**何时使用 WorldManager**:
|
||||
- MMO 游戏服务器(每个房间一个 World)
|
||||
- 游戏大厅系统(每个游戏房间完全隔离)
|
||||
- 需要运行多个完全独立的游戏实例
|
||||
- 服务器端模拟多个游戏世界
|
||||
|
||||
**何时使用 SceneManager**:
|
||||
- 单人游戏
|
||||
- 简单的多人游戏
|
||||
- 移动游戏
|
||||
- 场景之间需要切换但不需要同时运行
|
||||
|
||||
## 架构层次
|
||||
|
||||
WorldManager 在 ECS Framework 中的位置:
|
||||
|
||||
```
|
||||
Core (全局服务)
|
||||
└── WorldManager (世界管理)
|
||||
├── World 1 (游戏房间1)
|
||||
│ ├── GlobalSystem (全局系统)
|
||||
│ ├── Scene 1 (场景1)
|
||||
│ │ ├── EntitySystem
|
||||
│ │ ├── Entity
|
||||
│ │ └── Component
|
||||
│ └── Scene 2 (场景2)
|
||||
├── World 2 (游戏房间2)
|
||||
│ ├── GlobalSystem
|
||||
│ └── Scene 1
|
||||
└── World 3 (游戏房间3)
|
||||
```
|
||||
|
||||
WorldManager 为需要多世界隔离的高级应用提供了强大的管理能力。如果你的应用不需要多世界隔离,建议使用更简单的 [SceneManager](./scene-manager.md)。
|
||||
483
examples/core-demos/index.html
Normal file
483
examples/core-demos/index.html
Normal file
@@ -0,0 +1,483 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ECS Framework Core Demos</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
background: #0f0f23;
|
||||
color: #e0e0e0;
|
||||
overflow: hidden;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* 侧边栏 */
|
||||
.sidebar {
|
||||
width: 280px;
|
||||
background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%);
|
||||
border-right: 2px solid #0a3d62;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 4px 0 20px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 30px 20px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sidebar-header h1 {
|
||||
font-size: 1.5em;
|
||||
color: white;
|
||||
margin-bottom: 8px;
|
||||
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.sidebar-header p {
|
||||
font-size: 0.85em;
|
||||
color: rgba(255,255,255,0.9);
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.demo-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
.demo-category {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.category-title {
|
||||
padding: 12px 20px;
|
||||
color: #8892b0;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.demo-item {
|
||||
padding: 14px 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border-left: 3px solid transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.demo-item:hover {
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
border-left-color: #667eea;
|
||||
}
|
||||
|
||||
.demo-item.active {
|
||||
background: rgba(102, 126, 234, 0.2);
|
||||
border-left-color: #667eea;
|
||||
}
|
||||
|
||||
.demo-icon {
|
||||
font-size: 20px;
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.demo-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.demo-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #ccd6f6;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.demo-item.active .demo-name {
|
||||
color: #667eea;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.demo-desc {
|
||||
font-size: 11px;
|
||||
color: #8892b0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: 20px;
|
||||
border-top: 1px solid rgba(255,255,255,0.1);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.github-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 20px;
|
||||
background: rgba(102, 126, 234, 0.2);
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.github-link:hover {
|
||||
background: rgba(102, 126, 234, 0.3);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* 主内容区 */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content-header {
|
||||
padding: 25px 40px;
|
||||
background: #1a1a2e;
|
||||
border-bottom: 2px solid #0a3d62;
|
||||
}
|
||||
|
||||
.content-header h2 {
|
||||
font-size: 2em;
|
||||
color: #ccd6f6;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.content-header p {
|
||||
color: #8892b0;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.demo-canvas-container {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: #0a0a15;
|
||||
}
|
||||
|
||||
#demoCanvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 控制面板 */
|
||||
.control-panel {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
width: 320px;
|
||||
background: rgba(26, 26, 46, 0.95);
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(102, 126, 234, 0.3);
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
|
||||
backdrop-filter: blur(10px);
|
||||
max-height: calc(100% - 40px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.control-panel-header {
|
||||
padding: 15px 20px;
|
||||
background: linear-gradient(135deg, rgba(102, 126, 234, 0.2) 0%, rgba(118, 75, 162, 0.2) 100%);
|
||||
border-bottom: 1px solid rgba(102, 126, 234, 0.3);
|
||||
border-radius: 12px 12px 0 0;
|
||||
}
|
||||
|
||||
.control-panel-header h3 {
|
||||
color: #667eea;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.control-panel-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.control-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.control-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.control-section h4 {
|
||||
color: #8892b0;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px 16px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
button.secondary {
|
||||
background: rgba(102, 126, 234, 0.2);
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
button.secondary:hover {
|
||||
background: rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
button.danger {
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
}
|
||||
|
||||
button.success {
|
||||
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.input-group label {
|
||||
display: block;
|
||||
color: #8892b0;
|
||||
font-size: 12px;
|
||||
margin-bottom: 6px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.input-group input {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
background: rgba(255,255,255,0.05);
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
border-radius: 6px;
|
||||
color: #ccd6f6;
|
||||
font-size: 13px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.input-group input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
background: rgba(255,255,255,0.08);
|
||||
}
|
||||
|
||||
/* 统计信息 */
|
||||
.stats-panel {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
background: rgba(255,255,255,0.03);
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(255,255,255,0.05);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #8892b0;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
color: #667eea;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Toast通知 */
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 30px;
|
||||
right: 30px;
|
||||
background: rgba(26, 26, 46, 0.98);
|
||||
border: 1px solid #667eea;
|
||||
border-radius: 8px;
|
||||
padding: 15px 20px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
|
||||
transform: translateY(150%);
|
||||
transition: transform 0.3s ease;
|
||||
z-index: 1000;
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
.toast.show {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.toast-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.toast-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.toast-message {
|
||||
color: #ccd6f6;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(255,255,255,0.05);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(102, 126, 234, 0.5);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(102, 126, 234, 0.7);
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
.loading {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 4px solid rgba(102, 126, 234, 0.2);
|
||||
border-top-color: #667eea;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
margin-top: 15px;
|
||||
color: #8892b0;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-container">
|
||||
<!-- 侧边栏 -->
|
||||
<div class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h1>🎮 ECS Core Demos</h1>
|
||||
<p>交互式演示集合</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-list" id="demoList">
|
||||
<!-- Demo列表将通过JS动态生成 -->
|
||||
</div>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<a href="https://github.com/esengine/ecs-framework" target="_blank" class="github-link">
|
||||
<span>⭐</span>
|
||||
<span>GitHub</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<div class="main-content">
|
||||
<div class="content-header">
|
||||
<h2 id="demoTitle">选择一个演示开始</h2>
|
||||
<p id="demoDescription">从左侧菜单选择一个演示查看效果</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-canvas-container">
|
||||
<canvas id="demoCanvas"></canvas>
|
||||
|
||||
<!-- 控制面板 -->
|
||||
<div class="control-panel" id="controlPanel" style="display: none;">
|
||||
<div class="control-panel-header">
|
||||
<h3>控制面板</h3>
|
||||
</div>
|
||||
<div class="control-panel-content" id="controlPanelContent">
|
||||
<!-- 控制内容将由各个demo动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载动画 -->
|
||||
<div class="loading" id="loading" style="display: none;">
|
||||
<div class="loading-spinner"></div>
|
||||
<div class="loading-text">加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast通知 -->
|
||||
<div class="toast" id="toast">
|
||||
<div class="toast-content">
|
||||
<span class="toast-icon">✅</span>
|
||||
<span class="toast-message" id="toastMessage"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "ecs-worker-system-demo",
|
||||
"name": "ecs-core-demos",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ecs-worker-system-demo",
|
||||
"name": "ecs-core-demos",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@esengine/ecs-framework": "file:../../packages/core"
|
||||
@@ -17,8 +17,11 @@
|
||||
},
|
||||
"../../packages/core": {
|
||||
"name": "@esengine/ecs-framework",
|
||||
"version": "2.1.49",
|
||||
"version": "2.1.51",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"msgpack-lite": "^0.1.26"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.28.3",
|
||||
"@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1",
|
||||
@@ -29,6 +32,7 @@
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/msgpack-lite": "^0.1.11",
|
||||
"@types/node": "^20.19.17",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
@@ -553,9 +557,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
|
||||
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "ecs-worker-system-demo",
|
||||
"name": "ecs-core-demos",
|
||||
"version": "1.0.0",
|
||||
"description": "ECS Framework Worker System Demo",
|
||||
"description": "ECS Framework Core Demos - Multiple Interactive Examples",
|
||||
"main": "src/main.ts",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -15,4 +15,4 @@
|
||||
"dependencies": {
|
||||
"@esengine/ecs-framework": "file:../../packages/core"
|
||||
}
|
||||
}
|
||||
}
|
||||
99
examples/core-demos/src/demos/DemoBase.ts
Normal file
99
examples/core-demos/src/demos/DemoBase.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { Scene, Core } from '@esengine/ecs-framework';
|
||||
|
||||
export interface DemoInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
category: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export abstract class DemoBase {
|
||||
protected scene: Scene;
|
||||
protected canvas: HTMLCanvasElement;
|
||||
protected ctx: CanvasRenderingContext2D;
|
||||
protected controlPanel: HTMLElement;
|
||||
protected isRunning: boolean = false;
|
||||
protected animationFrameId: number | null = null;
|
||||
protected lastTime: number = 0;
|
||||
|
||||
constructor(canvas: HTMLCanvasElement, controlPanel: HTMLElement) {
|
||||
this.canvas = canvas;
|
||||
this.ctx = canvas.getContext('2d')!;
|
||||
this.controlPanel = controlPanel;
|
||||
this.scene = new Scene({ name: this.getInfo().name });
|
||||
|
||||
// 设置canvas大小
|
||||
this.resizeCanvas();
|
||||
window.addEventListener('resize', () => this.resizeCanvas());
|
||||
}
|
||||
|
||||
abstract getInfo(): DemoInfo;
|
||||
abstract setup(): void;
|
||||
abstract createControls(): void;
|
||||
|
||||
protected resizeCanvas() {
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
this.canvas.width = rect.width;
|
||||
this.canvas.height = rect.height;
|
||||
}
|
||||
|
||||
public start() {
|
||||
if (this.isRunning) return;
|
||||
this.isRunning = true;
|
||||
this.lastTime = performance.now();
|
||||
|
||||
// 设置当前场景到Core
|
||||
Core.setScene(this.scene);
|
||||
|
||||
this.scene.begin();
|
||||
this.loop();
|
||||
}
|
||||
|
||||
public stop() {
|
||||
this.isRunning = false;
|
||||
if (this.animationFrameId !== null) {
|
||||
cancelAnimationFrame(this.animationFrameId);
|
||||
this.animationFrameId = null;
|
||||
}
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.stop();
|
||||
this.scene.end();
|
||||
}
|
||||
|
||||
protected loop = () => {
|
||||
if (!this.isRunning) return;
|
||||
|
||||
// 计算deltaTime
|
||||
const currentTime = performance.now();
|
||||
const deltaTime = (currentTime - this.lastTime) / 1000; // 转换为秒
|
||||
this.lastTime = currentTime;
|
||||
|
||||
// 更新ECS框架
|
||||
Core.update(deltaTime);
|
||||
|
||||
// 渲染
|
||||
this.render();
|
||||
|
||||
// 继续循环
|
||||
this.animationFrameId = requestAnimationFrame(this.loop);
|
||||
}
|
||||
|
||||
protected abstract render(): void;
|
||||
|
||||
protected showToast(message: string, icon: string = '✅') {
|
||||
const toast = document.getElementById('toast')!;
|
||||
const toastMessage = document.getElementById('toastMessage')!;
|
||||
const toastIcon = toast.querySelector('.toast-icon')!;
|
||||
|
||||
toastIcon.textContent = icon;
|
||||
toastMessage.textContent = message;
|
||||
|
||||
toast.classList.add('show');
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('show');
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
468
examples/core-demos/src/demos/IncrementalSerializationDemo.ts
Normal file
468
examples/core-demos/src/demos/IncrementalSerializationDemo.ts
Normal file
@@ -0,0 +1,468 @@
|
||||
import { DemoBase, DemoInfo } from './DemoBase';
|
||||
import {
|
||||
Component,
|
||||
ECSComponent,
|
||||
Entity,
|
||||
EntitySystem,
|
||||
Matcher,
|
||||
Serializable,
|
||||
Serialize,
|
||||
IncrementalSerializer
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
// ===== 组件定义 =====
|
||||
@ECSComponent('IncDemo_Position')
|
||||
@Serializable({ version: 1, typeId: 'IncDemo_Position' })
|
||||
class PositionComponent extends Component {
|
||||
@Serialize() x: number = 0;
|
||||
@Serialize() y: number = 0;
|
||||
constructor(x: number = 0, y: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('IncDemo_Velocity')
|
||||
@Serializable({ version: 1, typeId: 'IncDemo_Velocity' })
|
||||
class VelocityComponent extends Component {
|
||||
@Serialize() vx: number = 0;
|
||||
@Serialize() vy: number = 0;
|
||||
constructor(vx: number = 0, vy: number = 0) {
|
||||
super();
|
||||
this.vx = vx;
|
||||
this.vy = vy;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('IncDemo_Renderable')
|
||||
@Serializable({ version: 1, typeId: 'IncDemo_Renderable' })
|
||||
class RenderableComponent extends Component {
|
||||
@Serialize() color: string = '#ffffff';
|
||||
@Serialize() radius: number = 10;
|
||||
constructor(color: string = '#ffffff', radius: number = 10) {
|
||||
super();
|
||||
this.color = color;
|
||||
this.radius = radius;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 系统定义 =====
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(PositionComponent, VelocityComponent));
|
||||
}
|
||||
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const [pos, vel] = this.getComponents(entity, PositionComponent, VelocityComponent);
|
||||
|
||||
pos.x += vel.vx;
|
||||
pos.y += vel.vy;
|
||||
|
||||
if (pos.x < 0 || pos.x > 1200) vel.vx *= -1;
|
||||
if (pos.y < 0 || pos.y > 600) vel.vy *= -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RenderSystem extends EntitySystem {
|
||||
private canvas: HTMLCanvasElement;
|
||||
private ctx: CanvasRenderingContext2D;
|
||||
|
||||
constructor(canvas: HTMLCanvasElement) {
|
||||
super(Matcher.all(PositionComponent, RenderableComponent));
|
||||
this.canvas = canvas;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) throw new Error('Failed to get canvas context');
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
this.ctx.fillStyle = '#0a0a15';
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
for (const entity of entities) {
|
||||
const [pos, render] = this.getComponents(entity, PositionComponent, RenderableComponent);
|
||||
|
||||
this.ctx.fillStyle = render.color;
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(pos.x, pos.y, render.radius, 0, Math.PI * 2);
|
||||
this.ctx.fill();
|
||||
|
||||
this.ctx.fillStyle = 'white';
|
||||
this.ctx.font = '10px Arial';
|
||||
this.ctx.textAlign = 'center';
|
||||
this.ctx.fillText(entity.name, pos.x, pos.y - render.radius - 5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class IncrementalSerializationDemo extends DemoBase {
|
||||
private renderSystem!: RenderSystem;
|
||||
private incrementalHistory: any[] = [];
|
||||
private autoSnapshotInterval: number | null = null;
|
||||
|
||||
getInfo(): DemoInfo {
|
||||
return {
|
||||
id: 'incremental-serialization',
|
||||
name: '增量序列化',
|
||||
description: '演示增量序列化功能,只保存场景变更而非完整状态,适用于网络同步和回放系统',
|
||||
category: '核心功能',
|
||||
icon: '🔄'
|
||||
};
|
||||
}
|
||||
|
||||
setup() {
|
||||
// 创建控制面板
|
||||
this.createControls();
|
||||
|
||||
// 添加系统
|
||||
this.renderSystem = new RenderSystem(this.canvas);
|
||||
this.scene.addEntityProcessor(new MovementSystem());
|
||||
this.scene.addEntityProcessor(this.renderSystem);
|
||||
|
||||
// 创建初始实体
|
||||
this.createInitialEntities();
|
||||
|
||||
// 创建基础快照
|
||||
this.scene.createIncrementalSnapshot();
|
||||
this.addToHistory('Initial State');
|
||||
}
|
||||
|
||||
private createInitialEntities() {
|
||||
// 创建玩家
|
||||
const player = this.scene.createEntity('Player');
|
||||
player.addComponent(new PositionComponent(600, 300));
|
||||
player.addComponent(new VelocityComponent(2, 1.5));
|
||||
player.addComponent(new RenderableComponent('#4a9eff', 15));
|
||||
|
||||
// 设置场景数据
|
||||
this.scene.sceneData.set('gameTime', 0);
|
||||
this.scene.sceneData.set('score', 0);
|
||||
}
|
||||
|
||||
private createRandomEntity() {
|
||||
const entity = this.scene.createEntity(`Entity_${Date.now()}`);
|
||||
entity.addComponent(new PositionComponent(
|
||||
Math.random() * this.canvas.width,
|
||||
Math.random() * this.canvas.height
|
||||
));
|
||||
entity.addComponent(new VelocityComponent(
|
||||
(Math.random() - 0.5) * 3,
|
||||
(Math.random() - 0.5) * 3
|
||||
));
|
||||
const colors = ['#ff6b6b', '#4ecdc4', '#ffe66d', '#a8dadc', '#f1faee'];
|
||||
entity.addComponent(new RenderableComponent(
|
||||
colors[Math.floor(Math.random() * colors.length)],
|
||||
5 + Math.random() * 10
|
||||
));
|
||||
}
|
||||
|
||||
private addToHistory(label: string) {
|
||||
const incremental = this.scene.serializeIncremental();
|
||||
const stats = IncrementalSerializer.getIncrementalStats(incremental);
|
||||
|
||||
// 计算JSON和二进制格式的大小
|
||||
const jsonSize = IncrementalSerializer.getIncrementalSize(incremental, 'json');
|
||||
const binarySize = IncrementalSerializer.getIncrementalSize(incremental, 'binary');
|
||||
|
||||
this.incrementalHistory.push({
|
||||
label,
|
||||
incremental,
|
||||
stats,
|
||||
timestamp: Date.now(),
|
||||
jsonSize,
|
||||
binarySize
|
||||
});
|
||||
|
||||
this.scene.updateIncrementalSnapshot();
|
||||
this.updateHistoryPanel();
|
||||
this.updateStats();
|
||||
}
|
||||
|
||||
createControls() {
|
||||
this.controlPanel.innerHTML = `
|
||||
<div class="control-section">
|
||||
<h4>实体控制</h4>
|
||||
<div class="button-group">
|
||||
<button id="addEntity" class="secondary">添加随机实体</button>
|
||||
<button id="removeEntity" class="danger">删除最后一个实体</button>
|
||||
<button id="modifyEntity" class="secondary">修改实体数据</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-section">
|
||||
<h4>增量快照</h4>
|
||||
<div class="button-group">
|
||||
<button id="captureSnapshot" class="success">捕获当前状态</button>
|
||||
<button id="clearHistory" class="danger">清空历史</button>
|
||||
</div>
|
||||
<div style="margin-top: 10px;">
|
||||
<label>
|
||||
<input type="checkbox" id="autoSnapshot">
|
||||
自动快照(每2秒)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-section">
|
||||
<h4>场景数据控制</h4>
|
||||
<div class="input-group">
|
||||
<label>游戏时间</label>
|
||||
<input type="number" id="gameTime" value="0" step="1">
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label>分数</label>
|
||||
<input type="number" id="score" value="0" step="10">
|
||||
</div>
|
||||
<button id="updateSceneData" class="secondary">更新场景数据</button>
|
||||
</div>
|
||||
|
||||
<div class="stats-panel">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">实体数量</div>
|
||||
<div class="stat-value" id="entityCount">0</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">历史记录</div>
|
||||
<div class="stat-value" id="historyCount">0</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">JSON大小</div>
|
||||
<div class="stat-value" id="jsonSize">0B</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">二进制大小</div>
|
||||
<div class="stat-value" id="binarySize">0B</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">压缩率</div>
|
||||
<div class="stat-value" id="compressionRatio">0%</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">总变更数</div>
|
||||
<div class="stat-value" id="totalChanges">0</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-section">
|
||||
<h4>增量历史 <small style="color: #8892b0;">(点击快照查看详情)</small></h4>
|
||||
<div style="max-height: 300px; overflow-y: auto; background: rgba(0,0,0,0.3); padding: 10px; border-radius: 6px;" id="historyPanel">
|
||||
暂无历史记录
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-section">
|
||||
<h4>快照详情</h4>
|
||||
<div style="max-height: 200px; overflow-y: auto; background: rgba(0,0,0,0.3); padding: 10px; border-radius: 6px; font-family: monospace; font-size: 11px; color: #8892b0;" id="snapshotDetails">
|
||||
点击历史记录查看详情...
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.bindEvents();
|
||||
this.updateStats();
|
||||
}
|
||||
|
||||
private bindEvents() {
|
||||
document.getElementById('addEntity')!.addEventListener('click', () => {
|
||||
this.createRandomEntity();
|
||||
this.addToHistory('添加实体');
|
||||
this.showToast('添加了一个随机实体');
|
||||
});
|
||||
|
||||
document.getElementById('removeEntity')!.addEventListener('click', () => {
|
||||
const entities = this.scene.entities.buffer;
|
||||
if (entities.length > 1) {
|
||||
const lastEntity = entities[entities.length - 1];
|
||||
lastEntity.destroy();
|
||||
this.addToHistory('删除实体');
|
||||
this.showToast('删除了最后一个实体');
|
||||
} else {
|
||||
this.showToast('至少保留一个实体', '⚠️');
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('modifyEntity')!.addEventListener('click', () => {
|
||||
const entities = this.scene.entities.buffer;
|
||||
if (entities.length > 0) {
|
||||
const randomEntity = entities[Math.floor(Math.random() * entities.length)];
|
||||
const pos = randomEntity.getComponent(PositionComponent);
|
||||
if (pos) {
|
||||
pos.x = Math.random() * this.canvas.width;
|
||||
pos.y = Math.random() * this.canvas.height;
|
||||
}
|
||||
this.addToHistory('修改实体位置');
|
||||
this.showToast(`修改了 ${randomEntity.name} 的位置`);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('captureSnapshot')!.addEventListener('click', () => {
|
||||
this.addToHistory('手动快照');
|
||||
this.showToast('已捕获当前状态', '📸');
|
||||
});
|
||||
|
||||
document.getElementById('clearHistory')!.addEventListener('click', () => {
|
||||
this.incrementalHistory = [];
|
||||
this.scene.createIncrementalSnapshot();
|
||||
this.addToHistory('清空后重新开始');
|
||||
this.showToast('历史记录已清空');
|
||||
});
|
||||
|
||||
document.getElementById('autoSnapshot')!.addEventListener('change', (e) => {
|
||||
const checkbox = e.target as HTMLInputElement;
|
||||
if (checkbox.checked) {
|
||||
this.autoSnapshotInterval = window.setInterval(() => {
|
||||
this.addToHistory('自动快照');
|
||||
}, 2000);
|
||||
this.showToast('自动快照已启用', '⏱️');
|
||||
} else {
|
||||
if (this.autoSnapshotInterval !== null) {
|
||||
clearInterval(this.autoSnapshotInterval);
|
||||
this.autoSnapshotInterval = null;
|
||||
}
|
||||
this.showToast('自动快照已禁用');
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('updateSceneData')!.addEventListener('click', () => {
|
||||
const gameTime = parseInt((document.getElementById('gameTime') as HTMLInputElement).value);
|
||||
const score = parseInt((document.getElementById('score') as HTMLInputElement).value);
|
||||
|
||||
this.scene.sceneData.set('gameTime', gameTime);
|
||||
this.scene.sceneData.set('score', score);
|
||||
|
||||
this.addToHistory('更新场景数据');
|
||||
this.showToast('场景数据已更新');
|
||||
});
|
||||
}
|
||||
|
||||
private updateHistoryPanel() {
|
||||
const panel = document.getElementById('historyPanel')!;
|
||||
|
||||
if (this.incrementalHistory.length === 0) {
|
||||
panel.innerHTML = '暂无历史记录';
|
||||
return;
|
||||
}
|
||||
|
||||
panel.innerHTML = this.incrementalHistory.map((item, index) => {
|
||||
const isLatest = index === this.incrementalHistory.length - 1;
|
||||
const time = new Date(item.timestamp).toLocaleTimeString();
|
||||
|
||||
return `
|
||||
<div class="history-item" data-index="${index}" style="
|
||||
padding: 8px;
|
||||
margin: 4px 0;
|
||||
background: ${isLatest ? 'rgba(74, 158, 255, 0.2)' : 'rgba(136, 146, 176, 0.1)'};
|
||||
border-left: 3px solid ${isLatest ? '#4a9eff' : '#8892b0'};
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<div>
|
||||
<strong>${item.label}</strong>
|
||||
${isLatest ? '<span style="color: #4a9eff; margin-left: 8px;">●</span>' : ''}
|
||||
</div>
|
||||
<small style="color: #8892b0;">${time}</small>
|
||||
</div>
|
||||
<div style="font-size: 11px; color: #8892b0; margin-top: 4px;">
|
||||
实体: +${item.stats.addedEntities} -${item.stats.removedEntities} ~${item.stats.updatedEntities} |
|
||||
组件: +${item.stats.addedComponents} -${item.stats.removedComponents} ~${item.stats.updatedComponents}
|
||||
</div>
|
||||
<div style="font-size: 11px; color: #8892b0; margin-top: 2px;">
|
||||
JSON: ${this.formatBytes(item.jsonSize)} |
|
||||
Binary: ${this.formatBytes(item.binarySize)} |
|
||||
<span style="color: #4ecdc4;">节省: ${((1 - item.binarySize / item.jsonSize) * 100).toFixed(1)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// 绑定点击事件
|
||||
panel.querySelectorAll('.history-item').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
const index = parseInt(item.getAttribute('data-index')!);
|
||||
this.showSnapshotDetails(index);
|
||||
});
|
||||
});
|
||||
|
||||
// 自动滚动到底部
|
||||
panel.scrollTop = panel.scrollHeight;
|
||||
}
|
||||
|
||||
private showSnapshotDetails(index: number) {
|
||||
const item = this.incrementalHistory[index];
|
||||
const detailsPanel = document.getElementById('snapshotDetails')!;
|
||||
|
||||
const compressionRatio = ((1 - item.binarySize / item.jsonSize) * 100).toFixed(1);
|
||||
|
||||
const details = {
|
||||
版本: item.incremental.version,
|
||||
基础版本: item.incremental.baseVersion,
|
||||
时间戳: new Date(item.incremental.timestamp).toLocaleString(),
|
||||
场景名称: item.incremental.sceneName,
|
||||
格式对比: {
|
||||
JSON大小: this.formatBytes(item.jsonSize),
|
||||
二进制大小: this.formatBytes(item.binarySize),
|
||||
压缩率: `${compressionRatio}%`,
|
||||
节省字节: this.formatBytes(item.jsonSize - item.binarySize)
|
||||
},
|
||||
统计: item.stats,
|
||||
实体变更: item.incremental.entityChanges.map((c: any) => ({
|
||||
操作: c.operation,
|
||||
实体ID: c.entityId,
|
||||
实体名称: c.entityName
|
||||
})),
|
||||
组件变更: item.incremental.componentChanges.map((c: any) => ({
|
||||
操作: c.operation,
|
||||
实体ID: c.entityId,
|
||||
组件类型: c.componentType
|
||||
})),
|
||||
场景数据变更: item.incremental.sceneDataChanges.map((c: any) => ({
|
||||
键: c.key,
|
||||
值: c.value,
|
||||
已删除: c.deleted
|
||||
}))
|
||||
};
|
||||
|
||||
detailsPanel.textContent = JSON.stringify(details, null, 2);
|
||||
}
|
||||
|
||||
private updateStats() {
|
||||
document.getElementById('entityCount')!.textContent = this.scene.entities.count.toString();
|
||||
document.getElementById('historyCount')!.textContent = this.incrementalHistory.length.toString();
|
||||
|
||||
if (this.incrementalHistory.length > 0) {
|
||||
const lastItem = this.incrementalHistory[this.incrementalHistory.length - 1];
|
||||
|
||||
document.getElementById('jsonSize')!.textContent = this.formatBytes(lastItem.jsonSize);
|
||||
document.getElementById('binarySize')!.textContent = this.formatBytes(lastItem.binarySize);
|
||||
|
||||
const compressionRatio = ((1 - lastItem.binarySize / lastItem.jsonSize) * 100).toFixed(1);
|
||||
const ratioElement = document.getElementById('compressionRatio')!;
|
||||
ratioElement.textContent = `${compressionRatio}%`;
|
||||
ratioElement.style.color = parseFloat(compressionRatio) > 30 ? '#4ecdc4' : '#ffe66d';
|
||||
|
||||
document.getElementById('totalChanges')!.textContent = lastItem.stats.totalChanges.toString();
|
||||
}
|
||||
}
|
||||
|
||||
private formatBytes(bytes: number): string {
|
||||
if (bytes < 1024) return `${bytes}B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
// RenderSystem会处理渲染
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
if (this.autoSnapshotInterval !== null) {
|
||||
clearInterval(this.autoSnapshotInterval);
|
||||
}
|
||||
super.destroy();
|
||||
}
|
||||
}
|
||||
386
examples/core-demos/src/demos/SerializationDemo.ts
Normal file
386
examples/core-demos/src/demos/SerializationDemo.ts
Normal file
@@ -0,0 +1,386 @@
|
||||
import { DemoBase, DemoInfo } from './DemoBase';
|
||||
import {
|
||||
Component,
|
||||
ECSComponent,
|
||||
Entity,
|
||||
EntitySystem,
|
||||
Matcher,
|
||||
Serializable,
|
||||
Serialize,
|
||||
SerializeAsMap
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
// ===== 组件定义 =====
|
||||
@ECSComponent('SerDemo_Position')
|
||||
@Serializable({ version: 1, typeId: 'SerDemo_Position' })
|
||||
class PositionComponent extends Component {
|
||||
@Serialize() x: number = 0;
|
||||
@Serialize() y: number = 0;
|
||||
constructor(x: number = 0, y: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('SerDemo_Velocity')
|
||||
@Serializable({ version: 1, typeId: 'SerDemo_Velocity' })
|
||||
class VelocityComponent extends Component {
|
||||
@Serialize() vx: number = 0;
|
||||
@Serialize() vy: number = 0;
|
||||
constructor(vx: number = 0, vy: number = 0) {
|
||||
super();
|
||||
this.vx = vx;
|
||||
this.vy = vy;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('SerDemo_Renderable')
|
||||
@Serializable({ version: 1, typeId: 'SerDemo_Renderable' })
|
||||
class RenderableComponent extends Component {
|
||||
@Serialize() color: string = '#ffffff';
|
||||
@Serialize() radius: number = 10;
|
||||
constructor(color: string = '#ffffff', radius: number = 10) {
|
||||
super();
|
||||
this.color = color;
|
||||
this.radius = radius;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('SerDemo_Player')
|
||||
@Serializable({ version: 1, typeId: 'SerDemo_Player' })
|
||||
class PlayerComponent extends Component {
|
||||
@Serialize() name: string = 'Player';
|
||||
@Serialize() level: number = 1;
|
||||
@Serialize() health: number = 100;
|
||||
@SerializeAsMap() inventory: Map<string, number> = new Map();
|
||||
constructor(name: string = 'Player') {
|
||||
super();
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 系统定义 =====
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(PositionComponent, VelocityComponent));
|
||||
}
|
||||
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const [pos, vel] = this.getComponents(entity, PositionComponent, VelocityComponent);
|
||||
|
||||
pos.x += vel.vx;
|
||||
pos.y += vel.vy;
|
||||
|
||||
// 边界反弹
|
||||
if (pos.x < 0 || pos.x > 1200) vel.vx *= -1;
|
||||
if (pos.y < 0 || pos.y > 600) vel.vy *= -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RenderSystem extends EntitySystem {
|
||||
private canvas: HTMLCanvasElement;
|
||||
private ctx: CanvasRenderingContext2D;
|
||||
|
||||
constructor(canvas: HTMLCanvasElement) {
|
||||
super(Matcher.all(PositionComponent, RenderableComponent));
|
||||
this.canvas = canvas;
|
||||
this.ctx = canvas.getContext('2d')!;
|
||||
}
|
||||
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
// 清空画布
|
||||
this.ctx.fillStyle = '#0a0a15';
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
// 渲染所有实体
|
||||
for (const entity of entities) {
|
||||
const [pos, render] = this.getComponents(entity, PositionComponent, RenderableComponent);
|
||||
|
||||
this.ctx.fillStyle = render.color;
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(pos.x, pos.y, render.radius, 0, Math.PI * 2);
|
||||
this.ctx.fill();
|
||||
|
||||
// 如果是玩家,显示名字
|
||||
const player = entity.getComponent(PlayerComponent);
|
||||
if (player) {
|
||||
this.ctx.fillStyle = 'white';
|
||||
this.ctx.font = '12px Arial';
|
||||
this.ctx.textAlign = 'center';
|
||||
this.ctx.fillText(player.name, pos.x, pos.y - render.radius - 5);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class SerializationDemo extends DemoBase {
|
||||
private renderSystem!: RenderSystem;
|
||||
private jsonData: string = '';
|
||||
private binaryData: Buffer | null = null;
|
||||
|
||||
getInfo(): DemoInfo {
|
||||
return {
|
||||
id: 'serialization',
|
||||
name: '场景序列化',
|
||||
description: '演示场景的序列化和反序列化功能,支持JSON和二进制格式',
|
||||
category: '核心功能',
|
||||
icon: '💾'
|
||||
};
|
||||
}
|
||||
|
||||
setup() {
|
||||
// @ECSComponent装饰器会自动注册组件到ComponentRegistry
|
||||
// ComponentRegistry会被序列化系统自动使用,无需手动注册
|
||||
|
||||
// 添加系统
|
||||
this.renderSystem = new RenderSystem(this.canvas);
|
||||
this.scene.addEntityProcessor(new MovementSystem());
|
||||
this.scene.addEntityProcessor(this.renderSystem);
|
||||
|
||||
// 创建初始实体
|
||||
this.createInitialEntities();
|
||||
|
||||
// 创建控制面板
|
||||
this.createControls();
|
||||
}
|
||||
|
||||
private createInitialEntities() {
|
||||
// 创建玩家
|
||||
const player = this.scene.createEntity('Player');
|
||||
player.addComponent(new PositionComponent(600, 300));
|
||||
player.addComponent(new VelocityComponent(2, 1.5));
|
||||
player.addComponent(new RenderableComponent('#4a9eff', 15));
|
||||
const playerComp = new PlayerComponent('Hero');
|
||||
playerComp.level = 5;
|
||||
playerComp.health = 100;
|
||||
playerComp.inventory.set('sword', 1);
|
||||
playerComp.inventory.set('potion', 5);
|
||||
player.addComponent(playerComp);
|
||||
|
||||
// 创建一些随机实体
|
||||
for (let i = 0; i < 5; i++) {
|
||||
this.createRandomEntity();
|
||||
}
|
||||
|
||||
// 设置场景数据
|
||||
this.scene.sceneData.set('weather', 'sunny');
|
||||
this.scene.sceneData.set('gameTime', 12.5);
|
||||
this.scene.sceneData.set('difficulty', 'normal');
|
||||
}
|
||||
|
||||
private createRandomEntity() {
|
||||
const entity = this.scene.createEntity(`Entity_${Date.now()}`);
|
||||
entity.addComponent(new PositionComponent(
|
||||
Math.random() * this.canvas.width,
|
||||
Math.random() * this.canvas.height
|
||||
));
|
||||
entity.addComponent(new VelocityComponent(
|
||||
(Math.random() - 0.5) * 3,
|
||||
(Math.random() - 0.5) * 3
|
||||
));
|
||||
const colors = ['#ff6b6b', '#4ecdc4', '#ffe66d', '#a8dadc', '#f1faee'];
|
||||
entity.addComponent(new RenderableComponent(
|
||||
colors[Math.floor(Math.random() * colors.length)],
|
||||
5 + Math.random() * 10
|
||||
));
|
||||
}
|
||||
|
||||
createControls() {
|
||||
this.controlPanel.innerHTML = `
|
||||
<div class="control-section">
|
||||
<h4>实体控制</h4>
|
||||
<div class="button-group">
|
||||
<button id="addEntity" class="secondary">添加随机实体</button>
|
||||
<button id="clearEntities" class="danger">清空所有实体</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-section">
|
||||
<h4>序列化操作</h4>
|
||||
<div class="button-group">
|
||||
<button id="serializeJSON">序列化为JSON</button>
|
||||
<button id="serializeBinary" class="success">序列化为二进制</button>
|
||||
<button id="deserialize" class="secondary">反序列化恢复</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-section">
|
||||
<h4>本地存储</h4>
|
||||
<div class="button-group">
|
||||
<button id="saveLocal" class="success">保存到LocalStorage</button>
|
||||
<button id="loadLocal" class="secondary">从LocalStorage加载</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-section">
|
||||
<h4>场景数据</h4>
|
||||
<div class="input-group">
|
||||
<label>天气</label>
|
||||
<input type="text" id="weather" value="sunny" placeholder="sunny/rainy/snowy">
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label>游戏时间</label>
|
||||
<input type="number" id="gameTime" value="12.5" step="0.1" min="0" max="24">
|
||||
</div>
|
||||
<button id="updateSceneData" class="secondary">更新场景数据</button>
|
||||
</div>
|
||||
|
||||
<div class="stats-panel">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">实体数量</div>
|
||||
<div class="stat-value" id="entityCount">0</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">JSON大小</div>
|
||||
<div class="stat-value" id="jsonSize">0B</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">二进制大小</div>
|
||||
<div class="stat-value" id="binarySize">0B</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">压缩率</div>
|
||||
<div class="stat-value" id="compressionRatio">0%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-section">
|
||||
<h4>序列化数据预览</h4>
|
||||
<div style="max-height: 200px; overflow-y: auto; background: rgba(0,0,0,0.3); padding: 10px; border-radius: 6px; font-family: monospace; font-size: 11px; color: #8892b0; word-break: break-all;" id="dataPreview">
|
||||
点击序列化按钮查看数据...
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 绑定事件
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
private bindEvents() {
|
||||
document.getElementById('addEntity')!.addEventListener('click', () => {
|
||||
this.createRandomEntity();
|
||||
this.updateStats();
|
||||
this.showToast('添加了一个随机实体');
|
||||
});
|
||||
|
||||
document.getElementById('clearEntities')!.addEventListener('click', () => {
|
||||
this.scene.destroyAllEntities();
|
||||
this.createInitialEntities();
|
||||
this.updateStats();
|
||||
this.showToast('场景已重置');
|
||||
});
|
||||
|
||||
document.getElementById('serializeJSON')!.addEventListener('click', () => {
|
||||
this.jsonData = this.scene.serialize({ format: 'json', pretty: true }) as string;
|
||||
this.updateDataPreview(this.jsonData, 'json');
|
||||
this.updateStats();
|
||||
this.showToast('已序列化为JSON格式');
|
||||
});
|
||||
|
||||
document.getElementById('serializeBinary')!.addEventListener('click', () => {
|
||||
this.binaryData = this.scene.serialize({ format: 'binary' }) as Buffer;
|
||||
const base64 = this.binaryData.toString('base64');
|
||||
this.updateDataPreview(`Binary Data (Base64):\n${base64.substring(0, 500)}...`, 'binary');
|
||||
this.updateStats();
|
||||
this.showToast('已序列化为二进制格式', '🔐');
|
||||
});
|
||||
|
||||
document.getElementById('deserialize')!.addEventListener('click', () => {
|
||||
const data = this.binaryData || this.jsonData;
|
||||
if (!data) {
|
||||
this.showToast('请先执行序列化操作', '⚠️');
|
||||
return;
|
||||
}
|
||||
|
||||
this.scene.deserialize(data, {
|
||||
strategy: 'replace'
|
||||
// componentRegistry会自动从ComponentRegistry获取,无需手动传入
|
||||
});
|
||||
|
||||
this.updateStats();
|
||||
this.showToast('场景已恢复');
|
||||
});
|
||||
|
||||
document.getElementById('saveLocal')!.addEventListener('click', () => {
|
||||
const jsonData = this.scene.serialize({ format: 'json' }) as string;
|
||||
localStorage.setItem('ecs_demo_scene', jsonData);
|
||||
this.showToast('已保存到LocalStorage', '💾');
|
||||
});
|
||||
|
||||
document.getElementById('loadLocal')!.addEventListener('click', () => {
|
||||
const data = localStorage.getItem('ecs_demo_scene');
|
||||
if (!data) {
|
||||
this.showToast('LocalStorage中没有保存的场景', '⚠️');
|
||||
return;
|
||||
}
|
||||
|
||||
this.scene.deserialize(data, {
|
||||
strategy: 'replace'
|
||||
// componentRegistry会自动从ComponentRegistry获取,无需手动传入
|
||||
});
|
||||
|
||||
this.updateStats();
|
||||
this.showToast('已从LocalStorage加载', '📂');
|
||||
});
|
||||
|
||||
document.getElementById('updateSceneData')!.addEventListener('click', () => {
|
||||
const weather = (document.getElementById('weather') as HTMLInputElement).value;
|
||||
const gameTime = parseFloat((document.getElementById('gameTime') as HTMLInputElement).value);
|
||||
|
||||
this.scene.sceneData.set('weather', weather);
|
||||
this.scene.sceneData.set('gameTime', gameTime);
|
||||
|
||||
this.showToast('场景数据已更新');
|
||||
});
|
||||
|
||||
// 初始更新统计
|
||||
this.updateStats();
|
||||
}
|
||||
|
||||
private updateDataPreview(data: string, format: string) {
|
||||
const preview = document.getElementById('dataPreview')!;
|
||||
if (format === 'json') {
|
||||
const truncated = data.length > 1000 ? data.substring(0, 1000) + '\n...(truncated)' : data;
|
||||
preview.textContent = truncated;
|
||||
} else {
|
||||
preview.textContent = data;
|
||||
}
|
||||
}
|
||||
|
||||
private updateStats() {
|
||||
const entityCount = this.scene.entities.count;
|
||||
document.getElementById('entityCount')!.textContent = entityCount.toString();
|
||||
|
||||
// 计算JSON大小
|
||||
if (this.jsonData) {
|
||||
const jsonSize = new Blob([this.jsonData]).size;
|
||||
document.getElementById('jsonSize')!.textContent = this.formatBytes(jsonSize);
|
||||
}
|
||||
|
||||
// 计算二进制大小
|
||||
if (this.binaryData) {
|
||||
const binarySize = this.binaryData.length;
|
||||
document.getElementById('binarySize')!.textContent = this.formatBytes(binarySize);
|
||||
|
||||
// 计算压缩率
|
||||
if (this.jsonData) {
|
||||
const jsonSize = new Blob([this.jsonData]).size;
|
||||
const ratio = ((1 - binarySize / jsonSize) * 100).toFixed(1);
|
||||
document.getElementById('compressionRatio')!.textContent = `${ratio}%`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private formatBytes(bytes: number): string {
|
||||
if (bytes < 1024) return `${bytes}B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
// RenderSystem会处理渲染
|
||||
}
|
||||
}
|
||||
832
examples/core-demos/src/demos/WorkerSystemDemo.ts
Normal file
832
examples/core-demos/src/demos/WorkerSystemDemo.ts
Normal file
@@ -0,0 +1,832 @@
|
||||
import { DemoBase, DemoInfo } from './DemoBase';
|
||||
import { Component, ECSComponent, WorkerEntitySystem, EntitySystem, Matcher, Entity, ECSSystem, PlatformManager, Time } from '@esengine/ecs-framework';
|
||||
import { BrowserAdapter } from '../platform/BrowserAdapter';
|
||||
|
||||
// ============ 组件定义 ============
|
||||
|
||||
@ECSComponent('WorkerDemo_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;
|
||||
}
|
||||
|
||||
set(x: number, y: number): void {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('WorkerDemo_Velocity')
|
||||
class Velocity extends Component {
|
||||
dx: number = 0;
|
||||
dy: number = 0;
|
||||
|
||||
constructor(dx: number = 0, dy: number = 0) {
|
||||
super();
|
||||
this.dx = dx;
|
||||
this.dy = dy;
|
||||
}
|
||||
|
||||
set(dx: number, dy: number): void {
|
||||
this.dx = dx;
|
||||
this.dy = dy;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('WorkerDemo_Physics')
|
||||
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('WorkerDemo_Renderable')
|
||||
class Renderable extends Component {
|
||||
color: string = '#ffffff';
|
||||
size: number = 5;
|
||||
shape: 'circle' | 'square' = 'circle';
|
||||
|
||||
constructor(color: string = '#ffffff', size: number = 5, shape: 'circle' | 'square' = 'circle') {
|
||||
super();
|
||||
this.color = color;
|
||||
this.size = size;
|
||||
this.shape = shape;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('WorkerDemo_Lifetime')
|
||||
class Lifetime extends Component {
|
||||
maxAge: number = 5;
|
||||
currentAge: number = 0;
|
||||
|
||||
constructor(maxAge: number = 5) {
|
||||
super();
|
||||
this.maxAge = maxAge;
|
||||
this.currentAge = 0;
|
||||
}
|
||||
|
||||
isDead(): boolean {
|
||||
return this.currentAge >= this.maxAge;
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 系统定义 ============
|
||||
|
||||
interface PhysicsEntityData {
|
||||
id: number;
|
||||
x: number;
|
||||
y: number;
|
||||
dx: number;
|
||||
dy: number;
|
||||
mass: number;
|
||||
bounce: number;
|
||||
friction: number;
|
||||
radius: number;
|
||||
}
|
||||
|
||||
interface PhysicsConfig {
|
||||
gravity: number;
|
||||
canvasWidth: number;
|
||||
canvasHeight: number;
|
||||
groundFriction: number;
|
||||
}
|
||||
|
||||
@ECSSystem('PhysicsWorkerSystem')
|
||||
class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsEntityData> {
|
||||
private physicsConfig: PhysicsConfig;
|
||||
|
||||
constructor(enableWorker: boolean, canvasWidth: number, canvasHeight: number) {
|
||||
const defaultConfig = {
|
||||
gravity: 100,
|
||||
canvasWidth,
|
||||
canvasHeight,
|
||||
groundFriction: 0.98
|
||||
};
|
||||
|
||||
const isSharedArrayBufferAvailable = typeof SharedArrayBuffer !== 'undefined' && self.crossOriginIsolated;
|
||||
|
||||
super(
|
||||
Matcher.empty().all(Position, Velocity, Physics),
|
||||
{
|
||||
enableWorker,
|
||||
workerCount: isSharedArrayBufferAvailable ? (navigator.hardwareConcurrency || 2) : 1,
|
||||
systemConfig: defaultConfig,
|
||||
useSharedArrayBuffer: true
|
||||
}
|
||||
);
|
||||
|
||||
this.physicsConfig = defaultConfig;
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
protected workerProcess(
|
||||
entities: PhysicsEntityData[],
|
||||
deltaTime: number,
|
||||
systemConfig?: PhysicsConfig
|
||||
): PhysicsEntityData[] {
|
||||
const config = systemConfig || this.physicsConfig;
|
||||
const result = entities.map(e => ({ ...e }));
|
||||
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
const 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 (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) {
|
||||
const nx = dx / distance;
|
||||
const ny = dy / distance;
|
||||
|
||||
const overlap = minDistance - distance;
|
||||
const separationX = nx * overlap * 0.5;
|
||||
const separationY = ny * overlap * 0.5;
|
||||
|
||||
ball1.x -= separationX;
|
||||
ball1.y -= separationY;
|
||||
ball2.x += separationX;
|
||||
ball2.y += separationY;
|
||||
|
||||
const relativeVelocityX = ball2.dx - ball1.dx;
|
||||
const relativeVelocityY = ball2.dy - ball1.dy;
|
||||
const velocityAlongNormal = relativeVelocityX * nx + relativeVelocityY * ny;
|
||||
|
||||
if (velocityAlongNormal > 0) continue;
|
||||
|
||||
const restitution = (ball1.bounce + ball2.bounce) * 0.5;
|
||||
const impulseScalar = -(1 + restitution) * velocityAlongNormal / (1/ball1.mass + 1/ball2.mass);
|
||||
|
||||
const impulseX = impulseScalar * nx;
|
||||
const impulseY = impulseScalar * ny;
|
||||
|
||||
ball1.dx -= impulseX / ball1.mass;
|
||||
ball1.dy -= impulseY / ball1.mass;
|
||||
ball2.dx += impulseX / ball2.mass;
|
||||
ball2.dy += impulseY / ball2.mass;
|
||||
|
||||
const energyLoss = 0.98;
|
||||
ball1.dx *= energyLoss;
|
||||
ball1.dy *= energyLoss;
|
||||
ball2.dx *= energyLoss;
|
||||
ball2.dy *= energyLoss;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected applyResult(entity: Entity, result: PhysicsEntityData): void {
|
||||
if (!entity || !entity.enabled) return;
|
||||
|
||||
const position = entity.getComponent(Position);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
if (!position || !velocity) return;
|
||||
|
||||
position.set(result.x, result.y);
|
||||
velocity.set(result.dx, result.dy);
|
||||
}
|
||||
|
||||
public updatePhysicsConfig(newConfig: Partial<PhysicsConfig>): void {
|
||||
Object.assign(this.physicsConfig, newConfig);
|
||||
this.updateConfig({ systemConfig: this.physicsConfig });
|
||||
}
|
||||
|
||||
public getPhysicsConfig(): PhysicsConfig {
|
||||
return { ...this.physicsConfig };
|
||||
}
|
||||
|
||||
protected getDefaultEntityDataSize(): number {
|
||||
return 9;
|
||||
}
|
||||
|
||||
protected writeEntityToBuffer(entityData: PhysicsEntityData, offset: number): void {
|
||||
const sharedArray = (this as any).sharedFloatArray as Float32Array;
|
||||
if (!sharedArray) return;
|
||||
|
||||
// 在第一个位置存储当前实体数量
|
||||
const currentEntityCount = Math.floor(offset / 9) + 1;
|
||||
sharedArray[0] = currentEntityCount;
|
||||
|
||||
// 数据从索引9开始存储(第一个9个位置用作元数据区域)
|
||||
const dataOffset = offset + 9;
|
||||
sharedArray[dataOffset + 0] = entityData.id;
|
||||
sharedArray[dataOffset + 1] = entityData.x;
|
||||
sharedArray[dataOffset + 2] = entityData.y;
|
||||
sharedArray[dataOffset + 3] = entityData.dx;
|
||||
sharedArray[dataOffset + 4] = entityData.dy;
|
||||
sharedArray[dataOffset + 5] = entityData.mass;
|
||||
sharedArray[dataOffset + 6] = entityData.bounce;
|
||||
sharedArray[dataOffset + 7] = entityData.friction;
|
||||
sharedArray[dataOffset + 8] = entityData.radius;
|
||||
}
|
||||
|
||||
protected readEntityFromBuffer(offset: number): PhysicsEntityData | null {
|
||||
const sharedArray = (this as any).sharedFloatArray as Float32Array;
|
||||
if (!sharedArray) return null;
|
||||
|
||||
// 数据从索引9开始存储
|
||||
const dataOffset = offset + 9;
|
||||
return {
|
||||
id: sharedArray[dataOffset + 0],
|
||||
x: sharedArray[dataOffset + 1],
|
||||
y: sharedArray[dataOffset + 2],
|
||||
dx: sharedArray[dataOffset + 3],
|
||||
dy: sharedArray[dataOffset + 4],
|
||||
mass: sharedArray[dataOffset + 5],
|
||||
bounce: sharedArray[dataOffset + 6],
|
||||
friction: sharedArray[dataOffset + 7],
|
||||
radius: sharedArray[dataOffset + 8]
|
||||
};
|
||||
}
|
||||
|
||||
protected getSharedArrayBufferProcessFunction(): any {
|
||||
return function(sharedFloatArray: Float32Array, startIndex: number, endIndex: number, deltaTime: number, systemConfig?: any) {
|
||||
const config = systemConfig || {
|
||||
gravity: 100,
|
||||
canvasWidth: 800,
|
||||
canvasHeight: 600,
|
||||
groundFriction: 0.98
|
||||
};
|
||||
|
||||
const actualEntityCount = sharedFloatArray[0];
|
||||
|
||||
// 基础物理更新
|
||||
for (let i = startIndex; i < endIndex && i < actualEntityCount; i++) {
|
||||
const offset = i * 9 + 9;
|
||||
|
||||
const id = sharedFloatArray[offset + 0];
|
||||
if (id === 0) continue;
|
||||
|
||||
let x = sharedFloatArray[offset + 1];
|
||||
let y = sharedFloatArray[offset + 2];
|
||||
let dx = sharedFloatArray[offset + 3];
|
||||
let dy = sharedFloatArray[offset + 4];
|
||||
const bounce = sharedFloatArray[offset + 6];
|
||||
const friction = sharedFloatArray[offset + 7];
|
||||
const 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 (let i = startIndex; i < endIndex && i < actualEntityCount; i++) {
|
||||
const offset1 = i * 9 + 9;
|
||||
const id1 = sharedFloatArray[offset1 + 0];
|
||||
if (id1 === 0) continue;
|
||||
|
||||
let x1 = sharedFloatArray[offset1 + 1];
|
||||
let y1 = sharedFloatArray[offset1 + 2];
|
||||
let dx1 = sharedFloatArray[offset1 + 3];
|
||||
let dy1 = sharedFloatArray[offset1 + 4];
|
||||
const mass1 = sharedFloatArray[offset1 + 5];
|
||||
const bounce1 = sharedFloatArray[offset1 + 6];
|
||||
const radius1 = sharedFloatArray[offset1 + 8];
|
||||
|
||||
for (let j = 0; j < actualEntityCount; j++) {
|
||||
if (i === j) continue;
|
||||
|
||||
const offset2 = j * 9 + 9;
|
||||
const id2 = sharedFloatArray[offset2 + 0];
|
||||
if (id2 === 0) continue;
|
||||
|
||||
const x2 = sharedFloatArray[offset2 + 1];
|
||||
const y2 = sharedFloatArray[offset2 + 2];
|
||||
const dx2 = sharedFloatArray[offset2 + 3];
|
||||
const dy2 = sharedFloatArray[offset2 + 4];
|
||||
const mass2 = sharedFloatArray[offset2 + 5];
|
||||
const bounce2 = sharedFloatArray[offset2 + 6];
|
||||
const radius2 = sharedFloatArray[offset2 + 8];
|
||||
|
||||
if (isNaN(x2) || isNaN(y2) || isNaN(radius2) || radius2 <= 0) continue;
|
||||
|
||||
const deltaX = x2 - x1;
|
||||
const deltaY = y2 - y1;
|
||||
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
||||
const minDistance = radius1 + radius2;
|
||||
|
||||
if (distance < minDistance && distance > 0) {
|
||||
const nx = deltaX / distance;
|
||||
const ny = deltaY / distance;
|
||||
|
||||
const overlap = minDistance - distance;
|
||||
const separationX = nx * overlap * 0.5;
|
||||
const separationY = ny * overlap * 0.5;
|
||||
|
||||
x1 -= separationX;
|
||||
y1 -= separationY;
|
||||
|
||||
const relativeVelocityX = dx2 - dx1;
|
||||
const relativeVelocityY = dy2 - dy1;
|
||||
const velocityAlongNormal = relativeVelocityX * nx + relativeVelocityY * ny;
|
||||
|
||||
if (velocityAlongNormal > 0) continue;
|
||||
|
||||
const restitution = (bounce1 + bounce2) * 0.5;
|
||||
const impulseScalar = -(1 + restitution) * velocityAlongNormal / (1/mass1 + 1/mass2);
|
||||
|
||||
const impulseX = impulseScalar * nx;
|
||||
const impulseY = impulseScalar * ny;
|
||||
|
||||
dx1 -= impulseX / mass1;
|
||||
dy1 -= impulseY / mass1;
|
||||
|
||||
const energyLoss = 0.98;
|
||||
dx1 *= energyLoss;
|
||||
dy1 *= energyLoss;
|
||||
}
|
||||
}
|
||||
|
||||
sharedFloatArray[offset1 + 1] = x1;
|
||||
sharedFloatArray[offset1 + 2] = y1;
|
||||
sharedFloatArray[offset1 + 3] = dx1;
|
||||
sharedFloatArray[offset1 + 4] = dy1;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ECSSystem('RenderSystem')
|
||||
class RenderSystem extends EntitySystem {
|
||||
private canvas: HTMLCanvasElement;
|
||||
private ctx: CanvasRenderingContext2D;
|
||||
|
||||
constructor(canvas: HTMLCanvasElement) {
|
||||
super(Matcher.all(Position, Renderable));
|
||||
this.canvas = canvas;
|
||||
this.ctx = canvas.getContext('2d')!;
|
||||
}
|
||||
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
this.ctx.fillStyle = '#000';
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
for (const entity of entities) {
|
||||
const position = this.requireComponent(entity, Position);
|
||||
const renderable = this.requireComponent(entity, Renderable);
|
||||
|
||||
this.ctx.fillStyle = renderable.color;
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(position.x, position.y, renderable.size, 0, Math.PI * 2);
|
||||
this.ctx.fill();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ECSSystem('LifetimeSystem')
|
||||
class LifetimeSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Lifetime));
|
||||
}
|
||||
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
const deltaTime = Time.deltaTime;
|
||||
|
||||
for (const entity of entities) {
|
||||
const lifetime = this.requireComponent(entity, Lifetime);
|
||||
|
||||
lifetime.currentAge += deltaTime;
|
||||
if (lifetime.isDead()) {
|
||||
entity.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Demo类 ============
|
||||
|
||||
export class WorkerSystemDemo extends DemoBase {
|
||||
private physicsSystem!: PhysicsWorkerSystem;
|
||||
private renderSystem!: RenderSystem;
|
||||
private lifetimeSystem!: LifetimeSystem;
|
||||
private currentFPS = 0;
|
||||
private frameCount = 0;
|
||||
private fpsUpdateTime = 0;
|
||||
private elements: { [key: string]: HTMLElement } = {};
|
||||
|
||||
getInfo(): DemoInfo {
|
||||
return {
|
||||
id: 'worker-system',
|
||||
name: 'Worker System',
|
||||
description: '演示 ECS 框架中的多线程物理计算能力',
|
||||
category: '核心功能',
|
||||
icon: '⚙️'
|
||||
};
|
||||
}
|
||||
|
||||
setup(): void {
|
||||
// 注册浏览器平台适配器
|
||||
const browserAdapter = new BrowserAdapter();
|
||||
PlatformManager.getInstance().registerAdapter(browserAdapter);
|
||||
|
||||
// 初始化系统
|
||||
this.physicsSystem = new PhysicsWorkerSystem(true, this.canvas.width, this.canvas.height);
|
||||
this.renderSystem = new RenderSystem(this.canvas);
|
||||
this.lifetimeSystem = new LifetimeSystem();
|
||||
|
||||
this.physicsSystem.updateOrder = 1;
|
||||
this.lifetimeSystem.updateOrder = 2;
|
||||
this.renderSystem.updateOrder = 3;
|
||||
|
||||
this.scene.addSystem(this.physicsSystem);
|
||||
this.scene.addSystem(this.lifetimeSystem);
|
||||
this.scene.addSystem(this.renderSystem);
|
||||
|
||||
// 创建控制面板
|
||||
this.createControls();
|
||||
|
||||
// 初始化UI元素引用
|
||||
this.initializeUIElements();
|
||||
this.bindEvents();
|
||||
|
||||
// 生成初始实体
|
||||
this.spawnInitialEntities(1000);
|
||||
}
|
||||
|
||||
createControls(): void {
|
||||
this.controlPanel.innerHTML = `
|
||||
<div style="background: #2a2a2a; padding: 20px; border-radius: 8px; height: 100%; overflow-y: auto;">
|
||||
<div style="margin-bottom: 15px;">
|
||||
<label style="display: block; margin-bottom: 5px; color: #ccc;">实体数量:</label>
|
||||
<input type="range" id="entityCount" min="100" max="10000" value="1000" step="100"
|
||||
style="width: 100%; margin-bottom: 5px;">
|
||||
<span id="entityCountValue" style="color: #fff;">1000</span>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 15px;">
|
||||
<label style="display: block; margin-bottom: 5px; color: #ccc;">Worker 设置:</label>
|
||||
<button id="toggleWorker" style="width: 100%; padding: 8px; margin-bottom: 5px;
|
||||
background: #4a9eff; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
||||
禁用 Worker
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 15px;">
|
||||
<button id="spawnParticles" style="width: 100%; padding: 8px; margin-bottom: 5px;
|
||||
background: #4a9eff; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
||||
生成粒子爆炸
|
||||
</button>
|
||||
<button id="clearEntities" style="width: 100%; padding: 8px; margin-bottom: 5px;
|
||||
background: #4a9eff; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
||||
清空所有实体
|
||||
</button>
|
||||
<button id="resetDemo" style="width: 100%; padding: 8px;
|
||||
background: #4a9eff; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
||||
重置演示
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 15px;">
|
||||
<label style="display: block; margin-bottom: 5px; color: #ccc;">物理参数:</label>
|
||||
<input type="range" id="gravity" min="0" max="500" value="100" step="10"
|
||||
style="width: 100%; margin-bottom: 5px;">
|
||||
<label style="color: #ccc;">重力: <span id="gravityValue">100</span></label>
|
||||
|
||||
<input type="range" id="friction" min="0" max="100" value="95" step="5"
|
||||
style="width: 100%; margin-top: 10px; margin-bottom: 5px;">
|
||||
<label style="color: #ccc;">摩擦力: <span id="frictionValue">95%</span></label>
|
||||
</div>
|
||||
|
||||
<div style="background: #1a1a1a; padding: 15px; border-radius: 8px; font-family: monospace; font-size: 12px;">
|
||||
<h3 style="margin-top: 0; color: #4a9eff;">性能统计</h3>
|
||||
<div style="margin: 5px 0; color: #ccc;">FPS: <span id="fps" style="color: #4eff4a;">0</span></div>
|
||||
<div style="margin: 5px 0; color: #ccc;">实体数量: <span id="entityCountStat" style="color: #fff;">0</span></div>
|
||||
<div style="margin: 5px 0; color: #ccc;">Worker状态: <span id="workerStatus" style="color: #ff4a4a;">未启用</span></div>
|
||||
<div style="margin: 5px 0; color: #ccc;">Worker负载: <span id="workerLoad" style="color: #fff;">N/A</span></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected render(): void {
|
||||
this.frameCount++;
|
||||
const currentTime = performance.now();
|
||||
|
||||
if (currentTime - this.fpsUpdateTime >= 1000) {
|
||||
this.currentFPS = this.frameCount;
|
||||
this.frameCount = 0;
|
||||
this.fpsUpdateTime = currentTime;
|
||||
}
|
||||
|
||||
this.updateUI();
|
||||
}
|
||||
|
||||
private initializeUIElements(): void {
|
||||
const elementIds = [
|
||||
'entityCount', 'entityCountValue', 'toggleWorker',
|
||||
'gravity', 'gravityValue', 'friction', 'frictionValue', 'spawnParticles',
|
||||
'clearEntities', 'resetDemo', 'fps', 'entityCountStat', 'workerStatus', 'workerLoad'
|
||||
];
|
||||
|
||||
for (const id of elementIds) {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
this.elements[id] = element;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bindEvents(): void {
|
||||
if (this.elements.entityCount && this.elements.entityCountValue) {
|
||||
const slider = this.elements.entityCount as HTMLInputElement;
|
||||
slider.addEventListener('input', () => {
|
||||
this.elements.entityCountValue.textContent = slider.value;
|
||||
});
|
||||
|
||||
slider.addEventListener('change', () => {
|
||||
const count = parseInt(slider.value);
|
||||
this.spawnInitialEntities(count);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.elements.toggleWorker) {
|
||||
this.elements.toggleWorker.addEventListener('click', () => {
|
||||
const workerEnabled = this.toggleWorker();
|
||||
this.elements.toggleWorker.textContent = workerEnabled ? '禁用 Worker' : '启用 Worker';
|
||||
});
|
||||
}
|
||||
|
||||
if (this.elements.gravity && this.elements.gravityValue) {
|
||||
const slider = this.elements.gravity as HTMLInputElement;
|
||||
slider.addEventListener('input', () => {
|
||||
this.elements.gravityValue.textContent = slider.value;
|
||||
});
|
||||
|
||||
slider.addEventListener('change', () => {
|
||||
const gravity = parseInt(slider.value);
|
||||
this.updateWorkerConfig({ gravity });
|
||||
});
|
||||
}
|
||||
|
||||
if (this.elements.friction && this.elements.frictionValue) {
|
||||
const slider = this.elements.friction as HTMLInputElement;
|
||||
slider.addEventListener('input', () => {
|
||||
const value = parseInt(slider.value);
|
||||
this.elements.frictionValue.textContent = `${value}%`;
|
||||
});
|
||||
|
||||
slider.addEventListener('change', () => {
|
||||
const friction = parseInt(slider.value) / 100;
|
||||
this.updateWorkerConfig({ friction });
|
||||
});
|
||||
}
|
||||
|
||||
if (this.elements.spawnParticles) {
|
||||
this.elements.spawnParticles.addEventListener('click', () => {
|
||||
const centerX = this.canvas.width / 2;
|
||||
const centerY = this.canvas.height / 2;
|
||||
this.spawnParticleExplosion(centerX, centerY, 100);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.elements.clearEntities) {
|
||||
this.elements.clearEntities.addEventListener('click', () => {
|
||||
this.clearAllEntities();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.elements.resetDemo) {
|
||||
this.elements.resetDemo.addEventListener('click', () => {
|
||||
(this.elements.entityCount as HTMLInputElement).value = '1000';
|
||||
this.elements.entityCountValue.textContent = '1000';
|
||||
(this.elements.gravity as HTMLInputElement).value = '100';
|
||||
this.elements.gravityValue.textContent = '100';
|
||||
(this.elements.friction as HTMLInputElement).value = '95';
|
||||
this.elements.frictionValue.textContent = '95%';
|
||||
|
||||
this.spawnInitialEntities(1000);
|
||||
this.updateWorkerConfig({ gravity: 100, friction: 0.95 });
|
||||
});
|
||||
}
|
||||
|
||||
this.canvas.addEventListener('click', (event) => {
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
const x = event.clientX - rect.left;
|
||||
const y = event.clientY - rect.top;
|
||||
this.spawnParticleExplosion(x, y, 30);
|
||||
});
|
||||
}
|
||||
|
||||
private updateUI(): void {
|
||||
const workerInfo = this.physicsSystem.getWorkerInfo();
|
||||
|
||||
if (this.elements.fps) {
|
||||
this.elements.fps.textContent = this.currentFPS.toString();
|
||||
}
|
||||
|
||||
if (this.elements.entityCountStat) {
|
||||
this.elements.entityCountStat.textContent = this.scene.entities.count.toString();
|
||||
}
|
||||
|
||||
if (this.elements.workerStatus) {
|
||||
if (workerInfo.enabled) {
|
||||
this.elements.workerStatus.textContent = `启用 (${workerInfo.workerCount} Workers)`;
|
||||
this.elements.workerStatus.style.color = '#4eff4a';
|
||||
} else {
|
||||
this.elements.workerStatus.textContent = '禁用';
|
||||
this.elements.workerStatus.style.color = '#ff4a4a';
|
||||
}
|
||||
}
|
||||
|
||||
if (this.elements.workerLoad) {
|
||||
const entityCount = this.scene.entities.count;
|
||||
if (workerInfo.enabled && entityCount > 0) {
|
||||
const entitiesPerWorker = Math.ceil(entityCount / workerInfo.workerCount);
|
||||
this.elements.workerLoad.textContent = `${entitiesPerWorker}/Worker (共${workerInfo.workerCount}个)`;
|
||||
} else {
|
||||
this.elements.workerLoad.textContent = 'N/A';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private spawnInitialEntities(count: number = 1000): void {
|
||||
this.clearAllEntities();
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
this.createParticle();
|
||||
}
|
||||
}
|
||||
|
||||
private createParticle(): void {
|
||||
const entity = this.scene.createEntity(`Particle_${Date.now()}_${Math.random()}`);
|
||||
|
||||
const x = Math.random() * (this.canvas.width - 20) + 10;
|
||||
const y = Math.random() * (this.canvas.height - 20) + 10;
|
||||
const dx = (Math.random() - 0.5) * 200;
|
||||
const dy = (Math.random() - 0.5) * 200;
|
||||
const mass = Math.random() * 3 + 2;
|
||||
const bounce = 0.85 + Math.random() * 0.15;
|
||||
const friction = 0.998 + Math.random() * 0.002;
|
||||
|
||||
const colors = [
|
||||
'#ff4444', '#44ff44', '#4444ff', '#ffff44', '#ff44ff', '#44ffff', '#ffffff',
|
||||
'#ff8844', '#88ff44', '#4488ff', '#ff4488', '#88ff88', '#8888ff', '#ffaa44'
|
||||
];
|
||||
const color = colors[Math.floor(Math.random() * colors.length)];
|
||||
const size = Math.random() * 6 + 3;
|
||||
|
||||
entity.addComponent(new Position(x, y));
|
||||
entity.addComponent(new Velocity(dx, dy));
|
||||
entity.addComponent(new Physics(mass, bounce, friction));
|
||||
entity.addComponent(new Renderable(color, size, 'circle'));
|
||||
entity.addComponent(new Lifetime(5 + Math.random() * 10));
|
||||
}
|
||||
|
||||
private spawnParticleExplosion(centerX: number, centerY: number, count: number = 50): void {
|
||||
for (let i = 0; i < count; i++) {
|
||||
const entity = this.scene.createEntity(`Explosion_${Date.now()}_${i}`);
|
||||
|
||||
const angle = (Math.PI * 2 * i) / count + (Math.random() - 0.5) * 0.5;
|
||||
const distance = Math.random() * 30;
|
||||
const x = centerX + Math.cos(angle) * distance;
|
||||
const y = centerY + Math.sin(angle) * distance;
|
||||
|
||||
const speed = 100 + Math.random() * 150;
|
||||
const dx = Math.cos(angle) * speed;
|
||||
const dy = Math.sin(angle) * speed;
|
||||
const mass = 0.5 + Math.random() * 1;
|
||||
const bounce = 0.8 + Math.random() * 0.2;
|
||||
|
||||
const colors = ['#ffaa00', '#ff6600', '#ff0066', '#ff3300', '#ffff00'];
|
||||
const color = colors[Math.floor(Math.random() * colors.length)];
|
||||
const size = Math.random() * 4 + 2;
|
||||
|
||||
entity.addComponent(new Position(x, y));
|
||||
entity.addComponent(new Velocity(dx, dy));
|
||||
entity.addComponent(new Physics(mass, bounce, 0.999));
|
||||
entity.addComponent(new Renderable(color, size, 'circle'));
|
||||
entity.addComponent(new Lifetime(2 + Math.random() * 3));
|
||||
}
|
||||
}
|
||||
|
||||
private clearAllEntities(): void {
|
||||
const entities = [...this.scene.entities.buffer];
|
||||
for (const entity of entities) {
|
||||
entity.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
private toggleWorker(): boolean {
|
||||
const workerInfo = this.physicsSystem.getWorkerInfo();
|
||||
const newWorkerEnabled = !workerInfo.enabled;
|
||||
|
||||
// 保存当前物理配置
|
||||
const currentConfig = this.physicsSystem.getPhysicsConfig();
|
||||
|
||||
this.scene.removeSystem(this.physicsSystem);
|
||||
this.physicsSystem = new PhysicsWorkerSystem(newWorkerEnabled, this.canvas.width, this.canvas.height);
|
||||
this.physicsSystem.updateOrder = 1;
|
||||
|
||||
// 恢复物理配置
|
||||
this.physicsSystem.updatePhysicsConfig(currentConfig);
|
||||
|
||||
this.scene.addSystem(this.physicsSystem);
|
||||
|
||||
return newWorkerEnabled;
|
||||
}
|
||||
|
||||
private updateWorkerConfig(config: { gravity?: number; friction?: number }): void {
|
||||
if (config.gravity !== undefined || config.friction !== undefined) {
|
||||
const physicsConfig = this.physicsSystem.getPhysicsConfig();
|
||||
this.physicsSystem.updatePhysicsConfig({
|
||||
gravity: config.gravity ?? physicsConfig.gravity,
|
||||
groundFriction: config.friction ?? physicsConfig.groundFriction
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
13
examples/core-demos/src/demos/index.ts
Normal file
13
examples/core-demos/src/demos/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { DemoBase } from './DemoBase';
|
||||
import { SerializationDemo } from './SerializationDemo';
|
||||
import { IncrementalSerializationDemo } from './IncrementalSerializationDemo';
|
||||
import { WorkerSystemDemo } from './WorkerSystemDemo';
|
||||
|
||||
export { DemoBase, SerializationDemo, IncrementalSerializationDemo, WorkerSystemDemo };
|
||||
|
||||
// Demo注册表
|
||||
export const DEMO_REGISTRY: typeof DemoBase[] = [
|
||||
SerializationDemo,
|
||||
IncrementalSerializationDemo,
|
||||
WorkerSystemDemo
|
||||
];
|
||||
171
examples/core-demos/src/main.ts
Normal file
171
examples/core-demos/src/main.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { DEMO_REGISTRY, DemoBase } from './demos';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
class DemoManager {
|
||||
private demos: Map<string, typeof DemoBase> = new Map();
|
||||
private currentDemo: DemoBase | null = null;
|
||||
private canvas: HTMLCanvasElement;
|
||||
private controlPanel: HTMLElement;
|
||||
|
||||
constructor() {
|
||||
// 初始化ECS Core
|
||||
Core.create({
|
||||
debug: true,
|
||||
enableEntitySystems: true
|
||||
});
|
||||
|
||||
this.canvas = document.getElementById('demoCanvas') as HTMLCanvasElement;
|
||||
this.controlPanel = document.getElementById('controlPanel') as HTMLElement;
|
||||
|
||||
// 注册所有demos
|
||||
for (const DemoClass of DEMO_REGISTRY) {
|
||||
const tempInstance = new DemoClass(this.canvas, this.controlPanel);
|
||||
const info = tempInstance.getInfo();
|
||||
this.demos.set(info.id, DemoClass);
|
||||
tempInstance.destroy();
|
||||
}
|
||||
|
||||
// 渲染demo列表
|
||||
this.renderDemoList();
|
||||
|
||||
// 自动加载第一个demo
|
||||
const firstDemo = DEMO_REGISTRY[0];
|
||||
if (firstDemo) {
|
||||
const tempInstance = new firstDemo(this.canvas, this.controlPanel);
|
||||
const info = tempInstance.getInfo();
|
||||
tempInstance.destroy();
|
||||
this.loadDemo(info.id);
|
||||
}
|
||||
}
|
||||
|
||||
private renderDemoList() {
|
||||
const demoList = document.getElementById('demoList')!;
|
||||
|
||||
// 按分类组织demos
|
||||
const categories = new Map<string, typeof DemoBase[]>();
|
||||
|
||||
for (const DemoClass of DEMO_REGISTRY) {
|
||||
const tempInstance = new DemoClass(this.canvas, this.controlPanel);
|
||||
const info = tempInstance.getInfo();
|
||||
tempInstance.destroy();
|
||||
|
||||
if (!categories.has(info.category)) {
|
||||
categories.set(info.category, []);
|
||||
}
|
||||
categories.get(info.category)!.push(DemoClass);
|
||||
}
|
||||
|
||||
// 渲染分类和demos
|
||||
let html = '';
|
||||
for (const [category, demoClasses] of categories) {
|
||||
html += `<div class="demo-category">`;
|
||||
html += `<div class="category-title">${category}</div>`;
|
||||
|
||||
for (const DemoClass of demoClasses) {
|
||||
const tempInstance = new DemoClass(this.canvas, this.controlPanel);
|
||||
const info = tempInstance.getInfo();
|
||||
tempInstance.destroy();
|
||||
|
||||
html += `
|
||||
<div class="demo-item" data-demo-id="${info.id}">
|
||||
<div class="demo-icon">${info.icon}</div>
|
||||
<div class="demo-info">
|
||||
<div class="demo-name">${info.name}</div>
|
||||
<div class="demo-desc">${info.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
html += `</div>`;
|
||||
}
|
||||
|
||||
demoList.innerHTML = html;
|
||||
|
||||
// 绑定点击事件
|
||||
demoList.querySelectorAll('.demo-item').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
const demoId = item.getAttribute('data-demo-id')!;
|
||||
this.loadDemo(demoId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private loadDemo(demoId: string) {
|
||||
// 停止并销毁当前demo
|
||||
if (this.currentDemo) {
|
||||
this.currentDemo.destroy();
|
||||
this.currentDemo = null;
|
||||
}
|
||||
|
||||
// 显示加载动画
|
||||
const loading = document.getElementById('loading')!;
|
||||
loading.style.display = 'block';
|
||||
|
||||
// 延迟加载,给用户反馈
|
||||
setTimeout(() => {
|
||||
const DemoClass = this.demos.get(demoId);
|
||||
if (!DemoClass) {
|
||||
console.error(`Demo ${demoId} not found`);
|
||||
loading.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 创建新demo
|
||||
this.currentDemo = new DemoClass(this.canvas, this.controlPanel);
|
||||
const info = this.currentDemo.getInfo();
|
||||
|
||||
// 更新页面标题和描述
|
||||
document.getElementById('demoTitle')!.textContent = info.name;
|
||||
document.getElementById('demoDescription')!.textContent = info.description;
|
||||
|
||||
// 设置demo
|
||||
this.currentDemo.setup();
|
||||
|
||||
// 显示控制面板
|
||||
this.controlPanel.style.display = 'block';
|
||||
|
||||
// 启动demo
|
||||
this.currentDemo.start();
|
||||
|
||||
// 更新菜单选中状态
|
||||
document.querySelectorAll('.demo-item').forEach(item => {
|
||||
item.classList.remove('active');
|
||||
if (item.getAttribute('data-demo-id') === demoId) {
|
||||
item.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
loading.style.display = 'none';
|
||||
|
||||
console.log(`✅ Demo "${info.name}" loaded successfully`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to load demo ${demoId}:`, error);
|
||||
loading.style.display = 'none';
|
||||
this.showError('加载演示失败:' + (error as Error).message);
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
private showError(message: string) {
|
||||
const toast = document.getElementById('toast')!;
|
||||
const toastMessage = document.getElementById('toastMessage')!;
|
||||
const toastIcon = toast.querySelector('.toast-icon')!;
|
||||
|
||||
toastIcon.textContent = '❌';
|
||||
toastMessage.textContent = message;
|
||||
toast.style.borderColor = '#f5576c';
|
||||
|
||||
toast.classList.add('show');
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('show');
|
||||
toast.style.borderColor = '#667eea';
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
new DemoManager();
|
||||
});
|
||||
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
@@ -14,12 +15,7 @@
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
"emitDecoratorMetadata": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
"include": ["src"]
|
||||
}
|
||||
15
examples/core-demos/vite.config.ts
Normal file
15
examples/core-demos/vite.config.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
server: {
|
||||
port: 3003,
|
||||
headers: {
|
||||
'Cross-Origin-Opener-Policy': 'same-origin',
|
||||
'Cross-Origin-Embedder-Policy': 'require-corp'
|
||||
}
|
||||
},
|
||||
build: {
|
||||
target: 'es2020',
|
||||
outDir: 'dist'
|
||||
}
|
||||
});
|
||||
@@ -1,177 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ECS Framework Worker System Demo</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
font-family: Arial, sans-serif;
|
||||
background: #1a1a1a;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #4a9eff;
|
||||
}
|
||||
|
||||
.demo-area {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.canvas-container {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#gameCanvas {
|
||||
border: 2px solid #4a9eff;
|
||||
background: #000;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.controls {
|
||||
width: 300px;
|
||||
background: #2a2a2a;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.control-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.control-group input, .control-group button {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
margin-bottom: 5px;
|
||||
border: 1px solid #555;
|
||||
background: #3a3a3a;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.control-group button {
|
||||
background: #4a9eff;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.control-group button:hover {
|
||||
background: #3a8eef;
|
||||
}
|
||||
|
||||
.control-group button:disabled {
|
||||
background: #555;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.stats {
|
||||
background: #2a2a2a;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.stats h3 {
|
||||
margin-top: 0;
|
||||
color: #4a9eff;
|
||||
}
|
||||
|
||||
.stat-line {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.worker-enabled {
|
||||
color: #4eff4a;
|
||||
}
|
||||
|
||||
.worker-disabled {
|
||||
color: #ff4a4a;
|
||||
}
|
||||
|
||||
.performance-high {
|
||||
color: #4eff4a;
|
||||
}
|
||||
|
||||
.performance-medium {
|
||||
color: #ffff4a;
|
||||
}
|
||||
|
||||
.performance-low {
|
||||
color: #ff4a4a;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>ECS Framework Worker System 演示</h1>
|
||||
|
||||
<div class="demo-area">
|
||||
<div class="canvas-container">
|
||||
<canvas id="gameCanvas" width="800" height="600"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<div class="control-group">
|
||||
<label>实体数量:</label>
|
||||
<input type="range" id="entityCount" min="100" max="10000" value="1000" step="100">
|
||||
<span id="entityCountValue">1000</span>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Worker 设置:</label>
|
||||
<button id="toggleWorker">禁用 Worker</button>
|
||||
<button id="toggleSAB">禁用 SharedArrayBuffer</button>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<button id="spawnParticles">生成粒子系统</button>
|
||||
<button id="clearEntities">清空所有实体</button>
|
||||
<button id="resetDemo">重置演示</button>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>物理参数:</label>
|
||||
<input type="range" id="gravity" min="0" max="500" value="100" step="10">
|
||||
<label>重力: <span id="gravityValue">100</span></label>
|
||||
|
||||
<input type="range" id="friction" min="0" max="100" value="95" step="5">
|
||||
<label>摩擦力: <span id="frictionValue">95%</span></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<h3>性能统计</h3>
|
||||
<div class="stat-line">FPS: <span id="fps">0</span></div>
|
||||
<div class="stat-line">实体数量: <span id="entityCountStat">0</span></div>
|
||||
<div class="stat-line">Worker状态: <span id="workerStatus" class="worker-disabled">未启用</span></div>
|
||||
<div class="stat-line">Worker负载: <span id="workerLoad">N/A</span></div>
|
||||
<div class="stat-line">运行模式: <span id="sabStatus" class="worker-disabled">同步模式</span></div>
|
||||
<div class="stat-line">物理系统耗时: <span id="physicsTime">0</span>ms</div>
|
||||
<div class="stat-line">渲染系统耗时: <span id="renderTime">0</span>ms</div>
|
||||
<div class="stat-line">总帧时间: <span id="frameTime">0</span>ms</div>
|
||||
<div class="stat-line">内存使用: <span id="memoryUsage">0</span>MB</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,187 +0,0 @@
|
||||
import { Scene } from '@esengine/ecs-framework';
|
||||
import { PhysicsWorkerSystem, RenderSystem, LifetimeSystem } from './systems';
|
||||
import { Position, Velocity, Physics, Renderable, Lifetime } from './components';
|
||||
|
||||
export class GameScene extends Scene {
|
||||
private canvas: HTMLCanvasElement;
|
||||
private physicsSystem!: PhysicsWorkerSystem;
|
||||
private renderSystem!: RenderSystem;
|
||||
private lifetimeSystem!: LifetimeSystem;
|
||||
|
||||
constructor(canvas: HTMLCanvasElement) {
|
||||
super();
|
||||
this.canvas = canvas;
|
||||
}
|
||||
|
||||
override initialize(): void {
|
||||
this.name = "WorkerDemoScene";
|
||||
|
||||
// 创建系统
|
||||
this.physicsSystem = new PhysicsWorkerSystem(true); // 默认启用Worker
|
||||
this.renderSystem = new RenderSystem(this.canvas);
|
||||
this.lifetimeSystem = new LifetimeSystem();
|
||||
|
||||
// 设置系统执行顺序
|
||||
this.physicsSystem.updateOrder = 1;
|
||||
this.lifetimeSystem.updateOrder = 2;
|
||||
this.renderSystem.updateOrder = 3;
|
||||
|
||||
// 添加系统到场景
|
||||
this.addSystem(this.physicsSystem);
|
||||
this.addSystem(this.lifetimeSystem);
|
||||
this.addSystem(this.renderSystem);
|
||||
}
|
||||
|
||||
override onStart(): void {
|
||||
console.log("Worker演示场景已启动");
|
||||
this.spawnInitialEntities();
|
||||
}
|
||||
|
||||
override unload(): void {
|
||||
console.log("Worker演示场景已卸载");
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成初始实体
|
||||
*/
|
||||
public spawnInitialEntities(count: number = 1000): void {
|
||||
this.clearAllEntities();
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
this.createParticle();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个粒子实体
|
||||
*/
|
||||
public createParticle(): void {
|
||||
const entity = this.createEntity(`Particle_${Date.now()}_${Math.random()}`);
|
||||
|
||||
// 随机位置
|
||||
const x = Math.random() * (this.canvas.width - 20) + 10;
|
||||
const y = Math.random() * (this.canvas.height - 20) + 10;
|
||||
|
||||
// 随机速度
|
||||
const dx = (Math.random() - 0.5) * 200;
|
||||
const dy = (Math.random() - 0.5) * 200;
|
||||
|
||||
const mass = Math.random() * 3 + 2;
|
||||
const bounce = 0.85 + Math.random() * 0.15;
|
||||
const friction = 0.998 + Math.random() * 0.002;
|
||||
|
||||
// 随机颜色和大小 - 增加更多颜色提高多样性
|
||||
const colors = [
|
||||
'#ff4444', '#44ff44', '#4444ff', '#ffff44', '#ff44ff', '#44ffff', '#ffffff',
|
||||
'#ff8844', '#88ff44', '#4488ff', '#ff4488', '#88ff88', '#8888ff', '#ffaa44',
|
||||
'#aaff44', '#44aaff', '#ff44aa', '#aa44ff', '#44ffaa', '#cccccc'
|
||||
];
|
||||
const color = colors[Math.floor(Math.random() * colors.length)];
|
||||
const size = Math.random() * 6 + 3;
|
||||
|
||||
// 添加组件
|
||||
entity.addComponent(new Position(x, y));
|
||||
entity.addComponent(new Velocity(dx, dy));
|
||||
entity.addComponent(new Physics(mass, bounce, friction));
|
||||
entity.addComponent(new Renderable(color, size, 'circle'));
|
||||
entity.addComponent(new Lifetime(5 + Math.random() * 10)); // 5-15秒生命周期
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成粒子爆发效果
|
||||
*/
|
||||
public spawnParticleExplosion(centerX: number, centerY: number, count: number = 50): void {
|
||||
for (let i = 0; i < count; i++) {
|
||||
const entity = this.createEntity(`Explosion_${Date.now()}_${i}`);
|
||||
|
||||
// 在中心点周围随机分布
|
||||
const angle = (Math.PI * 2 * i) / count + (Math.random() - 0.5) * 0.5;
|
||||
const distance = Math.random() * 30;
|
||||
const x = centerX + Math.cos(angle) * distance;
|
||||
const y = centerY + Math.sin(angle) * distance;
|
||||
|
||||
// 爆炸速度
|
||||
const speed = 100 + Math.random() * 150;
|
||||
const dx = Math.cos(angle) * speed;
|
||||
const dy = Math.sin(angle) * speed;
|
||||
|
||||
const mass = 0.5 + Math.random() * 1;
|
||||
const bounce = 0.8 + Math.random() * 0.2;
|
||||
|
||||
// 亮色
|
||||
const colors = ['#ffaa00', '#ff6600', '#ff0066', '#ff3300', '#ffff00'];
|
||||
const color = colors[Math.floor(Math.random() * colors.length)];
|
||||
const size = Math.random() * 4 + 2;
|
||||
|
||||
entity.addComponent(new Position(x, y));
|
||||
entity.addComponent(new Velocity(dx, dy));
|
||||
entity.addComponent(new Physics(mass, bounce, 0.999));
|
||||
entity.addComponent(new Renderable(color, size, 'circle'));
|
||||
entity.addComponent(new Lifetime(2 + Math.random() * 3)); // 短生命周期
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有实体
|
||||
*/
|
||||
public clearAllEntities(): void {
|
||||
const entities = [...this.entities.buffer]; // 复制数组避免修改原数组
|
||||
for (const entity of entities) {
|
||||
entity.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换Worker启用状态
|
||||
*/
|
||||
public toggleWorker(): boolean {
|
||||
const workerInfo = this.physicsSystem.getWorkerInfo();
|
||||
const newWorkerEnabled = !workerInfo.enabled;
|
||||
|
||||
// 重新创建物理系统
|
||||
this.removeSystem(this.physicsSystem);
|
||||
this.physicsSystem = new PhysicsWorkerSystem(newWorkerEnabled);
|
||||
this.physicsSystem.updateOrder = 1;
|
||||
this.addSystem(this.physicsSystem);
|
||||
|
||||
return newWorkerEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新Worker配置
|
||||
*/
|
||||
public updateWorkerConfig(config: { gravity?: number; friction?: number }): void {
|
||||
if (config.gravity !== undefined || config.friction !== undefined) {
|
||||
const physicsConfig = this.physicsSystem.getPhysicsConfig();
|
||||
this.physicsSystem.updatePhysicsConfig({
|
||||
gravity: config.gravity ?? physicsConfig.gravity,
|
||||
groundFriction: config.friction ?? physicsConfig.groundFriction
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换 SharedArrayBuffer 状态
|
||||
*/
|
||||
public toggleSharedArrayBuffer(): void {
|
||||
this.physicsSystem.disableSharedArrayBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取物理系统状态
|
||||
*/
|
||||
public getPhysicsSystemStatus() {
|
||||
return this.physicsSystem.getCurrentStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统信息
|
||||
*/
|
||||
public getSystemInfo() {
|
||||
return {
|
||||
physics: this.physicsSystem.getWorkerInfo(),
|
||||
entityCount: this.entities.count,
|
||||
physicsConfig: this.physicsSystem.getPhysicsConfig()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
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;
|
||||
}
|
||||
|
||||
scale(factor: number): void {
|
||||
this.dx *= factor;
|
||||
this.dy *= factor;
|
||||
}
|
||||
}
|
||||
|
||||
// 物理组件
|
||||
@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;
|
||||
shape: 'circle' | 'square' = 'circle';
|
||||
|
||||
constructor(color: string = '#ffffff', size: number = 5, shape: 'circle' | 'square' = 'circle') {
|
||||
super();
|
||||
this.color = color;
|
||||
this.size = size;
|
||||
this.shape = shape;
|
||||
}
|
||||
}
|
||||
|
||||
// 生命周期组件
|
||||
@ECSComponent('Lifetime')
|
||||
export class Lifetime extends Component {
|
||||
maxAge: number = 5;
|
||||
currentAge: number = 0;
|
||||
|
||||
constructor(maxAge: number = 5) {
|
||||
super();
|
||||
this.maxAge = maxAge;
|
||||
this.currentAge = 0;
|
||||
}
|
||||
|
||||
isDead(): boolean {
|
||||
return this.currentAge >= this.maxAge;
|
||||
}
|
||||
}
|
||||
@@ -1,376 +0,0 @@
|
||||
import { Core, PlatformManager } from '@esengine/ecs-framework';
|
||||
import { GameScene } from './GameScene';
|
||||
import { BrowserAdapter } from './platform/BrowserAdapter';
|
||||
|
||||
// 性能监控
|
||||
interface PerformanceStats {
|
||||
fps: number;
|
||||
frameTime: number;
|
||||
physicsTime: number;
|
||||
renderTime: number;
|
||||
memoryUsage: number;
|
||||
}
|
||||
|
||||
class WorkerDemo {
|
||||
private gameScene: GameScene;
|
||||
private canvas: HTMLCanvasElement;
|
||||
private isRunning = false;
|
||||
private lastTime = 0;
|
||||
private frameCount = 0;
|
||||
private fpsUpdateTime = 0;
|
||||
private currentFPS = 0;
|
||||
private lastWorkerStatusUpdate = 0;
|
||||
|
||||
// UI元素
|
||||
private elements: { [key: string]: HTMLElement } = {};
|
||||
|
||||
constructor() {
|
||||
// 注册浏览器适配器
|
||||
const browserAdapter = new BrowserAdapter();
|
||||
PlatformManager.getInstance().registerAdapter(browserAdapter);
|
||||
|
||||
// 获取canvas
|
||||
this.canvas = document.getElementById('gameCanvas') as HTMLCanvasElement;
|
||||
if (!this.canvas) {
|
||||
throw new Error('Canvas element not found');
|
||||
}
|
||||
|
||||
// 初始化UI元素引用
|
||||
this.initializeUIElements();
|
||||
|
||||
// 初始化ECS Core
|
||||
Core.create({
|
||||
debug: true,
|
||||
enableEntitySystems: true
|
||||
});
|
||||
|
||||
// 创建游戏场景
|
||||
this.gameScene = new GameScene(this.canvas);
|
||||
|
||||
// 设置场景
|
||||
Core.setScene(this.gameScene);
|
||||
|
||||
// 绑定事件
|
||||
this.bindEvents();
|
||||
|
||||
// 启动演示
|
||||
this.start();
|
||||
}
|
||||
|
||||
private initializeUIElements(): void {
|
||||
const elementIds = [
|
||||
'entityCount', 'entityCountValue', 'toggleWorker', 'toggleSAB',
|
||||
'gravity', 'gravityValue', 'friction', 'frictionValue', 'spawnParticles',
|
||||
'clearEntities', 'resetDemo', 'fps', 'entityCountStat', 'workerStatus', 'workerLoad',
|
||||
'physicsTime', 'renderTime', 'frameTime', 'memoryUsage', 'sabStatus'
|
||||
];
|
||||
|
||||
for (const id of elementIds) {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
this.elements[id] = element;
|
||||
} else {
|
||||
console.warn(`Element with id '${id}' not found`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bindEvents(): void {
|
||||
// 实体数量滑块
|
||||
if (this.elements.entityCount && this.elements.entityCountValue) {
|
||||
const slider = this.elements.entityCount as HTMLInputElement;
|
||||
slider.addEventListener('input', () => {
|
||||
this.elements.entityCountValue.textContent = slider.value;
|
||||
});
|
||||
|
||||
slider.addEventListener('change', () => {
|
||||
const count = parseInt(slider.value);
|
||||
this.gameScene.spawnInitialEntities(count);
|
||||
});
|
||||
}
|
||||
|
||||
// Worker切换按钮
|
||||
if (this.elements.toggleWorker) {
|
||||
this.elements.toggleWorker.addEventListener('click', () => {
|
||||
const workerEnabled = this.gameScene.toggleWorker();
|
||||
this.elements.toggleWorker.textContent = workerEnabled ? '禁用 Worker' : '启用 Worker';
|
||||
this.updateWorkerStatus();
|
||||
});
|
||||
}
|
||||
|
||||
// SharedArrayBuffer切换按钮
|
||||
if (this.elements.toggleSAB) {
|
||||
this.elements.toggleSAB.addEventListener('click', () => {
|
||||
this.gameScene.toggleSharedArrayBuffer();
|
||||
this.updateWorkerStatus();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 重力滑块
|
||||
if (this.elements.gravity && this.elements.gravityValue) {
|
||||
const slider = this.elements.gravity as HTMLInputElement;
|
||||
slider.addEventListener('input', () => {
|
||||
this.elements.gravityValue.textContent = slider.value;
|
||||
});
|
||||
|
||||
slider.addEventListener('change', () => {
|
||||
const gravity = parseInt(slider.value);
|
||||
this.gameScene.updateWorkerConfig({ gravity });
|
||||
});
|
||||
}
|
||||
|
||||
// 摩擦力滑块
|
||||
if (this.elements.friction && this.elements.frictionValue) {
|
||||
const slider = this.elements.friction as HTMLInputElement;
|
||||
slider.addEventListener('input', () => {
|
||||
const value = parseInt(slider.value);
|
||||
this.elements.frictionValue.textContent = `${value}%`;
|
||||
});
|
||||
|
||||
slider.addEventListener('change', () => {
|
||||
const friction = parseInt(slider.value) / 100;
|
||||
this.gameScene.updateWorkerConfig({ friction });
|
||||
});
|
||||
}
|
||||
|
||||
// 生成粒子按钮
|
||||
if (this.elements.spawnParticles) {
|
||||
this.elements.spawnParticles.addEventListener('click', () => {
|
||||
const centerX = this.canvas.width / 2;
|
||||
const centerY = this.canvas.height / 2;
|
||||
this.gameScene.spawnParticleExplosion(centerX, centerY, 100);
|
||||
});
|
||||
}
|
||||
|
||||
// 清空实体按钮
|
||||
if (this.elements.clearEntities) {
|
||||
this.elements.clearEntities.addEventListener('click', () => {
|
||||
this.gameScene.clearAllEntities();
|
||||
});
|
||||
}
|
||||
|
||||
// 重置演示按钮
|
||||
if (this.elements.resetDemo) {
|
||||
this.elements.resetDemo.addEventListener('click', () => {
|
||||
this.resetDemo();
|
||||
});
|
||||
}
|
||||
|
||||
// Canvas点击事件 - 在点击位置生成粒子爆发
|
||||
this.canvas.addEventListener('click', (event) => {
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
const x = event.clientX - rect.left;
|
||||
const y = event.clientY - rect.top;
|
||||
this.gameScene.spawnParticleExplosion(x, y, 30);
|
||||
});
|
||||
}
|
||||
|
||||
private start(): void {
|
||||
this.isRunning = true;
|
||||
this.lastTime = performance.now();
|
||||
this.gameLoop();
|
||||
console.log('Worker演示已启动');
|
||||
}
|
||||
|
||||
private gameLoop = (): void => {
|
||||
if (!this.isRunning) return;
|
||||
|
||||
const currentTime = performance.now();
|
||||
const deltaTime = (currentTime - this.lastTime) / 1000; // 转换为秒
|
||||
this.lastTime = currentTime;
|
||||
|
||||
// 更新ECS框架
|
||||
const frameStartTime = performance.now();
|
||||
Core.update(deltaTime);
|
||||
const frameEndTime = performance.now();
|
||||
|
||||
// 更新性能统计
|
||||
this.updatePerformanceStats({
|
||||
fps: this.currentFPS,
|
||||
frameTime: frameEndTime - frameStartTime,
|
||||
physicsTime: (window as any).physicsExecutionTime || 0,
|
||||
renderTime: (window as any).renderExecutionTime || 0,
|
||||
memoryUsage: this.getMemoryUsage()
|
||||
});
|
||||
|
||||
// 更新FPS计算
|
||||
this.frameCount++;
|
||||
if (currentTime - this.fpsUpdateTime >= 1000) {
|
||||
this.currentFPS = this.frameCount;
|
||||
this.frameCount = 0;
|
||||
this.fpsUpdateTime = currentTime;
|
||||
}
|
||||
|
||||
// 更新UI
|
||||
this.updateUI();
|
||||
|
||||
// 继续循环
|
||||
requestAnimationFrame(this.gameLoop);
|
||||
};
|
||||
|
||||
private updatePerformanceStats(stats: PerformanceStats): void {
|
||||
if (this.elements.fps) {
|
||||
this.elements.fps.textContent = stats.fps.toString();
|
||||
this.elements.fps.className = stats.fps >= 55 ? 'performance-high' :
|
||||
stats.fps >= 30 ? 'performance-medium' : 'performance-low';
|
||||
}
|
||||
|
||||
if (this.elements.frameTime) {
|
||||
this.elements.frameTime.textContent = stats.frameTime.toFixed(2);
|
||||
this.elements.frameTime.className = stats.frameTime <= 16 ? 'performance-high' :
|
||||
stats.frameTime <= 33 ? 'performance-medium' : 'performance-low';
|
||||
}
|
||||
|
||||
if (this.elements.physicsTime) {
|
||||
this.elements.physicsTime.textContent = stats.physicsTime.toFixed(2);
|
||||
this.elements.physicsTime.className = stats.physicsTime <= 8 ? 'performance-high' :
|
||||
stats.physicsTime <= 16 ? 'performance-medium' : 'performance-low';
|
||||
}
|
||||
|
||||
if (this.elements.renderTime) {
|
||||
this.elements.renderTime.textContent = stats.renderTime.toFixed(2);
|
||||
this.elements.renderTime.className = stats.renderTime <= 8 ? 'performance-high' :
|
||||
stats.renderTime <= 16 ? 'performance-medium' : 'performance-low';
|
||||
}
|
||||
|
||||
if (this.elements.memoryUsage) {
|
||||
this.elements.memoryUsage.textContent = stats.memoryUsage.toFixed(1);
|
||||
}
|
||||
}
|
||||
|
||||
private updateUI(): void {
|
||||
const currentTime = performance.now();
|
||||
const systemInfo = this.gameScene.getSystemInfo();
|
||||
|
||||
// 更新实体数量(每帧更新)
|
||||
if (this.elements.entityCountStat) {
|
||||
this.elements.entityCountStat.textContent = systemInfo.entityCount.toString();
|
||||
}
|
||||
|
||||
// 更新Worker状态(每500ms更新一次即可)
|
||||
if (currentTime - this.lastWorkerStatusUpdate >= 500) {
|
||||
this.updateWorkerStatus();
|
||||
this.lastWorkerStatusUpdate = currentTime;
|
||||
}
|
||||
|
||||
// 更新全局Worker信息供其他系统使用
|
||||
(window as any).workerInfo = systemInfo.physics;
|
||||
}
|
||||
|
||||
private updateWorkerStatus(): void {
|
||||
const systemInfo = this.gameScene.getSystemInfo();
|
||||
const workerInfo = systemInfo.physics;
|
||||
const entityCount = systemInfo.entityCount;
|
||||
const status = this.gameScene.getPhysicsSystemStatus();
|
||||
|
||||
if (this.elements.workerStatus) {
|
||||
if (workerInfo.enabled) {
|
||||
this.elements.workerStatus.textContent = `启用 (${workerInfo.workerCount} Workers)`;
|
||||
this.elements.workerStatus.className = 'worker-enabled';
|
||||
} else {
|
||||
this.elements.workerStatus.textContent = '禁用';
|
||||
this.elements.workerStatus.className = 'worker-disabled';
|
||||
}
|
||||
}
|
||||
|
||||
if (this.elements.workerLoad) {
|
||||
if (workerInfo.enabled && entityCount > 0) {
|
||||
const entitiesPerWorker = Math.ceil(entityCount / workerInfo.workerCount);
|
||||
this.elements.workerLoad.textContent = `${entitiesPerWorker}/Worker (共${workerInfo.workerCount}个)`;
|
||||
} else {
|
||||
this.elements.workerLoad.textContent = 'N/A';
|
||||
}
|
||||
}
|
||||
|
||||
// 更新 SharedArrayBuffer 状态
|
||||
if (this.elements.sabStatus) {
|
||||
const modeNames = {
|
||||
'shared-buffer': 'SharedArrayBuffer模式',
|
||||
'single-worker': '单Worker模式',
|
||||
'multi-worker': '多Worker模式',
|
||||
'sync': '同步模式'
|
||||
};
|
||||
|
||||
this.elements.sabStatus.textContent = modeNames[status.mode] || status.mode;
|
||||
this.elements.sabStatus.className = status.mode === 'shared-buffer' ? 'worker-enabled' : 'worker-disabled';
|
||||
}
|
||||
|
||||
// 更新 SharedArrayBuffer 按钮文本
|
||||
if (this.elements.toggleSAB) {
|
||||
if (status.sharedArrayBufferEnabled) {
|
||||
this.elements.toggleSAB.textContent = '禁用 SharedArrayBuffer';
|
||||
} else {
|
||||
this.elements.toggleSAB.textContent = '启用 SharedArrayBuffer';
|
||||
this.elements.toggleSAB.setAttribute('disabled', 'true'); // SAB 一旦禁用就无法重新启用
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getMemoryUsage(): number {
|
||||
if ('memory' in performance) {
|
||||
const memory = (performance as any).memory;
|
||||
return memory.usedJSHeapSize / (1024 * 1024); // MB
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private resetDemo(): void {
|
||||
// 重置所有控件到默认值
|
||||
if (this.elements.entityCount) {
|
||||
(this.elements.entityCount as HTMLInputElement).value = '1000';
|
||||
this.elements.entityCountValue.textContent = '1000';
|
||||
}
|
||||
|
||||
|
||||
if (this.elements.gravity) {
|
||||
(this.elements.gravity as HTMLInputElement).value = '100';
|
||||
this.elements.gravityValue.textContent = '100';
|
||||
}
|
||||
|
||||
if (this.elements.friction) {
|
||||
(this.elements.friction as HTMLInputElement).value = '95';
|
||||
this.elements.frictionValue.textContent = '95%';
|
||||
}
|
||||
|
||||
// 确保Worker被启用
|
||||
const workerInfo = this.gameScene.getSystemInfo().physics;
|
||||
if (!workerInfo.enabled) {
|
||||
this.gameScene.toggleWorker(); // 只有在禁用时才切换
|
||||
}
|
||||
if (this.elements.toggleWorker) {
|
||||
this.elements.toggleWorker.textContent = '禁用 Worker';
|
||||
}
|
||||
|
||||
// 重新生成实体
|
||||
this.gameScene.spawnInitialEntities(1000);
|
||||
|
||||
// 重置配置
|
||||
this.gameScene.updateWorkerConfig({
|
||||
gravity: 100,
|
||||
friction: 0.95
|
||||
});
|
||||
|
||||
console.log('演示已重置');
|
||||
}
|
||||
|
||||
public stop(): void {
|
||||
this.isRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 启动演示
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
try {
|
||||
new WorkerDemo();
|
||||
} catch (error) {
|
||||
console.error('启动演示失败:', error);
|
||||
document.body.innerHTML = `
|
||||
<div style="padding: 20px; color: red;">
|
||||
<h1>启动失败</h1>
|
||||
<p>错误: ${error}</p>
|
||||
<p>请确保浏览器支持Web Workers和Canvas API</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
import { EntitySystem, Matcher, Entity, ECSSystem, Time } from '@esengine/ecs-framework';
|
||||
import { Lifetime } from '../components';
|
||||
|
||||
@ECSSystem('LifetimeSystem')
|
||||
export class LifetimeSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(Lifetime));
|
||||
}
|
||||
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
const entitiesToRemove: Entity[] = [];
|
||||
|
||||
for (const entity of entities) {
|
||||
const lifetime = entity.getComponent(Lifetime)!;
|
||||
|
||||
// 更新年龄
|
||||
lifetime.currentAge += Time.deltaTime;
|
||||
|
||||
// 检查是否需要销毁
|
||||
if (lifetime.isDead()) {
|
||||
entitiesToRemove.push(entity);
|
||||
}
|
||||
}
|
||||
|
||||
// 销毁过期的实体
|
||||
for (const entity of entitiesToRemove) {
|
||||
entity.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,513 +0,0 @@
|
||||
import { WorkerEntitySystem, Matcher, Entity, ECSSystem, SharedArrayBufferProcessFunction } from '@esengine/ecs-framework';
|
||||
import { Position, Velocity, Physics, Renderable } from '../components';
|
||||
|
||||
interface PhysicsEntityData {
|
||||
id: number;
|
||||
x: number;
|
||||
y: number;
|
||||
dx: number;
|
||||
dy: number;
|
||||
mass: number;
|
||||
bounce: number;
|
||||
friction: number;
|
||||
radius: number;
|
||||
}
|
||||
|
||||
interface PhysicsConfig {
|
||||
gravity: number;
|
||||
canvasWidth: number;
|
||||
canvasHeight: number;
|
||||
groundFriction: number;
|
||||
}
|
||||
|
||||
@ECSSystem('PhysicsWorkerSystem')
|
||||
export class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsEntityData> {
|
||||
private physicsConfig: PhysicsConfig = {
|
||||
gravity: 100,
|
||||
canvasWidth: 800,
|
||||
canvasHeight: 600,
|
||||
groundFriction: 0.98 // 减少地面摩擦
|
||||
};
|
||||
|
||||
constructor(enableWorker: boolean = true) {
|
||||
const defaultConfig = {
|
||||
gravity: 100,
|
||||
canvasWidth: 800,
|
||||
canvasHeight: 600,
|
||||
groundFriction: 0.98
|
||||
};
|
||||
|
||||
// 检查 SharedArrayBuffer 是否可用
|
||||
const isSharedArrayBufferAvailable = typeof SharedArrayBuffer !== 'undefined' && self.crossOriginIsolated;
|
||||
|
||||
super(
|
||||
Matcher.empty().all(Position, Velocity, Physics),
|
||||
{
|
||||
enableWorker,
|
||||
// 当 SharedArrayBuffer 可用时使用多 Worker,否则使用单 Worker 保证碰撞检测完整性
|
||||
workerCount: isSharedArrayBufferAvailable ? (navigator.hardwareConcurrency || 2) : 1,
|
||||
systemConfig: defaultConfig,
|
||||
useSharedArrayBuffer: true // 优先使用 SharedArrayBuffer
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Worker处理函数 - 纯函数,会被序列化到Worker中执行
|
||||
* 注意:这个函数内部不能访问外部变量,必须是纯函数
|
||||
* 非SharedArrayBuffer模式:每个Worker只能看到分配给它的实体批次
|
||||
* 这会导致跨批次的碰撞检测缺失,但单批次内的碰撞是正确的
|
||||
*/
|
||||
protected workerProcess(
|
||||
entities: PhysicsEntityData[],
|
||||
deltaTime: number,
|
||||
systemConfig?: PhysicsConfig
|
||||
): PhysicsEntityData[] {
|
||||
const config = systemConfig || {
|
||||
gravity: 100,
|
||||
canvasWidth: 800,
|
||||
canvasHeight: 600,
|
||||
groundFriction: 0.98
|
||||
};
|
||||
|
||||
// 创建实体副本以避免修改原始数据
|
||||
const result = entities.map(e => ({ ...e }));
|
||||
|
||||
// 应用重力和基础物理
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
const 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 (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) {
|
||||
// 碰撞法线
|
||||
const nx = dx / distance;
|
||||
const ny = dy / distance;
|
||||
|
||||
// 分离小球以避免重叠
|
||||
const overlap = minDistance - distance;
|
||||
const separationX = nx * overlap * 0.5;
|
||||
const separationY = ny * overlap * 0.5;
|
||||
|
||||
ball1.x -= separationX;
|
||||
ball1.y -= separationY;
|
||||
ball2.x += separationX;
|
||||
ball2.y += separationY;
|
||||
|
||||
// 相对速度
|
||||
const relativeVelocityX = ball2.dx - ball1.dx;
|
||||
const relativeVelocityY = ball2.dy - ball1.dy;
|
||||
|
||||
// 沿碰撞法线的速度分量
|
||||
const velocityAlongNormal = relativeVelocityX * nx + relativeVelocityY * ny;
|
||||
|
||||
// 如果速度分量为正,小球正在分离,不需要处理
|
||||
if (velocityAlongNormal > 0) continue;
|
||||
|
||||
// 计算弹性系数(两球弹性的平均值)
|
||||
const restitution = (ball1.bounce + ball2.bounce) * 0.5;
|
||||
|
||||
// 计算冲量大小
|
||||
const impulseScalar = -(1 + restitution) * velocityAlongNormal / (1/ball1.mass + 1/ball2.mass);
|
||||
|
||||
// 应用冲量
|
||||
const impulseX = impulseScalar * nx;
|
||||
const impulseY = impulseScalar * ny;
|
||||
|
||||
ball1.dx -= impulseX / ball1.mass;
|
||||
ball1.dy -= impulseY / ball1.mass;
|
||||
ball2.dx += impulseX / ball2.mass;
|
||||
ball2.dy += impulseY / ball2.mass;
|
||||
|
||||
// 轻微的能量损失,保持活力
|
||||
const energyLoss = 0.98;
|
||||
ball1.dx *= energyLoss;
|
||||
ball1.dy *= energyLoss;
|
||||
ball2.dx *= energyLoss;
|
||||
ball2.dy *= energyLoss;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用处理结果
|
||||
*/
|
||||
protected applyResult(entity: Entity, result: PhysicsEntityData): void {
|
||||
// 检查实体是否仍然存在且有效
|
||||
if (!entity || !entity.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const position = entity.getComponent(Position);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
// 检查组件是否仍然存在(实体可能在Worker处理期间被修改)
|
||||
if (!position || !velocity) {
|
||||
return;
|
||||
}
|
||||
|
||||
position.set(result.x, result.y);
|
||||
velocity.set(result.dx, result.dy);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新物理配置
|
||||
*/
|
||||
public updatePhysicsConfig(newConfig: Partial<PhysicsConfig>): void {
|
||||
Object.assign(this.physicsConfig, newConfig);
|
||||
this.updateConfig({ systemConfig: this.physicsConfig });
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取物理配置
|
||||
*/
|
||||
public getPhysicsConfig(): PhysicsConfig {
|
||||
return { ...this.physicsConfig };
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用 SharedArrayBuffer(用于测试降级行为)
|
||||
*/
|
||||
public disableSharedArrayBuffer(): void {
|
||||
console.log(`[${this.systemName}] Disabling SharedArrayBuffer for testing - falling back to single Worker mode`);
|
||||
|
||||
// 使用正式的配置更新 API
|
||||
this.updateConfig({
|
||||
useSharedArrayBuffer: false
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前运行状态
|
||||
*/
|
||||
public getCurrentStatus(): {
|
||||
mode: 'shared-buffer' | 'single-worker' | 'multi-worker' | 'sync';
|
||||
sharedArrayBufferEnabled: boolean;
|
||||
workerCount: number;
|
||||
workerEnabled: boolean;
|
||||
} {
|
||||
const workerInfo = this.getWorkerInfo();
|
||||
|
||||
let mode: 'shared-buffer' | 'single-worker' | 'multi-worker' | 'sync' = 'sync';
|
||||
|
||||
if (workerInfo.enabled) {
|
||||
if (workerInfo.sharedArrayBufferEnabled && workerInfo.sharedArrayBufferSupported) {
|
||||
mode = 'shared-buffer';
|
||||
} else if (workerInfo.workerCount === 1) {
|
||||
mode = 'single-worker';
|
||||
} else {
|
||||
mode = 'multi-worker';
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
mode,
|
||||
sharedArrayBufferEnabled: workerInfo.sharedArrayBufferEnabled,
|
||||
workerCount: workerInfo.workerCount,
|
||||
workerEnabled: workerInfo.enabled
|
||||
};
|
||||
}
|
||||
|
||||
private startTime: number = 0;
|
||||
|
||||
|
||||
/**
|
||||
* 性能监控
|
||||
*/
|
||||
protected override onEnd(): void {
|
||||
super.onEnd();
|
||||
const endTime = performance.now();
|
||||
const executionTime = endTime - this.startTime;
|
||||
|
||||
// 发送性能数据到UI
|
||||
(window as any).physicsExecutionTime = executionTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取实体数据大小
|
||||
*/
|
||||
protected getDefaultEntityDataSize(): number {
|
||||
return 9; // id, x, y, dx, dy, mass, bounce, friction, radius
|
||||
}
|
||||
|
||||
/**
|
||||
* 将实体数据写入SharedArrayBuffer
|
||||
*/
|
||||
protected writeEntityToBuffer(entityData: PhysicsEntityData, offset: number): void {
|
||||
const sharedArray = (this as any).sharedFloatArray as Float32Array;
|
||||
if (!sharedArray) return;
|
||||
|
||||
// 在第一个位置存储当前实体数量,用于Worker函数判断实际有效数据范围
|
||||
const currentEntityCount = Math.floor(offset / 9) + 1;
|
||||
sharedArray[0] = currentEntityCount; // 元数据:实际实体数量
|
||||
|
||||
// 数据从索引9开始存储(第一个9个位置用作元数据区域)
|
||||
const dataOffset = offset + 9;
|
||||
sharedArray[dataOffset + 0] = entityData.id;
|
||||
sharedArray[dataOffset + 1] = entityData.x;
|
||||
sharedArray[dataOffset + 2] = entityData.y;
|
||||
sharedArray[dataOffset + 3] = entityData.dx;
|
||||
sharedArray[dataOffset + 4] = entityData.dy;
|
||||
sharedArray[dataOffset + 5] = entityData.mass;
|
||||
sharedArray[dataOffset + 6] = entityData.bounce;
|
||||
sharedArray[dataOffset + 7] = entityData.friction;
|
||||
sharedArray[dataOffset + 8] = entityData.radius;
|
||||
}
|
||||
|
||||
/**
|
||||
* 性能监控开始
|
||||
*/
|
||||
protected override onBegin(): void {
|
||||
super.onBegin();
|
||||
this.startTime = performance.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从SharedArrayBuffer读取实体数据
|
||||
*/
|
||||
protected readEntityFromBuffer(offset: number): PhysicsEntityData | null {
|
||||
const sharedArray = (this as any).sharedFloatArray as Float32Array;
|
||||
if (!sharedArray) return null;
|
||||
|
||||
// 数据从索引9开始存储(第一个9个位置用作元数据区域)
|
||||
const dataOffset = offset + 9;
|
||||
return {
|
||||
id: sharedArray[dataOffset + 0],
|
||||
x: sharedArray[dataOffset + 1],
|
||||
y: sharedArray[dataOffset + 2],
|
||||
dx: sharedArray[dataOffset + 3],
|
||||
dy: sharedArray[dataOffset + 4],
|
||||
mass: sharedArray[dataOffset + 5],
|
||||
bounce: sharedArray[dataOffset + 6],
|
||||
friction: sharedArray[dataOffset + 7],
|
||||
radius: sharedArray[dataOffset + 8]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* SharedArrayBuffer处理函数
|
||||
*/
|
||||
protected getSharedArrayBufferProcessFunction(): SharedArrayBufferProcessFunction {
|
||||
return function(sharedFloatArray: Float32Array, startIndex: number, endIndex: number, deltaTime: number, systemConfig?: any) {
|
||||
const config = systemConfig || {
|
||||
gravity: 100,
|
||||
canvasWidth: 800,
|
||||
canvasHeight: 600,
|
||||
groundFriction: 0.98
|
||||
};
|
||||
|
||||
// 读取实际实体数量(存储在第一个位置)
|
||||
const actualEntityCount = sharedFloatArray[0];
|
||||
|
||||
// 基础物理更新
|
||||
for (let i = startIndex; i < endIndex && i < actualEntityCount; i++) {
|
||||
const offset = i * 9 + 9; // 数据从索引9开始,加上元数据偏移
|
||||
|
||||
// 读取实体数据
|
||||
const id = sharedFloatArray[offset + 0];
|
||||
if (id === 0) continue; // 跳过无效实体
|
||||
|
||||
let x = sharedFloatArray[offset + 1];
|
||||
let y = sharedFloatArray[offset + 2];
|
||||
let dx = sharedFloatArray[offset + 3];
|
||||
let dy = sharedFloatArray[offset + 4];
|
||||
// const mass = sharedFloatArray[offset + 5]; // 未使用
|
||||
const bounce = sharedFloatArray[offset + 6];
|
||||
const friction = sharedFloatArray[offset + 7];
|
||||
const 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 (let i = startIndex; i < endIndex && i < actualEntityCount; i++) {
|
||||
const offset1 = i * 9 + 9; // 数据从索引9开始,加上元数据偏移
|
||||
const id1 = sharedFloatArray[offset1 + 0];
|
||||
if (id1 === 0) continue;
|
||||
|
||||
let x1 = sharedFloatArray[offset1 + 1];
|
||||
let y1 = sharedFloatArray[offset1 + 2];
|
||||
let dx1 = sharedFloatArray[offset1 + 3];
|
||||
let dy1 = sharedFloatArray[offset1 + 4];
|
||||
const mass1 = sharedFloatArray[offset1 + 5];
|
||||
const bounce1 = sharedFloatArray[offset1 + 6];
|
||||
const radius1 = sharedFloatArray[offset1 + 8];
|
||||
|
||||
// 检测与所有其他小球的碰撞(能看到所有实体,实现完整碰撞检测)
|
||||
for (let j = 0; j < actualEntityCount; j++) {
|
||||
if (i === j) continue;
|
||||
|
||||
const offset2 = j * 9 + 9; // 数据从索引9开始,加上元数据偏移
|
||||
const id2 = sharedFloatArray[offset2 + 0];
|
||||
if (id2 === 0) continue;
|
||||
|
||||
const x2 = sharedFloatArray[offset2 + 1];
|
||||
const y2 = sharedFloatArray[offset2 + 2];
|
||||
const dx2 = sharedFloatArray[offset2 + 3];
|
||||
const dy2 = sharedFloatArray[offset2 + 4];
|
||||
const mass2 = sharedFloatArray[offset2 + 5];
|
||||
const bounce2 = sharedFloatArray[offset2 + 6];
|
||||
const radius2 = sharedFloatArray[offset2 + 8];
|
||||
|
||||
// 额外检查:确保位置和半径都是有效值
|
||||
if (isNaN(x2) || isNaN(y2) || isNaN(radius2) || radius2 <= 0) continue;
|
||||
|
||||
// 计算距离
|
||||
const deltaX = x2 - x1;
|
||||
const deltaY = y2 - y1;
|
||||
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
||||
const minDistance = radius1 + radius2;
|
||||
|
||||
// 检测碰撞
|
||||
if (distance < minDistance && distance > 0) {
|
||||
// 碰撞法线
|
||||
const nx = deltaX / distance;
|
||||
const ny = deltaY / distance;
|
||||
|
||||
// 分离小球 - 只调整当前Worker负责的球
|
||||
const overlap = minDistance - distance;
|
||||
const separationX = nx * overlap * 0.5;
|
||||
const separationY = ny * overlap * 0.5;
|
||||
|
||||
x1 -= separationX;
|
||||
y1 -= separationY;
|
||||
|
||||
// 相对速度
|
||||
const relativeVelocityX = dx2 - dx1;
|
||||
const relativeVelocityY = dy2 - dy1;
|
||||
|
||||
// 沿碰撞法线的速度分量
|
||||
const velocityAlongNormal = relativeVelocityX * nx + relativeVelocityY * ny;
|
||||
|
||||
// 如果速度分量为正,小球正在分离
|
||||
if (velocityAlongNormal > 0) continue;
|
||||
|
||||
// 弹性系数
|
||||
const restitution = (bounce1 + bounce2) * 0.5;
|
||||
|
||||
// 冲量计算
|
||||
const impulseScalar = -(1 + restitution) * velocityAlongNormal / (1/mass1 + 1/mass2);
|
||||
|
||||
// 应用冲量到当前小球(只更新当前Worker负责的球)
|
||||
const impulseX = impulseScalar * nx;
|
||||
const impulseY = impulseScalar * ny;
|
||||
|
||||
dx1 -= impulseX / mass1;
|
||||
dy1 -= impulseY / mass1;
|
||||
|
||||
// 能量损失
|
||||
const energyLoss = 0.98;
|
||||
dx1 *= energyLoss;
|
||||
dy1 *= energyLoss;
|
||||
}
|
||||
}
|
||||
|
||||
// 只更新当前Worker负责的实体
|
||||
sharedFloatArray[offset1 + 1] = x1;
|
||||
sharedFloatArray[offset1 + 2] = y1;
|
||||
sharedFloatArray[offset1 + 3] = dx1;
|
||||
sharedFloatArray[offset1 + 4] = dy1;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
import { EntitySystem, Matcher, Entity, ECSSystem } from '@esengine/ecs-framework';
|
||||
import { Position, Renderable } from '../components';
|
||||
|
||||
@ECSSystem('RenderSystem')
|
||||
export class RenderSystem extends EntitySystem {
|
||||
private canvas: HTMLCanvasElement;
|
||||
private ctx: CanvasRenderingContext2D;
|
||||
private startTime: number = 0;
|
||||
private batchCount: number = 0;
|
||||
private drawCallCount: number = 0;
|
||||
|
||||
constructor(canvas: HTMLCanvasElement) {
|
||||
super(Matcher.empty().all(Position, Renderable));
|
||||
this.canvas = canvas;
|
||||
this.ctx = canvas.getContext('2d')!;
|
||||
}
|
||||
|
||||
protected override onBegin(): void {
|
||||
super.onBegin();
|
||||
this.startTime = performance.now();
|
||||
|
||||
// 清空画布
|
||||
this.ctx.fillStyle = '#000000';
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
}
|
||||
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
// 保持原始绘制顺序,但优化连续相同颜色的绘制
|
||||
let lastColor = '';
|
||||
this.drawCallCount = 0;
|
||||
|
||||
for (const entity of entities) {
|
||||
const position = entity.getComponent(Position)!;
|
||||
const renderable = entity.getComponent(Renderable)!;
|
||||
|
||||
// 只在颜色变化时设置fillStyle,减少状态切换
|
||||
if (renderable.color !== lastColor) {
|
||||
this.ctx.fillStyle = renderable.color;
|
||||
lastColor = renderable.color;
|
||||
}
|
||||
|
||||
if (renderable.shape === 'circle') {
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(position.x, position.y, renderable.size, 0, Math.PI * 2);
|
||||
this.ctx.fill();
|
||||
this.drawCallCount++;
|
||||
} else if (renderable.shape === 'square') {
|
||||
this.ctx.fillRect(
|
||||
position.x - renderable.size / 2,
|
||||
position.y - renderable.size / 2,
|
||||
renderable.size,
|
||||
renderable.size
|
||||
);
|
||||
this.drawCallCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// 计算颜色多样性用于显示
|
||||
const uniqueColors = new Set(entities.map(e => e.getComponent(Renderable)!.color));
|
||||
this.batchCount = uniqueColors.size;
|
||||
}
|
||||
|
||||
protected override onEnd(): void {
|
||||
super.onEnd();
|
||||
const endTime = performance.now();
|
||||
const executionTime = endTime - this.startTime;
|
||||
|
||||
// 发送性能数据到UI
|
||||
(window as any).renderExecutionTime = executionTime;
|
||||
|
||||
// 绘制调试信息
|
||||
this.drawDebugInfo();
|
||||
}
|
||||
|
||||
private drawDebugInfo(): void {
|
||||
const entities = this.entities;
|
||||
|
||||
this.ctx.fillStyle = '#00ff00';
|
||||
this.ctx.font = '14px Arial';
|
||||
this.ctx.fillText(`实体数量: ${entities.length}`, 10, 20);
|
||||
this.ctx.fillText(`渲染批次: ${this.batchCount}`, 10, 140);
|
||||
this.ctx.fillText(`绘制调用: ${this.drawCallCount}`, 10, 160);
|
||||
|
||||
const workerInfo = (window as any).workerInfo;
|
||||
if (workerInfo) {
|
||||
this.ctx.fillStyle = workerInfo.enabled ? '#00ff00' : '#ff0000';
|
||||
this.ctx.fillText(`Worker: ${workerInfo.enabled ? '启用' : '禁用'}`, 10, 40);
|
||||
|
||||
if (workerInfo.enabled) {
|
||||
this.ctx.fillStyle = '#ffff00';
|
||||
const entitiesPerWorker = Math.ceil(entities.length / workerInfo.workerCount);
|
||||
this.ctx.fillText(`每个Worker实体: ${entitiesPerWorker}`, 10, 60);
|
||||
this.ctx.fillText(`Worker数量: ${workerInfo.workerCount}`, 10, 80);
|
||||
}
|
||||
}
|
||||
|
||||
// 显示性能信息
|
||||
const physicsTime = (window as any).physicsExecutionTime || 0;
|
||||
const renderTime = (window as any).renderExecutionTime || 0;
|
||||
|
||||
this.ctx.fillStyle = physicsTime > 16 ? '#ff0000' : physicsTime > 8 ? '#ffff00' : '#00ff00';
|
||||
this.ctx.fillText(`物理: ${physicsTime.toFixed(2)}ms`, 10, 100);
|
||||
|
||||
this.ctx.fillStyle = renderTime > 16 ? '#ff0000' : renderTime > 8 ? '#ffff00' : '#00ff00';
|
||||
this.ctx.fillText(`渲染: ${renderTime.toFixed(2)}ms`, 10, 120);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export { PhysicsWorkerSystem } from './PhysicsWorkerSystem';
|
||||
export { RenderSystem } from './RenderSystem';
|
||||
export { LifetimeSystem } from './LifetimeSystem';
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
server: {
|
||||
port: 3000,
|
||||
open: true,
|
||||
headers: {
|
||||
'Cross-Origin-Embedder-Policy': 'require-corp',
|
||||
'Cross-Origin-Opener-Policy': 'same-origin'
|
||||
}
|
||||
},
|
||||
build: {
|
||||
target: 'es2020',
|
||||
outDir: 'dist',
|
||||
minify: false,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
format: 'es',
|
||||
manualChunks: undefined
|
||||
}
|
||||
}
|
||||
},
|
||||
esbuild: {
|
||||
target: 'es2020'
|
||||
}
|
||||
});
|
||||
2716
package-lock.json
generated
2716
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@@ -55,7 +55,11 @@
|
||||
"docs:api": "typedoc",
|
||||
"docs:api:watch": "typedoc --watch",
|
||||
"update:worker-demo": "npm run build:core && cd examples/worker-system-demo && npm run build && cd ../.. && npm run copy:worker-demo",
|
||||
"copy:worker-demo": "node scripts/update-worker-demo.js"
|
||||
"copy:worker-demo": "node scripts/update-worker-demo.js",
|
||||
"format": "prettier --write \"packages/**/src/**/*.{ts,tsx,js,jsx}\"",
|
||||
"format:check": "prettier --check \"packages/**/src/**/*.{ts,tsx,js,jsx}\"",
|
||||
"lint": "eslint \"packages/**/src/**/*.{ts,tsx,js,jsx}\"",
|
||||
"lint:fix": "eslint \"packages/**/src/**/*.{ts,tsx,js,jsx}\" --fix"
|
||||
},
|
||||
"author": "yhh",
|
||||
"license": "MIT",
|
||||
@@ -66,9 +70,13 @@
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^20.19.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.46.1",
|
||||
"@typescript-eslint/parser": "^8.46.1",
|
||||
"eslint": "^9.37.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"lerna": "^8.1.8",
|
||||
"prettier": "^3.6.2",
|
||||
"rimraf": "^5.0.0",
|
||||
"rollup": "^4.42.0",
|
||||
"rollup-plugin-dts": "^6.2.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@esengine/ecs-framework",
|
||||
"version": "2.1.51",
|
||||
"version": "2.2.5",
|
||||
"description": "用于Laya、Cocos Creator等JavaScript游戏引擎的高性能ECS框架",
|
||||
"main": "bin/index.js",
|
||||
"types": "bin/index.d.ts",
|
||||
@@ -75,5 +75,9 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/esengine/ecs-framework.git",
|
||||
"directory": "packages/core"
|
||||
},
|
||||
"dependencies": {
|
||||
"@msgpack/msgpack": "^3.0.0",
|
||||
"tslib": "^2.8.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,13 @@ module.exports = [
|
||||
})
|
||||
],
|
||||
external,
|
||||
onwarn(warning, warn) {
|
||||
// 忽略 msgpack-lite 的循环依赖警告
|
||||
if (warning.code === 'CIRCULAR_DEPENDENCY' && warning.ids && warning.ids.some(id => id.includes('msgpack-lite'))) {
|
||||
return;
|
||||
}
|
||||
warn(warning);
|
||||
},
|
||||
treeshake: {
|
||||
moduleSideEffects: false,
|
||||
propertyReadSideEffects: false,
|
||||
@@ -78,6 +85,12 @@ module.exports = [
|
||||
})
|
||||
],
|
||||
external,
|
||||
onwarn(warning, warn) {
|
||||
if (warning.code === 'CIRCULAR_DEPENDENCY' && warning.ids && warning.ids.some(id => id.includes('msgpack-lite'))) {
|
||||
return;
|
||||
}
|
||||
warn(warning);
|
||||
},
|
||||
treeshake: {
|
||||
moduleSideEffects: false
|
||||
}
|
||||
@@ -103,6 +116,12 @@ module.exports = [
|
||||
})
|
||||
],
|
||||
external: [],
|
||||
onwarn(warning, warn) {
|
||||
if (warning.code === 'CIRCULAR_DEPENDENCY' && warning.ids && warning.ids.some(id => id.includes('msgpack-lite'))) {
|
||||
return;
|
||||
}
|
||||
warn(warning);
|
||||
},
|
||||
treeshake: {
|
||||
moduleSideEffects: false
|
||||
}
|
||||
@@ -157,6 +176,12 @@ module.exports = [
|
||||
})
|
||||
],
|
||||
external: [],
|
||||
onwarn(warning, warn) {
|
||||
if (warning.code === 'CIRCULAR_DEPENDENCY' && warning.ids && warning.ids.some(id => id.includes('msgpack-lite'))) {
|
||||
return;
|
||||
}
|
||||
warn(warning);
|
||||
},
|
||||
treeshake: {
|
||||
moduleSideEffects: false
|
||||
}
|
||||
|
||||
@@ -1,149 +1,152 @@
|
||||
import { GlobalManager } from './Utils/GlobalManager';
|
||||
import { TimerManager } from './Utils/Timers/TimerManager';
|
||||
import { ITimer } from './Utils/Timers/ITimer';
|
||||
import { Timer } from './Utils/Timers/Timer';
|
||||
import { Time } from './Utils/Time';
|
||||
import { PerformanceMonitor } from './Utils/PerformanceMonitor';
|
||||
import { PoolManager } from './Utils/Pool/PoolManager';
|
||||
import { ECSFluentAPI, createECSAPI } from './ECS/Core/FluentAPI';
|
||||
import { IScene } from './ECS/IScene';
|
||||
import { WorldManager, IWorldManagerConfig } from './ECS/WorldManager';
|
||||
import { DebugManager } from './Utils/Debug';
|
||||
import { ICoreConfig, IECSDebugConfig } from './Types';
|
||||
import { ICoreConfig, IECSDebugConfig, IUpdatable, isUpdatable } from './Types';
|
||||
import { createLogger } from './Utils/Logger';
|
||||
import { SceneManager } from './ECS/SceneManager';
|
||||
import { IScene } from './ECS/IScene';
|
||||
import { ServiceContainer } from './Core/ServiceContainer';
|
||||
import { PluginManager } from './Core/PluginManager';
|
||||
import { IPlugin } from './Core/Plugin';
|
||||
import { WorldManager } from './ECS/WorldManager';
|
||||
import { DebugConfigService } from './Utils/Debug/DebugConfigService';
|
||||
import { createInstance } from './Core/DI/Decorators';
|
||||
|
||||
/**
|
||||
* 游戏引擎核心类
|
||||
*
|
||||
* 负责管理游戏的生命周期、场景切换、全局管理器和定时器系统。
|
||||
* 提供统一的游戏循环管理。
|
||||
*
|
||||
*
|
||||
* 职责:
|
||||
* - 提供全局服务(Timer、Performance、Pool等)
|
||||
* - 管理场景生命周期(内置SceneManager)
|
||||
* - 管理全局管理器的生命周期
|
||||
* - 提供统一的游戏循环更新入口
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 创建核心实例
|
||||
* const core = Core.create(true);
|
||||
*
|
||||
* // 设置场景
|
||||
* Core.scene = new MyScene();
|
||||
*
|
||||
* // 在游戏循环中更新(Laya引擎示例)
|
||||
* Laya.timer.frameLoop(1, this, () => {
|
||||
* const deltaTime = Laya.timer.delta / 1000;
|
||||
* // 初始化并设置场景
|
||||
* Core.create({ debug: true });
|
||||
* Core.setScene(new GameScene());
|
||||
*
|
||||
* // 游戏循环(自动更新全局服务和场景)
|
||||
* function gameLoop(deltaTime: number) {
|
||||
* Core.update(deltaTime);
|
||||
* });
|
||||
*
|
||||
* // 调度定时器
|
||||
* }
|
||||
*
|
||||
* // 使用定时器
|
||||
* Core.schedule(1.0, false, null, (timer) => {
|
||||
* Core._logger.info("1秒后执行");
|
||||
* console.log("1秒后执行");
|
||||
* });
|
||||
*
|
||||
* // 切换场景
|
||||
* Core.loadScene(new MenuScene()); // 延迟切换
|
||||
* Core.setScene(new GameScene()); // 立即切换
|
||||
*
|
||||
* // 获取当前场景
|
||||
* const currentScene = Core.scene;
|
||||
* ```
|
||||
*/
|
||||
export class Core {
|
||||
/**
|
||||
* 游戏暂停状态
|
||||
*
|
||||
*
|
||||
* 当设置为true时,游戏循环将暂停执行。
|
||||
*/
|
||||
public static paused = false;
|
||||
|
||||
/**
|
||||
* 默认World ID
|
||||
*
|
||||
* 用于单Scene模式的默认World标识
|
||||
*/
|
||||
private static readonly DEFAULT_WORLD_ID = '__default__';
|
||||
|
||||
/**
|
||||
* 默认Scene ID
|
||||
*
|
||||
* 用于单Scene模式的默认Scene标识
|
||||
*/
|
||||
private static readonly DEFAULT_SCENE_ID = '__main__';
|
||||
|
||||
/**
|
||||
* 全局核心实例
|
||||
*
|
||||
* 可能为null表示Core尚未初始化或已被销毁
|
||||
*/
|
||||
private static _instance: Core;
|
||||
private static _instance: Core | null = null;
|
||||
|
||||
/**
|
||||
* Core专用日志器
|
||||
*/
|
||||
private static _logger = createLogger('Core');
|
||||
|
||||
|
||||
/**
|
||||
* 实体系统启用状态
|
||||
*
|
||||
*
|
||||
* 控制是否启用ECS实体系统功能。
|
||||
*/
|
||||
public static entitySystemsEnabled: boolean;
|
||||
|
||||
|
||||
/**
|
||||
* 调试模式标志
|
||||
*
|
||||
*
|
||||
* 在调试模式下会启用额外的性能监控和错误检查。
|
||||
*/
|
||||
public readonly debug: boolean;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 全局管理器集合
|
||||
*
|
||||
* 存储所有注册的全局管理器实例。
|
||||
* 服务容器
|
||||
*
|
||||
* 管理所有服务的注册、解析和生命周期。
|
||||
*/
|
||||
public _globalManagers: GlobalManager[] = [];
|
||||
|
||||
private _serviceContainer: ServiceContainer;
|
||||
|
||||
/**
|
||||
* 定时器管理器
|
||||
*
|
||||
*
|
||||
* 负责管理所有的游戏定时器。
|
||||
*/
|
||||
public _timerManager: TimerManager;
|
||||
|
||||
/**
|
||||
* 性能监控器
|
||||
*
|
||||
*
|
||||
* 监控游戏性能并提供优化建议。
|
||||
*/
|
||||
public _performanceMonitor: PerformanceMonitor;
|
||||
|
||||
/**
|
||||
* 对象池管理器
|
||||
*
|
||||
*
|
||||
* 管理所有对象池的生命周期。
|
||||
*/
|
||||
public _poolManager: PoolManager;
|
||||
|
||||
/**
|
||||
* ECS流式API
|
||||
*
|
||||
* 提供便捷的ECS操作接口。
|
||||
*/
|
||||
public _ecsAPI?: ECSFluentAPI;
|
||||
|
||||
|
||||
/**
|
||||
* 调试管理器
|
||||
*
|
||||
*
|
||||
* 负责收集和发送调试数据。
|
||||
*/
|
||||
public _debugManager?: DebugManager;
|
||||
|
||||
/**
|
||||
* World管理器
|
||||
*
|
||||
* 管理多个World实例,支持多房间/多世界架构。
|
||||
* 场景管理器
|
||||
*
|
||||
* 管理当前场景的生命周期。
|
||||
*/
|
||||
public _worldManager?: WorldManager;
|
||||
private _sceneManager: SceneManager;
|
||||
|
||||
/**
|
||||
* World管理器
|
||||
*
|
||||
* 管理多个独立的World实例(可选)。
|
||||
*/
|
||||
private _worldManager: WorldManager;
|
||||
|
||||
/**
|
||||
* 插件管理器
|
||||
*
|
||||
* 管理所有插件的生命周期。
|
||||
*/
|
||||
private _pluginManager: PluginManager;
|
||||
|
||||
/**
|
||||
* Core配置
|
||||
*/
|
||||
private _config: ICoreConfig;
|
||||
|
||||
|
||||
/**
|
||||
* 创建核心实例
|
||||
*
|
||||
*
|
||||
* @param config - Core配置对象
|
||||
*/
|
||||
private constructor(config: ICoreConfig = {}) {
|
||||
@@ -156,37 +159,68 @@ export class Core {
|
||||
...config
|
||||
};
|
||||
|
||||
// 初始化服务容器
|
||||
this._serviceContainer = new ServiceContainer();
|
||||
|
||||
// 初始化管理器
|
||||
// 初始化定时器管理器
|
||||
this._timerManager = new TimerManager();
|
||||
Core.registerGlobalManager(this._timerManager);
|
||||
this._serviceContainer.registerInstance(TimerManager, this._timerManager);
|
||||
|
||||
// 初始化性能监控器
|
||||
this._performanceMonitor = PerformanceMonitor.instance;
|
||||
|
||||
this._performanceMonitor = new PerformanceMonitor();
|
||||
this._serviceContainer.registerInstance(PerformanceMonitor, this._performanceMonitor);
|
||||
|
||||
// 在调试模式下启用性能监控
|
||||
if (this._config.debug) {
|
||||
this._performanceMonitor.enable();
|
||||
}
|
||||
|
||||
// 初始化对象池管理器
|
||||
this._poolManager = PoolManager.getInstance();
|
||||
|
||||
this._poolManager = new PoolManager();
|
||||
this._serviceContainer.registerInstance(PoolManager, this._poolManager);
|
||||
|
||||
// 初始化场景管理器
|
||||
this._sceneManager = new SceneManager(this._performanceMonitor);
|
||||
this._serviceContainer.registerInstance(SceneManager, this._sceneManager);
|
||||
|
||||
// 设置场景切换回调,通知调试管理器
|
||||
this._sceneManager.setSceneChangedCallback(() => {
|
||||
if (this._debugManager) {
|
||||
this._debugManager.onSceneChanged();
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化World管理器
|
||||
this._worldManager = new WorldManager();
|
||||
this._serviceContainer.registerInstance(WorldManager, this._worldManager);
|
||||
|
||||
// 初始化插件管理器
|
||||
this._pluginManager = new PluginManager();
|
||||
this._pluginManager.initialize(this, this._serviceContainer);
|
||||
this._serviceContainer.registerInstance(PluginManager, this._pluginManager);
|
||||
|
||||
Core.entitySystemsEnabled = this._config.enableEntitySystems ?? true;
|
||||
this.debug = this._config.debug ?? true;
|
||||
|
||||
// 初始化调试管理器
|
||||
if (this._config.debugConfig?.enabled) {
|
||||
this._debugManager = new DebugManager(this, this._config.debugConfig);
|
||||
}
|
||||
const configService = new DebugConfigService();
|
||||
configService.setConfig(this._config.debugConfig);
|
||||
this._serviceContainer.registerInstance(DebugConfigService, configService);
|
||||
|
||||
this._serviceContainer.registerSingleton(DebugManager, (c) =>
|
||||
createInstance(DebugManager, c)
|
||||
);
|
||||
|
||||
this._debugManager = this._serviceContainer.resolve(DebugManager);
|
||||
}
|
||||
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取核心实例
|
||||
*
|
||||
*
|
||||
* @returns 全局核心实例
|
||||
*/
|
||||
public static get Instance() {
|
||||
@@ -194,104 +228,196 @@ export class Core {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前活动的场景(属性访问器)
|
||||
*
|
||||
* @returns 当前场景实例,如果没有则返回null
|
||||
* 获取服务容器
|
||||
*
|
||||
* 用于注册和解析自定义服务。
|
||||
*
|
||||
* @returns 服务容器实例
|
||||
* @throws 如果Core实例未创建
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 注册自定义服务
|
||||
* Core.services.registerSingleton(MyService);
|
||||
*
|
||||
* // 解析服务
|
||||
* const myService = Core.services.resolve(MyService);
|
||||
* ```
|
||||
*/
|
||||
public static get scene(): IScene | null {
|
||||
return this.getScene();
|
||||
public static get services(): ServiceContainer {
|
||||
if (!this._instance) {
|
||||
throw new Error('Core实例未创建,请先调用Core.create()');
|
||||
}
|
||||
return this._instance._serviceContainer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前活动的场景(方法调用)
|
||||
*
|
||||
* @returns 当前场景实例,如果没有则返回null
|
||||
* 获取World管理器
|
||||
*
|
||||
* 用于管理多个独立的World实例(高级用户)。
|
||||
*
|
||||
* @returns WorldManager实例
|
||||
* @throws 如果Core实例未创建
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 创建多个游戏房间
|
||||
* const wm = Core.worldManager;
|
||||
* const room1 = wm.createWorld('room_001');
|
||||
* room1.createScene('game', new GameScene());
|
||||
* room1.start();
|
||||
* ```
|
||||
*/
|
||||
public static getScene<T extends IScene>(): T | null {
|
||||
public static get worldManager(): WorldManager {
|
||||
if (!this._instance) {
|
||||
return null;
|
||||
throw new Error('Core实例未创建,请先调用Core.create()');
|
||||
}
|
||||
|
||||
// 确保默认World存在
|
||||
this._instance.ensureDefaultWorld();
|
||||
|
||||
const defaultWorld = this._instance._worldManager!.getWorld(this.DEFAULT_WORLD_ID);
|
||||
return defaultWorld?.getScene(this.DEFAULT_SCENE_ID) as T || null;
|
||||
return this._instance._worldManager;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 设置当前场景
|
||||
*
|
||||
* @param scene - 要设置的场景实例
|
||||
* @returns 设置的场景实例,便于链式调用
|
||||
*/
|
||||
public static setScene<T extends IScene>(scene: T): T {
|
||||
if (!this._instance) {
|
||||
throw new Error("Core实例未创建,请先调用Core.create()");
|
||||
}
|
||||
|
||||
// 确保默认World存在
|
||||
this._instance.ensureDefaultWorld();
|
||||
|
||||
const defaultWorld = this._instance._worldManager!.getWorld(this.DEFAULT_WORLD_ID)!;
|
||||
|
||||
// 移除旧的主Scene(如果存在)
|
||||
if (defaultWorld.getScene(this.DEFAULT_SCENE_ID)) {
|
||||
defaultWorld.removeScene(this.DEFAULT_SCENE_ID);
|
||||
}
|
||||
|
||||
// 添加新Scene到默认World
|
||||
defaultWorld.createScene(this.DEFAULT_SCENE_ID, scene);
|
||||
defaultWorld.setSceneActive(this.DEFAULT_SCENE_ID, true);
|
||||
|
||||
// 触发场景切换回调
|
||||
this._instance.onSceneChanged();
|
||||
|
||||
return scene;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 创建Core实例
|
||||
*
|
||||
*
|
||||
* 如果实例已存在,则返回现有实例。
|
||||
*
|
||||
*
|
||||
* @param config - Core配置,也可以直接传入boolean表示debug模式(向后兼容)
|
||||
* @returns Core实例
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 方式1:使用配置对象
|
||||
* Core.create({
|
||||
* debug: true,
|
||||
* enableEntitySystems: true,
|
||||
* debugConfig: {
|
||||
* enabled: true,
|
||||
* websocketUrl: 'ws://localhost:9229'
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* // 方式2:简单模式(向后兼容)
|
||||
* Core.create(true); // debug = true
|
||||
* ```
|
||||
*/
|
||||
public static create(config: ICoreConfig | boolean = true): Core {
|
||||
if (this._instance == null) {
|
||||
// 向后兼容:如果传入boolean,转换为配置对象
|
||||
const coreConfig: ICoreConfig = typeof config === 'boolean'
|
||||
const coreConfig: ICoreConfig = typeof config === 'boolean'
|
||||
? { debug: config, enableEntitySystems: true }
|
||||
: config;
|
||||
this._instance = new Core(coreConfig);
|
||||
} else {
|
||||
this._logger.warn('Core实例已创建,返回现有实例');
|
||||
}
|
||||
return this._instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新游戏逻辑
|
||||
*
|
||||
* 此方法应该在游戏引擎的更新循环中调用。
|
||||
*
|
||||
* @param deltaTime - 外部引擎提供的帧时间间隔(秒)
|
||||
*
|
||||
* 设置当前场景
|
||||
*
|
||||
* @param scene - 要设置的场景
|
||||
* @returns 设置的场景实例
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Laya引擎
|
||||
* Core.create({ debug: true });
|
||||
*
|
||||
* // 创建并设置场景
|
||||
* const gameScene = new GameScene();
|
||||
* Core.setScene(gameScene);
|
||||
* ```
|
||||
*/
|
||||
public static setScene<T extends IScene>(scene: T): T {
|
||||
if (!this._instance) {
|
||||
Core._logger.warn("Core实例未创建,请先调用Core.create()");
|
||||
throw new Error("Core实例未创建");
|
||||
}
|
||||
|
||||
return this._instance._sceneManager.setScene(scene);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前场景
|
||||
*
|
||||
* @returns 当前场景,如果没有场景则返回null
|
||||
*/
|
||||
public static get scene(): IScene | null {
|
||||
if (!this._instance) {
|
||||
return null;
|
||||
}
|
||||
return this._instance._sceneManager.currentScene;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取ECS流式API
|
||||
*
|
||||
* @returns ECS API实例,如果当前没有场景则返回null
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 使用流式API创建实体
|
||||
* const player = Core.ecsAPI?.createEntity('Player')
|
||||
* .addComponent(Position, 100, 100)
|
||||
* .addComponent(Velocity, 50, 0);
|
||||
*
|
||||
* // 查询实体
|
||||
* const enemies = Core.ecsAPI?.query(Enemy, Transform);
|
||||
*
|
||||
* // 发射事件
|
||||
* Core.ecsAPI?.emit('game:start', { level: 1 });
|
||||
* ```
|
||||
*/
|
||||
public static get ecsAPI() {
|
||||
if (!this._instance) {
|
||||
return null;
|
||||
}
|
||||
return this._instance._sceneManager.api;
|
||||
}
|
||||
|
||||
/**
|
||||
* 延迟加载场景(下一帧切换)
|
||||
*
|
||||
* @param scene - 要加载的场景
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 延迟切换场景(在下一帧生效)
|
||||
* Core.loadScene(new MenuScene());
|
||||
* ```
|
||||
*/
|
||||
public static loadScene<T extends IScene>(scene: T): void {
|
||||
if (!this._instance) {
|
||||
Core._logger.warn("Core实例未创建,请先调用Core.create()");
|
||||
return;
|
||||
}
|
||||
|
||||
this._instance._sceneManager.loadScene(scene);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新游戏逻辑
|
||||
*
|
||||
* 此方法应该在游戏引擎的更新循环中调用。
|
||||
* 会自动更新全局服务和当前场景。
|
||||
*
|
||||
* @param deltaTime - 外部引擎提供的帧时间间隔(秒)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 初始化
|
||||
* Core.create({ debug: true });
|
||||
* Core.setScene(new GameScene());
|
||||
*
|
||||
* // Laya引擎集成
|
||||
* Laya.timer.frameLoop(1, this, () => {
|
||||
* const deltaTime = Laya.timer.delta / 1000;
|
||||
* Core.update(deltaTime);
|
||||
* Core.update(deltaTime); // 自动更新全局服务和场景
|
||||
* });
|
||||
*
|
||||
* // Cocos Creator
|
||||
*
|
||||
* // Cocos Creator集成
|
||||
* update(deltaTime: number) {
|
||||
* Core.update(deltaTime);
|
||||
* Core.update(deltaTime); // 自动更新全局服务和场景
|
||||
* }
|
||||
*
|
||||
|
||||
* ```
|
||||
*/
|
||||
public static update(deltaTime: number): void {
|
||||
@@ -299,78 +425,49 @@ export class Core {
|
||||
Core._logger.warn("Core实例未创建,请先调用Core.create()");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
this._instance.updateInternal(deltaTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册全局管理器
|
||||
*
|
||||
* 将管理器添加到全局管理器列表中,并启用它。
|
||||
*
|
||||
* @param manager - 要注册的全局管理器
|
||||
*/
|
||||
public static registerGlobalManager(manager: GlobalManager) {
|
||||
this._instance._globalManagers.push(manager);
|
||||
manager.enabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销全局管理器
|
||||
*
|
||||
* 从全局管理器列表中移除管理器,并禁用它。
|
||||
*
|
||||
* @param manager - 要注销的全局管理器
|
||||
*/
|
||||
public static unregisterGlobalManager(manager: GlobalManager) {
|
||||
this._instance._globalManagers.splice(this._instance._globalManagers.indexOf(manager), 1);
|
||||
manager.enabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定类型的全局管理器
|
||||
*
|
||||
* @param type - 管理器类型构造函数
|
||||
* @returns 管理器实例,如果未找到则返回null
|
||||
*/
|
||||
public static getGlobalManager<T extends GlobalManager>(type: new (...args: unknown[]) => T): T | null {
|
||||
for (const manager of this._instance._globalManagers) {
|
||||
if (manager instanceof type)
|
||||
return manager as T;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调度定时器
|
||||
*
|
||||
*
|
||||
* 创建一个定时器,在指定时间后执行回调函数。
|
||||
*
|
||||
*
|
||||
* @param timeInSeconds - 延迟时间(秒)
|
||||
* @param repeats - 是否重复执行,默认为false
|
||||
* @param context - 回调函数的上下文,默认为null
|
||||
* @param onTime - 定时器触发时的回调函数
|
||||
* @returns 创建的定时器实例
|
||||
* @throws 如果Core实例未创建或onTime回调未提供
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 一次性定时器
|
||||
* Core.schedule(1.0, false, null, (timer) => {
|
||||
* console.log("1秒后执行一次");
|
||||
* });
|
||||
*
|
||||
* // 重复定时器
|
||||
* Core.schedule(0.5, true, null, (timer) => {
|
||||
* console.log("每0.5秒执行一次");
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
public static schedule<TContext = unknown>(timeInSeconds: number, repeats: boolean = false, context?: TContext, onTime?: (timer: ITimer<TContext>) => void): Timer<TContext> {
|
||||
if (!this._instance) {
|
||||
throw new Error('Core实例未创建,请先调用Core.create()');
|
||||
}
|
||||
if (!onTime) {
|
||||
throw new Error('onTime callback is required');
|
||||
}
|
||||
return this._instance._timerManager.schedule(timeInSeconds, repeats, context as TContext, onTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取ECS流式API
|
||||
*
|
||||
* @returns ECS API实例,如果未初始化则返回null
|
||||
*/
|
||||
public static get ecsAPI(): ECSFluentAPI | null {
|
||||
return this._instance?._ecsAPI || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用调试功能
|
||||
*
|
||||
*
|
||||
* @param config 调试配置
|
||||
*/
|
||||
public static enableDebug(config: IECSDebugConfig): void {
|
||||
@@ -382,7 +479,15 @@ export class Core {
|
||||
if (this._instance._debugManager) {
|
||||
this._instance._debugManager.updateConfig(config);
|
||||
} else {
|
||||
this._instance._debugManager = new DebugManager(this._instance, config);
|
||||
const configService = new DebugConfigService();
|
||||
configService.setConfig(config);
|
||||
this._instance._serviceContainer.registerInstance(DebugConfigService, configService);
|
||||
|
||||
this._instance._serviceContainer.registerSingleton(DebugManager, (c) =>
|
||||
createInstance(DebugManager, c)
|
||||
);
|
||||
|
||||
this._instance._debugManager = this._instance._serviceContainer.resolve(DebugManager);
|
||||
}
|
||||
|
||||
// 更新Core配置
|
||||
@@ -408,7 +513,7 @@ export class Core {
|
||||
|
||||
/**
|
||||
* 获取调试数据
|
||||
*
|
||||
*
|
||||
* @returns 当前调试数据,如果调试未启用则返回null
|
||||
*/
|
||||
public static getDebugData(): unknown {
|
||||
@@ -421,118 +526,114 @@ export class Core {
|
||||
|
||||
/**
|
||||
* 检查调试是否启用
|
||||
*
|
||||
*
|
||||
* @returns 调试状态
|
||||
*/
|
||||
public static get isDebugEnabled(): boolean {
|
||||
return this._instance?._config.debugConfig?.enabled || false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取WorldManager实例
|
||||
* 安装插件
|
||||
*
|
||||
* @param config 可选的WorldManager配置,用于覆盖默认配置
|
||||
* @returns WorldManager实例,如果未初始化则自动创建
|
||||
* @param plugin - 插件实例
|
||||
* @throws 如果Core实例未创建或插件安装失败
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* Core.create({ debug: true });
|
||||
*
|
||||
* // 安装插件
|
||||
* await Core.installPlugin(new MyPlugin());
|
||||
* ```
|
||||
*/
|
||||
public static getWorldManager(config?: Partial<IWorldManagerConfig>): WorldManager {
|
||||
public static async installPlugin(plugin: IPlugin): Promise<void> {
|
||||
if (!this._instance) {
|
||||
throw new Error("Core实例未创建,请先调用Core.create()");
|
||||
throw new Error('Core实例未创建,请先调用Core.create()');
|
||||
}
|
||||
|
||||
if (!this._instance._worldManager) {
|
||||
// 多World模式的配置(用户主动获取WorldManager)
|
||||
const defaultConfig = {
|
||||
maxWorlds: 50,
|
||||
autoCleanup: true,
|
||||
cleanupInterval: 60000,
|
||||
debug: this._instance._config.debug
|
||||
};
|
||||
|
||||
this._instance._worldManager = WorldManager.getInstance({
|
||||
...defaultConfig,
|
||||
...config // 用户传入的配置会覆盖默认配置
|
||||
});
|
||||
}
|
||||
|
||||
return this._instance._worldManager;
|
||||
await this._instance._pluginManager.install(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用World管理
|
||||
* 卸载插件
|
||||
*
|
||||
* 显式启用World功能,用于多房间/多世界架构
|
||||
* @param name - 插件名称
|
||||
* @throws 如果Core实例未创建或插件卸载失败
|
||||
*
|
||||
* @param config 可选的WorldManager配置,用于覆盖默认配置
|
||||
* @example
|
||||
* ```typescript
|
||||
* await Core.uninstallPlugin('my-plugin');
|
||||
* ```
|
||||
*/
|
||||
public static enableWorldManager(config?: Partial<IWorldManagerConfig>): WorldManager {
|
||||
return this.getWorldManager(config);
|
||||
public static async uninstallPlugin(name: string): Promise<void> {
|
||||
if (!this._instance) {
|
||||
throw new Error('Core实例未创建,请先调用Core.create()');
|
||||
}
|
||||
|
||||
await this._instance._pluginManager.uninstall(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保默认World存在
|
||||
*
|
||||
* 内部方法,用于懒初始化默认World
|
||||
* 获取插件实例
|
||||
*
|
||||
* @param name - 插件名称
|
||||
* @returns 插件实例,如果未安装则返回undefined
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const myPlugin = Core.getPlugin('my-plugin');
|
||||
* if (myPlugin) {
|
||||
* console.log(myPlugin.version);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
private ensureDefaultWorld(): void {
|
||||
if (!this._worldManager) {
|
||||
this._worldManager = WorldManager.getInstance({
|
||||
maxWorlds: 1, // 单场景用户只需要1个World
|
||||
autoCleanup: false, // 单场景不需要自动清理
|
||||
cleanupInterval: 0, // 禁用清理定时器
|
||||
debug: this._config.debug
|
||||
});
|
||||
public static getPlugin(name: string): IPlugin | undefined {
|
||||
if (!this._instance) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// 检查默认World是否存在
|
||||
if (!this._worldManager.getWorld(Core.DEFAULT_WORLD_ID)) {
|
||||
this._worldManager.createWorld(Core.DEFAULT_WORLD_ID, {
|
||||
name: 'DefaultWorld',
|
||||
maxScenes: 1,
|
||||
autoCleanup: false
|
||||
});
|
||||
this._worldManager.setWorldActive(Core.DEFAULT_WORLD_ID, true);
|
||||
}
|
||||
return this._instance._pluginManager.getPlugin(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景切换回调
|
||||
*
|
||||
* 在场景切换时调用,用于重置时间系统等。
|
||||
* 检查插件是否已安装
|
||||
*
|
||||
* @param name - 插件名称
|
||||
* @returns 是否已安装
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* if (Core.isPluginInstalled('my-plugin')) {
|
||||
* console.log('Plugin is installed');
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
public onSceneChanged() {
|
||||
Time.sceneChanged();
|
||||
|
||||
// 获取当前Scene(从默认World)
|
||||
const currentScene = Core.getScene();
|
||||
|
||||
// 初始化ECS API(如果场景支持)
|
||||
if (currentScene && currentScene.querySystem && currentScene.eventSystem) {
|
||||
this._ecsAPI = createECSAPI(currentScene, currentScene.querySystem, currentScene.eventSystem);
|
||||
public static isPluginInstalled(name: string): boolean {
|
||||
if (!this._instance) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 延迟调试管理器通知,避免在场景初始化过程中干扰属性
|
||||
if (this._debugManager) {
|
||||
queueMicrotask(() => {
|
||||
this._debugManager?.onSceneChanged();
|
||||
});
|
||||
}
|
||||
return this._instance._pluginManager.isInstalled(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化核心系统
|
||||
*
|
||||
*
|
||||
* 执行核心系统的初始化逻辑。
|
||||
*/
|
||||
protected initialize() {
|
||||
// 核心系统初始化
|
||||
Core._logger.info('Core initialized', {
|
||||
debug: this.debug,
|
||||
entitySystemsEnabled: Core.entitySystemsEnabled,
|
||||
debugEnabled: this._config.debugConfig?.enabled || false
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 内部更新方法
|
||||
*
|
||||
*
|
||||
* @param deltaTime - 帧时间间隔(秒)
|
||||
*/
|
||||
private updateInternal(deltaTime: number): void {
|
||||
@@ -549,44 +650,43 @@ export class Core {
|
||||
this._performanceMonitor.updateFPS(Time.deltaTime);
|
||||
}
|
||||
|
||||
// 更新全局管理器
|
||||
const managersStartTime = this._performanceMonitor.startMonitoring('GlobalManagers.update');
|
||||
for (const globalManager of this._globalManagers) {
|
||||
if (globalManager.enabled)
|
||||
globalManager.update();
|
||||
}
|
||||
this._performanceMonitor.endMonitoring('GlobalManagers.update', managersStartTime, this._globalManagers.length);
|
||||
// 更新所有可更新的服务
|
||||
const servicesStartTime = this._performanceMonitor.startMonitoring('Services.update');
|
||||
this._serviceContainer.updateAll(deltaTime);
|
||||
this._performanceMonitor.endMonitoring('Services.update', servicesStartTime, this._serviceContainer.getUpdatableCount());
|
||||
|
||||
// 更新对象池管理器
|
||||
this._poolManager.update();
|
||||
|
||||
// 更新所有World
|
||||
if (this._worldManager) {
|
||||
const worldsStartTime = this._performanceMonitor.startMonitoring('Worlds.update');
|
||||
const activeWorlds = this._worldManager.getActiveWorlds();
|
||||
let totalWorldEntities = 0;
|
||||
// 更新默认场景(通过 SceneManager)
|
||||
this._sceneManager.update();
|
||||
|
||||
for (const world of activeWorlds) {
|
||||
// 更新World的全局System
|
||||
world.updateGlobalSystems();
|
||||
|
||||
// 更新World中的所有Scene
|
||||
world.updateScenes();
|
||||
|
||||
// 统计实体数量(用于性能监控)
|
||||
const worldStats = world.getStats();
|
||||
totalWorldEntities += worldStats.totalEntities;
|
||||
}
|
||||
|
||||
this._performanceMonitor.endMonitoring('Worlds.update', worldsStartTime, totalWorldEntities);
|
||||
}
|
||||
|
||||
// 更新调试管理器(基于FPS的数据发送)
|
||||
if (this._debugManager) {
|
||||
this._debugManager.onFrameUpdate(deltaTime);
|
||||
}
|
||||
// 更新额外的 WorldManager
|
||||
this._worldManager.updateAll();
|
||||
|
||||
// 结束性能监控
|
||||
this._performanceMonitor.endMonitoring('Core.update', frameStartTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁Core实例
|
||||
*
|
||||
* 清理所有资源,通常在应用程序关闭时调用。
|
||||
*/
|
||||
public static destroy(): void {
|
||||
if (!this._instance) return;
|
||||
|
||||
// 停止调试管理器
|
||||
if (this._instance._debugManager) {
|
||||
this._instance._debugManager.stop();
|
||||
}
|
||||
|
||||
// 清理所有服务
|
||||
this._instance._serviceContainer.clear();
|
||||
|
||||
Core._logger.info('Core destroyed');
|
||||
|
||||
// 清空实例引用,允许重新创建Core实例
|
||||
this._instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
364
packages/core/src/Core/DI/Decorators.ts
Normal file
364
packages/core/src/Core/DI/Decorators.ts
Normal file
@@ -0,0 +1,364 @@
|
||||
/**
|
||||
* 依赖注入装饰器
|
||||
*
|
||||
* 提供 @Injectable、@Inject 和 @Updatable 装饰器,用于标记可注入的类和依赖注入点
|
||||
*/
|
||||
|
||||
import type { ServiceContainer } from '../ServiceContainer';
|
||||
import type { IService, ServiceType } from '../ServiceContainer';
|
||||
|
||||
/**
|
||||
* 依赖注入元数据键
|
||||
*/
|
||||
const INJECTABLE_METADATA_KEY = Symbol('injectable');
|
||||
const INJECT_METADATA_KEY = Symbol('inject');
|
||||
const INJECT_PARAMS_METADATA_KEY = Symbol('inject:params');
|
||||
const UPDATABLE_METADATA_KEY = Symbol('updatable');
|
||||
|
||||
/**
|
||||
* 依赖注入元数据存储
|
||||
*/
|
||||
const injectableMetadata = new WeakMap<any, InjectableMetadata>();
|
||||
const injectMetadata = new WeakMap<any, Map<number, ServiceType<any> | string | symbol>>();
|
||||
const updatableMetadata = new WeakMap<any, UpdatableMetadata>();
|
||||
|
||||
/**
|
||||
* 可注入元数据接口
|
||||
*/
|
||||
export interface InjectableMetadata {
|
||||
/**
|
||||
* 是否可注入
|
||||
*/
|
||||
injectable: boolean;
|
||||
|
||||
/**
|
||||
* 依赖列表
|
||||
*/
|
||||
dependencies: Array<ServiceType<any> | string | symbol>;
|
||||
|
||||
/**
|
||||
* 属性注入映射
|
||||
* key: 属性名, value: 服务类型
|
||||
*/
|
||||
properties?: Map<string | symbol, ServiceType<any>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 可更新元数据接口
|
||||
*/
|
||||
export interface UpdatableMetadata {
|
||||
/**
|
||||
* 是否可更新
|
||||
*/
|
||||
updatable: boolean;
|
||||
|
||||
/**
|
||||
* 更新优先级(数值越小越先执行,默认0)
|
||||
*/
|
||||
priority: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Injectable() 装饰器
|
||||
*
|
||||
* 标记类为可注入的服务,使其可以通过ServiceContainer进行依赖注入
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @Injectable()
|
||||
* class TimeService implements IService {
|
||||
* constructor() {}
|
||||
* dispose() {}
|
||||
* }
|
||||
*
|
||||
* @Injectable()
|
||||
* class PhysicsSystem extends EntitySystem {
|
||||
* constructor(
|
||||
* @Inject(TimeService) private timeService: TimeService
|
||||
* ) {
|
||||
* super();
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function Injectable(): ClassDecorator {
|
||||
return function <T extends Function>(target: T): T {
|
||||
const existing = injectableMetadata.get(target);
|
||||
|
||||
injectableMetadata.set(target, {
|
||||
injectable: true,
|
||||
dependencies: [],
|
||||
properties: existing?.properties
|
||||
});
|
||||
|
||||
return target;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @Updatable() 装饰器
|
||||
*
|
||||
* 标记服务类为可更新的,使其在每帧自动被ServiceContainer调用update方法。
|
||||
* 使用此装饰器的类必须实现IUpdatable接口(包含update方法)。
|
||||
*
|
||||
* @param priority - 更新优先级(数值越小越先执行,默认0)
|
||||
* @throws 如果类没有实现update方法,将在运行时抛出错误
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @Injectable()
|
||||
* @Updatable()
|
||||
* class TimerManager implements IService, IUpdatable {
|
||||
* update(deltaTime?: number) {
|
||||
* // 每帧更新逻辑
|
||||
* }
|
||||
* dispose() {}
|
||||
* }
|
||||
*
|
||||
* // 指定优先级
|
||||
* @Injectable()
|
||||
* @Updatable(10)
|
||||
* class PhysicsManager implements IService, IUpdatable {
|
||||
* update() { }
|
||||
* dispose() {}
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function Updatable(priority: number = 0): ClassDecorator {
|
||||
return function <T extends Function>(target: T): T {
|
||||
// 验证类原型上是否有update方法
|
||||
const prototype = (target as any).prototype;
|
||||
if (!prototype || typeof prototype.update !== 'function') {
|
||||
throw new Error(
|
||||
`@Updatable() decorator requires class ${target.name} to implement IUpdatable interface with update() method. ` +
|
||||
`Please add 'implements IUpdatable' and define update(deltaTime?: number): void method.`
|
||||
);
|
||||
}
|
||||
|
||||
// 标记为可更新
|
||||
updatableMetadata.set(target, {
|
||||
updatable: true,
|
||||
priority
|
||||
});
|
||||
|
||||
return target;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @Inject() 装饰器
|
||||
*
|
||||
* 标记构造函数参数需要注入的服务类型
|
||||
*
|
||||
* @param serviceType 服务类型标识符
|
||||
*/
|
||||
export function Inject(serviceType: ServiceType<any> | string | symbol): ParameterDecorator {
|
||||
return function (target: Object, propertyKey: string | symbol | undefined, parameterIndex: number) {
|
||||
// 获取或创建注入元数据
|
||||
let params = injectMetadata.get(target);
|
||||
if (!params) {
|
||||
params = new Map();
|
||||
injectMetadata.set(target, params);
|
||||
}
|
||||
|
||||
// 记录参数索引和服务类型的映射
|
||||
params.set(parameterIndex, serviceType);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @InjectProperty() 装饰器
|
||||
*
|
||||
* 通过属性装饰器注入依赖
|
||||
*
|
||||
* 注入时机:在构造函数执行后、onInitialize() 调用前完成
|
||||
*
|
||||
* @param serviceType 服务类型
|
||||
*/
|
||||
export function InjectProperty(serviceType: ServiceType<any>): PropertyDecorator {
|
||||
return function (target: any, propertyKey: string | symbol) {
|
||||
let metadata = injectableMetadata.get(target.constructor);
|
||||
|
||||
if (!metadata) {
|
||||
metadata = {
|
||||
injectable: true,
|
||||
dependencies: []
|
||||
};
|
||||
injectableMetadata.set(target.constructor, metadata);
|
||||
}
|
||||
|
||||
if (!metadata.properties) {
|
||||
metadata.properties = new Map();
|
||||
}
|
||||
|
||||
metadata.properties.set(propertyKey, serviceType);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查类是否标记为可注入
|
||||
*
|
||||
* @param target 目标类
|
||||
* @returns 是否可注入
|
||||
*/
|
||||
export function isInjectable(target: any): boolean {
|
||||
const metadata = injectableMetadata.get(target);
|
||||
return metadata?.injectable ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取类的依赖注入元数据
|
||||
*
|
||||
* @param target 目标类
|
||||
* @returns 依赖注入元数据
|
||||
*/
|
||||
export function getInjectableMetadata(target: any): InjectableMetadata | undefined {
|
||||
return injectableMetadata.get(target);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取构造函数参数的注入元数据
|
||||
*
|
||||
* @param target 目标类
|
||||
* @returns 参数索引到服务类型的映射
|
||||
*/
|
||||
export function getInjectMetadata(target: any): Map<number, ServiceType<any> | string | symbol> {
|
||||
return injectMetadata.get(target) || new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建实例并自动注入依赖
|
||||
*
|
||||
* @param constructor 构造函数
|
||||
* @param container 服务容器
|
||||
* @returns 创建的实例
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const instance = createInstance(MySystem, container);
|
||||
* ```
|
||||
*/
|
||||
export function createInstance<T>(
|
||||
constructor: new (...args: any[]) => T,
|
||||
container: ServiceContainer
|
||||
): T {
|
||||
// 获取参数注入元数据
|
||||
const injectParams = getInjectMetadata(constructor);
|
||||
|
||||
// 解析依赖
|
||||
const dependencies: any[] = [];
|
||||
|
||||
// 获取构造函数参数数量
|
||||
const paramCount = constructor.length;
|
||||
|
||||
for (let i = 0; i < paramCount; i++) {
|
||||
const serviceType = injectParams.get(i);
|
||||
|
||||
if (serviceType) {
|
||||
// 如果有显式的@Inject标记,使用标记的类型
|
||||
if (typeof serviceType === 'string' || typeof serviceType === 'symbol') {
|
||||
// 字符串或Symbol类型的服务标识
|
||||
throw new Error(
|
||||
`String and Symbol service identifiers are not yet supported in constructor injection. ` +
|
||||
`Please use class types for ${constructor.name} parameter ${i}`
|
||||
);
|
||||
} else {
|
||||
// 类类型
|
||||
dependencies.push(container.resolve(serviceType as ServiceType<any>));
|
||||
}
|
||||
} else {
|
||||
// 没有@Inject标记,传入undefined
|
||||
dependencies.push(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建实例
|
||||
return new constructor(...dependencies);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为实例注入属性依赖
|
||||
*
|
||||
* @param instance 目标实例
|
||||
* @param container 服务容器
|
||||
*/
|
||||
export function injectProperties<T>(instance: T, container: ServiceContainer): void {
|
||||
const constructor = (instance as any).constructor;
|
||||
const metadata = getInjectableMetadata(constructor);
|
||||
|
||||
if (!metadata?.properties || metadata.properties.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [propertyKey, serviceType] of metadata.properties) {
|
||||
const service = container.resolve(serviceType);
|
||||
|
||||
if (service !== null) {
|
||||
(instance as any)[propertyKey] = service;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查类是否标记为可更新
|
||||
*
|
||||
* @param target 目标类
|
||||
* @returns 是否可更新
|
||||
*/
|
||||
export function isUpdatable(target: any): boolean {
|
||||
const metadata = updatableMetadata.get(target);
|
||||
return metadata?.updatable ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取类的可更新元数据
|
||||
*
|
||||
* @param target 目标类
|
||||
* @returns 可更新元数据
|
||||
*/
|
||||
export function getUpdatableMetadata(target: any): UpdatableMetadata | undefined {
|
||||
return updatableMetadata.get(target);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册可注入的服务到容器
|
||||
*
|
||||
* 自动检测@Injectable装饰器并注册服务
|
||||
*
|
||||
* @param container 服务容器
|
||||
* @param serviceType 服务类型
|
||||
* @param singleton 是否注册为单例(默认true)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @Injectable()
|
||||
* class MyService implements IService {
|
||||
* dispose() {}
|
||||
* }
|
||||
*
|
||||
* // 自动注册
|
||||
* registerInjectable(Core.services, MyService);
|
||||
* ```
|
||||
*/
|
||||
export function registerInjectable<T extends IService>(
|
||||
container: ServiceContainer,
|
||||
serviceType: ServiceType<T>,
|
||||
singleton: boolean = true
|
||||
): void {
|
||||
if (!isInjectable(serviceType)) {
|
||||
throw new Error(
|
||||
`${serviceType.name} is not marked as @Injectable(). ` +
|
||||
`Please add @Injectable() decorator to the class.`
|
||||
);
|
||||
}
|
||||
|
||||
// 创建工厂函数,使用createInstance自动解析依赖
|
||||
const factory = (c: ServiceContainer) => createInstance(serviceType, c);
|
||||
|
||||
// 注册到容器
|
||||
if (singleton) {
|
||||
container.registerSingleton(serviceType, factory);
|
||||
} else {
|
||||
container.registerTransient(serviceType, factory);
|
||||
}
|
||||
}
|
||||
22
packages/core/src/Core/DI/index.ts
Normal file
22
packages/core/src/Core/DI/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 依赖注入模块
|
||||
*
|
||||
* 提供装饰器和工具函数,用于实现依赖注入模式
|
||||
*/
|
||||
|
||||
export {
|
||||
Injectable,
|
||||
Inject,
|
||||
InjectProperty,
|
||||
Updatable,
|
||||
isInjectable,
|
||||
getInjectableMetadata,
|
||||
getInjectMetadata,
|
||||
isUpdatable,
|
||||
getUpdatableMetadata,
|
||||
createInstance,
|
||||
injectProperties,
|
||||
registerInjectable
|
||||
} from './Decorators';
|
||||
|
||||
export type { InjectableMetadata, UpdatableMetadata } from './Decorators';
|
||||
124
packages/core/src/Core/Plugin.ts
Normal file
124
packages/core/src/Core/Plugin.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import type { Core } from '../Core';
|
||||
import type { ServiceContainer } from './ServiceContainer';
|
||||
|
||||
/**
|
||||
* 插件状态
|
||||
*/
|
||||
export enum PluginState {
|
||||
/**
|
||||
* 未安装
|
||||
*/
|
||||
NotInstalled = 'not_installed',
|
||||
|
||||
/**
|
||||
* 已安装
|
||||
*/
|
||||
Installed = 'installed',
|
||||
|
||||
/**
|
||||
* 安装失败
|
||||
*/
|
||||
Failed = 'failed'
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件接口
|
||||
*
|
||||
* 所有插件都必须实现此接口。
|
||||
* 插件提供了一种扩展框架功能的标准方式。
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* class MyPlugin implements IPlugin {
|
||||
* readonly name = 'my-plugin';
|
||||
* readonly version = '1.0.0';
|
||||
* readonly dependencies = ['other-plugin'];
|
||||
*
|
||||
* async install(core: Core, services: ServiceContainer) {
|
||||
* // 注册服务
|
||||
* services.registerSingleton(MyService);
|
||||
*
|
||||
* // 添加系统
|
||||
* const world = core.getWorld();
|
||||
* if (world) {
|
||||
* world.addSystem(new MySystem());
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* async uninstall() {
|
||||
* // 清理资源
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export interface IPlugin {
|
||||
/**
|
||||
* 插件唯一名称
|
||||
*
|
||||
* 用于依赖解析和插件管理。
|
||||
*/
|
||||
readonly name: string;
|
||||
|
||||
/**
|
||||
* 插件版本
|
||||
*
|
||||
* 遵循语义化版本规范 (semver)。
|
||||
*/
|
||||
readonly version: string;
|
||||
|
||||
/**
|
||||
* 依赖的其他插件名称列表
|
||||
*
|
||||
* 这些插件必须在当前插件之前安装。
|
||||
*/
|
||||
readonly dependencies?: readonly string[];
|
||||
|
||||
/**
|
||||
* 安装插件
|
||||
*
|
||||
* 在此方法中初始化插件,注册服务、系统等。
|
||||
* 可以是同步或异步的。
|
||||
*
|
||||
* @param core - Core实例,用于访问World等
|
||||
* @param services - 服务容器,用于注册服务
|
||||
*/
|
||||
install(core: Core, services: ServiceContainer): void | Promise<void>;
|
||||
|
||||
/**
|
||||
* 卸载插件
|
||||
*
|
||||
* 清理插件占用的资源。
|
||||
* 可以是同步或异步的。
|
||||
*/
|
||||
uninstall(): void | Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件元数据
|
||||
*/
|
||||
export interface IPluginMetadata {
|
||||
/**
|
||||
* 插件名称
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* 插件版本
|
||||
*/
|
||||
version: string;
|
||||
|
||||
/**
|
||||
* 插件状态
|
||||
*/
|
||||
state: PluginState;
|
||||
|
||||
/**
|
||||
* 安装时间戳
|
||||
*/
|
||||
installedAt?: number;
|
||||
|
||||
/**
|
||||
* 错误信息(如果安装失败)
|
||||
*/
|
||||
error?: string;
|
||||
}
|
||||
266
packages/core/src/Core/PluginManager.ts
Normal file
266
packages/core/src/Core/PluginManager.ts
Normal file
@@ -0,0 +1,266 @@
|
||||
import { IPlugin, IPluginMetadata, PluginState } from './Plugin';
|
||||
import type { IService } from './ServiceContainer';
|
||||
import type { Core } from '../Core';
|
||||
import type { ServiceContainer } from './ServiceContainer';
|
||||
import { createLogger } from '../Utils/Logger';
|
||||
|
||||
const logger = createLogger('PluginManager');
|
||||
|
||||
/**
|
||||
* 插件管理器
|
||||
*
|
||||
* 负责插件的注册、安装、卸载和生命周期管理。
|
||||
* 支持依赖检查和异步加载。
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const core = Core.create();
|
||||
* const pluginManager = core.getService(PluginManager);
|
||||
*
|
||||
* // 注册插件
|
||||
* await pluginManager.install(new MyPlugin());
|
||||
*
|
||||
* // 查询插件
|
||||
* const plugin = pluginManager.getPlugin('my-plugin');
|
||||
*
|
||||
* // 卸载插件
|
||||
* await pluginManager.uninstall('my-plugin');
|
||||
* ```
|
||||
*/
|
||||
export class PluginManager implements IService {
|
||||
/**
|
||||
* 已安装的插件
|
||||
*/
|
||||
private _plugins: Map<string, IPlugin> = new Map();
|
||||
|
||||
/**
|
||||
* 插件元数据
|
||||
*/
|
||||
private _metadata: Map<string, IPluginMetadata> = new Map();
|
||||
|
||||
/**
|
||||
* Core实例引用
|
||||
*/
|
||||
private _core: Core | null = null;
|
||||
|
||||
/**
|
||||
* 服务容器引用
|
||||
*/
|
||||
private _services: ServiceContainer | null = null;
|
||||
|
||||
/**
|
||||
* 初始化插件管理器
|
||||
*
|
||||
* @param core - Core实例
|
||||
* @param services - 服务容器
|
||||
*/
|
||||
public initialize(core: Core, services: ServiceContainer): void {
|
||||
this._core = core;
|
||||
this._services = services;
|
||||
logger.info('PluginManager initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* 安装插件
|
||||
*
|
||||
* 会自动检查依赖并按正确顺序安装。
|
||||
*
|
||||
* @param plugin - 插件实例
|
||||
* @throws 如果依赖检查失败或安装失败
|
||||
*/
|
||||
public async install(plugin: IPlugin): Promise<void> {
|
||||
if (!this._core || !this._services) {
|
||||
throw new Error('PluginManager not initialized. Call initialize() first.');
|
||||
}
|
||||
|
||||
// 检查是否已安装
|
||||
if (this._plugins.has(plugin.name)) {
|
||||
logger.warn(`Plugin ${plugin.name} is already installed`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查依赖
|
||||
if (plugin.dependencies && plugin.dependencies.length > 0) {
|
||||
this._checkDependencies(plugin);
|
||||
}
|
||||
|
||||
// 创建元数据
|
||||
const metadata: IPluginMetadata = {
|
||||
name: plugin.name,
|
||||
version: plugin.version,
|
||||
state: PluginState.NotInstalled,
|
||||
installedAt: Date.now()
|
||||
};
|
||||
|
||||
this._metadata.set(plugin.name, metadata);
|
||||
|
||||
try {
|
||||
// 调用插件的安装方法
|
||||
logger.info(`Installing plugin: ${plugin.name} v${plugin.version}`);
|
||||
await plugin.install(this._core, this._services);
|
||||
|
||||
// 标记为已安装
|
||||
this._plugins.set(plugin.name, plugin);
|
||||
metadata.state = PluginState.Installed;
|
||||
|
||||
logger.info(`Plugin ${plugin.name} installed successfully`);
|
||||
} catch (error) {
|
||||
// 安装失败
|
||||
metadata.state = PluginState.Failed;
|
||||
metadata.error = error instanceof Error ? error.message : String(error);
|
||||
|
||||
logger.error(`Failed to install plugin ${plugin.name}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 卸载插件
|
||||
*
|
||||
* @param name - 插件名称
|
||||
* @throws 如果插件未安装或卸载失败
|
||||
*/
|
||||
public async uninstall(name: string): Promise<void> {
|
||||
const plugin = this._plugins.get(name);
|
||||
if (!plugin) {
|
||||
throw new Error(`Plugin ${name} is not installed`);
|
||||
}
|
||||
|
||||
// 检查是否有其他插件依赖此插件
|
||||
this._checkDependents(name);
|
||||
|
||||
try {
|
||||
logger.info(`Uninstalling plugin: ${name}`);
|
||||
await plugin.uninstall();
|
||||
|
||||
// 从注册表中移除
|
||||
this._plugins.delete(name);
|
||||
this._metadata.delete(name);
|
||||
|
||||
logger.info(`Plugin ${name} uninstalled successfully`);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to uninstall plugin ${name}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取插件实例
|
||||
*
|
||||
* @param name - 插件名称
|
||||
* @returns 插件实例,如果未安装则返回undefined
|
||||
*/
|
||||
public getPlugin(name: string): IPlugin | undefined {
|
||||
return this._plugins.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取插件元数据
|
||||
*
|
||||
* @param name - 插件名称
|
||||
* @returns 插件元数据,如果未安装则返回undefined
|
||||
*/
|
||||
public getMetadata(name: string): IPluginMetadata | undefined {
|
||||
return this._metadata.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有已安装的插件
|
||||
*
|
||||
* @returns 插件列表
|
||||
*/
|
||||
public getAllPlugins(): IPlugin[] {
|
||||
return Array.from(this._plugins.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有插件元数据
|
||||
*
|
||||
* @returns 元数据列表
|
||||
*/
|
||||
public getAllMetadata(): IPluginMetadata[] {
|
||||
return Array.from(this._metadata.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查插件是否已安装
|
||||
*
|
||||
* @param name - 插件名称
|
||||
* @returns 是否已安装
|
||||
*/
|
||||
public isInstalled(name: string): boolean {
|
||||
return this._plugins.has(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查插件依赖
|
||||
*
|
||||
* @param plugin - 插件实例
|
||||
* @throws 如果依赖未满足
|
||||
*/
|
||||
private _checkDependencies(plugin: IPlugin): void {
|
||||
if (!plugin.dependencies) {
|
||||
return;
|
||||
}
|
||||
|
||||
const missingDeps: string[] = [];
|
||||
|
||||
for (const dep of plugin.dependencies) {
|
||||
if (!this._plugins.has(dep)) {
|
||||
missingDeps.push(dep);
|
||||
}
|
||||
}
|
||||
|
||||
if (missingDeps.length > 0) {
|
||||
throw new Error(
|
||||
`Plugin ${plugin.name} has unmet dependencies: ${missingDeps.join(', ')}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有其他插件依赖指定插件
|
||||
*
|
||||
* @param name - 插件名称
|
||||
* @throws 如果有其他插件依赖此插件
|
||||
*/
|
||||
private _checkDependents(name: string): void {
|
||||
const dependents: string[] = [];
|
||||
|
||||
for (const plugin of this._plugins.values()) {
|
||||
if (plugin.dependencies && plugin.dependencies.includes(name)) {
|
||||
dependents.push(plugin.name);
|
||||
}
|
||||
}
|
||||
|
||||
if (dependents.length > 0) {
|
||||
throw new Error(
|
||||
`Cannot uninstall plugin ${name}: it is required by ${dependents.join(', ')}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源
|
||||
*/
|
||||
public dispose(): void {
|
||||
// 卸载所有插件(逆序,先卸载依赖项)
|
||||
const plugins = Array.from(this._plugins.values()).reverse();
|
||||
|
||||
for (const plugin of plugins) {
|
||||
try {
|
||||
logger.info(`Disposing plugin: ${plugin.name}`);
|
||||
plugin.uninstall();
|
||||
} catch (error) {
|
||||
logger.error(`Error disposing plugin ${plugin.name}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
this._plugins.clear();
|
||||
this._metadata.clear();
|
||||
this._core = null;
|
||||
this._services = null;
|
||||
|
||||
logger.info('PluginManager disposed');
|
||||
}
|
||||
}
|
||||
423
packages/core/src/Core/ServiceContainer.ts
Normal file
423
packages/core/src/Core/ServiceContainer.ts
Normal file
@@ -0,0 +1,423 @@
|
||||
import { createLogger } from '../Utils/Logger';
|
||||
import { isUpdatable as checkUpdatable, getUpdatableMetadata } from './DI';
|
||||
|
||||
const logger = createLogger('ServiceContainer');
|
||||
|
||||
/**
|
||||
* 服务基础接口
|
||||
* 所有通过 ServiceContainer 管理的服务都应该实现此接口
|
||||
*/
|
||||
export interface IService {
|
||||
/**
|
||||
* 释放服务占用的资源
|
||||
* 当服务被注销或容器被清空时调用
|
||||
*/
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务类型
|
||||
*
|
||||
* 支持任意构造函数签名,以便与依赖注入装饰器配合使用
|
||||
*/
|
||||
export type ServiceType<T extends IService> = new (...args: any[]) => T;
|
||||
|
||||
/**
|
||||
* 服务生命周期
|
||||
*/
|
||||
export enum ServiceLifetime {
|
||||
/**
|
||||
* 单例模式 - 整个应用生命周期内只有一个实例
|
||||
*/
|
||||
Singleton = 'singleton',
|
||||
|
||||
/**
|
||||
* 瞬时模式 - 每次请求都创建新实例
|
||||
*/
|
||||
Transient = 'transient'
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务注册信息
|
||||
*/
|
||||
interface ServiceRegistration<T extends IService> {
|
||||
/**
|
||||
* 服务类型
|
||||
*/
|
||||
type: ServiceType<T>;
|
||||
|
||||
/**
|
||||
* 服务实例(单例模式)
|
||||
*/
|
||||
instance?: T;
|
||||
|
||||
/**
|
||||
* 工厂函数
|
||||
*/
|
||||
factory?: (container: ServiceContainer) => T;
|
||||
|
||||
/**
|
||||
* 生命周期
|
||||
*/
|
||||
lifetime: ServiceLifetime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务容器
|
||||
*
|
||||
* 负责管理所有服务的注册、解析和生命周期。
|
||||
* 支持依赖注入和服务定位模式。
|
||||
*
|
||||
* 特点:
|
||||
* - 单例和瞬时两种生命周期
|
||||
* - 支持工厂函数注册
|
||||
* - 支持实例注册
|
||||
* - 类型安全的服务解析
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const container = new ServiceContainer();
|
||||
*
|
||||
* // 注册单例服务
|
||||
* container.registerSingleton(TimerManager);
|
||||
*
|
||||
* // 注册工厂函数
|
||||
* container.registerSingleton(Logger, (c) => createLogger('App'));
|
||||
*
|
||||
* // 注册实例
|
||||
* container.registerInstance(Config, new Config());
|
||||
*
|
||||
* // 解析服务
|
||||
* const timer = container.resolve(TimerManager);
|
||||
* ```
|
||||
*/
|
||||
export class ServiceContainer {
|
||||
/**
|
||||
* 服务注册表
|
||||
*/
|
||||
private _services: Map<ServiceType<IService>, ServiceRegistration<IService>> = new Map();
|
||||
|
||||
/**
|
||||
* 正在解析的服务栈(用于循环依赖检测)
|
||||
*/
|
||||
private _resolving: Set<ServiceType<IService>> = new Set();
|
||||
|
||||
/**
|
||||
* 可更新的服务列表
|
||||
*
|
||||
* 自动收集所有使用@Updatable装饰器标记的服务,供Core统一更新
|
||||
* 按优先级排序(数值越小越先执行)
|
||||
*/
|
||||
private _updatableServices: Array<{ instance: any; priority: number }> = [];
|
||||
|
||||
/**
|
||||
* 注册单例服务
|
||||
*
|
||||
* @param type - 服务类型
|
||||
* @param factory - 可选的工厂函数
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 直接注册类型
|
||||
* container.registerSingleton(TimerManager);
|
||||
*
|
||||
* // 使用工厂函数
|
||||
* container.registerSingleton(Logger, (c) => {
|
||||
* return createLogger('App');
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
public registerSingleton<T extends IService>(
|
||||
type: ServiceType<T>,
|
||||
factory?: (container: ServiceContainer) => T
|
||||
): void {
|
||||
if (this._services.has(type as ServiceType<IService>)) {
|
||||
logger.warn(`Service ${type.name} is already registered`);
|
||||
return;
|
||||
}
|
||||
|
||||
this._services.set(type as ServiceType<IService>, {
|
||||
type: type as ServiceType<IService>,
|
||||
factory: factory as ((container: ServiceContainer) => IService) | undefined,
|
||||
lifetime: ServiceLifetime.Singleton
|
||||
});
|
||||
|
||||
logger.debug(`Registered singleton service: ${type.name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册瞬时服务
|
||||
*
|
||||
* 每次解析都会创建新实例。
|
||||
*
|
||||
* @param type - 服务类型
|
||||
* @param factory - 可选的工厂函数
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 每次解析都创建新实例
|
||||
* container.registerTransient(Command);
|
||||
* ```
|
||||
*/
|
||||
public registerTransient<T extends IService>(
|
||||
type: ServiceType<T>,
|
||||
factory?: (container: ServiceContainer) => T
|
||||
): void {
|
||||
if (this._services.has(type as ServiceType<IService>)) {
|
||||
logger.warn(`Service ${type.name} is already registered`);
|
||||
return;
|
||||
}
|
||||
|
||||
this._services.set(type as ServiceType<IService>, {
|
||||
type: type as ServiceType<IService>,
|
||||
factory: factory as ((container: ServiceContainer) => IService) | undefined,
|
||||
lifetime: ServiceLifetime.Transient
|
||||
});
|
||||
|
||||
logger.debug(`Registered transient service: ${type.name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册服务实例
|
||||
*
|
||||
* 直接注册已创建的实例,自动视为单例。
|
||||
*
|
||||
* @param type - 服务类型(构造函数,仅用作标识)
|
||||
* @param instance - 服务实例
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const config = new Config();
|
||||
* container.registerInstance(Config, config);
|
||||
* ```
|
||||
*/
|
||||
public registerInstance<T extends IService>(type: new (...args: any[]) => T, instance: T): void {
|
||||
if (this._services.has(type as ServiceType<IService>)) {
|
||||
logger.warn(`Service ${type.name} is already registered`);
|
||||
return;
|
||||
}
|
||||
|
||||
this._services.set(type as ServiceType<IService>, {
|
||||
type: type as ServiceType<IService>,
|
||||
instance: instance as IService,
|
||||
lifetime: ServiceLifetime.Singleton
|
||||
});
|
||||
|
||||
// 如果使用了@Updatable装饰器,添加到可更新列表
|
||||
if (checkUpdatable(type)) {
|
||||
const metadata = getUpdatableMetadata(type);
|
||||
const priority = metadata?.priority ?? 0;
|
||||
this._updatableServices.push({ instance, priority });
|
||||
|
||||
// 按优先级排序(数值越小越先执行)
|
||||
this._updatableServices.sort((a, b) => a.priority - b.priority);
|
||||
|
||||
logger.debug(`Service ${type.name} is updatable (priority: ${priority}), added to update list`);
|
||||
}
|
||||
|
||||
logger.debug(`Registered service instance: ${type.name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析服务
|
||||
*
|
||||
* @param type - 服务类型
|
||||
* @returns 服务实例
|
||||
* @throws 如果服务未注册或存在循环依赖
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const timer = container.resolve(TimerManager);
|
||||
* ```
|
||||
*/
|
||||
public resolve<T extends IService>(type: ServiceType<T>): T {
|
||||
const registration = this._services.get(type as ServiceType<IService>);
|
||||
|
||||
if (!registration) {
|
||||
throw new Error(`Service ${type.name} is not registered`);
|
||||
}
|
||||
|
||||
// 检测循环依赖
|
||||
if (this._resolving.has(type as ServiceType<IService>)) {
|
||||
const chain = Array.from(this._resolving).map(t => t.name).join(' -> ');
|
||||
throw new Error(`Circular dependency detected: ${chain} -> ${type.name}`);
|
||||
}
|
||||
|
||||
// 如果是单例且已经有实例,直接返回
|
||||
if (registration.lifetime === ServiceLifetime.Singleton && registration.instance) {
|
||||
return registration.instance as T;
|
||||
}
|
||||
|
||||
// 添加到解析栈
|
||||
this._resolving.add(type as ServiceType<IService>);
|
||||
|
||||
try {
|
||||
// 创建实例
|
||||
let instance: IService;
|
||||
|
||||
if (registration.factory) {
|
||||
// 使用工厂函数
|
||||
instance = registration.factory(this);
|
||||
} else {
|
||||
// 直接构造
|
||||
instance = new (registration.type)();
|
||||
}
|
||||
|
||||
// 如果是单例,缓存实例
|
||||
if (registration.lifetime === ServiceLifetime.Singleton) {
|
||||
registration.instance = instance;
|
||||
|
||||
// 如果使用了@Updatable装饰器,添加到可更新列表
|
||||
if (checkUpdatable(registration.type)) {
|
||||
const metadata = getUpdatableMetadata(registration.type);
|
||||
const priority = metadata?.priority ?? 0;
|
||||
this._updatableServices.push({ instance, priority });
|
||||
|
||||
// 按优先级排序(数值越小越先执行)
|
||||
this._updatableServices.sort((a, b) => a.priority - b.priority);
|
||||
|
||||
logger.debug(`Service ${type.name} is updatable (priority: ${priority}), added to update list`);
|
||||
}
|
||||
}
|
||||
|
||||
return instance as T;
|
||||
} finally {
|
||||
// 从解析栈移除
|
||||
this._resolving.delete(type as ServiceType<IService>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试解析服务
|
||||
*
|
||||
* 如果服务未注册,返回null而不是抛出异常。
|
||||
*
|
||||
* @param type - 服务类型
|
||||
* @returns 服务实例或null
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const timer = container.tryResolve(TimerManager);
|
||||
* if (timer) {
|
||||
* timer.schedule(...);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
public tryResolve<T extends IService>(type: ServiceType<T>): T | null {
|
||||
try {
|
||||
return this.resolve(type);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查服务是否已注册
|
||||
*
|
||||
* @param type - 服务类型
|
||||
* @returns 是否已注册
|
||||
*/
|
||||
public isRegistered<T extends IService>(type: ServiceType<T>): boolean {
|
||||
return this._services.has(type as ServiceType<IService>);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销服务
|
||||
*
|
||||
* @param type - 服务类型
|
||||
* @returns 是否成功注销
|
||||
*/
|
||||
public unregister<T extends IService>(type: ServiceType<T>): boolean {
|
||||
const registration = this._services.get(type as ServiceType<IService>);
|
||||
if (!registration) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果有单例实例,调用 dispose
|
||||
if (registration.instance) {
|
||||
// 从可更新列表中移除
|
||||
const index = this._updatableServices.findIndex(item => item.instance === registration.instance);
|
||||
if (index !== -1) {
|
||||
this._updatableServices.splice(index, 1);
|
||||
}
|
||||
|
||||
registration.instance.dispose();
|
||||
}
|
||||
|
||||
this._services.delete(type as ServiceType<IService>);
|
||||
logger.debug(`Unregistered service: ${type.name}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有服务
|
||||
*/
|
||||
public clear(): void {
|
||||
// 清理所有单例实例
|
||||
for (const [, registration] of this._services) {
|
||||
if (registration.instance) {
|
||||
registration.instance.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
this._services.clear();
|
||||
this._updatableServices = [];
|
||||
logger.debug('Cleared all services');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有已注册的服务类型
|
||||
*
|
||||
* @returns 服务类型数组
|
||||
*/
|
||||
public getRegisteredServices(): ServiceType<IService>[] {
|
||||
return Array.from(this._services.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新所有使用@Updatable装饰器标记的服务
|
||||
*
|
||||
* 此方法会按优先级顺序遍历所有可更新的服务并调用它们的update方法。
|
||||
* 所有服务在注册时已经由@Updatable装饰器验证过必须实现IUpdatable接口。
|
||||
* 通常在Core的主更新循环中调用。
|
||||
*
|
||||
* @param deltaTime - 可选的帧时间间隔(秒)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 在Core的update方法中
|
||||
* this._serviceContainer.updateAll(deltaTime);
|
||||
* ```
|
||||
*/
|
||||
public updateAll(deltaTime?: number): void {
|
||||
for (const { instance } of this._updatableServices) {
|
||||
instance.update(deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有可更新的服务数量
|
||||
*
|
||||
* @returns 可更新服务的数量
|
||||
*/
|
||||
public getUpdatableCount(): number {
|
||||
return this._updatableServices.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有已实例化的服务实例
|
||||
*
|
||||
* @returns 所有服务实例的数组
|
||||
*/
|
||||
public getAll(): IService[] {
|
||||
const instances: IService[] = [];
|
||||
|
||||
for (const descriptor of this._services.values()) {
|
||||
if (descriptor.instance) {
|
||||
instances.push(descriptor.instance);
|
||||
}
|
||||
}
|
||||
|
||||
return instances;
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,13 @@ export abstract class Component implements IComponent {
|
||||
*/
|
||||
public readonly id: number;
|
||||
|
||||
/**
|
||||
* 所属实体ID
|
||||
*
|
||||
* 存储实体ID而非引用,避免循环引用,符合ECS数据导向设计。
|
||||
*/
|
||||
public entityId: number | null = null;
|
||||
|
||||
/**
|
||||
* 创建组件实例
|
||||
*
|
||||
@@ -63,8 +70,7 @@ export abstract class Component implements IComponent {
|
||||
* 这是一个生命周期钩子,用于组件的初始化逻辑。
|
||||
* 虽然保留此方法,但建议将复杂的初始化逻辑放在 System 中处理。
|
||||
*/
|
||||
public onAddedToEntity(): void {
|
||||
}
|
||||
public onAddedToEntity(): void {}
|
||||
|
||||
/**
|
||||
* 组件从实体移除时的回调
|
||||
@@ -75,7 +81,5 @@ export abstract class Component implements IComponent {
|
||||
* 这是一个生命周期钩子,用于组件的清理逻辑。
|
||||
* 虽然保留此方法,但建议将复杂的清理逻辑放在 System 中处理。
|
||||
*/
|
||||
public onRemovedFromEntity(): void {
|
||||
}
|
||||
|
||||
}
|
||||
public onRemovedFromEntity(): void {}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {Entity} from '../Entity';
|
||||
import {ComponentType} from './ComponentStorage';
|
||||
import {BitMask64Data, BitMask64Utils, ComponentTypeManager} from "../Utils";
|
||||
import { Entity } from '../Entity';
|
||||
import { ComponentType, ComponentRegistry } from './ComponentStorage';
|
||||
import { BitMask64Data, BitMask64Utils } from "../Utils";
|
||||
import { BitMaskHashMap } from "../Utils/BitMaskHashMap";
|
||||
|
||||
/**
|
||||
* 原型标识符
|
||||
@@ -36,7 +37,7 @@ export interface ArchetypeQueryResult {
|
||||
*/
|
||||
export class ArchetypeSystem {
|
||||
/** 所有原型的映射表 */
|
||||
private _archetypes = new Map<number, Map<number, Archetype>>();
|
||||
private _archetypes = new BitMaskHashMap<Archetype>();
|
||||
|
||||
/** 实体到原型的映射 */
|
||||
private _entityToArchetype = new Map<Entity, Archetype>();
|
||||
@@ -57,15 +58,13 @@ export class ArchetypeSystem {
|
||||
const componentTypes = this.getEntityComponentTypes(entity);
|
||||
const archetypeId = this.generateArchetypeId(componentTypes);
|
||||
|
||||
let archetype = this.getArchetype(archetypeId);
|
||||
let archetype = this._archetypes.get(archetypeId);
|
||||
if (!archetype) {
|
||||
archetype = this.createArchetype(componentTypes);
|
||||
}
|
||||
|
||||
archetype.entities.add(entity);
|
||||
this._entityToArchetype.set(entity, archetype);
|
||||
|
||||
this.updateComponentIndexes(archetype, componentTypes, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -109,7 +108,7 @@ export class ArchetypeSystem {
|
||||
}
|
||||
|
||||
// 获取或创建新原型
|
||||
let newArchetype = this.getArchetype(newArchetypeId);
|
||||
let newArchetype = this._archetypes.get(newArchetypeId);
|
||||
if (!newArchetype) {
|
||||
newArchetype = this.createArchetype(newComponentTypes);
|
||||
}
|
||||
@@ -118,12 +117,6 @@ export class ArchetypeSystem {
|
||||
newArchetype.entities.add(entity);
|
||||
this._entityToArchetype.set(entity, newArchetype);
|
||||
|
||||
// 更新组件索引
|
||||
if (currentArchetype) {
|
||||
this.updateComponentIndexes(currentArchetype, currentArchetype.componentTypes, false);
|
||||
}
|
||||
this.updateComponentIndexes(newArchetype, newComponentTypes, true);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -139,19 +132,52 @@ export class ArchetypeSystem {
|
||||
let totalEntities = 0;
|
||||
|
||||
if (operation === 'AND') {
|
||||
// 生成查询的 BitMask
|
||||
const queryMask = this.generateArchetypeId(componentTypes);
|
||||
|
||||
// 使用 BitMask 位运算快速判断原型是否包含所有指定组件
|
||||
for (const archetype of this._allArchetypes) {
|
||||
if (BitMask64Utils.hasAll(archetype.id, queryMask)) {
|
||||
if (componentTypes.length === 0) {
|
||||
for (const archetype of this._allArchetypes) {
|
||||
matchingArchetypes.push(archetype);
|
||||
totalEntities += archetype.entities.size;
|
||||
}
|
||||
return { archetypes: matchingArchetypes, totalEntities };
|
||||
}
|
||||
|
||||
if (componentTypes.length === 1) {
|
||||
const archetypes = this._componentToArchetypes.get(componentTypes[0]);
|
||||
if (archetypes) {
|
||||
for (const archetype of archetypes) {
|
||||
matchingArchetypes.push(archetype);
|
||||
totalEntities += archetype.entities.size;
|
||||
}
|
||||
}
|
||||
return { archetypes: matchingArchetypes, totalEntities };
|
||||
}
|
||||
|
||||
let smallestSet: Set<Archetype> | undefined;
|
||||
let smallestSize = Infinity;
|
||||
|
||||
for (const componentType of componentTypes) {
|
||||
const archetypes = this._componentToArchetypes.get(componentType);
|
||||
if (!archetypes || archetypes.size === 0) {
|
||||
return { archetypes: [], totalEntities: 0 };
|
||||
}
|
||||
if (archetypes.size < smallestSize) {
|
||||
smallestSize = archetypes.size;
|
||||
smallestSet = archetypes;
|
||||
}
|
||||
}
|
||||
|
||||
const queryMask = this.generateArchetypeId(componentTypes);
|
||||
|
||||
if (smallestSet) {
|
||||
for (const archetype of smallestSet) {
|
||||
if (BitMask64Utils.hasAll(archetype.id, queryMask)) {
|
||||
matchingArchetypes.push(archetype);
|
||||
totalEntities += archetype.entities.size;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const foundArchetypes = new Set<Archetype>();
|
||||
|
||||
|
||||
for (const componentType of componentTypes) {
|
||||
const archetypes = this._componentToArchetypes.get(componentType);
|
||||
if (archetypes) {
|
||||
@@ -160,13 +186,13 @@ export class ArchetypeSystem {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for (const archetype of foundArchetypes) {
|
||||
matchingArchetypes.push(archetype);
|
||||
totalEntities += archetype.entities.size;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
archetypes: matchingArchetypes,
|
||||
totalEntities
|
||||
@@ -215,25 +241,14 @@ export class ArchetypeSystem {
|
||||
this._entityComponentTypesCache.clear();
|
||||
this._allArchetypes = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据原型ID获取原型
|
||||
* @param archetypeId
|
||||
* @private
|
||||
*/
|
||||
private getArchetype(archetypeId: ArchetypeId): Archetype | undefined {
|
||||
return this._archetypes.get(archetypeId.hi)?.get(archetypeId.lo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新所有原型数组
|
||||
*/
|
||||
private updateAllArchetypeArrays(): void {
|
||||
this._allArchetypes = [];
|
||||
for (const [, innerMap] of this._archetypes) {
|
||||
for (const [, archetype] of innerMap) {
|
||||
this._allArchetypes.push(archetype);
|
||||
}
|
||||
for (let archetype of this._archetypes.values()) {
|
||||
this._allArchetypes.push(archetype);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,10 +266,18 @@ export class ArchetypeSystem {
|
||||
|
||||
/**
|
||||
* 生成原型ID
|
||||
* 使用ComponentRegistry确保与Entity.componentMask使用相同的bitIndex
|
||||
*/
|
||||
private generateArchetypeId(componentTypes: ComponentType[]): ArchetypeId {
|
||||
let entityBits = ComponentTypeManager.instance.getEntityBits(componentTypes);
|
||||
return entityBits.getValue();
|
||||
let mask = BitMask64Utils.clone(BitMask64Utils.ZERO);
|
||||
for (const type of componentTypes) {
|
||||
if (!ComponentRegistry.isRegistered(type)) {
|
||||
ComponentRegistry.register(type);
|
||||
}
|
||||
const bitMask = ComponentRegistry.getBitMask(type);
|
||||
BitMask64Utils.orInPlace(mask, bitMask);
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -262,45 +285,26 @@ export class ArchetypeSystem {
|
||||
*/
|
||||
private createArchetype(componentTypes: ComponentType[]): Archetype {
|
||||
const id = this.generateArchetypeId(componentTypes);
|
||||
|
||||
|
||||
const archetype: Archetype = {
|
||||
id,
|
||||
componentTypes: [...componentTypes],
|
||||
entities: new Set<Entity>()
|
||||
};
|
||||
// 存储原型ID - 原型
|
||||
let archetypeGroup = this._archetypes.get(id.hi);
|
||||
if (!archetypeGroup) {
|
||||
archetypeGroup = new Map<number, Archetype>();
|
||||
this._archetypes.set(id.hi, archetypeGroup);
|
||||
}
|
||||
archetypeGroup.set(id.lo, archetype);
|
||||
// 更新数组
|
||||
this._archetypes.set(id,archetype);
|
||||
this.updateAllArchetypeArrays();
|
||||
return archetype;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新组件索引
|
||||
*/
|
||||
private updateComponentIndexes(archetype: Archetype, componentTypes: ComponentType[], add: boolean): void {
|
||||
|
||||
for (const componentType of componentTypes) {
|
||||
let archetypes = this._componentToArchetypes.get(componentType);
|
||||
if (!archetypes) {
|
||||
archetypes = new Set();
|
||||
this._componentToArchetypes.set(componentType, archetypes);
|
||||
}
|
||||
|
||||
if (add) {
|
||||
archetypes.add(archetype);
|
||||
} else {
|
||||
archetypes.delete(archetype);
|
||||
if (archetypes.size === 0) {
|
||||
this._componentToArchetypes.delete(componentType);
|
||||
}
|
||||
}
|
||||
archetypes.add(archetype);
|
||||
}
|
||||
|
||||
return archetype;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -8,24 +8,41 @@ export class ComponentPool<T extends Component> {
|
||||
private createFn: () => T;
|
||||
private resetFn?: (component: T) => void;
|
||||
private maxSize: number;
|
||||
private minSize: number;
|
||||
private growthFactor: number;
|
||||
|
||||
private stats = {
|
||||
totalCreated: 0,
|
||||
totalAcquired: 0,
|
||||
totalReleased: 0
|
||||
};
|
||||
|
||||
constructor(
|
||||
createFn: () => T,
|
||||
resetFn?: (component: T) => void,
|
||||
maxSize: number = 1000
|
||||
maxSize: number = 1000,
|
||||
minSize: number = 10,
|
||||
growthFactor: number = 1.5
|
||||
) {
|
||||
this.createFn = createFn;
|
||||
this.resetFn = resetFn;
|
||||
this.maxSize = maxSize;
|
||||
this.minSize = Math.max(1, minSize);
|
||||
this.growthFactor = Math.max(1.1, growthFactor);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一个组件实例
|
||||
*/
|
||||
acquire(): T {
|
||||
this.stats.totalAcquired++;
|
||||
|
||||
if (this.pool.length > 0) {
|
||||
return this.pool.pop()!;
|
||||
}
|
||||
|
||||
this.stats.totalCreated++;
|
||||
|
||||
return this.createFn();
|
||||
}
|
||||
|
||||
@@ -33,20 +50,41 @@ export class ComponentPool<T extends Component> {
|
||||
* 释放一个组件实例回池中
|
||||
*/
|
||||
release(component: T): void {
|
||||
if (this.pool.length < this.maxSize) {
|
||||
if (this.resetFn) {
|
||||
this.resetFn(component);
|
||||
}
|
||||
this.pool.push(component);
|
||||
this.stats.totalReleased++;
|
||||
|
||||
if (this.pool.length >= this.maxSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.resetFn) {
|
||||
this.resetFn(component);
|
||||
}
|
||||
|
||||
this.pool.push(component);
|
||||
}
|
||||
|
||||
/**
|
||||
* 预填充对象池
|
||||
*/
|
||||
prewarm(count: number): void {
|
||||
for (let i = 0; i < count && this.pool.length < this.maxSize; i++) {
|
||||
this.pool.push(this.createFn());
|
||||
const targetCount = Math.min(count, this.maxSize);
|
||||
|
||||
for (let i = this.pool.length; i < targetCount; i++) {
|
||||
const component = this.createFn();
|
||||
if (this.resetFn) {
|
||||
this.resetFn(component);
|
||||
}
|
||||
this.pool.push(component);
|
||||
this.stats.totalCreated++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动收缩池大小
|
||||
*/
|
||||
shrink(): void {
|
||||
while (this.pool.length > this.minSize) {
|
||||
this.pool.pop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +108,35 @@ export class ComponentPool<T extends Component> {
|
||||
getMaxSize(): number {
|
||||
return this.maxSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取统计信息
|
||||
*/
|
||||
getStats() {
|
||||
const hitRate = this.stats.totalAcquired === 0
|
||||
? 0
|
||||
: (this.stats.totalAcquired - this.stats.totalCreated) / this.stats.totalAcquired;
|
||||
|
||||
return {
|
||||
totalCreated: this.stats.totalCreated,
|
||||
totalAcquired: this.stats.totalAcquired,
|
||||
totalReleased: this.stats.totalReleased,
|
||||
hitRate: hitRate,
|
||||
currentSize: this.pool.length,
|
||||
maxSize: this.maxSize,
|
||||
minSize: this.minSize,
|
||||
utilizationRate: this.pool.length / this.maxSize
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件使用追踪
|
||||
*/
|
||||
interface ComponentUsageTracker {
|
||||
createCount: number;
|
||||
releaseCount: number;
|
||||
lastAccessTime: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,6 +145,10 @@ export class ComponentPool<T extends Component> {
|
||||
export class ComponentPoolManager {
|
||||
private static instance: ComponentPoolManager;
|
||||
private pools = new Map<string, ComponentPool<any>>();
|
||||
private usageTracker = new Map<string, ComponentUsageTracker>();
|
||||
|
||||
private autoCleanupInterval = 60000;
|
||||
private lastCleanupTime = 0;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
@@ -95,9 +166,16 @@ export class ComponentPoolManager {
|
||||
componentName: string,
|
||||
createFn: () => T,
|
||||
resetFn?: (component: T) => void,
|
||||
maxSize?: number
|
||||
maxSize?: number,
|
||||
minSize?: number
|
||||
): void {
|
||||
this.pools.set(componentName, new ComponentPool(createFn, resetFn, maxSize));
|
||||
this.pools.set(componentName, new ComponentPool(createFn, resetFn, maxSize, minSize));
|
||||
|
||||
this.usageTracker.set(componentName, {
|
||||
createCount: 0,
|
||||
releaseCount: 0,
|
||||
lastAccessTime: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,6 +183,9 @@ export class ComponentPoolManager {
|
||||
*/
|
||||
acquireComponent<T extends Component>(componentName: string): T | null {
|
||||
const pool = this.pools.get(componentName);
|
||||
|
||||
this.trackUsage(componentName, 'create');
|
||||
|
||||
return pool ? pool.acquire() : null;
|
||||
}
|
||||
|
||||
@@ -113,11 +194,71 @@ export class ComponentPoolManager {
|
||||
*/
|
||||
releaseComponent<T extends Component>(componentName: string, component: T): void {
|
||||
const pool = this.pools.get(componentName);
|
||||
|
||||
this.trackUsage(componentName, 'release');
|
||||
|
||||
if (pool) {
|
||||
pool.release(component);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 追踪使用情况
|
||||
*/
|
||||
private trackUsage(componentName: string, action: 'create' | 'release'): void {
|
||||
let tracker = this.usageTracker.get(componentName);
|
||||
|
||||
if (!tracker) {
|
||||
tracker = {
|
||||
createCount: 0,
|
||||
releaseCount: 0,
|
||||
lastAccessTime: Date.now()
|
||||
};
|
||||
this.usageTracker.set(componentName, tracker);
|
||||
}
|
||||
|
||||
if (action === 'create') {
|
||||
tracker.createCount++;
|
||||
} else {
|
||||
tracker.releaseCount++;
|
||||
}
|
||||
|
||||
tracker.lastAccessTime = Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动清理(定期调用)
|
||||
*/
|
||||
public update(): void {
|
||||
const now = Date.now();
|
||||
|
||||
if (now - this.lastCleanupTime < this.autoCleanupInterval) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [name, tracker] of this.usageTracker.entries()) {
|
||||
const inactive = now - tracker.lastAccessTime > 120000;
|
||||
|
||||
if (inactive) {
|
||||
const pool = this.pools.get(name);
|
||||
if (pool) {
|
||||
pool.shrink();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.lastCleanupTime = now;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取热点组件列表
|
||||
*/
|
||||
public getHotComponents(threshold: number = 100): string[] {
|
||||
return Array.from(this.usageTracker.entries())
|
||||
.filter(([_, tracker]) => tracker.createCount > threshold)
|
||||
.map(([name]) => name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 预热所有池
|
||||
*/
|
||||
@@ -137,10 +278,28 @@ export class ComponentPoolManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置管理器,移除所有注册的池
|
||||
* 重置管理器
|
||||
*/
|
||||
reset(): void {
|
||||
this.pools.clear();
|
||||
this.usageTracker.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取全局统计信息
|
||||
*/
|
||||
getGlobalStats() {
|
||||
const stats: any[] = [];
|
||||
|
||||
for (const [name, pool] of this.pools.entries()) {
|
||||
stats.push({
|
||||
componentName: name,
|
||||
poolStats: pool.getStats(),
|
||||
usage: this.usageTracker.get(name)
|
||||
});
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,7 +317,7 @@ export class ComponentPoolManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取池利用率信息(用于调试)
|
||||
* 获取池利用率信息
|
||||
*/
|
||||
getPoolUtilization(): Map<string, { used: number; total: number; utilization: number }> {
|
||||
const utilization = new Map();
|
||||
@@ -167,7 +326,7 @@ export class ComponentPoolManager {
|
||||
const maxSize = pool.getMaxSize();
|
||||
const used = maxSize - available;
|
||||
const utilRate = maxSize > 0 ? (used / maxSize * 100) : 0;
|
||||
|
||||
|
||||
utilization.set(name, {
|
||||
used: used,
|
||||
total: maxSize,
|
||||
@@ -183,11 +342,11 @@ export class ComponentPoolManager {
|
||||
getComponentUtilization(componentName: string): number {
|
||||
const pool = this.pools.get(componentName);
|
||||
if (!pool) return 0;
|
||||
|
||||
|
||||
const available = pool.getAvailableCount();
|
||||
const maxSize = pool.getMaxSize();
|
||||
const used = maxSize - available;
|
||||
|
||||
|
||||
return maxSize > 0 ? (used / maxSize * 100) : 0;
|
||||
}
|
||||
}
|
||||
@@ -54,10 +54,7 @@ export class ComponentRegistry {
|
||||
const typeName = getComponentTypeName(componentType);
|
||||
throw new Error(`Component type ${typeName} is not registered`);
|
||||
}
|
||||
|
||||
const mask: BitMask64Data = { lo: 0, hi: 0 };
|
||||
BitMask64Utils.setBitExtended(mask, bitIndex);
|
||||
return mask;
|
||||
return BitMask64Utils.create(bitIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,7 +14,9 @@ export class EntityBuilder {
|
||||
constructor(scene: IScene, storageManager: ComponentStorageManager) {
|
||||
this.scene = scene;
|
||||
this.storageManager = storageManager;
|
||||
this.entity = new Entity("", scene.identifierPool.checkOut());
|
||||
const id = scene.identifierPool.checkOut();
|
||||
this.entity = new Entity("", id);
|
||||
this.entity.scene = this.scene as any;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
404
packages/core/src/ECS/Core/Query/TypedQuery.ts
Normal file
404
packages/core/src/ECS/Core/Query/TypedQuery.ts
Normal file
@@ -0,0 +1,404 @@
|
||||
/**
|
||||
* 类型安全的Query查询系统
|
||||
*
|
||||
* 提供完整的TypeScript类型推断,在编译时确保类型安全
|
||||
*/
|
||||
|
||||
import type { Entity } from '../../Entity';
|
||||
import type { ComponentConstructor, ComponentInstance, ComponentTypeMap } from '../../../Types/TypeHelpers';
|
||||
import { Matcher, type QueryCondition } from '../../Utils/Matcher';
|
||||
|
||||
/**
|
||||
* 类型安全的查询结果
|
||||
*
|
||||
* 根据查询条件自动推断实体必定拥有的组件类型
|
||||
*/
|
||||
export class TypedQueryResult<TAll extends readonly ComponentConstructor[]> {
|
||||
private _entities: readonly Entity[];
|
||||
private _componentTypes: TAll;
|
||||
|
||||
constructor(entities: readonly Entity[], componentTypes: TAll) {
|
||||
this._entities = entities;
|
||||
this._componentTypes = componentTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取实体列表
|
||||
*/
|
||||
get entities(): readonly Entity[] {
|
||||
return this._entities;
|
||||
}
|
||||
|
||||
/**
|
||||
* 实体数量
|
||||
*/
|
||||
get length(): number {
|
||||
return this._entities.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 遍历所有实体
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* query.forEach((entity) => {
|
||||
* // entity.getComponent返回类型自动推断
|
||||
* const pos = entity.getComponent(Position); // Position类型
|
||||
* const vel = entity.getComponent(Velocity); // Velocity类型
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
forEach(callback: (entity: Entity, index: number) => void): void {
|
||||
this._entities.forEach(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 映射转换实体
|
||||
*/
|
||||
map<R>(callback: (entity: Entity, index: number) => R): R[] {
|
||||
return this._entities.map(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤实体
|
||||
*/
|
||||
filter(predicate: (entity: Entity, index: number) => boolean): TypedQueryResult<TAll> {
|
||||
return new TypedQueryResult(this._entities.filter(predicate), this._componentTypes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找第一个匹配的实体
|
||||
*/
|
||||
find(predicate: (entity: Entity, index: number) => boolean): Entity | undefined {
|
||||
return this._entities.find(predicate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否存在匹配的实体
|
||||
*/
|
||||
some(predicate: (entity: Entity, index: number) => boolean): boolean {
|
||||
return this._entities.some(predicate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否所有实体都匹配
|
||||
*/
|
||||
every(predicate: (entity: Entity, index: number) => boolean): boolean {
|
||||
return this._entities.every(predicate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定索引的实体
|
||||
*/
|
||||
get(index: number): Entity | undefined {
|
||||
return this._entities[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取第一个实体
|
||||
*/
|
||||
get first(): Entity | undefined {
|
||||
return this._entities[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最后一个实体
|
||||
*/
|
||||
get last(): Entity | undefined {
|
||||
return this._entities[this._entities.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查查询结果是否为空
|
||||
*/
|
||||
get isEmpty(): boolean {
|
||||
return this._entities.length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为数组
|
||||
*/
|
||||
toArray(): Entity[] {
|
||||
return [...this._entities];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取组件类型信息(用于调试)
|
||||
*/
|
||||
getComponentTypes(): readonly ComponentConstructor[] {
|
||||
return this._componentTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 迭代器支持
|
||||
*/
|
||||
[Symbol.iterator](): Iterator<Entity> {
|
||||
return this._entities[Symbol.iterator]();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的查询构建器
|
||||
*
|
||||
* 支持链式调用,自动推断查询结果的类型
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 基础查询
|
||||
* const query = new TypedQueryBuilder()
|
||||
* .withAll(Position, Velocity)
|
||||
* .build();
|
||||
*
|
||||
* // 复杂查询
|
||||
* const complexQuery = new TypedQueryBuilder()
|
||||
* .withAll(Transform, Renderer)
|
||||
* .withAny(BoxCollider, CircleCollider)
|
||||
* .withNone(Disabled)
|
||||
* .withTag(EntityTags.Enemy)
|
||||
* .build();
|
||||
* ```
|
||||
*/
|
||||
export class TypedQueryBuilder<
|
||||
TAll extends readonly ComponentConstructor[] = [],
|
||||
TAny extends readonly ComponentConstructor[] = [],
|
||||
TNone extends readonly ComponentConstructor[] = []
|
||||
> {
|
||||
private _all: TAll;
|
||||
private _any: TAny;
|
||||
private _none: TNone;
|
||||
private _tag?: number;
|
||||
private _name?: string;
|
||||
|
||||
constructor(
|
||||
all?: TAll,
|
||||
any?: TAny,
|
||||
none?: TNone,
|
||||
tag?: number,
|
||||
name?: string
|
||||
) {
|
||||
this._all = (all || []) as TAll;
|
||||
this._any = (any || []) as TAny;
|
||||
this._none = (none || []) as TNone;
|
||||
this._tag = tag;
|
||||
this._name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 要求实体拥有所有指定的组件
|
||||
*
|
||||
* @param types 组件类型
|
||||
* @returns 新的查询构建器,类型参数更新
|
||||
*/
|
||||
withAll<TNewAll extends readonly ComponentConstructor[]>(
|
||||
...types: TNewAll
|
||||
): TypedQueryBuilder<
|
||||
readonly [...TAll, ...TNewAll],
|
||||
TAny,
|
||||
TNone
|
||||
> {
|
||||
return new TypedQueryBuilder(
|
||||
[...this._all, ...types] as readonly [...TAll, ...TNewAll],
|
||||
this._any,
|
||||
this._none,
|
||||
this._tag,
|
||||
this._name
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 要求实体至少拥有一个指定的组件
|
||||
*
|
||||
* @param types 组件类型
|
||||
* @returns 新的查询构建器
|
||||
*/
|
||||
withAny<TNewAny extends readonly ComponentConstructor[]>(
|
||||
...types: TNewAny
|
||||
): TypedQueryBuilder<
|
||||
TAll,
|
||||
readonly [...TAny, ...TNewAny],
|
||||
TNone
|
||||
> {
|
||||
return new TypedQueryBuilder(
|
||||
this._all,
|
||||
[...this._any, ...types] as readonly [...TAny, ...TNewAny],
|
||||
this._none,
|
||||
this._tag,
|
||||
this._name
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 排除拥有指定组件的实体
|
||||
*
|
||||
* @param types 组件类型
|
||||
* @returns 新的查询构建器
|
||||
*/
|
||||
withNone<TNewNone extends readonly ComponentConstructor[]>(
|
||||
...types: TNewNone
|
||||
): TypedQueryBuilder<
|
||||
TAll,
|
||||
TAny,
|
||||
readonly [...TNone, ...TNewNone]
|
||||
> {
|
||||
return new TypedQueryBuilder(
|
||||
this._all,
|
||||
this._any,
|
||||
[...this._none, ...types] as readonly [...TNone, ...TNewNone],
|
||||
this._tag,
|
||||
this._name
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按标签过滤实体
|
||||
*
|
||||
* @param tag 标签值
|
||||
* @returns 新的查询构建器
|
||||
*/
|
||||
withTag(tag: number): TypedQueryBuilder<TAll, TAny, TNone> {
|
||||
return new TypedQueryBuilder(
|
||||
this._all,
|
||||
this._any,
|
||||
this._none,
|
||||
tag,
|
||||
this._name
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按名称过滤实体
|
||||
*
|
||||
* @param name 实体名称
|
||||
* @returns 新的查询构建器
|
||||
*/
|
||||
withName(name: string): TypedQueryBuilder<TAll, TAny, TNone> {
|
||||
return new TypedQueryBuilder(
|
||||
this._all,
|
||||
this._any,
|
||||
this._none,
|
||||
this._tag,
|
||||
name
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建Matcher对象
|
||||
*
|
||||
* @returns Matcher实例,用于传统查询API
|
||||
*/
|
||||
buildMatcher(): Matcher {
|
||||
let matcher = Matcher.complex();
|
||||
|
||||
if (this._all.length > 0) {
|
||||
matcher = matcher.all(...(this._all as unknown as ComponentConstructor[]));
|
||||
}
|
||||
|
||||
if (this._any.length > 0) {
|
||||
matcher = matcher.any(...(this._any as unknown as ComponentConstructor[]));
|
||||
}
|
||||
|
||||
if (this._none.length > 0) {
|
||||
matcher = matcher.none(...(this._none as unknown as ComponentConstructor[]));
|
||||
}
|
||||
|
||||
if (this._tag !== undefined) {
|
||||
matcher = matcher.withTag(this._tag);
|
||||
}
|
||||
|
||||
if (this._name !== undefined) {
|
||||
matcher = matcher.withName(this._name);
|
||||
}
|
||||
|
||||
return matcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取查询条件
|
||||
*
|
||||
* @returns 查询条件对象
|
||||
*/
|
||||
getCondition(): QueryCondition {
|
||||
return {
|
||||
all: [...this._all] as ComponentConstructor[],
|
||||
any: [...this._any] as ComponentConstructor[],
|
||||
none: [...this._none] as ComponentConstructor[],
|
||||
tag: this._tag,
|
||||
name: this._name
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取required组件类型(用于类型推断)
|
||||
*/
|
||||
getRequiredTypes(): TAll {
|
||||
return this._all;
|
||||
}
|
||||
|
||||
/**
|
||||
* 克隆查询构建器
|
||||
*/
|
||||
clone(): TypedQueryBuilder<TAll, TAny, TNone> {
|
||||
return new TypedQueryBuilder(
|
||||
[...this._all] as unknown as TAll,
|
||||
[...this._any] as unknown as TAny,
|
||||
[...this._none] as unknown as TNone,
|
||||
this._tag,
|
||||
this._name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建类型安全的查询构建器
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const query = createQuery()
|
||||
* .withAll(Position, Velocity)
|
||||
* .withNone(Disabled);
|
||||
*
|
||||
* // 在System或Scene中使用
|
||||
* const entities = scene.query(query);
|
||||
* entities.forEach(entity => {
|
||||
* const pos = entity.getComponent(Position); // 自动推断为Position
|
||||
* const vel = entity.getComponent(Velocity); // 自动推断为Velocity
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function createQuery(): TypedQueryBuilder<[], [], []> {
|
||||
return new TypedQueryBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建单组件查询的便捷方法
|
||||
*
|
||||
* @param componentType 组件类型
|
||||
* @returns 查询构建器
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const healthEntities = queryFor(HealthComponent);
|
||||
* ```
|
||||
*/
|
||||
export function queryFor<T extends ComponentConstructor>(
|
||||
componentType: T
|
||||
): TypedQueryBuilder<readonly [T], [], []> {
|
||||
return new TypedQueryBuilder([componentType] as readonly [T]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建多组件查询的便捷方法
|
||||
*
|
||||
* @param types 组件类型数组
|
||||
* @returns 查询构建器
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const movableEntities = queryForAll(Position, Velocity);
|
||||
* ```
|
||||
*/
|
||||
export function queryForAll<T extends readonly ComponentConstructor[]>(
|
||||
...types: T
|
||||
): TypedQueryBuilder<T, [], []> {
|
||||
return new TypedQueryBuilder(types);
|
||||
}
|
||||
@@ -4,42 +4,11 @@ import { ComponentRegistry, ComponentType } from './ComponentStorage';
|
||||
import { BitMask64Utils, BitMask64Data } from '../Utils/BigIntCompatibility';
|
||||
import { createLogger } from '../../Utils/Logger';
|
||||
import { getComponentTypeName } from '../Decorators';
|
||||
import { Archetype, ArchetypeSystem } from './ArchetypeSystem';
|
||||
import { ReactiveQuery, ReactiveQueryConfig } from './ReactiveQuery';
|
||||
import { QueryCondition, QueryConditionType, QueryResult } from './QueryTypes';
|
||||
|
||||
import { ComponentPoolManager } from './ComponentPool';
|
||||
import { ArchetypeSystem, Archetype, ArchetypeQueryResult } from './ArchetypeSystem';
|
||||
|
||||
/**
|
||||
* 查询条件类型
|
||||
*/
|
||||
export enum QueryConditionType {
|
||||
/** 必须包含所有指定组件 */
|
||||
ALL = 'all',
|
||||
/** 必须包含任意一个指定组件 */
|
||||
ANY = 'any',
|
||||
/** 不能包含任何指定组件 */
|
||||
NONE = 'none'
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询条件接口
|
||||
*/
|
||||
export interface QueryCondition {
|
||||
type: QueryConditionType;
|
||||
componentTypes: ComponentType[];
|
||||
mask: BitMask64Data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 实体查询结果接口
|
||||
*/
|
||||
export interface QueryResult {
|
||||
entities: readonly Entity[];
|
||||
count: number;
|
||||
/** 查询执行时间(毫秒) */
|
||||
executionTime: number;
|
||||
/** 是否来自缓存 */
|
||||
fromCache: boolean;
|
||||
}
|
||||
export { QueryCondition, QueryConditionType, QueryResult };
|
||||
|
||||
/**
|
||||
* 实体索引结构
|
||||
@@ -78,19 +47,16 @@ export class QuerySystem {
|
||||
private entities: Entity[] = [];
|
||||
private entityIndex: EntityIndex;
|
||||
|
||||
// 版本号,用于缓存失效
|
||||
private _version = 0;
|
||||
|
||||
// 查询缓存系统
|
||||
private queryCache = new Map<string, QueryCacheEntry>();
|
||||
private cacheMaxSize = 1000;
|
||||
private cacheTimeout = 5000; // 5秒缓存过期
|
||||
private cacheTimeout = 5000;
|
||||
|
||||
private componentMaskCache = new Map<string, BitMask64Data>();
|
||||
|
||||
private archetypeSystem: ArchetypeSystem;
|
||||
|
||||
// 性能统计
|
||||
private queryStats = {
|
||||
totalQueries: 0,
|
||||
cacheHits: 0,
|
||||
@@ -100,6 +66,9 @@ export class QuerySystem {
|
||||
dirtyChecks: 0
|
||||
};
|
||||
|
||||
private resultArrayPool: Entity[][] = [];
|
||||
private poolMaxSize = 50;
|
||||
|
||||
constructor() {
|
||||
this.entityIndex = {
|
||||
byTag: new Map(),
|
||||
@@ -109,19 +78,32 @@ export class QuerySystem {
|
||||
this.archetypeSystem = new ArchetypeSystem();
|
||||
}
|
||||
|
||||
private acquireResultArray(): Entity[] {
|
||||
if (this.resultArrayPool.length > 0) {
|
||||
return this.resultArrayPool.pop()!;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
private releaseResultArray(array: Entity[]): void {
|
||||
if (this.resultArrayPool.length < this.poolMaxSize) {
|
||||
array.length = 0;
|
||||
this.resultArrayPool.push(array);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置实体列表并重建索引
|
||||
*
|
||||
*
|
||||
* 当实体集合发生大规模变化时调用此方法。
|
||||
* 系统将重新构建所有索引以确保查询性能。
|
||||
*
|
||||
*
|
||||
* @param entities 新的实体列表
|
||||
*/
|
||||
public setEntities(entities: Entity[]): void {
|
||||
this.entities = entities;
|
||||
this.clearQueryCache();
|
||||
this.clearReactiveQueries();
|
||||
this.rebuildIndexes();
|
||||
}
|
||||
|
||||
@@ -141,12 +123,14 @@ export class QuerySystem {
|
||||
|
||||
this.archetypeSystem.addEntity(entity);
|
||||
|
||||
// 通知响应式查询
|
||||
this.notifyReactiveQueriesEntityAdded(entity);
|
||||
|
||||
// 只有在非延迟模式下才立即清理缓存
|
||||
if (!deferCacheClear) {
|
||||
this.clearQueryCache();
|
||||
}
|
||||
|
||||
|
||||
// 更新版本号
|
||||
this._version++;
|
||||
}
|
||||
@@ -224,14 +208,24 @@ export class QuerySystem {
|
||||
public removeEntity(entity: Entity): void {
|
||||
const index = this.entities.indexOf(entity);
|
||||
if (index !== -1) {
|
||||
const componentTypes: ComponentType[] = [];
|
||||
for (const component of entity.components) {
|
||||
componentTypes.push(component.constructor as ComponentType);
|
||||
}
|
||||
|
||||
this.entities.splice(index, 1);
|
||||
this.removeEntityFromIndexes(entity);
|
||||
|
||||
this.archetypeSystem.removeEntity(entity);
|
||||
|
||||
if (componentTypes.length > 0) {
|
||||
this.notifyReactiveQueriesEntityRemoved(entity, componentTypes);
|
||||
} else {
|
||||
this.notifyReactiveQueriesEntityRemovedFallback(entity);
|
||||
}
|
||||
|
||||
this.clearQueryCache();
|
||||
|
||||
// 更新版本号
|
||||
this._version++;
|
||||
}
|
||||
}
|
||||
@@ -259,6 +253,9 @@ export class QuerySystem {
|
||||
// 重新添加实体到索引(基于新的组件状态)
|
||||
this.addEntityToIndexes(entity);
|
||||
|
||||
// 通知响应式查询
|
||||
this.notifyReactiveQueriesEntityChanged(entity);
|
||||
|
||||
// 清理查询缓存,因为实体组件状态已改变
|
||||
this.clearQueryCache();
|
||||
|
||||
@@ -346,13 +343,13 @@ export class QuerySystem {
|
||||
|
||||
/**
|
||||
* 查询包含所有指定组件的实体
|
||||
*
|
||||
*
|
||||
* 返回同时包含所有指定组件类型的实体列表。
|
||||
* 系统会自动选择最高效的查询策略,包括索引查找和缓存机制。
|
||||
*
|
||||
* 内部使用响应式查询作为智能缓存,自动跟踪实体变化,性能更优。
|
||||
*
|
||||
* @param componentTypes 要查询的组件类型列表
|
||||
* @returns 查询结果,包含匹配的实体和性能信息
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 查询同时具有位置和速度组件的实体
|
||||
@@ -364,38 +361,20 @@ export class QuerySystem {
|
||||
const startTime = performance.now();
|
||||
this.queryStats.totalQueries++;
|
||||
|
||||
// 生成缓存键
|
||||
const cacheKey = this.generateCacheKey('all', componentTypes);
|
||||
// 使用内部响应式查询作为智能缓存
|
||||
const reactiveQuery = this.getOrCreateReactiveQuery(QueryConditionType.ALL, componentTypes);
|
||||
|
||||
// 检查缓存
|
||||
const cached = this.getFromCache(cacheKey);
|
||||
if (cached) {
|
||||
this.queryStats.cacheHits++;
|
||||
return {
|
||||
entities: cached,
|
||||
count: cached.length,
|
||||
executionTime: performance.now() - startTime,
|
||||
fromCache: true
|
||||
};
|
||||
}
|
||||
// 从响应式查询获取结果(永远是最新的)
|
||||
const entities = reactiveQuery.getEntities();
|
||||
|
||||
this.queryStats.archetypeHits++;
|
||||
const archetypeResult = this.archetypeSystem.queryArchetypes(componentTypes, 'AND');
|
||||
|
||||
const entities: Entity[] = [];
|
||||
for (const archetype of archetypeResult.archetypes) {
|
||||
for (const entity of archetype.entities) {
|
||||
entities.push(entity);
|
||||
}
|
||||
}
|
||||
|
||||
this.addToCache(cacheKey, entities);
|
||||
// 统计为缓存命中(响应式查询本质上是永不过期的智能缓存)
|
||||
this.queryStats.cacheHits++;
|
||||
|
||||
return {
|
||||
entities,
|
||||
count: entities.length,
|
||||
executionTime: performance.now() - startTime,
|
||||
fromCache: false
|
||||
fromCache: true
|
||||
};
|
||||
}
|
||||
|
||||
@@ -425,13 +404,13 @@ export class QuerySystem {
|
||||
|
||||
/**
|
||||
* 查询包含任意指定组件的实体
|
||||
*
|
||||
*
|
||||
* 返回包含任意一个指定组件类型的实体列表。
|
||||
* 使用集合合并算法确保高效的查询性能。
|
||||
*
|
||||
* 内部使用响应式查询作为智能缓存,自动跟踪实体变化,性能更优。
|
||||
*
|
||||
* @param componentTypes 要查询的组件类型列表
|
||||
* @returns 查询结果,包含匹配的实体和性能信息
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 查询具有武器或护甲组件的实体
|
||||
@@ -443,49 +422,32 @@ export class QuerySystem {
|
||||
const startTime = performance.now();
|
||||
this.queryStats.totalQueries++;
|
||||
|
||||
const cacheKey = this.generateCacheKey('any', componentTypes);
|
||||
// 使用内部响应式查询作为智能缓存
|
||||
const reactiveQuery = this.getOrCreateReactiveQuery(QueryConditionType.ANY, componentTypes);
|
||||
|
||||
// 检查缓存
|
||||
const cached = this.getFromCache(cacheKey);
|
||||
if (cached) {
|
||||
this.queryStats.cacheHits++;
|
||||
return {
|
||||
entities: cached,
|
||||
count: cached.length,
|
||||
executionTime: performance.now() - startTime,
|
||||
fromCache: true
|
||||
};
|
||||
}
|
||||
// 从响应式查询获取结果(永远是最新的)
|
||||
const entities = reactiveQuery.getEntities();
|
||||
|
||||
this.queryStats.archetypeHits++;
|
||||
const archetypeResult = this.archetypeSystem.queryArchetypes(componentTypes, 'OR');
|
||||
|
||||
const entities: Entity[] = [];
|
||||
for (const archetype of archetypeResult.archetypes) {
|
||||
for (const entity of archetype.entities) {
|
||||
entities.push(entity);
|
||||
}
|
||||
}
|
||||
|
||||
this.addToCache(cacheKey, entities);
|
||||
// 统计为缓存命中(响应式查询本质上是永不过期的智能缓存)
|
||||
this.queryStats.cacheHits++;
|
||||
|
||||
return {
|
||||
entities,
|
||||
count: entities.length,
|
||||
executionTime: performance.now() - startTime,
|
||||
fromCache: false
|
||||
fromCache: true
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询不包含任何指定组件的实体
|
||||
*
|
||||
*
|
||||
* 返回不包含任何指定组件类型的实体列表。
|
||||
* 适用于排除特定类型实体的查询场景。
|
||||
*
|
||||
* 内部使用响应式查询作为智能缓存,自动跟踪实体变化,性能更优。
|
||||
*
|
||||
* @param componentTypes 要排除的组件类型列表
|
||||
* @returns 查询结果,包含匹配的实体和性能信息
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 查询不具有AI和玩家控制组件的实体(如静态物体)
|
||||
@@ -497,32 +459,20 @@ export class QuerySystem {
|
||||
const startTime = performance.now();
|
||||
this.queryStats.totalQueries++;
|
||||
|
||||
const cacheKey = this.generateCacheKey('none', componentTypes);
|
||||
// 使用内部响应式查询作为智能缓存
|
||||
const reactiveQuery = this.getOrCreateReactiveQuery(QueryConditionType.NONE, componentTypes);
|
||||
|
||||
// 检查缓存
|
||||
const cached = this.getFromCache(cacheKey);
|
||||
if (cached) {
|
||||
this.queryStats.cacheHits++;
|
||||
return {
|
||||
entities: cached,
|
||||
count: cached.length,
|
||||
executionTime: performance.now() - startTime,
|
||||
fromCache: true
|
||||
};
|
||||
}
|
||||
// 从响应式查询获取结果(永远是最新的)
|
||||
const entities = reactiveQuery.getEntities();
|
||||
|
||||
const mask = this.createComponentMask(componentTypes);
|
||||
const entities = this.entities.filter(entity =>
|
||||
BitMask64Utils.hasNone(entity.componentMask, mask)
|
||||
);
|
||||
|
||||
this.addToCache(cacheKey, entities);
|
||||
// 统计为缓存命中(响应式查询本质上是永不过期的智能缓存)
|
||||
this.queryStats.cacheHits++;
|
||||
|
||||
return {
|
||||
entities,
|
||||
count: entities.length,
|
||||
executionTime: performance.now() - startTime,
|
||||
fromCache: false
|
||||
fromCache: true
|
||||
};
|
||||
}
|
||||
|
||||
@@ -745,6 +695,20 @@ export class QuerySystem {
|
||||
this.componentMaskCache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有响应式查询
|
||||
*
|
||||
* 销毁所有响应式查询实例并清理索引
|
||||
* 通常在setEntities时调用以确保缓存一致性
|
||||
*/
|
||||
private clearReactiveQueries(): void {
|
||||
for (const query of this._reactiveQueries.values()) {
|
||||
query.dispose();
|
||||
}
|
||||
this._reactiveQueries.clear();
|
||||
this._reactiveQueriesByComponent.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 高效的缓存键生成
|
||||
*/
|
||||
@@ -768,11 +732,111 @@ export class QuerySystem {
|
||||
|
||||
/**
|
||||
* 清理查询缓存
|
||||
*
|
||||
*
|
||||
* 用于外部调用清理缓存,通常在批量操作后使用。
|
||||
* 注意:此方法也会清理响应式查询缓存
|
||||
*/
|
||||
public clearCache(): void {
|
||||
this.clearQueryCache();
|
||||
this.clearReactiveQueries();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建响应式查询
|
||||
*
|
||||
* 响应式查询会自动跟踪实体/组件的变化,并通过事件通知订阅者。
|
||||
* 适合需要实时响应实体变化的场景(如UI更新、AI系统等)。
|
||||
*
|
||||
* @param componentTypes 查询的组件类型列表
|
||||
* @param config 可选的查询配置
|
||||
* @returns 响应式查询实例
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const query = querySystem.createReactiveQuery([Position, Velocity], {
|
||||
* enableBatchMode: true,
|
||||
* batchDelay: 16
|
||||
* });
|
||||
*
|
||||
* query.subscribe((change) => {
|
||||
* if (change.type === ReactiveQueryChangeType.ADDED) {
|
||||
* console.log('新实体:', change.entity);
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
public createReactiveQuery(
|
||||
componentTypes: ComponentType[],
|
||||
config?: ReactiveQueryConfig
|
||||
): ReactiveQuery {
|
||||
if (!componentTypes || componentTypes.length === 0) {
|
||||
throw new Error('组件类型列表不能为空');
|
||||
}
|
||||
|
||||
const mask = this.createComponentMask(componentTypes);
|
||||
const condition: QueryCondition = {
|
||||
type: QueryConditionType.ALL,
|
||||
componentTypes,
|
||||
mask
|
||||
};
|
||||
|
||||
const query = new ReactiveQuery(condition, config);
|
||||
|
||||
const initialEntities = this.executeTraditionalQuery(
|
||||
QueryConditionType.ALL,
|
||||
componentTypes
|
||||
);
|
||||
query.initializeWith(initialEntities);
|
||||
|
||||
const cacheKey = this.generateCacheKey('all', componentTypes);
|
||||
this._reactiveQueries.set(cacheKey, query);
|
||||
|
||||
for (const type of componentTypes) {
|
||||
let queries = this._reactiveQueriesByComponent.get(type);
|
||||
if (!queries) {
|
||||
queries = new Set();
|
||||
this._reactiveQueriesByComponent.set(type, queries);
|
||||
}
|
||||
queries.add(query);
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁响应式查询
|
||||
*
|
||||
* 清理查询占用的资源,包括监听器和实体引用。
|
||||
* 销毁后的查询不应再被使用。
|
||||
*
|
||||
* @param query 要销毁的响应式查询
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const query = querySystem.createReactiveQuery([Position, Velocity]);
|
||||
* // ... 使用查询
|
||||
* querySystem.destroyReactiveQuery(query);
|
||||
* ```
|
||||
*/
|
||||
public destroyReactiveQuery(query: ReactiveQuery): void {
|
||||
if (!query) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cacheKey = query.id;
|
||||
this._reactiveQueries.delete(cacheKey);
|
||||
|
||||
for (const type of query.condition.componentTypes) {
|
||||
const queries = this._reactiveQueriesByComponent.get(type);
|
||||
if (queries) {
|
||||
queries.delete(query);
|
||||
if (queries.size === 0) {
|
||||
this._reactiveQueriesByComponent.delete(type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -780,6 +844,7 @@ export class QuerySystem {
|
||||
*
|
||||
* 根据组件类型列表生成对应的位掩码。
|
||||
* 使用缓存避免重复计算。
|
||||
* 注意:必须使用ComponentRegistry来确保与Entity.componentMask使用相同的bitIndex
|
||||
*
|
||||
* @param componentTypes 组件类型列表
|
||||
* @returns 生成的位掩码
|
||||
@@ -787,8 +852,7 @@ export class QuerySystem {
|
||||
private createComponentMask(componentTypes: ComponentType[]): BitMask64Data {
|
||||
// 生成缓存键
|
||||
const cacheKey = componentTypes.map(t => {
|
||||
const name = getComponentTypeName(t);
|
||||
return name;
|
||||
return getComponentTypeName(t);
|
||||
}).sort().join(',');
|
||||
|
||||
// 检查缓存
|
||||
@@ -797,22 +861,15 @@ export class QuerySystem {
|
||||
return cached;
|
||||
}
|
||||
|
||||
// 使用ComponentRegistry而不是ComponentTypeManager,确保bitIndex一致
|
||||
let mask = BitMask64Utils.clone(BitMask64Utils.ZERO);
|
||||
let hasValidComponents = false;
|
||||
|
||||
for (const type of componentTypes) {
|
||||
try {
|
||||
const bitMask = ComponentRegistry.getBitMask(type);
|
||||
BitMask64Utils.orInPlace(mask, bitMask);
|
||||
hasValidComponents = true;
|
||||
} catch (error) {
|
||||
this._logger.warn(`组件类型 ${getComponentTypeName(type)} 未注册,跳过`);
|
||||
// 确保组件已注册
|
||||
if (!ComponentRegistry.isRegistered(type)) {
|
||||
ComponentRegistry.register(type);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有有效的组件类型,返回一个不可能匹配的掩码
|
||||
if (!hasValidComponents) {
|
||||
mask = { lo: 0xFFFFFFFF, hi: 0xFFFFFFFF };
|
||||
const bitMask = ComponentRegistry.getBitMask(type);
|
||||
BitMask64Utils.orInPlace(mask, bitMask);
|
||||
}
|
||||
|
||||
// 缓存结果
|
||||
@@ -886,7 +943,7 @@ export class QuerySystem {
|
||||
}))
|
||||
},
|
||||
cacheStats: {
|
||||
size: this.queryCache.size,
|
||||
size: this._reactiveQueries.size,
|
||||
hitRate: this.queryStats.totalQueries > 0 ?
|
||||
(this.queryStats.cacheHits / this.queryStats.totalQueries * 100).toFixed(2) + '%' : '0%'
|
||||
}
|
||||
@@ -895,12 +952,230 @@ export class QuerySystem {
|
||||
|
||||
/**
|
||||
* 获取实体所属的原型信息
|
||||
*
|
||||
*
|
||||
* @param entity 要查询的实体
|
||||
*/
|
||||
public getEntityArchetype(entity: Entity): Archetype | undefined {
|
||||
return this.archetypeSystem.getEntityArchetype(entity);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 响应式查询支持(内部智能缓存)
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 响应式查询集合(内部使用,作为智能缓存)
|
||||
* 传统查询API(queryAll/queryAny/queryNone)内部自动使用响应式查询优化性能
|
||||
*/
|
||||
private _reactiveQueries: Map<string, ReactiveQuery> = new Map();
|
||||
|
||||
/**
|
||||
* 按组件类型索引的响应式查询
|
||||
* 用于快速定位哪些查询关心某个组件类型
|
||||
*/
|
||||
private _reactiveQueriesByComponent: Map<ComponentType, Set<ReactiveQuery>> = new Map();
|
||||
|
||||
/**
|
||||
* 获取或创建内部响应式查询(作为智能缓存)
|
||||
*
|
||||
* @param queryType 查询类型
|
||||
* @param componentTypes 组件类型列表
|
||||
* @returns 响应式查询实例
|
||||
*/
|
||||
private getOrCreateReactiveQuery(
|
||||
queryType: QueryConditionType,
|
||||
componentTypes: ComponentType[]
|
||||
): ReactiveQuery {
|
||||
// 生成缓存键(与传统缓存键格式一致)
|
||||
const cacheKey = this.generateCacheKey(queryType, componentTypes);
|
||||
|
||||
// 检查是否已存在响应式查询
|
||||
let reactiveQuery = this._reactiveQueries.get(cacheKey);
|
||||
|
||||
if (!reactiveQuery) {
|
||||
// 创建查询条件
|
||||
const mask = this.createComponentMask(componentTypes);
|
||||
const condition: QueryCondition = {
|
||||
type: queryType,
|
||||
componentTypes,
|
||||
mask
|
||||
};
|
||||
|
||||
// 创建响应式查询(禁用批量模式,保持实时性)
|
||||
reactiveQuery = new ReactiveQuery(condition, {
|
||||
enableBatchMode: false,
|
||||
debug: false
|
||||
});
|
||||
|
||||
// 初始化查询结果(使用传统方式获取初始数据)
|
||||
const initialEntities = this.executeTraditionalQuery(queryType, componentTypes);
|
||||
reactiveQuery.initializeWith(initialEntities);
|
||||
|
||||
// 注册响应式查询
|
||||
this._reactiveQueries.set(cacheKey, reactiveQuery);
|
||||
|
||||
// 为每个组件类型注册索引
|
||||
for (const type of componentTypes) {
|
||||
let queries = this._reactiveQueriesByComponent.get(type);
|
||||
if (!queries) {
|
||||
queries = new Set();
|
||||
this._reactiveQueriesByComponent.set(type, queries);
|
||||
}
|
||||
queries.add(reactiveQuery);
|
||||
}
|
||||
|
||||
this._logger.debug(`创建内部响应式查询缓存: ${cacheKey}`);
|
||||
}
|
||||
|
||||
return reactiveQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行传统查询(内部使用,用于响应式查询初始化)
|
||||
*
|
||||
* @param queryType 查询类型
|
||||
* @param componentTypes 组件类型列表
|
||||
* @returns 匹配的实体列表
|
||||
*/
|
||||
private executeTraditionalQuery(
|
||||
queryType: QueryConditionType,
|
||||
componentTypes: ComponentType[]
|
||||
): Entity[] {
|
||||
switch (queryType) {
|
||||
case QueryConditionType.ALL: {
|
||||
const archetypeResult = this.archetypeSystem.queryArchetypes(componentTypes, 'AND');
|
||||
const entities: Entity[] = [];
|
||||
for (const archetype of archetypeResult.archetypes) {
|
||||
for (const entity of archetype.entities) {
|
||||
entities.push(entity);
|
||||
}
|
||||
}
|
||||
return entities;
|
||||
}
|
||||
case QueryConditionType.ANY: {
|
||||
const archetypeResult = this.archetypeSystem.queryArchetypes(componentTypes, 'OR');
|
||||
const entities: Entity[] = [];
|
||||
for (const archetype of archetypeResult.archetypes) {
|
||||
for (const entity of archetype.entities) {
|
||||
entities.push(entity);
|
||||
}
|
||||
}
|
||||
return entities;
|
||||
}
|
||||
case QueryConditionType.NONE: {
|
||||
const mask = this.createComponentMask(componentTypes);
|
||||
return this.entities.filter(entity =>
|
||||
BitMask64Utils.hasNone(entity.componentMask, mask)
|
||||
);
|
||||
}
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知所有响应式查询实体已添加
|
||||
*
|
||||
* 使用组件类型索引,只通知关心该实体组件的查询
|
||||
*
|
||||
* @param entity 添加的实体
|
||||
*/
|
||||
private notifyReactiveQueriesEntityAdded(entity: Entity): void {
|
||||
if (this._reactiveQueries.size === 0) return;
|
||||
|
||||
const notified = new Set<ReactiveQuery>();
|
||||
|
||||
for (const component of entity.components) {
|
||||
const componentType = component.constructor as ComponentType;
|
||||
const queries = this._reactiveQueriesByComponent.get(componentType);
|
||||
|
||||
if (queries) {
|
||||
for (const query of queries) {
|
||||
if (!notified.has(query)) {
|
||||
query.notifyEntityAdded(entity);
|
||||
notified.add(query);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知响应式查询实体已移除
|
||||
*
|
||||
* 使用组件类型索引,只通知关心该实体组件的查询
|
||||
*
|
||||
* @param entity 移除的实体
|
||||
* @param componentTypes 实体移除前的组件类型列表
|
||||
*/
|
||||
private notifyReactiveQueriesEntityRemoved(entity: Entity, componentTypes: ComponentType[]): void {
|
||||
if (this._reactiveQueries.size === 0) return;
|
||||
|
||||
const notified = new Set<ReactiveQuery>();
|
||||
|
||||
for (const componentType of componentTypes) {
|
||||
const queries = this._reactiveQueriesByComponent.get(componentType);
|
||||
if (queries) {
|
||||
for (const query of queries) {
|
||||
if (!notified.has(query)) {
|
||||
query.notifyEntityRemoved(entity);
|
||||
notified.add(query);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知响应式查询实体已移除(后备方案)
|
||||
*
|
||||
* 当实体已经清空组件时使用,通知所有查询
|
||||
*
|
||||
* @param entity 移除的实体
|
||||
*/
|
||||
private notifyReactiveQueriesEntityRemovedFallback(entity: Entity): void {
|
||||
if (this._reactiveQueries.size === 0) return;
|
||||
|
||||
for (const query of this._reactiveQueries.values()) {
|
||||
query.notifyEntityRemoved(entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知响应式查询实体已变化
|
||||
*
|
||||
* 使用混合策略:
|
||||
* 1. 首先通知关心实体当前组件的查询
|
||||
* 2. 然后通知所有其他查询(包括那些可能因为组件移除而不再匹配的查询)
|
||||
*
|
||||
* @param entity 变化的实体
|
||||
*/
|
||||
private notifyReactiveQueriesEntityChanged(entity: Entity): void {
|
||||
if (this._reactiveQueries.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const notified = new Set<ReactiveQuery>();
|
||||
|
||||
for (const component of entity.components) {
|
||||
const componentType = component.constructor as ComponentType;
|
||||
const queries = this._reactiveQueriesByComponent.get(componentType);
|
||||
if (queries) {
|
||||
for (const query of queries) {
|
||||
if (!notified.has(query)) {
|
||||
query.notifyEntityChanged(entity);
|
||||
notified.add(query);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const query of this._reactiveQueries.values()) {
|
||||
if (!notified.has(query)) {
|
||||
query.notifyEntityChanged(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
36
packages/core/src/ECS/Core/QueryTypes.ts
Normal file
36
packages/core/src/ECS/Core/QueryTypes.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { ComponentType } from './ComponentStorage';
|
||||
import { BitMask64Data } from '../Utils/BigIntCompatibility';
|
||||
import { Entity } from '../Entity';
|
||||
|
||||
/**
|
||||
* 查询条件类型
|
||||
*/
|
||||
export enum QueryConditionType {
|
||||
/** 必须包含所有指定组件 */
|
||||
ALL = 'all',
|
||||
/** 必须包含任意一个指定组件 */
|
||||
ANY = 'any',
|
||||
/** 不能包含任何指定组件 */
|
||||
NONE = 'none'
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询条件接口
|
||||
*/
|
||||
export interface QueryCondition {
|
||||
type: QueryConditionType;
|
||||
componentTypes: ComponentType[];
|
||||
mask: BitMask64Data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 实体查询结果接口
|
||||
*/
|
||||
export interface QueryResult {
|
||||
entities: readonly Entity[];
|
||||
count: number;
|
||||
/** 查询执行时间(毫秒) */
|
||||
executionTime: number;
|
||||
/** 是否来自缓存 */
|
||||
fromCache: boolean;
|
||||
}
|
||||
475
packages/core/src/ECS/Core/ReactiveQuery.ts
Normal file
475
packages/core/src/ECS/Core/ReactiveQuery.ts
Normal file
@@ -0,0 +1,475 @@
|
||||
import { Entity } from '../Entity';
|
||||
import { QueryCondition, QueryConditionType } from './QueryTypes';
|
||||
import { BitMask64Utils } from '../Utils/BigIntCompatibility';
|
||||
import { createLogger } from '../../Utils/Logger';
|
||||
|
||||
const logger = createLogger('ReactiveQuery');
|
||||
|
||||
/**
|
||||
* 响应式查询变化类型
|
||||
*/
|
||||
export enum ReactiveQueryChangeType {
|
||||
/** 实体添加到查询结果 */
|
||||
ADDED = 'added',
|
||||
/** 实体从查询结果移除 */
|
||||
REMOVED = 'removed',
|
||||
/** 查询结果批量更新 */
|
||||
BATCH_UPDATE = 'batch_update'
|
||||
}
|
||||
|
||||
/**
|
||||
* 响应式查询变化事件
|
||||
*/
|
||||
export interface ReactiveQueryChange {
|
||||
/** 变化类型 */
|
||||
type: ReactiveQueryChangeType;
|
||||
/** 变化的实体 */
|
||||
entity?: Entity;
|
||||
/** 批量变化的实体 */
|
||||
entities?: readonly Entity[];
|
||||
/** 新增的实体列表(仅batch_update时有效) */
|
||||
added?: readonly Entity[];
|
||||
/** 移除的实体列表(仅batch_update时有效) */
|
||||
removed?: readonly Entity[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 响应式查询监听器
|
||||
*/
|
||||
export type ReactiveQueryListener = (change: ReactiveQueryChange) => void;
|
||||
|
||||
/**
|
||||
* 响应式查询配置
|
||||
*/
|
||||
export interface ReactiveQueryConfig {
|
||||
/** 是否启用批量模式(减少通知频率) */
|
||||
enableBatchMode?: boolean;
|
||||
/** 批量模式的延迟时间(毫秒) */
|
||||
batchDelay?: number;
|
||||
/** 调试模式 */
|
||||
debug?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 响应式查询类
|
||||
*
|
||||
* 提供基于事件驱动的实体查询机制,只在实体/组件真正变化时触发通知。
|
||||
*
|
||||
* 核心特性:
|
||||
* - Event-driven: 基于事件的增量更新
|
||||
* - 精确通知: 只通知真正匹配的变化
|
||||
* - 性能优化: 避免每帧重复查询
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 创建响应式查询
|
||||
* const query = new ReactiveQuery(querySystem, {
|
||||
* type: QueryConditionType.ALL,
|
||||
* componentTypes: [Position, Velocity],
|
||||
* mask: createMask([Position, Velocity])
|
||||
* });
|
||||
*
|
||||
* // 订阅变化
|
||||
* query.subscribe((change) => {
|
||||
* if (change.type === ReactiveQueryChangeType.ADDED) {
|
||||
* console.log('新实体:', change.entity);
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* // 获取当前结果
|
||||
* const entities = query.getEntities();
|
||||
* ```
|
||||
*/
|
||||
export class ReactiveQuery {
|
||||
/** 当前查询结果 */
|
||||
private _entities: Entity[] = [];
|
||||
|
||||
/** 实体ID集合,用于快速查找 */
|
||||
private _entityIdSet: Set<number> = new Set();
|
||||
|
||||
/** 查询条件 */
|
||||
private readonly _condition: QueryCondition;
|
||||
|
||||
/** 监听器列表 */
|
||||
private _listeners: ReactiveQueryListener[] = [];
|
||||
|
||||
/** 配置 */
|
||||
private readonly _config: ReactiveQueryConfig;
|
||||
|
||||
/** 批量变化缓存 */
|
||||
private _batchChanges: {
|
||||
added: Entity[];
|
||||
removed: Entity[];
|
||||
timer: ReturnType<typeof setTimeout> | null;
|
||||
};
|
||||
|
||||
/** 查询ID(用于调试) */
|
||||
private readonly _id: string;
|
||||
|
||||
/** 是否已激活 */
|
||||
private _active: boolean = true;
|
||||
|
||||
constructor(condition: QueryCondition, config: ReactiveQueryConfig = {}) {
|
||||
this._condition = condition;
|
||||
this._config = {
|
||||
enableBatchMode: config.enableBatchMode ?? true,
|
||||
batchDelay: config.batchDelay ?? 16, // 默认一帧
|
||||
debug: config.debug ?? false
|
||||
};
|
||||
|
||||
this._id = this.generateQueryId();
|
||||
|
||||
this._batchChanges = {
|
||||
added: [],
|
||||
removed: [],
|
||||
timer: null
|
||||
};
|
||||
|
||||
if (this._config.debug) {
|
||||
logger.debug(`创建ReactiveQuery: ${this._id}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成查询ID
|
||||
*/
|
||||
private generateQueryId(): string {
|
||||
const typeStr = this._condition.type;
|
||||
const componentsStr = this._condition.componentTypes
|
||||
.map(t => t.name)
|
||||
.sort()
|
||||
.join(',');
|
||||
return `${typeStr}:${componentsStr}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅查询变化
|
||||
*
|
||||
* @param listener 监听器函数
|
||||
* @returns 取消订阅的函数
|
||||
*/
|
||||
public subscribe(listener: ReactiveQueryListener): () => void {
|
||||
if (!this._active) {
|
||||
throw new Error(`Cannot subscribe to disposed ReactiveQuery ${this._id}`);
|
||||
}
|
||||
|
||||
if (typeof listener !== 'function') {
|
||||
throw new TypeError('Listener must be a function');
|
||||
}
|
||||
|
||||
this._listeners.push(listener);
|
||||
|
||||
if (this._config.debug) {
|
||||
logger.debug(`订阅ReactiveQuery: ${this._id}, 监听器数量: ${this._listeners.length}`);
|
||||
}
|
||||
|
||||
return () => {
|
||||
const index = this._listeners.indexOf(listener);
|
||||
if (index !== -1) {
|
||||
this._listeners.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消所有订阅
|
||||
*/
|
||||
public unsubscribeAll(): void {
|
||||
this._listeners.length = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前查询结果
|
||||
*/
|
||||
public getEntities(): readonly Entity[] {
|
||||
return this._entities;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取查询结果数量
|
||||
*/
|
||||
public get count(): number {
|
||||
return this._entities.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查实体是否匹配查询条件
|
||||
*
|
||||
* @param entity 要检查的实体
|
||||
* @returns 是否匹配
|
||||
*/
|
||||
public matches(entity: Entity): boolean {
|
||||
const entityMask = entity.componentMask;
|
||||
|
||||
switch (this._condition.type) {
|
||||
case QueryConditionType.ALL:
|
||||
return BitMask64Utils.hasAll(entityMask, this._condition.mask);
|
||||
case QueryConditionType.ANY:
|
||||
return BitMask64Utils.hasAny(entityMask, this._condition.mask);
|
||||
case QueryConditionType.NONE:
|
||||
return BitMask64Utils.hasNone(entityMask, this._condition.mask);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知实体添加
|
||||
*
|
||||
* 当Scene中添加实体时调用
|
||||
*
|
||||
* @param entity 添加的实体
|
||||
*/
|
||||
public notifyEntityAdded(entity: Entity): void {
|
||||
if (!this._active) return;
|
||||
|
||||
// 检查实体是否匹配查询条件
|
||||
if (!this.matches(entity)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否已存在
|
||||
if (this._entityIdSet.has(entity.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 添加到结果集
|
||||
this._entities.push(entity);
|
||||
this._entityIdSet.add(entity.id);
|
||||
|
||||
// 通知监听器
|
||||
if (this._config.enableBatchMode) {
|
||||
this.addToBatch('added', entity);
|
||||
} else {
|
||||
this.notifyListeners({
|
||||
type: ReactiveQueryChangeType.ADDED,
|
||||
entity
|
||||
});
|
||||
}
|
||||
|
||||
if (this._config.debug) {
|
||||
logger.debug(`ReactiveQuery ${this._id}: 实体添加 ${entity.name}(${entity.id})`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知实体移除
|
||||
*
|
||||
* 当Scene中移除实体时调用
|
||||
*
|
||||
* @param entity 移除的实体
|
||||
*/
|
||||
public notifyEntityRemoved(entity: Entity): void {
|
||||
if (!this._active) return;
|
||||
|
||||
// 检查是否在结果集中
|
||||
if (!this._entityIdSet.has(entity.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 从结果集移除
|
||||
const index = this._entities.indexOf(entity);
|
||||
if (index !== -1) {
|
||||
this._entities.splice(index, 1);
|
||||
}
|
||||
this._entityIdSet.delete(entity.id);
|
||||
|
||||
// 通知监听器
|
||||
if (this._config.enableBatchMode) {
|
||||
this.addToBatch('removed', entity);
|
||||
} else {
|
||||
this.notifyListeners({
|
||||
type: ReactiveQueryChangeType.REMOVED,
|
||||
entity
|
||||
});
|
||||
}
|
||||
|
||||
if (this._config.debug) {
|
||||
logger.debug(`ReactiveQuery ${this._id}: 实体移除 ${entity.name}(${entity.id})`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知实体组件变化
|
||||
*
|
||||
* 当实体的组件发生变化时调用
|
||||
*
|
||||
* @param entity 变化的实体
|
||||
*/
|
||||
public notifyEntityChanged(entity: Entity): void {
|
||||
if (!this._active) return;
|
||||
|
||||
const wasMatching = this._entityIdSet.has(entity.id);
|
||||
const isMatching = this.matches(entity);
|
||||
|
||||
if (wasMatching && !isMatching) {
|
||||
// 实体不再匹配,从结果集移除
|
||||
this.notifyEntityRemoved(entity);
|
||||
} else if (!wasMatching && isMatching) {
|
||||
// 实体现在匹配,添加到结果集
|
||||
this.notifyEntityAdded(entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量初始化查询结果
|
||||
*
|
||||
* @param entities 初始实体列表
|
||||
*/
|
||||
public initializeWith(entities: readonly Entity[]): void {
|
||||
// 清空现有结果
|
||||
this._entities.length = 0;
|
||||
this._entityIdSet.clear();
|
||||
|
||||
// 筛选匹配的实体
|
||||
for (const entity of entities) {
|
||||
if (this.matches(entity)) {
|
||||
this._entities.push(entity);
|
||||
this._entityIdSet.add(entity.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (this._config.debug) {
|
||||
logger.debug(`ReactiveQuery ${this._id}: 初始化 ${this._entities.length} 个实体`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加到批量变化缓存
|
||||
*/
|
||||
private addToBatch(type: 'added' | 'removed', entity: Entity): void {
|
||||
if (type === 'added') {
|
||||
this._batchChanges.added.push(entity);
|
||||
} else {
|
||||
this._batchChanges.removed.push(entity);
|
||||
}
|
||||
|
||||
// 启动批量通知定时器
|
||||
if (this._batchChanges.timer === null) {
|
||||
this._batchChanges.timer = setTimeout(() => {
|
||||
this.flushBatchChanges();
|
||||
}, this._config.batchDelay);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新批量变化
|
||||
*/
|
||||
private flushBatchChanges(): void {
|
||||
if (this._batchChanges.added.length === 0 && this._batchChanges.removed.length === 0) {
|
||||
this._batchChanges.timer = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const added = [...this._batchChanges.added];
|
||||
const removed = [...this._batchChanges.removed];
|
||||
|
||||
// 清空缓存
|
||||
this._batchChanges.added.length = 0;
|
||||
this._batchChanges.removed.length = 0;
|
||||
this._batchChanges.timer = null;
|
||||
|
||||
// 通知监听器
|
||||
this.notifyListeners({
|
||||
type: ReactiveQueryChangeType.BATCH_UPDATE,
|
||||
added,
|
||||
removed,
|
||||
entities: this._entities
|
||||
});
|
||||
|
||||
if (this._config.debug) {
|
||||
logger.debug(`ReactiveQuery ${this._id}: 批量更新 +${added.length} -${removed.length}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知所有监听器
|
||||
*/
|
||||
private notifyListeners(change: ReactiveQueryChange): void {
|
||||
const listeners = [...this._listeners];
|
||||
|
||||
for (const listener of listeners) {
|
||||
try {
|
||||
listener(change);
|
||||
} catch (error) {
|
||||
logger.error(`ReactiveQuery ${this._id}: 监听器执行出错`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停响应式查询
|
||||
*
|
||||
* 暂停后不再响应实体变化,但可以继续获取当前结果
|
||||
*/
|
||||
public pause(): void {
|
||||
this._active = false;
|
||||
|
||||
// 清空批量变化缓存
|
||||
if (this._batchChanges.timer !== null) {
|
||||
clearTimeout(this._batchChanges.timer);
|
||||
this._batchChanges.timer = null;
|
||||
}
|
||||
this._batchChanges.added.length = 0;
|
||||
this._batchChanges.removed.length = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复响应式查询
|
||||
*/
|
||||
public resume(): void {
|
||||
this._active = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁响应式查询
|
||||
*
|
||||
* 释放所有资源,清空监听器和结果集
|
||||
*/
|
||||
public dispose(): void {
|
||||
if (this._batchChanges.timer !== null) {
|
||||
clearTimeout(this._batchChanges.timer);
|
||||
this._batchChanges.timer = null;
|
||||
}
|
||||
|
||||
this._batchChanges.added.length = 0;
|
||||
this._batchChanges.removed.length = 0;
|
||||
|
||||
this._active = false;
|
||||
this.unsubscribeAll();
|
||||
this._entities.length = 0;
|
||||
this._entityIdSet.clear();
|
||||
|
||||
if (this._config.debug) {
|
||||
logger.debug(`ReactiveQuery ${this._id}: 已销毁`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取查询条件
|
||||
*/
|
||||
public get condition(): QueryCondition {
|
||||
return this._condition;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取查询ID
|
||||
*/
|
||||
public get id(): string {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否激活
|
||||
*/
|
||||
public get active(): boolean {
|
||||
return this._active;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取监听器数量
|
||||
*/
|
||||
public get listenerCount(): number {
|
||||
return this._listeners.length;
|
||||
}
|
||||
}
|
||||
345
packages/core/src/ECS/Core/ReferenceTracker.ts
Normal file
345
packages/core/src/ECS/Core/ReferenceTracker.ts
Normal file
@@ -0,0 +1,345 @@
|
||||
import { Component } from '../Component';
|
||||
import type { Entity } from '../Entity';
|
||||
import type { IScene } from '../IScene';
|
||||
import { createLogger } from '../../Utils/Logger';
|
||||
|
||||
const logger = createLogger('ReferenceTracker');
|
||||
|
||||
/**
|
||||
* WeakRef 接口定义
|
||||
*
|
||||
* 用于 ES2020 环境下的类型定义
|
||||
*/
|
||||
interface IWeakRef<T extends object> {
|
||||
deref(): T | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* WeakRef Polyfill for ES2020 compatibility
|
||||
*
|
||||
* 为了兼容 Cocos Creator、Laya、微信小游戏等目标平台(仅支持 ES2020),
|
||||
* 提供 WeakRef 的 Polyfill 实现。
|
||||
*
|
||||
* - 现代浏览器:自动使用原生 WeakRef (自动 GC)
|
||||
* - 旧环境:使用 Polyfill (无自动 GC,但 Scene 销毁时会手动清理)
|
||||
*/
|
||||
class WeakRefPolyfill<T extends object> implements IWeakRef<T> {
|
||||
private _target: T;
|
||||
|
||||
constructor(target: T) {
|
||||
this._target = target;
|
||||
}
|
||||
|
||||
deref(): T | undefined {
|
||||
return this._target;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WeakRef 构造函数类型
|
||||
*/
|
||||
interface IWeakRefConstructor {
|
||||
new <T extends object>(target: T): IWeakRef<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* WeakRef 实现
|
||||
*
|
||||
* 优先使用原生 WeakRef,不支持时降级到 Polyfill
|
||||
*/
|
||||
const WeakRefImpl: IWeakRefConstructor = (
|
||||
(typeof globalThis !== 'undefined' && (globalThis as any).WeakRef) ||
|
||||
(typeof global !== 'undefined' && (global as any).WeakRef) ||
|
||||
(typeof window !== 'undefined' && (window as any).WeakRef) ||
|
||||
WeakRefPolyfill
|
||||
) as IWeakRefConstructor;
|
||||
|
||||
/**
|
||||
* Entity引用记录
|
||||
*/
|
||||
export interface EntityRefRecord {
|
||||
component: IWeakRef<Component>;
|
||||
propertyKey: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局EntityID到Scene的映射
|
||||
*
|
||||
* 使用全局Map记录每个Entity ID对应的Scene,用于装饰器通过Component.entityId查找Scene。
|
||||
*/
|
||||
const globalEntitySceneMap = new Map<number, IWeakRef<IScene>>();
|
||||
|
||||
/**
|
||||
* 通过Entity ID获取Scene
|
||||
*
|
||||
* @param entityId Entity ID
|
||||
* @returns Scene实例,如果不存在则返回null
|
||||
*/
|
||||
export function getSceneByEntityId(entityId: number): IScene | null {
|
||||
const sceneRef = globalEntitySceneMap.get(entityId);
|
||||
return sceneRef?.deref() || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Entity引用追踪器
|
||||
*
|
||||
* 追踪Component中对Entity的引用,当Entity被销毁时自动清理所有引用。
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const tracker = new ReferenceTracker();
|
||||
* tracker.registerReference(targetEntity, component, 'parent');
|
||||
* targetEntity.destroy(); // 自动将 component.parent 设为 null
|
||||
* ```
|
||||
*/
|
||||
export class ReferenceTracker {
|
||||
/**
|
||||
* Entity ID -> 引用该Entity的所有组件记录
|
||||
*/
|
||||
private _references: Map<number, Set<EntityRefRecord>> = new Map();
|
||||
|
||||
/**
|
||||
* 当前Scene的引用
|
||||
*/
|
||||
private _scene: IWeakRef<IScene> | null = null;
|
||||
|
||||
/**
|
||||
* 注册Entity引用
|
||||
*
|
||||
* @param entity 被引用的Entity
|
||||
* @param component 持有引用的Component
|
||||
* @param propertyKey Component中存储引用的属性名
|
||||
*/
|
||||
public registerReference(entity: Entity, component: Component, propertyKey: string): void {
|
||||
const entityId = entity.id;
|
||||
|
||||
let records = this._references.get(entityId);
|
||||
if (!records) {
|
||||
records = new Set();
|
||||
this._references.set(entityId, records);
|
||||
}
|
||||
|
||||
const existingRecord = this._findRecord(records, component, propertyKey);
|
||||
if (existingRecord) {
|
||||
return;
|
||||
}
|
||||
|
||||
records.add({
|
||||
component: new WeakRefImpl(component),
|
||||
propertyKey
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销Entity引用
|
||||
*
|
||||
* @param entity 被引用的Entity
|
||||
* @param component 持有引用的Component
|
||||
* @param propertyKey Component中存储引用的属性名
|
||||
*/
|
||||
public unregisterReference(entity: Entity, component: Component, propertyKey: string): void {
|
||||
const entityId = entity.id;
|
||||
const records = this._references.get(entityId);
|
||||
|
||||
if (!records) {
|
||||
return;
|
||||
}
|
||||
|
||||
const record = this._findRecord(records, component, propertyKey);
|
||||
if (record) {
|
||||
records.delete(record);
|
||||
|
||||
if (records.size === 0) {
|
||||
this._references.delete(entityId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理所有指向指定Entity的引用
|
||||
*
|
||||
* 将所有引用该Entity的Component属性设为null。
|
||||
*
|
||||
* @param entityId 被销毁的Entity ID
|
||||
*/
|
||||
public clearReferencesTo(entityId: number): void {
|
||||
const records = this._references.get(entityId);
|
||||
|
||||
if (!records) {
|
||||
return;
|
||||
}
|
||||
|
||||
const validRecords: EntityRefRecord[] = [];
|
||||
|
||||
for (const record of records) {
|
||||
const component = record.component.deref();
|
||||
|
||||
if (component) {
|
||||
validRecords.push(record);
|
||||
}
|
||||
}
|
||||
|
||||
for (const record of validRecords) {
|
||||
const component = record.component.deref();
|
||||
if (component) {
|
||||
(component as any)[record.propertyKey] = null;
|
||||
}
|
||||
}
|
||||
|
||||
this._references.delete(entityId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理Component的所有引用注册
|
||||
*
|
||||
* 当Component被移除时调用,清理该Component注册的所有引用。
|
||||
*
|
||||
* @param component 被移除的Component
|
||||
*/
|
||||
public clearComponentReferences(component: Component): void {
|
||||
for (const [entityId, records] of this._references.entries()) {
|
||||
const toDelete: EntityRefRecord[] = [];
|
||||
|
||||
for (const record of records) {
|
||||
const comp = record.component.deref();
|
||||
if (!comp || comp === component) {
|
||||
toDelete.push(record);
|
||||
}
|
||||
}
|
||||
|
||||
for (const record of toDelete) {
|
||||
records.delete(record);
|
||||
}
|
||||
|
||||
if (records.size === 0) {
|
||||
this._references.delete(entityId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指向指定Entity的所有引用记录
|
||||
*
|
||||
* @param entityId Entity ID
|
||||
* @returns 引用记录数组(仅包含有效引用)
|
||||
*/
|
||||
public getReferencesTo(entityId: number): EntityRefRecord[] {
|
||||
const records = this._references.get(entityId);
|
||||
if (!records) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const validRecords: EntityRefRecord[] = [];
|
||||
|
||||
for (const record of records) {
|
||||
const component = record.component.deref();
|
||||
if (component) {
|
||||
validRecords.push(record);
|
||||
}
|
||||
}
|
||||
|
||||
return validRecords;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理所有失效的WeakRef引用
|
||||
*
|
||||
* 遍历所有记录,移除已被GC回收的Component引用。
|
||||
*/
|
||||
public cleanup(): void {
|
||||
const entitiesToDelete: number[] = [];
|
||||
|
||||
for (const [entityId, records] of this._references.entries()) {
|
||||
const toDelete: EntityRefRecord[] = [];
|
||||
|
||||
for (const record of records) {
|
||||
if (!record.component.deref()) {
|
||||
toDelete.push(record);
|
||||
}
|
||||
}
|
||||
|
||||
for (const record of toDelete) {
|
||||
records.delete(record);
|
||||
}
|
||||
|
||||
if (records.size === 0) {
|
||||
entitiesToDelete.push(entityId);
|
||||
}
|
||||
}
|
||||
|
||||
for (const entityId of entitiesToDelete) {
|
||||
this._references.delete(entityId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置Scene引用
|
||||
*
|
||||
* @param scene Scene实例
|
||||
*/
|
||||
public setScene(scene: IScene): void {
|
||||
this._scene = new WeakRefImpl(scene);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册Entity到Scene的映射
|
||||
*
|
||||
* @param entityId Entity ID
|
||||
* @param scene Scene实例
|
||||
*/
|
||||
public registerEntityScene(entityId: number, scene: IScene): void {
|
||||
globalEntitySceneMap.set(entityId, new WeakRefImpl(scene));
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销Entity到Scene的映射
|
||||
*
|
||||
* @param entityId Entity ID
|
||||
*/
|
||||
public unregisterEntityScene(entityId: number): void {
|
||||
globalEntitySceneMap.delete(entityId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取调试信息
|
||||
*/
|
||||
public getDebugInfo(): object {
|
||||
const info: Record<string, any> = {};
|
||||
|
||||
for (const [entityId, records] of this._references.entries()) {
|
||||
const validRecords = [];
|
||||
for (const record of records) {
|
||||
const component = record.component.deref();
|
||||
if (component) {
|
||||
validRecords.push({
|
||||
componentId: component.id,
|
||||
propertyKey: record.propertyKey
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (validRecords.length > 0) {
|
||||
info[`entity_${entityId}`] = validRecords;
|
||||
}
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找指定的引用记录
|
||||
*/
|
||||
private _findRecord(
|
||||
records: Set<EntityRefRecord>,
|
||||
component: Component,
|
||||
propertyKey: string
|
||||
): EntityRefRecord | undefined {
|
||||
for (const record of records) {
|
||||
const comp = record.component.deref();
|
||||
if (comp === component && record.propertyKey === propertyKey) {
|
||||
return record;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
147
packages/core/src/ECS/Decorators/EntityRefDecorator.ts
Normal file
147
packages/core/src/ECS/Decorators/EntityRefDecorator.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import type { Entity } from '../Entity';
|
||||
import type { Component } from '../Component';
|
||||
import { getSceneByEntityId } from '../Core/ReferenceTracker';
|
||||
import { createLogger } from '../../Utils/Logger';
|
||||
|
||||
const logger = createLogger('EntityRefDecorator');
|
||||
|
||||
/**
|
||||
* EntityRef元数据的Symbol键
|
||||
*/
|
||||
export const ENTITY_REF_METADATA = Symbol('EntityRefMetadata');
|
||||
|
||||
/**
|
||||
* EntityRef值存储的Symbol键
|
||||
*/
|
||||
const ENTITY_REF_VALUES = Symbol('EntityRefValues');
|
||||
|
||||
/**
|
||||
* EntityRef元数据
|
||||
*/
|
||||
export interface EntityRefMetadata {
|
||||
properties: Set<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取或创建组件的EntityRef值存储Map
|
||||
*/
|
||||
function getValueMap(component: Component): Map<string, Entity | null> {
|
||||
let map = (component as any)[ENTITY_REF_VALUES];
|
||||
if (!map) {
|
||||
map = new Map<string, Entity | null>();
|
||||
(component as any)[ENTITY_REF_VALUES] = map;
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Entity引用装饰器
|
||||
*
|
||||
* 标记Component属性为Entity引用,自动追踪引用关系。
|
||||
* 当被引用的Entity销毁时,该属性会自动设为null。
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* class ParentComponent extends Component {
|
||||
* @EntityRef() parent: Entity | null = null;
|
||||
* }
|
||||
*
|
||||
* const parent = scene.createEntity('Parent');
|
||||
* const child = scene.createEntity('Child');
|
||||
* const comp = child.addComponent(new ParentComponent());
|
||||
*
|
||||
* comp.parent = parent;
|
||||
* parent.destroy(); // comp.parent 自动变为 null
|
||||
* ```
|
||||
*/
|
||||
export function EntityRef(): PropertyDecorator {
|
||||
return function (target: any, propertyKey: string | symbol) {
|
||||
const constructor = target.constructor;
|
||||
|
||||
let metadata: EntityRefMetadata = constructor[ENTITY_REF_METADATA];
|
||||
if (!metadata) {
|
||||
metadata = {
|
||||
properties: new Set()
|
||||
};
|
||||
constructor[ENTITY_REF_METADATA] = metadata;
|
||||
}
|
||||
|
||||
const propKeyString = typeof propertyKey === 'symbol' ? propertyKey.toString() : propertyKey;
|
||||
metadata.properties.add(propKeyString);
|
||||
|
||||
Object.defineProperty(target, propertyKey, {
|
||||
get: function (this: Component) {
|
||||
const valueMap = getValueMap(this);
|
||||
return valueMap.get(propKeyString) || null;
|
||||
},
|
||||
set: function (this: Component, newValue: Entity | null) {
|
||||
const valueMap = getValueMap(this);
|
||||
const oldValue = valueMap.get(propKeyString) || null;
|
||||
|
||||
if (oldValue === newValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scene = this.entityId !== null ? getSceneByEntityId(this.entityId) : null;
|
||||
|
||||
if (!scene || !scene.referenceTracker) {
|
||||
valueMap.set(propKeyString, newValue);
|
||||
return;
|
||||
}
|
||||
|
||||
const tracker = scene.referenceTracker;
|
||||
|
||||
if (oldValue) {
|
||||
tracker.unregisterReference(oldValue, this, propKeyString);
|
||||
}
|
||||
|
||||
if (newValue) {
|
||||
if (newValue.scene !== scene) {
|
||||
logger.error(`Cannot reference Entity from different Scene. Entity: ${newValue.name}, Scene: ${newValue.scene?.name || 'null'}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (newValue.isDestroyed) {
|
||||
logger.warn(`Cannot reference destroyed Entity: ${newValue.name}`);
|
||||
valueMap.set(propKeyString, null);
|
||||
return;
|
||||
}
|
||||
|
||||
tracker.registerReference(newValue, this, propKeyString);
|
||||
}
|
||||
|
||||
valueMap.set(propKeyString, newValue);
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Component的EntityRef元数据
|
||||
*
|
||||
* @param component Component实例或Component类
|
||||
* @returns EntityRef元数据,如果不存在则返回null
|
||||
*/
|
||||
export function getEntityRefMetadata(component: any): EntityRefMetadata | null {
|
||||
if (!component) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const constructor = typeof component === 'function'
|
||||
? component
|
||||
: component.constructor;
|
||||
|
||||
return constructor[ENTITY_REF_METADATA] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查Component是否有EntityRef属性
|
||||
*
|
||||
* @param component Component实例或Component类
|
||||
* @returns 如果有EntityRef属性返回true
|
||||
*/
|
||||
export function hasEntityRef(component: any): boolean {
|
||||
return getEntityRefMetadata(component) !== null;
|
||||
}
|
||||
@@ -39,34 +39,72 @@ export function ECSComponent(typeName: string) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* System元数据配置
|
||||
*/
|
||||
export interface SystemMetadata {
|
||||
/**
|
||||
* 更新顺序(数值越小越先执行,默认0)
|
||||
*/
|
||||
updateOrder?: number;
|
||||
|
||||
/**
|
||||
* 是否默认启用(默认true)
|
||||
*/
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统类型装饰器
|
||||
* 用于为系统类指定固定的类型名称,避免在代码混淆后失效
|
||||
*
|
||||
*
|
||||
* @param typeName 系统类型名称
|
||||
* @param metadata 系统元数据配置
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 基本使用
|
||||
* @ECSSystem('Movement')
|
||||
* class MovementSystem extends EntitySystem {
|
||||
* protected process(entities: Entity[]): void {
|
||||
* // 系统逻辑
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* // 配置更新顺序
|
||||
* @Injectable()
|
||||
* @ECSSystem('Physics', { updateOrder: 10 })
|
||||
* class PhysicsSystem extends EntitySystem {
|
||||
* constructor(@Inject(CollisionSystem) private collision: CollisionSystem) {
|
||||
* super(Matcher.empty().all(Transform, RigidBody));
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function ECSSystem(typeName: string) {
|
||||
export function ECSSystem(typeName: string, metadata?: SystemMetadata) {
|
||||
return function <T extends new (...args: any[]) => EntitySystem>(target: T): T {
|
||||
if (!typeName || typeof typeName !== 'string') {
|
||||
throw new Error('ECSSystem装饰器必须提供有效的类型名称');
|
||||
}
|
||||
|
||||
|
||||
// 在构造函数上存储类型名称
|
||||
(target as any)[SYSTEM_TYPE_NAME] = typeName;
|
||||
|
||||
|
||||
// 存储元数据
|
||||
if (metadata) {
|
||||
(target as any).__systemMetadata__ = metadata;
|
||||
}
|
||||
|
||||
return target;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取System的元数据
|
||||
*/
|
||||
export function getSystemMetadata(systemType: new (...args: any[]) => EntitySystem): SystemMetadata | undefined {
|
||||
return (systemType as any).__systemMetadata__;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取组件类型的名称,优先使用装饰器指定的名称
|
||||
*
|
||||
|
||||
@@ -5,6 +5,18 @@ export {
|
||||
getSystemTypeName,
|
||||
getComponentInstanceTypeName,
|
||||
getSystemInstanceTypeName,
|
||||
getSystemMetadata,
|
||||
COMPONENT_TYPE_NAME,
|
||||
SYSTEM_TYPE_NAME
|
||||
} from './TypeDecorators';
|
||||
} from './TypeDecorators';
|
||||
|
||||
export type { SystemMetadata } from './TypeDecorators';
|
||||
|
||||
export {
|
||||
EntityRef,
|
||||
getEntityRefMetadata,
|
||||
hasEntityRef,
|
||||
ENTITY_REF_METADATA
|
||||
} from './EntityRefDecorator';
|
||||
|
||||
export type { EntityRefMetadata } from './EntityRefDecorator';
|
||||
@@ -145,12 +145,6 @@ export class Entity {
|
||||
*/
|
||||
private _componentCache: Component[] | null = null;
|
||||
|
||||
/**
|
||||
* 本地组件存储(用于没有 Scene 的 Entity)
|
||||
* 当 Entity 添加到 Scene 时,组件会迁移到 Scene 的 componentStorageManager
|
||||
*/
|
||||
private _localComponents: Map<ComponentType, Component> = new Map();
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
@@ -186,28 +180,23 @@ export class Entity {
|
||||
*/
|
||||
private _rebuildComponentCache(): void {
|
||||
const components: Component[] = [];
|
||||
const mask = this._componentMask;
|
||||
|
||||
if (!this.scene?.componentStorageManager) {
|
||||
this._componentCache = components;
|
||||
return;
|
||||
}
|
||||
|
||||
const mask = this._componentMask;
|
||||
const maxBitIndex = ComponentRegistry.getRegisteredCount();
|
||||
|
||||
for (let bitIndex = 0; bitIndex < maxBitIndex; bitIndex++) {
|
||||
if (BitMask64Utils.getBitExtended(mask, bitIndex)) {
|
||||
if (BitMask64Utils.getBit(mask, bitIndex)) {
|
||||
const componentType = ComponentRegistry.getTypeByBitIndex(bitIndex);
|
||||
if (componentType) {
|
||||
let component: Component | null = null;
|
||||
|
||||
// 优先从 Scene 存储获取
|
||||
if (this.scene?.componentStorageManager) {
|
||||
component = this.scene.componentStorageManager.getComponent(
|
||||
this.id,
|
||||
componentType
|
||||
);
|
||||
}
|
||||
|
||||
// Fallback 到本地存储
|
||||
if (!component) {
|
||||
component = this._localComponents.get(componentType) || null;
|
||||
}
|
||||
const component = this.scene.componentStorageManager.getComponent(
|
||||
this.id,
|
||||
componentType
|
||||
);
|
||||
|
||||
if (component) {
|
||||
components.push(component);
|
||||
@@ -346,13 +335,19 @@ export class Entity {
|
||||
|
||||
/**
|
||||
* 创建并添加组件
|
||||
*
|
||||
* @param componentType - 组件类型
|
||||
*
|
||||
* @param componentType - 组件类型构造函数
|
||||
* @param args - 组件构造函数参数
|
||||
* @returns 创建的组件实例
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const position = entity.createComponent(Position, 100, 200);
|
||||
* const health = entity.createComponent(Health, 100);
|
||||
* ```
|
||||
*/
|
||||
public createComponent<T extends Component>(
|
||||
componentType: ComponentType<T>,
|
||||
componentType: ComponentType<T>,
|
||||
...args: any[]
|
||||
): T {
|
||||
const component = new componentType(...args);
|
||||
@@ -372,9 +367,6 @@ export class Entity {
|
||||
ComponentRegistry.register(componentType);
|
||||
}
|
||||
|
||||
// 存储到本地 Map
|
||||
this._localComponents.set(componentType, component);
|
||||
|
||||
// 更新位掩码
|
||||
const componentMask = ComponentRegistry.getBitMask(componentType);
|
||||
BitMask64Utils.orInPlace(this._componentMask, componentMask);
|
||||
@@ -387,26 +379,42 @@ export class Entity {
|
||||
|
||||
/**
|
||||
* 添加组件到实体
|
||||
*
|
||||
*
|
||||
* @param component - 要添加的组件实例
|
||||
* @returns 添加的组件实例
|
||||
* @throws {Error} 如果组件类型已存在
|
||||
* @throws {Error} 如果实体已存在该类型的组件
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const position = new Position(100, 200);
|
||||
* entity.addComponent(position);
|
||||
* ```
|
||||
*/
|
||||
public addComponent<T extends Component>(component: T): T {
|
||||
const componentType = component.constructor as ComponentType<T>;
|
||||
|
||||
|
||||
if (!this.scene) {
|
||||
throw new Error(`Entity must be added to Scene before adding components. Use scene.createEntity() instead of new Entity()`);
|
||||
}
|
||||
|
||||
if (!this.scene.componentStorageManager) {
|
||||
throw new Error(`Scene does not have componentStorageManager`);
|
||||
}
|
||||
|
||||
if (this.hasComponent(componentType)) {
|
||||
throw new Error(`Entity ${this.name} already has component ${getComponentTypeName(componentType)}`);
|
||||
}
|
||||
|
||||
this.addComponentInternal(component);
|
||||
|
||||
if (this.scene && this.scene.componentStorageManager) {
|
||||
this.scene.componentStorageManager.addComponent(this.id, component);
|
||||
}
|
||||
|
||||
this.scene.componentStorageManager.addComponent(this.id, component);
|
||||
|
||||
component.entityId = this.id;
|
||||
if (this.scene.referenceTracker) {
|
||||
this.scene.referenceTracker.registerEntityScene(this.id, this.scene);
|
||||
}
|
||||
component.onAddedToEntity();
|
||||
|
||||
|
||||
if (Entity.eventBus) {
|
||||
Entity.eventBus.emitComponentAdded({
|
||||
timestamp: Date.now(),
|
||||
@@ -418,7 +426,7 @@ export class Entity {
|
||||
component: component
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 通知所有相关的QuerySystem组件已变动
|
||||
Entity.notifyQuerySystems(this);
|
||||
@@ -429,8 +437,17 @@ export class Entity {
|
||||
/**
|
||||
* 获取指定类型的组件
|
||||
*
|
||||
* @param type - 组件类型
|
||||
* @returns 组件实例或null
|
||||
* @param type - 组件类型构造函数
|
||||
* @returns 组件实例,如果不存在则返回null
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const position = entity.getComponent(Position);
|
||||
* if (position) {
|
||||
* position.x += 10;
|
||||
* position.y += 20;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
public getComponent<T extends Component>(type: ComponentType<T>): T | null {
|
||||
// 快速检查:位掩码
|
||||
@@ -438,26 +455,31 @@ export class Entity {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 优先从 Scene 存储获取
|
||||
if (this.scene?.componentStorageManager) {
|
||||
const component = this.scene.componentStorageManager.getComponent(this.id, type);
|
||||
if (component) {
|
||||
return component;
|
||||
}
|
||||
// 从Scene存储获取
|
||||
if (!this.scene?.componentStorageManager) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Fallback 到本地存储
|
||||
return (this._localComponents.get(type) as T) || null;
|
||||
const component = this.scene.componentStorageManager.getComponent(this.id, type);
|
||||
return component as T | null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 检查实体是否有指定类型的组件
|
||||
*
|
||||
* @param type - 组件类型
|
||||
* @returns 如果有该组件则返回true
|
||||
* 检查实体是否拥有指定类型的组件
|
||||
*
|
||||
* @param type - 组件类型构造函数
|
||||
* @returns 如果实体拥有该组件返回true,否则返回false
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* if (entity.hasComponent(Position)) {
|
||||
* const position = entity.getComponent(Position)!;
|
||||
* position.x += 10;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
public hasComponent<T extends Component>(type: ComponentType<T>): boolean {
|
||||
if (!ComponentRegistry.isRegistered(type)) {
|
||||
@@ -470,13 +492,22 @@ export class Entity {
|
||||
|
||||
/**
|
||||
* 获取或创建指定类型的组件
|
||||
*
|
||||
* @param type - 组件类型
|
||||
* @param args - 组件构造函数参数(仅在创建时使用)
|
||||
*
|
||||
* 如果组件已存在则返回现有组件,否则创建新组件并添加到实体
|
||||
*
|
||||
* @param type - 组件类型构造函数
|
||||
* @param args - 组件构造函数参数(仅在创建新组件时使用)
|
||||
* @returns 组件实例
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 确保实体拥有Position组件
|
||||
* const position = entity.getOrCreateComponent(Position, 0, 0);
|
||||
* position.x = 100;
|
||||
* ```
|
||||
*/
|
||||
public getOrCreateComponent<T extends Component>(
|
||||
type: ComponentType<T>,
|
||||
type: ComponentType<T>,
|
||||
...args: any[]
|
||||
): T {
|
||||
let component = this.getComponent(type);
|
||||
@@ -500,24 +531,27 @@ export class Entity {
|
||||
|
||||
const bitIndex = ComponentRegistry.getBitIndex(componentType);
|
||||
|
||||
// 从本地存储移除
|
||||
this._localComponents.delete(componentType);
|
||||
|
||||
// 更新位掩码
|
||||
BitMask64Utils.clearBitExtended(this._componentMask, bitIndex);
|
||||
BitMask64Utils.clearBit(this._componentMask, bitIndex);
|
||||
|
||||
// 使缓存失效
|
||||
this._componentCache = null;
|
||||
|
||||
// 从 Scene 存储移除
|
||||
if (this.scene && this.scene.componentStorageManager) {
|
||||
// 从Scene存储移除
|
||||
if (this.scene?.componentStorageManager) {
|
||||
this.scene.componentStorageManager.removeComponent(this.id, componentType);
|
||||
}
|
||||
|
||||
if (this.scene?.referenceTracker) {
|
||||
this.scene.referenceTracker.clearComponentReferences(component);
|
||||
}
|
||||
|
||||
if (component.onRemovedFromEntity) {
|
||||
component.onRemovedFromEntity();
|
||||
}
|
||||
|
||||
component.entityId = null;
|
||||
|
||||
if (Entity.eventBus) {
|
||||
Entity.eventBus.emitComponentRemoved({
|
||||
timestamp: Date.now(),
|
||||
@@ -555,9 +589,6 @@ export class Entity {
|
||||
public removeAllComponents(): void {
|
||||
const componentsToRemove = [...this.components];
|
||||
|
||||
// 清除本地存储
|
||||
this._localComponents.clear();
|
||||
|
||||
// 清除位掩码
|
||||
BitMask64Utils.clear(this._componentMask);
|
||||
|
||||
@@ -567,7 +598,7 @@ export class Entity {
|
||||
for (const component of componentsToRemove) {
|
||||
const componentType = component.constructor as ComponentType;
|
||||
|
||||
if (this.scene && this.scene.componentStorageManager) {
|
||||
if (this.scene?.componentStorageManager) {
|
||||
this.scene.componentStorageManager.removeComponent(this.id, componentType);
|
||||
}
|
||||
|
||||
@@ -836,8 +867,8 @@ export class Entity {
|
||||
|
||||
/**
|
||||
* 销毁实体
|
||||
*
|
||||
* 移除所有组件、子实体并标记为已销毁。
|
||||
*
|
||||
* 移除所有组件、子实体并标记为已销毁
|
||||
*/
|
||||
public destroy(): void {
|
||||
if (this._isDestroyed) {
|
||||
@@ -845,29 +876,71 @@ export class Entity {
|
||||
}
|
||||
|
||||
this._isDestroyed = true;
|
||||
|
||||
|
||||
if (this.scene && this.scene.referenceTracker) {
|
||||
this.scene.referenceTracker.clearReferencesTo(this.id);
|
||||
this.scene.referenceTracker.unregisterEntityScene(this.id);
|
||||
}
|
||||
|
||||
const childrenToDestroy = [...this._children];
|
||||
for (const child of childrenToDestroy) {
|
||||
child.destroy();
|
||||
}
|
||||
|
||||
|
||||
if (this._parent) {
|
||||
this._parent.removeChild(this);
|
||||
}
|
||||
|
||||
|
||||
this.removeAllComponents();
|
||||
|
||||
|
||||
if (this.scene) {
|
||||
if (this.scene.querySystem) {
|
||||
this.scene.querySystem.removeEntity(this);
|
||||
}
|
||||
|
||||
|
||||
if (this.scene.entities) {
|
||||
this.scene.entities.remove(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量销毁所有子实体
|
||||
*/
|
||||
public destroyAllChildren(): void {
|
||||
if (this._children.length === 0) return;
|
||||
|
||||
const scene = this.scene;
|
||||
const toDestroy: Entity[] = [];
|
||||
|
||||
const collectChildren = (entity: Entity) => {
|
||||
for (const child of entity._children) {
|
||||
toDestroy.push(child);
|
||||
collectChildren(child);
|
||||
}
|
||||
};
|
||||
collectChildren(this);
|
||||
|
||||
for (const entity of toDestroy) {
|
||||
entity._isDestroyed = true;
|
||||
}
|
||||
|
||||
for (const entity of toDestroy) {
|
||||
entity.removeAllComponents();
|
||||
}
|
||||
|
||||
if (scene) {
|
||||
for (const entity of toDestroy) {
|
||||
scene.entities.remove(entity);
|
||||
scene.querySystem.removeEntity(entity);
|
||||
}
|
||||
|
||||
scene.clearSystemEntityCaches();
|
||||
}
|
||||
|
||||
this._children.length = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 比较实体
|
||||
*
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { Entity } from './Entity';
|
||||
import { EntityList } from './Utils/EntityList';
|
||||
import { EntityProcessorList } from './Utils/EntityProcessorList';
|
||||
import { IdentifierPool } from './Utils/IdentifierPool';
|
||||
import { EntitySystem } from './Systems/EntitySystem';
|
||||
import { ComponentStorageManager } from './Core/ComponentStorage';
|
||||
import { QuerySystem } from './Core/QuerySystem';
|
||||
import { TypeSafeEventSystem } from './Core/EventSystem';
|
||||
import type { ReferenceTracker } from './Core/ReferenceTracker';
|
||||
import type { ServiceContainer } from '../Core/ServiceContainer';
|
||||
|
||||
/**
|
||||
* 场景接口定义
|
||||
*
|
||||
*
|
||||
* 定义场景应该实现的核心功能和属性,使用接口而非继承提供更灵活的实现方式。
|
||||
*/
|
||||
export interface IScene {
|
||||
@@ -18,16 +19,30 @@ export interface IScene {
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* 场景自定义数据
|
||||
*
|
||||
* 用于存储场景级别的配置和状态数据,例如:
|
||||
* - 天气状态
|
||||
* - 时间设置
|
||||
* - 游戏难度
|
||||
* - 音频配置
|
||||
* - 关卡检查点
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* scene.sceneData.set('weather', 'rainy');
|
||||
* scene.sceneData.set('timeOfDay', 14.5);
|
||||
* scene.sceneData.set('checkpoint', { x: 100, y: 200 });
|
||||
* ```
|
||||
*/
|
||||
readonly sceneData: Map<string, any>;
|
||||
|
||||
/**
|
||||
* 场景中的实体集合
|
||||
*/
|
||||
readonly entities: EntityList;
|
||||
|
||||
/**
|
||||
* 实体系统处理器集合
|
||||
*/
|
||||
readonly entityProcessors: EntityProcessorList;
|
||||
|
||||
|
||||
/**
|
||||
* 标识符池
|
||||
*/
|
||||
@@ -48,6 +63,18 @@ export interface IScene {
|
||||
*/
|
||||
readonly eventSystem: TypeSafeEventSystem;
|
||||
|
||||
/**
|
||||
* 引用追踪器
|
||||
*/
|
||||
readonly referenceTracker: ReferenceTracker;
|
||||
|
||||
/**
|
||||
* 服务容器
|
||||
*
|
||||
* 场景级别的依赖注入容器,用于管理服务的生命周期。
|
||||
*/
|
||||
readonly services: ServiceContainer;
|
||||
|
||||
/**
|
||||
* 获取系统列表
|
||||
*/
|
||||
|
||||
@@ -1,42 +1,52 @@
|
||||
import { Entity } from './Entity';
|
||||
import { EntityList } from './Utils/EntityList';
|
||||
import { EntityProcessorList } from './Utils/EntityProcessorList';
|
||||
import { IdentifierPool } from './Utils/IdentifierPool';
|
||||
import { EntitySystem } from './Systems/EntitySystem';
|
||||
import { ComponentStorageManager } from './Core/ComponentStorage';
|
||||
import { ComponentStorageManager, ComponentRegistry } from './Core/ComponentStorage';
|
||||
import { QuerySystem } from './Core/QuerySystem';
|
||||
import { TypeSafeEventSystem } from './Core/EventSystem';
|
||||
import { EventBus } from './Core/EventBus';
|
||||
import { ReferenceTracker } from './Core/ReferenceTracker';
|
||||
import { IScene, ISceneConfig } from './IScene';
|
||||
import { getComponentInstanceTypeName, getSystemInstanceTypeName } from './Decorators';
|
||||
import { getComponentInstanceTypeName, getSystemInstanceTypeName, getSystemMetadata } from "./Decorators";
|
||||
import { TypedQueryBuilder } from './Core/Query/TypedQuery';
|
||||
import { SceneSerializer, SceneSerializationOptions, SceneDeserializationOptions } from './Serialization/SceneSerializer';
|
||||
import { IncrementalSerializer, IncrementalSnapshot, IncrementalSerializationOptions } from './Serialization/IncrementalSerializer';
|
||||
import { ComponentPoolManager } from './Core/ComponentPool';
|
||||
import { PerformanceMonitor } from '../Utils/PerformanceMonitor';
|
||||
import { ServiceContainer, type ServiceType } from '../Core/ServiceContainer';
|
||||
import { createInstance, isInjectable, injectProperties } from '../Core/DI';
|
||||
import { isUpdatable, getUpdatableMetadata } from '../Core/DI/Decorators';
|
||||
import { createLogger } from '../Utils/Logger';
|
||||
|
||||
/**
|
||||
* 游戏场景默认实现类
|
||||
*
|
||||
*
|
||||
* 实现IScene接口,提供场景的基础功能。
|
||||
* 推荐使用组合而非继承的方式来构建自定义场景。
|
||||
*/
|
||||
export class Scene implements IScene {
|
||||
/**
|
||||
* 场景名称
|
||||
*
|
||||
*
|
||||
* 用于标识和调试的友好名称。
|
||||
*/
|
||||
public name: string = "";
|
||||
|
||||
/**
|
||||
* 场景自定义数据
|
||||
*
|
||||
* 用于存储场景级别的配置和状态数据。
|
||||
*/
|
||||
public readonly sceneData: Map<string, any> = new Map();
|
||||
|
||||
/**
|
||||
* 场景中的实体集合
|
||||
*
|
||||
*
|
||||
* 管理场景内所有实体的生命周期。
|
||||
*/
|
||||
public readonly entities: EntityList;
|
||||
|
||||
/**
|
||||
* 实体系统处理器集合
|
||||
*
|
||||
* 管理场景内所有实体系统的执行。
|
||||
*/
|
||||
public readonly entityProcessors: EntityProcessorList;
|
||||
|
||||
/**
|
||||
* 实体ID池
|
||||
@@ -61,36 +71,117 @@ export class Scene implements IScene {
|
||||
|
||||
/**
|
||||
* 事件系统
|
||||
*
|
||||
*
|
||||
* 类型安全的事件系统。
|
||||
*/
|
||||
public readonly eventSystem: TypeSafeEventSystem;
|
||||
|
||||
|
||||
/**
|
||||
* 引用追踪器
|
||||
*
|
||||
* 追踪Component中对Entity的引用,当Entity销毁时自动清理引用。
|
||||
*/
|
||||
public readonly referenceTracker: ReferenceTracker;
|
||||
|
||||
/**
|
||||
* 服务容器
|
||||
*
|
||||
* 场景级别的依赖注入容器,用于管理EntitySystem和其他服务的生命周期。
|
||||
* 每个Scene拥有独立的服务容器,实现场景间的隔离。
|
||||
*/
|
||||
private readonly _services: ServiceContainer;
|
||||
|
||||
/**
|
||||
* 日志记录器
|
||||
*/
|
||||
private readonly logger: ReturnType<typeof createLogger>;
|
||||
|
||||
/**
|
||||
* 性能监控器缓存
|
||||
*
|
||||
* 用于监控场景和系统的性能。从 ServiceContainer 获取。
|
||||
*/
|
||||
private _performanceMonitor: PerformanceMonitor | null = null;
|
||||
|
||||
/**
|
||||
* 场景是否已开始运行
|
||||
*/
|
||||
private _didSceneBegin: boolean = false;
|
||||
|
||||
/**
|
||||
* 获取系统列表(兼容性属性)
|
||||
* 获取场景中所有已注册的EntitySystem
|
||||
*
|
||||
* 按updateOrder排序。
|
||||
*
|
||||
* @returns 系统列表
|
||||
*/
|
||||
public get systems(): EntitySystem[] {
|
||||
return this.entityProcessors.processors;
|
||||
// 从ServiceContainer获取所有EntitySystem实例
|
||||
const services = this._services.getAll();
|
||||
const systems: EntitySystem[] = [];
|
||||
|
||||
for (const service of services) {
|
||||
if (service instanceof EntitySystem) {
|
||||
systems.push(service);
|
||||
}
|
||||
}
|
||||
|
||||
// 按updateOrder排序
|
||||
systems.sort((a, b) => a.updateOrder - b.updateOrder);
|
||||
|
||||
return systems;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过类型获取System实例
|
||||
*
|
||||
* @param systemType System类型
|
||||
* @returns System实例,如果未找到则返回null
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const physics = scene.getSystem(PhysicsSystem);
|
||||
* if (physics) {
|
||||
* physics.doSomething();
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
public getSystem<T extends EntitySystem>(systemType: ServiceType<T>): T | null {
|
||||
return this._services.tryResolve(systemType) as T | null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取场景的服务容器
|
||||
*
|
||||
* 用于注册和解析场景级别的服务(如EntitySystem)。
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 注册服务
|
||||
* scene.services.registerSingleton(PhysicsSystem);
|
||||
*
|
||||
* // 解析服务
|
||||
* const physics = scene.services.resolve(PhysicsSystem);
|
||||
* ```
|
||||
*/
|
||||
public get services(): ServiceContainer {
|
||||
return this._services;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建场景实例
|
||||
*/
|
||||
constructor(config?: ISceneConfig) {
|
||||
this.entities = new EntityList(this);
|
||||
this.entityProcessors = new EntityProcessorList();
|
||||
this.identifierPool = new IdentifierPool();
|
||||
this.componentStorageManager = new ComponentStorageManager();
|
||||
this.querySystem = new QuerySystem();
|
||||
this.eventSystem = new TypeSafeEventSystem();
|
||||
this.referenceTracker = new ReferenceTracker();
|
||||
this._services = new ServiceContainer();
|
||||
this.logger = createLogger('Scene');
|
||||
|
||||
// 应用配置
|
||||
if (config?.name) {
|
||||
this.name = config.name;
|
||||
}
|
||||
@@ -98,7 +189,7 @@ export class Scene implements IScene {
|
||||
if (!Entity.eventBus) {
|
||||
Entity.eventBus = new EventBus(false);
|
||||
}
|
||||
|
||||
|
||||
if (Entity.eventBus) {
|
||||
Entity.eventBus.onComponentAdded((data: unknown) => {
|
||||
this.eventSystem.emitSync('component:added', data);
|
||||
@@ -106,6 +197,19 @@ export class Scene implements IScene {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取性能监控器
|
||||
*
|
||||
* 从 ServiceContainer 获取,如果未注册则创建默认实例(向后兼容)
|
||||
*/
|
||||
private get performanceMonitor(): PerformanceMonitor {
|
||||
if (!this._performanceMonitor) {
|
||||
this._performanceMonitor = this._services.tryResolve(PerformanceMonitor)
|
||||
?? new PerformanceMonitor();
|
||||
}
|
||||
return this._performanceMonitor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化场景
|
||||
*
|
||||
@@ -136,10 +240,6 @@ export class Scene implements IScene {
|
||||
* 这个方法会启动场景。它将启动实体处理器等,并调用onStart方法。
|
||||
*/
|
||||
public begin() {
|
||||
// 启动实体处理器
|
||||
if (this.entityProcessors != null)
|
||||
this.entityProcessors.begin();
|
||||
|
||||
// 标记场景已开始运行并调用onStart方法
|
||||
this._didSceneBegin = true;
|
||||
this.onStart();
|
||||
@@ -163,9 +263,8 @@ export class Scene implements IScene {
|
||||
// 清空组件存储
|
||||
this.componentStorageManager.clear();
|
||||
|
||||
// 结束实体处理器
|
||||
if (this.entityProcessors)
|
||||
this.entityProcessors.end();
|
||||
// 清空服务容器(会调用所有服务的dispose方法,包括所有EntitySystem)
|
||||
this._services.clear();
|
||||
|
||||
// 调用卸载方法
|
||||
this.unload();
|
||||
@@ -175,16 +274,32 @@ export class Scene implements IScene {
|
||||
* 更新场景
|
||||
*/
|
||||
public update() {
|
||||
// 更新实体列表(处理延迟操作)
|
||||
ComponentPoolManager.getInstance().update();
|
||||
|
||||
this.entities.updateLists();
|
||||
|
||||
// 更新实体处理器
|
||||
if (this.entityProcessors != null)
|
||||
this.entityProcessors.update();
|
||||
// 更新所有EntitySystem
|
||||
const systems = this.systems;
|
||||
for (const system of systems) {
|
||||
if (system.enabled) {
|
||||
try {
|
||||
system.update();
|
||||
} catch (error) {
|
||||
this.logger.error(`Error in system ${system.constructor.name}.update():`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新实体处理器后处理
|
||||
if (this.entityProcessors != null)
|
||||
this.entityProcessors.lateUpdate();
|
||||
// LateUpdate
|
||||
for (const system of systems) {
|
||||
if (system.enabled) {
|
||||
try {
|
||||
system.lateUpdate();
|
||||
} catch (error) {
|
||||
this.logger.error(`Error in system ${system.constructor.name}.lateUpdate():`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -204,7 +319,7 @@ export class Scene implements IScene {
|
||||
* 当实体或组件发生变化时调用
|
||||
*/
|
||||
public clearSystemEntityCaches(): void {
|
||||
for (const system of this.entityProcessors.processors) {
|
||||
for (const system of this.systems) {
|
||||
system.clearEntityCache();
|
||||
}
|
||||
}
|
||||
@@ -263,13 +378,35 @@ export class Scene implements IScene {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 批量销毁实体
|
||||
*/
|
||||
public destroyEntities(entities: Entity[]): void {
|
||||
if (entities.length === 0) return;
|
||||
|
||||
for (const entity of entities) {
|
||||
entity._isDestroyed = true;
|
||||
}
|
||||
|
||||
for (const entity of entities) {
|
||||
entity.removeAllComponents();
|
||||
}
|
||||
|
||||
for (const entity of entities) {
|
||||
this.entities.remove(entity);
|
||||
this.querySystem.removeEntity(entity);
|
||||
}
|
||||
|
||||
this.querySystem.clearCache();
|
||||
this.clearSystemEntityCaches();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从场景中删除所有实体
|
||||
*/
|
||||
public destroyAllEntities() {
|
||||
this.entities.removeAllEntities();
|
||||
|
||||
// 清理查询系统中的实体引用和缓存
|
||||
|
||||
this.querySystem.setEntities([]);
|
||||
}
|
||||
|
||||
@@ -320,19 +457,201 @@ export class Scene implements IScene {
|
||||
}
|
||||
|
||||
/**
|
||||
* 在场景中添加一个EntitySystem处理器
|
||||
* @param processor 处理器
|
||||
* 查询拥有所有指定组件的实体
|
||||
*
|
||||
* @param componentTypes - 组件类型数组
|
||||
* @returns 查询结果
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const result = scene.queryAll(Position, Velocity);
|
||||
* for (const entity of result.entities) {
|
||||
* const pos = entity.getComponent(Position);
|
||||
* const vel = entity.getComponent(Velocity);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
public addEntityProcessor(processor: EntitySystem) {
|
||||
if (this.entityProcessors.processors.includes(processor)) {
|
||||
return processor;
|
||||
public queryAll(...componentTypes: any[]): { entities: readonly Entity[] } {
|
||||
return this.querySystem.queryAll(...componentTypes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询拥有任意一个指定组件的实体
|
||||
*
|
||||
* @param componentTypes - 组件类型数组
|
||||
* @returns 查询结果
|
||||
*/
|
||||
public queryAny(...componentTypes: any[]): { entities: readonly Entity[] } {
|
||||
return this.querySystem.queryAny(...componentTypes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询不包含指定组件的实体
|
||||
*
|
||||
* @param componentTypes - 组件类型数组
|
||||
* @returns 查询结果
|
||||
*/
|
||||
public queryNone(...componentTypes: any[]): { entities: readonly Entity[] } {
|
||||
return this.querySystem.queryNone(...componentTypes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建类型安全的查询构建器
|
||||
*
|
||||
* @returns 查询构建器,支持链式调用
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 使用查询构建器
|
||||
* const matcher = scene.query()
|
||||
* .withAll(Position, Velocity)
|
||||
* .withNone(Disabled)
|
||||
* .buildMatcher();
|
||||
*
|
||||
* // 在System中使用
|
||||
* class MovementSystem extends EntitySystem {
|
||||
* constructor() {
|
||||
* super(matcher);
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
public query(): TypedQueryBuilder {
|
||||
return new TypedQueryBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* 在场景中添加一个EntitySystem处理器
|
||||
*
|
||||
* 支持两种使用方式:
|
||||
* 1. 传入类型(推荐):自动使用DI创建实例,支持@Injectable和@Inject装饰器
|
||||
* 2. 传入实例:直接使用提供的实例
|
||||
*
|
||||
* @param systemTypeOrInstance 系统类型或系统实例
|
||||
* @returns 添加的处理器实例
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 方式1:传入类型,自动DI(推荐)
|
||||
* @Injectable()
|
||||
* class PhysicsSystem extends EntitySystem {
|
||||
* constructor(@Inject(CollisionSystem) private collision: CollisionSystem) {
|
||||
* super(Matcher.empty().all(Transform));
|
||||
* }
|
||||
* }
|
||||
* scene.addEntityProcessor(PhysicsSystem);
|
||||
*
|
||||
* // 方式2:传入实例
|
||||
* const system = new MySystem();
|
||||
* scene.addEntityProcessor(system);
|
||||
* ```
|
||||
*/
|
||||
public addEntityProcessor<T extends EntitySystem>(
|
||||
systemTypeOrInstance: ServiceType<T> | T
|
||||
): T {
|
||||
let system: T;
|
||||
let constructor: any;
|
||||
|
||||
if (typeof systemTypeOrInstance === 'function') {
|
||||
constructor = systemTypeOrInstance;
|
||||
|
||||
if (this._services.isRegistered(constructor)) {
|
||||
const existingSystem = this._services.resolve(constructor) as T;
|
||||
this.logger.debug(`System ${constructor.name} already registered, returning existing instance`);
|
||||
return existingSystem;
|
||||
}
|
||||
|
||||
if (isInjectable(constructor)) {
|
||||
system = createInstance(constructor, this._services) as T;
|
||||
} else {
|
||||
system = new (constructor as any)() as T;
|
||||
}
|
||||
} else {
|
||||
system = systemTypeOrInstance;
|
||||
constructor = system.constructor;
|
||||
|
||||
if (this._services.isRegistered(constructor)) {
|
||||
const existingSystem = this._services.resolve(constructor);
|
||||
if (existingSystem === system) {
|
||||
this.logger.debug(`System ${constructor.name} instance already registered, returning it`);
|
||||
return system;
|
||||
} else {
|
||||
this.logger.warn(
|
||||
`Attempting to register a different instance of ${constructor.name}, ` +
|
||||
`but type is already registered. Returning existing instance.`
|
||||
);
|
||||
return existingSystem as T;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
processor.scene = this;
|
||||
this.entityProcessors.add(processor);
|
||||
processor.initialize();
|
||||
processor.setUpdateOrder(this.entityProcessors.count - 1);
|
||||
return processor;
|
||||
|
||||
system.scene = this;
|
||||
|
||||
system.setPerformanceMonitor(this.performanceMonitor);
|
||||
|
||||
const metadata = getSystemMetadata(constructor);
|
||||
if (metadata?.updateOrder !== undefined) {
|
||||
system.setUpdateOrder(metadata.updateOrder);
|
||||
}
|
||||
if (metadata?.enabled !== undefined) {
|
||||
system.enabled = metadata.enabled;
|
||||
}
|
||||
|
||||
this._services.registerInstance(constructor, system);
|
||||
|
||||
injectProperties(system, this._services);
|
||||
|
||||
system.initialize();
|
||||
|
||||
this.logger.debug(`System ${constructor.name} registered and initialized`);
|
||||
|
||||
return system;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量注册EntitySystem到场景(使用DI)
|
||||
*
|
||||
* 自动按照依赖顺序注册多个System。
|
||||
* 所有System必须使用@Injectable装饰器标记。
|
||||
*
|
||||
* @param systemTypes System类型数组
|
||||
* @returns 注册的System实例数组
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @Injectable()
|
||||
* @ECSSystem('Collision', { updateOrder: 5 })
|
||||
* class CollisionSystem extends EntitySystem implements IService {
|
||||
* constructor() { super(Matcher.empty().all(Collider)); }
|
||||
* dispose() {}
|
||||
* }
|
||||
*
|
||||
* @Injectable()
|
||||
* @ECSSystem('Physics', { updateOrder: 10 })
|
||||
* class PhysicsSystem extends EntitySystem implements IService {
|
||||
* constructor(@Inject(CollisionSystem) private collision: CollisionSystem) {
|
||||
* super(Matcher.empty().all(Transform, RigidBody));
|
||||
* }
|
||||
* dispose() {}
|
||||
* }
|
||||
*
|
||||
* // 批量注册(自动解析依赖顺序)
|
||||
* scene.registerSystems([
|
||||
* CollisionSystem,
|
||||
* PhysicsSystem, // 自动注入CollisionSystem
|
||||
* RenderSystem
|
||||
* ]);
|
||||
* ```
|
||||
*/
|
||||
public registerSystems(systemTypes: Array<ServiceType<EntitySystem>>): EntitySystem[] {
|
||||
const registeredSystems: EntitySystem[] = [];
|
||||
|
||||
for (const systemType of systemTypes) {
|
||||
const system = this.addEntityProcessor(systemType);
|
||||
registeredSystems.push(system);
|
||||
}
|
||||
|
||||
return registeredSystems;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -347,8 +666,13 @@ export class Scene implements IScene {
|
||||
* 从场景中删除EntitySystem处理器
|
||||
* @param processor 要删除的处理器
|
||||
*/
|
||||
public removeEntityProcessor(processor: EntitySystem) {
|
||||
this.entityProcessors.remove(processor);
|
||||
public removeEntityProcessor(processor: EntitySystem): void {
|
||||
const constructor = processor.constructor as any;
|
||||
|
||||
// 从ServiceContainer移除
|
||||
this._services.unregister(constructor);
|
||||
|
||||
// 重置System状态
|
||||
processor.reset();
|
||||
}
|
||||
|
||||
@@ -362,10 +686,24 @@ export class Scene implements IScene {
|
||||
|
||||
/**
|
||||
* 获取指定类型的EntitySystem处理器
|
||||
*
|
||||
* @deprecated 推荐使用依赖注入代替此方法。使用 `scene.services.resolve(SystemType)` 或在System构造函数中使用 `@Inject(SystemType)` 装饰器。
|
||||
*
|
||||
* @param type 处理器类型
|
||||
* @returns 处理器实例,如果未找到则返回null
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @Injectable()
|
||||
* class MySystem extends EntitySystem {
|
||||
* constructor(@Inject(PhysicsSystem) private physics: PhysicsSystem) {
|
||||
* super();
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
public getEntityProcessor<T extends EntitySystem>(type: new (...args: unknown[]) => T): T | null {
|
||||
return this.entityProcessors.getProcessor(type);
|
||||
return this._services.tryResolve(type as any) as T | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -378,7 +716,7 @@ export class Scene implements IScene {
|
||||
} {
|
||||
return {
|
||||
entityCount: this.entities.count,
|
||||
processorCount: this.entityProcessors.count,
|
||||
processorCount: this.systems.length,
|
||||
componentStorageStats: this.componentStorageManager.getAllStats()
|
||||
};
|
||||
}
|
||||
@@ -405,10 +743,11 @@ export class Scene implements IScene {
|
||||
}>;
|
||||
componentStats: Map<string, any>;
|
||||
} {
|
||||
const systems = this.systems;
|
||||
return {
|
||||
name: this.name || this.constructor.name,
|
||||
entityCount: this.entities.count,
|
||||
processorCount: this.entityProcessors.count,
|
||||
processorCount: systems.length,
|
||||
isRunning: this._didSceneBegin,
|
||||
entities: this.entities.buffer.map(entity => ({
|
||||
name: entity.name,
|
||||
@@ -416,7 +755,7 @@ export class Scene implements IScene {
|
||||
componentCount: entity.components.length,
|
||||
componentTypes: entity.components.map(c => getComponentInstanceTypeName(c))
|
||||
})),
|
||||
processors: this.entityProcessors.processors.map(processor => ({
|
||||
processors: systems.map(processor => ({
|
||||
name: getSystemInstanceTypeName(processor),
|
||||
updateOrder: processor.updateOrder,
|
||||
entityCount: (processor as any)._entities?.length || 0
|
||||
@@ -424,4 +763,200 @@ export class Scene implements IScene {
|
||||
componentStats: this.componentStorageManager.getAllStats()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化场景
|
||||
*
|
||||
* 将场景及其所有实体、组件序列化为JSON字符串或二进制Uint8Array
|
||||
*
|
||||
* @param options 序列化选项
|
||||
* @returns 序列化后的数据(JSON字符串或二进制Uint8Array)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // JSON格式
|
||||
* const jsonData = scene.serialize({
|
||||
* format: 'json',
|
||||
* pretty: true
|
||||
* });
|
||||
*
|
||||
* // 二进制格式(更小、更快)
|
||||
* const binaryData = scene.serialize({
|
||||
* format: 'binary'
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
public serialize(options?: SceneSerializationOptions): string | Uint8Array {
|
||||
return SceneSerializer.serialize(this, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 反序列化场景
|
||||
*
|
||||
* 从序列化数据恢复场景状态
|
||||
*
|
||||
* @param saveData 序列化的数据(JSON字符串或二进制Uint8Array)
|
||||
* @param options 反序列化选项
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 从JSON恢复(自动从ComponentRegistry获取组件类型)
|
||||
* scene.deserialize(jsonData, {
|
||||
* strategy: 'replace'
|
||||
* });
|
||||
*
|
||||
* // 从二进制恢复
|
||||
* scene.deserialize(binaryData, {
|
||||
* strategy: 'replace'
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
public deserialize(saveData: string | Uint8Array, options?: SceneDeserializationOptions): void {
|
||||
SceneSerializer.deserialize(this, saveData, options);
|
||||
}
|
||||
|
||||
// ==================== 增量序列化 API ====================
|
||||
|
||||
/** 增量序列化的基础快照 */
|
||||
private _incrementalBaseSnapshot?: any;
|
||||
|
||||
/**
|
||||
* 创建增量序列化的基础快照
|
||||
*
|
||||
* 在需要进行增量序列化前,先调用此方法创建基础快照
|
||||
*
|
||||
* @param options 序列化选项
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 创建基础快照
|
||||
* scene.createIncrementalSnapshot();
|
||||
*
|
||||
* // 进行一些修改...
|
||||
* entity.addComponent(new PositionComponent(100, 200));
|
||||
*
|
||||
* // 计算增量变更
|
||||
* const incremental = scene.serializeIncremental();
|
||||
* ```
|
||||
*/
|
||||
public createIncrementalSnapshot(options?: IncrementalSerializationOptions): void {
|
||||
this._incrementalBaseSnapshot = IncrementalSerializer.createSnapshot(this, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 增量序列化场景
|
||||
*
|
||||
* 只序列化相对于基础快照的变更部分
|
||||
*
|
||||
* @param options 序列化选项
|
||||
* @returns 增量快照对象
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 创建基础快照
|
||||
* scene.createIncrementalSnapshot();
|
||||
*
|
||||
* // 修改场景
|
||||
* const entity = scene.createEntity('NewEntity');
|
||||
* entity.addComponent(new PositionComponent(50, 100));
|
||||
*
|
||||
* // 获取增量变更
|
||||
* const incremental = scene.serializeIncremental();
|
||||
* console.log(`变更数量: ${incremental.entityChanges.length}`);
|
||||
*
|
||||
* // 序列化为JSON
|
||||
* const json = IncrementalSerializer.serializeIncremental(incremental);
|
||||
* ```
|
||||
*/
|
||||
public serializeIncremental(options?: IncrementalSerializationOptions): IncrementalSnapshot {
|
||||
if (!this._incrementalBaseSnapshot) {
|
||||
throw new Error('必须先调用 createIncrementalSnapshot() 创建基础快照');
|
||||
}
|
||||
|
||||
return IncrementalSerializer.computeIncremental(
|
||||
this,
|
||||
this._incrementalBaseSnapshot,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用增量变更到场景
|
||||
*
|
||||
* @param incremental 增量快照数据(IncrementalSnapshot对象、JSON字符串或二进制Uint8Array)
|
||||
* @param componentRegistry 组件类型注册表(可选,默认使用全局注册表)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 应用增量变更对象
|
||||
* scene.applyIncremental(incrementalSnapshot);
|
||||
*
|
||||
* // 从JSON字符串应用
|
||||
* const jsonData = IncrementalSerializer.serializeIncremental(snapshot, { format: 'json' });
|
||||
* scene.applyIncremental(jsonData);
|
||||
*
|
||||
* // 从二进制Uint8Array应用
|
||||
* const binaryData = IncrementalSerializer.serializeIncremental(snapshot, { format: 'binary' });
|
||||
* scene.applyIncremental(binaryData);
|
||||
* ```
|
||||
*/
|
||||
public applyIncremental(
|
||||
incremental: IncrementalSnapshot | string | Uint8Array,
|
||||
componentRegistry?: Map<string, any>
|
||||
): void {
|
||||
const isSerializedData = typeof incremental === 'string' ||
|
||||
incremental instanceof Uint8Array;
|
||||
|
||||
const snapshot = isSerializedData
|
||||
? IncrementalSerializer.deserializeIncremental(incremental as string | Uint8Array)
|
||||
: incremental as IncrementalSnapshot;
|
||||
|
||||
const registry = componentRegistry || ComponentRegistry.getAllComponentNames() as Map<string, any>;
|
||||
|
||||
IncrementalSerializer.applyIncremental(this, snapshot, registry);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新增量快照基准
|
||||
*
|
||||
* 将当前场景状态设为新的增量序列化基准
|
||||
*
|
||||
* @param options 序列化选项
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 创建初始快照
|
||||
* scene.createIncrementalSnapshot();
|
||||
*
|
||||
* // 进行一些修改并序列化
|
||||
* const incremental1 = scene.serializeIncremental();
|
||||
*
|
||||
* // 更新基准,之后的增量将基于当前状态
|
||||
* scene.updateIncrementalSnapshot();
|
||||
*
|
||||
* // 继续修改
|
||||
* const incremental2 = scene.serializeIncremental();
|
||||
* ```
|
||||
*/
|
||||
public updateIncrementalSnapshot(options?: IncrementalSerializationOptions): void {
|
||||
this.createIncrementalSnapshot(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除增量快照
|
||||
*
|
||||
* 释放快照占用的内存
|
||||
*/
|
||||
public clearIncrementalSnapshot(): void {
|
||||
this._incrementalBaseSnapshot = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有增量快照
|
||||
*
|
||||
* @returns 如果已创建增量快照返回true
|
||||
*/
|
||||
public hasIncrementalSnapshot(): boolean {
|
||||
return this._incrementalBaseSnapshot !== undefined;
|
||||
}
|
||||
}
|
||||
274
packages/core/src/ECS/SceneManager.ts
Normal file
274
packages/core/src/ECS/SceneManager.ts
Normal file
@@ -0,0 +1,274 @@
|
||||
import { IScene } from './IScene';
|
||||
import { ECSFluentAPI, createECSAPI } from './Core/FluentAPI';
|
||||
import { Time } from '../Utils/Time';
|
||||
import { createLogger } from '../Utils/Logger';
|
||||
import type { IService } from '../Core/ServiceContainer';
|
||||
import { World } from './World';
|
||||
import { PerformanceMonitor } from '../Utils/PerformanceMonitor';
|
||||
|
||||
/**
|
||||
* 单场景管理器
|
||||
*
|
||||
* 适用场景:
|
||||
* - 单人游戏
|
||||
* - 简单场景切换
|
||||
* - 不需要多World隔离的项目
|
||||
*
|
||||
* 特点:
|
||||
* - 轻量级,零额外开销
|
||||
* - 简单直观的API
|
||||
* - 支持延迟场景切换
|
||||
* - 自动管理ECS API
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 初始化Core
|
||||
* Core.create({ debug: true });
|
||||
*
|
||||
* // 创建场景管理器
|
||||
* const sceneManager = new SceneManager();
|
||||
*
|
||||
* // 设置场景
|
||||
* class GameScene extends Scene {
|
||||
* initialize() {
|
||||
* const player = this.createEntity('Player');
|
||||
* player.addComponent(new Transform(100, 100));
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* sceneManager.setScene(new GameScene());
|
||||
*
|
||||
* // 游戏循环
|
||||
* function gameLoop(deltaTime: number) {
|
||||
* Core.update(deltaTime); // 更新全局服务
|
||||
* sceneManager.update(); // 更新场景
|
||||
* }
|
||||
*
|
||||
* // 延迟切换场景(下一帧生效)
|
||||
* sceneManager.loadScene(new MenuScene());
|
||||
* ```
|
||||
*/
|
||||
export class SceneManager implements IService {
|
||||
/**
|
||||
* 内部默认World
|
||||
*/
|
||||
private _defaultWorld: World;
|
||||
|
||||
/**
|
||||
* 待切换的下一个场景(延迟切换用)
|
||||
*/
|
||||
private _nextScene: IScene | null = null;
|
||||
|
||||
/**
|
||||
* ECS流式API
|
||||
*/
|
||||
private _ecsAPI: ECSFluentAPI | null = null;
|
||||
|
||||
/**
|
||||
* 日志器
|
||||
*/
|
||||
private _logger = createLogger('SceneManager');
|
||||
|
||||
/**
|
||||
* 场景切换回调函数
|
||||
*/
|
||||
private _onSceneChangedCallback?: () => void;
|
||||
|
||||
/**
|
||||
* 性能监控器(从 Core 注入)
|
||||
*/
|
||||
private _performanceMonitor: PerformanceMonitor | null = null;
|
||||
|
||||
/**
|
||||
* 默认场景ID
|
||||
*/
|
||||
private static readonly DEFAULT_SCENE_ID = '__main__';
|
||||
|
||||
constructor(performanceMonitor?: PerformanceMonitor) {
|
||||
this._defaultWorld = new World({ name: '__default__' });
|
||||
this._defaultWorld.start();
|
||||
this._performanceMonitor = performanceMonitor || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置场景切换回调
|
||||
*
|
||||
* @param callback 场景切换时的回调函数
|
||||
* @internal
|
||||
*/
|
||||
public setSceneChangedCallback(callback: () => void): void {
|
||||
this._onSceneChangedCallback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前场景(立即切换)
|
||||
*
|
||||
* 会自动处理旧场景的结束和新场景的初始化。
|
||||
*
|
||||
* @param scene - 要设置的场景实例
|
||||
* @returns 返回设置的场景实例,便于链式调用
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const gameScene = sceneManager.setScene(new GameScene());
|
||||
* console.log(gameScene.name); // 可以立即使用返回的场景
|
||||
* ```
|
||||
*/
|
||||
public setScene<T extends IScene>(scene: T): T {
|
||||
// 移除旧场景
|
||||
this._defaultWorld.removeAllScenes();
|
||||
|
||||
// 注册全局 PerformanceMonitor 到 Scene 的 ServiceContainer
|
||||
if (this._performanceMonitor) {
|
||||
scene.services.registerInstance(PerformanceMonitor, this._performanceMonitor);
|
||||
}
|
||||
|
||||
// 通过 World 创建新场景
|
||||
this._defaultWorld.createScene(SceneManager.DEFAULT_SCENE_ID, scene);
|
||||
this._defaultWorld.setSceneActive(SceneManager.DEFAULT_SCENE_ID, true);
|
||||
|
||||
// 重建ECS API
|
||||
if (scene.querySystem && scene.eventSystem) {
|
||||
this._ecsAPI = createECSAPI(scene, scene.querySystem, scene.eventSystem);
|
||||
} else {
|
||||
this._ecsAPI = null;
|
||||
}
|
||||
|
||||
// 触发场景切换回调
|
||||
Time.sceneChanged();
|
||||
|
||||
// 通知调试管理器(通过回调)
|
||||
if (this._onSceneChangedCallback) {
|
||||
this._onSceneChangedCallback();
|
||||
}
|
||||
|
||||
this._logger.info(`Scene changed to: ${scene.name}`);
|
||||
|
||||
return scene;
|
||||
}
|
||||
|
||||
/**
|
||||
* 延迟加载场景(下一帧切换)
|
||||
*
|
||||
* 场景不会立即切换,而是在下一次调用 update() 时切换。
|
||||
* 这对于避免在当前帧的中途切换场景很有用。
|
||||
*
|
||||
* @param scene - 要加载的场景实例
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 在某个System中触发场景切换
|
||||
* class GameOverSystem extends EntitySystem {
|
||||
* process(entities: readonly Entity[]) {
|
||||
* if (playerHealth <= 0) {
|
||||
* sceneManager.loadScene(new GameOverScene());
|
||||
* // 当前帧继续执行,场景将在下一帧切换
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
public loadScene<T extends IScene>(scene: T): void {
|
||||
this._nextScene = scene;
|
||||
this._logger.info(`Scheduled scene load: ${scene.name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前活跃的场景
|
||||
*
|
||||
* @returns 当前场景实例,如果没有场景则返回null
|
||||
*/
|
||||
public get currentScene(): IScene | null {
|
||||
return this._defaultWorld.getScene(SceneManager.DEFAULT_SCENE_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取ECS流式API
|
||||
*
|
||||
* 提供便捷的实体查询、事件发射等功能。
|
||||
*
|
||||
* @returns ECS API实例,如果当前没有场景则返回null
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const api = sceneManager.api;
|
||||
* if (api) {
|
||||
* // 查询所有敌人
|
||||
* const enemies = api.find(Enemy, Transform);
|
||||
*
|
||||
* // 发射事件
|
||||
* api.emit('game:start', { level: 1 });
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
public get api(): ECSFluentAPI | null {
|
||||
return this._ecsAPI;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新场景
|
||||
*
|
||||
* 应该在每帧的游戏循环中调用。
|
||||
* 会自动处理延迟场景切换。
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* function gameLoop(deltaTime: number) {
|
||||
* Core.update(deltaTime);
|
||||
* sceneManager.update(); // 每帧调用
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
public update(): void {
|
||||
// 处理延迟场景切换
|
||||
if (this._nextScene) {
|
||||
this.setScene(this._nextScene);
|
||||
this._nextScene = null;
|
||||
}
|
||||
|
||||
// 通过 World 统一更新
|
||||
this._defaultWorld.updateGlobalSystems();
|
||||
this._defaultWorld.updateScenes();
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁场景管理器
|
||||
*
|
||||
* 会自动结束当前场景并清理所有资源。
|
||||
* 通常在应用程序关闭时调用。
|
||||
*/
|
||||
public destroy(): void {
|
||||
this._logger.info('SceneManager destroying');
|
||||
|
||||
this._defaultWorld.destroy();
|
||||
this._nextScene = null;
|
||||
this._ecsAPI = null;
|
||||
|
||||
this._logger.info('SceneManager destroyed');
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有活跃场景
|
||||
*
|
||||
* @returns 如果有活跃场景返回true,否则返回false
|
||||
*/
|
||||
public get hasScene(): boolean {
|
||||
return this._defaultWorld.getScene(SceneManager.DEFAULT_SCENE_ID) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有待切换的场景
|
||||
*
|
||||
* @returns 如果有待切换场景返回true,否则返回false
|
||||
*/
|
||||
public get hasPendingScene(): boolean {
|
||||
return this._nextScene !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源(IService接口)
|
||||
*/
|
||||
public dispose(): void {
|
||||
this.destroy();
|
||||
}
|
||||
}
|
||||
336
packages/core/src/ECS/Serialization/ComponentSerializer.ts
Normal file
336
packages/core/src/ECS/Serialization/ComponentSerializer.ts
Normal file
@@ -0,0 +1,336 @@
|
||||
/**
|
||||
* 组件序列化器
|
||||
*
|
||||
* 负责组件的序列化和反序列化操作
|
||||
*/
|
||||
|
||||
import { Component } from '../Component';
|
||||
import { ComponentType } from '../Core/ComponentStorage';
|
||||
import { getComponentTypeName } from '../Decorators';
|
||||
import {
|
||||
getSerializationMetadata,
|
||||
isSerializable,
|
||||
SerializationMetadata
|
||||
} from './SerializationDecorators';
|
||||
|
||||
/**
|
||||
* 序列化后的组件数据
|
||||
*/
|
||||
export interface SerializedComponent {
|
||||
/**
|
||||
* 组件类型名称
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* 序列化版本
|
||||
*/
|
||||
version: number;
|
||||
|
||||
/**
|
||||
* 组件数据
|
||||
*/
|
||||
data: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件序列化器类
|
||||
*/
|
||||
export class ComponentSerializer {
|
||||
/**
|
||||
* 序列化单个组件
|
||||
*
|
||||
* @param component 要序列化的组件实例
|
||||
* @returns 序列化后的组件数据,如果组件不可序列化则返回null
|
||||
*/
|
||||
public static serialize(component: Component): SerializedComponent | null {
|
||||
const metadata = getSerializationMetadata(component);
|
||||
|
||||
if (!metadata) {
|
||||
// 组件没有使用@Serializable装饰器,不可序列化
|
||||
return null;
|
||||
}
|
||||
|
||||
const componentType = component.constructor as ComponentType;
|
||||
const typeName = metadata.options.typeId || getComponentTypeName(componentType);
|
||||
const data: Record<string, any> = {};
|
||||
|
||||
// 序列化标记的字段
|
||||
for (const [fieldName, options] of metadata.fields) {
|
||||
const fieldKey = typeof fieldName === 'symbol' ? fieldName.toString() : fieldName;
|
||||
const value = (component as any)[fieldName];
|
||||
|
||||
// 跳过忽略的字段
|
||||
if (metadata.ignoredFields.has(fieldName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 使用自定义序列化器或默认序列化
|
||||
const serializedValue = options.serializer
|
||||
? options.serializer(value)
|
||||
: this.serializeValue(value);
|
||||
|
||||
// 使用别名或原始字段名
|
||||
const key = options.alias || fieldKey;
|
||||
data[key] = serializedValue;
|
||||
}
|
||||
|
||||
return {
|
||||
type: typeName,
|
||||
version: metadata.options.version,
|
||||
data
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 反序列化组件
|
||||
*
|
||||
* @param serializedData 序列化的组件数据
|
||||
* @param componentRegistry 组件类型注册表 (类型名 -> 构造函数)
|
||||
* @returns 反序列化后的组件实例,如果失败则返回null
|
||||
*/
|
||||
public static deserialize(
|
||||
serializedData: SerializedComponent,
|
||||
componentRegistry: Map<string, ComponentType>
|
||||
): Component | null {
|
||||
const componentClass = componentRegistry.get(serializedData.type);
|
||||
|
||||
if (!componentClass) {
|
||||
console.warn(`未找到组件类型: ${serializedData.type}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const metadata = getSerializationMetadata(componentClass);
|
||||
|
||||
if (!metadata) {
|
||||
console.warn(`组件 ${serializedData.type} 不可序列化`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 创建组件实例
|
||||
const component = new componentClass();
|
||||
|
||||
// 反序列化字段
|
||||
for (const [fieldName, options] of metadata.fields) {
|
||||
const fieldKey = typeof fieldName === 'symbol' ? fieldName.toString() : fieldName;
|
||||
const key = options.alias || fieldKey;
|
||||
const serializedValue = serializedData.data[key];
|
||||
|
||||
if (serializedValue === undefined) {
|
||||
continue; // 字段不存在于序列化数据中
|
||||
}
|
||||
|
||||
// 使用自定义反序列化器或默认反序列化
|
||||
const value = options.deserializer
|
||||
? options.deserializer(serializedValue)
|
||||
: this.deserializeValue(serializedValue);
|
||||
|
||||
(component as any)[fieldName] = value;
|
||||
}
|
||||
|
||||
return component;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量序列化组件
|
||||
*
|
||||
* @param components 组件数组
|
||||
* @returns 序列化后的组件数据数组
|
||||
*/
|
||||
public static serializeComponents(components: Component[]): SerializedComponent[] {
|
||||
const result: SerializedComponent[] = [];
|
||||
|
||||
for (const component of components) {
|
||||
const serialized = this.serialize(component);
|
||||
if (serialized) {
|
||||
result.push(serialized);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量反序列化组件
|
||||
*
|
||||
* @param serializedComponents 序列化的组件数据数组
|
||||
* @param componentRegistry 组件类型注册表
|
||||
* @returns 反序列化后的组件数组
|
||||
*/
|
||||
public static deserializeComponents(
|
||||
serializedComponents: SerializedComponent[],
|
||||
componentRegistry: Map<string, ComponentType>
|
||||
): Component[] {
|
||||
const result: Component[] = [];
|
||||
|
||||
for (const serialized of serializedComponents) {
|
||||
const component = this.deserialize(serialized, componentRegistry);
|
||||
if (component) {
|
||||
result.push(component);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认值序列化
|
||||
*
|
||||
* 处理基本类型、数组、对象等的序列化
|
||||
*/
|
||||
private static serializeValue(value: any): any {
|
||||
if (value === null || value === undefined) {
|
||||
return value;
|
||||
}
|
||||
|
||||
// 基本类型
|
||||
const type = typeof value;
|
||||
if (type === 'string' || type === 'number' || type === 'boolean') {
|
||||
return value;
|
||||
}
|
||||
|
||||
// 日期
|
||||
if (value instanceof Date) {
|
||||
return {
|
||||
__type: 'Date',
|
||||
value: value.toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
// 数组
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(item => this.serializeValue(item));
|
||||
}
|
||||
|
||||
// Map (如果没有使用@SerializeMap装饰器)
|
||||
if (value instanceof Map) {
|
||||
return {
|
||||
__type: 'Map',
|
||||
value: Array.from(value.entries())
|
||||
};
|
||||
}
|
||||
|
||||
// Set
|
||||
if (value instanceof Set) {
|
||||
return {
|
||||
__type: 'Set',
|
||||
value: Array.from(value)
|
||||
};
|
||||
}
|
||||
|
||||
// 普通对象
|
||||
if (type === 'object') {
|
||||
const result: Record<string, any> = {};
|
||||
for (const key in value) {
|
||||
if (value.hasOwnProperty(key)) {
|
||||
result[key] = this.serializeValue(value[key]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// 其他类型(函数等)不序列化
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认值反序列化
|
||||
*/
|
||||
private static deserializeValue(value: any): any {
|
||||
if (value === null || value === undefined) {
|
||||
return value;
|
||||
}
|
||||
|
||||
// 基本类型直接返回
|
||||
const type = typeof value;
|
||||
if (type === 'string' || type === 'number' || type === 'boolean') {
|
||||
return value;
|
||||
}
|
||||
|
||||
// 处理特殊类型标记
|
||||
if (type === 'object' && value.__type) {
|
||||
switch (value.__type) {
|
||||
case 'Date':
|
||||
return new Date(value.value);
|
||||
case 'Map':
|
||||
return new Map(value.value);
|
||||
case 'Set':
|
||||
return new Set(value.value);
|
||||
}
|
||||
}
|
||||
|
||||
// 数组
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(item => this.deserializeValue(item));
|
||||
}
|
||||
|
||||
// 普通对象
|
||||
if (type === 'object') {
|
||||
const result: Record<string, any> = {};
|
||||
for (const key in value) {
|
||||
if (value.hasOwnProperty(key)) {
|
||||
result[key] = this.deserializeValue(value[key]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证序列化数据的版本
|
||||
*
|
||||
* @param serializedData 序列化数据
|
||||
* @param expectedVersion 期望的版本号
|
||||
* @returns 版本是否匹配
|
||||
*/
|
||||
public static validateVersion(
|
||||
serializedData: SerializedComponent,
|
||||
expectedVersion: number
|
||||
): boolean {
|
||||
return serializedData.version === expectedVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取组件的序列化信息
|
||||
*
|
||||
* @param component 组件实例或组件类
|
||||
* @returns 序列化信息对象,包含类型名、版本、可序列化字段列表
|
||||
*/
|
||||
public static getSerializationInfo(component: Component | ComponentType): {
|
||||
type: string;
|
||||
version: number;
|
||||
fields: string[];
|
||||
ignoredFields: string[];
|
||||
isSerializable: boolean;
|
||||
} | null {
|
||||
const metadata = getSerializationMetadata(component);
|
||||
|
||||
if (!metadata) {
|
||||
return {
|
||||
type: 'unknown',
|
||||
version: 0,
|
||||
fields: [],
|
||||
ignoredFields: [],
|
||||
isSerializable: false
|
||||
};
|
||||
}
|
||||
|
||||
const componentType = typeof component === 'function'
|
||||
? component
|
||||
: (component.constructor as ComponentType);
|
||||
|
||||
return {
|
||||
type: metadata.options.typeId || getComponentTypeName(componentType),
|
||||
version: metadata.options.version,
|
||||
fields: Array.from(metadata.fields.keys()).map(k =>
|
||||
typeof k === 'symbol' ? k.toString() : k
|
||||
),
|
||||
ignoredFields: Array.from(metadata.ignoredFields).map(k =>
|
||||
typeof k === 'symbol' ? k.toString() : k
|
||||
),
|
||||
isSerializable: true
|
||||
};
|
||||
}
|
||||
}
|
||||
235
packages/core/src/ECS/Serialization/EntitySerializer.ts
Normal file
235
packages/core/src/ECS/Serialization/EntitySerializer.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
/**
|
||||
* 实体序列化器
|
||||
*
|
||||
* 负责实体的序列化和反序列化操作
|
||||
*/
|
||||
|
||||
import { Entity } from '../Entity';
|
||||
import { Component } from '../Component';
|
||||
import { ComponentType } from '../Core/ComponentStorage';
|
||||
import { ComponentSerializer, SerializedComponent } from './ComponentSerializer';
|
||||
import { IScene } from '../IScene';
|
||||
|
||||
/**
|
||||
* 序列化后的实体数据
|
||||
*/
|
||||
export interface SerializedEntity {
|
||||
/**
|
||||
* 实体ID
|
||||
*/
|
||||
id: number;
|
||||
|
||||
/**
|
||||
* 实体名称
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* 实体标签
|
||||
*/
|
||||
tag: number;
|
||||
|
||||
/**
|
||||
* 激活状态
|
||||
*/
|
||||
active: boolean;
|
||||
|
||||
/**
|
||||
* 启用状态
|
||||
*/
|
||||
enabled: boolean;
|
||||
|
||||
/**
|
||||
* 更新顺序
|
||||
*/
|
||||
updateOrder: number;
|
||||
|
||||
/**
|
||||
* 组件列表
|
||||
*/
|
||||
components: SerializedComponent[];
|
||||
|
||||
/**
|
||||
* 子实体列表
|
||||
*/
|
||||
children: SerializedEntity[];
|
||||
|
||||
/**
|
||||
* 父实体ID(如果有)
|
||||
*/
|
||||
parentId?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 实体序列化器类
|
||||
*/
|
||||
export class EntitySerializer {
|
||||
/**
|
||||
* 序列化单个实体
|
||||
*
|
||||
* @param entity 要序列化的实体
|
||||
* @param includeChildren 是否包含子实体(默认true)
|
||||
* @returns 序列化后的实体数据
|
||||
*/
|
||||
public static serialize(entity: Entity, includeChildren: boolean = true): SerializedEntity {
|
||||
const serializedComponents = ComponentSerializer.serializeComponents(
|
||||
Array.from(entity.components)
|
||||
);
|
||||
|
||||
const serializedEntity: SerializedEntity = {
|
||||
id: entity.id,
|
||||
name: entity.name,
|
||||
tag: entity.tag,
|
||||
active: entity.active,
|
||||
enabled: entity.enabled,
|
||||
updateOrder: entity.updateOrder,
|
||||
components: serializedComponents,
|
||||
children: []
|
||||
};
|
||||
|
||||
// 序列化父实体引用
|
||||
if (entity.parent) {
|
||||
serializedEntity.parentId = entity.parent.id;
|
||||
}
|
||||
|
||||
// 序列化子实体
|
||||
if (includeChildren) {
|
||||
for (const child of entity.children) {
|
||||
serializedEntity.children.push(this.serialize(child, true));
|
||||
}
|
||||
}
|
||||
|
||||
return serializedEntity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 反序列化实体
|
||||
*
|
||||
* @param serializedEntity 序列化的实体数据
|
||||
* @param componentRegistry 组件类型注册表
|
||||
* @param idGenerator 实体ID生成器(用于生成新ID或保持原ID)
|
||||
* @param preserveIds 是否保持原始ID(默认false)
|
||||
* @param scene 目标场景(可选,用于设置entity.scene以支持添加组件)
|
||||
* @returns 反序列化后的实体
|
||||
*/
|
||||
public static deserialize(
|
||||
serializedEntity: SerializedEntity,
|
||||
componentRegistry: Map<string, ComponentType>,
|
||||
idGenerator: () => number,
|
||||
preserveIds: boolean = false,
|
||||
scene?: IScene
|
||||
): Entity {
|
||||
// 创建实体(使用原始ID或新生成的ID)
|
||||
const entityId = preserveIds ? serializedEntity.id : idGenerator();
|
||||
const entity = new Entity(serializedEntity.name, entityId);
|
||||
|
||||
// 如果提供了scene,先设置entity.scene以支持添加组件
|
||||
if (scene) {
|
||||
entity.scene = scene;
|
||||
}
|
||||
|
||||
// 恢复实体属性
|
||||
entity.tag = serializedEntity.tag;
|
||||
entity.active = serializedEntity.active;
|
||||
entity.enabled = serializedEntity.enabled;
|
||||
entity.updateOrder = serializedEntity.updateOrder;
|
||||
|
||||
// 反序列化组件
|
||||
const components = ComponentSerializer.deserializeComponents(
|
||||
serializedEntity.components,
|
||||
componentRegistry
|
||||
);
|
||||
|
||||
for (const component of components) {
|
||||
entity.addComponent(component);
|
||||
}
|
||||
|
||||
// 反序列化子实体
|
||||
for (const childData of serializedEntity.children) {
|
||||
const childEntity = this.deserialize(
|
||||
childData,
|
||||
componentRegistry,
|
||||
idGenerator,
|
||||
preserveIds,
|
||||
scene
|
||||
);
|
||||
entity.addChild(childEntity);
|
||||
}
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量序列化实体
|
||||
*
|
||||
* @param entities 实体数组
|
||||
* @param includeChildren 是否包含子实体
|
||||
* @returns 序列化后的实体数据数组
|
||||
*/
|
||||
public static serializeEntities(
|
||||
entities: Entity[],
|
||||
includeChildren: boolean = true
|
||||
): SerializedEntity[] {
|
||||
const result: SerializedEntity[] = [];
|
||||
|
||||
for (const entity of entities) {
|
||||
// 只序列化顶层实体(没有父实体的实体)
|
||||
// 子实体会在父实体序列化时一并处理
|
||||
if (!entity.parent || !includeChildren) {
|
||||
result.push(this.serialize(entity, includeChildren));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量反序列化实体
|
||||
*
|
||||
* @param serializedEntities 序列化的实体数据数组
|
||||
* @param componentRegistry 组件类型注册表
|
||||
* @param idGenerator 实体ID生成器
|
||||
* @param preserveIds 是否保持原始ID
|
||||
* @param scene 目标场景(可选,用于设置entity.scene以支持添加组件)
|
||||
* @returns 反序列化后的实体数组
|
||||
*/
|
||||
public static deserializeEntities(
|
||||
serializedEntities: SerializedEntity[],
|
||||
componentRegistry: Map<string, ComponentType>,
|
||||
idGenerator: () => number,
|
||||
preserveIds: boolean = false,
|
||||
scene?: IScene
|
||||
): Entity[] {
|
||||
const result: Entity[] = [];
|
||||
|
||||
for (const serialized of serializedEntities) {
|
||||
const entity = this.deserialize(
|
||||
serialized,
|
||||
componentRegistry,
|
||||
idGenerator,
|
||||
preserveIds,
|
||||
scene
|
||||
);
|
||||
result.push(entity);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建实体的深拷贝
|
||||
*
|
||||
* @param entity 要拷贝的实体
|
||||
* @param componentRegistry 组件类型注册表
|
||||
* @param idGenerator ID生成器
|
||||
* @returns 拷贝后的新实体
|
||||
*/
|
||||
public static clone(
|
||||
entity: Entity,
|
||||
componentRegistry: Map<string, ComponentType>,
|
||||
idGenerator: () => number
|
||||
): Entity {
|
||||
const serialized = this.serialize(entity, true);
|
||||
return this.deserialize(serialized, componentRegistry, idGenerator, false);
|
||||
}
|
||||
}
|
||||
758
packages/core/src/ECS/Serialization/IncrementalSerializer.ts
Normal file
758
packages/core/src/ECS/Serialization/IncrementalSerializer.ts
Normal file
@@ -0,0 +1,758 @@
|
||||
/**
|
||||
* 增量序列化器
|
||||
*
|
||||
* 提供高性能的增量序列化支持,只序列化变更的数据
|
||||
* 适用于网络同步、大场景存档、时间回溯等场景
|
||||
*/
|
||||
|
||||
import type { IScene } from '../IScene';
|
||||
import { Entity } from '../Entity';
|
||||
import { Component } from '../Component';
|
||||
import { ComponentSerializer, SerializedComponent } from './ComponentSerializer';
|
||||
import { SerializedEntity } from './EntitySerializer';
|
||||
import { ComponentType } from '../Core/ComponentStorage';
|
||||
import { encode, decode } from '@msgpack/msgpack';
|
||||
|
||||
/**
|
||||
* 变更操作类型
|
||||
*/
|
||||
export enum ChangeOperation {
|
||||
/** 添加新实体 */
|
||||
EntityAdded = 'entity_added',
|
||||
/** 删除实体 */
|
||||
EntityRemoved = 'entity_removed',
|
||||
/** 实体属性更新 */
|
||||
EntityUpdated = 'entity_updated',
|
||||
/** 添加组件 */
|
||||
ComponentAdded = 'component_added',
|
||||
/** 删除组件 */
|
||||
ComponentRemoved = 'component_removed',
|
||||
/** 组件数据更新 */
|
||||
ComponentUpdated = 'component_updated',
|
||||
/** 场景数据更新 */
|
||||
SceneDataUpdated = 'scene_data_updated'
|
||||
}
|
||||
|
||||
/**
|
||||
* 实体变更记录
|
||||
*/
|
||||
export interface EntityChange {
|
||||
/** 操作类型 */
|
||||
operation: ChangeOperation;
|
||||
/** 实体ID */
|
||||
entityId: number;
|
||||
/** 实体名称(用于Added操作) */
|
||||
entityName?: string;
|
||||
/** 实体数据(用于Added/Updated操作) */
|
||||
entityData?: Partial<SerializedEntity>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件变更记录
|
||||
*/
|
||||
export interface ComponentChange {
|
||||
/** 操作类型 */
|
||||
operation: ChangeOperation;
|
||||
/** 实体ID */
|
||||
entityId: number;
|
||||
/** 组件类型名称 */
|
||||
componentType: string;
|
||||
/** 组件数据(用于Added/Updated操作) */
|
||||
componentData?: SerializedComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景数据变更记录
|
||||
*/
|
||||
export interface SceneDataChange {
|
||||
/** 操作类型 */
|
||||
operation: ChangeOperation;
|
||||
/** 变更的键 */
|
||||
key: string;
|
||||
/** 新值 */
|
||||
value: any;
|
||||
/** 是否删除 */
|
||||
deleted?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 增量序列化数据
|
||||
*/
|
||||
export interface IncrementalSnapshot {
|
||||
/** 快照版本号 */
|
||||
version: number;
|
||||
/** 时间戳 */
|
||||
timestamp: number;
|
||||
/** 场景名称 */
|
||||
sceneName: string;
|
||||
/** 基础版本号(相对于哪个快照的增量) */
|
||||
baseVersion: number;
|
||||
/** 实体变更列表 */
|
||||
entityChanges: EntityChange[];
|
||||
/** 组件变更列表 */
|
||||
componentChanges: ComponentChange[];
|
||||
/** 场景数据变更列表 */
|
||||
sceneDataChanges: SceneDataChange[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景快照(用于对比)
|
||||
*/
|
||||
interface SceneSnapshot {
|
||||
/** 快照版本号 */
|
||||
version: number;
|
||||
/** 实体ID集合 */
|
||||
entityIds: Set<number>;
|
||||
/** 实体数据映射 */
|
||||
entities: Map<number, {
|
||||
name: string;
|
||||
tag: number;
|
||||
active: boolean;
|
||||
enabled: boolean;
|
||||
updateOrder: number;
|
||||
parentId?: number;
|
||||
}>;
|
||||
/** 组件数据映射 (entityId -> componentType -> serializedData) */
|
||||
components: Map<number, Map<string, string>>; // 使用JSON字符串存储组件数据
|
||||
/** 场景自定义数据 */
|
||||
sceneData: Map<string, string>; // 使用JSON字符串存储场景数据
|
||||
}
|
||||
|
||||
/**
|
||||
* 增量序列化格式
|
||||
*/
|
||||
export type IncrementalSerializationFormat = 'json' | 'binary';
|
||||
|
||||
/**
|
||||
* 增量序列化选项
|
||||
*/
|
||||
export interface IncrementalSerializationOptions {
|
||||
/**
|
||||
* 是否包含组件数据的深度对比
|
||||
* 默认true,设为false可提升性能但可能漏掉组件内部字段变更
|
||||
*/
|
||||
deepComponentComparison?: boolean;
|
||||
|
||||
/**
|
||||
* 是否跟踪场景数据变更
|
||||
* 默认true
|
||||
*/
|
||||
trackSceneData?: boolean;
|
||||
|
||||
/**
|
||||
* 是否压缩快照(使用JSON序列化)
|
||||
* 默认false,设为true可减少内存占用但增加CPU开销
|
||||
*/
|
||||
compressSnapshot?: boolean;
|
||||
|
||||
/**
|
||||
* 序列化格式
|
||||
* - 'json': JSON格式(可读性好,方便调试)
|
||||
* - 'binary': MessagePack二进制格式(体积小,性能高)
|
||||
* 默认 'json'
|
||||
*/
|
||||
format?: IncrementalSerializationFormat;
|
||||
|
||||
/**
|
||||
* 是否美化JSON输出(仅在format='json'时有效)
|
||||
* 默认false
|
||||
*/
|
||||
pretty?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 增量序列化器类
|
||||
*/
|
||||
export class IncrementalSerializer {
|
||||
/** 当前快照版本号 */
|
||||
private static snapshotVersion = 0;
|
||||
|
||||
/**
|
||||
* 创建场景快照
|
||||
*
|
||||
* @param scene 要快照的场景
|
||||
* @param options 序列化选项
|
||||
* @returns 场景快照对象
|
||||
*/
|
||||
public static createSnapshot(
|
||||
scene: IScene,
|
||||
options?: IncrementalSerializationOptions
|
||||
): SceneSnapshot {
|
||||
const opts = {
|
||||
deepComponentComparison: true,
|
||||
trackSceneData: true,
|
||||
compressSnapshot: false,
|
||||
...options
|
||||
};
|
||||
|
||||
const snapshot: SceneSnapshot = {
|
||||
version: ++this.snapshotVersion,
|
||||
entityIds: new Set(),
|
||||
entities: new Map(),
|
||||
components: new Map(),
|
||||
sceneData: new Map()
|
||||
};
|
||||
|
||||
// 快照所有实体
|
||||
for (const entity of scene.entities.buffer) {
|
||||
snapshot.entityIds.add(entity.id);
|
||||
|
||||
// 存储实体基本信息
|
||||
snapshot.entities.set(entity.id, {
|
||||
name: entity.name,
|
||||
tag: entity.tag,
|
||||
active: entity.active,
|
||||
enabled: entity.enabled,
|
||||
updateOrder: entity.updateOrder,
|
||||
parentId: entity.parent?.id
|
||||
});
|
||||
|
||||
// 快照组件
|
||||
if (opts.deepComponentComparison) {
|
||||
const componentMap = new Map<string, string>();
|
||||
|
||||
for (const component of entity.components) {
|
||||
const serialized = ComponentSerializer.serialize(component);
|
||||
if (serialized) {
|
||||
// 使用JSON字符串存储,便于后续对比
|
||||
componentMap.set(
|
||||
serialized.type,
|
||||
JSON.stringify(serialized.data)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (componentMap.size > 0) {
|
||||
snapshot.components.set(entity.id, componentMap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 快照场景数据
|
||||
if (opts.trackSceneData) {
|
||||
for (const [key, value] of scene.sceneData) {
|
||||
snapshot.sceneData.set(key, JSON.stringify(value));
|
||||
}
|
||||
}
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算增量变更
|
||||
*
|
||||
* @param scene 当前场景
|
||||
* @param baseSnapshot 基础快照
|
||||
* @param options 序列化选项
|
||||
* @returns 增量快照
|
||||
*/
|
||||
public static computeIncremental(
|
||||
scene: IScene,
|
||||
baseSnapshot: SceneSnapshot,
|
||||
options?: IncrementalSerializationOptions
|
||||
): IncrementalSnapshot {
|
||||
const opts = {
|
||||
deepComponentComparison: true,
|
||||
trackSceneData: true,
|
||||
...options
|
||||
};
|
||||
|
||||
const incremental: IncrementalSnapshot = {
|
||||
version: ++this.snapshotVersion,
|
||||
timestamp: Date.now(),
|
||||
sceneName: scene.name,
|
||||
baseVersion: baseSnapshot.version,
|
||||
entityChanges: [],
|
||||
componentChanges: [],
|
||||
sceneDataChanges: []
|
||||
};
|
||||
|
||||
const currentEntityIds = new Set<number>();
|
||||
|
||||
// 检测实体变更
|
||||
for (const entity of scene.entities.buffer) {
|
||||
currentEntityIds.add(entity.id);
|
||||
|
||||
if (!baseSnapshot.entityIds.has(entity.id)) {
|
||||
// 新增实体
|
||||
incremental.entityChanges.push({
|
||||
operation: ChangeOperation.EntityAdded,
|
||||
entityId: entity.id,
|
||||
entityName: entity.name,
|
||||
entityData: {
|
||||
id: entity.id,
|
||||
name: entity.name,
|
||||
tag: entity.tag,
|
||||
active: entity.active,
|
||||
enabled: entity.enabled,
|
||||
updateOrder: entity.updateOrder,
|
||||
parentId: entity.parent?.id,
|
||||
components: [],
|
||||
children: []
|
||||
}
|
||||
});
|
||||
|
||||
// 新增实体的所有组件都是新增
|
||||
for (const component of entity.components) {
|
||||
const serialized = ComponentSerializer.serialize(component);
|
||||
if (serialized) {
|
||||
incremental.componentChanges.push({
|
||||
operation: ChangeOperation.ComponentAdded,
|
||||
entityId: entity.id,
|
||||
componentType: serialized.type,
|
||||
componentData: serialized
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 检查实体属性变更
|
||||
const oldData = baseSnapshot.entities.get(entity.id)!;
|
||||
const entityChanged =
|
||||
oldData.name !== entity.name ||
|
||||
oldData.tag !== entity.tag ||
|
||||
oldData.active !== entity.active ||
|
||||
oldData.enabled !== entity.enabled ||
|
||||
oldData.updateOrder !== entity.updateOrder ||
|
||||
oldData.parentId !== entity.parent?.id;
|
||||
|
||||
if (entityChanged) {
|
||||
incremental.entityChanges.push({
|
||||
operation: ChangeOperation.EntityUpdated,
|
||||
entityId: entity.id,
|
||||
entityData: {
|
||||
name: entity.name,
|
||||
tag: entity.tag,
|
||||
active: entity.active,
|
||||
enabled: entity.enabled,
|
||||
updateOrder: entity.updateOrder,
|
||||
parentId: entity.parent?.id
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 检查组件变更
|
||||
if (opts.deepComponentComparison) {
|
||||
this.detectComponentChanges(
|
||||
entity,
|
||||
baseSnapshot,
|
||||
incremental.componentChanges
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检测删除的实体
|
||||
for (const oldEntityId of baseSnapshot.entityIds) {
|
||||
if (!currentEntityIds.has(oldEntityId)) {
|
||||
incremental.entityChanges.push({
|
||||
operation: ChangeOperation.EntityRemoved,
|
||||
entityId: oldEntityId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 检测场景数据变更
|
||||
if (opts.trackSceneData) {
|
||||
this.detectSceneDataChanges(
|
||||
scene,
|
||||
baseSnapshot,
|
||||
incremental.sceneDataChanges
|
||||
);
|
||||
}
|
||||
|
||||
return incremental;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测组件变更
|
||||
*/
|
||||
private static detectComponentChanges(
|
||||
entity: Entity,
|
||||
baseSnapshot: SceneSnapshot,
|
||||
componentChanges: ComponentChange[]
|
||||
): void {
|
||||
const oldComponents = baseSnapshot.components.get(entity.id);
|
||||
const currentComponents = new Map<string, SerializedComponent>();
|
||||
|
||||
// 收集当前组件
|
||||
for (const component of entity.components) {
|
||||
const serialized = ComponentSerializer.serialize(component);
|
||||
if (serialized) {
|
||||
currentComponents.set(serialized.type, serialized);
|
||||
}
|
||||
}
|
||||
|
||||
// 检测新增和更新的组件
|
||||
for (const [type, serialized] of currentComponents) {
|
||||
const currentData = JSON.stringify(serialized.data);
|
||||
|
||||
if (!oldComponents || !oldComponents.has(type)) {
|
||||
// 新增组件
|
||||
componentChanges.push({
|
||||
operation: ChangeOperation.ComponentAdded,
|
||||
entityId: entity.id,
|
||||
componentType: type,
|
||||
componentData: serialized
|
||||
});
|
||||
} else if (oldComponents.get(type) !== currentData) {
|
||||
// 组件数据变更
|
||||
componentChanges.push({
|
||||
operation: ChangeOperation.ComponentUpdated,
|
||||
entityId: entity.id,
|
||||
componentType: type,
|
||||
componentData: serialized
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 检测删除的组件
|
||||
if (oldComponents) {
|
||||
for (const oldType of oldComponents.keys()) {
|
||||
if (!currentComponents.has(oldType)) {
|
||||
componentChanges.push({
|
||||
operation: ChangeOperation.ComponentRemoved,
|
||||
entityId: entity.id,
|
||||
componentType: oldType
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测场景数据变更
|
||||
*/
|
||||
private static detectSceneDataChanges(
|
||||
scene: IScene,
|
||||
baseSnapshot: SceneSnapshot,
|
||||
sceneDataChanges: SceneDataChange[]
|
||||
): void {
|
||||
const currentKeys = new Set<string>();
|
||||
|
||||
// 检测新增和更新的场景数据
|
||||
for (const [key, value] of scene.sceneData) {
|
||||
currentKeys.add(key);
|
||||
const currentValue = JSON.stringify(value);
|
||||
const oldValue = baseSnapshot.sceneData.get(key);
|
||||
|
||||
if (!oldValue || oldValue !== currentValue) {
|
||||
sceneDataChanges.push({
|
||||
operation: ChangeOperation.SceneDataUpdated,
|
||||
key,
|
||||
value
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 检测删除的场景数据
|
||||
for (const oldKey of baseSnapshot.sceneData.keys()) {
|
||||
if (!currentKeys.has(oldKey)) {
|
||||
sceneDataChanges.push({
|
||||
operation: ChangeOperation.SceneDataUpdated,
|
||||
key: oldKey,
|
||||
value: undefined,
|
||||
deleted: true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用增量变更到场景
|
||||
*
|
||||
* @param scene 目标场景
|
||||
* @param incremental 增量快照
|
||||
* @param componentRegistry 组件类型注册表
|
||||
*/
|
||||
public static applyIncremental(
|
||||
scene: IScene,
|
||||
incremental: IncrementalSnapshot,
|
||||
componentRegistry: Map<string, ComponentType>
|
||||
): void {
|
||||
// 应用实体变更
|
||||
for (const change of incremental.entityChanges) {
|
||||
switch (change.operation) {
|
||||
case ChangeOperation.EntityAdded:
|
||||
this.applyEntityAdded(scene, change);
|
||||
break;
|
||||
case ChangeOperation.EntityRemoved:
|
||||
this.applyEntityRemoved(scene, change);
|
||||
break;
|
||||
case ChangeOperation.EntityUpdated:
|
||||
this.applyEntityUpdated(scene, change);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 应用组件变更
|
||||
for (const change of incremental.componentChanges) {
|
||||
switch (change.operation) {
|
||||
case ChangeOperation.ComponentAdded:
|
||||
this.applyComponentAdded(scene, change, componentRegistry);
|
||||
break;
|
||||
case ChangeOperation.ComponentRemoved:
|
||||
this.applyComponentRemoved(scene, change, componentRegistry);
|
||||
break;
|
||||
case ChangeOperation.ComponentUpdated:
|
||||
this.applyComponentUpdated(scene, change, componentRegistry);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 应用场景数据变更
|
||||
for (const change of incremental.sceneDataChanges) {
|
||||
if (change.deleted) {
|
||||
scene.sceneData.delete(change.key);
|
||||
} else {
|
||||
scene.sceneData.set(change.key, change.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static applyEntityAdded(scene: IScene, change: EntityChange): void {
|
||||
if (!change.entityData) return;
|
||||
|
||||
const entity = new Entity(change.entityName || 'Entity', change.entityId);
|
||||
entity.tag = change.entityData.tag || 0;
|
||||
entity.active = change.entityData.active ?? true;
|
||||
entity.enabled = change.entityData.enabled ?? true;
|
||||
entity.updateOrder = change.entityData.updateOrder || 0;
|
||||
|
||||
scene.addEntity(entity);
|
||||
}
|
||||
|
||||
private static applyEntityRemoved(scene: IScene, change: EntityChange): void {
|
||||
const entity = scene.entities.findEntityById(change.entityId);
|
||||
if (entity) {
|
||||
entity.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
private static applyEntityUpdated(scene: IScene, change: EntityChange): void {
|
||||
if (!change.entityData) return;
|
||||
|
||||
const entity = scene.entities.findEntityById(change.entityId);
|
||||
if (!entity) return;
|
||||
|
||||
if (change.entityData.name !== undefined) entity.name = change.entityData.name;
|
||||
if (change.entityData.tag !== undefined) entity.tag = change.entityData.tag;
|
||||
if (change.entityData.active !== undefined) entity.active = change.entityData.active;
|
||||
if (change.entityData.enabled !== undefined) entity.enabled = change.entityData.enabled;
|
||||
if (change.entityData.updateOrder !== undefined) entity.updateOrder = change.entityData.updateOrder;
|
||||
|
||||
if (change.entityData.parentId !== undefined) {
|
||||
const newParent = scene.entities.findEntityById(change.entityData.parentId);
|
||||
if (newParent && entity.parent !== newParent) {
|
||||
if (entity.parent) {
|
||||
entity.parent.removeChild(entity);
|
||||
}
|
||||
newParent.addChild(entity);
|
||||
}
|
||||
} else if (entity.parent) {
|
||||
entity.parent.removeChild(entity);
|
||||
}
|
||||
}
|
||||
|
||||
private static applyComponentAdded(
|
||||
scene: IScene,
|
||||
change: ComponentChange,
|
||||
componentRegistry: Map<string, ComponentType>
|
||||
): void {
|
||||
if (!change.componentData) return;
|
||||
|
||||
const entity = scene.entities.findEntityById(change.entityId);
|
||||
if (!entity) return;
|
||||
|
||||
const component = ComponentSerializer.deserialize(change.componentData, componentRegistry);
|
||||
if (component) {
|
||||
entity.addComponent(component);
|
||||
}
|
||||
}
|
||||
|
||||
private static applyComponentRemoved(
|
||||
scene: IScene,
|
||||
change: ComponentChange,
|
||||
componentRegistry: Map<string, ComponentType>
|
||||
): void {
|
||||
const entity = scene.entities.findEntityById(change.entityId);
|
||||
if (!entity) return;
|
||||
|
||||
const componentClass = componentRegistry.get(change.componentType);
|
||||
if (!componentClass) return;
|
||||
|
||||
entity.removeComponentByType(componentClass);
|
||||
}
|
||||
|
||||
private static applyComponentUpdated(
|
||||
scene: IScene,
|
||||
change: ComponentChange,
|
||||
componentRegistry: Map<string, ComponentType>
|
||||
): void {
|
||||
if (!change.componentData) return;
|
||||
|
||||
const entity = scene.entities.findEntityById(change.entityId);
|
||||
if (!entity) return;
|
||||
|
||||
const componentClass = componentRegistry.get(change.componentType);
|
||||
if (!componentClass) return;
|
||||
|
||||
entity.removeComponentByType(componentClass);
|
||||
|
||||
const component = ComponentSerializer.deserialize(change.componentData, componentRegistry);
|
||||
if (component) {
|
||||
entity.addComponent(component);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化增量快照
|
||||
*
|
||||
* @param incremental 增量快照
|
||||
* @param options 序列化选项
|
||||
* @returns 序列化后的数据(JSON字符串或二进制Uint8Array)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // JSON格式(默认)
|
||||
* const jsonData = IncrementalSerializer.serializeIncremental(snapshot);
|
||||
*
|
||||
* // 二进制格式
|
||||
* const binaryData = IncrementalSerializer.serializeIncremental(snapshot, {
|
||||
* format: 'binary'
|
||||
* });
|
||||
*
|
||||
* // 美化JSON
|
||||
* const prettyJson = IncrementalSerializer.serializeIncremental(snapshot, {
|
||||
* format: 'json',
|
||||
* pretty: true
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
public static serializeIncremental(
|
||||
incremental: IncrementalSnapshot,
|
||||
options?: { format?: IncrementalSerializationFormat; pretty?: boolean }
|
||||
): string | Uint8Array {
|
||||
const opts = {
|
||||
format: 'json' as IncrementalSerializationFormat,
|
||||
pretty: false,
|
||||
...options
|
||||
};
|
||||
|
||||
if (opts.format === 'binary') {
|
||||
return encode(incremental);
|
||||
} else {
|
||||
return opts.pretty
|
||||
? JSON.stringify(incremental, null, 2)
|
||||
: JSON.stringify(incremental);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 反序列化增量快照
|
||||
*
|
||||
* @param data 序列化的数据(JSON字符串或二进制Uint8Array)
|
||||
* @returns 增量快照
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 从JSON反序列化
|
||||
* const snapshot = IncrementalSerializer.deserializeIncremental(jsonString);
|
||||
*
|
||||
* // 从二进制反序列化
|
||||
* const snapshot = IncrementalSerializer.deserializeIncremental(buffer);
|
||||
* ```
|
||||
*/
|
||||
public static deserializeIncremental(data: string | Uint8Array): IncrementalSnapshot {
|
||||
if (typeof data === 'string') {
|
||||
// JSON格式
|
||||
return JSON.parse(data);
|
||||
} else {
|
||||
// 二进制格式(MessagePack)
|
||||
return decode(data) as IncrementalSnapshot;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算增量快照的大小(字节)
|
||||
*
|
||||
* @param incremental 增量快照
|
||||
* @param format 序列化格式,默认为 'json'
|
||||
* @returns 字节数
|
||||
*/
|
||||
public static getIncrementalSize(
|
||||
incremental: IncrementalSnapshot,
|
||||
format: IncrementalSerializationFormat = 'json'
|
||||
): number {
|
||||
const data = this.serializeIncremental(incremental, { format });
|
||||
|
||||
if (typeof data === 'string') {
|
||||
// JSON格式:计算UTF-8编码后的字节数
|
||||
// 使用 Blob 来计算浏览器和 Node.js 环境兼容的字节数
|
||||
if (typeof Blob !== 'undefined') {
|
||||
return new Blob([data]).size;
|
||||
} else if (typeof Buffer !== 'undefined') {
|
||||
return Buffer.byteLength(data, 'utf8');
|
||||
} else {
|
||||
// 回退方案:粗略估算(不精确,但可用)
|
||||
return new TextEncoder().encode(data).length;
|
||||
}
|
||||
} else {
|
||||
// 二进制格式:直接返回Buffer长度
|
||||
return data.length;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取增量快照的统计信息
|
||||
*
|
||||
* @param incremental 增量快照
|
||||
* @returns 统计信息
|
||||
*/
|
||||
public static getIncrementalStats(incremental: IncrementalSnapshot): {
|
||||
totalChanges: number;
|
||||
entityChanges: number;
|
||||
componentChanges: number;
|
||||
sceneDataChanges: number;
|
||||
addedEntities: number;
|
||||
removedEntities: number;
|
||||
updatedEntities: number;
|
||||
addedComponents: number;
|
||||
removedComponents: number;
|
||||
updatedComponents: number;
|
||||
} {
|
||||
return {
|
||||
totalChanges:
|
||||
incremental.entityChanges.length +
|
||||
incremental.componentChanges.length +
|
||||
incremental.sceneDataChanges.length,
|
||||
entityChanges: incremental.entityChanges.length,
|
||||
componentChanges: incremental.componentChanges.length,
|
||||
sceneDataChanges: incremental.sceneDataChanges.length,
|
||||
addedEntities: incremental.entityChanges.filter(
|
||||
c => c.operation === ChangeOperation.EntityAdded
|
||||
).length,
|
||||
removedEntities: incremental.entityChanges.filter(
|
||||
c => c.operation === ChangeOperation.EntityRemoved
|
||||
).length,
|
||||
updatedEntities: incremental.entityChanges.filter(
|
||||
c => c.operation === ChangeOperation.EntityUpdated
|
||||
).length,
|
||||
addedComponents: incremental.componentChanges.filter(
|
||||
c => c.operation === ChangeOperation.ComponentAdded
|
||||
).length,
|
||||
removedComponents: incremental.componentChanges.filter(
|
||||
c => c.operation === ChangeOperation.ComponentRemoved
|
||||
).length,
|
||||
updatedComponents: incremental.componentChanges.filter(
|
||||
c => c.operation === ChangeOperation.ComponentUpdated
|
||||
).length
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置快照版本号(用于测试)
|
||||
*/
|
||||
public static resetVersion(): void {
|
||||
this.snapshotVersion = 0;
|
||||
}
|
||||
}
|
||||
559
packages/core/src/ECS/Serialization/SceneSerializer.ts
Normal file
559
packages/core/src/ECS/Serialization/SceneSerializer.ts
Normal file
@@ -0,0 +1,559 @@
|
||||
/**
|
||||
* 场景序列化器
|
||||
*
|
||||
* 负责整个场景的序列化和反序列化,包括实体、组件等
|
||||
*/
|
||||
|
||||
import type { IScene } from '../IScene';
|
||||
import { Entity } from '../Entity';
|
||||
import { Component } from '../Component';
|
||||
import { ComponentType, ComponentRegistry } from '../Core/ComponentStorage';
|
||||
import { EntitySerializer, SerializedEntity } from './EntitySerializer';
|
||||
import { getComponentTypeName } from '../Decorators';
|
||||
import { getSerializationMetadata } from './SerializationDecorators';
|
||||
import { encode, decode } from '@msgpack/msgpack';
|
||||
|
||||
/**
|
||||
* 场景序列化格式
|
||||
*/
|
||||
export type SerializationFormat = 'json' | 'binary';
|
||||
|
||||
/**
|
||||
* 场景序列化策略
|
||||
*/
|
||||
export type DeserializationStrategy = 'merge' | 'replace';
|
||||
|
||||
/**
|
||||
* 版本迁移函数
|
||||
*/
|
||||
export type MigrationFunction = (
|
||||
oldVersion: number,
|
||||
newVersion: number,
|
||||
data: any
|
||||
) => any;
|
||||
|
||||
/**
|
||||
* 场景序列化选项
|
||||
*/
|
||||
export interface SceneSerializationOptions {
|
||||
/**
|
||||
* 要序列化的组件类型列表
|
||||
* 如果未指定,则序列化所有可序列化的组件
|
||||
*/
|
||||
components?: ComponentType[];
|
||||
|
||||
/**
|
||||
* 是否序列化系统状态(当前不支持)
|
||||
*/
|
||||
systems?: boolean;
|
||||
|
||||
/**
|
||||
* 序列化格式
|
||||
*/
|
||||
format?: SerializationFormat;
|
||||
|
||||
/**
|
||||
* 是否美化JSON输出(仅在format='json'时有效)
|
||||
*/
|
||||
pretty?: boolean;
|
||||
|
||||
/**
|
||||
* 是否包含元数据(如序列化时间、版本等)
|
||||
*/
|
||||
includeMetadata?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景反序列化选项
|
||||
*/
|
||||
export interface SceneDeserializationOptions {
|
||||
/**
|
||||
* 反序列化策略
|
||||
* - 'merge': 合并到现有场景
|
||||
* - 'replace': 替换现有场景内容
|
||||
*/
|
||||
strategy?: DeserializationStrategy;
|
||||
|
||||
/**
|
||||
* 版本迁移函数
|
||||
*/
|
||||
migration?: MigrationFunction;
|
||||
|
||||
/**
|
||||
* 是否保持原始实体ID
|
||||
*/
|
||||
preserveIds?: boolean;
|
||||
|
||||
/**
|
||||
* 组件类型注册表
|
||||
* 如果未提供,将尝试从全局注册表获取
|
||||
*/
|
||||
componentRegistry?: Map<string, ComponentType>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化后的场景数据
|
||||
*/
|
||||
export interface SerializedScene {
|
||||
/**
|
||||
* 场景名称
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* 序列化版本
|
||||
*/
|
||||
version: number;
|
||||
|
||||
/**
|
||||
* 序列化时间戳
|
||||
*/
|
||||
timestamp?: number;
|
||||
|
||||
/**
|
||||
* 场景自定义数据
|
||||
*
|
||||
* 存储场景级别的配置和状态
|
||||
*/
|
||||
sceneData?: Record<string, any>;
|
||||
|
||||
/**
|
||||
* 实体列表
|
||||
*/
|
||||
entities: SerializedEntity[];
|
||||
|
||||
/**
|
||||
* 元数据
|
||||
*/
|
||||
metadata?: {
|
||||
entityCount: number;
|
||||
componentTypeCount: number;
|
||||
serializationOptions?: SceneSerializationOptions;
|
||||
};
|
||||
|
||||
/**
|
||||
* 组件类型注册信息
|
||||
*/
|
||||
componentTypeRegistry: Array<{
|
||||
typeName: string;
|
||||
version: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景序列化器类
|
||||
*/
|
||||
export class SceneSerializer {
|
||||
/**
|
||||
* 当前序列化版本
|
||||
*/
|
||||
private static readonly SERIALIZATION_VERSION = 1;
|
||||
|
||||
/**
|
||||
* 序列化场景
|
||||
*
|
||||
* @param scene 要序列化的场景
|
||||
* @param options 序列化选项
|
||||
* @returns 序列化后的数据(JSON字符串或二进制Uint8Array)
|
||||
*/
|
||||
public static serialize(scene: IScene, options?: SceneSerializationOptions): string | Uint8Array {
|
||||
const opts: SceneSerializationOptions = {
|
||||
systems: false,
|
||||
format: 'json',
|
||||
pretty: true,
|
||||
includeMetadata: true,
|
||||
...options
|
||||
};
|
||||
|
||||
// 过滤实体和组件
|
||||
const entities = this.filterEntities(scene, opts);
|
||||
|
||||
// 序列化实体
|
||||
const serializedEntities = EntitySerializer.serializeEntities(entities, true);
|
||||
|
||||
// 收集组件类型信息
|
||||
const componentTypeRegistry = this.buildComponentTypeRegistry(entities);
|
||||
|
||||
// 序列化场景自定义数据
|
||||
const sceneData = this.serializeSceneData(scene.sceneData);
|
||||
|
||||
// 构建序列化数据
|
||||
const serializedScene: SerializedScene = {
|
||||
name: scene.name,
|
||||
version: this.SERIALIZATION_VERSION,
|
||||
entities: serializedEntities,
|
||||
componentTypeRegistry
|
||||
};
|
||||
|
||||
// 添加场景数据(如果有)
|
||||
if (sceneData && Object.keys(sceneData).length > 0) {
|
||||
serializedScene.sceneData = sceneData;
|
||||
}
|
||||
|
||||
// 添加元数据
|
||||
if (opts.includeMetadata) {
|
||||
serializedScene.timestamp = Date.now();
|
||||
serializedScene.metadata = {
|
||||
entityCount: serializedEntities.length,
|
||||
componentTypeCount: componentTypeRegistry.length,
|
||||
serializationOptions: opts
|
||||
};
|
||||
}
|
||||
|
||||
// 根据格式返回数据
|
||||
if (opts.format === 'json') {
|
||||
return opts.pretty
|
||||
? JSON.stringify(serializedScene, null, 2)
|
||||
: JSON.stringify(serializedScene);
|
||||
} else {
|
||||
// 二进制格式(使用 MessagePack)
|
||||
return encode(serializedScene);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 反序列化场景
|
||||
*
|
||||
* @param scene 目标场景
|
||||
* @param saveData 序列化的数据(JSON字符串或二进制Uint8Array)
|
||||
* @param options 反序列化选项
|
||||
*/
|
||||
public static deserialize(
|
||||
scene: IScene,
|
||||
saveData: string | Uint8Array,
|
||||
options?: SceneDeserializationOptions
|
||||
): void {
|
||||
const opts: SceneDeserializationOptions = {
|
||||
strategy: 'replace',
|
||||
preserveIds: false,
|
||||
...options
|
||||
};
|
||||
|
||||
// 解析数据
|
||||
let serializedScene: SerializedScene;
|
||||
try {
|
||||
if (typeof saveData === 'string') {
|
||||
// JSON格式
|
||||
serializedScene = JSON.parse(saveData);
|
||||
} else {
|
||||
// 二进制格式(MessagePack)
|
||||
serializedScene = decode(saveData) as SerializedScene;
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to parse save data: ${error}`);
|
||||
}
|
||||
|
||||
// 版本迁移
|
||||
if (opts.migration && serializedScene.version !== this.SERIALIZATION_VERSION) {
|
||||
serializedScene = opts.migration(
|
||||
serializedScene.version,
|
||||
this.SERIALIZATION_VERSION,
|
||||
serializedScene
|
||||
);
|
||||
}
|
||||
|
||||
// 构建组件注册表
|
||||
const componentRegistry = opts.componentRegistry || this.getGlobalComponentRegistry();
|
||||
|
||||
// 根据策略处理场景
|
||||
if (opts.strategy === 'replace') {
|
||||
// 清空场景
|
||||
scene.destroyAllEntities();
|
||||
}
|
||||
|
||||
// ID生成器
|
||||
const idGenerator = () => scene.identifierPool.checkOut();
|
||||
|
||||
// 反序列化实体
|
||||
const entities = EntitySerializer.deserializeEntities(
|
||||
serializedScene.entities,
|
||||
componentRegistry,
|
||||
idGenerator,
|
||||
opts.preserveIds || false,
|
||||
scene
|
||||
);
|
||||
|
||||
// 将实体添加到场景
|
||||
for (const entity of entities) {
|
||||
scene.addEntity(entity, true);
|
||||
this.addChildrenRecursively(entity, scene);
|
||||
}
|
||||
|
||||
// 统一清理缓存(批量操作完成后)
|
||||
scene.querySystem.clearCache();
|
||||
scene.clearSystemEntityCaches();
|
||||
|
||||
// 反序列化场景自定义数据
|
||||
if (serializedScene.sceneData) {
|
||||
this.deserializeSceneData(serializedScene.sceneData, scene.sceneData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归添加实体的所有子实体到场景
|
||||
*
|
||||
* 修复反序列化时子实体丢失的问题:
|
||||
* EntitySerializer.deserialize会提前设置子实体的scene引用,
|
||||
* 导致Entity.addChild的条件判断(!child.scene)跳过scene.addEntity调用。
|
||||
* 因此需要在SceneSerializer中统一递归添加所有子实体。
|
||||
*
|
||||
* @param entity 父实体
|
||||
* @param scene 目标场景
|
||||
*/
|
||||
private static addChildrenRecursively(entity: Entity, scene: IScene): void {
|
||||
for (const child of entity.children) {
|
||||
scene.addEntity(child, true); // 延迟缓存清理
|
||||
this.addChildrenRecursively(child, scene); // 递归处理子实体的子实体
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化场景自定义数据
|
||||
*
|
||||
* 将 Map<string, any> 转换为普通对象
|
||||
*/
|
||||
private static serializeSceneData(sceneData: Map<string, any>): Record<string, any> {
|
||||
const result: Record<string, any> = {};
|
||||
|
||||
for (const [key, value] of sceneData) {
|
||||
result[key] = this.serializeValue(value);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 反序列化场景自定义数据
|
||||
*
|
||||
* 将普通对象还原为 Map<string, any>
|
||||
*/
|
||||
private static deserializeSceneData(
|
||||
data: Record<string, any>,
|
||||
targetMap: Map<string, any>
|
||||
): void {
|
||||
targetMap.clear();
|
||||
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
targetMap.set(key, this.deserializeValue(value));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化单个值
|
||||
*/
|
||||
private static serializeValue(value: any): any {
|
||||
if (value === null || value === undefined) {
|
||||
return value;
|
||||
}
|
||||
|
||||
// 基本类型
|
||||
const type = typeof value;
|
||||
if (type === 'string' || type === 'number' || type === 'boolean') {
|
||||
return value;
|
||||
}
|
||||
|
||||
// Date
|
||||
if (value instanceof Date) {
|
||||
return { __type: 'Date', value: value.toISOString() };
|
||||
}
|
||||
|
||||
// Map
|
||||
if (value instanceof Map) {
|
||||
return { __type: 'Map', value: Array.from(value.entries()) };
|
||||
}
|
||||
|
||||
// Set
|
||||
if (value instanceof Set) {
|
||||
return { __type: 'Set', value: Array.from(value) };
|
||||
}
|
||||
|
||||
// 数组
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(item => this.serializeValue(item));
|
||||
}
|
||||
|
||||
// 普通对象
|
||||
if (type === 'object') {
|
||||
const result: Record<string, any> = {};
|
||||
for (const key in value) {
|
||||
if (value.hasOwnProperty(key)) {
|
||||
result[key] = this.serializeValue(value[key]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// 其他类型不序列化
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 反序列化单个值
|
||||
*/
|
||||
private static deserializeValue(value: any): any {
|
||||
if (value === null || value === undefined) {
|
||||
return value;
|
||||
}
|
||||
|
||||
// 基本类型
|
||||
const type = typeof value;
|
||||
if (type === 'string' || type === 'number' || type === 'boolean') {
|
||||
return value;
|
||||
}
|
||||
|
||||
// 处理特殊类型标记
|
||||
if (type === 'object' && value.__type) {
|
||||
switch (value.__type) {
|
||||
case 'Date':
|
||||
return new Date(value.value);
|
||||
case 'Map':
|
||||
return new Map(value.value);
|
||||
case 'Set':
|
||||
return new Set(value.value);
|
||||
}
|
||||
}
|
||||
|
||||
// 数组
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(item => this.deserializeValue(item));
|
||||
}
|
||||
|
||||
// 普通对象
|
||||
if (type === 'object') {
|
||||
const result: Record<string, any> = {};
|
||||
for (const key in value) {
|
||||
if (value.hasOwnProperty(key)) {
|
||||
result[key] = this.deserializeValue(value[key]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤要序列化的实体和组件
|
||||
*/
|
||||
private static filterEntities(scene: IScene, options: SceneSerializationOptions): Entity[] {
|
||||
const entities = Array.from(scene.entities.buffer);
|
||||
|
||||
// 如果指定了组件类型过滤
|
||||
if (options.components && options.components.length > 0) {
|
||||
const componentTypeSet = new Set(options.components);
|
||||
|
||||
// 只返回拥有指定组件的实体
|
||||
return entities.filter(entity => {
|
||||
return Array.from(entity.components).some(component =>
|
||||
componentTypeSet.has(component.constructor as ComponentType)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return entities;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建组件类型注册表
|
||||
*/
|
||||
private static buildComponentTypeRegistry(
|
||||
entities: Entity[]
|
||||
): Array<{ typeName: string; version: number }> {
|
||||
const registry = new Map<string, number>();
|
||||
|
||||
for (const entity of entities) {
|
||||
for (const component of entity.components) {
|
||||
const componentType = component.constructor as ComponentType;
|
||||
const typeName = getComponentTypeName(componentType);
|
||||
const metadata = getSerializationMetadata(component);
|
||||
|
||||
if (metadata && !registry.has(typeName)) {
|
||||
registry.set(typeName, metadata.options.version);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(registry.entries()).map(([typeName, version]) => ({
|
||||
typeName,
|
||||
version
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取全局组件注册表
|
||||
*
|
||||
* 从所有已注册的组件类型构建注册表
|
||||
*/
|
||||
private static getGlobalComponentRegistry(): Map<string, ComponentType> {
|
||||
return ComponentRegistry.getAllComponentNames() as Map<string, ComponentType>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证保存数据的有效性
|
||||
*
|
||||
* @param saveData 序列化的数据
|
||||
* @returns 验证结果
|
||||
*/
|
||||
public static validate(saveData: string): {
|
||||
valid: boolean;
|
||||
version?: number;
|
||||
errors?: string[];
|
||||
} {
|
||||
const errors: string[] = [];
|
||||
|
||||
try {
|
||||
const data = JSON.parse(saveData);
|
||||
|
||||
if (!data.version) {
|
||||
errors.push('Missing version field');
|
||||
}
|
||||
|
||||
if (!data.entities || !Array.isArray(data.entities)) {
|
||||
errors.push('Missing or invalid entities field');
|
||||
}
|
||||
|
||||
if (!data.componentTypeRegistry || !Array.isArray(data.componentTypeRegistry)) {
|
||||
errors.push('Missing or invalid componentTypeRegistry field');
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
version: data.version,
|
||||
errors: errors.length > 0 ? errors : undefined
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
valid: false,
|
||||
errors: [`JSON parse error: ${error}`]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取保存数据的信息(不完全反序列化)
|
||||
*
|
||||
* @param saveData 序列化的数据
|
||||
* @returns 保存数据的元信息
|
||||
*/
|
||||
public static getInfo(saveData: string): {
|
||||
name: string;
|
||||
version: number;
|
||||
timestamp?: number;
|
||||
entityCount: number;
|
||||
componentTypeCount: number;
|
||||
} | null {
|
||||
try {
|
||||
const data: SerializedScene = JSON.parse(saveData);
|
||||
|
||||
return {
|
||||
name: data.name,
|
||||
version: data.version,
|
||||
timestamp: data.timestamp,
|
||||
entityCount: data.metadata?.entityCount || data.entities.length,
|
||||
componentTypeCount: data.componentTypeRegistry.length
|
||||
};
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
254
packages/core/src/ECS/Serialization/SerializationDecorators.ts
Normal file
254
packages/core/src/ECS/Serialization/SerializationDecorators.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
/**
|
||||
* 序列化装饰器
|
||||
*
|
||||
* 提供组件级别的序列化支持,包括字段级装饰器和类级装饰器
|
||||
*/
|
||||
|
||||
import { Component } from '../Component';
|
||||
|
||||
/**
|
||||
* 序列化元数据的Symbol键
|
||||
*/
|
||||
export const SERIALIZABLE_METADATA = Symbol('SerializableMetadata');
|
||||
export const SERIALIZE_FIELD = Symbol('SerializeField');
|
||||
export const SERIALIZE_OPTIONS = Symbol('SerializeOptions');
|
||||
|
||||
/**
|
||||
* 可序列化配置选项
|
||||
*/
|
||||
export interface SerializableOptions {
|
||||
/**
|
||||
* 序列化版本号,用于数据迁移
|
||||
*/
|
||||
version: number;
|
||||
|
||||
/**
|
||||
* 组件类型标识符(可选,默认使用类名)
|
||||
*/
|
||||
typeId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 字段序列化配置
|
||||
*/
|
||||
export interface FieldSerializeOptions {
|
||||
/**
|
||||
* 自定义序列化器
|
||||
*/
|
||||
serializer?: (value: any) => any;
|
||||
|
||||
/**
|
||||
* 自定义反序列化器
|
||||
*/
|
||||
deserializer?: (value: any) => any;
|
||||
|
||||
/**
|
||||
* 字段别名(用于序列化后的键名)
|
||||
*/
|
||||
alias?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化元数据
|
||||
*/
|
||||
export interface SerializationMetadata {
|
||||
options: SerializableOptions;
|
||||
fields: Map<string | symbol, FieldSerializeOptions>;
|
||||
ignoredFields: Set<string | symbol>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件可序列化装饰器
|
||||
*
|
||||
* 标记组件类为可序列化,必须与字段装饰器配合使用
|
||||
*
|
||||
* @param options 序列化配置选项
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @ECSComponent('Player')
|
||||
* @Serializable({ version: 1 })
|
||||
* class PlayerComponent extends Component {
|
||||
* @Serialize() name: string = 'Player';
|
||||
* @Serialize() level: number = 1;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function Serializable(options: SerializableOptions) {
|
||||
return function <T extends new (...args: any[]) => Component>(target: T): T {
|
||||
if (!options || typeof options.version !== 'number') {
|
||||
throw new Error('Serializable装饰器必须提供有效的版本号');
|
||||
}
|
||||
|
||||
// 初始化或获取现有元数据
|
||||
let metadata: SerializationMetadata = (target as any)[SERIALIZABLE_METADATA];
|
||||
if (!metadata) {
|
||||
metadata = {
|
||||
options,
|
||||
fields: new Map(),
|
||||
ignoredFields: new Set()
|
||||
};
|
||||
(target as any)[SERIALIZABLE_METADATA] = metadata;
|
||||
} else {
|
||||
metadata.options = options;
|
||||
}
|
||||
|
||||
return target;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 字段序列化装饰器
|
||||
*
|
||||
* 标记字段为可序列化
|
||||
*
|
||||
* @param options 字段序列化选项(可选)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @Serialize()
|
||||
* name: string = 'Player';
|
||||
*
|
||||
* @Serialize({ alias: 'hp' })
|
||||
* health: number = 100;
|
||||
* ```
|
||||
*/
|
||||
export function Serialize(options?: FieldSerializeOptions) {
|
||||
return function (target: any, propertyKey: string | symbol) {
|
||||
const constructor = target.constructor;
|
||||
|
||||
// 获取或创建元数据
|
||||
let metadata: SerializationMetadata = constructor[SERIALIZABLE_METADATA];
|
||||
if (!metadata) {
|
||||
metadata = {
|
||||
options: { version: 1 }, // 默认版本
|
||||
fields: new Map(),
|
||||
ignoredFields: new Set()
|
||||
};
|
||||
constructor[SERIALIZABLE_METADATA] = metadata;
|
||||
}
|
||||
|
||||
// 记录字段
|
||||
metadata.fields.set(propertyKey, options || {});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Map序列化装饰器
|
||||
*
|
||||
* 专门用于序列化Map类型字段
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @SerializeAsMap()
|
||||
* inventory: Map<string, number> = new Map();
|
||||
* ```
|
||||
*/
|
||||
export function SerializeAsMap() {
|
||||
return function (target: any, propertyKey: string | symbol) {
|
||||
Serialize({
|
||||
serializer: (value: Map<any, any>) => {
|
||||
if (!(value instanceof Map)) {
|
||||
return null;
|
||||
}
|
||||
return Array.from(value.entries());
|
||||
},
|
||||
deserializer: (value: any) => {
|
||||
if (!Array.isArray(value)) {
|
||||
return new Map();
|
||||
}
|
||||
return new Map(value);
|
||||
}
|
||||
})(target, propertyKey);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set序列化装饰器
|
||||
*
|
||||
* 专门用于序列化Set类型字段
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @SerializeAsSet()
|
||||
* tags: Set<string> = new Set();
|
||||
* ```
|
||||
*/
|
||||
export function SerializeAsSet() {
|
||||
return function (target: any, propertyKey: string | symbol) {
|
||||
Serialize({
|
||||
serializer: (value: Set<any>) => {
|
||||
if (!(value instanceof Set)) {
|
||||
return null;
|
||||
}
|
||||
return Array.from(value);
|
||||
},
|
||||
deserializer: (value: any) => {
|
||||
if (!Array.isArray(value)) {
|
||||
return new Set();
|
||||
}
|
||||
return new Set(value);
|
||||
}
|
||||
})(target, propertyKey);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 忽略序列化装饰器
|
||||
*
|
||||
* 标记字段不参与序列化
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @IgnoreSerialization()
|
||||
* tempCache: any = null;
|
||||
* ```
|
||||
*/
|
||||
export function IgnoreSerialization() {
|
||||
return function (target: any, propertyKey: string | symbol) {
|
||||
const constructor = target.constructor;
|
||||
|
||||
// 获取或创建元数据
|
||||
let metadata: SerializationMetadata = constructor[SERIALIZABLE_METADATA];
|
||||
if (!metadata) {
|
||||
metadata = {
|
||||
options: { version: 1 },
|
||||
fields: new Map(),
|
||||
ignoredFields: new Set()
|
||||
};
|
||||
constructor[SERIALIZABLE_METADATA] = metadata;
|
||||
}
|
||||
|
||||
// 记录忽略字段
|
||||
metadata.ignoredFields.add(propertyKey);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取组件的序列化元数据
|
||||
*
|
||||
* @param componentClass 组件类或组件实例
|
||||
* @returns 序列化元数据,如果组件不可序列化则返回null
|
||||
*/
|
||||
export function getSerializationMetadata(componentClass: any): SerializationMetadata | null {
|
||||
if (!componentClass) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 如果是实例,获取其构造函数
|
||||
const constructor = typeof componentClass === 'function'
|
||||
? componentClass
|
||||
: componentClass.constructor;
|
||||
|
||||
return constructor[SERIALIZABLE_METADATA] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查组件是否可序列化
|
||||
*
|
||||
* @param component 组件类或组件实例
|
||||
* @returns 如果组件可序列化返回true
|
||||
*/
|
||||
export function isSerializable(component: any): boolean {
|
||||
return getSerializationMetadata(component) !== null;
|
||||
}
|
||||
371
packages/core/src/ECS/Serialization/VersionMigration.ts
Normal file
371
packages/core/src/ECS/Serialization/VersionMigration.ts
Normal file
@@ -0,0 +1,371 @@
|
||||
/**
|
||||
* 版本迁移系统
|
||||
*
|
||||
* 提供组件和场景数据的版本迁移支持
|
||||
*/
|
||||
|
||||
import { SerializedComponent } from './ComponentSerializer';
|
||||
import { SerializedScene } from './SceneSerializer';
|
||||
|
||||
/**
|
||||
* 组件迁移函数
|
||||
*/
|
||||
export type ComponentMigrationFunction = (data: any, fromVersion: number, toVersion: number) => any;
|
||||
|
||||
/**
|
||||
* 场景迁移函数
|
||||
*/
|
||||
export type SceneMigrationFunction = (
|
||||
scene: SerializedScene,
|
||||
fromVersion: number,
|
||||
toVersion: number
|
||||
) => SerializedScene;
|
||||
|
||||
/**
|
||||
* 版本迁移管理器
|
||||
*/
|
||||
export class VersionMigrationManager {
|
||||
/**
|
||||
* 组件迁移函数注册表
|
||||
* Map<组件类型名, Map<版本号, 迁移函数>>
|
||||
*/
|
||||
private static componentMigrations = new Map<string, Map<number, ComponentMigrationFunction>>();
|
||||
|
||||
/**
|
||||
* 场景迁移函数注册表
|
||||
* Map<版本号, 迁移函数>
|
||||
*/
|
||||
private static sceneMigrations = new Map<number, SceneMigrationFunction>();
|
||||
|
||||
/**
|
||||
* 注册组件迁移函数
|
||||
*
|
||||
* @param componentType 组件类型名称
|
||||
* @param fromVersion 源版本号
|
||||
* @param toVersion 目标版本号
|
||||
* @param migration 迁移函数
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 从版本1迁移到版本2
|
||||
* VersionMigrationManager.registerComponentMigration(
|
||||
* 'PlayerComponent',
|
||||
* 1,
|
||||
* 2,
|
||||
* (data) => {
|
||||
* // 添加新字段
|
||||
* data.experience = 0;
|
||||
* return data;
|
||||
* }
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
public static registerComponentMigration(
|
||||
componentType: string,
|
||||
fromVersion: number,
|
||||
toVersion: number,
|
||||
migration: ComponentMigrationFunction
|
||||
): void {
|
||||
if (!this.componentMigrations.has(componentType)) {
|
||||
this.componentMigrations.set(componentType, new Map());
|
||||
}
|
||||
|
||||
const versionMap = this.componentMigrations.get(componentType)!;
|
||||
|
||||
// 使用fromVersion作为key,表示"从这个版本迁移"
|
||||
versionMap.set(fromVersion, migration);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册场景迁移函数
|
||||
*
|
||||
* @param fromVersion 源版本号
|
||||
* @param toVersion 目标版本号
|
||||
* @param migration 迁移函数
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* VersionMigrationManager.registerSceneMigration(
|
||||
* 1,
|
||||
* 2,
|
||||
* (scene) => {
|
||||
* // 迁移场景结构
|
||||
* scene.metadata = { ...scene.metadata, migratedFrom: 1 };
|
||||
* return scene;
|
||||
* }
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
public static registerSceneMigration(
|
||||
fromVersion: number,
|
||||
toVersion: number,
|
||||
migration: SceneMigrationFunction
|
||||
): void {
|
||||
this.sceneMigrations.set(fromVersion, migration);
|
||||
}
|
||||
|
||||
/**
|
||||
* 迁移组件数据
|
||||
*
|
||||
* @param component 序列化的组件数据
|
||||
* @param targetVersion 目标版本号
|
||||
* @returns 迁移后的组件数据
|
||||
*/
|
||||
public static migrateComponent(
|
||||
component: SerializedComponent,
|
||||
targetVersion: number
|
||||
): SerializedComponent {
|
||||
const currentVersion = component.version;
|
||||
|
||||
if (currentVersion === targetVersion) {
|
||||
return component; // 版本相同,无需迁移
|
||||
}
|
||||
|
||||
const migrations = this.componentMigrations.get(component.type);
|
||||
if (!migrations) {
|
||||
console.warn(`No migration path found for component ${component.type}`);
|
||||
return component;
|
||||
}
|
||||
|
||||
let migratedData = { ...component };
|
||||
let version = currentVersion;
|
||||
|
||||
// 执行迁移链
|
||||
while (version < targetVersion) {
|
||||
const migration = migrations.get(version);
|
||||
|
||||
if (!migration) {
|
||||
console.warn(
|
||||
`Missing migration from version ${version} to ${version + 1} for ${component.type}`
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
migratedData.data = migration(migratedData.data, version, version + 1);
|
||||
version++;
|
||||
}
|
||||
|
||||
migratedData.version = version;
|
||||
return migratedData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 迁移场景数据
|
||||
*
|
||||
* @param scene 序列化的场景数据
|
||||
* @param targetVersion 目标版本号
|
||||
* @returns 迁移后的场景数据
|
||||
*/
|
||||
public static migrateScene(scene: SerializedScene, targetVersion: number): SerializedScene {
|
||||
const currentVersion = scene.version;
|
||||
|
||||
if (currentVersion === targetVersion) {
|
||||
return scene; // 版本相同,无需迁移
|
||||
}
|
||||
|
||||
let migratedScene = { ...scene };
|
||||
let version = currentVersion;
|
||||
|
||||
// 执行场景级迁移
|
||||
while (version < targetVersion) {
|
||||
const migration = this.sceneMigrations.get(version);
|
||||
|
||||
if (!migration) {
|
||||
console.warn(`Missing scene migration from version ${version} to ${version + 1}`);
|
||||
break;
|
||||
}
|
||||
|
||||
migratedScene = migration(migratedScene, version, version + 1);
|
||||
version++;
|
||||
}
|
||||
|
||||
migratedScene.version = version;
|
||||
|
||||
// 迁移所有组件
|
||||
migratedScene = this.migrateSceneComponents(migratedScene);
|
||||
|
||||
return migratedScene;
|
||||
}
|
||||
|
||||
/**
|
||||
* 迁移场景中所有组件的版本
|
||||
*/
|
||||
private static migrateSceneComponents(scene: SerializedScene): SerializedScene {
|
||||
const migratedScene = { ...scene };
|
||||
|
||||
migratedScene.entities = scene.entities.map(entity => ({
|
||||
...entity,
|
||||
components: entity.components.map(component => {
|
||||
// 查找组件的目标版本
|
||||
const typeInfo = scene.componentTypeRegistry.find(
|
||||
t => t.typeName === component.type
|
||||
);
|
||||
|
||||
if (typeInfo && typeInfo.version !== component.version) {
|
||||
return this.migrateComponent(component, typeInfo.version);
|
||||
}
|
||||
|
||||
return component;
|
||||
}),
|
||||
children: this.migrateEntitiesComponents(entity.children, scene.componentTypeRegistry)
|
||||
}));
|
||||
|
||||
return migratedScene;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归迁移实体的组件
|
||||
*/
|
||||
private static migrateEntitiesComponents(
|
||||
entities: any[],
|
||||
typeRegistry: Array<{ typeName: string; version: number }>
|
||||
): any[] {
|
||||
return entities.map(entity => ({
|
||||
...entity,
|
||||
components: entity.components.map((component: SerializedComponent) => {
|
||||
const typeInfo = typeRegistry.find(t => t.typeName === component.type);
|
||||
|
||||
if (typeInfo && typeInfo.version !== component.version) {
|
||||
return this.migrateComponent(component, typeInfo.version);
|
||||
}
|
||||
|
||||
return component;
|
||||
}),
|
||||
children: this.migrateEntitiesComponents(entity.children, typeRegistry)
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有迁移函数
|
||||
*/
|
||||
public static clearMigrations(): void {
|
||||
this.componentMigrations.clear();
|
||||
this.sceneMigrations.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取组件的迁移路径
|
||||
*
|
||||
* @param componentType 组件类型名称
|
||||
* @returns 可用的迁移版本列表
|
||||
*/
|
||||
public static getComponentMigrationPath(componentType: string): number[] {
|
||||
const migrations = this.componentMigrations.get(componentType);
|
||||
if (!migrations) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Array.from(migrations.keys()).sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取场景的迁移路径
|
||||
*
|
||||
* @returns 可用的场景迁移版本列表
|
||||
*/
|
||||
public static getSceneMigrationPath(): number[] {
|
||||
return Array.from(this.sceneMigrations.keys()).sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否可以迁移组件
|
||||
*
|
||||
* @param componentType 组件类型名称
|
||||
* @param fromVersion 源版本
|
||||
* @param toVersion 目标版本
|
||||
* @returns 是否存在完整的迁移路径
|
||||
*/
|
||||
public static canMigrateComponent(
|
||||
componentType: string,
|
||||
fromVersion: number,
|
||||
toVersion: number
|
||||
): boolean {
|
||||
if (fromVersion === toVersion) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const migrations = this.componentMigrations.get(componentType);
|
||||
if (!migrations) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否存在完整的迁移路径
|
||||
for (let v = fromVersion; v < toVersion; v++) {
|
||||
if (!migrations.has(v)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否可以迁移场景
|
||||
*
|
||||
* @param fromVersion 源版本
|
||||
* @param toVersion 目标版本
|
||||
* @returns 是否存在完整的迁移路径
|
||||
*/
|
||||
public static canMigrateScene(fromVersion: number, toVersion: number): boolean {
|
||||
if (fromVersion === toVersion) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查是否存在完整的场景迁移路径
|
||||
for (let v = fromVersion; v < toVersion; v++) {
|
||||
if (!this.sceneMigrations.has(v)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 便捷的迁移构建器
|
||||
*
|
||||
* 提供链式API来定义迁移
|
||||
*/
|
||||
export class MigrationBuilder {
|
||||
private componentType?: string;
|
||||
private fromVersion: number = 1;
|
||||
private toVersion: number = 2;
|
||||
|
||||
/**
|
||||
* 设置组件类型
|
||||
*/
|
||||
public forComponent(componentType: string): this {
|
||||
this.componentType = componentType;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置版本范围
|
||||
*/
|
||||
public fromVersionToVersion(from: number, to: number): this {
|
||||
this.fromVersion = from;
|
||||
this.toVersion = to;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册迁移函数
|
||||
*/
|
||||
public migrate(migration: ComponentMigrationFunction | SceneMigrationFunction): void {
|
||||
if (this.componentType) {
|
||||
VersionMigrationManager.registerComponentMigration(
|
||||
this.componentType,
|
||||
this.fromVersion,
|
||||
this.toVersion,
|
||||
migration as ComponentMigrationFunction
|
||||
);
|
||||
} else {
|
||||
VersionMigrationManager.registerSceneMigration(
|
||||
this.fromVersion,
|
||||
this.toVersion,
|
||||
migration as SceneMigrationFunction
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
62
packages/core/src/ECS/Serialization/index.ts
Normal file
62
packages/core/src/ECS/Serialization/index.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* ECS序列化系统
|
||||
*
|
||||
* 提供完整的场景、实体和组件序列化支持
|
||||
*/
|
||||
|
||||
// 装饰器
|
||||
export {
|
||||
Serializable,
|
||||
Serialize,
|
||||
SerializeAsMap,
|
||||
SerializeAsSet,
|
||||
IgnoreSerialization,
|
||||
getSerializationMetadata,
|
||||
isSerializable,
|
||||
SERIALIZABLE_METADATA,
|
||||
SERIALIZE_FIELD,
|
||||
SERIALIZE_OPTIONS
|
||||
} from './SerializationDecorators';
|
||||
|
||||
export type {
|
||||
SerializableOptions,
|
||||
FieldSerializeOptions,
|
||||
SerializationMetadata
|
||||
} from './SerializationDecorators';
|
||||
|
||||
// 组件序列化器
|
||||
export { ComponentSerializer } from './ComponentSerializer';
|
||||
export type { SerializedComponent } from './ComponentSerializer';
|
||||
|
||||
// 实体序列化器
|
||||
export { EntitySerializer } from './EntitySerializer';
|
||||
export type { SerializedEntity } from './EntitySerializer';
|
||||
|
||||
// 场景序列化器
|
||||
export { SceneSerializer } from './SceneSerializer';
|
||||
export type {
|
||||
SerializedScene,
|
||||
SerializationFormat,
|
||||
DeserializationStrategy,
|
||||
MigrationFunction,
|
||||
SceneSerializationOptions,
|
||||
SceneDeserializationOptions
|
||||
} from './SceneSerializer';
|
||||
|
||||
// 版本迁移
|
||||
export { VersionMigrationManager, MigrationBuilder } from './VersionMigration';
|
||||
export type {
|
||||
ComponentMigrationFunction,
|
||||
SceneMigrationFunction
|
||||
} from './VersionMigration';
|
||||
|
||||
// 增量序列化
|
||||
export { IncrementalSerializer, ChangeOperation } from './IncrementalSerializer';
|
||||
export type {
|
||||
IncrementalSnapshot,
|
||||
IncrementalSerializationOptions,
|
||||
IncrementalSerializationFormat,
|
||||
EntityChange,
|
||||
ComponentChange,
|
||||
SceneDataChange
|
||||
} from './IncrementalSerializer';
|
||||
@@ -7,6 +7,8 @@ import type { QuerySystem } from '../Core/QuerySystem';
|
||||
import { getSystemInstanceTypeName } from '../Decorators';
|
||||
import { createLogger } from '../../Utils/Logger';
|
||||
import type { EventListenerConfig, TypeSafeEventSystem, EventHandler } from '../Core/EventSystem';
|
||||
import type { ComponentConstructor, ComponentInstance } from '../../Types/TypeHelpers';
|
||||
import type { IService } from '../../Core/ServiceContainer';
|
||||
|
||||
/**
|
||||
* 事件监听器记录
|
||||
@@ -21,17 +23,22 @@ interface EventListenerRecord {
|
||||
|
||||
/**
|
||||
* 实体系统的基类
|
||||
*
|
||||
*
|
||||
* 用于处理一组符合特定条件的实体。系统是ECS架构中的逻辑处理单元,
|
||||
* 负责对拥有特定组件组合的实体执行业务逻辑。
|
||||
*
|
||||
*
|
||||
* 支持泛型参数以提供类型安全的组件访问:
|
||||
*
|
||||
* @template TComponents - 系统需要的组件类型数组
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 传统方式
|
||||
* class MovementSystem extends EntitySystem {
|
||||
* constructor() {
|
||||
* super(Transform, Velocity);
|
||||
* super(Matcher.empty().all(Transform, Velocity));
|
||||
* }
|
||||
*
|
||||
*
|
||||
* protected process(entities: readonly Entity[]): void {
|
||||
* for (const entity of entities) {
|
||||
* const transform = entity.getComponent(Transform);
|
||||
@@ -40,12 +47,29 @@ interface EventListenerRecord {
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* // 类型安全方式
|
||||
* class MovementSystem extends EntitySystem<[typeof Transform, typeof Velocity]> {
|
||||
* constructor() {
|
||||
* super(Matcher.empty().all(Transform, Velocity));
|
||||
* }
|
||||
*
|
||||
* protected process(entities: readonly Entity[]): void {
|
||||
* for (const entity of entities) {
|
||||
* // 类型安全的组件访问
|
||||
* const [transform, velocity] = this.getComponents(entity);
|
||||
* transform.position.add(velocity.value);
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export abstract class EntitySystem implements ISystemBase {
|
||||
export abstract class EntitySystem<
|
||||
TComponents extends readonly ComponentConstructor[] = []
|
||||
> implements ISystemBase, IService {
|
||||
private _updateOrder: number;
|
||||
private _enabled: boolean;
|
||||
private _performanceMonitor: PerformanceMonitor;
|
||||
private _performanceMonitor: PerformanceMonitor | null;
|
||||
private _systemName: string;
|
||||
private _initialized: boolean;
|
||||
private _matcher: Matcher;
|
||||
@@ -125,7 +149,7 @@ export abstract class EntitySystem implements ISystemBase {
|
||||
constructor(matcher?: Matcher) {
|
||||
this._updateOrder = 0;
|
||||
this._enabled = true;
|
||||
this._performanceMonitor = PerformanceMonitor.instance;
|
||||
this._performanceMonitor = null;
|
||||
this._systemName = getSystemInstanceTypeName(this);
|
||||
this._initialized = false;
|
||||
this._matcher = matcher || Matcher.empty();
|
||||
@@ -169,6 +193,23 @@ export abstract class EntitySystem implements ISystemBase {
|
||||
this._scene = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置性能监控器
|
||||
*/
|
||||
public setPerformanceMonitor(monitor: PerformanceMonitor): void {
|
||||
this._performanceMonitor = monitor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取性能监控器
|
||||
*/
|
||||
private getPerformanceMonitor(): PerformanceMonitor {
|
||||
if (!this._performanceMonitor) {
|
||||
throw new Error(`${this._systemName}: PerformanceMonitor未注入,请确保在Core.create()之后再添加System到Scene`);
|
||||
}
|
||||
return this._performanceMonitor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取实体匹配器
|
||||
*/
|
||||
@@ -182,9 +223,6 @@ export abstract class EntitySystem implements ISystemBase {
|
||||
*/
|
||||
public setUpdateOrder(order: number): void {
|
||||
this._updateOrder = order;
|
||||
if (this.scene && this.scene.entityProcessors) {
|
||||
this.scene.entityProcessors.setDirty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -510,18 +548,20 @@ export abstract class EntitySystem implements ISystemBase {
|
||||
return;
|
||||
}
|
||||
|
||||
const startTime = this._performanceMonitor.startMonitoring(this._systemName);
|
||||
const monitor = this.getPerformanceMonitor();
|
||||
const startTime = monitor.startMonitoring(this._systemName);
|
||||
let entityCount = 0;
|
||||
|
||||
try {
|
||||
this.onBegin();
|
||||
// 查询实体并存储到帧缓存中
|
||||
// 响应式查询会自动维护最新的实体列表,updateEntityTracking会在检测到变化时invalidate
|
||||
this._entityCache.frame = this.queryEntities();
|
||||
entityCount = this._entityCache.frame.length;
|
||||
|
||||
this.process(this._entityCache.frame);
|
||||
} finally {
|
||||
this._performanceMonitor.endMonitoring(this._systemName, startTime, entityCount);
|
||||
monitor.endMonitoring(this._systemName, startTime, entityCount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -533,7 +573,8 @@ export abstract class EntitySystem implements ISystemBase {
|
||||
return;
|
||||
}
|
||||
|
||||
const startTime = this._performanceMonitor.startMonitoring(`${this._systemName}_Late`);
|
||||
const monitor = this.getPerformanceMonitor();
|
||||
const startTime = monitor.startMonitoring(`${this._systemName}_Late`);
|
||||
let entityCount = 0;
|
||||
|
||||
try {
|
||||
@@ -543,7 +584,7 @@ export abstract class EntitySystem implements ISystemBase {
|
||||
this.lateProcess(entities);
|
||||
this.onEnd();
|
||||
} finally {
|
||||
this._performanceMonitor.endMonitoring(`${this._systemName}_Late`, startTime, entityCount);
|
||||
monitor.endMonitoring(`${this._systemName}_Late`, startTime, entityCount);
|
||||
// 清理帧缓存
|
||||
this._entityCache.clearFrame();
|
||||
}
|
||||
@@ -603,27 +644,27 @@ export abstract class EntitySystem implements ISystemBase {
|
||||
|
||||
/**
|
||||
* 获取系统的性能数据
|
||||
*
|
||||
*
|
||||
* @returns 性能数据或undefined
|
||||
*/
|
||||
public getPerformanceData() {
|
||||
return this._performanceMonitor.getSystemData(this._systemName);
|
||||
return this.getPerformanceMonitor().getSystemData(this._systemName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统的性能统计
|
||||
*
|
||||
*
|
||||
* @returns 性能统计或undefined
|
||||
*/
|
||||
public getPerformanceStats() {
|
||||
return this._performanceMonitor.getSystemStats(this._systemName);
|
||||
return this.getPerformanceMonitor().getSystemStats(this._systemName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置系统的性能数据
|
||||
*/
|
||||
public resetPerformanceData(): void {
|
||||
this._performanceMonitor.resetSystem(this._systemName);
|
||||
this.getPerformanceMonitor().resetSystem(this._systemName);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -683,15 +724,43 @@ export abstract class EntitySystem implements ISystemBase {
|
||||
|
||||
/**
|
||||
* 当实体从系统中移除时调用
|
||||
*
|
||||
*
|
||||
* 子类可以重写此方法来处理实体移除事件。
|
||||
*
|
||||
*
|
||||
* @param entity 被移除的实体
|
||||
*/
|
||||
protected onRemoved(entity: Entity): void {
|
||||
// 子类可以重写此方法
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放系统资源
|
||||
*
|
||||
* 实现IService接口要求的dispose方法。
|
||||
* 当系统从Scene中移除或Scene销毁时调用。
|
||||
*
|
||||
* 默认行为:
|
||||
* - 移除所有事件监听器
|
||||
* - 清空所有缓存
|
||||
* - 重置初始化状态
|
||||
*
|
||||
* 子类可以重写此方法来清理自定义资源,但应该调用super.dispose()。
|
||||
*/
|
||||
public dispose(): void {
|
||||
// 移除所有事件监听器
|
||||
this.cleanupManualEventListeners();
|
||||
|
||||
// 清空所有缓存
|
||||
this._entityCache.clearAll();
|
||||
this._entityIdMap = null;
|
||||
|
||||
// 重置状态
|
||||
this._initialized = false;
|
||||
this._scene = null;
|
||||
|
||||
this.logger.debug(`System ${this._systemName} disposed`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加事件监听器
|
||||
*
|
||||
@@ -793,4 +862,235 @@ export abstract class EntitySystem implements ISystemBase {
|
||||
protected onDestroy(): void {
|
||||
// 子类可以重写此方法进行清理操作
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 类型安全的辅助方法
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 类型安全地获取单个组件
|
||||
*
|
||||
* 相比Entity.getComponent,此方法保证返回非空值,
|
||||
* 如果组件不存在会抛出错误而不是返回null
|
||||
*
|
||||
* @param entity 实体
|
||||
* @param componentType 组件类型
|
||||
* @returns 组件实例(保证非空)
|
||||
* @throws 如果组件不存在则抛出错误
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* protected process(entities: readonly Entity[]): void {
|
||||
* for (const entity of entities) {
|
||||
* const transform = this.requireComponent(entity, Transform);
|
||||
* // transform 保证非空,类型为 Transform
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
protected requireComponent<T extends ComponentConstructor>(
|
||||
entity: Entity,
|
||||
componentType: T
|
||||
): ComponentInstance<T> {
|
||||
const component = entity.getComponent(componentType as any);
|
||||
if (!component) {
|
||||
throw new Error(
|
||||
`Component ${componentType.name} not found on entity ${entity.name} in ${this.systemName}`
|
||||
);
|
||||
}
|
||||
return component as ComponentInstance<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量获取实体的所有必需组件
|
||||
*
|
||||
* 根据泛型参数TComponents推断返回类型,
|
||||
* 返回一个元组,包含所有组件实例
|
||||
*
|
||||
* @param entity 实体
|
||||
* @param components 组件类型数组
|
||||
* @returns 组件实例元组
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* class MySystem extends EntitySystem<[typeof Position, typeof Velocity]> {
|
||||
* protected process(entities: readonly Entity[]): void {
|
||||
* for (const entity of entities) {
|
||||
* const [pos, vel] = this.getComponents(entity, Position, Velocity);
|
||||
* // pos: Position, vel: Velocity (自动类型推断)
|
||||
* pos.x += vel.x;
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
protected getComponents<T extends readonly ComponentConstructor[]>(
|
||||
entity: Entity,
|
||||
...components: T
|
||||
): { [K in keyof T]: ComponentInstance<T[K]> } {
|
||||
return components.map((type) =>
|
||||
this.requireComponent(entity, type)
|
||||
) as any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 遍历实体并处理每个实体
|
||||
*
|
||||
* 提供更简洁的语法糖,避免手动遍历
|
||||
*
|
||||
* @param entities 实体列表
|
||||
* @param processor 处理函数
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* protected process(entities: readonly Entity[]): void {
|
||||
* this.forEach(entities, (entity) => {
|
||||
* const transform = this.requireComponent(entity, Transform);
|
||||
* transform.position.y -= 9.8 * Time.deltaTime;
|
||||
* });
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
protected forEach(
|
||||
entities: readonly Entity[],
|
||||
processor: (entity: Entity, index: number) => void
|
||||
): void {
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
processor(entities[i], i);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤实体
|
||||
*
|
||||
* @param entities 实体列表
|
||||
* @param predicate 过滤条件
|
||||
* @returns 过滤后的实体数组
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* protected process(entities: readonly Entity[]): void {
|
||||
* const activeEntities = this.filterEntities(entities, (entity) => {
|
||||
* const health = this.requireComponent(entity, Health);
|
||||
* return health.value > 0;
|
||||
* });
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
protected filterEntities(
|
||||
entities: readonly Entity[],
|
||||
predicate: (entity: Entity, index: number) => boolean
|
||||
): Entity[] {
|
||||
return Array.from(entities).filter(predicate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 映射实体到另一种类型
|
||||
*
|
||||
* @param entities 实体列表
|
||||
* @param mapper 映射函数
|
||||
* @returns 映射后的结果数组
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* protected process(entities: readonly Entity[]): void {
|
||||
* const positions = this.mapEntities(entities, (entity) => {
|
||||
* const transform = this.requireComponent(entity, Transform);
|
||||
* return transform.position;
|
||||
* });
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
protected mapEntities<R>(
|
||||
entities: readonly Entity[],
|
||||
mapper: (entity: Entity, index: number) => R
|
||||
): R[] {
|
||||
return Array.from(entities).map(mapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找第一个满足条件的实体
|
||||
*
|
||||
* @param entities 实体列表
|
||||
* @param predicate 查找条件
|
||||
* @returns 第一个满足条件的实体,或undefined
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* protected process(entities: readonly Entity[]): void {
|
||||
* const player = this.findEntity(entities, (entity) =>
|
||||
* entity.hasComponent(PlayerTag)
|
||||
* );
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
protected findEntity(
|
||||
entities: readonly Entity[],
|
||||
predicate: (entity: Entity, index: number) => boolean
|
||||
): Entity | undefined {
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
if (predicate(entities[i], i)) {
|
||||
return entities[i];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否存在满足条件的实体
|
||||
*
|
||||
* @param entities 实体列表
|
||||
* @param predicate 检查条件
|
||||
* @returns 是否存在满足条件的实体
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* protected process(entities: readonly Entity[]): void {
|
||||
* const hasLowHealth = this.someEntity(entities, (entity) => {
|
||||
* const health = this.requireComponent(entity, Health);
|
||||
* return health.value < 20;
|
||||
* });
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
protected someEntity(
|
||||
entities: readonly Entity[],
|
||||
predicate: (entity: Entity, index: number) => boolean
|
||||
): boolean {
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
if (predicate(entities[i], i)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否所有实体都满足条件
|
||||
*
|
||||
* @param entities 实体列表
|
||||
* @param predicate 检查条件
|
||||
* @returns 是否所有实体都满足条件
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* protected process(entities: readonly Entity[]): void {
|
||||
* const allHealthy = this.everyEntity(entities, (entity) => {
|
||||
* const health = this.requireComponent(entity, Health);
|
||||
* return health.value > 50;
|
||||
* });
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
protected everyEntity(
|
||||
entities: readonly Entity[],
|
||||
predicate: (entity: Entity, index: number) => boolean
|
||||
): boolean {
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
if (!predicate(entities[i], i)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -198,6 +198,7 @@ export abstract class WorkerEntitySystem<TEntityData = any> extends EntitySystem
|
||||
protected sharedBuffer: SharedArrayBuffer | null = null;
|
||||
protected sharedFloatArray: Float32Array | null = null;
|
||||
private platformAdapter: IPlatformAdapter;
|
||||
private hasLoggedSyncMode = false;
|
||||
|
||||
constructor(matcher?: Matcher, config: WorkerSystemConfig = {}) {
|
||||
super(matcher);
|
||||
@@ -449,7 +450,10 @@ export abstract class WorkerEntitySystem<TEntityData = any> extends EntitySystem
|
||||
}
|
||||
} else {
|
||||
// 同步处理(最后的fallback)
|
||||
this.logger.info(`${this.systemName}: Worker不可用,使用同步处理`);
|
||||
if (!this.hasLoggedSyncMode) {
|
||||
this.logger.info(`${this.systemName}: Worker不可用,使用同步处理`);
|
||||
this.hasLoggedSyncMode = true;
|
||||
}
|
||||
this.processSynchronously(entities);
|
||||
this.isProcessing = false;
|
||||
}
|
||||
|
||||
307
packages/core/src/ECS/TypedEntity.ts
Normal file
307
packages/core/src/ECS/TypedEntity.ts
Normal file
@@ -0,0 +1,307 @@
|
||||
/**
|
||||
* Entity类型安全工具函数
|
||||
*
|
||||
* 提供类型安全的组件操作工具函数,无需修改Entity类
|
||||
*/
|
||||
|
||||
import { Entity } from './Entity';
|
||||
import type { Component } from './Component';
|
||||
import type { ComponentType } from './Core/ComponentStorage';
|
||||
import type { ComponentConstructor, ComponentInstance } from '../Types/TypeHelpers';
|
||||
|
||||
/**
|
||||
* 获取组件,如果不存在则抛出错误
|
||||
*
|
||||
* @param entity - 实体实例
|
||||
* @param componentType - 组件类型构造函数
|
||||
* @returns 组件实例(保证非空)
|
||||
* @throws {Error} 如果组件不存在
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const position = requireComponent(entity, Position);
|
||||
* position.x += 10;
|
||||
* ```
|
||||
*/
|
||||
export function requireComponent<T extends ComponentConstructor>(
|
||||
entity: Entity,
|
||||
componentType: T
|
||||
): ComponentInstance<T> {
|
||||
const component = entity.getComponent(componentType as unknown as ComponentType<ComponentInstance<T>>);
|
||||
if (!component) {
|
||||
throw new Error(
|
||||
`Component ${componentType.name} not found on entity ${entity.name} (id: ${entity.id})`
|
||||
);
|
||||
}
|
||||
return component;
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试获取组件
|
||||
*
|
||||
* @param entity - 实体实例
|
||||
* @param componentType - 组件类型构造函数
|
||||
* @returns 组件实例或undefined
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const health = tryGetComponent(entity, Health);
|
||||
* if (health) {
|
||||
* health.value -= 10;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function tryGetComponent<T extends ComponentConstructor>(
|
||||
entity: Entity,
|
||||
componentType: T
|
||||
): ComponentInstance<T> | undefined {
|
||||
const component = entity.getComponent(componentType as unknown as ComponentType<ComponentInstance<T>>);
|
||||
return component !== null ? component : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量获取组件
|
||||
*
|
||||
* @param entity - 实体实例
|
||||
* @param types - 组件类型构造函数数组
|
||||
* @returns 组件实例元组,每个元素可能为null
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const [pos, vel, health] = getComponents(entity, Position, Velocity, Health);
|
||||
* if (pos && vel && health) {
|
||||
* pos.x += vel.dx;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function getComponents<T extends readonly ComponentConstructor[]>(
|
||||
entity: Entity,
|
||||
...types: T
|
||||
): { [K in keyof T]: ComponentInstance<T[K]> | null } {
|
||||
return types.map((type) =>
|
||||
entity.getComponent(type as unknown as ComponentType<ComponentInstance<typeof type>>)
|
||||
) as { [K in keyof T]: ComponentInstance<T[K]> | null };
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查实体是否拥有所有指定的组件
|
||||
*
|
||||
* @param entity - 实体实例
|
||||
* @param types - 组件类型构造函数数组
|
||||
* @returns 如果拥有所有组件返回true,否则返回false
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* if (hasComponents(entity, Position, Velocity)) {
|
||||
* const pos = entity.getComponent(Position)!;
|
||||
* const vel = entity.getComponent(Velocity)!;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function hasComponents(entity: Entity, ...types: ComponentConstructor[]): boolean {
|
||||
return types.every((type) => entity.hasComponent(type as unknown as ComponentType));
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查实体是否拥有至少一个指定的组件
|
||||
*
|
||||
* @param entity - 实体实例
|
||||
* @param types - 组件类型构造函数数组
|
||||
* @returns 如果拥有任意一个组件返回true,否则返回false
|
||||
*/
|
||||
export function hasAnyComponent(entity: Entity, ...types: ComponentConstructor[]): boolean {
|
||||
return types.some((type) => entity.hasComponent(type as unknown as ComponentType));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加组件并立即配置
|
||||
*
|
||||
* @param entity - 实体实例
|
||||
* @param component - 组件实例
|
||||
* @param configure - 配置回调函数
|
||||
* @returns 实体实例(支持链式调用)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* addAndConfigure(entity, new Health(), health => {
|
||||
* health.maxValue = 100;
|
||||
* health.value = 50;
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function addAndConfigure<T extends Component>(
|
||||
entity: Entity,
|
||||
component: T,
|
||||
configure: (component: T) => void
|
||||
): Entity {
|
||||
entity.addComponent(component);
|
||||
configure(component);
|
||||
return entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取或添加组件
|
||||
*
|
||||
* 如果组件已存在则返回现有组件,否则通过工厂函数创建并添加
|
||||
*
|
||||
* @param entity - 实体实例
|
||||
* @param componentType - 组件类型构造函数
|
||||
* @param factory - 组件工厂函数(仅在组件不存在时调用)
|
||||
* @returns 组件实例
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const health = getOrAddComponent(entity, Health, () => new Health(100));
|
||||
* ```
|
||||
*/
|
||||
export function getOrAddComponent<T extends ComponentConstructor>(
|
||||
entity: Entity,
|
||||
componentType: T,
|
||||
factory: () => ComponentInstance<T>
|
||||
): ComponentInstance<T> {
|
||||
let component = entity.getComponent(componentType as unknown as ComponentType<ComponentInstance<T>>);
|
||||
if (!component) {
|
||||
component = factory();
|
||||
entity.addComponent(component);
|
||||
}
|
||||
return component;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新组件的部分字段
|
||||
*
|
||||
* @param entity - 实体实例
|
||||
* @param componentType - 组件类型构造函数
|
||||
* @param data - 要更新的部分数据
|
||||
* @returns 如果更新成功返回true,组件不存在返回false
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* updateComponent(entity, Position, { x: 100 });
|
||||
* ```
|
||||
*/
|
||||
export function updateComponent<T extends ComponentConstructor>(
|
||||
entity: Entity,
|
||||
componentType: T,
|
||||
data: Partial<ComponentInstance<T>>
|
||||
): boolean {
|
||||
const component = entity.getComponent(componentType as unknown as ComponentType<ComponentInstance<T>>);
|
||||
if (!component) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Object.assign(component, data);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的实体构建器
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const player = buildEntity(scene.createEntity("Player"))
|
||||
* .with(new Position(100, 100))
|
||||
* .with(new Velocity(0, 0))
|
||||
* .withTag(1)
|
||||
* .build();
|
||||
* ```
|
||||
*/
|
||||
export class TypedEntityBuilder {
|
||||
private _entity: Entity;
|
||||
|
||||
constructor(entity: Entity) {
|
||||
this._entity = entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加组件
|
||||
*/
|
||||
with<T extends Component>(component: T): this {
|
||||
this._entity.addComponent(component);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加并配置组件
|
||||
*/
|
||||
withConfigured<T extends Component>(
|
||||
component: T,
|
||||
configure: (component: T) => void
|
||||
): this {
|
||||
this._entity.addComponent(component);
|
||||
configure(component);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置标签
|
||||
*/
|
||||
withTag(tag: number): this {
|
||||
this._entity.tag = tag;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置名称
|
||||
*/
|
||||
withName(name: string): this {
|
||||
this._entity.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置激活状态
|
||||
*/
|
||||
withActive(active: boolean): this {
|
||||
this._entity.active = active;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置启用状态
|
||||
*/
|
||||
withEnabled(enabled: boolean): this {
|
||||
this._entity.enabled = enabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置更新顺序
|
||||
*/
|
||||
withUpdateOrder(order: number): this {
|
||||
this._entity.updateOrder = order;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加子实体
|
||||
*/
|
||||
withChild(child: Entity): this {
|
||||
this._entity.addChild(child);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成构建
|
||||
*/
|
||||
build(): Entity {
|
||||
return this._entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取正在构建的实体
|
||||
*/
|
||||
get entity(): Entity {
|
||||
return this._entity;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建类型安全的实体构建器
|
||||
*
|
||||
* @param entity - 要包装的实体
|
||||
* @returns 实体构建器
|
||||
*/
|
||||
export function buildEntity(entity: Entity): TypedEntityBuilder {
|
||||
return new TypedEntityBuilder(entity);
|
||||
}
|
||||
@@ -1,31 +1,31 @@
|
||||
/**
|
||||
* 位枚举
|
||||
*/
|
||||
export enum SegmentPart {
|
||||
LOW = 0,
|
||||
HIGH = 1,
|
||||
}
|
||||
/**
|
||||
* 基础 64 位段结构
|
||||
* [低32位,高32位]
|
||||
*/
|
||||
export interface BitMask64Segment {
|
||||
/** 低32位(bit 0-31) */
|
||||
lo: number;
|
||||
/** 高32位(bit 32-63) */
|
||||
hi: number;
|
||||
}
|
||||
export type BitMask64Segment = [number,number]
|
||||
|
||||
/**
|
||||
* 位掩码数据结构
|
||||
* 基础模式(64位):使用 lo + hi 存储 64 位,segments 为空
|
||||
* 扩展模式(128+位):lo + hi 作为第一段,segments 存储额外的 64 位段
|
||||
* 基础模式(64位):使用 base[lo , hi] 存储 64 位,segments 为空
|
||||
* 扩展模式(128+位):base[lo , hi] 作为第一段,segments 存储额外的 64 位段
|
||||
* segments[0] 对应 bit 64-127,segments[1] 对应 bit 128-191,以此类推
|
||||
*/
|
||||
export interface BitMask64Data {
|
||||
/** 低32位(bit 0-31) */
|
||||
lo: number;
|
||||
/** 高32位(bit 32-63) */
|
||||
hi: number;
|
||||
base: BitMask64Segment;
|
||||
/** 扩展段数组,每个元素是一个 64 位段,用于超过 64 位的场景 */
|
||||
segments?: BitMask64Segment[];
|
||||
}
|
||||
|
||||
export class BitMask64Utils {
|
||||
/** 零掩码常量,所有位都为0 */
|
||||
public static readonly ZERO: BitMask64Data = { lo: 0, hi: 0 };
|
||||
public static readonly ZERO: Readonly<BitMask64Data> = { base: [0, 0], segments: undefined };
|
||||
|
||||
/**
|
||||
* 根据位索引创建64位掩码
|
||||
@@ -34,15 +34,12 @@ export class BitMask64Utils {
|
||||
* @throws 当位索引超出范围时抛出错误
|
||||
*/
|
||||
public static create(bitIndex: number): BitMask64Data {
|
||||
if (bitIndex < 0 || bitIndex >= 64) {
|
||||
throw new Error(`Bit index ${bitIndex} out of range [0, 63]`);
|
||||
}
|
||||
|
||||
if (bitIndex < 32) {
|
||||
return { lo: 1 << bitIndex, hi: 0 };
|
||||
} else {
|
||||
return { lo: 0, hi: 1 << (bitIndex - 32) };
|
||||
if (bitIndex < 0) {
|
||||
throw new Error(`Bit index ${bitIndex} out of range [0, ∞)`);
|
||||
}
|
||||
const mask: BitMask64Data = { base: [0, 0], segments: undefined };
|
||||
BitMask64Utils.setBit(mask, bitIndex);
|
||||
return mask;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,7 +48,7 @@ export class BitMask64Utils {
|
||||
* @returns 低32位为输入值、高32位为0的掩码
|
||||
*/
|
||||
public static fromNumber(value: number): BitMask64Data {
|
||||
return { lo: value >>> 0, hi: 0 };
|
||||
return { base: [value >>> 0, 0], segments: undefined};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,75 +58,50 @@ export class BitMask64Utils {
|
||||
* @returns 如果掩码包含bits中的任意位则返回true
|
||||
*/
|
||||
public static hasAny(mask: BitMask64Data, bits: BitMask64Data): boolean {
|
||||
// 检查第一个 64 位段
|
||||
if ((mask.lo & bits.lo) !== 0 || (mask.hi & bits.hi) !== 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 如果 bits 没有扩展段,检查完成
|
||||
if (!bits.segments || bits.segments.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果 bits 有扩展段但 mask 没有,返回 false
|
||||
if (!mask.segments || mask.segments.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查每个扩展段
|
||||
const minSegments = Math.min(mask.segments.length, bits.segments.length);
|
||||
for (let i = 0; i < minSegments; i++) {
|
||||
const maskSeg = mask.segments[i];
|
||||
const bitsSeg = bits.segments[i];
|
||||
if ((maskSeg.lo & bitsSeg.lo) !== 0 || (maskSeg.hi & bitsSeg.hi) !== 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
const bitsBase = bits.base;
|
||||
const maskBase = mask.base;
|
||||
const bitsSegments = bits.segments;
|
||||
const maskSegments = mask.segments;
|
||||
const baseHasAny = (maskBase[SegmentPart.LOW] & bitsBase[SegmentPart.LOW]) !== 0 || (maskBase[SegmentPart.HIGH] & bitsBase[SegmentPart.HIGH]) !== 0;
|
||||
// 基础区段就包含指定的位,或任意一个参数不含扩展区段,直接短路
|
||||
if(baseHasAny || !bitsSegments || !maskSegments) return baseHasAny;
|
||||
// 额外检查扩展区域是否包含指定的位 - 如果bitsSegments[index]不存在,会被转为NaN,NaN的位运算始终返回0
|
||||
return maskSegments.some((seg, index) => (seg[SegmentPart.LOW] & bitsSegments[index][SegmentPart.LOW]) !== 0 || (seg[SegmentPart.HIGH] & bitsSegments[index][SegmentPart.HIGH]) !== 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查掩码是否包含所有指定的位
|
||||
* 支持扩展模式,自动处理超过 64 位的掩码
|
||||
* @param mask 要检查的掩码
|
||||
* @param bits 指定的位模式
|
||||
* @returns 如果掩码包含bits中的所有位则返回true
|
||||
*/
|
||||
public static hasAll(mask: BitMask64Data, bits: BitMask64Data): boolean {
|
||||
// 检查第一个 64 位段
|
||||
if ((mask.lo & bits.lo) !== bits.lo || (mask.hi & bits.hi) !== bits.hi) {
|
||||
return false;
|
||||
}
|
||||
const maskBase = mask.base;
|
||||
const bitsBase = bits.base;
|
||||
const maskSegments = mask.segments;
|
||||
const bitsSegments = bits.segments;
|
||||
const baseHasAll = (maskBase[SegmentPart.LOW] & bitsBase[SegmentPart.LOW]) === bitsBase[SegmentPart.LOW] && (maskBase[SegmentPart.HIGH] & bitsBase[SegmentPart.HIGH]) === bitsBase[SegmentPart.HIGH];
|
||||
// 基础区域就不包含指定的位,或bits没有扩展区段,直接短路。
|
||||
if(!baseHasAll || !bitsSegments) return baseHasAll;
|
||||
|
||||
// 如果 bits 没有扩展段,检查完成
|
||||
if (!bits.segments || bits.segments.length === 0) {
|
||||
return true;
|
||||
// 扩展区段的hasAll匹配逻辑
|
||||
const maskSegmentsLength = maskSegments?.length ?? 0;
|
||||
// 对mask/bits中都存在的区段,进行hasAll判断
|
||||
if(maskSegments){
|
||||
for (let i = 0; i < Math.min(maskSegmentsLength,bitsSegments.length); i++) {
|
||||
if((maskSegments[i][SegmentPart.LOW] & bitsSegments[i][SegmentPart.LOW]) !== bitsSegments[i][SegmentPart.LOW] || (maskSegments[i][SegmentPart.HIGH] & bitsSegments[i][SegmentPart.HIGH]) !== bitsSegments[i][SegmentPart.HIGH]){
|
||||
// 存在不匹配的位,直接短路
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果 bits 有扩展段但 mask 没有,返回 false
|
||||
if (!mask.segments || mask.segments.length === 0) {
|
||||
// 检查 bits 的扩展段是否全为 0
|
||||
return bits.segments.every(seg => BitMask64Utils.isZero(seg));
|
||||
}
|
||||
|
||||
// 检查每个扩展段
|
||||
const minSegments = Math.min(mask.segments.length, bits.segments.length);
|
||||
for (let i = 0; i < minSegments; i++) {
|
||||
const maskSeg = mask.segments[i];
|
||||
const bitsSeg = bits.segments[i];
|
||||
if ((maskSeg.lo & bitsSeg.lo) !== bitsSeg.lo || (maskSeg.hi & bitsSeg.hi) !== bitsSeg.hi) {
|
||||
// 对mask中不存在,但bits中存在的区段,进行isZero判断
|
||||
for (let i = maskSegmentsLength; i < bitsSegments.length; i++) {
|
||||
if(bitsSegments[i][SegmentPart.LOW] !== 0 || bitsSegments[i][SegmentPart.HIGH] !== 0){
|
||||
// 存在不为0的区段,直接短路
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果 bits 有更多段,检查这些段是否为空
|
||||
for (let i = minSegments; i < bits.segments.length; i++) {
|
||||
if (!BitMask64Utils.isZero(bits.segments[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -140,7 +112,15 @@ export class BitMask64Utils {
|
||||
* @returns 如果掩码不包含bits中的任何位则返回true
|
||||
*/
|
||||
public static hasNone(mask: BitMask64Data, bits: BitMask64Data): boolean {
|
||||
return (mask.lo & bits.lo) === 0 && (mask.hi & bits.hi) === 0;
|
||||
const maskBase = mask.base;
|
||||
const bitsBase = bits.base;
|
||||
const maskSegments = mask.segments;
|
||||
const bitsSegments = bits.segments;
|
||||
const baseHasNone = (maskBase[SegmentPart.LOW] & bitsBase[SegmentPart.LOW]) === 0 && (maskBase[SegmentPart.HIGH] & bitsBase[SegmentPart.HIGH]) === 0;
|
||||
//不含扩展区域,或基础区域就包含指定的位,或bits不含拓展区段,直接短路。
|
||||
if(!maskSegments || !baseHasNone || !bitsSegments) return baseHasNone;
|
||||
// 额外检查扩展区域是否都包含指定的位 - 此时bitsSegments存在,如果bitsSegments[index]不存在,会被转为NaN,NaN的位运算始终返回0
|
||||
return maskSegments.every((seg, index) => (seg[SegmentPart.LOW] & bitsSegments[index][SegmentPart.LOW]) === 0 && (seg[SegmentPart.HIGH] & bitsSegments[index][SegmentPart.HIGH]) === 0);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -148,8 +128,14 @@ export class BitMask64Utils {
|
||||
* @param mask 要检查的掩码
|
||||
* @returns 如果掩码所有位都为0则返回true
|
||||
*/
|
||||
public static isZero(mask: BitMask64Data | BitMask64Segment): boolean {
|
||||
return mask.lo === 0 && mask.hi === 0;
|
||||
public static isZero(mask: BitMask64Data): boolean {
|
||||
const baseIsZero = mask.base[SegmentPart.LOW] === 0 && mask.base[SegmentPart.HIGH] === 0;
|
||||
if(!mask.segments || !baseIsZero){
|
||||
// 不含扩展区域,或基础区域值就不为0,直接短路
|
||||
return baseIsZero;
|
||||
}
|
||||
// 额外检查扩展区域是否都为0
|
||||
return mask.segments.every(seg => seg[SegmentPart.LOW] === 0 && seg[SegmentPart.HIGH] === 0);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -159,42 +145,82 @@ export class BitMask64Utils {
|
||||
* @returns 如果两个掩码完全相等则返回true
|
||||
*/
|
||||
public static equals(a: BitMask64Data, b: BitMask64Data): boolean {
|
||||
return a.lo === b.lo && a.hi === b.hi;
|
||||
let baseEquals = a.base[SegmentPart.LOW] === b.base[SegmentPart.LOW] && a.base[SegmentPart.HIGH] === b.base[SegmentPart.HIGH];
|
||||
// base不相等,或ab都没有扩展区域位,直接返回base比较结果
|
||||
if(!baseEquals || (!a.segments && !b.segments)) return baseEquals;
|
||||
// 不能假设a,b的segments都存在或长度相同.
|
||||
const aSegments = a.segments ?? [];
|
||||
const bSegments = b.segments ?? [];
|
||||
for (let i = 0; i < Math.max(aSegments.length, bSegments.length); i++) {
|
||||
const aSeg: BitMask64Segment | undefined = aSegments[i]; // 可能为undefined
|
||||
const bSeg: BitMask64Segment | undefined = bSegments[i]; // 可能为undefined
|
||||
if(aSeg && !bSeg){
|
||||
//bSeg不存在,则必须要求aSeg全为0
|
||||
if(aSeg[SegmentPart.LOW] !== 0 || aSeg[SegmentPart.HIGH] !== 0) return false;
|
||||
}else if(!aSeg && bSeg){
|
||||
//aSeg不存在,则必须要求bSeg全为0
|
||||
if(bSeg[SegmentPart.LOW] !== 0 || bSeg[SegmentPart.HIGH] !== 0) return false;
|
||||
}else{
|
||||
//理想状态:aSeg/bSeg都存在
|
||||
if(aSeg[SegmentPart.LOW] !== bSeg[SegmentPart.LOW] || aSeg[SegmentPart.HIGH] !== bSeg[SegmentPart.HIGH]) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置掩码中指定位为1
|
||||
* 设置掩码中指定位为1,必要时自动扩展
|
||||
* @param mask 要修改的掩码(原地修改)
|
||||
* @param bitIndex 位索引,范围 [0, 63]
|
||||
* @param bitIndex 位索引,不小于零
|
||||
* @throws 当位索引超出范围时抛出错误
|
||||
*/
|
||||
public static setBit(mask: BitMask64Data, bitIndex: number): void {
|
||||
if (bitIndex < 0 || bitIndex >= 64) {
|
||||
if (bitIndex < 0) {
|
||||
throw new Error(`Bit index ${bitIndex} out of range [0, 63]`);
|
||||
}
|
||||
|
||||
if (bitIndex < 32) {
|
||||
mask.lo |= (1 << bitIndex);
|
||||
const targetSeg = BitMask64Utils.getSegmentByBitIndex(mask, bitIndex)!;
|
||||
const mod = bitIndex & 63; // bitIndex % 64 优化方案
|
||||
if(mod < 32){
|
||||
targetSeg[SegmentPart.LOW] |= (1 << mod);
|
||||
} else {
|
||||
mask.hi |= (1 << (bitIndex - 32));
|
||||
targetSeg[SegmentPart.HIGH] |= (1 << (mod - 32));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除掩码中指定位为0
|
||||
* 获取掩码中指定位,如果位超出当前掩码的区段长度,则直接返回0
|
||||
* @param mask 掩码
|
||||
* @param bitIndex 位索引,不小于零
|
||||
*/
|
||||
public static getBit(mask: BitMask64Data, bitIndex: number): boolean {
|
||||
if (bitIndex < 0) {
|
||||
return false;
|
||||
}
|
||||
const targetSeg = BitMask64Utils.getSegmentByBitIndex(mask, bitIndex, false);
|
||||
if(!targetSeg) return false;
|
||||
const mod = bitIndex & 63; // bitIndex % 64 优化方案
|
||||
if(mod < 32){
|
||||
return (targetSeg[SegmentPart.LOW] & (1 << mod)) !== 0;
|
||||
} else {
|
||||
return (targetSeg[SegmentPart.HIGH] & (1 << (mod - 32))) !== 0;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 清除掩码中指定位为0,如果位超出当前掩码的区段长度,则什么也不做
|
||||
* @param mask 要修改的掩码(原地修改)
|
||||
* @param bitIndex 位索引,范围 [0, 63]
|
||||
* @throws 当位索引超出范围时抛出错误
|
||||
* @param bitIndex 位索引,不小于0
|
||||
*/
|
||||
public static clearBit(mask: BitMask64Data, bitIndex: number): void {
|
||||
if (bitIndex < 0 || bitIndex >= 64) {
|
||||
if (bitIndex < 0) {
|
||||
throw new Error(`Bit index ${bitIndex} out of range [0, 63]`);
|
||||
}
|
||||
|
||||
if (bitIndex < 32) {
|
||||
mask.lo &= ~(1 << bitIndex);
|
||||
const targetSeg = BitMask64Utils.getSegmentByBitIndex(mask, bitIndex, false);
|
||||
if(!targetSeg) return;
|
||||
const mod = bitIndex & 63; // bitIndex % 64 优化方案
|
||||
if(mod < 32){
|
||||
targetSeg[SegmentPart.LOW] &= ~(1 << mod);
|
||||
} else {
|
||||
mask.hi &= ~(1 << (bitIndex - 32));
|
||||
targetSeg[SegmentPart.HIGH] &= ~(1 << (mod - 32));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,24 +230,26 @@ export class BitMask64Utils {
|
||||
* @param other 用于按位或的掩码
|
||||
*/
|
||||
public static orInPlace(target: BitMask64Data, other: BitMask64Data): void {
|
||||
target.lo |= other.lo;
|
||||
target.hi |= other.hi;
|
||||
target.base[SegmentPart.LOW] |= other.base[SegmentPart.LOW];
|
||||
target.base[SegmentPart.HIGH] |= other.base[SegmentPart.HIGH];
|
||||
|
||||
// 处理扩展段
|
||||
if (other.segments && other.segments.length > 0) {
|
||||
const otherSegments = other.segments;
|
||||
if (otherSegments && otherSegments.length > 0) {
|
||||
if (!target.segments) {
|
||||
target.segments = [];
|
||||
}
|
||||
const targetSegments = target.segments;
|
||||
|
||||
// 确保 target 有足够的段
|
||||
while (target.segments.length < other.segments.length) {
|
||||
target.segments.push({ lo: 0, hi: 0 });
|
||||
while (targetSegments.length < otherSegments.length) {
|
||||
targetSegments.push([0, 0]);
|
||||
}
|
||||
|
||||
// 对每个段执行或操作
|
||||
for (let i = 0; i < other.segments.length; i++) {
|
||||
target.segments[i].lo |= other.segments[i].lo;
|
||||
target.segments[i].hi |= other.segments[i].hi;
|
||||
for (let i = 0; i < otherSegments.length; i++) {
|
||||
targetSegments[i][SegmentPart.LOW] |= otherSegments[i][SegmentPart.LOW];
|
||||
targetSegments[i][SegmentPart.HIGH] |= otherSegments[i][SegmentPart.HIGH];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -232,8 +260,27 @@ export class BitMask64Utils {
|
||||
* @param other 用于按位与的掩码
|
||||
*/
|
||||
public static andInPlace(target: BitMask64Data, other: BitMask64Data): void {
|
||||
target.lo &= other.lo;
|
||||
target.hi &= other.hi;
|
||||
target.base[SegmentPart.LOW] &= other.base[SegmentPart.LOW];
|
||||
target.base[SegmentPart.HIGH] &= other.base[SegmentPart.HIGH];
|
||||
|
||||
// 处理扩展段
|
||||
const otherSegments = other.segments;
|
||||
if (otherSegments && otherSegments.length > 0) {
|
||||
if (!target.segments) {
|
||||
target.segments = [];
|
||||
}
|
||||
const targetSegments = target.segments;
|
||||
// 确保 target 有足够的段
|
||||
while (targetSegments.length < otherSegments.length) {
|
||||
targetSegments.push([0, 0]);
|
||||
}
|
||||
|
||||
// 对每个段执行与操作
|
||||
for (let i = 0; i < otherSegments.length; i++) {
|
||||
targetSegments[i][SegmentPart.LOW] &= otherSegments[i][SegmentPart.LOW];
|
||||
targetSegments[i][SegmentPart.HIGH] &= otherSegments[i][SegmentPart.HIGH];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -242,8 +289,25 @@ export class BitMask64Utils {
|
||||
* @param other 用于按位异或的掩码
|
||||
*/
|
||||
public static xorInPlace(target: BitMask64Data, other: BitMask64Data): void {
|
||||
target.lo ^= other.lo;
|
||||
target.hi ^= other.hi;
|
||||
target.base[SegmentPart.LOW] ^= other.base[SegmentPart.LOW];
|
||||
target.base[SegmentPart.HIGH] ^= other.base[SegmentPart.HIGH];
|
||||
|
||||
// 处理扩展段
|
||||
const otherSegments = other.segments;
|
||||
if (!otherSegments || otherSegments.length == 0) return;
|
||||
if (!target.segments) target.segments = [];
|
||||
|
||||
const targetSegments = target.segments;
|
||||
// 确保 target 有足够的段
|
||||
while (targetSegments.length < otherSegments.length) {
|
||||
targetSegments.push([0, 0]);
|
||||
}
|
||||
|
||||
// 对每个段执行异或操作
|
||||
for (let i = 0; i < otherSegments.length; i++) {
|
||||
targetSegments[i][SegmentPart.LOW] ^= otherSegments[i][SegmentPart.LOW];
|
||||
targetSegments[i][SegmentPart.HIGH] ^= otherSegments[i][SegmentPart.HIGH];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -251,18 +315,43 @@ export class BitMask64Utils {
|
||||
* @param mask 要清除的掩码(原地修改)
|
||||
*/
|
||||
public static clear(mask: BitMask64Data): void {
|
||||
mask.lo = 0;
|
||||
mask.hi = 0;
|
||||
mask.base[SegmentPart.LOW] = 0;
|
||||
mask.base[SegmentPart.HIGH] = 0;
|
||||
for (let i = 0; i < (mask.segments?.length ?? 0); i++) {
|
||||
const seg = mask.segments![i];
|
||||
seg[SegmentPart.LOW] = 0;
|
||||
seg[SegmentPart.HIGH] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将源掩码的值复制到目标掩码
|
||||
* 将源掩码的值复制到目标掩码,如果source包含扩展段,则target也会至少扩展到source扩展段的长度
|
||||
* @param source 源掩码
|
||||
* @param target 目标掩码(原地修改)
|
||||
*/
|
||||
public static copy(source: BitMask64Data, target: BitMask64Data): void {
|
||||
target.lo = source.lo;
|
||||
target.hi = source.hi;
|
||||
BitMask64Utils.clear(target);
|
||||
target.base[SegmentPart.LOW] = source.base[SegmentPart.LOW];
|
||||
target.base[SegmentPart.HIGH] = source.base[SegmentPart.HIGH];
|
||||
// source没有扩展段,直接退出
|
||||
if(!source.segments || source.segments.length == 0) return;
|
||||
// 没有拓展段,则直接复制数组
|
||||
if(!target.segments){
|
||||
target.segments = source.segments.map(seg => [...seg]);
|
||||
return;
|
||||
}
|
||||
// source有扩展段,target扩展段不足,则补充长度
|
||||
const copyLength = source.segments.length - target.segments.length;
|
||||
for (let i = 0; i < copyLength; i++) {
|
||||
target.segments.push([0,0]);
|
||||
}
|
||||
// 逐个重写
|
||||
for (let i = 0; i < length; i++) {
|
||||
const targetSeg = target.segments![i];
|
||||
const sourSeg = source.segments![i];
|
||||
targetSeg[SegmentPart.LOW] = sourSeg[SegmentPart.LOW];
|
||||
targetSeg[SegmentPart.HIGH] = sourSeg[SegmentPart.HIGH];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -271,36 +360,65 @@ export class BitMask64Utils {
|
||||
* @returns 新的掩码对象,内容与源掩码相同
|
||||
*/
|
||||
public static clone(mask: BitMask64Data): BitMask64Data {
|
||||
return { lo: mask.lo, hi: mask.hi };
|
||||
return {
|
||||
base: mask.base.slice() as BitMask64Segment,
|
||||
segments: mask.segments ? mask.segments.map(seg => [...seg]) : undefined
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 将掩码转换为字符串表示
|
||||
* 将掩码转换为字符串表示,每个区段之间将使用空格分割。
|
||||
* @param mask 要转换的掩码
|
||||
* @param radix 进制,支持2(二进制)或16(十六进制),默认为2
|
||||
* @param radix 进制,支持2(二进制)或16(十六进制),默认为2,其他的值被视为2
|
||||
* @param printHead 打印头
|
||||
* @returns 掩码的字符串表示,二进制不带前缀,十六进制带0x前缀
|
||||
* @throws 当进制不支持时抛出错误
|
||||
*/
|
||||
public static toString(mask: BitMask64Data, radix: number = 2): string {
|
||||
if (radix === 2) {
|
||||
if (mask.hi === 0) {
|
||||
return mask.lo.toString(2);
|
||||
} else {
|
||||
const hiBits = mask.hi.toString(2);
|
||||
const loBits = mask.lo.toString(2).padStart(32, '0');
|
||||
return hiBits + loBits;
|
||||
public static toString(mask: BitMask64Data, radix: 2 | 16 = 2, printHead: boolean = false): string {
|
||||
if(radix != 2 && radix != 16) radix = 2;
|
||||
const totalLength = mask.segments?.length ?? 0;
|
||||
let result: string = '';
|
||||
if(printHead){
|
||||
let paddingLength = 0;
|
||||
if(radix === 2){
|
||||
paddingLength = 64 + 1 + 1;
|
||||
}else{
|
||||
paddingLength = 16 + 2 + 1;
|
||||
}
|
||||
} else if (radix === 16) {
|
||||
if (mask.hi === 0) {
|
||||
return '0x' + mask.lo.toString(16).toUpperCase();
|
||||
} else {
|
||||
const hiBits = mask.hi.toString(16).toUpperCase();
|
||||
const loBits = mask.lo.toString(16).toUpperCase().padStart(8, '0');
|
||||
return '0x' + hiBits + loBits;
|
||||
for (let i = 0; i <= totalLength; i++) {
|
||||
const title = i === 0 ? '0 (Base):' : `${i} (${64 * i}):`;
|
||||
result += title.toString().padEnd(paddingLength);
|
||||
}
|
||||
} else {
|
||||
throw new Error('Only radix 2 and 16 are supported');
|
||||
result += '\n';
|
||||
}
|
||||
|
||||
for (let i = -1; i < totalLength; i++) {
|
||||
let segResult = '';
|
||||
const bitMaskData = i == -1 ? mask.base : mask.segments![i];
|
||||
let hi = bitMaskData[SegmentPart.HIGH];
|
||||
let lo = bitMaskData[SegmentPart.LOW];
|
||||
if(radix == 2){
|
||||
const hiBits = hi.toString(2).padStart(32, '0');
|
||||
const loBits = lo.toString(2).padStart(32, '0');
|
||||
segResult = hiBits + '_' + loBits; //高低位之间使用_隔离
|
||||
}else{
|
||||
let hiBits = hi ? hi.toString(16).toUpperCase() : '';
|
||||
if(printHead){
|
||||
// 存在标头,则输出高位之前需要补齐位数
|
||||
hiBits = hiBits.padStart(8, '0');
|
||||
}
|
||||
let loBits = lo.toString(16).toUpperCase();
|
||||
if(hiBits){
|
||||
// 存在高位 则输出低位之前需要补齐位数
|
||||
loBits = loBits.padStart(8, '0');
|
||||
}
|
||||
segResult = '0x' + hiBits + loBits;
|
||||
}
|
||||
if(i === -1)
|
||||
result += segResult;
|
||||
else
|
||||
result += ' ' + segResult; // 不同段之间使用空格隔离
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -310,126 +428,49 @@ export class BitMask64Utils {
|
||||
*/
|
||||
public static popCount(mask: BitMask64Data): number {
|
||||
let count = 0;
|
||||
let lo = mask.lo;
|
||||
let hi = mask.hi;
|
||||
|
||||
while (lo) {
|
||||
lo &= lo - 1;
|
||||
count++;
|
||||
for (let i = -1; i < (mask.segments?.length ?? 0); i++) {
|
||||
const bitMaskData = i == -1 ? mask.base : mask.segments![i];
|
||||
let lo = bitMaskData[SegmentPart.LOW];
|
||||
let hi = bitMaskData[SegmentPart.HIGH];
|
||||
while (lo) {
|
||||
lo &= lo - 1;
|
||||
count++;
|
||||
}
|
||||
while (hi) {
|
||||
hi &= hi - 1;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
while (hi) {
|
||||
hi &= hi - 1;
|
||||
count++;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置扩展位(支持超过 64 位的索引)
|
||||
* @param mask 要修改的掩码(原地修改)
|
||||
* @param bitIndex 位索引(可以超过 63)
|
||||
* 获取包含目标位的BitMask64Segment
|
||||
* @param mask 要操作的掩码
|
||||
* @param bitIndex 目标位
|
||||
* @param createNewSegment 如果bitIndex超过了当前范围,是否自动补充扩展区域,默认为真
|
||||
* @private
|
||||
*/
|
||||
public static setBitExtended(mask: BitMask64Data, bitIndex: number): void {
|
||||
if (bitIndex < 0) {
|
||||
throw new Error('Bit index cannot be negative');
|
||||
}
|
||||
|
||||
if (bitIndex < 64) {
|
||||
BitMask64Utils.setBit(mask, bitIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算段索引和段内位索引
|
||||
const segmentIndex = Math.floor(bitIndex / 64) - 1;
|
||||
const localBitIndex = bitIndex % 64;
|
||||
|
||||
// 确保 segments 数组存在
|
||||
if (!mask.segments) {
|
||||
mask.segments = [];
|
||||
}
|
||||
|
||||
// 扩展 segments 数组
|
||||
while (mask.segments.length <= segmentIndex) {
|
||||
mask.segments.push({ lo: 0, hi: 0 });
|
||||
}
|
||||
|
||||
// 设置对应段的位
|
||||
const segment = mask.segments[segmentIndex];
|
||||
if (localBitIndex < 32) {
|
||||
segment.lo |= (1 << localBitIndex);
|
||||
} else {
|
||||
segment.hi |= (1 << (localBitIndex - 32));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取扩展位(支持超过 64 位的索引)
|
||||
* @param mask 要检查的掩码
|
||||
* @param bitIndex 位索引(可以超过 63)
|
||||
* @returns 如果位被设置则返回 true
|
||||
*/
|
||||
public static getBitExtended(mask: BitMask64Data, bitIndex: number): boolean {
|
||||
if (bitIndex < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (bitIndex < 64) {
|
||||
const testMask = BitMask64Utils.create(bitIndex);
|
||||
return BitMask64Utils.hasAny(mask, testMask);
|
||||
}
|
||||
|
||||
if (!mask.segments) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const segmentIndex = Math.floor(bitIndex / 64) - 1;
|
||||
if (segmentIndex >= mask.segments.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const localBitIndex = bitIndex % 64;
|
||||
const segment = mask.segments[segmentIndex];
|
||||
|
||||
if (localBitIndex < 32) {
|
||||
return (segment.lo & (1 << localBitIndex)) !== 0;
|
||||
} else {
|
||||
return (segment.hi & (1 << (localBitIndex - 32))) !== 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除扩展位(支持超过 64 位的索引)
|
||||
* @param mask 要修改的掩码(原地修改)
|
||||
* @param bitIndex 位索引(可以超过 63)
|
||||
*/
|
||||
public static clearBitExtended(mask: BitMask64Data, bitIndex: number): void {
|
||||
if (bitIndex < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (bitIndex < 64) {
|
||||
BitMask64Utils.clearBit(mask, bitIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mask.segments) {
|
||||
return;
|
||||
}
|
||||
|
||||
const segmentIndex = Math.floor(bitIndex / 64) - 1;
|
||||
if (segmentIndex >= mask.segments.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const localBitIndex = bitIndex % 64;
|
||||
const segment = mask.segments[segmentIndex];
|
||||
|
||||
if (localBitIndex < 32) {
|
||||
segment.lo &= ~(1 << localBitIndex);
|
||||
} else {
|
||||
segment.hi &= ~(1 << (localBitIndex - 32));
|
||||
private static getSegmentByBitIndex(mask: BitMask64Data,bitIndex: number, createNewSegment: boolean = true): BitMask64Segment | null{
|
||||
if(bitIndex <= 63){
|
||||
// 基础位
|
||||
return mask.base;
|
||||
}else{
|
||||
// 扩展位
|
||||
let segments = mask.segments;
|
||||
if(!segments) {
|
||||
if(!createNewSegment) return null;
|
||||
segments = mask.segments = [];
|
||||
}
|
||||
const targetSegIndex = (bitIndex >> 6) - 1; // Math.floor(bitIndex / 64) - 1的位运算优化
|
||||
if(segments.length <= targetSegIndex){
|
||||
if(!createNewSegment) return null;
|
||||
const diff = targetSegIndex - segments.length + 1;
|
||||
for (let i = 0; i < diff; i++) {
|
||||
segments.push([0, 0]);
|
||||
}
|
||||
}
|
||||
return segments[targetSegIndex];
|
||||
}
|
||||
}
|
||||
}
|
||||
143
packages/core/src/ECS/Utils/BitMaskHashMap.ts
Normal file
143
packages/core/src/ECS/Utils/BitMaskHashMap.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { BitMask64Data } from "./BigIntCompatibility";
|
||||
|
||||
// FlatHashMapFast.ts
|
||||
|
||||
/**
|
||||
* 高性能 HashMap,使用BitMask64Data作为Key。内部计算两层哈希:
|
||||
* - primaryHash: MurmurHash3(seed1) => 定位 bucket
|
||||
* - secondaryHash: MurmurHash3(seed2) => 处理 bucket 内碰撞判定
|
||||
*
|
||||
* 理论上,在1e5数量数据规模下碰撞概率在数学意义上的可忽略。
|
||||
* 在本地测试中,一千万次连续/随机BitMask64Data生成未发生一级哈希冲突,考虑到使用场景(原型系统、组件系统等)远达不到此数量级,因此可安全用于生产环境。
|
||||
*/
|
||||
export class BitMaskHashMap<T> {
|
||||
private buckets: Map<number, [number, T][]> = new Map();
|
||||
private _size = 0;
|
||||
|
||||
constructor() {}
|
||||
|
||||
get size(): number {
|
||||
return this._size;
|
||||
}
|
||||
|
||||
get innerBuckets(): Map<number, [number, T][]> {
|
||||
return this.buckets;
|
||||
}
|
||||
/** MurmurHash3 (32bit) 简化实现 */
|
||||
private murmur32(key: BitMask64Data, seed: number): number {
|
||||
let h = seed >>> 0;
|
||||
const mix = (k: number) => {
|
||||
k = Math.imul(k, 0xcc9e2d51) >>> 0; // 第一个 32 位魔术常数
|
||||
k = (k << 15) | (k >>> 17);
|
||||
k = Math.imul(k, 0x1b873593) >>> 0; // 第二个 32 位魔术常数
|
||||
h ^= k;
|
||||
h = (h << 13) | (h >>> 19);
|
||||
h = (Math.imul(h, 5) + 0xe6546b64) >>> 0;
|
||||
};
|
||||
|
||||
// base
|
||||
mix(key.base[0] >>> 0);
|
||||
mix(key.base[1] >>> 0);
|
||||
|
||||
// segments
|
||||
if (key.segments) {
|
||||
for (const seg of key.segments) {
|
||||
mix(seg[0] >>> 0);
|
||||
mix(seg[1] >>> 0);
|
||||
}
|
||||
}
|
||||
|
||||
h ^= (key.segments ? key.segments.length * 8 : 8);
|
||||
h ^= h >>> 16;
|
||||
h = Math.imul(h, 0x85ebca6b) >>> 0;
|
||||
h ^= h >>> 13;
|
||||
h = Math.imul(h, 0xc2b2ae35) >>> 0;
|
||||
h ^= h >>> 16;
|
||||
return h >>> 0;
|
||||
}
|
||||
|
||||
/** primaryHash + secondaryHash 计算 */
|
||||
private getHashes(key: BitMask64Data): [number, number] {
|
||||
const primary = this.murmur32(key, 0x9747b28c); // seed1
|
||||
const secondary = this.murmur32(key, 0x12345678); // seed2
|
||||
return [primary, secondary];
|
||||
}
|
||||
|
||||
set(key: BitMask64Data, value: T): this {
|
||||
const [primary, secondary] = this.getHashes(key);
|
||||
let bucket = this.buckets.get(primary);
|
||||
if (!bucket) {
|
||||
bucket = [];
|
||||
this.buckets.set(primary, bucket);
|
||||
}
|
||||
|
||||
// 查找是否存在 secondaryHash
|
||||
for (let i = 0; i < bucket.length; i++) {
|
||||
if (bucket[i][0] === secondary) {
|
||||
bucket[i][1] = value;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
// 新增
|
||||
bucket.push([secondary, value]);
|
||||
this._size++;
|
||||
return this;
|
||||
}
|
||||
|
||||
get(key: BitMask64Data): T | undefined {
|
||||
const [primary, secondary] = this.getHashes(key);
|
||||
const bucket = this.buckets.get(primary);
|
||||
if (!bucket) return undefined;
|
||||
for (let i = 0; i < bucket.length; i++) {
|
||||
if (bucket[i][0] === secondary) {
|
||||
return bucket[i][1];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
has(key: BitMask64Data): boolean {
|
||||
return this.get(key) !== undefined;
|
||||
}
|
||||
|
||||
delete(key: BitMask64Data): boolean {
|
||||
const [primary, secondary] = this.getHashes(key);
|
||||
const bucket = this.buckets.get(primary);
|
||||
if (!bucket) return false;
|
||||
for (let i = 0; i < bucket.length; i++) {
|
||||
if (bucket[i][0] === secondary) {
|
||||
bucket.splice(i, 1);
|
||||
this._size--;
|
||||
if (bucket.length === 0) {
|
||||
this.buckets.delete(primary);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.buckets.clear();
|
||||
this._size = 0;
|
||||
}
|
||||
|
||||
*entries(): IterableIterator<[BitMask64Data, T]> {
|
||||
for (const [_, bucket] of this.buckets) {
|
||||
for (const [secondary, value] of bucket) {
|
||||
// 无法还原原始 key(只存二级 hash),所以 entries 返回不了 key
|
||||
yield [undefined as any, value];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*values(): IterableIterator<T> {
|
||||
for (const bucket of this.buckets.values()) {
|
||||
for (const [_, value] of bucket) {
|
||||
yield value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { BitMask64Data, BitMask64Utils } from './BigIntCompatibility';
|
||||
import { SegmentPart, BitMask64Data, BitMask64Utils } from './BigIntCompatibility';
|
||||
|
||||
/**
|
||||
* 位集合类,用于高效的位操作
|
||||
* 自动扩展支持:默认 64 位,超过时自动扩展到 128/256 位
|
||||
* 扩展模式性能略有下降,但仍然比数组遍历快得多
|
||||
* 支持任意位的位运算操作.
|
||||
*/
|
||||
export class Bits {
|
||||
/** 存储位数据的掩码,支持扩展 */
|
||||
@@ -37,7 +36,7 @@ export class Bits {
|
||||
throw new Error('Bit index cannot be negative');
|
||||
}
|
||||
|
||||
BitMask64Utils.setBitExtended(this._value, index);
|
||||
BitMask64Utils.setBit(this._value, index);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,7 +49,7 @@ export class Bits {
|
||||
throw new Error('Bit index cannot be negative');
|
||||
}
|
||||
|
||||
BitMask64Utils.clearBitExtended(this._value, index);
|
||||
BitMask64Utils.clearBit(this._value, index);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -59,7 +58,7 @@ export class Bits {
|
||||
* @returns 如果位被设置为1则返回true,否则返回false
|
||||
*/
|
||||
public get(index: number): boolean {
|
||||
return BitMask64Utils.getBitExtended(this._value, index);
|
||||
return BitMask64Utils.getBit(this._value, index);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -163,16 +162,16 @@ export class Bits {
|
||||
|
||||
if (maxBits <= 32) {
|
||||
const mask = (1 << maxBits) - 1;
|
||||
result._value.lo = (~result._value.lo) & mask;
|
||||
result._value.hi = 0;
|
||||
result._value.base[SegmentPart.LOW] = (~result._value.base[SegmentPart.LOW]) & mask;
|
||||
result._value.base[SegmentPart.HIGH] = 0;
|
||||
} else {
|
||||
result._value.lo = ~result._value.lo;
|
||||
result._value.base[SegmentPart.LOW] = ~result._value.base[SegmentPart.LOW];
|
||||
if (maxBits < 64) {
|
||||
const remainingBits = maxBits - 32;
|
||||
const mask = (1 << remainingBits) - 1;
|
||||
result._value.hi = (~result._value.hi) & mask;
|
||||
result._value.base[SegmentPart.HIGH] = (~result._value.base[SegmentPart.HIGH]) & mask;
|
||||
} else {
|
||||
result._value.hi = ~result._value.hi;
|
||||
result._value.base[SegmentPart.HIGH] = ~result._value.base[SegmentPart.HIGH];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,9 +236,10 @@ export class Bits {
|
||||
* @param maxBits 最大位数,默认为64
|
||||
* @returns 二进制字符串表示,每8位用空格分隔
|
||||
*/
|
||||
public toBinaryString(maxBits: number = 64): string {
|
||||
if (maxBits > 64) maxBits = 64;
|
||||
|
||||
public toBinaryString(maxBits: number = 0): string {
|
||||
if(maxBits == 0){
|
||||
maxBits = 64 + (this._value.segments ? this._value.segments.length * 64 : 0);
|
||||
}
|
||||
let result = '';
|
||||
for (let i = maxBits - 1; i >= 0; i--) {
|
||||
result += this.get(i) ? '1' : '0';
|
||||
@@ -265,16 +265,16 @@ export class Bits {
|
||||
*/
|
||||
public static fromBinaryString(binaryString: string): Bits {
|
||||
const cleanString = binaryString.replace(/\s/g, '');
|
||||
let data: BitMask64Data;
|
||||
let data: BitMask64Data = { base: undefined!, segments: undefined};
|
||||
if (cleanString.length <= 32) {
|
||||
const num = parseInt(cleanString, 2);
|
||||
data = { lo: num >>> 0, hi: 0 };
|
||||
data.base = [num >>> 0, 0];
|
||||
} else {
|
||||
const loBits = cleanString.substring(cleanString.length - 32);
|
||||
const hiBits = cleanString.substring(0, cleanString.length - 32);
|
||||
const lo = parseInt(loBits, 2);
|
||||
const hi = parseInt(hiBits, 2);
|
||||
data = { lo: lo >>> 0, hi: hi >>> 0 };
|
||||
data.base = [lo >>> 0, hi >>> 0];
|
||||
}
|
||||
return new Bits(data);
|
||||
}
|
||||
@@ -286,16 +286,16 @@ export class Bits {
|
||||
*/
|
||||
public static fromHexString(hexString: string): Bits {
|
||||
const cleanString = hexString.replace(/^0x/i, '');
|
||||
let data: BitMask64Data;
|
||||
let data: BitMask64Data = { base: undefined!, segments: undefined};
|
||||
if (cleanString.length <= 8) {
|
||||
const num = parseInt(cleanString, 16);
|
||||
data = { lo: num >>> 0, hi: 0 };
|
||||
data.base = [num >>> 0, 0];
|
||||
} else {
|
||||
const loBits = cleanString.substring(cleanString.length - 8);
|
||||
const hiBits = cleanString.substring(0, cleanString.length - 8);
|
||||
const lo = parseInt(loBits, 16);
|
||||
const hi = parseInt(hiBits, 16);
|
||||
data = { lo: lo >>> 0, hi: hi >>> 0 };
|
||||
data.base = [lo >>> 0, hi >>> 0];
|
||||
}
|
||||
return new Bits(data);
|
||||
}
|
||||
@@ -318,16 +318,16 @@ export class Bits {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (this._value.hi !== 0) {
|
||||
if (this._value.base[SegmentPart.HIGH] !== 0) {
|
||||
for (let i = 31; i >= 0; i--) {
|
||||
if ((this._value.hi & (1 << i)) !== 0) {
|
||||
if ((this._value.base[SegmentPart.HIGH] & (1 << i)) !== 0) {
|
||||
return i + 32;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 31; i >= 0; i--) {
|
||||
if ((this._value.lo & (1 << i)) !== 0) {
|
||||
if ((this._value.base[SegmentPart.LOW] & (1 << i)) !== 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
@@ -345,13 +345,13 @@ export class Bits {
|
||||
}
|
||||
|
||||
for (let i = 0; i < 32; i++) {
|
||||
if ((this._value.lo & (1 << i)) !== 0) {
|
||||
if ((this._value.base[SegmentPart.LOW] & (1 << i)) !== 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < 32; i++) {
|
||||
if ((this._value.hi & (1 << i)) !== 0) {
|
||||
if ((this._value.base[SegmentPart.HIGH] & (1 << i)) !== 0) {
|
||||
return i + 32;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
import { Bits } from './Bits';
|
||||
import { getComponentTypeName } from '../Decorators';
|
||||
import { ComponentType } from "../../Types";
|
||||
|
||||
/**
|
||||
* 组件类型管理器
|
||||
* 负责管理组件类型的注册和ID分配
|
||||
* 支持无限数量的组件类型(通过自动扩展 BitMask)
|
||||
*/
|
||||
export class ComponentTypeManager {
|
||||
private static _instance: ComponentTypeManager;
|
||||
private _componentTypes = new Map<Function, number>();
|
||||
private _typeNames = new Map<number, string>();
|
||||
private _nextTypeId = 0;
|
||||
|
||||
/**
|
||||
* 获取单例实例
|
||||
*/
|
||||
public static get instance(): ComponentTypeManager {
|
||||
if (!ComponentTypeManager._instance) {
|
||||
ComponentTypeManager._instance = new ComponentTypeManager();
|
||||
}
|
||||
return ComponentTypeManager._instance;
|
||||
}
|
||||
|
||||
private constructor() {}
|
||||
|
||||
/**
|
||||
* 获取组件类型的ID
|
||||
* @param componentType 组件类型构造函数
|
||||
* @returns 组件类型ID
|
||||
*/
|
||||
public getTypeId(componentType: ComponentType): number {
|
||||
let typeId = this._componentTypes.get(componentType);
|
||||
|
||||
if (typeId === undefined) {
|
||||
typeId = this._nextTypeId++;
|
||||
this._componentTypes.set(componentType, typeId);
|
||||
this._typeNames.set(typeId, getComponentTypeName(componentType));
|
||||
}
|
||||
|
||||
return typeId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取组件类型名称
|
||||
* @param typeId 组件类型ID
|
||||
* @returns 组件类型名称
|
||||
*/
|
||||
public getTypeName(typeId: number): string {
|
||||
return this._typeNames.get(typeId) || 'Unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建包含指定组件类型的Bits对象
|
||||
* @param componentTypes 组件类型构造函数数组
|
||||
* @returns Bits对象
|
||||
*/
|
||||
public createBits(...componentTypes: ComponentType[]): Bits {
|
||||
const bits = new Bits();
|
||||
|
||||
for (const componentType of componentTypes) {
|
||||
const typeId = this.getTypeId(componentType);
|
||||
bits.set(typeId);
|
||||
}
|
||||
|
||||
return bits;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取实体的组件位掩码
|
||||
* @param components 组件数组
|
||||
* @returns Bits对象
|
||||
*/
|
||||
public getEntityBits(components: ComponentType[]): Bits {
|
||||
const bits = new Bits();
|
||||
|
||||
for (const component of components) {
|
||||
const typeId = this.getTypeId(component);
|
||||
bits.set(typeId);
|
||||
}
|
||||
|
||||
return bits;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置管理器(主要用于测试)
|
||||
*/
|
||||
public reset(): void {
|
||||
this._componentTypes.clear();
|
||||
this._typeNames.clear();
|
||||
this._nextTypeId = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已注册的组件类型数量
|
||||
*/
|
||||
public get registeredTypeCount(): number {
|
||||
return this._componentTypes.size;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ export { EntityProcessorList } from './EntityProcessorList';
|
||||
export { IdentifierPool } from './IdentifierPool';
|
||||
export { Matcher } from './Matcher';
|
||||
export { Bits } from './Bits';
|
||||
export { ComponentTypeManager } from './ComponentTypeManager';
|
||||
export { BitMask64Utils, BitMask64Data } from './BigIntCompatibility';
|
||||
export { SparseSet } from './SparseSet';
|
||||
export { ComponentSparseSet } from './ComponentSparseSet';
|
||||
@@ -183,6 +183,17 @@ export class World {
|
||||
return Array.from(this._scenes.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除所有Scene
|
||||
*/
|
||||
public removeAllScenes(): void {
|
||||
const sceneIds = Array.from(this._scenes.keys());
|
||||
for (const sceneId of sceneIds) {
|
||||
this.removeScene(sceneId);
|
||||
}
|
||||
logger.info(`从World '${this.name}' 中移除所有Scene`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置Scene激活状态
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { World, IWorldConfig } from './World';
|
||||
import { createLogger } from '../Utils/Logger';
|
||||
import type { IService } from '../Core/ServiceContainer';
|
||||
|
||||
const logger = createLogger('WorldManager');
|
||||
|
||||
@@ -30,40 +31,45 @@ export interface IWorldManagerConfig {
|
||||
|
||||
/**
|
||||
* World管理器 - 管理所有World实例
|
||||
*
|
||||
* WorldManager是全局单例,负责管理所有World的生命周期。
|
||||
*
|
||||
* WorldManager负责管理多个独立的World实例。
|
||||
* 每个World都是独立的ECS环境,可以包含多个Scene。
|
||||
*
|
||||
* 设计理念:
|
||||
* - Core负责单Scene的传统ECS管理
|
||||
* - World负责多Scene的管理和协调
|
||||
* - WorldManager负责多World的全局管理
|
||||
*
|
||||
*
|
||||
* 适用场景:
|
||||
* - MMO游戏的多房间管理
|
||||
* - 服务器端的多游戏实例
|
||||
* - 需要完全隔离的多个游戏环境
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 获取全局WorldManager
|
||||
* const worldManager = WorldManager.getInstance();
|
||||
*
|
||||
* // 创建WorldManager实例
|
||||
* const worldManager = new WorldManager({
|
||||
* maxWorlds: 100,
|
||||
* autoCleanup: true
|
||||
* });
|
||||
*
|
||||
* // 创建游戏房间World
|
||||
* const roomWorld = worldManager.createWorld('room_001', {
|
||||
* const room1 = worldManager.createWorld('room_001', {
|
||||
* name: 'GameRoom_001',
|
||||
* maxScenes: 5
|
||||
* });
|
||||
*
|
||||
* // 在游戏循环中更新所有World
|
||||
* worldManager.updateAll(deltaTime);
|
||||
* room1.setActive(true);
|
||||
*
|
||||
* // 游戏循环
|
||||
* function gameLoop(deltaTime: number) {
|
||||
* Core.update(deltaTime);
|
||||
* worldManager.updateAll(); // 更新所有活跃World
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export class WorldManager {
|
||||
private static _instance: WorldManager | null = null;
|
||||
|
||||
export class WorldManager implements IService {
|
||||
private readonly _config: IWorldManagerConfig;
|
||||
private readonly _worlds: Map<string, World> = new Map();
|
||||
private readonly _activeWorlds: Set<string> = new Set();
|
||||
private _cleanupTimer: NodeJS.Timeout | null = null;
|
||||
private _cleanupTimer: ReturnType<typeof setInterval> | null = null;
|
||||
private _isRunning: boolean = false;
|
||||
|
||||
private constructor(config: IWorldManagerConfig = {}) {
|
||||
public constructor(config: IWorldManagerConfig = {}) {
|
||||
this._config = {
|
||||
maxWorlds: 50,
|
||||
autoCleanup: true,
|
||||
@@ -72,6 +78,9 @@ export class WorldManager {
|
||||
...config
|
||||
};
|
||||
|
||||
// 默认启动运行状态
|
||||
this._isRunning = true;
|
||||
|
||||
logger.info('WorldManager已初始化', {
|
||||
maxWorlds: this._config.maxWorlds,
|
||||
autoCleanup: this._config.autoCleanup,
|
||||
@@ -81,26 +90,6 @@ export class WorldManager {
|
||||
this.startCleanupTimer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取WorldManager单例实例
|
||||
*/
|
||||
public static getInstance(config?: IWorldManagerConfig): WorldManager {
|
||||
if (!this._instance) {
|
||||
this._instance = new WorldManager(config);
|
||||
}
|
||||
return this._instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置WorldManager实例(主要用于测试)
|
||||
*/
|
||||
public static reset(): void {
|
||||
if (this._instance) {
|
||||
this._instance.destroy();
|
||||
this._instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== World管理 =====
|
||||
|
||||
/**
|
||||
@@ -205,9 +194,37 @@ export class WorldManager {
|
||||
|
||||
// ===== 批量操作 =====
|
||||
|
||||
/**
|
||||
* 更新所有活跃的World
|
||||
*
|
||||
* 应该在每帧的游戏循环中调用。
|
||||
* 会自动更新所有活跃World的全局系统和场景。
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* function gameLoop(deltaTime: number) {
|
||||
* Core.update(deltaTime); // 更新全局服务
|
||||
* worldManager.updateAll(); // 更新所有World
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
public updateAll(): void {
|
||||
if (!this._isRunning) return;
|
||||
|
||||
for (const worldId of this._activeWorlds) {
|
||||
const world = this._worlds.get(worldId);
|
||||
if (world && world.isActive) {
|
||||
// 更新World的全局System
|
||||
world.updateGlobalSystems();
|
||||
|
||||
// 更新World中的所有Scene
|
||||
world.updateScenes();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有激活的World
|
||||
* 注意:此方法供Core.update()使用
|
||||
*/
|
||||
public getActiveWorlds(): World[] {
|
||||
const activeWorlds: World[] = [];
|
||||
@@ -371,6 +388,14 @@ export class WorldManager {
|
||||
logger.info('WorldManager已销毁');
|
||||
}
|
||||
|
||||
/**
|
||||
* 实现 IService 接口的 dispose 方法
|
||||
* 调用 destroy 方法进行清理
|
||||
*/
|
||||
public dispose(): void {
|
||||
this.destroy();
|
||||
}
|
||||
|
||||
// ===== 私有方法 =====
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,9 +6,15 @@ export * from './Utils';
|
||||
export * from './Decorators';
|
||||
export { Scene } from './Scene';
|
||||
export { IScene, ISceneFactory, ISceneConfig } from './IScene';
|
||||
export { SceneManager } from './SceneManager';
|
||||
export { World, IWorldConfig } from './World';
|
||||
export { WorldManager, IWorldManagerConfig } from './WorldManager';
|
||||
export * from './Core/Events';
|
||||
export * from './Core/Query';
|
||||
export * from './Core/Storage';
|
||||
export * from './Core/StorageDecorators';
|
||||
export * from './Core/StorageDecorators';
|
||||
export * from './Serialization';
|
||||
export { ReferenceTracker, getSceneByEntityId } from './Core/ReferenceTracker';
|
||||
export type { EntityRefRecord } from './Core/ReferenceTracker';
|
||||
export { ReactiveQuery, ReactiveQueryChangeType } from './Core/ReactiveQuery';
|
||||
export type { ReactiveQueryChange, ReactiveQueryListener, ReactiveQueryConfig } from './Core/ReactiveQuery';
|
||||
359
packages/core/src/Plugins/DebugPlugin.ts
Normal file
359
packages/core/src/Plugins/DebugPlugin.ts
Normal file
@@ -0,0 +1,359 @@
|
||||
import type { Core } from '../Core';
|
||||
import type { ServiceContainer } from '../Core/ServiceContainer';
|
||||
import { IPlugin } from '../Core/Plugin';
|
||||
import { createLogger } from '../Utils/Logger';
|
||||
import type { Scene } from '../ECS/Scene';
|
||||
import type { IScene } from '../ECS/IScene';
|
||||
import type { Entity } from '../ECS/Entity';
|
||||
import type { Component } from '../ECS/Component';
|
||||
import type { EntitySystem } from '../ECS/Systems/EntitySystem';
|
||||
import { WorldManager } from '../ECS/WorldManager';
|
||||
import { Injectable, Inject } from '../Core/DI/Decorators';
|
||||
import type { IService } from '../Core/ServiceContainer';
|
||||
import type { PerformanceData } from '../Utils/PerformanceMonitor';
|
||||
|
||||
const logger = createLogger('DebugPlugin');
|
||||
|
||||
/**
|
||||
* ECS 调试插件统计信息
|
||||
*/
|
||||
export interface ECSDebugStats {
|
||||
scenes: SceneDebugInfo[];
|
||||
totalEntities: number;
|
||||
totalSystems: number;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景调试信息
|
||||
*/
|
||||
export interface SceneDebugInfo {
|
||||
name: string;
|
||||
entityCount: number;
|
||||
systems: SystemDebugInfo[];
|
||||
entities: EntityDebugInfo[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统调试信息
|
||||
*/
|
||||
export interface SystemDebugInfo {
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
updateOrder: number;
|
||||
entityCount: number;
|
||||
performance?: {
|
||||
avgExecutionTime: number;
|
||||
maxExecutionTime: number;
|
||||
totalCalls: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 实体调试信息
|
||||
*/
|
||||
export interface EntityDebugInfo {
|
||||
id: number;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
tag: number;
|
||||
componentCount: number;
|
||||
components: ComponentDebugInfo[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件调试信息
|
||||
*/
|
||||
export interface ComponentDebugInfo {
|
||||
type: string;
|
||||
data: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* ECS 调试插件
|
||||
*
|
||||
* 提供运行时调试功能:
|
||||
* - 实时查看实体和组件信息
|
||||
* - System 执行统计
|
||||
* - 性能监控
|
||||
* - 实体查询
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const core = Core.create();
|
||||
* const debugPlugin = new DebugPlugin({ autoStart: true, updateInterval: 1000 });
|
||||
* await core.pluginManager.install(debugPlugin);
|
||||
*
|
||||
* // 获取调试信息
|
||||
* const stats = debugPlugin.getStats();
|
||||
* console.log('Total entities:', stats.totalEntities);
|
||||
*
|
||||
* // 查询实体
|
||||
* const entities = debugPlugin.queryEntities({ tag: 1 });
|
||||
* ```
|
||||
*/
|
||||
@Injectable()
|
||||
export class DebugPlugin implements IPlugin, IService {
|
||||
readonly name = '@esengine/debug-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
private worldManager: WorldManager | null = null;
|
||||
private updateInterval: number;
|
||||
private updateTimer: any = null;
|
||||
private autoStart: boolean;
|
||||
|
||||
/**
|
||||
* 创建调试插件实例
|
||||
*
|
||||
* @param options - 配置选项
|
||||
*/
|
||||
constructor(options?: { autoStart?: boolean; updateInterval?: number }) {
|
||||
this.autoStart = options?.autoStart ?? false;
|
||||
this.updateInterval = options?.updateInterval ?? 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* 安装插件
|
||||
*/
|
||||
async install(core: Core, services: ServiceContainer): Promise<void> {
|
||||
this.worldManager = services.resolve(WorldManager);
|
||||
|
||||
logger.info('ECS Debug Plugin installed');
|
||||
|
||||
if (this.autoStart) {
|
||||
this.start();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 卸载插件
|
||||
*/
|
||||
async uninstall(): Promise<void> {
|
||||
this.stop();
|
||||
this.worldManager = null;
|
||||
|
||||
logger.info('ECS Debug Plugin uninstalled');
|
||||
}
|
||||
|
||||
/**
|
||||
* 实现 IService 接口
|
||||
*/
|
||||
public dispose(): void {
|
||||
this.stop();
|
||||
this.worldManager = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动调试监控
|
||||
*/
|
||||
public start(): void {
|
||||
if (this.updateTimer) {
|
||||
logger.warn('Debug monitoring already started');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('Starting debug monitoring');
|
||||
|
||||
this.updateTimer = setInterval(() => {
|
||||
this.logStats();
|
||||
}, this.updateInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止调试监控
|
||||
*/
|
||||
public stop(): void {
|
||||
if (this.updateTimer) {
|
||||
clearInterval(this.updateTimer);
|
||||
this.updateTimer = null;
|
||||
logger.info('Debug monitoring stopped');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前 ECS 统计信息
|
||||
*/
|
||||
public getStats(): ECSDebugStats {
|
||||
if (!this.worldManager) {
|
||||
throw new Error('Plugin not installed');
|
||||
}
|
||||
|
||||
const scenes: SceneDebugInfo[] = [];
|
||||
let totalEntities = 0;
|
||||
let totalSystems = 0;
|
||||
|
||||
const worlds = this.worldManager.getAllWorlds();
|
||||
|
||||
for (const world of worlds) {
|
||||
for (const scene of world.getAllScenes()) {
|
||||
const sceneInfo = this.getSceneInfo(scene);
|
||||
scenes.push(sceneInfo);
|
||||
totalEntities += sceneInfo.entityCount;
|
||||
totalSystems += sceneInfo.systems.length;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
scenes,
|
||||
totalEntities,
|
||||
totalSystems,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取场景调试信息
|
||||
*/
|
||||
public getSceneInfo(scene: IScene): SceneDebugInfo {
|
||||
const entities = scene.entities.buffer;
|
||||
const systems = scene.systems;
|
||||
|
||||
return {
|
||||
name: scene.name,
|
||||
entityCount: entities.length,
|
||||
systems: systems.map(sys => this.getSystemInfo(sys)),
|
||||
entities: entities.map(entity => this.getEntityInfo(entity))
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统调试信息
|
||||
*/
|
||||
private getSystemInfo(system: EntitySystem): SystemDebugInfo {
|
||||
const perfStats = system.getPerformanceStats();
|
||||
|
||||
return {
|
||||
name: system.constructor.name,
|
||||
enabled: system.enabled,
|
||||
updateOrder: system.updateOrder,
|
||||
entityCount: system.entities.length,
|
||||
performance: perfStats ? {
|
||||
avgExecutionTime: perfStats.averageTime,
|
||||
maxExecutionTime: perfStats.maxTime,
|
||||
totalCalls: perfStats.executionCount
|
||||
} : undefined
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取实体调试信息
|
||||
*/
|
||||
public getEntityInfo(entity: Entity): EntityDebugInfo {
|
||||
const components = entity.components;
|
||||
|
||||
return {
|
||||
id: entity.id,
|
||||
name: entity.name,
|
||||
enabled: entity.enabled,
|
||||
tag: entity.tag,
|
||||
componentCount: components.length,
|
||||
components: components.map(comp => this.getComponentInfo(comp))
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取组件调试信息
|
||||
*/
|
||||
private getComponentInfo(component: any): ComponentDebugInfo {
|
||||
const type = component.constructor.name;
|
||||
const data: any = {};
|
||||
|
||||
for (const key of Object.keys(component)) {
|
||||
if (!key.startsWith('_')) {
|
||||
const value = component[key];
|
||||
if (typeof value !== 'function') {
|
||||
data[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { type, data };
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询实体
|
||||
*
|
||||
* @param filter - 查询过滤器
|
||||
*/
|
||||
public queryEntities(filter: {
|
||||
sceneId?: string;
|
||||
tag?: number;
|
||||
name?: string;
|
||||
hasComponent?: string;
|
||||
}): EntityDebugInfo[] {
|
||||
if (!this.worldManager) {
|
||||
throw new Error('Plugin not installed');
|
||||
}
|
||||
|
||||
const results: EntityDebugInfo[] = [];
|
||||
const worlds = this.worldManager.getAllWorlds();
|
||||
|
||||
for (const world of worlds) {
|
||||
for (const scene of world.getAllScenes()) {
|
||||
if (filter.sceneId && scene.name !== filter.sceneId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const entity of scene.entities.buffer) {
|
||||
if (filter.tag !== undefined && entity.tag !== filter.tag) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (filter.name && !entity.name.includes(filter.name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (filter.hasComponent) {
|
||||
const hasComp = entity.components.some(
|
||||
c => c.constructor.name === filter.hasComponent
|
||||
);
|
||||
if (!hasComp) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
results.push(this.getEntityInfo(entity));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 打印统计信息到日志
|
||||
*/
|
||||
private logStats(): void {
|
||||
const stats = this.getStats();
|
||||
|
||||
logger.info('=== ECS Debug Stats ===');
|
||||
logger.info(`Total Entities: ${stats.totalEntities}`);
|
||||
logger.info(`Total Systems: ${stats.totalSystems}`);
|
||||
logger.info(`Scenes: ${stats.scenes.length}`);
|
||||
|
||||
for (const scene of stats.scenes) {
|
||||
logger.info(`\n[Scene: ${scene.name}]`);
|
||||
logger.info(` Entities: ${scene.entityCount}`);
|
||||
logger.info(` Systems: ${scene.systems.length}`);
|
||||
|
||||
for (const system of scene.systems) {
|
||||
const perfStr = system.performance
|
||||
? ` | Avg: ${system.performance.avgExecutionTime.toFixed(2)}ms, Max: ${system.performance.maxExecutionTime.toFixed(2)}ms`
|
||||
: '';
|
||||
logger.info(
|
||||
` - ${system.name} (${system.enabled ? 'enabled' : 'disabled'}) | Entities: ${system.entityCount}${perfStr}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('========================\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出调试数据为 JSON
|
||||
*/
|
||||
public exportJSON(): string {
|
||||
const stats = this.getStats();
|
||||
return JSON.stringify(stats, null, 2);
|
||||
}
|
||||
}
|
||||
1
packages/core/src/Plugins/index.ts
Normal file
1
packages/core/src/Plugins/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './DebugPlugin';
|
||||
20
packages/core/src/Types/IUpdatable.ts
Normal file
20
packages/core/src/Types/IUpdatable.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* 可更新接口
|
||||
*
|
||||
* 实现此接口的服务将在每帧被Core自动调用update方法
|
||||
*/
|
||||
export interface IUpdatable {
|
||||
/**
|
||||
* 每帧更新方法
|
||||
*
|
||||
* @param deltaTime - 帧时间间隔(秒),可选参数
|
||||
*/
|
||||
update(deltaTime?: number): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查对象是否实现了IUpdatable接口
|
||||
*/
|
||||
export function isUpdatable(obj: any): obj is IUpdatable {
|
||||
return obj && typeof obj.update === 'function';
|
||||
}
|
||||
295
packages/core/src/Types/TypeHelpers.ts
Normal file
295
packages/core/src/Types/TypeHelpers.ts
Normal file
@@ -0,0 +1,295 @@
|
||||
/**
|
||||
* TypeScript类型工具集
|
||||
*
|
||||
* 提供高级类型推断和类型安全的工具类型
|
||||
*/
|
||||
|
||||
import type { IComponent } from './index';
|
||||
import { Component } from '../ECS/Component';
|
||||
|
||||
/**
|
||||
* 组件类型提取器
|
||||
* 从组件构造函数中提取实例类型
|
||||
*/
|
||||
export type ComponentInstance<T> = T extends new (...args: any[]) => infer R ? R : never;
|
||||
|
||||
/**
|
||||
* 组件构造函数类型
|
||||
*
|
||||
* 与 ComponentType 保持一致,避免类型转换
|
||||
*/
|
||||
export type ComponentConstructor<T extends IComponent = IComponent> = new (...args: any[]) => T;
|
||||
|
||||
/**
|
||||
* 组件类型的通用约束
|
||||
*
|
||||
* 用于确保类型参数是有效的组件构造函数
|
||||
*/
|
||||
export type AnyComponentConstructor = ComponentConstructor<any>;
|
||||
|
||||
/**
|
||||
* 多组件类型提取
|
||||
* 从组件构造函数数组中提取所有实例类型的联合
|
||||
*/
|
||||
export type ExtractComponents<T extends readonly ComponentConstructor[]> = {
|
||||
[K in keyof T]: ComponentInstance<T[K]>;
|
||||
};
|
||||
|
||||
/**
|
||||
* 组件类型映射
|
||||
* 将组件构造函数数组转换为实例类型的元组
|
||||
*/
|
||||
export type ComponentTypeMap<T extends readonly ComponentConstructor[]> = {
|
||||
[K in keyof T]: T[K] extends ComponentConstructor<infer C> ? C : never;
|
||||
};
|
||||
|
||||
/**
|
||||
* 实体with组件的类型
|
||||
* 表示一个实体确定拥有某些组件
|
||||
*/
|
||||
export interface EntityWithComponents<T extends readonly ComponentConstructor[]> {
|
||||
readonly id: number;
|
||||
readonly name: string;
|
||||
|
||||
/**
|
||||
* 类型安全的组件获取
|
||||
* 确保返回非空的组件实例
|
||||
*/
|
||||
getComponent<C extends ComponentConstructor>(componentType: C): ComponentInstance<C>;
|
||||
|
||||
/**
|
||||
* 检查是否拥有组件
|
||||
*/
|
||||
hasComponent<C extends ComponentConstructor>(componentType: C): boolean;
|
||||
|
||||
/**
|
||||
* 获取所有组件
|
||||
*/
|
||||
readonly components: ComponentTypeMap<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询结果类型
|
||||
* 根据查询条件推断实体必定拥有的组件
|
||||
*/
|
||||
export type QueryResult<
|
||||
All extends readonly ComponentConstructor[] = [],
|
||||
Any extends readonly ComponentConstructor[] = [],
|
||||
None extends readonly ComponentConstructor[] = []
|
||||
> = {
|
||||
/**
|
||||
* 实体列表,确保拥有All中的所有组件
|
||||
*/
|
||||
readonly entities: ReadonlyArray<EntityWithComponents<All>>;
|
||||
|
||||
/**
|
||||
* 实体数量
|
||||
*/
|
||||
readonly length: number;
|
||||
|
||||
/**
|
||||
* 遍历实体
|
||||
*/
|
||||
forEach(callback: (entity: EntityWithComponents<All>, index: number) => void): void;
|
||||
|
||||
/**
|
||||
* 映射转换
|
||||
*/
|
||||
map<R>(callback: (entity: EntityWithComponents<All>, index: number) => R): R[];
|
||||
|
||||
/**
|
||||
* 过滤实体
|
||||
*/
|
||||
filter(predicate: (entity: EntityWithComponents<All>, index: number) => boolean): QueryResult<All, Any, None>;
|
||||
};
|
||||
|
||||
/**
|
||||
* System处理的实体类型
|
||||
* 根据Matcher推断System处理的实体类型
|
||||
*/
|
||||
export type SystemEntityType<M> = M extends {
|
||||
getCondition(): {
|
||||
all: infer All extends readonly ComponentConstructor[];
|
||||
};
|
||||
}
|
||||
? EntityWithComponents<All>
|
||||
: never;
|
||||
|
||||
/**
|
||||
* 组件字段类型提取
|
||||
* 提取组件中所有可序列化的字段
|
||||
*/
|
||||
export type SerializableFields<T> = {
|
||||
[K in keyof T]: T[K] extends Function ? never : K;
|
||||
}[keyof T];
|
||||
|
||||
/**
|
||||
* 只读组件类型
|
||||
* 将组件的所有字段转为只读
|
||||
*/
|
||||
export type ReadonlyComponent<T extends IComponent> = {
|
||||
readonly [K in keyof T]: T[K];
|
||||
};
|
||||
|
||||
/**
|
||||
* 部分组件类型
|
||||
* 用于组件更新操作
|
||||
*/
|
||||
export type PartialComponent<T extends IComponent> = {
|
||||
[K in SerializableFields<T>]?: T[K];
|
||||
};
|
||||
|
||||
/**
|
||||
* 组件类型约束
|
||||
* 确保类型参数是有效的组件
|
||||
*/
|
||||
export type ValidComponent<T> = T extends Component ? T : never;
|
||||
|
||||
/**
|
||||
* 组件数组约束
|
||||
* 确保数组中的所有元素都是组件构造函数
|
||||
*/
|
||||
export type ValidComponentArray<T extends readonly any[]> = T extends readonly ComponentConstructor[]
|
||||
? T
|
||||
: never;
|
||||
|
||||
/**
|
||||
* 事件处理器类型
|
||||
* 提供类型安全的事件处理
|
||||
*/
|
||||
export type TypedEventHandler<T> = (data: T) => void | Promise<void>;
|
||||
|
||||
/**
|
||||
* 系统生命周期钩子类型
|
||||
*/
|
||||
export interface SystemLifecycleHooks<T extends readonly ComponentConstructor[]> {
|
||||
/**
|
||||
* 实体添加到系统时调用
|
||||
*/
|
||||
onAdded?: (entity: EntityWithComponents<T>) => void;
|
||||
|
||||
/**
|
||||
* 实体从系统移除时调用
|
||||
*/
|
||||
onRemoved?: (entity: EntityWithComponents<T>) => void;
|
||||
|
||||
/**
|
||||
* 系统初始化时调用
|
||||
*/
|
||||
onInitialize?: () => void;
|
||||
|
||||
/**
|
||||
* 系统销毁时调用
|
||||
*/
|
||||
onDestroy?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fluent API构建器类型
|
||||
*/
|
||||
export interface TypeSafeBuilder<T> {
|
||||
/**
|
||||
* 完成构建
|
||||
*/
|
||||
build(): T;
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件池类型
|
||||
*/
|
||||
export interface ComponentPool<T extends IComponent> {
|
||||
/**
|
||||
* 从池中获取组件实例
|
||||
*/
|
||||
obtain(): T;
|
||||
|
||||
/**
|
||||
* 归还组件到池中
|
||||
*/
|
||||
free(component: T): void;
|
||||
|
||||
/**
|
||||
* 清空池
|
||||
*/
|
||||
clear(): void;
|
||||
|
||||
/**
|
||||
* 池中可用对象数量
|
||||
*/
|
||||
readonly available: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 实体查询条件类型
|
||||
*/
|
||||
export interface TypedQueryCondition<
|
||||
All extends readonly ComponentConstructor[] = [],
|
||||
Any extends readonly ComponentConstructor[] = [],
|
||||
None extends readonly ComponentConstructor[] = []
|
||||
> {
|
||||
all: All;
|
||||
any: Any;
|
||||
none: None;
|
||||
tag?: number;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件类型守卫
|
||||
*/
|
||||
export function isComponentType<T extends IComponent>(
|
||||
value: any
|
||||
): value is ComponentConstructor<T> {
|
||||
return typeof value === 'function' && value.prototype instanceof Component;
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的组件数组守卫
|
||||
*/
|
||||
export function isComponentArray(
|
||||
value: any[]
|
||||
): value is ComponentConstructor[] {
|
||||
return value.every(isComponentType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取组件类型名称(编译时)
|
||||
*/
|
||||
export type ComponentTypeName<T extends ComponentConstructor> = T extends {
|
||||
prototype: { constructor: { name: infer N } };
|
||||
}
|
||||
? N
|
||||
: string;
|
||||
|
||||
/**
|
||||
* 多组件类型名称联合
|
||||
*/
|
||||
export type ComponentTypeNames<T extends readonly ComponentConstructor[]> = {
|
||||
[K in keyof T]: ComponentTypeName<T[K]>;
|
||||
}[number];
|
||||
|
||||
/**
|
||||
* 深度只读类型
|
||||
*/
|
||||
export type DeepReadonly<T> = {
|
||||
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
|
||||
};
|
||||
|
||||
/**
|
||||
* 深度可选类型
|
||||
*/
|
||||
export type DeepPartial<T> = {
|
||||
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
|
||||
};
|
||||
|
||||
/**
|
||||
* 排除方法的类型
|
||||
*/
|
||||
export type DataOnly<T> = {
|
||||
[K in keyof T as T[K] extends Function ? never : K]: T[K];
|
||||
};
|
||||
|
||||
/**
|
||||
* 可序列化的组件数据
|
||||
*/
|
||||
export type SerializableComponent<T extends IComponent> = DeepPartial<DataOnly<T>>;
|
||||
@@ -2,6 +2,10 @@
|
||||
* 框架核心类型定义
|
||||
*/
|
||||
|
||||
// 导出TypeScript类型增强工具
|
||||
export * from './TypeHelpers';
|
||||
export * from './IUpdatable';
|
||||
|
||||
/**
|
||||
* 组件接口
|
||||
*
|
||||
@@ -12,7 +16,7 @@ export interface IComponent {
|
||||
/** 组件唯一标识符 */
|
||||
readonly id: number;
|
||||
/** 组件所属的实体ID */
|
||||
entityId?: string | number;
|
||||
entityId: number | null;
|
||||
|
||||
/** 组件添加到实体时的回调 */
|
||||
onAddedToEntity(): void;
|
||||
|
||||
45
packages/core/src/Utils/Debug/DebugConfigService.ts
Normal file
45
packages/core/src/Utils/Debug/DebugConfigService.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { IECSDebugConfig } from '../../Types';
|
||||
import { Injectable } from '../../Core/DI/Decorators';
|
||||
import type { IService } from '../../Core/ServiceContainer';
|
||||
|
||||
/**
|
||||
* 调试配置服务
|
||||
*
|
||||
* 管理调试系统的配置信息
|
||||
*/
|
||||
@Injectable()
|
||||
export class DebugConfigService implements IService {
|
||||
private _config: IECSDebugConfig;
|
||||
|
||||
constructor() {
|
||||
this._config = {
|
||||
enabled: false,
|
||||
websocketUrl: '',
|
||||
debugFrameRate: 30,
|
||||
autoReconnect: true,
|
||||
channels: {
|
||||
entities: true,
|
||||
systems: true,
|
||||
performance: true,
|
||||
components: true,
|
||||
scenes: true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public setConfig(config: IECSDebugConfig): void {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
public getConfig(): IECSDebugConfig {
|
||||
return this._config;
|
||||
}
|
||||
|
||||
public isEnabled(): boolean {
|
||||
return this._config.enabled;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
// 清理资源
|
||||
}
|
||||
}
|
||||
@@ -9,13 +9,21 @@ import { Component } from '../../ECS/Component';
|
||||
import { ComponentPoolManager } from '../../ECS/Core/ComponentPool';
|
||||
import { Pool } from '../../Utils/Pool';
|
||||
import { getComponentInstanceTypeName, getSystemInstanceTypeName } from '../../ECS/Decorators';
|
||||
import type { IService } from '../../Core/ServiceContainer';
|
||||
import type { IUpdatable } from '../../Types/IUpdatable';
|
||||
import { SceneManager } from '../../ECS/SceneManager';
|
||||
import { PerformanceMonitor } from '../PerformanceMonitor';
|
||||
import { Injectable, Inject, Updatable } from '../../Core/DI/Decorators';
|
||||
import { DebugConfigService } from './DebugConfigService';
|
||||
|
||||
/**
|
||||
* 调试管理器
|
||||
*
|
||||
* 整合所有调试数据收集器,负责收集和发送调试数据
|
||||
*/
|
||||
export class DebugManager {
|
||||
@Injectable()
|
||||
@Updatable()
|
||||
export class DebugManager implements IService, IUpdatable {
|
||||
private config: IECSDebugConfig;
|
||||
private webSocketManager: WebSocketManager;
|
||||
private entityCollector: EntityDataCollector;
|
||||
@@ -23,25 +31,29 @@ export class DebugManager {
|
||||
private performanceCollector: PerformanceDataCollector;
|
||||
private componentCollector: ComponentDataCollector;
|
||||
private sceneCollector: SceneDataCollector;
|
||||
private sceneProvider: () => any;
|
||||
private performanceMonitorProvider: () => any;
|
||||
private sceneManager: SceneManager;
|
||||
private performanceMonitor: PerformanceMonitor;
|
||||
|
||||
private frameCounter: number = 0;
|
||||
private lastSendTime: number = 0;
|
||||
private sendInterval: number;
|
||||
private isRunning: boolean = false;
|
||||
private originalConsole = {
|
||||
log: console.log.bind(console),
|
||||
debug: console.debug.bind(console),
|
||||
info: console.info.bind(console),
|
||||
warn: console.warn.bind(console),
|
||||
error: console.error.bind(console)
|
||||
};
|
||||
|
||||
/**
|
||||
* 构造调试管理器
|
||||
* @param core Core实例
|
||||
* @param config 调试配置
|
||||
*/
|
||||
constructor(core: any, config: IECSDebugConfig) {
|
||||
this.config = config;
|
||||
|
||||
// 设置提供器函数
|
||||
this.sceneProvider = () => (core as any).scene || (core.constructor as any).scene;
|
||||
this.performanceMonitorProvider = () => core._performanceMonitor;
|
||||
constructor(
|
||||
@Inject(SceneManager) sceneManager: SceneManager,
|
||||
@Inject(PerformanceMonitor) performanceMonitor: PerformanceMonitor,
|
||||
@Inject(DebugConfigService) configService: DebugConfigService
|
||||
) {
|
||||
this.config = configService.getConfig();
|
||||
this.sceneManager = sceneManager;
|
||||
this.performanceMonitor = performanceMonitor;
|
||||
|
||||
// 初始化数据收集器
|
||||
this.entityCollector = new EntityDataCollector();
|
||||
@@ -52,17 +64,20 @@ export class DebugManager {
|
||||
|
||||
// 初始化WebSocket管理器
|
||||
this.webSocketManager = new WebSocketManager(
|
||||
config.websocketUrl,
|
||||
config.autoReconnect !== false
|
||||
this.config.websocketUrl,
|
||||
this.config.autoReconnect !== false
|
||||
);
|
||||
|
||||
// 设置消息处理回调
|
||||
this.webSocketManager.setMessageHandler(this.handleMessage.bind(this));
|
||||
|
||||
// 计算发送间隔(基于帧率)
|
||||
const debugFrameRate = config.debugFrameRate || 30;
|
||||
const debugFrameRate = this.config.debugFrameRate || 30;
|
||||
this.sendInterval = 1000 / debugFrameRate;
|
||||
|
||||
// 拦截 console 日志
|
||||
this.interceptConsole();
|
||||
|
||||
this.start();
|
||||
}
|
||||
|
||||
@@ -86,6 +101,118 @@ export class DebugManager {
|
||||
this.webSocketManager.disconnect();
|
||||
}
|
||||
|
||||
/**
|
||||
* 拦截 console 日志并转发到编辑器
|
||||
*/
|
||||
private interceptConsole(): void {
|
||||
console.log = (...args: unknown[]) => {
|
||||
this.sendLog('info', this.formatLogMessage(args));
|
||||
this.originalConsole.log(...args);
|
||||
};
|
||||
|
||||
console.debug = (...args: unknown[]) => {
|
||||
this.sendLog('debug', this.formatLogMessage(args));
|
||||
this.originalConsole.debug(...args);
|
||||
};
|
||||
|
||||
console.info = (...args: unknown[]) => {
|
||||
this.sendLog('info', this.formatLogMessage(args));
|
||||
this.originalConsole.info(...args);
|
||||
};
|
||||
|
||||
console.warn = (...args: unknown[]) => {
|
||||
this.sendLog('warn', this.formatLogMessage(args));
|
||||
this.originalConsole.warn(...args);
|
||||
};
|
||||
|
||||
console.error = (...args: unknown[]) => {
|
||||
this.sendLog('error', this.formatLogMessage(args));
|
||||
this.originalConsole.error(...args);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日志消息
|
||||
*/
|
||||
private formatLogMessage(args: unknown[]): string {
|
||||
return args.map(arg => {
|
||||
if (typeof arg === 'string') return arg;
|
||||
if (arg instanceof Error) return `${arg.name}: ${arg.message}`;
|
||||
if (arg === null) return 'null';
|
||||
if (arg === undefined) return 'undefined';
|
||||
if (typeof arg === 'object') {
|
||||
try {
|
||||
return this.safeStringify(arg, 6);
|
||||
} catch {
|
||||
return Object.prototype.toString.call(arg);
|
||||
}
|
||||
}
|
||||
return String(arg);
|
||||
}).join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全的 JSON 序列化,支持循环引用和深度限制
|
||||
*/
|
||||
private safeStringify(obj: any, maxDepth: number = 6): string {
|
||||
const seen = new WeakSet();
|
||||
|
||||
const stringify = (value: any, depth: number): any => {
|
||||
if (value === null) return null;
|
||||
if (value === undefined) return undefined;
|
||||
if (typeof value !== 'object') return value;
|
||||
|
||||
if (depth >= maxDepth) {
|
||||
return '[Max Depth Reached]';
|
||||
}
|
||||
|
||||
if (seen.has(value)) {
|
||||
return '[Circular]';
|
||||
}
|
||||
|
||||
seen.add(value);
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
const result = value.map(item => stringify(item, depth + 1));
|
||||
seen.delete(value);
|
||||
return result;
|
||||
}
|
||||
|
||||
const result: any = {};
|
||||
for (const key in value) {
|
||||
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
||||
result[key] = stringify(value[key], depth + 1);
|
||||
}
|
||||
}
|
||||
seen.delete(value);
|
||||
return result;
|
||||
};
|
||||
|
||||
return JSON.stringify(stringify(obj, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送日志到编辑器
|
||||
*/
|
||||
private sendLog(level: string, message: string): void {
|
||||
if (!this.webSocketManager.getConnectionStatus()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.webSocketManager.send({
|
||||
type: 'log',
|
||||
data: {
|
||||
level,
|
||||
message,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
// 静默失败,避免递归日志
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新配置
|
||||
*/
|
||||
@@ -108,16 +235,12 @@ export class DebugManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 帧更新回调
|
||||
*/
|
||||
public onFrameUpdate(deltaTime: number): void {
|
||||
public update(deltaTime?: number): void {
|
||||
if (!this.isRunning || !this.config.enabled) return;
|
||||
|
||||
this.frameCounter++;
|
||||
const currentTime = Date.now();
|
||||
|
||||
// 基于配置的帧率发送数据
|
||||
if (currentTime - this.lastSendTime >= this.sendInterval) {
|
||||
this.sendDebugData();
|
||||
this.lastSendTime = currentTime;
|
||||
@@ -205,7 +328,8 @@ export class DebugManager {
|
||||
return;
|
||||
}
|
||||
|
||||
const expandedData = this.entityCollector.expandLazyObject(entityId, componentIndex, propertyPath);
|
||||
const scene = this.sceneManager.currentScene;
|
||||
const expandedData = this.entityCollector.expandLazyObject(entityId, componentIndex, propertyPath, scene);
|
||||
|
||||
this.webSocketManager.send({
|
||||
type: 'expand_lazy_object_response',
|
||||
@@ -237,7 +361,8 @@ export class DebugManager {
|
||||
return;
|
||||
}
|
||||
|
||||
const properties = this.entityCollector.getComponentProperties(entityId, componentIndex);
|
||||
const scene = this.sceneManager.currentScene;
|
||||
const properties = this.entityCollector.getComponentProperties(entityId, componentIndex, scene);
|
||||
|
||||
this.webSocketManager.send({
|
||||
type: 'get_component_properties_response',
|
||||
@@ -260,7 +385,8 @@ export class DebugManager {
|
||||
try {
|
||||
const { requestId } = message;
|
||||
|
||||
const rawEntityList = this.entityCollector.getRawEntityList();
|
||||
const scene = this.sceneManager.currentScene;
|
||||
const rawEntityList = this.entityCollector.getRawEntityList(scene);
|
||||
|
||||
this.webSocketManager.send({
|
||||
type: 'get_raw_entity_list_response',
|
||||
@@ -292,7 +418,8 @@ export class DebugManager {
|
||||
return;
|
||||
}
|
||||
|
||||
const entityDetails = this.entityCollector.getEntityDetails(entityId);
|
||||
const scene = this.sceneManager.currentScene;
|
||||
const entityDetails = this.entityCollector.getEntityDetails(entityId, scene);
|
||||
|
||||
this.webSocketManager.send({
|
||||
type: 'get_entity_details_response',
|
||||
@@ -338,7 +465,7 @@ export class DebugManager {
|
||||
|
||||
// 收集其他内存统计
|
||||
const baseMemoryInfo = this.collectBaseMemoryInfo();
|
||||
const scene = this.sceneProvider();
|
||||
const scene = this.sceneManager.currentScene;
|
||||
|
||||
// 使用专门的内存计算方法收集实体数据
|
||||
const entityData = this.entityCollector.collectEntityDataWithMemory(scene);
|
||||
@@ -452,7 +579,7 @@ export class DebugManager {
|
||||
/**
|
||||
* 收集组件内存统计(仅用于内存快照)
|
||||
*/
|
||||
private collectComponentMemoryStats(entityList: { buffer: Array<{ id: number; name?: string; destroyed?: boolean; components?: Component[] }> }): {
|
||||
private collectComponentMemoryStats(entityList: { buffer: Array<{ id: number; name?: string; destroyed?: boolean; components?: readonly Component[] }> }): {
|
||||
totalMemory: number;
|
||||
componentTypes: number;
|
||||
totalInstances: number;
|
||||
@@ -546,7 +673,7 @@ export class DebugManager {
|
||||
updateOrder: number;
|
||||
}>;
|
||||
} {
|
||||
const scene = this.sceneProvider();
|
||||
const scene = this.sceneManager.currentScene;
|
||||
let totalSystemMemory = 0;
|
||||
const systemBreakdown: Array<{
|
||||
name: string;
|
||||
@@ -556,11 +683,11 @@ export class DebugManager {
|
||||
}> = [];
|
||||
|
||||
try {
|
||||
const entityProcessors = scene?.entityProcessors;
|
||||
if (entityProcessors && entityProcessors.processors) {
|
||||
const systems = scene?.systems;
|
||||
if (systems) {
|
||||
const systemTypeMemoryCache = new Map<string, number>();
|
||||
|
||||
for (const system of entityProcessors.processors) {
|
||||
for (const system of systems) {
|
||||
const systemTypeName = getSystemInstanceTypeName(system);
|
||||
|
||||
let systemMemory: number;
|
||||
@@ -720,16 +847,15 @@ export class DebugManager {
|
||||
error?: string;
|
||||
} {
|
||||
try {
|
||||
const performanceMonitor = this.performanceMonitorProvider();
|
||||
if (!performanceMonitor) {
|
||||
if (!this.performanceMonitor) {
|
||||
return { enabled: false };
|
||||
}
|
||||
|
||||
const stats = performanceMonitor.getAllSystemStats();
|
||||
const warnings = performanceMonitor.getPerformanceWarnings();
|
||||
const stats = this.performanceMonitor.getAllSystemStats();
|
||||
const warnings = this.performanceMonitor.getPerformanceWarnings();
|
||||
|
||||
return {
|
||||
enabled: (performanceMonitor as { enabled?: boolean }).enabled ?? false,
|
||||
enabled: (this.performanceMonitor as { enabled?: boolean }).enabled ?? false,
|
||||
systemCount: stats.size,
|
||||
warnings: warnings.slice(0, 10), // 最多10个警告
|
||||
topSystems: Array.from(stats.entries()).map((entry) => {
|
||||
@@ -753,7 +879,7 @@ export class DebugManager {
|
||||
*/
|
||||
public getDebugData(): IECSDebugData {
|
||||
const currentTime = Date.now();
|
||||
const scene = this.sceneProvider();
|
||||
const scene = this.sceneManager.currentScene;
|
||||
|
||||
const debugData: IECSDebugData = {
|
||||
timestamp: currentTime,
|
||||
@@ -769,13 +895,11 @@ export class DebugManager {
|
||||
}
|
||||
|
||||
if (this.config.channels.systems) {
|
||||
const performanceMonitor = this.performanceMonitorProvider();
|
||||
debugData.systems = this.systemCollector.collectSystemData(performanceMonitor, scene);
|
||||
debugData.systems = this.systemCollector.collectSystemData(this.performanceMonitor, scene);
|
||||
}
|
||||
|
||||
if (this.config.channels.performance) {
|
||||
const performanceMonitor = this.performanceMonitorProvider();
|
||||
debugData.performance = this.performanceCollector.collectPerformanceData(performanceMonitor);
|
||||
debugData.performance = this.performanceCollector.collectPerformanceData(this.performanceMonitor);
|
||||
}
|
||||
|
||||
if (this.config.channels.components) {
|
||||
@@ -821,4 +945,18 @@ export class DebugManager {
|
||||
// console.error('[ECS Debug] 发送调试数据失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源
|
||||
*/
|
||||
public dispose(): void {
|
||||
this.stop();
|
||||
|
||||
// 恢复原始 console 方法
|
||||
console.log = this.originalConsole.log;
|
||||
console.debug = this.originalConsole.debug;
|
||||
console.info = this.originalConsole.info;
|
||||
console.warn = this.originalConsole.warn;
|
||||
console.error = this.originalConsole.error;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { IEntityDebugData } from '../../Types';
|
||||
import { Entity } from '../../ECS/Entity';
|
||||
import { Component } from '../../ECS/Component';
|
||||
import { ComponentTypeManager } from '../../ECS/Utils/ComponentTypeManager';
|
||||
import { getComponentInstanceTypeName, getSystemInstanceTypeName } from '../../ECS/Decorators';
|
||||
import { IScene } from '../../ECS/IScene';
|
||||
|
||||
@@ -264,8 +263,7 @@ export class EntityDataCollector {
|
||||
componentCount: entity.components?.length || 0,
|
||||
memory: 0
|
||||
}))
|
||||
.sort((a: any, b: any) => b.componentCount - a.componentCount)
|
||||
.slice(0, 10);
|
||||
.sort((a: any, b: any) => b.componentCount - a.componentCount);
|
||||
}
|
||||
|
||||
|
||||
@@ -304,7 +302,7 @@ export class EntityDataCollector {
|
||||
});
|
||||
|
||||
if (archetype.entities) {
|
||||
archetype.entities.slice(0, 5).forEach((entity: any) => {
|
||||
archetype.entities.forEach((entity: any) => {
|
||||
topEntities.push({
|
||||
id: entity.id.toString(),
|
||||
name: entity.name || `Entity_${entity.id}`,
|
||||
@@ -353,7 +351,7 @@ export class EntityDataCollector {
|
||||
});
|
||||
|
||||
if (archetype.entities) {
|
||||
archetype.entities.slice(0, 5).forEach((entity: any) => {
|
||||
archetype.entities.forEach((entity: any) => {
|
||||
topEntities.push({
|
||||
id: entity.id.toString(),
|
||||
name: entity.name || `Entity_${entity.id}`,
|
||||
@@ -722,20 +720,7 @@ export class EntityDataCollector {
|
||||
properties: Record<string, any>;
|
||||
}> {
|
||||
return components.map((component: Component) => {
|
||||
let typeName = getComponentInstanceTypeName(component);
|
||||
|
||||
if (!typeName || typeName === 'Object' || typeName === 'Function') {
|
||||
try {
|
||||
const typeManager = ComponentTypeManager.instance;
|
||||
const componentType = component.constructor as any;
|
||||
const typeId = typeManager.getTypeId(componentType);
|
||||
typeName = typeManager.getTypeName(typeId);
|
||||
} catch (error) {
|
||||
typeName = 'UnknownComponent';
|
||||
}
|
||||
}
|
||||
|
||||
// 提取实际的组件属性
|
||||
const typeName = getComponentInstanceTypeName(component);
|
||||
const properties: Record<string, any> = {};
|
||||
|
||||
try {
|
||||
|
||||
@@ -9,7 +9,7 @@ export class WebSocketManager {
|
||||
private reconnectInterval: number = 2000;
|
||||
private url: string;
|
||||
private autoReconnect: boolean;
|
||||
private reconnectTimer?: NodeJS.Timeout;
|
||||
private reconnectTimer?: ReturnType<typeof setTimeout>;
|
||||
private onOpen?: (event: Event) => void;
|
||||
private onClose?: (event: CloseEvent) => void;
|
||||
private onError?: (error: Event | any) => void;
|
||||
|
||||
@@ -4,4 +4,5 @@ export { PerformanceDataCollector } from './PerformanceDataCollector';
|
||||
export { ComponentDataCollector } from './ComponentDataCollector';
|
||||
export { SceneDataCollector } from './SceneDataCollector';
|
||||
export { WebSocketManager } from './WebSocketManager';
|
||||
export { DebugManager } from './DebugManager';
|
||||
export { DebugManager } from './DebugManager';
|
||||
export { DebugConfigService } from './DebugConfigService';
|
||||
@@ -99,20 +99,20 @@ export interface PerformanceThresholds {
|
||||
};
|
||||
}
|
||||
|
||||
import type { IService } from '../Core/ServiceContainer';
|
||||
|
||||
/**
|
||||
* 高性能监控器
|
||||
* 用于监控ECS系统的性能表现,提供详细的分析和优化建议
|
||||
*/
|
||||
export class PerformanceMonitor {
|
||||
private static _instance: PerformanceMonitor;
|
||||
|
||||
export class PerformanceMonitor implements IService {
|
||||
private _systemData = new Map<string, PerformanceData>();
|
||||
private _systemStats = new Map<string, PerformanceStats>();
|
||||
private _warnings: PerformanceWarning[] = [];
|
||||
private _isEnabled = false;
|
||||
private _maxRecentSamples = 60; // 保留最近60帧的数据
|
||||
private _maxWarnings = 100; // 最大警告数量
|
||||
|
||||
|
||||
// 性能阈值配置
|
||||
private _thresholds: PerformanceThresholds = {
|
||||
executionTime: { warning: 16.67, critical: 33.33 }, // 60fps和30fps对应的帧时间
|
||||
@@ -139,18 +139,8 @@ export class PerformanceMonitor {
|
||||
private _gcCount = 0;
|
||||
private _lastGcCheck = 0;
|
||||
private _gcCheckInterval = 1000;
|
||||
|
||||
/**
|
||||
* 获取单例实例
|
||||
*/
|
||||
public static get instance(): PerformanceMonitor {
|
||||
if (!PerformanceMonitor._instance) {
|
||||
PerformanceMonitor._instance = new PerformanceMonitor();
|
||||
}
|
||||
return PerformanceMonitor._instance;
|
||||
}
|
||||
|
||||
private constructor() {}
|
||||
constructor() {}
|
||||
|
||||
/**
|
||||
* 启用性能监控
|
||||
@@ -392,7 +382,7 @@ export class PerformanceMonitor {
|
||||
*/
|
||||
public setMaxRecentSamples(maxSamples: number): void {
|
||||
this._maxRecentSamples = maxSamples;
|
||||
|
||||
|
||||
// 裁剪现有数据
|
||||
for (const stats of this._systemStats.values()) {
|
||||
while (stats.recentTimes.length > maxSamples) {
|
||||
@@ -400,4 +390,16 @@ export class PerformanceMonitor {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源
|
||||
*/
|
||||
public dispose(): void {
|
||||
this._systemData.clear();
|
||||
this._systemStats.clear();
|
||||
this._warnings = [];
|
||||
this._fpsHistory = [];
|
||||
this._memoryHistory = [];
|
||||
this._isEnabled = false;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,18 @@
|
||||
import { IPoolable, PoolStats } from './IPoolable';
|
||||
import { Pool } from './Pool';
|
||||
import type { IService } from '../../Core/ServiceContainer';
|
||||
|
||||
/**
|
||||
* 池管理器
|
||||
* 统一管理所有对象池
|
||||
*/
|
||||
export class PoolManager {
|
||||
private static instance: PoolManager;
|
||||
export class PoolManager implements IService {
|
||||
private pools = new Map<string, Pool<any>>();
|
||||
private autoCompactInterval = 60000; // 60秒
|
||||
private lastCompactTime = 0;
|
||||
|
||||
public static getInstance(): PoolManager {
|
||||
if (!PoolManager.instance) {
|
||||
PoolManager.instance = new PoolManager();
|
||||
}
|
||||
return PoolManager.instance;
|
||||
constructor() {
|
||||
// 普通构造函数,不再使用单例模式
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -228,4 +225,12 @@ export class PoolManager {
|
||||
this.pools.clear();
|
||||
this.lastCompactTime = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源
|
||||
* 实现 IService 接口
|
||||
*/
|
||||
public dispose(): void {
|
||||
this.reset();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user