Compare commits
66 Commits
editor-v1.
...
feat/docs-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34de1e5edf | ||
|
|
94e0979941 | ||
|
|
0a3f2a3e21 | ||
|
|
9c30ab26a6 | ||
|
|
3c50795dee | ||
|
|
5a0d67b3f6 | ||
|
|
caf7622aa0 | ||
|
|
d746cf3bb8 | ||
|
|
d1ba10564a | ||
|
|
cf00e062f7 | ||
|
|
293ac2dca3 | ||
|
|
f7535a2aac | ||
|
|
ca18be32a8 | ||
|
|
025ce89ded | ||
|
|
2311419e71 | ||
|
|
373bdd5d2b | ||
|
|
b58e75d9a4 | ||
|
|
099809a98c | ||
|
|
83aee02540 | ||
|
|
cb1b171216 | ||
|
|
b64b489b89 | ||
|
|
13cb670a16 | ||
|
|
37ab494e4a | ||
|
|
e1d494b415 | ||
|
|
243b929d5e | ||
|
|
4a2362edf2 | ||
|
|
0c590d7c12 | ||
|
|
c2f8cb5272 | ||
|
|
55f644a091 | ||
|
|
d3dfaa7aac | ||
|
|
25e70a1d7b | ||
|
|
e2cca5e490 | ||
|
|
b3f7676452 | ||
|
|
e6fb80d0be | ||
|
|
88af781d78 | ||
|
|
15d5d37e50 | ||
|
|
b9aaf894d7 | ||
|
|
460cdb5af4 | ||
|
|
290bd9858e | ||
|
|
b42a7b4e43 | ||
|
|
189714c727 | ||
|
|
987051acd4 | ||
|
|
374e08a79e | ||
|
|
359886c72f | ||
|
|
f03b73b58e | ||
|
|
18d20df4da | ||
|
|
c5642a8605 | ||
|
|
673f5e5855 | ||
|
|
cabb625a17 | ||
|
|
b8f05b79b0 | ||
|
|
b22faaac86 | ||
|
|
107439d70c | ||
|
|
71869b1a58 | ||
|
|
9aed3134cf | ||
|
|
3ff57aff37 | ||
|
|
152c0541b8 | ||
|
|
7b14fa2da4 | ||
|
|
3fb6f919f8 | ||
|
|
551ca7805d | ||
|
|
8ab25fe293 | ||
|
|
eea7ed9e58 | ||
|
|
0279cf6d27 | ||
|
|
0dff1ad2ad | ||
|
|
95fbcca66f | ||
|
|
a61baa83a7 | ||
|
|
afebeecd68 |
8
.github/codeql/codeql-config.yml
vendored
Normal file
8
.github/codeql/codeql-config.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
name: "CodeQL Config"
|
||||||
|
|
||||||
|
# Paths to exclude from analysis
|
||||||
|
paths-ignore:
|
||||||
|
- thirdparty
|
||||||
|
- "**/node_modules"
|
||||||
|
- "**/dist"
|
||||||
|
- "**/bin"
|
||||||
99
.github/workflows/ci.yml
vendored
99
.github/workflows/ci.yml
vendored
@@ -8,6 +8,7 @@ on:
|
|||||||
- 'package.json'
|
- 'package.json'
|
||||||
- 'pnpm-lock.yaml'
|
- 'pnpm-lock.yaml'
|
||||||
- 'tsconfig.json'
|
- 'tsconfig.json'
|
||||||
|
- 'turbo.json'
|
||||||
- 'jest.config.*'
|
- 'jest.config.*'
|
||||||
- '.github/workflows/ci.yml'
|
- '.github/workflows/ci.yml'
|
||||||
pull_request:
|
pull_request:
|
||||||
@@ -17,11 +18,12 @@ on:
|
|||||||
- 'package.json'
|
- 'package.json'
|
||||||
- 'pnpm-lock.yaml'
|
- 'pnpm-lock.yaml'
|
||||||
- 'tsconfig.json'
|
- 'tsconfig.json'
|
||||||
|
- 'turbo.json'
|
||||||
- 'jest.config.*'
|
- 'jest.config.*'
|
||||||
- '.github/workflows/ci.yml'
|
- '.github/workflows/ci.yml'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
ci:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -31,7 +33,7 @@ jobs:
|
|||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v2
|
uses: pnpm/action-setup@v2
|
||||||
with:
|
with:
|
||||||
version: 8
|
version: 10
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -39,22 +41,47 @@ jobs:
|
|||||||
node-version: '20.x'
|
node-version: '20.x'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: pnpm install
|
|
||||||
|
|
||||||
- name: Install Rust stable
|
- name: Install Rust stable
|
||||||
uses: dtolnay/rust-toolchain@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
|
- name: Install wasm-pack
|
||||||
run: cargo install wasm-pack
|
|
||||||
|
|
||||||
- name: Build core package first
|
|
||||||
run: pnpm run build:core
|
|
||||||
|
|
||||||
- name: Build engine WASM package
|
|
||||||
run: |
|
run: |
|
||||||
cd packages/engine
|
if ! command -v wasm-pack &> /dev/null; then
|
||||||
pnpm run build
|
cargo install wasm-pack
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --no-frozen-lockfile
|
||||||
|
|
||||||
|
# 缓存 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
|
- name: Copy WASM files to ecs-engine-bindgen
|
||||||
run: |
|
run: |
|
||||||
@@ -64,22 +91,15 @@ jobs:
|
|||||||
cp packages/engine/pkg/es_engine_bg.wasm 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/
|
cp packages/engine/pkg/es_engine_bg.wasm.d.ts packages/ecs-engine-bindgen/src/wasm/
|
||||||
|
|
||||||
- name: Build dependent packages for type declarations
|
# 类型检查
|
||||||
run: |
|
|
||||||
cd packages/platform-common && pnpm run build
|
|
||||||
cd ../asset-system && pnpm run build
|
|
||||||
cd ../components && pnpm run build
|
|
||||||
|
|
||||||
- name: Build ecs-engine-bindgen
|
|
||||||
run: |
|
|
||||||
cd packages/ecs-engine-bindgen && pnpm run build
|
|
||||||
|
|
||||||
- name: Type check
|
- name: Type check
|
||||||
run: pnpm run type-check
|
run: pnpm run type-check
|
||||||
|
|
||||||
|
# Lint 检查
|
||||||
- name: Lint check
|
- name: Lint check
|
||||||
run: pnpm run lint
|
run: pnpm run lint
|
||||||
|
|
||||||
|
# 测试
|
||||||
- name: Run tests with coverage
|
- name: Run tests with coverage
|
||||||
run: pnpm run test:ci
|
run: pnpm run test:ci
|
||||||
|
|
||||||
@@ -92,39 +112,16 @@ jobs:
|
|||||||
name: codecov-umbrella
|
name: codecov-umbrella
|
||||||
fail_ci_if_error: false
|
fail_ci_if_error: false
|
||||||
|
|
||||||
build:
|
# 构建 npm 包
|
||||||
runs-on: ubuntu-latest
|
- name: Build npm packages
|
||||||
needs: test
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install pnpm
|
|
||||||
uses: pnpm/action-setup@v2
|
|
||||||
with:
|
|
||||||
version: 8
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '20.x'
|
|
||||||
cache: 'pnpm'
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: pnpm install
|
|
||||||
|
|
||||||
- name: Build project
|
|
||||||
run: pnpm run build
|
|
||||||
|
|
||||||
- name: Build npm package
|
|
||||||
run: pnpm run build:npm
|
run: pnpm run build:npm
|
||||||
|
|
||||||
|
# 上传构建产物
|
||||||
- name: Upload build artifacts
|
- name: Upload build artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: build-artifacts
|
name: build-artifacts
|
||||||
path: |
|
path: |
|
||||||
bin/
|
packages/*/dist/
|
||||||
dist/
|
packages/*/bin/
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
|||||||
2
.github/workflows/codecov.yml
vendored
2
.github/workflows/codecov.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
|||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v2
|
uses: pnpm/action-setup@v2
|
||||||
with:
|
with:
|
||||||
version: 8
|
version: 10
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
|
|||||||
1
.github/workflows/codeql.yml
vendored
1
.github/workflows/codeql.yml
vendored
@@ -31,6 +31,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
queries: security-and-quality
|
queries: security-and-quality
|
||||||
|
config-file: ./.github/codeql/codeql-config.yml
|
||||||
|
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v3
|
uses: github/codeql-action/autobuild@v3
|
||||||
|
|||||||
2
.github/workflows/commitlint.yml
vendored
2
.github/workflows/commitlint.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
|||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v2
|
uses: pnpm/action-setup@v2
|
||||||
with:
|
with:
|
||||||
version: 8
|
version: 10
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
|
|||||||
2
.github/workflows/docs.yml
vendored
2
.github/workflows/docs.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
|||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v2
|
uses: pnpm/action-setup@v2
|
||||||
with:
|
with:
|
||||||
version: 8
|
version: 10
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
|
|||||||
48
.github/workflows/release-editor.yml
vendored
48
.github/workflows/release-editor.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
|||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v2
|
uses: pnpm/action-setup@v2
|
||||||
with:
|
with:
|
||||||
version: 8
|
version: 10
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -71,28 +71,16 @@ jobs:
|
|||||||
node -e "const pkg=require('./package.json'); pkg.version='${{ github.event.inputs.version }}'; require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2)+'\n')"
|
node -e "const pkg=require('./package.json'); pkg.version='${{ github.event.inputs.version }}'; require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2)+'\n')"
|
||||||
node scripts/sync-version.js
|
node scripts/sync-version.js
|
||||||
|
|
||||||
- name: Build core package
|
|
||||||
run: pnpm run build:core
|
|
||||||
|
|
||||||
- name: Build editor-core package
|
|
||||||
run: |
|
|
||||||
cd packages/editor-core
|
|
||||||
pnpm run build
|
|
||||||
|
|
||||||
- name: Build behavior-tree package
|
|
||||||
run: |
|
|
||||||
cd packages/behavior-tree
|
|
||||||
pnpm run build
|
|
||||||
|
|
||||||
- name: Install wasm-pack
|
- name: Install wasm-pack
|
||||||
run: cargo install wasm-pack
|
run: cargo install wasm-pack
|
||||||
|
|
||||||
- name: Build engine package (Rust WASM)
|
# 使用 Turborepo 自动按依赖顺序构建所有包
|
||||||
run: |
|
# 这会自动处理:core -> asset-system -> editor-core -> ui -> 等等
|
||||||
cd packages/engine
|
- name: Build all packages with Turborepo
|
||||||
pnpm run build
|
run: pnpm run build
|
||||||
|
|
||||||
- name: Copy WASM files to ecs-engine-bindgen
|
- name: Copy WASM files to ecs-engine-bindgen
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
mkdir -p packages/ecs-engine-bindgen/src/wasm
|
mkdir -p packages/ecs-engine-bindgen/src/wasm
|
||||||
cp packages/engine/pkg/es_engine.js packages/ecs-engine-bindgen/src/wasm/
|
cp packages/engine/pkg/es_engine.js packages/ecs-engine-bindgen/src/wasm/
|
||||||
@@ -100,22 +88,6 @@ jobs:
|
|||||||
cp packages/engine/pkg/es_engine_bg.wasm 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/
|
cp packages/engine/pkg/es_engine_bg.wasm.d.ts packages/ecs-engine-bindgen/src/wasm/
|
||||||
|
|
||||||
- name: Build dependent packages for ecs-engine-bindgen
|
|
||||||
run: |
|
|
||||||
cd packages/platform-common && pnpm run build
|
|
||||||
cd ../asset-system && pnpm run build
|
|
||||||
cd ../components && pnpm run build
|
|
||||||
|
|
||||||
- name: Build ecs-engine-bindgen package
|
|
||||||
run: |
|
|
||||||
cd packages/ecs-engine-bindgen
|
|
||||||
pnpm run build
|
|
||||||
|
|
||||||
- name: Build platform-web package
|
|
||||||
run: |
|
|
||||||
cd packages/platform-web
|
|
||||||
pnpm run build
|
|
||||||
|
|
||||||
- name: Bundle runtime files for Tauri
|
- name: Bundle runtime files for Tauri
|
||||||
run: |
|
run: |
|
||||||
cd packages/editor-app
|
cd packages/editor-app
|
||||||
@@ -168,16 +140,16 @@ jobs:
|
|||||||
delete-branch: true
|
delete-branch: true
|
||||||
title: "chore(editor): Release v${{ github.event.inputs.version }}"
|
title: "chore(editor): Release v${{ github.event.inputs.version }}"
|
||||||
body: |
|
body: |
|
||||||
## 🚀 Release v${{ github.event.inputs.version }}
|
## Release v${{ github.event.inputs.version }}
|
||||||
|
|
||||||
This PR updates the editor version after successful release build.
|
This PR updates the editor version after successful release build.
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
- ✅ Updated `packages/editor-app/package.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 }}`
|
- Updated `packages/editor-app/src-tauri/tauri.conf.json` → `${{ github.event.inputs.version }}`
|
||||||
|
|
||||||
### Release
|
### 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.*
|
*This PR was automatically created by the release workflow.*
|
||||||
|
|||||||
30
.github/workflows/release.yml
vendored
30
.github/workflows/release.yml
vendored
@@ -11,6 +11,10 @@ on:
|
|||||||
- core
|
- core
|
||||||
- behavior-tree
|
- behavior-tree
|
||||||
- editor-core
|
- editor-core
|
||||||
|
- node-editor
|
||||||
|
- blueprint
|
||||||
|
- tilemap
|
||||||
|
- physics-rapier2d
|
||||||
version_type:
|
version_type:
|
||||||
description: '版本更新类型'
|
description: '版本更新类型'
|
||||||
required: true
|
required: true
|
||||||
@@ -44,7 +48,7 @@ jobs:
|
|||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v2
|
uses: pnpm/action-setup@v2
|
||||||
with:
|
with:
|
||||||
version: 8
|
version: 10
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -57,11 +61,17 @@ jobs:
|
|||||||
run: pnpm install
|
run: pnpm install
|
||||||
|
|
||||||
- name: Build core package (if needed)
|
- name: Build core package (if needed)
|
||||||
if: ${{ github.event.inputs.package == 'behavior-tree' || github.event.inputs.package == 'editor-core' }}
|
if: ${{ github.event.inputs.package != 'core' && github.event.inputs.package != 'node-editor' }}
|
||||||
run: |
|
run: |
|
||||||
cd packages/core
|
cd packages/core
|
||||||
pnpm run build
|
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
|
||||||
|
|
||||||
# - name: Run tests
|
# - name: Run tests
|
||||||
# run: |
|
# run: |
|
||||||
# cd packages/${{ github.event.inputs.package }}
|
# cd packages/${{ github.event.inputs.package }}
|
||||||
@@ -72,11 +82,19 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cd packages/${{ github.event.inputs.package }}
|
cd packages/${{ github.event.inputs.package }}
|
||||||
if [ "${{ github.event.inputs.version_type }}" = "custom" ]; then
|
if [ "${{ github.event.inputs.version_type }}" = "custom" ]; then
|
||||||
npm version ${{ github.event.inputs.custom_version }} --no-git-tag-version --allow-same-version
|
NEW_VERSION=${{ github.event.inputs.custom_version }}
|
||||||
else
|
else
|
||||||
npm version ${{ github.event.inputs.version_type }} --no-git-tag-version
|
# Get current version and bump it
|
||||||
|
CURRENT=$(node -p "require('./package.json').version")
|
||||||
|
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT"
|
||||||
|
case "${{ github.event.inputs.version_type }}" in
|
||||||
|
major) NEW_VERSION="$((MAJOR+1)).0.0" ;;
|
||||||
|
minor) NEW_VERSION="$MAJOR.$((MINOR+1)).0" ;;
|
||||||
|
patch) NEW_VERSION="$MAJOR.$MINOR.$((PATCH+1))" ;;
|
||||||
|
esac
|
||||||
fi
|
fi
|
||||||
NEW_VERSION=$(node -p "require('./package.json').version")
|
# Update package.json using node
|
||||||
|
node -e "const fs=require('fs'); const pkg=JSON.parse(fs.readFileSync('package.json')); pkg.version='$NEW_VERSION'; fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2)+'\n')"
|
||||||
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||||
echo "发布版本: $NEW_VERSION"
|
echo "发布版本: $NEW_VERSION"
|
||||||
|
|
||||||
@@ -90,7 +108,7 @@ jobs:
|
|||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
cd packages/${{ github.event.inputs.package }}/dist
|
cd packages/${{ github.event.inputs.package }}/dist
|
||||||
npm publish --access public
|
pnpm publish --access public --no-git-checks
|
||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
uses: peter-evans/create-pull-request@v6
|
uses: peter-evans/create-pull-request@v6
|
||||||
|
|||||||
2
.github/workflows/size-limit.yml
vendored
2
.github/workflows/size-limit.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
|||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v2
|
uses: pnpm/action-setup@v2
|
||||||
with:
|
with:
|
||||||
version: 8
|
version: 10
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -18,6 +18,9 @@ dist/
|
|||||||
.cache/
|
.cache/
|
||||||
.build-cache/
|
.build-cache/
|
||||||
|
|
||||||
|
# Turborepo
|
||||||
|
.turbo/
|
||||||
|
|
||||||
# IDE 配置
|
# IDE 配置
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|||||||
@@ -37,9 +37,6 @@ This project follows the [Conventional Commits](https://www.conventionalcommits.
|
|||||||
|
|
||||||
- **core**: 核心包 @esengine/ecs-framework
|
- **core**: 核心包 @esengine/ecs-framework
|
||||||
- **math**: 数学库包
|
- **math**: 数学库包
|
||||||
- **network-client**: 网络客户端包
|
|
||||||
- **network-server**: 网络服务端包
|
|
||||||
- **network-shared**: 网络共享包
|
|
||||||
- **editor**: 编辑器
|
- **editor**: 编辑器
|
||||||
- **docs**: 文档
|
- **docs**: 文档
|
||||||
|
|
||||||
|
|||||||
395
README.md
395
README.md
@@ -1,90 +1,61 @@
|
|||||||
# ECS Framework
|
# ESEngine
|
||||||
|
|
||||||
[](https://github.com/esengine/ecs-framework/actions)
|
**English** | [中文](./README_CN.md)
|
||||||
[](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)
|
|
||||||
|
|
||||||
<div align="center">
|
**[Documentation](https://esengine.github.io/ecs-framework/) | [API Reference](https://esengine.github.io/ecs-framework/api/) | [Examples](./examples/)**
|
||||||
|
|
||||||
<p>一个高性能的 TypeScript ECS (Entity-Component-System) 框架,专为现代游戏开发而设计。</p>
|
ESEngine is a cross-platform 2D game engine for creating games from a unified interface. It provides a comprehensive set of common tools so that developers can focus on making games without having to reinvent the wheel.
|
||||||
|
|
||||||
<p>A high-performance TypeScript ECS (Entity-Component-System) framework designed for modern game development.</p>
|
Games can be exported to multiple platforms including Web browsers, WeChat Mini Games, and other mini-game platforms.
|
||||||
|
|
||||||
</div>
|
## Free and Open Source
|
||||||
|
|
||||||
---
|
ESEngine is completely free and open source under the MIT license. No strings attached, no royalties. Your games are yours.
|
||||||
|
|
||||||
## 📊 项目统计 / Project Stats
|
## Features
|
||||||
|
|
||||||
<div align="center">
|
- **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
|
||||||
|
|
||||||
[](https://star-history.com/#esengine/ecs-framework&Date)
|
## Getting the Engine
|
||||||
|
|
||||||
</div>
|
### Using npm
|
||||||
|
|
||||||
<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
|
```bash
|
||||||
npm install @esengine/ecs-framework
|
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
|
```typescript
|
||||||
import { Core, Scene, Component, EntitySystem, ECSComponent, ECSSystem, Matcher, Time } from '@esengine/ecs-framework';
|
import {
|
||||||
|
Core, Scene, Entity, Component, EntitySystem,
|
||||||
|
Matcher, Time, ECSComponent, ECSSystem
|
||||||
|
} from '@esengine/ecs-framework';
|
||||||
|
|
||||||
// 定义组件
|
|
||||||
@ECSComponent('Position')
|
@ECSComponent('Position')
|
||||||
class Position extends Component {
|
class Position extends Component {
|
||||||
constructor(public x = 0, public y = 0) {
|
x = 0;
|
||||||
super();
|
y = 0;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ECSComponent('Velocity')
|
@ECSComponent('Velocity')
|
||||||
class Velocity extends Component {
|
class Velocity extends Component {
|
||||||
constructor(public dx = 0, public dy = 0) {
|
dx = 0;
|
||||||
super();
|
dy = 0;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建系统
|
|
||||||
@ECSSystem('Movement')
|
@ECSSystem('Movement')
|
||||||
class MovementSystem extends EntitySystem {
|
class MovementSystem extends EntitySystem {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -93,182 +64,182 @@ class MovementSystem extends EntitySystem {
|
|||||||
|
|
||||||
protected process(entities: readonly Entity[]): void {
|
protected process(entities: readonly Entity[]): void {
|
||||||
for (const entity of entities) {
|
for (const entity of entities) {
|
||||||
const position = entity.getComponent(Position)!;
|
const pos = entity.getComponent(Position);
|
||||||
const velocity = entity.getComponent(Velocity)!;
|
const vel = entity.getComponent(Velocity);
|
||||||
|
pos.x += vel.dx * Time.deltaTime;
|
||||||
position.x += velocity.dx * Time.deltaTime;
|
pos.y += vel.dy * Time.deltaTime;
|
||||||
position.y += velocity.dy * Time.deltaTime;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建场景并启动
|
|
||||||
class GameScene extends Scene {
|
|
||||||
protected initialize(): void {
|
|
||||||
this.addSystem(new MovementSystem());
|
|
||||||
|
|
||||||
const player = this.createEntity("Player");
|
|
||||||
player.addComponent(new Position(100, 100));
|
|
||||||
player.addComponent(new Velocity(50, 0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 启动游戏
|
|
||||||
Core.create();
|
Core.create();
|
||||||
Core.setScene(new GameScene());
|
const scene = new Scene();
|
||||||
|
scene.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;
|
||||||
|
|
||||||
// 游戏循环中更新
|
|
||||||
function gameLoop(deltaTime: number) {
|
|
||||||
Core.update(deltaTime);
|
Core.update(deltaTime);
|
||||||
|
requestAnimationFrame(gameLoop);
|
||||||
}
|
}
|
||||||
|
requestAnimationFrame(gameLoop);
|
||||||
```
|
```
|
||||||
|
|
||||||
## 核心特性
|
## Modules
|
||||||
|
|
||||||
- **实体查询** - 使用 Matcher API 进行高效的实体过滤
|
ESEngine is organized into modular packages. Each feature has a runtime module and an optional editor extension.
|
||||||
- **事件系统** - 类型安全的事件发布/订阅机制
|
|
||||||
- **性能优化** - SoA 存储优化,支持大规模实体处理
|
|
||||||
- **多线程支持** - Worker系统实现真正的并行计算,充分利用多核CPU
|
|
||||||
- **多场景** - 支持 World/Scene 分层架构
|
|
||||||
- **时间管理** - 内置定时器和时间控制系统
|
|
||||||
|
|
||||||
## 🏗️ 架构设计 / Architecture
|
### Core
|
||||||
|
|
||||||
```mermaid
|
| Package | Description |
|
||||||
graph TB
|
|---------|-------------|
|
||||||
A[Core 核心] --> B[World 世界]
|
| `@esengine/ecs-framework` | Core ECS framework with entity management, component system, and queries |
|
||||||
B --> C[Scene 场景]
|
| `@esengine/math` | Vector, matrix, and mathematical utilities |
|
||||||
C --> D[EntityManager 实体管理器]
|
| `@esengine/engine` | Rust/WASM 2D renderer |
|
||||||
C --> E[SystemManager 系统管理器]
|
| `@esengine/engine-core` | Engine module system and lifecycle management |
|
||||||
D --> F[Entity 实体]
|
|
||||||
F --> G[Component 组件]
|
|
||||||
E --> H[EntitySystem 实体系统]
|
|
||||||
E --> I[WorkerSystem 工作线程系统]
|
|
||||||
|
|
||||||
style A fill:#e1f5ff
|
### Runtime Modules
|
||||||
style B fill:#fff3e0
|
|
||||||
style C fill:#f3e5f5
|
| Package | Description |
|
||||||
style D fill:#e8f5e9
|
|---------|-------------|
|
||||||
style E fill:#fff9c4
|
| `@esengine/sprite` | 2D sprite rendering and animation |
|
||||||
style F fill:#ffebee
|
| `@esengine/tilemap` | Tile-based map rendering with animation support |
|
||||||
style G fill:#e0f2f1
|
| `@esengine/physics-rapier2d` | 2D physics simulation powered by Rapier |
|
||||||
style H fill:#fce4ec
|
| `@esengine/behavior-tree` | Behavior tree AI system |
|
||||||
style I fill:#f1f8e9
|
| `@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
|
||||||
```
|
```
|
||||||
|
|
||||||
## 平台支持
|
### Running the Editor
|
||||||
|
|
||||||
支持主流游戏引擎和 Web 平台:
|
```bash
|
||||||
|
cd packages/editor-app
|
||||||
|
pnpm tauri:dev
|
||||||
|
```
|
||||||
|
|
||||||
- **Cocos Creator**
|
### Project Structure
|
||||||
- **Laya 引擎**
|
|
||||||
- **原生 Web** - 浏览器环境直接运行
|
|
||||||
- **小游戏平台** - 微信、支付宝等小游戏
|
|
||||||
|
|
||||||
## ECS Framework Editor
|
```
|
||||||
|
ecs-framework/
|
||||||
|
├── packages/ Engine packages (runtime, editor, platform)
|
||||||
|
├── docs/ Documentation source
|
||||||
|
├── examples/ Example projects
|
||||||
|
├── scripts/ Build utilities
|
||||||
|
└── thirdparty/ Third-party dependencies
|
||||||
|
```
|
||||||
|
|
||||||
跨平台桌面编辑器,提供可视化开发和调试工具。
|
## Documentation
|
||||||
|
|
||||||
### 主要功能
|
- [Getting Started](https://esengine.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
|
||||||
|
|
||||||
[](https://github.com/esengine/ecs-framework/releases/latest)
|
## Contributing
|
||||||
|
|
||||||
支持 Windows、macOS (Intel & Apple Silicon)
|
Contributions are welcome. Please read the contributing guidelines before submitting a pull request.
|
||||||
|
|
||||||
### 截图
|
1. Fork the repository
|
||||||
|
2. Create a feature branch
|
||||||
|
3. Make changes with tests
|
||||||
|
4. Submit a pull request
|
||||||
|
|
||||||
<img src="screenshots/main_screetshot.png" alt="ECS Framework Editor" width="800">
|
## License
|
||||||
|
|
||||||
<details>
|
ESEngine is licensed under the [MIT License](LICENSE).
|
||||||
<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
README_CN.md
Normal file
246
README_CN.md
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
# 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) 开源。
|
||||||
@@ -9,6 +9,184 @@ const corePackageJson = JSON.parse(
|
|||||||
readFileSync(join(__dirname, '../../packages/core/package.json'), 'utf-8')
|
readFileSync(join(__dirname, '../../packages/core/package.json'), 'utf-8')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Import i18n messages
|
||||||
|
import en from './i18n/en.json' with { type: 'json' }
|
||||||
|
import zh from './i18n/zh.json' with { type: 'json' }
|
||||||
|
|
||||||
|
// 创建侧边栏配置 | Create sidebar config
|
||||||
|
// prefix: 路径前缀,如 '' 或 '/en' | Path prefix like '' or '/en'
|
||||||
|
function createSidebar(t, prefix = '') {
|
||||||
|
return {
|
||||||
|
[`${prefix}/guide/`]: [
|
||||||
|
{
|
||||||
|
text: t.sidebar.gettingStarted,
|
||||||
|
items: [
|
||||||
|
{ text: t.sidebar.quickStart, link: `${prefix}/guide/getting-started` },
|
||||||
|
{ text: t.sidebar.guideOverview, link: `${prefix}/guide/` }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t.sidebar.coreConcepts,
|
||||||
|
collapsed: false,
|
||||||
|
items: [
|
||||||
|
{ text: t.sidebar.entity, link: `${prefix}/guide/entity` },
|
||||||
|
{ text: t.sidebar.hierarchy, link: `${prefix}/guide/hierarchy` },
|
||||||
|
{ text: t.sidebar.component, link: `${prefix}/guide/component` },
|
||||||
|
{ text: t.sidebar.entityQuery, link: `${prefix}/guide/entity-query` },
|
||||||
|
{
|
||||||
|
text: t.sidebar.system,
|
||||||
|
link: `${prefix}/guide/system`,
|
||||||
|
items: [
|
||||||
|
{ text: t.sidebar.workerSystem, link: `${prefix}/guide/worker-system` }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t.sidebar.scene,
|
||||||
|
link: `${prefix}/guide/scene`,
|
||||||
|
items: [
|
||||||
|
{ text: t.sidebar.sceneManager, link: `${prefix}/guide/scene-manager` },
|
||||||
|
{ text: t.sidebar.worldManager, link: `${prefix}/guide/world-manager` }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t.sidebar.behaviorTree,
|
||||||
|
link: `${prefix}/guide/behavior-tree/`,
|
||||||
|
items: [
|
||||||
|
{ text: t.sidebar.btGettingStarted, link: `${prefix}/guide/behavior-tree/getting-started` },
|
||||||
|
{ text: t.sidebar.btCoreConcepts, link: `${prefix}/guide/behavior-tree/core-concepts` },
|
||||||
|
{ text: t.sidebar.btEditorGuide, link: `${prefix}/guide/behavior-tree/editor-guide` },
|
||||||
|
{ text: t.sidebar.btEditorWorkflow, link: `${prefix}/guide/behavior-tree/editor-workflow` },
|
||||||
|
{ text: t.sidebar.btCustomActions, link: `${prefix}/guide/behavior-tree/custom-actions` },
|
||||||
|
{ text: t.sidebar.btCocosIntegration, link: `${prefix}/guide/behavior-tree/cocos-integration` },
|
||||||
|
{ text: t.sidebar.btLayaIntegration, link: `${prefix}/guide/behavior-tree/laya-integration` },
|
||||||
|
{ text: t.sidebar.btAdvancedUsage, link: `${prefix}/guide/behavior-tree/advanced-usage` },
|
||||||
|
{ text: t.sidebar.btBestPractices, link: `${prefix}/guide/behavior-tree/best-practices` }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ text: t.sidebar.serialization, link: `${prefix}/guide/serialization` },
|
||||||
|
{ text: t.sidebar.eventSystem, link: `${prefix}/guide/event-system` },
|
||||||
|
{ text: t.sidebar.timeAndTimers, link: `${prefix}/guide/time-and-timers` },
|
||||||
|
{ text: t.sidebar.logging, link: `${prefix}/guide/logging` }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t.sidebar.advancedFeatures,
|
||||||
|
collapsed: false,
|
||||||
|
items: [
|
||||||
|
{ text: t.sidebar.serviceContainer, link: `${prefix}/guide/service-container` },
|
||||||
|
{ text: t.sidebar.pluginSystem, link: `${prefix}/guide/plugin-system` }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t.sidebar.platformAdapters,
|
||||||
|
link: `${prefix}/guide/platform-adapter`,
|
||||||
|
collapsed: false,
|
||||||
|
items: [
|
||||||
|
{ text: t.sidebar.browserAdapter, link: `${prefix}/guide/platform-adapter/browser` },
|
||||||
|
{ text: t.sidebar.wechatAdapter, link: `${prefix}/guide/platform-adapter/wechat-minigame` },
|
||||||
|
{ text: t.sidebar.nodejsAdapter, link: `${prefix}/guide/platform-adapter/nodejs` }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[`${prefix}/examples/`]: [
|
||||||
|
{
|
||||||
|
text: t.sidebar.examples,
|
||||||
|
items: [
|
||||||
|
{ text: t.sidebar.examplesOverview, link: `${prefix}/examples/` },
|
||||||
|
{ text: t.nav.workerDemo, link: `${prefix}/examples/worker-system-demo` }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[`${prefix}/api/`]: [
|
||||||
|
{
|
||||||
|
text: t.sidebar.apiReference,
|
||||||
|
items: [
|
||||||
|
{ text: t.sidebar.overview, link: `${prefix}/api/README` },
|
||||||
|
{
|
||||||
|
text: t.sidebar.coreClasses,
|
||||||
|
collapsed: false,
|
||||||
|
items: [
|
||||||
|
{ text: 'Core', link: `${prefix}/api/classes/Core` },
|
||||||
|
{ text: 'Scene', link: `${prefix}/api/classes/Scene` },
|
||||||
|
{ text: 'World', link: `${prefix}/api/classes/World` },
|
||||||
|
{ text: 'Entity', link: `${prefix}/api/classes/Entity` },
|
||||||
|
{ text: 'Component', link: `${prefix}/api/classes/Component` },
|
||||||
|
{ text: 'EntitySystem', link: `${prefix}/api/classes/EntitySystem` }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t.sidebar.systemClasses,
|
||||||
|
collapsed: true,
|
||||||
|
items: [
|
||||||
|
{ text: 'PassiveSystem', link: `${prefix}/api/classes/PassiveSystem` },
|
||||||
|
{ text: 'ProcessingSystem', link: `${prefix}/api/classes/ProcessingSystem` },
|
||||||
|
{ text: 'IntervalSystem', link: `${prefix}/api/classes/IntervalSystem` }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t.sidebar.utilities,
|
||||||
|
collapsed: true,
|
||||||
|
items: [
|
||||||
|
{ text: 'Matcher', link: `${prefix}/api/classes/Matcher` },
|
||||||
|
{ text: 'Time', link: `${prefix}/api/classes/Time` },
|
||||||
|
{ text: 'PerformanceMonitor', link: `${prefix}/api/classes/PerformanceMonitor` },
|
||||||
|
{ text: 'DebugManager', link: `${prefix}/api/classes/DebugManager` }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t.sidebar.interfaces,
|
||||||
|
collapsed: true,
|
||||||
|
items: [
|
||||||
|
{ text: 'IScene', link: `${prefix}/api/interfaces/IScene` },
|
||||||
|
{ text: 'IComponent', link: `${prefix}/api/interfaces/IComponent` },
|
||||||
|
{ text: 'ISystemBase', link: `${prefix}/api/interfaces/ISystemBase` },
|
||||||
|
{ text: 'ICoreConfig', link: `${prefix}/api/interfaces/ICoreConfig` }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t.sidebar.decorators,
|
||||||
|
collapsed: true,
|
||||||
|
items: [
|
||||||
|
{ text: '@ECSComponent', link: `${prefix}/api/functions/ECSComponent` },
|
||||||
|
{ text: '@ECSSystem', link: `${prefix}/api/functions/ECSSystem` }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t.sidebar.enums,
|
||||||
|
collapsed: true,
|
||||||
|
items: [
|
||||||
|
{ text: 'ECSEventType', link: `${prefix}/api/enumerations/ECSEventType` },
|
||||||
|
{ text: 'LogLevel', link: `${prefix}/api/enumerations/LogLevel` }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建导航配置 | Create nav config
|
||||||
|
// prefix: 路径前缀,如 '' 或 '/en' | Path prefix like '' or '/en'
|
||||||
|
function createNav(t, prefix = '') {
|
||||||
|
return [
|
||||||
|
{ text: t.nav.home, link: `${prefix}/` },
|
||||||
|
{ text: t.nav.quickStart, link: `${prefix}/guide/getting-started` },
|
||||||
|
{ text: t.nav.guide, link: `${prefix}/guide/` },
|
||||||
|
{ text: t.nav.api, link: `${prefix}/api/README` },
|
||||||
|
{
|
||||||
|
text: t.nav.examples,
|
||||||
|
items: [
|
||||||
|
{ text: t.nav.workerDemo, link: `${prefix}/examples/worker-system-demo` },
|
||||||
|
{ text: t.nav.lawnMowerDemo, link: 'https://github.com/esengine/lawn-mower-demo' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: `v${corePackageJson.version}`,
|
||||||
|
link: 'https://github.com/esengine/ecs-framework/releases'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
vite: {
|
vite: {
|
||||||
plugins: [
|
plugins: [
|
||||||
@@ -28,175 +206,49 @@ export default defineConfig({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
title: 'ECS Framework',
|
title: 'ESEngine',
|
||||||
description: '高性能TypeScript ECS框架 - 为游戏开发而生',
|
appearance: 'force-dark',
|
||||||
|
|
||||||
|
locales: {
|
||||||
|
root: {
|
||||||
|
label: '简体中文',
|
||||||
lang: 'zh-CN',
|
lang: 'zh-CN',
|
||||||
|
description: '高性能 TypeScript ECS 框架 - 为游戏开发而生',
|
||||||
|
themeConfig: {
|
||||||
|
nav: createNav(zh, ''),
|
||||||
|
sidebar: createSidebar(zh, ''),
|
||||||
|
editLink: {
|
||||||
|
pattern: 'https://github.com/esengine/ecs-framework/edit/master/docs/:path',
|
||||||
|
text: zh.common.editOnGithub
|
||||||
|
},
|
||||||
|
outline: {
|
||||||
|
level: [2, 3],
|
||||||
|
label: zh.common.onThisPage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
label: 'English',
|
||||||
|
lang: 'en',
|
||||||
|
link: '/en/',
|
||||||
|
description: 'High-performance TypeScript ECS Framework for Game Development',
|
||||||
|
themeConfig: {
|
||||||
|
nav: createNav(en, '/en'),
|
||||||
|
sidebar: createSidebar(en, '/en'),
|
||||||
|
editLink: {
|
||||||
|
pattern: 'https://github.com/esengine/ecs-framework/edit/master/docs/:path',
|
||||||
|
text: en.common.editOnGithub
|
||||||
|
},
|
||||||
|
outline: {
|
||||||
|
level: [2, 3],
|
||||||
|
label: en.common.onThisPage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
themeConfig: {
|
themeConfig: {
|
||||||
nav: [
|
siteTitle: 'ESEngine',
|
||||||
{ 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: [
|
socialLinks: [
|
||||||
{ icon: 'github', link: 'https://github.com/esengine/ecs-framework' }
|
{ icon: 'github', link: 'https://github.com/esengine/ecs-framework' }
|
||||||
@@ -207,18 +259,8 @@ export default defineConfig({
|
|||||||
copyright: 'Copyright © 2025 ECS Framework'
|
copyright: 'Copyright © 2025 ECS Framework'
|
||||||
},
|
},
|
||||||
|
|
||||||
editLink: {
|
|
||||||
pattern: 'https://github.com/esengine/ecs-framework/edit/master/docs/:path',
|
|
||||||
text: '在 GitHub 上编辑此页'
|
|
||||||
},
|
|
||||||
|
|
||||||
search: {
|
search: {
|
||||||
provider: 'local'
|
provider: 'local'
|
||||||
},
|
|
||||||
|
|
||||||
outline: {
|
|
||||||
level: [2, 3],
|
|
||||||
label: '目录'
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -227,7 +269,7 @@ export default defineConfig({
|
|||||||
['link', { rel: 'icon', href: '/favicon.ico' }]
|
['link', { rel: 'icon', href: '/favicon.ico' }]
|
||||||
],
|
],
|
||||||
|
|
||||||
base: '/ecs-framework/',
|
base: '/',
|
||||||
cleanUrls: true,
|
cleanUrls: true,
|
||||||
|
|
||||||
markdown: {
|
markdown: {
|
||||||
|
|||||||
85
docs/.vitepress/i18n/en.json
Normal file
85
docs/.vitepress/i18n/en.json
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"home": "Home",
|
||||||
|
"quickStart": "Quick Start",
|
||||||
|
"guide": "Guide",
|
||||||
|
"api": "API",
|
||||||
|
"examples": "Examples",
|
||||||
|
"workerDemo": "Worker System Demo",
|
||||||
|
"lawnMowerDemo": "Lawn Mower Demo"
|
||||||
|
},
|
||||||
|
"sidebar": {
|
||||||
|
"gettingStarted": "Getting Started",
|
||||||
|
"quickStart": "Quick Start",
|
||||||
|
"guideOverview": "Guide Overview",
|
||||||
|
"coreConcepts": "Core Concepts",
|
||||||
|
"entity": "Entity",
|
||||||
|
"hierarchy": "Hierarchy",
|
||||||
|
"component": "Component",
|
||||||
|
"entityQuery": "Entity Query",
|
||||||
|
"system": "System",
|
||||||
|
"workerSystem": "Worker System (Multithreading)",
|
||||||
|
"scene": "Scene",
|
||||||
|
"sceneManager": "SceneManager",
|
||||||
|
"worldManager": "WorldManager",
|
||||||
|
"behaviorTree": "Behavior Tree",
|
||||||
|
"btGettingStarted": "Getting Started",
|
||||||
|
"btCoreConcepts": "Core Concepts",
|
||||||
|
"btEditorGuide": "Editor Guide",
|
||||||
|
"btEditorWorkflow": "Editor Workflow",
|
||||||
|
"btCustomActions": "Custom Actions",
|
||||||
|
"btCocosIntegration": "Cocos Creator Integration",
|
||||||
|
"btLayaIntegration": "Laya Engine Integration",
|
||||||
|
"btAdvancedUsage": "Advanced Usage",
|
||||||
|
"btBestPractices": "Best Practices",
|
||||||
|
"serialization": "Serialization",
|
||||||
|
"eventSystem": "Event System",
|
||||||
|
"timeAndTimers": "Time and Timers",
|
||||||
|
"logging": "Logging",
|
||||||
|
"advancedFeatures": "Advanced Features",
|
||||||
|
"serviceContainer": "Service Container",
|
||||||
|
"pluginSystem": "Plugin System",
|
||||||
|
"platformAdapters": "Platform Adapters",
|
||||||
|
"browserAdapter": "Browser Adapter",
|
||||||
|
"wechatAdapter": "WeChat Mini Game Adapter",
|
||||||
|
"nodejsAdapter": "Node.js Adapter",
|
||||||
|
"examples": "Examples",
|
||||||
|
"examplesOverview": "Examples Overview",
|
||||||
|
"apiReference": "API Reference",
|
||||||
|
"overview": "Overview",
|
||||||
|
"coreClasses": "Core Classes",
|
||||||
|
"systemClasses": "System Classes",
|
||||||
|
"utilities": "Utilities",
|
||||||
|
"interfaces": "Interfaces",
|
||||||
|
"decorators": "Decorators",
|
||||||
|
"enums": "Enums"
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"title": "ESEngine - High-performance TypeScript ECS Framework",
|
||||||
|
"quickLinks": "Quick Links",
|
||||||
|
"viewDocs": "View Docs",
|
||||||
|
"getStarted": "Get Started",
|
||||||
|
"getStartedDesc": "From installation to your first ECS app, learn the core concepts in 5 minutes.",
|
||||||
|
"aiSystem": "AI System",
|
||||||
|
"behaviorTreeEditor": "Visual Behavior Tree Editor",
|
||||||
|
"behaviorTreeDesc": "Built-in AI behavior tree system with visual editing and real-time debugging.",
|
||||||
|
"coreFeatures": "Core Features",
|
||||||
|
"ecsArchitecture": "High-performance ECS Architecture",
|
||||||
|
"ecsArchitectureDesc": "Data-driven entity component system for large-scale entity processing with cache-friendly memory layout.",
|
||||||
|
"typeSupport": "Full Type Support",
|
||||||
|
"typeSupportDesc": "100% TypeScript with complete type definitions and compile-time checking for the best development experience.",
|
||||||
|
"visualBehaviorTree": "Visual Behavior Tree",
|
||||||
|
"visualBehaviorTreeDesc": "Built-in AI behavior tree system with visual editor, custom nodes, and real-time debugging.",
|
||||||
|
"multiPlatform": "Multi-Platform Support",
|
||||||
|
"multiPlatformDesc": "Support for browsers, Node.js, WeChat Mini Games, and seamless integration with major game engines.",
|
||||||
|
"modularDesign": "Modular Design",
|
||||||
|
"modularDesignDesc": "Core features packaged independently, import only what you need. Support for custom plugin extensions.",
|
||||||
|
"devTools": "Developer Tools",
|
||||||
|
"devToolsDesc": "Built-in performance monitoring, debugging tools, serialization system, and complete development toolchain.",
|
||||||
|
"learnMore": "Learn more →"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"editOnGithub": "Edit this page on GitHub",
|
||||||
|
"onThisPage": "On this page"
|
||||||
|
}
|
||||||
|
}
|
||||||
21
docs/.vitepress/i18n/index.ts
Normal file
21
docs/.vitepress/i18n/index.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import en from './en.json'
|
||||||
|
import zh from './zh.json'
|
||||||
|
|
||||||
|
export const messages = { en, zh }
|
||||||
|
|
||||||
|
export type Locale = 'en' | 'zh'
|
||||||
|
|
||||||
|
export function getLocaleMessages(locale: Locale) {
|
||||||
|
return messages[locale] || messages.en
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to get nested key value
|
||||||
|
export function t(messages: typeof en, key: string): string {
|
||||||
|
const keys = key.split('.')
|
||||||
|
let result: any = messages
|
||||||
|
for (const k of keys) {
|
||||||
|
result = result?.[k]
|
||||||
|
if (result === undefined) return key
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
85
docs/.vitepress/i18n/zh.json
Normal file
85
docs/.vitepress/i18n/zh.json
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"home": "首页",
|
||||||
|
"quickStart": "快速开始",
|
||||||
|
"guide": "指南",
|
||||||
|
"api": "API",
|
||||||
|
"examples": "示例",
|
||||||
|
"workerDemo": "Worker系统演示",
|
||||||
|
"lawnMowerDemo": "割草机演示"
|
||||||
|
},
|
||||||
|
"sidebar": {
|
||||||
|
"gettingStarted": "开始使用",
|
||||||
|
"quickStart": "快速开始",
|
||||||
|
"guideOverview": "指南概览",
|
||||||
|
"coreConcepts": "核心概念",
|
||||||
|
"entity": "实体类 (Entity)",
|
||||||
|
"hierarchy": "层级系统 (Hierarchy)",
|
||||||
|
"component": "组件系统 (Component)",
|
||||||
|
"entityQuery": "实体查询系统",
|
||||||
|
"system": "系统架构 (System)",
|
||||||
|
"workerSystem": "Worker系统 (多线程)",
|
||||||
|
"scene": "场景管理 (Scene)",
|
||||||
|
"sceneManager": "SceneManager",
|
||||||
|
"worldManager": "WorldManager",
|
||||||
|
"behaviorTree": "行为树系统 (Behavior Tree)",
|
||||||
|
"btGettingStarted": "快速开始",
|
||||||
|
"btCoreConcepts": "核心概念",
|
||||||
|
"btEditorGuide": "编辑器指南",
|
||||||
|
"btEditorWorkflow": "编辑器工作流",
|
||||||
|
"btCustomActions": "自定义动作组件",
|
||||||
|
"btCocosIntegration": "Cocos Creator集成",
|
||||||
|
"btLayaIntegration": "Laya引擎集成",
|
||||||
|
"btAdvancedUsage": "高级用法",
|
||||||
|
"btBestPractices": "最佳实践",
|
||||||
|
"serialization": "序列化系统 (Serialization)",
|
||||||
|
"eventSystem": "事件系统 (Event)",
|
||||||
|
"timeAndTimers": "时间和定时器 (Time)",
|
||||||
|
"logging": "日志系统 (Logger)",
|
||||||
|
"advancedFeatures": "高级特性",
|
||||||
|
"serviceContainer": "服务容器 (Service Container)",
|
||||||
|
"pluginSystem": "插件系统 (Plugin System)",
|
||||||
|
"platformAdapters": "平台适配器",
|
||||||
|
"browserAdapter": "浏览器适配器",
|
||||||
|
"wechatAdapter": "微信小游戏适配器",
|
||||||
|
"nodejsAdapter": "Node.js适配器",
|
||||||
|
"examples": "示例",
|
||||||
|
"examplesOverview": "示例概览",
|
||||||
|
"apiReference": "API 参考",
|
||||||
|
"overview": "概述",
|
||||||
|
"coreClasses": "核心类",
|
||||||
|
"systemClasses": "系统类",
|
||||||
|
"utilities": "工具类",
|
||||||
|
"interfaces": "接口",
|
||||||
|
"decorators": "装饰器",
|
||||||
|
"enums": "枚举"
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"title": "ESEngine - 高性能 TypeScript ECS 框架",
|
||||||
|
"quickLinks": "快速入口",
|
||||||
|
"viewDocs": "查看文档",
|
||||||
|
"getStarted": "快速开始",
|
||||||
|
"getStartedDesc": "从安装到创建第一个 ECS 应用,快速了解核心概念。",
|
||||||
|
"aiSystem": "AI 系统",
|
||||||
|
"behaviorTreeEditor": "行为树可视化编辑器",
|
||||||
|
"behaviorTreeDesc": "内置 AI 行为树系统,支持可视化编辑和实时调试。",
|
||||||
|
"coreFeatures": "核心特性",
|
||||||
|
"ecsArchitecture": "高性能 ECS 架构",
|
||||||
|
"ecsArchitectureDesc": "基于数据驱动的实体组件系统,支持大规模实体处理,缓存友好的内存布局。",
|
||||||
|
"typeSupport": "完整类型支持",
|
||||||
|
"typeSupportDesc": "100% TypeScript 编写,完整的类型定义和编译时检查,提供最佳的开发体验。",
|
||||||
|
"visualBehaviorTree": "可视化行为树",
|
||||||
|
"visualBehaviorTreeDesc": "内置 AI 行为树系统,提供可视化编辑器,支持自定义节点和实时调试。",
|
||||||
|
"multiPlatform": "多平台支持",
|
||||||
|
"multiPlatformDesc": "支持浏览器、Node.js、微信小游戏等多平台,可与主流游戏引擎无缝集成。",
|
||||||
|
"modularDesign": "模块化设计",
|
||||||
|
"modularDesignDesc": "核心功能独立打包,按需引入。支持自定义插件扩展,灵活适配不同项目。",
|
||||||
|
"devTools": "开发者工具",
|
||||||
|
"devToolsDesc": "内置性能监控、调试工具、序列化系统等,提供完整的开发工具链。",
|
||||||
|
"learnMore": "了解更多 →"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"editOnGithub": "在 GitHub 上编辑此页",
|
||||||
|
"onThisPage": "在这个页面上"
|
||||||
|
}
|
||||||
|
}
|
||||||
93
docs/.vitepress/theme/components/FeatureCard.vue
Normal file
93
docs/.vitepress/theme/components/FeatureCard.vue
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
title: String,
|
||||||
|
description: String,
|
||||||
|
icon: String,
|
||||||
|
link: String,
|
||||||
|
image: String
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<a :href="link" class="feature-card">
|
||||||
|
<div class="card-image" v-if="image">
|
||||||
|
<img :src="image" :alt="title" />
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="card-icon" v-if="icon && !image">{{ icon }}</div>
|
||||||
|
<h3 class="card-title">{{ title }}</h3>
|
||||||
|
<p class="card-description">{{ description }}</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.feature-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: var(--es-bg-elevated, #252526);
|
||||||
|
border: 1px solid var(--es-border-default, #3e3e42);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card:hover {
|
||||||
|
border-color: var(--es-primary, #007acc);
|
||||||
|
background: var(--es-bg-overlay, #2d2d2d);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 160px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: linear-gradient(135deg, #1e3a5f 0%, #1e1e1e 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-image img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card:hover .card-image img {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
padding: 16px;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--es-bg-input, #3c3c3c);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--es-text-inverse, #ffffff);
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-description {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--es-text-secondary, #9d9d9d);
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.6;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
422
docs/.vitepress/theme/components/ParticleHero.vue
Normal file
422
docs/.vitepress/theme/components/ParticleHero.vue
Normal file
@@ -0,0 +1,422 @@
|
|||||||
|
<script setup>
|
||||||
|
import { onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
|
||||||
|
const canvasRef = ref(null)
|
||||||
|
let animationId = null
|
||||||
|
let particles = []
|
||||||
|
let animationStartTime = null
|
||||||
|
let glowStartTime = null
|
||||||
|
|
||||||
|
// ESEngine 粒子颜色 - VS Code 风格配色(与编辑器统一)
|
||||||
|
const colors = ['#569CD6', '#4EC9B0', '#9CDCFE', '#C586C0', '#DCDCAA']
|
||||||
|
|
||||||
|
class Particle {
|
||||||
|
constructor(x, y, targetX, targetY) {
|
||||||
|
this.x = x
|
||||||
|
this.y = y
|
||||||
|
this.targetX = targetX
|
||||||
|
this.targetY = targetY
|
||||||
|
this.size = Math.random() * 2 + 1.5
|
||||||
|
this.alpha = Math.random() * 0.5 + 0.5
|
||||||
|
this.color = colors[Math.floor(Math.random() * colors.length)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createParticles(canvas, text, fontSize) {
|
||||||
|
const tempCanvas = document.createElement('canvas')
|
||||||
|
const tempCtx = tempCanvas.getContext('2d')
|
||||||
|
if (!tempCtx) return []
|
||||||
|
|
||||||
|
tempCtx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||||
|
const textMetrics = tempCtx.measureText(text)
|
||||||
|
const textWidth = textMetrics.width
|
||||||
|
const textHeight = fontSize
|
||||||
|
|
||||||
|
tempCanvas.width = textWidth + 40
|
||||||
|
tempCanvas.height = textHeight + 40
|
||||||
|
tempCtx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||||
|
tempCtx.textAlign = 'center'
|
||||||
|
tempCtx.textBaseline = 'middle'
|
||||||
|
tempCtx.fillStyle = '#ffffff'
|
||||||
|
tempCtx.fillText(text, tempCanvas.width / 2, tempCanvas.height / 2)
|
||||||
|
|
||||||
|
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height)
|
||||||
|
const pixels = imageData.data
|
||||||
|
const newParticles = []
|
||||||
|
const gap = 3
|
||||||
|
|
||||||
|
const width = canvas.width / (window.devicePixelRatio || 1)
|
||||||
|
const height = canvas.height / (window.devicePixelRatio || 1)
|
||||||
|
const offsetX = (width - tempCanvas.width) / 2
|
||||||
|
const offsetY = (height - tempCanvas.height) / 2
|
||||||
|
|
||||||
|
for (let y = 0; y < tempCanvas.height; y += gap) {
|
||||||
|
for (let x = 0; x < tempCanvas.width; x += gap) {
|
||||||
|
const index = (y * tempCanvas.width + x) * 4
|
||||||
|
const alpha = pixels[index + 3] || 0
|
||||||
|
|
||||||
|
if (alpha > 128) {
|
||||||
|
const angle = Math.random() * Math.PI * 2
|
||||||
|
const distance = Math.random() * Math.max(width, height)
|
||||||
|
|
||||||
|
newParticles.push(new Particle(
|
||||||
|
width / 2 + Math.cos(angle) * distance,
|
||||||
|
height / 2 + Math.sin(angle) * distance,
|
||||||
|
offsetX + x,
|
||||||
|
offsetY + y
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newParticles
|
||||||
|
}
|
||||||
|
|
||||||
|
function easeOutQuart(t) {
|
||||||
|
return 1 - Math.pow(1 - t, 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
function easeOutCubic(t) {
|
||||||
|
return 1 - Math.pow(1 - t, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
function animate(canvas, ctx) {
|
||||||
|
const dpr = window.devicePixelRatio || 1
|
||||||
|
const width = canvas.width / dpr
|
||||||
|
const height = canvas.height / dpr
|
||||||
|
|
||||||
|
const currentTime = performance.now()
|
||||||
|
const duration = 2500
|
||||||
|
const glowDuration = 600
|
||||||
|
|
||||||
|
const elapsed = currentTime - animationStartTime
|
||||||
|
const progress = Math.min(elapsed / duration, 1)
|
||||||
|
const easedProgress = easeOutQuart(progress)
|
||||||
|
|
||||||
|
// 透明背景
|
||||||
|
ctx.clearRect(0, 0, width, height)
|
||||||
|
|
||||||
|
// 计算发光进度
|
||||||
|
let glowProgress = 0
|
||||||
|
if (progress >= 1) {
|
||||||
|
if (glowStartTime === null) {
|
||||||
|
glowStartTime = currentTime
|
||||||
|
}
|
||||||
|
glowProgress = Math.min((currentTime - glowStartTime) / glowDuration, 1)
|
||||||
|
glowProgress = easeOutCubic(glowProgress)
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = 'ESEngine'
|
||||||
|
const fontSize = Math.min(width / 4, height / 3, 80)
|
||||||
|
const textY = height / 2
|
||||||
|
|
||||||
|
for (const particle of particles) {
|
||||||
|
const moveProgress = Math.min(easedProgress * 1.2, 1)
|
||||||
|
const currentX = particle.x + (particle.targetX - particle.x) * moveProgress
|
||||||
|
const currentY = particle.y + (particle.targetY - particle.y) * moveProgress
|
||||||
|
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(currentX, currentY, particle.size, 0, Math.PI * 2)
|
||||||
|
ctx.fillStyle = particle.color
|
||||||
|
ctx.globalAlpha = particle.alpha * (1 - glowProgress * 0.3)
|
||||||
|
ctx.fill()
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.globalAlpha = 1
|
||||||
|
|
||||||
|
if (glowProgress > 0) {
|
||||||
|
ctx.save()
|
||||||
|
ctx.shadowColor = '#3b9eff'
|
||||||
|
ctx.shadowBlur = 30 * glowProgress
|
||||||
|
ctx.fillStyle = `rgba(255, 255, 255, ${glowProgress})`
|
||||||
|
ctx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||||
|
ctx.textAlign = 'center'
|
||||||
|
ctx.textBaseline = 'middle'
|
||||||
|
ctx.fillText(text, width / 2, textY)
|
||||||
|
ctx.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (glowProgress >= 1) {
|
||||||
|
const breathe = 0.8 + Math.sin(currentTime / 1000) * 0.2
|
||||||
|
ctx.save()
|
||||||
|
ctx.shadowColor = '#3b9eff'
|
||||||
|
ctx.shadowBlur = 20 * breathe
|
||||||
|
ctx.fillStyle = '#ffffff'
|
||||||
|
ctx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||||
|
ctx.textAlign = 'center'
|
||||||
|
ctx.textBaseline = 'middle'
|
||||||
|
ctx.fillText(text, width / 2, textY)
|
||||||
|
ctx.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
animationId = requestAnimationFrame(() => animate(canvas, ctx))
|
||||||
|
}
|
||||||
|
|
||||||
|
function initCanvas() {
|
||||||
|
const canvas = canvasRef.value
|
||||||
|
if (!canvas) return
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
if (!ctx) return
|
||||||
|
|
||||||
|
const dpr = window.devicePixelRatio || 1
|
||||||
|
const container = canvas.parentElement
|
||||||
|
const width = container.offsetWidth
|
||||||
|
const height = container.offsetHeight
|
||||||
|
|
||||||
|
canvas.width = width * dpr
|
||||||
|
canvas.height = height * dpr
|
||||||
|
canvas.style.width = `${width}px`
|
||||||
|
canvas.style.height = `${height}px`
|
||||||
|
ctx.scale(dpr, dpr)
|
||||||
|
|
||||||
|
const text = 'ESEngine'
|
||||||
|
const fontSize = Math.min(width / 4, height / 3, 80)
|
||||||
|
|
||||||
|
particles = createParticles(canvas, text, fontSize)
|
||||||
|
animationStartTime = performance.now()
|
||||||
|
glowStartTime = null
|
||||||
|
|
||||||
|
if (animationId) {
|
||||||
|
cancelAnimationFrame(animationId)
|
||||||
|
}
|
||||||
|
|
||||||
|
animate(canvas, ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initCanvas()
|
||||||
|
window.addEventListener('resize', initCanvas)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (animationId) {
|
||||||
|
cancelAnimationFrame(animationId)
|
||||||
|
}
|
||||||
|
window.removeEventListener('resize', initCanvas)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="hero-section">
|
||||||
|
<div class="hero-container">
|
||||||
|
<!-- 左侧文字区域 -->
|
||||||
|
<div class="hero-text">
|
||||||
|
<div class="hero-logo">
|
||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="16" cy="16" r="14" stroke="#9147ff" stroke-width="2"/>
|
||||||
|
<path d="M10 10h8v2h-6v3h5v2h-5v3h6v2h-8v-12z" fill="#9147ff"/>
|
||||||
|
</svg>
|
||||||
|
<span>ESENGINE</span>
|
||||||
|
</div>
|
||||||
|
<h1 class="hero-title">
|
||||||
|
我们构建框架。<br/>
|
||||||
|
而你将创造游戏。
|
||||||
|
</h1>
|
||||||
|
<p class="hero-description">
|
||||||
|
ESEngine 是一个高性能的 TypeScript ECS 框架,为游戏开发者提供现代化的实体组件系统。
|
||||||
|
无论是 2D 还是 3D 游戏,都能帮助你快速构建可扩展的游戏架构。
|
||||||
|
</p>
|
||||||
|
<div class="hero-actions">
|
||||||
|
<a href="/guide/getting-started" class="btn-primary">开始使用</a>
|
||||||
|
<a href="https://github.com/esengine/ecs-framework" class="btn-secondary" target="_blank">了解更多</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧粒子动画区域 -->
|
||||||
|
<div class="hero-visual">
|
||||||
|
<div class="visual-container">
|
||||||
|
<canvas ref="canvasRef" class="particle-canvas"></canvas>
|
||||||
|
<div class="visual-label">
|
||||||
|
<span class="label-title">Entity Component System</span>
|
||||||
|
<span class="label-subtitle">High Performance Framework</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.hero-section {
|
||||||
|
background: #0d0d0d;
|
||||||
|
padding: 80px 0;
|
||||||
|
min-height: calc(100vh - 64px);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-container {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 48px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1.2fr;
|
||||||
|
gap: 64px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左侧文字 */
|
||||||
|
.hero-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
font-size: 3rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ffffff;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-description {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
color: #707070;
|
||||||
|
line-height: 1.7;
|
||||||
|
margin: 0;
|
||||||
|
max-width: 480px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary,
|
||||||
|
.btn-secondary {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 14px 28px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: #3b9eff;
|
||||||
|
color: #ffffff;
|
||||||
|
border: 1px solid #3b9eff;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: #5aadff;
|
||||||
|
border-color: #5aadff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: #a0a0a0;
|
||||||
|
border: 1px solid #2a2a2a;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #252525;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-visual {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visual-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
aspect-ratio: 4 / 3;
|
||||||
|
background: linear-gradient(135deg, #1a2a3a 0%, #1a1a1a 50%, #0d0d0d 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #2a2a2a;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 20px 60px rgba(59, 158, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.particle-canvas {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visual-label {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 24px;
|
||||||
|
left: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-title {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-subtitle {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #737373;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式 */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.hero-container {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 48px;
|
||||||
|
padding: 0 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section {
|
||||||
|
padding: 48px 0;
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
font-size: 2.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-description {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visual-container {
|
||||||
|
max-width: 100%;
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.hero-title {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary,
|
||||||
|
.btn-secondary {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
422
docs/.vitepress/theme/components/ParticleHeroEn.vue
Normal file
422
docs/.vitepress/theme/components/ParticleHeroEn.vue
Normal file
@@ -0,0 +1,422 @@
|
|||||||
|
<script setup>
|
||||||
|
import { onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
|
||||||
|
const canvasRef = ref(null)
|
||||||
|
let animationId = null
|
||||||
|
let particles = []
|
||||||
|
let animationStartTime = null
|
||||||
|
let glowStartTime = null
|
||||||
|
|
||||||
|
// ESEngine particle colors - VS Code style colors (unified with editor)
|
||||||
|
const colors = ['#569CD6', '#4EC9B0', '#9CDCFE', '#C586C0', '#DCDCAA']
|
||||||
|
|
||||||
|
class Particle {
|
||||||
|
constructor(x, y, targetX, targetY) {
|
||||||
|
this.x = x
|
||||||
|
this.y = y
|
||||||
|
this.targetX = targetX
|
||||||
|
this.targetY = targetY
|
||||||
|
this.size = Math.random() * 2 + 1.5
|
||||||
|
this.alpha = Math.random() * 0.5 + 0.5
|
||||||
|
this.color = colors[Math.floor(Math.random() * colors.length)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createParticles(canvas, text, fontSize) {
|
||||||
|
const tempCanvas = document.createElement('canvas')
|
||||||
|
const tempCtx = tempCanvas.getContext('2d')
|
||||||
|
if (!tempCtx) return []
|
||||||
|
|
||||||
|
tempCtx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||||
|
const textMetrics = tempCtx.measureText(text)
|
||||||
|
const textWidth = textMetrics.width
|
||||||
|
const textHeight = fontSize
|
||||||
|
|
||||||
|
tempCanvas.width = textWidth + 40
|
||||||
|
tempCanvas.height = textHeight + 40
|
||||||
|
tempCtx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||||
|
tempCtx.textAlign = 'center'
|
||||||
|
tempCtx.textBaseline = 'middle'
|
||||||
|
tempCtx.fillStyle = '#ffffff'
|
||||||
|
tempCtx.fillText(text, tempCanvas.width / 2, tempCanvas.height / 2)
|
||||||
|
|
||||||
|
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height)
|
||||||
|
const pixels = imageData.data
|
||||||
|
const newParticles = []
|
||||||
|
const gap = 3
|
||||||
|
|
||||||
|
const width = canvas.width / (window.devicePixelRatio || 1)
|
||||||
|
const height = canvas.height / (window.devicePixelRatio || 1)
|
||||||
|
const offsetX = (width - tempCanvas.width) / 2
|
||||||
|
const offsetY = (height - tempCanvas.height) / 2
|
||||||
|
|
||||||
|
for (let y = 0; y < tempCanvas.height; y += gap) {
|
||||||
|
for (let x = 0; x < tempCanvas.width; x += gap) {
|
||||||
|
const index = (y * tempCanvas.width + x) * 4
|
||||||
|
const alpha = pixels[index + 3] || 0
|
||||||
|
|
||||||
|
if (alpha > 128) {
|
||||||
|
const angle = Math.random() * Math.PI * 2
|
||||||
|
const distance = Math.random() * Math.max(width, height)
|
||||||
|
|
||||||
|
newParticles.push(new Particle(
|
||||||
|
width / 2 + Math.cos(angle) * distance,
|
||||||
|
height / 2 + Math.sin(angle) * distance,
|
||||||
|
offsetX + x,
|
||||||
|
offsetY + y
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newParticles
|
||||||
|
}
|
||||||
|
|
||||||
|
function easeOutQuart(t) {
|
||||||
|
return 1 - Math.pow(1 - t, 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
function easeOutCubic(t) {
|
||||||
|
return 1 - Math.pow(1 - t, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
function animate(canvas, ctx) {
|
||||||
|
const dpr = window.devicePixelRatio || 1
|
||||||
|
const width = canvas.width / dpr
|
||||||
|
const height = canvas.height / dpr
|
||||||
|
|
||||||
|
const currentTime = performance.now()
|
||||||
|
const duration = 2500
|
||||||
|
const glowDuration = 600
|
||||||
|
|
||||||
|
const elapsed = currentTime - animationStartTime
|
||||||
|
const progress = Math.min(elapsed / duration, 1)
|
||||||
|
const easedProgress = easeOutQuart(progress)
|
||||||
|
|
||||||
|
// Transparent background
|
||||||
|
ctx.clearRect(0, 0, width, height)
|
||||||
|
|
||||||
|
// Calculate glow progress
|
||||||
|
let glowProgress = 0
|
||||||
|
if (progress >= 1) {
|
||||||
|
if (glowStartTime === null) {
|
||||||
|
glowStartTime = currentTime
|
||||||
|
}
|
||||||
|
glowProgress = Math.min((currentTime - glowStartTime) / glowDuration, 1)
|
||||||
|
glowProgress = easeOutCubic(glowProgress)
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = 'ESEngine'
|
||||||
|
const fontSize = Math.min(width / 4, height / 3, 80)
|
||||||
|
const textY = height / 2
|
||||||
|
|
||||||
|
for (const particle of particles) {
|
||||||
|
const moveProgress = Math.min(easedProgress * 1.2, 1)
|
||||||
|
const currentX = particle.x + (particle.targetX - particle.x) * moveProgress
|
||||||
|
const currentY = particle.y + (particle.targetY - particle.y) * moveProgress
|
||||||
|
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(currentX, currentY, particle.size, 0, Math.PI * 2)
|
||||||
|
ctx.fillStyle = particle.color
|
||||||
|
ctx.globalAlpha = particle.alpha * (1 - glowProgress * 0.3)
|
||||||
|
ctx.fill()
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.globalAlpha = 1
|
||||||
|
|
||||||
|
if (glowProgress > 0) {
|
||||||
|
ctx.save()
|
||||||
|
ctx.shadowColor = '#3b9eff'
|
||||||
|
ctx.shadowBlur = 30 * glowProgress
|
||||||
|
ctx.fillStyle = `rgba(255, 255, 255, ${glowProgress})`
|
||||||
|
ctx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||||
|
ctx.textAlign = 'center'
|
||||||
|
ctx.textBaseline = 'middle'
|
||||||
|
ctx.fillText(text, width / 2, textY)
|
||||||
|
ctx.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (glowProgress >= 1) {
|
||||||
|
const breathe = 0.8 + Math.sin(currentTime / 1000) * 0.2
|
||||||
|
ctx.save()
|
||||||
|
ctx.shadowColor = '#3b9eff'
|
||||||
|
ctx.shadowBlur = 20 * breathe
|
||||||
|
ctx.fillStyle = '#ffffff'
|
||||||
|
ctx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||||
|
ctx.textAlign = 'center'
|
||||||
|
ctx.textBaseline = 'middle'
|
||||||
|
ctx.fillText(text, width / 2, textY)
|
||||||
|
ctx.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
animationId = requestAnimationFrame(() => animate(canvas, ctx))
|
||||||
|
}
|
||||||
|
|
||||||
|
function initCanvas() {
|
||||||
|
const canvas = canvasRef.value
|
||||||
|
if (!canvas) return
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
if (!ctx) return
|
||||||
|
|
||||||
|
const dpr = window.devicePixelRatio || 1
|
||||||
|
const container = canvas.parentElement
|
||||||
|
const width = container.offsetWidth
|
||||||
|
const height = container.offsetHeight
|
||||||
|
|
||||||
|
canvas.width = width * dpr
|
||||||
|
canvas.height = height * dpr
|
||||||
|
canvas.style.width = `${width}px`
|
||||||
|
canvas.style.height = `${height}px`
|
||||||
|
ctx.scale(dpr, dpr)
|
||||||
|
|
||||||
|
const text = 'ESEngine'
|
||||||
|
const fontSize = Math.min(width / 4, height / 3, 80)
|
||||||
|
|
||||||
|
particles = createParticles(canvas, text, fontSize)
|
||||||
|
animationStartTime = performance.now()
|
||||||
|
glowStartTime = null
|
||||||
|
|
||||||
|
if (animationId) {
|
||||||
|
cancelAnimationFrame(animationId)
|
||||||
|
}
|
||||||
|
|
||||||
|
animate(canvas, ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initCanvas()
|
||||||
|
window.addEventListener('resize', initCanvas)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (animationId) {
|
||||||
|
cancelAnimationFrame(animationId)
|
||||||
|
}
|
||||||
|
window.removeEventListener('resize', initCanvas)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="hero-section">
|
||||||
|
<div class="hero-container">
|
||||||
|
<!-- Left text area -->
|
||||||
|
<div class="hero-text">
|
||||||
|
<div class="hero-logo">
|
||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="16" cy="16" r="14" stroke="#9147ff" stroke-width="2"/>
|
||||||
|
<path d="M10 10h8v2h-6v3h5v2h-5v3h6v2h-8v-12z" fill="#9147ff"/>
|
||||||
|
</svg>
|
||||||
|
<span>ESENGINE</span>
|
||||||
|
</div>
|
||||||
|
<h1 class="hero-title">
|
||||||
|
We build the framework.<br/>
|
||||||
|
You create the game.
|
||||||
|
</h1>
|
||||||
|
<p class="hero-description">
|
||||||
|
ESEngine is a high-performance TypeScript ECS framework for game developers.
|
||||||
|
Whether 2D or 3D games, it helps you build scalable game architecture quickly.
|
||||||
|
</p>
|
||||||
|
<div class="hero-actions">
|
||||||
|
<a href="/en/guide/getting-started" class="btn-primary">Get Started</a>
|
||||||
|
<a href="https://github.com/esengine/ecs-framework" class="btn-secondary" target="_blank">Learn More</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right particle animation area -->
|
||||||
|
<div class="hero-visual">
|
||||||
|
<div class="visual-container">
|
||||||
|
<canvas ref="canvasRef" class="particle-canvas"></canvas>
|
||||||
|
<div class="visual-label">
|
||||||
|
<span class="label-title">Entity Component System</span>
|
||||||
|
<span class="label-subtitle">High Performance Framework</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.hero-section {
|
||||||
|
background: #0d0d0d;
|
||||||
|
padding: 80px 0;
|
||||||
|
min-height: calc(100vh - 64px);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-container {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 48px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1.2fr;
|
||||||
|
gap: 64px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Left text */
|
||||||
|
.hero-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
font-size: 3rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ffffff;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-description {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
color: #707070;
|
||||||
|
line-height: 1.7;
|
||||||
|
margin: 0;
|
||||||
|
max-width: 480px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary,
|
||||||
|
.btn-secondary {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 14px 28px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: #3b9eff;
|
||||||
|
color: #ffffff;
|
||||||
|
border: 1px solid #3b9eff;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: #5aadff;
|
||||||
|
border-color: #5aadff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: #a0a0a0;
|
||||||
|
border: 1px solid #2a2a2a;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #252525;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-visual {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visual-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
aspect-ratio: 4 / 3;
|
||||||
|
background: linear-gradient(135deg, #1a2a3a 0%, #1a1a1a 50%, #0d0d0d 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #2a2a2a;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 20px 60px rgba(59, 158, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.particle-canvas {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visual-label {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 24px;
|
||||||
|
left: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-title {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-subtitle {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #737373;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.hero-container {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 48px;
|
||||||
|
padding: 0 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section {
|
||||||
|
padding: 48px 0;
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
font-size: 2.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-description {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visual-container {
|
||||||
|
max-width: 100%;
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.hero-title {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary,
|
||||||
|
.btn-secondary {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
594
docs/.vitepress/theme/custom.css
Normal file
594
docs/.vitepress/theme/custom.css
Normal file
@@ -0,0 +1,594 @@
|
|||||||
|
:root {
|
||||||
|
color-scheme: dark;
|
||||||
|
--vp-nav-height: 64px;
|
||||||
|
|
||||||
|
--es-bg-base: #1e1e1e;
|
||||||
|
--es-bg-elevated: #252526;
|
||||||
|
--es-bg-overlay: #2d2d2d;
|
||||||
|
--es-bg-input: #3c3c3c;
|
||||||
|
--es-bg-inset: #181818;
|
||||||
|
--es-bg-hover: #2a2d2e;
|
||||||
|
--es-bg-active: #37373d;
|
||||||
|
--es-bg-sidebar: #262626;
|
||||||
|
--es-bg-card: #2a2a2a;
|
||||||
|
--es-bg-header: #2d2d2d;
|
||||||
|
|
||||||
|
--es-text-primary: #cccccc;
|
||||||
|
--es-text-secondary: #9d9d9d;
|
||||||
|
--es-text-tertiary: #6a6a6a;
|
||||||
|
--es-text-inverse: #ffffff;
|
||||||
|
--es-text-muted: #aaaaaa;
|
||||||
|
--es-text-dim: #6a6a6a;
|
||||||
|
|
||||||
|
--es-font-xs: 11px;
|
||||||
|
--es-font-sm: 12px;
|
||||||
|
--es-font-base: 13px;
|
||||||
|
--es-font-md: 14px;
|
||||||
|
--es-font-lg: 16px;
|
||||||
|
|
||||||
|
--es-border-default: #3a3a3a;
|
||||||
|
--es-border-subtle: #1a1a1a;
|
||||||
|
--es-border-strong: #4a4a4a;
|
||||||
|
|
||||||
|
--es-primary: #3b82f6;
|
||||||
|
--es-primary-hover: #2563eb;
|
||||||
|
--es-success: #4ade80;
|
||||||
|
--es-warning: #f59e0b;
|
||||||
|
--es-error: #ef4444;
|
||||||
|
--es-info: #3b82f6;
|
||||||
|
|
||||||
|
--es-selected: #3d5a80;
|
||||||
|
--es-selected-hover: #4a6a90;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--es-bg-base) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
html.dark {
|
||||||
|
--vp-c-bg: var(--es-bg-base);
|
||||||
|
--vp-c-bg-soft: var(--es-bg-elevated);
|
||||||
|
--vp-c-bg-mute: var(--es-bg-overlay);
|
||||||
|
--vp-c-bg-alt: var(--es-bg-sidebar);
|
||||||
|
--vp-c-text-1: var(--es-text-primary);
|
||||||
|
--vp-c-text-2: var(--es-text-tertiary);
|
||||||
|
--vp-c-text-3: var(--es-text-muted);
|
||||||
|
--vp-c-divider: var(--es-border-default);
|
||||||
|
--vp-c-divider-light: var(--es-border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
html:not(.dark) {
|
||||||
|
--vp-c-bg: var(--es-bg-base) !important;
|
||||||
|
--vp-c-bg-soft: var(--es-bg-elevated) !important;
|
||||||
|
--vp-c-bg-mute: var(--es-bg-overlay) !important;
|
||||||
|
--vp-c-bg-alt: var(--es-bg-sidebar) !important;
|
||||||
|
--vp-c-text-1: var(--es-text-primary) !important;
|
||||||
|
--vp-c-text-2: var(--es-text-tertiary) !important;
|
||||||
|
--vp-c-text-3: var(--es-text-muted) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPNav {
|
||||||
|
background: var(--es-bg-header) !important;
|
||||||
|
border-bottom: 1px solid var(--es-border-subtle) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPNav .VPNavBar {
|
||||||
|
background: var(--es-bg-header) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPNav .VPNavBar .wrapper {
|
||||||
|
background: var(--es-bg-header) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPNav .VPNavBar::before,
|
||||||
|
.VPNav .VPNavBar::after {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPNavBar {
|
||||||
|
background: var(--es-bg-header) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPNavBar::before {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPNavBarTitle .title {
|
||||||
|
color: var(--es-text-primary);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: var(--es-font-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPNavBarMenuLink {
|
||||||
|
color: var(--es-text-secondary) !important;
|
||||||
|
font-size: var(--es-font-sm) !important;
|
||||||
|
font-weight: 400 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPNavBarMenuLink:hover {
|
||||||
|
color: var(--es-text-primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPNavBarMenuLink.active {
|
||||||
|
color: var(--es-text-primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPNavBarSearch .DocSearch-Button {
|
||||||
|
background: var(--es-bg-input) !important;
|
||||||
|
border: 1px solid var(--es-border-default) !important;
|
||||||
|
border-radius: 2px;
|
||||||
|
height: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPSidebar {
|
||||||
|
background: var(--es-bg-sidebar) !important;
|
||||||
|
border-right: 1px solid var(--es-border-subtle) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPSidebarItem.level-0 > .item {
|
||||||
|
padding: 8px 0 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPSidebarItem.level-0 > .item > .text {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: var(--es-font-xs);
|
||||||
|
color: var(--es-text-secondary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPSidebarItem .link {
|
||||||
|
padding: 4px 8px;
|
||||||
|
margin: 1px 0;
|
||||||
|
border-radius: 2px;
|
||||||
|
color: var(--es-text-primary);
|
||||||
|
font-size: var(--es-font-sm);
|
||||||
|
transition: all 0.1s ease;
|
||||||
|
border-left: 2px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPSidebarItem .link:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
color: var(--es-text-inverse);
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPSidebarItem.is-active > .item > .link {
|
||||||
|
background: var(--es-selected);
|
||||||
|
color: var(--es-text-inverse);
|
||||||
|
border-left: 2px solid var(--es-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPSidebarItem.is-active > .item > .link:hover {
|
||||||
|
background: var(--es-selected-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPSidebarItem.level-1 .link {
|
||||||
|
padding-left: 20px;
|
||||||
|
font-size: var(--es-font-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPSidebarItem.level-2 .link {
|
||||||
|
padding-left: 32px;
|
||||||
|
font-size: var(--es-font-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPSidebarItem .caret {
|
||||||
|
color: var(--es-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPSidebarItem .caret:hover {
|
||||||
|
color: var(--es-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPContent {
|
||||||
|
background: var(--es-bg-card) !important;
|
||||||
|
padding-top: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPContent.has-sidebar {
|
||||||
|
background: var(--es-bg-card) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 首页布局修复 | Home page layout fix */
|
||||||
|
.VPPage {
|
||||||
|
padding-top: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Layout > .VPContent {
|
||||||
|
padding-top: var(--vp-nav-height) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPDoc {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPNavBar .content {
|
||||||
|
background: var(--es-bg-header) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPNavBar .content-body {
|
||||||
|
background: var(--es-bg-header) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPNavBar .divider {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPLocalNav {
|
||||||
|
background: var(--es-bg-header) !important;
|
||||||
|
border-bottom: 1px solid var(--es-border-subtle) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPNavScreenMenu {
|
||||||
|
background: var(--es-bg-base) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPNavScreen {
|
||||||
|
background: var(--es-bg-base) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.curtain {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPNav .curtain,
|
||||||
|
.VPNavBar .curtain {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[class*="curtain"] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPNav > div::before,
|
||||||
|
.VPNav > div::after {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc {
|
||||||
|
color: var(--es-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc h1 {
|
||||||
|
font-size: var(--es-font-lg);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--es-text-inverse);
|
||||||
|
border-bottom: none;
|
||||||
|
padding-bottom: 0;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc h2 {
|
||||||
|
font-size: var(--es-font-md);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--es-text-inverse);
|
||||||
|
border-bottom: none;
|
||||||
|
padding-bottom: 0;
|
||||||
|
margin-top: 32px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: var(--es-bg-header);
|
||||||
|
border-left: 3px solid var(--es-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc h3 {
|
||||||
|
font-size: var(--es-font-base);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--es-text-primary);
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc p {
|
||||||
|
color: var(--es-text-primary);
|
||||||
|
line-height: 1.7;
|
||||||
|
font-size: var(--es-font-base);
|
||||||
|
margin: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc ul,
|
||||||
|
.vp-doc ol {
|
||||||
|
padding-left: 20px;
|
||||||
|
margin: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc li {
|
||||||
|
line-height: 1.7;
|
||||||
|
margin: 4px 0;
|
||||||
|
color: var(--es-text-primary);
|
||||||
|
font-size: var(--es-font-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc li::marker {
|
||||||
|
color: var(--es-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc strong {
|
||||||
|
color: var(--es-text-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc a {
|
||||||
|
color: var(--es-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPDocAside {
|
||||||
|
padding-left: 16px;
|
||||||
|
border-left: 1px solid var(--es-border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPDocAsideOutline {
|
||||||
|
padding: 0;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPDocAsideOutline .content {
|
||||||
|
border: none !important;
|
||||||
|
padding-left: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPDocAsideOutline .outline-title {
|
||||||
|
font-size: var(--es-font-xs);
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: var(--es-text-secondary);
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPDocAsideOutline .outline-link {
|
||||||
|
color: var(--es-text-secondary);
|
||||||
|
font-size: var(--es-font-xs);
|
||||||
|
padding: 4px 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPDocAsideOutline .outline-link:hover {
|
||||||
|
color: var(--es-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPDocAsideOutline .outline-link.active {
|
||||||
|
color: var(--es-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPDocAsideOutline .outline-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div[class*='language-'] {
|
||||||
|
background: var(--es-bg-inset) !important;
|
||||||
|
border: 1px solid var(--es-border-default);
|
||||||
|
border-radius: 2px;
|
||||||
|
margin: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-code-group .tabs {
|
||||||
|
background: var(--es-bg-header);
|
||||||
|
border-bottom: 1px solid var(--es-border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc :not(pre) > code {
|
||||||
|
background: var(--es-bg-input);
|
||||||
|
color: var(--es-primary);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 2px;
|
||||||
|
font-size: var(--es-font-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc table {
|
||||||
|
display: table;
|
||||||
|
width: 100%;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 16px 0;
|
||||||
|
font-size: var(--es-font-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc tr {
|
||||||
|
border-bottom: 1px solid var(--es-border-subtle);
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc tr:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc tr:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc th {
|
||||||
|
background: var(--es-bg-header);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: var(--es-font-xs);
|
||||||
|
color: var(--es-text-secondary);
|
||||||
|
text-align: left;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-bottom: 1px solid var(--es-border-subtle);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc td {
|
||||||
|
font-size: var(--es-font-sm);
|
||||||
|
color: var(--es-text-primary);
|
||||||
|
padding: 8px 12px;
|
||||||
|
vertical-align: top;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc td:first-child {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--es-text-primary);
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc .warning,
|
||||||
|
.vp-doc .custom-block.warning {
|
||||||
|
background: rgba(245, 158, 11, 0.08);
|
||||||
|
border: none;
|
||||||
|
border-left: 3px solid var(--es-warning);
|
||||||
|
border-radius: 0 2px 2px 0;
|
||||||
|
padding: 10px 12px;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc .warning .custom-block-title,
|
||||||
|
.vp-doc .custom-block.warning .custom-block-title {
|
||||||
|
color: var(--es-warning);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: var(--es-font-xs);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc .warning p {
|
||||||
|
color: var(--es-text-primary);
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--es-font-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc .tip,
|
||||||
|
.vp-doc .custom-block.tip {
|
||||||
|
background: rgba(59, 130, 246, 0.08);
|
||||||
|
border: none;
|
||||||
|
border-left: 3px solid var(--es-primary);
|
||||||
|
border-radius: 0 2px 2px 0;
|
||||||
|
padding: 10px 12px;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc .tip .custom-block-title,
|
||||||
|
.vp-doc .custom-block.tip .custom-block-title {
|
||||||
|
color: var(--es-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: var(--es-font-xs);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc .tip p {
|
||||||
|
color: var(--es-text-primary);
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--es-font-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc .info,
|
||||||
|
.vp-doc .custom-block.info {
|
||||||
|
background: rgba(74, 222, 128, 0.08);
|
||||||
|
border: none;
|
||||||
|
border-left: 3px solid var(--es-success);
|
||||||
|
border-radius: 0 2px 2px 0;
|
||||||
|
padding: 10px 12px;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc .info .custom-block-title,
|
||||||
|
.vp-doc .custom-block.info .custom-block-title {
|
||||||
|
color: var(--es-success);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: var(--es-font-xs);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc .danger,
|
||||||
|
.vp-doc .custom-block.danger {
|
||||||
|
background: rgba(239, 68, 68, 0.08);
|
||||||
|
border: none;
|
||||||
|
border-left: 3px solid var(--es-error);
|
||||||
|
border-radius: 0 2px 2px 0;
|
||||||
|
padding: 10px 12px;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc .danger .custom-block-title,
|
||||||
|
.vp-doc .custom-block.danger .custom-block-title {
|
||||||
|
color: var(--es-error);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: var(--es-font-xs);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc .card {
|
||||||
|
background: var(--es-bg-sidebar);
|
||||||
|
border: 1px solid var(--es-border-subtle);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 12px;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc .card-title {
|
||||||
|
font-size: var(--es-font-sm);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--es-text-primary);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc .card-description {
|
||||||
|
font-size: var(--es-font-xs);
|
||||||
|
color: var(--es-text-muted);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc .tag {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--es-border-default);
|
||||||
|
border-radius: 2px;
|
||||||
|
color: var(--es-text-secondary);
|
||||||
|
font-size: var(--es-font-xs);
|
||||||
|
margin-right: 4px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPFooter {
|
||||||
|
background: var(--es-bg-sidebar) !important;
|
||||||
|
border-top: 1px solid var(--es-border-subtle) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: var(--es-bg-card);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--es-border-strong);
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 2px solid var(--es-bg-card);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #5a5a5a;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-corner {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-container {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-section {
|
||||||
|
padding: 32px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.VPDoc .content {
|
||||||
|
padding: 16px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
14
docs/.vitepress/theme/index.js
Normal file
14
docs/.vitepress/theme/index.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import DefaultTheme from 'vitepress/theme'
|
||||||
|
import ParticleHero from './components/ParticleHero.vue'
|
||||||
|
import ParticleHeroEn from './components/ParticleHeroEn.vue'
|
||||||
|
import FeatureCard from './components/FeatureCard.vue'
|
||||||
|
import './custom.css'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
extends: DefaultTheme,
|
||||||
|
enhanceApp({ app }) {
|
||||||
|
app.component('ParticleHero', ParticleHero)
|
||||||
|
app.component('ParticleHeroEn', ParticleHeroEn)
|
||||||
|
app.component('FeatureCard', FeatureCard)
|
||||||
|
}
|
||||||
|
}
|
||||||
412
docs/en/guide/getting-started.md
Normal file
412
docs/en/guide/getting-started.md
Normal file
@@ -0,0 +1,412 @@
|
|||||||
|
# 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](/en/api/README)
|
||||||
|
- Explore more [practical examples](/en/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)
|
||||||
|
```
|
||||||
43
docs/en/guide/index.md
Normal file
43
docs/en/guide/index.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Guide
|
||||||
|
|
||||||
|
Welcome to the ECS Framework Guide. This guide covers the core concepts and usage of the framework.
|
||||||
|
|
||||||
|
## Core Concepts
|
||||||
|
|
||||||
|
### [Entity](./entity.md)
|
||||||
|
Learn the basics of ECS architecture - how to use entities, lifecycle management, and best practices.
|
||||||
|
|
||||||
|
### [Component](./component.md)
|
||||||
|
Learn how to create and use components for modular game feature design.
|
||||||
|
|
||||||
|
### [System](./system.md)
|
||||||
|
Master system development to implement game logic processing.
|
||||||
|
|
||||||
|
### [Entity Query & Matcher](./entity-query.md)
|
||||||
|
Learn to use Matcher for entity filtering and queries with `all`, `any`, `none`, `nothing` conditions.
|
||||||
|
|
||||||
|
### [Scene](./scene.md)
|
||||||
|
Understand scene lifecycle, system management, and entity container features.
|
||||||
|
|
||||||
|
### [Event System](./event-system.md)
|
||||||
|
Master the type-safe event system for component communication and system coordination.
|
||||||
|
|
||||||
|
### [Serialization](./serialization.md)
|
||||||
|
Master serialization for scenes, entities, and components. Supports full and incremental serialization for game saves, network sync, and more.
|
||||||
|
|
||||||
|
### [Time and Timers](./time-and-timers.md)
|
||||||
|
Learn time management and timer systems for precise game logic timing control.
|
||||||
|
|
||||||
|
### [Logging](./logging.md)
|
||||||
|
Master the leveled logging system for debugging, monitoring, and error tracking.
|
||||||
|
|
||||||
|
### [Platform Adapter](./platform-adapter.md)
|
||||||
|
Learn how to implement and register platform adapters for browsers, mini-games, Node.js, and more.
|
||||||
|
|
||||||
|
## Advanced Features
|
||||||
|
|
||||||
|
### [Service Container](./service-container.md)
|
||||||
|
Master dependency injection and service management for loosely-coupled architecture.
|
||||||
|
|
||||||
|
### [Plugin System](./plugin-system.md)
|
||||||
|
Learn how to develop and use plugins to extend framework functionality.
|
||||||
317
docs/en/index.md
Normal file
317
docs/en/index.md
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
---
|
||||||
|
layout: page
|
||||||
|
title: ESEngine - High-performance TypeScript ECS Framework
|
||||||
|
---
|
||||||
|
|
||||||
|
<ParticleHeroEn />
|
||||||
|
|
||||||
|
<section class="news-section">
|
||||||
|
<div class="news-container">
|
||||||
|
<div class="news-header">
|
||||||
|
<h2 class="news-title">Quick Links</h2>
|
||||||
|
<a href="/en/guide/" class="news-more">View Docs</a>
|
||||||
|
</div>
|
||||||
|
<div class="news-grid">
|
||||||
|
<a href="/en/guide/getting-started" class="news-card">
|
||||||
|
<div class="news-card-image" style="background: linear-gradient(135deg, #1e3a5f 0%, #1e1e1e 100%);">
|
||||||
|
<div class="news-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24"><path fill="#4fc1ff" d="M12 3L1 9l4 2.18v6L12 21l7-3.82v-6l2-1.09V17h2V9zm6.82 6L12 12.72L5.18 9L12 5.28zM17 16l-5 2.72L7 16v-3.73L12 15l5-2.73z"/></svg>
|
||||||
|
</div>
|
||||||
|
<span class="news-badge">Quick Start</span>
|
||||||
|
</div>
|
||||||
|
<div class="news-card-content">
|
||||||
|
<h3>Get Started in 5 Minutes</h3>
|
||||||
|
<p>From installation to your first ECS app, learn the core concepts quickly.</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a href="/en/guide/behavior-tree/" class="news-card">
|
||||||
|
<div class="news-card-image" style="background: linear-gradient(135deg, #1e3a5f 0%, #1e1e1e 100%);">
|
||||||
|
<div class="news-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24"><path fill="#4ec9b0" d="M12 2a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m3 20h-1v-7l-2-2l-2 2v7H9v-7.5l-2 2V22H6v-6l3-3l1-3.5c-.3.4-.6.7-1 1L6 9v1H4V8l5-3c.5-.3 1.1-.5 1.7-.5H11c.6 0 1.2.2 1.7.5l5 3v2h-2V9l-3 1.5c-.4-.3-.7-.6-1-1l1 3.5l3 3v6Z"/></svg>
|
||||||
|
</div>
|
||||||
|
<span class="news-badge">AI System</span>
|
||||||
|
</div>
|
||||||
|
<div class="news-card-content">
|
||||||
|
<h3>Visual Behavior Tree Editor</h3>
|
||||||
|
<p>Built-in AI behavior tree system with visual editing and real-time debugging.</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="features-section">
|
||||||
|
<div class="features-container">
|
||||||
|
<h2 class="features-title">Core Features</h2>
|
||||||
|
<div class="features-grid">
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#4fc1ff" d="M13 2.05v2.02c3.95.49 7 3.85 7 7.93c0 1.45-.39 2.79-1.06 3.95l1.59 1.09A9.94 9.94 0 0 0 22 12c0-5.18-3.95-9.45-9-9.95M12 19c-3.87 0-7-3.13-7-7c0-3.53 2.61-6.43 6-6.92V2.05c-5.06.5-9 4.76-9 9.95c0 5.52 4.47 10 9.99 10c3.31 0 6.24-1.61 8.06-4.09l-1.6-1.1A7.93 7.93 0 0 1 12 19"/><path fill="#4fc1ff" d="M12 6a6 6 0 0 0-6 6c0 3.31 2.69 6 6 6a6 6 0 0 0 0-12m0 10c-2.21 0-4-1.79-4-4s1.79-4 4-4s4 1.79 4 4s-1.79 4-4 4"/></svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="feature-title">High-performance ECS Architecture</h3>
|
||||||
|
<p class="feature-desc">Data-driven entity component system for large-scale entity processing with cache-friendly memory layout.</p>
|
||||||
|
<a href="/en/guide/entity" class="feature-link">Learn more</a>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#569cd6" d="M3 3h18v18H3zm16.525 13.707c0-.795-.272-1.425-.816-1.89c-.544-.465-1.404-.804-2.58-1.016l-1.704-.296c-.616-.104-1.052-.26-1.308-.468c-.256-.21-.384-.468-.384-.776c0-.392.168-.7.504-.924c.336-.224.8-.336 1.392-.336c.56 0 1.008.124 1.344.372c.336.248.536.584.6 1.008h2.016c-.08-.96-.464-1.716-1.152-2.268c-.688-.552-1.6-.828-2.736-.828c-1.2 0-2.148.3-2.844.9c-.696.6-1.044 1.38-1.044 2.34c0 .76.252 1.368.756 1.824c.504.456 1.308.792 2.412.996l1.704.312c.624.12 1.068.28 1.332.48c.264.2.396.46.396.78c0 .424-.192.756-.576.996c-.384.24-.9.36-1.548.36c-.672 0-1.2-.14-1.584-.42c-.384-.28-.608-.668-.672-1.164H8.868c.048 1.016.46 1.808 1.236 2.376c.776.568 1.796.852 3.06.852c1.24 0 2.22-.292 2.94-.876c.72-.584 1.08-1.364 1.08-2.34z"/></svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="feature-title">Full Type Support</h3>
|
||||||
|
<p class="feature-desc">100% TypeScript with complete type definitions and compile-time checking for the best development experience.</p>
|
||||||
|
<a href="/en/guide/component" class="feature-link">Learn more</a>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#4ec9b0" d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10s10-4.5 10-10S17.5 2 12 2m0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8s8 3.59 8 8s-3.59 8-8 8m-5-8l4-4v3h4v2h-4v3z"/></svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="feature-title">Visual Behavior Tree</h3>
|
||||||
|
<p class="feature-desc">Built-in AI behavior tree system with visual editor, custom nodes, and real-time debugging.</p>
|
||||||
|
<a href="/en/guide/behavior-tree/" class="feature-link">Learn more</a>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#c586c0" d="M4 6h18V4H4c-1.1 0-2 .9-2 2v11H0v3h14v-3H4zm19 2h-6c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h6c.55 0 1-.45 1-1V9c0-.55-.45-1-1-1m-1 9h-4v-7h4z"/></svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="feature-title">Multi-Platform Support</h3>
|
||||||
|
<p class="feature-desc">Support for browsers, Node.js, WeChat Mini Games, and seamless integration with major game engines.</p>
|
||||||
|
<a href="/en/guide/platform-adapter" class="feature-link">Learn more</a>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#dcdcaa" d="M4 3h6v2H4v14h6v2H4c-1.1 0-2-.9-2-2V5c0-1.1.9-2 2-2m9 0h6c1.1 0 2 .9 2 2v14c0 1.1-.9 2-2 2h-6v-2h6V5h-6zm-1 7h4v2h-4z"/></svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="feature-title">Modular Design</h3>
|
||||||
|
<p class="feature-desc">Core features packaged independently, import only what you need. Support for custom plugin extensions.</p>
|
||||||
|
<a href="/en/guide/plugin-system" class="feature-link">Learn more</a>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#9cdcfe" d="M22.7 19l-9.1-9.1c.9-2.3.4-5-1.5-6.9c-2-2-5-2.4-7.4-1.3L9 6L6 9L1.6 4.7C.4 7.1.9 10.1 2.9 12.1c1.9 1.9 4.6 2.4 6.9 1.5l9.1 9.1c.4.4 1 .4 1.4 0l2.3-2.3c.5-.4.5-1.1.1-1.4"/></svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="feature-title">Developer Tools</h3>
|
||||||
|
<p class="feature-desc">Built-in performance monitoring, debugging tools, serialization system, and complete development toolchain.</p>
|
||||||
|
<a href="/en/guide/logging" class="feature-link">Learn more</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Home page specific styles */
|
||||||
|
.news-section {
|
||||||
|
background: #0d0d0d;
|
||||||
|
padding: 64px 0;
|
||||||
|
border-top: 1px solid #2a2a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-container {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ffffff;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-more {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 1px solid #2a2a2a;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #a0a0a0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-more:hover {
|
||||||
|
background: #252525;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card {
|
||||||
|
display: flex;
|
||||||
|
background: #1f1f1f;
|
||||||
|
border: 1px solid #2a2a2a;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card:hover {
|
||||||
|
border-color: #3b9eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card-image {
|
||||||
|
width: 200px;
|
||||||
|
min-height: 140px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 16px;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-icon {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 12px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #3a3a3a;
|
||||||
|
border-radius: 16px;
|
||||||
|
color: #a0a0a0;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card-content {
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card-content h3 {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card-content p {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #707070;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-section {
|
||||||
|
background: #0d0d0d;
|
||||||
|
padding: 64px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-container {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ffffff;
|
||||||
|
margin: 0 0 32px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card {
|
||||||
|
background: #1f1f1f;
|
||||||
|
border: 1px solid #2a2a2a;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card:hover {
|
||||||
|
border-color: #3b9eff;
|
||||||
|
background: #252525;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
background: #0d0d0d;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-desc {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #707070;
|
||||||
|
line-height: 1.7;
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-link {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #3b9eff;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.news-container,
|
||||||
|
.features-container {
|
||||||
|
padding: 0 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.news-card {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card-image {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -55,25 +55,92 @@ class Health extends Component {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 组件装饰器
|
### @ECSComponent 装饰器
|
||||||
|
|
||||||
**必须使用 `@ECSComponent` 装饰器**,这确保了:
|
`@ECSComponent` 是组件类必须使用的装饰器,它为组件提供了类型标识和元数据管理。
|
||||||
- 组件在代码混淆后仍能正确识别
|
|
||||||
- 提供稳定的类型名称用于序列化和调试
|
#### 为什么必须使用
|
||||||
- 框架能正确管理组件注册
|
|
||||||
|
| 功能 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| **类型识别** | 提供稳定的类型名称,代码混淆后仍能正确识别 |
|
||||||
|
| **序列化支持** | 序列化/反序列化时使用该名称作为类型标识 |
|
||||||
|
| **组件注册** | 自动注册到 ComponentRegistry,分配唯一的位掩码 |
|
||||||
|
| **调试支持** | 在调试工具和日志中显示可读的组件名称 |
|
||||||
|
|
||||||
|
#### 基本语法
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// 正确的用法
|
@ECSComponent(typeName: string)
|
||||||
|
```
|
||||||
|
|
||||||
|
- `typeName`: 组件的类型名称,建议使用与类名相同或相近的名称
|
||||||
|
|
||||||
|
#### 使用示例
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ 正确的用法
|
||||||
@ECSComponent('Velocity')
|
@ECSComponent('Velocity')
|
||||||
class Velocity extends Component {
|
class Velocity extends Component {
|
||||||
dx: number = 0;
|
dx: number = 0;
|
||||||
dy: number = 0;
|
dy: number = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 错误的用法 - 没有装饰器
|
// ✅ 推荐:类型名与类名保持一致
|
||||||
class BadComponent extends Component {
|
@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,6 +121,65 @@ 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
|
```typescript
|
||||||
@@ -493,6 +552,65 @@ const matcher2 = matcher.any(VelocityComponent);
|
|||||||
console.log(matcher === matcher2); // false
|
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
|
## 相关 API
|
||||||
|
|
||||||
- [Matcher](../api/classes/Matcher.md) - 查询条件描述符 API 参考
|
- [Matcher](../api/classes/Matcher.md) - 查询条件描述符 API 参考
|
||||||
|
|||||||
@@ -9,6 +9,12 @@
|
|||||||
- 提供唯一标识(ID)
|
- 提供唯一标识(ID)
|
||||||
- 管理组件的生命周期
|
- 管理组件的生命周期
|
||||||
|
|
||||||
|
::: tip 关于父子层级关系
|
||||||
|
实体间的父子层级关系通过 `HierarchyComponent` 和 `HierarchySystem` 管理,而非 Entity 内置属性。这种设计遵循 ECS 组合原则 —— 只有需要层级关系的实体才添加此组件。
|
||||||
|
|
||||||
|
详见 [层级系统](./hierarchy.md) 文档。
|
||||||
|
:::
|
||||||
|
|
||||||
## 创建实体
|
## 创建实体
|
||||||
|
|
||||||
**重要提示:实体必须通过场景创建,不支持手动创建!**
|
**重要提示:实体必须通过场景创建,不支持手动创建!**
|
||||||
@@ -286,3 +292,9 @@ entity.components.forEach(component => {
|
|||||||
```
|
```
|
||||||
|
|
||||||
实体是 ECS 架构的核心概念之一,理解如何正确使用实体将帮助你构建高效、可维护的游戏代码。
|
实体是 ECS 架构的核心概念之一,理解如何正确使用实体将帮助你构建高效、可维护的游戏代码。
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
- 了解 [层级系统](./hierarchy.md) 建立实体间的父子关系
|
||||||
|
- 了解 [组件系统](./component.md) 为实体添加功能
|
||||||
|
- 了解 [场景管理](./scene.md) 组织和管理实体
|
||||||
@@ -23,7 +23,6 @@ import { Core } from '@esengine/ecs-framework'
|
|||||||
// 方式1:使用配置对象(推荐)
|
// 方式1:使用配置对象(推荐)
|
||||||
const core = Core.create({
|
const core = Core.create({
|
||||||
debug: true, // 启用调试模式,提供详细的日志和性能监控
|
debug: true, // 启用调试模式,提供详细的日志和性能监控
|
||||||
enableEntitySystems: true, // 启用实体系统,这是ECS的核心功能
|
|
||||||
debugConfig: { // 可选:高级调试配置
|
debugConfig: { // 可选:高级调试配置
|
||||||
enabled: false, // 是否启用WebSocket调试服务器
|
enabled: false, // 是否启用WebSocket调试服务器
|
||||||
websocketUrl: 'ws://localhost:8080',
|
websocketUrl: 'ws://localhost:8080',
|
||||||
@@ -39,12 +38,11 @@ const core = Core.create({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 方式2:简化创建(向后兼容)
|
// 方式2:简化创建(向后兼容)
|
||||||
const core = Core.create(true); // 等同于 { debug: true, enableEntitySystems: true }
|
const core = Core.create(true); // 等同于 { debug: true }
|
||||||
|
|
||||||
// 方式3:生产环境配置
|
// 方式3:生产环境配置
|
||||||
const core = Core.create({
|
const core = Core.create({
|
||||||
debug: false, // 生产环境关闭调试
|
debug: false // 生产环境关闭调试
|
||||||
enableEntitySystems: true
|
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -55,9 +53,6 @@ interface ICoreConfig {
|
|||||||
/** 是否启用调试模式 - 影响日志级别和性能监控 */
|
/** 是否启用调试模式 - 影响日志级别和性能监控 */
|
||||||
debug?: boolean;
|
debug?: boolean;
|
||||||
|
|
||||||
/** 是否启用实体系统 - 核心ECS功能开关 */
|
|
||||||
enableEntitySystems?: boolean;
|
|
||||||
|
|
||||||
/** 高级调试配置 - 用于开发工具集成 */
|
/** 高级调试配置 - 用于开发工具集成 */
|
||||||
debugConfig?: {
|
debugConfig?: {
|
||||||
enabled: boolean; // 是否启用调试服务器
|
enabled: boolean; // 是否启用调试服务器
|
||||||
|
|||||||
437
docs/guide/hierarchy.md
Normal file
437
docs/guide/hierarchy.md
Normal file
@@ -0,0 +1,437 @@
|
|||||||
|
# 层级系统
|
||||||
|
|
||||||
|
在游戏开发中,实体间的父子层级关系是常见需求。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,6 +13,9 @@
|
|||||||
### [系统架构 (System)](./system.md)
|
### [系统架构 (System)](./system.md)
|
||||||
掌握系统的编写方法,实现游戏逻辑的处理。
|
掌握系统的编写方法,实现游戏逻辑的处理。
|
||||||
|
|
||||||
|
### [实体查询与 Matcher](./entity-query.md)
|
||||||
|
学习使用 Matcher 进行实体筛选和查询,掌握 `all`、`any`、`none`、`nothing` 等匹配条件。
|
||||||
|
|
||||||
### [场景管理 (Scene)](./scene.md)
|
### [场景管理 (Scene)](./scene.md)
|
||||||
了解场景的生命周期、系统管理和实体容器功能。
|
了解场景的生命周期、系统管理和实体容器功能。
|
||||||
|
|
||||||
|
|||||||
@@ -190,6 +190,106 @@ 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;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### 场景自定义数据
|
### 场景自定义数据
|
||||||
|
|
||||||
除了实体和组件,还可以序列化场景级别的配置数据:
|
除了实体和组件,还可以序列化场景级别的配置数据:
|
||||||
|
|||||||
@@ -33,6 +33,26 @@ 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-标识符模式)。
|
||||||
|
|
||||||
#### 生命周期
|
#### 生命周期
|
||||||
|
|
||||||
服务容器支持两种生命周期:
|
服务容器支持两种生命周期:
|
||||||
@@ -333,21 +353,20 @@ class GameService implements IService {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### @Inject 装饰器
|
### @InjectProperty 装饰器
|
||||||
|
|
||||||
在构造函数中注入依赖:
|
通过属性装饰器注入依赖。注入时机是在构造函数执行后、`onInitialize()` 调用前完成:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Injectable, Inject, IService } from '@esengine/ecs-framework';
|
import { Injectable, InjectProperty, IService } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
class PlayerService implements IService {
|
class PlayerService implements IService {
|
||||||
constructor(
|
@InjectProperty(DataService)
|
||||||
@Inject(DataService) private data: DataService,
|
private data!: DataService;
|
||||||
@Inject(GameService) private game: GameService
|
|
||||||
) {
|
@InjectProperty(GameService)
|
||||||
// data 和 game 会自动从容器中解析
|
private game!: GameService;
|
||||||
}
|
|
||||||
|
|
||||||
dispose(): void {
|
dispose(): void {
|
||||||
// 清理资源
|
// 清理资源
|
||||||
@@ -355,6 +374,35 @@ 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` 自动处理依赖注入:
|
使用 `registerInjectable` 自动处理依赖注入:
|
||||||
@@ -362,10 +410,10 @@ class PlayerService implements IService {
|
|||||||
```typescript
|
```typescript
|
||||||
import { registerInjectable } from '@esengine/ecs-framework';
|
import { registerInjectable } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
// 注册服务(会自动解析@Inject依赖)
|
// 注册服务(会自动解析 @InjectProperty 依赖)
|
||||||
registerInjectable(Core.services, PlayerService);
|
registerInjectable(Core.services, PlayerService);
|
||||||
|
|
||||||
// 解析时会自动注入依赖
|
// 解析时会自动注入属性依赖
|
||||||
const player = Core.services.resolve(PlayerService);
|
const player = Core.services.resolve(PlayerService);
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -493,22 +541,164 @@ registerInjectable(Core.services, NetworkService);
|
|||||||
|
|
||||||
## 高级用法
|
## 高级用法
|
||||||
|
|
||||||
### 服务替换(测试)
|
### 接口与 Symbol 标识符模式
|
||||||
|
|
||||||
在测试中替换真实服务为模拟服务:
|
在大型项目或需要跨平台适配的游戏中,推荐使用"接口 + Symbol.for 标识符"模式。这种模式实现了真正的依赖倒置,让代码依赖于抽象而非具体实现。
|
||||||
|
|
||||||
|
#### 为什么使用 Symbol.for
|
||||||
|
|
||||||
|
- **跨包共享**: `Symbol.for('key')` 在全局 Symbol 注册表中创建/获取 Symbol,确保不同包中使用相同的标识符
|
||||||
|
- **接口解耦**: 消费者只依赖接口定义,不依赖具体实现类
|
||||||
|
- **可替换实现**: 可以在运行时注入不同的实现(如测试 Mock、不同平台适配)
|
||||||
|
|
||||||
|
#### 定义接口和标识符
|
||||||
|
|
||||||
|
以音频服务为例,游戏需要在不同平台(Web、微信小游戏、原生App)使用不同的音频实现:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// 测试代码
|
// IAudioService.ts - 定义接口和标识符
|
||||||
class MockDataService implements IService {
|
export interface IAudioService {
|
||||||
getData(key: string) {
|
dispose(): void;
|
||||||
return 'mock data';
|
playSound(id: string): void;
|
||||||
|
playMusic(id: string, loop?: boolean): void;
|
||||||
|
stopMusic(): void;
|
||||||
|
setVolume(volume: number): void;
|
||||||
|
preload(id: string, url: string): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
Core.services.registerInstance(DataService, new MockDataService());
|
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!
|
||||||
```
|
```
|
||||||
|
|
||||||
### 循环依赖检测
|
### 循环依赖检测
|
||||||
|
|||||||
@@ -157,8 +157,45 @@ const nameMatcher = Matcher.byName("Player"); // 匹配名称为 "Player" 的实
|
|||||||
|
|
||||||
// 单组件匹配
|
// 单组件匹配
|
||||||
const componentMatcher = Matcher.byComponent(Health); // 匹配拥有 Health 组件的实体
|
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) 文档。
|
||||||
|
|
||||||
## 系统生命周期
|
## 系统生命周期
|
||||||
|
|
||||||
系统提供了完整的生命周期回调:
|
系统提供了完整的生命周期回调:
|
||||||
@@ -563,9 +600,28 @@ class GameSystem extends EntitySystem {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. 使用装饰器
|
### 2. 使用 @ECSSystem 装饰器
|
||||||
|
|
||||||
**必须使用 `@ECSSystem` 装饰器**:
|
`@ECSSystem` 是系统类必须使用的装饰器,它为系统提供类型标识和元数据管理。
|
||||||
|
|
||||||
|
#### 为什么必须使用
|
||||||
|
|
||||||
|
| 功能 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| **类型识别** | 提供稳定的系统名称,代码混淆后仍能正确识别 |
|
||||||
|
| **调试支持** | 在性能监控、日志和调试工具中显示可读的系统名称 |
|
||||||
|
| **系统管理** | 通过名称查找和管理系统 |
|
||||||
|
| **序列化支持** | 场景序列化时可以记录系统配置 |
|
||||||
|
|
||||||
|
#### 基本语法
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@ECSSystem(systemName: string)
|
||||||
|
```
|
||||||
|
|
||||||
|
- `systemName`: 系统的名称,建议使用描述性的名称
|
||||||
|
|
||||||
|
#### 使用示例
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// ✅ 正确的用法
|
// ✅ 正确的用法
|
||||||
@@ -574,12 +630,41 @@ class PhysicsSystem extends EntitySystem {
|
|||||||
// 系统实现
|
// 系统实现
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ✅ 推荐:使用描述性的名称
|
||||||
|
@ECSSystem('PlayerMovement')
|
||||||
|
class PlayerMovementSystem extends EntitySystem {
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.all(Player, Position, Velocity));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ❌ 错误的用法 - 没有装饰器
|
// ❌ 错误的用法 - 没有装饰器
|
||||||
class BadSystem extends EntitySystem {
|
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. 合理的更新顺序
|
### 3. 合理的更新顺序
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
|
|||||||
336
docs/index.md
336
docs/index.md
@@ -1,23 +1,317 @@
|
|||||||
---
|
---
|
||||||
layout: home
|
layout: page
|
||||||
|
title: ESEngine - 高性能 TypeScript ECS 框架
|
||||||
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
|
|
||||||
|
|
||||||
features:
|
|
||||||
- title: 高性能
|
|
||||||
details: 支持大规模实体处理
|
|
||||||
- title: 类型安全
|
|
||||||
details: 完整的TypeScript支持,编译时类型检查
|
|
||||||
- title: 模块化设计
|
|
||||||
details: 核心功能独立打包,支持多平台
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
<ParticleHero />
|
||||||
|
|
||||||
|
<section class="news-section">
|
||||||
|
<div class="news-container">
|
||||||
|
<div class="news-header">
|
||||||
|
<h2 class="news-title">快速入口</h2>
|
||||||
|
<a href="/guide/" class="news-more">查看文档</a>
|
||||||
|
</div>
|
||||||
|
<div class="news-grid">
|
||||||
|
<a href="/guide/getting-started" class="news-card">
|
||||||
|
<div class="news-card-image" style="background: linear-gradient(135deg, #1e3a5f 0%, #1e1e1e 100%);">
|
||||||
|
<div class="news-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24"><path fill="#4fc1ff" d="M12 3L1 9l4 2.18v6L12 21l7-3.82v-6l2-1.09V17h2V9zm6.82 6L12 12.72L5.18 9L12 5.28zM17 16l-5 2.72L7 16v-3.73L12 15l5-2.73z"/></svg>
|
||||||
|
</div>
|
||||||
|
<span class="news-badge">快速开始</span>
|
||||||
|
</div>
|
||||||
|
<div class="news-card-content">
|
||||||
|
<h3>5 分钟上手 ESEngine</h3>
|
||||||
|
<p>从安装到创建第一个 ECS 应用,快速了解核心概念。</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a href="/guide/behavior-tree/" class="news-card">
|
||||||
|
<div class="news-card-image" style="background: linear-gradient(135deg, #1e3a5f 0%, #1e1e1e 100%);">
|
||||||
|
<div class="news-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24"><path fill="#4ec9b0" d="M12 2a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m3 20h-1v-7l-2-2l-2 2v7H9v-7.5l-2 2V22H6v-6l3-3l1-3.5c-.3.4-.6.7-1 1L6 9v1H4V8l5-3c.5-.3 1.1-.5 1.7-.5H11c.6 0 1.2.2 1.7.5l5 3v2h-2V9l-3 1.5c-.4-.3-.7-.6-1-1l1 3.5l3 3v6Z"/></svg>
|
||||||
|
</div>
|
||||||
|
<span class="news-badge">AI 系统</span>
|
||||||
|
</div>
|
||||||
|
<div class="news-card-content">
|
||||||
|
<h3>行为树可视化编辑器</h3>
|
||||||
|
<p>内置 AI 行为树系统,支持可视化编辑和实时调试。</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="features-section">
|
||||||
|
<div class="features-container">
|
||||||
|
<h2 class="features-title">核心特性</h2>
|
||||||
|
<div class="features-grid">
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#4fc1ff" d="M13 2.05v2.02c3.95.49 7 3.85 7 7.93c0 1.45-.39 2.79-1.06 3.95l1.59 1.09A9.94 9.94 0 0 0 22 12c0-5.18-3.95-9.45-9-9.95M12 19c-3.87 0-7-3.13-7-7c0-3.53 2.61-6.43 6-6.92V2.05c-5.06.5-9 4.76-9 9.95c0 5.52 4.47 10 9.99 10c3.31 0 6.24-1.61 8.06-4.09l-1.6-1.1A7.93 7.93 0 0 1 12 19"/><path fill="#4fc1ff" d="M12 6a6 6 0 0 0-6 6c0 3.31 2.69 6 6 6a6 6 0 0 0 0-12m0 10c-2.21 0-4-1.79-4-4s1.79-4 4-4s4 1.79 4 4s-1.79 4-4 4"/></svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="feature-title">高性能 ECS 架构</h3>
|
||||||
|
<p class="feature-desc">基于数据驱动的实体组件系统,支持大规模实体处理,缓存友好的内存布局。</p>
|
||||||
|
<a href="/guide/entity" class="feature-link">了解更多 →</a>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#569cd6" d="M3 3h18v18H3zm16.525 13.707c0-.795-.272-1.425-.816-1.89c-.544-.465-1.404-.804-2.58-1.016l-1.704-.296c-.616-.104-1.052-.26-1.308-.468c-.256-.21-.384-.468-.384-.776c0-.392.168-.7.504-.924c.336-.224.8-.336 1.392-.336c.56 0 1.008.124 1.344.372c.336.248.536.584.6 1.008h2.016c-.08-.96-.464-1.716-1.152-2.268c-.688-.552-1.6-.828-2.736-.828c-1.2 0-2.148.3-2.844.9c-.696.6-1.044 1.38-1.044 2.34c0 .76.252 1.368.756 1.824c.504.456 1.308.792 2.412.996l1.704.312c.624.12 1.068.28 1.332.48c.264.2.396.46.396.78c0 .424-.192.756-.576.996c-.384.24-.9.36-1.548.36c-.672 0-1.2-.14-1.584-.42c-.384-.28-.608-.668-.672-1.164H8.868c.048 1.016.46 1.808 1.236 2.376c.776.568 1.796.852 3.06.852c1.24 0 2.22-.292 2.94-.876c.72-.584 1.08-1.364 1.08-2.34z"/></svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="feature-title">完整类型支持</h3>
|
||||||
|
<p class="feature-desc">100% TypeScript 编写,完整的类型定义和编译时检查,提供最佳的开发体验。</p>
|
||||||
|
<a href="/guide/component" class="feature-link">了解更多 →</a>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#4ec9b0" d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10s10-4.5 10-10S17.5 2 12 2m0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8s8 3.59 8 8s-3.59 8-8 8m-5-8l4-4v3h4v2h-4v3z"/></svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="feature-title">可视化行为树</h3>
|
||||||
|
<p class="feature-desc">内置 AI 行为树系统,提供可视化编辑器,支持自定义节点和实时调试。</p>
|
||||||
|
<a href="/guide/behavior-tree/" class="feature-link">了解更多 →</a>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#c586c0" d="M4 6h18V4H4c-1.1 0-2 .9-2 2v11H0v3h14v-3H4zm19 2h-6c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h6c.55 0 1-.45 1-1V9c0-.55-.45-1-1-1m-1 9h-4v-7h4z"/></svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="feature-title">多平台支持</h3>
|
||||||
|
<p class="feature-desc">支持浏览器、Node.js、微信小游戏等多平台,可与主流游戏引擎无缝集成。</p>
|
||||||
|
<a href="/guide/platform-adapter" class="feature-link">了解更多 →</a>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#dcdcaa" d="M4 3h6v2H4v14h6v2H4c-1.1 0-2-.9-2-2V5c0-1.1.9-2 2-2m9 0h6c1.1 0 2 .9 2 2v14c0 1.1-.9 2-2 2h-6v-2h6V5h-6zm-1 7h4v2h-4z"/></svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="feature-title">模块化设计</h3>
|
||||||
|
<p class="feature-desc">核心功能独立打包,按需引入。支持自定义插件扩展,灵活适配不同项目。</p>
|
||||||
|
<a href="/guide/plugin-system" class="feature-link">了解更多 →</a>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#9cdcfe" d="M22.7 19l-9.1-9.1c.9-2.3.4-5-1.5-6.9c-2-2-5-2.4-7.4-1.3L9 6L6 9L1.6 4.7C.4 7.1.9 10.1 2.9 12.1c1.9 1.9 4.6 2.4 6.9 1.5l9.1 9.1c.4.4 1 .4 1.4 0l2.3-2.3c.5-.4.5-1.1.1-1.4"/></svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="feature-title">开发者工具</h3>
|
||||||
|
<p class="feature-desc">内置性能监控、调试工具、序列化系统等,提供完整的开发工具链。</p>
|
||||||
|
<a href="/guide/logging" class="feature-link">了解更多 →</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 首页专用样式 | Home page specific styles */
|
||||||
|
.news-section {
|
||||||
|
background: #0d0d0d;
|
||||||
|
padding: 64px 0;
|
||||||
|
border-top: 1px solid #2a2a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-container {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ffffff;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-more {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 1px solid #2a2a2a;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #a0a0a0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-more:hover {
|
||||||
|
background: #252525;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card {
|
||||||
|
display: flex;
|
||||||
|
background: #1f1f1f;
|
||||||
|
border: 1px solid #2a2a2a;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card:hover {
|
||||||
|
border-color: #3b9eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card-image {
|
||||||
|
width: 200px;
|
||||||
|
min-height: 140px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 16px;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-icon {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 12px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #3a3a3a;
|
||||||
|
border-radius: 16px;
|
||||||
|
color: #a0a0a0;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card-content {
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card-content h3 {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card-content p {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #707070;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-section {
|
||||||
|
background: #0d0d0d;
|
||||||
|
padding: 64px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-container {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ffffff;
|
||||||
|
margin: 0 0 32px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card {
|
||||||
|
background: #1f1f1f;
|
||||||
|
border: 1px solid #2a2a2a;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card:hover {
|
||||||
|
border-color: #3b9eff;
|
||||||
|
background: #252525;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
background: #0d0d0d;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-desc {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #707070;
|
||||||
|
line-height: 1.7;
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-link {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #3b9eff;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.news-container,
|
||||||
|
.features-container {
|
||||||
|
padding: 0 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.news-card {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card-image {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
1
docs/public/CNAME
Normal file
1
docs/public/CNAME
Normal file
@@ -0,0 +1 @@
|
|||||||
|
esengine.cn
|
||||||
352
examples/core-demos/pnpm-lock.yaml
generated
Normal file
352
examples/core-demos/pnpm-lock.yaml
generated
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
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
|
||||||
30
package.json
30
package.json
@@ -3,6 +3,7 @@
|
|||||||
"version": "2.1.29",
|
"version": "2.1.29",
|
||||||
"description": "ECS Framework Monorepo - 高性能ECS框架及其网络插件",
|
"description": "ECS Framework Monorepo - 高性能ECS框架及其网络插件",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"packageManager": "pnpm@10.22.0",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
@@ -17,16 +18,18 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"bootstrap": "lerna bootstrap",
|
"bootstrap": "lerna bootstrap",
|
||||||
"clean": "lerna run clean",
|
"clean": "turbo run clean",
|
||||||
"build": "npm run build:core && npm run build:math",
|
"build": "turbo run build",
|
||||||
"build:core": "cd packages/core && npm run build",
|
"build:filter": "turbo run build --filter",
|
||||||
"build:math": "cd packages/math && npm run build",
|
"build:core": "turbo run build --filter=@esengine/ecs-framework",
|
||||||
"build:npm": "npm run build:npm:core && npm run build:npm:math",
|
"build:math": "turbo run build --filter=@esengine/ecs-framework-math",
|
||||||
|
"build:editor": "turbo run build --filter=@esengine/editor-app...",
|
||||||
|
"build:npm": "turbo run build:npm",
|
||||||
"build:npm:core": "cd packages/core && npm run build:npm",
|
"build:npm:core": "cd packages/core && npm run build:npm",
|
||||||
"build:npm:math": "cd packages/math && npm run build:npm",
|
"build:npm:math": "cd packages/math && npm run build:npm",
|
||||||
"test": "lerna run test",
|
"test": "turbo run test",
|
||||||
"test:coverage": "lerna run test:coverage",
|
"test:coverage": "turbo run test:coverage",
|
||||||
"test:ci": "lerna run test:ci",
|
"test:ci": "turbo run test:ci",
|
||||||
"prepare:publish": "npm run build:npm && node scripts/pre-publish-check.cjs",
|
"prepare:publish": "npm run build:npm && node scripts/pre-publish-check.cjs",
|
||||||
"sync:versions": "node scripts/sync-versions.cjs",
|
"sync:versions": "node scripts/sync-versions.cjs",
|
||||||
"publish:all": "npm run prepare:publish && npm run publish:all:dist",
|
"publish:all": "npm run prepare:publish && npm run publish:all:dist",
|
||||||
@@ -51,11 +54,12 @@
|
|||||||
"copy:worker-demo": "node scripts/update-worker-demo.js",
|
"copy:worker-demo": "node scripts/update-worker-demo.js",
|
||||||
"format": "prettier --write \"packages/**/src/**/*.{ts,tsx,js,jsx}\"",
|
"format": "prettier --write \"packages/**/src/**/*.{ts,tsx,js,jsx}\"",
|
||||||
"format:check": "prettier --check \"packages/**/src/**/*.{ts,tsx,js,jsx}\"",
|
"format:check": "prettier --check \"packages/**/src/**/*.{ts,tsx,js,jsx}\"",
|
||||||
"type-check": "lerna run type-check",
|
"type-check": "turbo run type-check",
|
||||||
"lint": "eslint \"packages/**/src/**/*.{ts,tsx,js,jsx}\"",
|
"lint": "turbo run lint",
|
||||||
"lint:fix": "eslint \"packages/**/src/**/*.{ts,tsx,js,jsx}\" --fix",
|
"lint:fix": "turbo run lint:fix",
|
||||||
"build:wasm": "cd packages/engine && wasm-pack build --dev --out-dir pkg",
|
"build:wasm": "cd packages/engine && wasm-pack build --dev --out-dir pkg",
|
||||||
"build:wasm:release": "cd packages/engine && wasm-pack build --release --out-dir pkg"
|
"build:wasm:release": "cd packages/engine && wasm-pack build --release --out-dir pkg",
|
||||||
|
"copy-modules": "node scripts/copy-engine-modules.mjs"
|
||||||
},
|
},
|
||||||
"author": "yhh",
|
"author": "yhh",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -92,6 +96,7 @@
|
|||||||
"semver": "^7.6.3",
|
"semver": "^7.6.3",
|
||||||
"size-limit": "^11.0.2",
|
"size-limit": "^11.0.2",
|
||||||
"ts-jest": "^29.4.0",
|
"ts-jest": "^29.4.0",
|
||||||
|
"turbo": "^2.6.1",
|
||||||
"typedoc": "^0.28.13",
|
"typedoc": "^0.28.13",
|
||||||
"typedoc-plugin-markdown": "^4.9.0",
|
"typedoc-plugin-markdown": "^4.9.0",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
@@ -116,3 +121,4 @@
|
|||||||
"ws": "^8.18.2"
|
"ws": "^8.18.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
50
packages/asset-system-editor/package.json
Normal file
50
packages/asset-system-editor/package.json
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
39
packages/asset-system-editor/src/index.ts
Normal file
39
packages/asset-system-editor/src/index.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* 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';
|
||||||
424
packages/asset-system-editor/src/meta/AssetMetaFile.ts
Normal file
424
packages/asset-system-editor/src/meta/AssetMetaFile.ts
Normal file
@@ -0,0 +1,424 @@
|
|||||||
|
/**
|
||||||
|
* 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>;
|
||||||
|
}
|
||||||
408
packages/asset-system-editor/src/packing/AssetPacker.ts
Normal file
408
packages/asset-system-editor/src/packing/AssetPacker.ts
Normal file
@@ -0,0 +1,408 @@
|
|||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
36
packages/asset-system-editor/tsconfig.json
Normal file
36
packages/asset-system-editor/tsconfig.json
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"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"]
|
||||||
|
}
|
||||||
10
packages/asset-system-editor/tsup.config.ts
Normal file
10
packages/asset-system-editor/tsup.config.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from 'tsup';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
entry: ['src/index.ts'],
|
||||||
|
format: ['esm'],
|
||||||
|
dts: true,
|
||||||
|
clean: true,
|
||||||
|
sourcemap: true,
|
||||||
|
external: ['@esengine/asset-system']
|
||||||
|
});
|
||||||
41
packages/asset-system/module.json
Normal file
41
packages/asset-system/module.json
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
@@ -2,24 +2,24 @@
|
|||||||
"name": "@esengine/asset-system",
|
"name": "@esengine/asset-system",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Asset management system for ES Engine",
|
"description": "Asset management system for ES Engine",
|
||||||
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "dist/index.mjs",
|
"module": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"import": "./dist/index.mjs",
|
"types": "./dist/index.d.ts",
|
||||||
"require": "./dist/index.js",
|
"import": "./dist/index.js"
|
||||||
"types": "./dist/index.d.ts"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "rollup -c",
|
"build": "tsup",
|
||||||
"build:npm": "npm run build",
|
"build:watch": "tsup --watch",
|
||||||
"clean": "rimraf dist",
|
"clean": "rimraf dist",
|
||||||
"type-check": "npx tsc --noEmit"
|
"type-check": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"ecs",
|
"ecs",
|
||||||
@@ -29,16 +29,11 @@
|
|||||||
],
|
],
|
||||||
"author": "yhh",
|
"author": "yhh",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
|
||||||
"@esengine/ecs-framework": "^2.0.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-commonjs": "^28.0.3",
|
"@esengine/ecs-framework": "workspace:*",
|
||||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
"@esengine/build-config": "workspace:*",
|
||||||
"@rollup/plugin-typescript": "^11.1.6",
|
|
||||||
"rimraf": "^5.0.0",
|
"rimraf": "^5.0.0",
|
||||||
"rollup": "^4.42.0",
|
"tsup": "^8.0.0",
|
||||||
"rollup-plugin-dts": "^6.2.1",
|
|
||||||
"typescript": "^5.8.3"
|
"typescript": "^5.8.3"
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
import typescript from '@rollup/plugin-typescript';
|
|
||||||
import resolve from '@rollup/plugin-node-resolve';
|
|
||||||
import commonjs from '@rollup/plugin-commonjs';
|
|
||||||
import dts from 'rollup-plugin-dts';
|
|
||||||
|
|
||||||
const external = ['@esengine/ecs-framework'];
|
|
||||||
|
|
||||||
export default [
|
|
||||||
// ESM and CJS builds
|
|
||||||
{
|
|
||||||
input: 'src/index.ts',
|
|
||||||
output: [
|
|
||||||
{
|
|
||||||
file: 'dist/index.js',
|
|
||||||
format: 'cjs',
|
|
||||||
sourcemap: true,
|
|
||||||
exports: 'named'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
file: 'dist/index.mjs',
|
|
||||||
format: 'es',
|
|
||||||
sourcemap: true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
external,
|
|
||||||
plugins: [
|
|
||||||
resolve({
|
|
||||||
preferBuiltins: false,
|
|
||||||
browser: true
|
|
||||||
}),
|
|
||||||
commonjs(),
|
|
||||||
typescript({
|
|
||||||
tsconfig: './tsconfig.json',
|
|
||||||
declaration: false,
|
|
||||||
declarationMap: false
|
|
||||||
})
|
|
||||||
]
|
|
||||||
},
|
|
||||||
// Type definitions
|
|
||||||
{
|
|
||||||
input: 'src/index.ts',
|
|
||||||
output: {
|
|
||||||
file: 'dist/index.d.ts',
|
|
||||||
format: 'es'
|
|
||||||
},
|
|
||||||
external,
|
|
||||||
plugins: [dts()]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
272
packages/asset-system/src/bundle/BundleFormat.ts
Normal file
272
packages/asset-system/src/bundle/BundleFormat.ts
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
/**
|
||||||
|
* 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';
|
||||||
|
}
|
||||||
@@ -22,6 +22,76 @@ export class AssetDatabase {
|
|||||||
private readonly _dependencies = new Map<AssetGUID, Set<AssetGUID>>();
|
private readonly _dependencies = new Map<AssetGUID, Set<AssetGUID>>();
|
||||||
private readonly _dependents = 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
|
* Add asset to database
|
||||||
* 添加资产到数据库
|
* 添加资产到数据库
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ import {
|
|||||||
IAssetManager,
|
IAssetManager,
|
||||||
IAssetLoadQueue
|
IAssetLoadQueue
|
||||||
} from '../interfaces/IAssetManager';
|
} from '../interfaces/IAssetManager';
|
||||||
import { IAssetLoader, IAssetLoaderFactory } from '../interfaces/IAssetLoader';
|
import { IAssetLoader, IAssetLoaderFactory, IAssetParseContext } from '../interfaces/IAssetLoader';
|
||||||
|
import { IAssetReader, IAssetContent } from '../interfaces/IAssetReader';
|
||||||
import { AssetCache } from './AssetCache';
|
import { AssetCache } from './AssetCache';
|
||||||
import { AssetLoadQueue } from './AssetLoadQueue';
|
import { AssetLoadQueue } from './AssetLoadQueue';
|
||||||
import { AssetLoaderFactory } from '../loaders/AssetLoaderFactory';
|
import { AssetLoaderFactory } from '../loaders/AssetLoaderFactory';
|
||||||
@@ -55,6 +56,9 @@ export class AssetManager implements IAssetManager {
|
|||||||
private readonly _loaderFactory: IAssetLoaderFactory;
|
private readonly _loaderFactory: IAssetLoaderFactory;
|
||||||
private readonly _database: AssetDatabase;
|
private readonly _database: AssetDatabase;
|
||||||
|
|
||||||
|
/** Asset reader for file operations. | 用于文件操作的资产读取器。 */
|
||||||
|
private _reader: IAssetReader | null = null;
|
||||||
|
|
||||||
private _nextHandle: AssetHandle = 1;
|
private _nextHandle: AssetHandle = 1;
|
||||||
|
|
||||||
private _statistics = {
|
private _statistics = {
|
||||||
@@ -71,12 +75,35 @@ export class AssetManager implements IAssetManager {
|
|||||||
this._loaderFactory = new AssetLoaderFactory();
|
this._loaderFactory = new AssetLoaderFactory();
|
||||||
this._database = new AssetDatabase();
|
this._database = new AssetDatabase();
|
||||||
|
|
||||||
// 如果提供了目录,初始化数据库 / Initialize database if catalog provided
|
|
||||||
if (catalog) {
|
if (catalog) {
|
||||||
this.initializeFromCatalog(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
|
* Initialize from catalog
|
||||||
* 从目录初始化
|
* 从目录初始化
|
||||||
@@ -135,7 +162,19 @@ export class AssetManager implements IAssetManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 创建加载器 / Create loader
|
// 创建加载器 / Create loader
|
||||||
const loader = this._loaderFactory.createLoader(metadata.type);
|
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) {
|
if (!loader) {
|
||||||
throw AssetLoadError.unsupportedType(guid, metadata.type);
|
throw AssetLoadError.unsupportedType(guid, metadata.type);
|
||||||
}
|
}
|
||||||
@@ -184,32 +223,89 @@ export class AssetManager implements IAssetManager {
|
|||||||
startTime: number,
|
startTime: number,
|
||||||
entry: AssetEntry
|
entry: AssetEntry
|
||||||
): Promise<IAssetLoadResult<T>> {
|
): Promise<IAssetLoadResult<T>> {
|
||||||
// 加载依赖 / Load dependencies
|
if (!this._reader) {
|
||||||
|
throw new Error('Asset reader not set. Call setReader() first.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load dependencies first.
|
||||||
|
// 先加载依赖。
|
||||||
if (metadata.dependencies.length > 0) {
|
if (metadata.dependencies.length > 0) {
|
||||||
await this.loadDependencies(metadata.dependencies, options);
|
await this.loadDependencies(metadata.dependencies, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行加载 / Execute loading
|
// Resolve absolute path.
|
||||||
const result = await loader.load(metadata.path, metadata, options);
|
// 解析绝对路径。
|
||||||
|
const absolutePath = this._database.resolveAbsolutePath(metadata.path);
|
||||||
|
|
||||||
// 更新条目 / Update entry
|
// Read content based on loader's content type.
|
||||||
entry.asset = result.asset;
|
// 根据加载器的内容类型读取内容。
|
||||||
|
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;
|
entry.state = AssetState.Loaded;
|
||||||
|
|
||||||
// 缓存资产 / Cache asset
|
// Cache asset.
|
||||||
this._cache.set(metadata.guid, result.asset);
|
// 缓存资产。
|
||||||
|
this._cache.set(metadata.guid, asset);
|
||||||
|
|
||||||
// 更新统计 / Update statistics
|
// Update statistics.
|
||||||
|
// 更新统计。
|
||||||
this._statistics.loadedCount++;
|
this._statistics.loadedCount++;
|
||||||
|
|
||||||
const loadResult: IAssetLoadResult<T> = {
|
return {
|
||||||
asset: result.asset as T,
|
asset: asset as T,
|
||||||
handle: entry.handle,
|
handle: entry.handle,
|
||||||
metadata,
|
metadata,
|
||||||
loadTime: performance.now() - startTime
|
loadTime: performance.now() - startTime
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return loadResult;
|
/**
|
||||||
|
* 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}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -238,17 +334,7 @@ export class AssetManager implements IAssetManager {
|
|||||||
let metadata = this._database.getMetadataByPath(path);
|
let metadata = this._database.getMetadataByPath(path);
|
||||||
if (!metadata) {
|
if (!metadata) {
|
||||||
// 动态创建元数据 / Create metadata dynamically
|
// 动态创建元数据 / Create metadata dynamically
|
||||||
const fileExt = path.substring(path.lastIndexOf('.')).toLowerCase();
|
const assetType = this.resolveAssetType(path);
|
||||||
let assetType = AssetType.Custom;
|
|
||||||
|
|
||||||
// 根据文件扩展名确定资产类型 / Determine asset type by file extension
|
|
||||||
if (['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp'].includes(fileExt)) {
|
|
||||||
assetType = AssetType.Texture;
|
|
||||||
} else if (['.json'].includes(fileExt)) {
|
|
||||||
assetType = AssetType.Json;
|
|
||||||
} else if (['.txt', '.md', '.xml', '.yaml'].includes(fileExt)) {
|
|
||||||
assetType = AssetType.Text;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成唯一GUID / Generate unique GUID
|
// 生成唯一GUID / Generate unique GUID
|
||||||
const dynamicGuid = `dynamic_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
const dynamicGuid = `dynamic_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
@@ -271,15 +357,59 @@ export class AssetManager implements IAssetManager {
|
|||||||
this._database.addAsset(metadata);
|
this._database.addAsset(metadata);
|
||||||
this._pathToGuid.set(path, metadata.guid);
|
this._pathToGuid.set(path, metadata.guid);
|
||||||
} else {
|
} 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);
|
this._pathToGuid.set(path, metadata.guid);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.loadAsset<T>(metadata.guid, options);
|
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);
|
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
|
* Load multiple assets
|
||||||
* 批量加载资产
|
* 批量加载资产
|
||||||
@@ -383,6 +513,19 @@ export class AssetManager implements IAssetManager {
|
|||||||
return this.getAsset<T>(guid);
|
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
|
* Check if asset is loaded
|
||||||
* 检查资产是否已加载
|
* 检查资产是否已加载
|
||||||
|
|||||||
@@ -86,11 +86,9 @@ export class AssetPathResolver {
|
|||||||
// 应用自定义转换器(如果提供)
|
// 应用自定义转换器(如果提供)
|
||||||
if (this.config.pathTransformer) {
|
if (this.config.pathTransformer) {
|
||||||
path = this.config.pathTransformer(path);
|
path = this.config.pathTransformer(path);
|
||||||
// Re-validate after transformation
|
// Transformer output is trusted (may be absolute path or asset:// URL)
|
||||||
const postTransform = PathValidator.validate(path);
|
// 转换器输出是可信的(可能是绝对路径或 asset:// URL)
|
||||||
if (!postTransform.valid) {
|
return path;
|
||||||
throw new Error(`Path transformer produced invalid path: ${postTransform.reason}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Platform-specific resolution
|
// Platform-specific resolution
|
||||||
|
|||||||
@@ -1,14 +1,29 @@
|
|||||||
/**
|
/**
|
||||||
* Asset System for ECS Framework
|
* Asset System for ECS Framework
|
||||||
* ECS框架的资产系统
|
* 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
|
// Types
|
||||||
export * from './types/AssetTypes';
|
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
|
// Interfaces
|
||||||
export * from './interfaces/IAssetLoader';
|
export * from './interfaces/IAssetLoader';
|
||||||
export * from './interfaces/IAssetManager';
|
export * from './interfaces/IAssetManager';
|
||||||
|
export * from './interfaces/IAssetReader';
|
||||||
|
export * from './interfaces/IResourceComponent';
|
||||||
|
|
||||||
// Core
|
// Core
|
||||||
export { AssetManager } from './core/AssetManager';
|
export { AssetManager } from './core/AssetManager';
|
||||||
@@ -30,6 +45,13 @@ export { BinaryLoader } from './loaders/BinaryLoader';
|
|||||||
export { EngineIntegration } from './integration/EngineIntegration';
|
export { EngineIntegration } from './integration/EngineIntegration';
|
||||||
export type { IEngineBridge } 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
|
// Default instance
|
||||||
import { AssetManager } from './core/AssetManager';
|
import { AssetManager } from './core/AssetManager';
|
||||||
|
|
||||||
@@ -43,9 +65,12 @@ export const assetManager = new AssetManager();
|
|||||||
* Initialize asset system with catalog
|
* Initialize asset system with catalog
|
||||||
* 使用目录初始化资产系统
|
* 使用目录初始化资产系统
|
||||||
*/
|
*/
|
||||||
export function initializeAssetSystem(catalog?: any): AssetManager {
|
export function initializeAssetSystem(catalog?: IAssetCatalog): AssetManager {
|
||||||
if (catalog) {
|
if (catalog) {
|
||||||
return new AssetManager(catalog);
|
return new AssetManager(catalog);
|
||||||
}
|
}
|
||||||
return assetManager;
|
return assetManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Re-export IAssetCatalog for initializeAssetSystem signature
|
||||||
|
import type { IAssetCatalog } from './types/AssetTypes';
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
import { AssetManager } from '../core/AssetManager';
|
import { AssetManager } from '../core/AssetManager';
|
||||||
import { AssetGUID } from '../types/AssetTypes';
|
import { AssetGUID } from '../types/AssetTypes';
|
||||||
import { ITextureAsset } from '../interfaces/IAssetLoader';
|
import { ITextureAsset } from '../interfaces/IAssetLoader';
|
||||||
|
import { globalPathResolver } from '../core/AssetPathResolver';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Engine bridge interface
|
* Engine bridge interface
|
||||||
@@ -63,24 +64,34 @@ export class EngineIntegration {
|
|||||||
/**
|
/**
|
||||||
* Load texture for component
|
* Load texture for component
|
||||||
* 为组件加载纹理
|
* 为组件加载纹理
|
||||||
|
*
|
||||||
|
* AssetManager 内部会处理路径解析,这里只需传入原始路径。
|
||||||
|
* AssetManager handles path resolution internally, just pass the original path here.
|
||||||
*/
|
*/
|
||||||
async loadTextureForComponent(texturePath: string): Promise<number> {
|
async loadTextureForComponent(texturePath: string): Promise<number> {
|
||||||
// 检查是否已有纹理ID / Check if texture ID exists
|
// 检查缓存(使用原始路径作为键)
|
||||||
|
// Check cache (using original path as key)
|
||||||
const existingId = this._pathToTextureId.get(texturePath);
|
const existingId = this._pathToTextureId.get(texturePath);
|
||||||
if (existingId) {
|
if (existingId) {
|
||||||
return existingId;
|
return existingId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 通过资产系统加载 / Load through asset system
|
// 通过资产系统加载(AssetManager 内部会解析路径)
|
||||||
|
// Load through asset system (AssetManager resolves path internally)
|
||||||
const result = await this._assetManager.loadAssetByPath<ITextureAsset>(texturePath);
|
const result = await this._assetManager.loadAssetByPath<ITextureAsset>(texturePath);
|
||||||
const textureAsset = result.asset;
|
const textureAsset = result.asset;
|
||||||
|
|
||||||
// 如果有引擎桥接,上传到GPU / Upload to GPU if bridge exists
|
// 如果有引擎桥接,上传到GPU
|
||||||
|
// Upload to GPU if bridge exists
|
||||||
|
// 使用 globalPathResolver 将路径转换为引擎可用的 URL
|
||||||
|
// Use globalPathResolver to convert path to engine-compatible URL
|
||||||
if (this._engineBridge && textureAsset.data) {
|
if (this._engineBridge && textureAsset.data) {
|
||||||
await this._engineBridge.loadTexture(textureAsset.textureId, texturePath);
|
const engineUrl = globalPathResolver.resolve(texturePath);
|
||||||
|
await this._engineBridge.loadTexture(textureAsset.textureId, engineUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 缓存映射 / Cache mapping
|
// 缓存映射(使用原始路径作为键,避免重复解析)
|
||||||
|
// Cache mapping (using original path as key to avoid re-resolving)
|
||||||
this._pathToTextureId.set(texturePath, textureAsset.textureId);
|
this._pathToTextureId.set(texturePath, textureAsset.textureId);
|
||||||
|
|
||||||
return textureAsset.textureId;
|
return textureAsset.textureId;
|
||||||
@@ -150,6 +161,25 @@ export class EngineIntegration {
|
|||||||
return results;
|
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
|
* Unload texture
|
||||||
* 卸载纹理
|
* 卸载纹理
|
||||||
|
|||||||
@@ -7,40 +7,64 @@ import {
|
|||||||
AssetType,
|
AssetType,
|
||||||
AssetGUID,
|
AssetGUID,
|
||||||
IAssetLoadOptions,
|
IAssetLoadOptions,
|
||||||
IAssetMetadata,
|
IAssetMetadata
|
||||||
IAssetLoadResult
|
|
||||||
} from '../types/AssetTypes';
|
} from '../types/AssetTypes';
|
||||||
|
import type { IAssetContent, AssetContentType } from './IAssetReader';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base asset loader interface
|
* 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> {
|
export interface IAssetLoader<T = unknown> {
|
||||||
/** 支持的资产类型 / Supported asset type */
|
/** Supported asset type. | 支持的资产类型。 */
|
||||||
readonly supportedType: AssetType;
|
readonly supportedType: AssetType;
|
||||||
|
|
||||||
/** 支持的文件扩展名 / Supported file extensions */
|
/** Supported file extensions. | 支持的文件扩展名。 */
|
||||||
readonly supportedExtensions: string[];
|
readonly supportedExtensions: string[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load an asset from the given path
|
* Required content type for this loader.
|
||||||
* 从指定路径加载资产
|
* 此加载器需要的内容类型。
|
||||||
|
*
|
||||||
|
* - 'text': For JSON, shader, material files
|
||||||
|
* - 'binary': For binary formats
|
||||||
|
* - 'image': For textures
|
||||||
|
* - 'audio': For audio files
|
||||||
*/
|
*/
|
||||||
load(
|
readonly contentType: AssetContentType;
|
||||||
path: string,
|
|
||||||
metadata: IAssetMetadata,
|
|
||||||
options?: IAssetLoadOptions
|
|
||||||
): Promise<IAssetLoadResult<T>>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate if the loader can handle this asset
|
* Parse asset from content.
|
||||||
* 验证加载器是否可以处理此资产
|
* 从内容解析资产。
|
||||||
|
*
|
||||||
|
* @param content - File content. | 文件内容。
|
||||||
|
* @param context - Parse context. | 解析上下文。
|
||||||
|
* @returns Parsed asset. | 解析后的资产。
|
||||||
*/
|
*/
|
||||||
canLoad(path: string, metadata: IAssetMetadata): boolean;
|
parse(content: IAssetContent, context: IAssetParseContext): Promise<T>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dispose loaded asset and free resources
|
* Dispose loaded asset and free resources.
|
||||||
* 释放已加载的资产并释放资源
|
* 释放已加载的资产。
|
||||||
*/
|
*/
|
||||||
dispose(asset: T): void;
|
dispose(asset: T): void;
|
||||||
}
|
}
|
||||||
@@ -73,6 +97,18 @@ export interface IAssetLoaderFactory {
|
|||||||
* 检查类型是否有加载器
|
* 检查类型是否有加载器
|
||||||
*/
|
*/
|
||||||
hasLoader(type: AssetType): boolean;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -69,6 +69,12 @@ export interface IAssetManager {
|
|||||||
*/
|
*/
|
||||||
getAssetByHandle<T = unknown>(handle: AssetHandle): T | null;
|
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
|
* Check if asset is loaded
|
||||||
* 检查资产是否已加载
|
* 检查资产是否已加载
|
||||||
|
|||||||
90
packages/asset-system/src/interfaces/IAssetReader.ts
Normal file
90
packages/asset-system/src/interfaces/IAssetReader.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
/**
|
||||||
|
* 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');
|
||||||
62
packages/asset-system/src/interfaces/IResourceComponent.ts
Normal file
62
packages/asset-system/src/interfaces/IResourceComponent.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* 资源组件接口 - 用于依赖运行时资源的组件(纹理、音频等)
|
||||||
|
* 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'
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -72,6 +72,57 @@ export class AssetLoaderFactory implements IAssetLoaderFactory {
|
|||||||
return this._loaders.has(type);
|
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
|
* Get all registered loaders
|
||||||
* 获取所有注册的加载器
|
* 获取所有注册的加载器
|
||||||
|
|||||||
@@ -3,14 +3,9 @@
|
|||||||
* 二进制资产加载器
|
* 二进制资产加载器
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import { AssetType } from '../types/AssetTypes';
|
||||||
AssetType,
|
import { IAssetLoader, IBinaryAsset, IAssetParseContext } from '../interfaces/IAssetLoader';
|
||||||
IAssetLoadOptions,
|
import { IAssetContent, AssetContentType } from '../interfaces/IAssetReader';
|
||||||
IAssetMetadata,
|
|
||||||
IAssetLoadResult,
|
|
||||||
AssetLoadError
|
|
||||||
} from '../types/AssetTypes';
|
|
||||||
import { IAssetLoader, IBinaryAsset } from '../interfaces/IAssetLoader';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Binary loader implementation
|
* Binary loader implementation
|
||||||
@@ -22,144 +17,27 @@ export class BinaryLoader implements IAssetLoader<IBinaryAsset> {
|
|||||||
'.bin', '.dat', '.raw', '.bytes',
|
'.bin', '.dat', '.raw', '.bytes',
|
||||||
'.wasm', '.so', '.dll', '.dylib'
|
'.wasm', '.so', '.dll', '.dylib'
|
||||||
];
|
];
|
||||||
|
readonly contentType: AssetContentType = 'binary';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load binary asset
|
* Parse binary from content.
|
||||||
* 加载二进制资产
|
* 从内容解析二进制。
|
||||||
*/
|
*/
|
||||||
async load(
|
async parse(content: IAssetContent, _context: IAssetParseContext): Promise<IBinaryAsset> {
|
||||||
path: string,
|
if (!content.binary) {
|
||||||
metadata: IAssetMetadata,
|
throw new Error('Binary content is empty');
|
||||||
options?: IAssetLoadOptions
|
|
||||||
): Promise<IAssetLoadResult<IBinaryAsset>> {
|
|
||||||
const startTime = performance.now();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await this.fetchWithTimeout(path, options?.timeout);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取MIME类型 / Get MIME type
|
|
||||||
const mimeType = response.headers.get('content-type') || undefined;
|
|
||||||
|
|
||||||
// 获取总大小用于进度回调 / Get total size for progress callback
|
|
||||||
const contentLength = response.headers.get('content-length');
|
|
||||||
const total = contentLength ? parseInt(contentLength, 10) : 0;
|
|
||||||
|
|
||||||
// 读取响应 / Read response
|
|
||||||
let data: ArrayBuffer;
|
|
||||||
if (options?.onProgress && total > 0) {
|
|
||||||
data = await this.readResponseWithProgress(response, total, options.onProgress);
|
|
||||||
} else {
|
|
||||||
data = await response.arrayBuffer();
|
|
||||||
}
|
|
||||||
|
|
||||||
const asset: IBinaryAsset = {
|
|
||||||
data,
|
|
||||||
mimeType
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
asset,
|
data: content.binary
|
||||||
handle: 0,
|
|
||||||
metadata,
|
|
||||||
loadTime: performance.now() - startTime
|
|
||||||
};
|
};
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
throw new AssetLoadError(
|
|
||||||
`Failed to load binary: ${error.message}`,
|
|
||||||
metadata.guid,
|
|
||||||
AssetType.Binary,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
throw AssetLoadError.fileNotFound(metadata.guid, path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch with timeout
|
|
||||||
* 带超时的fetch
|
|
||||||
*/
|
|
||||||
private async fetchWithTimeout(url: string, timeout = 30000): Promise<Response> {
|
|
||||||
const controller = new AbortController();
|
|
||||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(url, {
|
|
||||||
signal: controller.signal,
|
|
||||||
mode: 'cors',
|
|
||||||
credentials: 'same-origin'
|
|
||||||
});
|
|
||||||
return response;
|
|
||||||
} finally {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read response with progress
|
|
||||||
* 带进度读取响应
|
|
||||||
*/
|
|
||||||
private async readResponseWithProgress(
|
|
||||||
response: Response,
|
|
||||||
total: number,
|
|
||||||
onProgress: (progress: number) => void
|
|
||||||
): Promise<ArrayBuffer> {
|
|
||||||
const reader = response.body?.getReader();
|
|
||||||
if (!reader) {
|
|
||||||
return response.arrayBuffer();
|
|
||||||
}
|
|
||||||
|
|
||||||
const chunks: Uint8Array[] = [];
|
|
||||||
let receivedLength = 0;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const { done, value } = await reader.read();
|
|
||||||
|
|
||||||
if (done) break;
|
|
||||||
|
|
||||||
chunks.push(value);
|
|
||||||
receivedLength += value.length;
|
|
||||||
|
|
||||||
// 报告进度 / Report progress
|
|
||||||
onProgress(receivedLength / total);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 合并chunks到ArrayBuffer / Merge chunks into ArrayBuffer
|
|
||||||
const result = new Uint8Array(receivedLength);
|
|
||||||
let position = 0;
|
|
||||||
for (const chunk of chunks) {
|
|
||||||
result.set(chunk, position);
|
|
||||||
position += chunk.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate if the loader can handle this asset
|
|
||||||
* 验证加载器是否可以处理此资产
|
|
||||||
*/
|
|
||||||
canLoad(path: string, _metadata: IAssetMetadata): boolean {
|
|
||||||
const ext = path.toLowerCase().substring(path.lastIndexOf('.'));
|
|
||||||
return this.supportedExtensions.includes(ext);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Estimate memory usage for the asset
|
|
||||||
* 估算资产的内存使用量
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dispose loaded asset
|
* Dispose loaded asset
|
||||||
* 释放已加载的资产
|
* 释放已加载的资产
|
||||||
*/
|
*/
|
||||||
dispose(asset: IBinaryAsset): void {
|
dispose(asset: IBinaryAsset): void {
|
||||||
// ArrayBuffer无法直接释放,但可以清空引用 / Can't directly release ArrayBuffer, but clear reference
|
|
||||||
(asset as any).data = null;
|
(asset as any).data = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,9 @@
|
|||||||
* JSON资产加载器
|
* JSON资产加载器
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import { AssetType } from '../types/AssetTypes';
|
||||||
AssetType,
|
import { IAssetLoader, IJsonAsset, IAssetParseContext } from '../interfaces/IAssetLoader';
|
||||||
IAssetLoadOptions,
|
import { IAssetContent, AssetContentType } from '../interfaces/IAssetReader';
|
||||||
IAssetMetadata,
|
|
||||||
IAssetLoadResult,
|
|
||||||
AssetLoadError
|
|
||||||
} from '../types/AssetTypes';
|
|
||||||
import { IAssetLoader, IJsonAsset } from '../interfaces/IAssetLoader';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JSON loader implementation
|
* JSON loader implementation
|
||||||
@@ -19,144 +14,27 @@ import { IAssetLoader, IJsonAsset } from '../interfaces/IAssetLoader';
|
|||||||
export class JsonLoader implements IAssetLoader<IJsonAsset> {
|
export class JsonLoader implements IAssetLoader<IJsonAsset> {
|
||||||
readonly supportedType = AssetType.Json;
|
readonly supportedType = AssetType.Json;
|
||||||
readonly supportedExtensions = ['.json', '.jsonc'];
|
readonly supportedExtensions = ['.json', '.jsonc'];
|
||||||
|
readonly contentType: AssetContentType = 'text';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load JSON asset
|
* Parse JSON from text content.
|
||||||
* 加载JSON资产
|
* 从文本内容解析JSON。
|
||||||
*/
|
*/
|
||||||
async load(
|
async parse(content: IAssetContent, _context: IAssetParseContext): Promise<IJsonAsset> {
|
||||||
path: string,
|
if (!content.text) {
|
||||||
metadata: IAssetMetadata,
|
throw new Error('JSON content is empty');
|
||||||
options?: IAssetLoadOptions
|
|
||||||
): Promise<IAssetLoadResult<IJsonAsset>> {
|
|
||||||
const startTime = performance.now();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await this.fetchWithTimeout(path, options?.timeout);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取总大小用于进度回调 / Get total size for progress callback
|
|
||||||
const contentLength = response.headers.get('content-length');
|
|
||||||
const total = contentLength ? parseInt(contentLength, 10) : 0;
|
|
||||||
|
|
||||||
// 读取响应 / Read response
|
|
||||||
let jsonData: unknown;
|
|
||||||
if (options?.onProgress && total > 0) {
|
|
||||||
jsonData = await this.readResponseWithProgress(response, total, options.onProgress);
|
|
||||||
} else {
|
|
||||||
jsonData = await response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
const asset: IJsonAsset = {
|
|
||||||
data: jsonData
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
asset,
|
data: JSON.parse(content.text)
|
||||||
handle: 0,
|
|
||||||
metadata,
|
|
||||||
loadTime: performance.now() - startTime
|
|
||||||
};
|
};
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
throw new AssetLoadError(
|
|
||||||
`Failed to load JSON: ${error.message}`,
|
|
||||||
metadata.guid,
|
|
||||||
AssetType.Json,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
throw AssetLoadError.fileNotFound(metadata.guid, path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch with timeout
|
|
||||||
* 带超时的fetch
|
|
||||||
*/
|
|
||||||
private async fetchWithTimeout(url: string, timeout = 30000): Promise<Response> {
|
|
||||||
const controller = new AbortController();
|
|
||||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(url, {
|
|
||||||
signal: controller.signal,
|
|
||||||
mode: 'cors',
|
|
||||||
credentials: 'same-origin'
|
|
||||||
});
|
|
||||||
return response;
|
|
||||||
} finally {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read response with progress
|
|
||||||
* 带进度读取响应
|
|
||||||
*/
|
|
||||||
private async readResponseWithProgress(
|
|
||||||
response: Response,
|
|
||||||
total: number,
|
|
||||||
onProgress: (progress: number) => void
|
|
||||||
): Promise<unknown> {
|
|
||||||
const reader = response.body?.getReader();
|
|
||||||
if (!reader) {
|
|
||||||
return response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
const chunks: Uint8Array[] = [];
|
|
||||||
let receivedLength = 0;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const { done, value } = await reader.read();
|
|
||||||
|
|
||||||
if (done) break;
|
|
||||||
|
|
||||||
chunks.push(value);
|
|
||||||
receivedLength += value.length;
|
|
||||||
|
|
||||||
// 报告进度 / Report progress
|
|
||||||
onProgress(receivedLength / total);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 合并chunks / Merge chunks
|
|
||||||
const allChunks = new Uint8Array(receivedLength);
|
|
||||||
let position = 0;
|
|
||||||
for (const chunk of chunks) {
|
|
||||||
allChunks.set(chunk, position);
|
|
||||||
position += chunk.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解码为字符串并解析JSON / Decode to string and parse JSON
|
|
||||||
const decoder = new TextDecoder('utf-8');
|
|
||||||
const jsonString = decoder.decode(allChunks);
|
|
||||||
return JSON.parse(jsonString);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate if the loader can handle this asset
|
|
||||||
* 验证加载器是否可以处理此资产
|
|
||||||
*/
|
|
||||||
canLoad(path: string, _metadata: IAssetMetadata): boolean {
|
|
||||||
const ext = path.toLowerCase().substring(path.lastIndexOf('.'));
|
|
||||||
return this.supportedExtensions.includes(ext);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Estimate memory usage for the asset
|
|
||||||
* 估算资产的内存使用量
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dispose loaded asset
|
* Dispose loaded asset
|
||||||
* 释放已加载的资产
|
* 释放已加载的资产
|
||||||
*/
|
*/
|
||||||
dispose(asset: IJsonAsset): void {
|
dispose(asset: IJsonAsset): void {
|
||||||
// JSON资产通常不需要特殊清理 / JSON assets usually don't need special cleanup
|
|
||||||
// 但可以清空引用以帮助GC / But can clear references to help GC
|
|
||||||
(asset as any).data = null;
|
(asset as any).data = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,9 @@
|
|||||||
* 文本资产加载器
|
* 文本资产加载器
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import { AssetType } from '../types/AssetTypes';
|
||||||
AssetType,
|
import { IAssetLoader, ITextAsset, IAssetParseContext } from '../interfaces/IAssetLoader';
|
||||||
IAssetLoadOptions,
|
import { IAssetContent, AssetContentType } from '../interfaces/IAssetReader';
|
||||||
IAssetMetadata,
|
|
||||||
IAssetLoadResult,
|
|
||||||
AssetLoadError
|
|
||||||
} from '../types/AssetTypes';
|
|
||||||
import { IAssetLoader, ITextAsset } from '../interfaces/IAssetLoader';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Text loader implementation
|
* Text loader implementation
|
||||||
@@ -19,115 +14,21 @@ import { IAssetLoader, ITextAsset } from '../interfaces/IAssetLoader';
|
|||||||
export class TextLoader implements IAssetLoader<ITextAsset> {
|
export class TextLoader implements IAssetLoader<ITextAsset> {
|
||||||
readonly supportedType = AssetType.Text;
|
readonly supportedType = AssetType.Text;
|
||||||
readonly supportedExtensions = ['.txt', '.text', '.md', '.csv', '.xml', '.html', '.css', '.js', '.ts'];
|
readonly supportedExtensions = ['.txt', '.text', '.md', '.csv', '.xml', '.html', '.css', '.js', '.ts'];
|
||||||
|
readonly contentType: AssetContentType = 'text';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load text asset
|
* Parse text from content.
|
||||||
* 加载文本资产
|
* 从内容解析文本。
|
||||||
*/
|
*/
|
||||||
async load(
|
async parse(content: IAssetContent, _context: IAssetParseContext): Promise<ITextAsset> {
|
||||||
path: string,
|
if (!content.text) {
|
||||||
metadata: IAssetMetadata,
|
throw new Error('Text content is empty');
|
||||||
options?: IAssetLoadOptions
|
|
||||||
): Promise<IAssetLoadResult<ITextAsset>> {
|
|
||||||
const startTime = performance.now();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await this.fetchWithTimeout(path, options?.timeout);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取总大小用于进度回调 / Get total size for progress callback
|
|
||||||
const contentLength = response.headers.get('content-length');
|
|
||||||
const total = contentLength ? parseInt(contentLength, 10) : 0;
|
|
||||||
|
|
||||||
// 读取响应 / Read response
|
|
||||||
let content: string;
|
|
||||||
if (options?.onProgress && total > 0) {
|
|
||||||
content = await this.readResponseWithProgress(response, total, options.onProgress);
|
|
||||||
} else {
|
|
||||||
content = await response.text();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检测编码 / Detect encoding
|
|
||||||
const encoding = this.detectEncoding(content);
|
|
||||||
|
|
||||||
const asset: ITextAsset = {
|
|
||||||
content,
|
|
||||||
encoding
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
asset,
|
content: content.text,
|
||||||
handle: 0,
|
encoding: this.detectEncoding(content.text)
|
||||||
metadata,
|
|
||||||
loadTime: performance.now() - startTime
|
|
||||||
};
|
};
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
throw new AssetLoadError(
|
|
||||||
`Failed to load text: ${error.message}`,
|
|
||||||
metadata.guid,
|
|
||||||
AssetType.Text,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
throw AssetLoadError.fileNotFound(metadata.guid, path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch with timeout
|
|
||||||
* 带超时的fetch
|
|
||||||
*/
|
|
||||||
private async fetchWithTimeout(url: string, timeout = 30000): Promise<Response> {
|
|
||||||
const controller = new AbortController();
|
|
||||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(url, {
|
|
||||||
signal: controller.signal,
|
|
||||||
mode: 'cors',
|
|
||||||
credentials: 'same-origin'
|
|
||||||
});
|
|
||||||
return response;
|
|
||||||
} finally {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read response with progress
|
|
||||||
* 带进度读取响应
|
|
||||||
*/
|
|
||||||
private async readResponseWithProgress(
|
|
||||||
response: Response,
|
|
||||||
total: number,
|
|
||||||
onProgress: (progress: number) => void
|
|
||||||
): Promise<string> {
|
|
||||||
const reader = response.body?.getReader();
|
|
||||||
if (!reader) {
|
|
||||||
return response.text();
|
|
||||||
}
|
|
||||||
|
|
||||||
const decoder = new TextDecoder('utf-8');
|
|
||||||
let result = '';
|
|
||||||
let receivedLength = 0;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const { done, value } = await reader.read();
|
|
||||||
|
|
||||||
if (done) break;
|
|
||||||
|
|
||||||
receivedLength += value.length;
|
|
||||||
result += decoder.decode(value, { stream: true });
|
|
||||||
|
|
||||||
// 报告进度 / Report progress
|
|
||||||
onProgress(receivedLength / total);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -135,38 +36,20 @@ export class TextLoader implements IAssetLoader<ITextAsset> {
|
|||||||
* 检测文本编码
|
* 检测文本编码
|
||||||
*/
|
*/
|
||||||
private detectEncoding(content: string): 'utf8' | 'utf16' | 'ascii' {
|
private detectEncoding(content: string): 'utf8' | 'utf16' | 'ascii' {
|
||||||
// 简单的编码检测 / Simple encoding detection
|
|
||||||
// 检查是否包含非ASCII字符 / Check for non-ASCII characters
|
|
||||||
for (let i = 0; i < content.length; i++) {
|
for (let i = 0; i < content.length; i++) {
|
||||||
const charCode = content.charCodeAt(i);
|
const charCode = content.charCodeAt(i);
|
||||||
if (charCode > 127) {
|
if (charCode > 127) {
|
||||||
// 包含非ASCII字符,可能是UTF-8或UTF-16 / Contains non-ASCII, likely UTF-8 or UTF-16
|
|
||||||
return charCode > 255 ? 'utf16' : 'utf8';
|
return charCode > 255 ? 'utf16' : 'utf8';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 'ascii';
|
return 'ascii';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate if the loader can handle this asset
|
|
||||||
* 验证加载器是否可以处理此资产
|
|
||||||
*/
|
|
||||||
canLoad(path: string, _metadata: IAssetMetadata): boolean {
|
|
||||||
const ext = path.toLowerCase().substring(path.lastIndexOf('.'));
|
|
||||||
return this.supportedExtensions.includes(ext);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Estimate memory usage for the asset
|
|
||||||
* 估算资产的内存使用量
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dispose loaded asset
|
* Dispose loaded asset
|
||||||
* 释放已加载的资产
|
* 释放已加载的资产
|
||||||
*/
|
*/
|
||||||
dispose(asset: ITextAsset): void {
|
dispose(asset: ITextAsset): void {
|
||||||
// 清空内容以帮助GC / Clear content to help GC
|
|
||||||
(asset as any).content = '';
|
(asset as any).content = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,9 @@
|
|||||||
* 纹理资产加载器
|
* 纹理资产加载器
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import { AssetType } from '../types/AssetTypes';
|
||||||
AssetType,
|
import { IAssetLoader, ITextureAsset, IAssetParseContext } from '../interfaces/IAssetLoader';
|
||||||
IAssetLoadOptions,
|
import { IAssetContent, AssetContentType } from '../interfaces/IAssetReader';
|
||||||
IAssetMetadata,
|
|
||||||
IAssetLoadResult,
|
|
||||||
AssetLoadError
|
|
||||||
} from '../types/AssetTypes';
|
|
||||||
import { IAssetLoader, ITextureAsset } from '../interfaces/IAssetLoader';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Texture loader implementation
|
* Texture loader implementation
|
||||||
@@ -19,147 +14,36 @@ import { IAssetLoader, ITextureAsset } from '../interfaces/IAssetLoader';
|
|||||||
export class TextureLoader implements IAssetLoader<ITextureAsset> {
|
export class TextureLoader implements IAssetLoader<ITextureAsset> {
|
||||||
readonly supportedType = AssetType.Texture;
|
readonly supportedType = AssetType.Texture;
|
||||||
readonly supportedExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp'];
|
readonly supportedExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp'];
|
||||||
|
readonly contentType: AssetContentType = 'image';
|
||||||
|
|
||||||
private static _nextTextureId = 1;
|
private static _nextTextureId = 1;
|
||||||
private readonly _loadedTextures = new Map<string, ITextureAsset>();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load texture asset
|
* Parse texture from image content.
|
||||||
* 加载纹理资产
|
* 从图片内容解析纹理。
|
||||||
*/
|
*/
|
||||||
async load(
|
async parse(content: IAssetContent, context: IAssetParseContext): Promise<ITextureAsset> {
|
||||||
path: string,
|
if (!content.image) {
|
||||||
metadata: IAssetMetadata,
|
throw new Error('Texture content is empty');
|
||||||
options?: IAssetLoadOptions
|
|
||||||
): Promise<IAssetLoadResult<ITextureAsset>> {
|
|
||||||
const startTime = performance.now();
|
|
||||||
|
|
||||||
// 检查缓存 / Check cache
|
|
||||||
if (!options?.forceReload && this._loadedTextures.has(path)) {
|
|
||||||
const cached = this._loadedTextures.get(path)!;
|
|
||||||
return {
|
|
||||||
asset: cached,
|
|
||||||
handle: cached.textureId,
|
|
||||||
metadata,
|
|
||||||
loadTime: 0
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const image = content.image;
|
||||||
// 创建图像元素 / Create image element
|
|
||||||
const image = await this.loadImage(path, options);
|
|
||||||
|
|
||||||
// 创建纹理资产 / Create texture asset
|
|
||||||
const textureAsset: ITextureAsset = {
|
const textureAsset: ITextureAsset = {
|
||||||
textureId: TextureLoader._nextTextureId++,
|
textureId: TextureLoader._nextTextureId++,
|
||||||
width: image.width,
|
width: image.width,
|
||||||
height: image.height,
|
height: image.height,
|
||||||
format: 'rgba', // 默认格式 / Default format
|
format: 'rgba',
|
||||||
hasMipmaps: false,
|
hasMipmaps: false,
|
||||||
data: image
|
data: image
|
||||||
};
|
};
|
||||||
|
|
||||||
// 缓存纹理 / Cache texture
|
// Upload to GPU if bridge exists.
|
||||||
this._loadedTextures.set(path, textureAsset);
|
|
||||||
|
|
||||||
// 触发引擎纹理加载(如果有引擎桥接) / Trigger engine texture loading if bridge exists
|
|
||||||
if (typeof window !== 'undefined' && (window as any).engineBridge) {
|
if (typeof window !== 'undefined' && (window as any).engineBridge) {
|
||||||
await this.uploadToGPU(textureAsset, path);
|
await this.uploadToGPU(textureAsset, context.metadata.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return textureAsset;
|
||||||
asset: textureAsset,
|
|
||||||
handle: textureAsset.textureId,
|
|
||||||
metadata,
|
|
||||||
loadTime: performance.now() - startTime
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
throw AssetLoadError.fileNotFound(metadata.guid, path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load image from URL
|
|
||||||
* 从URL加载图像
|
|
||||||
*/
|
|
||||||
private async loadImage(url: string, options?: IAssetLoadOptions): Promise<HTMLImageElement> {
|
|
||||||
// For Tauri asset URLs, use fetch to load the image
|
|
||||||
// 对于Tauri资产URL,使用fetch加载图像
|
|
||||||
if (url.startsWith('http://asset.localhost/') || url.startsWith('asset://')) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(url);
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to fetch image: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
const blob = await response.blob();
|
|
||||||
const blobUrl = URL.createObjectURL(blob);
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const image = new Image();
|
|
||||||
image.onload = () => {
|
|
||||||
// Clean up blob URL after loading
|
|
||||||
// 加载后清理blob URL
|
|
||||||
URL.revokeObjectURL(blobUrl);
|
|
||||||
resolve(image);
|
|
||||||
};
|
|
||||||
image.onerror = () => {
|
|
||||||
URL.revokeObjectURL(blobUrl);
|
|
||||||
reject(new Error(`Failed to load image from blob: ${url}`));
|
|
||||||
};
|
|
||||||
image.src = blobUrl;
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(`Failed to load Tauri asset: ${url} - ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// For regular URLs, use standard Image loading
|
|
||||||
// 对于常规URL,使用标准Image加载
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const image = new Image();
|
|
||||||
image.crossOrigin = 'anonymous';
|
|
||||||
|
|
||||||
// 超时处理 / Timeout handling
|
|
||||||
const timeout = options?.timeout || 30000;
|
|
||||||
const timeoutId = setTimeout(() => {
|
|
||||||
reject(new Error(`Image load timeout: ${url}`));
|
|
||||||
}, timeout);
|
|
||||||
|
|
||||||
// 进度回调 / Progress callback
|
|
||||||
if (options?.onProgress) {
|
|
||||||
// 图像加载没有真正的进度事件,模拟进度 / Images don't have real progress events, simulate
|
|
||||||
let progress = 0;
|
|
||||||
const progressInterval = setInterval(() => {
|
|
||||||
progress = Math.min(progress + 0.1, 0.9);
|
|
||||||
options.onProgress!(progress);
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
image.onload = () => {
|
|
||||||
clearInterval(progressInterval);
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
options.onProgress!(1);
|
|
||||||
resolve(image);
|
|
||||||
};
|
|
||||||
|
|
||||||
image.onerror = () => {
|
|
||||||
clearInterval(progressInterval);
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
reject(new Error(`Failed to load image: ${url}`));
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
image.onload = () => {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
resolve(image);
|
|
||||||
};
|
|
||||||
|
|
||||||
image.onerror = () => {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
reject(new Error(`Failed to load image: ${url}`));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
image.src = url;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -173,34 +57,12 @@ export class TextureLoader implements IAssetLoader<ITextureAsset> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate if the loader can handle this asset
|
|
||||||
* 验证加载器是否可以处理此资产
|
|
||||||
*/
|
|
||||||
canLoad(path: string, _metadata: IAssetMetadata): boolean {
|
|
||||||
const ext = path.toLowerCase().substring(path.lastIndexOf('.'));
|
|
||||||
return this.supportedExtensions.includes(ext);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Estimate memory usage for the asset
|
|
||||||
* 估算资产的内存使用量
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dispose loaded asset
|
* Dispose loaded asset
|
||||||
* 释放已加载的资产
|
* 释放已加载的资产
|
||||||
*/
|
*/
|
||||||
dispose(asset: ITextureAsset): void {
|
dispose(asset: ITextureAsset): void {
|
||||||
// 从缓存中移除 / Remove from cache
|
// Release GPU resources.
|
||||||
for (const [path, cached] of this._loadedTextures.entries()) {
|
|
||||||
if (cached === asset) {
|
|
||||||
this._loadedTextures.delete(path);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 释放GPU资源 / Release GPU resources
|
|
||||||
if (typeof window !== 'undefined' && (window as any).engineBridge) {
|
if (typeof window !== 'undefined' && (window as any).engineBridge) {
|
||||||
const bridge = (window as any).engineBridge;
|
const bridge = (window as any).engineBridge;
|
||||||
if (bridge.unloadTexture) {
|
if (bridge.unloadTexture) {
|
||||||
@@ -208,7 +70,7 @@ export class TextureLoader implements IAssetLoader<ITextureAsset> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清理图像数据 / Clean up image data
|
// Clean up image data.
|
||||||
if (asset.data instanceof HTMLImageElement) {
|
if (asset.data instanceof HTMLImageElement) {
|
||||||
asset.data.src = '';
|
asset.data.src = '';
|
||||||
}
|
}
|
||||||
|
|||||||
275
packages/asset-system/src/runtime/RuntimeCatalog.ts
Normal file
275
packages/asset-system/src/runtime/RuntimeCatalog.ts
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
/**
|
||||||
|
* 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();
|
||||||
155
packages/asset-system/src/services/SceneResourceManager.ts
Normal file
155
packages/asset-system/src/services/SceneResourceManager.ts
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
/**
|
||||||
|
* 场景资源管理器 - 集中式场景资源加载
|
||||||
|
* 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,41 +33,50 @@ export enum AssetState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asset types supported by the system
|
* 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 enum AssetType {
|
export type AssetType = string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Built-in asset types provided by asset-system
|
||||||
|
* asset-system 提供的内置资产类型
|
||||||
|
*/
|
||||||
|
export const AssetType = {
|
||||||
/** 纹理 */
|
/** 纹理 */
|
||||||
Texture = 'texture',
|
Texture: 'texture',
|
||||||
/** 网格 */
|
/** 网格 */
|
||||||
Mesh = 'mesh',
|
Mesh: 'mesh',
|
||||||
/** 材质 */
|
/** 材质 */
|
||||||
Material = 'material',
|
Material: 'material',
|
||||||
/** 着色器 */
|
/** 着色器 */
|
||||||
Shader = 'shader',
|
Shader: 'shader',
|
||||||
/** 音频 */
|
/** 音频 */
|
||||||
Audio = 'audio',
|
Audio: 'audio',
|
||||||
/** 字体 */
|
/** 字体 */
|
||||||
Font = 'font',
|
Font: 'font',
|
||||||
/** 预制体 */
|
/** 预制体 */
|
||||||
Prefab = 'prefab',
|
Prefab: 'prefab',
|
||||||
/** 场景 */
|
/** 场景 */
|
||||||
Scene = 'scene',
|
Scene: 'scene',
|
||||||
/** 脚本 */
|
/** 脚本 */
|
||||||
Script = 'script',
|
Script: 'script',
|
||||||
/** 动画片段 */
|
/** 动画片段 */
|
||||||
AnimationClip = 'animation',
|
AnimationClip: 'animation',
|
||||||
/** 行为树 */
|
|
||||||
BehaviorTree = 'behaviortree',
|
|
||||||
/** JSON数据 */
|
/** JSON数据 */
|
||||||
Json = 'json',
|
Json: 'json',
|
||||||
/** 文本 */
|
/** 文本 */
|
||||||
Text = 'text',
|
Text: 'text',
|
||||||
/** 二进制 */
|
/** 二进制 */
|
||||||
Binary = 'binary',
|
Binary: 'binary',
|
||||||
/** 自定义 */
|
/** 自定义 */
|
||||||
Custom = 'custom'
|
Custom: 'custom'
|
||||||
}
|
} as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Platform variants for assets
|
* Platform variants for assets
|
||||||
|
|||||||
@@ -6,29 +6,77 @@
|
|||||||
* 验证并清理资产路径以确保安全
|
* 验证并清理资产路径以确保安全
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
export class PathValidator {
|
||||||
// Dangerous path patterns
|
// Dangerous path patterns (without absolute path checks)
|
||||||
private static readonly DANGEROUS_PATTERNS = [
|
private static readonly DANGEROUS_PATTERNS_STRICT = [
|
||||||
/\.\.[/\\]/g, // Path traversal attempts (..)
|
/\.\.[/\\]/g, // Path traversal attempts (..)
|
||||||
/^[/\\]/, // Absolute paths on Unix
|
/^[/\\]/, // Absolute paths on Unix
|
||||||
/^[a-zA-Z]:[/\\]/, // Absolute paths on Windows
|
/^[a-zA-Z]:[/\\]/, // Absolute paths on Windows
|
||||||
/[<>:"|?*]/, // Invalid characters for Windows paths
|
|
||||||
/\0/, // Null bytes
|
/\0/, // Null bytes
|
||||||
/%00/, // URL encoded null bytes
|
/%00/, // URL encoded null bytes
|
||||||
/\.\.%2[fF]/ // URL encoded path traversal
|
/\.\.%2[fF]/ // URL encoded path traversal
|
||||||
];
|
];
|
||||||
|
|
||||||
// Valid path characters (alphanumeric, dash, underscore, dot, slash)
|
// 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\-_./\\@]+$/;
|
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
|
// Maximum path length
|
||||||
private static readonly MAX_PATH_LENGTH = 260;
|
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
|
* Validate if a path is safe
|
||||||
* 验证路径是否安全
|
* 验证路径是否安全
|
||||||
*/
|
*/
|
||||||
static validate(path: string): { valid: boolean; reason?: string } {
|
static validate(path: string, options?: PathValidationOptions): { valid: boolean; reason?: string } {
|
||||||
|
const opts = { ...this._globalOptions, ...options };
|
||||||
|
|
||||||
// Check for null/undefined/empty
|
// Check for null/undefined/empty
|
||||||
if (!path || typeof path !== 'string') {
|
if (!path || typeof path !== 'string') {
|
||||||
return { valid: false, reason: 'Path is empty or invalid' };
|
return { valid: false, reason: 'Path is empty or invalid' };
|
||||||
@@ -39,15 +87,29 @@ export class PathValidator {
|
|||||||
return { valid: false, reason: `Path exceeds maximum length of ${this.MAX_PATH_LENGTH} characters` };
|
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
|
// Check for dangerous patterns
|
||||||
for (const pattern of this.DANGEROUS_PATTERNS) {
|
for (const pattern of patterns) {
|
||||||
if (pattern.test(path)) {
|
if (pattern.test(path)) {
|
||||||
return { valid: false, reason: 'Path contains dangerous pattern' };
|
return { valid: false, reason: 'Path contains dangerous pattern' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for valid characters
|
// Check for valid characters
|
||||||
if (!this.VALID_PATH_REGEX.test(path)) {
|
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: false, reason: 'Path contains invalid characters' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
81
packages/asset-system/src/utils/UVHelper.ts
Normal file
81
packages/asset-system/src/utils/UVHelper.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
/**
|
||||||
|
* 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(', ')}]`);
|
||||||
|
}
|
||||||
|
}
|
||||||
22
packages/asset-system/tsconfig.build.json
Normal file
22
packages/asset-system/tsconfig.build.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"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"]
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
"target": "ES2020",
|
"target": "ES2020",
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"composite": true,
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"declarationMap": true,
|
"declarationMap": true,
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
|
|||||||
7
packages/asset-system/tsup.config.ts
Normal file
7
packages/asset-system/tsup.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'tsup';
|
||||||
|
import { runtimeOnlyPreset } from '../build-config/src/presets/plugin-tsup';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
...runtimeOnlyPreset(),
|
||||||
|
tsconfig: 'tsconfig.build.json'
|
||||||
|
});
|
||||||
43
packages/audio/module.json
Normal file
43
packages/audio/module.json
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
46
packages/audio/package.json
Normal file
46
packages/audio/package.json
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
28
packages/audio/src/AudioPlugin.ts
Normal file
28
packages/audio/src/AudioPlugin.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
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()
|
||||||
|
};
|
||||||
43
packages/audio/src/AudioSourceComponent.ts
Normal file
43
packages/audio/src/AudioSourceComponent.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
2
packages/audio/src/index.ts
Normal file
2
packages/audio/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { AudioSourceComponent } from './AudioSourceComponent';
|
||||||
|
export { AudioPlugin } from './AudioPlugin';
|
||||||
12
packages/audio/tsconfig.build.json
Normal file
12
packages/audio/tsconfig.build.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": false,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
||||||
|
}
|
||||||
13
packages/audio/tsconfig.json
Normal file
13
packages/audio/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"],
|
||||||
|
"references": [
|
||||||
|
{ "path": "../core" }
|
||||||
|
]
|
||||||
|
}
|
||||||
7
packages/audio/tsup.config.ts
Normal file
7
packages/audio/tsup.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'tsup';
|
||||||
|
import { runtimeOnlyPreset } from '../build-config/src/presets/plugin-tsup';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
...runtimeOnlyPreset(),
|
||||||
|
tsconfig: 'tsconfig.build.json'
|
||||||
|
});
|
||||||
@@ -1,59 +1,49 @@
|
|||||||
{
|
{
|
||||||
"name": "@esengine/behavior-tree-editor",
|
"name": "@esengine/behavior-tree-editor",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Behavior Tree Editor Plugin for ECS Framework",
|
"description": "Editor support for @esengine/behavior-tree - visual editor, inspectors, and tools",
|
||||||
"type": "module",
|
"main": "dist/index.js",
|
||||||
"main": "dist/index.esm.js",
|
"module": "dist/index.js",
|
||||||
"module": "dist/index.esm.js",
|
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rimraf bin dist tsconfig.tsbuildinfo",
|
"build": "tsup",
|
||||||
"prebuild": "npm run clean",
|
"build:watch": "tsup --watch",
|
||||||
"build": "npm run build:tsc && npm run copy:css && npm run build:rollup",
|
"type-check": "tsc --noEmit",
|
||||||
"build:tsc": "tsc",
|
"clean": "rimraf dist"
|
||||||
"copy:css": "node scripts/copy-css.js",
|
},
|
||||||
"build:rollup": "rollup -c",
|
"dependencies": {
|
||||||
"dev": "rollup -c -w"
|
"@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": [
|
"keywords": [
|
||||||
"ecs",
|
"ecs",
|
||||||
"behavior-tree",
|
"behavior-tree",
|
||||||
"editor",
|
"editor"
|
||||||
"plugin"
|
|
||||||
],
|
],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"devDependencies": {
|
|
||||||
"@esengine/behavior-tree": "workspace:*",
|
|
||||||
"@esengine/ecs-framework": "workspace:*",
|
|
||||||
"@esengine/editor-core": "workspace:*",
|
|
||||||
"@rollup/plugin-commonjs": "^28.0.1",
|
|
||||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
|
||||||
"@types/react": "^18.3.18",
|
|
||||||
"lucide-react": "^0.469.0",
|
|
||||||
"react": "^18.3.1",
|
|
||||||
"rimraf": "^6.0.1",
|
|
||||||
"rollup": "^4.28.1",
|
|
||||||
"rollup-plugin-copy": "^3.5.0",
|
|
||||||
"rollup-plugin-dts": "^6.1.1",
|
|
||||||
"rollup-plugin-postcss": "^4.0.2",
|
|
||||||
"typescript": "^5.8.2",
|
|
||||||
"zustand": "^5.0.2"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@esengine/behavior-tree": "*",
|
|
||||||
"@esengine/ecs-framework": "*",
|
|
||||||
"@esengine/editor-core": "*",
|
|
||||||
"@tauri-apps/api": "*",
|
|
||||||
"@tauri-apps/plugin-dialog": "*",
|
|
||||||
"@tauri-apps/plugin-http": "*",
|
|
||||||
"lucide-react": "^0.469.0",
|
|
||||||
"react": "^18.3.1",
|
|
||||||
"tsyringe": "*",
|
|
||||||
"zustand": "^5.0.2"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"mobx": "^6.15.0",
|
|
||||||
"mobx-react-lite": "^4.1.1"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
const resolve = require('@rollup/plugin-node-resolve');
|
|
||||||
const commonjs = require('@rollup/plugin-commonjs');
|
|
||||||
const dts = require('rollup-plugin-dts').default;
|
|
||||||
const postcss = require('rollup-plugin-postcss');
|
|
||||||
|
|
||||||
const external = [
|
|
||||||
'react',
|
|
||||||
'react/jsx-runtime',
|
|
||||||
'zustand',
|
|
||||||
'zustand/middleware',
|
|
||||||
'lucide-react',
|
|
||||||
'@esengine/ecs-framework',
|
|
||||||
'@esengine/editor-core',
|
|
||||||
'@esengine/behavior-tree',
|
|
||||||
'tsyringe',
|
|
||||||
'@tauri-apps/api/core',
|
|
||||||
'@tauri-apps/plugin-dialog'
|
|
||||||
];
|
|
||||||
|
|
||||||
module.exports = [
|
|
||||||
{
|
|
||||||
input: 'bin/index.js',
|
|
||||||
output: {
|
|
||||||
file: 'dist/index.esm.js',
|
|
||||||
format: 'es',
|
|
||||||
sourcemap: true,
|
|
||||||
exports: 'named',
|
|
||||||
inlineDynamicImports: true
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
resolve({
|
|
||||||
extensions: ['.js', '.jsx']
|
|
||||||
}),
|
|
||||||
postcss({
|
|
||||||
inject: true,
|
|
||||||
minimize: false
|
|
||||||
}),
|
|
||||||
commonjs()
|
|
||||||
],
|
|
||||||
external,
|
|
||||||
onwarn(warning, warn) {
|
|
||||||
if (warning.code === 'CIRCULAR_DEPENDENCY' || warning.code === 'THIS_IS_UNDEFINED') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
warn(warning);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// 类型定义构建
|
|
||||||
{
|
|
||||||
input: 'bin/index.d.ts',
|
|
||||||
output: {
|
|
||||||
file: 'dist/index.d.ts',
|
|
||||||
format: 'es'
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
dts({
|
|
||||||
respectExternal: true
|
|
||||||
})
|
|
||||||
],
|
|
||||||
external: [
|
|
||||||
...external,
|
|
||||||
/\.css$/
|
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { readdirSync, statSync, copyFileSync, mkdirSync } from 'fs';
|
|
||||||
import { join, dirname, relative } from 'path';
|
|
||||||
|
|
||||||
function copyCSS(srcDir, destDir) {
|
|
||||||
const files = readdirSync(srcDir);
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
const srcPath = join(srcDir, file);
|
|
||||||
const stat = statSync(srcPath);
|
|
||||||
|
|
||||||
if (stat.isDirectory()) {
|
|
||||||
copyCSS(srcPath, destDir);
|
|
||||||
} else if (file.endsWith('.css')) {
|
|
||||||
const relativePath = relative('src', srcPath);
|
|
||||||
const destPath = join(destDir, relativePath);
|
|
||||||
|
|
||||||
mkdirSync(dirname(destPath), { recursive: true });
|
|
||||||
copyFileSync(srcPath, destPath);
|
|
||||||
console.log(`Copied: ${relativePath}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
copyCSS('src', 'bin');
|
|
||||||
console.log('CSS files copied successfully!');
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
import { singleton } from 'tsyringe';
|
|
||||||
import { Core, createLogger } from '@esengine/ecs-framework';
|
|
||||||
import { CompilerRegistry, IEditorModule, IModuleContext, PanelPosition } from '@esengine/editor-core';
|
|
||||||
import { BehaviorTreeService } from './services/BehaviorTreeService';
|
|
||||||
import { BehaviorTreeCompiler } from './compiler/BehaviorTreeCompiler';
|
|
||||||
import { BehaviorTreeNodeInspectorProvider } from './providers/BehaviorTreeNodeInspectorProvider';
|
|
||||||
import { BehaviorTreeEditorPanel } from './components/panels/BehaviorTreeEditorPanel';
|
|
||||||
|
|
||||||
const logger = createLogger('BehaviorTreeModule');
|
|
||||||
|
|
||||||
@singleton()
|
|
||||||
export class BehaviorTreeModule implements IEditorModule {
|
|
||||||
readonly id = 'behavior-tree';
|
|
||||||
readonly name = 'Behavior Tree Editor';
|
|
||||||
readonly version = '1.0.0';
|
|
||||||
|
|
||||||
async load(context: IModuleContext): Promise<void> {
|
|
||||||
logger.info('[BehaviorTreeModule] Loading behavior tree editor module...');
|
|
||||||
|
|
||||||
this.registerServices(context);
|
|
||||||
this.registerCompilers();
|
|
||||||
this.registerInspectors(context);
|
|
||||||
this.registerCommands(context);
|
|
||||||
this.registerPanels(context);
|
|
||||||
this.subscribeEvents(context);
|
|
||||||
|
|
||||||
logger.info('[BehaviorTreeModule] Behavior tree editor module loaded');
|
|
||||||
}
|
|
||||||
|
|
||||||
private registerServices(context: IModuleContext): void {
|
|
||||||
context.container.register(BehaviorTreeService, { useClass: BehaviorTreeService });
|
|
||||||
logger.info('[BehaviorTreeModule] Services registered');
|
|
||||||
}
|
|
||||||
|
|
||||||
private registerCompilers(): void {
|
|
||||||
const compilerRegistry = Core.services.resolve(CompilerRegistry);
|
|
||||||
if (compilerRegistry) {
|
|
||||||
const compiler = new BehaviorTreeCompiler();
|
|
||||||
compilerRegistry.register(compiler);
|
|
||||||
logger.info('[BehaviorTreeModule] Compiler registered');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private registerInspectors(context: IModuleContext): void {
|
|
||||||
const provider = new BehaviorTreeNodeInspectorProvider();
|
|
||||||
context.inspectorRegistry.register(provider);
|
|
||||||
logger.info('[BehaviorTreeModule] Inspector provider registered');
|
|
||||||
}
|
|
||||||
|
|
||||||
async unload(): Promise<void> {
|
|
||||||
logger.info('[BehaviorTreeModule] Unloading behavior tree editor module...');
|
|
||||||
}
|
|
||||||
|
|
||||||
private registerCommands(context: IModuleContext): void {
|
|
||||||
context.commands.register({
|
|
||||||
id: 'behavior-tree.new',
|
|
||||||
label: 'New Behavior Tree',
|
|
||||||
icon: 'file-plus',
|
|
||||||
execute: async () => {
|
|
||||||
const service = context.container.resolve(BehaviorTreeService);
|
|
||||||
await service.createNew();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
context.commands.register({
|
|
||||||
id: 'behavior-tree.open',
|
|
||||||
label: 'Open Behavior Tree',
|
|
||||||
icon: 'folder-open',
|
|
||||||
execute: async () => {
|
|
||||||
logger.info('Open behavior tree');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
context.commands.register({
|
|
||||||
id: 'behavior-tree.save',
|
|
||||||
label: 'Save Behavior Tree',
|
|
||||||
icon: 'save',
|
|
||||||
keybinding: { key: 'S', ctrl: true },
|
|
||||||
execute: async () => {
|
|
||||||
logger.info('Save behavior tree');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private registerPanels(context: IModuleContext): void {
|
|
||||||
logger.info('[BehaviorTreeModule] Registering panels...');
|
|
||||||
|
|
||||||
context.panels.register({
|
|
||||||
id: 'behavior-tree-editor',
|
|
||||||
title: '行为树编辑器',
|
|
||||||
icon: 'GitBranch',
|
|
||||||
component: BehaviorTreeEditorPanel,
|
|
||||||
position: PanelPosition.Center,
|
|
||||||
defaultSize: 400,
|
|
||||||
closable: true,
|
|
||||||
isDynamic: true
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.info('[BehaviorTreeModule] Panel registered: behavior-tree-editor');
|
|
||||||
}
|
|
||||||
|
|
||||||
private subscribeEvents(_context: IModuleContext): void {
|
|
||||||
// 文件加载由 BehaviorTreeEditorPanel 处理
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,158 +1,29 @@
|
|||||||
import type { Core, ServiceContainer } from '@esengine/ecs-framework';
|
/**
|
||||||
import {
|
* Behavior Tree Plugin Manifest
|
||||||
IEditorPlugin,
|
* 行为树插件清单
|
||||||
EditorPluginCategory,
|
*/
|
||||||
CompilerRegistry,
|
|
||||||
InspectorRegistry,
|
|
||||||
PanelPosition,
|
|
||||||
type FileCreationTemplate,
|
|
||||||
type FileActionHandler,
|
|
||||||
type PanelDescriptor
|
|
||||||
} from '@esengine/editor-core';
|
|
||||||
import { BehaviorTreeService } from './services/BehaviorTreeService';
|
|
||||||
import { FileSystemService } from './services/FileSystemService';
|
|
||||||
import { BehaviorTreeCompiler } from './compiler/BehaviorTreeCompiler';
|
|
||||||
import { BehaviorTreeNodeInspectorProvider } from './providers/BehaviorTreeNodeInspectorProvider';
|
|
||||||
import { BehaviorTreeEditorPanel } from './components/panels/BehaviorTreeEditorPanel';
|
|
||||||
import { useBehaviorTreeDataStore } from './stores';
|
|
||||||
import { createElement } from 'react';
|
|
||||||
import { GitBranch } from 'lucide-react';
|
|
||||||
import { createRootNode } from './domain/constants/RootNode';
|
|
||||||
import type { IService, ServiceType } from '@esengine/ecs-framework';
|
|
||||||
import { createLogger } from '@esengine/ecs-framework';
|
|
||||||
|
|
||||||
const logger = createLogger('BehaviorTreePlugin');
|
import type { ModuleManifest } from '@esengine/editor-runtime';
|
||||||
|
|
||||||
export class BehaviorTreePlugin implements IEditorPlugin {
|
/**
|
||||||
readonly name = '@esengine/behavior-tree-editor';
|
* 插件清单
|
||||||
readonly version = '1.0.0';
|
*/
|
||||||
readonly displayName = 'Behavior Tree Editor';
|
export const manifest: ModuleManifest = {
|
||||||
readonly category = EditorPluginCategory.Tool;
|
id: '@esengine/behavior-tree',
|
||||||
readonly description = 'Visual behavior tree editor for game AI development';
|
name: '@esengine/behavior-tree',
|
||||||
readonly icon = 'GitBranch';
|
displayName: 'Behavior Tree System',
|
||||||
|
version: '1.0.0',
|
||||||
private services?: ServiceContainer;
|
description: 'AI 行为树系统,支持可视化编辑和运行时执行',
|
||||||
private registeredServices: Set<ServiceType<IService>> = new Set();
|
category: 'AI',
|
||||||
private fileActionHandler?: FileActionHandler;
|
icon: 'GitBranch',
|
||||||
private fileCreationTemplate?: FileCreationTemplate;
|
isCore: false,
|
||||||
|
defaultEnabled: true,
|
||||||
async install(core: Core, services: ServiceContainer): Promise<void> {
|
isEngineModule: false,
|
||||||
this.services = services;
|
canContainContent: false,
|
||||||
this.registerServices(services);
|
dependencies: ['engine-core'],
|
||||||
this.registerCompilers(services);
|
exports: {
|
||||||
this.registerInspectors(services);
|
components: ['BehaviorTreeRuntimeComponent'],
|
||||||
this.registerFileActions(services);
|
systems: ['BehaviorTreeExecutionSystem'],
|
||||||
}
|
loaders: ['BehaviorTreeLoader']
|
||||||
|
|
||||||
async uninstall(): Promise<void> {
|
|
||||||
if (this.services) {
|
|
||||||
for (const serviceType of this.registeredServices) {
|
|
||||||
this.services.unregister(serviceType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.registeredServices.clear();
|
|
||||||
useBehaviorTreeDataStore.getState().reset();
|
|
||||||
this.services = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
registerPanels(): PanelDescriptor[] {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
id: 'behavior-tree-editor',
|
|
||||||
title: 'Behavior Tree Editor',
|
|
||||||
position: PanelPosition.Center,
|
|
||||||
closable: true,
|
|
||||||
component: BehaviorTreeEditorPanel,
|
|
||||||
order: 100,
|
|
||||||
isDynamic: true // 标记为动态面板
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private registerServices(services: ServiceContainer): void {
|
|
||||||
// 先注册 FileSystemService(BehaviorTreeService 依赖它)
|
|
||||||
if (services.isRegistered(FileSystemService)) {
|
|
||||||
services.unregister(FileSystemService);
|
|
||||||
}
|
|
||||||
services.registerSingleton(FileSystemService);
|
|
||||||
this.registeredServices.add(FileSystemService);
|
|
||||||
|
|
||||||
// 再注册 BehaviorTreeService
|
|
||||||
if (services.isRegistered(BehaviorTreeService)) {
|
|
||||||
services.unregister(BehaviorTreeService);
|
|
||||||
}
|
|
||||||
services.registerSingleton(BehaviorTreeService);
|
|
||||||
this.registeredServices.add(BehaviorTreeService);
|
|
||||||
}
|
|
||||||
|
|
||||||
private registerCompilers(services: ServiceContainer): void {
|
|
||||||
try {
|
|
||||||
const compilerRegistry = services.resolve(CompilerRegistry);
|
|
||||||
const compiler = new BehaviorTreeCompiler();
|
|
||||||
compilerRegistry.register(compiler);
|
|
||||||
logger.info('Successfully registered BehaviorTreeCompiler');
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Failed to register compiler:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private registerInspectors(services: ServiceContainer): void {
|
|
||||||
const inspectorRegistry = services.resolve(InspectorRegistry);
|
|
||||||
if (inspectorRegistry) {
|
|
||||||
const provider = new BehaviorTreeNodeInspectorProvider();
|
|
||||||
inspectorRegistry.register(provider);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private registerFileActions(services: ServiceContainer): void {
|
|
||||||
this.fileCreationTemplate = {
|
|
||||||
label: 'Behavior Tree',
|
|
||||||
extension: 'btree',
|
|
||||||
defaultFileName: 'NewBehaviorTree',
|
|
||||||
icon: createElement(GitBranch, { size: 16 }),
|
|
||||||
createContent: (fileName: string) => {
|
|
||||||
// 创建根节点
|
|
||||||
const rootNode = createRootNode();
|
|
||||||
const rootNodeData = {
|
|
||||||
id: rootNode.id,
|
|
||||||
type: rootNode.template.type,
|
|
||||||
displayName: rootNode.template.displayName,
|
|
||||||
data: rootNode.data,
|
|
||||||
position: {
|
|
||||||
x: rootNode.position.x,
|
|
||||||
y: rootNode.position.y
|
|
||||||
},
|
|
||||||
children: []
|
|
||||||
};
|
|
||||||
|
|
||||||
const emptyTree = {
|
|
||||||
name: fileName.replace('.btree', ''),
|
|
||||||
nodes: [rootNodeData],
|
|
||||||
connections: [],
|
|
||||||
variables: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
return JSON.stringify(emptyTree, null, 2);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.fileActionHandler = {
|
|
||||||
extensions: ['btree'],
|
|
||||||
onDoubleClick: async (filePath: string) => {
|
|
||||||
const service = services.resolve(BehaviorTreeService);
|
|
||||||
if (service) {
|
|
||||||
await service.loadFromFile(filePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
registerFileActionHandlers(): FileActionHandler[] {
|
|
||||||
return this.fileActionHandler ? [this.fileActionHandler] : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
registerFileCreationTemplates(): FileCreationTemplate[] {
|
|
||||||
return this.fileCreationTemplate ? [this.fileCreationTemplate] : [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
26
packages/behavior-tree-editor/src/PluginContext.ts
Normal file
26
packages/behavior-tree-editor/src/PluginContext.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
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,5 +1,5 @@
|
|||||||
import { Connection } from '../../../domain/models/Connection';
|
import { Connection } from '../../../domain/models/Connection';
|
||||||
import { BaseCommand } from '@esengine/editor-core';
|
import { BaseCommand } from '@esengine/editor-runtime';
|
||||||
import { ITreeState } from '../ITreeState';
|
import { ITreeState } from '../ITreeState';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Node } from '../../../domain/models/Node';
|
import { Node } from '../../../domain/models/Node';
|
||||||
import { BaseCommand } from '@esengine/editor-core';
|
import { BaseCommand } from '@esengine/editor-runtime';
|
||||||
import { ITreeState } from '../ITreeState';
|
import { ITreeState } from '../ITreeState';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Node } from '../../../domain/models/Node';
|
import { Node } from '../../../domain/models/Node';
|
||||||
import { BaseCommand } from '@esengine/editor-core';
|
import { BaseCommand } from '@esengine/editor-runtime';
|
||||||
import { ITreeState } from '../ITreeState';
|
import { ITreeState } from '../ITreeState';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Position } from '../../../domain/value-objects/Position';
|
import { Position } from '../../../domain/value-objects/Position';
|
||||||
import { BaseCommand, ICommand } from '@esengine/editor-core';
|
import { BaseCommand, ICommand } from '@esengine/editor-runtime';
|
||||||
import { ITreeState } from '../ITreeState';
|
import { ITreeState } from '../ITreeState';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Connection } from '../../../domain/models/Connection';
|
import { Connection } from '../../../domain/models/Connection';
|
||||||
import { BaseCommand } from '@esengine/editor-core';
|
import { BaseCommand } from '@esengine/editor-runtime';
|
||||||
import { ITreeState } from '../ITreeState';
|
import { ITreeState } from '../ITreeState';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { BaseCommand } from '@esengine/editor-core';
|
import { BaseCommand } from '@esengine/editor-runtime';
|
||||||
import { ITreeState } from '../ITreeState';
|
import { ITreeState } from '../ITreeState';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { GlobalBlackboardConfig, BlackboardValueType, BlackboardVariable } from '@esengine/behavior-tree';
|
import { type GlobalBlackboardConfig, BlackboardValueType, type BlackboardVariable } from '@esengine/behavior-tree';
|
||||||
import { createLogger } from '@esengine/ecs-framework';
|
import { createLogger } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
const logger = createLogger('GlobalBlackboardService');
|
const logger = createLogger('GlobalBlackboardService');
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { create } from 'zustand';
|
import { createStore } from '@esengine/editor-runtime';
|
||||||
import { NodeTemplates, NodeTemplate } from '@esengine/behavior-tree';
|
|
||||||
|
const create = createStore;
|
||||||
|
import { NodeTemplates, type NodeTemplate } from '@esengine/behavior-tree';
|
||||||
import { BehaviorTree } from '../../domain/models/BehaviorTree';
|
import { BehaviorTree } from '../../domain/models/BehaviorTree';
|
||||||
import { Node } from '../../domain/models/Node';
|
import { Node } from '../../domain/models/Node';
|
||||||
import { Connection, ConnectionType } from '../../domain/models/Connection';
|
import { Connection, ConnectionType } from '../../domain/models/Connection';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Connection, ConnectionType } from '../../domain/models/Connection';
|
import { Connection, ConnectionType } from '../../domain/models/Connection';
|
||||||
import { CommandManager } from '@esengine/editor-core';
|
import { CommandManager } from '@esengine/editor-runtime';
|
||||||
import { AddConnectionCommand } from '../commands/tree/AddConnectionCommand';
|
import { AddConnectionCommand } from '../commands/tree/AddConnectionCommand';
|
||||||
import { ITreeState } from '../commands/ITreeState';
|
import { ITreeState } from '../commands/ITreeState';
|
||||||
import { IValidator } from '../../domain/interfaces/IValidator';
|
import { IValidator } from '../../domain/interfaces/IValidator';
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { NodeTemplate } from '@esengine/behavior-tree';
|
import type { NodeTemplate } from '@esengine/behavior-tree';
|
||||||
import { Node } from '../../domain/models/Node';
|
import { Node } from '../../domain/models/Node';
|
||||||
import { Position } from '../../domain/value-objects/Position';
|
import { Position } from '../../domain/value-objects/Position';
|
||||||
import { INodeFactory } from '../../domain/interfaces/INodeFactory';
|
import { INodeFactory } from '../../domain/interfaces/INodeFactory';
|
||||||
import { CommandManager } from '@esengine/editor-core';
|
import { CommandManager } from '@esengine/editor-runtime';
|
||||||
import { CreateNodeCommand } from '../commands/tree/CreateNodeCommand';
|
import { CreateNodeCommand } from '../commands/tree/CreateNodeCommand';
|
||||||
import { ITreeState } from '../commands/ITreeState';
|
import { ITreeState } from '../commands/ITreeState';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CommandManager, ICommand } from '@esengine/editor-core';
|
import { CommandManager, ICommand } from '@esengine/editor-runtime';
|
||||||
import { DeleteNodeCommand } from '../commands/tree/DeleteNodeCommand';
|
import { DeleteNodeCommand } from '../commands/tree/DeleteNodeCommand';
|
||||||
import { RemoveConnectionCommand } from '../commands/tree/RemoveConnectionCommand';
|
import { RemoveConnectionCommand } from '../commands/tree/RemoveConnectionCommand';
|
||||||
import { ITreeState } from '../commands/ITreeState';
|
import { ITreeState } from '../commands/ITreeState';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Position } from '../../domain/value-objects/Position';
|
import { Position } from '../../domain/value-objects/Position';
|
||||||
import { CommandManager } from '@esengine/editor-core';
|
import { CommandManager } from '@esengine/editor-runtime';
|
||||||
import { MoveNodeCommand } from '../commands/tree/MoveNodeCommand';
|
import { MoveNodeCommand } from '../commands/tree/MoveNodeCommand';
|
||||||
import { ITreeState } from '../commands/ITreeState';
|
import { ITreeState } from '../commands/ITreeState';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CommandManager } from '@esengine/editor-core';
|
import { CommandManager } from '@esengine/editor-runtime';
|
||||||
import { RemoveConnectionCommand } from '../commands/tree/RemoveConnectionCommand';
|
import { RemoveConnectionCommand } from '../commands/tree/RemoveConnectionCommand';
|
||||||
import { ITreeState } from '../commands/ITreeState';
|
import { ITreeState } from '../commands/ITreeState';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CommandManager } from '@esengine/editor-core';
|
import { CommandManager } from '@esengine/editor-runtime';
|
||||||
import { UpdateNodeDataCommand } from '../commands/tree/UpdateNodeDataCommand';
|
import { UpdateNodeDataCommand } from '../commands/tree/UpdateNodeDataCommand';
|
||||||
import { ITreeState } from '../commands/ITreeState';
|
import { ITreeState } from '../commands/ITreeState';
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import {
|
||||||
import { ICompiler, CompileResult, CompilerContext, IFileSystem } from '@esengine/editor-core';
|
React,
|
||||||
import { File, FolderTree, FolderOpen } from 'lucide-react';
|
useState,
|
||||||
|
useEffect,
|
||||||
|
type ICompiler,
|
||||||
|
type CompileResult,
|
||||||
|
type CompilerContext,
|
||||||
|
type IFileSystem,
|
||||||
|
Icons,
|
||||||
|
createLogger,
|
||||||
|
} from '@esengine/editor-runtime';
|
||||||
import { GlobalBlackboardTypeGenerator } from '../generators/GlobalBlackboardTypeGenerator';
|
import { GlobalBlackboardTypeGenerator } from '../generators/GlobalBlackboardTypeGenerator';
|
||||||
import { EditorFormatConverter, BehaviorTreeAssetSerializer } from '@esengine/behavior-tree';
|
import { EditorFormatConverter, BehaviorTreeAssetSerializer } from '@esengine/behavior-tree';
|
||||||
import { useBehaviorTreeDataStore } from '../application/state/BehaviorTreeDataStore';
|
import { useBehaviorTreeDataStore } from '../application/state/BehaviorTreeDataStore';
|
||||||
import { createLogger } from '@esengine/ecs-framework';
|
|
||||||
|
const { File, FolderTree, FolderOpen } = Icons;
|
||||||
|
|
||||||
const logger = createLogger('BehaviorTreeCompiler');
|
const logger = createLogger('BehaviorTreeCompiler');
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react';
|
import { React, useEffect, useMemo, useRef, useState, useCallback } from '@esengine/editor-runtime';
|
||||||
import { NodeTemplate, BlackboardValueType } from '@esengine/behavior-tree';
|
import { BlackboardValueType, type NodeTemplate } from '@esengine/behavior-tree';
|
||||||
import { useBehaviorTreeDataStore, BehaviorTreeNode, ROOT_NODE_ID } from '../stores';
|
import { useBehaviorTreeDataStore, BehaviorTreeNode, ROOT_NODE_ID } from '../stores';
|
||||||
import { useUIStore } from '../stores';
|
import { useUIStore } from '../stores';
|
||||||
import { showToast as notificationShowToast } from '../services/NotificationService';
|
import { showToast as notificationShowToast } from '../services/NotificationService';
|
||||||
@@ -462,8 +462,13 @@ export const BehaviorTreeEditor: React.FC<BehaviorTreeEditorProps> = ({
|
|||||||
handleNodeMouseUp();
|
handleNodeMouseUp();
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPortPosition = (nodeId: string, propertyName?: string, portType: 'input' | 'output' = 'output') =>
|
// 使用 useCallback 包装 getPortPosition,确保在 canvasScale/canvasOffset 变化时更新
|
||||||
getPortPositionUtil(canvasRef, canvasOffset, canvasScale, nodes, nodeId, propertyName, portType, draggingNodeId, dragDelta, selectedNodeIds);
|
// Use useCallback to wrap getPortPosition to ensure updates when canvasScale/canvasOffset changes
|
||||||
|
const getPortPosition = useCallback(
|
||||||
|
(nodeId: string, propertyName?: string, portType: 'input' | 'output' = 'output') =>
|
||||||
|
getPortPositionUtil(canvasRef, canvasOffset, canvasScale, nodes, nodeId, propertyName, portType, draggingNodeId, dragDelta, selectedNodeIds),
|
||||||
|
[canvasOffset, canvasScale, nodes, draggingNodeId, dragDelta, selectedNodeIds]
|
||||||
|
);
|
||||||
|
|
||||||
stopExecutionRef.current = handleStop;
|
stopExecutionRef.current = handleStop;
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user