Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3c7c3c98af | |||
| be7b3afb4a | |||
| 3e037f4ae0 |
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"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": "error",
|
||||
"@typescript-eslint/no-unsafe-assignment": "warn",
|
||||
"@typescript-eslint/no-unsafe-member-access": "warn",
|
||||
"@typescript-eslint/no-unsafe-call": "warn",
|
||||
"@typescript-eslint/no-unsafe-return": "warn",
|
||||
"@typescript-eslint/no-unsafe-argument": "warn",
|
||||
"@typescript-eslint/no-unsafe-function-type": "error",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-unnecessary-type-assertion": "error",
|
||||
"@typescript-eslint/naming-convention": [
|
||||
"error",
|
||||
{
|
||||
"selector": "memberLike",
|
||||
"modifiers": ["private"],
|
||||
"format": ["camelCase"],
|
||||
"leadingUnderscore": "require"
|
||||
},
|
||||
{
|
||||
"selector": "memberLike",
|
||||
"modifiers": ["public"],
|
||||
"format": ["camelCase"],
|
||||
"leadingUnderscore": "forbid"
|
||||
},
|
||||
{
|
||||
"selector": "memberLike",
|
||||
"modifiers": ["protected"],
|
||||
"format": ["camelCase"],
|
||||
"leadingUnderscore": "require"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"node_modules/",
|
||||
"dist/",
|
||||
"bin/",
|
||||
"build/",
|
||||
"coverage/",
|
||||
"thirdparty/",
|
||||
"examples/lawn-mower-demo/",
|
||||
"extensions/",
|
||||
"*.min.js",
|
||||
"*.d.ts"
|
||||
]
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
name: "CodeQL Config"
|
||||
|
||||
# Paths to exclude from analysis
|
||||
paths-ignore:
|
||||
- thirdparty
|
||||
- "**/node_modules"
|
||||
- "**/dist"
|
||||
- "**/bin"
|
||||
+37
-74
@@ -6,9 +6,8 @@ on:
|
||||
paths:
|
||||
- 'packages/**'
|
||||
- 'package.json'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'package-lock.json'
|
||||
- 'tsconfig.json'
|
||||
- 'turbo.json'
|
||||
- 'jest.config.*'
|
||||
- '.github/workflows/ci.yml'
|
||||
pull_request:
|
||||
@@ -16,112 +15,76 @@ on:
|
||||
paths:
|
||||
- 'packages/**'
|
||||
- 'package.json'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'package-lock.json'
|
||||
- 'tsconfig.json'
|
||||
- 'turbo.json'
|
||||
- 'jest.config.*'
|
||||
- '.github/workflows/ci.yml'
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: wasm32-unknown-unknown
|
||||
|
||||
# 缓存 Rust 编译结果
|
||||
- name: Cache Rust dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: packages/engine
|
||||
cache-on-failure: true
|
||||
|
||||
# 缓存 wasm-pack
|
||||
- name: Cache wasm-pack
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cargo/bin/wasm-pack
|
||||
key: wasm-pack-${{ runner.os }}
|
||||
|
||||
- name: Install wasm-pack
|
||||
run: |
|
||||
if ! command -v wasm-pack &> /dev/null; then
|
||||
cargo install wasm-pack
|
||||
fi
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
run: npm ci
|
||||
|
||||
# 缓存 Turbo
|
||||
- name: Cache Turbo
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .turbo
|
||||
key: turbo-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
turbo-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
|
||||
turbo-${{ runner.os }}-
|
||||
|
||||
# 构建所有包
|
||||
- name: Build all packages
|
||||
run: pnpm run build
|
||||
|
||||
- name: Copy WASM files to ecs-engine-bindgen
|
||||
run: |
|
||||
mkdir -p packages/ecs-engine-bindgen/src/wasm
|
||||
cp packages/engine/pkg/es_engine.js packages/ecs-engine-bindgen/src/wasm/
|
||||
cp packages/engine/pkg/es_engine.d.ts packages/ecs-engine-bindgen/src/wasm/
|
||||
cp packages/engine/pkg/es_engine_bg.wasm packages/ecs-engine-bindgen/src/wasm/
|
||||
cp packages/engine/pkg/es_engine_bg.wasm.d.ts packages/ecs-engine-bindgen/src/wasm/
|
||||
|
||||
# 类型检查
|
||||
- name: Type check
|
||||
run: pnpm run type-check
|
||||
run: npm run type-check
|
||||
|
||||
# Lint 检查
|
||||
- name: Lint check
|
||||
run: pnpm run lint
|
||||
run: npm run lint
|
||||
|
||||
- name: Build core package first
|
||||
run: npm run build:core
|
||||
|
||||
# 测试
|
||||
- name: Run tests with coverage
|
||||
run: pnpm run test:ci
|
||||
run: npm run test:ci
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
continue-on-error: true
|
||||
with:
|
||||
file: ./coverage/lcov.info
|
||||
flags: unittests
|
||||
name: codecov-umbrella
|
||||
fail_ci_if_error: false
|
||||
|
||||
# 构建 npm 包
|
||||
- name: Build npm packages
|
||||
run: pnpm run build:npm
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
|
||||
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 dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build project
|
||||
run: npm run build
|
||||
|
||||
- name: Build npm package
|
||||
run: npm run build:npm
|
||||
|
||||
# 上传构建产物
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-artifacts
|
||||
path: |
|
||||
packages/*/dist/
|
||||
packages/*/bin/
|
||||
retention-days: 7
|
||||
bin/
|
||||
dist/
|
||||
retention-days: 7
|
||||
@@ -14,34 +14,28 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
cache: 'pnpm'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
run: npm ci
|
||||
|
||||
- name: Run tests with coverage
|
||||
run: |
|
||||
cd packages/core
|
||||
pnpm run test:coverage
|
||||
npm run test:coverage
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
continue-on-error: true
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./packages/core/coverage/coverage-final.json
|
||||
flags: core
|
||||
name: core-coverage
|
||||
fail_ci_if_error: false
|
||||
fail_ci_if_error: true
|
||||
verbose: true
|
||||
|
||||
- name: Upload coverage artifact
|
||||
|
||||
@@ -31,7 +31,6 @@ jobs:
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: security-and-quality
|
||||
config-file: ./.github/codeql/codeql-config.yml
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
@@ -17,20 +17,15 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
cache: 'pnpm'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install commitlint
|
||||
run: |
|
||||
pnpm add -D @commitlint/config-conventional @commitlint/cli
|
||||
npm install --save-dev @commitlint/config-conventional @commitlint/cli
|
||||
|
||||
- name: Validate PR commits
|
||||
run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose
|
||||
|
||||
@@ -29,31 +29,26 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
cache: 'pnpm'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
run: npm ci
|
||||
|
||||
- name: Build core package
|
||||
run: pnpm run build:core
|
||||
run: npm run build:core
|
||||
|
||||
- name: Generate API documentation
|
||||
run: pnpm run docs:api
|
||||
run: npm run docs:api
|
||||
|
||||
- name: Build documentation
|
||||
run: pnpm run docs:build
|
||||
run: npm run docs:build
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
|
||||
@@ -33,16 +33,11 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
cache: 'pnpm'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -62,36 +57,39 @@ jobs:
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: pnpm install
|
||||
run: npm ci
|
||||
|
||||
- name: Update version in config files (for manual trigger)
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |
|
||||
cd packages/editor-app
|
||||
node -e "const pkg=require('./package.json'); pkg.version='${{ github.event.inputs.version }}'; require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2)+'\n')"
|
||||
# 临时更新版本号用于构建(不提交到仓库)
|
||||
npm version ${{ github.event.inputs.version }} --no-git-tag-version
|
||||
node scripts/sync-version.js
|
||||
|
||||
- name: Install wasm-pack
|
||||
run: cargo install wasm-pack
|
||||
- name: Cache TypeScript build
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
packages/core/bin
|
||||
packages/editor-core/dist
|
||||
packages/behavior-tree/bin
|
||||
key: ${{ runner.os }}-ts-build-${{ hashFiles('packages/core/src/**', 'packages/editor-core/src/**', 'packages/behavior-tree/src/**') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-ts-build-
|
||||
|
||||
# 使用 Turborepo 自动按依赖顺序构建所有包
|
||||
# 这会自动处理:core -> asset-system -> editor-core -> ui -> 等等
|
||||
- name: Build all packages with Turborepo
|
||||
run: pnpm run build
|
||||
- name: Build core package
|
||||
run: npm run build:core
|
||||
|
||||
- name: Copy WASM files to ecs-engine-bindgen
|
||||
shell: bash
|
||||
- name: Build editor-core package
|
||||
run: |
|
||||
mkdir -p packages/ecs-engine-bindgen/src/wasm
|
||||
cp packages/engine/pkg/es_engine.js packages/ecs-engine-bindgen/src/wasm/
|
||||
cp packages/engine/pkg/es_engine.d.ts packages/ecs-engine-bindgen/src/wasm/
|
||||
cp packages/engine/pkg/es_engine_bg.wasm packages/ecs-engine-bindgen/src/wasm/
|
||||
cp packages/engine/pkg/es_engine_bg.wasm.d.ts packages/ecs-engine-bindgen/src/wasm/
|
||||
cd packages/editor-core
|
||||
npm run build
|
||||
|
||||
- name: Bundle runtime files for Tauri
|
||||
- name: Build behavior-tree package
|
||||
run: |
|
||||
cd packages/editor-app
|
||||
node scripts/bundle-runtime.mjs
|
||||
cd packages/behavior-tree
|
||||
npm run build
|
||||
|
||||
- name: Build Tauri app
|
||||
uses: tauri-apps/tauri-action@v0.5
|
||||
@@ -128,7 +126,7 @@ jobs:
|
||||
- name: Update version files
|
||||
run: |
|
||||
cd packages/editor-app
|
||||
node -e "const pkg=require('./package.json'); pkg.version='${{ github.event.inputs.version }}'; require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2)+'\n')"
|
||||
npm version ${{ github.event.inputs.version }} --no-git-tag-version
|
||||
node scripts/sync-version.js
|
||||
|
||||
- name: Create Pull Request
|
||||
@@ -140,16 +138,16 @@ jobs:
|
||||
delete-branch: true
|
||||
title: "chore(editor): Release v${{ github.event.inputs.version }}"
|
||||
body: |
|
||||
## Release v${{ github.event.inputs.version }}
|
||||
## 🚀 Release v${{ github.event.inputs.version }}
|
||||
|
||||
This PR updates the editor version after successful release build.
|
||||
|
||||
### Changes
|
||||
- Updated `packages/editor-app/package.json` → `${{ github.event.inputs.version }}`
|
||||
- Updated `packages/editor-app/src-tauri/tauri.conf.json` → `${{ github.event.inputs.version }}`
|
||||
- ✅ Updated `packages/editor-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 }})
|
||||
- 📦 [GitHub Release](https://github.com/${{ github.repository }}/releases/tag/editor-v${{ github.event.inputs.version }})
|
||||
|
||||
---
|
||||
*This PR was automatically created by the release workflow.*
|
||||
|
||||
@@ -11,10 +11,6 @@ on:
|
||||
- core
|
||||
- behavior-tree
|
||||
- editor-core
|
||||
- node-editor
|
||||
- blueprint
|
||||
- tilemap
|
||||
- physics-rapier2d
|
||||
version_type:
|
||||
description: '版本更新类型'
|
||||
required: true
|
||||
@@ -45,32 +41,21 @@ jobs:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
cache: 'pnpm'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
run: npm ci
|
||||
|
||||
- name: Build core package (if needed)
|
||||
if: ${{ github.event.inputs.package != 'core' && github.event.inputs.package != 'node-editor' }}
|
||||
if: ${{ github.event.inputs.package == 'behavior-tree' || github.event.inputs.package == 'editor-core' }}
|
||||
run: |
|
||||
cd packages/core
|
||||
pnpm run build
|
||||
|
||||
- name: Build node-editor package (if needed for blueprint)
|
||||
if: ${{ github.event.inputs.package == 'blueprint' }}
|
||||
run: |
|
||||
cd packages/node-editor
|
||||
pnpm run build
|
||||
npm run build
|
||||
|
||||
# - name: Run tests
|
||||
# run: |
|
||||
@@ -82,33 +67,25 @@ jobs:
|
||||
run: |
|
||||
cd packages/${{ github.event.inputs.package }}
|
||||
if [ "${{ github.event.inputs.version_type }}" = "custom" ]; then
|
||||
NEW_VERSION=${{ github.event.inputs.custom_version }}
|
||||
npm version ${{ github.event.inputs.custom_version }} --no-git-tag-version --allow-same-version
|
||||
else
|
||||
# Get current version and bump it
|
||||
CURRENT=$(node -p "require('./package.json').version")
|
||||
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT"
|
||||
case "${{ github.event.inputs.version_type }}" in
|
||||
major) NEW_VERSION="$((MAJOR+1)).0.0" ;;
|
||||
minor) NEW_VERSION="$MAJOR.$((MINOR+1)).0" ;;
|
||||
patch) NEW_VERSION="$MAJOR.$MINOR.$((PATCH+1))" ;;
|
||||
esac
|
||||
npm version ${{ github.event.inputs.version_type }} --no-git-tag-version
|
||||
fi
|
||||
# Update package.json using node
|
||||
node -e "const fs=require('fs'); const pkg=JSON.parse(fs.readFileSync('package.json')); pkg.version='$NEW_VERSION'; fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2)+'\n')"
|
||||
NEW_VERSION=$(node -p "require('./package.json').version")
|
||||
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "发布版本: $NEW_VERSION"
|
||||
|
||||
- name: Build package
|
||||
run: |
|
||||
cd packages/${{ github.event.inputs.package }}
|
||||
pnpm run build:npm
|
||||
npm run build:npm
|
||||
|
||||
- name: Publish to npm
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: |
|
||||
cd packages/${{ github.event.inputs.package }}/dist
|
||||
pnpm publish --access public --no-git-checks
|
||||
npm publish --access public
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
|
||||
@@ -22,24 +22,19 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
cache: 'pnpm'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
run: npm ci
|
||||
|
||||
- name: Build core package
|
||||
run: |
|
||||
cd packages/core
|
||||
pnpm run build:npm
|
||||
npm run build:npm
|
||||
|
||||
- name: Check bundle size
|
||||
uses: andresz1/size-limit-action@v1
|
||||
|
||||
+2
-6
@@ -16,10 +16,6 @@ dist/
|
||||
*.tmp
|
||||
*.temp
|
||||
.cache/
|
||||
.build-cache/
|
||||
|
||||
# Turborepo
|
||||
.turbo/
|
||||
|
||||
# IDE 配置
|
||||
.idea/
|
||||
@@ -52,9 +48,9 @@ logs/
|
||||
coverage/
|
||||
*.lcov
|
||||
|
||||
# 包管理器锁文件(忽略yarn,保留pnpm)
|
||||
# 包管理器锁文件(保留npm的,忽略其他的)
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
|
||||
# 文档生成
|
||||
docs/api/
|
||||
|
||||
@@ -37,6 +37,9 @@ This project follows the [Conventional Commits](https://www.conventionalcommits.
|
||||
|
||||
- **core**: 核心包 @esengine/ecs-framework
|
||||
- **math**: 数学库包
|
||||
- **network-client**: 网络客户端包
|
||||
- **network-server**: 网络服务端包
|
||||
- **network-shared**: 网络共享包
|
||||
- **editor**: 编辑器
|
||||
- **docs**: 文档
|
||||
|
||||
|
||||
@@ -1,61 +1,90 @@
|
||||
# ESEngine
|
||||
# ECS Framework
|
||||
|
||||
**English** | [中文](./README_CN.md)
|
||||
[](https://github.com/esengine/ecs-framework/actions)
|
||||
[](https://codecov.io/gh/esengine/ecs-framework)
|
||||
[](https://badge.fury.io/js/%40esengine%2Fecs-framework)
|
||||
[](https://www.npmjs.com/package/@esengine/ecs-framework)
|
||||
[](https://bundlephobia.com/package/@esengine/ecs-framework)
|
||||
[](https://www.typescriptlang.org/)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](#contributors)
|
||||
[](https://github.com/esengine/ecs-framework/stargazers)
|
||||
[](https://deepwiki.com/esengine/ecs-framework)
|
||||
|
||||
**[Documentation](https://esengine.github.io/ecs-framework/) | [API Reference](https://esengine.github.io/ecs-framework/api/) | [Examples](./examples/)**
|
||||
<div align="center">
|
||||
|
||||
ESEngine is a cross-platform 2D game engine for creating games from a unified interface. It provides a comprehensive set of common tools so that developers can focus on making games without having to reinvent the wheel.
|
||||
<p>一个高性能的 TypeScript ECS (Entity-Component-System) 框架,专为现代游戏开发而设计。</p>
|
||||
|
||||
Games can be exported to multiple platforms including Web browsers, WeChat Mini Games, and other mini-game platforms.
|
||||
<p>A high-performance TypeScript ECS (Entity-Component-System) framework designed for modern game development.</p>
|
||||
|
||||
## Free and Open Source
|
||||
</div>
|
||||
|
||||
ESEngine is completely free and open source under the MIT license. No strings attached, no royalties. Your games are yours.
|
||||
---
|
||||
|
||||
## Features
|
||||
## 📊 项目统计 / Project Stats
|
||||
|
||||
- **Data-Driven Architecture**: Built on Entity-Component-System (ECS) pattern for flexible and performant game logic
|
||||
- **High-Performance Rendering**: Rust/WebAssembly 2D renderer with sprite batching and WebGL 2.0 backend
|
||||
- **Visual Editor**: Cross-platform desktop editor with scene management, asset browser, and visual tools
|
||||
- **Modular Design**: Use only what you need. Each feature is a separate module that can be included independently
|
||||
- **Multi-Platform**: Deploy to Web, WeChat Mini Games, and more from a single codebase
|
||||
<div align="center">
|
||||
|
||||
## Getting the Engine
|
||||
[](https://star-history.com/#esengine/ecs-framework&Date)
|
||||
|
||||
### Using npm
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
||||
<a href="https://github.com/esengine/ecs-framework/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=esengine/ecs-framework" />
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
### 📈 下载趋势 / Download Trends
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://www.npmjs.com/package/@esengine/ecs-framework)
|
||||
|
||||
[](https://npmtrends.com/@esengine/ecs-framework)
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 特性
|
||||
|
||||
- **高性能** - 针对大规模实体优化,支持SoA存储和批量处理
|
||||
- **多线程计算** - Worker系统支持真正的并行处理,充分利用多核CPU性能
|
||||
- **类型安全** - 完整的TypeScript支持,编译时类型检查
|
||||
- **现代架构** - 支持多World、多Scene的分层架构设计
|
||||
- **开发友好** - 内置调试工具和性能监控
|
||||
- **跨平台** - 支持Cocos Creator、Laya引擎和Web平台
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
npm install @esengine/ecs-framework
|
||||
```
|
||||
|
||||
### Building from Source
|
||||
|
||||
See [Building from Source](#building-from-source) for detailed instructions.
|
||||
|
||||
### Editor Download
|
||||
|
||||
Pre-built editor binaries are available on the [Releases](https://github.com/esengine/ecs-framework/releases) page for Windows and macOS.
|
||||
|
||||
## Quick Start
|
||||
## 快速开始
|
||||
|
||||
```typescript
|
||||
import {
|
||||
Core, Scene, Entity, Component, EntitySystem,
|
||||
Matcher, Time, ECSComponent, ECSSystem
|
||||
} from '@esengine/ecs-framework';
|
||||
import { Core, Scene, Component, EntitySystem, ECSComponent, ECSSystem, Matcher, Time } from '@esengine/ecs-framework';
|
||||
|
||||
// 定义组件
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
x = 0;
|
||||
y = 0;
|
||||
constructor(public x = 0, public y = 0) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('Velocity')
|
||||
class Velocity extends Component {
|
||||
dx = 0;
|
||||
dy = 0;
|
||||
constructor(public dx = 0, public dy = 0) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
// 创建系统
|
||||
@ECSSystem('Movement')
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
@@ -64,182 +93,182 @@ class MovementSystem extends EntitySystem {
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const pos = entity.getComponent(Position);
|
||||
const vel = entity.getComponent(Velocity);
|
||||
pos.x += vel.dx * Time.deltaTime;
|
||||
pos.y += vel.dy * Time.deltaTime;
|
||||
const position = entity.getComponent(Position)!;
|
||||
const velocity = entity.getComponent(Velocity)!;
|
||||
|
||||
position.x += velocity.dx * Time.deltaTime;
|
||||
position.y += velocity.dy * Time.deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Core.create();
|
||||
const scene = new Scene();
|
||||
scene.addSystem(new MovementSystem());
|
||||
// 创建场景并启动
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.addSystem(new MovementSystem());
|
||||
|
||||
const player = scene.createEntity('Player');
|
||||
player.addComponent(new Position());
|
||||
player.addComponent(new Velocity());
|
||||
|
||||
Core.setScene(scene);
|
||||
|
||||
// Game loop
|
||||
let lastTime = 0;
|
||||
function gameLoop(currentTime: number) {
|
||||
const deltaTime = (currentTime - lastTime) / 1000;
|
||||
lastTime = currentTime;
|
||||
|
||||
Core.update(deltaTime);
|
||||
requestAnimationFrame(gameLoop);
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Position(100, 100));
|
||||
player.addComponent(new Velocity(50, 0));
|
||||
}
|
||||
}
|
||||
|
||||
// 启动游戏
|
||||
Core.create();
|
||||
Core.setScene(new GameScene());
|
||||
|
||||
// 游戏循环中更新
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime);
|
||||
}
|
||||
requestAnimationFrame(gameLoop);
|
||||
```
|
||||
|
||||
## Modules
|
||||
## 核心特性
|
||||
|
||||
ESEngine is organized into modular packages. Each feature has a runtime module and an optional editor extension.
|
||||
- **实体查询** - 使用 Matcher API 进行高效的实体过滤
|
||||
- **事件系统** - 类型安全的事件发布/订阅机制
|
||||
- **性能优化** - SoA 存储优化,支持大规模实体处理
|
||||
- **多线程支持** - Worker系统实现真正的并行计算,充分利用多核CPU
|
||||
- **多场景** - 支持 World/Scene 分层架构
|
||||
- **时间管理** - 内置定时器和时间控制系统
|
||||
|
||||
### Core
|
||||
## 🏗️ 架构设计 / Architecture
|
||||
|
||||
| Package | Description |
|
||||
|---------|-------------|
|
||||
| `@esengine/ecs-framework` | Core ECS framework with entity management, component system, and queries |
|
||||
| `@esengine/math` | Vector, matrix, and mathematical utilities |
|
||||
| `@esengine/engine` | Rust/WASM 2D renderer |
|
||||
| `@esengine/engine-core` | Engine module system and lifecycle management |
|
||||
```mermaid
|
||||
graph TB
|
||||
A[Core 核心] --> B[World 世界]
|
||||
B --> C[Scene 场景]
|
||||
C --> D[EntityManager 实体管理器]
|
||||
C --> E[SystemManager 系统管理器]
|
||||
D --> F[Entity 实体]
|
||||
F --> G[Component 组件]
|
||||
E --> H[EntitySystem 实体系统]
|
||||
E --> I[WorkerSystem 工作线程系统]
|
||||
|
||||
### Runtime Modules
|
||||
|
||||
| Package | Description |
|
||||
|---------|-------------|
|
||||
| `@esengine/sprite` | 2D sprite rendering and animation |
|
||||
| `@esengine/tilemap` | Tile-based map rendering with animation support |
|
||||
| `@esengine/physics-rapier2d` | 2D physics simulation powered by Rapier |
|
||||
| `@esengine/behavior-tree` | Behavior tree AI system |
|
||||
| `@esengine/blueprint` | Visual scripting runtime |
|
||||
| `@esengine/camera` | Camera control and management |
|
||||
| `@esengine/audio` | Audio playback |
|
||||
| `@esengine/ui` | UI components |
|
||||
| `@esengine/material-system` | Material and shader system |
|
||||
| `@esengine/asset-system` | Asset loading and management |
|
||||
|
||||
### Editor Extensions
|
||||
|
||||
| Package | Description |
|
||||
|---------|-------------|
|
||||
| `@esengine/sprite-editor` | Sprite inspector and tools |
|
||||
| `@esengine/tilemap-editor` | Visual tilemap editor with brush tools |
|
||||
| `@esengine/physics-rapier2d-editor` | Physics collider visualization and editing |
|
||||
| `@esengine/behavior-tree-editor` | Visual behavior tree editor |
|
||||
| `@esengine/blueprint-editor` | Visual scripting editor |
|
||||
| `@esengine/material-editor` | Material and shader editor |
|
||||
| `@esengine/shader-editor` | Shader code editor |
|
||||
|
||||
### Platform
|
||||
|
||||
| Package | Description |
|
||||
|---------|-------------|
|
||||
| `@esengine/platform-common` | Platform abstraction interfaces |
|
||||
| `@esengine/platform-web` | Web browser runtime |
|
||||
| `@esengine/platform-wechat` | WeChat Mini Game runtime |
|
||||
|
||||
## Editor
|
||||
|
||||
ESEngine Editor is a cross-platform desktop application built with Tauri and React.
|
||||
|
||||
### Features
|
||||
|
||||
- Scene hierarchy and entity management
|
||||
- Component inspector with custom editors
|
||||
- Asset browser with drag-and-drop support
|
||||
- Tilemap editor with paint, fill, and selection tools
|
||||
- Behavior tree visual editor
|
||||
- Blueprint visual scripting
|
||||
- Material and shader editing
|
||||
- Built-in performance profiler
|
||||
- Localization support (English, Chinese)
|
||||
|
||||
### Screenshot
|
||||
|
||||

|
||||
|
||||
## Supported Platforms
|
||||
|
||||
| Platform | Runtime | Editor |
|
||||
|----------|---------|--------|
|
||||
| Web Browser | Yes | - |
|
||||
| Windows | - | Yes |
|
||||
| macOS | - | Yes |
|
||||
| WeChat Mini Game | In Progress | - |
|
||||
| Playable Ads | Planned | - |
|
||||
| Android | Planned | - |
|
||||
| iOS | Planned | - |
|
||||
| Windows Native | Planned | - |
|
||||
| Other Platforms | Planned | - |
|
||||
|
||||
## Building from Source
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 18 or later
|
||||
- pnpm 10 or later
|
||||
- Rust toolchain (for WASM renderer)
|
||||
- wasm-pack
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone https://github.com/esengine/ecs-framework.git
|
||||
cd ecs-framework
|
||||
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Build all packages
|
||||
pnpm build
|
||||
|
||||
# Build WASM renderer (optional)
|
||||
pnpm build:wasm
|
||||
style A fill:#e1f5ff
|
||||
style B fill:#fff3e0
|
||||
style C fill:#f3e5f5
|
||||
style D fill:#e8f5e9
|
||||
style E fill:#fff9c4
|
||||
style F fill:#ffebee
|
||||
style G fill:#e0f2f1
|
||||
style H fill:#fce4ec
|
||||
style I fill:#f1f8e9
|
||||
```
|
||||
|
||||
### Running the Editor
|
||||
## 平台支持
|
||||
|
||||
```bash
|
||||
cd packages/editor-app
|
||||
pnpm tauri:dev
|
||||
```
|
||||
支持主流游戏引擎和 Web 平台:
|
||||
|
||||
### Project Structure
|
||||
- **Cocos Creator**
|
||||
- **Laya 引擎**
|
||||
- **原生 Web** - 浏览器环境直接运行
|
||||
- **小游戏平台** - 微信、支付宝等小游戏
|
||||
|
||||
```
|
||||
ecs-framework/
|
||||
├── packages/ Engine packages (runtime, editor, platform)
|
||||
├── docs/ Documentation source
|
||||
├── examples/ Example projects
|
||||
├── scripts/ Build utilities
|
||||
└── thirdparty/ Third-party dependencies
|
||||
```
|
||||
## ECS Framework Editor
|
||||
|
||||
## Documentation
|
||||
跨平台桌面编辑器,提供可视化开发和调试工具。
|
||||
|
||||
- [Getting Started](https://esengine.github.io/ecs-framework/guide/getting-started.html)
|
||||
- [Architecture Guide](https://esengine.github.io/ecs-framework/guide/)
|
||||
- [API Reference](https://esengine.github.io/ecs-framework/api/)
|
||||
### 主要功能
|
||||
|
||||
## Community
|
||||
- **场景管理** - 可视化场景层级和实体管理
|
||||
- **组件检视** - 实时查看和编辑实体组件
|
||||
- **性能分析** - 内置 Profiler 监控系统性能
|
||||
- **插件系统** - 可扩展的插件架构
|
||||
- **远程调试** - 连接运行中的游戏进行实时调试
|
||||
- **自动更新** - 支持热更新,自动获取最新版本
|
||||
|
||||
- [GitHub Issues](https://github.com/esengine/ecs-framework/issues) - Bug reports and feature requests
|
||||
- [GitHub Discussions](https://github.com/esengine/ecs-framework/discussions) - Questions and ideas
|
||||
### 下载
|
||||
|
||||
## Contributing
|
||||
[](https://github.com/esengine/ecs-framework/releases/latest)
|
||||
|
||||
Contributions are welcome. Please read the contributing guidelines before submitting a pull request.
|
||||
支持 Windows、macOS (Intel & Apple Silicon)
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make changes with tests
|
||||
4. Submit a pull request
|
||||
### 截图
|
||||
|
||||
## License
|
||||
<img src="screenshots/main_screetshot.png" alt="ECS Framework Editor" width="800">
|
||||
|
||||
ESEngine is licensed under the [MIT License](LICENSE).
|
||||
<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>
|
||||
|
||||
## 示例项目
|
||||
|
||||
- [Worker系统演示](https://esengine.github.io/ecs-framework/demos/worker-system/) - 多线程物理系统演示,展示高性能并行计算
|
||||
- [割草机演示](https://github.com/esengine/lawn-mower-demo) - 完整的游戏示例
|
||||
|
||||
## 文档
|
||||
|
||||
- [📚 AI智能文档](https://deepwiki.com/esengine/ecs-framework) - AI助手随时解答你的问题
|
||||
- [快速入门](https://esengine.github.io/ecs-framework/guide/getting-started.html) - 详细教程和平台集成
|
||||
- [完整指南](https://esengine.github.io/ecs-framework/guide/) - ECS 概念和使用指南
|
||||
- [API 参考](https://esengine.github.io/ecs-framework/api/) - 完整 API 文档
|
||||
|
||||
## 生态系统
|
||||
|
||||
- [路径寻找](https://github.com/esengine/ecs-astar) - A*、BFS、Dijkstra 算法
|
||||
- [AI 系统](https://github.com/esengine/BehaviourTree-ai) - 行为树、效用 AI
|
||||
|
||||
## 💪 支持项目 / Support the Project
|
||||
|
||||
如果这个项目对你有帮助,请考虑:
|
||||
|
||||
If this project helps you, please consider:
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://github.com/sponsors/esengine)
|
||||
[](https://github.com/esengine/ecs-framework)
|
||||
|
||||
</div>
|
||||
|
||||
- ⭐ 给项目点个 Star
|
||||
- 🐛 报告 Bug 或提出新功能
|
||||
- 📝 改进文档
|
||||
- 💖 成为赞助者
|
||||
|
||||
## 社区与支持
|
||||
|
||||
- [问题反馈](https://github.com/esengine/ecs-framework/issues) - Bug 报告和功能建议
|
||||
- [讨论区](https://github.com/esengine/ecs-framework/discussions) - 提问、分享想法
|
||||
- [QQ 交流群](https://jq.qq.com/?_wv=1027&k=29w1Nud6) - ecs游戏框架交流
|
||||
|
||||
## 贡献者 / Contributors
|
||||
|
||||
感谢所有为这个项目做出贡献的人!
|
||||
|
||||
Thanks goes to these wonderful people:
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||
<!-- prettier-ignore-start -->
|
||||
<!-- markdownlint-disable -->
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/esengine"><img src="https://avatars.githubusercontent.com/esengine?s=100" width="100px;" alt="esengine"/><br /><sub><b>esengine</b></sub></a><br /><a href="#maintenance-esengine" title="Maintenance">🚧</a> <a href="https://github.com/esengine/ecs-framework/commits?author=esengine" title="Code">💻</a> <a href="#design-esengine" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/foxling"><img src="https://avatars.githubusercontent.com/foxling?s=100" width="100px;" alt="LING YE"/><br /><sub><b>LING YE</b></sub></a><br /><a href="https://github.com/esengine/ecs-framework/commits?author=foxling" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MirageTank"><img src="https://avatars.githubusercontent.com/MirageTank?s=100" width="100px;" alt="MirageTank"/><br /><sub><b>MirageTank</b></sub></a><br /><a href="https://github.com/esengine/ecs-framework/commits?author=MirageTank" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- markdownlint-restore -->
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
本项目遵循 [all-contributors](https://github.com/all-contributors/all-contributors) 规范。欢迎任何形式的贡献!
|
||||
|
||||
## 许可证
|
||||
|
||||
[MIT](LICENSE) © 2025 ECS Framework
|
||||
|
||||
-246
@@ -1,246 +0,0 @@
|
||||
# ESEngine
|
||||
|
||||
[English](./README.md) | **中文**
|
||||
|
||||
**[文档](https://esengine.github.io/ecs-framework/) | [API 参考](https://esengine.github.io/ecs-framework/api/) | [示例](./examples/)**
|
||||
|
||||
ESEngine 是一个跨平台 2D 游戏引擎,提供统一的开发界面。它包含完整的常用工具集,让开发者专注于游戏创作本身。
|
||||
|
||||
游戏可以导出到多个平台,包括 Web 浏览器、微信小游戏等小游戏平台。
|
||||
|
||||
## 免费开源
|
||||
|
||||
ESEngine 基于 MIT 协议完全免费开源。无附加条件,无版税。你的游戏完全属于你。
|
||||
|
||||
## 特性
|
||||
|
||||
- **数据驱动架构**:基于 ECS(实体-组件-系统)模式构建,提供灵活高效的游戏逻辑
|
||||
- **高性能渲染**:Rust/WebAssembly 2D 渲染器,支持精灵批处理和 WebGL 2.0
|
||||
- **可视化编辑器**:跨平台桌面编辑器,包含场景管理、资源浏览器和可视化工具
|
||||
- **模块化设计**:按需使用,每个功能都是独立模块,可单独引入
|
||||
- **多平台支持**:一套代码部署到 Web、微信小游戏等多个平台
|
||||
|
||||
## 获取引擎
|
||||
|
||||
### 通过 npm 安装
|
||||
|
||||
```bash
|
||||
npm install @esengine/ecs-framework
|
||||
```
|
||||
|
||||
### 从源码构建
|
||||
|
||||
详见 [从源码构建](#从源码构建) 章节。
|
||||
|
||||
### 编辑器下载
|
||||
|
||||
预编译的编辑器可在 [Releases](https://github.com/esengine/ecs-framework/releases) 页面下载,支持 Windows 和 macOS。
|
||||
|
||||
## 快速开始
|
||||
|
||||
```typescript
|
||||
import {
|
||||
Core, Scene, Entity, Component, EntitySystem,
|
||||
Matcher, Time, ECSComponent, ECSSystem
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
x = 0;
|
||||
y = 0;
|
||||
}
|
||||
|
||||
@ECSComponent('Velocity')
|
||||
class Velocity extends Component {
|
||||
dx = 0;
|
||||
dy = 0;
|
||||
}
|
||||
|
||||
@ECSSystem('Movement')
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Position, Velocity));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const pos = entity.getComponent(Position);
|
||||
const vel = entity.getComponent(Velocity);
|
||||
pos.x += vel.dx * Time.deltaTime;
|
||||
pos.y += vel.dy * Time.deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Core.create();
|
||||
const scene = new Scene();
|
||||
scene.addSystem(new MovementSystem());
|
||||
|
||||
const player = scene.createEntity('Player');
|
||||
player.addComponent(new Position());
|
||||
player.addComponent(new Velocity());
|
||||
|
||||
Core.setScene(scene);
|
||||
|
||||
// 游戏循环
|
||||
let lastTime = 0;
|
||||
function gameLoop(currentTime: number) {
|
||||
const deltaTime = (currentTime - lastTime) / 1000;
|
||||
lastTime = currentTime;
|
||||
|
||||
Core.update(deltaTime);
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
requestAnimationFrame(gameLoop);
|
||||
```
|
||||
|
||||
## 模块
|
||||
|
||||
ESEngine 采用模块化组织。每个功能都有运行时模块和可选的编辑器扩展。
|
||||
|
||||
### 核心
|
||||
|
||||
| 包名 | 描述 |
|
||||
|------|------|
|
||||
| `@esengine/ecs-framework` | ECS 框架核心,包含实体管理、组件系统和查询 |
|
||||
| `@esengine/math` | 向量、矩阵和数学工具 |
|
||||
| `@esengine/engine` | Rust/WASM 2D 渲染器 |
|
||||
| `@esengine/engine-core` | 引擎模块系统和生命周期管理 |
|
||||
|
||||
### 运行时模块
|
||||
|
||||
| 包名 | 描述 |
|
||||
|------|------|
|
||||
| `@esengine/sprite` | 2D 精灵渲染和动画 |
|
||||
| `@esengine/tilemap` | Tilemap 渲染,支持动画 |
|
||||
| `@esengine/physics-rapier2d` | 基于 Rapier 的 2D 物理模拟 |
|
||||
| `@esengine/behavior-tree` | 行为树 AI 系统 |
|
||||
| `@esengine/blueprint` | 可视化脚本运行时 |
|
||||
| `@esengine/camera` | 相机控制和管理 |
|
||||
| `@esengine/audio` | 音频播放 |
|
||||
| `@esengine/ui` | UI 组件 |
|
||||
| `@esengine/material-system` | 材质和着色器系统 |
|
||||
| `@esengine/asset-system` | 资源加载和管理 |
|
||||
|
||||
### 编辑器扩展
|
||||
|
||||
| 包名 | 描述 |
|
||||
|------|------|
|
||||
| `@esengine/sprite-editor` | 精灵检视器和工具 |
|
||||
| `@esengine/tilemap-editor` | 可视化 Tilemap 编辑器,支持笔刷工具 |
|
||||
| `@esengine/physics-rapier2d-editor` | 物理碰撞体可视化和编辑 |
|
||||
| `@esengine/behavior-tree-editor` | 可视化行为树编辑器 |
|
||||
| `@esengine/blueprint-editor` | 可视化脚本编辑器 |
|
||||
| `@esengine/material-editor` | 材质和着色器编辑器 |
|
||||
| `@esengine/shader-editor` | 着色器代码编辑器 |
|
||||
|
||||
### 平台
|
||||
|
||||
| 包名 | 描述 |
|
||||
|------|------|
|
||||
| `@esengine/platform-common` | 平台抽象接口 |
|
||||
| `@esengine/platform-web` | Web 浏览器运行时 |
|
||||
| `@esengine/platform-wechat` | 微信小游戏运行时 |
|
||||
|
||||
## 编辑器
|
||||
|
||||
ESEngine 编辑器是基于 Tauri 和 React 构建的跨平台桌面应用。
|
||||
|
||||
### 功能
|
||||
|
||||
- 场景层级和实体管理
|
||||
- 组件检视器,支持自定义编辑器
|
||||
- 资源浏览器,支持拖放
|
||||
- Tilemap 编辑器,支持绘制、填充、选择工具
|
||||
- 行为树可视化编辑器
|
||||
- 蓝图可视化脚本
|
||||
- 材质和着色器编辑
|
||||
- 内置性能分析器
|
||||
- 多语言支持(英文、中文)
|
||||
|
||||
### 截图
|
||||
|
||||

|
||||
|
||||
## 支持的平台
|
||||
|
||||
| 平台 | 运行时 | 编辑器 |
|
||||
|------|--------|--------|
|
||||
| Web 浏览器 | 支持 | - |
|
||||
| Windows | - | 支持 |
|
||||
| macOS | - | 支持 |
|
||||
| 微信小游戏 | 开发中 | - |
|
||||
| Playable 可玩广告 | 计划中 | - |
|
||||
| Android | 计划中 | - |
|
||||
| iOS | 计划中 | - |
|
||||
| Windows 原生 | 计划中 | - |
|
||||
| 其他平台 | 计划中 | - |
|
||||
|
||||
## 从源码构建
|
||||
|
||||
### 前置要求
|
||||
|
||||
- Node.js 18 或更高版本
|
||||
- pnpm 10 或更高版本
|
||||
- Rust 工具链(用于 WASM 渲染器)
|
||||
- wasm-pack
|
||||
|
||||
### 安装
|
||||
|
||||
```bash
|
||||
# 克隆仓库
|
||||
git clone https://github.com/esengine/ecs-framework.git
|
||||
cd ecs-framework
|
||||
|
||||
# 安装依赖
|
||||
pnpm install
|
||||
|
||||
# 构建所有包
|
||||
pnpm build
|
||||
|
||||
# 构建 WASM 渲染器(可选)
|
||||
pnpm build:wasm
|
||||
```
|
||||
|
||||
### 运行编辑器
|
||||
|
||||
```bash
|
||||
cd packages/editor-app
|
||||
pnpm tauri:dev
|
||||
```
|
||||
|
||||
### 项目结构
|
||||
|
||||
```
|
||||
ecs-framework/
|
||||
├── packages/ 引擎包(运行时、编辑器、平台)
|
||||
├── docs/ 文档源码
|
||||
├── examples/ 示例项目
|
||||
├── scripts/ 构建工具
|
||||
└── thirdparty/ 第三方依赖
|
||||
```
|
||||
|
||||
## 文档
|
||||
|
||||
- [快速入门](https://esengine.github.io/ecs-framework/guide/getting-started.html)
|
||||
- [架构指南](https://esengine.github.io/ecs-framework/guide/)
|
||||
- [API 参考](https://esengine.github.io/ecs-framework/api/)
|
||||
|
||||
## 社区
|
||||
|
||||
- [GitHub Issues](https://github.com/esengine/ecs-framework/issues) - Bug 反馈和功能建议
|
||||
- [GitHub Discussions](https://github.com/esengine/ecs-framework/discussions) - 问题和想法
|
||||
- [QQ 交流群](https://jq.qq.com/?_wv=1027&k=29w1Nud6) - 中文社区
|
||||
|
||||
## 贡献
|
||||
|
||||
欢迎贡献代码。提交 PR 前请阅读贡献指南。
|
||||
|
||||
1. Fork 仓库
|
||||
2. 创建功能分支
|
||||
3. 修改代码并测试
|
||||
4. 提交 PR
|
||||
|
||||
## 许可证
|
||||
|
||||
ESEngine 基于 [MIT 协议](LICENSE) 开源。
|
||||
+179
-222
@@ -9,185 +9,6 @@ const corePackageJson = JSON.parse(
|
||||
readFileSync(join(__dirname, '../../packages/core/package.json'), 'utf-8')
|
||||
)
|
||||
|
||||
// Import i18n messages
|
||||
import en from './i18n/en.json' with { type: 'json' }
|
||||
import zh from './i18n/zh.json' with { type: 'json' }
|
||||
|
||||
// 创建侧边栏配置 | Create sidebar config
|
||||
// prefix: 路径前缀,如 '' 或 '/en' | Path prefix like '' or '/en'
|
||||
function createSidebar(t, prefix = '') {
|
||||
return {
|
||||
[`${prefix}/guide/`]: [
|
||||
{
|
||||
text: t.sidebar.gettingStarted,
|
||||
items: [
|
||||
{ text: t.sidebar.quickStart, link: `${prefix}/guide/getting-started` },
|
||||
{ text: t.sidebar.guideOverview, link: `${prefix}/guide/` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.coreConcepts,
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: t.sidebar.entity, link: `${prefix}/guide/entity` },
|
||||
{ text: t.sidebar.hierarchy, link: `${prefix}/guide/hierarchy` },
|
||||
{ text: t.sidebar.component, link: `${prefix}/guide/component` },
|
||||
{ text: t.sidebar.entityQuery, link: `${prefix}/guide/entity-query` },
|
||||
{
|
||||
text: t.sidebar.system,
|
||||
link: `${prefix}/guide/system`,
|
||||
items: [
|
||||
{ text: t.sidebar.workerSystem, link: `${prefix}/guide/worker-system` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.scene,
|
||||
link: `${prefix}/guide/scene`,
|
||||
items: [
|
||||
{ text: t.sidebar.sceneManager, link: `${prefix}/guide/scene-manager` },
|
||||
{ text: t.sidebar.worldManager, link: `${prefix}/guide/world-manager` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.behaviorTree,
|
||||
link: `${prefix}/guide/behavior-tree/`,
|
||||
items: [
|
||||
{ text: t.sidebar.btGettingStarted, link: `${prefix}/guide/behavior-tree/getting-started` },
|
||||
{ text: t.sidebar.btCoreConcepts, link: `${prefix}/guide/behavior-tree/core-concepts` },
|
||||
{ text: t.sidebar.btEditorGuide, link: `${prefix}/guide/behavior-tree/editor-guide` },
|
||||
{ text: t.sidebar.btEditorWorkflow, link: `${prefix}/guide/behavior-tree/editor-workflow` },
|
||||
{ text: t.sidebar.btCustomActions, link: `${prefix}/guide/behavior-tree/custom-actions` },
|
||||
{ text: t.sidebar.btCocosIntegration, link: `${prefix}/guide/behavior-tree/cocos-integration` },
|
||||
{ text: t.sidebar.btLayaIntegration, link: `${prefix}/guide/behavior-tree/laya-integration` },
|
||||
{ text: t.sidebar.btAdvancedUsage, link: `${prefix}/guide/behavior-tree/advanced-usage` },
|
||||
{ text: t.sidebar.btBestPractices, link: `${prefix}/guide/behavior-tree/best-practices` }
|
||||
]
|
||||
},
|
||||
{ text: t.sidebar.serialization, link: `${prefix}/guide/serialization` },
|
||||
{ text: t.sidebar.eventSystem, link: `${prefix}/guide/event-system` },
|
||||
{ text: t.sidebar.timeAndTimers, link: `${prefix}/guide/time-and-timers` },
|
||||
{ text: t.sidebar.logging, link: `${prefix}/guide/logging` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.advancedFeatures,
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: t.sidebar.serviceContainer, link: `${prefix}/guide/service-container` },
|
||||
{ text: t.sidebar.pluginSystem, link: `${prefix}/guide/plugin-system` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.platformAdapters,
|
||||
link: `${prefix}/guide/platform-adapter`,
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: t.sidebar.browserAdapter, link: `${prefix}/guide/platform-adapter/browser` },
|
||||
{ text: t.sidebar.wechatAdapter, link: `${prefix}/guide/platform-adapter/wechat-minigame` },
|
||||
{ text: t.sidebar.nodejsAdapter, link: `${prefix}/guide/platform-adapter/nodejs` }
|
||||
]
|
||||
}
|
||||
],
|
||||
[`${prefix}/examples/`]: [
|
||||
{
|
||||
text: t.sidebar.examples,
|
||||
items: [
|
||||
{ text: t.sidebar.examplesOverview, link: `${prefix}/examples/` },
|
||||
{ text: t.nav.workerDemo, link: `${prefix}/examples/worker-system-demo` }
|
||||
]
|
||||
}
|
||||
],
|
||||
[`${prefix}/api/`]: [
|
||||
{
|
||||
text: t.sidebar.apiReference,
|
||||
items: [
|
||||
{ text: t.sidebar.overview, link: `${prefix}/api/README` },
|
||||
{
|
||||
text: t.sidebar.coreClasses,
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: 'Core', link: `${prefix}/api/classes/Core` },
|
||||
{ text: 'Scene', link: `${prefix}/api/classes/Scene` },
|
||||
{ text: 'World', link: `${prefix}/api/classes/World` },
|
||||
{ text: 'Entity', link: `${prefix}/api/classes/Entity` },
|
||||
{ text: 'Component', link: `${prefix}/api/classes/Component` },
|
||||
{ text: 'EntitySystem', link: `${prefix}/api/classes/EntitySystem` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.systemClasses,
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'PassiveSystem', link: `${prefix}/api/classes/PassiveSystem` },
|
||||
{ text: 'ProcessingSystem', link: `${prefix}/api/classes/ProcessingSystem` },
|
||||
{ text: 'IntervalSystem', link: `${prefix}/api/classes/IntervalSystem` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.utilities,
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Matcher', link: `${prefix}/api/classes/Matcher` },
|
||||
{ text: 'Time', link: `${prefix}/api/classes/Time` },
|
||||
{ text: 'PerformanceMonitor', link: `${prefix}/api/classes/PerformanceMonitor` },
|
||||
{ text: 'DebugManager', link: `${prefix}/api/classes/DebugManager` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.interfaces,
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'IScene', link: `${prefix}/api/interfaces/IScene` },
|
||||
{ text: 'IComponent', link: `${prefix}/api/interfaces/IComponent` },
|
||||
{ text: 'ISystemBase', link: `${prefix}/api/interfaces/ISystemBase` },
|
||||
{ text: 'ICoreConfig', link: `${prefix}/api/interfaces/ICoreConfig` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.decorators,
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: '@ECSComponent', link: `${prefix}/api/functions/ECSComponent` },
|
||||
{ text: '@ECSSystem', link: `${prefix}/api/functions/ECSSystem` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.enums,
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'ECSEventType', link: `${prefix}/api/enumerations/ECSEventType` },
|
||||
{ text: 'LogLevel', link: `${prefix}/api/enumerations/LogLevel` }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// 创建导航配置 | Create nav config
|
||||
// prefix: 路径前缀,如 '' 或 '/en' | Path prefix like '' or '/en'
|
||||
function createNav(t, prefix = '') {
|
||||
return [
|
||||
{ text: t.nav.home, link: `${prefix}/` },
|
||||
{ text: t.nav.quickStart, link: `${prefix}/guide/getting-started` },
|
||||
{ text: t.nav.guide, link: `${prefix}/guide/` },
|
||||
{ text: t.nav.api, link: `${prefix}/api/README` },
|
||||
{
|
||||
text: t.nav.examples,
|
||||
items: [
|
||||
{ text: t.nav.workerDemo, link: `${prefix}/examples/worker-system-demo` },
|
||||
{ text: t.nav.lawnMowerDemo, link: 'https://github.com/esengine/lawn-mower-demo' }
|
||||
]
|
||||
},
|
||||
{ text: t.nav.changelog, link: `${prefix}/changelog` },
|
||||
{
|
||||
text: `v${corePackageJson.version}`,
|
||||
link: 'https://github.com/esengine/ecs-framework/releases'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
vite: {
|
||||
plugins: [
|
||||
@@ -207,49 +28,175 @@ export default defineConfig({
|
||||
}
|
||||
}
|
||||
},
|
||||
title: 'ESEngine',
|
||||
appearance: 'force-dark',
|
||||
|
||||
locales: {
|
||||
root: {
|
||||
label: '简体中文',
|
||||
lang: 'zh-CN',
|
||||
description: '高性能 TypeScript ECS 框架 - 为游戏开发而生',
|
||||
themeConfig: {
|
||||
nav: createNav(zh, ''),
|
||||
sidebar: createSidebar(zh, ''),
|
||||
editLink: {
|
||||
pattern: 'https://github.com/esengine/ecs-framework/edit/master/docs/:path',
|
||||
text: zh.common.editOnGithub
|
||||
},
|
||||
outline: {
|
||||
level: [2, 3],
|
||||
label: zh.common.onThisPage
|
||||
}
|
||||
}
|
||||
},
|
||||
en: {
|
||||
label: 'English',
|
||||
lang: 'en',
|
||||
link: '/en/',
|
||||
description: 'High-performance TypeScript ECS Framework for Game Development',
|
||||
themeConfig: {
|
||||
nav: createNav(en, '/en'),
|
||||
sidebar: createSidebar(en, '/en'),
|
||||
editLink: {
|
||||
pattern: 'https://github.com/esengine/ecs-framework/edit/master/docs/:path',
|
||||
text: en.common.editOnGithub
|
||||
},
|
||||
outline: {
|
||||
level: [2, 3],
|
||||
label: en.common.onThisPage
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
title: 'ECS Framework',
|
||||
description: '高性能TypeScript ECS框架 - 为游戏开发而生',
|
||||
lang: 'zh-CN',
|
||||
|
||||
themeConfig: {
|
||||
siteTitle: 'ESEngine',
|
||||
nav: [
|
||||
{ text: '首页', link: '/' },
|
||||
{ text: '快速开始', link: '/guide/getting-started' },
|
||||
{ text: '指南', link: '/guide/' },
|
||||
{ text: 'API', link: '/api/README' },
|
||||
{
|
||||
text: '示例',
|
||||
items: [
|
||||
{ text: 'Worker系统演示', link: '/examples/worker-system-demo' },
|
||||
{ text: '割草机演示', link: 'https://github.com/esengine/lawn-mower-demo' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: `v${corePackageJson.version}`,
|
||||
link: 'https://github.com/esengine/ecs-framework/releases'
|
||||
}
|
||||
],
|
||||
|
||||
sidebar: {
|
||||
'/guide/': [
|
||||
{
|
||||
text: '开始使用',
|
||||
items: [
|
||||
{ text: '快速开始', link: '/guide/getting-started' },
|
||||
{ text: '指南概览', link: '/guide/' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '核心概念',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: '实体类 (Entity)', link: '/guide/entity' },
|
||||
{ text: '组件系统 (Component)', link: '/guide/component' },
|
||||
{ text: '实体查询系统', link: '/guide/entity-query' },
|
||||
{
|
||||
text: '系统架构 (System)',
|
||||
link: '/guide/system',
|
||||
items: [
|
||||
{ text: 'Worker系统 (多线程)', link: '/guide/worker-system' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '场景管理 (Scene)',
|
||||
link: '/guide/scene',
|
||||
items: [
|
||||
{ text: 'SceneManager', link: '/guide/scene-manager' },
|
||||
{ text: 'WorldManager', link: '/guide/world-manager' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '行为树系统 (Behavior Tree)',
|
||||
link: '/guide/behavior-tree/',
|
||||
items: [
|
||||
{ text: '快速开始', link: '/guide/behavior-tree/getting-started' },
|
||||
{ text: '核心概念', link: '/guide/behavior-tree/core-concepts' },
|
||||
{ text: '编辑器指南', link: '/guide/behavior-tree/editor-guide' },
|
||||
{ text: '编辑器工作流', link: '/guide/behavior-tree/editor-workflow' },
|
||||
{ text: '自定义动作组件', link: '/guide/behavior-tree/custom-actions' },
|
||||
{ text: 'Cocos Creator集成', link: '/guide/behavior-tree/cocos-integration' },
|
||||
{ text: 'Laya引擎集成', link: '/guide/behavior-tree/laya-integration' },
|
||||
{ text: '高级用法', link: '/guide/behavior-tree/advanced-usage' },
|
||||
{ text: '最佳实践', link: '/guide/behavior-tree/best-practices' }
|
||||
]
|
||||
},
|
||||
{ text: '序列化系统 (Serialization)', link: '/guide/serialization' },
|
||||
{ text: '事件系统 (Event)', link: '/guide/event-system' },
|
||||
{ text: '时间和定时器 (Time)', link: '/guide/time-and-timers' },
|
||||
{ text: '日志系统 (Logger)', link: '/guide/logging' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '高级特性',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: '服务容器 (Service Container)', link: '/guide/service-container' },
|
||||
{ text: '插件系统 (Plugin System)', link: '/guide/plugin-system' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '平台适配器',
|
||||
link: '/guide/platform-adapter',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: '浏览器适配器', link: '/guide/platform-adapter/browser' },
|
||||
{ text: '微信小游戏适配器', link: '/guide/platform-adapter/wechat-minigame' },
|
||||
{ text: 'Node.js适配器', link: '/guide/platform-adapter/nodejs' }
|
||||
]
|
||||
}
|
||||
],
|
||||
'/examples/': [
|
||||
{
|
||||
text: '示例',
|
||||
items: [
|
||||
{ text: '示例概览', link: '/examples/' },
|
||||
{ text: 'Worker系统演示', link: '/examples/worker-system-demo' }
|
||||
]
|
||||
}
|
||||
],
|
||||
'/api/': [
|
||||
{
|
||||
text: 'API 参考',
|
||||
items: [
|
||||
{ text: '概述', link: '/api/README' },
|
||||
{
|
||||
text: '核心类',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: 'Core', link: '/api/classes/Core' },
|
||||
{ text: 'Scene', link: '/api/classes/Scene' },
|
||||
{ text: 'World', link: '/api/classes/World' },
|
||||
{ text: 'Entity', link: '/api/classes/Entity' },
|
||||
{ text: 'Component', link: '/api/classes/Component' },
|
||||
{ text: 'EntitySystem', link: '/api/classes/EntitySystem' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '系统类',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'PassiveSystem', link: '/api/classes/PassiveSystem' },
|
||||
{ text: 'ProcessingSystem', link: '/api/classes/ProcessingSystem' },
|
||||
{ text: 'IntervalSystem', link: '/api/classes/IntervalSystem' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '工具类',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Matcher', link: '/api/classes/Matcher' },
|
||||
{ text: 'Time', link: '/api/classes/Time' },
|
||||
{ text: 'PerformanceMonitor', link: '/api/classes/PerformanceMonitor' },
|
||||
{ text: 'DebugManager', link: '/api/classes/DebugManager' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '接口',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'IScene', link: '/api/interfaces/IScene' },
|
||||
{ text: 'IComponent', link: '/api/interfaces/IComponent' },
|
||||
{ text: 'ISystemBase', link: '/api/interfaces/ISystemBase' },
|
||||
{ text: 'ICoreConfig', link: '/api/interfaces/ICoreConfig' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '装饰器',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: '@ECSComponent', link: '/api/functions/ECSComponent' },
|
||||
{ text: '@ECSSystem', link: '/api/functions/ECSSystem' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '枚举',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'ECSEventType', link: '/api/enumerations/ECSEventType' },
|
||||
{ text: 'LogLevel', link: '/api/enumerations/LogLevel' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
socialLinks: [
|
||||
{ icon: 'github', link: 'https://github.com/esengine/ecs-framework' }
|
||||
@@ -260,8 +207,18 @@ export default defineConfig({
|
||||
copyright: 'Copyright © 2025 ECS Framework'
|
||||
},
|
||||
|
||||
editLink: {
|
||||
pattern: 'https://github.com/esengine/ecs-framework/edit/master/docs/:path',
|
||||
text: '在 GitHub 上编辑此页'
|
||||
},
|
||||
|
||||
search: {
|
||||
provider: 'local'
|
||||
},
|
||||
|
||||
outline: {
|
||||
level: [2, 3],
|
||||
label: '目录'
|
||||
}
|
||||
},
|
||||
|
||||
@@ -270,7 +227,7 @@ export default defineConfig({
|
||||
['link', { rel: 'icon', href: '/favicon.ico' }]
|
||||
],
|
||||
|
||||
base: '/',
|
||||
base: '/ecs-framework/',
|
||||
cleanUrls: true,
|
||||
|
||||
markdown: {
|
||||
@@ -280,4 +237,4 @@ export default defineConfig({
|
||||
dark: 'github-dark'
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,86 +0,0 @@
|
||||
{
|
||||
"nav": {
|
||||
"home": "Home",
|
||||
"quickStart": "Quick Start",
|
||||
"guide": "Guide",
|
||||
"api": "API",
|
||||
"examples": "Examples",
|
||||
"workerDemo": "Worker System Demo",
|
||||
"lawnMowerDemo": "Lawn Mower Demo",
|
||||
"changelog": "Changelog"
|
||||
},
|
||||
"sidebar": {
|
||||
"gettingStarted": "Getting Started",
|
||||
"quickStart": "Quick Start",
|
||||
"guideOverview": "Guide Overview",
|
||||
"coreConcepts": "Core Concepts",
|
||||
"entity": "Entity",
|
||||
"hierarchy": "Hierarchy",
|
||||
"component": "Component",
|
||||
"entityQuery": "Entity Query",
|
||||
"system": "System",
|
||||
"workerSystem": "Worker System (Multithreading)",
|
||||
"scene": "Scene",
|
||||
"sceneManager": "SceneManager",
|
||||
"worldManager": "WorldManager",
|
||||
"behaviorTree": "Behavior Tree",
|
||||
"btGettingStarted": "Getting Started",
|
||||
"btCoreConcepts": "Core Concepts",
|
||||
"btEditorGuide": "Editor Guide",
|
||||
"btEditorWorkflow": "Editor Workflow",
|
||||
"btCustomActions": "Custom Actions",
|
||||
"btCocosIntegration": "Cocos Creator Integration",
|
||||
"btLayaIntegration": "Laya Engine Integration",
|
||||
"btAdvancedUsage": "Advanced Usage",
|
||||
"btBestPractices": "Best Practices",
|
||||
"serialization": "Serialization",
|
||||
"eventSystem": "Event System",
|
||||
"timeAndTimers": "Time and Timers",
|
||||
"logging": "Logging",
|
||||
"advancedFeatures": "Advanced Features",
|
||||
"serviceContainer": "Service Container",
|
||||
"pluginSystem": "Plugin System",
|
||||
"platformAdapters": "Platform Adapters",
|
||||
"browserAdapter": "Browser Adapter",
|
||||
"wechatAdapter": "WeChat Mini Game Adapter",
|
||||
"nodejsAdapter": "Node.js Adapter",
|
||||
"examples": "Examples",
|
||||
"examplesOverview": "Examples Overview",
|
||||
"apiReference": "API Reference",
|
||||
"overview": "Overview",
|
||||
"coreClasses": "Core Classes",
|
||||
"systemClasses": "System Classes",
|
||||
"utilities": "Utilities",
|
||||
"interfaces": "Interfaces",
|
||||
"decorators": "Decorators",
|
||||
"enums": "Enums"
|
||||
},
|
||||
"home": {
|
||||
"title": "ESEngine - High-performance TypeScript ECS Framework",
|
||||
"quickLinks": "Quick Links",
|
||||
"viewDocs": "View Docs",
|
||||
"getStarted": "Get Started",
|
||||
"getStartedDesc": "From installation to your first ECS app, learn the core concepts in 5 minutes.",
|
||||
"aiSystem": "AI System",
|
||||
"behaviorTreeEditor": "Visual Behavior Tree Editor",
|
||||
"behaviorTreeDesc": "Built-in AI behavior tree system with visual editing and real-time debugging.",
|
||||
"coreFeatures": "Core Features",
|
||||
"ecsArchitecture": "High-performance ECS Architecture",
|
||||
"ecsArchitectureDesc": "Data-driven entity component system for large-scale entity processing with cache-friendly memory layout.",
|
||||
"typeSupport": "Full Type Support",
|
||||
"typeSupportDesc": "100% TypeScript with complete type definitions and compile-time checking for the best development experience.",
|
||||
"visualBehaviorTree": "Visual Behavior Tree",
|
||||
"visualBehaviorTreeDesc": "Built-in AI behavior tree system with visual editor, custom nodes, and real-time debugging.",
|
||||
"multiPlatform": "Multi-Platform Support",
|
||||
"multiPlatformDesc": "Support for browsers, Node.js, WeChat Mini Games, and seamless integration with major game engines.",
|
||||
"modularDesign": "Modular Design",
|
||||
"modularDesignDesc": "Core features packaged independently, import only what you need. Support for custom plugin extensions.",
|
||||
"devTools": "Developer Tools",
|
||||
"devToolsDesc": "Built-in performance monitoring, debugging tools, serialization system, and complete development toolchain.",
|
||||
"learnMore": "Learn more →"
|
||||
},
|
||||
"common": {
|
||||
"editOnGithub": "Edit this page on GitHub",
|
||||
"onThisPage": "On this page"
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import en from './en.json'
|
||||
import zh from './zh.json'
|
||||
|
||||
export const messages = { en, zh }
|
||||
|
||||
export type Locale = 'en' | 'zh'
|
||||
|
||||
export function getLocaleMessages(locale: Locale) {
|
||||
return messages[locale] || messages.en
|
||||
}
|
||||
|
||||
// Helper to get nested key value
|
||||
export function t(messages: typeof en, key: string): string {
|
||||
const keys = key.split('.')
|
||||
let result: any = messages
|
||||
for (const k of keys) {
|
||||
result = result?.[k]
|
||||
if (result === undefined) return key
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
{
|
||||
"nav": {
|
||||
"home": "首页",
|
||||
"quickStart": "快速开始",
|
||||
"guide": "指南",
|
||||
"api": "API",
|
||||
"examples": "示例",
|
||||
"workerDemo": "Worker系统演示",
|
||||
"lawnMowerDemo": "割草机演示",
|
||||
"changelog": "更新日志"
|
||||
},
|
||||
"sidebar": {
|
||||
"gettingStarted": "开始使用",
|
||||
"quickStart": "快速开始",
|
||||
"guideOverview": "指南概览",
|
||||
"coreConcepts": "核心概念",
|
||||
"entity": "实体类 (Entity)",
|
||||
"hierarchy": "层级系统 (Hierarchy)",
|
||||
"component": "组件系统 (Component)",
|
||||
"entityQuery": "实体查询系统",
|
||||
"system": "系统架构 (System)",
|
||||
"workerSystem": "Worker系统 (多线程)",
|
||||
"scene": "场景管理 (Scene)",
|
||||
"sceneManager": "SceneManager",
|
||||
"worldManager": "WorldManager",
|
||||
"behaviorTree": "行为树系统 (Behavior Tree)",
|
||||
"btGettingStarted": "快速开始",
|
||||
"btCoreConcepts": "核心概念",
|
||||
"btEditorGuide": "编辑器指南",
|
||||
"btEditorWorkflow": "编辑器工作流",
|
||||
"btCustomActions": "自定义动作组件",
|
||||
"btCocosIntegration": "Cocos Creator集成",
|
||||
"btLayaIntegration": "Laya引擎集成",
|
||||
"btAdvancedUsage": "高级用法",
|
||||
"btBestPractices": "最佳实践",
|
||||
"serialization": "序列化系统 (Serialization)",
|
||||
"eventSystem": "事件系统 (Event)",
|
||||
"timeAndTimers": "时间和定时器 (Time)",
|
||||
"logging": "日志系统 (Logger)",
|
||||
"advancedFeatures": "高级特性",
|
||||
"serviceContainer": "服务容器 (Service Container)",
|
||||
"pluginSystem": "插件系统 (Plugin System)",
|
||||
"platformAdapters": "平台适配器",
|
||||
"browserAdapter": "浏览器适配器",
|
||||
"wechatAdapter": "微信小游戏适配器",
|
||||
"nodejsAdapter": "Node.js适配器",
|
||||
"examples": "示例",
|
||||
"examplesOverview": "示例概览",
|
||||
"apiReference": "API 参考",
|
||||
"overview": "概述",
|
||||
"coreClasses": "核心类",
|
||||
"systemClasses": "系统类",
|
||||
"utilities": "工具类",
|
||||
"interfaces": "接口",
|
||||
"decorators": "装饰器",
|
||||
"enums": "枚举"
|
||||
},
|
||||
"home": {
|
||||
"title": "ESEngine - 高性能 TypeScript ECS 框架",
|
||||
"quickLinks": "快速入口",
|
||||
"viewDocs": "查看文档",
|
||||
"getStarted": "快速开始",
|
||||
"getStartedDesc": "从安装到创建第一个 ECS 应用,快速了解核心概念。",
|
||||
"aiSystem": "AI 系统",
|
||||
"behaviorTreeEditor": "行为树可视化编辑器",
|
||||
"behaviorTreeDesc": "内置 AI 行为树系统,支持可视化编辑和实时调试。",
|
||||
"coreFeatures": "核心特性",
|
||||
"ecsArchitecture": "高性能 ECS 架构",
|
||||
"ecsArchitectureDesc": "基于数据驱动的实体组件系统,支持大规模实体处理,缓存友好的内存布局。",
|
||||
"typeSupport": "完整类型支持",
|
||||
"typeSupportDesc": "100% TypeScript 编写,完整的类型定义和编译时检查,提供最佳的开发体验。",
|
||||
"visualBehaviorTree": "可视化行为树",
|
||||
"visualBehaviorTreeDesc": "内置 AI 行为树系统,提供可视化编辑器,支持自定义节点和实时调试。",
|
||||
"multiPlatform": "多平台支持",
|
||||
"multiPlatformDesc": "支持浏览器、Node.js、微信小游戏等多平台,可与主流游戏引擎无缝集成。",
|
||||
"modularDesign": "模块化设计",
|
||||
"modularDesignDesc": "核心功能独立打包,按需引入。支持自定义插件扩展,灵活适配不同项目。",
|
||||
"devTools": "开发者工具",
|
||||
"devToolsDesc": "内置性能监控、调试工具、序列化系统等,提供完整的开发工具链。",
|
||||
"learnMore": "了解更多 →"
|
||||
},
|
||||
"common": {
|
||||
"editOnGithub": "在 GitHub 上编辑此页",
|
||||
"onThisPage": "在这个页面上"
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
title: String,
|
||||
description: String,
|
||||
icon: String,
|
||||
link: String,
|
||||
image: String
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a :href="link" class="feature-card">
|
||||
<div class="card-image" v-if="image">
|
||||
<img :src="image" :alt="title" />
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="card-icon" v-if="icon && !image">{{ icon }}</div>
|
||||
<h3 class="card-title">{{ title }}</h3>
|
||||
<p class="card-description">{{ description }}</p>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.feature-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--es-bg-elevated, #252526);
|
||||
border: 1px solid var(--es-border-default, #3e3e42);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
border-color: var(--es-primary, #007acc);
|
||||
background: var(--es-bg-overlay, #2d2d2d);
|
||||
}
|
||||
|
||||
.card-image {
|
||||
width: 100%;
|
||||
height: 160px;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #1e3a5f 0%, #1e1e1e 100%);
|
||||
}
|
||||
|
||||
.card-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.feature-card:hover .card-image img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 16px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 12px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--es-bg-input, #3c3c3c);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--es-text-inverse, #ffffff);
|
||||
margin: 0 0 8px 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.card-description {
|
||||
font-size: 12px;
|
||||
color: var(--es-text-secondary, #9d9d9d);
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -1,422 +0,0 @@
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
const canvasRef = ref(null)
|
||||
let animationId = null
|
||||
let particles = []
|
||||
let animationStartTime = null
|
||||
let glowStartTime = null
|
||||
|
||||
// ESEngine 粒子颜色 - VS Code 风格配色(与编辑器统一)
|
||||
const colors = ['#569CD6', '#4EC9B0', '#9CDCFE', '#C586C0', '#DCDCAA']
|
||||
|
||||
class Particle {
|
||||
constructor(x, y, targetX, targetY) {
|
||||
this.x = x
|
||||
this.y = y
|
||||
this.targetX = targetX
|
||||
this.targetY = targetY
|
||||
this.size = Math.random() * 2 + 1.5
|
||||
this.alpha = Math.random() * 0.5 + 0.5
|
||||
this.color = colors[Math.floor(Math.random() * colors.length)]
|
||||
}
|
||||
}
|
||||
|
||||
function createParticles(canvas, text, fontSize) {
|
||||
const tempCanvas = document.createElement('canvas')
|
||||
const tempCtx = tempCanvas.getContext('2d')
|
||||
if (!tempCtx) return []
|
||||
|
||||
tempCtx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||
const textMetrics = tempCtx.measureText(text)
|
||||
const textWidth = textMetrics.width
|
||||
const textHeight = fontSize
|
||||
|
||||
tempCanvas.width = textWidth + 40
|
||||
tempCanvas.height = textHeight + 40
|
||||
tempCtx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||
tempCtx.textAlign = 'center'
|
||||
tempCtx.textBaseline = 'middle'
|
||||
tempCtx.fillStyle = '#ffffff'
|
||||
tempCtx.fillText(text, tempCanvas.width / 2, tempCanvas.height / 2)
|
||||
|
||||
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height)
|
||||
const pixels = imageData.data
|
||||
const newParticles = []
|
||||
const gap = 3
|
||||
|
||||
const width = canvas.width / (window.devicePixelRatio || 1)
|
||||
const height = canvas.height / (window.devicePixelRatio || 1)
|
||||
const offsetX = (width - tempCanvas.width) / 2
|
||||
const offsetY = (height - tempCanvas.height) / 2
|
||||
|
||||
for (let y = 0; y < tempCanvas.height; y += gap) {
|
||||
for (let x = 0; x < tempCanvas.width; x += gap) {
|
||||
const index = (y * tempCanvas.width + x) * 4
|
||||
const alpha = pixels[index + 3] || 0
|
||||
|
||||
if (alpha > 128) {
|
||||
const angle = Math.random() * Math.PI * 2
|
||||
const distance = Math.random() * Math.max(width, height)
|
||||
|
||||
newParticles.push(new Particle(
|
||||
width / 2 + Math.cos(angle) * distance,
|
||||
height / 2 + Math.sin(angle) * distance,
|
||||
offsetX + x,
|
||||
offsetY + y
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newParticles
|
||||
}
|
||||
|
||||
function easeOutQuart(t) {
|
||||
return 1 - Math.pow(1 - t, 4)
|
||||
}
|
||||
|
||||
function easeOutCubic(t) {
|
||||
return 1 - Math.pow(1 - t, 3)
|
||||
}
|
||||
|
||||
function animate(canvas, ctx) {
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
const width = canvas.width / dpr
|
||||
const height = canvas.height / dpr
|
||||
|
||||
const currentTime = performance.now()
|
||||
const duration = 2500
|
||||
const glowDuration = 600
|
||||
|
||||
const elapsed = currentTime - animationStartTime
|
||||
const progress = Math.min(elapsed / duration, 1)
|
||||
const easedProgress = easeOutQuart(progress)
|
||||
|
||||
// 透明背景
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
|
||||
// 计算发光进度
|
||||
let glowProgress = 0
|
||||
if (progress >= 1) {
|
||||
if (glowStartTime === null) {
|
||||
glowStartTime = currentTime
|
||||
}
|
||||
glowProgress = Math.min((currentTime - glowStartTime) / glowDuration, 1)
|
||||
glowProgress = easeOutCubic(glowProgress)
|
||||
}
|
||||
|
||||
const text = 'ESEngine'
|
||||
const fontSize = Math.min(width / 4, height / 3, 80)
|
||||
const textY = height / 2
|
||||
|
||||
for (const particle of particles) {
|
||||
const moveProgress = Math.min(easedProgress * 1.2, 1)
|
||||
const currentX = particle.x + (particle.targetX - particle.x) * moveProgress
|
||||
const currentY = particle.y + (particle.targetY - particle.y) * moveProgress
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.arc(currentX, currentY, particle.size, 0, Math.PI * 2)
|
||||
ctx.fillStyle = particle.color
|
||||
ctx.globalAlpha = particle.alpha * (1 - glowProgress * 0.3)
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
ctx.globalAlpha = 1
|
||||
|
||||
if (glowProgress > 0) {
|
||||
ctx.save()
|
||||
ctx.shadowColor = '#3b9eff'
|
||||
ctx.shadowBlur = 30 * glowProgress
|
||||
ctx.fillStyle = `rgba(255, 255, 255, ${glowProgress})`
|
||||
ctx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillText(text, width / 2, textY)
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
if (glowProgress >= 1) {
|
||||
const breathe = 0.8 + Math.sin(currentTime / 1000) * 0.2
|
||||
ctx.save()
|
||||
ctx.shadowColor = '#3b9eff'
|
||||
ctx.shadowBlur = 20 * breathe
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillText(text, width / 2, textY)
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
animationId = requestAnimationFrame(() => animate(canvas, ctx))
|
||||
}
|
||||
|
||||
function initCanvas() {
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas) return
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
const container = canvas.parentElement
|
||||
const width = container.offsetWidth
|
||||
const height = container.offsetHeight
|
||||
|
||||
canvas.width = width * dpr
|
||||
canvas.height = height * dpr
|
||||
canvas.style.width = `${width}px`
|
||||
canvas.style.height = `${height}px`
|
||||
ctx.scale(dpr, dpr)
|
||||
|
||||
const text = 'ESEngine'
|
||||
const fontSize = Math.min(width / 4, height / 3, 80)
|
||||
|
||||
particles = createParticles(canvas, text, fontSize)
|
||||
animationStartTime = performance.now()
|
||||
glowStartTime = null
|
||||
|
||||
if (animationId) {
|
||||
cancelAnimationFrame(animationId)
|
||||
}
|
||||
|
||||
animate(canvas, ctx)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initCanvas()
|
||||
window.addEventListener('resize', initCanvas)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (animationId) {
|
||||
cancelAnimationFrame(animationId)
|
||||
}
|
||||
window.removeEventListener('resize', initCanvas)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="hero-section">
|
||||
<div class="hero-container">
|
||||
<!-- 左侧文字区域 -->
|
||||
<div class="hero-text">
|
||||
<div class="hero-logo">
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="16" cy="16" r="14" stroke="#9147ff" stroke-width="2"/>
|
||||
<path d="M10 10h8v2h-6v3h5v2h-5v3h6v2h-8v-12z" fill="#9147ff"/>
|
||||
</svg>
|
||||
<span>ESENGINE</span>
|
||||
</div>
|
||||
<h1 class="hero-title">
|
||||
我们构建框架。<br/>
|
||||
而你将创造游戏。
|
||||
</h1>
|
||||
<p class="hero-description">
|
||||
ESEngine 是一个高性能的 TypeScript ECS 框架,为游戏开发者提供现代化的实体组件系统。
|
||||
无论是 2D 还是 3D 游戏,都能帮助你快速构建可扩展的游戏架构。
|
||||
</p>
|
||||
<div class="hero-actions">
|
||||
<a href="/guide/getting-started" class="btn-primary">开始使用</a>
|
||||
<a href="https://github.com/esengine/ecs-framework" class="btn-secondary" target="_blank">了解更多</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧粒子动画区域 -->
|
||||
<div class="hero-visual">
|
||||
<div class="visual-container">
|
||||
<canvas ref="canvasRef" class="particle-canvas"></canvas>
|
||||
<div class="visual-label">
|
||||
<span class="label-title">Entity Component System</span>
|
||||
<span class="label-subtitle">High Performance Framework</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.hero-section {
|
||||
background: #0d0d0d;
|
||||
padding: 80px 0;
|
||||
min-height: calc(100vh - 64px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.hero-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 48px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.2fr;
|
||||
gap: 64px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 左侧文字 */
|
||||
.hero-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.hero-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: #ffffff;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
line-height: 1.2;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.hero-description {
|
||||
font-size: 1.125rem;
|
||||
color: #707070;
|
||||
line-height: 1.7;
|
||||
margin: 0;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.btn-primary,
|
||||
.btn-secondary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 14px 28px;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
font-size: 0.9375rem;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #3b9eff;
|
||||
color: #ffffff;
|
||||
border: 1px solid #3b9eff;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #5aadff;
|
||||
border-color: #5aadff;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #1a1a1a;
|
||||
color: #a0a0a0;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #252525;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.hero-visual {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.visual-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
aspect-ratio: 4 / 3;
|
||||
background: linear-gradient(135deg, #1a2a3a 0%, #1a1a1a 50%, #0d0d0d 100%);
|
||||
border-radius: 12px;
|
||||
border: 1px solid #2a2a2a;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20px 60px rgba(59, 158, 255, 0.1);
|
||||
}
|
||||
|
||||
.particle-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.visual-label {
|
||||
position: absolute;
|
||||
bottom: 24px;
|
||||
left: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.label-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.label-subtitle {
|
||||
font-size: 0.875rem;
|
||||
color: #737373;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 1024px) {
|
||||
.hero-container {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 48px;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.hero-section {
|
||||
padding: 48px 0;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 2.25rem;
|
||||
}
|
||||
|
||||
.hero-description {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.visual-container {
|
||||
max-width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.hero-title {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn-primary,
|
||||
.btn-secondary {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,422 +0,0 @@
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
const canvasRef = ref(null)
|
||||
let animationId = null
|
||||
let particles = []
|
||||
let animationStartTime = null
|
||||
let glowStartTime = null
|
||||
|
||||
// ESEngine particle colors - VS Code style colors (unified with editor)
|
||||
const colors = ['#569CD6', '#4EC9B0', '#9CDCFE', '#C586C0', '#DCDCAA']
|
||||
|
||||
class Particle {
|
||||
constructor(x, y, targetX, targetY) {
|
||||
this.x = x
|
||||
this.y = y
|
||||
this.targetX = targetX
|
||||
this.targetY = targetY
|
||||
this.size = Math.random() * 2 + 1.5
|
||||
this.alpha = Math.random() * 0.5 + 0.5
|
||||
this.color = colors[Math.floor(Math.random() * colors.length)]
|
||||
}
|
||||
}
|
||||
|
||||
function createParticles(canvas, text, fontSize) {
|
||||
const tempCanvas = document.createElement('canvas')
|
||||
const tempCtx = tempCanvas.getContext('2d')
|
||||
if (!tempCtx) return []
|
||||
|
||||
tempCtx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||
const textMetrics = tempCtx.measureText(text)
|
||||
const textWidth = textMetrics.width
|
||||
const textHeight = fontSize
|
||||
|
||||
tempCanvas.width = textWidth + 40
|
||||
tempCanvas.height = textHeight + 40
|
||||
tempCtx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||
tempCtx.textAlign = 'center'
|
||||
tempCtx.textBaseline = 'middle'
|
||||
tempCtx.fillStyle = '#ffffff'
|
||||
tempCtx.fillText(text, tempCanvas.width / 2, tempCanvas.height / 2)
|
||||
|
||||
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height)
|
||||
const pixels = imageData.data
|
||||
const newParticles = []
|
||||
const gap = 3
|
||||
|
||||
const width = canvas.width / (window.devicePixelRatio || 1)
|
||||
const height = canvas.height / (window.devicePixelRatio || 1)
|
||||
const offsetX = (width - tempCanvas.width) / 2
|
||||
const offsetY = (height - tempCanvas.height) / 2
|
||||
|
||||
for (let y = 0; y < tempCanvas.height; y += gap) {
|
||||
for (let x = 0; x < tempCanvas.width; x += gap) {
|
||||
const index = (y * tempCanvas.width + x) * 4
|
||||
const alpha = pixels[index + 3] || 0
|
||||
|
||||
if (alpha > 128) {
|
||||
const angle = Math.random() * Math.PI * 2
|
||||
const distance = Math.random() * Math.max(width, height)
|
||||
|
||||
newParticles.push(new Particle(
|
||||
width / 2 + Math.cos(angle) * distance,
|
||||
height / 2 + Math.sin(angle) * distance,
|
||||
offsetX + x,
|
||||
offsetY + y
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newParticles
|
||||
}
|
||||
|
||||
function easeOutQuart(t) {
|
||||
return 1 - Math.pow(1 - t, 4)
|
||||
}
|
||||
|
||||
function easeOutCubic(t) {
|
||||
return 1 - Math.pow(1 - t, 3)
|
||||
}
|
||||
|
||||
function animate(canvas, ctx) {
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
const width = canvas.width / dpr
|
||||
const height = canvas.height / dpr
|
||||
|
||||
const currentTime = performance.now()
|
||||
const duration = 2500
|
||||
const glowDuration = 600
|
||||
|
||||
const elapsed = currentTime - animationStartTime
|
||||
const progress = Math.min(elapsed / duration, 1)
|
||||
const easedProgress = easeOutQuart(progress)
|
||||
|
||||
// Transparent background
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
|
||||
// Calculate glow progress
|
||||
let glowProgress = 0
|
||||
if (progress >= 1) {
|
||||
if (glowStartTime === null) {
|
||||
glowStartTime = currentTime
|
||||
}
|
||||
glowProgress = Math.min((currentTime - glowStartTime) / glowDuration, 1)
|
||||
glowProgress = easeOutCubic(glowProgress)
|
||||
}
|
||||
|
||||
const text = 'ESEngine'
|
||||
const fontSize = Math.min(width / 4, height / 3, 80)
|
||||
const textY = height / 2
|
||||
|
||||
for (const particle of particles) {
|
||||
const moveProgress = Math.min(easedProgress * 1.2, 1)
|
||||
const currentX = particle.x + (particle.targetX - particle.x) * moveProgress
|
||||
const currentY = particle.y + (particle.targetY - particle.y) * moveProgress
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.arc(currentX, currentY, particle.size, 0, Math.PI * 2)
|
||||
ctx.fillStyle = particle.color
|
||||
ctx.globalAlpha = particle.alpha * (1 - glowProgress * 0.3)
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
ctx.globalAlpha = 1
|
||||
|
||||
if (glowProgress > 0) {
|
||||
ctx.save()
|
||||
ctx.shadowColor = '#3b9eff'
|
||||
ctx.shadowBlur = 30 * glowProgress
|
||||
ctx.fillStyle = `rgba(255, 255, 255, ${glowProgress})`
|
||||
ctx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillText(text, width / 2, textY)
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
if (glowProgress >= 1) {
|
||||
const breathe = 0.8 + Math.sin(currentTime / 1000) * 0.2
|
||||
ctx.save()
|
||||
ctx.shadowColor = '#3b9eff'
|
||||
ctx.shadowBlur = 20 * breathe
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillText(text, width / 2, textY)
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
animationId = requestAnimationFrame(() => animate(canvas, ctx))
|
||||
}
|
||||
|
||||
function initCanvas() {
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas) return
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
const container = canvas.parentElement
|
||||
const width = container.offsetWidth
|
||||
const height = container.offsetHeight
|
||||
|
||||
canvas.width = width * dpr
|
||||
canvas.height = height * dpr
|
||||
canvas.style.width = `${width}px`
|
||||
canvas.style.height = `${height}px`
|
||||
ctx.scale(dpr, dpr)
|
||||
|
||||
const text = 'ESEngine'
|
||||
const fontSize = Math.min(width / 4, height / 3, 80)
|
||||
|
||||
particles = createParticles(canvas, text, fontSize)
|
||||
animationStartTime = performance.now()
|
||||
glowStartTime = null
|
||||
|
||||
if (animationId) {
|
||||
cancelAnimationFrame(animationId)
|
||||
}
|
||||
|
||||
animate(canvas, ctx)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initCanvas()
|
||||
window.addEventListener('resize', initCanvas)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (animationId) {
|
||||
cancelAnimationFrame(animationId)
|
||||
}
|
||||
window.removeEventListener('resize', initCanvas)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="hero-section">
|
||||
<div class="hero-container">
|
||||
<!-- Left text area -->
|
||||
<div class="hero-text">
|
||||
<div class="hero-logo">
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="16" cy="16" r="14" stroke="#9147ff" stroke-width="2"/>
|
||||
<path d="M10 10h8v2h-6v3h5v2h-5v3h6v2h-8v-12z" fill="#9147ff"/>
|
||||
</svg>
|
||||
<span>ESENGINE</span>
|
||||
</div>
|
||||
<h1 class="hero-title">
|
||||
We build the framework.<br/>
|
||||
You create the game.
|
||||
</h1>
|
||||
<p class="hero-description">
|
||||
ESEngine is a high-performance TypeScript ECS framework for game developers.
|
||||
Whether 2D or 3D games, it helps you build scalable game architecture quickly.
|
||||
</p>
|
||||
<div class="hero-actions">
|
||||
<a href="/en/guide/getting-started" class="btn-primary">Get Started</a>
|
||||
<a href="https://github.com/esengine/ecs-framework" class="btn-secondary" target="_blank">Learn More</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right particle animation area -->
|
||||
<div class="hero-visual">
|
||||
<div class="visual-container">
|
||||
<canvas ref="canvasRef" class="particle-canvas"></canvas>
|
||||
<div class="visual-label">
|
||||
<span class="label-title">Entity Component System</span>
|
||||
<span class="label-subtitle">High Performance Framework</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.hero-section {
|
||||
background: #0d0d0d;
|
||||
padding: 80px 0;
|
||||
min-height: calc(100vh - 64px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.hero-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 48px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.2fr;
|
||||
gap: 64px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Left text */
|
||||
.hero-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.hero-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: #ffffff;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
line-height: 1.2;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.hero-description {
|
||||
font-size: 1.125rem;
|
||||
color: #707070;
|
||||
line-height: 1.7;
|
||||
margin: 0;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.btn-primary,
|
||||
.btn-secondary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 14px 28px;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
font-size: 0.9375rem;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #3b9eff;
|
||||
color: #ffffff;
|
||||
border: 1px solid #3b9eff;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #5aadff;
|
||||
border-color: #5aadff;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #1a1a1a;
|
||||
color: #a0a0a0;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #252525;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.hero-visual {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.visual-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
aspect-ratio: 4 / 3;
|
||||
background: linear-gradient(135deg, #1a2a3a 0%, #1a1a1a 50%, #0d0d0d 100%);
|
||||
border-radius: 12px;
|
||||
border: 1px solid #2a2a2a;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20px 60px rgba(59, 158, 255, 0.1);
|
||||
}
|
||||
|
||||
.particle-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.visual-label {
|
||||
position: absolute;
|
||||
bottom: 24px;
|
||||
left: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.label-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.label-subtitle {
|
||||
font-size: 0.875rem;
|
||||
color: #737373;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 1024px) {
|
||||
.hero-container {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 48px;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.hero-section {
|
||||
padding: 48px 0;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 2.25rem;
|
||||
}
|
||||
|
||||
.hero-description {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.visual-container {
|
||||
max-width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.hero-title {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn-primary,
|
||||
.btn-secondary {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,594 +0,0 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--vp-nav-height: 64px;
|
||||
|
||||
--es-bg-base: #1e1e1e;
|
||||
--es-bg-elevated: #252526;
|
||||
--es-bg-overlay: #2d2d2d;
|
||||
--es-bg-input: #3c3c3c;
|
||||
--es-bg-inset: #181818;
|
||||
--es-bg-hover: #2a2d2e;
|
||||
--es-bg-active: #37373d;
|
||||
--es-bg-sidebar: #262626;
|
||||
--es-bg-card: #2a2a2a;
|
||||
--es-bg-header: #2d2d2d;
|
||||
|
||||
--es-text-primary: #cccccc;
|
||||
--es-text-secondary: #9d9d9d;
|
||||
--es-text-tertiary: #6a6a6a;
|
||||
--es-text-inverse: #ffffff;
|
||||
--es-text-muted: #aaaaaa;
|
||||
--es-text-dim: #6a6a6a;
|
||||
|
||||
--es-font-xs: 11px;
|
||||
--es-font-sm: 12px;
|
||||
--es-font-base: 13px;
|
||||
--es-font-md: 14px;
|
||||
--es-font-lg: 16px;
|
||||
|
||||
--es-border-default: #3a3a3a;
|
||||
--es-border-subtle: #1a1a1a;
|
||||
--es-border-strong: #4a4a4a;
|
||||
|
||||
--es-primary: #3b82f6;
|
||||
--es-primary-hover: #2563eb;
|
||||
--es-success: #4ade80;
|
||||
--es-warning: #f59e0b;
|
||||
--es-error: #ef4444;
|
||||
--es-info: #3b82f6;
|
||||
|
||||
--es-selected: #3d5a80;
|
||||
--es-selected-hover: #4a6a90;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--es-bg-base) !important;
|
||||
}
|
||||
|
||||
html,
|
||||
html.dark {
|
||||
--vp-c-bg: var(--es-bg-base);
|
||||
--vp-c-bg-soft: var(--es-bg-elevated);
|
||||
--vp-c-bg-mute: var(--es-bg-overlay);
|
||||
--vp-c-bg-alt: var(--es-bg-sidebar);
|
||||
--vp-c-text-1: var(--es-text-primary);
|
||||
--vp-c-text-2: var(--es-text-tertiary);
|
||||
--vp-c-text-3: var(--es-text-muted);
|
||||
--vp-c-divider: var(--es-border-default);
|
||||
--vp-c-divider-light: var(--es-border-subtle);
|
||||
}
|
||||
|
||||
html:not(.dark) {
|
||||
--vp-c-bg: var(--es-bg-base) !important;
|
||||
--vp-c-bg-soft: var(--es-bg-elevated) !important;
|
||||
--vp-c-bg-mute: var(--es-bg-overlay) !important;
|
||||
--vp-c-bg-alt: var(--es-bg-sidebar) !important;
|
||||
--vp-c-text-1: var(--es-text-primary) !important;
|
||||
--vp-c-text-2: var(--es-text-tertiary) !important;
|
||||
--vp-c-text-3: var(--es-text-muted) !important;
|
||||
}
|
||||
|
||||
.VPNav {
|
||||
background: var(--es-bg-header) !important;
|
||||
border-bottom: 1px solid var(--es-border-subtle) !important;
|
||||
}
|
||||
|
||||
.VPNav .VPNavBar {
|
||||
background: var(--es-bg-header) !important;
|
||||
}
|
||||
|
||||
.VPNav .VPNavBar .wrapper {
|
||||
background: var(--es-bg-header) !important;
|
||||
}
|
||||
|
||||
.VPNav .VPNavBar::before,
|
||||
.VPNav .VPNavBar::after {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.VPNavBar {
|
||||
background: var(--es-bg-header) !important;
|
||||
}
|
||||
|
||||
.VPNavBar::before {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.VPNavBarTitle .title {
|
||||
color: var(--es-text-primary);
|
||||
font-weight: 500;
|
||||
font-size: var(--es-font-base);
|
||||
}
|
||||
|
||||
.VPNavBarMenuLink {
|
||||
color: var(--es-text-secondary) !important;
|
||||
font-size: var(--es-font-sm) !important;
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
|
||||
.VPNavBarMenuLink:hover {
|
||||
color: var(--es-text-primary) !important;
|
||||
}
|
||||
|
||||
.VPNavBarMenuLink.active {
|
||||
color: var(--es-text-primary) !important;
|
||||
}
|
||||
|
||||
.VPNavBarSearch .DocSearch-Button {
|
||||
background: var(--es-bg-input) !important;
|
||||
border: 1px solid var(--es-border-default) !important;
|
||||
border-radius: 2px;
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
.VPSidebar {
|
||||
background: var(--es-bg-sidebar) !important;
|
||||
border-right: 1px solid var(--es-border-subtle) !important;
|
||||
}
|
||||
|
||||
.VPSidebarItem.level-0 > .item {
|
||||
padding: 8px 0 4px 0;
|
||||
}
|
||||
|
||||
.VPSidebarItem.level-0 > .item > .text {
|
||||
font-weight: 600;
|
||||
font-size: var(--es-font-xs);
|
||||
color: var(--es-text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.VPSidebarItem .link {
|
||||
padding: 4px 8px;
|
||||
margin: 1px 0;
|
||||
border-radius: 2px;
|
||||
color: var(--es-text-primary);
|
||||
font-size: var(--es-font-sm);
|
||||
transition: all 0.1s ease;
|
||||
border-left: 2px solid transparent;
|
||||
}
|
||||
|
||||
.VPSidebarItem .link:hover {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
color: var(--es-text-inverse);
|
||||
}
|
||||
|
||||
.VPSidebarItem.is-active > .item > .link {
|
||||
background: var(--es-selected);
|
||||
color: var(--es-text-inverse);
|
||||
border-left: 2px solid var(--es-primary);
|
||||
}
|
||||
|
||||
.VPSidebarItem.is-active > .item > .link:hover {
|
||||
background: var(--es-selected-hover);
|
||||
}
|
||||
|
||||
.VPSidebarItem.level-1 .link {
|
||||
padding-left: 20px;
|
||||
font-size: var(--es-font-sm);
|
||||
}
|
||||
|
||||
.VPSidebarItem.level-2 .link {
|
||||
padding-left: 32px;
|
||||
font-size: var(--es-font-sm);
|
||||
}
|
||||
|
||||
.VPSidebarItem .caret {
|
||||
color: var(--es-text-secondary);
|
||||
}
|
||||
|
||||
.VPSidebarItem .caret:hover {
|
||||
color: var(--es-text-primary);
|
||||
}
|
||||
|
||||
.VPContent {
|
||||
background: var(--es-bg-card) !important;
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
.VPContent.has-sidebar {
|
||||
background: var(--es-bg-card) !important;
|
||||
}
|
||||
|
||||
/* 首页布局修复 | Home page layout fix */
|
||||
.VPPage {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
.Layout > .VPContent {
|
||||
padding-top: var(--vp-nav-height) !important;
|
||||
}
|
||||
|
||||
.VPDoc {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.VPNavBar .content {
|
||||
background: var(--es-bg-header) !important;
|
||||
}
|
||||
|
||||
.VPNavBar .content-body {
|
||||
background: var(--es-bg-header) !important;
|
||||
}
|
||||
|
||||
.VPNavBar .divider {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.VPLocalNav {
|
||||
background: var(--es-bg-header) !important;
|
||||
border-bottom: 1px solid var(--es-border-subtle) !important;
|
||||
}
|
||||
|
||||
.VPNavScreenMenu {
|
||||
background: var(--es-bg-base) !important;
|
||||
}
|
||||
|
||||
.VPNavScreen {
|
||||
background: var(--es-bg-base) !important;
|
||||
}
|
||||
|
||||
.curtain {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.VPNav .curtain,
|
||||
.VPNavBar .curtain {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
[class*="curtain"] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.VPNav > div::before,
|
||||
.VPNav > div::after {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.vp-doc {
|
||||
color: var(--es-text-primary);
|
||||
}
|
||||
|
||||
.vp-doc h1 {
|
||||
font-size: var(--es-font-lg);
|
||||
font-weight: 600;
|
||||
color: var(--es-text-inverse);
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
margin-bottom: 16px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.vp-doc h2 {
|
||||
font-size: var(--es-font-md);
|
||||
font-weight: 600;
|
||||
color: var(--es-text-inverse);
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
margin-top: 32px;
|
||||
margin-bottom: 12px;
|
||||
padding: 6px 12px;
|
||||
background: var(--es-bg-header);
|
||||
border-left: 3px solid var(--es-primary);
|
||||
}
|
||||
|
||||
.vp-doc h3 {
|
||||
font-size: var(--es-font-base);
|
||||
font-weight: 600;
|
||||
color: var(--es-text-primary);
|
||||
margin-top: 20px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.vp-doc p {
|
||||
color: var(--es-text-primary);
|
||||
line-height: 1.7;
|
||||
font-size: var(--es-font-base);
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.vp-doc ul,
|
||||
.vp-doc ol {
|
||||
padding-left: 20px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.vp-doc li {
|
||||
line-height: 1.7;
|
||||
margin: 4px 0;
|
||||
color: var(--es-text-primary);
|
||||
font-size: var(--es-font-base);
|
||||
}
|
||||
|
||||
.vp-doc li::marker {
|
||||
color: var(--es-text-secondary);
|
||||
}
|
||||
|
||||
.vp-doc strong {
|
||||
color: var(--es-text-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.vp-doc a {
|
||||
color: var(--es-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.vp-doc a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.VPDocAside {
|
||||
padding-left: 16px;
|
||||
border-left: 1px solid var(--es-border-subtle);
|
||||
}
|
||||
|
||||
.VPDocAsideOutline {
|
||||
padding: 0;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.VPDocAsideOutline .content {
|
||||
border: none !important;
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
.VPDocAsideOutline .outline-title {
|
||||
font-size: var(--es-font-xs);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--es-text-secondary);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.VPDocAsideOutline .outline-link {
|
||||
color: var(--es-text-secondary);
|
||||
font-size: var(--es-font-xs);
|
||||
padding: 4px 0;
|
||||
line-height: 1.4;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.VPDocAsideOutline .outline-link:hover {
|
||||
color: var(--es-text-primary);
|
||||
}
|
||||
|
||||
.VPDocAsideOutline .outline-link.active {
|
||||
color: var(--es-primary);
|
||||
}
|
||||
|
||||
.VPDocAsideOutline .outline-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div[class*='language-'] {
|
||||
background: var(--es-bg-inset) !important;
|
||||
border: 1px solid var(--es-border-default);
|
||||
border-radius: 2px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.vp-code-group .tabs {
|
||||
background: var(--es-bg-header);
|
||||
border-bottom: 1px solid var(--es-border-subtle);
|
||||
}
|
||||
|
||||
.vp-doc :not(pre) > code {
|
||||
background: var(--es-bg-input);
|
||||
color: var(--es-primary);
|
||||
padding: 2px 6px;
|
||||
border-radius: 2px;
|
||||
font-size: var(--es-font-xs);
|
||||
}
|
||||
|
||||
.vp-doc table {
|
||||
display: table;
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-collapse: collapse;
|
||||
margin: 16px 0;
|
||||
font-size: var(--es-font-sm);
|
||||
}
|
||||
|
||||
.vp-doc tr {
|
||||
border-bottom: 1px solid var(--es-border-subtle);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.vp-doc tr:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.vp-doc tr:hover {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
.vp-doc th {
|
||||
background: var(--es-bg-header);
|
||||
font-weight: 600;
|
||||
font-size: var(--es-font-xs);
|
||||
color: var(--es-text-secondary);
|
||||
text-align: left;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--es-border-subtle);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.vp-doc td {
|
||||
font-size: var(--es-font-sm);
|
||||
color: var(--es-text-primary);
|
||||
padding: 8px 12px;
|
||||
vertical-align: top;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.vp-doc td:first-child {
|
||||
font-weight: 500;
|
||||
color: var(--es-text-primary);
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.vp-doc .warning,
|
||||
.vp-doc .custom-block.warning {
|
||||
background: rgba(245, 158, 11, 0.08);
|
||||
border: none;
|
||||
border-left: 3px solid var(--es-warning);
|
||||
border-radius: 0 2px 2px 0;
|
||||
padding: 10px 12px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.vp-doc .warning .custom-block-title,
|
||||
.vp-doc .custom-block.warning .custom-block-title {
|
||||
color: var(--es-warning);
|
||||
font-weight: 600;
|
||||
font-size: var(--es-font-xs);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.vp-doc .warning p {
|
||||
color: var(--es-text-primary);
|
||||
margin: 0;
|
||||
font-size: var(--es-font-xs);
|
||||
}
|
||||
|
||||
.vp-doc .tip,
|
||||
.vp-doc .custom-block.tip {
|
||||
background: rgba(59, 130, 246, 0.08);
|
||||
border: none;
|
||||
border-left: 3px solid var(--es-primary);
|
||||
border-radius: 0 2px 2px 0;
|
||||
padding: 10px 12px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.vp-doc .tip .custom-block-title,
|
||||
.vp-doc .custom-block.tip .custom-block-title {
|
||||
color: var(--es-primary);
|
||||
font-weight: 600;
|
||||
font-size: var(--es-font-xs);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.vp-doc .tip p {
|
||||
color: var(--es-text-primary);
|
||||
margin: 0;
|
||||
font-size: var(--es-font-xs);
|
||||
}
|
||||
|
||||
.vp-doc .info,
|
||||
.vp-doc .custom-block.info {
|
||||
background: rgba(74, 222, 128, 0.08);
|
||||
border: none;
|
||||
border-left: 3px solid var(--es-success);
|
||||
border-radius: 0 2px 2px 0;
|
||||
padding: 10px 12px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.vp-doc .info .custom-block-title,
|
||||
.vp-doc .custom-block.info .custom-block-title {
|
||||
color: var(--es-success);
|
||||
font-weight: 600;
|
||||
font-size: var(--es-font-xs);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.vp-doc .danger,
|
||||
.vp-doc .custom-block.danger {
|
||||
background: rgba(239, 68, 68, 0.08);
|
||||
border: none;
|
||||
border-left: 3px solid var(--es-error);
|
||||
border-radius: 0 2px 2px 0;
|
||||
padding: 10px 12px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.vp-doc .danger .custom-block-title,
|
||||
.vp-doc .custom-block.danger .custom-block-title {
|
||||
color: var(--es-error);
|
||||
font-weight: 600;
|
||||
font-size: var(--es-font-xs);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.vp-doc .card {
|
||||
background: var(--es-bg-sidebar);
|
||||
border: 1px solid var(--es-border-subtle);
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.vp-doc .card-title {
|
||||
font-size: var(--es-font-sm);
|
||||
font-weight: 600;
|
||||
color: var(--es-text-primary);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.vp-doc .card-description {
|
||||
font-size: var(--es-font-xs);
|
||||
color: var(--es-text-muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.vp-doc .tag {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
background: transparent;
|
||||
border: 1px solid var(--es-border-default);
|
||||
border-radius: 2px;
|
||||
color: var(--es-text-secondary);
|
||||
font-size: var(--es-font-xs);
|
||||
margin-right: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.VPFooter {
|
||||
background: var(--es-bg-sidebar) !important;
|
||||
border-top: 1px solid var(--es-border-subtle) !important;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--es-bg-card);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--es-border-strong);
|
||||
border-radius: 4px;
|
||||
border: 2px solid var(--es-bg-card);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #5a5a5a;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.home-container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.home-section {
|
||||
padding: 32px 0;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.VPDoc .content {
|
||||
padding: 16px !important;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
import ParticleHero from './components/ParticleHero.vue'
|
||||
import ParticleHeroEn from './components/ParticleHeroEn.vue'
|
||||
import FeatureCard from './components/FeatureCard.vue'
|
||||
import './custom.css'
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
enhanceApp({ app }) {
|
||||
app.component('ParticleHero', ParticleHero)
|
||||
app.component('ParticleHeroEn', ParticleHeroEn)
|
||||
app.component('FeatureCard', FeatureCard)
|
||||
}
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
本文档记录 `@esengine/ecs-framework` 核心库的版本更新历史。
|
||||
|
||||
---
|
||||
|
||||
## v2.2.21 (2025-12-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **迭代安全修复**: 修复 `process`/`lateProcess` 迭代时组件变化导致跳过实体的问题 (#272)
|
||||
- 在系统处理过程中添加/移除组件不再导致实体被意外跳过
|
||||
|
||||
### Performance
|
||||
|
||||
- **HierarchySystem 性能优化**: 优化层级系统避免每帧遍历所有实体 (#279)
|
||||
- 使用脏实体集合代替每帧遍历所有实体
|
||||
- 静态场景下 `process()` 从 O(n) 优化为 O(1)
|
||||
- 1000 实体静态场景: 81.79μs → 0.07μs (快 1168 倍)
|
||||
- 10000 实体静态场景: 939.43μs → 0.56μs (快 1677 倍)
|
||||
- 服务端模拟 (100房间 x 100实体): 2.7ms → 1.4ms 每 tick
|
||||
|
||||
---
|
||||
|
||||
## v2.2.20 (2025-12-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **系统 onAdded 回调修复**: 修复系统 `onAdded` 回调受注册顺序影响的问题 (#270)
|
||||
- 系统初始化时会对已存在的匹配实体触发 `onAdded` 回调
|
||||
- 新增 `matchesEntity(entity)` 方法,用于检查实体是否匹配系统的查询条件
|
||||
- Scene 新增 `notifySystemsEntityAdded/Removed` 方法,确保所有系统都能收到实体变更通知
|
||||
|
||||
---
|
||||
|
||||
## v2.2.19 (2025-12-03)
|
||||
|
||||
### Features
|
||||
|
||||
- **系统稳定排序**: 添加系统稳定排序支持 (#257)
|
||||
- 新增 `addOrder` 属性,用于 `updateOrder` 相同时的稳定排序
|
||||
- 确保相同优先级的系统按添加顺序执行
|
||||
|
||||
- **模块配置**: 添加 `module.json` 配置文件 (#256)
|
||||
- 定义模块 ID、名称、版本等元信息
|
||||
- 支持模块依赖声明和导出配置
|
||||
|
||||
---
|
||||
|
||||
## v2.2.18 (2025-11-30)
|
||||
|
||||
### Features
|
||||
|
||||
- **高级性能分析器**: 实现全新的性能分析 SDK (#248)
|
||||
- `ProfilerSDK`: 统一的性能分析接口
|
||||
- 手动采样标记 (`beginSample`/`endSample`)
|
||||
- 自动作用域测量 (`measure`/`measureAsync`)
|
||||
- 调用层级追踪和调用图生成
|
||||
- 计数器和仪表支持
|
||||
- `AdvancedProfilerCollector`: 高级性能数据收集器
|
||||
- 帧时间统计和历史记录
|
||||
- 内存快照和 GC 检测
|
||||
- 长任务检测 (Long Task API)
|
||||
- 性能报告生成
|
||||
- `DebugManager`: 调试管理器
|
||||
- 统一的调试工具入口
|
||||
- 性能分析器集成
|
||||
|
||||
- **属性装饰器增强**: 扩展 `@serialize` 装饰器功能 (#247)
|
||||
- 支持更多序列化选项配置
|
||||
|
||||
### Improvements
|
||||
|
||||
- **EntitySystem 测试覆盖**: 添加完整的系统测试用例 (#240)
|
||||
- 覆盖实体查询、缓存、生命周期等场景
|
||||
|
||||
- **Matcher 增强**: 优化匹配器功能 (#240)
|
||||
- 改进匹配逻辑和性能
|
||||
|
||||
---
|
||||
|
||||
## v2.2.17 (2025-11-28)
|
||||
|
||||
### Features
|
||||
|
||||
- **ComponentRegistry 增强**: 添加组件注册表新功能 (#244)
|
||||
- 支持通过名称注册和查询组件类型
|
||||
- 添加组件掩码缓存优化性能
|
||||
|
||||
- **序列化装饰器改进**: 增强 `@serialize` 装饰器 (#244)
|
||||
- 支持更灵活的序列化配置
|
||||
- 改进嵌套对象序列化
|
||||
|
||||
- **EntitySystem 生命周期**: 新增系统生命周期方法 (#244)
|
||||
- `onSceneStart()`: 场景开始时调用
|
||||
- `onSceneStop()`: 场景停止时调用
|
||||
|
||||
---
|
||||
|
||||
## v2.2.16 (2025-11-27)
|
||||
|
||||
### Features
|
||||
|
||||
- **组件生命周期**: 添加组件生命周期回调支持 (#237)
|
||||
- `onDeserialized()`: 组件从场景文件加载或快照恢复后调用,用于恢复运行时数据
|
||||
|
||||
- **ServiceContainer 增强**: 改进服务容器功能 (#237)
|
||||
- 支持 `Symbol.for()` 模式的服务标识
|
||||
- 新增 `@InjectProperty` 属性注入装饰器
|
||||
- 改进服务解析和生命周期管理
|
||||
|
||||
- **SceneSerializer 增强**: 场景序列化器新功能 (#237)
|
||||
- 支持更多组件类型的序列化
|
||||
- 改进反序列化错误处理
|
||||
|
||||
- **属性装饰器扩展**: 扩展 `@serialize` 装饰器 (#238)
|
||||
- 支持 `@range`、`@slider` 等编辑器提示
|
||||
- 支持 `@dropdown`、`@color` 等 UI 类型
|
||||
- 支持 `@asset` 资源引用类型
|
||||
|
||||
### Improvements
|
||||
|
||||
- **Matcher 测试**: 添加 Matcher 匹配器测试用例 (#240)
|
||||
- **EntitySystem 测试**: 添加实体系统完整测试覆盖 (#240)
|
||||
|
||||
---
|
||||
|
||||
## 版本说明
|
||||
|
||||
- **主版本号**: 重大不兼容更新
|
||||
- **次版本号**: 新功能添加(向后兼容)
|
||||
- **修订版本号**: Bug 修复和小改进
|
||||
|
||||
## 相关链接
|
||||
|
||||
- [GitHub Releases](https://github.com/esengine/ecs-framework/releases)
|
||||
- [NPM Package](https://www.npmjs.com/package/@esengine/ecs-framework)
|
||||
- [文档首页](./index.md)
|
||||
@@ -1,138 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
This document records the version update history of the `@esengine/ecs-framework` core library.
|
||||
|
||||
---
|
||||
|
||||
## v2.2.21 (2025-12-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Iteration safety fix**: Fix issue where component changes during `process`/`lateProcess` iteration caused entities to be skipped (#272)
|
||||
- Adding/removing components during system processing no longer causes entities to be unexpectedly skipped
|
||||
|
||||
### Performance
|
||||
|
||||
- **HierarchySystem optimization**: Optimize hierarchy system to avoid iterating all entities every frame (#279)
|
||||
- Use dirty entity set instead of iterating all entities
|
||||
- Static scene `process()` optimized from O(n) to O(1)
|
||||
- 1000 entities static scene: 81.79μs → 0.07μs (1168x faster)
|
||||
- 10000 entities static scene: 939.43μs → 0.56μs (1677x faster)
|
||||
- Server simulation (100 rooms x 100 entities): 2.7ms → 1.4ms per tick
|
||||
|
||||
---
|
||||
|
||||
## v2.2.20 (2025-12-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **System onAdded callback fix**: Fix issue where system `onAdded` callback was affected by registration order (#270)
|
||||
- System initialization now triggers `onAdded` callback for existing matching entities
|
||||
- Added `matchesEntity(entity)` method to check if an entity matches the system's query condition
|
||||
- Scene added `notifySystemsEntityAdded/Removed` methods to ensure all systems receive entity change notifications
|
||||
|
||||
---
|
||||
|
||||
## v2.2.19 (2025-12-03)
|
||||
|
||||
### Features
|
||||
|
||||
- **System stable sorting**: Add stable sorting support for systems (#257)
|
||||
- Added `addOrder` property for stable sorting when `updateOrder` is the same
|
||||
- Ensures systems with same priority execute in add order
|
||||
|
||||
- **Module configuration**: Add `module.json` configuration file (#256)
|
||||
- Define module ID, name, version and other metadata
|
||||
- Support module dependency declaration and export configuration
|
||||
|
||||
---
|
||||
|
||||
## v2.2.18 (2025-11-30)
|
||||
|
||||
### Features
|
||||
|
||||
- **Advanced Performance Profiler**: Implement new performance analysis SDK (#248)
|
||||
- `ProfilerSDK`: Unified performance analysis interface
|
||||
- Manual sampling markers (`beginSample`/`endSample`)
|
||||
- Automatic scope measurement (`measure`/`measureAsync`)
|
||||
- Call hierarchy tracking and call graph generation
|
||||
- Counter and gauge support
|
||||
- `AdvancedProfilerCollector`: Advanced performance data collector
|
||||
- Frame time statistics and history
|
||||
- Memory snapshots and GC detection
|
||||
- Long task detection (Long Task API)
|
||||
- Performance report generation
|
||||
- `DebugManager`: Debug manager
|
||||
- Unified debug tool entry point
|
||||
- Profiler integration
|
||||
|
||||
- **Property decorator enhancement**: Extend `@serialize` decorator functionality (#247)
|
||||
- Support more serialization option configurations
|
||||
|
||||
### Improvements
|
||||
|
||||
- **EntitySystem test coverage**: Add complete system test cases (#240)
|
||||
- Cover entity query, cache, lifecycle scenarios
|
||||
|
||||
- **Matcher enhancement**: Optimize matcher functionality (#240)
|
||||
- Improved matching logic and performance
|
||||
|
||||
---
|
||||
|
||||
## v2.2.17 (2025-11-28)
|
||||
|
||||
### Features
|
||||
|
||||
- **ComponentRegistry enhancement**: Add new component registry features (#244)
|
||||
- Support registering and querying component types by name
|
||||
- Add component mask caching for performance optimization
|
||||
|
||||
- **Serialization decorator improvements**: Enhance `@serialize` decorator (#244)
|
||||
- Support more flexible serialization configuration
|
||||
- Improved nested object serialization
|
||||
|
||||
- **EntitySystem lifecycle**: Add new system lifecycle methods (#244)
|
||||
- `onSceneStart()`: Called when scene starts
|
||||
- `onSceneStop()`: Called when scene stops
|
||||
|
||||
---
|
||||
|
||||
## v2.2.16 (2025-11-27)
|
||||
|
||||
### Features
|
||||
|
||||
- **Component lifecycle**: Add component lifecycle callback support (#237)
|
||||
- `onDeserialized()`: Called after component is loaded from scene file or snapshot restore, used to restore runtime data
|
||||
|
||||
- **ServiceContainer enhancement**: Improve service container functionality (#237)
|
||||
- Support `Symbol.for()` pattern for service identifiers
|
||||
- Added `@InjectProperty` property injection decorator
|
||||
- Improved service resolution and lifecycle management
|
||||
|
||||
- **SceneSerializer enhancement**: New scene serializer features (#237)
|
||||
- Support serialization of more component types
|
||||
- Improved deserialization error handling
|
||||
|
||||
- **Property decorator extension**: Extend `@serialize` decorator (#238)
|
||||
- Support `@range`, `@slider` and other editor hints
|
||||
- Support `@dropdown`, `@color` and other UI types
|
||||
- Support `@asset` resource reference type
|
||||
|
||||
### Improvements
|
||||
|
||||
- **Matcher tests**: Add Matcher test cases (#240)
|
||||
- **EntitySystem tests**: Add complete entity system test coverage (#240)
|
||||
|
||||
---
|
||||
|
||||
## Version Notes
|
||||
|
||||
- **Major version**: Breaking changes
|
||||
- **Minor version**: New features (backward compatible)
|
||||
- **Patch version**: Bug fixes and improvements
|
||||
|
||||
## Related Links
|
||||
|
||||
- [GitHub Releases](https://github.com/esengine/ecs-framework/releases)
|
||||
- [NPM Package](https://www.npmjs.com/package/@esengine/ecs-framework)
|
||||
- [Documentation Home](./index.md)
|
||||
@@ -1,412 +0,0 @@
|
||||
# Quick Start
|
||||
|
||||
This guide will help you get started with ECS Framework, from installation to creating your first ECS application.
|
||||
|
||||
## Installation
|
||||
|
||||
### NPM Installation
|
||||
|
||||
```bash
|
||||
# Using npm
|
||||
npm install @esengine/ecs-framework
|
||||
```
|
||||
|
||||
## Initialize Core
|
||||
|
||||
### Basic Initialization
|
||||
|
||||
The core of ECS Framework is the `Core` class, a singleton that manages the entire framework lifecycle.
|
||||
|
||||
```typescript
|
||||
import { Core } from '@esengine/ecs-framework'
|
||||
|
||||
// Method 1: Using config object (recommended)
|
||||
const core = Core.create({
|
||||
debug: true, // Enable debug mode for detailed logs and performance monitoring
|
||||
debugConfig: { // Optional: Advanced debug configuration
|
||||
enabled: false, // Whether to enable WebSocket debug server
|
||||
websocketUrl: 'ws://localhost:8080',
|
||||
debugFrameRate: 30, // Debug data send frame rate
|
||||
channels: {
|
||||
entities: true,
|
||||
systems: true,
|
||||
performance: true,
|
||||
components: true,
|
||||
scenes: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Method 2: Simplified creation (backward compatible)
|
||||
const core = Core.create(true); // Equivalent to { debug: true }
|
||||
|
||||
// Method 3: Production environment configuration
|
||||
const core = Core.create({
|
||||
debug: false // Disable debug in production
|
||||
});
|
||||
```
|
||||
|
||||
### Core Configuration Details
|
||||
|
||||
```typescript
|
||||
interface ICoreConfig {
|
||||
/** Enable debug mode - affects log level and performance monitoring */
|
||||
debug?: boolean;
|
||||
|
||||
/** Advanced debug configuration - for dev tools integration */
|
||||
debugConfig?: {
|
||||
enabled: boolean; // Enable debug server
|
||||
websocketUrl: string; // WebSocket server URL
|
||||
autoReconnect?: boolean; // Auto reconnect
|
||||
debugFrameRate?: 60 | 30 | 15; // Debug data send frame rate
|
||||
channels: { // Data channel configuration
|
||||
entities: boolean; // Entity data
|
||||
systems: boolean; // System data
|
||||
performance: boolean; // Performance data
|
||||
components: boolean; // Component data
|
||||
scenes: boolean; // Scene data
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Core Instance Management
|
||||
|
||||
Core uses singleton pattern, accessible via static property after creation:
|
||||
|
||||
```typescript
|
||||
// Create instance
|
||||
const core = Core.create(true);
|
||||
|
||||
// Get created instance
|
||||
const instance = Core.Instance; // Returns current instance, null if not created
|
||||
```
|
||||
|
||||
### Game Loop Integration
|
||||
|
||||
**Important**: Before creating entities and systems, you need to understand how to integrate ECS Framework into your game engine.
|
||||
|
||||
`Core.update(deltaTime)` is the framework heartbeat, must be called every frame. It handles:
|
||||
- Updating the built-in Time class
|
||||
- Updating all global managers (timers, object pools, etc.)
|
||||
- Updating all entity systems in all scenes
|
||||
- Processing entity creation and destruction
|
||||
- Collecting performance data (in debug mode)
|
||||
|
||||
See engine integration examples: [Game Engine Integration](#game-engine-integration)
|
||||
|
||||
## Create Your First ECS Application
|
||||
|
||||
### 1. Define Components
|
||||
|
||||
Components are pure data containers that store entity state:
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent } from '@esengine/ecs-framework'
|
||||
|
||||
// Position component
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
x: number = 0
|
||||
y: number = 0
|
||||
|
||||
constructor(x: number = 0, y: number = 0) {
|
||||
super()
|
||||
this.x = x
|
||||
this.y = y
|
||||
}
|
||||
}
|
||||
|
||||
// Velocity component
|
||||
@ECSComponent('Velocity')
|
||||
class Velocity extends Component {
|
||||
dx: number = 0
|
||||
dy: number = 0
|
||||
|
||||
constructor(dx: number = 0, dy: number = 0) {
|
||||
super()
|
||||
this.dx = dx
|
||||
this.dy = dy
|
||||
}
|
||||
}
|
||||
|
||||
// Sprite component
|
||||
@ECSComponent('Sprite')
|
||||
class Sprite extends Component {
|
||||
texture: string = ''
|
||||
width: number = 32
|
||||
height: number = 32
|
||||
|
||||
constructor(texture: string, width: number = 32, height: number = 32) {
|
||||
super()
|
||||
this.texture = texture
|
||||
this.width = width
|
||||
this.height = height
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Create Entity Systems
|
||||
|
||||
Systems contain game logic and process entities with specific components. ECS Framework provides Matcher-based entity filtering:
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, Matcher, Time, ECSSystem } from '@esengine/ecs-framework'
|
||||
|
||||
// Movement system - handles position and velocity
|
||||
@ECSSystem('MovementSystem')
|
||||
class MovementSystem extends EntitySystem {
|
||||
|
||||
constructor() {
|
||||
// Use Matcher to define target entities: must have both Position and Velocity
|
||||
super(Matcher.empty().all(Position, Velocity))
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// process method receives all matching entities
|
||||
for (const entity of entities) {
|
||||
const position = entity.getComponent(Position)!
|
||||
const velocity = entity.getComponent(Velocity)!
|
||||
|
||||
// Update position (using framework's Time class)
|
||||
position.x += velocity.dx * Time.deltaTime
|
||||
position.y += velocity.dy * Time.deltaTime
|
||||
|
||||
// Boundary check example
|
||||
if (position.x < 0) position.x = 0
|
||||
if (position.y < 0) position.y = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Render system - handles visible objects
|
||||
@ECSSystem('RenderSystem')
|
||||
class RenderSystem extends EntitySystem {
|
||||
|
||||
constructor() {
|
||||
// Must have Position and Sprite, optional Velocity (for direction)
|
||||
super(Matcher.empty().all(Position, Sprite).any(Velocity))
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const position = entity.getComponent(Position)!
|
||||
const sprite = entity.getComponent(Sprite)!
|
||||
const velocity = entity.getComponent(Velocity) // May be null
|
||||
|
||||
// Flip sprite based on velocity direction (optional logic)
|
||||
let flipX = false
|
||||
if (velocity && velocity.dx < 0) {
|
||||
flipX = true
|
||||
}
|
||||
|
||||
// Render logic (pseudocode here)
|
||||
this.drawSprite(sprite.texture, position.x, position.y, sprite.width, sprite.height, flipX)
|
||||
}
|
||||
}
|
||||
|
||||
private drawSprite(texture: string, x: number, y: number, width: number, height: number, flipX: boolean = false) {
|
||||
// Actual render implementation depends on your game engine
|
||||
const direction = flipX ? '<-' : '->'
|
||||
console.log(`Render ${texture} at (${x.toFixed(1)}, ${y.toFixed(1)}) direction: ${direction}`)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### 3. Create Scene
|
||||
|
||||
Recommended to extend Scene class for custom scenes:
|
||||
|
||||
```typescript
|
||||
import { Scene } from '@esengine/ecs-framework'
|
||||
|
||||
// Recommended: Extend Scene for custom scene
|
||||
class GameScene extends Scene {
|
||||
|
||||
initialize(): void {
|
||||
// Scene initialization logic
|
||||
this.name = "MainScene";
|
||||
|
||||
// Add systems to scene
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
}
|
||||
|
||||
onStart(): void {
|
||||
// Logic when scene starts running
|
||||
console.log("Game scene started");
|
||||
}
|
||||
|
||||
unload(): void {
|
||||
// Cleanup logic when scene unloads
|
||||
console.log("Game scene unloaded");
|
||||
}
|
||||
}
|
||||
|
||||
// Create and set scene
|
||||
const gameScene = new GameScene();
|
||||
Core.setScene(gameScene);
|
||||
```
|
||||
|
||||
### 4. Create Entities
|
||||
|
||||
```typescript
|
||||
// Create player entity
|
||||
const player = gameScene.createEntity("Player");
|
||||
player.addComponent(new Position(100, 100));
|
||||
player.addComponent(new Velocity(50, 30)); // Move 50px/sec (x), 30px/sec (y)
|
||||
player.addComponent(new Sprite("player.png", 64, 64));
|
||||
```
|
||||
|
||||
## Scene Management
|
||||
|
||||
Core has built-in scene management, very simple to use:
|
||||
|
||||
```typescript
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// Initialize Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// Create and set scene
|
||||
class GameScene extends Scene {
|
||||
initialize(): void {
|
||||
this.name = "GamePlay";
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
}
|
||||
}
|
||||
|
||||
const gameScene = new GameScene();
|
||||
Core.setScene(gameScene);
|
||||
|
||||
// Game loop (auto-updates scene)
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // Auto-updates global services and scene
|
||||
}
|
||||
|
||||
// Switch scenes
|
||||
Core.loadScene(new MenuScene()); // Delayed switch (next frame)
|
||||
Core.setScene(new GameScene()); // Immediate switch
|
||||
|
||||
// Access current scene
|
||||
const currentScene = Core.scene;
|
||||
|
||||
// Using fluent API
|
||||
const player = Core.ecsAPI?.createEntity('Player')
|
||||
.addComponent(Position, 100, 100)
|
||||
.addComponent(Velocity, 50, 0);
|
||||
```
|
||||
|
||||
### Advanced: Using WorldManager for Multi-World
|
||||
|
||||
Only for complex server-side applications (MMO game servers, game room systems, etc.):
|
||||
|
||||
```typescript
|
||||
import { Core, WorldManager } from '@esengine/ecs-framework';
|
||||
|
||||
// Initialize Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// Get WorldManager from service container (Core auto-creates and registers it)
|
||||
const worldManager = Core.services.resolve(WorldManager);
|
||||
|
||||
// Create multiple independent game worlds
|
||||
const room1 = worldManager.createWorld('room_001');
|
||||
const room2 = worldManager.createWorld('room_002');
|
||||
|
||||
// Create scenes in each world
|
||||
const gameScene1 = room1.createScene('game', new GameScene());
|
||||
const gameScene2 = room2.createScene('game', new GameScene());
|
||||
|
||||
// Activate scenes
|
||||
room1.setSceneActive('game', true);
|
||||
room2.setSceneActive('game', true);
|
||||
|
||||
// Game loop (need to manually update worlds)
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // Update global services
|
||||
worldManager.updateAll(); // Manually update all worlds
|
||||
}
|
||||
```
|
||||
|
||||
## Game Engine Integration
|
||||
|
||||
### Laya Engine Integration
|
||||
|
||||
```typescript
|
||||
import { Stage } from "laya/display/Stage";
|
||||
import { Laya } from "Laya";
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
// Initialize Laya
|
||||
Laya.init(800, 600).then(() => {
|
||||
// Initialize ECS
|
||||
Core.create(true);
|
||||
Core.setScene(new GameScene());
|
||||
|
||||
// Start game loop
|
||||
Laya.timer.frameLoop(1, this, () => {
|
||||
const deltaTime = Laya.timer.delta / 1000;
|
||||
Core.update(deltaTime); // Auto-updates global services and scene
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Cocos Creator Integration
|
||||
|
||||
```typescript
|
||||
import { Component, _decorator } from 'cc';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
const { ccclass } = _decorator;
|
||||
|
||||
@ccclass('ECSGameManager')
|
||||
export class ECSGameManager extends Component {
|
||||
onLoad() {
|
||||
// Initialize ECS
|
||||
Core.create(true);
|
||||
Core.setScene(new GameScene());
|
||||
}
|
||||
|
||||
update(deltaTime: number) {
|
||||
// Auto-updates global services and scene
|
||||
Core.update(deltaTime);
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
// Cleanup resources
|
||||
Core.destroy();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Next Steps
|
||||
|
||||
You've successfully created your first ECS application! Next you can:
|
||||
|
||||
- Check the complete [API Documentation](/api/README)
|
||||
- Explore more [practical examples](/examples/)
|
||||
|
||||
## FAQ
|
||||
|
||||
### Why isn't my system executing?
|
||||
|
||||
Ensure:
|
||||
1. System is added to scene: `this.addSystem(system)` (in Scene's initialize method)
|
||||
2. Scene is set: `Core.setScene(scene)`
|
||||
3. Game loop is calling: `Core.update(deltaTime)`
|
||||
|
||||
### How to debug ECS applications?
|
||||
|
||||
Enable debug mode:
|
||||
|
||||
```typescript
|
||||
Core.create({ debug: true })
|
||||
|
||||
// Get debug data
|
||||
const debugData = Core.getDebugData()
|
||||
console.log(debugData)
|
||||
```
|
||||
@@ -1,43 +0,0 @@
|
||||
# Guide
|
||||
|
||||
Welcome to the ECS Framework Guide. This guide covers the core concepts and usage of the framework.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### [Entity](/guide/entity)
|
||||
Learn the basics of ECS architecture - how to use entities, lifecycle management, and best practices.
|
||||
|
||||
### [Component](/guide/component)
|
||||
Learn how to create and use components for modular game feature design.
|
||||
|
||||
### [System](/guide/system)
|
||||
Master system development to implement game logic processing.
|
||||
|
||||
### [Entity Query & Matcher](/guide/entity-query)
|
||||
Learn to use Matcher for entity filtering and queries with `all`, `any`, `none`, `nothing` conditions.
|
||||
|
||||
### [Scene](/guide/scene)
|
||||
Understand scene lifecycle, system management, and entity container features.
|
||||
|
||||
### [Event System](/guide/event-system)
|
||||
Master the type-safe event system for component communication and system coordination.
|
||||
|
||||
### [Serialization](/guide/serialization)
|
||||
Master serialization for scenes, entities, and components. Supports full and incremental serialization for game saves, network sync, and more.
|
||||
|
||||
### [Time and Timers](/guide/time-and-timers)
|
||||
Learn time management and timer systems for precise game logic timing control.
|
||||
|
||||
### [Logging](/guide/logging)
|
||||
Master the leveled logging system for debugging, monitoring, and error tracking.
|
||||
|
||||
### [Platform Adapter](/guide/platform-adapter)
|
||||
Learn how to implement and register platform adapters for browsers, mini-games, Node.js, and more.
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### [Service Container](/guide/service-container)
|
||||
Master dependency injection and service management for loosely-coupled architecture.
|
||||
|
||||
### [Plugin System](/guide/plugin-system)
|
||||
Learn how to develop and use plugins to extend framework functionality.
|
||||
@@ -1,820 +0,0 @@
|
||||
# System Architecture
|
||||
|
||||
In ECS architecture, Systems are where business logic is processed. Systems are responsible for performing operations on entities that have specific component combinations, serving as the logic processing units of ECS architecture.
|
||||
|
||||
## Basic Concepts
|
||||
|
||||
Systems are concrete classes that inherit from the `EntitySystem` abstract base class, used for:
|
||||
- Defining entity processing logic (such as movement, collision detection, rendering, etc.)
|
||||
- Filtering entities based on component combinations
|
||||
- Providing lifecycle management and performance monitoring
|
||||
- Managing entity add/remove events
|
||||
|
||||
## System Types
|
||||
|
||||
The framework provides several different system base classes:
|
||||
|
||||
### EntitySystem - Base System
|
||||
|
||||
The most basic system class, all other systems inherit from it:
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, ECSSystem, Matcher } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSSystem('Movement')
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// Use Matcher to define entity conditions to process
|
||||
super(Matcher.all(Position, Velocity));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const position = entity.getComponent(Position);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
if (position && velocity) {
|
||||
position.x += velocity.dx * Time.deltaTime;
|
||||
position.y += velocity.dy * Time.deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ProcessingSystem - Processing System
|
||||
|
||||
Suitable for systems that don't need to process entities individually:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Physics')
|
||||
class PhysicsSystem extends ProcessingSystem {
|
||||
constructor() {
|
||||
super(); // No Matcher needed
|
||||
}
|
||||
|
||||
public processSystem(): void {
|
||||
// Execute physics world step
|
||||
this.physicsWorld.step(Time.deltaTime);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### PassiveSystem - Passive System
|
||||
|
||||
Passive systems don't actively process, mainly used for listening to entity add and remove events:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('EntityTracker')
|
||||
class EntityTrackerSystem extends PassiveSystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Health));
|
||||
}
|
||||
|
||||
protected onAdded(entity: Entity): void {
|
||||
console.log(`Health entity added: ${entity.name}`);
|
||||
}
|
||||
|
||||
protected onRemoved(entity: Entity): void {
|
||||
console.log(`Health entity removed: ${entity.name}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### IntervalSystem - Interval System
|
||||
|
||||
Systems that execute at fixed time intervals:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('AutoSave')
|
||||
class AutoSaveSystem extends IntervalSystem {
|
||||
constructor() {
|
||||
// Execute every 5 seconds
|
||||
super(5.0, Matcher.all(SaveData));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
console.log('Executing auto save...');
|
||||
// Save game data
|
||||
this.saveGameData(entities);
|
||||
}
|
||||
|
||||
private saveGameData(entities: readonly Entity[]): void {
|
||||
// Save logic
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### WorkerEntitySystem - Multi-threaded System
|
||||
|
||||
A Web Worker-based multi-threaded processing system, suitable for compute-intensive tasks, capable of fully utilizing multi-core CPU performance.
|
||||
|
||||
Worker systems provide true parallel computing capabilities, support SharedArrayBuffer optimization, and have automatic fallback support. Particularly suitable for physics simulation, particle systems, AI computation, and similar scenarios.
|
||||
|
||||
**For detailed content, please refer to: [Worker System](/guide/worker-system)**
|
||||
|
||||
## Entity Matcher
|
||||
|
||||
Matcher is used to define which entities a system needs to process. It provides flexible condition combinations:
|
||||
|
||||
### Basic Match Conditions
|
||||
|
||||
```typescript
|
||||
// Must have both Position and Velocity components
|
||||
const matcher1 = Matcher.all(Position, Velocity);
|
||||
|
||||
// Must have at least one of Health or Shield components
|
||||
const matcher2 = Matcher.any(Health, Shield);
|
||||
|
||||
// Must not have Dead component
|
||||
const matcher3 = Matcher.none(Dead);
|
||||
```
|
||||
|
||||
### Compound Match Conditions
|
||||
|
||||
```typescript
|
||||
// Complex combination conditions
|
||||
const complexMatcher = Matcher.all(Position, Velocity)
|
||||
.any(Player, Enemy)
|
||||
.none(Dead, Disabled);
|
||||
|
||||
@ECSSystem('Combat')
|
||||
class CombatSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(complexMatcher);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Special Match Conditions
|
||||
|
||||
```typescript
|
||||
// Match by tag
|
||||
const tagMatcher = Matcher.byTag(1); // Match entities with tag 1
|
||||
|
||||
// Match by name
|
||||
const nameMatcher = Matcher.byName("Player"); // Match entities named "Player"
|
||||
|
||||
// Single component match
|
||||
const componentMatcher = Matcher.byComponent(Health); // Match entities with Health component
|
||||
|
||||
// Match no entities
|
||||
const nothingMatcher = Matcher.nothing(); // For systems that only need lifecycle callbacks
|
||||
```
|
||||
|
||||
### Empty Matcher vs Nothing Matcher
|
||||
|
||||
```typescript
|
||||
// empty() - Empty condition, matches all entities
|
||||
const emptyMatcher = Matcher.empty();
|
||||
|
||||
// nothing() - Matches no entities, for systems that only need lifecycle methods
|
||||
const nothingMatcher = Matcher.nothing();
|
||||
|
||||
// Use case: Systems that only need onBegin/onEnd lifecycle
|
||||
@ECSSystem('FrameTimer')
|
||||
class FrameTimerSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.nothing()); // Process no entities
|
||||
}
|
||||
|
||||
protected onBegin(): void {
|
||||
// Execute at the start of each frame, e.g., record frame start time
|
||||
console.log('Frame started');
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Never called because there are no matching entities
|
||||
}
|
||||
|
||||
protected onEnd(): void {
|
||||
// Execute at the end of each frame
|
||||
console.log('Frame ended');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **Tip**: For more details on Matcher and entity queries, please refer to the [Entity Query System](/guide/entity-query) documentation.
|
||||
|
||||
## System Lifecycle
|
||||
|
||||
Systems provide complete lifecycle callbacks:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Example')
|
||||
class ExampleSystem extends EntitySystem {
|
||||
protected onInitialize(): void {
|
||||
console.log('System initialized');
|
||||
// Called when system is added to scene, for initializing resources
|
||||
}
|
||||
|
||||
protected onBegin(): void {
|
||||
// Called before each frame's processing begins
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Main processing logic
|
||||
for (const entity of entities) {
|
||||
// Process each entity
|
||||
// Safe to add/remove components here without affecting current iteration
|
||||
}
|
||||
}
|
||||
|
||||
protected lateProcess(entities: readonly Entity[]): void {
|
||||
// Post-processing after main process
|
||||
// Safe to add/remove components here without affecting current iteration
|
||||
}
|
||||
|
||||
protected onEnd(): void {
|
||||
// Called after each frame's processing ends
|
||||
}
|
||||
|
||||
protected onDestroy(): void {
|
||||
console.log('System destroyed');
|
||||
// Called when system is removed from scene, for cleaning up resources
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Entity Event Listening
|
||||
|
||||
Systems can listen for entity add and remove events:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('EnemyManager')
|
||||
class EnemyManagerSystem extends EntitySystem {
|
||||
private enemyCount = 0;
|
||||
|
||||
constructor() {
|
||||
super(Matcher.all(Enemy, Health));
|
||||
}
|
||||
|
||||
protected onAdded(entity: Entity): void {
|
||||
this.enemyCount++;
|
||||
console.log(`Enemy joined battle, current enemy count: ${this.enemyCount}`);
|
||||
|
||||
// Can set initial state for new enemies here
|
||||
const health = entity.getComponent(Health);
|
||||
if (health) {
|
||||
health.current = health.max;
|
||||
}
|
||||
}
|
||||
|
||||
protected onRemoved(entity: Entity): void {
|
||||
this.enemyCount--;
|
||||
console.log(`Enemy removed, remaining enemies: ${this.enemyCount}`);
|
||||
|
||||
// Check if all enemies are defeated
|
||||
if (this.enemyCount === 0) {
|
||||
this.scene?.eventSystem.emitSync('all_enemies_defeated');
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Important: Timing of onAdded/onRemoved Calls
|
||||
|
||||
> **Note**: `onAdded` and `onRemoved` callbacks are called **synchronously**, executing immediately **before** `addComponent`/`removeComponent` returns.
|
||||
|
||||
This means:
|
||||
|
||||
```typescript
|
||||
// Wrong: Chain assignment executes after onAdded
|
||||
const comp = entity.addComponent(new ClickComponent());
|
||||
comp.element = this._element; // At this point onAdded has already executed!
|
||||
|
||||
// Correct: Pass initial values through constructor
|
||||
const comp = entity.addComponent(new ClickComponent(this._element));
|
||||
|
||||
// Or use the createComponent method
|
||||
const comp = entity.createComponent(ClickComponent, this._element);
|
||||
```
|
||||
|
||||
**Why this design?**
|
||||
|
||||
The event-driven design ensures that `onAdded`/`onRemoved` callbacks are not affected by system registration order. When a component is added, all systems listening for that component receive notification immediately, rather than waiting until the next frame.
|
||||
|
||||
**Best Practices:**
|
||||
|
||||
1. Component initial values should be passed through the **constructor**
|
||||
2. Don't rely on setting properties after `addComponent` returns
|
||||
3. If you need to access component properties in `onAdded`, ensure those properties are set at construction time
|
||||
|
||||
### Safely Modifying Components in process/lateProcess
|
||||
|
||||
When iterating entities in `process` or `lateProcess`, you can safely add or remove components without affecting the current iteration:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Damage')
|
||||
class DamageSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Health, DamageReceiver));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health);
|
||||
const damage = entity.getComponent(DamageReceiver);
|
||||
|
||||
if (health && damage) {
|
||||
health.current -= damage.amount;
|
||||
|
||||
// Safe: removing component won't affect current iteration
|
||||
entity.removeComponent(damage);
|
||||
|
||||
if (health.current <= 0) {
|
||||
// Safe: adding component won't affect current iteration
|
||||
entity.addComponent(new Dead());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The framework creates a snapshot of the entity list before each `process`/`lateProcess` call, ensuring that component changes during iteration won't cause entities to be skipped or processed multiple times.
|
||||
|
||||
## System Properties and Methods
|
||||
|
||||
### Important Properties
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Example')
|
||||
class ExampleSystem extends EntitySystem {
|
||||
showSystemInfo(): void {
|
||||
console.log(`System name: ${this.systemName}`); // System name
|
||||
console.log(`Update order: ${this.updateOrder}`); // Update order
|
||||
console.log(`Is enabled: ${this.enabled}`); // Enabled state
|
||||
console.log(`Entity count: ${this.entities.length}`); // Number of matched entities
|
||||
console.log(`Scene: ${this.scene?.name}`); // Parent scene
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Entity Access
|
||||
|
||||
```typescript
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Method 1: Use entity list from parameter
|
||||
for (const entity of entities) {
|
||||
// Process entity
|
||||
}
|
||||
|
||||
// Method 2: Use this.entities property (same as parameter)
|
||||
for (const entity of this.entities) {
|
||||
// Process entity
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Controlling System Execution
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Conditional')
|
||||
class ConditionalSystem extends EntitySystem {
|
||||
private shouldProcess = true;
|
||||
|
||||
protected onCheckProcessing(): boolean {
|
||||
// Return false to skip this processing
|
||||
return this.shouldProcess && this.entities.length > 0;
|
||||
}
|
||||
|
||||
public pause(): void {
|
||||
this.shouldProcess = false;
|
||||
}
|
||||
|
||||
public resume(): void {
|
||||
this.shouldProcess = true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Event System Integration
|
||||
|
||||
Systems can conveniently listen for and send events:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('GameLogic')
|
||||
class GameLogicSystem extends EntitySystem {
|
||||
protected onInitialize(): void {
|
||||
// Add event listeners (automatically cleaned up when system is destroyed)
|
||||
this.addEventListener('player_died', this.onPlayerDied.bind(this));
|
||||
this.addEventListener('level_complete', this.onLevelComplete.bind(this));
|
||||
}
|
||||
|
||||
private onPlayerDied(data: any): void {
|
||||
console.log('Player died, restarting game');
|
||||
// Handle player death logic
|
||||
}
|
||||
|
||||
private onLevelComplete(data: any): void {
|
||||
console.log('Level complete, loading next level');
|
||||
// Handle level completion logic
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Send events during processing
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health);
|
||||
if (health && health.current <= 0) {
|
||||
this.scene?.eventSystem.emitSync('entity_died', { entity });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Monitoring
|
||||
|
||||
Systems have built-in performance monitoring:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Performance')
|
||||
class PerformanceSystem extends EntitySystem {
|
||||
protected onEnd(): void {
|
||||
// Get performance data
|
||||
const perfData = this.getPerformanceData();
|
||||
if (perfData) {
|
||||
console.log(`Execution time: ${perfData.executionTime.toFixed(2)}ms`);
|
||||
}
|
||||
|
||||
// Get performance statistics
|
||||
const stats = this.getPerformanceStats();
|
||||
if (stats) {
|
||||
console.log(`Average execution time: ${stats.averageTime.toFixed(2)}ms`);
|
||||
}
|
||||
}
|
||||
|
||||
public resetPerformance(): void {
|
||||
this.resetPerformanceData();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## System Management
|
||||
|
||||
### Adding Systems to Scene
|
||||
|
||||
The framework provides two ways to add systems: pass an instance or pass a type (automatic dependency injection).
|
||||
|
||||
```typescript
|
||||
// Add systems in scene subclass
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Method 1: Pass instance
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
|
||||
// Method 2: Pass type (automatic dependency injection)
|
||||
this.addEntityProcessor(PhysicsSystem);
|
||||
|
||||
// Set system update order
|
||||
const movementSystem = this.getSystem(MovementSystem);
|
||||
if (movementSystem) {
|
||||
movementSystem.updateOrder = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### System Dependency Injection
|
||||
|
||||
Systems implement the `IService` interface and support obtaining other services or systems through dependency injection:
|
||||
|
||||
```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 {
|
||||
// Use injected service
|
||||
this.collision.detectCollisions(entities);
|
||||
}
|
||||
|
||||
// Implement IService interface dispose method
|
||||
public dispose(): void {
|
||||
// Clean up resources
|
||||
}
|
||||
}
|
||||
|
||||
// Just pass the type when using, framework will auto-inject dependencies
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Automatic dependency injection
|
||||
this.addEntityProcessor(PhysicsSystem);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
- Use `@Injectable()` decorator to mark systems that need dependency injection
|
||||
- Use `@Inject()` decorator in constructor parameters to declare dependencies
|
||||
- Systems must implement the `dispose()` method (IService interface requirement)
|
||||
- Use `addEntityProcessor(Type)` instead of `addSystem(new Type())` to enable dependency injection
|
||||
|
||||
### System Update Order
|
||||
|
||||
System execution order is determined by the `updateOrder` property. Lower values execute first:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Input')
|
||||
class InputSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(InputComponent));
|
||||
this.updateOrder = -100; // Input system executes first
|
||||
}
|
||||
}
|
||||
|
||||
@ECSSystem('Physics')
|
||||
class PhysicsSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(RigidBody));
|
||||
this.updateOrder = 0; // Default order
|
||||
}
|
||||
}
|
||||
|
||||
@ECSSystem('Render')
|
||||
class RenderSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Sprite, Transform));
|
||||
this.updateOrder = 100; // Render system executes last
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Stable Sorting: addOrder
|
||||
|
||||
When multiple systems have the same `updateOrder`, the framework uses `addOrder` (add order) as a secondary sorting criterion to ensure stable and predictable results:
|
||||
|
||||
```typescript
|
||||
// Both systems have default updateOrder of 0
|
||||
@ECSSystem('SystemA')
|
||||
class SystemA extends EntitySystem { /* ... */ }
|
||||
|
||||
@ECSSystem('SystemB')
|
||||
class SystemB extends EntitySystem { /* ... */ }
|
||||
|
||||
// Add order determines execution order
|
||||
scene.addSystem(new SystemA()); // addOrder = 0, executes first
|
||||
scene.addSystem(new SystemB()); // addOrder = 1, executes second
|
||||
```
|
||||
|
||||
> **Note**: `addOrder` is automatically set by the framework when calling `addSystem`, no manual management needed. This ensures systems with the same `updateOrder` execute in their addition order, avoiding random behavior from unstable sorting.
|
||||
|
||||
## Complex System Examples
|
||||
|
||||
### Collision Detection System
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Collision')
|
||||
class CollisionSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Transform, Collider));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Simple n² collision detection
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
for (let j = i + 1; j < entities.length; j++) {
|
||||
this.checkCollision(entities[i], entities[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private checkCollision(entityA: Entity, entityB: Entity): void {
|
||||
const transformA = entityA.getComponent(Transform);
|
||||
const transformB = entityB.getComponent(Transform);
|
||||
const colliderA = entityA.getComponent(Collider);
|
||||
const colliderB = entityB.getComponent(Collider);
|
||||
|
||||
if (this.isColliding(transformA, colliderA, transformB, colliderB)) {
|
||||
// Send collision event
|
||||
this.scene?.eventSystem.emitSync('collision', {
|
||||
entityA,
|
||||
entityB
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private isColliding(transformA: Transform, colliderA: Collider,
|
||||
transformB: Transform, colliderB: Collider): boolean {
|
||||
// Collision detection logic
|
||||
return false; // Simplified example
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### State Machine System
|
||||
|
||||
```typescript
|
||||
@ECSSystem('StateMachine')
|
||||
class StateMachineSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(StateMachine));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const stateMachine = entity.getComponent(StateMachine);
|
||||
if (stateMachine) {
|
||||
stateMachine.updateTimer(Time.deltaTime);
|
||||
this.updateState(entity, stateMachine);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateState(entity: Entity, stateMachine: StateMachine): void {
|
||||
switch (stateMachine.currentState) {
|
||||
case EntityState.Idle:
|
||||
this.handleIdleState(entity, stateMachine);
|
||||
break;
|
||||
case EntityState.Moving:
|
||||
this.handleMovingState(entity, stateMachine);
|
||||
break;
|
||||
case EntityState.Attacking:
|
||||
this.handleAttackingState(entity, stateMachine);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private handleIdleState(entity: Entity, stateMachine: StateMachine): void {
|
||||
// Idle state logic
|
||||
}
|
||||
|
||||
private handleMovingState(entity: Entity, stateMachine: StateMachine): void {
|
||||
// Moving state logic
|
||||
}
|
||||
|
||||
private handleAttackingState(entity: Entity, stateMachine: StateMachine): void {
|
||||
// Attacking state logic
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Single Responsibility for Systems
|
||||
|
||||
```typescript
|
||||
// Good system design - single responsibility
|
||||
@ECSSystem('Movement')
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Position, Velocity));
|
||||
}
|
||||
}
|
||||
|
||||
@ECSSystem('Rendering')
|
||||
class RenderingSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Sprite, Transform));
|
||||
}
|
||||
}
|
||||
|
||||
// Avoid - too many responsibilities
|
||||
@ECSSystem('GameSystem')
|
||||
class GameSystem extends EntitySystem {
|
||||
// One system handling movement, rendering, sound effects, and more
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Use @ECSSystem Decorator
|
||||
|
||||
`@ECSSystem` is a required decorator for system classes, providing type identification and metadata management.
|
||||
|
||||
#### Why It's Required
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| **Type Identification** | Provides stable system names that remain correct after code obfuscation |
|
||||
| **Debug Support** | Shows readable system names in performance monitoring, logs, and debug tools |
|
||||
| **System Management** | Find and manage systems by name |
|
||||
| **Serialization Support** | Records system configuration during scene serialization |
|
||||
|
||||
#### Basic Syntax
|
||||
|
||||
```typescript
|
||||
@ECSSystem(systemName: string)
|
||||
```
|
||||
|
||||
- `systemName`: The system's name, recommend using descriptive names
|
||||
|
||||
#### Usage Example
|
||||
|
||||
```typescript
|
||||
// Correct usage
|
||||
@ECSSystem('Physics')
|
||||
class PhysicsSystem extends EntitySystem {
|
||||
// System implementation
|
||||
}
|
||||
|
||||
// Recommended: Use descriptive names
|
||||
@ECSSystem('PlayerMovement')
|
||||
class PlayerMovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Player, Position, Velocity));
|
||||
}
|
||||
}
|
||||
|
||||
// Wrong - no decorator
|
||||
class BadSystem extends EntitySystem {
|
||||
// Systems defined this way may have issues in production:
|
||||
// 1. Class name changes after code minification, can't identify correctly
|
||||
// 2. Performance monitoring and debug tools show incorrect names
|
||||
}
|
||||
```
|
||||
|
||||
#### System Name Usage
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Combat')
|
||||
class CombatSystem extends EntitySystem {
|
||||
protected onInitialize(): void {
|
||||
// Access system name using systemName property
|
||||
console.log(`System ${this.systemName} initialized`); // Output: System Combat initialized
|
||||
}
|
||||
}
|
||||
|
||||
// Find system by name
|
||||
const combat = scene.getSystemByName('Combat');
|
||||
|
||||
// Performance monitoring displays system name
|
||||
const perfData = combatSystem.getPerformanceData();
|
||||
console.log(`${combatSystem.systemName} execution time: ${perfData?.executionTime}ms`);
|
||||
```
|
||||
|
||||
### 3. Proper Update Order
|
||||
|
||||
```typescript
|
||||
// Set system update order by logical sequence
|
||||
@ECSSystem('Input')
|
||||
class InputSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super();
|
||||
this.updateOrder = -100; // Process input first
|
||||
}
|
||||
}
|
||||
|
||||
@ECSSystem('Logic')
|
||||
class GameLogicSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super();
|
||||
this.updateOrder = 0; // Process game logic
|
||||
}
|
||||
}
|
||||
|
||||
@ECSSystem('Render')
|
||||
class RenderSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super();
|
||||
this.updateOrder = 100; // Render last
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Avoid Direct References Between Systems
|
||||
|
||||
```typescript
|
||||
// Avoid: Direct system references
|
||||
@ECSSystem('Bad')
|
||||
class BadSystem extends EntitySystem {
|
||||
private otherSystem: SomeOtherSystem; // Avoid direct references to other systems
|
||||
}
|
||||
|
||||
// Recommended: Communicate through event system
|
||||
@ECSSystem('Good')
|
||||
class GoodSystem extends EntitySystem {
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Communicate with other systems through event system
|
||||
this.scene?.eventSystem.emitSync('data_updated', { entities });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Clean Up Resources Promptly
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Resource')
|
||||
class ResourceSystem extends EntitySystem {
|
||||
private resources: Map<string, any> = new Map();
|
||||
|
||||
protected onDestroy(): void {
|
||||
// Clean up resources
|
||||
for (const [key, resource] of this.resources) {
|
||||
if (resource.dispose) {
|
||||
resource.dispose();
|
||||
}
|
||||
}
|
||||
this.resources.clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Systems are the logic processing core of ECS architecture. Properly designing and using systems makes your game code more modular, efficient, and maintainable.
|
||||
@@ -1,317 +0,0 @@
|
||||
---
|
||||
layout: page
|
||||
title: ESEngine - High-performance TypeScript ECS Framework
|
||||
---
|
||||
|
||||
<ParticleHeroEn />
|
||||
|
||||
<section class="news-section">
|
||||
<div class="news-container">
|
||||
<div class="news-header">
|
||||
<h2 class="news-title">Quick Links</h2>
|
||||
<a href="/en/guide/" class="news-more">View Docs</a>
|
||||
</div>
|
||||
<div class="news-grid">
|
||||
<a href="/en/guide/getting-started" class="news-card">
|
||||
<div class="news-card-image" style="background: linear-gradient(135deg, #1e3a5f 0%, #1e1e1e 100%);">
|
||||
<div class="news-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24"><path fill="#4fc1ff" d="M12 3L1 9l4 2.18v6L12 21l7-3.82v-6l2-1.09V17h2V9zm6.82 6L12 12.72L5.18 9L12 5.28zM17 16l-5 2.72L7 16v-3.73L12 15l5-2.73z"/></svg>
|
||||
</div>
|
||||
<span class="news-badge">Quick Start</span>
|
||||
</div>
|
||||
<div class="news-card-content">
|
||||
<h3>Get Started in 5 Minutes</h3>
|
||||
<p>From installation to your first ECS app, learn the core concepts quickly.</p>
|
||||
</div>
|
||||
</a>
|
||||
<a href="/en/guide/behavior-tree/" class="news-card">
|
||||
<div class="news-card-image" style="background: linear-gradient(135deg, #1e3a5f 0%, #1e1e1e 100%);">
|
||||
<div class="news-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24"><path fill="#4ec9b0" d="M12 2a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m3 20h-1v-7l-2-2l-2 2v7H9v-7.5l-2 2V22H6v-6l3-3l1-3.5c-.3.4-.6.7-1 1L6 9v1H4V8l5-3c.5-.3 1.1-.5 1.7-.5H11c.6 0 1.2.2 1.7.5l5 3v2h-2V9l-3 1.5c-.4-.3-.7-.6-1-1l1 3.5l3 3v6Z"/></svg>
|
||||
</div>
|
||||
<span class="news-badge">AI System</span>
|
||||
</div>
|
||||
<div class="news-card-content">
|
||||
<h3>Visual Behavior Tree Editor</h3>
|
||||
<p>Built-in AI behavior tree system with visual editing and real-time debugging.</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="features-section">
|
||||
<div class="features-container">
|
||||
<h2 class="features-title">Core Features</h2>
|
||||
<div class="features-grid">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#4fc1ff" d="M13 2.05v2.02c3.95.49 7 3.85 7 7.93c0 1.45-.39 2.79-1.06 3.95l1.59 1.09A9.94 9.94 0 0 0 22 12c0-5.18-3.95-9.45-9-9.95M12 19c-3.87 0-7-3.13-7-7c0-3.53 2.61-6.43 6-6.92V2.05c-5.06.5-9 4.76-9 9.95c0 5.52 4.47 10 9.99 10c3.31 0 6.24-1.61 8.06-4.09l-1.6-1.1A7.93 7.93 0 0 1 12 19"/><path fill="#4fc1ff" d="M12 6a6 6 0 0 0-6 6c0 3.31 2.69 6 6 6a6 6 0 0 0 0-12m0 10c-2.21 0-4-1.79-4-4s1.79-4 4-4s4 1.79 4 4s-1.79 4-4 4"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">High-performance ECS Architecture</h3>
|
||||
<p class="feature-desc">Data-driven entity component system for large-scale entity processing with cache-friendly memory layout.</p>
|
||||
<a href="/en/guide/entity" class="feature-link">Learn more</a>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#569cd6" d="M3 3h18v18H3zm16.525 13.707c0-.795-.272-1.425-.816-1.89c-.544-.465-1.404-.804-2.58-1.016l-1.704-.296c-.616-.104-1.052-.26-1.308-.468c-.256-.21-.384-.468-.384-.776c0-.392.168-.7.504-.924c.336-.224.8-.336 1.392-.336c.56 0 1.008.124 1.344.372c.336.248.536.584.6 1.008h2.016c-.08-.96-.464-1.716-1.152-2.268c-.688-.552-1.6-.828-2.736-.828c-1.2 0-2.148.3-2.844.9c-.696.6-1.044 1.38-1.044 2.34c0 .76.252 1.368.756 1.824c.504.456 1.308.792 2.412.996l1.704.312c.624.12 1.068.28 1.332.48c.264.2.396.46.396.78c0 .424-.192.756-.576.996c-.384.24-.9.36-1.548.36c-.672 0-1.2-.14-1.584-.42c-.384-.28-.608-.668-.672-1.164H8.868c.048 1.016.46 1.808 1.236 2.376c.776.568 1.796.852 3.06.852c1.24 0 2.22-.292 2.94-.876c.72-.584 1.08-1.364 1.08-2.34z"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">Full Type Support</h3>
|
||||
<p class="feature-desc">100% TypeScript with complete type definitions and compile-time checking for the best development experience.</p>
|
||||
<a href="/en/guide/component" class="feature-link">Learn more</a>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#4ec9b0" d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10s10-4.5 10-10S17.5 2 12 2m0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8s8 3.59 8 8s-3.59 8-8 8m-5-8l4-4v3h4v2h-4v3z"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">Visual Behavior Tree</h3>
|
||||
<p class="feature-desc">Built-in AI behavior tree system with visual editor, custom nodes, and real-time debugging.</p>
|
||||
<a href="/en/guide/behavior-tree/" class="feature-link">Learn more</a>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#c586c0" d="M4 6h18V4H4c-1.1 0-2 .9-2 2v11H0v3h14v-3H4zm19 2h-6c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h6c.55 0 1-.45 1-1V9c0-.55-.45-1-1-1m-1 9h-4v-7h4z"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">Multi-Platform Support</h3>
|
||||
<p class="feature-desc">Support for browsers, Node.js, WeChat Mini Games, and seamless integration with major game engines.</p>
|
||||
<a href="/en/guide/platform-adapter" class="feature-link">Learn more</a>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#dcdcaa" d="M4 3h6v2H4v14h6v2H4c-1.1 0-2-.9-2-2V5c0-1.1.9-2 2-2m9 0h6c1.1 0 2 .9 2 2v14c0 1.1-.9 2-2 2h-6v-2h6V5h-6zm-1 7h4v2h-4z"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">Modular Design</h3>
|
||||
<p class="feature-desc">Core features packaged independently, import only what you need. Support for custom plugin extensions.</p>
|
||||
<a href="/en/guide/plugin-system" class="feature-link">Learn more</a>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#9cdcfe" d="M22.7 19l-9.1-9.1c.9-2.3.4-5-1.5-6.9c-2-2-5-2.4-7.4-1.3L9 6L6 9L1.6 4.7C.4 7.1.9 10.1 2.9 12.1c1.9 1.9 4.6 2.4 6.9 1.5l9.1 9.1c.4.4 1 .4 1.4 0l2.3-2.3c.5-.4.5-1.1.1-1.4"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">Developer Tools</h3>
|
||||
<p class="feature-desc">Built-in performance monitoring, debugging tools, serialization system, and complete development toolchain.</p>
|
||||
<a href="/en/guide/logging" class="feature-link">Learn more</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style scoped>
|
||||
/* Home page specific styles */
|
||||
.news-section {
|
||||
background: #0d0d0d;
|
||||
padding: 64px 0;
|
||||
border-top: 1px solid #2a2a2a;
|
||||
}
|
||||
|
||||
.news-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 48px;
|
||||
}
|
||||
|
||||
.news-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.news-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.news-more {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 20px;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 6px;
|
||||
color: #a0a0a0;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.news-more:hover {
|
||||
background: #252525;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.news-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.news-card {
|
||||
display: flex;
|
||||
background: #1f1f1f;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.news-card:hover {
|
||||
border-color: #3b9eff;
|
||||
}
|
||||
|
||||
.news-card-image {
|
||||
width: 200px;
|
||||
min-height: 140px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.news-icon {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.news-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
background: transparent;
|
||||
border: 1px solid #3a3a3a;
|
||||
border-radius: 16px;
|
||||
color: #a0a0a0;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.news-card-content {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.news-card-content h3 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.news-card-content p {
|
||||
font-size: 0.875rem;
|
||||
color: #707070;
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.features-section {
|
||||
background: #0d0d0d;
|
||||
padding: 64px 0;
|
||||
}
|
||||
|
||||
.features-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 48px;
|
||||
}
|
||||
|
||||
.features-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
margin: 0 0 32px 0;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: #1f1f1f;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
border-color: #3b9eff;
|
||||
background: #252525;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: #0d0d0d;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.feature-desc {
|
||||
font-size: 14px;
|
||||
color: #707070;
|
||||
line-height: 1.7;
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
|
||||
.feature-link {
|
||||
font-size: 14px;
|
||||
color: #3b9eff;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.feature-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.news-container,
|
||||
.features-container {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.news-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.news-card {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.news-card-image {
|
||||
width: 100%;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+8
-75
@@ -55,94 +55,27 @@ class Health extends Component {
|
||||
}
|
||||
```
|
||||
|
||||
### @ECSComponent 装饰器
|
||||
### 组件装饰器
|
||||
|
||||
`@ECSComponent` 是组件类必须使用的装饰器,它为组件提供了类型标识和元数据管理。
|
||||
|
||||
#### 为什么必须使用
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| **类型识别** | 提供稳定的类型名称,代码混淆后仍能正确识别 |
|
||||
| **序列化支持** | 序列化/反序列化时使用该名称作为类型标识 |
|
||||
| **组件注册** | 自动注册到 ComponentRegistry,分配唯一的位掩码 |
|
||||
| **调试支持** | 在调试工具和日志中显示可读的组件名称 |
|
||||
|
||||
#### 基本语法
|
||||
**必须使用 `@ECSComponent` 装饰器**,这确保了:
|
||||
- 组件在代码混淆后仍能正确识别
|
||||
- 提供稳定的类型名称用于序列化和调试
|
||||
- 框架能正确管理组件注册
|
||||
|
||||
```typescript
|
||||
@ECSComponent(typeName: string)
|
||||
```
|
||||
|
||||
- `typeName`: 组件的类型名称,建议使用与类名相同或相近的名称
|
||||
|
||||
#### 使用示例
|
||||
|
||||
```typescript
|
||||
// ✅ 正确的用法
|
||||
// 正确的用法
|
||||
@ECSComponent('Velocity')
|
||||
class Velocity extends Component {
|
||||
dx: number = 0;
|
||||
dy: number = 0;
|
||||
}
|
||||
|
||||
// ✅ 推荐:类型名与类名保持一致
|
||||
@ECSComponent('PlayerController')
|
||||
class PlayerController extends Component {
|
||||
speed: number = 5;
|
||||
}
|
||||
|
||||
// ❌ 错误的用法 - 没有装饰器
|
||||
// 错误的用法 - 没有装饰器
|
||||
class BadComponent extends Component {
|
||||
// 这样定义的组件可能在生产环境出现问题:
|
||||
// 1. 代码压缩后类名变化,无法正确序列化
|
||||
// 2. 组件未注册到框架,查询和匹配可能失效
|
||||
// 这样定义的组件可能在生产环境出现问题
|
||||
}
|
||||
```
|
||||
|
||||
#### 与 @Serializable 配合使用
|
||||
|
||||
当组件需要支持序列化时,`@ECSComponent` 和 `@Serializable` 需要一起使用:
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent, Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSComponent('Player')
|
||||
@Serializable({ version: 1 })
|
||||
class PlayerComponent extends Component {
|
||||
@Serialize()
|
||||
name: string = '';
|
||||
|
||||
@Serialize()
|
||||
level: number = 1;
|
||||
|
||||
// 不使用 @Serialize() 的字段不会被序列化
|
||||
private _cachedData: any = null;
|
||||
}
|
||||
```
|
||||
|
||||
> **注意**:`@ECSComponent` 的 `typeName` 和 `@Serializable` 的 `typeId` 可以不同。如果 `@Serializable` 没有指定 `typeId`,则默认使用 `@ECSComponent` 的 `typeName`。
|
||||
|
||||
#### 组件类型名的唯一性
|
||||
|
||||
每个组件的类型名应该是唯一的:
|
||||
|
||||
```typescript
|
||||
// ❌ 错误:两个组件使用相同的类型名
|
||||
@ECSComponent('Health')
|
||||
class HealthComponent extends Component { }
|
||||
|
||||
@ECSComponent('Health') // 冲突!
|
||||
class EnemyHealthComponent extends Component { }
|
||||
|
||||
// ✅ 正确:使用不同的类型名
|
||||
@ECSComponent('PlayerHealth')
|
||||
class PlayerHealthComponent extends Component { }
|
||||
|
||||
@ECSComponent('EnemyHealth')
|
||||
class EnemyHealthComponent extends Component { }
|
||||
```
|
||||
|
||||
## 组件生命周期
|
||||
|
||||
组件提供了生命周期钩子,可以重写来执行特定的逻辑:
|
||||
|
||||
@@ -121,65 +121,6 @@ class CombatSystem extends EntitySystem {
|
||||
}
|
||||
```
|
||||
|
||||
#### nothing() - 不匹配任何实体
|
||||
|
||||
用于创建只需要生命周期方法(`onBegin`、`onEnd`)但不需要处理实体的系统。
|
||||
|
||||
```typescript
|
||||
class FrameTimerSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// 不匹配任何实体
|
||||
super(Matcher.nothing());
|
||||
}
|
||||
|
||||
protected onBegin(): void {
|
||||
// 每帧开始时执行
|
||||
Performance.markFrameStart();
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 永远不会被调用,因为没有匹配的实体
|
||||
}
|
||||
|
||||
protected onEnd(): void {
|
||||
// 每帧结束时执行
|
||||
Performance.markFrameEnd();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### empty() vs nothing() 的区别
|
||||
|
||||
| 方法 | 行为 | 使用场景 |
|
||||
|------|------|----------|
|
||||
| `Matcher.empty()` | 匹配**所有**实体 | 需要处理场景中所有实体 |
|
||||
| `Matcher.nothing()` | 不匹配**任何**实体 | 只需要生命周期回调,不处理实体 |
|
||||
|
||||
```typescript
|
||||
// empty() - 返回场景中的所有实体
|
||||
class AllEntitiesSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty());
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// entities 包含场景中的所有实体
|
||||
console.log(`场景中共有 ${entities.length} 个实体`);
|
||||
}
|
||||
}
|
||||
|
||||
// nothing() - 不返回任何实体
|
||||
class NoEntitiesSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.nothing());
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// entities 永远是空数组,此方法不会被调用
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 按标签查询
|
||||
|
||||
```typescript
|
||||
@@ -552,65 +493,6 @@ const matcher2 = matcher.any(VelocityComponent);
|
||||
console.log(matcher === matcher2); // false
|
||||
```
|
||||
|
||||
## Matcher API 快速参考
|
||||
|
||||
### 静态创建方法
|
||||
|
||||
| 方法 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `Matcher.all(...types)` | 必须包含所有指定组件 | `Matcher.all(Position, Velocity)` |
|
||||
| `Matcher.any(...types)` | 至少包含一个指定组件 | `Matcher.any(Health, Shield)` |
|
||||
| `Matcher.none(...types)` | 不能包含任何指定组件 | `Matcher.none(Dead)` |
|
||||
| `Matcher.byTag(tag)` | 按标签查询 | `Matcher.byTag(1)` |
|
||||
| `Matcher.byName(name)` | 按名称查询 | `Matcher.byName("Player")` |
|
||||
| `Matcher.byComponent(type)` | 按单个组件查询 | `Matcher.byComponent(Health)` |
|
||||
| `Matcher.empty()` | 创建空匹配器(匹配所有实体) | `Matcher.empty()` |
|
||||
| `Matcher.nothing()` | 不匹配任何实体 | `Matcher.nothing()` |
|
||||
| `Matcher.complex()` | 创建复杂查询构建器 | `Matcher.complex()` |
|
||||
|
||||
### 链式方法
|
||||
|
||||
| 方法 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `.all(...types)` | 添加必须包含的组件 | `.all(Position)` |
|
||||
| `.any(...types)` | 添加可选组件(至少一个) | `.any(Weapon, Magic)` |
|
||||
| `.none(...types)` | 添加排除的组件 | `.none(Dead)` |
|
||||
| `.exclude(...types)` | `.none()` 的别名 | `.exclude(Disabled)` |
|
||||
| `.one(...types)` | `.any()` 的别名 | `.one(Player, Enemy)` |
|
||||
| `.withTag(tag)` | 添加标签条件 | `.withTag(1)` |
|
||||
| `.withName(name)` | 添加名称条件 | `.withName("Boss")` |
|
||||
| `.withComponent(type)` | 添加单组件条件 | `.withComponent(Health)` |
|
||||
|
||||
### 实用方法
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `.getCondition()` | 获取查询条件(只读) |
|
||||
| `.isEmpty()` | 检查是否为空条件 |
|
||||
| `.isNothing()` | 检查是否为 nothing 匹配器 |
|
||||
| `.clone()` | 克隆匹配器 |
|
||||
| `.reset()` | 重置所有条件 |
|
||||
| `.toString()` | 获取字符串表示 |
|
||||
|
||||
### 常用组合示例
|
||||
|
||||
```typescript
|
||||
// 基础移动系统
|
||||
Matcher.all(Position, Velocity)
|
||||
|
||||
// 可攻击的活着的实体
|
||||
Matcher.all(Position, Health)
|
||||
.any(Weapon, Magic)
|
||||
.none(Dead, Disabled)
|
||||
|
||||
// 所有带标签的敌人
|
||||
Matcher.byTag(Tags.ENEMY)
|
||||
.all(AIComponent)
|
||||
|
||||
// 只需要生命周期的系统
|
||||
Matcher.nothing()
|
||||
```
|
||||
|
||||
## 相关 API
|
||||
|
||||
- [Matcher](../api/classes/Matcher.md) - 查询条件描述符 API 参考
|
||||
|
||||
+1
-13
@@ -9,12 +9,6 @@
|
||||
- 提供唯一标识(ID)
|
||||
- 管理组件的生命周期
|
||||
|
||||
::: tip 关于父子层级关系
|
||||
实体间的父子层级关系通过 `HierarchyComponent` 和 `HierarchySystem` 管理,而非 Entity 内置属性。这种设计遵循 ECS 组合原则 —— 只有需要层级关系的实体才添加此组件。
|
||||
|
||||
详见 [层级系统](./hierarchy.md) 文档。
|
||||
:::
|
||||
|
||||
## 创建实体
|
||||
|
||||
**重要提示:实体必须通过场景创建,不支持手动创建!**
|
||||
@@ -291,10 +285,4 @@ entity.components.forEach(component => {
|
||||
});
|
||||
```
|
||||
|
||||
实体是 ECS 架构的核心概念之一,理解如何正确使用实体将帮助你构建高效、可维护的游戏代码。
|
||||
|
||||
## 下一步
|
||||
|
||||
- 了解 [层级系统](./hierarchy.md) 建立实体间的父子关系
|
||||
- 了解 [组件系统](./component.md) 为实体添加功能
|
||||
- 了解 [场景管理](./scene.md) 组织和管理实体
|
||||
实体是 ECS 架构的核心概念之一,理解如何正确使用实体将帮助你构建高效、可维护的游戏代码。
|
||||
@@ -23,6 +23,7 @@ import { Core } from '@esengine/ecs-framework'
|
||||
// 方式1:使用配置对象(推荐)
|
||||
const core = Core.create({
|
||||
debug: true, // 启用调试模式,提供详细的日志和性能监控
|
||||
enableEntitySystems: true, // 启用实体系统,这是ECS的核心功能
|
||||
debugConfig: { // 可选:高级调试配置
|
||||
enabled: false, // 是否启用WebSocket调试服务器
|
||||
websocketUrl: 'ws://localhost:8080',
|
||||
@@ -38,11 +39,12 @@ const core = Core.create({
|
||||
});
|
||||
|
||||
// 方式2:简化创建(向后兼容)
|
||||
const core = Core.create(true); // 等同于 { debug: true }
|
||||
const core = Core.create(true); // 等同于 { debug: true, enableEntitySystems: true }
|
||||
|
||||
// 方式3:生产环境配置
|
||||
const core = Core.create({
|
||||
debug: false // 生产环境关闭调试
|
||||
debug: false, // 生产环境关闭调试
|
||||
enableEntitySystems: true
|
||||
});
|
||||
```
|
||||
|
||||
@@ -53,6 +55,9 @@ interface ICoreConfig {
|
||||
/** 是否启用调试模式 - 影响日志级别和性能监控 */
|
||||
debug?: boolean;
|
||||
|
||||
/** 是否启用实体系统 - 核心ECS功能开关 */
|
||||
enableEntitySystems?: boolean;
|
||||
|
||||
/** 高级调试配置 - 用于开发工具集成 */
|
||||
debugConfig?: {
|
||||
enabled: boolean; // 是否启用调试服务器
|
||||
|
||||
@@ -1,437 +0,0 @@
|
||||
# 层级系统
|
||||
|
||||
在游戏开发中,实体间的父子层级关系是常见需求。ECS Framework 采用组件化方式管理层级关系,通过 `HierarchyComponent` 和 `HierarchySystem` 实现,完全遵循 ECS 组合原则。
|
||||
|
||||
## 设计理念
|
||||
|
||||
### 为什么不在 Entity 中内置层级?
|
||||
|
||||
传统的游戏对象模型(如 Unity 的 GameObject)将层级关系内置于实体中。ECS Framework 选择组件化方案的原因:
|
||||
|
||||
1. **ECS 组合原则**:层级是一种"功能",应该通过组件添加,而非所有实体都具备
|
||||
2. **按需使用**:只有需要层级关系的实体才添加 `HierarchyComponent`
|
||||
3. **数据与逻辑分离**:`HierarchyComponent` 存储数据,`HierarchySystem` 处理逻辑
|
||||
4. **序列化友好**:层级关系作为组件数据可以轻松序列化和反序列化
|
||||
|
||||
## 基本概念
|
||||
|
||||
### HierarchyComponent
|
||||
|
||||
存储层级关系数据的组件:
|
||||
|
||||
```typescript
|
||||
import { HierarchyComponent } from '@esengine/ecs-framework';
|
||||
|
||||
// HierarchyComponent 的核心属性
|
||||
interface HierarchyComponent {
|
||||
parentId: number | null; // 父实体 ID,null 表示根实体
|
||||
childIds: number[]; // 子实体 ID 列表
|
||||
depth: number; // 在层级中的深度(由系统维护)
|
||||
bActiveInHierarchy: boolean; // 在层级中是否激活(由系统维护)
|
||||
}
|
||||
```
|
||||
|
||||
### HierarchySystem
|
||||
|
||||
处理层级逻辑的系统,提供所有层级操作的 API:
|
||||
|
||||
```typescript
|
||||
import { HierarchySystem } from '@esengine/ecs-framework';
|
||||
|
||||
// 获取系统
|
||||
const hierarchySystem = scene.getEntityProcessor(HierarchySystem);
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 添加系统到场景
|
||||
|
||||
```typescript
|
||||
import { Scene, HierarchySystem } from '@esengine/ecs-framework';
|
||||
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 添加层级系统
|
||||
this.addSystem(new HierarchySystem());
|
||||
|
||||
// 添加其他系统...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 建立父子关系
|
||||
|
||||
```typescript
|
||||
// 创建实体
|
||||
const parent = scene.createEntity("Parent");
|
||||
const child1 = scene.createEntity("Child1");
|
||||
const child2 = scene.createEntity("Child2");
|
||||
|
||||
// 获取层级系统
|
||||
const hierarchySystem = scene.getEntityProcessor(HierarchySystem);
|
||||
|
||||
// 设置父子关系(自动添加 HierarchyComponent)
|
||||
hierarchySystem.setParent(child1, parent);
|
||||
hierarchySystem.setParent(child2, parent);
|
||||
|
||||
// 现在 parent 有两个子实体
|
||||
```
|
||||
|
||||
### 查询层级
|
||||
|
||||
```typescript
|
||||
// 获取父实体
|
||||
const parentEntity = hierarchySystem.getParent(child1);
|
||||
|
||||
// 获取所有子实体
|
||||
const children = hierarchySystem.getChildren(parent);
|
||||
|
||||
// 获取子实体数量
|
||||
const count = hierarchySystem.getChildCount(parent);
|
||||
|
||||
// 检查是否有子实体
|
||||
const hasKids = hierarchySystem.hasChildren(parent);
|
||||
|
||||
// 获取在层级中的深度
|
||||
const depth = hierarchySystem.getDepth(child1); // 返回 1
|
||||
```
|
||||
|
||||
## API 参考
|
||||
|
||||
### 父子关系操作
|
||||
|
||||
#### setParent
|
||||
|
||||
设置实体的父级:
|
||||
|
||||
```typescript
|
||||
// 设置父级
|
||||
hierarchySystem.setParent(child, parent);
|
||||
|
||||
// 移动到根级(无父级)
|
||||
hierarchySystem.setParent(child, null);
|
||||
```
|
||||
|
||||
#### insertChildAt
|
||||
|
||||
在指定位置插入子实体:
|
||||
|
||||
```typescript
|
||||
// 在第一个位置插入
|
||||
hierarchySystem.insertChildAt(parent, child, 0);
|
||||
|
||||
// 追加到末尾
|
||||
hierarchySystem.insertChildAt(parent, child, -1);
|
||||
```
|
||||
|
||||
#### removeChild
|
||||
|
||||
从父级移除子实体(子实体变为根级):
|
||||
|
||||
```typescript
|
||||
const success = hierarchySystem.removeChild(parent, child);
|
||||
```
|
||||
|
||||
#### removeAllChildren
|
||||
|
||||
移除所有子实体:
|
||||
|
||||
```typescript
|
||||
hierarchySystem.removeAllChildren(parent);
|
||||
```
|
||||
|
||||
### 层级查询
|
||||
|
||||
#### getParent / getChildren
|
||||
|
||||
```typescript
|
||||
const parent = hierarchySystem.getParent(entity);
|
||||
const children = hierarchySystem.getChildren(entity);
|
||||
```
|
||||
|
||||
#### getRoot
|
||||
|
||||
获取实体的根节点:
|
||||
|
||||
```typescript
|
||||
const root = hierarchySystem.getRoot(deepChild);
|
||||
```
|
||||
|
||||
#### getRootEntities
|
||||
|
||||
获取所有根实体(没有父级的实体):
|
||||
|
||||
```typescript
|
||||
const roots = hierarchySystem.getRootEntities();
|
||||
```
|
||||
|
||||
#### isAncestorOf / isDescendantOf
|
||||
|
||||
检查祖先/后代关系:
|
||||
|
||||
```typescript
|
||||
// grandparent -> parent -> child
|
||||
const isAncestor = hierarchySystem.isAncestorOf(grandparent, child); // true
|
||||
const isDescendant = hierarchySystem.isDescendantOf(child, grandparent); // true
|
||||
```
|
||||
|
||||
### 层级遍历
|
||||
|
||||
#### findChild
|
||||
|
||||
根据名称查找子实体:
|
||||
|
||||
```typescript
|
||||
// 直接子级中查找
|
||||
const child = hierarchySystem.findChild(parent, "ChildName");
|
||||
|
||||
// 递归查找所有后代
|
||||
const deepChild = hierarchySystem.findChild(parent, "DeepChild", true);
|
||||
```
|
||||
|
||||
#### findChildrenByTag
|
||||
|
||||
根据标签查找子实体:
|
||||
|
||||
```typescript
|
||||
// 查找直接子级
|
||||
const tagged = hierarchySystem.findChildrenByTag(parent, TAG_ENEMY);
|
||||
|
||||
// 递归查找
|
||||
const allTagged = hierarchySystem.findChildrenByTag(parent, TAG_ENEMY, true);
|
||||
```
|
||||
|
||||
#### forEachChild
|
||||
|
||||
遍历子实体:
|
||||
|
||||
```typescript
|
||||
// 遍历直接子级
|
||||
hierarchySystem.forEachChild(parent, (child) => {
|
||||
console.log(child.name);
|
||||
});
|
||||
|
||||
// 递归遍历所有后代
|
||||
hierarchySystem.forEachChild(parent, (child) => {
|
||||
console.log(child.name);
|
||||
}, true);
|
||||
```
|
||||
|
||||
### 层级状态
|
||||
|
||||
#### isActiveInHierarchy
|
||||
|
||||
检查实体在层级中是否激活(考虑所有祖先的激活状态):
|
||||
|
||||
```typescript
|
||||
// 如果 parent.active = false,即使 child.active = true
|
||||
// isActiveInHierarchy(child) 也会返回 false
|
||||
const activeInHierarchy = hierarchySystem.isActiveInHierarchy(child);
|
||||
```
|
||||
|
||||
#### getDepth
|
||||
|
||||
获取实体在层级中的深度(根实体深度为 0):
|
||||
|
||||
```typescript
|
||||
const depth = hierarchySystem.getDepth(entity);
|
||||
```
|
||||
|
||||
### 扁平化层级(用于 UI 渲染)
|
||||
|
||||
```typescript
|
||||
// 用于实现可展开/折叠的层级树视图
|
||||
const expandedIds = new Set([parent.id]);
|
||||
|
||||
const flatNodes = hierarchySystem.flattenHierarchy(expandedIds);
|
||||
// 返回 [{ entity, depth, bHasChildren, bIsExpanded }, ...]
|
||||
```
|
||||
|
||||
## 完整示例
|
||||
|
||||
### 创建游戏角色层级
|
||||
|
||||
```typescript
|
||||
import {
|
||||
Scene,
|
||||
HierarchySystem,
|
||||
HierarchyComponent
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
class GameScene extends Scene {
|
||||
private hierarchySystem!: HierarchySystem;
|
||||
|
||||
protected initialize(): void {
|
||||
// 添加层级系统
|
||||
this.hierarchySystem = new HierarchySystem();
|
||||
this.addSystem(this.hierarchySystem);
|
||||
|
||||
// 创建角色层级
|
||||
this.createPlayerHierarchy();
|
||||
}
|
||||
|
||||
private createPlayerHierarchy(): void {
|
||||
// 根实体
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Transform(0, 0));
|
||||
|
||||
// 身体部件
|
||||
const body = this.createEntity("Body");
|
||||
body.addComponent(new Sprite("body.png"));
|
||||
this.hierarchySystem.setParent(body, player);
|
||||
|
||||
// 武器(挂载在身体上)
|
||||
const weapon = this.createEntity("Weapon");
|
||||
weapon.addComponent(new Sprite("sword.png"));
|
||||
this.hierarchySystem.setParent(weapon, body);
|
||||
|
||||
// 特效(挂载在武器上)
|
||||
const effect = this.createEntity("WeaponEffect");
|
||||
effect.addComponent(new ParticleEmitter());
|
||||
this.hierarchySystem.setParent(effect, weapon);
|
||||
|
||||
// 查询层级信息
|
||||
console.log(`Player 层级深度: ${this.hierarchySystem.getDepth(player)}`); // 0
|
||||
console.log(`Weapon 层级深度: ${this.hierarchySystem.getDepth(weapon)}`); // 2
|
||||
console.log(`Effect 层级深度: ${this.hierarchySystem.getDepth(effect)}`); // 3
|
||||
}
|
||||
|
||||
public equipNewWeapon(weaponName: string): void {
|
||||
const body = this.findEntity("Body");
|
||||
const oldWeapon = this.hierarchySystem.findChild(body!, "Weapon");
|
||||
|
||||
if (oldWeapon) {
|
||||
// 移除旧武器的所有子实体
|
||||
this.hierarchySystem.removeAllChildren(oldWeapon);
|
||||
oldWeapon.destroy();
|
||||
}
|
||||
|
||||
// 创建新武器
|
||||
const newWeapon = this.createEntity("Weapon");
|
||||
newWeapon.addComponent(new Sprite(`${weaponName}.png`));
|
||||
this.hierarchySystem.setParent(newWeapon, body!);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 层级变换系统
|
||||
|
||||
结合 Transform 组件实现层级变换:
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, Matcher, HierarchySystem, HierarchyComponent } from '@esengine/ecs-framework';
|
||||
|
||||
class HierarchyTransformSystem extends EntitySystem {
|
||||
private hierarchySystem!: HierarchySystem;
|
||||
|
||||
constructor() {
|
||||
super(Matcher.empty().all(Transform, HierarchyComponent));
|
||||
}
|
||||
|
||||
public onAddedToScene(): void {
|
||||
// 获取层级系统引用
|
||||
this.hierarchySystem = this.scene!.getEntityProcessor(HierarchySystem)!;
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 按深度排序,确保父级先更新
|
||||
const sorted = [...entities].sort((a, b) => {
|
||||
return this.hierarchySystem.getDepth(a) - this.hierarchySystem.getDepth(b);
|
||||
});
|
||||
|
||||
for (const entity of sorted) {
|
||||
const transform = entity.getComponent(Transform)!;
|
||||
const parent = this.hierarchySystem.getParent(entity);
|
||||
|
||||
if (parent) {
|
||||
const parentTransform = parent.getComponent(Transform);
|
||||
if (parentTransform) {
|
||||
// 计算世界坐标
|
||||
transform.worldX = parentTransform.worldX + transform.localX;
|
||||
transform.worldY = parentTransform.worldY + transform.localY;
|
||||
}
|
||||
} else {
|
||||
// 根实体,本地坐标即世界坐标
|
||||
transform.worldX = transform.localX;
|
||||
transform.worldY = transform.localY;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 缓存机制
|
||||
|
||||
`HierarchySystem` 内置了缓存机制:
|
||||
|
||||
- `depth` 和 `bActiveInHierarchy` 由系统自动维护
|
||||
- 使用 `bCacheDirty` 标记优化更新
|
||||
- 层级变化时自动标记所有子级缓存为脏
|
||||
|
||||
### 最佳实践
|
||||
|
||||
1. **避免深层嵌套**:系统限制最大深度为 32 层
|
||||
2. **批量操作**:构建复杂层级时,尽量一次性设置好所有父子关系
|
||||
3. **按需添加**:只有真正需要层级关系的实体才添加 `HierarchyComponent`
|
||||
4. **缓存系统引用**:避免每次调用都获取 `HierarchySystem`
|
||||
|
||||
```typescript
|
||||
// 好的做法
|
||||
class MySystem extends EntitySystem {
|
||||
private hierarchySystem!: HierarchySystem;
|
||||
|
||||
onAddedToScene() {
|
||||
this.hierarchySystem = this.scene!.getEntityProcessor(HierarchySystem)!;
|
||||
}
|
||||
|
||||
process() {
|
||||
// 使用缓存的引用
|
||||
const parent = this.hierarchySystem.getParent(entity);
|
||||
}
|
||||
}
|
||||
|
||||
// 避免的做法
|
||||
process() {
|
||||
// 每次都获取,性能较差
|
||||
const system = this.scene!.getEntityProcessor(HierarchySystem);
|
||||
}
|
||||
```
|
||||
|
||||
## 迁移指南
|
||||
|
||||
如果你之前使用的是旧版 Entity 内置的层级 API,请参考以下迁移指南:
|
||||
|
||||
| 旧 API (已移除) | 新 API |
|
||||
|----------------|--------|
|
||||
| `entity.parent` | `hierarchySystem.getParent(entity)` |
|
||||
| `entity.children` | `hierarchySystem.getChildren(entity)` |
|
||||
| `entity.addChild(child)` | `hierarchySystem.setParent(child, entity)` |
|
||||
| `entity.removeChild(child)` | `hierarchySystem.removeChild(entity, child)` |
|
||||
| `entity.findChild(name)` | `hierarchySystem.findChild(entity, name)` |
|
||||
| `entity.activeInHierarchy` | `hierarchySystem.isActiveInHierarchy(entity)` |
|
||||
|
||||
### 迁移示例
|
||||
|
||||
```typescript
|
||||
// 旧代码
|
||||
const parent = scene.createEntity("Parent");
|
||||
const child = scene.createEntity("Child");
|
||||
parent.addChild(child);
|
||||
const found = parent.findChild("Child");
|
||||
|
||||
// 新代码
|
||||
const hierarchySystem = scene.getEntityProcessor(HierarchySystem);
|
||||
|
||||
const parent = scene.createEntity("Parent");
|
||||
const child = scene.createEntity("Child");
|
||||
hierarchySystem.setParent(child, parent);
|
||||
const found = hierarchySystem.findChild(parent, "Child");
|
||||
```
|
||||
|
||||
## 下一步
|
||||
|
||||
- 了解 [实体类](./entity.md) 的其他功能
|
||||
- 了解 [场景管理](./scene.md) 如何组织实体和系统
|
||||
- 了解 [组件系统](./component.md) 如何定义和使用组件
|
||||
@@ -13,9 +13,6 @@
|
||||
### [系统架构 (System)](./system.md)
|
||||
掌握系统的编写方法,实现游戏逻辑的处理。
|
||||
|
||||
### [实体查询与 Matcher](./entity-query.md)
|
||||
学习使用 Matcher 进行实体筛选和查询,掌握 `all`、`any`、`none`、`nothing` 等匹配条件。
|
||||
|
||||
### [场景管理 (Scene)](./scene.md)
|
||||
了解场景的生命周期、系统管理和实体容器功能。
|
||||
|
||||
|
||||
@@ -190,106 +190,6 @@ class CollectionsComponent extends Component {
|
||||
}
|
||||
```
|
||||
|
||||
### 组件继承与序列化
|
||||
|
||||
框架完整支持组件类的继承,子类会自动继承父类的序列化字段,同时可以添加自己的字段。
|
||||
|
||||
#### 基础继承
|
||||
|
||||
```typescript
|
||||
// 基类组件
|
||||
@ECSComponent('Collider2DBase')
|
||||
@Serializable({ version: 1, typeId: 'Collider2DBase' })
|
||||
abstract class Collider2DBase extends Component {
|
||||
@Serialize()
|
||||
public friction: number = 0.5;
|
||||
|
||||
@Serialize()
|
||||
public restitution: number = 0.0;
|
||||
|
||||
@Serialize()
|
||||
public isTrigger: boolean = false;
|
||||
}
|
||||
|
||||
// 子类组件 - 自动继承父类的序列化字段
|
||||
@ECSComponent('BoxCollider2D')
|
||||
@Serializable({ version: 1, typeId: 'BoxCollider2D' })
|
||||
class BoxCollider2DComponent extends Collider2DBase {
|
||||
@Serialize()
|
||||
public width: number = 1.0;
|
||||
|
||||
@Serialize()
|
||||
public height: number = 1.0;
|
||||
}
|
||||
|
||||
// 另一个子类组件
|
||||
@ECSComponent('CircleCollider2D')
|
||||
@Serializable({ version: 1, typeId: 'CircleCollider2D' })
|
||||
class CircleCollider2DComponent extends Collider2DBase {
|
||||
@Serialize()
|
||||
public radius: number = 0.5;
|
||||
}
|
||||
```
|
||||
|
||||
#### 继承规则
|
||||
|
||||
1. **字段继承**:子类自动继承父类所有被 `@Serialize()` 标记的字段
|
||||
2. **独立元数据**:每个子类维护独立的序列化元数据,修改子类不会影响父类或其他子类
|
||||
3. **typeId 区分**:使用 `typeId` 选项为每个类指定唯一标识,确保反序列化时能正确识别组件类型
|
||||
|
||||
#### 使用 typeId 的重要性
|
||||
|
||||
当使用组件继承时,**强烈建议**为每个类设置唯一的 `typeId`:
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:明确指定 typeId
|
||||
@Serializable({ version: 1, typeId: 'BoxCollider2D' })
|
||||
class BoxCollider2DComponent extends Collider2DBase { }
|
||||
|
||||
@Serializable({ version: 1, typeId: 'CircleCollider2D' })
|
||||
class CircleCollider2DComponent extends Collider2DBase { }
|
||||
|
||||
// ⚠️ 不推荐:依赖类名作为 typeId
|
||||
// 在代码压缩后类名可能变化,导致反序列化失败
|
||||
@Serializable({ version: 1 })
|
||||
class BoxCollider2DComponent extends Collider2DBase { }
|
||||
```
|
||||
|
||||
#### 子类覆盖父类字段
|
||||
|
||||
子类可以重新声明父类的字段以修改其序列化选项:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('SpecialCollider')
|
||||
@Serializable({ version: 1, typeId: 'SpecialCollider' })
|
||||
class SpecialColliderComponent extends Collider2DBase {
|
||||
// 覆盖父类字段,使用不同的别名
|
||||
@Serialize({ alias: 'fric' })
|
||||
public override friction: number = 0.8;
|
||||
|
||||
@Serialize()
|
||||
public specialProperty: string = '';
|
||||
}
|
||||
```
|
||||
|
||||
#### 忽略继承的字段
|
||||
|
||||
使用 `@IgnoreSerialization()` 可以在子类中忽略从父类继承的字段:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('TriggerOnly')
|
||||
@Serializable({ version: 1, typeId: 'TriggerOnly' })
|
||||
class TriggerOnlyCollider extends Collider2DBase {
|
||||
// 忽略父类的 friction 和 restitution 字段
|
||||
// 因为 Trigger 不需要物理材质属性
|
||||
@IgnoreSerialization()
|
||||
public override friction: number = 0;
|
||||
|
||||
@IgnoreSerialization()
|
||||
public override restitution: number = 0;
|
||||
}
|
||||
```
|
||||
|
||||
### 场景自定义数据
|
||||
|
||||
除了实体和组件,还可以序列化场景级别的配置数据:
|
||||
|
||||
+24
-263
@@ -33,26 +33,6 @@ class MyService implements IService {
|
||||
}
|
||||
```
|
||||
|
||||
#### 服务标识符(ServiceIdentifier)
|
||||
|
||||
服务标识符用于在容器中唯一标识一个服务,支持两种类型:
|
||||
|
||||
- **类构造函数**: 直接使用服务类作为标识符,适用于具体实现类
|
||||
- **Symbol**: 使用 Symbol 作为标识符,适用于接口抽象(推荐用于插件和跨包场景)
|
||||
|
||||
```typescript
|
||||
// 方式1: 使用类作为标识符
|
||||
Core.services.registerSingleton(DataService);
|
||||
const data = Core.services.resolve(DataService);
|
||||
|
||||
// 方式2: 使用 Symbol 作为标识符(推荐用于接口)
|
||||
const IFileSystem = Symbol.for('IFileSystem');
|
||||
Core.services.registerInstance(IFileSystem, new TauriFileSystem());
|
||||
const fs = Core.services.resolve<IFileSystem>(IFileSystem);
|
||||
```
|
||||
|
||||
> **提示**: 使用 `Symbol.for()` 而非 `Symbol()` 可确保跨包/跨模块共享同一个标识符。详见[高级用法 - 接口与 Symbol 标识符模式](#接口与-symbol-标识符模式)。
|
||||
|
||||
#### 生命周期
|
||||
|
||||
服务容器支持两种生命周期:
|
||||
@@ -64,13 +44,7 @@ const fs = Core.services.resolve<IFileSystem>(IFileSystem);
|
||||
|
||||
### 访问服务容器
|
||||
|
||||
ECS Framework 提供了三级服务容器:
|
||||
|
||||
> **版本说明**:World 服务容器功能在 v2.2.13+ 版本中可用
|
||||
|
||||
#### Core 级别服务容器
|
||||
|
||||
应用程序全局服务容器,可以通过 `Core.services` 访问:
|
||||
Core 类内置了服务容器,可以通过 `Core.services` 访问:
|
||||
|
||||
```typescript
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
@@ -78,53 +52,10 @@ import { Core } from '@esengine/ecs-framework';
|
||||
// 初始化Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 访问全局服务容器
|
||||
// 访问服务容器
|
||||
const container = Core.services;
|
||||
```
|
||||
|
||||
#### World 级别服务容器
|
||||
|
||||
每个 World 拥有独立的服务容器,用于管理 World 范围内的服务:
|
||||
|
||||
```typescript
|
||||
import { World } from '@esengine/ecs-framework';
|
||||
|
||||
// 创建 World
|
||||
const world = new World({ name: 'GameWorld' });
|
||||
|
||||
// 访问 World 级别的服务容器
|
||||
const worldContainer = world.services;
|
||||
|
||||
// 注册 World 级别的服务
|
||||
world.services.registerSingleton(RoomManager);
|
||||
```
|
||||
|
||||
#### Scene 级别服务容器
|
||||
|
||||
每个 Scene 拥有独立的服务容器,用于管理 Scene 范围内的服务:
|
||||
|
||||
```typescript
|
||||
// 访问 Scene 级别的服务容器
|
||||
const sceneContainer = scene.services;
|
||||
|
||||
// 注册 Scene 级别的服务
|
||||
scene.services.registerSingleton(PhysicsSystem);
|
||||
```
|
||||
|
||||
#### 服务容器层级
|
||||
|
||||
```
|
||||
Core.services (应用程序全局)
|
||||
└─ World.services (World 级别)
|
||||
└─ Scene.services (Scene 级别)
|
||||
```
|
||||
|
||||
不同级别的服务容器是独立的,服务不会自动向上或向下查找。选择合适的容器级别:
|
||||
|
||||
- **Core.services**: 应用程序级别的全局服务(配置、插件管理器等)
|
||||
- **World.services**: World 级别的服务(房间管理器、多人游戏状态等)
|
||||
- **Scene.services**: Scene 级别的服务(ECS 系统、场景特定逻辑等)
|
||||
|
||||
### 注册服务
|
||||
|
||||
#### 注册单例服务
|
||||
@@ -353,20 +284,21 @@ class GameService implements IService {
|
||||
}
|
||||
```
|
||||
|
||||
### @InjectProperty 装饰器
|
||||
### @Inject 装饰器
|
||||
|
||||
通过属性装饰器注入依赖。注入时机是在构造函数执行后、`onInitialize()` 调用前完成:
|
||||
在构造函数中注入依赖:
|
||||
|
||||
```typescript
|
||||
import { Injectable, InjectProperty, IService } from '@esengine/ecs-framework';
|
||||
import { Injectable, Inject, IService } from '@esengine/ecs-framework';
|
||||
|
||||
@Injectable()
|
||||
class PlayerService implements IService {
|
||||
@InjectProperty(DataService)
|
||||
private data!: DataService;
|
||||
|
||||
@InjectProperty(GameService)
|
||||
private game!: GameService;
|
||||
constructor(
|
||||
@Inject(DataService) private data: DataService,
|
||||
@Inject(GameService) private game: GameService
|
||||
) {
|
||||
// data 和 game 会自动从容器中解析
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
// 清理资源
|
||||
@@ -374,35 +306,6 @@ class PlayerService implements IService {
|
||||
}
|
||||
```
|
||||
|
||||
在 EntitySystem 中使用属性注入:
|
||||
|
||||
```typescript
|
||||
@Injectable()
|
||||
class CombatSystem extends EntitySystem {
|
||||
@InjectProperty(TimeService)
|
||||
private timeService!: TimeService;
|
||||
|
||||
@InjectProperty(AudioService)
|
||||
private audio!: AudioService;
|
||||
|
||||
constructor() {
|
||||
super(Matcher.all(Health, Attack));
|
||||
}
|
||||
|
||||
onInitialize(): void {
|
||||
// 此时属性已注入完成,可以安全使用
|
||||
console.log('Delta time:', this.timeService.getDeltaTime());
|
||||
}
|
||||
|
||||
processEntity(entity: Entity): void {
|
||||
// 使用注入的服务
|
||||
this.audio.playSound('attack');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **注意**: 属性声明时使用 `!` 断言(如 `private data!: DataService`),表示该属性会在使用前被注入。
|
||||
|
||||
### 注册可注入服务
|
||||
|
||||
使用 `registerInjectable` 自动处理依赖注入:
|
||||
@@ -410,10 +313,10 @@ class CombatSystem extends EntitySystem {
|
||||
```typescript
|
||||
import { registerInjectable } from '@esengine/ecs-framework';
|
||||
|
||||
// 注册服务(会自动解析 @InjectProperty 依赖)
|
||||
// 注册服务(会自动解析@Inject依赖)
|
||||
registerInjectable(Core.services, PlayerService);
|
||||
|
||||
// 解析时会自动注入属性依赖
|
||||
// 解析时会自动注入依赖
|
||||
const player = Core.services.resolve(PlayerService);
|
||||
```
|
||||
|
||||
@@ -541,164 +444,22 @@ registerInjectable(Core.services, NetworkService);
|
||||
|
||||
## 高级用法
|
||||
|
||||
### 接口与 Symbol 标识符模式
|
||||
### 服务替换(测试)
|
||||
|
||||
在大型项目或需要跨平台适配的游戏中,推荐使用"接口 + Symbol.for 标识符"模式。这种模式实现了真正的依赖倒置,让代码依赖于抽象而非具体实现。
|
||||
|
||||
#### 为什么使用 Symbol.for
|
||||
|
||||
- **跨包共享**: `Symbol.for('key')` 在全局 Symbol 注册表中创建/获取 Symbol,确保不同包中使用相同的标识符
|
||||
- **接口解耦**: 消费者只依赖接口定义,不依赖具体实现类
|
||||
- **可替换实现**: 可以在运行时注入不同的实现(如测试 Mock、不同平台适配)
|
||||
|
||||
#### 定义接口和标识符
|
||||
|
||||
以音频服务为例,游戏需要在不同平台(Web、微信小游戏、原生App)使用不同的音频实现:
|
||||
在测试中替换真实服务为模拟服务:
|
||||
|
||||
```typescript
|
||||
// IAudioService.ts - 定义接口和标识符
|
||||
export interface IAudioService {
|
||||
dispose(): void;
|
||||
playSound(id: string): void;
|
||||
playMusic(id: string, loop?: boolean): void;
|
||||
stopMusic(): void;
|
||||
setVolume(volume: number): void;
|
||||
preload(id: string, url: string): Promise<void>;
|
||||
// 测试代码
|
||||
class MockDataService implements IService {
|
||||
getData(key: string) {
|
||||
return 'mock data';
|
||||
}
|
||||
|
||||
dispose(): void {}
|
||||
}
|
||||
|
||||
// 使用 Symbol.for 确保跨包共享同一个 Symbol
|
||||
export const IAudioService = Symbol.for('IAudioService');
|
||||
```
|
||||
|
||||
#### 实现接口
|
||||
|
||||
```typescript
|
||||
// WebAudioService.ts - Web 平台实现
|
||||
import { IAudioService } from './IAudioService';
|
||||
|
||||
export class WebAudioService implements IAudioService {
|
||||
private audioContext: AudioContext;
|
||||
private sounds: Map<string, AudioBuffer> = new Map();
|
||||
|
||||
constructor() {
|
||||
this.audioContext = new AudioContext();
|
||||
}
|
||||
|
||||
playSound(id: string): void {
|
||||
const buffer = this.sounds.get(id);
|
||||
if (buffer) {
|
||||
const source = this.audioContext.createBufferSource();
|
||||
source.buffer = buffer;
|
||||
source.connect(this.audioContext.destination);
|
||||
source.start();
|
||||
}
|
||||
}
|
||||
|
||||
async preload(id: string, url: string): Promise<void> {
|
||||
const response = await fetch(url);
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
|
||||
this.sounds.set(id, audioBuffer);
|
||||
}
|
||||
|
||||
// ... 其他方法实现
|
||||
|
||||
dispose(): void {
|
||||
this.audioContext.close();
|
||||
this.sounds.clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// WechatAudioService.ts - 微信小游戏平台实现
|
||||
export class WechatAudioService implements IAudioService {
|
||||
private innerAudioContexts: Map<string, WechatMinigame.InnerAudioContext> = new Map();
|
||||
|
||||
playSound(id: string): void {
|
||||
const ctx = this.innerAudioContexts.get(id);
|
||||
if (ctx) {
|
||||
ctx.play();
|
||||
}
|
||||
}
|
||||
|
||||
async preload(id: string, url: string): Promise<void> {
|
||||
const ctx = wx.createInnerAudioContext();
|
||||
ctx.src = url;
|
||||
this.innerAudioContexts.set(id, ctx);
|
||||
}
|
||||
|
||||
// ... 其他方法实现
|
||||
|
||||
dispose(): void {
|
||||
for (const ctx of this.innerAudioContexts.values()) {
|
||||
ctx.destroy();
|
||||
}
|
||||
this.innerAudioContexts.clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 注册和使用
|
||||
|
||||
```typescript
|
||||
import { IAudioService } from './IAudioService';
|
||||
import { WebAudioService } from './WebAudioService';
|
||||
import { WechatAudioService } from './WechatAudioService';
|
||||
|
||||
// 根据平台注册不同实现
|
||||
if (typeof wx !== 'undefined') {
|
||||
Core.services.registerInstance(IAudioService, new WechatAudioService());
|
||||
} else {
|
||||
Core.services.registerInstance(IAudioService, new WebAudioService());
|
||||
}
|
||||
|
||||
// 业务代码中使用 - 不关心具体实现
|
||||
const audio = Core.services.resolve<IAudioService>(IAudioService);
|
||||
await audio.preload('explosion', '/sounds/explosion.mp3');
|
||||
audio.playSound('explosion');
|
||||
```
|
||||
|
||||
#### 跨模块使用
|
||||
|
||||
```typescript
|
||||
// 在游戏系统中使用
|
||||
import { IAudioService } from '@mygame/core';
|
||||
|
||||
class CombatSystem extends EntitySystem {
|
||||
private audio: IAudioService;
|
||||
|
||||
initialize(): void {
|
||||
// 获取音频服务,不需要知道具体实现
|
||||
this.audio = this.scene.services.resolve<IAudioService>(IAudioService);
|
||||
}
|
||||
|
||||
onEntityDeath(entity: Entity): void {
|
||||
this.audio.playSound('death');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Symbol vs Symbol.for
|
||||
|
||||
```typescript
|
||||
// Symbol() - 每次创建唯一的 Symbol
|
||||
const sym1 = Symbol('test');
|
||||
const sym2 = Symbol('test');
|
||||
console.log(sym1 === sym2); // false - 不同的 Symbol
|
||||
|
||||
// Symbol.for() - 在全局注册表中共享
|
||||
const sym3 = Symbol.for('test');
|
||||
const sym4 = Symbol.for('test');
|
||||
console.log(sym3 === sym4); // true - 同一个 Symbol
|
||||
|
||||
// 跨包场景
|
||||
// package-a/index.ts
|
||||
export const IMyService = Symbol.for('IMyService');
|
||||
|
||||
// package-b/index.ts (不同的包)
|
||||
const IMyService = Symbol.for('IMyService');
|
||||
// 与 package-a 中的是同一个 Symbol!
|
||||
// 注册模拟服务(用于测试)
|
||||
Core.services.registerInstance(DataService, new MockDataService());
|
||||
```
|
||||
|
||||
### 循环依赖检测
|
||||
|
||||
+3
-173
@@ -157,45 +157,8 @@ const nameMatcher = Matcher.byName("Player"); // 匹配名称为 "Player" 的实
|
||||
|
||||
// 单组件匹配
|
||||
const componentMatcher = Matcher.byComponent(Health); // 匹配拥有 Health 组件的实体
|
||||
|
||||
// 不匹配任何实体
|
||||
const nothingMatcher = Matcher.nothing(); // 用于只需要生命周期回调的系统
|
||||
```
|
||||
|
||||
### 空匹配器 vs Nothing 匹配器
|
||||
|
||||
```typescript
|
||||
// empty() - 空条件,匹配所有实体
|
||||
const emptyMatcher = Matcher.empty();
|
||||
|
||||
// nothing() - 不匹配任何实体,用于只需要生命周期方法的系统
|
||||
const nothingMatcher = Matcher.nothing();
|
||||
|
||||
// 使用场景:只需要 onBegin/onEnd 生命周期的系统
|
||||
@ECSSystem('FrameTimer')
|
||||
class FrameTimerSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.nothing()); // 不处理任何实体
|
||||
}
|
||||
|
||||
protected onBegin(): void {
|
||||
// 每帧开始时执行,例如:记录帧开始时间
|
||||
console.log('帧开始');
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 永远不会被调用,因为没有匹配的实体
|
||||
}
|
||||
|
||||
protected onEnd(): void {
|
||||
// 每帧结束时执行
|
||||
console.log('帧结束');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> 💡 **提示**:更多关于 Matcher 和实体查询的详细用法,请参考 [实体查询系统](/guide/entity-query) 文档。
|
||||
|
||||
## 系统生命周期
|
||||
|
||||
系统提供了完整的生命周期回调:
|
||||
@@ -216,13 +179,11 @@ class ExampleSystem extends EntitySystem {
|
||||
// 主要的处理逻辑
|
||||
for (const entity of entities) {
|
||||
// 处理每个实体
|
||||
// ✅ 可以安全地在这里添加/移除组件,不会影响当前迭代
|
||||
}
|
||||
}
|
||||
|
||||
protected lateProcess(entities: readonly Entity[]): void {
|
||||
// 主处理之后的后期处理
|
||||
// ✅ 可以安全地在这里添加/移除组件,不会影响当前迭代
|
||||
}
|
||||
|
||||
protected onEnd(): void {
|
||||
@@ -272,68 +233,6 @@ class EnemyManagerSystem extends EntitySystem {
|
||||
}
|
||||
```
|
||||
|
||||
### 重要:onAdded/onRemoved 的调用时机
|
||||
|
||||
> ⚠️ **注意**:`onAdded` 和 `onRemoved` 回调是**同步调用**的,会在 `addComponent`/`removeComponent` 返回**之前**立即执行。
|
||||
|
||||
这意味着:
|
||||
|
||||
```typescript
|
||||
// ❌ 错误的用法:链式赋值在 onAdded 之后才执行
|
||||
const comp = entity.addComponent(new ClickComponent());
|
||||
comp.element = this._element; // 此时 onAdded 已经执行完了!
|
||||
|
||||
// ✅ 正确的用法:通过构造函数传入初始值
|
||||
const comp = entity.addComponent(new ClickComponent(this._element));
|
||||
|
||||
// ✅ 或者使用 createComponent 方法
|
||||
const comp = entity.createComponent(ClickComponent, this._element);
|
||||
```
|
||||
|
||||
**为什么这样设计?**
|
||||
|
||||
事件驱动设计确保 `onAdded`/`onRemoved` 回调不受系统注册顺序的影响。当组件被添加时,所有监听该组件的系统都会立即收到通知,而不是等到下一帧。
|
||||
|
||||
**最佳实践:**
|
||||
|
||||
1. 组件的初始值应该通过**构造函数**传入
|
||||
2. 不要依赖 `addComponent` 返回后再设置属性
|
||||
3. 如果需要在 `onAdded` 中访问组件属性,确保这些属性在构造时已经设置
|
||||
|
||||
### 在 process/lateProcess 中安全地修改组件
|
||||
|
||||
在 `process` 或 `lateProcess` 中迭代实体时,可以安全地添加或移除组件,不会影响当前的迭代过程:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Damage')
|
||||
class DamageSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Health, DamageReceiver));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health);
|
||||
const damage = entity.getComponent(DamageReceiver);
|
||||
|
||||
if (health && damage) {
|
||||
health.current -= damage.amount;
|
||||
|
||||
// ✅ 安全:移除组件不会影响当前迭代
|
||||
entity.removeComponent(damage);
|
||||
|
||||
if (health.current <= 0) {
|
||||
// ✅ 安全:添加组件也不会影响当前迭代
|
||||
entity.addComponent(new Dead());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
框架会在每次 `process`/`lateProcess` 调用前创建实体列表的快照,确保迭代过程中的组件变化不会导致跳过实体或重复处理。
|
||||
|
||||
## 系统属性和方法
|
||||
|
||||
### 重要属性
|
||||
@@ -521,8 +420,6 @@ class GameScene extends Scene {
|
||||
|
||||
### 系统更新顺序
|
||||
|
||||
系统的执行顺序由 `updateOrder` 属性决定,数值越小越先执行:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Input')
|
||||
class InputSystem extends EntitySystem {
|
||||
@@ -549,25 +446,6 @@ class RenderSystem extends EntitySystem {
|
||||
}
|
||||
```
|
||||
|
||||
#### 稳定排序:addOrder
|
||||
|
||||
当多个系统的 `updateOrder` 相同时,框架使用 `addOrder`(添加顺序)作为第二排序条件,确保排序结果稳定可预测:
|
||||
|
||||
```typescript
|
||||
// 这两个系统 updateOrder 都是默认值 0
|
||||
@ECSSystem('SystemA')
|
||||
class SystemA extends EntitySystem { /* ... */ }
|
||||
|
||||
@ECSSystem('SystemB')
|
||||
class SystemB extends EntitySystem { /* ... */ }
|
||||
|
||||
// 添加顺序决定了执行顺序
|
||||
scene.addSystem(new SystemA()); // addOrder = 0,先执行
|
||||
scene.addSystem(new SystemB()); // addOrder = 1,后执行
|
||||
```
|
||||
|
||||
> **注意**:`addOrder` 由框架在 `addSystem` 时自动设置,无需手动管理。这确保了相同 `updateOrder` 的系统按照添加顺序执行,避免了排序不稳定导致的随机行为。
|
||||
|
||||
## 复杂系统示例
|
||||
|
||||
### 碰撞检测系统
|
||||
@@ -685,28 +563,9 @@ class GameSystem extends EntitySystem {
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 使用 @ECSSystem 装饰器
|
||||
### 2. 使用装饰器
|
||||
|
||||
`@ECSSystem` 是系统类必须使用的装饰器,它为系统提供类型标识和元数据管理。
|
||||
|
||||
#### 为什么必须使用
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| **类型识别** | 提供稳定的系统名称,代码混淆后仍能正确识别 |
|
||||
| **调试支持** | 在性能监控、日志和调试工具中显示可读的系统名称 |
|
||||
| **系统管理** | 通过名称查找和管理系统 |
|
||||
| **序列化支持** | 场景序列化时可以记录系统配置 |
|
||||
|
||||
#### 基本语法
|
||||
|
||||
```typescript
|
||||
@ECSSystem(systemName: string)
|
||||
```
|
||||
|
||||
- `systemName`: 系统的名称,建议使用描述性的名称
|
||||
|
||||
#### 使用示例
|
||||
**必须使用 `@ECSSystem` 装饰器**:
|
||||
|
||||
```typescript
|
||||
// ✅ 正确的用法
|
||||
@@ -715,41 +574,12 @@ class PhysicsSystem extends EntitySystem {
|
||||
// 系统实现
|
||||
}
|
||||
|
||||
// ✅ 推荐:使用描述性的名称
|
||||
@ECSSystem('PlayerMovement')
|
||||
class PlayerMovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Player, Position, Velocity));
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ 错误的用法 - 没有装饰器
|
||||
class BadSystem extends EntitySystem {
|
||||
// 这样定义的系统可能在生产环境出现问题:
|
||||
// 1. 代码压缩后类名变化,无法正确识别
|
||||
// 2. 性能监控和调试工具显示不正确的名称
|
||||
// 这样定义的系统可能在生产环境出现问题
|
||||
}
|
||||
```
|
||||
|
||||
#### 系统名称的作用
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Combat')
|
||||
class CombatSystem extends EntitySystem {
|
||||
protected onInitialize(): void {
|
||||
// 使用 systemName 属性访问系统名称
|
||||
console.log(`系统 ${this.systemName} 已初始化`); // 输出: 系统 Combat 已初始化
|
||||
}
|
||||
}
|
||||
|
||||
// 通过名称查找系统
|
||||
const combat = scene.getSystemByName('Combat');
|
||||
|
||||
// 性能监控中会显示系统名称
|
||||
const perfData = combatSystem.getPerformanceData();
|
||||
console.log(`${combatSystem.systemName} 执行时间: ${perfData?.executionTime}ms`);
|
||||
```
|
||||
|
||||
### 3. 合理的更新顺序
|
||||
|
||||
```typescript
|
||||
|
||||
@@ -435,7 +435,7 @@ const worldManager = Core.services.resolve(WorldManager);
|
||||
// {
|
||||
// maxWorlds: 50,
|
||||
// autoCleanup: true,
|
||||
// cleanupFrameInterval: 1800 // 间隔多少帧清理闲置 World
|
||||
// cleanupInterval: 30000 // 30 秒
|
||||
// }
|
||||
```
|
||||
|
||||
|
||||
+20
-314
@@ -1,317 +1,23 @@
|
||||
---
|
||||
layout: page
|
||||
title: ESEngine - 高性能 TypeScript ECS 框架
|
||||
---
|
||||
layout: home
|
||||
|
||||
<ParticleHero />
|
||||
hero:
|
||||
name: "ECS Framework"
|
||||
text: "高性能ECS框架"
|
||||
tagline: "为Javascript游戏开发而设计"
|
||||
actions:
|
||||
- theme: brand
|
||||
text: 快速开始
|
||||
link: /guide/getting-started
|
||||
- theme: alt
|
||||
text: 查看示例
|
||||
link: https://github.com/esengine/lawn-mower-demo
|
||||
|
||||
<section class="news-section">
|
||||
<div class="news-container">
|
||||
<div class="news-header">
|
||||
<h2 class="news-title">快速入口</h2>
|
||||
<a href="/guide/" class="news-more">查看文档</a>
|
||||
</div>
|
||||
<div class="news-grid">
|
||||
<a href="/guide/getting-started" class="news-card">
|
||||
<div class="news-card-image" style="background: linear-gradient(135deg, #1e3a5f 0%, #1e1e1e 100%);">
|
||||
<div class="news-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24"><path fill="#4fc1ff" d="M12 3L1 9l4 2.18v6L12 21l7-3.82v-6l2-1.09V17h2V9zm6.82 6L12 12.72L5.18 9L12 5.28zM17 16l-5 2.72L7 16v-3.73L12 15l5-2.73z"/></svg>
|
||||
</div>
|
||||
<span class="news-badge">快速开始</span>
|
||||
</div>
|
||||
<div class="news-card-content">
|
||||
<h3>5 分钟上手 ESEngine</h3>
|
||||
<p>从安装到创建第一个 ECS 应用,快速了解核心概念。</p>
|
||||
</div>
|
||||
</a>
|
||||
<a href="/guide/behavior-tree/" class="news-card">
|
||||
<div class="news-card-image" style="background: linear-gradient(135deg, #1e3a5f 0%, #1e1e1e 100%);">
|
||||
<div class="news-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24"><path fill="#4ec9b0" d="M12 2a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m3 20h-1v-7l-2-2l-2 2v7H9v-7.5l-2 2V22H6v-6l3-3l1-3.5c-.3.4-.6.7-1 1L6 9v1H4V8l5-3c.5-.3 1.1-.5 1.7-.5H11c.6 0 1.2.2 1.7.5l5 3v2h-2V9l-3 1.5c-.4-.3-.7-.6-1-1l1 3.5l3 3v6Z"/></svg>
|
||||
</div>
|
||||
<span class="news-badge">AI 系统</span>
|
||||
</div>
|
||||
<div class="news-card-content">
|
||||
<h3>行为树可视化编辑器</h3>
|
||||
<p>内置 AI 行为树系统,支持可视化编辑和实时调试。</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="features-section">
|
||||
<div class="features-container">
|
||||
<h2 class="features-title">核心特性</h2>
|
||||
<div class="features-grid">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#4fc1ff" d="M13 2.05v2.02c3.95.49 7 3.85 7 7.93c0 1.45-.39 2.79-1.06 3.95l1.59 1.09A9.94 9.94 0 0 0 22 12c0-5.18-3.95-9.45-9-9.95M12 19c-3.87 0-7-3.13-7-7c0-3.53 2.61-6.43 6-6.92V2.05c-5.06.5-9 4.76-9 9.95c0 5.52 4.47 10 9.99 10c3.31 0 6.24-1.61 8.06-4.09l-1.6-1.1A7.93 7.93 0 0 1 12 19"/><path fill="#4fc1ff" d="M12 6a6 6 0 0 0-6 6c0 3.31 2.69 6 6 6a6 6 0 0 0 0-12m0 10c-2.21 0-4-1.79-4-4s1.79-4 4-4s4 1.79 4 4s-1.79 4-4 4"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">高性能 ECS 架构</h3>
|
||||
<p class="feature-desc">基于数据驱动的实体组件系统,支持大规模实体处理,缓存友好的内存布局。</p>
|
||||
<a href="/guide/entity" class="feature-link">了解更多 →</a>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#569cd6" d="M3 3h18v18H3zm16.525 13.707c0-.795-.272-1.425-.816-1.89c-.544-.465-1.404-.804-2.58-1.016l-1.704-.296c-.616-.104-1.052-.26-1.308-.468c-.256-.21-.384-.468-.384-.776c0-.392.168-.7.504-.924c.336-.224.8-.336 1.392-.336c.56 0 1.008.124 1.344.372c.336.248.536.584.6 1.008h2.016c-.08-.96-.464-1.716-1.152-2.268c-.688-.552-1.6-.828-2.736-.828c-1.2 0-2.148.3-2.844.9c-.696.6-1.044 1.38-1.044 2.34c0 .76.252 1.368.756 1.824c.504.456 1.308.792 2.412.996l1.704.312c.624.12 1.068.28 1.332.48c.264.2.396.46.396.78c0 .424-.192.756-.576.996c-.384.24-.9.36-1.548.36c-.672 0-1.2-.14-1.584-.42c-.384-.28-.608-.668-.672-1.164H8.868c.048 1.016.46 1.808 1.236 2.376c.776.568 1.796.852 3.06.852c1.24 0 2.22-.292 2.94-.876c.72-.584 1.08-1.364 1.08-2.34z"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">完整类型支持</h3>
|
||||
<p class="feature-desc">100% TypeScript 编写,完整的类型定义和编译时检查,提供最佳的开发体验。</p>
|
||||
<a href="/guide/component" class="feature-link">了解更多 →</a>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#4ec9b0" d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10s10-4.5 10-10S17.5 2 12 2m0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8s8 3.59 8 8s-3.59 8-8 8m-5-8l4-4v3h4v2h-4v3z"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">可视化行为树</h3>
|
||||
<p class="feature-desc">内置 AI 行为树系统,提供可视化编辑器,支持自定义节点和实时调试。</p>
|
||||
<a href="/guide/behavior-tree/" class="feature-link">了解更多 →</a>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#c586c0" d="M4 6h18V4H4c-1.1 0-2 .9-2 2v11H0v3h14v-3H4zm19 2h-6c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h6c.55 0 1-.45 1-1V9c0-.55-.45-1-1-1m-1 9h-4v-7h4z"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">多平台支持</h3>
|
||||
<p class="feature-desc">支持浏览器、Node.js、微信小游戏等多平台,可与主流游戏引擎无缝集成。</p>
|
||||
<a href="/guide/platform-adapter" class="feature-link">了解更多 →</a>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#dcdcaa" d="M4 3h6v2H4v14h6v2H4c-1.1 0-2-.9-2-2V5c0-1.1.9-2 2-2m9 0h6c1.1 0 2 .9 2 2v14c0 1.1-.9 2-2 2h-6v-2h6V5h-6zm-1 7h4v2h-4z"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">模块化设计</h3>
|
||||
<p class="feature-desc">核心功能独立打包,按需引入。支持自定义插件扩展,灵活适配不同项目。</p>
|
||||
<a href="/guide/plugin-system" class="feature-link">了解更多 →</a>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#9cdcfe" d="M22.7 19l-9.1-9.1c.9-2.3.4-5-1.5-6.9c-2-2-5-2.4-7.4-1.3L9 6L6 9L1.6 4.7C.4 7.1.9 10.1 2.9 12.1c1.9 1.9 4.6 2.4 6.9 1.5l9.1 9.1c.4.4 1 .4 1.4 0l2.3-2.3c.5-.4.5-1.1.1-1.4"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">开发者工具</h3>
|
||||
<p class="feature-desc">内置性能监控、调试工具、序列化系统等,提供完整的开发工具链。</p>
|
||||
<a href="/guide/logging" class="feature-link">了解更多 →</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style scoped>
|
||||
/* 首页专用样式 | Home page specific styles */
|
||||
.news-section {
|
||||
background: #0d0d0d;
|
||||
padding: 64px 0;
|
||||
border-top: 1px solid #2a2a2a;
|
||||
}
|
||||
|
||||
.news-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 48px;
|
||||
}
|
||||
|
||||
.news-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.news-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.news-more {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 20px;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 6px;
|
||||
color: #a0a0a0;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.news-more:hover {
|
||||
background: #252525;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.news-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.news-card {
|
||||
display: flex;
|
||||
background: #1f1f1f;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.news-card:hover {
|
||||
border-color: #3b9eff;
|
||||
}
|
||||
|
||||
.news-card-image {
|
||||
width: 200px;
|
||||
min-height: 140px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.news-icon {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.news-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
background: transparent;
|
||||
border: 1px solid #3a3a3a;
|
||||
border-radius: 16px;
|
||||
color: #a0a0a0;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.news-card-content {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.news-card-content h3 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.news-card-content p {
|
||||
font-size: 0.875rem;
|
||||
color: #707070;
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.features-section {
|
||||
background: #0d0d0d;
|
||||
padding: 64px 0;
|
||||
}
|
||||
|
||||
.features-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 48px;
|
||||
}
|
||||
|
||||
.features-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
margin: 0 0 32px 0;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: #1f1f1f;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
border-color: #3b9eff;
|
||||
background: #252525;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: #0d0d0d;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.feature-desc {
|
||||
font-size: 14px;
|
||||
color: #707070;
|
||||
line-height: 1.7;
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
|
||||
.feature-link {
|
||||
font-size: 14px;
|
||||
color: #3b9eff;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.feature-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.news-container,
|
||||
.features-container {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.news-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.news-card {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.news-card-image {
|
||||
width: 100%;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
features:
|
||||
- title: 高性能
|
||||
details: 支持大规模实体处理
|
||||
- title: 类型安全
|
||||
details: 完整的TypeScript支持,编译时类型检查
|
||||
- title: 模块化设计
|
||||
details: 核心功能独立打包,支持多平台
|
||||
---
|
||||
@@ -1 +0,0 @@
|
||||
esengine.cn
|
||||
+24
-30
@@ -10,43 +10,38 @@ export default [
|
||||
parser: tseslint.parser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module',
|
||||
project: true,
|
||||
tsconfigRootDir: import.meta.dirname
|
||||
sourceType: 'module'
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
'semi': ['warn', 'always'],
|
||||
'quotes': ['warn', 'single', { avoidEscape: true }],
|
||||
'indent': ['warn', 4, {
|
||||
SwitchCase: 1,
|
||||
ignoredNodes: [
|
||||
'PropertyDefinition[decorators.length > 0]',
|
||||
'TSTypeParameterInstantiation'
|
||||
]
|
||||
}],
|
||||
'semi': 'warn',
|
||||
'quotes': 'warn',
|
||||
'indent': 'off',
|
||||
'no-trailing-spaces': 'warn',
|
||||
'eol-last': ['warn', 'always'],
|
||||
'comma-dangle': ['warn', 'never'],
|
||||
'object-curly-spacing': ['warn', 'always'],
|
||||
'array-bracket-spacing': ['warn', 'never'],
|
||||
'arrow-parens': ['warn', 'always'],
|
||||
'no-multiple-empty-lines': ['warn', { max: 2, maxEOF: 1 }],
|
||||
'eol-last': 'warn',
|
||||
'comma-dangle': 'warn',
|
||||
'object-curly-spacing': 'warn',
|
||||
'array-bracket-spacing': 'warn',
|
||||
'arrow-parens': 'warn',
|
||||
'prefer-const': 'warn',
|
||||
'no-multiple-empty-lines': 'warn',
|
||||
'no-console': 'off',
|
||||
'no-empty': 'warn',
|
||||
'no-case-declarations': 'warn',
|
||||
'no-useless-catch': 'warn',
|
||||
'no-prototype-builtins': 'warn',
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'warn',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'warn',
|
||||
'@typescript-eslint/no-unsafe-call': 'warn',
|
||||
'@typescript-eslint/no-unsafe-return': 'warn',
|
||||
'@typescript-eslint/no-unsafe-argument': 'warn',
|
||||
'@typescript-eslint/no-unsafe-function-type': 'warn',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||
'@typescript-eslint/no-unsafe-call': 'off',
|
||||
'@typescript-eslint/no-unsafe-return': 'off',
|
||||
'@typescript-eslint/no-unsafe-argument': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
|
||||
'@typescript-eslint/no-non-null-assertion': 'off'
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/no-require-imports': 'warn',
|
||||
'@typescript-eslint/no-this-alias': 'warn',
|
||||
'no-case-declarations': 'warn',
|
||||
'no-prototype-builtins': 'warn',
|
||||
'no-empty': 'warn',
|
||||
'no-useless-catch': 'warn'
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -65,8 +60,7 @@ export default [
|
||||
'examples/lawn-mower-demo/**',
|
||||
'extensions/**',
|
||||
'**/*.min.js',
|
||||
'**/*.d.ts',
|
||||
'**/wasm/**'
|
||||
'**/*.d.ts'
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
Generated
-352
@@ -1,352 +0,0 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@esengine/ecs-framework':
|
||||
specifier: file:../../packages/core
|
||||
version: file:../../packages/core
|
||||
devDependencies:
|
||||
typescript:
|
||||
specifier: ^5.0.0
|
||||
version: 5.9.3
|
||||
vite:
|
||||
specifier: ^4.0.0
|
||||
version: 4.5.14
|
||||
|
||||
packages:
|
||||
|
||||
'@esbuild/android-arm64@0.18.20':
|
||||
resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm@0.18.20':
|
||||
resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-x64@0.18.20':
|
||||
resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/darwin-arm64@0.18.20':
|
||||
resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-x64@0.18.20':
|
||||
resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/freebsd-arm64@0.18.20':
|
||||
resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-x64@0.18.20':
|
||||
resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/linux-arm64@0.18.20':
|
||||
resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm@0.18.20':
|
||||
resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ia32@0.18.20':
|
||||
resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-loong64@0.18.20':
|
||||
resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-mips64el@0.18.20':
|
||||
resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ppc64@0.18.20':
|
||||
resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-riscv64@0.18.20':
|
||||
resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-s390x@0.18.20':
|
||||
resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-x64@0.18.20':
|
||||
resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/netbsd-x64@0.18.20':
|
||||
resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/openbsd-x64@0.18.20':
|
||||
resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/sunos-x64@0.18.20':
|
||||
resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@esbuild/win32-arm64@0.18.20':
|
||||
resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-ia32@0.18.20':
|
||||
resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-x64@0.18.20':
|
||||
resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@esengine/ecs-framework@file:../../packages/core':
|
||||
resolution: {directory: ../../packages/core, type: directory}
|
||||
|
||||
esbuild@0.18.20:
|
||||
resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==}
|
||||
engines: {node: '>=12'}
|
||||
hasBin: true
|
||||
|
||||
fsevents@2.3.3:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
nanoid@3.3.11:
|
||||
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
postcss@8.5.6:
|
||||
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
rollup@3.29.5:
|
||||
resolution: {integrity: sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==}
|
||||
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
|
||||
hasBin: true
|
||||
|
||||
source-map-js@1.2.1:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
typescript@5.9.3:
|
||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
vite@4.5.14:
|
||||
resolution: {integrity: sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@types/node': '>= 14'
|
||||
less: '*'
|
||||
lightningcss: ^1.21.0
|
||||
sass: '*'
|
||||
stylus: '*'
|
||||
sugarss: '*'
|
||||
terser: ^5.4.0
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
less:
|
||||
optional: true
|
||||
lightningcss:
|
||||
optional: true
|
||||
sass:
|
||||
optional: true
|
||||
stylus:
|
||||
optional: true
|
||||
sugarss:
|
||||
optional: true
|
||||
terser:
|
||||
optional: true
|
||||
|
||||
snapshots:
|
||||
|
||||
'@esbuild/android-arm64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esengine/ecs-framework@file:../../packages/core':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
esbuild@0.18.20:
|
||||
optionalDependencies:
|
||||
'@esbuild/android-arm': 0.18.20
|
||||
'@esbuild/android-arm64': 0.18.20
|
||||
'@esbuild/android-x64': 0.18.20
|
||||
'@esbuild/darwin-arm64': 0.18.20
|
||||
'@esbuild/darwin-x64': 0.18.20
|
||||
'@esbuild/freebsd-arm64': 0.18.20
|
||||
'@esbuild/freebsd-x64': 0.18.20
|
||||
'@esbuild/linux-arm': 0.18.20
|
||||
'@esbuild/linux-arm64': 0.18.20
|
||||
'@esbuild/linux-ia32': 0.18.20
|
||||
'@esbuild/linux-loong64': 0.18.20
|
||||
'@esbuild/linux-mips64el': 0.18.20
|
||||
'@esbuild/linux-ppc64': 0.18.20
|
||||
'@esbuild/linux-riscv64': 0.18.20
|
||||
'@esbuild/linux-s390x': 0.18.20
|
||||
'@esbuild/linux-x64': 0.18.20
|
||||
'@esbuild/netbsd-x64': 0.18.20
|
||||
'@esbuild/openbsd-x64': 0.18.20
|
||||
'@esbuild/sunos-x64': 0.18.20
|
||||
'@esbuild/win32-arm64': 0.18.20
|
||||
'@esbuild/win32-ia32': 0.18.20
|
||||
'@esbuild/win32-x64': 0.18.20
|
||||
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
nanoid@3.3.11: {}
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
postcss@8.5.6:
|
||||
dependencies:
|
||||
nanoid: 3.3.11
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
rollup@3.29.5:
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
tslib@2.8.1: {}
|
||||
|
||||
typescript@5.9.3: {}
|
||||
|
||||
vite@4.5.14:
|
||||
dependencies:
|
||||
esbuild: 0.18.20
|
||||
postcss: 8.5.6
|
||||
rollup: 3.29.5
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
Generated
+27061
File diff suppressed because it is too large
Load Diff
+24
-22
@@ -3,7 +3,6 @@
|
||||
"version": "2.1.29",
|
||||
"description": "ECS Framework Monorepo - 高性能ECS框架及其网络插件",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.22.0",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
@@ -18,26 +17,36 @@
|
||||
],
|
||||
"scripts": {
|
||||
"bootstrap": "lerna bootstrap",
|
||||
"clean": "turbo run clean",
|
||||
"build": "turbo run build",
|
||||
"build:filter": "turbo run build --filter",
|
||||
"build:core": "turbo run build --filter=@esengine/ecs-framework",
|
||||
"build:math": "turbo run build --filter=@esengine/ecs-framework-math",
|
||||
"build:editor": "turbo run build --filter=@esengine/editor-app...",
|
||||
"build:npm": "turbo run build:npm",
|
||||
"clean": "lerna run clean",
|
||||
"build": "npm run build:core && npm run build:math && npm run build:network-shared && npm run build:network-client && npm run build:network-server",
|
||||
"build:core": "cd packages/core && npm run build",
|
||||
"build:math": "cd packages/math && npm run build",
|
||||
"build:network-shared": "cd packages/network-shared && npm run build",
|
||||
"build:network-client": "cd packages/network-client && npm run build",
|
||||
"build:network-server": "cd packages/network-server && npm run build",
|
||||
"build:npm": "npm run build:npm:core && npm run build:npm:math && npm run build:npm:network-shared && npm run build:npm:network-client && npm run build:npm:network-server",
|
||||
"build:npm:core": "cd packages/core && npm run build:npm",
|
||||
"build:npm:math": "cd packages/math && npm run build:npm",
|
||||
"test": "turbo run test",
|
||||
"test:coverage": "turbo run test:coverage",
|
||||
"test:ci": "turbo run test:ci",
|
||||
"build:npm:network-shared": "cd packages/network-shared && npm run build:npm",
|
||||
"build:npm:network-client": "cd packages/network-client && npm run build:npm",
|
||||
"build:npm:network-server": "cd packages/network-server && npm run build:npm",
|
||||
"test": "lerna run test",
|
||||
"test:coverage": "lerna run test:coverage",
|
||||
"test:ci": "lerna run test:ci",
|
||||
"prepare:publish": "npm run build:npm && node scripts/pre-publish-check.cjs",
|
||||
"sync:versions": "node scripts/sync-versions.cjs",
|
||||
"publish:all": "npm run prepare:publish && npm run publish:all:dist",
|
||||
"publish:all:dist": "npm run publish:core && npm run publish:math",
|
||||
"publish:all:dist": "npm run publish:core && npm run publish:math && npm run publish:network-shared && npm run publish:network-client && npm run publish:network-server",
|
||||
"publish:core": "cd packages/core && npm run publish:npm",
|
||||
"publish:core:patch": "cd packages/core && npm run publish:patch",
|
||||
"publish:math": "cd packages/math && npm run publish:npm",
|
||||
"publish:math:patch": "cd packages/math && npm run publish:patch",
|
||||
"publish:network-shared": "cd packages/network-shared && npm run publish:npm",
|
||||
"publish:network-shared:patch": "cd packages/network-shared && npm run publish:patch",
|
||||
"publish:network-client": "cd packages/network-client && npm run publish:npm",
|
||||
"publish:network-client:patch": "cd packages/network-client && npm run publish:patch",
|
||||
"publish:network-server": "cd packages/network-server && npm run publish:npm",
|
||||
"publish:network-server:patch": "cd packages/network-server && npm run publish:patch",
|
||||
"publish": "lerna publish",
|
||||
"version": "lerna version",
|
||||
"release": "semantic-release",
|
||||
@@ -54,19 +63,15 @@
|
||||
"copy:worker-demo": "node scripts/update-worker-demo.js",
|
||||
"format": "prettier --write \"packages/**/src/**/*.{ts,tsx,js,jsx}\"",
|
||||
"format:check": "prettier --check \"packages/**/src/**/*.{ts,tsx,js,jsx}\"",
|
||||
"type-check": "turbo run type-check",
|
||||
"lint": "turbo run lint",
|
||||
"lint:fix": "turbo run lint:fix",
|
||||
"build:wasm": "cd packages/engine && wasm-pack build --dev --out-dir pkg",
|
||||
"build:wasm:release": "cd packages/engine && wasm-pack build --release --out-dir pkg",
|
||||
"copy-modules": "node scripts/copy-engine-modules.mjs"
|
||||
"type-check": "lerna run type-check",
|
||||
"lint": "eslint \"packages/**/src/**/*.{ts,tsx,js,jsx}\"",
|
||||
"lint:fix": "eslint \"packages/**/src/**/*.{ts,tsx,js,jsx}\" --fix"
|
||||
},
|
||||
"author": "yhh",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^18.6.0",
|
||||
"@commitlint/config-conventional": "^18.6.0",
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@iconify/json": "^2.2.388",
|
||||
"@rollup/plugin-commonjs": "^28.0.3",
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
@@ -96,11 +101,9 @@
|
||||
"semver": "^7.6.3",
|
||||
"size-limit": "^11.0.2",
|
||||
"ts-jest": "^29.4.0",
|
||||
"turbo": "^2.6.1",
|
||||
"typedoc": "^0.28.13",
|
||||
"typedoc-plugin-markdown": "^4.9.0",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.47.0",
|
||||
"unplugin-icons": "^22.3.0",
|
||||
"vitepress": "^1.6.4"
|
||||
},
|
||||
@@ -121,4 +124,3 @@
|
||||
"ws": "^8.18.2"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"name": "@esengine/asset-system-editor",
|
||||
"version": "1.0.0",
|
||||
"description": "Editor-side asset management: meta files, packing, and bundling",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"build:watch": "tsup --watch",
|
||||
"clean": "rimraf dist",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"keywords": [
|
||||
"ecs",
|
||||
"asset",
|
||||
"editor",
|
||||
"bundle",
|
||||
"packing"
|
||||
],
|
||||
"author": "yhh",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@esengine/asset-system": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@esengine/build-config": "workspace:*",
|
||||
"rimraf": "^5.0.0",
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/esengine/ecs-framework.git",
|
||||
"directory": "packages/asset-system-editor"
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
/**
|
||||
* Asset System Editor
|
||||
* 资产系统编辑器模块
|
||||
*
|
||||
* Editor-side asset management:
|
||||
* - Meta files (.meta) management
|
||||
* - Asset packing and bundling
|
||||
* - Import settings
|
||||
*
|
||||
* 编辑器端资产管理:
|
||||
* - 元数据文件 (.meta) 管理
|
||||
* - 资产打包和捆绑
|
||||
* - 导入设置
|
||||
*/
|
||||
|
||||
// Meta file management
|
||||
export {
|
||||
AssetMetaManager,
|
||||
type IAssetMeta,
|
||||
type IImportSettings,
|
||||
type IMetaFileSystem,
|
||||
generateGUID,
|
||||
getMetaFilePath,
|
||||
inferAssetType,
|
||||
getDefaultImportSettings,
|
||||
createAssetMeta,
|
||||
serializeAssetMeta,
|
||||
parseAssetMeta,
|
||||
isValidGUID
|
||||
} from './meta/AssetMetaFile';
|
||||
|
||||
// Asset packing
|
||||
export {
|
||||
AssetPacker,
|
||||
collectSceneAssets,
|
||||
type IPackingResult,
|
||||
type IPackedBundle,
|
||||
type IAssetFileReader
|
||||
} from './packing/AssetPacker';
|
||||
@@ -1,424 +0,0 @@
|
||||
/**
|
||||
* Asset Meta File (.meta) Management
|
||||
* 资产元数据文件 (.meta) 管理
|
||||
*
|
||||
* Each asset file has a companion .meta file that stores:
|
||||
* - GUID: Persistent unique identifier
|
||||
* - Import settings: How to process the asset
|
||||
* - Labels: User-defined tags
|
||||
*
|
||||
* 每个资产文件都有一个配套的 .meta 文件,存储:
|
||||
* - GUID:持久化唯一标识符
|
||||
* - 导入设置:如何处理资产
|
||||
* - 标签:用户定义的标签
|
||||
*/
|
||||
|
||||
import { AssetGUID, AssetType } from '@esengine/asset-system';
|
||||
|
||||
/**
|
||||
* Meta file content structure
|
||||
* 元数据文件内容结构
|
||||
*/
|
||||
export interface IAssetMeta {
|
||||
/** Persistent unique identifier | 持久化唯一标识符 */
|
||||
guid: AssetGUID;
|
||||
/** Asset type | 资产类型 */
|
||||
type: AssetType;
|
||||
/** Import settings | 导入设置 */
|
||||
importSettings?: IImportSettings;
|
||||
/** User-defined labels | 用户定义的标签 */
|
||||
labels?: string[];
|
||||
/** Meta file version | 元数据文件版本 */
|
||||
version: number;
|
||||
/** Last modified timestamp | 最后修改时间戳 */
|
||||
lastModified?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Import settings for different asset types
|
||||
* 不同资产类型的导入设置
|
||||
*/
|
||||
export interface IImportSettings {
|
||||
// Texture settings | 纹理设置
|
||||
maxSize?: number;
|
||||
compression?: 'none' | 'dxt' | 'etc2' | 'astc' | 'webp';
|
||||
generateMipmaps?: boolean;
|
||||
filterMode?: 'point' | 'bilinear' | 'trilinear';
|
||||
wrapMode?: 'clamp' | 'repeat' | 'mirror';
|
||||
premultiplyAlpha?: boolean;
|
||||
|
||||
// Audio settings | 音频设置
|
||||
audioFormat?: 'mp3' | 'ogg' | 'wav';
|
||||
sampleRate?: number;
|
||||
channels?: 1 | 2;
|
||||
normalize?: boolean;
|
||||
|
||||
// General settings | 通用设置
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new UUID v4
|
||||
* 生成新的 UUID v4
|
||||
*/
|
||||
export function generateGUID(): AssetGUID {
|
||||
// Use crypto.randomUUID if available (modern browsers/Node 19+)
|
||||
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
|
||||
// Fallback implementation
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||
const r = (Math.random() * 16) | 0;
|
||||
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get meta file path for an asset
|
||||
* 获取资产的元数据文件路径
|
||||
*/
|
||||
export function getMetaFilePath(assetPath: string): string {
|
||||
return `${assetPath}.meta`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer asset type from file extension
|
||||
* 根据文件扩展名推断资产类型
|
||||
*/
|
||||
export function inferAssetType(path: string): AssetType {
|
||||
const ext = path.split('.').pop()?.toLowerCase() || '';
|
||||
|
||||
const typeMap: Record<string, AssetType> = {
|
||||
// Textures
|
||||
png: 'texture',
|
||||
jpg: 'texture',
|
||||
jpeg: 'texture',
|
||||
gif: 'texture',
|
||||
webp: 'texture',
|
||||
bmp: 'texture',
|
||||
svg: 'texture',
|
||||
|
||||
// Audio
|
||||
mp3: 'audio',
|
||||
wav: 'audio',
|
||||
ogg: 'audio',
|
||||
m4a: 'audio',
|
||||
flac: 'audio',
|
||||
|
||||
// Data
|
||||
json: 'json',
|
||||
txt: 'text',
|
||||
xml: 'text',
|
||||
csv: 'text',
|
||||
|
||||
// Scenes and prefabs
|
||||
ecs: 'scene',
|
||||
prefab: 'prefab',
|
||||
|
||||
// Fonts
|
||||
ttf: 'font',
|
||||
otf: 'font',
|
||||
woff: 'font',
|
||||
woff2: 'font',
|
||||
|
||||
// Shaders
|
||||
glsl: 'shader',
|
||||
vert: 'shader',
|
||||
frag: 'shader',
|
||||
|
||||
// Custom types (plugins)
|
||||
tilemap: 'tilemap',
|
||||
tileset: 'tileset',
|
||||
btree: 'behavior-tree',
|
||||
bp: 'blueprint',
|
||||
mat: 'material'
|
||||
};
|
||||
|
||||
return typeMap[ext] || 'binary';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default import settings for asset type
|
||||
* 获取资产类型的默认导入设置
|
||||
*/
|
||||
export function getDefaultImportSettings(type: AssetType): IImportSettings {
|
||||
switch (type) {
|
||||
case 'texture':
|
||||
return {
|
||||
maxSize: 2048,
|
||||
compression: 'none',
|
||||
generateMipmaps: false,
|
||||
filterMode: 'bilinear',
|
||||
wrapMode: 'clamp',
|
||||
premultiplyAlpha: false
|
||||
};
|
||||
|
||||
case 'audio':
|
||||
return {
|
||||
audioFormat: 'mp3',
|
||||
sampleRate: 44100,
|
||||
channels: 2,
|
||||
normalize: false
|
||||
};
|
||||
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new meta file content
|
||||
* 创建新的元数据文件内容
|
||||
*/
|
||||
export function createAssetMeta(assetPath: string, overrides?: Partial<IAssetMeta>): IAssetMeta {
|
||||
const type = overrides?.type || inferAssetType(assetPath);
|
||||
|
||||
return {
|
||||
guid: overrides?.guid || generateGUID(),
|
||||
type,
|
||||
importSettings: overrides?.importSettings || getDefaultImportSettings(type),
|
||||
labels: overrides?.labels || [],
|
||||
version: 1,
|
||||
lastModified: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize meta to JSON string
|
||||
* 将元数据序列化为 JSON 字符串
|
||||
*/
|
||||
export function serializeAssetMeta(meta: IAssetMeta): string {
|
||||
return JSON.stringify(meta, null, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse meta from JSON string
|
||||
* 从 JSON 字符串解析元数据
|
||||
*/
|
||||
export function parseAssetMeta(json: string): IAssetMeta {
|
||||
const meta = JSON.parse(json) as IAssetMeta;
|
||||
|
||||
// Validate required fields
|
||||
if (!meta.guid || typeof meta.guid !== 'string') {
|
||||
throw new Error('Invalid meta file: missing or invalid guid');
|
||||
}
|
||||
if (!meta.type || typeof meta.type !== 'string') {
|
||||
throw new Error('Invalid meta file: missing or invalid type');
|
||||
}
|
||||
|
||||
// Set defaults for optional fields
|
||||
meta.version = meta.version || 1;
|
||||
meta.labels = meta.labels || [];
|
||||
meta.importSettings = meta.importSettings || {};
|
||||
|
||||
return meta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate GUID format (UUID v4)
|
||||
* 验证 GUID 格式 (UUID v4)
|
||||
*/
|
||||
export function isValidGUID(guid: string): boolean {
|
||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
return uuidRegex.test(guid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset Meta File Manager
|
||||
* 资产元数据文件管理器
|
||||
*
|
||||
* Handles reading/writing .meta files through a file system interface.
|
||||
*/
|
||||
export class AssetMetaManager {
|
||||
private _cache = new Map<string, IAssetMeta>();
|
||||
private _guidToPath = new Map<AssetGUID, string>();
|
||||
|
||||
/**
|
||||
* File system interface for reading/writing files
|
||||
* 用于读写文件的文件系统接口
|
||||
*/
|
||||
private _fs: IMetaFileSystem | null = null;
|
||||
|
||||
/**
|
||||
* Set file system interface
|
||||
* 设置文件系统接口
|
||||
*/
|
||||
setFileSystem(fs: IMetaFileSystem): void {
|
||||
this._fs = fs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create meta for an asset
|
||||
* 获取或创建资产的元数据
|
||||
*/
|
||||
async getOrCreateMeta(assetPath: string): Promise<IAssetMeta> {
|
||||
// Check cache first
|
||||
const cached = this._cache.get(assetPath);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const metaPath = getMetaFilePath(assetPath);
|
||||
|
||||
// Try to read existing meta file
|
||||
if (this._fs) {
|
||||
try {
|
||||
if (await this._fs.exists(metaPath)) {
|
||||
const content = await this._fs.readText(metaPath);
|
||||
const meta = parseAssetMeta(content);
|
||||
this._cache.set(assetPath, meta);
|
||||
this._guidToPath.set(meta.guid, assetPath);
|
||||
return meta;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`Failed to read meta file: ${metaPath}`, e);
|
||||
}
|
||||
}
|
||||
|
||||
// Create new meta
|
||||
const meta = createAssetMeta(assetPath);
|
||||
this._cache.set(assetPath, meta);
|
||||
this._guidToPath.set(meta.guid, assetPath);
|
||||
|
||||
// Save to file system
|
||||
if (this._fs) {
|
||||
try {
|
||||
await this._fs.writeText(metaPath, serializeAssetMeta(meta));
|
||||
} catch (e) {
|
||||
console.warn(`Failed to write meta file: ${metaPath}`, e);
|
||||
}
|
||||
}
|
||||
|
||||
return meta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get meta by GUID
|
||||
* 根据 GUID 获取元数据
|
||||
*/
|
||||
getMetaByGUID(guid: AssetGUID): IAssetMeta | undefined {
|
||||
const path = this._guidToPath.get(guid);
|
||||
return path ? this._cache.get(path) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get asset path by GUID
|
||||
* 根据 GUID 获取资产路径
|
||||
*/
|
||||
getPathByGUID(guid: AssetGUID): string | undefined {
|
||||
return this._guidToPath.get(guid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get GUID by asset path
|
||||
* 根据资产路径获取 GUID
|
||||
*/
|
||||
async getGUIDByPath(assetPath: string): Promise<AssetGUID> {
|
||||
const meta = await this.getOrCreateMeta(assetPath);
|
||||
return meta.guid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update meta and save
|
||||
* 更新元数据并保存
|
||||
*/
|
||||
async updateMeta(assetPath: string, updates: Partial<IAssetMeta>): Promise<void> {
|
||||
const meta = await this.getOrCreateMeta(assetPath);
|
||||
|
||||
// Apply updates
|
||||
Object.assign(meta, updates);
|
||||
meta.lastModified = Date.now();
|
||||
meta.version++;
|
||||
|
||||
// Update cache
|
||||
this._cache.set(assetPath, meta);
|
||||
|
||||
// Handle GUID change (rare, but possible)
|
||||
if (updates.guid && updates.guid !== meta.guid) {
|
||||
this._guidToPath.delete(meta.guid);
|
||||
this._guidToPath.set(updates.guid, assetPath);
|
||||
}
|
||||
|
||||
// Save to file system
|
||||
if (this._fs) {
|
||||
const metaPath = getMetaFilePath(assetPath);
|
||||
await this._fs.writeText(metaPath, serializeAssetMeta(meta));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle asset rename
|
||||
* 处理资产重命名
|
||||
*/
|
||||
async handleAssetRename(oldPath: string, newPath: string): Promise<void> {
|
||||
const meta = this._cache.get(oldPath);
|
||||
if (meta) {
|
||||
// Update cache with new path
|
||||
this._cache.delete(oldPath);
|
||||
this._cache.set(newPath, meta);
|
||||
this._guidToPath.set(meta.guid, newPath);
|
||||
|
||||
// Move meta file
|
||||
if (this._fs) {
|
||||
const oldMetaPath = getMetaFilePath(oldPath);
|
||||
const newMetaPath = getMetaFilePath(newPath);
|
||||
|
||||
if (await this._fs.exists(oldMetaPath)) {
|
||||
const content = await this._fs.readText(oldMetaPath);
|
||||
await this._fs.writeText(newMetaPath, content);
|
||||
await this._fs.delete(oldMetaPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle asset delete
|
||||
* 处理资产删除
|
||||
*/
|
||||
async handleAssetDelete(assetPath: string): Promise<void> {
|
||||
const meta = this._cache.get(assetPath);
|
||||
if (meta) {
|
||||
this._cache.delete(assetPath);
|
||||
this._guidToPath.delete(meta.guid);
|
||||
|
||||
// Delete meta file
|
||||
if (this._fs) {
|
||||
const metaPath = getMetaFilePath(assetPath);
|
||||
if (await this._fs.exists(metaPath)) {
|
||||
await this._fs.delete(metaPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cache
|
||||
* 清除缓存
|
||||
*/
|
||||
clear(): void {
|
||||
this._cache.clear();
|
||||
this._guidToPath.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all cached metas
|
||||
* 获取所有缓存的元数据
|
||||
*/
|
||||
getAllMetas(): Map<string, IAssetMeta> {
|
||||
return new Map(this._cache);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* File system interface for meta file operations
|
||||
* 元数据文件操作的文件系统接口
|
||||
*/
|
||||
export interface IMetaFileSystem {
|
||||
exists(path: string): Promise<boolean>;
|
||||
readText(path: string): Promise<string>;
|
||||
writeText(path: string, content: string): Promise<void>;
|
||||
delete(path: string): Promise<void>;
|
||||
}
|
||||
@@ -1,408 +0,0 @@
|
||||
/**
|
||||
* Asset Packer
|
||||
* 资产打包器
|
||||
*
|
||||
* Collects and packs assets into bundles for runtime loading.
|
||||
* 收集并将资产打包成运行时加载的包。
|
||||
*/
|
||||
|
||||
import {
|
||||
AssetGUID,
|
||||
AssetType,
|
||||
IBundleManifest,
|
||||
IBundleAssetInfo,
|
||||
IRuntimeCatalog,
|
||||
IRuntimeBundleInfo,
|
||||
IRuntimeAssetLocation,
|
||||
IAssetToPack,
|
||||
IBundlePackOptions
|
||||
} from '@esengine/asset-system';
|
||||
import { IAssetMeta } from '../meta/AssetMetaFile';
|
||||
|
||||
/**
|
||||
* Packing result
|
||||
* 打包结果
|
||||
*/
|
||||
export interface IPackingResult {
|
||||
/** Generated bundles | 生成的包 */
|
||||
bundles: IPackedBundle[];
|
||||
/** Runtime catalog | 运行时目录 */
|
||||
catalog: IRuntimeCatalog;
|
||||
/** Total size in bytes | 总大小 */
|
||||
totalSize: number;
|
||||
/** Number of assets packed | 打包的资产数量 */
|
||||
assetCount: number;
|
||||
/** Packing duration in ms | 打包耗时 */
|
||||
duration: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Packed bundle
|
||||
* 已打包的包
|
||||
*/
|
||||
export interface IPackedBundle {
|
||||
/** Bundle name | 包名称 */
|
||||
name: string;
|
||||
/** Bundle data | 包数据 */
|
||||
data: ArrayBuffer;
|
||||
/** Bundle manifest | 包清单 */
|
||||
manifest: IBundleManifest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset file reader interface
|
||||
* 资产文件读取器接口
|
||||
*/
|
||||
export interface IAssetFileReader {
|
||||
readBinary(path: string): Promise<ArrayBuffer>;
|
||||
readText(path: string): Promise<string>;
|
||||
exists(path: string): Promise<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset Packer
|
||||
* 资产打包器
|
||||
*/
|
||||
export class AssetPacker {
|
||||
private _fileReader: IAssetFileReader | null = null;
|
||||
private _assets: IAssetToPack[] = [];
|
||||
private _metas = new Map<AssetGUID, IAssetMeta>();
|
||||
|
||||
/**
|
||||
* Set file reader for loading asset data
|
||||
* 设置用于加载资产数据的文件读取器
|
||||
*/
|
||||
setFileReader(reader: IAssetFileReader): void {
|
||||
this._fileReader = reader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add asset to pack
|
||||
* 添加要打包的资产
|
||||
*/
|
||||
addAsset(asset: IAssetToPack, meta?: IAssetMeta): void {
|
||||
this._assets.push(asset);
|
||||
if (meta) {
|
||||
this._metas.set(asset.guid, meta);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add multiple assets
|
||||
* 添加多个资产
|
||||
*/
|
||||
addAssets(assets: IAssetToPack[]): void {
|
||||
for (const asset of assets) {
|
||||
this.addAsset(asset);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all added assets
|
||||
* 清除所有已添加的资产
|
||||
*/
|
||||
clear(): void {
|
||||
this._assets = [];
|
||||
this._metas.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pack assets into bundles
|
||||
* 将资产打包成包
|
||||
*/
|
||||
async pack(options: IBundlePackOptions = { name: 'main' }): Promise<IPackingResult> {
|
||||
const startTime = Date.now();
|
||||
|
||||
// Group assets for bundling
|
||||
const groups = this._groupAssets(options);
|
||||
|
||||
// Pack each group into a bundle
|
||||
const bundles: IPackedBundle[] = [];
|
||||
const catalogAssets: Record<AssetGUID, IRuntimeAssetLocation> = {};
|
||||
const catalogBundles: Record<string, IRuntimeBundleInfo> = {};
|
||||
|
||||
for (const [bundleName, assets] of groups) {
|
||||
const packed = await this._packBundle(bundleName, assets, options);
|
||||
bundles.push(packed);
|
||||
|
||||
// Add to catalog
|
||||
catalogBundles[bundleName] = {
|
||||
url: `assets/${bundleName}.bundle`,
|
||||
size: packed.data.byteLength,
|
||||
hash: await this._hashBuffer(packed.data),
|
||||
preload: bundleName === 'core' || bundleName === 'main'
|
||||
};
|
||||
|
||||
// Add asset locations
|
||||
for (const assetInfo of packed.manifest.assets) {
|
||||
catalogAssets[assetInfo.guid] = {
|
||||
bundle: bundleName,
|
||||
offset: assetInfo.offset,
|
||||
size: assetInfo.size,
|
||||
type: assetInfo.type,
|
||||
name: assetInfo.name
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Create catalog
|
||||
const catalog: IRuntimeCatalog = {
|
||||
version: '1.0',
|
||||
createdAt: Date.now(),
|
||||
bundles: catalogBundles,
|
||||
assets: catalogAssets
|
||||
};
|
||||
|
||||
const totalSize = bundles.reduce((sum, b) => sum + b.data.byteLength, 0);
|
||||
|
||||
return {
|
||||
bundles,
|
||||
catalog,
|
||||
totalSize,
|
||||
assetCount: this._assets.length,
|
||||
duration: Date.now() - startTime
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Pack assets by type (textures.bundle, audio.bundle, etc.)
|
||||
* 按类型打包资产
|
||||
*/
|
||||
async packByType(): Promise<IPackingResult> {
|
||||
return this.pack({
|
||||
name: 'main',
|
||||
groupByType: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Group assets for bundling
|
||||
* 分组资产以便打包
|
||||
*/
|
||||
private _groupAssets(options: IBundlePackOptions): Map<string, IAssetToPack[]> {
|
||||
const groups = new Map<string, IAssetToPack[]>();
|
||||
|
||||
if (options.groupByType) {
|
||||
// Group by asset type
|
||||
for (const asset of this._assets) {
|
||||
const bundleName = this._getBundleNameForType(asset.type);
|
||||
const group = groups.get(bundleName) || [];
|
||||
group.push(asset);
|
||||
groups.set(bundleName, group);
|
||||
}
|
||||
} else {
|
||||
// Single bundle
|
||||
groups.set(options.name, [...this._assets]);
|
||||
}
|
||||
|
||||
// Handle max size splitting
|
||||
if (options.maxSize) {
|
||||
const splitGroups = new Map<string, IAssetToPack[]>();
|
||||
|
||||
for (const [name, assets] of groups) {
|
||||
let currentSize = 0;
|
||||
let partIndex = 0;
|
||||
let currentGroup: IAssetToPack[] = [];
|
||||
|
||||
for (const asset of assets) {
|
||||
const assetSize = asset.data?.byteLength || 0;
|
||||
|
||||
if (currentSize + assetSize > options.maxSize && currentGroup.length > 0) {
|
||||
splitGroups.set(`${name}_${partIndex}`, currentGroup);
|
||||
partIndex++;
|
||||
currentGroup = [];
|
||||
currentSize = 0;
|
||||
}
|
||||
|
||||
currentGroup.push(asset);
|
||||
currentSize += assetSize;
|
||||
}
|
||||
|
||||
if (currentGroup.length > 0) {
|
||||
const finalName = partIndex > 0 ? `${name}_${partIndex}` : name;
|
||||
splitGroups.set(finalName, currentGroup);
|
||||
}
|
||||
}
|
||||
|
||||
return splitGroups;
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bundle name for asset type
|
||||
* 获取资产类型的包名称
|
||||
*/
|
||||
private _getBundleNameForType(type: AssetType): string {
|
||||
const typeGroups: Record<string, string[]> = {
|
||||
textures: ['texture'],
|
||||
audio: ['audio'],
|
||||
data: ['json', 'text', 'binary', 'scene', 'prefab'],
|
||||
fonts: ['font'],
|
||||
shaders: ['shader', 'material'],
|
||||
tilemaps: ['tilemap', 'tileset'],
|
||||
scripts: ['behavior-tree', 'blueprint']
|
||||
};
|
||||
|
||||
for (const [bundleName, types] of Object.entries(typeGroups)) {
|
||||
if (types.includes(type)) {
|
||||
return bundleName;
|
||||
}
|
||||
}
|
||||
|
||||
return 'misc';
|
||||
}
|
||||
|
||||
/**
|
||||
* Pack a single bundle
|
||||
* 打包单个包
|
||||
*/
|
||||
private async _packBundle(
|
||||
name: string,
|
||||
assets: IAssetToPack[],
|
||||
_options: IBundlePackOptions
|
||||
): Promise<IPackedBundle> {
|
||||
const assetInfos: IBundleAssetInfo[] = [];
|
||||
const dataChunks: ArrayBuffer[] = [];
|
||||
let currentOffset = 0;
|
||||
|
||||
// Load and pack each asset
|
||||
for (const asset of assets) {
|
||||
let data = asset.data;
|
||||
|
||||
// Load data if not provided
|
||||
if (!data && this._fileReader) {
|
||||
try {
|
||||
data = await this._fileReader.readBinary(asset.path);
|
||||
} catch (e) {
|
||||
console.warn(`[AssetPacker] Failed to load asset: ${asset.path}`, e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
console.warn(`[AssetPacker] No data for asset: ${asset.guid}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Align to 4 bytes
|
||||
const padding = (4 - (data.byteLength % 4)) % 4;
|
||||
const paddedSize = data.byteLength + padding;
|
||||
|
||||
assetInfos.push({
|
||||
guid: asset.guid,
|
||||
name: asset.name,
|
||||
type: asset.type,
|
||||
offset: currentOffset,
|
||||
size: data.byteLength
|
||||
});
|
||||
|
||||
// Add data with padding
|
||||
dataChunks.push(data);
|
||||
if (padding > 0) {
|
||||
dataChunks.push(new ArrayBuffer(padding));
|
||||
}
|
||||
|
||||
currentOffset += paddedSize;
|
||||
}
|
||||
|
||||
// Combine all data
|
||||
const totalSize = dataChunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
|
||||
const bundleData = new Uint8Array(totalSize);
|
||||
let offset = 0;
|
||||
|
||||
for (const chunk of dataChunks) {
|
||||
bundleData.set(new Uint8Array(chunk), offset);
|
||||
offset += chunk.byteLength;
|
||||
}
|
||||
|
||||
// Create manifest
|
||||
const manifest: IBundleManifest = {
|
||||
name,
|
||||
version: '1.0',
|
||||
hash: await this._hashBuffer(bundleData.buffer),
|
||||
compression: 'none',
|
||||
size: bundleData.byteLength,
|
||||
assets: assetInfos,
|
||||
dependencies: [],
|
||||
createdAt: Date.now()
|
||||
};
|
||||
|
||||
return {
|
||||
name,
|
||||
data: bundleData.buffer,
|
||||
manifest
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash a buffer using SHA-256
|
||||
* 使用 SHA-256 哈希缓冲区
|
||||
*/
|
||||
private async _hashBuffer(buffer: ArrayBuffer): Promise<string> {
|
||||
// Use Web Crypto API if available
|
||||
if (typeof crypto !== 'undefined' && crypto.subtle) {
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('').slice(0, 16);
|
||||
}
|
||||
|
||||
// Fallback: simple hash
|
||||
const view = new Uint8Array(buffer);
|
||||
let hash = 0;
|
||||
for (let i = 0; i < view.length; i++) {
|
||||
hash = ((hash << 5) - hash) + view[i];
|
||||
hash = hash & hash;
|
||||
}
|
||||
return Math.abs(hash).toString(16).padStart(16, '0');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect assets referenced by a scene
|
||||
* 收集场景引用的资产
|
||||
*/
|
||||
export async function collectSceneAssets(
|
||||
sceneData: unknown,
|
||||
_metaManager: { getPathByGUID: (guid: AssetGUID) => string | undefined }
|
||||
): Promise<AssetGUID[]> {
|
||||
const guids = new Set<AssetGUID>();
|
||||
|
||||
// Recursively find all GUID references
|
||||
function findGUIDs(obj: unknown): void {
|
||||
if (!obj || typeof obj !== 'object') return;
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
for (const item of obj) {
|
||||
findGUIDs(item);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const record = obj as Record<string, unknown>;
|
||||
|
||||
// Check for GUID fields
|
||||
for (const [key, value] of Object.entries(record)) {
|
||||
if (key.endsWith('Guid') || key.endsWith('GUID') || key === 'guid') {
|
||||
if (typeof value === 'string' && isValidGUID(value)) {
|
||||
guids.add(value);
|
||||
}
|
||||
} else if (typeof value === 'object') {
|
||||
findGUIDs(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
findGUIDs(sceneData);
|
||||
return Array.from(guids);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate GUID format
|
||||
* 验证 GUID 格式
|
||||
*/
|
||||
function isValidGUID(guid: string): boolean {
|
||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
return uuidRegex.test(guid);
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": false,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"sourceMap": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictBindCallApply": true,
|
||||
"strictPropertyInitialization": true,
|
||||
"noImplicitThis": true,
|
||||
"alwaysStrict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/index.ts'],
|
||||
format: ['esm'],
|
||||
dts: true,
|
||||
clean: true,
|
||||
sourcemap: true,
|
||||
external: ['@esengine/asset-system']
|
||||
});
|
||||
@@ -1,41 +0,0 @@
|
||||
{
|
||||
"id": "asset-system",
|
||||
"name": "@esengine/asset-system",
|
||||
"displayName": "Asset System",
|
||||
"description": "Asset loading, caching and management | 资源加载、缓存和管理",
|
||||
"version": "1.0.0",
|
||||
"category": "Core",
|
||||
"icon": "FolderOpen",
|
||||
"tags": [
|
||||
"asset",
|
||||
"resource",
|
||||
"loader"
|
||||
],
|
||||
"isCore": true,
|
||||
"defaultEnabled": true,
|
||||
"isEngineModule": true,
|
||||
"canContainContent": false,
|
||||
"platforms": [
|
||||
"web",
|
||||
"desktop",
|
||||
"mobile"
|
||||
],
|
||||
"dependencies": [
|
||||
"core"
|
||||
],
|
||||
"exports": {
|
||||
"loaders": [
|
||||
"TextureLoader",
|
||||
"JsonLoader",
|
||||
"TextLoader",
|
||||
"BinaryLoader"
|
||||
],
|
||||
"other": [
|
||||
"AssetManager",
|
||||
"AssetDatabase",
|
||||
"AssetCache"
|
||||
]
|
||||
},
|
||||
"requiresWasm": false,
|
||||
"outputPath": "dist/index.js"
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
{
|
||||
"name": "@esengine/asset-system",
|
||||
"version": "1.0.0",
|
||||
"description": "Asset management system for ES Engine",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"build:watch": "tsup --watch",
|
||||
"clean": "rimraf dist",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"keywords": [
|
||||
"ecs",
|
||||
"asset",
|
||||
"resource",
|
||||
"bundle"
|
||||
],
|
||||
"author": "yhh",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@esengine/ecs-framework": "workspace:*",
|
||||
"@esengine/build-config": "workspace:*",
|
||||
"rimraf": "^5.0.0",
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/esengine/ecs-framework.git",
|
||||
"directory": "packages/asset-system"
|
||||
}
|
||||
}
|
||||
@@ -1,272 +0,0 @@
|
||||
/**
|
||||
* Asset Bundle Format Definitions
|
||||
* 资产包格式定义
|
||||
*
|
||||
* Binary format for efficient asset storage and loading.
|
||||
* 用于高效资产存储和加载的二进制格式。
|
||||
*/
|
||||
|
||||
import { AssetGUID, AssetType } from '../types/AssetTypes';
|
||||
|
||||
/**
|
||||
* Bundle file magic number
|
||||
* 包文件魔数
|
||||
*/
|
||||
export const BUNDLE_MAGIC = 'ESBNDL';
|
||||
|
||||
/**
|
||||
* Bundle format version
|
||||
* 包格式版本
|
||||
*/
|
||||
export const BUNDLE_VERSION = 1;
|
||||
|
||||
/**
|
||||
* Bundle compression types
|
||||
* 包压缩类型
|
||||
*/
|
||||
export enum BundleCompression {
|
||||
None = 0,
|
||||
Gzip = 1,
|
||||
Brotli = 2
|
||||
}
|
||||
|
||||
/**
|
||||
* Bundle flags
|
||||
* 包标志
|
||||
*/
|
||||
export enum BundleFlags {
|
||||
None = 0,
|
||||
Compressed = 1 << 0,
|
||||
Encrypted = 1 << 1,
|
||||
Streaming = 1 << 2
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset type codes for binary serialization
|
||||
* 用于二进制序列化的资产类型代码
|
||||
*/
|
||||
export const AssetTypeCode: Record<string, number> = {
|
||||
texture: 1,
|
||||
audio: 2,
|
||||
json: 3,
|
||||
text: 4,
|
||||
binary: 5,
|
||||
scene: 6,
|
||||
prefab: 7,
|
||||
font: 8,
|
||||
shader: 9,
|
||||
material: 10,
|
||||
mesh: 11,
|
||||
animation: 12,
|
||||
tilemap: 20,
|
||||
tileset: 21,
|
||||
'behavior-tree': 22,
|
||||
blueprint: 23
|
||||
};
|
||||
|
||||
/**
|
||||
* Bundle header structure (32 bytes)
|
||||
* 包头结构 (32 字节)
|
||||
*/
|
||||
export interface IBundleHeader {
|
||||
/** Magic number "ESBNDL" | 魔数 */
|
||||
magic: string;
|
||||
/** Format version | 格式版本 */
|
||||
version: number;
|
||||
/** Bundle flags | 包标志 */
|
||||
flags: BundleFlags;
|
||||
/** Compression type | 压缩类型 */
|
||||
compression: BundleCompression;
|
||||
/** Number of assets | 资产数量 */
|
||||
assetCount: number;
|
||||
/** TOC offset from start | TOC 偏移量 */
|
||||
tocOffset: number;
|
||||
/** Data offset from start | 数据偏移量 */
|
||||
dataOffset: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Table of Contents entry (40 bytes per entry)
|
||||
* 目录条目 (每条 40 字节)
|
||||
*/
|
||||
export interface IBundleTocEntry {
|
||||
/** Asset GUID (16 bytes as UUID binary) | 资产 GUID */
|
||||
guid: AssetGUID;
|
||||
/** Asset type code | 资产类型代码 */
|
||||
typeCode: number;
|
||||
/** Offset from data section start | 相对于数据段起始的偏移 */
|
||||
offset: number;
|
||||
/** Compressed size in bytes | 压缩后大小 */
|
||||
compressedSize: number;
|
||||
/** Uncompressed size in bytes | 未压缩大小 */
|
||||
uncompressedSize: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bundle manifest (JSON sidecar file)
|
||||
* 包清单 (JSON 附属文件)
|
||||
*/
|
||||
export interface IBundleManifest {
|
||||
/** Bundle name | 包名称 */
|
||||
name: string;
|
||||
/** Bundle version | 包版本 */
|
||||
version: string;
|
||||
/** Content hash for integrity | 内容哈希 */
|
||||
hash: string;
|
||||
/** Compression type | 压缩类型 */
|
||||
compression: 'none' | 'gzip' | 'brotli';
|
||||
/** Total bundle size | 包总大小 */
|
||||
size: number;
|
||||
/** Assets in this bundle | 包含的资产 */
|
||||
assets: IBundleAssetInfo[];
|
||||
/** Dependencies on other bundles | 依赖的其他包 */
|
||||
dependencies: string[];
|
||||
/** Creation timestamp | 创建时间戳 */
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset info in bundle manifest
|
||||
* 包清单中的资产信息
|
||||
*/
|
||||
export interface IBundleAssetInfo {
|
||||
/** Asset GUID | 资产 GUID */
|
||||
guid: AssetGUID;
|
||||
/** Asset name (for debugging) | 资产名称 (用于调试) */
|
||||
name: string;
|
||||
/** Asset type | 资产类型 */
|
||||
type: AssetType;
|
||||
/** Offset in bundle | 包内偏移 */
|
||||
offset: number;
|
||||
/** Size in bytes | 大小 */
|
||||
size: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runtime catalog format (loaded in browser)
|
||||
* 运行时目录格式 (在浏览器中加载)
|
||||
*/
|
||||
export interface IRuntimeCatalog {
|
||||
/** Catalog version | 目录版本 */
|
||||
version: string;
|
||||
/** Creation timestamp | 创建时间戳 */
|
||||
createdAt: number;
|
||||
/** Available bundles | 可用的包 */
|
||||
bundles: Record<string, IRuntimeBundleInfo>;
|
||||
/** Asset GUID to location mapping | 资产 GUID 到位置的映射 */
|
||||
assets: Record<AssetGUID, IRuntimeAssetLocation>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bundle info in runtime catalog
|
||||
* 运行时目录中的包信息
|
||||
*/
|
||||
export interface IRuntimeBundleInfo {
|
||||
/** Bundle URL (relative to catalog) | 包 URL */
|
||||
url: string;
|
||||
/** Bundle size in bytes | 包大小 */
|
||||
size: number;
|
||||
/** Content hash | 内容哈希 */
|
||||
hash: string;
|
||||
/** Whether bundle is preloaded | 是否预加载 */
|
||||
preload?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset location in runtime catalog
|
||||
* 运行时目录中的资产位置
|
||||
*/
|
||||
export interface IRuntimeAssetLocation {
|
||||
/** Bundle name containing this asset | 包含此资产的包名 */
|
||||
bundle: string;
|
||||
/** Offset within bundle | 包内偏移 */
|
||||
offset: number;
|
||||
/** Size in bytes | 大小 */
|
||||
size: number;
|
||||
/** Asset type | 资产类型 */
|
||||
type: AssetType;
|
||||
/** Asset name (for debugging) | 资产名称 */
|
||||
name?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bundle packing options
|
||||
* 包打包选项
|
||||
*/
|
||||
export interface IBundlePackOptions {
|
||||
/** Bundle name | 包名称 */
|
||||
name: string;
|
||||
/** Compression type | 压缩类型 */
|
||||
compression?: BundleCompression;
|
||||
/** Maximum bundle size (split if exceeded) | 最大包大小 */
|
||||
maxSize?: number;
|
||||
/** Group assets by type | 按类型分组资产 */
|
||||
groupByType?: boolean;
|
||||
/** Include asset names in bundle | 在包中包含资产名称 */
|
||||
includeNames?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset to pack
|
||||
* 要打包的资产
|
||||
*/
|
||||
export interface IAssetToPack {
|
||||
/** Asset GUID | 资产 GUID */
|
||||
guid: AssetGUID;
|
||||
/** Asset path (for reading) | 资产路径 */
|
||||
path: string;
|
||||
/** Asset type | 资产类型 */
|
||||
type: AssetType;
|
||||
/** Asset name | 资产名称 */
|
||||
name: string;
|
||||
/** Raw data (or null to read from path) | 原始数据 */
|
||||
data?: ArrayBuffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse GUID from 16-byte binary
|
||||
* 从 16 字节二进制解析 GUID
|
||||
*/
|
||||
export function parseGUIDFromBinary(bytes: Uint8Array): AssetGUID {
|
||||
const hex = Array.from(bytes)
|
||||
.map(b => b.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
|
||||
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize GUID to 16-byte binary
|
||||
* 将 GUID 序列化为 16 字节二进制
|
||||
*/
|
||||
export function serializeGUIDToBinary(guid: AssetGUID): Uint8Array {
|
||||
const hex = guid.replace(/-/g, '');
|
||||
const bytes = new Uint8Array(16);
|
||||
|
||||
for (let i = 0; i < 16; i++) {
|
||||
bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get type code from asset type string
|
||||
* 从资产类型字符串获取类型代码
|
||||
*/
|
||||
export function getAssetTypeCode(type: AssetType): number {
|
||||
return AssetTypeCode[type] || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get asset type string from type code
|
||||
* 从类型代码获取资产类型字符串
|
||||
*/
|
||||
export function getAssetTypeFromCode(code: number): AssetType {
|
||||
for (const [type, typeCode] of Object.entries(AssetTypeCode)) {
|
||||
if (typeCode === code) {
|
||||
return type as AssetType;
|
||||
}
|
||||
}
|
||||
return 'binary';
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
/**
|
||||
* Asset cache implementation
|
||||
* 资产缓存实现
|
||||
*/
|
||||
|
||||
import { AssetGUID } from '../types/AssetTypes';
|
||||
|
||||
/**
|
||||
* Cache entry
|
||||
* 缓存条目
|
||||
*/
|
||||
interface CacheEntry {
|
||||
guid: AssetGUID;
|
||||
asset: unknown;
|
||||
lastAccessTime: number;
|
||||
accessCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset cache implementation
|
||||
* 资产缓存实现
|
||||
*/
|
||||
export class AssetCache {
|
||||
private readonly _cache = new Map<AssetGUID, CacheEntry>();
|
||||
|
||||
constructor() {
|
||||
// 无配置,无限制缓存 / No config, unlimited cache
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached asset
|
||||
* 获取缓存的资产
|
||||
*/
|
||||
get<T = unknown>(guid: AssetGUID): T | null {
|
||||
const entry = this._cache.get(guid);
|
||||
if (!entry) return null;
|
||||
|
||||
// 更新访问信息 / Update access info
|
||||
entry.lastAccessTime = Date.now();
|
||||
entry.accessCount++;
|
||||
|
||||
return entry.asset as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cached asset
|
||||
* 设置缓存的资产
|
||||
*/
|
||||
set<T = unknown>(guid: AssetGUID, asset: T): void {
|
||||
const now = Date.now();
|
||||
const entry: CacheEntry = {
|
||||
guid,
|
||||
asset,
|
||||
lastAccessTime: now,
|
||||
accessCount: 1
|
||||
};
|
||||
|
||||
// 如果已存在,更新 / Update if exists
|
||||
const oldEntry = this._cache.get(guid);
|
||||
if (oldEntry) {
|
||||
entry.accessCount = oldEntry.accessCount + 1;
|
||||
}
|
||||
|
||||
this._cache.set(guid, entry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if asset is cached
|
||||
* 检查资产是否缓存
|
||||
*/
|
||||
has(guid: AssetGUID): boolean {
|
||||
return this._cache.has(guid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove asset from cache
|
||||
* 从缓存移除资产
|
||||
*/
|
||||
remove(guid: AssetGUID): void {
|
||||
this._cache.delete(guid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cache
|
||||
* 清空所有缓存
|
||||
*/
|
||||
clear(): void {
|
||||
this._cache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache size
|
||||
* 获取缓存大小
|
||||
*/
|
||||
getSize(): number {
|
||||
return this._cache.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all cached GUIDs
|
||||
* 获取所有缓存的GUID
|
||||
*/
|
||||
getAllGuids(): AssetGUID[] {
|
||||
return Array.from(this._cache.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache statistics
|
||||
* 获取缓存统计
|
||||
*/
|
||||
getStatistics(): {
|
||||
count: number;
|
||||
entries: Array<{
|
||||
guid: AssetGUID;
|
||||
accessCount: number;
|
||||
lastAccessTime: number;
|
||||
}>;
|
||||
} {
|
||||
const entries = Array.from(this._cache.values()).map((entry) => ({
|
||||
guid: entry.guid,
|
||||
accessCount: entry.accessCount,
|
||||
lastAccessTime: entry.lastAccessTime
|
||||
}));
|
||||
|
||||
return {
|
||||
count: this._cache.size,
|
||||
entries
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,501 +0,0 @@
|
||||
/**
|
||||
* Asset database for managing asset metadata
|
||||
* 用于管理资产元数据的资产数据库
|
||||
*/
|
||||
|
||||
import {
|
||||
AssetGUID,
|
||||
AssetType,
|
||||
IAssetMetadata,
|
||||
IAssetCatalogEntry
|
||||
} from '../types/AssetTypes';
|
||||
|
||||
/**
|
||||
* Asset database implementation
|
||||
* 资产数据库实现
|
||||
*/
|
||||
export class AssetDatabase {
|
||||
private readonly _metadata = new Map<AssetGUID, IAssetMetadata>();
|
||||
private readonly _pathToGuid = new Map<string, AssetGUID>();
|
||||
private readonly _typeToGuids = new Map<AssetType, Set<AssetGUID>>();
|
||||
private readonly _labelToGuids = new Map<string, Set<AssetGUID>>();
|
||||
private readonly _dependencies = new Map<AssetGUID, Set<AssetGUID>>();
|
||||
private readonly _dependents = new Map<AssetGUID, Set<AssetGUID>>();
|
||||
|
||||
/** Project root path for resolving relative paths. | 项目根路径,用于解析相对路径。 */
|
||||
private _projectRoot: string | null = null;
|
||||
|
||||
/**
|
||||
* Set project root path.
|
||||
* 设置项目根路径。
|
||||
*
|
||||
* @param path - Absolute path to project root. | 项目根目录的绝对路径。
|
||||
*/
|
||||
setProjectRoot(path: string): void {
|
||||
this._projectRoot = path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get project root path.
|
||||
* 获取项目根路径。
|
||||
*/
|
||||
getProjectRoot(): string | null {
|
||||
return this._projectRoot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve relative path to absolute path.
|
||||
* 将相对路径解析为绝对路径。
|
||||
*
|
||||
* @param relativePath - Relative asset path (e.g., "assets/texture.png"). | 相对资产路径。
|
||||
* @returns Absolute file system path. | 绝对文件系统路径。
|
||||
*/
|
||||
resolveAbsolutePath(relativePath: string): string {
|
||||
// Already absolute path (Windows or Unix).
|
||||
// 已经是绝对路径。
|
||||
if (relativePath.match(/^[a-zA-Z]:/) || relativePath.startsWith('/')) {
|
||||
return relativePath;
|
||||
}
|
||||
|
||||
// No project root set, return as-is.
|
||||
// 未设置项目根路径,原样返回。
|
||||
if (!this._projectRoot) {
|
||||
return relativePath;
|
||||
}
|
||||
|
||||
// Join with project root.
|
||||
// 与项目根路径拼接。
|
||||
const separator = this._projectRoot.includes('\\') ? '\\' : '/';
|
||||
const normalizedPath = relativePath.replace(/[/\\]/g, separator);
|
||||
return `${this._projectRoot}${separator}${normalizedPath}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert absolute path to relative path.
|
||||
* 将绝对路径转换为相对路径。
|
||||
*
|
||||
* @param absolutePath - Absolute file system path. | 绝对文件系统路径。
|
||||
* @returns Relative asset path, or null if not under project root. | 相对资产路径。
|
||||
*/
|
||||
toRelativePath(absolutePath: string): string | null {
|
||||
if (!this._projectRoot) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalizedAbs = absolutePath.replace(/\\/g, '/');
|
||||
const normalizedRoot = this._projectRoot.replace(/\\/g, '/');
|
||||
|
||||
if (normalizedAbs.startsWith(normalizedRoot)) {
|
||||
return normalizedAbs.substring(normalizedRoot.length + 1);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add asset to database
|
||||
* 添加资产到数据库
|
||||
*/
|
||||
addAsset(metadata: IAssetMetadata): void {
|
||||
const { guid, path, type, labels, dependencies } = metadata;
|
||||
|
||||
// 存储元数据 / Store metadata
|
||||
this._metadata.set(guid, metadata);
|
||||
this._pathToGuid.set(path, guid);
|
||||
|
||||
// 按类型索引 / Index by type
|
||||
if (!this._typeToGuids.has(type)) {
|
||||
this._typeToGuids.set(type, new Set());
|
||||
}
|
||||
this._typeToGuids.get(type)!.add(guid);
|
||||
|
||||
// 按标签索引 / Index by labels
|
||||
labels.forEach((label) => {
|
||||
if (!this._labelToGuids.has(label)) {
|
||||
this._labelToGuids.set(label, new Set());
|
||||
}
|
||||
this._labelToGuids.get(label)!.add(guid);
|
||||
});
|
||||
|
||||
// 建立依赖关系 / Establish dependencies
|
||||
this.updateDependencies(guid, dependencies);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove asset from database
|
||||
* 从数据库移除资产
|
||||
*/
|
||||
removeAsset(guid: AssetGUID): void {
|
||||
const metadata = this._metadata.get(guid);
|
||||
if (!metadata) return;
|
||||
|
||||
// 清理元数据 / Clean up metadata
|
||||
this._metadata.delete(guid);
|
||||
this._pathToGuid.delete(metadata.path);
|
||||
|
||||
// 清理类型索引 / Clean up type index
|
||||
const typeSet = this._typeToGuids.get(metadata.type);
|
||||
if (typeSet) {
|
||||
typeSet.delete(guid);
|
||||
if (typeSet.size === 0) {
|
||||
this._typeToGuids.delete(metadata.type);
|
||||
}
|
||||
}
|
||||
|
||||
// 清理标签索引 / Clean up label indices
|
||||
metadata.labels.forEach((label) => {
|
||||
const labelSet = this._labelToGuids.get(label);
|
||||
if (labelSet) {
|
||||
labelSet.delete(guid);
|
||||
if (labelSet.size === 0) {
|
||||
this._labelToGuids.delete(label);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 清理依赖关系 / Clean up dependencies
|
||||
this.clearDependencies(guid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update asset metadata
|
||||
* 更新资产元数据
|
||||
*/
|
||||
updateAsset(guid: AssetGUID, updates: Partial<IAssetMetadata>): void {
|
||||
const metadata = this._metadata.get(guid);
|
||||
if (!metadata) return;
|
||||
|
||||
// 如果路径改变,更新索引 / Update index if path changed
|
||||
if (updates.path && updates.path !== metadata.path) {
|
||||
this._pathToGuid.delete(metadata.path);
|
||||
this._pathToGuid.set(updates.path, guid);
|
||||
}
|
||||
|
||||
// 如果类型改变,更新索引 / Update index if type changed
|
||||
if (updates.type && updates.type !== metadata.type) {
|
||||
const oldTypeSet = this._typeToGuids.get(metadata.type);
|
||||
if (oldTypeSet) {
|
||||
oldTypeSet.delete(guid);
|
||||
}
|
||||
|
||||
if (!this._typeToGuids.has(updates.type)) {
|
||||
this._typeToGuids.set(updates.type, new Set());
|
||||
}
|
||||
this._typeToGuids.get(updates.type)!.add(guid);
|
||||
}
|
||||
|
||||
// 如果依赖改变,更新关系 / Update relations if dependencies changed
|
||||
if (updates.dependencies) {
|
||||
this.updateDependencies(guid, updates.dependencies);
|
||||
}
|
||||
|
||||
// 合并更新 / Merge updates
|
||||
Object.assign(metadata, updates);
|
||||
metadata.lastModified = Date.now();
|
||||
metadata.version++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get asset metadata
|
||||
* 获取资产元数据
|
||||
*/
|
||||
getMetadata(guid: AssetGUID): IAssetMetadata | undefined {
|
||||
return this._metadata.get(guid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get metadata by path
|
||||
* 通过路径获取元数据
|
||||
*/
|
||||
getMetadataByPath(path: string): IAssetMetadata | undefined {
|
||||
const guid = this._pathToGuid.get(path);
|
||||
return guid ? this._metadata.get(guid) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find assets by type
|
||||
* 按类型查找资产
|
||||
*/
|
||||
findAssetsByType(type: AssetType): AssetGUID[] {
|
||||
const guids = this._typeToGuids.get(type);
|
||||
return guids ? Array.from(guids) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find assets by label
|
||||
* 按标签查找资产
|
||||
*/
|
||||
findAssetsByLabel(label: string): AssetGUID[] {
|
||||
const guids = this._labelToGuids.get(label);
|
||||
return guids ? Array.from(guids) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find assets by multiple labels (AND operation)
|
||||
* 按多个标签查找资产(AND操作)
|
||||
*/
|
||||
findAssetsByLabels(labels: string[]): AssetGUID[] {
|
||||
if (labels.length === 0) return [];
|
||||
|
||||
let result: Set<AssetGUID> | null = null;
|
||||
|
||||
for (const label of labels) {
|
||||
const labelGuids = this._labelToGuids.get(label);
|
||||
if (!labelGuids || labelGuids.size === 0) return [];
|
||||
|
||||
if (!result) {
|
||||
result = new Set(labelGuids);
|
||||
} else {
|
||||
// 交集 / Intersection
|
||||
const intersection = new Set<AssetGUID>();
|
||||
labelGuids.forEach((guid) => {
|
||||
if (result!.has(guid)) {
|
||||
intersection.add(guid);
|
||||
}
|
||||
});
|
||||
result = intersection;
|
||||
}
|
||||
}
|
||||
|
||||
return result ? Array.from(result) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Search assets by query
|
||||
* 通过查询搜索资产
|
||||
*/
|
||||
searchAssets(query: {
|
||||
name?: string;
|
||||
type?: AssetType;
|
||||
labels?: string[];
|
||||
path?: string;
|
||||
}): AssetGUID[] {
|
||||
let results = Array.from(this._metadata.keys());
|
||||
|
||||
// 按名称过滤 / Filter by name
|
||||
if (query.name) {
|
||||
const nameLower = query.name.toLowerCase();
|
||||
results = results.filter((guid) => {
|
||||
const metadata = this._metadata.get(guid)!;
|
||||
return metadata.name.toLowerCase().includes(nameLower);
|
||||
});
|
||||
}
|
||||
|
||||
// 按类型过滤 / Filter by type
|
||||
if (query.type) {
|
||||
const typeGuids = this._typeToGuids.get(query.type);
|
||||
if (!typeGuids) return [];
|
||||
results = results.filter((guid) => typeGuids.has(guid));
|
||||
}
|
||||
|
||||
// 按标签过滤 / Filter by labels
|
||||
if (query.labels && query.labels.length > 0) {
|
||||
const labelResults = this.findAssetsByLabels(query.labels);
|
||||
const labelSet = new Set(labelResults);
|
||||
results = results.filter((guid) => labelSet.has(guid));
|
||||
}
|
||||
|
||||
// 按路径过滤 / Filter by path
|
||||
if (query.path) {
|
||||
const pathLower = query.path.toLowerCase();
|
||||
results = results.filter((guid) => {
|
||||
const metadata = this._metadata.get(guid)!;
|
||||
return metadata.path.toLowerCase().includes(pathLower);
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get asset dependencies
|
||||
* 获取资产依赖
|
||||
*/
|
||||
getDependencies(guid: AssetGUID): AssetGUID[] {
|
||||
const deps = this._dependencies.get(guid);
|
||||
return deps ? Array.from(deps) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get asset dependents (assets that depend on this one)
|
||||
* 获取资产的依赖者(依赖此资产的其他资产)
|
||||
*/
|
||||
getDependents(guid: AssetGUID): AssetGUID[] {
|
||||
const deps = this._dependents.get(guid);
|
||||
return deps ? Array.from(deps) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all dependencies recursively
|
||||
* 递归获取所有依赖
|
||||
*/
|
||||
getAllDependencies(guid: AssetGUID, visited = new Set<AssetGUID>()): AssetGUID[] {
|
||||
if (visited.has(guid)) return [];
|
||||
visited.add(guid);
|
||||
|
||||
const result: AssetGUID[] = [];
|
||||
const directDeps = this.getDependencies(guid);
|
||||
|
||||
for (const dep of directDeps) {
|
||||
result.push(dep);
|
||||
const transitiveDeps = this.getAllDependencies(dep, visited);
|
||||
result.push(...transitiveDeps);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for circular dependencies
|
||||
* 检查循环依赖
|
||||
*/
|
||||
hasCircularDependency(guid: AssetGUID): boolean {
|
||||
const visited = new Set<AssetGUID>();
|
||||
const recursionStack = new Set<AssetGUID>();
|
||||
|
||||
const checkCycle = (current: AssetGUID): boolean => {
|
||||
visited.add(current);
|
||||
recursionStack.add(current);
|
||||
|
||||
const deps = this.getDependencies(current);
|
||||
for (const dep of deps) {
|
||||
if (!visited.has(dep)) {
|
||||
if (checkCycle(dep)) return true;
|
||||
} else if (recursionStack.has(dep)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
recursionStack.delete(current);
|
||||
return false;
|
||||
};
|
||||
|
||||
return checkCycle(guid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update dependencies
|
||||
* 更新依赖关系
|
||||
*/
|
||||
private updateDependencies(guid: AssetGUID, newDependencies: AssetGUID[]): void {
|
||||
// 清除旧的依赖关系 / Clear old dependencies
|
||||
this.clearDependencies(guid);
|
||||
|
||||
// 建立新的依赖关系 / Establish new dependencies
|
||||
if (newDependencies.length > 0) {
|
||||
this._dependencies.set(guid, new Set(newDependencies));
|
||||
|
||||
// 更新被依赖关系 / Update dependent relations
|
||||
newDependencies.forEach((dep) => {
|
||||
if (!this._dependents.has(dep)) {
|
||||
this._dependents.set(dep, new Set());
|
||||
}
|
||||
this._dependents.get(dep)!.add(guid);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear dependencies
|
||||
* 清除依赖关系
|
||||
*/
|
||||
private clearDependencies(guid: AssetGUID): void {
|
||||
// 清除依赖 / Clear dependencies
|
||||
const deps = this._dependencies.get(guid);
|
||||
if (deps) {
|
||||
deps.forEach((dep) => {
|
||||
const dependents = this._dependents.get(dep);
|
||||
if (dependents) {
|
||||
dependents.delete(guid);
|
||||
if (dependents.size === 0) {
|
||||
this._dependents.delete(dep);
|
||||
}
|
||||
}
|
||||
});
|
||||
this._dependencies.delete(guid);
|
||||
}
|
||||
|
||||
// 清除被依赖 / Clear dependents
|
||||
const dependents = this._dependents.get(guid);
|
||||
if (dependents) {
|
||||
dependents.forEach((dependent) => {
|
||||
const dependencies = this._dependencies.get(dependent);
|
||||
if (dependencies) {
|
||||
dependencies.delete(guid);
|
||||
if (dependencies.size === 0) {
|
||||
this._dependencies.delete(dependent);
|
||||
}
|
||||
}
|
||||
});
|
||||
this._dependents.delete(guid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get database statistics
|
||||
* 获取数据库统计
|
||||
*/
|
||||
getStatistics(): {
|
||||
totalAssets: number;
|
||||
assetsByType: Map<AssetType, number>;
|
||||
totalDependencies: number;
|
||||
assetsWithDependencies: number;
|
||||
circularDependencies: number;
|
||||
} {
|
||||
const assetsByType = new Map<AssetType, number>();
|
||||
this._typeToGuids.forEach((guids, type) => {
|
||||
assetsByType.set(type, guids.size);
|
||||
});
|
||||
|
||||
let circularDependencies = 0;
|
||||
this._metadata.forEach((_, guid) => {
|
||||
if (this.hasCircularDependency(guid)) {
|
||||
circularDependencies++;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
totalAssets: this._metadata.size,
|
||||
assetsByType,
|
||||
totalDependencies: Array.from(this._dependencies.values()).reduce(
|
||||
(sum, deps) => sum + deps.size,
|
||||
0
|
||||
),
|
||||
assetsWithDependencies: this._dependencies.size,
|
||||
circularDependencies
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Export to catalog entries
|
||||
* 导出为目录条目
|
||||
*/
|
||||
exportToCatalog(): IAssetCatalogEntry[] {
|
||||
const entries: IAssetCatalogEntry[] = [];
|
||||
|
||||
this._metadata.forEach((metadata) => {
|
||||
entries.push({
|
||||
guid: metadata.guid,
|
||||
path: metadata.path,
|
||||
type: metadata.type,
|
||||
size: metadata.size,
|
||||
hash: metadata.hash
|
||||
});
|
||||
});
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear database
|
||||
* 清空数据库
|
||||
*/
|
||||
clear(): void {
|
||||
this._metadata.clear();
|
||||
this._pathToGuid.clear();
|
||||
this._typeToGuids.clear();
|
||||
this._labelToGuids.clear();
|
||||
this._dependencies.clear();
|
||||
this._dependents.clear();
|
||||
}
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
/**
|
||||
* Priority-based asset loading queue
|
||||
* 基于优先级的资产加载队列
|
||||
*/
|
||||
|
||||
import { AssetGUID, IAssetLoadOptions } from '../types/AssetTypes';
|
||||
import { IAssetLoadQueue } from '../interfaces/IAssetManager';
|
||||
|
||||
/**
|
||||
* Queue item
|
||||
* 队列项
|
||||
*/
|
||||
interface QueueItem {
|
||||
guid: AssetGUID;
|
||||
priority: number;
|
||||
options?: IAssetLoadOptions;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset load queue implementation
|
||||
* 资产加载队列实现
|
||||
*/
|
||||
export class AssetLoadQueue implements IAssetLoadQueue {
|
||||
private readonly _queue: QueueItem[] = [];
|
||||
private readonly _guidToIndex = new Map<AssetGUID, number>();
|
||||
|
||||
/**
|
||||
* Add to queue
|
||||
* 添加到队列
|
||||
*/
|
||||
enqueue(guid: AssetGUID, priority: number, options?: IAssetLoadOptions): void {
|
||||
// 检查是否已在队列中 / Check if already in queue
|
||||
if (this._guidToIndex.has(guid)) {
|
||||
this.reprioritize(guid, priority);
|
||||
return;
|
||||
}
|
||||
|
||||
const item: QueueItem = {
|
||||
guid,
|
||||
priority,
|
||||
options,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
// 二分查找插入位置 / Binary search for insertion position
|
||||
const index = this.findInsertIndex(priority);
|
||||
this._queue.splice(index, 0, item);
|
||||
|
||||
// 更新索引映射 / Update index mapping
|
||||
this.updateIndices(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove from queue
|
||||
* 从队列移除
|
||||
*/
|
||||
dequeue(): { guid: AssetGUID; options?: IAssetLoadOptions } | null {
|
||||
if (this._queue.length === 0) return null;
|
||||
|
||||
const item = this._queue.shift();
|
||||
if (!item) return null;
|
||||
|
||||
// 更新索引映射 / Update index mapping
|
||||
this._guidToIndex.delete(item.guid);
|
||||
this.updateIndices(0);
|
||||
|
||||
return {
|
||||
guid: item.guid,
|
||||
options: item.options
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if queue is empty
|
||||
* 检查队列是否为空
|
||||
*/
|
||||
isEmpty(): boolean {
|
||||
return this._queue.length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get queue size
|
||||
* 获取队列大小
|
||||
*/
|
||||
getSize(): number {
|
||||
return this._queue.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear queue
|
||||
* 清空队列
|
||||
*/
|
||||
clear(): void {
|
||||
this._queue.length = 0;
|
||||
this._guidToIndex.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reprioritize item
|
||||
* 重新设置优先级
|
||||
*/
|
||||
reprioritize(guid: AssetGUID, newPriority: number): void {
|
||||
const index = this._guidToIndex.get(guid);
|
||||
if (index === undefined) return;
|
||||
|
||||
const item = this._queue[index];
|
||||
if (!item || item.priority === newPriority) return;
|
||||
|
||||
// 移除旧项 / Remove old item
|
||||
this._queue.splice(index, 1);
|
||||
this._guidToIndex.delete(guid);
|
||||
|
||||
// 重新插入 / Reinsert with new priority
|
||||
item.priority = newPriority;
|
||||
const newIndex = this.findInsertIndex(newPriority);
|
||||
this._queue.splice(newIndex, 0, item);
|
||||
|
||||
// 更新索引 / Update indices
|
||||
this.updateIndices(Math.min(index, newIndex));
|
||||
}
|
||||
|
||||
/**
|
||||
* Find insertion index for priority
|
||||
* 查找优先级的插入索引
|
||||
*/
|
||||
private findInsertIndex(priority: number): number {
|
||||
let left = 0;
|
||||
let right = this._queue.length;
|
||||
|
||||
while (left < right) {
|
||||
const mid = Math.floor((left + right) / 2);
|
||||
// 高优先级在前 / Higher priority first
|
||||
if (this._queue[mid].priority >= priority) {
|
||||
left = mid + 1;
|
||||
} else {
|
||||
right = mid;
|
||||
}
|
||||
}
|
||||
|
||||
return left;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update indices after modification
|
||||
* 修改后更新索引
|
||||
*/
|
||||
private updateIndices(startIndex: number): void {
|
||||
for (let i = startIndex; i < this._queue.length; i++) {
|
||||
this._guidToIndex.set(this._queue[i].guid, i);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get queue items (for debugging)
|
||||
* 获取队列项(用于调试)
|
||||
*/
|
||||
getItems(): ReadonlyArray<{
|
||||
guid: AssetGUID;
|
||||
priority: number;
|
||||
waitTime: number;
|
||||
}> {
|
||||
const now = Date.now();
|
||||
return this._queue.map((item) => ({
|
||||
guid: item.guid,
|
||||
priority: item.priority,
|
||||
waitTime: now - item.timestamp
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove specific item from queue
|
||||
* 从队列中移除特定项
|
||||
*/
|
||||
remove(guid: AssetGUID): boolean {
|
||||
const index = this._guidToIndex.get(guid);
|
||||
if (index === undefined) return false;
|
||||
|
||||
this._queue.splice(index, 1);
|
||||
this._guidToIndex.delete(guid);
|
||||
this.updateIndices(index);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if guid is in queue
|
||||
* 检查guid是否在队列中
|
||||
*/
|
||||
contains(guid: AssetGUID): boolean {
|
||||
return this._guidToIndex.has(guid);
|
||||
}
|
||||
}
|
||||
@@ -1,684 +0,0 @@
|
||||
/**
|
||||
* Asset manager implementation
|
||||
* 资产管理器实现
|
||||
*/
|
||||
|
||||
import {
|
||||
AssetGUID,
|
||||
AssetHandle,
|
||||
AssetType,
|
||||
AssetState,
|
||||
IAssetLoadOptions,
|
||||
IAssetLoadResult,
|
||||
IAssetReferenceInfo,
|
||||
IAssetPreloadGroup,
|
||||
IAssetLoadProgress,
|
||||
IAssetMetadata,
|
||||
AssetLoadError,
|
||||
IAssetCatalog
|
||||
} from '../types/AssetTypes';
|
||||
import {
|
||||
IAssetManager,
|
||||
IAssetLoadQueue
|
||||
} from '../interfaces/IAssetManager';
|
||||
import { IAssetLoader, IAssetLoaderFactory, IAssetParseContext } from '../interfaces/IAssetLoader';
|
||||
import { IAssetReader, IAssetContent } from '../interfaces/IAssetReader';
|
||||
import { AssetCache } from './AssetCache';
|
||||
import { AssetLoadQueue } from './AssetLoadQueue';
|
||||
import { AssetLoaderFactory } from '../loaders/AssetLoaderFactory';
|
||||
import { AssetDatabase } from './AssetDatabase';
|
||||
|
||||
/**
|
||||
* Asset entry in the manager
|
||||
* 管理器中的资产条目
|
||||
*/
|
||||
interface AssetEntry {
|
||||
guid: AssetGUID;
|
||||
handle: AssetHandle;
|
||||
asset: unknown;
|
||||
metadata: IAssetMetadata;
|
||||
state: AssetState;
|
||||
referenceCount: number;
|
||||
lastAccessTime: number;
|
||||
loadPromise?: Promise<IAssetLoadResult>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset manager implementation
|
||||
* 资产管理器实现
|
||||
*/
|
||||
export class AssetManager implements IAssetManager {
|
||||
private readonly _assets = new Map<AssetGUID, AssetEntry>();
|
||||
private readonly _handleToGuid = new Map<AssetHandle, AssetGUID>();
|
||||
private readonly _pathToGuid = new Map<string, AssetGUID>();
|
||||
private readonly _cache: AssetCache;
|
||||
private readonly _loadQueue: IAssetLoadQueue;
|
||||
private readonly _loaderFactory: IAssetLoaderFactory;
|
||||
private readonly _database: AssetDatabase;
|
||||
|
||||
/** Asset reader for file operations. | 用于文件操作的资产读取器。 */
|
||||
private _reader: IAssetReader | null = null;
|
||||
|
||||
private _nextHandle: AssetHandle = 1;
|
||||
|
||||
private _statistics = {
|
||||
loadedCount: 0,
|
||||
failedCount: 0
|
||||
};
|
||||
|
||||
private _isDisposed = false;
|
||||
private _loadingCount = 0;
|
||||
|
||||
constructor(catalog?: IAssetCatalog) {
|
||||
this._cache = new AssetCache();
|
||||
this._loadQueue = new AssetLoadQueue();
|
||||
this._loaderFactory = new AssetLoaderFactory();
|
||||
this._database = new AssetDatabase();
|
||||
|
||||
if (catalog) {
|
||||
this.initializeFromCatalog(catalog);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set asset reader.
|
||||
* 设置资产读取器。
|
||||
*/
|
||||
setReader(reader: IAssetReader): void {
|
||||
this._reader = reader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set project root path for resolving relative paths.
|
||||
* 设置项目根路径用于解析相对路径。
|
||||
*/
|
||||
setProjectRoot(path: string): void {
|
||||
this._database.setProjectRoot(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the asset database.
|
||||
* 获取资产数据库。
|
||||
*/
|
||||
getDatabase(): AssetDatabase {
|
||||
return this._database;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize from catalog
|
||||
* 从目录初始化
|
||||
*/
|
||||
private initializeFromCatalog(catalog: IAssetCatalog): void {
|
||||
catalog.entries.forEach((entry, guid) => {
|
||||
const metadata: IAssetMetadata = {
|
||||
guid,
|
||||
path: entry.path,
|
||||
type: entry.type,
|
||||
name: entry.path.split('/').pop() || '',
|
||||
size: entry.size,
|
||||
hash: entry.hash,
|
||||
dependencies: [],
|
||||
labels: [],
|
||||
tags: new Map(),
|
||||
lastModified: Date.now(),
|
||||
version: 1
|
||||
};
|
||||
|
||||
this._database.addAsset(metadata);
|
||||
this._pathToGuid.set(entry.path, guid);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load asset by GUID
|
||||
* 通过GUID加载资产
|
||||
*/
|
||||
async loadAsset<T = unknown>(
|
||||
guid: AssetGUID,
|
||||
options?: IAssetLoadOptions
|
||||
): Promise<IAssetLoadResult<T>> {
|
||||
// 检查是否已加载 / Check if already loaded
|
||||
const entry = this._assets.get(guid);
|
||||
if (entry) {
|
||||
if (entry.state === AssetState.Loaded && !options?.forceReload) {
|
||||
entry.lastAccessTime = Date.now();
|
||||
return {
|
||||
asset: entry.asset as T,
|
||||
handle: entry.handle,
|
||||
metadata: entry.metadata,
|
||||
loadTime: 0
|
||||
};
|
||||
}
|
||||
|
||||
if (entry.state === AssetState.Loading && entry.loadPromise) {
|
||||
return entry.loadPromise as Promise<IAssetLoadResult<T>>;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取元数据 / Get metadata
|
||||
const metadata = this._database.getMetadata(guid);
|
||||
if (!metadata) {
|
||||
throw AssetLoadError.fileNotFound(guid, 'Unknown');
|
||||
}
|
||||
|
||||
// 创建加载器 / Create loader
|
||||
let loader = this._loaderFactory.createLoader(metadata.type);
|
||||
|
||||
// 如果没有找到 loader 且类型是 Custom,尝试重新解析类型
|
||||
// If no loader found and type is Custom, try to re-resolve the type
|
||||
if (!loader && metadata.type === AssetType.Custom) {
|
||||
const newType = this.resolveAssetType(metadata.path);
|
||||
if (newType !== AssetType.Custom) {
|
||||
// 更新 metadata 类型 / Update metadata type
|
||||
this._database.updateAsset(guid, { type: newType });
|
||||
loader = this._loaderFactory.createLoader(newType);
|
||||
}
|
||||
}
|
||||
|
||||
if (!loader) {
|
||||
throw AssetLoadError.unsupportedType(guid, metadata.type);
|
||||
}
|
||||
|
||||
// 开始加载 / Start loading
|
||||
const loadStartTime = performance.now();
|
||||
const newEntry: AssetEntry = {
|
||||
guid,
|
||||
handle: this._nextHandle++,
|
||||
asset: null,
|
||||
metadata,
|
||||
state: AssetState.Loading,
|
||||
referenceCount: 0,
|
||||
lastAccessTime: Date.now()
|
||||
};
|
||||
|
||||
this._assets.set(guid, newEntry);
|
||||
this._handleToGuid.set(newEntry.handle, guid);
|
||||
this._loadingCount++;
|
||||
|
||||
// 创建加载Promise / Create loading promise
|
||||
const loadPromise = this.performLoad<T>(loader, metadata, options, loadStartTime, newEntry);
|
||||
newEntry.loadPromise = loadPromise;
|
||||
|
||||
try {
|
||||
const result = await loadPromise;
|
||||
return result;
|
||||
} catch (error) {
|
||||
this._statistics.failedCount++;
|
||||
newEntry.state = AssetState.Failed;
|
||||
throw error;
|
||||
} finally {
|
||||
this._loadingCount--;
|
||||
delete newEntry.loadPromise;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform asset loading
|
||||
* 执行资产加载
|
||||
*/
|
||||
private async performLoad<T>(
|
||||
loader: IAssetLoader,
|
||||
metadata: IAssetMetadata,
|
||||
options: IAssetLoadOptions | undefined,
|
||||
startTime: number,
|
||||
entry: AssetEntry
|
||||
): Promise<IAssetLoadResult<T>> {
|
||||
if (!this._reader) {
|
||||
throw new Error('Asset reader not set. Call setReader() first.');
|
||||
}
|
||||
|
||||
// Load dependencies first.
|
||||
// 先加载依赖。
|
||||
if (metadata.dependencies.length > 0) {
|
||||
await this.loadDependencies(metadata.dependencies, options);
|
||||
}
|
||||
|
||||
// Resolve absolute path.
|
||||
// 解析绝对路径。
|
||||
const absolutePath = this._database.resolveAbsolutePath(metadata.path);
|
||||
|
||||
// Read content based on loader's content type.
|
||||
// 根据加载器的内容类型读取内容。
|
||||
const content = await this.readContent(loader.contentType, absolutePath);
|
||||
|
||||
// Create parse context.
|
||||
// 创建解析上下文。
|
||||
const context: IAssetParseContext = {
|
||||
metadata,
|
||||
options,
|
||||
loadDependency: async <D>(relativePath: string) => {
|
||||
const result = await this.loadAssetByPath<D>(relativePath, options);
|
||||
return result.asset;
|
||||
}
|
||||
};
|
||||
|
||||
// Parse asset.
|
||||
// 解析资产。
|
||||
const asset = await loader.parse(content, context);
|
||||
|
||||
// Update entry.
|
||||
// 更新条目。
|
||||
entry.asset = asset;
|
||||
entry.state = AssetState.Loaded;
|
||||
|
||||
// Cache asset.
|
||||
// 缓存资产。
|
||||
this._cache.set(metadata.guid, asset);
|
||||
|
||||
// Update statistics.
|
||||
// 更新统计。
|
||||
this._statistics.loadedCount++;
|
||||
|
||||
return {
|
||||
asset: asset as T,
|
||||
handle: entry.handle,
|
||||
metadata,
|
||||
loadTime: performance.now() - startTime
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Read content based on content type.
|
||||
* 根据内容类型读取内容。
|
||||
*/
|
||||
private async readContent(contentType: string, absolutePath: string): Promise<IAssetContent> {
|
||||
if (!this._reader) {
|
||||
throw new Error('Asset reader not set');
|
||||
}
|
||||
|
||||
switch (contentType) {
|
||||
case 'text': {
|
||||
const text = await this._reader.readText(absolutePath);
|
||||
return { type: 'text', text };
|
||||
}
|
||||
case 'binary': {
|
||||
const binary = await this._reader.readBinary(absolutePath);
|
||||
return { type: 'binary', binary };
|
||||
}
|
||||
case 'image': {
|
||||
const image = await this._reader.loadImage(absolutePath);
|
||||
return { type: 'image', image };
|
||||
}
|
||||
case 'audio': {
|
||||
const audioBuffer = await this._reader.loadAudio(absolutePath);
|
||||
return { type: 'audio', audioBuffer };
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown content type: ${contentType}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load dependencies
|
||||
* 加载依赖
|
||||
*/
|
||||
private async loadDependencies(
|
||||
dependencies: AssetGUID[],
|
||||
options?: IAssetLoadOptions
|
||||
): Promise<void> {
|
||||
const promises = dependencies.map((dep) => this.loadAsset(dep, options));
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load asset by path
|
||||
* 通过路径加载资产
|
||||
*/
|
||||
async loadAssetByPath<T = unknown>(
|
||||
path: string,
|
||||
options?: IAssetLoadOptions
|
||||
): Promise<IAssetLoadResult<T>> {
|
||||
const guid = this._pathToGuid.get(path);
|
||||
if (!guid) {
|
||||
// 尝试从数据库查找 / Try to find from database
|
||||
let metadata = this._database.getMetadataByPath(path);
|
||||
if (!metadata) {
|
||||
// 动态创建元数据 / Create metadata dynamically
|
||||
const assetType = this.resolveAssetType(path);
|
||||
|
||||
// 生成唯一GUID / Generate unique GUID
|
||||
const dynamicGuid = `dynamic_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
metadata = {
|
||||
guid: dynamicGuid,
|
||||
path: path,
|
||||
type: assetType,
|
||||
name: path.split('/').pop() || path.split('\\').pop() || 'unnamed',
|
||||
size: 0, // 动态加载时未知大小 / Unknown size for dynamic loading
|
||||
hash: '',
|
||||
dependencies: [],
|
||||
labels: [],
|
||||
tags: new Map(),
|
||||
lastModified: Date.now(),
|
||||
version: 1
|
||||
};
|
||||
|
||||
// 注册到数据库 / Register to database
|
||||
this._database.addAsset(metadata);
|
||||
this._pathToGuid.set(path, metadata.guid);
|
||||
} else {
|
||||
// 如果之前缓存的类型是 Custom,检查是否现在有注册的 loader 可以处理
|
||||
// If previously cached as Custom, check if a registered loader can now handle it
|
||||
if (metadata.type === AssetType.Custom) {
|
||||
const newType = this.resolveAssetType(path);
|
||||
if (newType !== AssetType.Custom) {
|
||||
metadata.type = newType;
|
||||
}
|
||||
}
|
||||
this._pathToGuid.set(path, metadata.guid);
|
||||
}
|
||||
|
||||
return this.loadAsset<T>(metadata.guid, options);
|
||||
}
|
||||
|
||||
// 同样检查已缓存的资产,如果类型是 Custom 但现在有 loader 可以处理
|
||||
// Also check cached assets, if type is Custom but now a loader can handle it
|
||||
const entry = this._assets.get(guid);
|
||||
if (entry && entry.metadata.type === AssetType.Custom) {
|
||||
const newType = this.resolveAssetType(path);
|
||||
if (newType !== AssetType.Custom) {
|
||||
entry.metadata.type = newType;
|
||||
}
|
||||
}
|
||||
|
||||
return this.loadAsset<T>(guid, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve asset type from path
|
||||
* 从路径解析资产类型
|
||||
*/
|
||||
private resolveAssetType(path: string): AssetType {
|
||||
// 首先尝试从已注册的加载器获取资产类型 / First try to get asset type from registered loaders
|
||||
const loaderType = (this._loaderFactory as AssetLoaderFactory).getAssetTypeByPath(path);
|
||||
if (loaderType !== null) {
|
||||
return loaderType;
|
||||
}
|
||||
|
||||
// 如果没有找到匹配的加载器,使用默认的扩展名映射 / Fallback to default extension mapping
|
||||
const fileExt = path.substring(path.lastIndexOf('.')).toLowerCase();
|
||||
|
||||
// 默认支持的基础类型 / Default supported basic types
|
||||
if (['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp'].includes(fileExt)) {
|
||||
return AssetType.Texture;
|
||||
} else if (['.json'].includes(fileExt)) {
|
||||
return AssetType.Json;
|
||||
} else if (['.txt', '.md', '.xml', '.yaml'].includes(fileExt)) {
|
||||
return AssetType.Text;
|
||||
}
|
||||
|
||||
return AssetType.Custom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load multiple assets
|
||||
* 批量加载资产
|
||||
*/
|
||||
async loadAssets(
|
||||
guids: AssetGUID[],
|
||||
options?: IAssetLoadOptions
|
||||
): Promise<Map<AssetGUID, IAssetLoadResult>> {
|
||||
const results = new Map<AssetGUID, IAssetLoadResult>();
|
||||
|
||||
// 并行加载所有资产 / Load all assets in parallel
|
||||
const promises = guids.map(async (guid) => {
|
||||
try {
|
||||
const result = await this.loadAsset(guid, options);
|
||||
results.set(guid, result);
|
||||
} catch (error) {
|
||||
console.error(`Failed to load asset ${guid}:`, error);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Preload asset group
|
||||
* 预加载资产组
|
||||
*/
|
||||
async preloadGroup(
|
||||
group: IAssetPreloadGroup,
|
||||
onProgress?: (progress: IAssetLoadProgress) => void
|
||||
): Promise<void> {
|
||||
const totalCount = group.assets.length;
|
||||
let loadedCount = 0;
|
||||
let loadedBytes = 0;
|
||||
let totalBytes = 0;
|
||||
|
||||
// 计算总大小 / Calculate total size
|
||||
for (const guid of group.assets) {
|
||||
const metadata = this._database.getMetadata(guid);
|
||||
if (metadata) {
|
||||
totalBytes += metadata.size;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载每个资产 / Load each asset
|
||||
for (const guid of group.assets) {
|
||||
const metadata = this._database.getMetadata(guid);
|
||||
if (!metadata) continue;
|
||||
|
||||
if (onProgress) {
|
||||
onProgress({
|
||||
currentAsset: metadata.name,
|
||||
loadedCount,
|
||||
totalCount,
|
||||
loadedBytes,
|
||||
totalBytes,
|
||||
progress: loadedCount / totalCount
|
||||
});
|
||||
}
|
||||
|
||||
await this.loadAsset(guid, { priority: group.priority });
|
||||
|
||||
loadedCount++;
|
||||
loadedBytes += metadata.size;
|
||||
}
|
||||
|
||||
// 最终进度 / Final progress
|
||||
if (onProgress) {
|
||||
onProgress({
|
||||
currentAsset: '',
|
||||
loadedCount: totalCount,
|
||||
totalCount,
|
||||
loadedBytes: totalBytes,
|
||||
totalBytes,
|
||||
progress: 1
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get loaded asset
|
||||
* 获取已加载的资产
|
||||
*/
|
||||
getAsset<T = unknown>(guid: AssetGUID): T | null {
|
||||
const entry = this._assets.get(guid);
|
||||
if (entry && entry.state === AssetState.Loaded) {
|
||||
entry.lastAccessTime = Date.now();
|
||||
return entry.asset as T;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get asset by handle
|
||||
* 通过句柄获取资产
|
||||
*/
|
||||
getAssetByHandle<T = unknown>(handle: AssetHandle): T | null {
|
||||
const guid = this._handleToGuid.get(handle);
|
||||
if (!guid) return null;
|
||||
return this.getAsset<T>(guid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get loaded asset by path (synchronous)
|
||||
* 通过路径获取已加载的资产(同步)
|
||||
*
|
||||
* Returns the asset if it's already loaded, null otherwise.
|
||||
* 如果资产已加载则返回资产,否则返回 null。
|
||||
*/
|
||||
getAssetByPath<T = unknown>(path: string): T | null {
|
||||
const guid = this._pathToGuid.get(path);
|
||||
if (!guid) return null;
|
||||
return this.getAsset<T>(guid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if asset is loaded
|
||||
* 检查资产是否已加载
|
||||
*/
|
||||
isLoaded(guid: AssetGUID): boolean {
|
||||
const entry = this._assets.get(guid);
|
||||
return entry?.state === AssetState.Loaded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get asset state
|
||||
* 获取资产状态
|
||||
*/
|
||||
getAssetState(guid: AssetGUID): AssetState {
|
||||
const entry = this._assets.get(guid);
|
||||
return entry?.state || AssetState.Unloaded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unload asset
|
||||
* 卸载资产
|
||||
*/
|
||||
unloadAsset(guid: AssetGUID): void {
|
||||
const entry = this._assets.get(guid);
|
||||
if (!entry) return;
|
||||
|
||||
// 检查引用计数 / Check reference count
|
||||
if (entry.referenceCount > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取加载器以释放资源 / Get loader to dispose resources
|
||||
const loader = this._loaderFactory.createLoader(entry.metadata.type);
|
||||
if (loader) {
|
||||
loader.dispose(entry.asset);
|
||||
}
|
||||
|
||||
// 清理条目 / Clean up entry
|
||||
this._handleToGuid.delete(entry.handle);
|
||||
this._assets.delete(guid);
|
||||
this._cache.remove(guid);
|
||||
|
||||
// 更新统计 / Update statistics
|
||||
this._statistics.loadedCount--;
|
||||
|
||||
entry.state = AssetState.Unloaded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unload all assets
|
||||
* 卸载所有资产
|
||||
*/
|
||||
unloadAllAssets(): void {
|
||||
const guids = Array.from(this._assets.keys());
|
||||
guids.forEach((guid) => this.unloadAsset(guid));
|
||||
}
|
||||
|
||||
/**
|
||||
* Unload unused assets
|
||||
* 卸载未使用的资产
|
||||
*/
|
||||
unloadUnusedAssets(): void {
|
||||
const guids = Array.from(this._assets.keys());
|
||||
guids.forEach((guid) => {
|
||||
const entry = this._assets.get(guid);
|
||||
if (entry && entry.referenceCount === 0) {
|
||||
this.unloadAsset(guid);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add reference to asset
|
||||
* 增加资产引用
|
||||
*/
|
||||
addReference(guid: AssetGUID): void {
|
||||
const entry = this._assets.get(guid);
|
||||
if (entry) {
|
||||
entry.referenceCount++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove reference from asset
|
||||
* 移除资产引用
|
||||
*/
|
||||
removeReference(guid: AssetGUID): void {
|
||||
const entry = this._assets.get(guid);
|
||||
if (entry && entry.referenceCount > 0) {
|
||||
entry.referenceCount--;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get reference info
|
||||
* 获取引用信息
|
||||
*/
|
||||
getReferenceInfo(guid: AssetGUID): IAssetReferenceInfo | null {
|
||||
const entry = this._assets.get(guid);
|
||||
if (!entry) return null;
|
||||
|
||||
return {
|
||||
guid,
|
||||
handle: entry.handle,
|
||||
referenceCount: entry.referenceCount,
|
||||
lastAccessTime: entry.lastAccessTime,
|
||||
state: entry.state
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Register custom loader
|
||||
* 注册自定义加载器
|
||||
*/
|
||||
registerLoader(type: AssetType, loader: IAssetLoader): void {
|
||||
this._loaderFactory.registerLoader(type, loader);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get asset statistics
|
||||
* 获取资产统计信息
|
||||
*/
|
||||
getStatistics(): { loadedCount: number; loadQueue: number; failedCount: number } {
|
||||
return {
|
||||
loadedCount: this._statistics.loadedCount,
|
||||
loadQueue: this._loadQueue.getSize(),
|
||||
failedCount: this._statistics.failedCount
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cache
|
||||
* 清空缓存
|
||||
*/
|
||||
clearCache(): void {
|
||||
this._cache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose manager
|
||||
* 释放管理器
|
||||
*/
|
||||
dispose(): void {
|
||||
if (this._isDisposed) return;
|
||||
|
||||
this.unloadAllAssets();
|
||||
this._cache.clear();
|
||||
this._loadQueue.clear();
|
||||
this._assets.clear();
|
||||
this._handleToGuid.clear();
|
||||
this._pathToGuid.clear();
|
||||
|
||||
this._isDisposed = true;
|
||||
}
|
||||
}
|
||||
@@ -1,241 +0,0 @@
|
||||
/**
|
||||
* Asset path resolver for different platforms and protocols
|
||||
* 不同平台和协议的资产路径解析器
|
||||
*/
|
||||
|
||||
import { AssetPlatform } from '../types/AssetTypes';
|
||||
import { PathValidator } from '../utils/PathValidator';
|
||||
|
||||
/**
|
||||
* Asset path resolver configuration
|
||||
* 资产路径解析器配置
|
||||
*/
|
||||
export interface IAssetPathConfig {
|
||||
/** Base URL for web assets | Web资产的基础URL */
|
||||
baseUrl?: string;
|
||||
|
||||
/** Asset directory path | 资产目录路径 */
|
||||
assetDir?: string;
|
||||
|
||||
/** Asset host for asset:// protocol | 资产协议的主机名 */
|
||||
assetHost?: string;
|
||||
|
||||
/** Current platform | 当前平台 */
|
||||
platform?: AssetPlatform;
|
||||
|
||||
/** Custom path transformer | 自定义路径转换器 */
|
||||
pathTransformer?: (path: string) => string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset path resolver
|
||||
* 资产路径解析器
|
||||
*/
|
||||
export class AssetPathResolver {
|
||||
private config: IAssetPathConfig;
|
||||
|
||||
constructor(config: IAssetPathConfig = {}) {
|
||||
this.config = {
|
||||
baseUrl: '',
|
||||
assetDir: 'assets',
|
||||
platform: AssetPlatform.H5,
|
||||
...config
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update configuration
|
||||
* 更新配置
|
||||
*/
|
||||
updateConfig(config: Partial<IAssetPathConfig>): void {
|
||||
this.config = { ...this.config, ...config };
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve asset path to full URL
|
||||
* 解析资产路径为完整URL
|
||||
*/
|
||||
resolve(path: string): string {
|
||||
// Validate input path
|
||||
const validation = PathValidator.validate(path);
|
||||
if (!validation.valid) {
|
||||
console.warn(`Invalid asset path: ${path} - ${validation.reason}`);
|
||||
// Sanitize the path instead of throwing
|
||||
path = PathValidator.sanitize(path);
|
||||
if (!path) {
|
||||
throw new Error(`Cannot resolve invalid path: ${validation.reason}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Already a full URL
|
||||
// 已经是完整URL
|
||||
if (this.isAbsoluteUrl(path)) {
|
||||
return path;
|
||||
}
|
||||
|
||||
// Data URL
|
||||
// 数据URL
|
||||
if (path.startsWith('data:')) {
|
||||
return path;
|
||||
}
|
||||
|
||||
// Normalize the path
|
||||
path = PathValidator.normalize(path);
|
||||
|
||||
// Apply custom transformer if provided
|
||||
// 应用自定义转换器(如果提供)
|
||||
if (this.config.pathTransformer) {
|
||||
path = this.config.pathTransformer(path);
|
||||
// Transformer output is trusted (may be absolute path or asset:// URL)
|
||||
// 转换器输出是可信的(可能是绝对路径或 asset:// URL)
|
||||
return path;
|
||||
}
|
||||
|
||||
// Platform-specific resolution
|
||||
// 平台特定解析
|
||||
switch (this.config.platform) {
|
||||
case AssetPlatform.H5:
|
||||
return this.resolveH5Path(path);
|
||||
|
||||
case AssetPlatform.WeChat:
|
||||
return this.resolveWeChatPath(path);
|
||||
|
||||
case AssetPlatform.Playable:
|
||||
return this.resolvePlayablePath(path);
|
||||
|
||||
case AssetPlatform.Android:
|
||||
case AssetPlatform.iOS:
|
||||
return this.resolveMobilePath(path);
|
||||
|
||||
case AssetPlatform.Editor:
|
||||
return this.resolveEditorPath(path);
|
||||
|
||||
default:
|
||||
return this.resolveH5Path(path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve path for H5 platform
|
||||
* 解析H5平台路径
|
||||
*/
|
||||
private resolveH5Path(path: string): string {
|
||||
// Remove leading slash if present
|
||||
// 移除开头的斜杠(如果存在)
|
||||
path = path.replace(/^\//, '');
|
||||
|
||||
// Combine with base URL and asset directory
|
||||
// 与基础URL和资产目录结合
|
||||
const base = this.config.baseUrl || (typeof window !== 'undefined' ? window.location.origin : '');
|
||||
const assetDir = this.config.assetDir || 'assets';
|
||||
|
||||
return `${base}/${assetDir}/${path}`.replace(/\/+/g, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve path for WeChat Mini Game
|
||||
* 解析微信小游戏路径
|
||||
*/
|
||||
private resolveWeChatPath(path: string): string {
|
||||
// WeChat mini games use relative paths
|
||||
// 微信小游戏使用相对路径
|
||||
return `${this.config.assetDir}/${path}`.replace(/\/+/g, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve path for Playable Ads platform
|
||||
* 解析试玩广告平台路径
|
||||
*/
|
||||
private resolvePlayablePath(path: string): string {
|
||||
// Playable ads typically use base64 embedded resources or relative paths
|
||||
// 试玩广告通常使用base64内嵌资源或相对路径
|
||||
|
||||
// If custom transformer is provided (e.g., for base64 encoding)
|
||||
// 如果提供了自定义转换器(例如用于base64编码)
|
||||
if (this.config.pathTransformer) {
|
||||
return this.config.pathTransformer(path);
|
||||
}
|
||||
|
||||
// Default to relative path without directory prefix
|
||||
// 默认使用不带目录前缀的相对路径
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve path for mobile platform (Android/iOS)
|
||||
* 解析移动平台路径(Android/iOS)
|
||||
*/
|
||||
private resolveMobilePath(path: string): string {
|
||||
// Mobile platforms use relative paths or file:// protocol
|
||||
// 移动平台使用相对路径或file://协议
|
||||
return `./${this.config.assetDir}/${path}`.replace(/\/+/g, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve path for Editor platform (Tauri)
|
||||
* 解析编辑器平台路径(Tauri)
|
||||
*/
|
||||
private resolveEditorPath(path: string): string {
|
||||
// For Tauri editor, use pathTransformer if provided
|
||||
// 对于Tauri编辑器,使用pathTransformer(如果提供)
|
||||
if (this.config.pathTransformer) {
|
||||
return this.config.pathTransformer(path);
|
||||
}
|
||||
|
||||
// Use configurable asset host or default to 'localhost'
|
||||
// 使用可配置的资产主机或默认为 'localhost'
|
||||
const host = this.config.assetHost || 'localhost';
|
||||
const sanitizedPath = PathValidator.sanitize(path);
|
||||
return `asset://${host}/${sanitizedPath}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if path is absolute URL
|
||||
* 检查路径是否为绝对URL
|
||||
*/
|
||||
private isAbsoluteUrl(path: string): boolean {
|
||||
return /^(https?:\/\/|file:\/\/|asset:\/\/)/.test(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get asset directory from path
|
||||
* 从路径获取资产目录
|
||||
*/
|
||||
getAssetDirectory(path: string): string {
|
||||
const resolved = this.resolve(path);
|
||||
const lastSlash = resolved.lastIndexOf('/');
|
||||
return lastSlash >= 0 ? resolved.substring(0, lastSlash) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get asset filename from path
|
||||
* 从路径获取资产文件名
|
||||
*/
|
||||
getAssetFilename(path: string): string {
|
||||
const resolved = this.resolve(path);
|
||||
const lastSlash = resolved.lastIndexOf('/');
|
||||
return lastSlash >= 0 ? resolved.substring(lastSlash + 1) : resolved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Join paths
|
||||
* 连接路径
|
||||
*/
|
||||
join(...paths: string[]): string {
|
||||
return paths.join('/').replace(/\/+/g, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize path
|
||||
* 规范化路径
|
||||
*/
|
||||
normalize(path: string): string {
|
||||
return path.replace(/\\/g, '/').replace(/\/+/g, '/');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Global asset path resolver instance
|
||||
* 全局资产路径解析器实例
|
||||
*/
|
||||
export const globalPathResolver = new AssetPathResolver();
|
||||
@@ -1,338 +0,0 @@
|
||||
/**
|
||||
* Asset reference for lazy loading
|
||||
* 用于懒加载的资产引用
|
||||
*/
|
||||
|
||||
import { AssetGUID, IAssetLoadOptions, AssetState } from '../types/AssetTypes';
|
||||
import { IAssetManager } from '../interfaces/IAssetManager';
|
||||
|
||||
/**
|
||||
* Asset reference class for lazy loading
|
||||
* 懒加载资产引用类
|
||||
*/
|
||||
export class AssetReference<T = unknown> {
|
||||
private _guid: AssetGUID;
|
||||
private _asset?: T;
|
||||
private _loadPromise?: Promise<T>;
|
||||
private _manager?: IAssetManager;
|
||||
private _isReleased = false;
|
||||
private _autoRelease = false;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* 构造函数
|
||||
*/
|
||||
constructor(guid: AssetGUID, manager?: IAssetManager) {
|
||||
this._guid = guid;
|
||||
this._manager = manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get asset GUID
|
||||
* 获取资产GUID
|
||||
*/
|
||||
get guid(): AssetGUID {
|
||||
return this._guid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if asset is loaded
|
||||
* 检查资产是否已加载
|
||||
*/
|
||||
get isLoaded(): boolean {
|
||||
return this._asset !== undefined && !this._isReleased;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get asset synchronously (returns null if not loaded)
|
||||
* 同步获取资产(如果未加载则返回null)
|
||||
*/
|
||||
get asset(): T | null {
|
||||
return this._asset ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set asset manager
|
||||
* 设置资产管理器
|
||||
*/
|
||||
setManager(manager: IAssetManager): void {
|
||||
this._manager = manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load asset asynchronously
|
||||
* 异步加载资产
|
||||
*/
|
||||
async loadAsync(options?: IAssetLoadOptions): Promise<T> {
|
||||
if (this._isReleased) {
|
||||
throw new Error(`Asset reference ${this._guid} has been released`);
|
||||
}
|
||||
|
||||
// 如果已经加载,直接返回 / Return if already loaded
|
||||
if (this._asset !== undefined) {
|
||||
return this._asset;
|
||||
}
|
||||
|
||||
// 如果正在加载,返回加载Promise / Return loading promise if loading
|
||||
if (this._loadPromise) {
|
||||
return this._loadPromise;
|
||||
}
|
||||
|
||||
if (!this._manager) {
|
||||
throw new Error('Asset manager not set for AssetReference');
|
||||
}
|
||||
|
||||
// 开始加载 / Start loading
|
||||
this._loadPromise = this.performLoad(options);
|
||||
|
||||
try {
|
||||
const asset = await this._loadPromise;
|
||||
return asset;
|
||||
} finally {
|
||||
this._loadPromise = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform asset loading
|
||||
* 执行资产加载
|
||||
*/
|
||||
private async performLoad(options?: IAssetLoadOptions): Promise<T> {
|
||||
if (!this._manager) {
|
||||
throw new Error('Asset manager not set');
|
||||
}
|
||||
|
||||
const result = await this._manager.loadAsset<T>(this._guid, options);
|
||||
this._asset = result.asset;
|
||||
|
||||
// 增加引用计数 / Increase reference count
|
||||
this._manager.addReference(this._guid);
|
||||
|
||||
return this._asset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Release asset reference
|
||||
* 释放资产引用
|
||||
*/
|
||||
release(): void {
|
||||
if (this._isReleased) return;
|
||||
|
||||
if (this._manager && this._asset !== undefined) {
|
||||
// 减少引用计数 / Decrease reference count
|
||||
this._manager.removeReference(this._guid);
|
||||
|
||||
// 如果引用计数为0,可以考虑卸载 / Consider unloading if reference count is 0
|
||||
const refInfo = this._manager.getReferenceInfo(this._guid);
|
||||
if (refInfo && refInfo.referenceCount === 0 && this._autoRelease) {
|
||||
this._manager.unloadAsset(this._guid);
|
||||
}
|
||||
}
|
||||
|
||||
this._asset = undefined;
|
||||
this._isReleased = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set auto-release mode
|
||||
* 设置自动释放模式
|
||||
*/
|
||||
setAutoRelease(autoRelease: boolean): void {
|
||||
this._autoRelease = autoRelease;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate reference
|
||||
* 验证引用
|
||||
*/
|
||||
validate(): boolean {
|
||||
if (!this._manager) return false;
|
||||
|
||||
const state = this._manager.getAssetState(this._guid);
|
||||
return state !== AssetState.Failed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get asset state
|
||||
* 获取资产状态
|
||||
*/
|
||||
getState(): AssetState {
|
||||
if (this._isReleased) return AssetState.Unloaded;
|
||||
if (!this._manager) return AssetState.Unloaded;
|
||||
|
||||
return this._manager.getAssetState(this._guid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone reference
|
||||
* 克隆引用
|
||||
*/
|
||||
clone(): AssetReference<T> {
|
||||
const newRef = new AssetReference<T>(this._guid, this._manager);
|
||||
newRef.setAutoRelease(this._autoRelease);
|
||||
return newRef;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to JSON
|
||||
* 转换为JSON
|
||||
*/
|
||||
toJSON(): { guid: AssetGUID } {
|
||||
return { guid: this._guid };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create from JSON
|
||||
* 从JSON创建
|
||||
*/
|
||||
static fromJSON<T = unknown>(
|
||||
json: { guid: AssetGUID },
|
||||
manager?: IAssetManager
|
||||
): AssetReference<T> {
|
||||
return new AssetReference<T>(json.guid, manager);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Weak asset reference that doesn't prevent unloading
|
||||
* 不阻止卸载的弱资产引用
|
||||
*/
|
||||
export class WeakAssetReference<T = unknown> {
|
||||
private _guid: AssetGUID;
|
||||
private _manager?: IAssetManager;
|
||||
|
||||
constructor(guid: AssetGUID, manager?: IAssetManager) {
|
||||
this._guid = guid;
|
||||
this._manager = manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get asset GUID
|
||||
* 获取资产GUID
|
||||
*/
|
||||
get guid(): AssetGUID {
|
||||
return this._guid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try get asset without loading
|
||||
* 尝试获取资产而不加载
|
||||
*/
|
||||
tryGet(): T | null {
|
||||
if (!this._manager) return null;
|
||||
return this._manager.getAsset<T>(this._guid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load asset if not loaded
|
||||
* 如果未加载则加载资产
|
||||
*/
|
||||
async loadAsync(options?: IAssetLoadOptions): Promise<T> {
|
||||
if (!this._manager) {
|
||||
throw new Error('Asset manager not set');
|
||||
}
|
||||
|
||||
const result = await this._manager.loadAsset<T>(this._guid, options);
|
||||
// 不增加引用计数 / Don't increase reference count for weak reference
|
||||
return result.asset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if asset is loaded
|
||||
* 检查资产是否已加载
|
||||
*/
|
||||
isLoaded(): boolean {
|
||||
if (!this._manager) return false;
|
||||
return this._manager.isLoaded(this._guid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set asset manager
|
||||
* 设置资产管理器
|
||||
*/
|
||||
setManager(manager: IAssetManager): void {
|
||||
this._manager = manager;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset reference array for managing multiple references
|
||||
* 用于管理多个引用的资产引用数组
|
||||
*/
|
||||
export class AssetReferenceArray<T = unknown> {
|
||||
private _references: AssetReference<T>[] = [];
|
||||
private _manager?: IAssetManager;
|
||||
|
||||
constructor(guids: AssetGUID[] = [], manager?: IAssetManager) {
|
||||
this._manager = manager;
|
||||
this._references = guids.map((guid) => new AssetReference<T>(guid, manager));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add reference
|
||||
* 添加引用
|
||||
*/
|
||||
add(guid: AssetGUID): void {
|
||||
this._references.push(new AssetReference<T>(guid, this._manager));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove reference
|
||||
* 移除引用
|
||||
*/
|
||||
remove(guid: AssetGUID): boolean {
|
||||
const index = this._references.findIndex((ref) => ref.guid === guid);
|
||||
if (index >= 0) {
|
||||
this._references[index].release();
|
||||
this._references.splice(index, 1);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all assets
|
||||
* 加载所有资产
|
||||
*/
|
||||
async loadAllAsync(options?: IAssetLoadOptions): Promise<T[]> {
|
||||
const promises = this._references.map((ref) => ref.loadAsync(options));
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Release all references
|
||||
* 释放所有引用
|
||||
*/
|
||||
releaseAll(): void {
|
||||
this._references.forEach((ref) => ref.release());
|
||||
this._references = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all loaded assets
|
||||
* 获取所有已加载的资产
|
||||
*/
|
||||
getLoadedAssets(): T[] {
|
||||
return this._references
|
||||
.filter((ref) => ref.isLoaded)
|
||||
.map((ref) => ref.asset!)
|
||||
.filter((asset) => asset !== null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get reference count
|
||||
* 获取引用数量
|
||||
*/
|
||||
get count(): number {
|
||||
return this._references.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set asset manager
|
||||
* 设置资产管理器
|
||||
*/
|
||||
setManager(manager: IAssetManager): void {
|
||||
this._manager = manager;
|
||||
this._references.forEach((ref) => ref.setManager(manager));
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
/**
|
||||
* Asset System for ECS Framework
|
||||
* ECS框架的资产系统
|
||||
*
|
||||
* Runtime-focused asset management:
|
||||
* - Asset loading and caching
|
||||
* - GUID-based asset resolution
|
||||
* - Bundle loading
|
||||
*
|
||||
* For editor-side functionality (meta files, packing), use @esengine/asset-system-editor
|
||||
*/
|
||||
|
||||
// Types
|
||||
export * from './types/AssetTypes';
|
||||
|
||||
// Bundle format (shared types for runtime and editor)
|
||||
export * from './bundle/BundleFormat';
|
||||
|
||||
// Runtime catalog
|
||||
export { RuntimeCatalog, runtimeCatalog } from './runtime/RuntimeCatalog';
|
||||
|
||||
// Interfaces
|
||||
export * from './interfaces/IAssetLoader';
|
||||
export * from './interfaces/IAssetManager';
|
||||
export * from './interfaces/IAssetReader';
|
||||
export * from './interfaces/IResourceComponent';
|
||||
|
||||
// Core
|
||||
export { AssetManager } from './core/AssetManager';
|
||||
export { AssetCache } from './core/AssetCache';
|
||||
export { AssetDatabase } from './core/AssetDatabase';
|
||||
export { AssetLoadQueue } from './core/AssetLoadQueue';
|
||||
export { AssetReference, WeakAssetReference, AssetReferenceArray } from './core/AssetReference';
|
||||
export { AssetPathResolver, globalPathResolver } from './core/AssetPathResolver';
|
||||
export type { IAssetPathConfig } from './core/AssetPathResolver';
|
||||
|
||||
// Loaders
|
||||
export { AssetLoaderFactory } from './loaders/AssetLoaderFactory';
|
||||
export { TextureLoader } from './loaders/TextureLoader';
|
||||
export { JsonLoader } from './loaders/JsonLoader';
|
||||
export { TextLoader } from './loaders/TextLoader';
|
||||
export { BinaryLoader } from './loaders/BinaryLoader';
|
||||
|
||||
// Integration
|
||||
export { EngineIntegration } from './integration/EngineIntegration';
|
||||
export type { IEngineBridge } from './integration/EngineIntegration';
|
||||
|
||||
// Services
|
||||
export { SceneResourceManager } from './services/SceneResourceManager';
|
||||
export type { IResourceLoader } from './services/SceneResourceManager';
|
||||
|
||||
// Utils
|
||||
export { UVHelper } from './utils/UVHelper';
|
||||
|
||||
// Default instance
|
||||
import { AssetManager } from './core/AssetManager';
|
||||
|
||||
/**
|
||||
* Default asset manager instance
|
||||
* 默认资产管理器实例
|
||||
*/
|
||||
export const assetManager = new AssetManager();
|
||||
|
||||
/**
|
||||
* Initialize asset system with catalog
|
||||
* 使用目录初始化资产系统
|
||||
*/
|
||||
export function initializeAssetSystem(catalog?: IAssetCatalog): AssetManager {
|
||||
if (catalog) {
|
||||
return new AssetManager(catalog);
|
||||
}
|
||||
return assetManager;
|
||||
}
|
||||
|
||||
// Re-export IAssetCatalog for initializeAssetSystem signature
|
||||
import type { IAssetCatalog } from './types/AssetTypes';
|
||||
@@ -1,247 +0,0 @@
|
||||
/**
|
||||
* Engine integration for asset system
|
||||
* 资产系统的引擎集成
|
||||
*/
|
||||
|
||||
import { AssetManager } from '../core/AssetManager';
|
||||
import { AssetGUID } from '../types/AssetTypes';
|
||||
import { ITextureAsset } from '../interfaces/IAssetLoader';
|
||||
import { globalPathResolver } from '../core/AssetPathResolver';
|
||||
|
||||
/**
|
||||
* Engine bridge interface
|
||||
* 引擎桥接接口
|
||||
*/
|
||||
export interface IEngineBridge {
|
||||
/**
|
||||
* Load texture to GPU
|
||||
* 加载纹理到GPU
|
||||
*/
|
||||
loadTexture(id: number, url: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Load multiple textures
|
||||
* 批量加载纹理
|
||||
*/
|
||||
loadTextures(requests: Array<{ id: number; url: string }>): Promise<void>;
|
||||
|
||||
/**
|
||||
* Unload texture from GPU
|
||||
* 从GPU卸载纹理
|
||||
*/
|
||||
unloadTexture(id: number): void;
|
||||
|
||||
/**
|
||||
* Get texture info
|
||||
* 获取纹理信息
|
||||
*/
|
||||
getTextureInfo(id: number): { width: number; height: number } | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset system engine integration
|
||||
* 资产系统引擎集成
|
||||
*/
|
||||
export class EngineIntegration {
|
||||
private _assetManager: AssetManager;
|
||||
private _engineBridge?: IEngineBridge;
|
||||
private _textureIdMap = new Map<AssetGUID, number>();
|
||||
private _pathToTextureId = new Map<string, number>();
|
||||
|
||||
constructor(assetManager: AssetManager, engineBridge?: IEngineBridge) {
|
||||
this._assetManager = assetManager;
|
||||
this._engineBridge = engineBridge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set engine bridge
|
||||
* 设置引擎桥接
|
||||
*/
|
||||
setEngineBridge(bridge: IEngineBridge): void {
|
||||
this._engineBridge = bridge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load texture for component
|
||||
* 为组件加载纹理
|
||||
*
|
||||
* AssetManager 内部会处理路径解析,这里只需传入原始路径。
|
||||
* AssetManager handles path resolution internally, just pass the original path here.
|
||||
*/
|
||||
async loadTextureForComponent(texturePath: string): Promise<number> {
|
||||
// 检查缓存(使用原始路径作为键)
|
||||
// Check cache (using original path as key)
|
||||
const existingId = this._pathToTextureId.get(texturePath);
|
||||
if (existingId) {
|
||||
return existingId;
|
||||
}
|
||||
|
||||
// 通过资产系统加载(AssetManager 内部会解析路径)
|
||||
// Load through asset system (AssetManager resolves path internally)
|
||||
const result = await this._assetManager.loadAssetByPath<ITextureAsset>(texturePath);
|
||||
const textureAsset = result.asset;
|
||||
|
||||
// 如果有引擎桥接,上传到GPU
|
||||
// Upload to GPU if bridge exists
|
||||
// 使用 globalPathResolver 将路径转换为引擎可用的 URL
|
||||
// Use globalPathResolver to convert path to engine-compatible URL
|
||||
if (this._engineBridge && textureAsset.data) {
|
||||
const engineUrl = globalPathResolver.resolve(texturePath);
|
||||
await this._engineBridge.loadTexture(textureAsset.textureId, engineUrl);
|
||||
}
|
||||
|
||||
// 缓存映射(使用原始路径作为键,避免重复解析)
|
||||
// Cache mapping (using original path as key to avoid re-resolving)
|
||||
this._pathToTextureId.set(texturePath, textureAsset.textureId);
|
||||
|
||||
return textureAsset.textureId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load texture by GUID
|
||||
* 通过GUID加载纹理
|
||||
*/
|
||||
async loadTextureByGuid(guid: AssetGUID): Promise<number> {
|
||||
// 检查是否已有纹理ID / Check if texture ID exists
|
||||
const existingId = this._textureIdMap.get(guid);
|
||||
if (existingId) {
|
||||
return existingId;
|
||||
}
|
||||
|
||||
// 通过资产系统加载 / Load through asset system
|
||||
const result = await this._assetManager.loadAsset<ITextureAsset>(guid);
|
||||
const textureAsset = result.asset;
|
||||
|
||||
// 如果有引擎桥接,上传到GPU / Upload to GPU if bridge exists
|
||||
if (this._engineBridge && textureAsset.data) {
|
||||
const metadata = result.metadata;
|
||||
await this._engineBridge.loadTexture(textureAsset.textureId, metadata.path);
|
||||
}
|
||||
|
||||
// 缓存映射 / Cache mapping
|
||||
this._textureIdMap.set(guid, textureAsset.textureId);
|
||||
|
||||
return textureAsset.textureId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch load textures
|
||||
* 批量加载纹理
|
||||
*/
|
||||
async loadTexturesBatch(paths: string[]): Promise<Map<string, number>> {
|
||||
const results = new Map<string, number>();
|
||||
|
||||
// 收集需要加载的纹理 / Collect textures to load
|
||||
const toLoad: string[] = [];
|
||||
for (const path of paths) {
|
||||
const existingId = this._pathToTextureId.get(path);
|
||||
if (existingId) {
|
||||
results.set(path, existingId);
|
||||
} else {
|
||||
toLoad.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
if (toLoad.length === 0) {
|
||||
return results;
|
||||
}
|
||||
|
||||
// 并行加载所有纹理 / Load all textures in parallel
|
||||
const loadPromises = toLoad.map(async (path) => {
|
||||
try {
|
||||
const id = await this.loadTextureForComponent(path);
|
||||
results.set(path, id);
|
||||
} catch (error) {
|
||||
console.error(`Failed to load texture: ${path}`, error);
|
||||
results.set(path, 0); // 使用默认纹理ID / Use default texture ID
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(loadPromises);
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量加载资源(通用方法,支持 IResourceLoader 接口)
|
||||
* Load resources in batch (generic method for IResourceLoader interface)
|
||||
*
|
||||
* @param paths 资源路径数组 / Array of resource paths
|
||||
* @param type 资源类型 / Resource type
|
||||
* @returns 路径到运行时 ID 的映射 / Map of paths to runtime IDs
|
||||
*/
|
||||
async loadResourcesBatch(paths: string[], type: 'texture' | 'audio' | 'font' | 'data'): Promise<Map<string, number>> {
|
||||
// 目前只支持纹理 / Currently only supports textures
|
||||
if (type === 'texture') {
|
||||
return this.loadTexturesBatch(paths);
|
||||
}
|
||||
|
||||
// 其他资源类型暂未实现 / Other resource types not yet implemented
|
||||
console.warn(`[EngineIntegration] Resource type '${type}' not yet supported`);
|
||||
return new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unload texture
|
||||
* 卸载纹理
|
||||
*/
|
||||
unloadTexture(textureId: number): void {
|
||||
// 从引擎卸载 / Unload from engine
|
||||
if (this._engineBridge) {
|
||||
this._engineBridge.unloadTexture(textureId);
|
||||
}
|
||||
|
||||
// 清理映射 / Clean up mappings
|
||||
for (const [path, id] of this._pathToTextureId.entries()) {
|
||||
if (id === textureId) {
|
||||
this._pathToTextureId.delete(path);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (const [guid, id] of this._textureIdMap.entries()) {
|
||||
if (id === textureId) {
|
||||
this._textureIdMap.delete(guid);
|
||||
// 也从资产管理器卸载 / Also unload from asset manager
|
||||
this._assetManager.unloadAsset(guid);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get texture ID for path
|
||||
* 获取路径的纹理ID
|
||||
*/
|
||||
getTextureId(path: string): number | null {
|
||||
return this._pathToTextureId.get(path) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Preload textures for scene
|
||||
* 为场景预加载纹理
|
||||
*/
|
||||
async preloadSceneTextures(texturePaths: string[]): Promise<void> {
|
||||
await this.loadTexturesBatch(texturePaths);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all texture mappings
|
||||
* 清空所有纹理映射
|
||||
*/
|
||||
clearTextureMappings(): void {
|
||||
this._textureIdMap.clear();
|
||||
this._pathToTextureId.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get statistics
|
||||
* 获取统计信息
|
||||
*/
|
||||
getStatistics(): {
|
||||
loadedTextures: number;
|
||||
} {
|
||||
return {
|
||||
loadedTextures: this._pathToTextureId.size
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,258 +0,0 @@
|
||||
/**
|
||||
* Asset loader interfaces
|
||||
* 资产加载器接口
|
||||
*/
|
||||
|
||||
import {
|
||||
AssetType,
|
||||
AssetGUID,
|
||||
IAssetLoadOptions,
|
||||
IAssetMetadata
|
||||
} from '../types/AssetTypes';
|
||||
import type { IAssetContent, AssetContentType } from './IAssetReader';
|
||||
|
||||
/**
|
||||
* Parse context provided to loaders.
|
||||
* 提供给加载器的解析上下文。
|
||||
*/
|
||||
export interface IAssetParseContext {
|
||||
/** Asset metadata. | 资产元数据。 */
|
||||
metadata: IAssetMetadata;
|
||||
/** Load options. | 加载选项。 */
|
||||
options?: IAssetLoadOptions;
|
||||
/**
|
||||
* Load a dependency asset by relative path.
|
||||
* 通过相对路径加载依赖资产。
|
||||
*/
|
||||
loadDependency<D = unknown>(relativePath: string): Promise<D>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset loader interface.
|
||||
* 资产加载器接口。
|
||||
*
|
||||
* Loaders only parse content, file reading is handled by AssetManager.
|
||||
* 加载器只负责解析内容,文件读取由 AssetManager 处理。
|
||||
*/
|
||||
export interface IAssetLoader<T = unknown> {
|
||||
/** Supported asset type. | 支持的资产类型。 */
|
||||
readonly supportedType: AssetType;
|
||||
|
||||
/** Supported file extensions. | 支持的文件扩展名。 */
|
||||
readonly supportedExtensions: string[];
|
||||
|
||||
/**
|
||||
* Required content type for this loader.
|
||||
* 此加载器需要的内容类型。
|
||||
*
|
||||
* - 'text': For JSON, shader, material files
|
||||
* - 'binary': For binary formats
|
||||
* - 'image': For textures
|
||||
* - 'audio': For audio files
|
||||
*/
|
||||
readonly contentType: AssetContentType;
|
||||
|
||||
/**
|
||||
* Parse asset from content.
|
||||
* 从内容解析资产。
|
||||
*
|
||||
* @param content - File content. | 文件内容。
|
||||
* @param context - Parse context. | 解析上下文。
|
||||
* @returns Parsed asset. | 解析后的资产。
|
||||
*/
|
||||
parse(content: IAssetContent, context: IAssetParseContext): Promise<T>;
|
||||
|
||||
/**
|
||||
* Dispose loaded asset and free resources.
|
||||
* 释放已加载的资产。
|
||||
*/
|
||||
dispose(asset: T): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset loader factory interface
|
||||
* 资产加载器工厂接口
|
||||
*/
|
||||
export interface IAssetLoaderFactory {
|
||||
/**
|
||||
* Create loader for specific asset type
|
||||
* 为特定资产类型创建加载器
|
||||
*/
|
||||
createLoader(type: AssetType): IAssetLoader | null;
|
||||
|
||||
/**
|
||||
* Register custom loader
|
||||
* 注册自定义加载器
|
||||
*/
|
||||
registerLoader(type: AssetType, loader: IAssetLoader): void;
|
||||
|
||||
/**
|
||||
* Unregister loader
|
||||
* 注销加载器
|
||||
*/
|
||||
unregisterLoader(type: AssetType): void;
|
||||
|
||||
/**
|
||||
* Check if loader exists for type
|
||||
* 检查类型是否有加载器
|
||||
*/
|
||||
hasLoader(type: AssetType): boolean;
|
||||
|
||||
/**
|
||||
* Get asset type by file extension
|
||||
* 根据文件扩展名获取资产类型
|
||||
*/
|
||||
getAssetTypeByExtension(extension: string): AssetType | null;
|
||||
|
||||
/**
|
||||
* Get asset type by file path
|
||||
* 根据文件路径获取资产类型
|
||||
*/
|
||||
getAssetTypeByPath(path: string): AssetType | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Texture asset interface
|
||||
* 纹理资产接口
|
||||
*/
|
||||
export interface ITextureAsset {
|
||||
/** WebGL纹理ID / WebGL texture ID */
|
||||
textureId: number;
|
||||
/** 宽度 / Width */
|
||||
width: number;
|
||||
/** 高度 / Height */
|
||||
height: number;
|
||||
/** 格式 / Format */
|
||||
format: 'rgba' | 'rgb' | 'alpha';
|
||||
/** 是否有Mipmap / Has mipmaps */
|
||||
hasMipmaps: boolean;
|
||||
/** 原始数据(如果可用) / Raw image data if available */
|
||||
data?: ImageData | HTMLImageElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mesh asset interface
|
||||
* 网格资产接口
|
||||
*/
|
||||
export interface IMeshAsset {
|
||||
/** 顶点数据 / Vertex data */
|
||||
vertices: Float32Array;
|
||||
/** 索引数据 / Index data */
|
||||
indices: Uint16Array | Uint32Array;
|
||||
/** 法线数据 / Normal data */
|
||||
normals?: Float32Array;
|
||||
/** UV坐标 / UV coordinates */
|
||||
uvs?: Float32Array;
|
||||
/** 切线数据 / Tangent data */
|
||||
tangents?: Float32Array;
|
||||
/** 边界盒 / Axis-aligned bounding box */
|
||||
bounds: {
|
||||
min: [number, number, number];
|
||||
max: [number, number, number];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Audio asset interface
|
||||
* 音频资产接口
|
||||
*/
|
||||
export interface IAudioAsset {
|
||||
/** 音频缓冲区 / Audio buffer */
|
||||
buffer: AudioBuffer;
|
||||
/** 时长(秒) / Duration in seconds */
|
||||
duration: number;
|
||||
/** 采样率 / Sample rate */
|
||||
sampleRate: number;
|
||||
/** 声道数 / Number of channels */
|
||||
channels: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Material asset interface
|
||||
* 材质资产接口
|
||||
*/
|
||||
export interface IMaterialAsset {
|
||||
/** 着色器名称 / Shader name */
|
||||
shader: string;
|
||||
/** 材质属性 / Material properties */
|
||||
properties: Map<string, unknown>;
|
||||
/** 纹理映射 / Texture slot mappings */
|
||||
textures: Map<string, AssetGUID>;
|
||||
/** 渲染状态 / Render states */
|
||||
renderStates: {
|
||||
cullMode?: 'none' | 'front' | 'back';
|
||||
blendMode?: 'none' | 'alpha' | 'additive' | 'multiply';
|
||||
depthTest?: boolean;
|
||||
depthWrite?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefab asset interface
|
||||
* 预制体资产接口
|
||||
*/
|
||||
export interface IPrefabAsset {
|
||||
/** 根实体数据 / Serialized entity hierarchy */
|
||||
root: unknown;
|
||||
/** 包含的组件类型 / Component types used in prefab */
|
||||
componentTypes: string[];
|
||||
/** 引用的资产 / All referenced assets */
|
||||
referencedAssets: AssetGUID[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Scene asset interface
|
||||
* 场景资产接口
|
||||
*/
|
||||
export interface ISceneAsset {
|
||||
/** 场景名称 / Scene name */
|
||||
name: string;
|
||||
/** 实体列表 / Serialized entity list */
|
||||
entities: unknown[];
|
||||
/** 场景设置 / Scene settings */
|
||||
settings: {
|
||||
/** 环境光 / Ambient light */
|
||||
ambientLight?: [number, number, number];
|
||||
/** 雾效 / Fog settings */
|
||||
fog?: {
|
||||
enabled: boolean;
|
||||
color: [number, number, number];
|
||||
density: number;
|
||||
};
|
||||
/** 天空盒 / Skybox asset */
|
||||
skybox?: AssetGUID;
|
||||
};
|
||||
/** 引用的资产 / All referenced assets */
|
||||
referencedAssets: AssetGUID[];
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON asset interface
|
||||
* JSON资产接口
|
||||
*/
|
||||
export interface IJsonAsset {
|
||||
/** JSON数据 / JSON data */
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Text asset interface
|
||||
* 文本资产接口
|
||||
*/
|
||||
export interface ITextAsset {
|
||||
/** 文本内容 / Text content */
|
||||
content: string;
|
||||
/** 编码格式 / Encoding */
|
||||
encoding: 'utf8' | 'utf16' | 'ascii';
|
||||
}
|
||||
|
||||
/**
|
||||
* Binary asset interface
|
||||
* 二进制资产接口
|
||||
*/
|
||||
export interface IBinaryAsset {
|
||||
/** 二进制数据 / Binary data */
|
||||
data: ArrayBuffer;
|
||||
/** MIME类型 / MIME type */
|
||||
mimeType?: string;
|
||||
}
|
||||
@@ -1,334 +0,0 @@
|
||||
/**
|
||||
* Asset manager interfaces
|
||||
* 资产管理器接口
|
||||
*/
|
||||
|
||||
import {
|
||||
AssetGUID,
|
||||
AssetHandle,
|
||||
AssetType,
|
||||
AssetState,
|
||||
IAssetLoadOptions,
|
||||
IAssetLoadResult,
|
||||
IAssetReferenceInfo,
|
||||
IAssetPreloadGroup,
|
||||
IAssetLoadProgress
|
||||
} from '../types/AssetTypes';
|
||||
import { IAssetLoader } from './IAssetLoader';
|
||||
|
||||
/**
|
||||
* Asset manager interface
|
||||
* 资产管理器接口
|
||||
*/
|
||||
export interface IAssetManager {
|
||||
/**
|
||||
* Load asset by GUID
|
||||
* 通过GUID加载资产
|
||||
*/
|
||||
loadAsset<T = unknown>(
|
||||
guid: AssetGUID,
|
||||
options?: IAssetLoadOptions
|
||||
): Promise<IAssetLoadResult<T>>;
|
||||
|
||||
/**
|
||||
* Load asset by path
|
||||
* 通过路径加载资产
|
||||
*/
|
||||
loadAssetByPath<T = unknown>(
|
||||
path: string,
|
||||
options?: IAssetLoadOptions
|
||||
): Promise<IAssetLoadResult<T>>;
|
||||
|
||||
/**
|
||||
* Load multiple assets
|
||||
* 批量加载资产
|
||||
*/
|
||||
loadAssets(
|
||||
guids: AssetGUID[],
|
||||
options?: IAssetLoadOptions
|
||||
): Promise<Map<AssetGUID, IAssetLoadResult>>;
|
||||
|
||||
/**
|
||||
* Preload asset group
|
||||
* 预加载资产组
|
||||
*/
|
||||
preloadGroup(
|
||||
group: IAssetPreloadGroup,
|
||||
onProgress?: (progress: IAssetLoadProgress) => void
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Get loaded asset
|
||||
* 获取已加载的资产
|
||||
*/
|
||||
getAsset<T = unknown>(guid: AssetGUID): T | null;
|
||||
|
||||
/**
|
||||
* Get asset by handle
|
||||
* 通过句柄获取资产
|
||||
*/
|
||||
getAssetByHandle<T = unknown>(handle: AssetHandle): T | null;
|
||||
|
||||
/**
|
||||
* Get loaded asset by path (synchronous)
|
||||
* 通过路径获取已加载的资产(同步)
|
||||
*/
|
||||
getAssetByPath<T = unknown>(path: string): T | null;
|
||||
|
||||
/**
|
||||
* Check if asset is loaded
|
||||
* 检查资产是否已加载
|
||||
*/
|
||||
isLoaded(guid: AssetGUID): boolean;
|
||||
|
||||
/**
|
||||
* Get asset state
|
||||
* 获取资产状态
|
||||
*/
|
||||
getAssetState(guid: AssetGUID): AssetState;
|
||||
|
||||
/**
|
||||
* Unload asset
|
||||
* 卸载资产
|
||||
*/
|
||||
unloadAsset(guid: AssetGUID): void;
|
||||
|
||||
/**
|
||||
* Unload all assets
|
||||
* 卸载所有资产
|
||||
*/
|
||||
unloadAllAssets(): void;
|
||||
|
||||
/**
|
||||
* Unload unused assets
|
||||
* 卸载未使用的资产
|
||||
*/
|
||||
unloadUnusedAssets(): void;
|
||||
|
||||
/**
|
||||
* Add reference to asset
|
||||
* 增加资产引用
|
||||
*/
|
||||
addReference(guid: AssetGUID): void;
|
||||
|
||||
/**
|
||||
* Remove reference from asset
|
||||
* 移除资产引用
|
||||
*/
|
||||
removeReference(guid: AssetGUID): void;
|
||||
|
||||
/**
|
||||
* Get reference info
|
||||
* 获取引用信息
|
||||
*/
|
||||
getReferenceInfo(guid: AssetGUID): IAssetReferenceInfo | null;
|
||||
|
||||
/**
|
||||
* Register custom loader
|
||||
* 注册自定义加载器
|
||||
*/
|
||||
registerLoader(type: AssetType, loader: IAssetLoader): void;
|
||||
|
||||
/**
|
||||
* Get asset statistics
|
||||
* 获取资产统计信息
|
||||
*/
|
||||
getStatistics(): {
|
||||
loadedCount: number;
|
||||
loadQueue: number;
|
||||
failedCount: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear cache
|
||||
* 清空缓存
|
||||
*/
|
||||
clearCache(): void;
|
||||
|
||||
/**
|
||||
* Dispose manager
|
||||
* 释放管理器
|
||||
*/
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset cache interface
|
||||
* 资产缓存接口
|
||||
*/
|
||||
export interface IAssetCache {
|
||||
/**
|
||||
* Get cached asset
|
||||
* 获取缓存的资产
|
||||
*/
|
||||
get<T = unknown>(guid: AssetGUID): T | null;
|
||||
|
||||
/**
|
||||
* Set cached asset
|
||||
* 设置缓存的资产
|
||||
*/
|
||||
set<T = unknown>(guid: AssetGUID, asset: T, size: number): void;
|
||||
|
||||
/**
|
||||
* Check if asset is cached
|
||||
* 检查资产是否已缓存
|
||||
*/
|
||||
has(guid: AssetGUID): boolean;
|
||||
|
||||
/**
|
||||
* Remove from cache
|
||||
* 从缓存中移除
|
||||
*/
|
||||
remove(guid: AssetGUID): void;
|
||||
|
||||
/**
|
||||
* Clear all cache
|
||||
* 清空所有缓存
|
||||
*/
|
||||
clear(): void;
|
||||
|
||||
/**
|
||||
* Get cache size
|
||||
* 获取缓存大小
|
||||
*/
|
||||
getSize(): number;
|
||||
|
||||
/**
|
||||
* Get cached asset count
|
||||
* 获取缓存资产数量
|
||||
*/
|
||||
getCount(): number;
|
||||
|
||||
/**
|
||||
* Evict assets based on policy
|
||||
* 根据策略驱逐资产
|
||||
*/
|
||||
evict(targetSize: number): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset loading queue interface
|
||||
* 资产加载队列接口
|
||||
*/
|
||||
export interface IAssetLoadQueue {
|
||||
/**
|
||||
* Add to queue
|
||||
* 添加到队列
|
||||
*/
|
||||
enqueue(
|
||||
guid: AssetGUID,
|
||||
priority: number,
|
||||
options?: IAssetLoadOptions
|
||||
): void;
|
||||
|
||||
/**
|
||||
* Remove from queue
|
||||
* 从队列移除
|
||||
*/
|
||||
dequeue(): {
|
||||
guid: AssetGUID;
|
||||
options?: IAssetLoadOptions;
|
||||
} | null;
|
||||
|
||||
/**
|
||||
* Check if queue is empty
|
||||
* 检查队列是否为空
|
||||
*/
|
||||
isEmpty(): boolean;
|
||||
|
||||
/**
|
||||
* Get queue size
|
||||
* 获取队列大小
|
||||
*/
|
||||
getSize(): number;
|
||||
|
||||
/**
|
||||
* Clear queue
|
||||
* 清空队列
|
||||
*/
|
||||
clear(): void;
|
||||
|
||||
/**
|
||||
* Reprioritize item
|
||||
* 重新设置优先级
|
||||
*/
|
||||
reprioritize(guid: AssetGUID, newPriority: number): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset dependency resolver interface
|
||||
* 资产依赖解析器接口
|
||||
*/
|
||||
export interface IAssetDependencyResolver {
|
||||
/**
|
||||
* Resolve dependencies for asset
|
||||
* 解析资产的依赖
|
||||
*/
|
||||
resolveDependencies(guid: AssetGUID): Promise<AssetGUID[]>;
|
||||
|
||||
/**
|
||||
* Get direct dependencies
|
||||
* 获取直接依赖
|
||||
*/
|
||||
getDirectDependencies(guid: AssetGUID): AssetGUID[];
|
||||
|
||||
/**
|
||||
* Get all dependencies recursively
|
||||
* 递归获取所有依赖
|
||||
*/
|
||||
getAllDependencies(guid: AssetGUID): AssetGUID[];
|
||||
|
||||
/**
|
||||
* Check for circular dependencies
|
||||
* 检查循环依赖
|
||||
*/
|
||||
hasCircularDependency(guid: AssetGUID): boolean;
|
||||
|
||||
/**
|
||||
* Build dependency graph
|
||||
* 构建依赖图
|
||||
*/
|
||||
buildDependencyGraph(guids: AssetGUID[]): Map<AssetGUID, AssetGUID[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset streaming interface
|
||||
* 资产流式加载接口
|
||||
*/
|
||||
export interface IAssetStreaming {
|
||||
/**
|
||||
* Start streaming assets
|
||||
* 开始流式加载资产
|
||||
*/
|
||||
startStreaming(guids: AssetGUID[]): void;
|
||||
|
||||
/**
|
||||
* Stop streaming
|
||||
* 停止流式加载
|
||||
*/
|
||||
stopStreaming(): void;
|
||||
|
||||
/**
|
||||
* Pause streaming
|
||||
* 暂停流式加载
|
||||
*/
|
||||
pauseStreaming(): void;
|
||||
|
||||
/**
|
||||
* Resume streaming
|
||||
* 恢复流式加载
|
||||
*/
|
||||
resumeStreaming(): void;
|
||||
|
||||
/**
|
||||
* Set streaming budget per frame
|
||||
* 设置每帧流式加载预算
|
||||
*/
|
||||
setFrameBudget(milliseconds: number): void;
|
||||
|
||||
/**
|
||||
* Get streaming progress
|
||||
* 获取流式加载进度
|
||||
*/
|
||||
getProgress(): IAssetLoadProgress;
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
/**
|
||||
* Asset Reader Interface
|
||||
* 资产读取器接口
|
||||
*
|
||||
* Provides unified file reading abstraction across different platforms.
|
||||
* 提供跨平台的统一文件读取抽象。
|
||||
*/
|
||||
|
||||
/**
|
||||
* Asset content types.
|
||||
* 资产内容类型。
|
||||
*/
|
||||
export type AssetContentType = 'text' | 'binary' | 'image' | 'audio';
|
||||
|
||||
/**
|
||||
* Asset content result.
|
||||
* 资产内容结果。
|
||||
*/
|
||||
export interface IAssetContent {
|
||||
/** Content type. | 内容类型。 */
|
||||
type: AssetContentType;
|
||||
/** Text content (for text/json files). | 文本内容。 */
|
||||
text?: string;
|
||||
/** Binary content. | 二进制内容。 */
|
||||
binary?: ArrayBuffer;
|
||||
/** Image element (for textures). | 图片元素。 */
|
||||
image?: HTMLImageElement;
|
||||
/** Audio buffer (for audio files). | 音频缓冲区。 */
|
||||
audioBuffer?: AudioBuffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset reader interface.
|
||||
* 资产读取器接口。
|
||||
*
|
||||
* Abstracts platform-specific file reading operations.
|
||||
* 抽象平台特定的文件读取操作。
|
||||
*/
|
||||
export interface IAssetReader {
|
||||
/**
|
||||
* Read file as text.
|
||||
* 读取文件为文本。
|
||||
*
|
||||
* @param absolutePath - Absolute file path. | 绝对文件路径。
|
||||
* @returns Text content. | 文本内容。
|
||||
*/
|
||||
readText(absolutePath: string): Promise<string>;
|
||||
|
||||
/**
|
||||
* Read file as binary.
|
||||
* 读取文件为二进制。
|
||||
*
|
||||
* @param absolutePath - Absolute file path. | 绝对文件路径。
|
||||
* @returns Binary content. | 二进制内容。
|
||||
*/
|
||||
readBinary(absolutePath: string): Promise<ArrayBuffer>;
|
||||
|
||||
/**
|
||||
* Load image from file.
|
||||
* 从文件加载图片。
|
||||
*
|
||||
* @param absolutePath - Absolute file path. | 绝对文件路径。
|
||||
* @returns Image element. | 图片元素。
|
||||
*/
|
||||
loadImage(absolutePath: string): Promise<HTMLImageElement>;
|
||||
|
||||
/**
|
||||
* Load audio from file.
|
||||
* 从文件加载音频。
|
||||
*
|
||||
* @param absolutePath - Absolute file path. | 绝对文件路径。
|
||||
* @returns Audio buffer. | 音频缓冲区。
|
||||
*/
|
||||
loadAudio(absolutePath: string): Promise<AudioBuffer>;
|
||||
|
||||
/**
|
||||
* Check if file exists.
|
||||
* 检查文件是否存在。
|
||||
*
|
||||
* @param absolutePath - Absolute file path. | 绝对文件路径。
|
||||
* @returns True if exists. | 是否存在。
|
||||
*/
|
||||
exists(absolutePath: string): Promise<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Service identifier for IAssetReader.
|
||||
* IAssetReader 的服务标识符。
|
||||
*/
|
||||
export const IAssetReaderService = Symbol.for('IAssetReaderService');
|
||||
@@ -1,62 +0,0 @@
|
||||
/**
|
||||
* 资源组件接口 - 用于依赖运行时资源的组件(纹理、音频等)
|
||||
* Interface for components that depend on runtime resources (textures, audio, etc.)
|
||||
*
|
||||
* 实现此接口的组件可以参与 SceneResourceManager 管理的集中式资源加载
|
||||
* Components implementing this interface can participate in centralized resource loading managed by SceneResourceManager
|
||||
*/
|
||||
|
||||
/**
|
||||
* 资源引用 - 包含路径和运行时 ID
|
||||
* Resource reference with path and runtime ID
|
||||
*/
|
||||
export interface ResourceReference {
|
||||
/** 资源路径(例如 "assets/sprites/player.png")/ Asset path (e.g., "assets/sprites/player.png") */
|
||||
path: string;
|
||||
/** 引擎分配的运行时资源 ID(例如 GPU 上的纹理 ID)/ Runtime resource ID assigned by engine (e.g., texture ID on GPU) */
|
||||
runtimeId?: number;
|
||||
/** 资源类型标识符 / Resource type identifier */
|
||||
type: 'texture' | 'audio' | 'font' | 'data';
|
||||
}
|
||||
|
||||
/**
|
||||
* 资源组件接口
|
||||
* Resource component interface
|
||||
*
|
||||
* 实现此接口的组件可以在场景启动前由 SceneResourceManager 集中加载资源
|
||||
* Components implementing this interface can have their resources loaded centrally by SceneResourceManager before the scene starts
|
||||
*/
|
||||
export interface IResourceComponent {
|
||||
/**
|
||||
* 获取此组件需要的所有资源引用
|
||||
* Get all resource references needed by this component
|
||||
*
|
||||
* 在场景加载期间调用以收集资源路径
|
||||
* Called during scene loading to collect resource paths
|
||||
*/
|
||||
getResourceReferences(): ResourceReference[];
|
||||
|
||||
/**
|
||||
* 设置已加载资源的运行时 ID
|
||||
* Set runtime IDs for loaded resources
|
||||
*
|
||||
* 在 SceneResourceManager 加载资源后调用
|
||||
* Called after resources are loaded by SceneResourceManager
|
||||
*
|
||||
* @param pathToId 资源路径到运行时 ID 的映射 / Map of resource paths to runtime IDs
|
||||
*/
|
||||
setResourceIds(pathToId: Map<string, number>): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型守卫 - 检查组件是否实现了 IResourceComponent
|
||||
* Type guard to check if a component implements IResourceComponent
|
||||
*/
|
||||
export function isResourceComponent(component: any): component is IResourceComponent {
|
||||
return (
|
||||
component !== null &&
|
||||
typeof component === 'object' &&
|
||||
typeof component.getResourceReferences === 'function' &&
|
||||
typeof component.setResourceIds === 'function'
|
||||
);
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
/**
|
||||
* Asset loader factory implementation
|
||||
* 资产加载器工厂实现
|
||||
*/
|
||||
|
||||
import { AssetType } from '../types/AssetTypes';
|
||||
import { IAssetLoader, IAssetLoaderFactory } from '../interfaces/IAssetLoader';
|
||||
import { TextureLoader } from './TextureLoader';
|
||||
import { JsonLoader } from './JsonLoader';
|
||||
import { TextLoader } from './TextLoader';
|
||||
import { BinaryLoader } from './BinaryLoader';
|
||||
|
||||
/**
|
||||
* Asset loader factory
|
||||
* 资产加载器工厂
|
||||
*/
|
||||
export class AssetLoaderFactory implements IAssetLoaderFactory {
|
||||
private readonly _loaders = new Map<AssetType, IAssetLoader>();
|
||||
|
||||
constructor() {
|
||||
// 注册默认加载器 / Register default loaders
|
||||
this.registerDefaultLoaders();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register default loaders
|
||||
* 注册默认加载器
|
||||
*/
|
||||
private registerDefaultLoaders(): void {
|
||||
// 纹理加载器 / Texture loader
|
||||
this._loaders.set(AssetType.Texture, new TextureLoader());
|
||||
|
||||
// JSON加载器 / JSON loader
|
||||
this._loaders.set(AssetType.Json, new JsonLoader());
|
||||
|
||||
// 文本加载器 / Text loader
|
||||
this._loaders.set(AssetType.Text, new TextLoader());
|
||||
|
||||
// 二进制加载器 / Binary loader
|
||||
this._loaders.set(AssetType.Binary, new BinaryLoader());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create loader for specific asset type
|
||||
* 为特定资产类型创建加载器
|
||||
*/
|
||||
createLoader(type: AssetType): IAssetLoader | null {
|
||||
return this._loaders.get(type) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register custom loader
|
||||
* 注册自定义加载器
|
||||
*/
|
||||
registerLoader(type: AssetType, loader: IAssetLoader): void {
|
||||
this._loaders.set(type, loader);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister loader
|
||||
* 注销加载器
|
||||
*/
|
||||
unregisterLoader(type: AssetType): void {
|
||||
this._loaders.delete(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if loader exists for type
|
||||
* 检查类型是否有加载器
|
||||
*/
|
||||
hasLoader(type: AssetType): boolean {
|
||||
return this._loaders.has(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get asset type by file extension
|
||||
* 根据文件扩展名获取资产类型
|
||||
*
|
||||
* @param extension - File extension including dot (e.g., '.btree', '.png')
|
||||
* @returns Asset type if a loader supports this extension, null otherwise
|
||||
*/
|
||||
getAssetTypeByExtension(extension: string): AssetType | null {
|
||||
const ext = extension.toLowerCase();
|
||||
for (const [type, loader] of this._loaders) {
|
||||
if (loader.supportedExtensions.some(e => e.toLowerCase() === ext)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get asset type by file path
|
||||
* 根据文件路径获取资产类型
|
||||
*
|
||||
* Checks for compound extensions (like .tilemap.json) first, then simple extensions
|
||||
*
|
||||
* @param path - File path
|
||||
* @returns Asset type if a loader supports this file, null otherwise
|
||||
*/
|
||||
getAssetTypeByPath(path: string): AssetType | null {
|
||||
const lowerPath = path.toLowerCase();
|
||||
|
||||
// First check compound extensions (e.g., .tilemap.json)
|
||||
for (const [type, loader] of this._loaders) {
|
||||
for (const ext of loader.supportedExtensions) {
|
||||
if (ext.includes('.') && ext.split('.').length > 2) {
|
||||
// This is a compound extension like .tilemap.json
|
||||
if (lowerPath.endsWith(ext.toLowerCase())) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Then check simple extensions
|
||||
const lastDot = path.lastIndexOf('.');
|
||||
if (lastDot !== -1) {
|
||||
const ext = path.substring(lastDot).toLowerCase();
|
||||
return this.getAssetTypeByExtension(ext);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered loaders
|
||||
* 获取所有注册的加载器
|
||||
*/
|
||||
getRegisteredTypes(): AssetType[] {
|
||||
return Array.from(this._loaders.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all loaders
|
||||
* 清空所有加载器
|
||||
*/
|
||||
clear(): void {
|
||||
this._loaders.clear();
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
/**
|
||||
* Binary asset loader
|
||||
* 二进制资产加载器
|
||||
*/
|
||||
|
||||
import { AssetType } from '../types/AssetTypes';
|
||||
import { IAssetLoader, IBinaryAsset, IAssetParseContext } from '../interfaces/IAssetLoader';
|
||||
import { IAssetContent, AssetContentType } from '../interfaces/IAssetReader';
|
||||
|
||||
/**
|
||||
* Binary loader implementation
|
||||
* 二进制加载器实现
|
||||
*/
|
||||
export class BinaryLoader implements IAssetLoader<IBinaryAsset> {
|
||||
readonly supportedType = AssetType.Binary;
|
||||
readonly supportedExtensions = [
|
||||
'.bin', '.dat', '.raw', '.bytes',
|
||||
'.wasm', '.so', '.dll', '.dylib'
|
||||
];
|
||||
readonly contentType: AssetContentType = 'binary';
|
||||
|
||||
/**
|
||||
* Parse binary from content.
|
||||
* 从内容解析二进制。
|
||||
*/
|
||||
async parse(content: IAssetContent, _context: IAssetParseContext): Promise<IBinaryAsset> {
|
||||
if (!content.binary) {
|
||||
throw new Error('Binary content is empty');
|
||||
}
|
||||
|
||||
return {
|
||||
data: content.binary
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose loaded asset
|
||||
* 释放已加载的资产
|
||||
*/
|
||||
dispose(asset: IBinaryAsset): void {
|
||||
(asset as any).data = null;
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
/**
|
||||
* JSON asset loader
|
||||
* JSON资产加载器
|
||||
*/
|
||||
|
||||
import { AssetType } from '../types/AssetTypes';
|
||||
import { IAssetLoader, IJsonAsset, IAssetParseContext } from '../interfaces/IAssetLoader';
|
||||
import { IAssetContent, AssetContentType } from '../interfaces/IAssetReader';
|
||||
|
||||
/**
|
||||
* JSON loader implementation
|
||||
* JSON加载器实现
|
||||
*/
|
||||
export class JsonLoader implements IAssetLoader<IJsonAsset> {
|
||||
readonly supportedType = AssetType.Json;
|
||||
readonly supportedExtensions = ['.json', '.jsonc'];
|
||||
readonly contentType: AssetContentType = 'text';
|
||||
|
||||
/**
|
||||
* Parse JSON from text content.
|
||||
* 从文本内容解析JSON。
|
||||
*/
|
||||
async parse(content: IAssetContent, _context: IAssetParseContext): Promise<IJsonAsset> {
|
||||
if (!content.text) {
|
||||
throw new Error('JSON content is empty');
|
||||
}
|
||||
|
||||
return {
|
||||
data: JSON.parse(content.text)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose loaded asset
|
||||
* 释放已加载的资产
|
||||
*/
|
||||
dispose(asset: IJsonAsset): void {
|
||||
(asset as any).data = null;
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
/**
|
||||
* Text asset loader
|
||||
* 文本资产加载器
|
||||
*/
|
||||
|
||||
import { AssetType } from '../types/AssetTypes';
|
||||
import { IAssetLoader, ITextAsset, IAssetParseContext } from '../interfaces/IAssetLoader';
|
||||
import { IAssetContent, AssetContentType } from '../interfaces/IAssetReader';
|
||||
|
||||
/**
|
||||
* Text loader implementation
|
||||
* 文本加载器实现
|
||||
*/
|
||||
export class TextLoader implements IAssetLoader<ITextAsset> {
|
||||
readonly supportedType = AssetType.Text;
|
||||
readonly supportedExtensions = ['.txt', '.text', '.md', '.csv', '.xml', '.html', '.css', '.js', '.ts'];
|
||||
readonly contentType: AssetContentType = 'text';
|
||||
|
||||
/**
|
||||
* Parse text from content.
|
||||
* 从内容解析文本。
|
||||
*/
|
||||
async parse(content: IAssetContent, _context: IAssetParseContext): Promise<ITextAsset> {
|
||||
if (!content.text) {
|
||||
throw new Error('Text content is empty');
|
||||
}
|
||||
|
||||
return {
|
||||
content: content.text,
|
||||
encoding: this.detectEncoding(content.text)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect text encoding
|
||||
* 检测文本编码
|
||||
*/
|
||||
private detectEncoding(content: string): 'utf8' | 'utf16' | 'ascii' {
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
const charCode = content.charCodeAt(i);
|
||||
if (charCode > 127) {
|
||||
return charCode > 255 ? 'utf16' : 'utf8';
|
||||
}
|
||||
}
|
||||
return 'ascii';
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose loaded asset
|
||||
* 释放已加载的资产
|
||||
*/
|
||||
dispose(asset: ITextAsset): void {
|
||||
(asset as any).content = '';
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
/**
|
||||
* Texture asset loader
|
||||
* 纹理资产加载器
|
||||
*/
|
||||
|
||||
import { AssetType } from '../types/AssetTypes';
|
||||
import { IAssetLoader, ITextureAsset, IAssetParseContext } from '../interfaces/IAssetLoader';
|
||||
import { IAssetContent, AssetContentType } from '../interfaces/IAssetReader';
|
||||
|
||||
/**
|
||||
* Texture loader implementation
|
||||
* 纹理加载器实现
|
||||
*/
|
||||
export class TextureLoader implements IAssetLoader<ITextureAsset> {
|
||||
readonly supportedType = AssetType.Texture;
|
||||
readonly supportedExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp'];
|
||||
readonly contentType: AssetContentType = 'image';
|
||||
|
||||
private static _nextTextureId = 1;
|
||||
|
||||
/**
|
||||
* Parse texture from image content.
|
||||
* 从图片内容解析纹理。
|
||||
*/
|
||||
async parse(content: IAssetContent, context: IAssetParseContext): Promise<ITextureAsset> {
|
||||
if (!content.image) {
|
||||
throw new Error('Texture content is empty');
|
||||
}
|
||||
|
||||
const image = content.image;
|
||||
|
||||
const textureAsset: ITextureAsset = {
|
||||
textureId: TextureLoader._nextTextureId++,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
format: 'rgba',
|
||||
hasMipmaps: false,
|
||||
data: image
|
||||
};
|
||||
|
||||
// Upload to GPU if bridge exists.
|
||||
if (typeof window !== 'undefined' && (window as any).engineBridge) {
|
||||
await this.uploadToGPU(textureAsset, context.metadata.path);
|
||||
}
|
||||
|
||||
return textureAsset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload texture to GPU
|
||||
* 上传纹理到GPU
|
||||
*/
|
||||
private async uploadToGPU(textureAsset: ITextureAsset, path: string): Promise<void> {
|
||||
const bridge = (window as any).engineBridge;
|
||||
if (bridge && bridge.loadTexture) {
|
||||
await bridge.loadTexture(textureAsset.textureId, path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose loaded asset
|
||||
* 释放已加载的资产
|
||||
*/
|
||||
dispose(asset: ITextureAsset): void {
|
||||
// Release GPU resources.
|
||||
if (typeof window !== 'undefined' && (window as any).engineBridge) {
|
||||
const bridge = (window as any).engineBridge;
|
||||
if (bridge.unloadTexture) {
|
||||
bridge.unloadTexture(asset.textureId);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up image data.
|
||||
if (asset.data instanceof HTMLImageElement) {
|
||||
asset.data.src = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,275 +0,0 @@
|
||||
/**
|
||||
* Runtime Catalog for Asset Resolution
|
||||
* 资产解析的运行时目录
|
||||
*
|
||||
* Provides GUID-based asset lookup at runtime.
|
||||
* 提供运行时基于 GUID 的资产查找。
|
||||
*/
|
||||
|
||||
import { AssetGUID, AssetType } from '../types/AssetTypes';
|
||||
import {
|
||||
IRuntimeCatalog,
|
||||
IRuntimeAssetLocation,
|
||||
IRuntimeBundleInfo
|
||||
} from '../bundle/BundleFormat';
|
||||
|
||||
/**
|
||||
* Runtime Catalog Manager
|
||||
* 运行时目录管理器
|
||||
*
|
||||
* Loads and manages the asset catalog for runtime GUID resolution.
|
||||
*/
|
||||
export class RuntimeCatalog {
|
||||
private _catalog: IRuntimeCatalog | null = null;
|
||||
private _loadedBundles = new Map<string, ArrayBuffer>();
|
||||
private _loadingBundles = new Map<string, Promise<ArrayBuffer>>();
|
||||
private _baseUrl: string = './';
|
||||
|
||||
/**
|
||||
* Set base URL for loading catalog and bundles
|
||||
* 设置加载目录和包的基础 URL
|
||||
*/
|
||||
setBaseUrl(url: string): void {
|
||||
this._baseUrl = url.endsWith('/') ? url : `${url}/`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load catalog from URL
|
||||
* 从 URL 加载目录
|
||||
*/
|
||||
async loadCatalog(catalogUrl?: string): Promise<void> {
|
||||
const url = catalogUrl || `${this._baseUrl}asset-catalog.json`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load catalog: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
this._catalog = this._parseCatalog(data);
|
||||
|
||||
console.log(`[RuntimeCatalog] Loaded catalog with ${Object.keys(this._catalog.assets).length} assets`);
|
||||
} catch (error) {
|
||||
console.error('[RuntimeCatalog] Failed to load catalog:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize with pre-loaded catalog data
|
||||
* 使用预加载的目录数据初始化
|
||||
*/
|
||||
initWithData(catalogData: IRuntimeCatalog): void {
|
||||
this._catalog = catalogData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if catalog is loaded
|
||||
* 检查目录是否已加载
|
||||
*/
|
||||
isLoaded(): boolean {
|
||||
return this._catalog !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get asset location by GUID
|
||||
* 根据 GUID 获取资产位置
|
||||
*/
|
||||
getAssetLocation(guid: AssetGUID): IRuntimeAssetLocation | null {
|
||||
if (!this._catalog) {
|
||||
console.warn('[RuntimeCatalog] Catalog not loaded');
|
||||
return null;
|
||||
}
|
||||
|
||||
return this._catalog.assets[guid] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if asset exists in catalog
|
||||
* 检查资产是否存在于目录中
|
||||
*/
|
||||
hasAsset(guid: AssetGUID): boolean {
|
||||
return this._catalog?.assets[guid] !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all assets of a specific type
|
||||
* 获取特定类型的所有资产
|
||||
*/
|
||||
getAssetsByType(type: AssetType): AssetGUID[] {
|
||||
if (!this._catalog) return [];
|
||||
|
||||
return Object.entries(this._catalog.assets)
|
||||
.filter(([_, loc]) => loc.type === type)
|
||||
.map(([guid]) => guid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bundle info
|
||||
* 获取包信息
|
||||
*/
|
||||
getBundleInfo(bundleName: string): IRuntimeBundleInfo | null {
|
||||
return this._catalog?.bundles[bundleName] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a bundle
|
||||
* 加载包
|
||||
*/
|
||||
async loadBundle(bundleName: string): Promise<ArrayBuffer> {
|
||||
// Return cached bundle
|
||||
const cached = this._loadedBundles.get(bundleName);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Return pending load
|
||||
const pending = this._loadingBundles.get(bundleName);
|
||||
if (pending) {
|
||||
return pending;
|
||||
}
|
||||
|
||||
// Start new load
|
||||
const bundleInfo = this.getBundleInfo(bundleName);
|
||||
if (!bundleInfo) {
|
||||
throw new Error(`Bundle not found in catalog: ${bundleName}`);
|
||||
}
|
||||
|
||||
const loadPromise = this._fetchBundle(bundleInfo);
|
||||
this._loadingBundles.set(bundleName, loadPromise);
|
||||
|
||||
try {
|
||||
const data = await loadPromise;
|
||||
this._loadedBundles.set(bundleName, data);
|
||||
return data;
|
||||
} finally {
|
||||
this._loadingBundles.delete(bundleName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load asset data by GUID
|
||||
* 根据 GUID 加载资产数据
|
||||
*/
|
||||
async loadAssetData(guid: AssetGUID): Promise<ArrayBuffer> {
|
||||
const location = this.getAssetLocation(guid);
|
||||
if (!location) {
|
||||
throw new Error(`Asset not found in catalog: ${guid}`);
|
||||
}
|
||||
|
||||
// Load the bundle containing this asset
|
||||
const bundleData = await this.loadBundle(location.bundle);
|
||||
|
||||
// Extract asset data from bundle
|
||||
return bundleData.slice(location.offset, location.offset + location.size);
|
||||
}
|
||||
|
||||
/**
|
||||
* Preload bundles marked for preloading
|
||||
* 预加载标记为预加载的包
|
||||
*/
|
||||
async preloadBundles(): Promise<void> {
|
||||
if (!this._catalog) return;
|
||||
|
||||
const preloadPromises: Promise<void>[] = [];
|
||||
|
||||
for (const [name, info] of Object.entries(this._catalog.bundles)) {
|
||||
if (info.preload) {
|
||||
preloadPromises.push(
|
||||
this.loadBundle(name).then(() => {
|
||||
console.log(`[RuntimeCatalog] Preloaded bundle: ${name}`);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(preloadPromises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unload a bundle from memory
|
||||
* 从内存卸载包
|
||||
*/
|
||||
unloadBundle(bundleName: string): void {
|
||||
this._loadedBundles.delete(bundleName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all loaded bundles
|
||||
* 清除所有已加载的包
|
||||
*/
|
||||
clearBundles(): void {
|
||||
this._loadedBundles.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get catalog statistics
|
||||
* 获取目录统计信息
|
||||
*/
|
||||
getStatistics(): {
|
||||
totalAssets: number;
|
||||
totalBundles: number;
|
||||
loadedBundles: number;
|
||||
assetsByType: Record<string, number>;
|
||||
} {
|
||||
if (!this._catalog) {
|
||||
return {
|
||||
totalAssets: 0,
|
||||
totalBundles: 0,
|
||||
loadedBundles: 0,
|
||||
assetsByType: {}
|
||||
};
|
||||
}
|
||||
|
||||
const assetsByType: Record<string, number> = {};
|
||||
for (const loc of Object.values(this._catalog.assets)) {
|
||||
assetsByType[loc.type] = (assetsByType[loc.type] || 0) + 1;
|
||||
}
|
||||
|
||||
return {
|
||||
totalAssets: Object.keys(this._catalog.assets).length,
|
||||
totalBundles: Object.keys(this._catalog.bundles).length,
|
||||
loadedBundles: this._loadedBundles.size,
|
||||
assetsByType
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse catalog JSON to typed structure
|
||||
* 将目录 JSON 解析为类型化结构
|
||||
*/
|
||||
private _parseCatalog(data: unknown): IRuntimeCatalog {
|
||||
const raw = data as Record<string, unknown>;
|
||||
|
||||
return {
|
||||
version: (raw.version as string) || '1.0',
|
||||
createdAt: (raw.createdAt as number) || Date.now(),
|
||||
bundles: (raw.bundles as Record<string, IRuntimeBundleInfo>) || {},
|
||||
assets: (raw.assets as Record<AssetGUID, IRuntimeAssetLocation>) || {}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch bundle data
|
||||
* 获取包数据
|
||||
*/
|
||||
private async _fetchBundle(info: IRuntimeBundleInfo): Promise<ArrayBuffer> {
|
||||
const url = info.url.startsWith('http')
|
||||
? info.url
|
||||
: `${this._baseUrl}${info.url}`;
|
||||
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load bundle: ${url} (${response.status})`);
|
||||
}
|
||||
|
||||
return response.arrayBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Global runtime catalog instance
|
||||
* 全局运行时目录实例
|
||||
*/
|
||||
export const runtimeCatalog = new RuntimeCatalog();
|
||||
@@ -1,155 +0,0 @@
|
||||
/**
|
||||
* 场景资源管理器 - 集中式场景资源加载
|
||||
* SceneResourceManager - Centralized resource loading for scenes
|
||||
*
|
||||
* 扫描场景中所有组件,收集资源引用,批量加载资源,并将运行时 ID 分配回组件
|
||||
* Scans all components in a scene, collects resource references, batch-loads them, and assigns runtime IDs back to components
|
||||
*/
|
||||
|
||||
import type { Scene } from '@esengine/ecs-framework';
|
||||
import { isResourceComponent, type ResourceReference } from '../interfaces/IResourceComponent';
|
||||
|
||||
/**
|
||||
* 资源加载器接口
|
||||
* Resource loader interface
|
||||
*/
|
||||
export interface IResourceLoader {
|
||||
/**
|
||||
* 批量加载资源并返回路径到 ID 的映射
|
||||
* Load a batch of resources and return path-to-ID mapping
|
||||
* @param paths 资源路径数组 / Array of resource paths
|
||||
* @param type 资源类型 / Resource type
|
||||
* @returns 路径到运行时 ID 的映射 / Map of paths to runtime IDs
|
||||
*/
|
||||
loadResourcesBatch(paths: string[], type: ResourceReference['type']): Promise<Map<string, number>>;
|
||||
}
|
||||
|
||||
export class SceneResourceManager {
|
||||
private resourceLoader: IResourceLoader | null = null;
|
||||
|
||||
/**
|
||||
* 设置资源加载器实现
|
||||
* Set the resource loader implementation
|
||||
*
|
||||
* 应由引擎集成层调用
|
||||
* This should be called by the engine integration layer
|
||||
*
|
||||
* @param loader 资源加载器实例 / Resource loader instance
|
||||
*/
|
||||
setResourceLoader(loader: IResourceLoader): void {
|
||||
this.resourceLoader = loader;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载场景所需的所有资源
|
||||
* Load all resources required by a scene
|
||||
*
|
||||
* 流程 / Process:
|
||||
* 1. 扫描所有实体并从 IResourceComponent 实现中收集资源引用
|
||||
* Scan all entities and collect resource references from IResourceComponent implementations
|
||||
* 2. 按类型分组资源(纹理、音频等)
|
||||
* Group resources by type (texture, audio, etc.)
|
||||
* 3. 批量加载每种资源类型
|
||||
* Batch load each resource type
|
||||
* 4. 将运行时 ID 分配回组件
|
||||
* Assign runtime IDs back to components
|
||||
*
|
||||
* @param scene 要加载资源的场景 / The scene to load resources for
|
||||
* @returns 当所有资源加载完成时解析的 Promise / Promise that resolves when all resources are loaded
|
||||
*/
|
||||
async loadSceneResources(scene: Scene): Promise<void> {
|
||||
if (!this.resourceLoader) {
|
||||
console.warn('[SceneResourceManager] No resource loader set, skipping resource loading');
|
||||
return;
|
||||
}
|
||||
|
||||
// 从组件收集所有资源引用 / Collect all resource references from components
|
||||
const resourceRefs = this.collectResourceReferences(scene);
|
||||
|
||||
if (resourceRefs.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 按资源类型分组 / Group by resource type
|
||||
const resourcesByType = new Map<ResourceReference['type'], Set<string>>();
|
||||
for (const ref of resourceRefs) {
|
||||
if (!resourcesByType.has(ref.type)) {
|
||||
resourcesByType.set(ref.type, new Set());
|
||||
}
|
||||
resourcesByType.get(ref.type)!.add(ref.path);
|
||||
}
|
||||
|
||||
// 批量加载每种资源类型 / Load each resource type in batch
|
||||
const allResourceIds = new Map<string, number>();
|
||||
|
||||
for (const [type, paths] of resourcesByType) {
|
||||
const pathsArray = Array.from(paths);
|
||||
|
||||
try {
|
||||
const resourceIds = await this.resourceLoader.loadResourcesBatch(pathsArray, type);
|
||||
|
||||
// 合并到总映射表 / Merge into combined map
|
||||
for (const [path, id] of resourceIds) {
|
||||
allResourceIds.set(path, id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[SceneResourceManager] Failed to load ${type} resources:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// 将资源 ID 分配回组件 / Assign resource IDs back to components
|
||||
this.assignResourceIds(scene, allResourceIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从场景实体收集所有资源引用
|
||||
* Collect all resource references from scene entities
|
||||
*/
|
||||
private collectResourceReferences(scene: Scene): ResourceReference[] {
|
||||
const refs: ResourceReference[] = [];
|
||||
|
||||
for (const entity of scene.entities.buffer) {
|
||||
for (const component of entity.components) {
|
||||
if (isResourceComponent(component)) {
|
||||
const componentRefs = component.getResourceReferences();
|
||||
refs.push(...componentRefs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return refs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将已加载的资源 ID 分配回组件
|
||||
* Assign loaded resource IDs back to components
|
||||
*
|
||||
* @param scene 场景 / Scene
|
||||
* @param pathToId 路径到 ID 的映射 / Path to ID mapping
|
||||
*/
|
||||
private assignResourceIds(scene: Scene, pathToId: Map<string, number>): void {
|
||||
for (const entity of scene.entities.buffer) {
|
||||
for (const component of entity.components) {
|
||||
if (isResourceComponent(component)) {
|
||||
component.setResourceIds(pathToId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 卸载场景使用的所有资源
|
||||
* Unload all resources used by a scene
|
||||
*
|
||||
* 在场景销毁时调用
|
||||
* Called when a scene is being destroyed
|
||||
*
|
||||
* @param scene 要卸载资源的场景 / The scene to unload resources for
|
||||
*/
|
||||
async unloadSceneResources(_scene: Scene): Promise<void> {
|
||||
// TODO: 实现资源卸载 / Implement resource unloading
|
||||
// 需要跟踪资源引用计数,仅在不再使用时卸载
|
||||
// Need to track resource reference counts and only unload when no longer used
|
||||
console.log('[SceneResourceManager] Scene resource unloading not yet implemented');
|
||||
}
|
||||
}
|
||||
@@ -1,413 +0,0 @@
|
||||
/**
|
||||
* Core asset system types and enums
|
||||
* 核心资产系统类型和枚举
|
||||
*/
|
||||
|
||||
/**
|
||||
* Unique identifier for assets across the project
|
||||
* 项目中资产的唯一标识符
|
||||
*/
|
||||
export type AssetGUID = string;
|
||||
|
||||
/**
|
||||
* Runtime asset handle for efficient access
|
||||
* 运行时资产句柄,用于高效访问
|
||||
*/
|
||||
export type AssetHandle = number;
|
||||
|
||||
/**
|
||||
* Asset loading state
|
||||
* 资产加载状态
|
||||
*/
|
||||
export enum AssetState {
|
||||
/** 未加载 */
|
||||
Unloaded = 'unloaded',
|
||||
/** 加载中 */
|
||||
Loading = 'loading',
|
||||
/** 已加载 */
|
||||
Loaded = 'loaded',
|
||||
/** 加载失败 */
|
||||
Failed = 'failed',
|
||||
/** 释放中 */
|
||||
Disposing = 'disposing'
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset type - string based for extensibility
|
||||
* 资产类型 - 使用字符串以支持插件扩展
|
||||
*
|
||||
* Plugins can define their own asset types by using custom strings.
|
||||
* Built-in types are provided as constants below.
|
||||
* 插件可以通过使用自定义字符串定义自己的资产类型。
|
||||
* 内置类型作为常量提供如下。
|
||||
*/
|
||||
export type AssetType = string;
|
||||
|
||||
/**
|
||||
* Built-in asset types provided by asset-system
|
||||
* asset-system 提供的内置资产类型
|
||||
*/
|
||||
export const AssetType = {
|
||||
/** 纹理 */
|
||||
Texture: 'texture',
|
||||
/** 网格 */
|
||||
Mesh: 'mesh',
|
||||
/** 材质 */
|
||||
Material: 'material',
|
||||
/** 着色器 */
|
||||
Shader: 'shader',
|
||||
/** 音频 */
|
||||
Audio: 'audio',
|
||||
/** 字体 */
|
||||
Font: 'font',
|
||||
/** 预制体 */
|
||||
Prefab: 'prefab',
|
||||
/** 场景 */
|
||||
Scene: 'scene',
|
||||
/** 脚本 */
|
||||
Script: 'script',
|
||||
/** 动画片段 */
|
||||
AnimationClip: 'animation',
|
||||
/** JSON数据 */
|
||||
Json: 'json',
|
||||
/** 文本 */
|
||||
Text: 'text',
|
||||
/** 二进制 */
|
||||
Binary: 'binary',
|
||||
/** 自定义 */
|
||||
Custom: 'custom'
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Platform variants for assets
|
||||
* 资产的平台变体
|
||||
*/
|
||||
export enum AssetPlatform {
|
||||
/** H5平台(浏览器) */
|
||||
H5 = 'h5',
|
||||
/** 微信小游戏 */
|
||||
WeChat = 'wechat',
|
||||
/** 试玩广告(Playable Ads) */
|
||||
Playable = 'playable',
|
||||
/** Android平台 */
|
||||
Android = 'android',
|
||||
/** iOS平台 */
|
||||
iOS = 'ios',
|
||||
/** 编辑器(Tauri桌面) */
|
||||
Editor = 'editor'
|
||||
}
|
||||
|
||||
/**
|
||||
* Quality levels for asset variants
|
||||
* 资产变体的质量级别
|
||||
*/
|
||||
export enum AssetQuality {
|
||||
/** 低质量 */
|
||||
Low = 'low',
|
||||
/** 中等质量 */
|
||||
Medium = 'medium',
|
||||
/** 高质量 */
|
||||
High = 'high',
|
||||
/** 超高质量 */
|
||||
Ultra = 'ultra'
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset metadata stored in the database
|
||||
* 存储在数据库中的资产元数据
|
||||
*/
|
||||
export interface IAssetMetadata {
|
||||
/** 全局唯一标识符 */
|
||||
guid: AssetGUID;
|
||||
/** 资产路径 */
|
||||
path: string;
|
||||
/** 资产类型 */
|
||||
type: AssetType;
|
||||
/** 资产名称 */
|
||||
name: string;
|
||||
/** 文件大小(字节) / File size in bytes */
|
||||
size: number;
|
||||
/** 内容哈希值 / Content hash for versioning */
|
||||
hash: string;
|
||||
/** 依赖的其他资产 / Dependencies on other assets */
|
||||
dependencies: AssetGUID[];
|
||||
/** 资产标签 / User-defined labels for categorization */
|
||||
labels: string[];
|
||||
/** 自定义标签 / Custom metadata tags */
|
||||
tags: Map<string, string>;
|
||||
/** 导入设置 / Import-time settings */
|
||||
importSettings?: Record<string, unknown>;
|
||||
/** 最后修改时间 / Unix timestamp of last modification */
|
||||
lastModified: number;
|
||||
/** 版本号 / Asset version number */
|
||||
version: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset variant descriptor
|
||||
* 资产变体描述符
|
||||
*/
|
||||
export interface IAssetVariant {
|
||||
/** 目标平台 */
|
||||
platform: AssetPlatform;
|
||||
/** 质量级别 */
|
||||
quality: AssetQuality;
|
||||
/** 本地化语言 / Language code for localized assets */
|
||||
locale?: string;
|
||||
/** 主题变体 / Theme identifier (e.g., 'dark', 'light') */
|
||||
theme?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset load options
|
||||
* 资产加载选项
|
||||
*/
|
||||
export interface IAssetLoadOptions {
|
||||
/** 加载优先级(0-100,越高越优先) / Priority level 0-100, higher loads first */
|
||||
priority?: number;
|
||||
/** 是否异步加载 / Use async loading */
|
||||
async?: boolean;
|
||||
/** 指定加载的变体 / Specific variant to load */
|
||||
variant?: IAssetVariant;
|
||||
/** 强制重新加载 / Force reload even if cached */
|
||||
forceReload?: boolean;
|
||||
/** 超时时间(毫秒) / Timeout in milliseconds */
|
||||
timeout?: number;
|
||||
/** 进度回调 / Progress callback (0-1) */
|
||||
onProgress?: (progress: number) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset bundle manifest
|
||||
* 资产包清单
|
||||
*/
|
||||
export interface IAssetBundleManifest {
|
||||
/** 包名称 */
|
||||
name: string;
|
||||
/** 版本号 */
|
||||
version: string;
|
||||
/** 内容哈希 / Content hash for integrity check */
|
||||
hash: string;
|
||||
/** 压缩类型 */
|
||||
compression?: 'none' | 'gzip' | 'brotli';
|
||||
/** 包含的资产列表 / Assets contained in this bundle */
|
||||
assets: AssetGUID[];
|
||||
/** 依赖的其他包 / Other bundles this depends on */
|
||||
dependencies: string[];
|
||||
/** 包大小(字节) / Bundle size in bytes */
|
||||
size: number;
|
||||
/** 创建时间戳 / Creation timestamp */
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset loading result
|
||||
* 资产加载结果
|
||||
*/
|
||||
export interface IAssetLoadResult<T = unknown> {
|
||||
/** 加载的资产实例 */
|
||||
asset: T;
|
||||
/** 资产句柄 */
|
||||
handle: AssetHandle;
|
||||
/** 资产元数据 */
|
||||
metadata: IAssetMetadata;
|
||||
/** 加载耗时(毫秒) / Load time in milliseconds */
|
||||
loadTime: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset loading error
|
||||
* 资产加载错误
|
||||
*/
|
||||
export class AssetLoadError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly guid: AssetGUID,
|
||||
public readonly type: AssetType,
|
||||
public readonly cause?: Error
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'AssetLoadError';
|
||||
Object.setPrototypeOf(this, new.target.prototype);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method for file not found error
|
||||
* 文件未找到错误的工厂方法
|
||||
*/
|
||||
static fileNotFound(guid: AssetGUID, path: string): AssetLoadError {
|
||||
return new AssetLoadError(`Asset file not found: ${path}`, guid, AssetType.Custom);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method for unsupported type error
|
||||
* 不支持的类型错误的工厂方法
|
||||
*/
|
||||
static unsupportedType(guid: AssetGUID, type: AssetType): AssetLoadError {
|
||||
return new AssetLoadError(`Unsupported asset type: ${type}`, guid, type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method for load timeout error
|
||||
* 加载超时错误的工厂方法
|
||||
*/
|
||||
static loadTimeout(guid: AssetGUID, type: AssetType, timeout: number): AssetLoadError {
|
||||
return new AssetLoadError(`Asset load timeout after ${timeout}ms`, guid, type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method for dependency failed error
|
||||
* 依赖加载失败错误的工厂方法
|
||||
*/
|
||||
static dependencyFailed(guid: AssetGUID, type: AssetType, depGuid: AssetGUID): AssetLoadError {
|
||||
return new AssetLoadError(`Dependency failed to load: ${depGuid}`, guid, type);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset reference counting info
|
||||
* 资产引用计数信息
|
||||
*/
|
||||
export interface IAssetReferenceInfo {
|
||||
/** 资产GUID */
|
||||
guid: AssetGUID;
|
||||
/** 资产句柄 */
|
||||
handle: AssetHandle;
|
||||
/** 引用计数 */
|
||||
referenceCount: number;
|
||||
/** 最后访问时间 / Unix timestamp of last access */
|
||||
lastAccessTime: number;
|
||||
/** 当前状态 */
|
||||
state: AssetState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset import options
|
||||
* 资产导入选项
|
||||
*/
|
||||
export interface IAssetImportOptions {
|
||||
/** 资产类型 */
|
||||
type: AssetType;
|
||||
/** 生成Mipmap / Generate mipmaps for textures */
|
||||
generateMipmaps?: boolean;
|
||||
/** 纹理压缩格式 / Texture compression format */
|
||||
compression?: 'none' | 'dxt' | 'etc2' | 'astc';
|
||||
/** 最大纹理尺寸 / Maximum texture dimension */
|
||||
maxTextureSize?: number;
|
||||
/** 生成LOD / Generate LODs for meshes */
|
||||
generateLODs?: boolean;
|
||||
/** 优化网格 / Optimize mesh geometry */
|
||||
optimizeMesh?: boolean;
|
||||
/** 音频格式 / Audio encoding format */
|
||||
audioFormat?: 'mp3' | 'ogg' | 'wav';
|
||||
/** 自定义处理器 / Custom processor plugin name */
|
||||
customProcessor?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset usage statistics
|
||||
* 资产使用统计
|
||||
*/
|
||||
export interface IAssetUsageStats {
|
||||
/** 资产GUID */
|
||||
guid: AssetGUID;
|
||||
/** 加载次数 */
|
||||
loadCount: number;
|
||||
/** 总加载时间(毫秒) / Total time spent loading in ms */
|
||||
totalLoadTime: number;
|
||||
/** 平均加载时间(毫秒) / Average load time in ms */
|
||||
averageLoadTime: number;
|
||||
/** 最后使用时间 / Unix timestamp of last use */
|
||||
lastUsedTime: number;
|
||||
/** 被引用的资产列表 / Assets that reference this one */
|
||||
referencedBy: AssetGUID[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset preload group
|
||||
* 资产预加载组
|
||||
*/
|
||||
export interface IAssetPreloadGroup {
|
||||
/** 组名称 */
|
||||
name: string;
|
||||
/** 包含的资产 */
|
||||
assets: AssetGUID[];
|
||||
/** 加载优先级 / Load priority 0-100 */
|
||||
priority: number;
|
||||
/** 是否必需 / Must be loaded before scene start */
|
||||
required: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset loading progress info
|
||||
* 资产加载进度信息
|
||||
*/
|
||||
export interface IAssetLoadProgress {
|
||||
/** 当前加载的资产 */
|
||||
currentAsset: string;
|
||||
/** 已加载数量 */
|
||||
loadedCount: number;
|
||||
/** 总数量 */
|
||||
totalCount: number;
|
||||
/** 已加载字节数 */
|
||||
loadedBytes: number;
|
||||
/** 总字节数 */
|
||||
totalBytes: number;
|
||||
/** 进度百分比(0-1) / Progress value 0-1 */
|
||||
progress: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset catalog entry for runtime lookups
|
||||
* 运行时查找的资产目录条目
|
||||
*/
|
||||
export interface IAssetCatalogEntry {
|
||||
/** 资产GUID */
|
||||
guid: AssetGUID;
|
||||
/** 资产路径 */
|
||||
path: string;
|
||||
/** 资产类型 */
|
||||
type: AssetType;
|
||||
/** 所在包名称 / Bundle containing this asset */
|
||||
bundleName?: string;
|
||||
/** 可用变体 / Available variants */
|
||||
variants?: IAssetVariant[];
|
||||
/** 大小(字节) / Size in bytes */
|
||||
size: number;
|
||||
/** 内容哈希 / Content hash */
|
||||
hash: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runtime asset catalog
|
||||
* 运行时资产目录
|
||||
*/
|
||||
export interface IAssetCatalog {
|
||||
/** 版本号 */
|
||||
version: string;
|
||||
/** 创建时间戳 / Creation timestamp */
|
||||
createdAt: number;
|
||||
/** 所有目录条目 / All catalog entries */
|
||||
entries: Map<AssetGUID, IAssetCatalogEntry>;
|
||||
/** 此目录中的包 / Bundles in this catalog */
|
||||
bundles: Map<string, IAssetBundleManifest>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset hot-reload event
|
||||
* 资产热重载事件
|
||||
*/
|
||||
export interface IAssetHotReloadEvent {
|
||||
/** 资产GUID */
|
||||
guid: AssetGUID;
|
||||
/** 资产路径 */
|
||||
path: string;
|
||||
/** 资产类型 */
|
||||
type: AssetType;
|
||||
/** 旧版本哈希 / Previous version hash */
|
||||
oldHash: string;
|
||||
/** 新版本哈希 / New version hash */
|
||||
newHash: string;
|
||||
/** 时间戳 */
|
||||
timestamp: number;
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
/**
|
||||
* Path Validator
|
||||
* 路径验证器
|
||||
*
|
||||
* Validates and sanitizes asset paths for security
|
||||
* 验证并清理资产路径以确保安全
|
||||
*/
|
||||
|
||||
/**
|
||||
* Path validation options.
|
||||
* 路径验证选项。
|
||||
*/
|
||||
export interface PathValidationOptions {
|
||||
/** Allow absolute paths (for editor environment). | 允许绝对路径(用于编辑器环境)。 */
|
||||
allowAbsolutePaths?: boolean;
|
||||
/** Allow URLs (http://, https://, asset://). | 允许 URL。 */
|
||||
allowUrls?: boolean;
|
||||
}
|
||||
|
||||
export class PathValidator {
|
||||
// Dangerous path patterns (without absolute path checks)
|
||||
private static readonly DANGEROUS_PATTERNS_STRICT = [
|
||||
/\.\.[/\\]/g, // Path traversal attempts (..)
|
||||
/^[/\\]/, // Absolute paths on Unix
|
||||
/^[a-zA-Z]:[/\\]/, // Absolute paths on Windows
|
||||
/\0/, // Null bytes
|
||||
/%00/, // URL encoded null bytes
|
||||
/\.\.%2[fF]/ // URL encoded path traversal
|
||||
];
|
||||
|
||||
// Dangerous path patterns (allowing absolute paths)
|
||||
private static readonly DANGEROUS_PATTERNS_RELAXED = [
|
||||
/\.\.[/\\]/g, // Path traversal attempts (..)
|
||||
/\0/, // Null bytes
|
||||
/%00/, // URL encoded null bytes
|
||||
/\.\.%2[fF]/ // URL encoded path traversal
|
||||
];
|
||||
|
||||
// Valid path characters for relative paths (alphanumeric, dash, underscore, dot, slash)
|
||||
private static readonly VALID_PATH_REGEX = /^[a-zA-Z0-9\-_./\\@]+$/;
|
||||
|
||||
// Valid path characters for absolute paths (includes colon for Windows drives)
|
||||
private static readonly VALID_ABSOLUTE_PATH_REGEX = /^[a-zA-Z0-9\-_./\\@:]+$/;
|
||||
|
||||
// URL pattern
|
||||
private static readonly URL_REGEX = /^(https?|asset|blob|data):\/\//;
|
||||
|
||||
// Maximum path length
|
||||
private static readonly MAX_PATH_LENGTH = 1024;
|
||||
|
||||
/** Global options for path validation. | 路径验证的全局选项。 */
|
||||
private static _globalOptions: PathValidationOptions = {
|
||||
allowAbsolutePaths: false,
|
||||
allowUrls: true
|
||||
};
|
||||
|
||||
/**
|
||||
* Set global validation options.
|
||||
* 设置全局验证选项。
|
||||
*/
|
||||
static setGlobalOptions(options: PathValidationOptions): void {
|
||||
this._globalOptions = { ...this._globalOptions, ...options };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current global options.
|
||||
* 获取当前全局选项。
|
||||
*/
|
||||
static getGlobalOptions(): PathValidationOptions {
|
||||
return { ...this._globalOptions };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if a path is safe
|
||||
* 验证路径是否安全
|
||||
*/
|
||||
static validate(path: string, options?: PathValidationOptions): { valid: boolean; reason?: string } {
|
||||
const opts = { ...this._globalOptions, ...options };
|
||||
|
||||
// Check for null/undefined/empty
|
||||
if (!path || typeof path !== 'string') {
|
||||
return { valid: false, reason: 'Path is empty or invalid' };
|
||||
}
|
||||
|
||||
// Check length
|
||||
if (path.length > this.MAX_PATH_LENGTH) {
|
||||
return { valid: false, reason: `Path exceeds maximum length of ${this.MAX_PATH_LENGTH} characters` };
|
||||
}
|
||||
|
||||
// Allow URLs if enabled
|
||||
if (opts.allowUrls && this.URL_REGEX.test(path)) {
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
// Choose patterns based on options
|
||||
const patterns = opts.allowAbsolutePaths
|
||||
? this.DANGEROUS_PATTERNS_RELAXED
|
||||
: this.DANGEROUS_PATTERNS_STRICT;
|
||||
|
||||
// Check for dangerous patterns
|
||||
for (const pattern of patterns) {
|
||||
if (pattern.test(path)) {
|
||||
return { valid: false, reason: 'Path contains dangerous pattern' };
|
||||
}
|
||||
}
|
||||
|
||||
// Check for valid characters
|
||||
const validCharsRegex = opts.allowAbsolutePaths
|
||||
? this.VALID_ABSOLUTE_PATH_REGEX
|
||||
: this.VALID_PATH_REGEX;
|
||||
|
||||
if (!validCharsRegex.test(path)) {
|
||||
return { valid: false, reason: 'Path contains invalid characters' };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize a path
|
||||
* 清理路径
|
||||
*/
|
||||
static sanitize(path: string): string {
|
||||
if (!path || typeof path !== 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Remove dangerous patterns
|
||||
let sanitized = path;
|
||||
|
||||
// Remove path traversal (apply repeatedly until fully removed)
|
||||
let prev;
|
||||
do {
|
||||
prev = sanitized;
|
||||
sanitized = sanitized.replace(/\.\.[/\\]/g, '');
|
||||
} while (sanitized !== prev);
|
||||
|
||||
// Remove leading slashes
|
||||
sanitized = sanitized.replace(/^[/\\]+/, '');
|
||||
|
||||
// Remove null bytes
|
||||
sanitized = sanitized.replace(/\0/g, '');
|
||||
sanitized = sanitized.replace(/%00/g, '');
|
||||
|
||||
// Remove invalid Windows characters
|
||||
sanitized = sanitized.replace(/[<>:"|?*]/g, '_');
|
||||
|
||||
// Normalize slashes
|
||||
sanitized = sanitized.replace(/\\/g, '/');
|
||||
|
||||
// Remove double slashes
|
||||
sanitized = sanitized.replace(/\/+/g, '/');
|
||||
|
||||
// Trim whitespace
|
||||
sanitized = sanitized.trim();
|
||||
|
||||
// Truncate if too long
|
||||
if (sanitized.length > this.MAX_PATH_LENGTH) {
|
||||
sanitized = sanitized.substring(0, this.MAX_PATH_LENGTH);
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if path is trying to escape the base directory
|
||||
* 检查路径是否试图逃离基础目录
|
||||
*/
|
||||
static isPathTraversal(path: string): boolean {
|
||||
const normalized = path.replace(/\\/g, '/');
|
||||
return normalized.includes('../') || normalized.includes('..\\');
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a path for consistent handling
|
||||
* 规范化路径以便一致处理
|
||||
*/
|
||||
static normalize(path: string): string {
|
||||
if (!path) return '';
|
||||
|
||||
// Sanitize first
|
||||
let normalized = this.sanitize(path);
|
||||
|
||||
// Convert backslashes to forward slashes
|
||||
normalized = normalized.replace(/\\/g, '/');
|
||||
|
||||
// Remove trailing slash (except for root)
|
||||
if (normalized.length > 1 && normalized.endsWith('/')) {
|
||||
normalized = normalized.slice(0, -1);
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Join path segments safely
|
||||
* 安全地连接路径段
|
||||
*/
|
||||
static join(...segments: string[]): string {
|
||||
const validSegments = segments
|
||||
.filter((s) => s && typeof s === 'string')
|
||||
.map((s) => this.sanitize(s))
|
||||
.filter((s) => s.length > 0);
|
||||
|
||||
if (validSegments.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return this.normalize(validSegments.join('/'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file extension safely
|
||||
* 安全地获取文件扩展名
|
||||
*/
|
||||
static getExtension(path: string): string {
|
||||
const sanitized = this.sanitize(path);
|
||||
const lastDot = sanitized.lastIndexOf('.');
|
||||
const lastSlash = sanitized.lastIndexOf('/');
|
||||
|
||||
if (lastDot > lastSlash && lastDot > 0) {
|
||||
return sanitized.substring(lastDot + 1).toLowerCase();
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
/**
|
||||
* UV Coordinate Helper
|
||||
* UV 坐标辅助工具
|
||||
*
|
||||
* 引擎使用图像坐标系:
|
||||
* Engine uses image coordinate system:
|
||||
* - 原点 (0, 0) 在左上角 | Origin at top-left
|
||||
* - V 轴向下增长 | V-axis increases downward
|
||||
* - UV 格式:[u0, v0, u1, v1] 其中 v0 < v1
|
||||
*/
|
||||
export class UVHelper {
|
||||
/**
|
||||
* Calculate UV coordinates for a texture region
|
||||
* 计算纹理区域的 UV 坐标
|
||||
*/
|
||||
static calculateUV(
|
||||
imageRect: { x: number; y: number; width: number; height: number },
|
||||
textureSize: { width: number; height: number }
|
||||
): [number, number, number, number] {
|
||||
const { x, y, width, height } = imageRect;
|
||||
const { width: tw, height: th } = textureSize;
|
||||
|
||||
return [
|
||||
x / tw, // u0
|
||||
y / th, // v0
|
||||
(x + width) / tw, // u1
|
||||
(y + height) / th // v1
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate UV coordinates for a tile in a tileset
|
||||
* 计算 tileset 中某个 tile 的 UV 坐标
|
||||
*/
|
||||
static calculateTileUV(
|
||||
tileIndex: number,
|
||||
tilesetInfo: {
|
||||
columns: number;
|
||||
tileWidth: number;
|
||||
tileHeight: number;
|
||||
imageWidth: number;
|
||||
imageHeight: number;
|
||||
margin?: number;
|
||||
spacing?: number;
|
||||
}
|
||||
): [number, number, number, number] | null {
|
||||
if (tileIndex < 0) return null;
|
||||
|
||||
const {
|
||||
columns,
|
||||
tileWidth,
|
||||
tileHeight,
|
||||
imageWidth,
|
||||
imageHeight,
|
||||
margin = 0,
|
||||
spacing = 0
|
||||
} = tilesetInfo;
|
||||
|
||||
const col = tileIndex % columns;
|
||||
const row = Math.floor(tileIndex / columns);
|
||||
const x = margin + col * (tileWidth + spacing);
|
||||
const y = margin + row * (tileHeight + spacing);
|
||||
|
||||
return this.calculateUV(
|
||||
{ x, y, width: tileWidth, height: tileHeight },
|
||||
{ width: imageWidth, height: imageHeight }
|
||||
);
|
||||
}
|
||||
|
||||
static validateUV(uv: [number, number, number, number]): boolean {
|
||||
const [u0, v0, u1, v1] = uv;
|
||||
return u0 >= 0 && u0 <= 1 && u1 >= 0 && u1 <= 1 &&
|
||||
v0 >= 0 && v0 <= 1 && v1 >= 0 && v1 <= 1 &&
|
||||
u0 < u1 && v0 < v1;
|
||||
}
|
||||
|
||||
static debugPrint(uv: [number, number, number, number], label?: string): void {
|
||||
const prefix = label ? `[${label}] ` : '';
|
||||
console.log(`${prefix}UV: [${uv.map(n => n.toFixed(4)).join(', ')}]`);
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ES2020",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2020", "DOM"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"composite": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": false,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"sourceMap": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictBindCallApply": true,
|
||||
"strictPropertyInitialization": true,
|
||||
"noImplicitThis": true,
|
||||
"alwaysStrict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
import { runtimeOnlyPreset } from '../build-config/src/presets/plugin-tsup';
|
||||
|
||||
export default defineConfig({
|
||||
...runtimeOnlyPreset(),
|
||||
tsconfig: 'tsconfig.build.json'
|
||||
});
|
||||
@@ -1,43 +0,0 @@
|
||||
{
|
||||
"id": "audio",
|
||||
"name": "@esengine/audio",
|
||||
"displayName": "Audio",
|
||||
"description": "Audio playback and sound effects | 音频播放和音效",
|
||||
"version": "1.0.0",
|
||||
"category": "Audio",
|
||||
"icon": "Volume2",
|
||||
"tags": [
|
||||
"audio",
|
||||
"sound",
|
||||
"music"
|
||||
],
|
||||
"isCore": false,
|
||||
"defaultEnabled": false,
|
||||
"isEngineModule": true,
|
||||
"canContainContent": true,
|
||||
"platforms": [
|
||||
"web",
|
||||
"desktop",
|
||||
"mobile"
|
||||
],
|
||||
"dependencies": [
|
||||
"core",
|
||||
"asset-system"
|
||||
],
|
||||
"exports": {
|
||||
"components": [
|
||||
"AudioSourceComponent",
|
||||
"AudioListenerComponent"
|
||||
],
|
||||
"systems": [
|
||||
"AudioSystem"
|
||||
],
|
||||
"other": [
|
||||
"AudioClip",
|
||||
"AudioMixer"
|
||||
]
|
||||
},
|
||||
"requiresWasm": false,
|
||||
"outputPath": "dist/index.js",
|
||||
"pluginExport": "AudioPlugin"
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
{
|
||||
"name": "@esengine/audio",
|
||||
"version": "1.0.0",
|
||||
"description": "ECS-based audio system",
|
||||
"esengine": {
|
||||
"plugin": true,
|
||||
"pluginExport": "AudioPlugin",
|
||||
"category": "audio",
|
||||
"isEnginePlugin": true
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"build:watch": "tsup --watch",
|
||||
"type-check": "tsc --noEmit",
|
||||
"clean": "rimraf dist"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@esengine/ecs-framework": "workspace:*",
|
||||
"@esengine/engine-core": "workspace:*",
|
||||
"@esengine/build-config": "workspace:*",
|
||||
"rimraf": "^5.0.5",
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"keywords": [
|
||||
"ecs",
|
||||
"audio",
|
||||
"sound",
|
||||
"music"
|
||||
],
|
||||
"author": "yhh",
|
||||
"license": "MIT"
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import type { ComponentRegistry as ComponentRegistryType } from '@esengine/ecs-framework';
|
||||
import type { IRuntimeModule, IPlugin, ModuleManifest } from '@esengine/engine-core';
|
||||
import { AudioSourceComponent } from './AudioSourceComponent';
|
||||
|
||||
class AudioRuntimeModule implements IRuntimeModule {
|
||||
registerComponents(registry: typeof ComponentRegistryType): void {
|
||||
registry.register(AudioSourceComponent);
|
||||
}
|
||||
}
|
||||
|
||||
const manifest: ModuleManifest = {
|
||||
id: 'audio',
|
||||
name: '@esengine/audio',
|
||||
displayName: 'Audio',
|
||||
version: '1.0.0',
|
||||
description: '音频组件',
|
||||
category: 'Audio',
|
||||
isCore: false,
|
||||
defaultEnabled: true,
|
||||
isEngineModule: true,
|
||||
dependencies: ['core', 'asset-system'],
|
||||
exports: { components: ['AudioSourceComponent'] }
|
||||
};
|
||||
|
||||
export const AudioPlugin: IPlugin = {
|
||||
manifest,
|
||||
runtimeModule: new AudioRuntimeModule()
|
||||
};
|
||||
@@ -1,43 +0,0 @@
|
||||
import { Component, ECSComponent, Serializable, Serialize, Property } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSComponent('AudioSource')
|
||||
@Serializable({ version: 1, typeId: 'AudioSource' })
|
||||
export class AudioSourceComponent extends Component {
|
||||
@Serialize()
|
||||
@Property({ type: 'asset', label: 'Audio Clip', assetType: 'audio' })
|
||||
clip: string = '';
|
||||
|
||||
/** 范围 [0, 1] */
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Volume', min: 0, max: 1, step: 0.01 })
|
||||
volume: number = 1;
|
||||
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Pitch', min: 0.1, max: 3, step: 0.1 })
|
||||
pitch: number = 1;
|
||||
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Loop' })
|
||||
loop: boolean = false;
|
||||
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Play On Awake' })
|
||||
playOnAwake: boolean = false;
|
||||
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Mute' })
|
||||
mute: boolean = false;
|
||||
|
||||
/** 0 = 2D, 1 = 3D */
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Spatial Blend', min: 0, max: 1, step: 0.1 })
|
||||
spatialBlend: number = 0;
|
||||
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Min Distance' })
|
||||
minDistance: number = 1;
|
||||
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Max Distance' })
|
||||
maxDistance: number = 500;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export { AudioSourceComponent } from './AudioSourceComponent';
|
||||
export { AudioPlugin } from './AudioPlugin';
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": false,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"],
|
||||
"references": [
|
||||
{ "path": "../core" }
|
||||
]
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
import { runtimeOnlyPreset } from '../build-config/src/presets/plugin-tsup';
|
||||
|
||||
export default defineConfig({
|
||||
...runtimeOnlyPreset(),
|
||||
tsconfig: 'tsconfig.build.json'
|
||||
});
|
||||
@@ -1,49 +0,0 @@
|
||||
{
|
||||
"name": "@esengine/behavior-tree-editor",
|
||||
"version": "1.0.0",
|
||||
"description": "Editor support for @esengine/behavior-tree - visual editor, inspectors, and tools",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"build:watch": "tsup --watch",
|
||||
"type-check": "tsc --noEmit",
|
||||
"clean": "rimraf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@esengine/behavior-tree": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@esengine/ecs-framework": "workspace:*",
|
||||
"@esengine/engine-core": "workspace:*",
|
||||
"@esengine/editor-core": "workspace:*",
|
||||
"@esengine/editor-runtime": "workspace:*",
|
||||
"@esengine/node-editor": "workspace:*",
|
||||
"@esengine/build-config": "workspace:*",
|
||||
"lucide-react": "^0.545.0",
|
||||
"react": "^18.3.1",
|
||||
"zustand": "^5.0.8",
|
||||
"@types/react": "^18.3.12",
|
||||
"rimraf": "^5.0.5",
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"keywords": [
|
||||
"ecs",
|
||||
"behavior-tree",
|
||||
"editor"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT"
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
/**
|
||||
* Behavior Tree Plugin Manifest
|
||||
* 行为树插件清单
|
||||
*/
|
||||
|
||||
import type { ModuleManifest } from '@esengine/editor-runtime';
|
||||
|
||||
/**
|
||||
* 插件清单
|
||||
*/
|
||||
export const manifest: ModuleManifest = {
|
||||
id: '@esengine/behavior-tree',
|
||||
name: '@esengine/behavior-tree',
|
||||
displayName: 'Behavior Tree System',
|
||||
version: '1.0.0',
|
||||
description: 'AI 行为树系统,支持可视化编辑和运行时执行',
|
||||
category: 'AI',
|
||||
icon: 'GitBranch',
|
||||
isCore: false,
|
||||
defaultEnabled: true,
|
||||
isEngineModule: false,
|
||||
canContainContent: false,
|
||||
dependencies: ['engine-core'],
|
||||
exports: {
|
||||
components: ['BehaviorTreeRuntimeComponent'],
|
||||
systems: ['BehaviorTreeExecutionSystem'],
|
||||
loaders: ['BehaviorTreeLoader']
|
||||
}
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
import type { ServiceContainer } from '@esengine/editor-runtime';
|
||||
|
||||
/**
|
||||
* 插件上下文
|
||||
* 存储插件安装时传入的服务容器引用
|
||||
*/
|
||||
class PluginContextClass {
|
||||
private _services: ServiceContainer | null = null;
|
||||
|
||||
setServices(services: ServiceContainer): void {
|
||||
this._services = services;
|
||||
}
|
||||
|
||||
getServices(): ServiceContainer {
|
||||
if (!this._services) {
|
||||
throw new Error('PluginContext not initialized. Make sure the plugin is properly installed.');
|
||||
}
|
||||
return this._services;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this._services = null;
|
||||
}
|
||||
}
|
||||
|
||||
export const PluginContext = new PluginContextClass();
|
||||
@@ -1,203 +0,0 @@
|
||||
import { ICommand } from './ICommand';
|
||||
|
||||
/**
|
||||
* 命令历史记录配置
|
||||
*/
|
||||
export interface CommandManagerConfig {
|
||||
/**
|
||||
* 最大历史记录数量
|
||||
*/
|
||||
maxHistorySize?: number;
|
||||
|
||||
/**
|
||||
* 是否自动合并相似命令
|
||||
*/
|
||||
autoMerge?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 命令管理器
|
||||
* 管理命令的执行、撤销、重做以及历史记录
|
||||
*/
|
||||
export class CommandManager {
|
||||
private undoStack: ICommand[] = [];
|
||||
private redoStack: ICommand[] = [];
|
||||
private readonly config: Required<CommandManagerConfig>;
|
||||
private isExecuting = false;
|
||||
|
||||
constructor(config: CommandManagerConfig = {}) {
|
||||
this.config = {
|
||||
maxHistorySize: config.maxHistorySize ?? 100,
|
||||
autoMerge: config.autoMerge ?? true
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行命令
|
||||
*/
|
||||
execute(command: ICommand): void {
|
||||
if (this.isExecuting) {
|
||||
throw new Error('不能在命令执行过程中执行新命令');
|
||||
}
|
||||
|
||||
this.isExecuting = true;
|
||||
|
||||
try {
|
||||
command.execute();
|
||||
|
||||
if (this.config.autoMerge && this.undoStack.length > 0) {
|
||||
const lastCommand = this.undoStack[this.undoStack.length - 1];
|
||||
if (lastCommand && lastCommand.canMergeWith(command)) {
|
||||
const mergedCommand = lastCommand.mergeWith(command);
|
||||
this.undoStack[this.undoStack.length - 1] = mergedCommand;
|
||||
this.redoStack = [];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.undoStack.push(command);
|
||||
this.redoStack = [];
|
||||
|
||||
if (this.undoStack.length > this.config.maxHistorySize) {
|
||||
this.undoStack.shift();
|
||||
}
|
||||
} finally {
|
||||
this.isExecuting = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤销上一个命令
|
||||
*/
|
||||
undo(): void {
|
||||
if (this.isExecuting) {
|
||||
throw new Error('不能在命令执行过程中撤销');
|
||||
}
|
||||
|
||||
const command = this.undoStack.pop();
|
||||
if (!command) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isExecuting = true;
|
||||
|
||||
try {
|
||||
command.undo();
|
||||
this.redoStack.push(command);
|
||||
} catch (error) {
|
||||
this.undoStack.push(command);
|
||||
throw error;
|
||||
} finally {
|
||||
this.isExecuting = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重做上一个被撤销的命令
|
||||
*/
|
||||
redo(): void {
|
||||
if (this.isExecuting) {
|
||||
throw new Error('不能在命令执行过程中重做');
|
||||
}
|
||||
|
||||
const command = this.redoStack.pop();
|
||||
if (!command) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isExecuting = true;
|
||||
|
||||
try {
|
||||
command.execute();
|
||||
this.undoStack.push(command);
|
||||
} catch (error) {
|
||||
this.redoStack.push(command);
|
||||
throw error;
|
||||
} finally {
|
||||
this.isExecuting = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否可以撤销
|
||||
*/
|
||||
canUndo(): boolean {
|
||||
return this.undoStack.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否可以重做
|
||||
*/
|
||||
canRedo(): boolean {
|
||||
return this.redoStack.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取撤销栈的描述列表
|
||||
*/
|
||||
getUndoHistory(): string[] {
|
||||
return this.undoStack.map((cmd) => cmd.getDescription());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取重做栈的描述列表
|
||||
*/
|
||||
getRedoHistory(): string[] {
|
||||
return this.redoStack.map((cmd) => cmd.getDescription());
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有历史记录
|
||||
*/
|
||||
clear(): void {
|
||||
this.undoStack = [];
|
||||
this.redoStack = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量执行命令(作为单一操作,可以一次撤销)
|
||||
*/
|
||||
executeBatch(commands: ICommand[]): void {
|
||||
if (commands.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const batchCommand = new BatchCommand(commands);
|
||||
this.execute(batchCommand);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量命令
|
||||
* 将多个命令组合为一个命令
|
||||
*/
|
||||
class BatchCommand implements ICommand {
|
||||
constructor(private readonly commands: ICommand[]) {}
|
||||
|
||||
execute(): void {
|
||||
for (const command of this.commands) {
|
||||
command.execute();
|
||||
}
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
for (let i = this.commands.length - 1; i >= 0; i--) {
|
||||
const command = this.commands[i];
|
||||
if (command) {
|
||||
command.undo();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return `批量操作 (${this.commands.length} 个命令)`;
|
||||
}
|
||||
|
||||
canMergeWith(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
mergeWith(): ICommand {
|
||||
throw new Error('批量命令不支持合并');
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/**
|
||||
* 命令接口
|
||||
* 实现命令模式,支持撤销/重做功能
|
||||
*/
|
||||
export interface ICommand {
|
||||
/**
|
||||
* 执行命令
|
||||
*/
|
||||
execute(): void;
|
||||
|
||||
/**
|
||||
* 撤销命令
|
||||
*/
|
||||
undo(): void;
|
||||
|
||||
/**
|
||||
* 获取命令描述(用于显示历史记录)
|
||||
*/
|
||||
getDescription(): string;
|
||||
|
||||
/**
|
||||
* 检查命令是否可以合并
|
||||
* 用于优化撤销/重做历史,例如连续的移动操作可以合并为一个
|
||||
*/
|
||||
canMergeWith(other: ICommand): boolean;
|
||||
|
||||
/**
|
||||
* 与另一个命令合并
|
||||
*/
|
||||
mergeWith(other: ICommand): ICommand;
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { BehaviorTree } from '../../domain/models/BehaviorTree';
|
||||
|
||||
/**
|
||||
* 行为树状态接口
|
||||
* 命令通过此接口操作状态
|
||||
*/
|
||||
export interface ITreeState {
|
||||
/**
|
||||
* 获取当前行为树
|
||||
*/
|
||||
getTree(): BehaviorTree;
|
||||
|
||||
/**
|
||||
* 设置行为树
|
||||
*/
|
||||
setTree(tree: BehaviorTree): void;
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { Connection } from '../../../domain/models/Connection';
|
||||
import { BaseCommand } from '@esengine/editor-runtime';
|
||||
import { ITreeState } from '../ITreeState';
|
||||
|
||||
/**
|
||||
* 添加连接命令
|
||||
*/
|
||||
export class AddConnectionCommand extends BaseCommand {
|
||||
constructor(
|
||||
private readonly state: ITreeState,
|
||||
private readonly connection: Connection
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const tree = this.state.getTree();
|
||||
const newTree = tree.addConnection(this.connection);
|
||||
this.state.setTree(newTree);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
const tree = this.state.getTree();
|
||||
const newTree = tree.removeConnection(
|
||||
this.connection.from,
|
||||
this.connection.to,
|
||||
this.connection.fromProperty,
|
||||
this.connection.toProperty
|
||||
);
|
||||
this.state.setTree(newTree);
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return `添加连接: ${this.connection.from} -> ${this.connection.to}`;
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { Node } from '../../../domain/models/Node';
|
||||
import { BaseCommand } from '@esengine/editor-runtime';
|
||||
import { ITreeState } from '../ITreeState';
|
||||
|
||||
/**
|
||||
* 创建节点命令
|
||||
*/
|
||||
export class CreateNodeCommand extends BaseCommand {
|
||||
private createdNodeId: string;
|
||||
|
||||
constructor(
|
||||
private readonly state: ITreeState,
|
||||
private readonly node: Node
|
||||
) {
|
||||
super();
|
||||
this.createdNodeId = node.id;
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const tree = this.state.getTree();
|
||||
const newTree = tree.addNode(this.node);
|
||||
this.state.setTree(newTree);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
const tree = this.state.getTree();
|
||||
const newTree = tree.removeNode(this.createdNodeId);
|
||||
this.state.setTree(newTree);
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return `创建节点: ${this.node.template.displayName}`;
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { Node } from '../../../domain/models/Node';
|
||||
import { BaseCommand } from '@esengine/editor-runtime';
|
||||
import { ITreeState } from '../ITreeState';
|
||||
|
||||
/**
|
||||
* 删除节点命令
|
||||
*/
|
||||
export class DeleteNodeCommand extends BaseCommand {
|
||||
private deletedNode: Node | null = null;
|
||||
|
||||
constructor(
|
||||
private readonly state: ITreeState,
|
||||
private readonly nodeId: string
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const tree = this.state.getTree();
|
||||
this.deletedNode = tree.getNode(this.nodeId);
|
||||
const newTree = tree.removeNode(this.nodeId);
|
||||
this.state.setTree(newTree);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
if (!this.deletedNode) {
|
||||
throw new Error('无法撤销:未保存已删除的节点');
|
||||
}
|
||||
|
||||
const tree = this.state.getTree();
|
||||
const newTree = tree.addNode(this.deletedNode);
|
||||
this.state.setTree(newTree);
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return `删除节点: ${this.deletedNode?.template.displayName ?? this.nodeId}`;
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
import { Position } from '../../../domain/value-objects/Position';
|
||||
import { BaseCommand, ICommand } from '@esengine/editor-runtime';
|
||||
import { ITreeState } from '../ITreeState';
|
||||
|
||||
/**
|
||||
* 移动节点命令
|
||||
* 支持合并连续的移动操作
|
||||
*/
|
||||
export class MoveNodeCommand extends BaseCommand {
|
||||
private oldPosition: Position;
|
||||
|
||||
constructor(
|
||||
private readonly state: ITreeState,
|
||||
private readonly nodeId: string,
|
||||
private readonly newPosition: Position
|
||||
) {
|
||||
super();
|
||||
const tree = this.state.getTree();
|
||||
const node = tree.getNode(nodeId);
|
||||
this.oldPosition = node.position;
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const tree = this.state.getTree();
|
||||
const newTree = tree.updateNode(this.nodeId, (node) =>
|
||||
node.moveToPosition(this.newPosition)
|
||||
);
|
||||
this.state.setTree(newTree);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
const tree = this.state.getTree();
|
||||
const newTree = tree.updateNode(this.nodeId, (node) =>
|
||||
node.moveToPosition(this.oldPosition)
|
||||
);
|
||||
this.state.setTree(newTree);
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return `移动节点: ${this.nodeId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动命令可以合并
|
||||
*/
|
||||
canMergeWith(other: ICommand): boolean {
|
||||
if (!(other instanceof MoveNodeCommand)) {
|
||||
return false;
|
||||
}
|
||||
return this.nodeId === other.nodeId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并移动命令
|
||||
* 保留初始位置,更新最终位置
|
||||
*/
|
||||
mergeWith(other: ICommand): ICommand {
|
||||
if (!(other instanceof MoveNodeCommand)) {
|
||||
throw new Error('只能与 MoveNodeCommand 合并');
|
||||
}
|
||||
|
||||
if (this.nodeId !== other.nodeId) {
|
||||
throw new Error('只能合并同一节点的移动命令');
|
||||
}
|
||||
|
||||
const merged = new MoveNodeCommand(
|
||||
this.state,
|
||||
this.nodeId,
|
||||
other.newPosition
|
||||
);
|
||||
merged.oldPosition = this.oldPosition;
|
||||
return merged;
|
||||
}
|
||||
}
|
||||
-50
@@ -1,50 +0,0 @@
|
||||
import { Connection } from '../../../domain/models/Connection';
|
||||
import { BaseCommand } from '@esengine/editor-runtime';
|
||||
import { ITreeState } from '../ITreeState';
|
||||
|
||||
/**
|
||||
* 移除连接命令
|
||||
*/
|
||||
export class RemoveConnectionCommand extends BaseCommand {
|
||||
private removedConnection: Connection | null = null;
|
||||
|
||||
constructor(
|
||||
private readonly state: ITreeState,
|
||||
private readonly from: string,
|
||||
private readonly to: string,
|
||||
private readonly fromProperty?: string,
|
||||
private readonly toProperty?: string
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const tree = this.state.getTree();
|
||||
|
||||
const connection = tree.connections.find((c) =>
|
||||
c.matches(this.from, this.to, this.fromProperty, this.toProperty)
|
||||
);
|
||||
|
||||
if (!connection) {
|
||||
throw new Error(`连接不存在: ${this.from} -> ${this.to}`);
|
||||
}
|
||||
|
||||
this.removedConnection = connection;
|
||||
const newTree = tree.removeConnection(this.from, this.to, this.fromProperty, this.toProperty);
|
||||
this.state.setTree(newTree);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
if (!this.removedConnection) {
|
||||
throw new Error('无法撤销:未保存已删除的连接');
|
||||
}
|
||||
|
||||
const tree = this.state.getTree();
|
||||
const newTree = tree.addConnection(this.removedConnection);
|
||||
this.state.setTree(newTree);
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return `移除连接: ${this.from} -> ${this.to}`;
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import { BaseCommand } from '@esengine/editor-runtime';
|
||||
import { ITreeState } from '../ITreeState';
|
||||
|
||||
/**
|
||||
* 更新节点数据命令
|
||||
*/
|
||||
export class UpdateNodeDataCommand extends BaseCommand {
|
||||
private oldData: Record<string, unknown>;
|
||||
|
||||
constructor(
|
||||
private readonly state: ITreeState,
|
||||
private readonly nodeId: string,
|
||||
private readonly newData: Record<string, unknown>
|
||||
) {
|
||||
super();
|
||||
const tree = this.state.getTree();
|
||||
const node = tree.getNode(nodeId);
|
||||
this.oldData = node.data;
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const tree = this.state.getTree();
|
||||
const newTree = tree.updateNode(this.nodeId, (node) =>
|
||||
node.updateData(this.newData)
|
||||
);
|
||||
this.state.setTree(newTree);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
const tree = this.state.getTree();
|
||||
const newTree = tree.updateNode(this.nodeId, (node) =>
|
||||
node.updateData(this.oldData)
|
||||
);
|
||||
this.state.setTree(newTree);
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return `更新节点数据: ${this.nodeId}`;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user