Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8662449dcf | |||
| 1834bc2068 | |||
| c23c6c21db | |||
| b494283e9c | |||
| 9b334f36e1 | |||
| 7f8d2eb142 | |||
| 9d3eeb1980 | |||
| 0bcb675c3b | |||
| 574b4d08a3 | |||
| d64e463a71 | |||
| 792fd05c85 | |||
| 7814b97ace | |||
| 75be905f14 | |||
| 01293590e8 | |||
| b236b729b4 | |||
| 0170dc6e9c | |||
| 7834328ae0 | |||
| 39fa797299 | |||
| 03229ffb59 | |||
| 844a770335 | |||
| c8dc9869a3 | |||
| 38755c9014 | |||
| 5d5537e4c7 | |||
| 9da9f5f068 |
@@ -25,9 +25,6 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
ci:
|
ci:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
|
||||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
|
||||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@@ -70,7 +67,17 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --no-frozen-lockfile
|
run: pnpm install --no-frozen-lockfile
|
||||||
|
|
||||||
# 构建所有包 (使用 Turborepo Remote Cache)
|
# 缓存 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
|
- name: Build all packages
|
||||||
run: pnpm run build
|
run: pnpm run build
|
||||||
|
|
||||||
|
|||||||
@@ -156,25 +156,18 @@ jobs:
|
|||||||
if: steps.check-signpath.outputs.enabled == 'true'
|
if: steps.check-signpath.outputs.enabled == 'true'
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Get artifact ID
|
- name: Download Windows artifact
|
||||||
|
if: steps.check-signpath.outputs.enabled == 'true'
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: windows-unsigned
|
||||||
|
path: ./artifacts
|
||||||
|
|
||||||
|
- name: List artifacts for signing
|
||||||
if: steps.check-signpath.outputs.enabled == 'true'
|
if: steps.check-signpath.outputs.enabled == 'true'
|
||||||
id: get-artifact
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: |
|
run: |
|
||||||
# 获取 windows-unsigned artifact 的 ID
|
echo "Files to be signed:"
|
||||||
ARTIFACT_ID=$(gh api \
|
find ./artifacts -type f \( -name "*.exe" -o -name "*.msi" \) | head -20
|
||||||
-H "Accept: application/vnd.github+json" \
|
|
||||||
"/repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts" \
|
|
||||||
--jq '.artifacts[] | select(.name == "windows-unsigned") | .id')
|
|
||||||
|
|
||||||
if [ -z "$ARTIFACT_ID" ]; then
|
|
||||||
echo "Error: Could not find artifact 'windows-unsigned'"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "artifact-id=$ARTIFACT_ID" >> $GITHUB_OUTPUT
|
|
||||||
echo "Found artifact ID: $ARTIFACT_ID"
|
|
||||||
|
|
||||||
- name: Submit to SignPath for code signing
|
- name: Submit to SignPath for code signing
|
||||||
if: steps.check-signpath.outputs.enabled == 'true'
|
if: steps.check-signpath.outputs.enabled == 'true'
|
||||||
@@ -185,8 +178,8 @@ jobs:
|
|||||||
organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }}
|
organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }}
|
||||||
project-slug: 'ecs-framework'
|
project-slug: 'ecs-framework'
|
||||||
signing-policy-slug: 'test-signing'
|
signing-policy-slug: 'test-signing'
|
||||||
artifact-configuration-slug: 'initial'
|
artifact-configuration-slug: 'default'
|
||||||
github-artifact-id: ${{ steps.get-artifact.outputs.artifact-id }}
|
github-artifact-name: 'windows-unsigned'
|
||||||
wait-for-completion: true
|
wait-for-completion: true
|
||||||
wait-for-completion-timeout-in-seconds: 600
|
wait-for-completion-timeout-in-seconds: 600
|
||||||
output-artifact-directory: './signed'
|
output-artifact-directory: './signed'
|
||||||
@@ -197,8 +190,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
files: ./signed/*
|
files: ./signed/*
|
||||||
tag_name: ${{ github.event_name == 'workflow_dispatch' && format('editor-v{0}', github.event.inputs.version) || github.ref_name }}
|
tag_name: ${{ github.event_name == 'workflow_dispatch' && format('editor-v{0}', github.event.inputs.version) || github.ref_name }}
|
||||||
# 保持 Draft 状态,需要手动发布 | Keep as draft, require manual publish
|
draft: false
|
||||||
draft: true
|
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
|||||||
@@ -90,7 +90,3 @@ docs/.vitepress/dist/
|
|||||||
# Tauri 捆绑输出
|
# Tauri 捆绑输出
|
||||||
**/src-tauri/target/release/bundle/
|
**/src-tauri/target/release/bundle/
|
||||||
**/src-tauri/target/debug/bundle/
|
**/src-tauri/target/debug/bundle/
|
||||||
|
|
||||||
# Rust 构建产物
|
|
||||||
**/engine-shared/target/
|
|
||||||
external/
|
|
||||||
|
|||||||
@@ -29,8 +29,6 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
> **Just need ECS?** The core ECS framework [`@esengine/ecs-framework`](./packages/core/) can be used standalone with Cocos Creator, Laya, or any JS engine. [View ECS Documentation](./packages/core/README.md)
|
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
ESEngine is a cross-platform 2D game engine built from the ground up with modern web technologies. It provides a comprehensive toolset that enables developers to focus on creating games rather than building infrastructure.
|
ESEngine is a cross-platform 2D game engine built from the ground up with modern web technologies. It provides a comprehensive toolset that enables developers to focus on creating games rather than building infrastructure.
|
||||||
@@ -130,63 +128,50 @@ requestAnimationFrame(gameLoop);
|
|||||||
|
|
||||||
## Packages
|
## Packages
|
||||||
|
|
||||||
ESEngine is organized as a monorepo with 50+ modular packages. Install only what you need.
|
ESEngine is organized as a monorepo with modular packages.
|
||||||
|
|
||||||
### Essential
|
### Core
|
||||||
|
|
||||||
```bash
|
| Package | Description |
|
||||||
npm install @esengine/ecs-framework # Core ECS (can be used standalone)
|
|---------|-------------|
|
||||||
npm install @esengine/engine-core # Full engine with module system
|
| `@esengine/ecs-framework` | Core ECS framework with entity management, component system, and queries |
|
||||||
```
|
| `@esengine/math` | Vector, matrix, and mathematical utilities |
|
||||||
|
| `@esengine/engine` | Rust/WASM 2D renderer |
|
||||||
|
| `@esengine/engine-core` | Engine module system and lifecycle management |
|
||||||
|
|
||||||
### Popular Modules
|
### Runtime
|
||||||
|
|
||||||
| Category | Packages |
|
| Package | Description |
|
||||||
|----------|----------|
|
|---------|-------------|
|
||||||
| **Rendering** | `sprite`, `tilemap`, `particle`, `mesh-3d`, `fairygui` |
|
| `@esengine/sprite` | 2D sprite rendering and animation |
|
||||||
| **Physics** | `physics-rapier2d` |
|
| `@esengine/tilemap` | Tile-based map rendering |
|
||||||
| **AI & Logic** | `behavior-tree`, `blueprint` |
|
| `@esengine/physics-rapier2d` | 2D physics simulation (Rapier) |
|
||||||
| **Network** | `network`, `network-server` |
|
| `@esengine/behavior-tree` | Behavior tree AI system |
|
||||||
| **Platform** | `platform-web`, `platform-wechat` |
|
| `@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 |
|
||||||
|
|
||||||
<details>
|
### Editor Extensions
|
||||||
<summary><b>View all 50+ packages</b></summary>
|
|
||||||
|
|
||||||
#### Core
|
| Package | Description |
|
||||||
- `@esengine/ecs-framework` - ECS framework core
|
|---------|-------------|
|
||||||
- `@esengine/math` - Vector, matrix utilities
|
| `@esengine/sprite-editor` | Sprite inspector and tools |
|
||||||
- `@esengine/engine` - Rust/WASM renderer
|
| `@esengine/tilemap-editor` | Visual tilemap editor |
|
||||||
- `@esengine/engine-core` - Module lifecycle
|
| `@esengine/physics-rapier2d-editor` | Physics collider visualization |
|
||||||
|
| `@esengine/behavior-tree-editor` | Visual behavior tree editor |
|
||||||
|
| `@esengine/blueprint-editor` | Visual scripting editor |
|
||||||
|
| `@esengine/material-editor` | Material editor |
|
||||||
|
|
||||||
#### Runtime
|
### Platform
|
||||||
- `@esengine/sprite` - 2D sprites & animation
|
|
||||||
- `@esengine/tilemap` - Tile-based maps
|
|
||||||
- `@esengine/particle` - Particle effects
|
|
||||||
- `@esengine/physics-rapier2d` - 2D physics
|
|
||||||
- `@esengine/behavior-tree` - AI behavior trees
|
|
||||||
- `@esengine/blueprint` - Visual scripting
|
|
||||||
- `@esengine/camera` - Camera system
|
|
||||||
- `@esengine/audio` - Audio playback
|
|
||||||
- `@esengine/fairygui` - FairyGUI integration
|
|
||||||
- `@esengine/mesh-3d` - 3D mesh (FBX/GLTF/OBJ)
|
|
||||||
- `@esengine/material-system` - Materials & shaders
|
|
||||||
- `@esengine/asset-system` - Asset management
|
|
||||||
- `@esengine/world-streaming` - Large world streaming
|
|
||||||
|
|
||||||
#### Network
|
| Package | Description |
|
||||||
- `@esengine/network` - Client (TSRPC)
|
|---------|-------------|
|
||||||
- `@esengine/network-server` - Server runtime
|
| `@esengine/platform-common` | Platform abstraction interfaces |
|
||||||
- `@esengine/network-protocols` - Shared protocols
|
| `@esengine/platform-web` | Web browser runtime |
|
||||||
|
| `@esengine/platform-wechat` | WeChat Mini Game runtime |
|
||||||
#### Editor Extensions
|
|
||||||
All runtime modules have corresponding `-editor` packages for visual editing.
|
|
||||||
|
|
||||||
#### Platform
|
|
||||||
- `@esengine/platform-common` - Platform abstraction
|
|
||||||
- `@esengine/platform-web` - Web runtime
|
|
||||||
- `@esengine/platform-wechat` - WeChat Mini Game
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## Editor
|
## Editor
|
||||||
|
|
||||||
@@ -253,24 +238,13 @@ pnpm tauri:dev
|
|||||||
|
|
||||||
```
|
```
|
||||||
esengine/
|
esengine/
|
||||||
├── packages/
|
├── packages/ # Engine packages (runtime, editor, platform)
|
||||||
│ ├── core/ # ECS Framework (@esengine/ecs-framework)
|
├── docs/ # Documentation source
|
||||||
│ ├── math/ # Math library (@esengine/math)
|
├── examples/ # Example projects
|
||||||
│ ├── engine-core/ # Engine lifecycle management
|
├── scripts/ # Build utilities
|
||||||
│ ├── sprite/ # 2D sprite rendering
|
└── thirdparty/ # Third-party dependencies
|
||||||
│ ├── tilemap/ # Tilemap system
|
|
||||||
│ ├── physics-rapier2d/ # Physics engine
|
|
||||||
│ ├── behavior-tree/ # AI behavior trees
|
|
||||||
│ ├── editor-app/ # Desktop editor (Tauri)
|
|
||||||
│ └── ... # Other modules
|
|
||||||
├── docs/ # Documentation source
|
|
||||||
├── examples/ # Example projects
|
|
||||||
├── scripts/ # Build utilities
|
|
||||||
└── thirdparty/ # Third-party dependencies
|
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Looking for ECS source code?** The ECS framework is in `packages/core/`
|
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
- [Getting Started](https://esengine.cn/guide/getting-started.html)
|
- [Getting Started](https://esengine.cn/guide/getting-started.html)
|
||||||
@@ -279,7 +253,6 @@ esengine/
|
|||||||
|
|
||||||
## Community
|
## Community
|
||||||
|
|
||||||
- [Discord](https://discord.gg/gCAgzXFW) - Chat with the community
|
|
||||||
- [GitHub Issues](https://github.com/esengine/esengine/issues) - Bug reports and feature requests
|
- [GitHub Issues](https://github.com/esengine/esengine/issues) - Bug reports and feature requests
|
||||||
- [GitHub Discussions](https://github.com/esengine/esengine/discussions) - Questions and ideas
|
- [GitHub Discussions](https://github.com/esengine/esengine/discussions) - Questions and ideas
|
||||||
|
|
||||||
|
|||||||
+41
-68
@@ -29,8 +29,6 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
> **只需要 ECS?** 核心 ECS 框架 [`@esengine/ecs-framework`](./packages/core/) 可独立使用,支持 Cocos Creator、Laya 或任何 JS 引擎。[查看 ECS 文档](./packages/core/README_CN.md)
|
|
||||||
|
|
||||||
## 概述
|
## 概述
|
||||||
|
|
||||||
ESEngine 是一款基于现代 Web 技术从零构建的跨平台 2D 游戏引擎。它提供完整的工具集,让开发者专注于游戏创作而非基础设施搭建。
|
ESEngine 是一款基于现代 Web 技术从零构建的跨平台 2D 游戏引擎。它提供完整的工具集,让开发者专注于游戏创作而非基础设施搭建。
|
||||||
@@ -130,63 +128,50 @@ requestAnimationFrame(gameLoop);
|
|||||||
|
|
||||||
## 模块
|
## 模块
|
||||||
|
|
||||||
ESEngine 采用 Monorepo 组织,包含 50+ 个模块化包。按需引入即可。
|
ESEngine 采用 Monorepo 组织,包含多个模块化包。
|
||||||
|
|
||||||
### 核心安装
|
### 核心
|
||||||
|
|
||||||
```bash
|
| 包名 | 描述 |
|
||||||
npm install @esengine/ecs-framework # ECS 核心(可独立使用)
|
|
||||||
npm install @esengine/engine-core # 完整引擎模块系统
|
|
||||||
```
|
|
||||||
|
|
||||||
### 常用模块
|
|
||||||
|
|
||||||
| 分类 | 包名 |
|
|
||||||
|------|------|
|
|------|------|
|
||||||
| **渲染** | `sprite`, `tilemap`, `particle`, `mesh-3d`, `fairygui` |
|
| `@esengine/ecs-framework` | ECS 框架核心,包含实体管理、组件系统和查询 |
|
||||||
| **物理** | `physics-rapier2d` |
|
| `@esengine/math` | 向量、矩阵和数学工具 |
|
||||||
| **AI 逻辑** | `behavior-tree`, `blueprint` |
|
| `@esengine/engine` | Rust/WASM 2D 渲染器 |
|
||||||
| **网络** | `network`, `network-server` |
|
| `@esengine/engine-core` | 引擎模块系统和生命周期管理 |
|
||||||
| **平台** | `platform-web`, `platform-wechat` |
|
|
||||||
|
|
||||||
<details>
|
### 运行时
|
||||||
<summary><b>查看全部 50+ 个包</b></summary>
|
|
||||||
|
|
||||||
#### 核心
|
| 包名 | 描述 |
|
||||||
- `@esengine/ecs-framework` - ECS 框架核心
|
|------|------|
|
||||||
- `@esengine/math` - 向量、矩阵工具
|
| `@esengine/sprite` | 2D 精灵渲染和动画 |
|
||||||
- `@esengine/engine` - Rust/WASM 渲染器
|
| `@esengine/tilemap` | Tilemap 渲染 |
|
||||||
- `@esengine/engine-core` - 模块生命周期
|
| `@esengine/physics-rapier2d` | 2D 物理模拟 (Rapier) |
|
||||||
|
| `@esengine/behavior-tree` | 行为树 AI 系统 |
|
||||||
|
| `@esengine/blueprint` | 可视化脚本运行时 |
|
||||||
|
| `@esengine/camera` | 相机控制和管理 |
|
||||||
|
| `@esengine/audio` | 音频播放 |
|
||||||
|
| `@esengine/ui` | UI 组件 |
|
||||||
|
| `@esengine/material-system` | 材质和着色器系统 |
|
||||||
|
| `@esengine/asset-system` | 资源加载和管理 |
|
||||||
|
|
||||||
#### 运行时
|
### 编辑器扩展
|
||||||
- `@esengine/sprite` - 2D 精灵和动画
|
|
||||||
- `@esengine/tilemap` - 瓦片地图
|
|
||||||
- `@esengine/particle` - 粒子特效
|
|
||||||
- `@esengine/physics-rapier2d` - 2D 物理
|
|
||||||
- `@esengine/behavior-tree` - AI 行为树
|
|
||||||
- `@esengine/blueprint` - 可视化脚本
|
|
||||||
- `@esengine/camera` - 相机系统
|
|
||||||
- `@esengine/audio` - 音频播放
|
|
||||||
- `@esengine/fairygui` - FairyGUI 集成
|
|
||||||
- `@esengine/mesh-3d` - 3D 模型 (FBX/GLTF/OBJ)
|
|
||||||
- `@esengine/material-system` - 材质和着色器
|
|
||||||
- `@esengine/asset-system` - 资源管理
|
|
||||||
- `@esengine/world-streaming` - 大世界流式加载
|
|
||||||
|
|
||||||
#### 网络
|
| 包名 | 描述 |
|
||||||
- `@esengine/network` - 客户端 (TSRPC)
|
|------|------|
|
||||||
- `@esengine/network-server` - 服务端运行时
|
| `@esengine/sprite-editor` | 精灵检视器和工具 |
|
||||||
- `@esengine/network-protocols` - 共享协议
|
| `@esengine/tilemap-editor` | 可视化 Tilemap 编辑器 |
|
||||||
|
| `@esengine/physics-rapier2d-editor` | 物理碰撞体可视化 |
|
||||||
|
| `@esengine/behavior-tree-editor` | 可视化行为树编辑器 |
|
||||||
|
| `@esengine/blueprint-editor` | 可视化脚本编辑器 |
|
||||||
|
| `@esengine/material-editor` | 材质编辑器 |
|
||||||
|
|
||||||
#### 编辑器扩展
|
### 平台
|
||||||
所有运行时模块都有对应的 `-editor` 包用于可视化编辑。
|
|
||||||
|
|
||||||
#### 平台
|
| 包名 | 描述 |
|
||||||
- `@esengine/platform-common` - 平台抽象层
|
|------|------|
|
||||||
- `@esengine/platform-web` - Web 运行时
|
| `@esengine/platform-common` | 平台抽象接口 |
|
||||||
- `@esengine/platform-wechat` - 微信小游戏
|
| `@esengine/platform-web` | Web 浏览器运行时 |
|
||||||
|
| `@esengine/platform-wechat` | 微信小游戏运行时 |
|
||||||
</details>
|
|
||||||
|
|
||||||
## 编辑器
|
## 编辑器
|
||||||
|
|
||||||
@@ -253,24 +238,13 @@ pnpm tauri:dev
|
|||||||
|
|
||||||
```
|
```
|
||||||
esengine/
|
esengine/
|
||||||
├── packages/
|
├── packages/ # 引擎包(运行时、编辑器、平台)
|
||||||
│ ├── core/ # ECS 框架 (@esengine/ecs-framework)
|
├── docs/ # 文档源码
|
||||||
│ ├── math/ # 数学库 (@esengine/math)
|
├── examples/ # 示例项目
|
||||||
│ ├── engine-core/ # 引擎生命周期管理
|
├── scripts/ # 构建工具
|
||||||
│ ├── sprite/ # 2D 精灵渲染
|
└── thirdparty/ # 第三方依赖
|
||||||
│ ├── tilemap/ # Tilemap 系统
|
|
||||||
│ ├── physics-rapier2d/ # 物理引擎
|
|
||||||
│ ├── behavior-tree/ # AI 行为树
|
|
||||||
│ ├── editor-app/ # 桌面编辑器 (Tauri)
|
|
||||||
│ └── ... # 其他模块
|
|
||||||
├── docs/ # 文档源码
|
|
||||||
├── examples/ # 示例项目
|
|
||||||
├── scripts/ # 构建工具
|
|
||||||
└── thirdparty/ # 第三方依赖
|
|
||||||
```
|
```
|
||||||
|
|
||||||
> **寻找 ECS 源码?** ECS 框架位于 `packages/core/`
|
|
||||||
|
|
||||||
## 文档
|
## 文档
|
||||||
|
|
||||||
- [快速入门](https://esengine.cn/guide/getting-started.html)
|
- [快速入门](https://esengine.cn/guide/getting-started.html)
|
||||||
@@ -279,10 +253,9 @@ esengine/
|
|||||||
|
|
||||||
## 社区
|
## 社区
|
||||||
|
|
||||||
- [Discord](https://discord.gg/gCAgzXFW) - 国际社区
|
|
||||||
- [QQ 交流群](https://jq.qq.com/?_wv=1027&k=29w1Nud6) - 中文社区
|
|
||||||
- [GitHub Issues](https://github.com/esengine/esengine/issues) - Bug 反馈和功能建议
|
- [GitHub Issues](https://github.com/esengine/esengine/issues) - Bug 反馈和功能建议
|
||||||
- [GitHub Discussions](https://github.com/esengine/esengine/discussions) - 问题和想法
|
- [GitHub Discussions](https://github.com/esengine/esengine/discussions) - 问题和想法
|
||||||
|
- [QQ 交流群](https://jq.qq.com/?_wv=1027&k=29w1Nud6) - 中文社区
|
||||||
|
|
||||||
## 贡献
|
## 贡献
|
||||||
|
|
||||||
|
|||||||
@@ -1,663 +0,0 @@
|
|||||||
# ESEngine 材质系统统一架构重构方案
|
|
||||||
|
|
||||||
## 问题概述
|
|
||||||
|
|
||||||
当前 UI 和 Scene (Sprite) 两套渲染系统存在大量代码重复:
|
|
||||||
|
|
||||||
| 重复项 | Sprite | UI | 重复度 |
|
|
||||||
|--------|--------|----|----|
|
|
||||||
| 材质属性覆盖接口 | `MaterialPropertyOverride` | `UIMaterialPropertyOverride` | 100% |
|
|
||||||
| 材质方法 (12个) | `SpriteComponent` | `UIRenderComponent` | 100% |
|
|
||||||
| ShinyEffect 组件 | `ShinyEffectComponent` | `UIShinyEffectComponent` | 99% |
|
|
||||||
| ShinyEffect 系统 | `ShinyEffectSystem` | `UIShinyEffectSystem` | 98% |
|
|
||||||
|
|
||||||
**根本原因**:缺乏统一的材质覆盖接口抽象层。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 一、统一材质覆盖接口
|
|
||||||
|
|
||||||
### 1.1 定义通用接口
|
|
||||||
|
|
||||||
在 `@esengine/material-system` 包中定义统一接口:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// packages/material-system/src/interfaces/IMaterialOverridable.ts
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Material property override definition.
|
|
||||||
* 材质属性覆盖定义。
|
|
||||||
*/
|
|
||||||
export interface MaterialPropertyOverride {
|
|
||||||
type: 'float' | 'vec2' | 'vec3' | 'vec4' | 'color' | 'int';
|
|
||||||
value: number | number[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export type MaterialOverrides = Record<string, MaterialPropertyOverride>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface for components that support material property overrides.
|
|
||||||
* 支持材质属性覆盖的组件接口。
|
|
||||||
*/
|
|
||||||
export interface IMaterialOverridable {
|
|
||||||
/** Material GUID for asset reference | 材质资产引用的 GUID */
|
|
||||||
materialGuid: string;
|
|
||||||
|
|
||||||
/** Current material overrides | 当前材质覆盖 */
|
|
||||||
readonly materialOverrides: MaterialOverrides;
|
|
||||||
|
|
||||||
/** Get current material ID | 获取当前材质 ID */
|
|
||||||
getMaterialId(): number;
|
|
||||||
|
|
||||||
/** Set material ID | 设置材质 ID */
|
|
||||||
setMaterialId(id: number): void;
|
|
||||||
|
|
||||||
// Uniform setters
|
|
||||||
setOverrideFloat(name: string, value: number): this;
|
|
||||||
setOverrideVec2(name: string, x: number, y: number): this;
|
|
||||||
setOverrideVec3(name: string, x: number, y: number, z: number): this;
|
|
||||||
setOverrideVec4(name: string, x: number, y: number, z: number, w: number): this;
|
|
||||||
setOverrideColor(name: string, r: number, g: number, b: number, a?: number): this;
|
|
||||||
setOverrideInt(name: string, value: number): this;
|
|
||||||
|
|
||||||
// Uniform getters
|
|
||||||
getOverride(name: string): MaterialPropertyOverride | undefined;
|
|
||||||
removeOverride(name: string): this;
|
|
||||||
clearOverrides(): this;
|
|
||||||
hasOverrides(): boolean;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 1.2 创建 Mixin 实现
|
|
||||||
|
|
||||||
使用 Mixin 模式避免代码重复:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// packages/material-system/src/mixins/MaterialOverridableMixin.ts
|
|
||||||
|
|
||||||
import type { MaterialPropertyOverride, MaterialOverrides } from '../interfaces/IMaterialOverridable';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mixin that provides material override functionality.
|
|
||||||
* 提供材质覆盖功能的 Mixin。
|
|
||||||
*/
|
|
||||||
export function MaterialOverridableMixin<TBase extends new (...args: any[]) => {}>(Base: TBase) {
|
|
||||||
return class extends Base {
|
|
||||||
materialGuid: string = '';
|
|
||||||
private _materialId: number = 0;
|
|
||||||
private _materialOverrides: MaterialOverrides = {};
|
|
||||||
|
|
||||||
get materialOverrides(): MaterialOverrides {
|
|
||||||
return this._materialOverrides;
|
|
||||||
}
|
|
||||||
|
|
||||||
getMaterialId(): number {
|
|
||||||
return this._materialId;
|
|
||||||
}
|
|
||||||
|
|
||||||
setMaterialId(id: number): void {
|
|
||||||
this._materialId = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
setOverrideFloat(name: string, value: number): this {
|
|
||||||
this._materialOverrides[name] = { type: 'float', value };
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
setOverrideVec2(name: string, x: number, y: number): this {
|
|
||||||
this._materialOverrides[name] = { type: 'vec2', value: [x, y] };
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
setOverrideVec3(name: string, x: number, y: number, z: number): this {
|
|
||||||
this._materialOverrides[name] = { type: 'vec3', value: [x, y, z] };
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
setOverrideVec4(name: string, x: number, y: number, z: number, w: number): this {
|
|
||||||
this._materialOverrides[name] = { type: 'vec4', value: [x, y, z, w] };
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
setOverrideColor(name: string, r: number, g: number, b: number, a: number = 1.0): this {
|
|
||||||
this._materialOverrides[name] = { type: 'color', value: [r, g, b, a] };
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
setOverrideInt(name: string, value: number): this {
|
|
||||||
this._materialOverrides[name] = { type: 'int', value: Math.floor(value) };
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
getOverride(name: string): MaterialPropertyOverride | undefined {
|
|
||||||
return this._materialOverrides[name];
|
|
||||||
}
|
|
||||||
|
|
||||||
removeOverride(name: string): this {
|
|
||||||
delete this._materialOverrides[name];
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
clearOverrides(): this {
|
|
||||||
this._materialOverrides = {};
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
hasOverrides(): boolean {
|
|
||||||
return Object.keys(this._materialOverrides).length > 0;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 二、Shader Property 元数据系统
|
|
||||||
|
|
||||||
### 2.1 定义属性元数据接口
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// packages/material-system/src/interfaces/IShaderProperty.ts
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shader property UI metadata.
|
|
||||||
* 着色器属性 UI 元数据。
|
|
||||||
*/
|
|
||||||
export interface ShaderPropertyMeta {
|
|
||||||
/** Property type | 属性类型 */
|
|
||||||
type: 'float' | 'vec2' | 'vec3' | 'vec4' | 'color' | 'int' | 'texture';
|
|
||||||
|
|
||||||
/** Display label (supports i18n key) | 显示标签(支持 i18n 键) */
|
|
||||||
label: string;
|
|
||||||
|
|
||||||
/** Property group for organization | 属性分组 */
|
|
||||||
group?: string;
|
|
||||||
|
|
||||||
/** Default value | 默认值 */
|
|
||||||
default?: number | number[] | string;
|
|
||||||
|
|
||||||
// Numeric constraints
|
|
||||||
min?: number;
|
|
||||||
max?: number;
|
|
||||||
step?: number;
|
|
||||||
|
|
||||||
/** UI hints | UI 提示 */
|
|
||||||
hint?: 'range' | 'angle' | 'hdr' | 'normal';
|
|
||||||
|
|
||||||
/** Tooltip description | 工具提示描述 */
|
|
||||||
tooltip?: string;
|
|
||||||
|
|
||||||
/** Whether to hide in inspector | 是否在检查器中隐藏 */
|
|
||||||
hidden?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extended shader definition with property metadata.
|
|
||||||
* 带属性元数据的扩展着色器定义。
|
|
||||||
*/
|
|
||||||
export interface ShaderAssetDefinition {
|
|
||||||
/** Shader name | 着色器名称 */
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
/** Display name for UI | UI 显示名称 */
|
|
||||||
displayName?: string;
|
|
||||||
|
|
||||||
/** Shader description | 着色器描述 */
|
|
||||||
description?: string;
|
|
||||||
|
|
||||||
/** Vertex shader source (inline or path) | 顶点着色器源(内联或路径)*/
|
|
||||||
vertexSource: string;
|
|
||||||
|
|
||||||
/** Fragment shader source (inline or path) | 片段着色器源(内联或路径)*/
|
|
||||||
fragmentSource: string;
|
|
||||||
|
|
||||||
/** Property metadata for inspector | 检查器属性元数据 */
|
|
||||||
properties?: Record<string, ShaderPropertyMeta>;
|
|
||||||
|
|
||||||
/** Render queue / order | 渲染队列/顺序 */
|
|
||||||
renderQueue?: number;
|
|
||||||
|
|
||||||
/** Preset blend mode | 预设混合模式 */
|
|
||||||
blendMode?: 'alpha' | 'additive' | 'multiply' | 'opaque';
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.2 .shader 资产文件格式
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"$schema": "esengine://schemas/shader.json",
|
|
||||||
"version": 1,
|
|
||||||
"name": "Shiny",
|
|
||||||
"displayName": "闪光效果 | Shiny Effect",
|
|
||||||
"description": "扫光高亮动画着色器 | Sweeping highlight animation shader",
|
|
||||||
|
|
||||||
"vertexSource": "./shaders/sprite.vert",
|
|
||||||
"fragmentSource": "./shaders/shiny.frag",
|
|
||||||
|
|
||||||
"blendMode": "alpha",
|
|
||||||
"renderQueue": 2000,
|
|
||||||
|
|
||||||
"properties": {
|
|
||||||
"u_shinyProgress": {
|
|
||||||
"type": "float",
|
|
||||||
"label": "进度 | Progress",
|
|
||||||
"group": "Animation",
|
|
||||||
"default": 0,
|
|
||||||
"min": 0,
|
|
||||||
"max": 1,
|
|
||||||
"step": 0.01,
|
|
||||||
"hidden": true
|
|
||||||
},
|
|
||||||
"u_shinyWidth": {
|
|
||||||
"type": "float",
|
|
||||||
"label": "宽度 | Width",
|
|
||||||
"group": "Effect",
|
|
||||||
"default": 0.25,
|
|
||||||
"min": 0,
|
|
||||||
"max": 1,
|
|
||||||
"step": 0.01,
|
|
||||||
"tooltip": "闪光带宽度 | Width of the shiny band"
|
|
||||||
},
|
|
||||||
"u_shinyRotation": {
|
|
||||||
"type": "float",
|
|
||||||
"label": "角度 | Rotation",
|
|
||||||
"group": "Effect",
|
|
||||||
"default": 2.25,
|
|
||||||
"min": 0,
|
|
||||||
"max": 6.28,
|
|
||||||
"step": 0.01,
|
|
||||||
"hint": "angle"
|
|
||||||
},
|
|
||||||
"u_shinySoftness": {
|
|
||||||
"type": "float",
|
|
||||||
"label": "柔和度 | Softness",
|
|
||||||
"group": "Effect",
|
|
||||||
"default": 1.0,
|
|
||||||
"min": 0,
|
|
||||||
"max": 1,
|
|
||||||
"step": 0.01
|
|
||||||
},
|
|
||||||
"u_shinyBrightness": {
|
|
||||||
"type": "float",
|
|
||||||
"label": "亮度 | Brightness",
|
|
||||||
"group": "Effect",
|
|
||||||
"default": 1.0,
|
|
||||||
"min": 0,
|
|
||||||
"max": 2,
|
|
||||||
"step": 0.01
|
|
||||||
},
|
|
||||||
"u_shinyGloss": {
|
|
||||||
"type": "float",
|
|
||||||
"label": "光泽度 | Gloss",
|
|
||||||
"group": "Effect",
|
|
||||||
"default": 1.0,
|
|
||||||
"min": 0,
|
|
||||||
"max": 1,
|
|
||||||
"step": 0.01,
|
|
||||||
"tooltip": "0=白色高光, 1=带颜色 | 0=white shine, 1=color-tinted"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 三、统一效果组件/系统架构
|
|
||||||
|
|
||||||
### 3.1 抽取通用 ShinyEffect 基类
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// packages/material-system/src/effects/BaseShinyEffect.ts
|
|
||||||
|
|
||||||
import { Component, Property, Serializable, Serialize } from '@esengine/ecs-framework';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base shiny effect configuration (shared between UI and Sprite).
|
|
||||||
* 基础闪光效果配置(UI 和 Sprite 共享)。
|
|
||||||
*/
|
|
||||||
export abstract class BaseShinyEffect extends Component {
|
|
||||||
// ============= Effect Parameters =============
|
|
||||||
@Serialize()
|
|
||||||
@Property({ type: 'number', label: 'Width', min: 0, max: 1, step: 0.01 })
|
|
||||||
public width: number = 0.25;
|
|
||||||
|
|
||||||
@Serialize()
|
|
||||||
@Property({ type: 'number', label: 'Rotation', min: 0, max: 360, step: 1 })
|
|
||||||
public rotation: number = 129;
|
|
||||||
|
|
||||||
@Serialize()
|
|
||||||
@Property({ type: 'number', label: 'Softness', min: 0, max: 1, step: 0.01 })
|
|
||||||
public softness: number = 1.0;
|
|
||||||
|
|
||||||
@Serialize()
|
|
||||||
@Property({ type: 'number', label: 'Brightness', min: 0, max: 2, step: 0.01 })
|
|
||||||
public brightness: number = 1.0;
|
|
||||||
|
|
||||||
@Serialize()
|
|
||||||
@Property({ type: 'number', label: 'Gloss', min: 0, max: 2, step: 0.01 })
|
|
||||||
public gloss: number = 1.0;
|
|
||||||
|
|
||||||
// ============= Animation Settings =============
|
|
||||||
@Serialize()
|
|
||||||
@Property({ type: 'boolean', label: 'Play' })
|
|
||||||
public play: boolean = true;
|
|
||||||
|
|
||||||
@Serialize()
|
|
||||||
@Property({ type: 'boolean', label: 'Loop' })
|
|
||||||
public loop: boolean = true;
|
|
||||||
|
|
||||||
@Serialize()
|
|
||||||
@Property({ type: 'number', label: 'Duration', min: 0.1, step: 0.1 })
|
|
||||||
public duration: number = 2.0;
|
|
||||||
|
|
||||||
@Serialize()
|
|
||||||
@Property({ type: 'number', label: 'Loop Delay', min: 0, step: 0.1 })
|
|
||||||
public loopDelay: number = 2.0;
|
|
||||||
|
|
||||||
@Serialize()
|
|
||||||
@Property({ type: 'number', label: 'Initial Delay', min: 0, step: 0.1 })
|
|
||||||
public initialDelay: number = 0;
|
|
||||||
|
|
||||||
// ============= Runtime State =============
|
|
||||||
public progress: number = 0;
|
|
||||||
public elapsedTime: number = 0;
|
|
||||||
public inDelay: boolean = false;
|
|
||||||
public delayRemaining: number = 0;
|
|
||||||
public initialDelayProcessed: boolean = false;
|
|
||||||
|
|
||||||
reset(): void {
|
|
||||||
this.progress = 0;
|
|
||||||
this.elapsedTime = 0;
|
|
||||||
this.inDelay = false;
|
|
||||||
this.delayRemaining = 0;
|
|
||||||
this.initialDelayProcessed = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
start(): void {
|
|
||||||
this.reset();
|
|
||||||
this.play = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
stop(): void {
|
|
||||||
this.play = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
getRotationRadians(): number {
|
|
||||||
return this.rotation * Math.PI / 180;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.2 通用动画更新逻辑
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// packages/material-system/src/effects/ShinyEffectAnimator.ts
|
|
||||||
|
|
||||||
import type { BaseShinyEffect } from './BaseShinyEffect';
|
|
||||||
import type { IMaterialOverridable } from '../interfaces/IMaterialOverridable';
|
|
||||||
import { BuiltInShaders } from '../types';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shared animator logic for shiny effect.
|
|
||||||
* 闪光效果共享的动画逻辑。
|
|
||||||
*/
|
|
||||||
export class ShinyEffectAnimator {
|
|
||||||
/**
|
|
||||||
* Update animation state.
|
|
||||||
* 更新动画状态。
|
|
||||||
*/
|
|
||||||
static updateAnimation(shiny: BaseShinyEffect, deltaTime: number): void {
|
|
||||||
if (!shiny.initialDelayProcessed && shiny.initialDelay > 0) {
|
|
||||||
shiny.delayRemaining = shiny.initialDelay;
|
|
||||||
shiny.inDelay = true;
|
|
||||||
shiny.initialDelayProcessed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shiny.inDelay) {
|
|
||||||
shiny.delayRemaining -= deltaTime;
|
|
||||||
if (shiny.delayRemaining <= 0) {
|
|
||||||
shiny.inDelay = false;
|
|
||||||
shiny.elapsedTime = 0;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
shiny.elapsedTime += deltaTime;
|
|
||||||
shiny.progress = Math.min(shiny.elapsedTime / shiny.duration, 1.0);
|
|
||||||
|
|
||||||
if (shiny.progress >= 1.0) {
|
|
||||||
if (shiny.loop) {
|
|
||||||
shiny.inDelay = true;
|
|
||||||
shiny.delayRemaining = shiny.loopDelay;
|
|
||||||
shiny.progress = 0;
|
|
||||||
shiny.elapsedTime = 0;
|
|
||||||
} else {
|
|
||||||
shiny.play = false;
|
|
||||||
shiny.progress = 1.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply material overrides.
|
|
||||||
* 应用材质覆盖。
|
|
||||||
*/
|
|
||||||
static applyMaterialOverrides(shiny: BaseShinyEffect, target: IMaterialOverridable): void {
|
|
||||||
if (target.getMaterialId() === 0) {
|
|
||||||
target.setMaterialId(BuiltInShaders.Shiny);
|
|
||||||
}
|
|
||||||
|
|
||||||
target.setOverrideFloat('u_shinyProgress', shiny.progress);
|
|
||||||
target.setOverrideFloat('u_shinyWidth', shiny.width);
|
|
||||||
target.setOverrideFloat('u_shinyRotation', shiny.getRotationRadians());
|
|
||||||
target.setOverrideFloat('u_shinySoftness', shiny.softness);
|
|
||||||
target.setOverrideFloat('u_shinyBrightness', shiny.brightness);
|
|
||||||
target.setOverrideFloat('u_shinyGloss', shiny.gloss);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 四、Material Inspector 设计
|
|
||||||
|
|
||||||
### 4.1 组件架构
|
|
||||||
|
|
||||||
```
|
|
||||||
MaterialPropertiesEditor (容器组件)
|
|
||||||
├── ShaderSelector (着色器选择器)
|
|
||||||
├── PropertyGroup (属性分组)
|
|
||||||
│ ├── FloatProperty (浮点属性)
|
|
||||||
│ ├── VectorProperty (向量属性)
|
|
||||||
│ ├── ColorProperty (颜色属性)
|
|
||||||
│ └── TextureProperty (纹理属性)
|
|
||||||
└── OverrideIndicator (覆盖指示器)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.2 核心组件
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// packages/editor-app/src/components/inspectors/material/MaterialPropertiesEditor.tsx
|
|
||||||
|
|
||||||
interface MaterialPropertiesEditorProps {
|
|
||||||
/** Target component implementing IMaterialOverridable */
|
|
||||||
target: IMaterialOverridable;
|
|
||||||
/** Current shader definition with property metadata */
|
|
||||||
shaderDef?: ShaderAssetDefinition;
|
|
||||||
/** Callback when property changes */
|
|
||||||
onChange?: (name: string, value: MaterialPropertyOverride) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MaterialPropertiesEditor: React.FC<MaterialPropertiesEditorProps> = ({
|
|
||||||
target,
|
|
||||||
shaderDef,
|
|
||||||
onChange
|
|
||||||
}) => {
|
|
||||||
// Group properties by their group field
|
|
||||||
const groupedProps = useMemo(() => {
|
|
||||||
if (!shaderDef?.properties) return {};
|
|
||||||
|
|
||||||
const groups: Record<string, Array<[string, ShaderPropertyMeta]>> = {};
|
|
||||||
for (const [name, meta] of Object.entries(shaderDef.properties)) {
|
|
||||||
if (meta.hidden) continue;
|
|
||||||
const group = meta.group || 'Default';
|
|
||||||
if (!groups[group]) groups[group] = [];
|
|
||||||
groups[group].push([name, meta]);
|
|
||||||
}
|
|
||||||
return groups;
|
|
||||||
}, [shaderDef]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="material-properties-editor">
|
|
||||||
<ShaderSelector
|
|
||||||
currentShaderId={target.getMaterialId()}
|
|
||||||
onSelect={(id) => target.setMaterialId(id)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{Object.entries(groupedProps).map(([group, props]) => (
|
|
||||||
<PropertyGroup key={group} title={group}>
|
|
||||||
{props.map(([name, meta]) => (
|
|
||||||
<PropertyField
|
|
||||||
key={name}
|
|
||||||
name={name}
|
|
||||||
meta={meta}
|
|
||||||
value={target.getOverride(name)?.value ?? meta.default}
|
|
||||||
onChange={(value) => {
|
|
||||||
applyOverride(target, name, meta.type, value);
|
|
||||||
onChange?.(name, target.getOverride(name)!);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</PropertyGroup>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 五、实施计划
|
|
||||||
|
|
||||||
### Phase 1: 接口层 (1-2 天)
|
|
||||||
|
|
||||||
1. **创建 IMaterialOverridable 接口** (`packages/material-system/src/interfaces/`)
|
|
||||||
2. **创建 MaterialOverridableMixin** (`packages/material-system/src/mixins/`)
|
|
||||||
3. **导出新接口** (`packages/material-system/src/index.ts`)
|
|
||||||
|
|
||||||
### Phase 2: 重构现有组件 (2-3 天)
|
|
||||||
|
|
||||||
1. **修改 SpriteComponent**:实现 `IMaterialOverridable`,使用 Mixin
|
|
||||||
2. **修改 UIRenderComponent**:实现 `IMaterialOverridable`,使用 Mixin
|
|
||||||
3. **删除重复代码**:移除各组件中的重复材质方法
|
|
||||||
|
|
||||||
### Phase 3: 统一效果系统 (2-3 天)
|
|
||||||
|
|
||||||
1. **创建 BaseShinyEffect** (`packages/material-system/src/effects/`)
|
|
||||||
2. **创建 ShinyEffectAnimator** (`packages/material-system/src/effects/`)
|
|
||||||
3. **重构 ShinyEffectComponent**:继承 BaseShinyEffect
|
|
||||||
4. **重构 UIShinyEffectComponent**:继承 BaseShinyEffect
|
|
||||||
5. **重构系统**:使用 ShinyEffectAnimator
|
|
||||||
|
|
||||||
### Phase 4: Shader Property 系统 (2-3 天)
|
|
||||||
|
|
||||||
1. **定义 ShaderPropertyMeta 接口**
|
|
||||||
2. **扩展 ShaderDefinition** 添加 properties 字段
|
|
||||||
3. **创建 ShaderLoader** 支持 .shader 文件
|
|
||||||
4. **注册内置着色器属性元数据**
|
|
||||||
|
|
||||||
### Phase 5: Material Inspector (3-4 天)
|
|
||||||
|
|
||||||
1. **创建 MaterialPropertiesEditor 组件**
|
|
||||||
2. **创建 PropertyField 组件** (Float, Vector, Color, Texture)
|
|
||||||
3. **集成到现有 Inspector 系统**
|
|
||||||
4. **支持实时预览**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 六、文件修改清单
|
|
||||||
|
|
||||||
| 优先级 | 包 | 文件 | 操作 |
|
|
||||||
|--------|-----|------|------|
|
|
||||||
| P0 | material-system | `src/interfaces/IMaterialOverridable.ts` | 新建 |
|
|
||||||
| P0 | material-system | `src/mixins/MaterialOverridableMixin.ts` | 新建 |
|
|
||||||
| P0 | material-system | `src/interfaces/IShaderProperty.ts` | 新建 |
|
|
||||||
| P1 | material-system | `src/effects/BaseShinyEffect.ts` | 新建 |
|
|
||||||
| P1 | material-system | `src/effects/ShinyEffectAnimator.ts` | 新建 |
|
|
||||||
| P1 | sprite | `src/SpriteComponent.ts` | 重构 |
|
|
||||||
| P1 | ui | `src/components/UIRenderComponent.ts` | 重构 |
|
|
||||||
| P2 | sprite | `src/ShinyEffectComponent.ts` | 重构 |
|
|
||||||
| P2 | ui | `src/components/UIShinyEffectComponent.ts` | 重构 |
|
|
||||||
| P2 | sprite | `src/systems/ShinyEffectSystem.ts` | 重构 |
|
|
||||||
| P2 | ui | `src/systems/render/UIShinyEffectSystem.ts` | 重构 |
|
|
||||||
| P3 | material-system | `src/loaders/ShaderLoader.ts` | 扩展 |
|
|
||||||
| P3 | editor-app | `src/components/inspectors/material/*` | 新建 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 七、Transform 组件统一(可选)
|
|
||||||
|
|
||||||
### 7.1 现状分析
|
|
||||||
|
|
||||||
| 特性 | TransformComponent | UITransformComponent |
|
|
||||||
|------|-------------------|---------------------|
|
|
||||||
| **坐标系** | 绝对坐标 (position.x/y/z) | 相对锚点坐标 (x/y + anchor) |
|
|
||||||
| **尺寸** | ❌ 无 | ✅ width/height + 约束 |
|
|
||||||
| **锚点系统** | ❌ 无 | ✅ anchorMin/Max |
|
|
||||||
| **3D 支持** | ✅ IVector3 | ❌ 纯 2D |
|
|
||||||
| **可见性** | ❌ 无 | ✅ visible, alpha |
|
|
||||||
|
|
||||||
### 7.2 结论
|
|
||||||
|
|
||||||
**不建议完全合并**,但可提取公共基类:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// packages/engine-core/src/interfaces/ITransformBase.ts
|
|
||||||
|
|
||||||
export interface ITransformBase {
|
|
||||||
/** 旋转角度(度) | Rotation in degrees */
|
|
||||||
rotation: number;
|
|
||||||
|
|
||||||
/** X 缩放 | Scale X */
|
|
||||||
scaleX: number;
|
|
||||||
|
|
||||||
/** Y 缩放 | Scale Y */
|
|
||||||
scaleY: number;
|
|
||||||
|
|
||||||
/** 本地到世界矩阵 | Local to world matrix */
|
|
||||||
readonly localToWorldMatrix: Matrix2D;
|
|
||||||
|
|
||||||
/** 是否需要更新 | Dirty flag */
|
|
||||||
isDirty: boolean;
|
|
||||||
|
|
||||||
/** 世界坐标 X | World position X */
|
|
||||||
readonly worldX: number;
|
|
||||||
|
|
||||||
/** 世界坐标 Y | World position Y */
|
|
||||||
readonly worldY: number;
|
|
||||||
|
|
||||||
/** 世界旋转 | World rotation */
|
|
||||||
readonly worldRotation: number;
|
|
||||||
|
|
||||||
/** 世界缩放 X | World scale X */
|
|
||||||
readonly worldScaleX: number;
|
|
||||||
|
|
||||||
/** 世界缩放 Y | World scale Y */
|
|
||||||
readonly worldScaleY: number;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7.3 收益
|
|
||||||
|
|
||||||
- 渲染系统可以统一处理 `ITransformBase`
|
|
||||||
- 减少 SpriteRenderSystem 和 UIRenderSystem 的重复
|
|
||||||
- Gizmo 系统可以共享变换操作逻辑
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 八、向后兼容性
|
|
||||||
|
|
||||||
1. **接口兼容**:现有组件的 API 保持不变
|
|
||||||
2. **序列化兼容**:不改变现有序列化格式
|
|
||||||
3. **渐进迁移**:可分阶段进行,不影响现有功能
|
|
||||||
@@ -4,19 +4,6 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## v2.4.1 (2025-12-23)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- 修复 `IntervalSystem` 时间累加 bug,间隔计时更加准确
|
|
||||||
- 修复 Cocos Creator 兼容性问题,类型导出更完整
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
|
|
||||||
- 新增 `Core.paused` 属性文档说明
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## v2.4.0 (2025-12-15)
|
## v2.4.0 (2025-12-15)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -4,19 +4,6 @@ This document records the version update history of the `@esengine/ecs-framework
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## v2.4.1 (2025-12-23)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- Fix `IntervalSystem` time accumulation bug, interval timing is now more accurate
|
|
||||||
- Fix Cocos Creator compatibility issue, more complete type exports
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
|
|
||||||
- Add `Core.paused` property documentation
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## v2.4.0 (2025-12-15)
|
## v2.4.0 (2025-12-15)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -1,402 +0,0 @@
|
|||||||
# Time and Timer System
|
|
||||||
|
|
||||||
The ECS framework provides a complete time management and timer system, including time scaling, frame time calculation, and flexible timer scheduling.
|
|
||||||
|
|
||||||
## Time Class
|
|
||||||
|
|
||||||
The Time class is the core of the framework's time management, providing all game time-related functionality.
|
|
||||||
|
|
||||||
### Basic Time Properties
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { Time } from '@esengine/ecs-framework';
|
|
||||||
|
|
||||||
class GameSystem extends EntitySystem {
|
|
||||||
protected process(entities: readonly Entity[]): void {
|
|
||||||
// Get frame time (seconds)
|
|
||||||
const deltaTime = Time.deltaTime;
|
|
||||||
|
|
||||||
// Get unscaled frame time
|
|
||||||
const unscaledDelta = Time.unscaledDeltaTime;
|
|
||||||
|
|
||||||
// Get total game time
|
|
||||||
const totalTime = Time.totalTime;
|
|
||||||
|
|
||||||
// Get current frame count
|
|
||||||
const frameCount = Time.frameCount;
|
|
||||||
|
|
||||||
console.log(`Frame ${frameCount}, delta: ${deltaTime}s, total: ${totalTime}s`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Game Pause
|
|
||||||
|
|
||||||
The framework provides two pause methods for different scenarios:
|
|
||||||
|
|
||||||
#### Core.paused (Recommended)
|
|
||||||
|
|
||||||
`Core.paused` is a **true pause** - when set, the entire game loop stops:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { Core } from '@esengine/ecs-framework';
|
|
||||||
|
|
||||||
class PauseMenuSystem extends EntitySystem {
|
|
||||||
public pauseGame(): void {
|
|
||||||
// True pause - all systems stop executing
|
|
||||||
Core.paused = true;
|
|
||||||
console.log('Game paused');
|
|
||||||
}
|
|
||||||
|
|
||||||
public resumeGame(): void {
|
|
||||||
// Resume game
|
|
||||||
Core.paused = false;
|
|
||||||
console.log('Game resumed');
|
|
||||||
}
|
|
||||||
|
|
||||||
public togglePause(): void {
|
|
||||||
Core.paused = !Core.paused;
|
|
||||||
console.log(Core.paused ? 'Game paused' : 'Game resumed');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Time.timeScale = 0
|
|
||||||
|
|
||||||
`Time.timeScale = 0` only makes `deltaTime` become 0, **systems still execute**:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
class SlowMotionSystem extends EntitySystem {
|
|
||||||
public freezeTime(): void {
|
|
||||||
// Time freeze - systems still execute, just deltaTime = 0
|
|
||||||
Time.timeScale = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Comparison
|
|
||||||
|
|
||||||
| Feature | `Core.paused = true` | `Time.timeScale = 0` |
|
|
||||||
|---------|---------------------|---------------------|
|
|
||||||
| System Execution | Completely stopped | Still running |
|
|
||||||
| CPU Overhead | Zero | Normal overhead |
|
|
||||||
| Time Updates | Stopped | Continues (deltaTime=0) |
|
|
||||||
| Timers | Stopped | Continues (but time doesn't advance) |
|
|
||||||
| Use Cases | Pause menu, game pause | Slow motion, bullet time effects |
|
|
||||||
|
|
||||||
**Recommendations**:
|
|
||||||
- Pause menu, true game pause → Use `Core.paused = true`
|
|
||||||
- Slow motion, bullet time effects → Use `Time.timeScale`
|
|
||||||
|
|
||||||
### Time Scaling
|
|
||||||
|
|
||||||
The Time class supports time scaling for slow motion, fast forward, and other effects:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
class TimeControlSystem extends EntitySystem {
|
|
||||||
public enableSlowMotion(): void {
|
|
||||||
// Set to slow motion (50% speed)
|
|
||||||
Time.timeScale = 0.5;
|
|
||||||
console.log('Slow motion enabled');
|
|
||||||
}
|
|
||||||
|
|
||||||
public enableFastForward(): void {
|
|
||||||
// Set to fast forward (200% speed)
|
|
||||||
Time.timeScale = 2.0;
|
|
||||||
console.log('Fast forward enabled');
|
|
||||||
}
|
|
||||||
|
|
||||||
public enableBulletTime(): void {
|
|
||||||
// Bullet time effect (10% speed)
|
|
||||||
Time.timeScale = 0.1;
|
|
||||||
console.log('Bullet time enabled');
|
|
||||||
}
|
|
||||||
|
|
||||||
public resumeNormalSpeed(): void {
|
|
||||||
// Resume normal speed
|
|
||||||
Time.timeScale = 1.0;
|
|
||||||
console.log('Normal speed resumed');
|
|
||||||
}
|
|
||||||
|
|
||||||
protected process(entities: readonly Entity[]): void {
|
|
||||||
// deltaTime is affected by timeScale
|
|
||||||
const scaledDelta = Time.deltaTime; // Affected by time scale
|
|
||||||
const realDelta = Time.unscaledDeltaTime; // Not affected by time scale
|
|
||||||
|
|
||||||
for (const entity of entities) {
|
|
||||||
const movement = entity.getComponent(Movement);
|
|
||||||
if (movement) {
|
|
||||||
// Use scaled time for game logic updates
|
|
||||||
movement.update(scaledDelta);
|
|
||||||
}
|
|
||||||
|
|
||||||
const ui = entity.getComponent(UIComponent);
|
|
||||||
if (ui) {
|
|
||||||
// UI animations use real time, not affected by game time scale
|
|
||||||
ui.update(realDelta);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Time Check Utilities
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
class CooldownSystem extends EntitySystem {
|
|
||||||
private lastAttackTime = 0;
|
|
||||||
private lastSpawnTime = 0;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super(Matcher.all(Weapon));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected process(entities: readonly Entity[]): void {
|
|
||||||
// Check attack cooldown
|
|
||||||
if (Time.checkEvery(1.5, this.lastAttackTime)) {
|
|
||||||
this.performAttack();
|
|
||||||
this.lastAttackTime = Time.totalTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check spawn interval
|
|
||||||
if (Time.checkEvery(3.0, this.lastSpawnTime)) {
|
|
||||||
this.spawnEnemy();
|
|
||||||
this.lastSpawnTime = Time.totalTime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private performAttack(): void {
|
|
||||||
console.log('Performing attack!');
|
|
||||||
}
|
|
||||||
|
|
||||||
private spawnEnemy(): void {
|
|
||||||
console.log('Spawning enemy!');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Core.schedule Timer System
|
|
||||||
|
|
||||||
Core provides powerful timer scheduling functionality for creating one-time or repeating timers.
|
|
||||||
|
|
||||||
### Basic Timer Usage
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { Core } from '@esengine/ecs-framework';
|
|
||||||
|
|
||||||
class GameScene extends Scene {
|
|
||||||
protected initialize(): void {
|
|
||||||
// Create one-time timers
|
|
||||||
this.createOneTimeTimers();
|
|
||||||
|
|
||||||
// Create repeating timers
|
|
||||||
this.createRepeatingTimers();
|
|
||||||
|
|
||||||
// Create timers with context
|
|
||||||
this.createContextTimers();
|
|
||||||
}
|
|
||||||
|
|
||||||
private createOneTimeTimers(): void {
|
|
||||||
// Execute once after 2 seconds
|
|
||||||
Core.schedule(2.0, false, null, (timer) => {
|
|
||||||
console.log('Executed after 2 second delay');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Show tip after 5 seconds
|
|
||||||
Core.schedule(5.0, false, this, (timer) => {
|
|
||||||
const scene = timer.getContext<GameScene>();
|
|
||||||
scene.showTip('Game tip: 5 seconds have passed!');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private createRepeatingTimers(): void {
|
|
||||||
// Execute every second
|
|
||||||
const heartbeatTimer = Core.schedule(1.0, true, null, (timer) => {
|
|
||||||
console.log(`Game heartbeat - Total time: ${Time.totalTime.toFixed(1)}s`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Save timer reference for later control
|
|
||||||
this.saveTimerReference(heartbeatTimer);
|
|
||||||
}
|
|
||||||
|
|
||||||
private createContextTimers(): void {
|
|
||||||
const gameData = { score: 0, level: 1 };
|
|
||||||
|
|
||||||
// Add score every 2 seconds
|
|
||||||
Core.schedule(2.0, true, gameData, (timer) => {
|
|
||||||
const data = timer.getContext<typeof gameData>();
|
|
||||||
data.score += 10;
|
|
||||||
console.log(`Score increased! Current score: ${data.score}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private saveTimerReference(timer: any): void {
|
|
||||||
// Can stop timer later
|
|
||||||
setTimeout(() => {
|
|
||||||
timer.stop();
|
|
||||||
console.log('Timer stopped');
|
|
||||||
}, 10000); // Stop after 10 seconds
|
|
||||||
}
|
|
||||||
|
|
||||||
private showTip(message: string): void {
|
|
||||||
console.log('Tip:', message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Timer Control
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
class TimerControlExample {
|
|
||||||
private attackTimer: any;
|
|
||||||
private spawnerTimer: any;
|
|
||||||
|
|
||||||
public startCombat(): void {
|
|
||||||
// Start attack timer
|
|
||||||
this.attackTimer = Core.schedule(0.5, true, this, (timer) => {
|
|
||||||
const self = timer.getContext<TimerControlExample>();
|
|
||||||
self.performAttack();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Start enemy spawn timer
|
|
||||||
this.spawnerTimer = Core.schedule(3.0, true, null, (timer) => {
|
|
||||||
this.spawnEnemy();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public stopCombat(): void {
|
|
||||||
// Stop all combat-related timers
|
|
||||||
if (this.attackTimer) {
|
|
||||||
this.attackTimer.stop();
|
|
||||||
console.log('Attack timer stopped');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.spawnerTimer) {
|
|
||||||
this.spawnerTimer.stop();
|
|
||||||
console.log('Spawn timer stopped');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public resetAttackTimer(): void {
|
|
||||||
// Reset attack timer
|
|
||||||
if (this.attackTimer) {
|
|
||||||
this.attackTimer.reset();
|
|
||||||
console.log('Attack timer reset');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private performAttack(): void {
|
|
||||||
console.log('Performing attack');
|
|
||||||
}
|
|
||||||
|
|
||||||
private spawnEnemy(): void {
|
|
||||||
console.log('Spawning enemy');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
### 1. Use Appropriate Time Types
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
class MovementSystem extends EntitySystem {
|
|
||||||
protected process(entities: readonly Entity[]): void {
|
|
||||||
for (const entity of entities) {
|
|
||||||
const movement = entity.getComponent(Movement);
|
|
||||||
|
|
||||||
// Use scaled time for game logic
|
|
||||||
movement.position.x += movement.velocity.x * Time.deltaTime;
|
|
||||||
|
|
||||||
// Use real time for UI animations (not affected by game pause)
|
|
||||||
const ui = entity.getComponent(UIAnimation);
|
|
||||||
if (ui) {
|
|
||||||
ui.update(Time.unscaledDeltaTime);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Timer Management
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
class TimerManager {
|
|
||||||
private timers: any[] = [];
|
|
||||||
|
|
||||||
public createManagedTimer(duration: number, repeats: boolean, callback: () => void): any {
|
|
||||||
const timer = Core.schedule(duration, repeats, null, callback);
|
|
||||||
this.timers.push(timer);
|
|
||||||
return timer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public stopAllTimers(): void {
|
|
||||||
for (const timer of this.timers) {
|
|
||||||
timer.stop();
|
|
||||||
}
|
|
||||||
this.timers = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public cleanupCompletedTimers(): void {
|
|
||||||
this.timers = this.timers.filter(timer => !timer.isDone);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Avoid Too Many Timers
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Avoid: Creating a timer for each entity
|
|
||||||
class BadExample extends EntitySystem {
|
|
||||||
protected onAdded(entity: Entity): void {
|
|
||||||
Core.schedule(1.0, true, entity, (timer) => {
|
|
||||||
// One timer per entity - poor performance
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recommended: Manage time uniformly in the system
|
|
||||||
class GoodExample extends EntitySystem {
|
|
||||||
private lastUpdateTime = 0;
|
|
||||||
|
|
||||||
protected process(entities: readonly Entity[]): void {
|
|
||||||
// Execute logic once per second
|
|
||||||
if (Time.checkEvery(1.0, this.lastUpdateTime)) {
|
|
||||||
this.processAllEntities(entities);
|
|
||||||
this.lastUpdateTime = Time.totalTime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private processAllEntities(entities: readonly Entity[]): void {
|
|
||||||
// Batch process all entities
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Timer Context Usage
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface TimerContext {
|
|
||||||
entityId: number;
|
|
||||||
duration: number;
|
|
||||||
onComplete: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
class ContextualTimerExample {
|
|
||||||
public createEntityTimer(entityId: number, duration: number, onComplete: () => void): void {
|
|
||||||
const context: TimerContext = {
|
|
||||||
entityId,
|
|
||||||
duration,
|
|
||||||
onComplete
|
|
||||||
};
|
|
||||||
|
|
||||||
Core.schedule(duration, false, context, (timer) => {
|
|
||||||
const ctx = timer.getContext<TimerContext>();
|
|
||||||
console.log(`Timer for entity ${ctx.entityId} completed`);
|
|
||||||
ctx.onComplete();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The time and timer system is an essential tool in game development. Using these features correctly will make your game logic more precise and controllable.
|
|
||||||
@@ -30,64 +30,6 @@ class GameSystem extends EntitySystem {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 游戏暂停
|
|
||||||
|
|
||||||
框架提供两种暂停方式,适用于不同场景:
|
|
||||||
|
|
||||||
#### Core.paused(推荐)
|
|
||||||
|
|
||||||
`Core.paused` 是**真正的暂停**,设置后整个游戏循环停止:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { Core } from '@esengine/ecs-framework';
|
|
||||||
|
|
||||||
class PauseMenuSystem extends EntitySystem {
|
|
||||||
public pauseGame(): void {
|
|
||||||
// 真正暂停 - 所有系统停止执行
|
|
||||||
Core.paused = true;
|
|
||||||
console.log('游戏已暂停');
|
|
||||||
}
|
|
||||||
|
|
||||||
public resumeGame(): void {
|
|
||||||
// 恢复游戏
|
|
||||||
Core.paused = false;
|
|
||||||
console.log('游戏已恢复');
|
|
||||||
}
|
|
||||||
|
|
||||||
public togglePause(): void {
|
|
||||||
Core.paused = !Core.paused;
|
|
||||||
console.log(Core.paused ? '游戏已暂停' : '游戏已恢复');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Time.timeScale = 0
|
|
||||||
|
|
||||||
`Time.timeScale = 0` 只是让 `deltaTime` 变为 0,**系统仍然在执行**:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
class SlowMotionSystem extends EntitySystem {
|
|
||||||
public freezeTime(): void {
|
|
||||||
// 时间冻结 - 系统仍在执行,只是 deltaTime = 0
|
|
||||||
Time.timeScale = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 两种方式对比
|
|
||||||
|
|
||||||
| 特性 | `Core.paused = true` | `Time.timeScale = 0` |
|
|
||||||
|------|---------------------|---------------------|
|
|
||||||
| 系统执行 | ❌ 完全停止 | ✅ 仍在执行 |
|
|
||||||
| CPU 开销 | 零 | 正常开销 |
|
|
||||||
| Time 更新 | ❌ 停止 | ✅ 继续(deltaTime=0) |
|
|
||||||
| 定时器 | ❌ 停止 | ✅ 继续(但时间不走) |
|
|
||||||
| 适用场景 | 暂停菜单、游戏暂停 | 慢动作、时间冻结特效 |
|
|
||||||
|
|
||||||
**推荐**:
|
|
||||||
- 暂停菜单、真正的游戏暂停 → 使用 `Core.paused = true`
|
|
||||||
- 慢动作、子弹时间等特效 → 使用 `Time.timeScale`
|
|
||||||
|
|
||||||
### 时间缩放
|
### 时间缩放
|
||||||
|
|
||||||
Time 类支持时间缩放功能,可以实现慢动作、快进等效果:
|
Time 类支持时间缩放功能,可以实现慢动作、快进等效果:
|
||||||
@@ -106,10 +48,10 @@ class TimeControlSystem extends EntitySystem {
|
|||||||
console.log('快进模式启用');
|
console.log('快进模式启用');
|
||||||
}
|
}
|
||||||
|
|
||||||
public enableBulletTime(): void {
|
public pauseGame(): void {
|
||||||
// 子弹时间效果(10%速度)
|
// 暂停游戏(时间静止)
|
||||||
Time.timeScale = 0.1;
|
Time.timeScale = 0;
|
||||||
console.log('子弹时间启用');
|
console.log('游戏暂停');
|
||||||
}
|
}
|
||||||
|
|
||||||
public resumeNormalSpeed(): void {
|
public resumeNormalSpeed(): void {
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "asset-system-editor",
|
|
||||||
"name": "@esengine/asset-system-editor",
|
|
||||||
"displayName": "Asset System Editor",
|
|
||||||
"description": "Asset packing and bundling tools | 资产打包工具",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"category": "Editor",
|
|
||||||
"icon": "Package",
|
|
||||||
"isEditorPlugin": true,
|
|
||||||
"runtimeModule": "@esengine/asset-system",
|
|
||||||
"exports": {
|
|
||||||
"services": ["AssetPacker", "AssetBundler"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -165,10 +165,7 @@ export function inferAssetType(path: string): AssetType {
|
|||||||
btree: 'behavior-tree',
|
btree: 'behavior-tree',
|
||||||
bp: 'blueprint',
|
bp: 'blueprint',
|
||||||
mat: 'material',
|
mat: 'material',
|
||||||
particle: 'particle',
|
particle: 'particle'
|
||||||
|
|
||||||
// FairyGUI
|
|
||||||
fui: 'fui'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return typeMap[ext] || 'binary';
|
return typeMap[ext] || 'binary';
|
||||||
|
|||||||
@@ -30,9 +30,9 @@
|
|||||||
"author": "yhh",
|
"author": "yhh",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@esengine/build-config": "workspace:*",
|
|
||||||
"@esengine/ecs-framework": "workspace:*",
|
"@esengine/ecs-framework": "workspace:*",
|
||||||
"@esengine/engine-core": "workspace:*",
|
"@esengine/engine-core": "workspace:*",
|
||||||
|
"@esengine/build-config": "workspace:*",
|
||||||
"rimraf": "^5.0.0",
|
"rimraf": "^5.0.0",
|
||||||
"tsup": "^8.0.0",
|
"tsup": "^8.0.0",
|
||||||
"typescript": "^5.8.3"
|
"typescript": "^5.8.3"
|
||||||
@@ -44,9 +44,5 @@
|
|||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/esengine/esengine.git",
|
"url": "https://github.com/esengine/esengine.git",
|
||||||
"directory": "packages/asset-system"
|
"directory": "packages/asset-system"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@types/pako": "^2.0.4",
|
|
||||||
"pako": "^2.1.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,47 +10,6 @@ import {
|
|||||||
IAssetCatalogEntry
|
IAssetCatalogEntry
|
||||||
} from '../types/AssetTypes';
|
} from '../types/AssetTypes';
|
||||||
|
|
||||||
/**
|
|
||||||
* 纹理 Sprite 信息(从 meta 文件的 importSettings 读取)
|
|
||||||
* Texture sprite info (read from meta file's importSettings)
|
|
||||||
*/
|
|
||||||
export interface ITextureSpriteInfo {
|
|
||||||
/**
|
|
||||||
* 九宫格切片边距 [top, right, bottom, left]
|
|
||||||
* Nine-patch slice border
|
|
||||||
*/
|
|
||||||
sliceBorder?: [number, number, number, number];
|
|
||||||
/**
|
|
||||||
* Sprite 锚点 [x, y](0-1 归一化)
|
|
||||||
* Sprite pivot point (0-1 normalized)
|
|
||||||
*/
|
|
||||||
pivot?: [number, number];
|
|
||||||
/**
|
|
||||||
* 纹理宽度(可选,需要纹理已加载)
|
|
||||||
* Texture width (optional, requires texture to be loaded)
|
|
||||||
*/
|
|
||||||
width?: number;
|
|
||||||
/**
|
|
||||||
* 纹理高度(可选,需要纹理已加载)
|
|
||||||
* Texture height (optional, requires texture to be loaded)
|
|
||||||
*/
|
|
||||||
height?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sprite settings in import settings
|
|
||||||
* 导入设置中的 Sprite 设置
|
|
||||||
*/
|
|
||||||
interface ISpriteSettings {
|
|
||||||
sliceBorder?: [number, number, number, number];
|
|
||||||
pivot?: [number, number];
|
|
||||||
pixelsPerUnit?: number;
|
|
||||||
/** Texture width (from import settings) | 纹理宽度(来自导入设置) */
|
|
||||||
width?: number;
|
|
||||||
/** Texture height (from import settings) | 纹理高度(来自导入设置) */
|
|
||||||
height?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asset database implementation
|
* Asset database implementation
|
||||||
* 资产数据库实现
|
* 资产数据库实现
|
||||||
@@ -253,41 +212,6 @@ export class AssetDatabase {
|
|||||||
return guid ? this._metadata.get(guid) : undefined;
|
return guid ? this._metadata.get(guid) : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get texture sprite info from metadata
|
|
||||||
* 从元数据获取纹理 Sprite 信息
|
|
||||||
*
|
|
||||||
* Extracts spriteSettings from importSettings if available.
|
|
||||||
* 如果可用,从 importSettings 提取 spriteSettings。
|
|
||||||
*
|
|
||||||
* @param guid - Texture asset GUID | 纹理资产 GUID
|
|
||||||
* @returns Sprite info or undefined if not found/not a texture | Sprite 信息或未找到/非纹理则为 undefined
|
|
||||||
*/
|
|
||||||
getTextureSpriteInfo(guid: AssetGUID): ITextureSpriteInfo | undefined {
|
|
||||||
const metadata = this._metadata.get(guid);
|
|
||||||
if (!metadata) return undefined;
|
|
||||||
|
|
||||||
// Check if it's a texture asset
|
|
||||||
// 检查是否是纹理资产
|
|
||||||
if (metadata.type !== AssetType.Texture) return undefined;
|
|
||||||
|
|
||||||
// Extract spriteSettings from importSettings
|
|
||||||
// 从 importSettings 提取 spriteSettings
|
|
||||||
const importSettings = metadata.importSettings as Record<string, unknown> | undefined;
|
|
||||||
const spriteSettings = importSettings?.spriteSettings as ISpriteSettings | undefined;
|
|
||||||
|
|
||||||
if (!spriteSettings) return undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
sliceBorder: spriteSettings.sliceBorder,
|
|
||||||
pivot: spriteSettings.pivot,
|
|
||||||
// Include dimensions from import settings if available
|
|
||||||
// 如果可用,包含来自导入设置的尺寸
|
|
||||||
width: spriteSettings.width,
|
|
||||||
height: spriteSettings.height
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find assets by type
|
* Find assets by type
|
||||||
* 按类型查找资产
|
* 按类型查找资产
|
||||||
|
|||||||
@@ -132,10 +132,7 @@ export class AssetManager implements IAssetManager {
|
|||||||
labels: [],
|
labels: [],
|
||||||
tags: new Map(),
|
tags: new Map(),
|
||||||
lastModified: Date.now(),
|
lastModified: Date.now(),
|
||||||
version: 1,
|
version: 1
|
||||||
// Include importSettings for sprite slicing (nine-patch), etc.
|
|
||||||
// 包含 importSettings 以支持精灵切片(九宫格)等功能
|
|
||||||
importSettings: entry.importSettings
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this._database.addAsset(metadata);
|
this._database.addAsset(metadata);
|
||||||
@@ -176,11 +173,7 @@ export class AssetManager implements IAssetManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 创建加载器 / Create loader
|
// 创建加载器 / Create loader
|
||||||
// 优先使用基于路径的加载器选择,支持多个加载器对应同一资产类型
|
let loader = this._loaderFactory.createLoader(metadata.type);
|
||||||
// 例如 Model3D 类型支持 GLTF/FBX/OBJ,根据扩展名选择正确的加载器
|
|
||||||
// Prefer path-based loader selection, supports multiple loaders for same asset type
|
|
||||||
// e.g., Model3D type supports GLTF/FBX/OBJ, selects correct loader by extension
|
|
||||||
let loader = this._loaderFactory.createLoaderForPath(metadata.path);
|
|
||||||
|
|
||||||
// 如果没有找到 loader 且类型是 Custom,尝试重新解析类型
|
// 如果没有找到 loader 且类型是 Custom,尝试重新解析类型
|
||||||
// If no loader found and type is Custom, try to re-resolve the type
|
// If no loader found and type is Custom, try to re-resolve the type
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ export { RuntimeCatalog, runtimeCatalog } from './runtime/RuntimeCatalog';
|
|||||||
export * from './interfaces/IAssetLoader';
|
export * from './interfaces/IAssetLoader';
|
||||||
export * from './interfaces/IAssetManager';
|
export * from './interfaces/IAssetManager';
|
||||||
export * from './interfaces/IAssetReader';
|
export * from './interfaces/IAssetReader';
|
||||||
export * from './interfaces/IAssetFileLoader';
|
|
||||||
export * from './interfaces/IResourceComponent';
|
export * from './interfaces/IResourceComponent';
|
||||||
|
|
||||||
// Core
|
// Core
|
||||||
@@ -57,31 +56,15 @@ export { BinaryLoader } from './loaders/BinaryLoader';
|
|||||||
export { AudioLoader } from './loaders/AudioLoader';
|
export { AudioLoader } from './loaders/AudioLoader';
|
||||||
export { PrefabLoader } from './loaders/PrefabLoader';
|
export { PrefabLoader } from './loaders/PrefabLoader';
|
||||||
|
|
||||||
// 3D Model Loaders | 3D 模型加载器
|
|
||||||
export { GLTFLoader } from './loaders/GLTFLoader';
|
|
||||||
export { OBJLoader } from './loaders/OBJLoader';
|
|
||||||
export { FBXLoader } from './loaders/FBXLoader';
|
|
||||||
|
|
||||||
// Integration
|
// Integration
|
||||||
export { EngineIntegration } from './integration/EngineIntegration';
|
export { EngineIntegration } from './integration/EngineIntegration';
|
||||||
export type { ITextureEngineBridge, TextureLoadCallback } from './integration/EngineIntegration';
|
export type { ITextureEngineBridge } from './integration/EngineIntegration';
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
export { SceneResourceManager } from './services/SceneResourceManager';
|
export { SceneResourceManager } from './services/SceneResourceManager';
|
||||||
export type { IResourceLoader } from './services/SceneResourceManager';
|
export type { IResourceLoader } from './services/SceneResourceManager';
|
||||||
export { PathResolutionService } from './services/PathResolutionService';
|
export { PathResolutionService } from './services/PathResolutionService';
|
||||||
|
|
||||||
// Asset Metadata Service (primary API for sprite info)
|
|
||||||
// 资产元数据服务(sprite 信息的主要 API)
|
|
||||||
export {
|
|
||||||
setGlobalAssetDatabase,
|
|
||||||
getGlobalAssetDatabase,
|
|
||||||
setGlobalEngineBridge,
|
|
||||||
getGlobalEngineBridge,
|
|
||||||
getTextureSpriteInfo
|
|
||||||
} from './services/AssetMetadataService';
|
|
||||||
export type { ITextureSpriteInfo } from './core/AssetDatabase';
|
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
export { UVHelper } from './utils/UVHelper';
|
export { UVHelper } from './utils/UVHelper';
|
||||||
export {
|
export {
|
||||||
|
|||||||
@@ -31,6 +31,12 @@ export interface ITextureEngineBridge {
|
|||||||
*/
|
*/
|
||||||
unloadTexture(id: number): void;
|
unloadTexture(id: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get texture info
|
||||||
|
* 获取纹理信息
|
||||||
|
*/
|
||||||
|
getTextureInfo(id: number): { width: number; height: number } | null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get or load texture by path.
|
* Get or load texture by path.
|
||||||
* 按路径获取或加载纹理。
|
* 按路径获取或加载纹理。
|
||||||
@@ -103,20 +109,6 @@ export interface ITextureEngineBridge {
|
|||||||
* @returns Promise that resolves when texture is ready | 纹理就绪时解析的 Promise
|
* @returns Promise that resolves when texture is ready | 纹理就绪时解析的 Promise
|
||||||
*/
|
*/
|
||||||
loadTextureAsync?(id: number, url: string): Promise<void>;
|
loadTextureAsync?(id: number, url: string): Promise<void>;
|
||||||
|
|
||||||
/**
|
|
||||||
* Get texture info by path.
|
|
||||||
* 通过路径获取纹理信息。
|
|
||||||
*
|
|
||||||
* This is the primary API for getting texture dimensions.
|
|
||||||
* The Rust engine is the single source of truth for texture dimensions.
|
|
||||||
* 这是获取纹理尺寸的主要 API。
|
|
||||||
* Rust 引擎是纹理尺寸的唯一事实来源。
|
|
||||||
*
|
|
||||||
* @param path Image path/URL | 图片路径/URL
|
|
||||||
* @returns Texture info or null if not loaded | 纹理信息或未加载则为 null
|
|
||||||
*/
|
|
||||||
getTextureInfoByPath?(path: string): { width: number; height: number } | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -139,43 +131,10 @@ interface DataAssetEntry {
|
|||||||
path: string;
|
path: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Texture load callback type
|
|
||||||
* 纹理加载回调类型
|
|
||||||
*/
|
|
||||||
export type TextureLoadCallback = (guid: string, path: string, textureId: number) => void;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asset system engine integration
|
* Asset system engine integration
|
||||||
* 资产系统引擎集成
|
* 资产系统引擎集成
|
||||||
*/
|
*/
|
||||||
/**
|
|
||||||
* Texture sprite info (nine-patch border, pivot, etc.)
|
|
||||||
* 纹理 Sprite 信息(九宫格边距、锚点等)
|
|
||||||
*/
|
|
||||||
export interface ITextureSpriteInfo {
|
|
||||||
/**
|
|
||||||
* 九宫格切片边距 [top, right, bottom, left]
|
|
||||||
* Nine-patch slice border
|
|
||||||
*/
|
|
||||||
sliceBorder?: [number, number, number, number];
|
|
||||||
/**
|
|
||||||
* Sprite 锚点 [x, y](0-1 归一化)
|
|
||||||
* Sprite pivot point (0-1 normalized)
|
|
||||||
*/
|
|
||||||
pivot?: [number, number];
|
|
||||||
/**
|
|
||||||
* 纹理宽度
|
|
||||||
* Texture width
|
|
||||||
*/
|
|
||||||
width: number;
|
|
||||||
/**
|
|
||||||
* 纹理高度
|
|
||||||
* Texture height
|
|
||||||
*/
|
|
||||||
height: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class EngineIntegration {
|
export class EngineIntegration {
|
||||||
private _assetManager: AssetManager;
|
private _assetManager: AssetManager;
|
||||||
private _engineBridge?: ITextureEngineBridge;
|
private _engineBridge?: ITextureEngineBridge;
|
||||||
@@ -187,54 +146,6 @@ export class EngineIntegration {
|
|||||||
// Path-stable ID cache (persists across Play/Stop cycles)
|
// Path-stable ID cache (persists across Play/Stop cycles)
|
||||||
private static _pathIdCache = new Map<string, number>();
|
private static _pathIdCache = new Map<string, number>();
|
||||||
|
|
||||||
// 纹理 Sprite 信息缓存(全局静态,可供渲染系统访问)
|
|
||||||
// Texture sprite info cache (global static, accessible by render systems)
|
|
||||||
private static _textureSpriteInfoCache = new Map<AssetGUID, ITextureSpriteInfo>();
|
|
||||||
|
|
||||||
// 纹理加载回调(用于动态图集集成等)
|
|
||||||
// Texture load callback (for dynamic atlas integration, etc.)
|
|
||||||
private static _textureLoadCallbacks: TextureLoadCallback[] = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a callback to be called when textures are loaded
|
|
||||||
* 注册纹理加载时调用的回调
|
|
||||||
*
|
|
||||||
* This can be used for dynamic atlas integration.
|
|
||||||
* 可用于动态图集集成。
|
|
||||||
*
|
|
||||||
* @param callback - Callback function | 回调函数
|
|
||||||
*/
|
|
||||||
static onTextureLoad(callback: TextureLoadCallback): void {
|
|
||||||
if (!EngineIntegration._textureLoadCallbacks.includes(callback)) {
|
|
||||||
EngineIntegration._textureLoadCallbacks.push(callback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a texture load callback
|
|
||||||
* 移除纹理加载回调
|
|
||||||
*/
|
|
||||||
static removeTextureLoadCallback(callback: TextureLoadCallback): void {
|
|
||||||
const index = EngineIntegration._textureLoadCallbacks.indexOf(callback);
|
|
||||||
if (index >= 0) {
|
|
||||||
EngineIntegration._textureLoadCallbacks.splice(index, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notify all callbacks of a texture load
|
|
||||||
* 通知所有回调纹理已加载
|
|
||||||
*/
|
|
||||||
private static notifyTextureLoad(guid: string, path: string, textureId: number): void {
|
|
||||||
for (const callback of EngineIntegration._textureLoadCallbacks) {
|
|
||||||
try {
|
|
||||||
callback(guid, path, textureId);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[EngineIntegration] Error in texture load callback:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Audio resource mappings | 音频资源映射
|
// Audio resource mappings | 音频资源映射
|
||||||
private _audioIdMap = new Map<AssetGUID, number>();
|
private _audioIdMap = new Map<AssetGUID, number>();
|
||||||
private _pathToAudioId = new Map<string, number>();
|
private _pathToAudioId = new Map<string, number>();
|
||||||
@@ -368,16 +279,6 @@ export class EngineIntegration {
|
|||||||
const result = await this._assetManager.loadAsset<ITextureAsset>(guid);
|
const result = await this._assetManager.loadAsset<ITextureAsset>(guid);
|
||||||
const metadata = result.metadata;
|
const metadata = result.metadata;
|
||||||
const assetPath = metadata.path;
|
const assetPath = metadata.path;
|
||||||
const textureAsset = result.asset;
|
|
||||||
|
|
||||||
// 缓存 sprite 信息(九宫格边距等)到静态缓存
|
|
||||||
// Cache sprite info (slice border, etc.) to static cache
|
|
||||||
EngineIntegration._textureSpriteInfoCache.set(guid, {
|
|
||||||
sliceBorder: textureAsset.sliceBorder,
|
|
||||||
pivot: textureAsset.pivot,
|
|
||||||
width: textureAsset.width,
|
|
||||||
height: textureAsset.height
|
|
||||||
});
|
|
||||||
|
|
||||||
// 生成路径稳定 ID
|
// 生成路径稳定 ID
|
||||||
// Generate path-stable ID
|
// Generate path-stable ID
|
||||||
@@ -408,37 +309,9 @@ export class EngineIntegration {
|
|||||||
this._textureIdMap.set(guid, stableId);
|
this._textureIdMap.set(guid, stableId);
|
||||||
this._pathToTextureId.set(assetPath, stableId);
|
this._pathToTextureId.set(assetPath, stableId);
|
||||||
|
|
||||||
// 通知回调(用于动态图集等)
|
|
||||||
// Notify callbacks (for dynamic atlas, etc.)
|
|
||||||
EngineIntegration.notifyTextureLoad(guid, engineUrl, stableId);
|
|
||||||
|
|
||||||
return stableId;
|
return stableId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get texture sprite info by GUID (static method for render system access)
|
|
||||||
* 通过 GUID 获取纹理 Sprite 信息(静态方法,供渲染系统访问)
|
|
||||||
*
|
|
||||||
* Returns cached sprite info including nine-patch slice border.
|
|
||||||
* Must call loadTextureByGuid first to populate the cache.
|
|
||||||
* 返回缓存的 sprite 信息,包括九宫格边距。
|
|
||||||
* 必须先调用 loadTextureByGuid 来填充缓存。
|
|
||||||
*
|
|
||||||
* @param guid - Texture asset GUID | 纹理资产 GUID
|
|
||||||
* @returns Sprite info or undefined if not loaded | Sprite 信息或未加载则为 undefined
|
|
||||||
*/
|
|
||||||
static getTextureSpriteInfo(guid: AssetGUID): ITextureSpriteInfo | undefined {
|
|
||||||
return EngineIntegration._textureSpriteInfoCache.get(guid);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear texture sprite info cache
|
|
||||||
* 清除纹理 Sprite 信息缓存
|
|
||||||
*/
|
|
||||||
static clearTextureSpriteInfoCache(): void {
|
|
||||||
EngineIntegration._textureSpriteInfoCache.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Batch load textures
|
* Batch load textures
|
||||||
* 批量加载纹理
|
* 批量加载纹理
|
||||||
|
|||||||
@@ -1,103 +0,0 @@
|
|||||||
/**
|
|
||||||
* Asset File Loader Interface
|
|
||||||
* 资产文件加载器接口
|
|
||||||
*
|
|
||||||
* High-level file loading abstraction that combines path resolution
|
|
||||||
* with platform-specific file reading.
|
|
||||||
* 高级文件加载抽象,结合路径解析和平台特定的文件读取。
|
|
||||||
*
|
|
||||||
* This is the unified entry point for all file loading in the engine.
|
|
||||||
* Different from IAssetLoader (which parses content), this interface
|
|
||||||
* handles the actual file fetching from asset paths.
|
|
||||||
* 这是引擎中所有文件加载的统一入口。
|
|
||||||
* 与 IAssetLoader(解析内容)不同,此接口处理从资产路径获取文件。
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asset file loader interface.
|
|
||||||
* 资产文件加载器接口。
|
|
||||||
*
|
|
||||||
* Provides a unified API for loading files from asset paths (relative to project).
|
|
||||||
* Different platforms provide their own implementations.
|
|
||||||
* 提供从资产路径(相对于项目)加载文件的统一 API。
|
|
||||||
* 不同平台提供各自的实现。
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* // Get global loader
|
|
||||||
* const loader = getGlobalAssetFileLoader();
|
|
||||||
*
|
|
||||||
* // Load image from asset path (relative to project)
|
|
||||||
* const image = await loader.loadImage('assets/demo/button.png');
|
|
||||||
*
|
|
||||||
* // Load text content
|
|
||||||
* const json = await loader.loadText('assets/config.json');
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export interface IAssetFileLoader {
|
|
||||||
/**
|
|
||||||
* Load image from asset path.
|
|
||||||
* 从资产路径加载图片。
|
|
||||||
*
|
|
||||||
* @param assetPath - Asset path relative to project (e.g., "assets/demo/button.png").
|
|
||||||
* 相对于项目的资产路径。
|
|
||||||
* @returns Promise resolving to HTMLImageElement. | 返回 HTMLImageElement 的 Promise。
|
|
||||||
*/
|
|
||||||
loadImage(assetPath: string): Promise<HTMLImageElement>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load text content from asset path.
|
|
||||||
* 从资产路径加载文本内容。
|
|
||||||
*
|
|
||||||
* @param assetPath - Asset path relative to project. | 相对于项目的资产路径。
|
|
||||||
* @returns Promise resolving to text content. | 返回文本内容的 Promise。
|
|
||||||
*/
|
|
||||||
loadText(assetPath: string): Promise<string>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load binary data from asset path.
|
|
||||||
* 从资产路径加载二进制数据。
|
|
||||||
*
|
|
||||||
* @param assetPath - Asset path relative to project. | 相对于项目的资产路径。
|
|
||||||
* @returns Promise resolving to ArrayBuffer. | 返回 ArrayBuffer 的 Promise。
|
|
||||||
*/
|
|
||||||
loadBinary(assetPath: string): Promise<ArrayBuffer>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if asset file exists.
|
|
||||||
* 检查资产文件是否存在。
|
|
||||||
*
|
|
||||||
* @param assetPath - Asset path relative to project. | 相对于项目的资产路径。
|
|
||||||
* @returns Promise resolving to boolean. | 返回布尔值的 Promise。
|
|
||||||
*/
|
|
||||||
exists(assetPath: string): Promise<boolean>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Global asset file loader instance.
|
|
||||||
* 全局资产文件加载器实例。
|
|
||||||
*/
|
|
||||||
let globalAssetFileLoader: IAssetFileLoader | null = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the global asset file loader.
|
|
||||||
* 设置全局资产文件加载器。
|
|
||||||
*
|
|
||||||
* Should be called during engine initialization with platform-specific implementation.
|
|
||||||
* 应在引擎初始化期间使用平台特定的实现调用。
|
|
||||||
*
|
|
||||||
* @param loader - Asset file loader instance or null. | 资产文件加载器实例或 null。
|
|
||||||
*/
|
|
||||||
export function setGlobalAssetFileLoader(loader: IAssetFileLoader | null): void {
|
|
||||||
globalAssetFileLoader = loader;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the global asset file loader.
|
|
||||||
* 获取全局资产文件加载器。
|
|
||||||
*
|
|
||||||
* @returns Asset file loader instance or null. | 资产文件加载器实例或 null。
|
|
||||||
*/
|
|
||||||
export function getGlobalAssetFileLoader(): IAssetFileLoader | null {
|
|
||||||
return globalAssetFileLoader;
|
|
||||||
}
|
|
||||||
@@ -80,29 +80,12 @@ export interface IAssetLoaderFactory {
|
|||||||
*/
|
*/
|
||||||
createLoader(type: AssetType): IAssetLoader | null;
|
createLoader(type: AssetType): IAssetLoader | null;
|
||||||
|
|
||||||
/**
|
|
||||||
* Create loader for a specific file path (selects by extension)
|
|
||||||
* 为特定文件路径创建加载器(按扩展名选择)
|
|
||||||
*
|
|
||||||
* This method is preferred over createLoader() when multiple loaders
|
|
||||||
* support the same asset type (e.g., Model3D with GLTF/OBJ/FBX).
|
|
||||||
* 当多个加载器支持相同资产类型时(如 Model3D 的 GLTF/OBJ/FBX),
|
|
||||||
* 优先使用此方法而非 createLoader()。
|
|
||||||
*/
|
|
||||||
createLoaderForPath(path: string): IAssetLoader | null;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register custom loader
|
* Register custom loader
|
||||||
* 注册自定义加载器
|
* 注册自定义加载器
|
||||||
*/
|
*/
|
||||||
registerLoader(type: AssetType, loader: IAssetLoader): void;
|
registerLoader(type: AssetType, loader: IAssetLoader): void;
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a loader for a specific file extension
|
|
||||||
* 为特定文件扩展名注册加载器
|
|
||||||
*/
|
|
||||||
registerExtensionLoader(extension: string, loader: IAssetLoader): void;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unregister loader
|
* Unregister loader
|
||||||
* 注销加载器
|
* 注销加载器
|
||||||
@@ -161,24 +144,6 @@ export interface ITextureAsset {
|
|||||||
hasMipmaps: boolean;
|
hasMipmaps: boolean;
|
||||||
/** 原始数据(如果可用) / Raw image data if available */
|
/** 原始数据(如果可用) / Raw image data if available */
|
||||||
data?: ImageData | HTMLImageElement;
|
data?: ImageData | HTMLImageElement;
|
||||||
|
|
||||||
// ===== Sprite Settings =====
|
|
||||||
// ===== Sprite 设置 =====
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 九宫格切片边距 [top, right, bottom, left]
|
|
||||||
* Nine-patch slice border
|
|
||||||
*
|
|
||||||
* Defines the non-stretchable borders for nine-patch rendering.
|
|
||||||
* 定义九宫格渲染时不可拉伸的边框区域。
|
|
||||||
*/
|
|
||||||
sliceBorder?: [number, number, number, number];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sprite 锚点 [x, y](0-1 归一化)
|
|
||||||
* Sprite pivot point (0-1 normalized)
|
|
||||||
*/
|
|
||||||
pivot?: [number, number];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -218,109 +183,24 @@ export interface IAudioAsset {
|
|||||||
channels: number;
|
channels: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Shader property type
|
|
||||||
* 着色器属性类型
|
|
||||||
*/
|
|
||||||
export type ShaderPropertyType = 'float' | 'vec2' | 'vec3' | 'vec4' | 'int' | 'sampler2D' | 'mat3' | 'mat4';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shader property definition
|
|
||||||
* 着色器属性定义
|
|
||||||
*/
|
|
||||||
export interface IShaderProperty {
|
|
||||||
/** 属性名称(uniform 名) / Property name (uniform name) */
|
|
||||||
name: string;
|
|
||||||
/** 属性类型 / Property type */
|
|
||||||
type: ShaderPropertyType;
|
|
||||||
/** 默认值 / Default value */
|
|
||||||
default: number | number[];
|
|
||||||
/** 显示名称(编辑器用) / Display name for editor */
|
|
||||||
displayName?: string;
|
|
||||||
/** 值范围(用于 float/int) / Value range for float/int */
|
|
||||||
range?: [number, number];
|
|
||||||
/** 是否隐藏(内部使用) / Hidden from inspector */
|
|
||||||
hidden?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shader asset interface
|
|
||||||
* 着色器资产接口
|
|
||||||
*
|
|
||||||
* Shader assets contain GLSL source code and property definitions.
|
|
||||||
* 着色器资产包含 GLSL 源代码和属性定义。
|
|
||||||
*/
|
|
||||||
export interface IShaderAsset {
|
|
||||||
/** 着色器名称 / Shader name (e.g., "UI/Shiny") */
|
|
||||||
name: string;
|
|
||||||
/** 顶点着色器源代码 / Vertex shader GLSL source */
|
|
||||||
vertex: string;
|
|
||||||
/** 片段着色器源代码 / Fragment shader GLSL source */
|
|
||||||
fragment: string;
|
|
||||||
/** 属性定义列表 / Property definitions */
|
|
||||||
properties: IShaderProperty[];
|
|
||||||
/** 编译后的着色器 ID(运行时填充) / Compiled shader ID (runtime) */
|
|
||||||
shaderId?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Material property value
|
|
||||||
* 材质属性值
|
|
||||||
*/
|
|
||||||
export type MaterialPropertyValue = number | number[] | string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Material animator configuration
|
|
||||||
* 材质动画器配置
|
|
||||||
*/
|
|
||||||
export interface IMaterialAnimator {
|
|
||||||
/** 要动画的属性名 / Property to animate */
|
|
||||||
property: string;
|
|
||||||
/** 起始值 / Start value */
|
|
||||||
from: number;
|
|
||||||
/** 结束值 / End value */
|
|
||||||
to: number;
|
|
||||||
/** 持续时间(秒) / Duration in seconds */
|
|
||||||
duration: number;
|
|
||||||
/** 是否循环 / Loop animation */
|
|
||||||
loop?: boolean;
|
|
||||||
/** 循环间隔(秒) / Delay between loops */
|
|
||||||
loopDelay?: number;
|
|
||||||
/** 缓动函数 / Easing function */
|
|
||||||
easing?: 'linear' | 'easeIn' | 'easeOut' | 'easeInOut';
|
|
||||||
/** 是否自动播放 / Auto play on start */
|
|
||||||
autoPlay?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Material asset interface
|
* Material asset interface
|
||||||
* 材质资产接口
|
* 材质资产接口
|
||||||
*
|
|
||||||
* Material assets reference a shader and define property values.
|
|
||||||
* 材质资产引用着色器并定义属性值。
|
|
||||||
*/
|
*/
|
||||||
export interface IMaterialAsset {
|
export interface IMaterialAsset {
|
||||||
/** 材质名称 / Material name */
|
/** 着色器名称 / Shader name */
|
||||||
name: string;
|
|
||||||
/** 着色器 GUID 或内置路径 / Shader GUID or built-in path (e.g., "builtin://shaders/Shiny") */
|
|
||||||
shader: string;
|
shader: string;
|
||||||
/** 材质属性值 / Material property values */
|
/** 材质属性 / Material properties */
|
||||||
properties: Record<string, MaterialPropertyValue>;
|
properties: Map<string, unknown>;
|
||||||
/** 纹理映射 / Texture slot mappings (property name -> texture GUID) */
|
/** 纹理映射 / Texture slot mappings */
|
||||||
textures?: Record<string, AssetGUID>;
|
textures: Map<string, AssetGUID>;
|
||||||
/** 渲染状态 / Render states */
|
/** 渲染状态 / Render states */
|
||||||
renderStates?: {
|
renderStates: {
|
||||||
cullMode?: 'none' | 'front' | 'back';
|
cullMode?: 'none' | 'front' | 'back';
|
||||||
blendMode?: 'none' | 'alpha' | 'additive' | 'multiply' | 'screen';
|
blendMode?: 'none' | 'alpha' | 'additive' | 'multiply';
|
||||||
depthTest?: boolean;
|
depthTest?: boolean;
|
||||||
depthWrite?: boolean;
|
depthWrite?: boolean;
|
||||||
};
|
};
|
||||||
/** 动画器配置(可选) / Animator configuration (optional) */
|
|
||||||
animator?: IMaterialAnimator;
|
|
||||||
/** 运行时:编译后的着色器 ID / Runtime: compiled shader ID */
|
|
||||||
_shaderId?: number;
|
|
||||||
/** 运行时:引擎材质 ID / Runtime: engine material ID */
|
|
||||||
_materialId?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 预制体资产接口从专用文件导出 | Prefab asset interface exported from dedicated file
|
// 预制体资产接口从专用文件导出 | Prefab asset interface exported from dedicated file
|
||||||
@@ -382,235 +262,3 @@ export interface IBinaryAsset {
|
|||||||
/** MIME类型 / MIME type */
|
/** MIME类型 / MIME type */
|
||||||
mimeType?: string;
|
mimeType?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== GLTF/GLB 3D Model Types =====
|
|
||||||
// ===== GLTF/GLB 3D 模型类型 =====
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Bounding box interface
|
|
||||||
* 边界盒接口
|
|
||||||
*/
|
|
||||||
export interface IBoundingBox {
|
|
||||||
/** 最小坐标 [x, y, z] | Minimum coordinates */
|
|
||||||
min: [number, number, number];
|
|
||||||
/** 最大坐标 [x, y, z] | Maximum coordinates */
|
|
||||||
max: [number, number, number];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extended mesh data with name and material reference
|
|
||||||
* 扩展的网格数据,包含名称和材质引用
|
|
||||||
*/
|
|
||||||
export interface IMeshData extends IMeshAsset {
|
|
||||||
/** 网格名称 | Mesh name */
|
|
||||||
name: string;
|
|
||||||
/** 引用的材质索引 | Referenced material index */
|
|
||||||
materialIndex: number;
|
|
||||||
/** 顶点颜色(如果有)| Vertex colors if available */
|
|
||||||
colors?: Float32Array;
|
|
||||||
|
|
||||||
// ===== Skinning data for skeletal animation =====
|
|
||||||
// ===== 骨骼动画蒙皮数据 =====
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Joint indices per vertex (4 influences, GLTF JOINTS_0)
|
|
||||||
* 每顶点的关节索引(4 个影响,GLTF JOINTS_0)
|
|
||||||
* Format: [j0, j1, j2, j3] for each vertex
|
|
||||||
*/
|
|
||||||
joints?: Uint8Array | Uint16Array;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Joint weights per vertex (4 influences, GLTF WEIGHTS_0)
|
|
||||||
* 每顶点的关节权重(4 个影响,GLTF WEIGHTS_0)
|
|
||||||
* Format: [w0, w1, w2, w3] for each vertex, should sum to 1.0
|
|
||||||
*/
|
|
||||||
weights?: Float32Array;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GLTF material definition
|
|
||||||
* GLTF 材质定义
|
|
||||||
*/
|
|
||||||
export interface IGLTFMaterial {
|
|
||||||
/** 材质名称 | Material name */
|
|
||||||
name: string;
|
|
||||||
/** 基础颜色 [r, g, b, a] | Base color factor */
|
|
||||||
baseColorFactor: [number, number, number, number];
|
|
||||||
/** 基础颜色纹理索引 | Base color texture index (-1 if none) */
|
|
||||||
baseColorTextureIndex: number;
|
|
||||||
/** 金属度 (0-1) | Metallic factor */
|
|
||||||
metallicFactor: number;
|
|
||||||
/** 粗糙度 (0-1) | Roughness factor */
|
|
||||||
roughnessFactor: number;
|
|
||||||
/** 金属粗糙度纹理索引 | Metallic-roughness texture index */
|
|
||||||
metallicRoughnessTextureIndex: number;
|
|
||||||
/** 法线纹理索引 | Normal texture index */
|
|
||||||
normalTextureIndex: number;
|
|
||||||
/** 法线缩放 | Normal scale */
|
|
||||||
normalScale: number;
|
|
||||||
/** 遮挡纹理索引 | Occlusion texture index */
|
|
||||||
occlusionTextureIndex: number;
|
|
||||||
/** 遮挡强度 | Occlusion strength */
|
|
||||||
occlusionStrength: number;
|
|
||||||
/** 自发光因子 [r, g, b] | Emissive factor */
|
|
||||||
emissiveFactor: [number, number, number];
|
|
||||||
/** 自发光纹理索引 | Emissive texture index */
|
|
||||||
emissiveTextureIndex: number;
|
|
||||||
/** Alpha 模式 | Alpha mode */
|
|
||||||
alphaMode: 'OPAQUE' | 'MASK' | 'BLEND';
|
|
||||||
/** Alpha 剔除阈值 | Alpha cutoff */
|
|
||||||
alphaCutoff: number;
|
|
||||||
/** 是否双面 | Double sided */
|
|
||||||
doubleSided: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GLTF texture info
|
|
||||||
* GLTF 纹理信息
|
|
||||||
*/
|
|
||||||
export interface IGLTFTextureInfo {
|
|
||||||
/** 纹理名称 | Texture name */
|
|
||||||
name?: string;
|
|
||||||
/** 图像数据(嵌入式)| Image data (embedded) */
|
|
||||||
imageData?: ArrayBuffer;
|
|
||||||
/** 图像 MIME 类型 | Image MIME type */
|
|
||||||
mimeType?: string;
|
|
||||||
/** 外部 URI(非嵌入)| External URI (non-embedded) */
|
|
||||||
uri?: string;
|
|
||||||
/** 加载后的纹理资产 GUID | Loaded texture asset GUID */
|
|
||||||
textureGuid?: AssetGUID;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GLTF node (scene hierarchy)
|
|
||||||
* GLTF 节点(场景层级)
|
|
||||||
*/
|
|
||||||
export interface IGLTFNode {
|
|
||||||
/** 节点名称 | Node name */
|
|
||||||
name: string;
|
|
||||||
/** 网格索引(可选)| Mesh index (optional) */
|
|
||||||
meshIndex?: number;
|
|
||||||
/** 子节点索引列表 | Child node indices */
|
|
||||||
children: number[];
|
|
||||||
/** 变换信息 | Transform info */
|
|
||||||
transform: {
|
|
||||||
/** 位置 [x, y, z] | Position */
|
|
||||||
position: [number, number, number];
|
|
||||||
/** 旋转四元数 [x, y, z, w] | Rotation quaternion */
|
|
||||||
rotation: [number, number, number, number];
|
|
||||||
/** 缩放 [x, y, z] | Scale */
|
|
||||||
scale: [number, number, number];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Animation channel target
|
|
||||||
* 动画通道目标
|
|
||||||
*/
|
|
||||||
export interface IAnimationChannelTarget {
|
|
||||||
/** 目标节点索引 | Target node index */
|
|
||||||
nodeIndex: number;
|
|
||||||
/** 目标属性 | Target property */
|
|
||||||
path: 'translation' | 'rotation' | 'scale' | 'weights';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Animation sampler
|
|
||||||
* 动画采样器
|
|
||||||
*/
|
|
||||||
export interface IAnimationSampler {
|
|
||||||
/** 输入时间数组 | Input time array */
|
|
||||||
input: Float32Array;
|
|
||||||
/** 输出值数组 | Output values array */
|
|
||||||
output: Float32Array;
|
|
||||||
/** 插值类型 | Interpolation type */
|
|
||||||
interpolation: 'LINEAR' | 'STEP' | 'CUBICSPLINE';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Animation channel
|
|
||||||
* 动画通道
|
|
||||||
*/
|
|
||||||
export interface IAnimationChannel {
|
|
||||||
/** 采样器索引 | Sampler index */
|
|
||||||
samplerIndex: number;
|
|
||||||
/** 目标 | Target */
|
|
||||||
target: IAnimationChannelTarget;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Animation clip from GLTF
|
|
||||||
* GLTF 动画片段
|
|
||||||
*/
|
|
||||||
export interface IGLTFAnimationClip {
|
|
||||||
/** 动画名称 | Animation name */
|
|
||||||
name: string;
|
|
||||||
/** 动画时长(秒)| Duration in seconds */
|
|
||||||
duration: number;
|
|
||||||
/** 采样器列表 | Sampler list */
|
|
||||||
samplers: IAnimationSampler[];
|
|
||||||
/** 通道列表 | Channel list */
|
|
||||||
channels: IAnimationChannel[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Skeleton joint
|
|
||||||
* 骨骼关节
|
|
||||||
*/
|
|
||||||
export interface ISkeletonJoint {
|
|
||||||
/** 关节名称 | Joint name */
|
|
||||||
name: string;
|
|
||||||
/** 节点索引 | Node index */
|
|
||||||
nodeIndex: number;
|
|
||||||
/** 父关节索引(-1 表示根)| Parent joint index (-1 for root) */
|
|
||||||
parentIndex: number;
|
|
||||||
/** 逆绑定矩阵 (4x4) | Inverse bind matrix */
|
|
||||||
inverseBindMatrix: Float32Array;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Skeleton data
|
|
||||||
* 骨骼数据
|
|
||||||
*/
|
|
||||||
export interface ISkeletonData {
|
|
||||||
/** 关节列表 | Joint list */
|
|
||||||
joints: ISkeletonJoint[];
|
|
||||||
/** 根关节索引 | Root joint index */
|
|
||||||
rootJointIndex: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GLTF/GLB 3D model asset interface
|
|
||||||
* GLTF/GLB 3D 模型资产接口
|
|
||||||
*/
|
|
||||||
export interface IGLTFAsset {
|
|
||||||
/** 模型名称 | Model name */
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
/** 网格数据列表 | Mesh data list */
|
|
||||||
meshes: IMeshData[];
|
|
||||||
|
|
||||||
/** 材质列表 | Material list */
|
|
||||||
materials: IGLTFMaterial[];
|
|
||||||
|
|
||||||
/** 纹理信息列表 | Texture info list */
|
|
||||||
textures: IGLTFTextureInfo[];
|
|
||||||
|
|
||||||
/** 场景层级节点 | Scene hierarchy nodes */
|
|
||||||
nodes: IGLTFNode[];
|
|
||||||
|
|
||||||
/** 根节点索引列表 | Root node indices */
|
|
||||||
rootNodes: number[];
|
|
||||||
|
|
||||||
/** 动画片段列表(可选)| Animation clips (optional) */
|
|
||||||
animations?: IGLTFAnimationClip[];
|
|
||||||
|
|
||||||
/** 骨骼数据(可选)| Skeleton data (optional) */
|
|
||||||
skeleton?: ISkeletonData;
|
|
||||||
|
|
||||||
/** 整体边界盒 | Overall bounding box */
|
|
||||||
bounds: IBoundingBox;
|
|
||||||
|
|
||||||
/** 源文件路径 | Source file path */
|
|
||||||
sourcePath?: string;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -11,24 +11,14 @@ import { TextLoader } from './TextLoader';
|
|||||||
import { BinaryLoader } from './BinaryLoader';
|
import { BinaryLoader } from './BinaryLoader';
|
||||||
import { AudioLoader } from './AudioLoader';
|
import { AudioLoader } from './AudioLoader';
|
||||||
import { PrefabLoader } from './PrefabLoader';
|
import { PrefabLoader } from './PrefabLoader';
|
||||||
import { GLTFLoader } from './GLTFLoader';
|
|
||||||
import { OBJLoader } from './OBJLoader';
|
|
||||||
import { FBXLoader } from './FBXLoader';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asset loader factory
|
* Asset loader factory
|
||||||
* 资产加载器工厂
|
* 资产加载器工厂
|
||||||
*
|
|
||||||
* Supports multiple loaders per asset type (selected by file extension).
|
|
||||||
* 支持每种资产类型的多个加载器(按文件扩展名选择)。
|
|
||||||
*/
|
*/
|
||||||
export class AssetLoaderFactory implements IAssetLoaderFactory {
|
export class AssetLoaderFactory implements IAssetLoaderFactory {
|
||||||
private readonly _loaders = new Map<AssetType, IAssetLoader>();
|
private readonly _loaders = new Map<AssetType, IAssetLoader>();
|
||||||
|
|
||||||
/** Extension -> Loader map for precise loader selection */
|
|
||||||
/** 扩展名 -> 加载器映射,用于精确选择加载器 */
|
|
||||||
private readonly _extensionLoaders = new Map<string, IAssetLoader>();
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// 注册默认加载器 / Register default loaders
|
// 注册默认加载器 / Register default loaders
|
||||||
this.registerDefaultLoaders();
|
this.registerDefaultLoaders();
|
||||||
@@ -56,34 +46,6 @@ export class AssetLoaderFactory implements IAssetLoaderFactory {
|
|||||||
|
|
||||||
// 预制体加载器 / Prefab loader
|
// 预制体加载器 / Prefab loader
|
||||||
this._loaders.set(AssetType.Prefab, new PrefabLoader());
|
this._loaders.set(AssetType.Prefab, new PrefabLoader());
|
||||||
|
|
||||||
// 3D模型加载器 / 3D Model loaders
|
|
||||||
// Default is GLTF, but OBJ and FBX are also supported
|
|
||||||
// 默认是 GLTF,但也支持 OBJ 和 FBX
|
|
||||||
const gltfLoader = new GLTFLoader();
|
|
||||||
const objLoader = new OBJLoader();
|
|
||||||
const fbxLoader = new FBXLoader();
|
|
||||||
|
|
||||||
this._loaders.set(AssetType.Model3D, gltfLoader);
|
|
||||||
|
|
||||||
// Register extension-specific loaders
|
|
||||||
// 注册特定扩展名的加载器
|
|
||||||
this.registerExtensionLoader('.gltf', gltfLoader);
|
|
||||||
this.registerExtensionLoader('.glb', gltfLoader);
|
|
||||||
this.registerExtensionLoader('.obj', objLoader);
|
|
||||||
this.registerExtensionLoader('.fbx', fbxLoader);
|
|
||||||
|
|
||||||
// 注:Shader 和 Material 加载器由 material-system 模块注册
|
|
||||||
// Note: Shader and Material loaders are registered by material-system module
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a loader for a specific file extension
|
|
||||||
* 为特定文件扩展名注册加载器
|
|
||||||
*/
|
|
||||||
registerExtensionLoader(extension: string, loader: IAssetLoader): void {
|
|
||||||
const ext = extension.toLowerCase().startsWith('.') ? extension.toLowerCase() : `.${extension.toLowerCase()}`;
|
|
||||||
this._extensionLoaders.set(ext, loader);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -94,38 +56,6 @@ export class AssetLoaderFactory implements IAssetLoaderFactory {
|
|||||||
return this._loaders.get(type) || null;
|
return this._loaders.get(type) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Create loader for a specific file path (selects by extension)
|
|
||||||
* 为特定文件路径创建加载器(按扩展名选择)
|
|
||||||
*
|
|
||||||
* This method is preferred over createLoader() when multiple loaders
|
|
||||||
* support the same asset type (e.g., Model3D with GLTF/OBJ/FBX).
|
|
||||||
* 当多个加载器支持相同资产类型时(如 Model3D 的 GLTF/OBJ/FBX),
|
|
||||||
* 优先使用此方法而非 createLoader()。
|
|
||||||
*/
|
|
||||||
createLoaderForPath(path: string): IAssetLoader | null {
|
|
||||||
const lastDot = path.lastIndexOf('.');
|
|
||||||
if (lastDot !== -1) {
|
|
||||||
const ext = path.substring(lastDot).toLowerCase();
|
|
||||||
|
|
||||||
// First try extension-specific loader
|
|
||||||
// 首先尝试特定扩展名的加载器
|
|
||||||
const extLoader = this._extensionLoaders.get(ext);
|
|
||||||
if (extLoader) {
|
|
||||||
return extLoader;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall back to type-based lookup
|
|
||||||
// 回退到基于类型的查找
|
|
||||||
const type = this.getAssetTypeByPath(path);
|
|
||||||
if (type) {
|
|
||||||
return this.createLoader(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register custom loader
|
* Register custom loader
|
||||||
* 注册自定义加载器
|
* 注册自定义加载器
|
||||||
@@ -159,16 +89,6 @@ export class AssetLoaderFactory implements IAssetLoaderFactory {
|
|||||||
*/
|
*/
|
||||||
getAssetTypeByExtension(extension: string): AssetType | null {
|
getAssetTypeByExtension(extension: string): AssetType | null {
|
||||||
const ext = extension.toLowerCase();
|
const ext = extension.toLowerCase();
|
||||||
|
|
||||||
// Check extension-specific loaders first
|
|
||||||
// 首先检查特定扩展名的加载器
|
|
||||||
const extLoader = this._extensionLoaders.get(ext);
|
|
||||||
if (extLoader) {
|
|
||||||
return extLoader.supportedType;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall back to type-based loaders
|
|
||||||
// 回退到基于类型的加载器
|
|
||||||
for (const [type, loader] of this._loaders) {
|
for (const [type, loader] of this._loaders) {
|
||||||
if (loader.supportedExtensions.some(e => e.toLowerCase() === ext)) {
|
if (loader.supportedExtensions.some(e => e.toLowerCase() === ext)) {
|
||||||
return type;
|
return type;
|
||||||
@@ -236,22 +156,14 @@ export class AssetLoaderFactory implements IAssetLoaderFactory {
|
|||||||
getAllSupportedExtensions(): string[] {
|
getAllSupportedExtensions(): string[] {
|
||||||
const extensions = new Set<string>();
|
const extensions = new Set<string>();
|
||||||
|
|
||||||
// From type-based loaders
|
|
||||||
// 从基于类型的加载器
|
|
||||||
for (const loader of this._loaders.values()) {
|
for (const loader of this._loaders.values()) {
|
||||||
for (const ext of loader.supportedExtensions) {
|
for (const ext of loader.supportedExtensions) {
|
||||||
|
// 转换为 glob 模式 | Convert to glob pattern
|
||||||
const cleanExt = ext.startsWith('.') ? ext.substring(1) : ext;
|
const cleanExt = ext.startsWith('.') ? ext.substring(1) : ext;
|
||||||
extensions.add(`*.${cleanExt}`);
|
extensions.add(`*.${cleanExt}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// From extension-specific loaders
|
|
||||||
// 从特定扩展名的加载器
|
|
||||||
for (const ext of this._extensionLoaders.keys()) {
|
|
||||||
const cleanExt = ext.startsWith('.') ? ext.substring(1) : ext;
|
|
||||||
extensions.add(`*.${cleanExt}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Array.from(extensions);
|
return Array.from(extensions);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,8 +176,6 @@ export class AssetLoaderFactory implements IAssetLoaderFactory {
|
|||||||
getExtensionTypeMap(): Record<string, string> {
|
getExtensionTypeMap(): Record<string, string> {
|
||||||
const map: Record<string, string> = {};
|
const map: Record<string, string> = {};
|
||||||
|
|
||||||
// From type-based loaders
|
|
||||||
// 从基于类型的加载器
|
|
||||||
for (const [type, loader] of this._loaders) {
|
for (const [type, loader] of this._loaders) {
|
||||||
for (const ext of loader.supportedExtensions) {
|
for (const ext of loader.supportedExtensions) {
|
||||||
const cleanExt = ext.startsWith('.') ? ext.substring(1) : ext;
|
const cleanExt = ext.startsWith('.') ? ext.substring(1) : ext;
|
||||||
@@ -273,13 +183,6 @@ export class AssetLoaderFactory implements IAssetLoaderFactory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// From extension-specific loaders
|
|
||||||
// 从特定扩展名的加载器
|
|
||||||
for (const [ext, loader] of this._extensionLoaders) {
|
|
||||||
const cleanExt = ext.startsWith('.') ? ext.substring(1) : ext;
|
|
||||||
map[cleanExt.toLowerCase()] = loader.supportedType;
|
|
||||||
}
|
|
||||||
|
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,994 +0,0 @@
|
|||||||
/**
|
|
||||||
* GLTF/GLB model loader implementation
|
|
||||||
* GLTF/GLB 模型加载器实现
|
|
||||||
*
|
|
||||||
* Supports:
|
|
||||||
* - GLTF 2.0 (.gltf with external/embedded resources)
|
|
||||||
* - GLB (.glb binary format)
|
|
||||||
* - PBR materials
|
|
||||||
* - Scene hierarchy
|
|
||||||
* - Animations (basic)
|
|
||||||
* - Skinning (basic)
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { AssetType } from '../types/AssetTypes';
|
|
||||||
import type { IAssetContent, AssetContentType } from '../interfaces/IAssetReader';
|
|
||||||
import type {
|
|
||||||
IAssetLoader,
|
|
||||||
IAssetParseContext,
|
|
||||||
IGLTFAsset,
|
|
||||||
IMeshData,
|
|
||||||
IGLTFMaterial,
|
|
||||||
IGLTFTextureInfo,
|
|
||||||
IGLTFNode,
|
|
||||||
IGLTFAnimationClip,
|
|
||||||
IAnimationSampler,
|
|
||||||
IAnimationChannel,
|
|
||||||
IBoundingBox,
|
|
||||||
ISkeletonData,
|
|
||||||
ISkeletonJoint
|
|
||||||
} from '../interfaces/IAssetLoader';
|
|
||||||
|
|
||||||
// ===== GLTF JSON Schema Types =====
|
|
||||||
|
|
||||||
interface GLTFJson {
|
|
||||||
asset: { version: string; generator?: string };
|
|
||||||
scene?: number;
|
|
||||||
scenes?: GLTFScene[];
|
|
||||||
nodes?: GLTFNodeDef[];
|
|
||||||
meshes?: GLTFMeshDef[];
|
|
||||||
accessors?: GLTFAccessor[];
|
|
||||||
bufferViews?: GLTFBufferView[];
|
|
||||||
buffers?: GLTFBuffer[];
|
|
||||||
materials?: GLTFMaterialDef[];
|
|
||||||
textures?: GLTFTextureDef[];
|
|
||||||
images?: GLTFImage[];
|
|
||||||
samplers?: GLTFSampler[];
|
|
||||||
animations?: GLTFAnimation[];
|
|
||||||
skins?: GLTFSkin[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GLTFScene {
|
|
||||||
name?: string;
|
|
||||||
nodes?: number[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GLTFNodeDef {
|
|
||||||
name?: string;
|
|
||||||
mesh?: number;
|
|
||||||
children?: number[];
|
|
||||||
translation?: [number, number, number];
|
|
||||||
rotation?: [number, number, number, number];
|
|
||||||
scale?: [number, number, number];
|
|
||||||
matrix?: number[];
|
|
||||||
skin?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GLTFMeshDef {
|
|
||||||
name?: string;
|
|
||||||
primitives: GLTFPrimitive[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GLTFPrimitive {
|
|
||||||
attributes: Record<string, number>;
|
|
||||||
indices?: number;
|
|
||||||
material?: number;
|
|
||||||
mode?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GLTFAccessor {
|
|
||||||
bufferView?: number;
|
|
||||||
byteOffset?: number;
|
|
||||||
componentType: number;
|
|
||||||
count: number;
|
|
||||||
type: string;
|
|
||||||
min?: number[];
|
|
||||||
max?: number[];
|
|
||||||
normalized?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GLTFBufferView {
|
|
||||||
buffer: number;
|
|
||||||
byteOffset?: number;
|
|
||||||
byteLength: number;
|
|
||||||
byteStride?: number;
|
|
||||||
target?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GLTFBuffer {
|
|
||||||
uri?: string;
|
|
||||||
byteLength: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GLTFMaterialDef {
|
|
||||||
name?: string;
|
|
||||||
pbrMetallicRoughness?: {
|
|
||||||
baseColorFactor?: [number, number, number, number];
|
|
||||||
baseColorTexture?: { index: number };
|
|
||||||
metallicFactor?: number;
|
|
||||||
roughnessFactor?: number;
|
|
||||||
metallicRoughnessTexture?: { index: number };
|
|
||||||
};
|
|
||||||
normalTexture?: { index: number; scale?: number };
|
|
||||||
occlusionTexture?: { index: number; strength?: number };
|
|
||||||
emissiveFactor?: [number, number, number];
|
|
||||||
emissiveTexture?: { index: number };
|
|
||||||
alphaMode?: 'OPAQUE' | 'MASK' | 'BLEND';
|
|
||||||
alphaCutoff?: number;
|
|
||||||
doubleSided?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GLTFTextureDef {
|
|
||||||
source?: number;
|
|
||||||
sampler?: number;
|
|
||||||
name?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GLTFImage {
|
|
||||||
uri?: string;
|
|
||||||
mimeType?: string;
|
|
||||||
bufferView?: number;
|
|
||||||
name?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GLTFSampler {
|
|
||||||
magFilter?: number;
|
|
||||||
minFilter?: number;
|
|
||||||
wrapS?: number;
|
|
||||||
wrapT?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GLTFAnimation {
|
|
||||||
name?: string;
|
|
||||||
channels: GLTFAnimationChannel[];
|
|
||||||
samplers: GLTFAnimationSampler[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GLTFAnimationChannel {
|
|
||||||
sampler: number;
|
|
||||||
target: {
|
|
||||||
node?: number;
|
|
||||||
path: 'translation' | 'rotation' | 'scale' | 'weights';
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GLTFAnimationSampler {
|
|
||||||
input: number;
|
|
||||||
output: number;
|
|
||||||
interpolation?: 'LINEAR' | 'STEP' | 'CUBICSPLINE';
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GLTFSkin {
|
|
||||||
name?: string;
|
|
||||||
inverseBindMatrices?: number;
|
|
||||||
skeleton?: number;
|
|
||||||
joints: number[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Component Type Constants =====
|
|
||||||
const COMPONENT_TYPE_BYTE = 5120;
|
|
||||||
const COMPONENT_TYPE_UNSIGNED_BYTE = 5121;
|
|
||||||
const COMPONENT_TYPE_SHORT = 5122;
|
|
||||||
const COMPONENT_TYPE_UNSIGNED_SHORT = 5123;
|
|
||||||
const COMPONENT_TYPE_UNSIGNED_INT = 5125;
|
|
||||||
const COMPONENT_TYPE_FLOAT = 5126;
|
|
||||||
|
|
||||||
// ===== GLB Constants =====
|
|
||||||
const GLB_MAGIC = 0x46546C67; // 'glTF'
|
|
||||||
const GLB_VERSION = 2;
|
|
||||||
const GLB_CHUNK_TYPE_JSON = 0x4E4F534A; // 'JSON'
|
|
||||||
const GLB_CHUNK_TYPE_BIN = 0x004E4942; // 'BIN\0'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GLTF/GLB model loader
|
|
||||||
* GLTF/GLB 模型加载器
|
|
||||||
*/
|
|
||||||
export class GLTFLoader implements IAssetLoader<IGLTFAsset> {
|
|
||||||
readonly supportedType = AssetType.Model3D;
|
|
||||||
readonly supportedExtensions = ['.gltf', '.glb'];
|
|
||||||
readonly contentType: AssetContentType = 'binary';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse GLTF/GLB content
|
|
||||||
* 解析 GLTF/GLB 内容
|
|
||||||
*/
|
|
||||||
async parse(content: IAssetContent, context: IAssetParseContext): Promise<IGLTFAsset> {
|
|
||||||
const binary = content.binary;
|
|
||||||
if (!binary) {
|
|
||||||
throw new Error('GLTF loader requires binary content');
|
|
||||||
}
|
|
||||||
|
|
||||||
const isGLB = this.isGLB(binary);
|
|
||||||
let json: GLTFJson;
|
|
||||||
let binaryChunk: ArrayBuffer | null = null;
|
|
||||||
|
|
||||||
if (isGLB) {
|
|
||||||
const glbData = this.parseGLB(binary);
|
|
||||||
json = glbData.json;
|
|
||||||
binaryChunk = glbData.binary;
|
|
||||||
} else {
|
|
||||||
// GLTF is JSON text
|
|
||||||
const decoder = new TextDecoder('utf-8');
|
|
||||||
const text = decoder.decode(binary);
|
|
||||||
json = JSON.parse(text) as GLTFJson;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate GLTF version
|
|
||||||
if (!json.asset?.version?.startsWith('2.')) {
|
|
||||||
throw new Error(`Unsupported GLTF version: ${json.asset?.version}. Only GLTF 2.x is supported.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load external buffers if needed
|
|
||||||
const buffers = await this.loadBuffers(json, binaryChunk, context);
|
|
||||||
|
|
||||||
// Parse all components
|
|
||||||
const meshes = this.parseMeshes(json, buffers);
|
|
||||||
const materials = this.parseMaterials(json);
|
|
||||||
const textures = await this.parseTextures(json, buffers, context);
|
|
||||||
const nodes = this.parseNodes(json);
|
|
||||||
const rootNodes = this.getRootNodes(json);
|
|
||||||
const animations = this.parseAnimations(json, buffers);
|
|
||||||
const skeleton = this.parseSkeleton(json, buffers);
|
|
||||||
const bounds = this.calculateBounds(meshes);
|
|
||||||
|
|
||||||
// Get model name from file path
|
|
||||||
const pathParts = context.metadata.path.split(/[\\/]/);
|
|
||||||
const fileName = pathParts[pathParts.length - 1];
|
|
||||||
const name = fileName.replace(/\.(gltf|glb)$/i, '');
|
|
||||||
|
|
||||||
return {
|
|
||||||
name,
|
|
||||||
meshes,
|
|
||||||
materials,
|
|
||||||
textures,
|
|
||||||
nodes,
|
|
||||||
rootNodes,
|
|
||||||
animations: animations.length > 0 ? animations : undefined,
|
|
||||||
skeleton,
|
|
||||||
bounds,
|
|
||||||
sourcePath: context.metadata.path
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dispose GLTF asset
|
|
||||||
* 释放 GLTF 资产
|
|
||||||
*/
|
|
||||||
dispose(asset: IGLTFAsset): void {
|
|
||||||
// Clear mesh data
|
|
||||||
for (const mesh of asset.meshes) {
|
|
||||||
(mesh as { vertices: Float32Array | null }).vertices = null!;
|
|
||||||
(mesh as { indices: Uint16Array | Uint32Array | null }).indices = null!;
|
|
||||||
if (mesh.normals) (mesh as { normals: Float32Array | null }).normals = null;
|
|
||||||
if (mesh.uvs) (mesh as { uvs: Float32Array | null }).uvs = null;
|
|
||||||
if (mesh.colors) (mesh as { colors: Float32Array | null }).colors = null;
|
|
||||||
}
|
|
||||||
asset.meshes.length = 0;
|
|
||||||
asset.materials.length = 0;
|
|
||||||
asset.textures.length = 0;
|
|
||||||
asset.nodes.length = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Private Methods =====
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if content is GLB format
|
|
||||||
*/
|
|
||||||
private isGLB(data: ArrayBuffer): boolean {
|
|
||||||
if (data.byteLength < 12) return false;
|
|
||||||
const view = new DataView(data);
|
|
||||||
return view.getUint32(0, true) === GLB_MAGIC;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse GLB binary format
|
|
||||||
*/
|
|
||||||
private parseGLB(data: ArrayBuffer): { json: GLTFJson; binary: ArrayBuffer | null } {
|
|
||||||
const view = new DataView(data);
|
|
||||||
|
|
||||||
// Header
|
|
||||||
const magic = view.getUint32(0, true);
|
|
||||||
const version = view.getUint32(4, true);
|
|
||||||
const length = view.getUint32(8, true);
|
|
||||||
|
|
||||||
if (magic !== GLB_MAGIC) {
|
|
||||||
throw new Error('Invalid GLB magic number');
|
|
||||||
}
|
|
||||||
if (version !== GLB_VERSION) {
|
|
||||||
throw new Error(`Unsupported GLB version: ${version}`);
|
|
||||||
}
|
|
||||||
if (length !== data.byteLength) {
|
|
||||||
throw new Error('GLB length mismatch');
|
|
||||||
}
|
|
||||||
|
|
||||||
let json: GLTFJson | null = null;
|
|
||||||
let binary: ArrayBuffer | null = null;
|
|
||||||
let offset = 12;
|
|
||||||
|
|
||||||
// Parse chunks
|
|
||||||
while (offset < length) {
|
|
||||||
const chunkLength = view.getUint32(offset, true);
|
|
||||||
const chunkType = view.getUint32(offset + 4, true);
|
|
||||||
const chunkData = data.slice(offset + 8, offset + 8 + chunkLength);
|
|
||||||
|
|
||||||
if (chunkType === GLB_CHUNK_TYPE_JSON) {
|
|
||||||
const decoder = new TextDecoder('utf-8');
|
|
||||||
json = JSON.parse(decoder.decode(chunkData)) as GLTFJson;
|
|
||||||
} else if (chunkType === GLB_CHUNK_TYPE_BIN) {
|
|
||||||
binary = chunkData;
|
|
||||||
}
|
|
||||||
|
|
||||||
offset += 8 + chunkLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!json) {
|
|
||||||
throw new Error('GLB missing JSON chunk');
|
|
||||||
}
|
|
||||||
|
|
||||||
return { json, binary };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load buffer data
|
|
||||||
*/
|
|
||||||
private async loadBuffers(
|
|
||||||
json: GLTFJson,
|
|
||||||
binaryChunk: ArrayBuffer | null,
|
|
||||||
_context: IAssetParseContext
|
|
||||||
): Promise<ArrayBuffer[]> {
|
|
||||||
const buffers: ArrayBuffer[] = [];
|
|
||||||
|
|
||||||
if (!json.buffers) return buffers;
|
|
||||||
|
|
||||||
for (let i = 0; i < json.buffers.length; i++) {
|
|
||||||
const bufferDef = json.buffers[i];
|
|
||||||
|
|
||||||
if (!bufferDef.uri) {
|
|
||||||
// GLB embedded binary chunk
|
|
||||||
if (binaryChunk && i === 0) {
|
|
||||||
buffers.push(binaryChunk);
|
|
||||||
} else {
|
|
||||||
throw new Error(`Buffer ${i} has no URI and no binary chunk available`);
|
|
||||||
}
|
|
||||||
} else if (bufferDef.uri.startsWith('data:')) {
|
|
||||||
// Data URI
|
|
||||||
buffers.push(this.decodeDataUri(bufferDef.uri));
|
|
||||||
} else {
|
|
||||||
// External file - not supported yet, would need asset loader context
|
|
||||||
throw new Error(`External buffer URIs not supported yet: ${bufferDef.uri}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return buffers;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decode base64 data URI
|
|
||||||
*/
|
|
||||||
private decodeDataUri(uri: string): ArrayBuffer {
|
|
||||||
const match = uri.match(/^data:[^;]*;base64,(.*)$/);
|
|
||||||
if (!match) {
|
|
||||||
throw new Error('Invalid data URI format');
|
|
||||||
}
|
|
||||||
|
|
||||||
const base64 = match[1];
|
|
||||||
const binaryString = atob(base64);
|
|
||||||
const bytes = new Uint8Array(binaryString.length);
|
|
||||||
for (let i = 0; i < binaryString.length; i++) {
|
|
||||||
bytes[i] = binaryString.charCodeAt(i);
|
|
||||||
}
|
|
||||||
return bytes.buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get accessor data as typed array
|
|
||||||
*/
|
|
||||||
private getAccessorData(
|
|
||||||
json: GLTFJson,
|
|
||||||
buffers: ArrayBuffer[],
|
|
||||||
accessorIndex: number
|
|
||||||
): { data: ArrayBufferView; count: number; componentCount: number } {
|
|
||||||
const accessor = json.accessors![accessorIndex];
|
|
||||||
const bufferView = json.bufferViews![accessor.bufferView!];
|
|
||||||
const buffer = buffers[bufferView.buffer];
|
|
||||||
|
|
||||||
const byteOffset = (bufferView.byteOffset || 0) + (accessor.byteOffset || 0);
|
|
||||||
const componentCount = this.getComponentCount(accessor.type);
|
|
||||||
const elementCount = accessor.count * componentCount;
|
|
||||||
|
|
||||||
let data: ArrayBufferView;
|
|
||||||
|
|
||||||
switch (accessor.componentType) {
|
|
||||||
case COMPONENT_TYPE_BYTE:
|
|
||||||
data = new Int8Array(buffer, byteOffset, elementCount);
|
|
||||||
break;
|
|
||||||
case COMPONENT_TYPE_UNSIGNED_BYTE:
|
|
||||||
data = new Uint8Array(buffer, byteOffset, elementCount);
|
|
||||||
break;
|
|
||||||
case COMPONENT_TYPE_SHORT:
|
|
||||||
data = new Int16Array(buffer, byteOffset, elementCount);
|
|
||||||
break;
|
|
||||||
case COMPONENT_TYPE_UNSIGNED_SHORT:
|
|
||||||
data = new Uint16Array(buffer, byteOffset, elementCount);
|
|
||||||
break;
|
|
||||||
case COMPONENT_TYPE_UNSIGNED_INT:
|
|
||||||
data = new Uint32Array(buffer, byteOffset, elementCount);
|
|
||||||
break;
|
|
||||||
case COMPONENT_TYPE_FLOAT:
|
|
||||||
data = new Float32Array(buffer, byteOffset, elementCount);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Error(`Unsupported component type: ${accessor.componentType}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { data, count: accessor.count, componentCount };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get component count from accessor type
|
|
||||||
*/
|
|
||||||
private getComponentCount(type: string): number {
|
|
||||||
switch (type) {
|
|
||||||
case 'SCALAR': return 1;
|
|
||||||
case 'VEC2': return 2;
|
|
||||||
case 'VEC3': return 3;
|
|
||||||
case 'VEC4': return 4;
|
|
||||||
case 'MAT2': return 4;
|
|
||||||
case 'MAT3': return 9;
|
|
||||||
case 'MAT4': return 16;
|
|
||||||
default:
|
|
||||||
throw new Error(`Unknown accessor type: ${type}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse all meshes
|
|
||||||
*/
|
|
||||||
private parseMeshes(json: GLTFJson, buffers: ArrayBuffer[]): IMeshData[] {
|
|
||||||
const meshes: IMeshData[] = [];
|
|
||||||
|
|
||||||
if (!json.meshes) return meshes;
|
|
||||||
|
|
||||||
for (const meshDef of json.meshes) {
|
|
||||||
for (const primitive of meshDef.primitives) {
|
|
||||||
// Only support triangles (mode 4 or undefined)
|
|
||||||
if (primitive.mode !== undefined && primitive.mode !== 4) {
|
|
||||||
console.warn('Skipping non-triangle primitive');
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mesh = this.parsePrimitive(json, buffers, primitive, meshDef.name || 'Mesh');
|
|
||||||
meshes.push(mesh);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return meshes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse a single primitive
|
|
||||||
*/
|
|
||||||
private parsePrimitive(
|
|
||||||
json: GLTFJson,
|
|
||||||
buffers: ArrayBuffer[],
|
|
||||||
primitive: GLTFPrimitive,
|
|
||||||
name: string
|
|
||||||
): IMeshData {
|
|
||||||
// Position (required)
|
|
||||||
const positionAccessor = primitive.attributes['POSITION'];
|
|
||||||
if (positionAccessor === undefined) {
|
|
||||||
throw new Error('Mesh primitive missing POSITION attribute');
|
|
||||||
}
|
|
||||||
const positionData = this.getAccessorData(json, buffers, positionAccessor);
|
|
||||||
const vertices = new Float32Array(positionData.data.buffer, (positionData.data as Float32Array).byteOffset, positionData.count * 3);
|
|
||||||
|
|
||||||
// Indices (optional, generate sequential if missing)
|
|
||||||
let indices: Uint16Array | Uint32Array;
|
|
||||||
if (primitive.indices !== undefined) {
|
|
||||||
const indexData = this.getAccessorData(json, buffers, primitive.indices);
|
|
||||||
if (indexData.data instanceof Uint32Array) {
|
|
||||||
indices = indexData.data;
|
|
||||||
} else if (indexData.data instanceof Uint16Array) {
|
|
||||||
indices = indexData.data;
|
|
||||||
} else {
|
|
||||||
// Convert to Uint32Array
|
|
||||||
indices = new Uint32Array(indexData.count);
|
|
||||||
for (let i = 0; i < indexData.count; i++) {
|
|
||||||
indices[i] = (indexData.data as Uint8Array)[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Generate sequential indices
|
|
||||||
indices = new Uint32Array(positionData.count);
|
|
||||||
for (let i = 0; i < positionData.count; i++) {
|
|
||||||
indices[i] = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normals (optional)
|
|
||||||
let normals: Float32Array | undefined;
|
|
||||||
const normalAccessor = primitive.attributes['NORMAL'];
|
|
||||||
if (normalAccessor !== undefined) {
|
|
||||||
const normalData = this.getAccessorData(json, buffers, normalAccessor);
|
|
||||||
normals = new Float32Array(normalData.data.buffer, (normalData.data as Float32Array).byteOffset, normalData.count * 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
// UVs (optional, TEXCOORD_0)
|
|
||||||
let uvs: Float32Array | undefined;
|
|
||||||
const uvAccessor = primitive.attributes['TEXCOORD_0'];
|
|
||||||
if (uvAccessor !== undefined) {
|
|
||||||
const uvData = this.getAccessorData(json, buffers, uvAccessor);
|
|
||||||
uvs = new Float32Array(uvData.data.buffer, (uvData.data as Float32Array).byteOffset, uvData.count * 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vertex colors (optional, COLOR_0)
|
|
||||||
let colors: Float32Array | undefined;
|
|
||||||
const colorAccessor = primitive.attributes['COLOR_0'];
|
|
||||||
if (colorAccessor !== undefined) {
|
|
||||||
const colorData = this.getAccessorData(json, buffers, colorAccessor);
|
|
||||||
// Normalize if needed
|
|
||||||
if (colorData.data instanceof Float32Array) {
|
|
||||||
colors = colorData.data;
|
|
||||||
} else {
|
|
||||||
// Convert from normalized bytes
|
|
||||||
colors = new Float32Array(colorData.count * colorData.componentCount);
|
|
||||||
const source = colorData.data as Uint8Array;
|
|
||||||
for (let i = 0; i < source.length; i++) {
|
|
||||||
colors[i] = source[i] / 255;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tangents (optional)
|
|
||||||
let tangents: Float32Array | undefined;
|
|
||||||
const tangentAccessor = primitive.attributes['TANGENT'];
|
|
||||||
if (tangentAccessor !== undefined) {
|
|
||||||
const tangentData = this.getAccessorData(json, buffers, tangentAccessor);
|
|
||||||
tangents = new Float32Array(tangentData.data.buffer, (tangentData.data as Float32Array).byteOffset, tangentData.count * 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skinning: JOINTS_0 (bone indices per vertex)
|
|
||||||
// 蒙皮:JOINTS_0(每顶点的骨骼索引)
|
|
||||||
let joints: Uint8Array | Uint16Array | undefined;
|
|
||||||
const jointsAccessor = primitive.attributes['JOINTS_0'];
|
|
||||||
if (jointsAccessor !== undefined) {
|
|
||||||
const jointsData = this.getAccessorData(json, buffers, jointsAccessor);
|
|
||||||
if (jointsData.data instanceof Uint8Array) {
|
|
||||||
joints = new Uint8Array(jointsData.data.buffer, jointsData.data.byteOffset, jointsData.count * 4);
|
|
||||||
} else if (jointsData.data instanceof Uint16Array) {
|
|
||||||
joints = new Uint16Array(jointsData.data.buffer, jointsData.data.byteOffset, jointsData.count * 4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skinning: WEIGHTS_0 (bone weights per vertex)
|
|
||||||
// 蒙皮:WEIGHTS_0(每顶点的骨骼权重)
|
|
||||||
let weights: Float32Array | undefined;
|
|
||||||
const weightsAccessor = primitive.attributes['WEIGHTS_0'];
|
|
||||||
if (weightsAccessor !== undefined) {
|
|
||||||
const weightsData = this.getAccessorData(json, buffers, weightsAccessor);
|
|
||||||
if (weightsData.data instanceof Float32Array) {
|
|
||||||
weights = new Float32Array(weightsData.data.buffer, weightsData.data.byteOffset, weightsData.count * 4);
|
|
||||||
} else if (weightsData.data instanceof Uint8Array) {
|
|
||||||
// Convert from normalized Uint8 to floats
|
|
||||||
weights = new Float32Array(weightsData.count * 4);
|
|
||||||
const source = weightsData.data;
|
|
||||||
for (let i = 0; i < source.length; i++) {
|
|
||||||
weights[i] = source[i] / 255;
|
|
||||||
}
|
|
||||||
} else if (weightsData.data instanceof Uint16Array) {
|
|
||||||
// Convert from normalized Uint16 to floats
|
|
||||||
weights = new Float32Array(weightsData.count * 4);
|
|
||||||
const source = weightsData.data;
|
|
||||||
for (let i = 0; i < source.length; i++) {
|
|
||||||
weights[i] = source[i] / 65535;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate bounds
|
|
||||||
const bounds = this.calculateMeshBounds(vertices);
|
|
||||||
|
|
||||||
return {
|
|
||||||
name,
|
|
||||||
vertices,
|
|
||||||
indices,
|
|
||||||
normals,
|
|
||||||
uvs,
|
|
||||||
tangents,
|
|
||||||
colors,
|
|
||||||
joints,
|
|
||||||
weights,
|
|
||||||
bounds,
|
|
||||||
materialIndex: primitive.material ?? -1
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate mesh bounding box
|
|
||||||
*/
|
|
||||||
private calculateMeshBounds(vertices: Float32Array): IBoundingBox {
|
|
||||||
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
|
||||||
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
|
||||||
|
|
||||||
for (let i = 0; i < vertices.length; i += 3) {
|
|
||||||
const x = vertices[i];
|
|
||||||
const y = vertices[i + 1];
|
|
||||||
const z = vertices[i + 2];
|
|
||||||
|
|
||||||
minX = Math.min(minX, x);
|
|
||||||
minY = Math.min(minY, y);
|
|
||||||
minZ = Math.min(minZ, z);
|
|
||||||
maxX = Math.max(maxX, x);
|
|
||||||
maxY = Math.max(maxY, y);
|
|
||||||
maxZ = Math.max(maxZ, z);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
min: [minX, minY, minZ],
|
|
||||||
max: [maxX, maxY, maxZ]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse all materials
|
|
||||||
*/
|
|
||||||
private parseMaterials(json: GLTFJson): IGLTFMaterial[] {
|
|
||||||
const materials: IGLTFMaterial[] = [];
|
|
||||||
|
|
||||||
if (!json.materials) {
|
|
||||||
// Add default material
|
|
||||||
materials.push(this.createDefaultMaterial());
|
|
||||||
return materials;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const matDef of json.materials) {
|
|
||||||
const pbr = matDef.pbrMetallicRoughness || {};
|
|
||||||
|
|
||||||
materials.push({
|
|
||||||
name: matDef.name || 'Material',
|
|
||||||
baseColorFactor: pbr.baseColorFactor || [1, 1, 1, 1],
|
|
||||||
baseColorTextureIndex: pbr.baseColorTexture?.index ?? -1,
|
|
||||||
metallicFactor: pbr.metallicFactor ?? 1,
|
|
||||||
roughnessFactor: pbr.roughnessFactor ?? 1,
|
|
||||||
metallicRoughnessTextureIndex: pbr.metallicRoughnessTexture?.index ?? -1,
|
|
||||||
normalTextureIndex: matDef.normalTexture?.index ?? -1,
|
|
||||||
normalScale: matDef.normalTexture?.scale ?? 1,
|
|
||||||
occlusionTextureIndex: matDef.occlusionTexture?.index ?? -1,
|
|
||||||
occlusionStrength: matDef.occlusionTexture?.strength ?? 1,
|
|
||||||
emissiveFactor: matDef.emissiveFactor || [0, 0, 0],
|
|
||||||
emissiveTextureIndex: matDef.emissiveTexture?.index ?? -1,
|
|
||||||
alphaMode: matDef.alphaMode || 'OPAQUE',
|
|
||||||
alphaCutoff: matDef.alphaCutoff ?? 0.5,
|
|
||||||
doubleSided: matDef.doubleSided ?? false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return materials;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create default material
|
|
||||||
*/
|
|
||||||
private createDefaultMaterial(): IGLTFMaterial {
|
|
||||||
return {
|
|
||||||
name: 'Default',
|
|
||||||
baseColorFactor: [0.8, 0.8, 0.8, 1],
|
|
||||||
baseColorTextureIndex: -1,
|
|
||||||
metallicFactor: 0,
|
|
||||||
roughnessFactor: 0.5,
|
|
||||||
metallicRoughnessTextureIndex: -1,
|
|
||||||
normalTextureIndex: -1,
|
|
||||||
normalScale: 1,
|
|
||||||
occlusionTextureIndex: -1,
|
|
||||||
occlusionStrength: 1,
|
|
||||||
emissiveFactor: [0, 0, 0],
|
|
||||||
emissiveTextureIndex: -1,
|
|
||||||
alphaMode: 'OPAQUE',
|
|
||||||
alphaCutoff: 0.5,
|
|
||||||
doubleSided: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse textures
|
|
||||||
*/
|
|
||||||
private async parseTextures(
|
|
||||||
json: GLTFJson,
|
|
||||||
buffers: ArrayBuffer[],
|
|
||||||
_context: IAssetParseContext
|
|
||||||
): Promise<IGLTFTextureInfo[]> {
|
|
||||||
const textures: IGLTFTextureInfo[] = [];
|
|
||||||
|
|
||||||
if (!json.textures || !json.images) return textures;
|
|
||||||
|
|
||||||
for (const texDef of json.textures) {
|
|
||||||
if (texDef.source === undefined) {
|
|
||||||
textures.push({});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const imageDef = json.images[texDef.source];
|
|
||||||
const textureInfo: IGLTFTextureInfo = {
|
|
||||||
name: imageDef.name || texDef.name
|
|
||||||
};
|
|
||||||
|
|
||||||
if (imageDef.bufferView !== undefined) {
|
|
||||||
// Embedded image
|
|
||||||
const bufferView = json.bufferViews![imageDef.bufferView];
|
|
||||||
const buffer = buffers[bufferView.buffer];
|
|
||||||
const byteOffset = bufferView.byteOffset || 0;
|
|
||||||
textureInfo.imageData = buffer.slice(byteOffset, byteOffset + bufferView.byteLength);
|
|
||||||
textureInfo.mimeType = imageDef.mimeType;
|
|
||||||
} else if (imageDef.uri) {
|
|
||||||
if (imageDef.uri.startsWith('data:')) {
|
|
||||||
// Data URI
|
|
||||||
textureInfo.imageData = this.decodeDataUri(imageDef.uri);
|
|
||||||
const mimeMatch = imageDef.uri.match(/^data:(.*?);/);
|
|
||||||
textureInfo.mimeType = mimeMatch?.[1];
|
|
||||||
} else {
|
|
||||||
// External URI
|
|
||||||
textureInfo.uri = imageDef.uri;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
textures.push(textureInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
return textures;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse scene nodes
|
|
||||||
*/
|
|
||||||
private parseNodes(json: GLTFJson): IGLTFNode[] {
|
|
||||||
const nodes: IGLTFNode[] = [];
|
|
||||||
|
|
||||||
if (!json.nodes) return nodes;
|
|
||||||
|
|
||||||
for (const nodeDef of json.nodes) {
|
|
||||||
let position: [number, number, number] = [0, 0, 0];
|
|
||||||
let rotation: [number, number, number, number] = [0, 0, 0, 1];
|
|
||||||
let scale: [number, number, number] = [1, 1, 1];
|
|
||||||
|
|
||||||
if (nodeDef.matrix) {
|
|
||||||
// Decompose matrix
|
|
||||||
const m = nodeDef.matrix;
|
|
||||||
// Extract translation
|
|
||||||
position = [m[12], m[13], m[14]];
|
|
||||||
// Extract scale
|
|
||||||
scale = [
|
|
||||||
Math.sqrt(m[0] * m[0] + m[1] * m[1] + m[2] * m[2]),
|
|
||||||
Math.sqrt(m[4] * m[4] + m[5] * m[5] + m[6] * m[6]),
|
|
||||||
Math.sqrt(m[8] * m[8] + m[9] * m[9] + m[10] * m[10])
|
|
||||||
];
|
|
||||||
// Extract rotation (simplified, assumes no shear)
|
|
||||||
rotation = this.matrixToQuaternion(m, scale);
|
|
||||||
} else {
|
|
||||||
if (nodeDef.translation) {
|
|
||||||
position = nodeDef.translation;
|
|
||||||
}
|
|
||||||
if (nodeDef.rotation) {
|
|
||||||
rotation = nodeDef.rotation;
|
|
||||||
}
|
|
||||||
if (nodeDef.scale) {
|
|
||||||
scale = nodeDef.scale;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nodes.push({
|
|
||||||
name: nodeDef.name || 'Node',
|
|
||||||
meshIndex: nodeDef.mesh,
|
|
||||||
children: nodeDef.children || [],
|
|
||||||
transform: { position, rotation, scale }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return nodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract quaternion from matrix
|
|
||||||
*/
|
|
||||||
private matrixToQuaternion(m: number[], scale: [number, number, number]): [number, number, number, number] {
|
|
||||||
// Normalize rotation matrix
|
|
||||||
const sx = scale[0], sy = scale[1], sz = scale[2];
|
|
||||||
const m00 = m[0] / sx, m01 = m[4] / sy, m02 = m[8] / sz;
|
|
||||||
const m10 = m[1] / sx, m11 = m[5] / sy, m12 = m[9] / sz;
|
|
||||||
const m20 = m[2] / sx, m21 = m[6] / sy, m22 = m[10] / sz;
|
|
||||||
|
|
||||||
const trace = m00 + m11 + m22;
|
|
||||||
let x: number, y: number, z: number, w: number;
|
|
||||||
|
|
||||||
if (trace > 0) {
|
|
||||||
const s = 0.5 / Math.sqrt(trace + 1.0);
|
|
||||||
w = 0.25 / s;
|
|
||||||
x = (m21 - m12) * s;
|
|
||||||
y = (m02 - m20) * s;
|
|
||||||
z = (m10 - m01) * s;
|
|
||||||
} else if (m00 > m11 && m00 > m22) {
|
|
||||||
const s = 2.0 * Math.sqrt(1.0 + m00 - m11 - m22);
|
|
||||||
w = (m21 - m12) / s;
|
|
||||||
x = 0.25 * s;
|
|
||||||
y = (m01 + m10) / s;
|
|
||||||
z = (m02 + m20) / s;
|
|
||||||
} else if (m11 > m22) {
|
|
||||||
const s = 2.0 * Math.sqrt(1.0 + m11 - m00 - m22);
|
|
||||||
w = (m02 - m20) / s;
|
|
||||||
x = (m01 + m10) / s;
|
|
||||||
y = 0.25 * s;
|
|
||||||
z = (m12 + m21) / s;
|
|
||||||
} else {
|
|
||||||
const s = 2.0 * Math.sqrt(1.0 + m22 - m00 - m11);
|
|
||||||
w = (m10 - m01) / s;
|
|
||||||
x = (m02 + m20) / s;
|
|
||||||
y = (m12 + m21) / s;
|
|
||||||
z = 0.25 * s;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [x, y, z, w];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get root node indices
|
|
||||||
*/
|
|
||||||
private getRootNodes(json: GLTFJson): number[] {
|
|
||||||
const sceneIndex = json.scene ?? 0;
|
|
||||||
const scene = json.scenes?.[sceneIndex];
|
|
||||||
return scene?.nodes || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse animations
|
|
||||||
*/
|
|
||||||
private parseAnimations(json: GLTFJson, buffers: ArrayBuffer[]): IGLTFAnimationClip[] {
|
|
||||||
const animations: IGLTFAnimationClip[] = [];
|
|
||||||
|
|
||||||
if (!json.animations) return animations;
|
|
||||||
|
|
||||||
for (const animDef of json.animations) {
|
|
||||||
const samplers: IAnimationSampler[] = [];
|
|
||||||
const channels: IAnimationChannel[] = [];
|
|
||||||
let duration = 0;
|
|
||||||
|
|
||||||
// Parse samplers
|
|
||||||
for (const samplerDef of animDef.samplers) {
|
|
||||||
const inputData = this.getAccessorData(json, buffers, samplerDef.input);
|
|
||||||
const outputData = this.getAccessorData(json, buffers, samplerDef.output);
|
|
||||||
|
|
||||||
const input = new Float32Array(inputData.data.buffer, (inputData.data as Float32Array).byteOffset, inputData.count);
|
|
||||||
const output = new Float32Array(outputData.data.buffer, (outputData.data as Float32Array).byteOffset, outputData.count * outputData.componentCount);
|
|
||||||
|
|
||||||
// Update duration
|
|
||||||
if (input.length > 0) {
|
|
||||||
duration = Math.max(duration, input[input.length - 1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
samplers.push({
|
|
||||||
input,
|
|
||||||
output,
|
|
||||||
interpolation: samplerDef.interpolation || 'LINEAR'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse channels
|
|
||||||
for (const channelDef of animDef.channels) {
|
|
||||||
if (channelDef.target.node === undefined) continue;
|
|
||||||
|
|
||||||
channels.push({
|
|
||||||
samplerIndex: channelDef.sampler,
|
|
||||||
target: {
|
|
||||||
nodeIndex: channelDef.target.node,
|
|
||||||
path: channelDef.target.path
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
animations.push({
|
|
||||||
name: animDef.name || 'Animation',
|
|
||||||
duration,
|
|
||||||
samplers,
|
|
||||||
channels
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return animations;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse skeleton/skin data
|
|
||||||
*/
|
|
||||||
private parseSkeleton(json: GLTFJson, buffers: ArrayBuffer[]): ISkeletonData | undefined {
|
|
||||||
if (!json.skins || json.skins.length === 0) return undefined;
|
|
||||||
|
|
||||||
// Use first skin
|
|
||||||
const skin = json.skins[0];
|
|
||||||
const joints: ISkeletonJoint[] = [];
|
|
||||||
|
|
||||||
// Load inverse bind matrices
|
|
||||||
let inverseBindMatrices: Float32Array | null = null;
|
|
||||||
if (skin.inverseBindMatrices !== undefined) {
|
|
||||||
const ibmData = this.getAccessorData(json, buffers, skin.inverseBindMatrices);
|
|
||||||
inverseBindMatrices = new Float32Array(ibmData.data.buffer, (ibmData.data as Float32Array).byteOffset, ibmData.count * 16);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build joint hierarchy
|
|
||||||
const jointIndexMap = new Map<number, number>();
|
|
||||||
for (let i = 0; i < skin.joints.length; i++) {
|
|
||||||
jointIndexMap.set(skin.joints[i], i);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < skin.joints.length; i++) {
|
|
||||||
const nodeIndex = skin.joints[i];
|
|
||||||
const node = json.nodes![nodeIndex];
|
|
||||||
|
|
||||||
// Find parent
|
|
||||||
let parentIndex = -1;
|
|
||||||
for (const [idx, jointIdx] of jointIndexMap) {
|
|
||||||
if (jointIdx !== i) {
|
|
||||||
const parentNode = json.nodes![idx];
|
|
||||||
if (parentNode.children?.includes(nodeIndex)) {
|
|
||||||
parentIndex = jointIdx;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ibm = new Float32Array(16);
|
|
||||||
if (inverseBindMatrices) {
|
|
||||||
for (let j = 0; j < 16; j++) {
|
|
||||||
ibm[j] = inverseBindMatrices[i * 16 + j];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Identity matrix
|
|
||||||
ibm[0] = ibm[5] = ibm[10] = ibm[15] = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
joints.push({
|
|
||||||
name: node.name || `Joint_${i}`,
|
|
||||||
nodeIndex,
|
|
||||||
parentIndex,
|
|
||||||
inverseBindMatrix: ibm
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find root joint
|
|
||||||
let rootJointIndex = 0;
|
|
||||||
for (let i = 0; i < joints.length; i++) {
|
|
||||||
if (joints[i].parentIndex === -1) {
|
|
||||||
rootJointIndex = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
joints,
|
|
||||||
rootJointIndex
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate combined bounds for all meshes
|
|
||||||
*/
|
|
||||||
private calculateBounds(meshes: IMeshData[]): IBoundingBox {
|
|
||||||
if (meshes.length === 0) {
|
|
||||||
return { min: [0, 0, 0], max: [0, 0, 0] };
|
|
||||||
}
|
|
||||||
|
|
||||||
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
|
||||||
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
|
||||||
|
|
||||||
for (const mesh of meshes) {
|
|
||||||
minX = Math.min(minX, mesh.bounds.min[0]);
|
|
||||||
minY = Math.min(minY, mesh.bounds.min[1]);
|
|
||||||
minZ = Math.min(minZ, mesh.bounds.min[2]);
|
|
||||||
maxX = Math.max(maxX, mesh.bounds.max[0]);
|
|
||||||
maxY = Math.max(maxY, mesh.bounds.max[1]);
|
|
||||||
maxZ = Math.max(maxZ, mesh.bounds.max[2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
min: [minX, minY, minZ],
|
|
||||||
max: [maxX, maxY, maxZ]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,553 +0,0 @@
|
|||||||
/**
|
|
||||||
* OBJ model loader implementation
|
|
||||||
* OBJ 模型加载器实现
|
|
||||||
*
|
|
||||||
* Supports:
|
|
||||||
* - Wavefront OBJ format (.obj)
|
|
||||||
* - Vertices, normals, texture coordinates
|
|
||||||
* - Triangular and quad faces (quads are triangulated)
|
|
||||||
* - Multiple objects/groups
|
|
||||||
* - MTL material references (materials loaded separately)
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { AssetType } from '../types/AssetTypes';
|
|
||||||
import type { IAssetContent, AssetContentType } from '../interfaces/IAssetReader';
|
|
||||||
import type {
|
|
||||||
IAssetLoader,
|
|
||||||
IAssetParseContext,
|
|
||||||
IGLTFAsset,
|
|
||||||
IMeshData,
|
|
||||||
IGLTFMaterial,
|
|
||||||
IGLTFNode,
|
|
||||||
IBoundingBox
|
|
||||||
} from '../interfaces/IAssetLoader';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parsed OBJ data structure
|
|
||||||
* 解析后的 OBJ 数据结构
|
|
||||||
*/
|
|
||||||
interface OBJParseResult {
|
|
||||||
positions: number[];
|
|
||||||
normals: number[];
|
|
||||||
uvs: number[];
|
|
||||||
objects: OBJObject[];
|
|
||||||
mtlLib?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface OBJObject {
|
|
||||||
name: string;
|
|
||||||
material?: string;
|
|
||||||
faces: OBJFace[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface OBJFace {
|
|
||||||
vertices: OBJVertex[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface OBJVertex {
|
|
||||||
positionIndex: number;
|
|
||||||
uvIndex?: number;
|
|
||||||
normalIndex?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* OBJ model loader
|
|
||||||
* OBJ 模型加载器
|
|
||||||
*/
|
|
||||||
export class OBJLoader implements IAssetLoader<IGLTFAsset> {
|
|
||||||
readonly supportedType = AssetType.Model3D;
|
|
||||||
readonly supportedExtensions = ['.obj'];
|
|
||||||
readonly contentType: AssetContentType = 'text';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse OBJ content
|
|
||||||
* 解析 OBJ 内容
|
|
||||||
*/
|
|
||||||
async parse(content: IAssetContent, context: IAssetParseContext): Promise<IGLTFAsset> {
|
|
||||||
const text = content.text;
|
|
||||||
if (!text) {
|
|
||||||
throw new Error('OBJ loader requires text content');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse OBJ text
|
|
||||||
// 解析 OBJ 文本
|
|
||||||
const objData = this.parseOBJ(text);
|
|
||||||
|
|
||||||
// Convert to meshes
|
|
||||||
// 转换为网格
|
|
||||||
const meshes = this.buildMeshes(objData);
|
|
||||||
|
|
||||||
// Create default materials
|
|
||||||
// 创建默认材质
|
|
||||||
const materials = this.buildMaterials(objData);
|
|
||||||
|
|
||||||
// Build nodes (one per object)
|
|
||||||
// 构建节点(每个对象一个)
|
|
||||||
const nodes: IGLTFNode[] = meshes.map((mesh, index) => ({
|
|
||||||
name: mesh.name,
|
|
||||||
meshIndex: index,
|
|
||||||
children: [],
|
|
||||||
transform: {
|
|
||||||
position: [0, 0, 0],
|
|
||||||
rotation: [0, 0, 0, 1],
|
|
||||||
scale: [1, 1, 1]
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Calculate overall bounds
|
|
||||||
// 计算总边界
|
|
||||||
const bounds = this.calculateBounds(meshes);
|
|
||||||
|
|
||||||
// Get model name from file path
|
|
||||||
// 从文件路径获取模型名称
|
|
||||||
const pathParts = context.metadata.path.split(/[\\/]/);
|
|
||||||
const fileName = pathParts[pathParts.length - 1];
|
|
||||||
const name = fileName.replace(/\.obj$/i, '');
|
|
||||||
|
|
||||||
return {
|
|
||||||
name,
|
|
||||||
meshes,
|
|
||||||
materials,
|
|
||||||
textures: [],
|
|
||||||
nodes,
|
|
||||||
rootNodes: nodes.map((_, i) => i),
|
|
||||||
bounds,
|
|
||||||
sourcePath: context.metadata.path
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dispose OBJ asset
|
|
||||||
* 释放 OBJ 资产
|
|
||||||
*/
|
|
||||||
dispose(asset: IGLTFAsset): void {
|
|
||||||
for (const mesh of asset.meshes) {
|
|
||||||
(mesh as { vertices: Float32Array | null }).vertices = null!;
|
|
||||||
(mesh as { indices: Uint16Array | Uint32Array | null }).indices = null!;
|
|
||||||
}
|
|
||||||
asset.meshes.length = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Private Methods =====
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse OBJ text format
|
|
||||||
* 解析 OBJ 文本格式
|
|
||||||
*/
|
|
||||||
private parseOBJ(text: string): OBJParseResult {
|
|
||||||
const lines = text.split('\n');
|
|
||||||
|
|
||||||
const positions: number[] = [];
|
|
||||||
const normals: number[] = [];
|
|
||||||
const uvs: number[] = [];
|
|
||||||
const objects: OBJObject[] = [];
|
|
||||||
|
|
||||||
let currentObject: OBJObject = { name: 'default', faces: [] };
|
|
||||||
let mtlLib: string | undefined;
|
|
||||||
|
|
||||||
for (let lineNum = 0; lineNum < lines.length; lineNum++) {
|
|
||||||
const line = lines[lineNum].trim();
|
|
||||||
|
|
||||||
// Skip comments and empty lines
|
|
||||||
// 跳过注释和空行
|
|
||||||
if (line.length === 0 || line.startsWith('#')) continue;
|
|
||||||
|
|
||||||
const parts = line.split(/\s+/);
|
|
||||||
const keyword = parts[0];
|
|
||||||
|
|
||||||
switch (keyword) {
|
|
||||||
case 'v': // Vertex position
|
|
||||||
positions.push(
|
|
||||||
parseFloat(parts[1]) || 0,
|
|
||||||
parseFloat(parts[2]) || 0,
|
|
||||||
parseFloat(parts[3]) || 0
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'vn': // Vertex normal
|
|
||||||
normals.push(
|
|
||||||
parseFloat(parts[1]) || 0,
|
|
||||||
parseFloat(parts[2]) || 0,
|
|
||||||
parseFloat(parts[3]) || 0
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'vt': // Texture coordinate
|
|
||||||
uvs.push(
|
|
||||||
parseFloat(parts[1]) || 0,
|
|
||||||
parseFloat(parts[2]) || 0
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'f': // Face
|
|
||||||
const face = this.parseFace(parts.slice(1));
|
|
||||||
if (face.vertices.length >= 3) {
|
|
||||||
// Triangulate if more than 3 vertices (fan triangulation)
|
|
||||||
// 如果超过 3 个顶点则三角化(扇形三角化)
|
|
||||||
for (let i = 1; i < face.vertices.length - 1; i++) {
|
|
||||||
currentObject.faces.push({
|
|
||||||
vertices: [
|
|
||||||
face.vertices[0],
|
|
||||||
face.vertices[i],
|
|
||||||
face.vertices[i + 1]
|
|
||||||
]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'o': // Object name
|
|
||||||
case 'g': // Group name
|
|
||||||
if (currentObject.faces.length > 0) {
|
|
||||||
objects.push(currentObject);
|
|
||||||
}
|
|
||||||
currentObject = {
|
|
||||||
name: parts.slice(1).join(' ') || 'unnamed',
|
|
||||||
faces: []
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'usemtl': // Material reference
|
|
||||||
// If current object has faces with different material, split it
|
|
||||||
// 如果当前对象有不同材质的面,则拆分
|
|
||||||
if (currentObject.faces.length > 0 && currentObject.material) {
|
|
||||||
objects.push(currentObject);
|
|
||||||
currentObject = {
|
|
||||||
name: `${currentObject.name}_${parts[1]}`,
|
|
||||||
faces: [],
|
|
||||||
material: parts[1]
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
currentObject.material = parts[1];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'mtllib': // MTL library reference
|
|
||||||
mtlLib = parts[1];
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 's': // Smoothing group (ignored)
|
|
||||||
case 'l': // Line (ignored)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Push last object
|
|
||||||
// 推送最后一个对象
|
|
||||||
if (currentObject.faces.length > 0) {
|
|
||||||
objects.push(currentObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no objects were created, create one from default
|
|
||||||
// 如果没有创建对象,从默认创建一个
|
|
||||||
if (objects.length === 0 && currentObject.faces.length === 0) {
|
|
||||||
throw new Error('OBJ file contains no geometry');
|
|
||||||
}
|
|
||||||
|
|
||||||
return { positions, normals, uvs, objects, mtlLib };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse a face definition
|
|
||||||
* 解析面定义
|
|
||||||
*
|
|
||||||
* Format: v, v/vt, v/vt/vn, v//vn
|
|
||||||
*/
|
|
||||||
private parseFace(parts: string[]): OBJFace {
|
|
||||||
const vertices: OBJVertex[] = [];
|
|
||||||
|
|
||||||
for (const part of parts) {
|
|
||||||
const indices = part.split('/');
|
|
||||||
const vertex: OBJVertex = {
|
|
||||||
positionIndex: parseInt(indices[0], 10) - 1 // OBJ is 1-indexed
|
|
||||||
};
|
|
||||||
|
|
||||||
if (indices.length > 1 && indices[1]) {
|
|
||||||
vertex.uvIndex = parseInt(indices[1], 10) - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (indices.length > 2 && indices[2]) {
|
|
||||||
vertex.normalIndex = parseInt(indices[2], 10) - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
vertices.push(vertex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { vertices };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build mesh data from parsed OBJ
|
|
||||||
* 从解析的 OBJ 构建网格数据
|
|
||||||
*/
|
|
||||||
private buildMeshes(objData: OBJParseResult): IMeshData[] {
|
|
||||||
const meshes: IMeshData[] = [];
|
|
||||||
|
|
||||||
for (const obj of objData.objects) {
|
|
||||||
const mesh = this.buildMesh(obj, objData);
|
|
||||||
meshes.push(mesh);
|
|
||||||
}
|
|
||||||
|
|
||||||
return meshes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build a single mesh from OBJ object
|
|
||||||
* 从 OBJ 对象构建单个网格
|
|
||||||
*/
|
|
||||||
private buildMesh(obj: OBJObject, objData: OBJParseResult): IMeshData {
|
|
||||||
// OBJ uses indexed vertices, but indices can reference different
|
|
||||||
// position/uv/normal combinations, so we need to expand
|
|
||||||
// OBJ 使用索引顶点,但索引可以引用不同的 position/uv/normal 组合,所以需要展开
|
|
||||||
|
|
||||||
const positions: number[] = [];
|
|
||||||
const normals: number[] = [];
|
|
||||||
const uvs: number[] = [];
|
|
||||||
const indices: number[] = [];
|
|
||||||
|
|
||||||
// Map to track unique vertex combinations
|
|
||||||
// 用于跟踪唯一顶点组合的映射
|
|
||||||
const vertexMap = new Map<string, number>();
|
|
||||||
let vertexIndex = 0;
|
|
||||||
|
|
||||||
for (const face of obj.faces) {
|
|
||||||
const faceIndices: number[] = [];
|
|
||||||
|
|
||||||
for (const vertex of face.vertices) {
|
|
||||||
// Create unique key for this vertex combination
|
|
||||||
// 为此顶点组合创建唯一键
|
|
||||||
const key = `${vertex.positionIndex}/${vertex.uvIndex ?? ''}/${vertex.normalIndex ?? ''}`;
|
|
||||||
|
|
||||||
let index = vertexMap.get(key);
|
|
||||||
if (index === undefined) {
|
|
||||||
// New unique vertex - add to arrays
|
|
||||||
// 新的唯一顶点 - 添加到数组
|
|
||||||
index = vertexIndex++;
|
|
||||||
vertexMap.set(key, index);
|
|
||||||
|
|
||||||
// Position
|
|
||||||
const pi = vertex.positionIndex * 3;
|
|
||||||
positions.push(
|
|
||||||
objData.positions[pi] ?? 0,
|
|
||||||
objData.positions[pi + 1] ?? 0,
|
|
||||||
objData.positions[pi + 2] ?? 0
|
|
||||||
);
|
|
||||||
|
|
||||||
// UV
|
|
||||||
if (vertex.uvIndex !== undefined) {
|
|
||||||
const ui = vertex.uvIndex * 2;
|
|
||||||
uvs.push(
|
|
||||||
objData.uvs[ui] ?? 0,
|
|
||||||
1 - (objData.uvs[ui + 1] ?? 0) // Flip V coordinate
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
uvs.push(0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normal
|
|
||||||
if (vertex.normalIndex !== undefined) {
|
|
||||||
const ni = vertex.normalIndex * 3;
|
|
||||||
normals.push(
|
|
||||||
objData.normals[ni] ?? 0,
|
|
||||||
objData.normals[ni + 1] ?? 0,
|
|
||||||
objData.normals[ni + 2] ?? 0
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
normals.push(0, 1, 0); // Default up normal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
faceIndices.push(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add triangle indices
|
|
||||||
// 添加三角形索引
|
|
||||||
if (faceIndices.length === 3) {
|
|
||||||
indices.push(faceIndices[0], faceIndices[1], faceIndices[2]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate bounds
|
|
||||||
// 计算边界
|
|
||||||
const bounds = this.calculateMeshBounds(positions);
|
|
||||||
|
|
||||||
// Generate normals if not provided
|
|
||||||
// 如果未提供法线则生成
|
|
||||||
const hasValidNormals = objData.normals.length > 0;
|
|
||||||
const finalNormals = hasValidNormals
|
|
||||||
? new Float32Array(normals)
|
|
||||||
: this.generateNormals(positions, indices);
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: obj.name,
|
|
||||||
vertices: new Float32Array(positions),
|
|
||||||
indices: new Uint32Array(indices),
|
|
||||||
normals: finalNormals,
|
|
||||||
uvs: new Float32Array(uvs),
|
|
||||||
bounds,
|
|
||||||
materialIndex: -1 // Material resolved by name
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate flat normals for mesh
|
|
||||||
* 为网格生成平面法线
|
|
||||||
*/
|
|
||||||
private generateNormals(positions: number[], indices: number[]): Float32Array {
|
|
||||||
const normals = new Float32Array(positions.length);
|
|
||||||
|
|
||||||
for (let i = 0; i < indices.length; i += 3) {
|
|
||||||
const i0 = indices[i] * 3;
|
|
||||||
const i1 = indices[i + 1] * 3;
|
|
||||||
const i2 = indices[i + 2] * 3;
|
|
||||||
|
|
||||||
// Get triangle vertices
|
|
||||||
const v0x = positions[i0], v0y = positions[i0 + 1], v0z = positions[i0 + 2];
|
|
||||||
const v1x = positions[i1], v1y = positions[i1 + 1], v1z = positions[i1 + 2];
|
|
||||||
const v2x = positions[i2], v2y = positions[i2 + 1], v2z = positions[i2 + 2];
|
|
||||||
|
|
||||||
// Calculate edge vectors
|
|
||||||
const e1x = v1x - v0x, e1y = v1y - v0y, e1z = v1z - v0z;
|
|
||||||
const e2x = v2x - v0x, e2y = v2y - v0y, e2z = v2z - v0z;
|
|
||||||
|
|
||||||
// Cross product
|
|
||||||
const nx = e1y * e2z - e1z * e2y;
|
|
||||||
const ny = e1z * e2x - e1x * e2z;
|
|
||||||
const nz = e1x * e2y - e1y * e2x;
|
|
||||||
|
|
||||||
// Add to vertex normals (will be normalized later or kept as-is for flat shading)
|
|
||||||
normals[i0] += nx; normals[i0 + 1] += ny; normals[i0 + 2] += nz;
|
|
||||||
normals[i1] += nx; normals[i1 + 1] += ny; normals[i1 + 2] += nz;
|
|
||||||
normals[i2] += nx; normals[i2 + 1] += ny; normals[i2 + 2] += nz;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalize
|
|
||||||
for (let i = 0; i < normals.length; i += 3) {
|
|
||||||
const len = Math.sqrt(normals[i] ** 2 + normals[i + 1] ** 2 + normals[i + 2] ** 2);
|
|
||||||
if (len > 0) {
|
|
||||||
normals[i] /= len;
|
|
||||||
normals[i + 1] /= len;
|
|
||||||
normals[i + 2] /= len;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return normals;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build default materials
|
|
||||||
* 构建默认材质
|
|
||||||
*/
|
|
||||||
private buildMaterials(objData: OBJParseResult): IGLTFMaterial[] {
|
|
||||||
// Create one default material per unique material name
|
|
||||||
// 为每个唯一的材质名称创建一个默认材质
|
|
||||||
const materialNames = new Set<string>();
|
|
||||||
for (const obj of objData.objects) {
|
|
||||||
if (obj.material) {
|
|
||||||
materialNames.add(obj.material);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const materials: IGLTFMaterial[] = [];
|
|
||||||
|
|
||||||
// Default material
|
|
||||||
materials.push({
|
|
||||||
name: 'Default',
|
|
||||||
baseColorFactor: [0.8, 0.8, 0.8, 1],
|
|
||||||
baseColorTextureIndex: -1,
|
|
||||||
metallicFactor: 0,
|
|
||||||
roughnessFactor: 0.5,
|
|
||||||
metallicRoughnessTextureIndex: -1,
|
|
||||||
normalTextureIndex: -1,
|
|
||||||
normalScale: 1,
|
|
||||||
occlusionTextureIndex: -1,
|
|
||||||
occlusionStrength: 1,
|
|
||||||
emissiveFactor: [0, 0, 0],
|
|
||||||
emissiveTextureIndex: -1,
|
|
||||||
alphaMode: 'OPAQUE',
|
|
||||||
alphaCutoff: 0.5,
|
|
||||||
doubleSided: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Named materials (with placeholder values)
|
|
||||||
for (const name of materialNames) {
|
|
||||||
materials.push({
|
|
||||||
name,
|
|
||||||
baseColorFactor: [0.8, 0.8, 0.8, 1],
|
|
||||||
baseColorTextureIndex: -1,
|
|
||||||
metallicFactor: 0,
|
|
||||||
roughnessFactor: 0.5,
|
|
||||||
metallicRoughnessTextureIndex: -1,
|
|
||||||
normalTextureIndex: -1,
|
|
||||||
normalScale: 1,
|
|
||||||
occlusionTextureIndex: -1,
|
|
||||||
occlusionStrength: 1,
|
|
||||||
emissiveFactor: [0, 0, 0],
|
|
||||||
emissiveTextureIndex: -1,
|
|
||||||
alphaMode: 'OPAQUE',
|
|
||||||
alphaCutoff: 0.5,
|
|
||||||
doubleSided: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return materials;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate mesh bounding box
|
|
||||||
* 计算网格边界盒
|
|
||||||
*/
|
|
||||||
private calculateMeshBounds(positions: number[]): IBoundingBox {
|
|
||||||
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
|
||||||
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
|
||||||
|
|
||||||
for (let i = 0; i < positions.length; i += 3) {
|
|
||||||
const x = positions[i];
|
|
||||||
const y = positions[i + 1];
|
|
||||||
const z = positions[i + 2];
|
|
||||||
|
|
||||||
minX = Math.min(minX, x);
|
|
||||||
minY = Math.min(minY, y);
|
|
||||||
minZ = Math.min(minZ, z);
|
|
||||||
maxX = Math.max(maxX, x);
|
|
||||||
maxY = Math.max(maxY, y);
|
|
||||||
maxZ = Math.max(maxZ, z);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isFinite(minX)) {
|
|
||||||
return { min: [0, 0, 0], max: [0, 0, 0] };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
min: [minX, minY, minZ],
|
|
||||||
max: [maxX, maxY, maxZ]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate combined bounds for all meshes
|
|
||||||
* 计算所有网格的组合边界
|
|
||||||
*/
|
|
||||||
private calculateBounds(meshes: IMeshData[]): IBoundingBox {
|
|
||||||
if (meshes.length === 0) {
|
|
||||||
return { min: [0, 0, 0], max: [0, 0, 0] };
|
|
||||||
}
|
|
||||||
|
|
||||||
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
|
||||||
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
|
||||||
|
|
||||||
for (const mesh of meshes) {
|
|
||||||
minX = Math.min(minX, mesh.bounds.min[0]);
|
|
||||||
minY = Math.min(minY, mesh.bounds.min[1]);
|
|
||||||
minZ = Math.min(minZ, mesh.bounds.min[2]);
|
|
||||||
maxX = Math.max(maxX, mesh.bounds.max[0]);
|
|
||||||
maxY = Math.max(maxY, mesh.bounds.max[1]);
|
|
||||||
maxZ = Math.max(maxZ, mesh.bounds.max[2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
min: [minX, minY, minZ],
|
|
||||||
max: [maxX, maxY, maxZ]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,16 +16,6 @@ interface IEngineBridgeGlobal {
|
|||||||
unloadTexture?(textureId: number): void;
|
unloadTexture?(textureId: number): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sprite settings from texture meta
|
|
||||||
* 纹理 meta 中的 Sprite 设置
|
|
||||||
*/
|
|
||||||
interface ISpriteSettings {
|
|
||||||
sliceBorder?: [number, number, number, number];
|
|
||||||
pivot?: [number, number];
|
|
||||||
pixelsPerUnit?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取全局引擎桥接
|
* 获取全局引擎桥接
|
||||||
* Get global engine bridge
|
* Get global engine bridge
|
||||||
@@ -71,22 +61,13 @@ export class TextureLoader implements IAssetLoader<ITextureAsset> {
|
|||||||
|
|
||||||
const image = content.image;
|
const image = content.image;
|
||||||
|
|
||||||
// Read sprite settings from import settings
|
|
||||||
// 从导入设置读取 sprite 设置
|
|
||||||
const importSettings = context.metadata.importSettings as Record<string, unknown> | undefined;
|
|
||||||
const spriteSettings = importSettings?.spriteSettings as ISpriteSettings | undefined;
|
|
||||||
|
|
||||||
const textureAsset: ITextureAsset = {
|
const textureAsset: ITextureAsset = {
|
||||||
textureId: TextureLoader._nextTextureId++,
|
textureId: TextureLoader._nextTextureId++,
|
||||||
width: image.width,
|
width: image.width,
|
||||||
height: image.height,
|
height: image.height,
|
||||||
format: 'rgba',
|
format: 'rgba',
|
||||||
hasMipmaps: false,
|
hasMipmaps: false,
|
||||||
data: image,
|
data: image
|
||||||
// Include sprite settings if available
|
|
||||||
// 如果有则包含 sprite 设置
|
|
||||||
sliceBorder: spriteSettings?.sliceBorder,
|
|
||||||
pivot: spriteSettings?.pivot
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Upload to GPU if bridge exists.
|
// Upload to GPU if bridge exists.
|
||||||
|
|||||||
@@ -1,139 +0,0 @@
|
|||||||
/**
|
|
||||||
* Asset Metadata Service
|
|
||||||
* 资产元数据服务
|
|
||||||
*
|
|
||||||
* Provides global access to asset metadata without requiring asset loading.
|
|
||||||
* This service is independent of the texture loading path, allowing
|
|
||||||
* render systems to query sprite info regardless of how textures are loaded.
|
|
||||||
*
|
|
||||||
* 提供对资产元数据的全局访问,无需加载资产。
|
|
||||||
* 此服务独立于纹理加载路径,允许渲染系统查询 sprite 信息,
|
|
||||||
* 无论纹理是如何加载的。
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { AssetDatabase, ITextureSpriteInfo } from '../core/AssetDatabase';
|
|
||||||
import type { AssetGUID } from '../types/AssetTypes';
|
|
||||||
import type { ITextureEngineBridge } from '../integration/EngineIntegration';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Global asset database instance
|
|
||||||
* 全局资产数据库实例
|
|
||||||
*/
|
|
||||||
let globalAssetDatabase: AssetDatabase | null = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Global engine bridge instance
|
|
||||||
* 全局引擎桥实例
|
|
||||||
*
|
|
||||||
* Used to query texture dimensions from Rust engine (single source of truth).
|
|
||||||
* 用于从 Rust 引擎查询纹理尺寸(唯一事实来源)。
|
|
||||||
*/
|
|
||||||
let globalEngineBridge: ITextureEngineBridge | null = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the global asset database
|
|
||||||
* 设置全局资产数据库
|
|
||||||
*
|
|
||||||
* Should be called during engine initialization.
|
|
||||||
* 应在引擎初始化期间调用。
|
|
||||||
*
|
|
||||||
* @param database - AssetDatabase instance | AssetDatabase 实例
|
|
||||||
*/
|
|
||||||
export function setGlobalAssetDatabase(database: AssetDatabase | null): void {
|
|
||||||
globalAssetDatabase = database;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the global asset database
|
|
||||||
* 获取全局资产数据库
|
|
||||||
*
|
|
||||||
* @returns AssetDatabase instance or null | AssetDatabase 实例或 null
|
|
||||||
*/
|
|
||||||
export function getGlobalAssetDatabase(): AssetDatabase | null {
|
|
||||||
return globalAssetDatabase;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the global engine bridge
|
|
||||||
* 设置全局引擎桥
|
|
||||||
*
|
|
||||||
* The engine bridge is used to query texture dimensions directly from Rust engine.
|
|
||||||
* This is the single source of truth for texture dimensions.
|
|
||||||
* 引擎桥用于直接从 Rust 引擎查询纹理尺寸。
|
|
||||||
* 这是纹理尺寸的唯一事实来源。
|
|
||||||
*
|
|
||||||
* @param bridge - ITextureEngineBridge instance | ITextureEngineBridge 实例
|
|
||||||
*/
|
|
||||||
export function setGlobalEngineBridge(bridge: ITextureEngineBridge | null): void {
|
|
||||||
globalEngineBridge = bridge;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the global engine bridge
|
|
||||||
* 获取全局引擎桥
|
|
||||||
*
|
|
||||||
* @returns ITextureEngineBridge instance or null | ITextureEngineBridge 实例或 null
|
|
||||||
*/
|
|
||||||
export function getGlobalEngineBridge(): ITextureEngineBridge | null {
|
|
||||||
return globalEngineBridge;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get texture sprite info by GUID
|
|
||||||
* 通过 GUID 获取纹理 Sprite 信息
|
|
||||||
*
|
|
||||||
* This is the primary API for render systems to query nine-patch/sprite info.
|
|
||||||
* It combines data from:
|
|
||||||
* - Asset metadata (sliceBorder, pivot) from AssetDatabase
|
|
||||||
* - Texture dimensions (width, height) from Rust engine (single source of truth)
|
|
||||||
*
|
|
||||||
* 这是渲染系统查询九宫格/sprite 信息的主要 API。
|
|
||||||
* 它合并来自:
|
|
||||||
* - AssetDatabase 的资产元数据(sliceBorder, pivot)
|
|
||||||
* - Rust 引擎的纹理尺寸(width, height)(唯一事实来源)
|
|
||||||
*
|
|
||||||
* @param guid - Texture asset GUID | 纹理资产 GUID
|
|
||||||
* @returns Sprite info or undefined | Sprite 信息或 undefined
|
|
||||||
*/
|
|
||||||
export function getTextureSpriteInfo(guid: AssetGUID): ITextureSpriteInfo | undefined {
|
|
||||||
// Get sprite settings from metadata
|
|
||||||
// 从元数据获取 sprite 设置
|
|
||||||
const metadataInfo = globalAssetDatabase?.getTextureSpriteInfo(guid);
|
|
||||||
|
|
||||||
// Get texture dimensions from Rust engine (single source of truth)
|
|
||||||
// 从 Rust 引擎获取纹理尺寸(唯一事实来源)
|
|
||||||
let dimensions: { width: number; height: number } | undefined;
|
|
||||||
|
|
||||||
if (globalEngineBridge?.getTextureInfoByPath && globalAssetDatabase) {
|
|
||||||
// Get asset path from database
|
|
||||||
// 从数据库获取资产路径
|
|
||||||
const metadata = globalAssetDatabase.getMetadata(guid);
|
|
||||||
if (metadata?.path) {
|
|
||||||
const engineInfo = globalEngineBridge.getTextureInfoByPath(metadata.path);
|
|
||||||
if (engineInfo) {
|
|
||||||
dimensions = engineInfo;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no metadata and no dimensions, return undefined
|
|
||||||
// 如果没有元数据也没有尺寸,返回 undefined
|
|
||||||
if (!metadataInfo && !dimensions) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge the two sources
|
|
||||||
// 合并两个数据源
|
|
||||||
// Prefer engine dimensions (runtime loaded), fallback to metadata dimensions (catalog stored)
|
|
||||||
// 优先使用引擎尺寸(运行时加载),后备使用元数据尺寸(目录存储)
|
|
||||||
return {
|
|
||||||
sliceBorder: metadataInfo?.sliceBorder,
|
|
||||||
pivot: metadataInfo?.pivot,
|
|
||||||
width: dimensions?.width ?? metadataInfo?.width,
|
|
||||||
height: dimensions?.height ?? metadataInfo?.height
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-export type for convenience
|
|
||||||
// 为方便起见重新导出类型
|
|
||||||
export type { ITextureSpriteInfo };
|
|
||||||
@@ -52,8 +52,6 @@ export const AssetType = {
|
|||||||
Texture: 'texture',
|
Texture: 'texture',
|
||||||
/** 网格 */
|
/** 网格 */
|
||||||
Mesh: 'mesh',
|
Mesh: 'mesh',
|
||||||
/** 3D模型 (GLTF/GLB) | 3D Model */
|
|
||||||
Model3D: 'model3d',
|
|
||||||
/** 材质 */
|
/** 材质 */
|
||||||
Material: 'material',
|
Material: 'material',
|
||||||
/** 着色器 */
|
/** 着色器 */
|
||||||
@@ -408,12 +406,6 @@ export interface IAssetCatalogEntry {
|
|||||||
|
|
||||||
/** 可用变体 / Available variants (platform/quality specific) */
|
/** 可用变体 / Available variants (platform/quality specific) */
|
||||||
variants?: IAssetVariant[];
|
variants?: IAssetVariant[];
|
||||||
|
|
||||||
/**
|
|
||||||
* Import settings (e.g., sprite slicing for nine-patch)
|
|
||||||
* 导入设置(如九宫格切片信息)
|
|
||||||
*/
|
|
||||||
importSettings?: Record<string, unknown>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "behavior-tree-editor",
|
|
||||||
"name": "@esengine/behavior-tree-editor",
|
|
||||||
"displayName": "Behavior Tree Editor",
|
|
||||||
"description": "Visual behavior tree editor | 可视化行为树编辑器",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"category": "Editor",
|
|
||||||
"icon": "GitBranch",
|
|
||||||
"isEditorPlugin": true,
|
|
||||||
"runtimeModule": "@esengine/behavior-tree",
|
|
||||||
"exports": {
|
|
||||||
"inspectors": ["BehaviorTreeComponentInspector"],
|
|
||||||
"panels": ["BehaviorTreeEditorPanel"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -24,9 +24,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@esengine/behavior-tree": "workspace:*"
|
"@esengine/behavior-tree": "workspace:*"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
|
||||||
"@esengine/editor-core": "workspace:*"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@esengine/ecs-framework": "workspace:*",
|
"@esengine/ecs-framework": "workspace:*",
|
||||||
"@esengine/engine-core": "workspace:*",
|
"@esengine/engine-core": "workspace:*",
|
||||||
|
|||||||
@@ -24,64 +24,64 @@ export interface GlobalBlackboardVariable {
|
|||||||
* 管理跨行为树共享的全局变量
|
* 管理跨行为树共享的全局变量
|
||||||
*/
|
*/
|
||||||
export class GlobalBlackboardService {
|
export class GlobalBlackboardService {
|
||||||
private static _instance: GlobalBlackboardService;
|
private static instance: GlobalBlackboardService;
|
||||||
private _variables: Map<string, GlobalBlackboardVariable> = new Map();
|
private variables: Map<string, GlobalBlackboardVariable> = new Map();
|
||||||
private _changeCallbacks: Array<() => void> = [];
|
private changeCallbacks: Array<() => void> = [];
|
||||||
private _projectPath: string | null = null;
|
private projectPath: string | null = null;
|
||||||
|
|
||||||
private constructor() {}
|
private constructor() {}
|
||||||
|
|
||||||
static getInstance(): GlobalBlackboardService {
|
static getInstance(): GlobalBlackboardService {
|
||||||
if (!this._instance) {
|
if (!this.instance) {
|
||||||
this._instance = new GlobalBlackboardService();
|
this.instance = new GlobalBlackboardService();
|
||||||
}
|
}
|
||||||
return this._instance;
|
return this.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置项目路径
|
* 设置项目路径
|
||||||
*/
|
*/
|
||||||
setProjectPath(path: string | null): void {
|
setProjectPath(path: string | null): void {
|
||||||
this._projectPath = path;
|
this.projectPath = path;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取项目路径
|
* 获取项目路径
|
||||||
*/
|
*/
|
||||||
getProjectPath(): string | null {
|
getProjectPath(): string | null {
|
||||||
return this._projectPath;
|
return this.projectPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 添加全局变量
|
* 添加全局变量
|
||||||
*/
|
*/
|
||||||
addVariable(variable: GlobalBlackboardVariable): void {
|
addVariable(variable: GlobalBlackboardVariable): void {
|
||||||
if (this._variables.has(variable.key)) {
|
if (this.variables.has(variable.key)) {
|
||||||
throw new Error(`全局变量 "${variable.key}" 已存在`);
|
throw new Error(`全局变量 "${variable.key}" 已存在`);
|
||||||
}
|
}
|
||||||
this._variables.set(variable.key, variable);
|
this.variables.set(variable.key, variable);
|
||||||
this._notifyChange();
|
this.notifyChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新全局变量
|
* 更新全局变量
|
||||||
*/
|
*/
|
||||||
updateVariable(key: string, updates: Partial<Omit<GlobalBlackboardVariable, 'key'>>): void {
|
updateVariable(key: string, updates: Partial<Omit<GlobalBlackboardVariable, 'key'>>): void {
|
||||||
const variable = this._variables.get(key);
|
const variable = this.variables.get(key);
|
||||||
if (!variable) {
|
if (!variable) {
|
||||||
throw new Error(`全局变量 "${key}" 不存在`);
|
throw new Error(`全局变量 "${key}" 不存在`);
|
||||||
}
|
}
|
||||||
this._variables.set(key, { ...variable, ...updates });
|
this.variables.set(key, { ...variable, ...updates });
|
||||||
this._notifyChange();
|
this.notifyChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除全局变量
|
* 删除全局变量
|
||||||
*/
|
*/
|
||||||
deleteVariable(key: string): boolean {
|
deleteVariable(key: string): boolean {
|
||||||
const result = this._variables.delete(key);
|
const result = this.variables.delete(key);
|
||||||
if (result) {
|
if (result) {
|
||||||
this._notifyChange();
|
this.notifyChange();
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -90,36 +90,36 @@ export class GlobalBlackboardService {
|
|||||||
* 重命名全局变量
|
* 重命名全局变量
|
||||||
*/
|
*/
|
||||||
renameVariable(oldKey: string, newKey: string): void {
|
renameVariable(oldKey: string, newKey: string): void {
|
||||||
if (!this._variables.has(oldKey)) {
|
if (!this.variables.has(oldKey)) {
|
||||||
throw new Error(`全局变量 "${oldKey}" 不存在`);
|
throw new Error(`全局变量 "${oldKey}" 不存在`);
|
||||||
}
|
}
|
||||||
if (this._variables.has(newKey)) {
|
if (this.variables.has(newKey)) {
|
||||||
throw new Error(`全局变量 "${newKey}" 已存在`);
|
throw new Error(`全局变量 "${newKey}" 已存在`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const variable = this._variables.get(oldKey)!;
|
const variable = this.variables.get(oldKey)!;
|
||||||
this._variables.delete(oldKey);
|
this.variables.delete(oldKey);
|
||||||
this._variables.set(newKey, { ...variable, key: newKey });
|
this.variables.set(newKey, { ...variable, key: newKey });
|
||||||
this._notifyChange();
|
this.notifyChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取全局变量
|
* 获取全局变量
|
||||||
*/
|
*/
|
||||||
getVariable(key: string): GlobalBlackboardVariable | undefined {
|
getVariable(key: string): GlobalBlackboardVariable | undefined {
|
||||||
return this._variables.get(key);
|
return this.variables.get(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取所有全局变量
|
* 获取所有全局变量
|
||||||
*/
|
*/
|
||||||
getAllVariables(): GlobalBlackboardVariable[] {
|
getAllVariables(): GlobalBlackboardVariable[] {
|
||||||
return Array.from(this._variables.values());
|
return Array.from(this.variables.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
getVariablesMap(): Record<string, GlobalBlackboardValue> {
|
getVariablesMap(): Record<string, GlobalBlackboardValue> {
|
||||||
const map: Record<string, GlobalBlackboardValue> = {};
|
const map: Record<string, GlobalBlackboardValue> = {};
|
||||||
for (const [, variable] of this._variables) {
|
for (const [, variable] of this.variables) {
|
||||||
map[variable.key] = variable.defaultValue;
|
map[variable.key] = variable.defaultValue;
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
@@ -129,15 +129,15 @@ export class GlobalBlackboardService {
|
|||||||
* 检查变量是否存在
|
* 检查变量是否存在
|
||||||
*/
|
*/
|
||||||
hasVariable(key: string): boolean {
|
hasVariable(key: string): boolean {
|
||||||
return this._variables.has(key);
|
return this.variables.has(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清空所有变量
|
* 清空所有变量
|
||||||
*/
|
*/
|
||||||
clear(): void {
|
clear(): void {
|
||||||
this._variables.clear();
|
this.variables.clear();
|
||||||
this._notifyChange();
|
this.notifyChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -146,7 +146,7 @@ export class GlobalBlackboardService {
|
|||||||
toConfig(): GlobalBlackboardConfig {
|
toConfig(): GlobalBlackboardConfig {
|
||||||
const variables: BlackboardVariable[] = [];
|
const variables: BlackboardVariable[] = [];
|
||||||
|
|
||||||
for (const variable of this._variables.values()) {
|
for (const variable of this.variables.values()) {
|
||||||
variables.push({
|
variables.push({
|
||||||
name: variable.key,
|
name: variable.key,
|
||||||
type: variable.type,
|
type: variable.type,
|
||||||
@@ -162,11 +162,11 @@ export class GlobalBlackboardService {
|
|||||||
* 从配置导入
|
* 从配置导入
|
||||||
*/
|
*/
|
||||||
fromConfig(config: GlobalBlackboardConfig): void {
|
fromConfig(config: GlobalBlackboardConfig): void {
|
||||||
this._variables.clear();
|
this.variables.clear();
|
||||||
|
|
||||||
if (config.variables && Array.isArray(config.variables)) {
|
if (config.variables && Array.isArray(config.variables)) {
|
||||||
for (const variable of config.variables) {
|
for (const variable of config.variables) {
|
||||||
this._variables.set(variable.name, {
|
this.variables.set(variable.name, {
|
||||||
key: variable.name,
|
key: variable.name,
|
||||||
type: variable.type,
|
type: variable.type,
|
||||||
defaultValue: variable.value as GlobalBlackboardValue,
|
defaultValue: variable.value as GlobalBlackboardValue,
|
||||||
@@ -175,7 +175,7 @@ export class GlobalBlackboardService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this._notifyChange();
|
this.notifyChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -202,17 +202,17 @@ export class GlobalBlackboardService {
|
|||||||
* 监听变化
|
* 监听变化
|
||||||
*/
|
*/
|
||||||
onChange(callback: () => void): () => void {
|
onChange(callback: () => void): () => void {
|
||||||
this._changeCallbacks.push(callback);
|
this.changeCallbacks.push(callback);
|
||||||
return () => {
|
return () => {
|
||||||
const index = this._changeCallbacks.indexOf(callback);
|
const index = this.changeCallbacks.indexOf(callback);
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
this._changeCallbacks.splice(index, 1);
|
this.changeCallbacks.splice(index, 1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private _notifyChange(): void {
|
private notifyChange(): void {
|
||||||
this._changeCallbacks.forEach((cb) => {
|
this.changeCallbacks.forEach((cb) => {
|
||||||
try {
|
try {
|
||||||
cb();
|
cb();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -536,15 +536,15 @@ export const useBehaviorTreeDataStore = create<BehaviorTreeDataState>((set, get)
|
|||||||
* 将 Zustand Store 适配为 ITreeState 接口
|
* 将 Zustand Store 适配为 ITreeState 接口
|
||||||
*/
|
*/
|
||||||
export class TreeStateAdapter implements ITreeState {
|
export class TreeStateAdapter implements ITreeState {
|
||||||
private static _instance: TreeStateAdapter | null = null;
|
private static instance: TreeStateAdapter | null = null;
|
||||||
|
|
||||||
private constructor() {}
|
private constructor() {}
|
||||||
|
|
||||||
static getInstance(): TreeStateAdapter {
|
static getInstance(): TreeStateAdapter {
|
||||||
if (!TreeStateAdapter._instance) {
|
if (!TreeStateAdapter.instance) {
|
||||||
TreeStateAdapter._instance = new TreeStateAdapter();
|
TreeStateAdapter.instance = new TreeStateAdapter();
|
||||||
}
|
}
|
||||||
return TreeStateAdapter._instance;
|
return TreeStateAdapter.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
getTree(): BehaviorTree {
|
getTree(): BehaviorTree {
|
||||||
|
|||||||
@@ -36,63 +36,63 @@ export interface NodePropertyConfig {
|
|||||||
* 提供编辑器级别的节点注册和管理功能
|
* 提供编辑器级别的节点注册和管理功能
|
||||||
*/
|
*/
|
||||||
export class NodeRegistryService {
|
export class NodeRegistryService {
|
||||||
private static _instance: NodeRegistryService;
|
private static instance: NodeRegistryService;
|
||||||
private _customTemplates: Map<string, NodeTemplate> = new Map();
|
private customTemplates: Map<string, NodeTemplate> = new Map();
|
||||||
private _registrationCallbacks: Array<(template: NodeTemplate) => void> = [];
|
private registrationCallbacks: Array<(template: NodeTemplate) => void> = [];
|
||||||
|
|
||||||
private constructor() {}
|
private constructor() {}
|
||||||
|
|
||||||
static getInstance(): NodeRegistryService {
|
static getInstance(): NodeRegistryService {
|
||||||
if (!this._instance) {
|
if (!this.instance) {
|
||||||
this._instance = new NodeRegistryService();
|
this.instance = new NodeRegistryService();
|
||||||
}
|
}
|
||||||
return this._instance;
|
return this.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 注册自定义节点类型
|
* 注册自定义节点类型
|
||||||
*/
|
*/
|
||||||
registerNode(config: NodeRegistrationConfig): void {
|
registerNode(config: NodeRegistrationConfig): void {
|
||||||
const nodeType = this._mapStringToNodeType(config.type);
|
const nodeType = this.mapStringToNodeType(config.type);
|
||||||
|
|
||||||
const metadata: NodeMetadata = {
|
const metadata: NodeMetadata = {
|
||||||
implementationType: config.implementationType,
|
implementationType: config.implementationType,
|
||||||
nodeType: nodeType,
|
nodeType: nodeType,
|
||||||
displayName: config.displayName,
|
displayName: config.displayName,
|
||||||
description: config.description || '',
|
description: config.description || '',
|
||||||
category: config.category || this._getDefaultCategory(config.type),
|
category: config.category || this.getDefaultCategory(config.type),
|
||||||
configSchema: this._convertPropertiesToSchema(config.properties || []),
|
configSchema: this.convertPropertiesToSchema(config.properties || []),
|
||||||
childrenConstraints: this._getChildrenConstraints(config)
|
childrenConstraints: this.getChildrenConstraints(config)
|
||||||
};
|
};
|
||||||
|
|
||||||
class DummyExecutor {}
|
class DummyExecutor {}
|
||||||
NodeMetadataRegistry.register(DummyExecutor, metadata);
|
NodeMetadataRegistry.register(DummyExecutor, metadata);
|
||||||
|
|
||||||
const template = this._createTemplate(config, metadata);
|
const template = this.createTemplate(config, metadata);
|
||||||
this._customTemplates.set(config.implementationType, template);
|
this.customTemplates.set(config.implementationType, template);
|
||||||
|
|
||||||
this._registrationCallbacks.forEach((cb) => cb(template));
|
this.registrationCallbacks.forEach((cb) => cb(template));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 注销节点类型
|
* 注销节点类型
|
||||||
*/
|
*/
|
||||||
unregisterNode(implementationType: string): boolean {
|
unregisterNode(implementationType: string): boolean {
|
||||||
return this._customTemplates.delete(implementationType);
|
return this.customTemplates.delete(implementationType);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取所有自定义模板
|
* 获取所有自定义模板
|
||||||
*/
|
*/
|
||||||
getCustomTemplates(): NodeTemplate[] {
|
getCustomTemplates(): NodeTemplate[] {
|
||||||
return Array.from(this._customTemplates.values());
|
return Array.from(this.customTemplates.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查节点类型是否已注册
|
* 检查节点类型是否已注册
|
||||||
*/
|
*/
|
||||||
hasNode(implementationType: string): boolean {
|
hasNode(implementationType: string): boolean {
|
||||||
return this._customTemplates.has(implementationType) ||
|
return this.customTemplates.has(implementationType) ||
|
||||||
NodeMetadataRegistry.getMetadata(implementationType) !== undefined;
|
NodeMetadataRegistry.getMetadata(implementationType) !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,16 +100,16 @@ export class NodeRegistryService {
|
|||||||
* 监听节点注册事件
|
* 监听节点注册事件
|
||||||
*/
|
*/
|
||||||
onNodeRegistered(callback: (template: NodeTemplate) => void): () => void {
|
onNodeRegistered(callback: (template: NodeTemplate) => void): () => void {
|
||||||
this._registrationCallbacks.push(callback);
|
this.registrationCallbacks.push(callback);
|
||||||
return () => {
|
return () => {
|
||||||
const index = this._registrationCallbacks.indexOf(callback);
|
const index = this.registrationCallbacks.indexOf(callback);
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
this._registrationCallbacks.splice(index, 1);
|
this.registrationCallbacks.splice(index, 1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private _mapStringToNodeType(type: string): NodeType {
|
private mapStringToNodeType(type: string): NodeType {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'composite': return NodeType.Composite;
|
case 'composite': return NodeType.Composite;
|
||||||
case 'decorator': return NodeType.Decorator;
|
case 'decorator': return NodeType.Decorator;
|
||||||
@@ -119,7 +119,7 @@ export class NodeRegistryService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getDefaultCategory(type: string): string {
|
private getDefaultCategory(type: string): string {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'composite': return '组合';
|
case 'composite': return '组合';
|
||||||
case 'decorator': return '装饰器';
|
case 'decorator': return '装饰器';
|
||||||
@@ -129,12 +129,12 @@ export class NodeRegistryService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _convertPropertiesToSchema(properties: NodePropertyConfig[]): Record<string, any> {
|
private convertPropertiesToSchema(properties: NodePropertyConfig[]): Record<string, any> {
|
||||||
const schema: Record<string, any> = {};
|
const schema: Record<string, any> = {};
|
||||||
|
|
||||||
for (const prop of properties) {
|
for (const prop of properties) {
|
||||||
schema[prop.name] = {
|
schema[prop.name] = {
|
||||||
type: this._mapPropertyType(prop.type),
|
type: this.mapPropertyType(prop.type),
|
||||||
default: prop.defaultValue,
|
default: prop.defaultValue,
|
||||||
description: prop.description,
|
description: prop.description,
|
||||||
min: prop.min,
|
min: prop.min,
|
||||||
@@ -146,7 +146,7 @@ export class NodeRegistryService {
|
|||||||
return schema;
|
return schema;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _mapPropertyType(type: string): string {
|
private mapPropertyType(type: string): string {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'string':
|
case 'string':
|
||||||
case 'code':
|
case 'code':
|
||||||
@@ -162,7 +162,7 @@ export class NodeRegistryService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getChildrenConstraints(config: NodeRegistrationConfig): { min?: number; max?: number } | undefined {
|
private getChildrenConstraints(config: NodeRegistrationConfig): { min?: number; max?: number } | undefined {
|
||||||
if (config.minChildren !== undefined || config.maxChildren !== undefined) {
|
if (config.minChildren !== undefined || config.maxChildren !== undefined) {
|
||||||
return {
|
return {
|
||||||
min: config.minChildren,
|
min: config.minChildren,
|
||||||
@@ -183,7 +183,7 @@ export class NodeRegistryService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _createTemplate(config: NodeRegistrationConfig, metadata: NodeMetadata): NodeTemplate {
|
private createTemplate(config: NodeRegistrationConfig, metadata: NodeMetadata): NodeTemplate {
|
||||||
const defaultConfig: any = {
|
const defaultConfig: any = {
|
||||||
nodeType: config.type
|
nodeType: config.type
|
||||||
};
|
};
|
||||||
@@ -212,10 +212,10 @@ export class NodeRegistryService {
|
|||||||
const template: NodeTemplate = {
|
const template: NodeTemplate = {
|
||||||
type: metadata.nodeType,
|
type: metadata.nodeType,
|
||||||
displayName: config.displayName,
|
displayName: config.displayName,
|
||||||
category: config.category || this._getDefaultCategory(config.type),
|
category: config.category || this.getDefaultCategory(config.type),
|
||||||
description: config.description || '',
|
description: config.description || '',
|
||||||
icon: config.icon || this._getDefaultIcon(config.type),
|
icon: config.icon || this.getDefaultIcon(config.type),
|
||||||
color: config.color || this._getDefaultColor(config.type),
|
color: config.color || this.getDefaultColor(config.type),
|
||||||
className: config.implementationType,
|
className: config.implementationType,
|
||||||
defaultConfig,
|
defaultConfig,
|
||||||
properties: (config.properties || []).map((p) => ({
|
properties: (config.properties || []).map((p) => ({
|
||||||
@@ -242,7 +242,7 @@ export class NodeRegistryService {
|
|||||||
return template;
|
return template;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getDefaultIcon(type: string): string {
|
private getDefaultIcon(type: string): string {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'composite': return 'GitBranch';
|
case 'composite': return 'GitBranch';
|
||||||
case 'decorator': return 'Settings';
|
case 'decorator': return 'Settings';
|
||||||
@@ -252,7 +252,7 @@ export class NodeRegistryService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getDefaultColor(type: string): string {
|
private getDefaultColor(type: string): string {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'composite': return '#1976d2';
|
case 'composite': return '#1976d2';
|
||||||
case 'decorator': return '#fb8c00';
|
case 'decorator': return '#fb8c00';
|
||||||
|
|||||||
@@ -4,33 +4,33 @@ import type { MessageHub } from '@esengine/editor-runtime';
|
|||||||
const logger = createLogger('NotificationService');
|
const logger = createLogger('NotificationService');
|
||||||
|
|
||||||
export class NotificationService {
|
export class NotificationService {
|
||||||
private static _instance: NotificationService;
|
private static instance: NotificationService;
|
||||||
private _messageHub: MessageHub | null = null;
|
private messageHub: MessageHub | null = null;
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
// 延迟获取 MessageHub,因为初始化时可能还不可用
|
// 延迟获取 MessageHub,因为初始化时可能还不可用
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getMessageHub(): MessageHub | null {
|
private getMessageHub(): MessageHub | null {
|
||||||
if (!this._messageHub && PluginAPI.isAvailable) {
|
if (!this.messageHub && PluginAPI.isAvailable) {
|
||||||
try {
|
try {
|
||||||
this._messageHub = PluginAPI.messageHub;
|
this.messageHub = PluginAPI.messageHub;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn('MessageHub not available');
|
logger.warn('MessageHub not available');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return this._messageHub;
|
return this.messageHub;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static getInstance(): NotificationService {
|
public static getInstance(): NotificationService {
|
||||||
if (!NotificationService._instance) {
|
if (!NotificationService.instance) {
|
||||||
NotificationService._instance = new NotificationService();
|
NotificationService.instance = new NotificationService();
|
||||||
}
|
}
|
||||||
return NotificationService._instance;
|
return NotificationService.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public showToast(message: string, type: 'success' | 'error' | 'warning' | 'info' = 'info'): void {
|
public showToast(message: string, type: 'success' | 'error' | 'warning' | 'info' = 'info'): void {
|
||||||
const hub = this._getMessageHub();
|
const hub = this.getMessageHub();
|
||||||
if (!hub) {
|
if (!hub) {
|
||||||
logger.info(`[Toast ${type}] ${message}`);
|
logger.info(`[Toast ${type}] ${message}`);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
{ "path": "../core" },
|
{ "path": "../core" },
|
||||||
{ "path": "../engine-core" },
|
{ "path": "../engine-core" },
|
||||||
{ "path": "../editor-core" },
|
{ "path": "../editor-core" },
|
||||||
|
{ "path": "../ui" },
|
||||||
{ "path": "../editor-runtime" }
|
{ "path": "../editor-runtime" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "blueprint-editor",
|
|
||||||
"name": "@esengine/blueprint-editor",
|
|
||||||
"displayName": "Blueprint Editor",
|
|
||||||
"description": "Visual scripting editor | 可视化脚本编辑器",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"category": "Editor",
|
|
||||||
"icon": "Workflow",
|
|
||||||
"isEditorPlugin": true,
|
|
||||||
"runtimeModule": "@esengine/blueprint",
|
|
||||||
"exports": {
|
|
||||||
"inspectors": ["BlueprintComponentInspector"],
|
|
||||||
"panels": ["BlueprintEditorPanel"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -24,9 +24,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@esengine/blueprint": "workspace:*"
|
"@esengine/blueprint": "workspace:*"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
|
||||||
"@esengine/editor-core": "workspace:*"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@esengine/ecs-framework": "workspace:*",
|
"@esengine/ecs-framework": "workspace:*",
|
||||||
"@esengine/engine-core": "workspace:*",
|
"@esengine/engine-core": "workspace:*",
|
||||||
|
|||||||
@@ -1,575 +0,0 @@
|
|||||||
/**
|
|
||||||
* @zh 蓝图组合器接口和实现
|
|
||||||
* @en Blueprint Composer Interface and Implementation
|
|
||||||
*
|
|
||||||
* @zh 将多个蓝图片段组合成一个完整的蓝图
|
|
||||||
* @en Composes multiple blueprint fragments into a complete blueprint
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { BlueprintAsset, BlueprintVariable } from '../types/blueprint';
|
|
||||||
import type { BlueprintNode, BlueprintConnection } from '../types/nodes';
|
|
||||||
import type { IBlueprintFragment } from './BlueprintFragment';
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 槽位定义 | Slot Definition
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 片段槽位
|
|
||||||
* @en Fragment slot
|
|
||||||
*
|
|
||||||
* @zh 组合器中放置片段的位置
|
|
||||||
* @en A position in the composer where a fragment is placed
|
|
||||||
*/
|
|
||||||
export interface FragmentSlot {
|
|
||||||
/**
|
|
||||||
* @zh 槽位 ID
|
|
||||||
* @en Slot ID
|
|
||||||
*/
|
|
||||||
readonly id: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 槽位名称
|
|
||||||
* @en Slot name
|
|
||||||
*/
|
|
||||||
readonly name: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 放置的片段
|
|
||||||
* @en Placed fragment
|
|
||||||
*/
|
|
||||||
readonly fragment: IBlueprintFragment;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 在组合图中的位置偏移
|
|
||||||
* @en Position offset in the composed graph
|
|
||||||
*/
|
|
||||||
readonly position: { x: number; y: number };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 槽位间连接
|
|
||||||
* @en Connection between slots
|
|
||||||
*/
|
|
||||||
export interface SlotConnection {
|
|
||||||
/**
|
|
||||||
* @zh 连接 ID
|
|
||||||
* @en Connection ID
|
|
||||||
*/
|
|
||||||
readonly id: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 源槽位 ID
|
|
||||||
* @en Source slot ID
|
|
||||||
*/
|
|
||||||
readonly fromSlotId: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 源引脚名称
|
|
||||||
* @en Source pin name
|
|
||||||
*/
|
|
||||||
readonly fromPin: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 目标槽位 ID
|
|
||||||
* @en Target slot ID
|
|
||||||
*/
|
|
||||||
readonly toSlotId: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 目标引脚名称
|
|
||||||
* @en Target pin name
|
|
||||||
*/
|
|
||||||
readonly toPin: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 组合器接口 | Composer Interface
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 蓝图组合器接口
|
|
||||||
* @en Blueprint composer interface
|
|
||||||
*
|
|
||||||
* @zh 用于将多个蓝图片段组合成一个完整蓝图
|
|
||||||
* @en Used to compose multiple blueprint fragments into a complete blueprint
|
|
||||||
*/
|
|
||||||
export interface IBlueprintComposer {
|
|
||||||
/**
|
|
||||||
* @zh 组合器名称
|
|
||||||
* @en Composer name
|
|
||||||
*/
|
|
||||||
readonly name: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 获取所有槽位
|
|
||||||
* @en Get all slots
|
|
||||||
*/
|
|
||||||
getSlots(): FragmentSlot[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 获取所有连接
|
|
||||||
* @en Get all connections
|
|
||||||
*/
|
|
||||||
getConnections(): SlotConnection[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 添加片段到槽位
|
|
||||||
* @en Add fragment to slot
|
|
||||||
*
|
|
||||||
* @param fragment - @zh 蓝图片段 @en Blueprint fragment
|
|
||||||
* @param slotId - @zh 槽位 ID @en Slot ID
|
|
||||||
* @param options - @zh 选项 @en Options
|
|
||||||
*/
|
|
||||||
addFragment(
|
|
||||||
fragment: IBlueprintFragment,
|
|
||||||
slotId: string,
|
|
||||||
options?: {
|
|
||||||
name?: string;
|
|
||||||
position?: { x: number; y: number };
|
|
||||||
}
|
|
||||||
): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 移除槽位
|
|
||||||
* @en Remove slot
|
|
||||||
*
|
|
||||||
* @param slotId - @zh 槽位 ID @en Slot ID
|
|
||||||
*/
|
|
||||||
removeSlot(slotId: string): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 连接两个槽位的引脚
|
|
||||||
* @en Connect pins between two slots
|
|
||||||
*
|
|
||||||
* @param fromSlotId - @zh 源槽位 ID @en Source slot ID
|
|
||||||
* @param fromPin - @zh 源引脚名称 @en Source pin name
|
|
||||||
* @param toSlotId - @zh 目标槽位 ID @en Target slot ID
|
|
||||||
* @param toPin - @zh 目标引脚名称 @en Target pin name
|
|
||||||
*/
|
|
||||||
connect(
|
|
||||||
fromSlotId: string,
|
|
||||||
fromPin: string,
|
|
||||||
toSlotId: string,
|
|
||||||
toPin: string
|
|
||||||
): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 断开连接
|
|
||||||
* @en Disconnect
|
|
||||||
*
|
|
||||||
* @param connectionId - @zh 连接 ID @en Connection ID
|
|
||||||
*/
|
|
||||||
disconnect(connectionId: string): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 验证组合是否有效
|
|
||||||
* @en Validate if the composition is valid
|
|
||||||
*/
|
|
||||||
validate(): CompositionValidationResult;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 编译成蓝图资产
|
|
||||||
* @en Compile into blueprint asset
|
|
||||||
*/
|
|
||||||
compile(): BlueprintAsset;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 清空组合器
|
|
||||||
* @en Clear the composer
|
|
||||||
*/
|
|
||||||
clear(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 验证结果 | Validation Result
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 组合验证结果
|
|
||||||
* @en Composition validation result
|
|
||||||
*/
|
|
||||||
export interface CompositionValidationResult {
|
|
||||||
/**
|
|
||||||
* @zh 是否有效
|
|
||||||
* @en Whether valid
|
|
||||||
*/
|
|
||||||
readonly isValid: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 错误列表
|
|
||||||
* @en Error list
|
|
||||||
*/
|
|
||||||
readonly errors: CompositionError[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 警告列表
|
|
||||||
* @en Warning list
|
|
||||||
*/
|
|
||||||
readonly warnings: CompositionWarning[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 组合错误
|
|
||||||
* @en Composition error
|
|
||||||
*/
|
|
||||||
export interface CompositionError {
|
|
||||||
readonly type: 'missing-connection' | 'type-mismatch' | 'cycle-detected' | 'invalid-slot';
|
|
||||||
readonly message: string;
|
|
||||||
readonly slotId?: string;
|
|
||||||
readonly pinName?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 组合警告
|
|
||||||
* @en Composition warning
|
|
||||||
*/
|
|
||||||
export interface CompositionWarning {
|
|
||||||
readonly type: 'unused-output' | 'unconnected-input';
|
|
||||||
readonly message: string;
|
|
||||||
readonly slotId?: string;
|
|
||||||
readonly pinName?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 组合器实现 | Composer Implementation
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 蓝图组合器实现
|
|
||||||
* @en Blueprint composer implementation
|
|
||||||
*/
|
|
||||||
export class BlueprintComposer implements IBlueprintComposer {
|
|
||||||
readonly name: string;
|
|
||||||
|
|
||||||
private slots: Map<string, FragmentSlot> = new Map();
|
|
||||||
private connections: Map<string, SlotConnection> = new Map();
|
|
||||||
private connectionIdCounter = 0;
|
|
||||||
|
|
||||||
constructor(name: string) {
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
getSlots(): FragmentSlot[] {
|
|
||||||
return Array.from(this.slots.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
getConnections(): SlotConnection[] {
|
|
||||||
return Array.from(this.connections.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
addFragment(
|
|
||||||
fragment: IBlueprintFragment,
|
|
||||||
slotId: string,
|
|
||||||
options?: {
|
|
||||||
name?: string;
|
|
||||||
position?: { x: number; y: number };
|
|
||||||
}
|
|
||||||
): void {
|
|
||||||
if (this.slots.has(slotId)) {
|
|
||||||
throw new Error(`Slot '${slotId}' already exists`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const slot: FragmentSlot = {
|
|
||||||
id: slotId,
|
|
||||||
name: options?.name ?? fragment.name,
|
|
||||||
fragment,
|
|
||||||
position: options?.position ?? { x: 0, y: 0 }
|
|
||||||
};
|
|
||||||
|
|
||||||
this.slots.set(slotId, slot);
|
|
||||||
}
|
|
||||||
|
|
||||||
removeSlot(slotId: string): void {
|
|
||||||
if (!this.slots.has(slotId)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove all connections involving this slot
|
|
||||||
const toRemove: string[] = [];
|
|
||||||
for (const [id, conn] of this.connections) {
|
|
||||||
if (conn.fromSlotId === slotId || conn.toSlotId === slotId) {
|
|
||||||
toRemove.push(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const id of toRemove) {
|
|
||||||
this.connections.delete(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.slots.delete(slotId);
|
|
||||||
}
|
|
||||||
|
|
||||||
connect(
|
|
||||||
fromSlotId: string,
|
|
||||||
fromPin: string,
|
|
||||||
toSlotId: string,
|
|
||||||
toPin: string
|
|
||||||
): void {
|
|
||||||
const fromSlot = this.slots.get(fromSlotId);
|
|
||||||
const toSlot = this.slots.get(toSlotId);
|
|
||||||
|
|
||||||
if (!fromSlot) {
|
|
||||||
throw new Error(`Source slot '${fromSlotId}' not found`);
|
|
||||||
}
|
|
||||||
if (!toSlot) {
|
|
||||||
throw new Error(`Target slot '${toSlotId}' not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fromPinDef = fromSlot.fragment.outputs.find(p => p.name === fromPin);
|
|
||||||
const toPinDef = toSlot.fragment.inputs.find(p => p.name === toPin);
|
|
||||||
|
|
||||||
if (!fromPinDef) {
|
|
||||||
throw new Error(`Output pin '${fromPin}' not found in slot '${fromSlotId}'`);
|
|
||||||
}
|
|
||||||
if (!toPinDef) {
|
|
||||||
throw new Error(`Input pin '${toPin}' not found in slot '${toSlotId}'`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const connectionId = `conn_${++this.connectionIdCounter}`;
|
|
||||||
|
|
||||||
const connection: SlotConnection = {
|
|
||||||
id: connectionId,
|
|
||||||
fromSlotId,
|
|
||||||
fromPin,
|
|
||||||
toSlotId,
|
|
||||||
toPin
|
|
||||||
};
|
|
||||||
|
|
||||||
this.connections.set(connectionId, connection);
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnect(connectionId: string): void {
|
|
||||||
this.connections.delete(connectionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
validate(): CompositionValidationResult {
|
|
||||||
const errors: CompositionError[] = [];
|
|
||||||
const warnings: CompositionWarning[] = [];
|
|
||||||
|
|
||||||
// Check for required inputs without connections
|
|
||||||
for (const slot of this.slots.values()) {
|
|
||||||
for (const input of slot.fragment.inputs) {
|
|
||||||
const hasConnection = Array.from(this.connections.values()).some(
|
|
||||||
c => c.toSlotId === slot.id && c.toPin === input.name
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!hasConnection && input.defaultValue === undefined) {
|
|
||||||
warnings.push({
|
|
||||||
type: 'unconnected-input',
|
|
||||||
message: `Input '${input.name}' in slot '${slot.id}' is not connected`,
|
|
||||||
slotId: slot.id,
|
|
||||||
pinName: input.name
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for unused outputs
|
|
||||||
for (const output of slot.fragment.outputs) {
|
|
||||||
const hasConnection = Array.from(this.connections.values()).some(
|
|
||||||
c => c.fromSlotId === slot.id && c.fromPin === output.name
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!hasConnection) {
|
|
||||||
warnings.push({
|
|
||||||
type: 'unused-output',
|
|
||||||
message: `Output '${output.name}' in slot '${slot.id}' is not connected`,
|
|
||||||
slotId: slot.id,
|
|
||||||
pinName: output.name
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check type compatibility
|
|
||||||
for (const conn of this.connections.values()) {
|
|
||||||
const fromSlot = this.slots.get(conn.fromSlotId);
|
|
||||||
const toSlot = this.slots.get(conn.toSlotId);
|
|
||||||
|
|
||||||
if (!fromSlot || !toSlot) {
|
|
||||||
errors.push({
|
|
||||||
type: 'invalid-slot',
|
|
||||||
message: `Invalid slot reference in connection '${conn.id}'`
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fromPinDef = fromSlot.fragment.outputs.find(p => p.name === conn.fromPin);
|
|
||||||
const toPinDef = toSlot.fragment.inputs.find(p => p.name === conn.toPin);
|
|
||||||
|
|
||||||
if (fromPinDef && toPinDef && fromPinDef.type !== toPinDef.type) {
|
|
||||||
if (fromPinDef.type !== 'any' && toPinDef.type !== 'any') {
|
|
||||||
errors.push({
|
|
||||||
type: 'type-mismatch',
|
|
||||||
message: `Type mismatch: '${fromPinDef.type}' -> '${toPinDef.type}' in connection '${conn.id}'`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
isValid: errors.length === 0,
|
|
||||||
errors,
|
|
||||||
warnings
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
compile(): BlueprintAsset {
|
|
||||||
const nodes: BlueprintNode[] = [];
|
|
||||||
const connections: BlueprintConnection[] = [];
|
|
||||||
const variables: BlueprintVariable[] = [];
|
|
||||||
|
|
||||||
const nodeIdMap = new Map<string, Map<string, string>>();
|
|
||||||
|
|
||||||
// Copy nodes from each fragment with new IDs
|
|
||||||
let nodeIdCounter = 0;
|
|
||||||
for (const slot of this.slots.values()) {
|
|
||||||
const slotNodeMap = new Map<string, string>();
|
|
||||||
nodeIdMap.set(slot.id, slotNodeMap);
|
|
||||||
|
|
||||||
for (const node of slot.fragment.graph.nodes) {
|
|
||||||
const newNodeId = `node_${++nodeIdCounter}`;
|
|
||||||
slotNodeMap.set(node.id, newNodeId);
|
|
||||||
|
|
||||||
nodes.push({
|
|
||||||
...node,
|
|
||||||
id: newNodeId,
|
|
||||||
position: {
|
|
||||||
x: node.position.x + slot.position.x,
|
|
||||||
y: node.position.y + slot.position.y
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy internal connections
|
|
||||||
for (const conn of slot.fragment.graph.connections) {
|
|
||||||
const newFromId = slotNodeMap.get(conn.fromNodeId);
|
|
||||||
const newToId = slotNodeMap.get(conn.toNodeId);
|
|
||||||
|
|
||||||
if (newFromId && newToId) {
|
|
||||||
connections.push({
|
|
||||||
...conn,
|
|
||||||
id: `conn_internal_${connections.length}`,
|
|
||||||
fromNodeId: newFromId,
|
|
||||||
toNodeId: newToId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy variables (with slot prefix to avoid conflicts)
|
|
||||||
for (const variable of slot.fragment.graph.variables) {
|
|
||||||
variables.push({
|
|
||||||
...variable,
|
|
||||||
name: `${slot.id}_${variable.name}`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create connections between slots based on exposed pins
|
|
||||||
for (const slotConn of this.connections.values()) {
|
|
||||||
const fromSlot = this.slots.get(slotConn.fromSlotId);
|
|
||||||
const toSlot = this.slots.get(slotConn.toSlotId);
|
|
||||||
|
|
||||||
if (!fromSlot || !toSlot) continue;
|
|
||||||
|
|
||||||
const fromPinDef = fromSlot.fragment.outputs.find(p => p.name === slotConn.fromPin);
|
|
||||||
const toPinDef = toSlot.fragment.inputs.find(p => p.name === slotConn.toPin);
|
|
||||||
|
|
||||||
if (!fromPinDef || !toPinDef) continue;
|
|
||||||
|
|
||||||
const fromNodeMap = nodeIdMap.get(slotConn.fromSlotId);
|
|
||||||
const toNodeMap = nodeIdMap.get(slotConn.toSlotId);
|
|
||||||
|
|
||||||
if (!fromNodeMap || !toNodeMap) continue;
|
|
||||||
|
|
||||||
const fromNodeId = fromNodeMap.get(fromPinDef.internalNodeId);
|
|
||||||
const toNodeId = toNodeMap.get(toPinDef.internalNodeId);
|
|
||||||
|
|
||||||
if (fromNodeId && toNodeId) {
|
|
||||||
connections.push({
|
|
||||||
id: `conn_slot_${connections.length}`,
|
|
||||||
fromNodeId,
|
|
||||||
fromPin: fromPinDef.internalPinName,
|
|
||||||
toNodeId,
|
|
||||||
toPin: toPinDef.internalPinName
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
version: 1,
|
|
||||||
type: 'blueprint',
|
|
||||||
metadata: {
|
|
||||||
name: this.name,
|
|
||||||
description: `Composed from ${this.slots.size} fragments`,
|
|
||||||
createdAt: Date.now(),
|
|
||||||
modifiedAt: Date.now()
|
|
||||||
},
|
|
||||||
variables,
|
|
||||||
nodes,
|
|
||||||
connections
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
clear(): void {
|
|
||||||
this.slots.clear();
|
|
||||||
this.connections.clear();
|
|
||||||
this.connectionIdCounter = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 工厂函数 | Factory Functions
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 创建蓝图组合器
|
|
||||||
* @en Create blueprint composer
|
|
||||||
*/
|
|
||||||
export function createComposer(name: string): IBlueprintComposer {
|
|
||||||
return new BlueprintComposer(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 组合资产格式 | Composition Asset Format
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 蓝图组合资产格式
|
|
||||||
* @en Blueprint composition asset format
|
|
||||||
*/
|
|
||||||
export interface BlueprintCompositionAsset {
|
|
||||||
/**
|
|
||||||
* @zh 格式版本
|
|
||||||
* @en Format version
|
|
||||||
*/
|
|
||||||
version: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 资产类型标识
|
|
||||||
* @en Asset type identifier
|
|
||||||
*/
|
|
||||||
type: 'blueprint-composition';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 组合名称
|
|
||||||
* @en Composition name
|
|
||||||
*/
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 槽位数据
|
|
||||||
* @en Slot data
|
|
||||||
*/
|
|
||||||
slots: Array<{
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
fragmentId: string;
|
|
||||||
position: { x: number; y: number };
|
|
||||||
}>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 连接数据
|
|
||||||
* @en Connection data
|
|
||||||
*/
|
|
||||||
connections: SlotConnection[];
|
|
||||||
}
|
|
||||||
@@ -1,351 +0,0 @@
|
|||||||
/**
|
|
||||||
* @zh 蓝图片段接口和实现
|
|
||||||
* @en Blueprint Fragment Interface and Implementation
|
|
||||||
*
|
|
||||||
* @zh 定义可重用的蓝图片段,用于组合系统
|
|
||||||
* @en Defines reusable blueprint fragments for the composition system
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { BlueprintAsset } from '../types/blueprint';
|
|
||||||
import type { BlueprintPinType } from '../types/pins';
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 暴露引脚定义 | Exposed Pin Definition
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 暴露引脚定义
|
|
||||||
* @en Exposed pin definition
|
|
||||||
*
|
|
||||||
* @zh 片段对外暴露的引脚,可与其他片段连接
|
|
||||||
* @en Pins exposed by the fragment that can be connected to other fragments
|
|
||||||
*/
|
|
||||||
export interface ExposedPin {
|
|
||||||
/**
|
|
||||||
* @zh 引脚名称
|
|
||||||
* @en Pin name
|
|
||||||
*/
|
|
||||||
readonly name: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 显示名称
|
|
||||||
* @en Display name
|
|
||||||
*/
|
|
||||||
readonly displayName: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 引脚类型
|
|
||||||
* @en Pin type
|
|
||||||
*/
|
|
||||||
readonly type: BlueprintPinType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 引脚方向
|
|
||||||
* @en Pin direction
|
|
||||||
*/
|
|
||||||
readonly direction: 'input' | 'output';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 描述
|
|
||||||
* @en Description
|
|
||||||
*/
|
|
||||||
readonly description?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 默认值(仅输入引脚)
|
|
||||||
* @en Default value (input pins only)
|
|
||||||
*/
|
|
||||||
readonly defaultValue?: unknown;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 关联的内部节点 ID
|
|
||||||
* @en Associated internal node ID
|
|
||||||
*/
|
|
||||||
readonly internalNodeId: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 关联的内部引脚名称
|
|
||||||
* @en Associated internal pin name
|
|
||||||
*/
|
|
||||||
readonly internalPinName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 蓝图片段接口 | Blueprint Fragment Interface
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 蓝图片段接口
|
|
||||||
* @en Blueprint fragment interface
|
|
||||||
*
|
|
||||||
* @zh 代表一个可重用的蓝图逻辑单元,如技能、卡牌效果等
|
|
||||||
* @en Represents a reusable unit of blueprint logic, such as skills, card effects, etc.
|
|
||||||
*/
|
|
||||||
export interface IBlueprintFragment {
|
|
||||||
/**
|
|
||||||
* @zh 片段唯一标识
|
|
||||||
* @en Fragment unique identifier
|
|
||||||
*/
|
|
||||||
readonly id: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 片段名称
|
|
||||||
* @en Fragment name
|
|
||||||
*/
|
|
||||||
readonly name: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 片段描述
|
|
||||||
* @en Fragment description
|
|
||||||
*/
|
|
||||||
readonly description?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 片段分类
|
|
||||||
* @en Fragment category
|
|
||||||
*/
|
|
||||||
readonly category?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 片段标签
|
|
||||||
* @en Fragment tags
|
|
||||||
*/
|
|
||||||
readonly tags?: string[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 暴露的输入引脚
|
|
||||||
* @en Exposed input pins
|
|
||||||
*/
|
|
||||||
readonly inputs: ExposedPin[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 暴露的输出引脚
|
|
||||||
* @en Exposed output pins
|
|
||||||
*/
|
|
||||||
readonly outputs: ExposedPin[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 内部蓝图图
|
|
||||||
* @en Internal blueprint graph
|
|
||||||
*/
|
|
||||||
readonly graph: BlueprintAsset;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 片段版本
|
|
||||||
* @en Fragment version
|
|
||||||
*/
|
|
||||||
readonly version?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 图标名称
|
|
||||||
* @en Icon name
|
|
||||||
*/
|
|
||||||
readonly icon?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 颜色(用于可视化)
|
|
||||||
* @en Color (for visualization)
|
|
||||||
*/
|
|
||||||
readonly color?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 蓝图片段实现 | Blueprint Fragment Implementation
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 蓝图片段配置
|
|
||||||
* @en Blueprint fragment configuration
|
|
||||||
*/
|
|
||||||
export interface BlueprintFragmentConfig {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description?: string;
|
|
||||||
category?: string;
|
|
||||||
tags?: string[];
|
|
||||||
inputs?: ExposedPin[];
|
|
||||||
outputs?: ExposedPin[];
|
|
||||||
graph: BlueprintAsset;
|
|
||||||
version?: string;
|
|
||||||
icon?: string;
|
|
||||||
color?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 蓝图片段实现
|
|
||||||
* @en Blueprint fragment implementation
|
|
||||||
*/
|
|
||||||
export class BlueprintFragment implements IBlueprintFragment {
|
|
||||||
readonly id: string;
|
|
||||||
readonly name: string;
|
|
||||||
readonly description?: string;
|
|
||||||
readonly category?: string;
|
|
||||||
readonly tags?: string[];
|
|
||||||
readonly inputs: ExposedPin[];
|
|
||||||
readonly outputs: ExposedPin[];
|
|
||||||
readonly graph: BlueprintAsset;
|
|
||||||
readonly version?: string;
|
|
||||||
readonly icon?: string;
|
|
||||||
readonly color?: string;
|
|
||||||
|
|
||||||
constructor(config: BlueprintFragmentConfig) {
|
|
||||||
this.id = config.id;
|
|
||||||
this.name = config.name;
|
|
||||||
this.description = config.description;
|
|
||||||
this.category = config.category;
|
|
||||||
this.tags = config.tags;
|
|
||||||
this.inputs = config.inputs ?? [];
|
|
||||||
this.outputs = config.outputs ?? [];
|
|
||||||
this.graph = config.graph;
|
|
||||||
this.version = config.version;
|
|
||||||
this.icon = config.icon;
|
|
||||||
this.color = config.color;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 获取所有暴露引脚
|
|
||||||
* @en Get all exposed pins
|
|
||||||
*/
|
|
||||||
getAllExposedPins(): ExposedPin[] {
|
|
||||||
return [...this.inputs, ...this.outputs];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 通过名称查找输入引脚
|
|
||||||
* @en Find input pin by name
|
|
||||||
*/
|
|
||||||
findInput(name: string): ExposedPin | undefined {
|
|
||||||
return this.inputs.find(p => p.name === name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 通过名称查找输出引脚
|
|
||||||
* @en Find output pin by name
|
|
||||||
*/
|
|
||||||
findOutput(name: string): ExposedPin | undefined {
|
|
||||||
return this.outputs.find(p => p.name === name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 工厂函数 | Factory Functions
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 创建暴露引脚
|
|
||||||
* @en Create exposed pin
|
|
||||||
*/
|
|
||||||
export function createExposedPin(
|
|
||||||
name: string,
|
|
||||||
type: BlueprintPinType,
|
|
||||||
direction: 'input' | 'output',
|
|
||||||
internalNodeId: string,
|
|
||||||
internalPinName: string,
|
|
||||||
options?: {
|
|
||||||
displayName?: string;
|
|
||||||
description?: string;
|
|
||||||
defaultValue?: unknown;
|
|
||||||
}
|
|
||||||
): ExposedPin {
|
|
||||||
return {
|
|
||||||
name,
|
|
||||||
displayName: options?.displayName ?? name,
|
|
||||||
type,
|
|
||||||
direction,
|
|
||||||
description: options?.description,
|
|
||||||
defaultValue: options?.defaultValue,
|
|
||||||
internalNodeId,
|
|
||||||
internalPinName
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 创建蓝图片段
|
|
||||||
* @en Create blueprint fragment
|
|
||||||
*/
|
|
||||||
export function createFragment(config: BlueprintFragmentConfig): IBlueprintFragment {
|
|
||||||
return new BlueprintFragment(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 片段资产格式 | Fragment Asset Format
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 蓝图片段资产格式
|
|
||||||
* @en Blueprint fragment asset format
|
|
||||||
*
|
|
||||||
* @zh 用于序列化和反序列化片段
|
|
||||||
* @en Used for serializing and deserializing fragments
|
|
||||||
*/
|
|
||||||
export interface BlueprintFragmentAsset {
|
|
||||||
/**
|
|
||||||
* @zh 格式版本
|
|
||||||
* @en Format version
|
|
||||||
*/
|
|
||||||
version: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 资产类型标识
|
|
||||||
* @en Asset type identifier
|
|
||||||
*/
|
|
||||||
type: 'blueprint-fragment';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 片段数据
|
|
||||||
* @en Fragment data
|
|
||||||
*/
|
|
||||||
fragment: {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description?: string;
|
|
||||||
category?: string;
|
|
||||||
tags?: string[];
|
|
||||||
inputs: ExposedPin[];
|
|
||||||
outputs: ExposedPin[];
|
|
||||||
version?: string;
|
|
||||||
icon?: string;
|
|
||||||
color?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 内部蓝图图
|
|
||||||
* @en Internal blueprint graph
|
|
||||||
*/
|
|
||||||
graph: BlueprintAsset;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 从资产创建片段
|
|
||||||
* @en Create fragment from asset
|
|
||||||
*/
|
|
||||||
export function fragmentFromAsset(asset: BlueprintFragmentAsset): IBlueprintFragment {
|
|
||||||
return new BlueprintFragment({
|
|
||||||
...asset.fragment,
|
|
||||||
graph: asset.graph
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 将片段转为资产
|
|
||||||
* @en Convert fragment to asset
|
|
||||||
*/
|
|
||||||
export function fragmentToAsset(fragment: IBlueprintFragment): BlueprintFragmentAsset {
|
|
||||||
return {
|
|
||||||
version: 1,
|
|
||||||
type: 'blueprint-fragment',
|
|
||||||
fragment: {
|
|
||||||
id: fragment.id,
|
|
||||||
name: fragment.name,
|
|
||||||
description: fragment.description,
|
|
||||||
category: fragment.category,
|
|
||||||
tags: fragment.tags,
|
|
||||||
inputs: fragment.inputs,
|
|
||||||
outputs: fragment.outputs,
|
|
||||||
version: fragment.version,
|
|
||||||
icon: fragment.icon,
|
|
||||||
color: fragment.color
|
|
||||||
},
|
|
||||||
graph: fragment.graph
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,208 +0,0 @@
|
|||||||
/**
|
|
||||||
* @zh 片段注册表
|
|
||||||
* @en Fragment Registry
|
|
||||||
*
|
|
||||||
* @zh 管理和查询蓝图片段
|
|
||||||
* @en Manages and queries blueprint fragments
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { IBlueprintFragment } from './BlueprintFragment';
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 片段注册表接口 | Fragment Registry Interface
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 片段过滤器
|
|
||||||
* @en Fragment filter
|
|
||||||
*/
|
|
||||||
export interface FragmentFilter {
|
|
||||||
/**
|
|
||||||
* @zh 按分类过滤
|
|
||||||
* @en Filter by category
|
|
||||||
*/
|
|
||||||
category?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 按标签过滤(任意匹配)
|
|
||||||
* @en Filter by tags (any match)
|
|
||||||
*/
|
|
||||||
tags?: string[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 按名称搜索
|
|
||||||
* @en Search by name
|
|
||||||
*/
|
|
||||||
search?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 片段注册表接口
|
|
||||||
* @en Fragment registry interface
|
|
||||||
*/
|
|
||||||
export interface IFragmentRegistry {
|
|
||||||
/**
|
|
||||||
* @zh 注册片段
|
|
||||||
* @en Register fragment
|
|
||||||
*/
|
|
||||||
register(fragment: IBlueprintFragment): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 注销片段
|
|
||||||
* @en Unregister fragment
|
|
||||||
*/
|
|
||||||
unregister(id: string): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 获取片段
|
|
||||||
* @en Get fragment
|
|
||||||
*/
|
|
||||||
get(id: string): IBlueprintFragment | undefined;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 检查片段是否存在
|
|
||||||
* @en Check if fragment exists
|
|
||||||
*/
|
|
||||||
has(id: string): boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 获取所有片段
|
|
||||||
* @en Get all fragments
|
|
||||||
*/
|
|
||||||
getAll(): IBlueprintFragment[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 按条件过滤片段
|
|
||||||
* @en Filter fragments by criteria
|
|
||||||
*/
|
|
||||||
filter(filter: FragmentFilter): IBlueprintFragment[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 获取所有分类
|
|
||||||
* @en Get all categories
|
|
||||||
*/
|
|
||||||
getCategories(): string[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 获取所有标签
|
|
||||||
* @en Get all tags
|
|
||||||
*/
|
|
||||||
getTags(): string[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 清空注册表
|
|
||||||
* @en Clear registry
|
|
||||||
*/
|
|
||||||
clear(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 片段注册表实现 | Fragment Registry Implementation
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 片段注册表实现
|
|
||||||
* @en Fragment registry implementation
|
|
||||||
*/
|
|
||||||
export class FragmentRegistry implements IFragmentRegistry {
|
|
||||||
private fragments: Map<string, IBlueprintFragment> = new Map();
|
|
||||||
|
|
||||||
register(fragment: IBlueprintFragment): void {
|
|
||||||
if (this.fragments.has(fragment.id)) {
|
|
||||||
console.warn(`Fragment '${fragment.id}' already registered, overwriting`);
|
|
||||||
}
|
|
||||||
this.fragments.set(fragment.id, fragment);
|
|
||||||
}
|
|
||||||
|
|
||||||
unregister(id: string): void {
|
|
||||||
this.fragments.delete(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
get(id: string): IBlueprintFragment | undefined {
|
|
||||||
return this.fragments.get(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
has(id: string): boolean {
|
|
||||||
return this.fragments.has(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
getAll(): IBlueprintFragment[] {
|
|
||||||
return Array.from(this.fragments.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
filter(filter: FragmentFilter): IBlueprintFragment[] {
|
|
||||||
let results = this.getAll();
|
|
||||||
|
|
||||||
if (filter.category) {
|
|
||||||
results = results.filter(f => f.category === filter.category);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filter.tags && filter.tags.length > 0) {
|
|
||||||
results = results.filter(f =>
|
|
||||||
f.tags && filter.tags!.some(t => f.tags!.includes(t))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filter.search) {
|
|
||||||
const searchLower = filter.search.toLowerCase();
|
|
||||||
results = results.filter(f =>
|
|
||||||
f.name.toLowerCase().includes(searchLower) ||
|
|
||||||
f.description?.toLowerCase().includes(searchLower)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
getCategories(): string[] {
|
|
||||||
const categories = new Set<string>();
|
|
||||||
for (const fragment of this.fragments.values()) {
|
|
||||||
if (fragment.category) {
|
|
||||||
categories.add(fragment.category);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Array.from(categories).sort();
|
|
||||||
}
|
|
||||||
|
|
||||||
getTags(): string[] {
|
|
||||||
const tags = new Set<string>();
|
|
||||||
for (const fragment of this.fragments.values()) {
|
|
||||||
if (fragment.tags) {
|
|
||||||
for (const tag of fragment.tags) {
|
|
||||||
tags.add(tag);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Array.from(tags).sort();
|
|
||||||
}
|
|
||||||
|
|
||||||
clear(): void {
|
|
||||||
this.fragments.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 获取片段数量
|
|
||||||
* @en Get fragment count
|
|
||||||
*/
|
|
||||||
get size(): number {
|
|
||||||
return this.fragments.size;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 单例实例 | Singleton Instance
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 默认片段注册表实例
|
|
||||||
* @en Default fragment registry instance
|
|
||||||
*/
|
|
||||||
export const defaultFragmentRegistry = new FragmentRegistry();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 创建片段注册表
|
|
||||||
* @en Create fragment registry
|
|
||||||
*/
|
|
||||||
export function createFragmentRegistry(): IFragmentRegistry {
|
|
||||||
return new FragmentRegistry();
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
/**
|
|
||||||
* @zh 蓝图组合系统导出
|
|
||||||
* @en Blueprint Composition System Export
|
|
||||||
*/
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 片段 | Fragment
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
export type {
|
|
||||||
ExposedPin,
|
|
||||||
IBlueprintFragment,
|
|
||||||
BlueprintFragmentConfig,
|
|
||||||
BlueprintFragmentAsset
|
|
||||||
} from './BlueprintFragment';
|
|
||||||
|
|
||||||
export {
|
|
||||||
BlueprintFragment,
|
|
||||||
createExposedPin,
|
|
||||||
createFragment,
|
|
||||||
fragmentFromAsset,
|
|
||||||
fragmentToAsset
|
|
||||||
} from './BlueprintFragment';
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 组合器 | Composer
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
export type {
|
|
||||||
FragmentSlot,
|
|
||||||
SlotConnection,
|
|
||||||
IBlueprintComposer,
|
|
||||||
CompositionValidationResult,
|
|
||||||
CompositionError,
|
|
||||||
CompositionWarning,
|
|
||||||
BlueprintCompositionAsset
|
|
||||||
} from './BlueprintComposer';
|
|
||||||
|
|
||||||
export {
|
|
||||||
BlueprintComposer,
|
|
||||||
createComposer
|
|
||||||
} from './BlueprintComposer';
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 注册表 | Registry
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
export type {
|
|
||||||
FragmentFilter,
|
|
||||||
IFragmentRegistry
|
|
||||||
} from './FragmentRegistry';
|
|
||||||
|
|
||||||
export {
|
|
||||||
FragmentRegistry,
|
|
||||||
defaultFragmentRegistry,
|
|
||||||
createFragmentRegistry
|
|
||||||
} from './FragmentRegistry';
|
|
||||||
@@ -9,12 +9,6 @@ export * from './types';
|
|||||||
// Runtime
|
// Runtime
|
||||||
export * from './runtime';
|
export * from './runtime';
|
||||||
|
|
||||||
// Triggers
|
|
||||||
export * from './triggers';
|
|
||||||
|
|
||||||
// Composition
|
|
||||||
export * from './composition';
|
|
||||||
|
|
||||||
// Nodes (import to register)
|
// Nodes (import to register)
|
||||||
import './nodes';
|
import './nodes';
|
||||||
|
|
||||||
|
|||||||
@@ -1,118 +0,0 @@
|
|||||||
/**
|
|
||||||
* @zh 碰撞事件节点 - 碰撞发生时触发
|
|
||||||
* @en Event Collision Node - Triggered on collision events
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
|
|
||||||
import { ExecutionResult } from '../../runtime/ExecutionContext';
|
|
||||||
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh EventCollisionEnter 节点模板
|
|
||||||
* @en EventCollisionEnter node template
|
|
||||||
*/
|
|
||||||
export const EventCollisionEnterTemplate: BlueprintNodeTemplate = {
|
|
||||||
type: 'EventCollisionEnter',
|
|
||||||
title: 'Event Collision Enter',
|
|
||||||
category: 'event',
|
|
||||||
color: '#CC0000',
|
|
||||||
description: 'Triggered when collision starts / 碰撞开始时触发',
|
|
||||||
keywords: ['collision', 'enter', 'hit', 'overlap', 'event'],
|
|
||||||
menuPath: ['Event', 'Collision', 'Enter'],
|
|
||||||
inputs: [],
|
|
||||||
outputs: [
|
|
||||||
{
|
|
||||||
name: 'exec',
|
|
||||||
type: 'exec',
|
|
||||||
displayName: ''
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'otherEntityId',
|
|
||||||
type: 'string',
|
|
||||||
displayName: 'Other Entity'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'pointX',
|
|
||||||
type: 'float',
|
|
||||||
displayName: 'Point X'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'pointY',
|
|
||||||
type: 'float',
|
|
||||||
displayName: 'Point Y'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'normalX',
|
|
||||||
type: 'float',
|
|
||||||
displayName: 'Normal X'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'normalY',
|
|
||||||
type: 'float',
|
|
||||||
displayName: 'Normal Y'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh EventCollisionEnter 节点执行器
|
|
||||||
* @en EventCollisionEnter node executor
|
|
||||||
*/
|
|
||||||
@RegisterNode(EventCollisionEnterTemplate)
|
|
||||||
export class EventCollisionEnterExecutor implements INodeExecutor {
|
|
||||||
execute(_node: BlueprintNode): ExecutionResult {
|
|
||||||
return {
|
|
||||||
nextExec: 'exec',
|
|
||||||
outputs: {
|
|
||||||
otherEntityId: '',
|
|
||||||
pointX: 0,
|
|
||||||
pointY: 0,
|
|
||||||
normalX: 0,
|
|
||||||
normalY: 0
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh EventCollisionExit 节点模板
|
|
||||||
* @en EventCollisionExit node template
|
|
||||||
*/
|
|
||||||
export const EventCollisionExitTemplate: BlueprintNodeTemplate = {
|
|
||||||
type: 'EventCollisionExit',
|
|
||||||
title: 'Event Collision Exit',
|
|
||||||
category: 'event',
|
|
||||||
color: '#CC0000',
|
|
||||||
description: 'Triggered when collision ends / 碰撞结束时触发',
|
|
||||||
keywords: ['collision', 'exit', 'end', 'separate', 'event'],
|
|
||||||
menuPath: ['Event', 'Collision', 'Exit'],
|
|
||||||
inputs: [],
|
|
||||||
outputs: [
|
|
||||||
{
|
|
||||||
name: 'exec',
|
|
||||||
type: 'exec',
|
|
||||||
displayName: ''
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'otherEntityId',
|
|
||||||
type: 'string',
|
|
||||||
displayName: 'Other Entity'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh EventCollisionExit 节点执行器
|
|
||||||
* @en EventCollisionExit node executor
|
|
||||||
*/
|
|
||||||
@RegisterNode(EventCollisionExitTemplate)
|
|
||||||
export class EventCollisionExitExecutor implements INodeExecutor {
|
|
||||||
execute(_node: BlueprintNode): ExecutionResult {
|
|
||||||
return {
|
|
||||||
nextExec: 'exec',
|
|
||||||
outputs: {
|
|
||||||
otherEntityId: ''
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
/**
|
|
||||||
* @zh 输入事件节点 - 输入触发时触发
|
|
||||||
* @en Event Input Node - Triggered on input events
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
|
|
||||||
import { ExecutionContext, ExecutionResult } from '../../runtime/ExecutionContext';
|
|
||||||
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh EventInput 节点模板
|
|
||||||
* @en EventInput node template
|
|
||||||
*/
|
|
||||||
export const EventInputTemplate: BlueprintNodeTemplate = {
|
|
||||||
type: 'EventInput',
|
|
||||||
title: 'Event Input',
|
|
||||||
category: 'event',
|
|
||||||
color: '#CC0000',
|
|
||||||
description: 'Triggered when input action occurs / 输入动作发生时触发',
|
|
||||||
keywords: ['input', 'key', 'button', 'action', 'event'],
|
|
||||||
menuPath: ['Event', 'Input'],
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: 'action',
|
|
||||||
type: 'string',
|
|
||||||
displayName: 'Action',
|
|
||||||
defaultValue: ''
|
|
||||||
}
|
|
||||||
],
|
|
||||||
outputs: [
|
|
||||||
{
|
|
||||||
name: 'exec',
|
|
||||||
type: 'exec',
|
|
||||||
displayName: ''
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'action',
|
|
||||||
type: 'string',
|
|
||||||
displayName: 'Action'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'value',
|
|
||||||
type: 'float',
|
|
||||||
displayName: 'Value'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'pressed',
|
|
||||||
type: 'bool',
|
|
||||||
displayName: 'Pressed'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'released',
|
|
||||||
type: 'bool',
|
|
||||||
displayName: 'Released'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh EventInput 节点执行器
|
|
||||||
* @en EventInput node executor
|
|
||||||
*
|
|
||||||
* @zh 注意:事件节点的输出由 VM 在触发时通过 setOutputs 设置
|
|
||||||
* @en Note: Event node outputs are set by VM via setOutputs when triggered
|
|
||||||
*/
|
|
||||||
@RegisterNode(EventInputTemplate)
|
|
||||||
export class EventInputExecutor implements INodeExecutor {
|
|
||||||
execute(node: BlueprintNode, _context: ExecutionContext): ExecutionResult {
|
|
||||||
return {
|
|
||||||
nextExec: 'exec',
|
|
||||||
outputs: {
|
|
||||||
action: node.data?.action ?? '',
|
|
||||||
value: 0,
|
|
||||||
pressed: false,
|
|
||||||
released: false
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
/**
|
|
||||||
* @zh 消息事件节点 - 接收消息时触发
|
|
||||||
* @en Event Message Node - Triggered when message is received
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
|
|
||||||
import { ExecutionResult } from '../../runtime/ExecutionContext';
|
|
||||||
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh EventMessage 节点模板
|
|
||||||
* @en EventMessage node template
|
|
||||||
*/
|
|
||||||
export const EventMessageTemplate: BlueprintNodeTemplate = {
|
|
||||||
type: 'EventMessage',
|
|
||||||
title: 'Event Message',
|
|
||||||
category: 'event',
|
|
||||||
color: '#CC0000',
|
|
||||||
description: 'Triggered when a message is received / 接收到消息时触发',
|
|
||||||
keywords: ['message', 'receive', 'broadcast', 'event', 'signal'],
|
|
||||||
menuPath: ['Event', 'Message'],
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: 'messageName',
|
|
||||||
type: 'string',
|
|
||||||
displayName: 'Message Name',
|
|
||||||
defaultValue: ''
|
|
||||||
}
|
|
||||||
],
|
|
||||||
outputs: [
|
|
||||||
{
|
|
||||||
name: 'exec',
|
|
||||||
type: 'exec',
|
|
||||||
displayName: ''
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'messageName',
|
|
||||||
type: 'string',
|
|
||||||
displayName: 'Message'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'senderId',
|
|
||||||
type: 'string',
|
|
||||||
displayName: 'Sender ID'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'payload',
|
|
||||||
type: 'any',
|
|
||||||
displayName: 'Payload'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh EventMessage 节点执行器
|
|
||||||
* @en EventMessage node executor
|
|
||||||
*/
|
|
||||||
@RegisterNode(EventMessageTemplate)
|
|
||||||
export class EventMessageExecutor implements INodeExecutor {
|
|
||||||
execute(node: BlueprintNode): ExecutionResult {
|
|
||||||
return {
|
|
||||||
nextExec: 'exec',
|
|
||||||
outputs: {
|
|
||||||
messageName: node.data?.messageName ?? '',
|
|
||||||
senderId: '',
|
|
||||||
payload: null
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
/**
|
|
||||||
* @zh 状态事件节点 - 状态机状态变化时触发
|
|
||||||
* @en Event State Node - Triggered on state machine state changes
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
|
|
||||||
import { ExecutionResult } from '../../runtime/ExecutionContext';
|
|
||||||
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh EventStateEnter 节点模板
|
|
||||||
* @en EventStateEnter node template
|
|
||||||
*/
|
|
||||||
export const EventStateEnterTemplate: BlueprintNodeTemplate = {
|
|
||||||
type: 'EventStateEnter',
|
|
||||||
title: 'Event State Enter',
|
|
||||||
category: 'event',
|
|
||||||
color: '#CC0000',
|
|
||||||
description: 'Triggered when entering a state / 进入状态时触发',
|
|
||||||
keywords: ['state', 'enter', 'fsm', 'machine', 'event'],
|
|
||||||
menuPath: ['Event', 'State', 'Enter'],
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: 'stateName',
|
|
||||||
type: 'string',
|
|
||||||
displayName: 'State Name',
|
|
||||||
defaultValue: ''
|
|
||||||
}
|
|
||||||
],
|
|
||||||
outputs: [
|
|
||||||
{
|
|
||||||
name: 'exec',
|
|
||||||
type: 'exec',
|
|
||||||
displayName: ''
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'stateMachineId',
|
|
||||||
type: 'string',
|
|
||||||
displayName: 'State Machine'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'currentState',
|
|
||||||
type: 'string',
|
|
||||||
displayName: 'Current State'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'previousState',
|
|
||||||
type: 'string',
|
|
||||||
displayName: 'Previous State'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh EventStateEnter 节点执行器
|
|
||||||
* @en EventStateEnter node executor
|
|
||||||
*/
|
|
||||||
@RegisterNode(EventStateEnterTemplate)
|
|
||||||
export class EventStateEnterExecutor implements INodeExecutor {
|
|
||||||
execute(node: BlueprintNode): ExecutionResult {
|
|
||||||
return {
|
|
||||||
nextExec: 'exec',
|
|
||||||
outputs: {
|
|
||||||
stateMachineId: '',
|
|
||||||
currentState: node.data?.stateName ?? '',
|
|
||||||
previousState: ''
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh EventStateExit 节点模板
|
|
||||||
* @en EventStateExit node template
|
|
||||||
*/
|
|
||||||
export const EventStateExitTemplate: BlueprintNodeTemplate = {
|
|
||||||
type: 'EventStateExit',
|
|
||||||
title: 'Event State Exit',
|
|
||||||
category: 'event',
|
|
||||||
color: '#CC0000',
|
|
||||||
description: 'Triggered when exiting a state / 退出状态时触发',
|
|
||||||
keywords: ['state', 'exit', 'leave', 'fsm', 'machine', 'event'],
|
|
||||||
menuPath: ['Event', 'State', 'Exit'],
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: 'stateName',
|
|
||||||
type: 'string',
|
|
||||||
displayName: 'State Name',
|
|
||||||
defaultValue: ''
|
|
||||||
}
|
|
||||||
],
|
|
||||||
outputs: [
|
|
||||||
{
|
|
||||||
name: 'exec',
|
|
||||||
type: 'exec',
|
|
||||||
displayName: ''
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'stateMachineId',
|
|
||||||
type: 'string',
|
|
||||||
displayName: 'State Machine'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'currentState',
|
|
||||||
type: 'string',
|
|
||||||
displayName: 'Current State'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'previousState',
|
|
||||||
type: 'string',
|
|
||||||
displayName: 'Previous State'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh EventStateExit 节点执行器
|
|
||||||
* @en EventStateExit node executor
|
|
||||||
*/
|
|
||||||
@RegisterNode(EventStateExitTemplate)
|
|
||||||
export class EventStateExitExecutor implements INodeExecutor {
|
|
||||||
execute(node: BlueprintNode): ExecutionResult {
|
|
||||||
return {
|
|
||||||
nextExec: 'exec',
|
|
||||||
outputs: {
|
|
||||||
stateMachineId: '',
|
|
||||||
currentState: '',
|
|
||||||
previousState: node.data?.stateName ?? ''
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
/**
|
|
||||||
* @zh 定时器事件节点 - 定时器触发时调用
|
|
||||||
* @en Event Timer Node - Triggered when timer fires
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
|
|
||||||
import { ExecutionResult } from '../../runtime/ExecutionContext';
|
|
||||||
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh EventTimer 节点模板
|
|
||||||
* @en EventTimer node template
|
|
||||||
*/
|
|
||||||
export const EventTimerTemplate: BlueprintNodeTemplate = {
|
|
||||||
type: 'EventTimer',
|
|
||||||
title: 'Event Timer',
|
|
||||||
category: 'event',
|
|
||||||
color: '#CC0000',
|
|
||||||
description: 'Triggered when a timer fires / 定时器触发时执行',
|
|
||||||
keywords: ['timer', 'delay', 'schedule', 'event', 'interval'],
|
|
||||||
menuPath: ['Event', 'Timer'],
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: 'timerId',
|
|
||||||
type: 'string',
|
|
||||||
displayName: 'Timer ID',
|
|
||||||
defaultValue: ''
|
|
||||||
}
|
|
||||||
],
|
|
||||||
outputs: [
|
|
||||||
{
|
|
||||||
name: 'exec',
|
|
||||||
type: 'exec',
|
|
||||||
displayName: ''
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'timerId',
|
|
||||||
type: 'string',
|
|
||||||
displayName: 'Timer ID'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'isRepeating',
|
|
||||||
type: 'bool',
|
|
||||||
displayName: 'Is Repeating'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'timesFired',
|
|
||||||
type: 'int',
|
|
||||||
displayName: 'Times Fired'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh EventTimer 节点执行器
|
|
||||||
* @en EventTimer node executor
|
|
||||||
*/
|
|
||||||
@RegisterNode(EventTimerTemplate)
|
|
||||||
export class EventTimerExecutor implements INodeExecutor {
|
|
||||||
execute(node: BlueprintNode): ExecutionResult {
|
|
||||||
return {
|
|
||||||
nextExec: 'exec',
|
|
||||||
outputs: {
|
|
||||||
timerId: node.data?.timerId ?? '',
|
|
||||||
isRepeating: false,
|
|
||||||
timesFired: 0
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* @zh 事件节点 - 蓝图执行的入口点
|
* Event Nodes - Entry points for blueprint execution
|
||||||
* @en Event Nodes - Entry points for blueprint execution
|
* 事件节点 - 蓝图执行的入口点
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// 生命周期事件 | Lifecycle events
|
|
||||||
export * from './EventBeginPlay';
|
export * from './EventBeginPlay';
|
||||||
export * from './EventTick';
|
export * from './EventTick';
|
||||||
export * from './EventEndPlay';
|
export * from './EventEndPlay';
|
||||||
|
|
||||||
// 触发器事件 | Trigger events
|
|
||||||
export * from './EventInput';
|
|
||||||
export * from './EventCollision';
|
|
||||||
export * from './EventMessage';
|
|
||||||
export * from './EventTimer';
|
|
||||||
export * from './EventState';
|
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
* 蓝图组件 - 将蓝图附加到实体
|
* 蓝图组件 - 将蓝图附加到实体
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Entity, IScene } from '@esengine/ecs-framework';
|
|
||||||
import { BlueprintAsset } from '../types/blueprint';
|
import { BlueprintAsset } from '../types/blueprint';
|
||||||
import { BlueprintVM } from './BlueprintVM';
|
import { BlueprintVM } from './BlueprintVM';
|
||||||
|
import { IEntity, IScene } from './ExecutionContext';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component interface for ECS integration
|
* Component interface for ECS integration
|
||||||
@@ -56,7 +56,7 @@ export function createBlueprintComponentData(): IBlueprintComponent {
|
|||||||
*/
|
*/
|
||||||
export function initializeBlueprintVM(
|
export function initializeBlueprintVM(
|
||||||
component: IBlueprintComponent,
|
component: IBlueprintComponent,
|
||||||
entity: Entity,
|
entity: IEntity,
|
||||||
scene: IScene
|
scene: IScene
|
||||||
): void {
|
): void {
|
||||||
if (!component.blueprintAsset) {
|
if (!component.blueprintAsset) {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
* 蓝图执行系统 - 管理蓝图生命周期和执行
|
* 蓝图执行系统 - 管理蓝图生命周期和执行
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Entity, IScene } from '@esengine/ecs-framework';
|
|
||||||
import {
|
import {
|
||||||
IBlueprintComponent,
|
IBlueprintComponent,
|
||||||
initializeBlueprintVM,
|
initializeBlueprintVM,
|
||||||
@@ -11,6 +10,7 @@ import {
|
|||||||
tickBlueprint,
|
tickBlueprint,
|
||||||
cleanupBlueprint
|
cleanupBlueprint
|
||||||
} from './BlueprintComponent';
|
} from './BlueprintComponent';
|
||||||
|
import { IEntity, IScene } from './ExecutionContext';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Blueprint system interface for engine integration
|
* Blueprint system interface for engine integration
|
||||||
@@ -31,7 +31,7 @@ export interface IBlueprintSystem {
|
|||||||
* Entity with blueprint component
|
* Entity with blueprint component
|
||||||
* 带有蓝图组件的实体
|
* 带有蓝图组件的实体
|
||||||
*/
|
*/
|
||||||
export interface IBlueprintEntity extends Entity {
|
export interface IBlueprintEntity extends IEntity {
|
||||||
/** Blueprint component data (蓝图组件数据) */
|
/** Blueprint component data (蓝图组件数据) */
|
||||||
blueprintComponent: IBlueprintComponent;
|
blueprintComponent: IBlueprintComponent;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,9 @@
|
|||||||
* 蓝图虚拟机 - 执行蓝图图
|
* 蓝图虚拟机 - 执行蓝图图
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Entity, IScene } from '@esengine/ecs-framework';
|
|
||||||
import { BlueprintNode } from '../types/nodes';
|
import { BlueprintNode } from '../types/nodes';
|
||||||
import { BlueprintAsset } from '../types/blueprint';
|
import { BlueprintAsset } from '../types/blueprint';
|
||||||
import { ExecutionContext, ExecutionResult } from './ExecutionContext';
|
import { ExecutionContext, ExecutionResult, IEntity, IScene } from './ExecutionContext';
|
||||||
import { NodeRegistry } from './NodeRegistry';
|
import { NodeRegistry } from './NodeRegistry';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -58,7 +57,7 @@ export class BlueprintVM {
|
|||||||
/** Debug mode (调试模式) */
|
/** Debug mode (调试模式) */
|
||||||
debug: boolean = false;
|
debug: boolean = false;
|
||||||
|
|
||||||
constructor(blueprint: BlueprintAsset, entity: Entity, scene: IScene) {
|
constructor(blueprint: BlueprintAsset, entity: IEntity, scene: IScene) {
|
||||||
this._context = new ExecutionContext(blueprint, entity, scene);
|
this._context = new ExecutionContext(blueprint, entity, scene);
|
||||||
this._cacheEventNodes();
|
this._cacheEventNodes();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
* 执行上下文 - 蓝图执行的运行时上下文
|
* 执行上下文 - 蓝图执行的运行时上下文
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Entity, IScene } from '@esengine/ecs-framework';
|
|
||||||
import { BlueprintNode, BlueprintConnection } from '../types/nodes';
|
import { BlueprintNode, BlueprintConnection } from '../types/nodes';
|
||||||
import { BlueprintAsset } from '../types/blueprint';
|
import { BlueprintAsset } from '../types/blueprint';
|
||||||
|
|
||||||
@@ -43,6 +42,31 @@ export interface ExecutionResult {
|
|||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entity interface (minimal for decoupling)
|
||||||
|
* 实体接口(最小化以解耦)
|
||||||
|
*/
|
||||||
|
export interface IEntity {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
active: boolean;
|
||||||
|
getComponent<T>(type: new (...args: unknown[]) => T): T | null;
|
||||||
|
addComponent<T>(component: T): T;
|
||||||
|
removeComponent<T>(type: new (...args: unknown[]) => T): void;
|
||||||
|
hasComponent<T>(type: new (...args: unknown[]) => T): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scene interface (minimal for decoupling)
|
||||||
|
* 场景接口(最小化以解耦)
|
||||||
|
*/
|
||||||
|
export interface IScene {
|
||||||
|
createEntity(name?: string): IEntity;
|
||||||
|
destroyEntity(entity: IEntity): void;
|
||||||
|
findEntityByName(name: string): IEntity | null;
|
||||||
|
findEntitiesByTag(tag: number): IEntity[];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execution context provides access to runtime services
|
* Execution context provides access to runtime services
|
||||||
* 执行上下文提供对运行时服务的访问
|
* 执行上下文提供对运行时服务的访问
|
||||||
@@ -52,7 +76,7 @@ export class ExecutionContext {
|
|||||||
readonly blueprint: BlueprintAsset;
|
readonly blueprint: BlueprintAsset;
|
||||||
|
|
||||||
/** Owner entity (所有者实体) */
|
/** Owner entity (所有者实体) */
|
||||||
readonly entity: Entity;
|
readonly entity: IEntity;
|
||||||
|
|
||||||
/** Current scene (当前场景) */
|
/** Current scene (当前场景) */
|
||||||
readonly scene: IScene;
|
readonly scene: IScene;
|
||||||
@@ -81,7 +105,7 @@ export class ExecutionContext {
|
|||||||
/** Connection lookup by source (按源的连接查找) */
|
/** Connection lookup by source (按源的连接查找) */
|
||||||
private _connectionsBySource: Map<string, BlueprintConnection[]> = new Map();
|
private _connectionsBySource: Map<string, BlueprintConnection[]> = new Map();
|
||||||
|
|
||||||
constructor(blueprint: BlueprintAsset, entity: Entity, scene: IScene) {
|
constructor(blueprint: BlueprintAsset, entity: IEntity, scene: IScene) {
|
||||||
this.blueprint = blueprint;
|
this.blueprint = blueprint;
|
||||||
this.entity = entity;
|
this.entity = entity;
|
||||||
this.scene = scene;
|
this.scene = scene;
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
/**
|
|
||||||
* Blueprint 模块服务令牌
|
|
||||||
* Blueprint module service tokens
|
|
||||||
*
|
|
||||||
* 定义 blueprint 模块导出的服务令牌。
|
|
||||||
* Defines service tokens exported by blueprint module.
|
|
||||||
*
|
|
||||||
* 注意:当前 Blueprint 模块主要通过组件和系统工作,
|
|
||||||
* 暂时没有需要通过服务令牌暴露的全局服务。
|
|
||||||
* 未来可能添加 BlueprintDebuggerToken 等。
|
|
||||||
*
|
|
||||||
* Note: Blueprint module currently works mainly through components and systems,
|
|
||||||
* no global services need to be exposed via service tokens yet.
|
|
||||||
* May add BlueprintDebuggerToken etc. in the future.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// 当前无服务令牌
|
|
||||||
// No service tokens currently
|
|
||||||
|
|
||||||
// 预留导出位置,便于将来扩展
|
|
||||||
// Reserved export location for future extension
|
|
||||||
export {};
|
|
||||||
@@ -1,497 +0,0 @@
|
|||||||
/**
|
|
||||||
* @zh 蓝图触发器
|
|
||||||
* @en Blueprint Trigger
|
|
||||||
*
|
|
||||||
* @zh 定义触发器的核心实现
|
|
||||||
* @en Defines core trigger implementation
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { TriggerType, ITriggerContext } from './TriggerTypes';
|
|
||||||
import type { ITriggerCondition } from './TriggerCondition';
|
|
||||||
import { AlwaysTrueCondition } from './TriggerCondition';
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 触发器接口 | Trigger Interface
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 触发器回调函数类型
|
|
||||||
* @en Trigger callback function type
|
|
||||||
*/
|
|
||||||
export type TriggerCallback = (context: ITriggerContext) => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 蓝图触发器接口
|
|
||||||
* @en Blueprint trigger interface
|
|
||||||
*/
|
|
||||||
export interface IBlueprintTrigger {
|
|
||||||
/**
|
|
||||||
* @zh 触发器唯一标识
|
|
||||||
* @en Trigger unique identifier
|
|
||||||
*/
|
|
||||||
readonly id: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 触发器类型
|
|
||||||
* @en Trigger type
|
|
||||||
*/
|
|
||||||
readonly type: TriggerType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 触发器条件
|
|
||||||
* @en Trigger conditions
|
|
||||||
*/
|
|
||||||
readonly condition: ITriggerCondition;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 是否启用
|
|
||||||
* @en Is enabled
|
|
||||||
*/
|
|
||||||
enabled: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 优先级(越高越先执行)
|
|
||||||
* @en Priority (higher executes first)
|
|
||||||
*/
|
|
||||||
readonly priority: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 检查是否应该触发
|
|
||||||
* @en Check if should fire
|
|
||||||
*/
|
|
||||||
shouldFire(context: ITriggerContext): boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 执行触发器
|
|
||||||
* @en Execute trigger
|
|
||||||
*/
|
|
||||||
fire(context: ITriggerContext): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 触发器配置
|
|
||||||
* @en Trigger configuration
|
|
||||||
*/
|
|
||||||
export interface TriggerConfig {
|
|
||||||
/**
|
|
||||||
* @zh 触发器 ID
|
|
||||||
* @en Trigger ID
|
|
||||||
*/
|
|
||||||
id?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 触发器类型
|
|
||||||
* @en Trigger type
|
|
||||||
*/
|
|
||||||
type: TriggerType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 触发条件
|
|
||||||
* @en Trigger condition
|
|
||||||
*/
|
|
||||||
condition?: ITriggerCondition;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 是否启用
|
|
||||||
* @en Is enabled
|
|
||||||
*/
|
|
||||||
enabled?: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 优先级
|
|
||||||
* @en Priority
|
|
||||||
*/
|
|
||||||
priority?: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 回调函数
|
|
||||||
* @en Callback function
|
|
||||||
*/
|
|
||||||
callback?: TriggerCallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 触发器实现 | Trigger Implementation
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
let _triggerId = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 生成唯一触发器 ID
|
|
||||||
* @en Generate unique trigger ID
|
|
||||||
*/
|
|
||||||
function generateTriggerId(): string {
|
|
||||||
return `trigger_${++_triggerId}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 蓝图触发器实现
|
|
||||||
* @en Blueprint trigger implementation
|
|
||||||
*/
|
|
||||||
export class BlueprintTrigger implements IBlueprintTrigger {
|
|
||||||
readonly id: string;
|
|
||||||
readonly type: TriggerType;
|
|
||||||
readonly condition: ITriggerCondition;
|
|
||||||
readonly priority: number;
|
|
||||||
enabled: boolean;
|
|
||||||
|
|
||||||
private readonly _callback?: TriggerCallback;
|
|
||||||
private readonly _callbacks: Set<TriggerCallback> = new Set();
|
|
||||||
|
|
||||||
constructor(config: TriggerConfig) {
|
|
||||||
this.id = config.id ?? generateTriggerId();
|
|
||||||
this.type = config.type;
|
|
||||||
this.condition = config.condition ?? new AlwaysTrueCondition();
|
|
||||||
this.priority = config.priority ?? 0;
|
|
||||||
this.enabled = config.enabled ?? true;
|
|
||||||
this._callback = config.callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 检查是否应该触发
|
|
||||||
* @en Check if should fire
|
|
||||||
*/
|
|
||||||
shouldFire(context: ITriggerContext): boolean {
|
|
||||||
if (!this.enabled) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (context.type !== this.type && this.type !== 'custom') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.condition.evaluate(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 执行触发器
|
|
||||||
* @en Execute trigger
|
|
||||||
*/
|
|
||||||
fire(context: ITriggerContext): void {
|
|
||||||
if (this._callback) {
|
|
||||||
this._callback(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const callback of this._callbacks) {
|
|
||||||
callback(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 添加回调
|
|
||||||
* @en Add callback
|
|
||||||
*/
|
|
||||||
addCallback(callback: TriggerCallback): void {
|
|
||||||
this._callbacks.add(callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 移除回调
|
|
||||||
* @en Remove callback
|
|
||||||
*/
|
|
||||||
removeCallback(callback: TriggerCallback): void {
|
|
||||||
this._callbacks.delete(callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 清除所有回调
|
|
||||||
* @en Clear all callbacks
|
|
||||||
*/
|
|
||||||
clearCallbacks(): void {
|
|
||||||
this._callbacks.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 触发器注册表 | Trigger Registry
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 触发器注册表接口
|
|
||||||
* @en Trigger registry interface
|
|
||||||
*/
|
|
||||||
export interface ITriggerRegistry {
|
|
||||||
/**
|
|
||||||
* @zh 注册触发器
|
|
||||||
* @en Register trigger
|
|
||||||
*/
|
|
||||||
register(trigger: IBlueprintTrigger): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 注销触发器
|
|
||||||
* @en Unregister trigger
|
|
||||||
*/
|
|
||||||
unregister(triggerId: string): boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 获取触发器
|
|
||||||
* @en Get trigger
|
|
||||||
*/
|
|
||||||
get(triggerId: string): IBlueprintTrigger | undefined;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 获取所有触发器
|
|
||||||
* @en Get all triggers
|
|
||||||
*/
|
|
||||||
getAll(): IBlueprintTrigger[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 按类型获取触发器
|
|
||||||
* @en Get triggers by type
|
|
||||||
*/
|
|
||||||
getByType(type: TriggerType): IBlueprintTrigger[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 清除所有触发器
|
|
||||||
* @en Clear all triggers
|
|
||||||
*/
|
|
||||||
clear(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 触发器注册表实现
|
|
||||||
* @en Trigger registry implementation
|
|
||||||
*/
|
|
||||||
export class TriggerRegistry implements ITriggerRegistry {
|
|
||||||
private readonly _triggers: Map<string, IBlueprintTrigger> = new Map();
|
|
||||||
private readonly _triggersByType: Map<TriggerType, Set<string>> = new Map();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 注册触发器
|
|
||||||
* @en Register trigger
|
|
||||||
*/
|
|
||||||
register(trigger: IBlueprintTrigger): void {
|
|
||||||
if (this._triggers.has(trigger.id)) {
|
|
||||||
console.warn(`Trigger ${trigger.id} already registered, overwriting`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._triggers.set(trigger.id, trigger);
|
|
||||||
|
|
||||||
if (!this._triggersByType.has(trigger.type)) {
|
|
||||||
this._triggersByType.set(trigger.type, new Set());
|
|
||||||
}
|
|
||||||
this._triggersByType.get(trigger.type)!.add(trigger.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 注销触发器
|
|
||||||
* @en Unregister trigger
|
|
||||||
*/
|
|
||||||
unregister(triggerId: string): boolean {
|
|
||||||
const trigger = this._triggers.get(triggerId);
|
|
||||||
if (!trigger) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._triggers.delete(triggerId);
|
|
||||||
|
|
||||||
const typeSet = this._triggersByType.get(trigger.type);
|
|
||||||
if (typeSet) {
|
|
||||||
typeSet.delete(triggerId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 获取触发器
|
|
||||||
* @en Get trigger
|
|
||||||
*/
|
|
||||||
get(triggerId: string): IBlueprintTrigger | undefined {
|
|
||||||
return this._triggers.get(triggerId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 获取所有触发器
|
|
||||||
* @en Get all triggers
|
|
||||||
*/
|
|
||||||
getAll(): IBlueprintTrigger[] {
|
|
||||||
return Array.from(this._triggers.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 按类型获取触发器
|
|
||||||
* @en Get triggers by type
|
|
||||||
*/
|
|
||||||
getByType(type: TriggerType): IBlueprintTrigger[] {
|
|
||||||
const typeSet = this._triggersByType.get(type);
|
|
||||||
if (!typeSet) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const triggers: IBlueprintTrigger[] = [];
|
|
||||||
for (const id of typeSet) {
|
|
||||||
const trigger = this._triggers.get(id);
|
|
||||||
if (trigger) {
|
|
||||||
triggers.push(trigger);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return triggers.sort((a, b) => b.priority - a.priority);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 清除所有触发器
|
|
||||||
* @en Clear all triggers
|
|
||||||
*/
|
|
||||||
clear(): void {
|
|
||||||
this._triggers.clear();
|
|
||||||
this._triggersByType.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 获取触发器数量
|
|
||||||
* @en Get trigger count
|
|
||||||
*/
|
|
||||||
get count(): number {
|
|
||||||
return this._triggers.size;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 工厂函数 | Factory Functions
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 创建触发器
|
|
||||||
* @en Create trigger
|
|
||||||
*/
|
|
||||||
export function createTrigger(config: TriggerConfig): BlueprintTrigger {
|
|
||||||
return new BlueprintTrigger(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 创建 Tick 触发器
|
|
||||||
* @en Create tick trigger
|
|
||||||
*/
|
|
||||||
export function createTickTrigger(
|
|
||||||
callback?: TriggerCallback,
|
|
||||||
options?: { id?: string; condition?: ITriggerCondition; priority?: number }
|
|
||||||
): BlueprintTrigger {
|
|
||||||
return new BlueprintTrigger({
|
|
||||||
id: options?.id,
|
|
||||||
type: 'tick',
|
|
||||||
condition: options?.condition,
|
|
||||||
priority: options?.priority,
|
|
||||||
callback
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 创建输入触发器
|
|
||||||
* @en Create input trigger
|
|
||||||
*/
|
|
||||||
export function createInputTrigger(
|
|
||||||
callback?: TriggerCallback,
|
|
||||||
options?: { id?: string; condition?: ITriggerCondition; priority?: number }
|
|
||||||
): BlueprintTrigger {
|
|
||||||
return new BlueprintTrigger({
|
|
||||||
id: options?.id,
|
|
||||||
type: 'input',
|
|
||||||
condition: options?.condition,
|
|
||||||
priority: options?.priority,
|
|
||||||
callback
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 创建碰撞触发器
|
|
||||||
* @en Create collision trigger
|
|
||||||
*/
|
|
||||||
export function createCollisionTrigger(
|
|
||||||
callback?: TriggerCallback,
|
|
||||||
options?: { id?: string; condition?: ITriggerCondition; priority?: number }
|
|
||||||
): BlueprintTrigger {
|
|
||||||
return new BlueprintTrigger({
|
|
||||||
id: options?.id,
|
|
||||||
type: 'collision',
|
|
||||||
condition: options?.condition,
|
|
||||||
priority: options?.priority,
|
|
||||||
callback
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 创建消息触发器
|
|
||||||
* @en Create message trigger
|
|
||||||
*/
|
|
||||||
export function createMessageTrigger(
|
|
||||||
callback?: TriggerCallback,
|
|
||||||
options?: { id?: string; condition?: ITriggerCondition; priority?: number }
|
|
||||||
): BlueprintTrigger {
|
|
||||||
return new BlueprintTrigger({
|
|
||||||
id: options?.id,
|
|
||||||
type: 'message',
|
|
||||||
condition: options?.condition,
|
|
||||||
priority: options?.priority,
|
|
||||||
callback
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 创建定时器触发器
|
|
||||||
* @en Create timer trigger
|
|
||||||
*/
|
|
||||||
export function createTimerTrigger(
|
|
||||||
callback?: TriggerCallback,
|
|
||||||
options?: { id?: string; condition?: ITriggerCondition; priority?: number }
|
|
||||||
): BlueprintTrigger {
|
|
||||||
return new BlueprintTrigger({
|
|
||||||
id: options?.id,
|
|
||||||
type: 'timer',
|
|
||||||
condition: options?.condition,
|
|
||||||
priority: options?.priority,
|
|
||||||
callback
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 创建状态进入触发器
|
|
||||||
* @en Create state enter trigger
|
|
||||||
*/
|
|
||||||
export function createStateEnterTrigger(
|
|
||||||
callback?: TriggerCallback,
|
|
||||||
options?: { id?: string; condition?: ITriggerCondition; priority?: number }
|
|
||||||
): BlueprintTrigger {
|
|
||||||
return new BlueprintTrigger({
|
|
||||||
id: options?.id,
|
|
||||||
type: 'stateEnter',
|
|
||||||
condition: options?.condition,
|
|
||||||
priority: options?.priority,
|
|
||||||
callback
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 创建状态退出触发器
|
|
||||||
* @en Create state exit trigger
|
|
||||||
*/
|
|
||||||
export function createStateExitTrigger(
|
|
||||||
callback?: TriggerCallback,
|
|
||||||
options?: { id?: string; condition?: ITriggerCondition; priority?: number }
|
|
||||||
): BlueprintTrigger {
|
|
||||||
return new BlueprintTrigger({
|
|
||||||
id: options?.id,
|
|
||||||
type: 'stateExit',
|
|
||||||
condition: options?.condition,
|
|
||||||
priority: options?.priority,
|
|
||||||
callback
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 创建自定义触发器
|
|
||||||
* @en Create custom trigger
|
|
||||||
*/
|
|
||||||
export function createCustomTrigger(
|
|
||||||
callback?: TriggerCallback,
|
|
||||||
options?: { id?: string; condition?: ITriggerCondition; priority?: number }
|
|
||||||
): BlueprintTrigger {
|
|
||||||
return new BlueprintTrigger({
|
|
||||||
id: options?.id,
|
|
||||||
type: 'custom',
|
|
||||||
condition: options?.condition,
|
|
||||||
priority: options?.priority,
|
|
||||||
callback
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,479 +0,0 @@
|
|||||||
/**
|
|
||||||
* @zh 触发器条件系统
|
|
||||||
* @en Trigger Condition System
|
|
||||||
*
|
|
||||||
* @zh 提供触发器触发前的条件检查能力
|
|
||||||
* @en Provides condition checking before trigger fires
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type {
|
|
||||||
ITriggerContext,
|
|
||||||
TriggerType,
|
|
||||||
IInputTriggerContext,
|
|
||||||
IMessageTriggerContext,
|
|
||||||
IStateTriggerContext,
|
|
||||||
ITimerTriggerContext,
|
|
||||||
ICollisionTriggerContext,
|
|
||||||
ICustomTriggerContext
|
|
||||||
} from './TriggerTypes';
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 条件接口 | Condition Interface
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 触发器条件接口
|
|
||||||
* @en Trigger condition interface
|
|
||||||
*/
|
|
||||||
export interface ITriggerCondition {
|
|
||||||
/**
|
|
||||||
* @zh 条件类型标识
|
|
||||||
* @en Condition type identifier
|
|
||||||
*/
|
|
||||||
readonly type: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 评估条件是否满足
|
|
||||||
* @en Evaluate if condition is met
|
|
||||||
*
|
|
||||||
* @param context - @zh 触发器上下文 @en Trigger context
|
|
||||||
* @returns @zh 条件是否满足 @en Whether condition is met
|
|
||||||
*/
|
|
||||||
evaluate(context: ITriggerContext): boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 条件组合逻辑
|
|
||||||
* @en Condition combination logic
|
|
||||||
*/
|
|
||||||
export type ConditionLogic = 'and' | 'or';
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 复合条件 | Composite Conditions
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 复合条件 - 组合多个条件
|
|
||||||
* @en Composite condition - combines multiple conditions
|
|
||||||
*/
|
|
||||||
export class CompositeCondition implements ITriggerCondition {
|
|
||||||
readonly type = 'composite';
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly _conditions: ITriggerCondition[],
|
|
||||||
private readonly _logic: ConditionLogic = 'and'
|
|
||||||
) {}
|
|
||||||
|
|
||||||
evaluate(context: ITriggerContext): boolean {
|
|
||||||
if (this._conditions.length === 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._logic === 'and') {
|
|
||||||
return this._conditions.every(c => c.evaluate(context));
|
|
||||||
} else {
|
|
||||||
return this._conditions.some(c => c.evaluate(context));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 非条件 - 取反
|
|
||||||
* @en Not condition - negates
|
|
||||||
*/
|
|
||||||
export class NotCondition implements ITriggerCondition {
|
|
||||||
readonly type = 'not';
|
|
||||||
|
|
||||||
constructor(private readonly _condition: ITriggerCondition) {}
|
|
||||||
|
|
||||||
evaluate(context: ITriggerContext): boolean {
|
|
||||||
return !this._condition.evaluate(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 通用条件 | Generic Conditions
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 始终为真的条件
|
|
||||||
* @en Always true condition
|
|
||||||
*/
|
|
||||||
export class AlwaysTrueCondition implements ITriggerCondition {
|
|
||||||
readonly type = 'alwaysTrue';
|
|
||||||
|
|
||||||
evaluate(_context: ITriggerContext): boolean {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 始终为假的条件
|
|
||||||
* @en Always false condition
|
|
||||||
*/
|
|
||||||
export class AlwaysFalseCondition implements ITriggerCondition {
|
|
||||||
readonly type = 'alwaysFalse';
|
|
||||||
|
|
||||||
evaluate(_context: ITriggerContext): boolean {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 触发器类型条件
|
|
||||||
* @en Trigger type condition
|
|
||||||
*/
|
|
||||||
export class TriggerTypeCondition implements ITriggerCondition {
|
|
||||||
readonly type = 'triggerType';
|
|
||||||
|
|
||||||
constructor(private readonly _allowedTypes: TriggerType[]) {}
|
|
||||||
|
|
||||||
evaluate(context: ITriggerContext): boolean {
|
|
||||||
return this._allowedTypes.includes(context.type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 实体 ID 条件
|
|
||||||
* @en Entity ID condition
|
|
||||||
*/
|
|
||||||
export class EntityIdCondition implements ITriggerCondition {
|
|
||||||
readonly type = 'entityId';
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly _entityId: string,
|
|
||||||
private readonly _checkSource: boolean = true
|
|
||||||
) {}
|
|
||||||
|
|
||||||
evaluate(context: ITriggerContext): boolean {
|
|
||||||
if (this._checkSource) {
|
|
||||||
return context.sourceEntityId === this._entityId;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 自定义函数条件
|
|
||||||
* @en Custom function condition
|
|
||||||
*/
|
|
||||||
export class FunctionCondition implements ITriggerCondition {
|
|
||||||
readonly type = 'function';
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly _predicate: (context: ITriggerContext) => boolean
|
|
||||||
) {}
|
|
||||||
|
|
||||||
evaluate(context: ITriggerContext): boolean {
|
|
||||||
return this._predicate(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 特定类型条件 | Type-Specific Conditions
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 输入动作条件
|
|
||||||
* @en Input action condition
|
|
||||||
*/
|
|
||||||
export class InputActionCondition implements ITriggerCondition {
|
|
||||||
readonly type = 'inputAction';
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly _action: string,
|
|
||||||
private readonly _checkPressed?: boolean,
|
|
||||||
private readonly _checkReleased?: boolean
|
|
||||||
) {}
|
|
||||||
|
|
||||||
evaluate(context: ITriggerContext): boolean {
|
|
||||||
if (context.type !== 'input') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const inputContext = context as unknown as IInputTriggerContext;
|
|
||||||
|
|
||||||
if (inputContext.action !== this._action) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._checkPressed !== undefined && inputContext.pressed !== this._checkPressed) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._checkReleased !== undefined && inputContext.released !== this._checkReleased) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 消息名称条件
|
|
||||||
* @en Message name condition
|
|
||||||
*/
|
|
||||||
export class MessageNameCondition implements ITriggerCondition {
|
|
||||||
readonly type = 'messageName';
|
|
||||||
|
|
||||||
constructor(private readonly _messageName: string) {}
|
|
||||||
|
|
||||||
evaluate(context: ITriggerContext): boolean {
|
|
||||||
if (context.type !== 'message') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const messageContext = context as unknown as IMessageTriggerContext;
|
|
||||||
return messageContext.messageName === this._messageName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 状态名称条件
|
|
||||||
* @en State name condition
|
|
||||||
*/
|
|
||||||
export class StateNameCondition implements ITriggerCondition {
|
|
||||||
readonly type = 'stateName';
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly _stateName: string,
|
|
||||||
private readonly _checkCurrent: boolean = true
|
|
||||||
) {}
|
|
||||||
|
|
||||||
evaluate(context: ITriggerContext): boolean {
|
|
||||||
if (context.type !== 'stateEnter' && context.type !== 'stateExit') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const stateContext = context as unknown as IStateTriggerContext;
|
|
||||||
|
|
||||||
if (this._checkCurrent) {
|
|
||||||
return stateContext.currentState === this._stateName;
|
|
||||||
} else {
|
|
||||||
return stateContext.previousState === this._stateName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 定时器 ID 条件
|
|
||||||
* @en Timer ID condition
|
|
||||||
*/
|
|
||||||
export class TimerIdCondition implements ITriggerCondition {
|
|
||||||
readonly type = 'timerId';
|
|
||||||
|
|
||||||
constructor(private readonly _timerId: string) {}
|
|
||||||
|
|
||||||
evaluate(context: ITriggerContext): boolean {
|
|
||||||
if (context.type !== 'timer') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const timerContext = context as unknown as ITimerTriggerContext;
|
|
||||||
return timerContext.timerId === this._timerId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 碰撞实体条件
|
|
||||||
* @en Collision entity condition
|
|
||||||
*/
|
|
||||||
export class CollisionEntityCondition implements ITriggerCondition {
|
|
||||||
readonly type = 'collisionEntity';
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly _otherEntityId?: string,
|
|
||||||
private readonly _checkEnter?: boolean,
|
|
||||||
private readonly _checkExit?: boolean
|
|
||||||
) {}
|
|
||||||
|
|
||||||
evaluate(context: ITriggerContext): boolean {
|
|
||||||
if (context.type !== 'collision') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const collisionContext = context as unknown as ICollisionTriggerContext;
|
|
||||||
|
|
||||||
if (this._otherEntityId !== undefined && collisionContext.otherEntityId !== this._otherEntityId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._checkEnter !== undefined && collisionContext.isEnter !== this._checkEnter) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._checkExit !== undefined && collisionContext.isExit !== this._checkExit) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 自定义事件名称条件
|
|
||||||
* @en Custom event name condition
|
|
||||||
*/
|
|
||||||
export class CustomEventCondition implements ITriggerCondition {
|
|
||||||
readonly type = 'customEvent';
|
|
||||||
|
|
||||||
constructor(private readonly _eventName: string) {}
|
|
||||||
|
|
||||||
evaluate(context: ITriggerContext): boolean {
|
|
||||||
if (context.type !== 'custom') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const customContext = context as unknown as ICustomTriggerContext;
|
|
||||||
return customContext.eventName === this._eventName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 条件构建器 | Condition Builder
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 条件构建器 - 链式 API
|
|
||||||
* @en Condition builder - fluent API
|
|
||||||
*/
|
|
||||||
export class ConditionBuilder {
|
|
||||||
private _conditions: ITriggerCondition[] = [];
|
|
||||||
private _logic: ConditionLogic = 'and';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 设置组合逻辑为 AND
|
|
||||||
* @en Set combination logic to AND
|
|
||||||
*/
|
|
||||||
and(): this {
|
|
||||||
this._logic = 'and';
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 设置组合逻辑为 OR
|
|
||||||
* @en Set combination logic to OR
|
|
||||||
*/
|
|
||||||
or(): this {
|
|
||||||
this._logic = 'or';
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 添加触发器类型条件
|
|
||||||
* @en Add trigger type condition
|
|
||||||
*/
|
|
||||||
ofType(...types: TriggerType[]): this {
|
|
||||||
this._conditions.push(new TriggerTypeCondition(types));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 添加实体 ID 条件
|
|
||||||
* @en Add entity ID condition
|
|
||||||
*/
|
|
||||||
fromEntity(entityId: string): this {
|
|
||||||
this._conditions.push(new EntityIdCondition(entityId));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 添加输入动作条件
|
|
||||||
* @en Add input action condition
|
|
||||||
*/
|
|
||||||
onInput(action: string, options?: { pressed?: boolean; released?: boolean }): this {
|
|
||||||
this._conditions.push(new InputActionCondition(action, options?.pressed, options?.released));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 添加消息条件
|
|
||||||
* @en Add message condition
|
|
||||||
*/
|
|
||||||
onMessage(messageName: string): this {
|
|
||||||
this._conditions.push(new MessageNameCondition(messageName));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 添加状态条件
|
|
||||||
* @en Add state condition
|
|
||||||
*/
|
|
||||||
onState(stateName: string, checkCurrent: boolean = true): this {
|
|
||||||
this._conditions.push(new StateNameCondition(stateName, checkCurrent));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 添加定时器条件
|
|
||||||
* @en Add timer condition
|
|
||||||
*/
|
|
||||||
onTimer(timerId: string): this {
|
|
||||||
this._conditions.push(new TimerIdCondition(timerId));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 添加碰撞条件
|
|
||||||
* @en Add collision condition
|
|
||||||
*/
|
|
||||||
onCollision(options?: { entityId?: string; isEnter?: boolean; isExit?: boolean }): this {
|
|
||||||
this._conditions.push(new CollisionEntityCondition(
|
|
||||||
options?.entityId,
|
|
||||||
options?.isEnter,
|
|
||||||
options?.isExit
|
|
||||||
));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 添加自定义事件条件
|
|
||||||
* @en Add custom event condition
|
|
||||||
*/
|
|
||||||
onCustomEvent(eventName: string): this {
|
|
||||||
this._conditions.push(new CustomEventCondition(eventName));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 添加自定义函数条件
|
|
||||||
* @en Add custom function condition
|
|
||||||
*/
|
|
||||||
where(predicate: (context: ITriggerContext) => boolean): this {
|
|
||||||
this._conditions.push(new FunctionCondition(predicate));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 添加取反条件
|
|
||||||
* @en Add negated condition
|
|
||||||
*/
|
|
||||||
not(condition: ITriggerCondition): this {
|
|
||||||
this._conditions.push(new NotCondition(condition));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 构建条件
|
|
||||||
* @en Build condition
|
|
||||||
*/
|
|
||||||
build(): ITriggerCondition {
|
|
||||||
if (this._conditions.length === 0) {
|
|
||||||
return new AlwaysTrueCondition();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._conditions.length === 1) {
|
|
||||||
return this._conditions[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return new CompositeCondition(this._conditions, this._logic);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 创建条件构建器
|
|
||||||
* @en Create condition builder
|
|
||||||
*/
|
|
||||||
export function condition(): ConditionBuilder {
|
|
||||||
return new ConditionBuilder();
|
|
||||||
}
|
|
||||||
@@ -1,461 +0,0 @@
|
|||||||
/**
|
|
||||||
* @zh 触发器调度器
|
|
||||||
* @en Trigger Dispatcher
|
|
||||||
*
|
|
||||||
* @zh 负责分发触发器事件到订阅者
|
|
||||||
* @en Responsible for dispatching trigger events to subscribers
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { TriggerType, ITriggerContext } from './TriggerTypes';
|
|
||||||
import type { IBlueprintTrigger, ITriggerRegistry, TriggerCallback } from './BlueprintTrigger';
|
|
||||||
import { TriggerRegistry } from './BlueprintTrigger';
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 调度器接口 | Dispatcher Interface
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 触发结果
|
|
||||||
* @en Trigger result
|
|
||||||
*/
|
|
||||||
export interface TriggerResult {
|
|
||||||
/**
|
|
||||||
* @zh 触发器 ID
|
|
||||||
* @en Trigger ID
|
|
||||||
*/
|
|
||||||
triggerId: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 是否成功
|
|
||||||
* @en Is successful
|
|
||||||
*/
|
|
||||||
success: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 错误信息
|
|
||||||
* @en Error message
|
|
||||||
*/
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 调度结果
|
|
||||||
* @en Dispatch result
|
|
||||||
*/
|
|
||||||
export interface DispatchResult {
|
|
||||||
/**
|
|
||||||
* @zh 上下文
|
|
||||||
* @en Context
|
|
||||||
*/
|
|
||||||
context: ITriggerContext;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 触发的触发器数量
|
|
||||||
* @en Number of triggers fired
|
|
||||||
*/
|
|
||||||
triggeredCount: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 各触发器结果
|
|
||||||
* @en Results of each trigger
|
|
||||||
*/
|
|
||||||
results: TriggerResult[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 触发器调度器接口
|
|
||||||
* @en Trigger dispatcher interface
|
|
||||||
*/
|
|
||||||
export interface ITriggerDispatcher {
|
|
||||||
/**
|
|
||||||
* @zh 调度触发器
|
|
||||||
* @en Dispatch trigger
|
|
||||||
*/
|
|
||||||
dispatch(context: ITriggerContext): DispatchResult;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 异步调度触发器
|
|
||||||
* @en Async dispatch trigger
|
|
||||||
*/
|
|
||||||
dispatchAsync(context: ITriggerContext): Promise<DispatchResult>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 订阅触发器类型
|
|
||||||
* @en Subscribe to trigger type
|
|
||||||
*/
|
|
||||||
subscribe(type: TriggerType, callback: TriggerCallback): () => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 取消订阅
|
|
||||||
* @en Unsubscribe
|
|
||||||
*/
|
|
||||||
unsubscribe(type: TriggerType, callback: TriggerCallback): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 获取注册表
|
|
||||||
* @en Get registry
|
|
||||||
*/
|
|
||||||
readonly registry: ITriggerRegistry;
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 调度器实现 | Dispatcher Implementation
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 触发器调度器实现
|
|
||||||
* @en Trigger dispatcher implementation
|
|
||||||
*/
|
|
||||||
export class TriggerDispatcher implements ITriggerDispatcher {
|
|
||||||
private readonly _registry: ITriggerRegistry;
|
|
||||||
private readonly _typeSubscribers: Map<TriggerType, Set<TriggerCallback>> = new Map();
|
|
||||||
private readonly _globalSubscribers: Set<TriggerCallback> = new Set();
|
|
||||||
private _isDispatching: boolean = false;
|
|
||||||
private _pendingContexts: ITriggerContext[] = [];
|
|
||||||
|
|
||||||
constructor(registry?: ITriggerRegistry) {
|
|
||||||
this._registry = registry ?? new TriggerRegistry();
|
|
||||||
}
|
|
||||||
|
|
||||||
get registry(): ITriggerRegistry {
|
|
||||||
return this._registry;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 调度触发器
|
|
||||||
* @en Dispatch trigger
|
|
||||||
*/
|
|
||||||
dispatch(context: ITriggerContext): DispatchResult {
|
|
||||||
if (this._isDispatching) {
|
|
||||||
this._pendingContexts.push(context);
|
|
||||||
return {
|
|
||||||
context,
|
|
||||||
triggeredCount: 0,
|
|
||||||
results: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
this._isDispatching = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = this._doDispatch(context);
|
|
||||||
|
|
||||||
while (this._pendingContexts.length > 0) {
|
|
||||||
const pendingContext = this._pendingContexts.shift()!;
|
|
||||||
this._doDispatch(pendingContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} finally {
|
|
||||||
this._isDispatching = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 执行调度
|
|
||||||
* @en Do dispatch
|
|
||||||
*/
|
|
||||||
private _doDispatch(context: ITriggerContext): DispatchResult {
|
|
||||||
const results: TriggerResult[] = [];
|
|
||||||
let triggeredCount = 0;
|
|
||||||
|
|
||||||
const triggers = this._registry.getByType(context.type);
|
|
||||||
|
|
||||||
for (const trigger of triggers) {
|
|
||||||
if (trigger.shouldFire(context)) {
|
|
||||||
try {
|
|
||||||
trigger.fire(context);
|
|
||||||
triggeredCount++;
|
|
||||||
results.push({
|
|
||||||
triggerId: trigger.id,
|
|
||||||
success: true
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
results.push({
|
|
||||||
triggerId: trigger.id,
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : String(error)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this._notifySubscribers(context);
|
|
||||||
|
|
||||||
return {
|
|
||||||
context,
|
|
||||||
triggeredCount,
|
|
||||||
results
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 通知订阅者
|
|
||||||
* @en Notify subscribers
|
|
||||||
*/
|
|
||||||
private _notifySubscribers(context: ITriggerContext): void {
|
|
||||||
const typeSubscribers = this._typeSubscribers.get(context.type);
|
|
||||||
if (typeSubscribers) {
|
|
||||||
for (const callback of typeSubscribers) {
|
|
||||||
try {
|
|
||||||
callback(context);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Trigger subscriber error: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const callback of this._globalSubscribers) {
|
|
||||||
try {
|
|
||||||
callback(context);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Global trigger subscriber error: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 异步调度触发器
|
|
||||||
* @en Async dispatch trigger
|
|
||||||
*/
|
|
||||||
async dispatchAsync(context: ITriggerContext): Promise<DispatchResult> {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
queueMicrotask(() => {
|
|
||||||
resolve(this.dispatch(context));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 订阅触发器类型
|
|
||||||
* @en Subscribe to trigger type
|
|
||||||
*/
|
|
||||||
subscribe(type: TriggerType, callback: TriggerCallback): () => void {
|
|
||||||
if (!this._typeSubscribers.has(type)) {
|
|
||||||
this._typeSubscribers.set(type, new Set());
|
|
||||||
}
|
|
||||||
|
|
||||||
this._typeSubscribers.get(type)!.add(callback);
|
|
||||||
|
|
||||||
return () => this.unsubscribe(type, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 取消订阅
|
|
||||||
* @en Unsubscribe
|
|
||||||
*/
|
|
||||||
unsubscribe(type: TriggerType, callback: TriggerCallback): void {
|
|
||||||
const subscribers = this._typeSubscribers.get(type);
|
|
||||||
if (subscribers) {
|
|
||||||
subscribers.delete(callback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 订阅所有触发器
|
|
||||||
* @en Subscribe to all triggers
|
|
||||||
*/
|
|
||||||
subscribeAll(callback: TriggerCallback): () => void {
|
|
||||||
this._globalSubscribers.add(callback);
|
|
||||||
return () => this.unsubscribeAll(callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 取消订阅所有
|
|
||||||
* @en Unsubscribe from all
|
|
||||||
*/
|
|
||||||
unsubscribeAll(callback: TriggerCallback): void {
|
|
||||||
this._globalSubscribers.delete(callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 清除所有订阅
|
|
||||||
* @en Clear all subscriptions
|
|
||||||
*/
|
|
||||||
clearSubscriptions(): void {
|
|
||||||
this._typeSubscribers.clear();
|
|
||||||
this._globalSubscribers.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 实体触发器管理器 | Entity Trigger Manager
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 实体触发器管理器接口
|
|
||||||
* @en Entity trigger manager interface
|
|
||||||
*/
|
|
||||||
export interface IEntityTriggerManager {
|
|
||||||
/**
|
|
||||||
* @zh 为实体注册触发器
|
|
||||||
* @en Register trigger for entity
|
|
||||||
*/
|
|
||||||
registerForEntity(entityId: string, trigger: IBlueprintTrigger): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 注销实体的触发器
|
|
||||||
* @en Unregister trigger from entity
|
|
||||||
*/
|
|
||||||
unregisterFromEntity(entityId: string, triggerId: string): boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 获取实体的所有触发器
|
|
||||||
* @en Get all triggers for entity
|
|
||||||
*/
|
|
||||||
getEntityTriggers(entityId: string): IBlueprintTrigger[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 清除实体的所有触发器
|
|
||||||
* @en Clear all triggers for entity
|
|
||||||
*/
|
|
||||||
clearEntityTriggers(entityId: string): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 调度器
|
|
||||||
* @en Dispatcher
|
|
||||||
*/
|
|
||||||
readonly dispatcher: ITriggerDispatcher;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 实体触发器管理器实现
|
|
||||||
* @en Entity trigger manager implementation
|
|
||||||
*/
|
|
||||||
export class EntityTriggerManager implements IEntityTriggerManager {
|
|
||||||
private readonly _dispatcher: ITriggerDispatcher;
|
|
||||||
private readonly _entityTriggers: Map<string, Set<string>> = new Map();
|
|
||||||
|
|
||||||
constructor(dispatcher?: ITriggerDispatcher) {
|
|
||||||
this._dispatcher = dispatcher ?? new TriggerDispatcher();
|
|
||||||
}
|
|
||||||
|
|
||||||
get dispatcher(): ITriggerDispatcher {
|
|
||||||
return this._dispatcher;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 为实体注册触发器
|
|
||||||
* @en Register trigger for entity
|
|
||||||
*/
|
|
||||||
registerForEntity(entityId: string, trigger: IBlueprintTrigger): void {
|
|
||||||
this._dispatcher.registry.register(trigger);
|
|
||||||
|
|
||||||
if (!this._entityTriggers.has(entityId)) {
|
|
||||||
this._entityTriggers.set(entityId, new Set());
|
|
||||||
}
|
|
||||||
|
|
||||||
this._entityTriggers.get(entityId)!.add(trigger.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 注销实体的触发器
|
|
||||||
* @en Unregister trigger from entity
|
|
||||||
*/
|
|
||||||
unregisterFromEntity(entityId: string, triggerId: string): boolean {
|
|
||||||
const entitySet = this._entityTriggers.get(entityId);
|
|
||||||
if (!entitySet) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!entitySet.has(triggerId)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
entitySet.delete(triggerId);
|
|
||||||
return this._dispatcher.registry.unregister(triggerId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 获取实体的所有触发器
|
|
||||||
* @en Get all triggers for entity
|
|
||||||
*/
|
|
||||||
getEntityTriggers(entityId: string): IBlueprintTrigger[] {
|
|
||||||
const entitySet = this._entityTriggers.get(entityId);
|
|
||||||
if (!entitySet) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const triggers: IBlueprintTrigger[] = [];
|
|
||||||
for (const triggerId of entitySet) {
|
|
||||||
const trigger = this._dispatcher.registry.get(triggerId);
|
|
||||||
if (trigger) {
|
|
||||||
triggers.push(trigger);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return triggers;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 清除实体的所有触发器
|
|
||||||
* @en Clear all triggers for entity
|
|
||||||
*/
|
|
||||||
clearEntityTriggers(entityId: string): void {
|
|
||||||
const entitySet = this._entityTriggers.get(entityId);
|
|
||||||
if (!entitySet) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const triggerId of entitySet) {
|
|
||||||
this._dispatcher.registry.unregister(triggerId);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._entityTriggers.delete(entityId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 调度触发器到实体
|
|
||||||
* @en Dispatch trigger to entity
|
|
||||||
*/
|
|
||||||
dispatchToEntity(entityId: string, context: ITriggerContext): DispatchResult {
|
|
||||||
const entityTriggers = this.getEntityTriggers(entityId);
|
|
||||||
const results: TriggerResult[] = [];
|
|
||||||
let triggeredCount = 0;
|
|
||||||
|
|
||||||
for (const trigger of entityTriggers) {
|
|
||||||
if (trigger.shouldFire(context)) {
|
|
||||||
try {
|
|
||||||
trigger.fire(context);
|
|
||||||
triggeredCount++;
|
|
||||||
results.push({
|
|
||||||
triggerId: trigger.id,
|
|
||||||
success: true
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
results.push({
|
|
||||||
triggerId: trigger.id,
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : String(error)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
context,
|
|
||||||
triggeredCount,
|
|
||||||
results
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 工厂函数 | Factory Functions
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 创建触发器调度器
|
|
||||||
* @en Create trigger dispatcher
|
|
||||||
*/
|
|
||||||
export function createTriggerDispatcher(registry?: ITriggerRegistry): TriggerDispatcher {
|
|
||||||
return new TriggerDispatcher(registry);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 创建实体触发器管理器
|
|
||||||
* @en Create entity trigger manager
|
|
||||||
*/
|
|
||||||
export function createEntityTriggerManager(dispatcher?: ITriggerDispatcher): EntityTriggerManager {
|
|
||||||
return new EntityTriggerManager(dispatcher);
|
|
||||||
}
|
|
||||||
@@ -1,400 +0,0 @@
|
|||||||
/**
|
|
||||||
* @zh 蓝图触发器类型定义
|
|
||||||
* @en Blueprint Trigger Type Definitions
|
|
||||||
*
|
|
||||||
* @zh 定义触发器的核心类型和接口
|
|
||||||
* @en Defines core types and interfaces for triggers
|
|
||||||
*/
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 触发器类型 | Trigger Types
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 触发器类型枚举
|
|
||||||
* @en Trigger type enumeration
|
|
||||||
*/
|
|
||||||
export type TriggerType =
|
|
||||||
| 'tick' // 每帧触发 | Every frame
|
|
||||||
| 'input' // 输入事件 | Input event
|
|
||||||
| 'collision' // 碰撞事件 | Collision event
|
|
||||||
| 'message' // 消息事件 | Message event
|
|
||||||
| 'timer' // 定时器事件 | Timer event
|
|
||||||
| 'stateEnter' // 状态进入 | State enter
|
|
||||||
| 'stateExit' // 状态退出 | State exit
|
|
||||||
| 'custom'; // 自定义事件 | Custom event
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 触发器类型常量
|
|
||||||
* @en Trigger type constants
|
|
||||||
*/
|
|
||||||
export const TriggerTypes = {
|
|
||||||
TICK: 'tick' as const,
|
|
||||||
INPUT: 'input' as const,
|
|
||||||
COLLISION: 'collision' as const,
|
|
||||||
MESSAGE: 'message' as const,
|
|
||||||
TIMER: 'timer' as const,
|
|
||||||
STATE_ENTER: 'stateEnter' as const,
|
|
||||||
STATE_EXIT: 'stateExit' as const,
|
|
||||||
CUSTOM: 'custom' as const
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 触发器上下文 | Trigger Context
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 触发器上下文基础接口
|
|
||||||
* @en Trigger context base interface
|
|
||||||
*/
|
|
||||||
export interface ITriggerContext {
|
|
||||||
/**
|
|
||||||
* @zh 触发器类型
|
|
||||||
* @en Trigger type
|
|
||||||
*/
|
|
||||||
readonly type: TriggerType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 触发时间戳
|
|
||||||
* @en Trigger timestamp
|
|
||||||
*/
|
|
||||||
readonly timestamp: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 触发源实体 ID
|
|
||||||
* @en Source entity ID
|
|
||||||
*/
|
|
||||||
readonly sourceEntityId?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 附加数据
|
|
||||||
* @en Additional data
|
|
||||||
*/
|
|
||||||
readonly data?: Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh Tick 触发器上下文
|
|
||||||
* @en Tick trigger context
|
|
||||||
*/
|
|
||||||
export interface ITickTriggerContext extends ITriggerContext {
|
|
||||||
readonly type: 'tick';
|
|
||||||
/**
|
|
||||||
* @zh 增量时间(秒)
|
|
||||||
* @en Delta time (seconds)
|
|
||||||
*/
|
|
||||||
readonly deltaTime: number;
|
|
||||||
/**
|
|
||||||
* @zh 帧计数
|
|
||||||
* @en Frame count
|
|
||||||
*/
|
|
||||||
readonly frameCount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 输入触发器上下文
|
|
||||||
* @en Input trigger context
|
|
||||||
*/
|
|
||||||
export interface IInputTriggerContext extends ITriggerContext {
|
|
||||||
readonly type: 'input';
|
|
||||||
/**
|
|
||||||
* @zh 输入动作名称
|
|
||||||
* @en Input action name
|
|
||||||
*/
|
|
||||||
readonly action: string;
|
|
||||||
/**
|
|
||||||
* @zh 输入值
|
|
||||||
* @en Input value
|
|
||||||
*/
|
|
||||||
readonly value: number | boolean;
|
|
||||||
/**
|
|
||||||
* @zh 是否刚按下
|
|
||||||
* @en Is just pressed
|
|
||||||
*/
|
|
||||||
readonly pressed?: boolean;
|
|
||||||
/**
|
|
||||||
* @zh 是否刚释放
|
|
||||||
* @en Is just released
|
|
||||||
*/
|
|
||||||
readonly released?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 碰撞触发器上下文
|
|
||||||
* @en Collision trigger context
|
|
||||||
*/
|
|
||||||
export interface ICollisionTriggerContext extends ITriggerContext {
|
|
||||||
readonly type: 'collision';
|
|
||||||
/**
|
|
||||||
* @zh 碰撞的另一个实体 ID
|
|
||||||
* @en Other entity ID in collision
|
|
||||||
*/
|
|
||||||
readonly otherEntityId: string;
|
|
||||||
/**
|
|
||||||
* @zh 碰撞点
|
|
||||||
* @en Collision point
|
|
||||||
*/
|
|
||||||
readonly point?: { x: number; y: number };
|
|
||||||
/**
|
|
||||||
* @zh 碰撞法线
|
|
||||||
* @en Collision normal
|
|
||||||
*/
|
|
||||||
readonly normal?: { x: number; y: number };
|
|
||||||
/**
|
|
||||||
* @zh 是否开始碰撞
|
|
||||||
* @en Is collision start
|
|
||||||
*/
|
|
||||||
readonly isEnter: boolean;
|
|
||||||
/**
|
|
||||||
* @zh 是否结束碰撞
|
|
||||||
* @en Is collision end
|
|
||||||
*/
|
|
||||||
readonly isExit: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 消息触发器上下文
|
|
||||||
* @en Message trigger context
|
|
||||||
*/
|
|
||||||
export interface IMessageTriggerContext extends ITriggerContext {
|
|
||||||
readonly type: 'message';
|
|
||||||
/**
|
|
||||||
* @zh 消息名称
|
|
||||||
* @en Message name
|
|
||||||
*/
|
|
||||||
readonly messageName: string;
|
|
||||||
/**
|
|
||||||
* @zh 发送者 ID
|
|
||||||
* @en Sender ID
|
|
||||||
*/
|
|
||||||
readonly senderId?: string;
|
|
||||||
/**
|
|
||||||
* @zh 消息负载
|
|
||||||
* @en Message payload
|
|
||||||
*/
|
|
||||||
readonly payload?: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 定时器触发器上下文
|
|
||||||
* @en Timer trigger context
|
|
||||||
*/
|
|
||||||
export interface ITimerTriggerContext extends ITriggerContext {
|
|
||||||
readonly type: 'timer';
|
|
||||||
/**
|
|
||||||
* @zh 定时器 ID
|
|
||||||
* @en Timer ID
|
|
||||||
*/
|
|
||||||
readonly timerId: string;
|
|
||||||
/**
|
|
||||||
* @zh 是否循环触发
|
|
||||||
* @en Is repeating
|
|
||||||
*/
|
|
||||||
readonly isRepeating: boolean;
|
|
||||||
/**
|
|
||||||
* @zh 已触发次数
|
|
||||||
* @en Times fired
|
|
||||||
*/
|
|
||||||
readonly timesFired: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 状态触发器上下文
|
|
||||||
* @en State trigger context
|
|
||||||
*/
|
|
||||||
export interface IStateTriggerContext extends ITriggerContext {
|
|
||||||
readonly type: 'stateEnter' | 'stateExit';
|
|
||||||
/**
|
|
||||||
* @zh 状态机 ID
|
|
||||||
* @en State machine ID
|
|
||||||
*/
|
|
||||||
readonly stateMachineId: string;
|
|
||||||
/**
|
|
||||||
* @zh 当前状态
|
|
||||||
* @en Current state
|
|
||||||
*/
|
|
||||||
readonly currentState: string;
|
|
||||||
/**
|
|
||||||
* @zh 之前状态
|
|
||||||
* @en Previous state
|
|
||||||
*/
|
|
||||||
readonly previousState?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 自定义触发器上下文
|
|
||||||
* @en Custom trigger context
|
|
||||||
*/
|
|
||||||
export interface ICustomTriggerContext extends ITriggerContext {
|
|
||||||
readonly type: 'custom';
|
|
||||||
/**
|
|
||||||
* @zh 事件名称
|
|
||||||
* @en Event name
|
|
||||||
*/
|
|
||||||
readonly eventName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 所有触发器上下文的联合类型
|
|
||||||
* @en Union type of all trigger contexts
|
|
||||||
*/
|
|
||||||
export type TriggerContext =
|
|
||||||
| ITickTriggerContext
|
|
||||||
| IInputTriggerContext
|
|
||||||
| ICollisionTriggerContext
|
|
||||||
| IMessageTriggerContext
|
|
||||||
| ITimerTriggerContext
|
|
||||||
| IStateTriggerContext
|
|
||||||
| ICustomTriggerContext;
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 工厂函数 | Factory Functions
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 创建 Tick 触发器上下文
|
|
||||||
* @en Create tick trigger context
|
|
||||||
*/
|
|
||||||
export function createTickContext(
|
|
||||||
deltaTime: number,
|
|
||||||
frameCount: number,
|
|
||||||
sourceEntityId?: string
|
|
||||||
): ITickTriggerContext {
|
|
||||||
return {
|
|
||||||
type: 'tick',
|
|
||||||
timestamp: Date.now(),
|
|
||||||
deltaTime,
|
|
||||||
frameCount,
|
|
||||||
sourceEntityId
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 创建输入触发器上下文
|
|
||||||
* @en Create input trigger context
|
|
||||||
*/
|
|
||||||
export function createInputContext(
|
|
||||||
action: string,
|
|
||||||
value: number | boolean,
|
|
||||||
options?: {
|
|
||||||
pressed?: boolean;
|
|
||||||
released?: boolean;
|
|
||||||
sourceEntityId?: string;
|
|
||||||
}
|
|
||||||
): IInputTriggerContext {
|
|
||||||
return {
|
|
||||||
type: 'input',
|
|
||||||
timestamp: Date.now(),
|
|
||||||
action,
|
|
||||||
value,
|
|
||||||
pressed: options?.pressed,
|
|
||||||
released: options?.released,
|
|
||||||
sourceEntityId: options?.sourceEntityId
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 创建碰撞触发器上下文
|
|
||||||
* @en Create collision trigger context
|
|
||||||
*/
|
|
||||||
export function createCollisionContext(
|
|
||||||
otherEntityId: string,
|
|
||||||
isEnter: boolean,
|
|
||||||
options?: {
|
|
||||||
point?: { x: number; y: number };
|
|
||||||
normal?: { x: number; y: number };
|
|
||||||
sourceEntityId?: string;
|
|
||||||
}
|
|
||||||
): ICollisionTriggerContext {
|
|
||||||
return {
|
|
||||||
type: 'collision',
|
|
||||||
timestamp: Date.now(),
|
|
||||||
otherEntityId,
|
|
||||||
isEnter,
|
|
||||||
isExit: !isEnter,
|
|
||||||
point: options?.point,
|
|
||||||
normal: options?.normal,
|
|
||||||
sourceEntityId: options?.sourceEntityId
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 创建消息触发器上下文
|
|
||||||
* @en Create message trigger context
|
|
||||||
*/
|
|
||||||
export function createMessageContext(
|
|
||||||
messageName: string,
|
|
||||||
payload?: unknown,
|
|
||||||
options?: {
|
|
||||||
senderId?: string;
|
|
||||||
sourceEntityId?: string;
|
|
||||||
}
|
|
||||||
): IMessageTriggerContext {
|
|
||||||
return {
|
|
||||||
type: 'message',
|
|
||||||
timestamp: Date.now(),
|
|
||||||
messageName,
|
|
||||||
payload,
|
|
||||||
senderId: options?.senderId,
|
|
||||||
sourceEntityId: options?.sourceEntityId
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 创建定时器触发器上下文
|
|
||||||
* @en Create timer trigger context
|
|
||||||
*/
|
|
||||||
export function createTimerContext(
|
|
||||||
timerId: string,
|
|
||||||
isRepeating: boolean,
|
|
||||||
timesFired: number,
|
|
||||||
sourceEntityId?: string
|
|
||||||
): ITimerTriggerContext {
|
|
||||||
return {
|
|
||||||
type: 'timer',
|
|
||||||
timestamp: Date.now(),
|
|
||||||
timerId,
|
|
||||||
isRepeating,
|
|
||||||
timesFired,
|
|
||||||
sourceEntityId
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 创建状态触发器上下文
|
|
||||||
* @en Create state trigger context
|
|
||||||
*/
|
|
||||||
export function createStateContext(
|
|
||||||
type: 'stateEnter' | 'stateExit',
|
|
||||||
stateMachineId: string,
|
|
||||||
currentState: string,
|
|
||||||
previousState?: string,
|
|
||||||
sourceEntityId?: string
|
|
||||||
): IStateTriggerContext {
|
|
||||||
return {
|
|
||||||
type,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
stateMachineId,
|
|
||||||
currentState,
|
|
||||||
previousState,
|
|
||||||
sourceEntityId
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 创建自定义触发器上下文
|
|
||||||
* @en Create custom trigger context
|
|
||||||
*/
|
|
||||||
export function createCustomContext(
|
|
||||||
eventName: string,
|
|
||||||
data?: Record<string, unknown>,
|
|
||||||
sourceEntityId?: string
|
|
||||||
): ICustomTriggerContext {
|
|
||||||
return {
|
|
||||||
type: 'custom',
|
|
||||||
timestamp: Date.now(),
|
|
||||||
eventName,
|
|
||||||
data,
|
|
||||||
sourceEntityId
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
/**
|
|
||||||
* @zh 蓝图触发器模块
|
|
||||||
* @en Blueprint Triggers Module
|
|
||||||
*
|
|
||||||
* @zh 提供蓝图触发器系统的所有导出
|
|
||||||
* @en Provides all exports for the blueprint trigger system
|
|
||||||
*/
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 触发器类型 | Trigger Types
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
export type {
|
|
||||||
TriggerType,
|
|
||||||
ITriggerContext,
|
|
||||||
ITickTriggerContext,
|
|
||||||
IInputTriggerContext,
|
|
||||||
ICollisionTriggerContext,
|
|
||||||
IMessageTriggerContext,
|
|
||||||
ITimerTriggerContext,
|
|
||||||
IStateTriggerContext,
|
|
||||||
ICustomTriggerContext,
|
|
||||||
TriggerContext
|
|
||||||
} from './TriggerTypes';
|
|
||||||
|
|
||||||
export {
|
|
||||||
TriggerTypes,
|
|
||||||
createTickContext,
|
|
||||||
createInputContext,
|
|
||||||
createCollisionContext,
|
|
||||||
createMessageContext,
|
|
||||||
createTimerContext,
|
|
||||||
createStateContext,
|
|
||||||
createCustomContext
|
|
||||||
} from './TriggerTypes';
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 触发器条件 | Trigger Conditions
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
export type {
|
|
||||||
ITriggerCondition,
|
|
||||||
ConditionLogic
|
|
||||||
} from './TriggerCondition';
|
|
||||||
|
|
||||||
export {
|
|
||||||
CompositeCondition,
|
|
||||||
NotCondition,
|
|
||||||
AlwaysTrueCondition,
|
|
||||||
AlwaysFalseCondition,
|
|
||||||
TriggerTypeCondition,
|
|
||||||
EntityIdCondition,
|
|
||||||
FunctionCondition,
|
|
||||||
InputActionCondition,
|
|
||||||
MessageNameCondition,
|
|
||||||
StateNameCondition,
|
|
||||||
TimerIdCondition,
|
|
||||||
CollisionEntityCondition,
|
|
||||||
CustomEventCondition,
|
|
||||||
ConditionBuilder,
|
|
||||||
condition
|
|
||||||
} from './TriggerCondition';
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 蓝图触发器 | Blueprint Trigger
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
export type {
|
|
||||||
TriggerCallback,
|
|
||||||
IBlueprintTrigger,
|
|
||||||
TriggerConfig,
|
|
||||||
ITriggerRegistry
|
|
||||||
} from './BlueprintTrigger';
|
|
||||||
|
|
||||||
export {
|
|
||||||
BlueprintTrigger,
|
|
||||||
TriggerRegistry,
|
|
||||||
createTrigger,
|
|
||||||
createTickTrigger,
|
|
||||||
createInputTrigger,
|
|
||||||
createCollisionTrigger,
|
|
||||||
createMessageTrigger,
|
|
||||||
createTimerTrigger,
|
|
||||||
createStateEnterTrigger,
|
|
||||||
createStateExitTrigger,
|
|
||||||
createCustomTrigger
|
|
||||||
} from './BlueprintTrigger';
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 触发器调度器 | Trigger Dispatcher
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
export type {
|
|
||||||
TriggerResult,
|
|
||||||
DispatchResult,
|
|
||||||
ITriggerDispatcher,
|
|
||||||
IEntityTriggerManager
|
|
||||||
} from './TriggerDispatcher';
|
|
||||||
|
|
||||||
export {
|
|
||||||
TriggerDispatcher,
|
|
||||||
EntityTriggerManager,
|
|
||||||
createTriggerDispatcher,
|
|
||||||
createEntityTriggerManager
|
|
||||||
} from './TriggerDispatcher';
|
|
||||||
@@ -91,9 +91,6 @@ export const STANDARD_EXTERNALS = [
|
|||||||
'zustand',
|
'zustand',
|
||||||
'immer',
|
'immer',
|
||||||
|
|
||||||
// Tauri (由宿主应用提供) | Provided by host app
|
|
||||||
/^@tauri-apps\//,
|
|
||||||
|
|
||||||
// 所有 @esengine 包
|
// 所有 @esengine 包
|
||||||
/^@esengine\//,
|
/^@esengine\//,
|
||||||
] as const;
|
] as const;
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "camera-editor",
|
|
||||||
"name": "@esengine/camera-editor",
|
|
||||||
"displayName": "Camera Editor",
|
|
||||||
"description": "Editor support for camera system | 相机系统编辑器支持",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"category": "Editor",
|
|
||||||
"icon": "Camera",
|
|
||||||
"isEditorPlugin": true,
|
|
||||||
"runtimeModule": "@esengine/camera",
|
|
||||||
"exports": {
|
|
||||||
"inspectors": ["CameraComponentInspector"],
|
|
||||||
"gizmos": ["CameraGizmo"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -21,15 +21,10 @@
|
|||||||
"type-check": "tsc --noEmit",
|
"type-check": "tsc --noEmit",
|
||||||
"clean": "rimraf dist"
|
"clean": "rimraf dist"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
|
||||||
"@esengine/camera": "workspace:*"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@esengine/editor-core": "workspace:*"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@esengine/ecs-framework": "workspace:*",
|
"@esengine/ecs-framework": "workspace:*",
|
||||||
"@esengine/engine-core": "workspace:*",
|
"@esengine/engine-core": "workspace:*",
|
||||||
|
"@esengine/camera": "workspace:*",
|
||||||
"@esengine/editor-core": "workspace:*",
|
"@esengine/editor-core": "workspace:*",
|
||||||
"@esengine/build-config": "workspace:*",
|
"@esengine/build-config": "workspace:*",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ import type {
|
|||||||
import {
|
import {
|
||||||
EntityStoreService,
|
EntityStoreService,
|
||||||
MessageHub,
|
MessageHub,
|
||||||
EditorComponentRegistry
|
ComponentRegistry
|
||||||
} from '@esengine/editor-core';
|
} from '@esengine/editor-core';
|
||||||
import { CameraComponent } from '@esengine/camera';
|
import { CameraComponent } from '@esengine/camera';
|
||||||
|
|
||||||
export class CameraEditorModule implements IEditorModuleLoader {
|
export class CameraEditorModule implements IEditorModuleLoader {
|
||||||
async install(services: ServiceContainer): Promise<void> {
|
async install(services: ServiceContainer): Promise<void> {
|
||||||
const componentRegistry = services.resolve(EditorComponentRegistry);
|
const componentRegistry = services.resolve(ComponentRegistry);
|
||||||
if (componentRegistry) {
|
if (componentRegistry) {
|
||||||
componentRegistry.register({
|
componentRegistry.register({
|
||||||
name: 'Camera',
|
name: 'Camera',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { IComponentRegistry, IScene } from '@esengine/ecs-framework';
|
import type { IComponentRegistry, IScene } from '@esengine/ecs-framework';
|
||||||
import type { IRuntimeModule, IRuntimePlugin, ModuleManifest, SystemContext } from '@esengine/engine-core';
|
import type { IRuntimeModule, IRuntimePlugin, ModuleManifest, SystemContext } from '@esengine/engine-core';
|
||||||
import { RenderConfigServiceToken } from '@esengine/engine-core';
|
import { EngineBridgeToken } from '@esengine/engine-core';
|
||||||
import { CameraComponent } from './CameraComponent';
|
import { CameraComponent } from './CameraComponent';
|
||||||
import { CameraSystem } from './CameraSystem';
|
import { CameraSystem } from './CameraSystem';
|
||||||
|
|
||||||
@@ -10,15 +10,15 @@ class CameraRuntimeModule implements IRuntimeModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createSystems(scene: IScene, context: SystemContext): void {
|
createSystems(scene: IScene, context: SystemContext): void {
|
||||||
// 从服务注册表获取渲染配置服务 | Get render config service from registry
|
// 从服务注册表获取 EngineBridge | Get EngineBridge from service registry
|
||||||
const renderConfig = context.services.get(RenderConfigServiceToken);
|
const bridge = context.services.get(EngineBridgeToken);
|
||||||
if (!renderConfig) {
|
if (!bridge) {
|
||||||
console.warn('[CameraPlugin] RenderConfigService not found, CameraSystem will not be created');
|
console.warn('[CameraPlugin] EngineBridge not found, CameraSystem will not be created');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建并添加 CameraSystem | Create and add CameraSystem
|
// 创建并添加 CameraSystem | Create and add CameraSystem
|
||||||
const cameraSystem = new CameraSystem(renderConfig);
|
const cameraSystem = new CameraSystem(bridge);
|
||||||
scene.addSystem(cameraSystem);
|
scene.addSystem(cameraSystem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,18 +4,18 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { EntitySystem, Matcher, Entity, ECSSystem } from '@esengine/ecs-framework';
|
import { EntitySystem, Matcher, Entity, ECSSystem } from '@esengine/ecs-framework';
|
||||||
import type { IRenderConfigService } from '@esengine/engine-core';
|
import type { IEngineBridge } from '@esengine/engine-core';
|
||||||
import { CameraComponent } from './CameraComponent';
|
import { CameraComponent } from './CameraComponent';
|
||||||
|
|
||||||
@ECSSystem('Camera', { updateOrder: -100 })
|
@ECSSystem('Camera', { updateOrder: -100 })
|
||||||
export class CameraSystem extends EntitySystem {
|
export class CameraSystem extends EntitySystem {
|
||||||
private renderConfig: IRenderConfigService;
|
private bridge: IEngineBridge;
|
||||||
private lastAppliedCameraId: number | null = null;
|
private lastAppliedCameraId: number | null = null;
|
||||||
|
|
||||||
constructor(renderConfig: IRenderConfigService) {
|
constructor(bridge: IEngineBridge) {
|
||||||
// Match entities with CameraComponent
|
// Match entities with CameraComponent
|
||||||
super(Matcher.empty().all(CameraComponent));
|
super(Matcher.empty().all(CameraComponent));
|
||||||
this.renderConfig = renderConfig;
|
this.bridge = bridge;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override onBegin(): void {
|
protected override onBegin(): void {
|
||||||
@@ -47,6 +47,6 @@ export class CameraSystem extends EntitySystem {
|
|||||||
const r = parseInt(bgColor.slice(1, 3), 16) / 255;
|
const r = parseInt(bgColor.slice(1, 3), 16) / 255;
|
||||||
const g = parseInt(bgColor.slice(3, 5), 16) / 255;
|
const g = parseInt(bgColor.slice(3, 5), 16) / 255;
|
||||||
const b = parseInt(bgColor.slice(5, 7), 16) / 255;
|
const b = parseInt(bgColor.slice(5, 7), 16) / 255;
|
||||||
this.renderConfig.setClearColor(r, g, b, 1.0);
|
this.bridge.setClearColor(r, g, b, 1.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,188 +0,0 @@
|
|||||||
<h1 align="center">
|
|
||||||
@esengine/ecs-framework
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<strong>High-performance ECS Framework for JavaScript Game Engines</strong>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<a href="https://www.npmjs.com/package/@esengine/ecs-framework"><img src="https://img.shields.io/npm/v/@esengine/ecs-framework?style=flat-square&color=blue" alt="npm"></a>
|
|
||||||
<a href="https://github.com/esengine/esengine/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="license"></a>
|
|
||||||
<img src="https://img.shields.io/badge/TypeScript-5.0+-blue?style=flat-square&logo=typescript&logoColor=white" alt="TypeScript">
|
|
||||||
<img src="https://img.shields.io/badge/zero-dependencies-brightgreen?style=flat-square" alt="zero dependencies">
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<b>English</b> | <a href="./README_CN.md">中文</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
A standalone, zero-dependency ECS (Entity-Component-System) framework designed for use with **any** JavaScript game engine:
|
|
||||||
|
|
||||||
- **Cocos Creator**
|
|
||||||
- **Laya**
|
|
||||||
- **Egret**
|
|
||||||
- **Phaser**
|
|
||||||
- **Or your own engine**
|
|
||||||
|
|
||||||
This package is the core of [ESEngine](https://github.com/esengine/esengine), but can be used completely independently.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
### npm / pnpm / yarn
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install @esengine/ecs-framework
|
|
||||||
```
|
|
||||||
|
|
||||||
### Clone Source Code Only
|
|
||||||
|
|
||||||
If you only want the ECS framework source code (not the full ESEngine):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Step 1: Clone repo skeleton without downloading files (requires Git 2.25+)
|
|
||||||
git clone --filter=blob:none --sparse https://github.com/esengine/esengine.git
|
|
||||||
|
|
||||||
# Step 2: Enter directory
|
|
||||||
cd esengine
|
|
||||||
|
|
||||||
# Step 3: Specify which folder to checkout
|
|
||||||
git sparse-checkout set packages/core
|
|
||||||
|
|
||||||
# Now you only have packages/core/ - other folders are not downloaded
|
|
||||||
```
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import {
|
|
||||||
Core, Scene, Entity, Component, EntitySystem,
|
|
||||||
Matcher, Time, ECSComponent, ECSSystem
|
|
||||||
} from '@esengine/ecs-framework';
|
|
||||||
|
|
||||||
// Define components (pure data)
|
|
||||||
@ECSComponent('Position')
|
|
||||||
class Position extends Component {
|
|
||||||
x = 0;
|
|
||||||
y = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@ECSComponent('Velocity')
|
|
||||||
class Velocity extends Component {
|
|
||||||
dx = 0;
|
|
||||||
dy = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define system (logic)
|
|
||||||
@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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize
|
|
||||||
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);
|
|
||||||
|
|
||||||
// Game loop (integrate with your engine's loop)
|
|
||||||
function update(dt: number) {
|
|
||||||
Core.update(dt);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Integration Examples
|
|
||||||
|
|
||||||
### With Cocos Creator
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { _decorator, Component as CCComponent } from 'cc';
|
|
||||||
import { Core, Scene } from '@esengine/ecs-framework';
|
|
||||||
|
|
||||||
const { ccclass } = _decorator;
|
|
||||||
|
|
||||||
@ccclass('GameManager')
|
|
||||||
export class GameManager extends CCComponent {
|
|
||||||
private scene: Scene;
|
|
||||||
|
|
||||||
onLoad() {
|
|
||||||
Core.create();
|
|
||||||
this.scene = new Scene();
|
|
||||||
// Register your systems...
|
|
||||||
Core.setScene(this.scene);
|
|
||||||
}
|
|
||||||
|
|
||||||
update(dt: number) {
|
|
||||||
Core.update(dt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### With Laya
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { Core, Scene } from '@esengine/ecs-framework';
|
|
||||||
|
|
||||||
export class Main {
|
|
||||||
private scene: Scene;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
Core.create();
|
|
||||||
this.scene = new Scene();
|
|
||||||
Core.setScene(this.scene);
|
|
||||||
|
|
||||||
Laya.timer.frameLoop(1, this, this.onUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
onUpdate() {
|
|
||||||
Core.update(Laya.timer.delta / 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
| Feature | Description |
|
|
||||||
|---------|-------------|
|
|
||||||
| **Zero Dependencies** | No external runtime dependencies |
|
|
||||||
| **Type-Safe Queries** | Fluent API with full TypeScript support |
|
|
||||||
| **Change Detection** | Epoch-based dirty tracking for optimization |
|
|
||||||
| **Serialization** | Built-in scene serialization and snapshots |
|
|
||||||
| **Service Container** | Dependency injection for systems |
|
|
||||||
| **Performance Monitoring** | Built-in profiling tools |
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
- [API Reference](https://esengine.cn/api/README)
|
|
||||||
- [Architecture Guide](https://esengine.cn/guide/)
|
|
||||||
- [Full ESEngine Documentation](https://esengine.cn/)
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT License - Use freely in commercial and open source projects.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
Part of <a href="https://github.com/esengine/esengine">ESEngine</a> · Can be used standalone
|
|
||||||
</p>
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
<h1 align="center">
|
|
||||||
@esengine/ecs-framework
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<strong>适用于 JavaScript 游戏引擎的高性能 ECS 框架</strong>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<a href="https://www.npmjs.com/package/@esengine/ecs-framework"><img src="https://img.shields.io/npm/v/@esengine/ecs-framework?style=flat-square&color=blue" alt="npm"></a>
|
|
||||||
<a href="https://github.com/esengine/esengine/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="license"></a>
|
|
||||||
<img src="https://img.shields.io/badge/TypeScript-5.0+-blue?style=flat-square&logo=typescript&logoColor=white" alt="TypeScript">
|
|
||||||
<img src="https://img.shields.io/badge/zero-dependencies-brightgreen?style=flat-square" alt="零依赖">
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<a href="./README.md">English</a> | <b>中文</b>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
|
|
||||||
一个独立的、零依赖的 ECS(实体-组件-系统)框架,可与**任何** JavaScript 游戏引擎配合使用:
|
|
||||||
|
|
||||||
- **Cocos Creator**
|
|
||||||
- **Laya**
|
|
||||||
- **Egret**
|
|
||||||
- **Phaser**
|
|
||||||
- **或你自己的引擎**
|
|
||||||
|
|
||||||
这个包是 [ESEngine](https://github.com/esengine/esengine) 的核心,但可以完全独立使用。
|
|
||||||
|
|
||||||
## 安装
|
|
||||||
|
|
||||||
### npm / pnpm / yarn
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install @esengine/ecs-framework
|
|
||||||
```
|
|
||||||
|
|
||||||
### 仅克隆源码
|
|
||||||
|
|
||||||
如果你只想要 ECS 框架源码(不需要完整的 ESEngine):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 第一步:克隆仓库骨架,不下载文件内容(需要 Git 2.25+)
|
|
||||||
git clone --filter=blob:none --sparse https://github.com/esengine/esengine.git
|
|
||||||
|
|
||||||
# 第二步:进入目录
|
|
||||||
cd esengine
|
|
||||||
|
|
||||||
# 第三步:指定要检出的目录
|
|
||||||
git sparse-checkout set packages/core
|
|
||||||
|
|
||||||
# 完成!现在你只有 packages/core/ 目录,其他文件夹不会下载
|
|
||||||
```
|
|
||||||
|
|
||||||
## 快速开始
|
|
||||||
|
|
||||||
```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);
|
|
||||||
|
|
||||||
// 游戏循环(与你的引擎循环集成)
|
|
||||||
function update(dt: number) {
|
|
||||||
Core.update(dt);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 集成示例
|
|
||||||
|
|
||||||
### Cocos Creator
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { _decorator, Component as CCComponent } from 'cc';
|
|
||||||
import { Core, Scene } from '@esengine/ecs-framework';
|
|
||||||
|
|
||||||
const { ccclass } = _decorator;
|
|
||||||
|
|
||||||
@ccclass('GameManager')
|
|
||||||
export class GameManager extends CCComponent {
|
|
||||||
private scene: Scene;
|
|
||||||
|
|
||||||
onLoad() {
|
|
||||||
Core.create();
|
|
||||||
this.scene = new Scene();
|
|
||||||
// 注册你的系统...
|
|
||||||
Core.setScene(this.scene);
|
|
||||||
}
|
|
||||||
|
|
||||||
update(dt: number) {
|
|
||||||
Core.update(dt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Laya
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { Core, Scene } from '@esengine/ecs-framework';
|
|
||||||
|
|
||||||
export class Main {
|
|
||||||
private scene: Scene;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
Core.create();
|
|
||||||
this.scene = new Scene();
|
|
||||||
Core.setScene(this.scene);
|
|
||||||
|
|
||||||
Laya.timer.frameLoop(1, this, this.onUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
onUpdate() {
|
|
||||||
Core.update(Laya.timer.delta / 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 特性
|
|
||||||
|
|
||||||
| 特性 | 描述 |
|
|
||||||
|------|------|
|
|
||||||
| **零依赖** | 无外部运行时依赖 |
|
|
||||||
| **类型安全查询** | 流畅的 API,完整 TypeScript 支持 |
|
|
||||||
| **变更检测** | 基于 Epoch 的脏标记优化 |
|
|
||||||
| **序列化** | 内置场景序列化和快照 |
|
|
||||||
| **服务容器** | 系统的依赖注入 |
|
|
||||||
| **性能监控** | 内置性能分析工具 |
|
|
||||||
|
|
||||||
## 文档
|
|
||||||
|
|
||||||
- [API 参考](https://esengine.cn/api/README)
|
|
||||||
- [架构指南](https://esengine.cn/guide/)
|
|
||||||
- [完整 ESEngine 文档](https://esengine.cn/)
|
|
||||||
|
|
||||||
## 许可证
|
|
||||||
|
|
||||||
MIT 协议 - 可自由用于商业和开源项目。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<a href="https://github.com/esengine/esengine">ESEngine</a> 的一部分 · 可独立使用
|
|
||||||
</p>
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@esengine/ecs-framework",
|
"name": "@esengine/ecs-framework",
|
||||||
"version": "2.4.2",
|
"version": "2.4.0",
|
||||||
"description": "用于Laya、Cocos Creator等JavaScript游戏引擎的高性能ECS框架",
|
"description": "用于Laya、Cocos Creator等JavaScript游戏引擎的高性能ECS框架",
|
||||||
"main": "dist/index.cjs",
|
"main": "dist/index.cjs",
|
||||||
"module": "dist/index.mjs",
|
"module": "dist/index.mjs",
|
||||||
|
|||||||
+188
-168
@@ -18,72 +18,69 @@ import { DebugConfigService } from './Utils/Debug/DebugConfigService';
|
|||||||
import { createInstance } from './Core/DI/Decorators';
|
import { createInstance } from './Core/DI/Decorators';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 游戏引擎核心类
|
* 游戏引擎核心类
|
||||||
* @en Game engine core class
|
|
||||||
*
|
*
|
||||||
* @zh 职责:
|
* 职责:
|
||||||
* - 提供全局服务(Timer、Performance、Pool等)
|
* - 提供全局服务(Timer、Performance、Pool等)
|
||||||
* - 管理场景生命周期(内置SceneManager)
|
* - 管理场景生命周期(内置SceneManager)
|
||||||
* - 管理全局管理器的生命周期
|
* - 管理全局管理器的生命周期
|
||||||
* - 提供统一的游戏循环更新入口
|
* - 提供统一的游戏循环更新入口
|
||||||
* @en Responsibilities:
|
|
||||||
* - Provide global services (Timer, Performance, Pool, etc.)
|
|
||||||
* - Manage scene lifecycle (built-in SceneManager)
|
|
||||||
* - Manage global manager lifecycles
|
|
||||||
* - Provide unified game loop update entry
|
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
* // @zh 初始化并设置场景 | @en Initialize and set scene
|
* // 初始化并设置场景
|
||||||
* Core.create({ debug: true });
|
* Core.create({ debug: true });
|
||||||
* Core.setScene(new GameScene());
|
* Core.setScene(new GameScene());
|
||||||
*
|
*
|
||||||
* // @zh 游戏循环(自动更新全局服务和场景)| @en Game loop (auto-updates global services and scene)
|
* // 游戏循环(自动更新全局服务和场景)
|
||||||
* function gameLoop(deltaTime: number) {
|
* function gameLoop(deltaTime: number) {
|
||||||
* Core.update(deltaTime);
|
* Core.update(deltaTime);
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* // @zh 使用定时器 | @en Use timer
|
* // 使用定时器
|
||||||
* Core.schedule(1.0, false, null, (timer) => {
|
* Core.schedule(1.0, false, null, (timer) => {
|
||||||
* console.log("Executed after 1 second");
|
* console.log("1秒后执行");
|
||||||
* });
|
* });
|
||||||
*
|
*
|
||||||
* // @zh 切换场景 | @en Switch scene
|
* // 切换场景
|
||||||
* Core.loadScene(new MenuScene()); // @zh 延迟切换 | @en Deferred switch
|
* Core.loadScene(new MenuScene()); // 延迟切换
|
||||||
* Core.setScene(new GameScene()); // @zh 立即切换 | @en Immediate switch
|
* Core.setScene(new GameScene()); // 立即切换
|
||||||
*
|
*
|
||||||
* // @zh 获取当前场景 | @en Get current scene
|
* // 获取当前场景
|
||||||
* const currentScene = Core.scene;
|
* const currentScene = Core.scene;
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export class Core {
|
export class Core {
|
||||||
/**
|
/**
|
||||||
* @zh 游戏暂停状态,当设置为true时,游戏循环将暂停执行
|
* 游戏暂停状态
|
||||||
* @en Game paused state, when set to true, game loop will pause execution
|
*
|
||||||
|
* 当设置为true时,游戏循环将暂停执行。
|
||||||
*/
|
*/
|
||||||
public static paused = false;
|
public static paused = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 全局核心实例,可能为null表示Core尚未初始化或已被销毁
|
* 全局核心实例
|
||||||
* @en Global core instance, null means Core is not initialized or destroyed
|
*
|
||||||
|
* 可能为null表示Core尚未初始化或已被销毁
|
||||||
*/
|
*/
|
||||||
private static _instance: Core | null = null;
|
private static _instance: Core | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh Core专用日志器
|
* Core专用日志器
|
||||||
* @en Core logger
|
|
||||||
*/
|
*/
|
||||||
private static readonly _logger = createLogger('Core');
|
private static _logger = createLogger('Core');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 调试模式标志,在调试模式下会启用额外的性能监控和错误检查
|
* 调试模式标志
|
||||||
* @en Debug mode flag, enables additional performance monitoring and error checking in debug mode
|
*
|
||||||
|
* 在调试模式下会启用额外的性能监控和错误检查。
|
||||||
*/
|
*/
|
||||||
public readonly debug: boolean;
|
public readonly debug: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 服务容器,管理所有服务的注册、解析和生命周期
|
* 服务容器
|
||||||
* @en Service container for managing registration, resolution, and lifecycle of all services
|
*
|
||||||
|
* 管理所有服务的注册、解析和生命周期。
|
||||||
*/
|
*/
|
||||||
private _serviceContainer: ServiceContainer;
|
private _serviceContainer: ServiceContainer;
|
||||||
|
|
||||||
@@ -93,79 +90,110 @@ export class Core {
|
|||||||
private _debugManager?: DebugManager;
|
private _debugManager?: DebugManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 场景管理器,管理当前场景的生命周期
|
* 场景管理器
|
||||||
* @en Scene manager for managing current scene lifecycle
|
*
|
||||||
|
* 管理当前场景的生命周期。
|
||||||
*/
|
*/
|
||||||
private _sceneManager: SceneManager;
|
private _sceneManager: SceneManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh World管理器,管理多个独立的World实例(可选)
|
* World管理器
|
||||||
* @en World manager for managing multiple independent World instances (optional)
|
*
|
||||||
|
* 管理多个独立的World实例(可选)。
|
||||||
*/
|
*/
|
||||||
private _worldManager: WorldManager;
|
private _worldManager: WorldManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 插件管理器,管理所有插件的生命周期
|
* 插件管理器
|
||||||
* @en Plugin manager for managing all plugin lifecycles
|
*
|
||||||
|
* 管理所有插件的生命周期。
|
||||||
*/
|
*/
|
||||||
private _pluginManager: PluginManager;
|
private _pluginManager: PluginManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 插件服务注册表,基于 ServiceToken 的类型安全服务注册表
|
* 插件服务注册表
|
||||||
* @en Plugin service registry, type-safe service registry based on ServiceToken
|
*
|
||||||
|
* 基于 ServiceToken 的类型安全服务注册表。
|
||||||
|
* Type-safe service registry based on ServiceToken.
|
||||||
*/
|
*/
|
||||||
private _pluginServiceRegistry: PluginServiceRegistry;
|
private _pluginServiceRegistry: PluginServiceRegistry;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh Core配置
|
* Core配置
|
||||||
* @en Core configuration
|
|
||||||
*/
|
*/
|
||||||
private _config: ICoreConfig;
|
private _config: ICoreConfig;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 创建核心实例
|
* 创建核心实例
|
||||||
* @en Create core instance
|
|
||||||
*
|
*
|
||||||
* @param config - @zh Core配置对象 @en Core configuration object
|
* @param config - Core配置对象
|
||||||
*/
|
*/
|
||||||
private constructor(config: ICoreConfig = {}) {
|
private constructor(config: ICoreConfig = {}) {
|
||||||
Core._instance = this;
|
Core._instance = this;
|
||||||
this._config = { debug: true, ...config };
|
|
||||||
|
// 保存配置
|
||||||
|
this._config = {
|
||||||
|
debug: true,
|
||||||
|
...config
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化服务容器
|
||||||
this._serviceContainer = new ServiceContainer();
|
this._serviceContainer = new ServiceContainer();
|
||||||
|
|
||||||
|
// 初始化定时器管理器
|
||||||
this._timerManager = new TimerManager();
|
this._timerManager = new TimerManager();
|
||||||
this._serviceContainer.registerInstance(TimerManager, this._timerManager);
|
this._serviceContainer.registerInstance(TimerManager, this._timerManager);
|
||||||
|
|
||||||
|
// 初始化性能监控器
|
||||||
this._performanceMonitor = new PerformanceMonitor();
|
this._performanceMonitor = new PerformanceMonitor();
|
||||||
this._serviceContainer.registerInstance(PerformanceMonitor, this._performanceMonitor);
|
this._serviceContainer.registerInstance(PerformanceMonitor, this._performanceMonitor);
|
||||||
|
|
||||||
|
// 在调试模式下启用性能监控
|
||||||
if (this._config.debug) {
|
if (this._config.debug) {
|
||||||
this._performanceMonitor.enable();
|
this._performanceMonitor.enable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 初始化对象池管理器
|
||||||
this._poolManager = new PoolManager();
|
this._poolManager = new PoolManager();
|
||||||
this._serviceContainer.registerInstance(PoolManager, this._poolManager);
|
this._serviceContainer.registerInstance(PoolManager, this._poolManager);
|
||||||
|
|
||||||
|
// 初始化场景管理器
|
||||||
this._sceneManager = new SceneManager(this._performanceMonitor);
|
this._sceneManager = new SceneManager(this._performanceMonitor);
|
||||||
this._serviceContainer.registerInstance(SceneManager, this._sceneManager);
|
this._serviceContainer.registerInstance(SceneManager, this._sceneManager);
|
||||||
this._sceneManager.setSceneChangedCallback(() => this._debugManager?.onSceneChanged());
|
|
||||||
|
|
||||||
|
// 设置场景切换回调,通知调试管理器
|
||||||
|
this._sceneManager.setSceneChangedCallback(() => {
|
||||||
|
if (this._debugManager) {
|
||||||
|
this._debugManager.onSceneChanged();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化World管理器
|
||||||
this._worldManager = new WorldManager({ debug: !!this._config.debug, ...this._config.worldManagerConfig });
|
this._worldManager = new WorldManager({ debug: !!this._config.debug, ...this._config.worldManagerConfig });
|
||||||
this._serviceContainer.registerInstance(WorldManager, this._worldManager);
|
this._serviceContainer.registerInstance(WorldManager, this._worldManager);
|
||||||
|
|
||||||
|
// 初始化插件管理器
|
||||||
this._pluginManager = new PluginManager();
|
this._pluginManager = new PluginManager();
|
||||||
this._pluginManager.initialize(this, this._serviceContainer);
|
this._pluginManager.initialize(this, this._serviceContainer);
|
||||||
this._serviceContainer.registerInstance(PluginManager, this._pluginManager);
|
this._serviceContainer.registerInstance(PluginManager, this._pluginManager);
|
||||||
|
|
||||||
|
// 初始化插件服务注册表
|
||||||
|
// Initialize plugin service registry
|
||||||
this._pluginServiceRegistry = new PluginServiceRegistry();
|
this._pluginServiceRegistry = new PluginServiceRegistry();
|
||||||
this._serviceContainer.registerInstance(PluginServiceRegistry, this._pluginServiceRegistry);
|
this._serviceContainer.registerInstance(PluginServiceRegistry, this._pluginServiceRegistry);
|
||||||
|
|
||||||
this.debug = this._config.debug ?? true;
|
this.debug = this._config.debug ?? true;
|
||||||
|
|
||||||
|
// 初始化调试管理器
|
||||||
if (this._config.debugConfig?.enabled) {
|
if (this._config.debugConfig?.enabled) {
|
||||||
const configService = new DebugConfigService();
|
const configService = new DebugConfigService();
|
||||||
configService.setConfig(this._config.debugConfig);
|
configService.setConfig(this._config.debugConfig);
|
||||||
this._serviceContainer.registerInstance(DebugConfigService, configService);
|
this._serviceContainer.registerInstance(DebugConfigService, configService);
|
||||||
this._serviceContainer.registerSingleton(DebugManager, (c) => createInstance(DebugManager, c));
|
|
||||||
|
this._serviceContainer.registerSingleton(DebugManager, (c) =>
|
||||||
|
createInstance(DebugManager, c)
|
||||||
|
);
|
||||||
|
|
||||||
this._debugManager = this._serviceContainer.resolve(DebugManager);
|
this._debugManager = this._serviceContainer.resolve(DebugManager);
|
||||||
this._debugManager.onInitialize();
|
this._debugManager.onInitialize();
|
||||||
}
|
}
|
||||||
@@ -174,88 +202,82 @@ export class Core {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 获取核心实例
|
* 获取核心实例
|
||||||
* @en Get core instance
|
|
||||||
*
|
*
|
||||||
* @returns @zh 全局核心实例 @en Global core instance
|
* @returns 全局核心实例
|
||||||
*/
|
*/
|
||||||
public static get Instance() {
|
public static get Instance() {
|
||||||
return this._instance;
|
return this._instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 获取服务容器
|
* 获取服务容器
|
||||||
* @en Get service container
|
|
||||||
*
|
*
|
||||||
* @zh 用于注册和解析自定义服务。
|
* 用于注册和解析自定义服务。
|
||||||
* @en Used for registering and resolving custom services.
|
|
||||||
*
|
*
|
||||||
* @returns @zh 服务容器实例 @en Service container instance
|
* @returns 服务容器实例
|
||||||
* @throws @zh 如果Core实例未创建 @en If Core instance is not created
|
* @throws 如果Core实例未创建
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
* // @zh 注册自定义服务 | @en Register custom service
|
* // 注册自定义服务
|
||||||
* Core.services.registerSingleton(MyService);
|
* Core.services.registerSingleton(MyService);
|
||||||
*
|
*
|
||||||
* // @zh 解析服务 | @en Resolve service
|
* // 解析服务
|
||||||
* const myService = Core.services.resolve(MyService);
|
* const myService = Core.services.resolve(MyService);
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
public static get services(): ServiceContainer {
|
public static get services(): ServiceContainer {
|
||||||
if (!this._instance) {
|
if (!this._instance) {
|
||||||
throw new Error('Core instance not created, call Core.create() first | Core实例未创建,请先调用Core.create()');
|
throw new Error('Core实例未创建,请先调用Core.create()');
|
||||||
}
|
}
|
||||||
return this._instance._serviceContainer;
|
return this._instance._serviceContainer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 获取插件服务注册表
|
* 获取插件服务注册表
|
||||||
* @en Get plugin service registry
|
|
||||||
*
|
*
|
||||||
* @zh 用于基于 ServiceToken 的类型安全服务注册和获取。
|
* 用于基于 ServiceToken 的类型安全服务注册和获取。
|
||||||
* @en For type-safe service registration and retrieval based on ServiceToken.
|
* For type-safe service registration and retrieval based on ServiceToken.
|
||||||
*
|
*
|
||||||
* @returns @zh PluginServiceRegistry 实例 @en PluginServiceRegistry instance
|
* @returns PluginServiceRegistry 实例
|
||||||
* @throws @zh 如果 Core 实例未创建 @en If Core instance is not created
|
* @throws 如果 Core 实例未创建
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
* import { createServiceToken } from '@esengine/ecs-framework';
|
* import { createServiceToken } from '@esengine/ecs-framework';
|
||||||
*
|
*
|
||||||
* // @zh 定义服务令牌 | @en Define service token
|
* // 定义服务令牌
|
||||||
* const MyServiceToken = createServiceToken<IMyService>('myService');
|
* const MyServiceToken = createServiceToken<IMyService>('myService');
|
||||||
*
|
*
|
||||||
* // @zh 注册服务 | @en Register service
|
* // 注册服务
|
||||||
* Core.pluginServices.register(MyServiceToken, myServiceInstance);
|
* Core.pluginServices.register(MyServiceToken, myServiceInstance);
|
||||||
*
|
*
|
||||||
* // @zh 获取服务(可选)| @en Get service (optional)
|
* // 获取服务(可选)
|
||||||
* const service = Core.pluginServices.get(MyServiceToken);
|
* const service = Core.pluginServices.get(MyServiceToken);
|
||||||
*
|
*
|
||||||
* // @zh 获取服务(必需,不存在则抛异常)| @en Get service (required, throws if not found)
|
* // 获取服务(必需,不存在则抛异常)
|
||||||
* const service = Core.pluginServices.require(MyServiceToken);
|
* const service = Core.pluginServices.require(MyServiceToken);
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
public static get pluginServices(): PluginServiceRegistry {
|
public static get pluginServices(): PluginServiceRegistry {
|
||||||
if (!this._instance) {
|
if (!this._instance) {
|
||||||
throw new Error('Core instance not created, call Core.create() first | Core实例未创建,请先调用Core.create()');
|
throw new Error('Core实例未创建,请先调用Core.create()');
|
||||||
}
|
}
|
||||||
return this._instance._pluginServiceRegistry;
|
return this._instance._pluginServiceRegistry;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 获取World管理器
|
* 获取World管理器
|
||||||
* @en Get World manager
|
|
||||||
*
|
*
|
||||||
* @zh 用于管理多个独立的World实例(高级用户)。
|
* 用于管理多个独立的World实例(高级用户)。
|
||||||
* @en For managing multiple independent World instances (advanced users).
|
|
||||||
*
|
*
|
||||||
* @returns @zh WorldManager实例 @en WorldManager instance
|
* @returns WorldManager实例
|
||||||
* @throws @zh 如果Core实例未创建 @en If Core instance is not created
|
* @throws 如果Core实例未创建
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
* // @zh 创建多个游戏房间 | @en Create multiple game rooms
|
* // 创建多个游戏房间
|
||||||
* const wm = Core.worldManager;
|
* const wm = Core.worldManager;
|
||||||
* const room1 = wm.createWorld('room_001');
|
* const room1 = wm.createWorld('room_001');
|
||||||
* room1.createScene('game', new GameScene());
|
* room1.createScene('game', new GameScene());
|
||||||
@@ -264,24 +286,22 @@ export class Core {
|
|||||||
*/
|
*/
|
||||||
public static get worldManager(): WorldManager {
|
public static get worldManager(): WorldManager {
|
||||||
if (!this._instance) {
|
if (!this._instance) {
|
||||||
throw new Error('Core instance not created, call Core.create() first | Core实例未创建,请先调用Core.create()');
|
throw new Error('Core实例未创建,请先调用Core.create()');
|
||||||
}
|
}
|
||||||
return this._instance._worldManager;
|
return this._instance._worldManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 创建Core实例
|
* 创建Core实例
|
||||||
* @en Create Core instance
|
|
||||||
*
|
*
|
||||||
* @zh 如果实例已存在,则返回现有实例。
|
* 如果实例已存在,则返回现有实例。
|
||||||
* @en If instance already exists, returns the existing instance.
|
|
||||||
*
|
*
|
||||||
* @param config - @zh Core配置,也可以直接传入boolean表示debug模式(向后兼容) @en Core config, can also pass boolean for debug mode (backward compatible)
|
* @param config - Core配置,也可以直接传入boolean表示debug模式(向后兼容)
|
||||||
* @returns @zh Core实例 @en Core instance
|
* @returns Core实例
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
* // @zh 方式1:使用配置对象 | @en Method 1: Use config object
|
* // 方式1:使用配置对象
|
||||||
* Core.create({
|
* Core.create({
|
||||||
* debug: true,
|
* debug: true,
|
||||||
* debugConfig: {
|
* debugConfig: {
|
||||||
@@ -290,7 +310,7 @@ export class Core {
|
|||||||
* }
|
* }
|
||||||
* });
|
* });
|
||||||
*
|
*
|
||||||
* // @zh 方式2:简单模式(向后兼容)| @en Method 2: Simple mode (backward compatible)
|
* // 方式2:简单模式(向后兼容)
|
||||||
* Core.create(true); // debug = true
|
* Core.create(true); // debug = true
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
@@ -308,17 +328,16 @@ export class Core {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 设置当前场景
|
* 设置当前场景
|
||||||
* @en Set current scene
|
|
||||||
*
|
*
|
||||||
* @param scene - @zh 要设置的场景 @en The scene to set
|
* @param scene - 要设置的场景
|
||||||
* @returns @zh 设置的场景实例 @en The scene instance that was set
|
* @returns 设置的场景实例
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
* Core.create({ debug: true });
|
* Core.create({ debug: true });
|
||||||
*
|
*
|
||||||
* // @zh 创建并设置场景 | @en Create and set scene
|
* // 创建并设置场景
|
||||||
* const gameScene = new GameScene();
|
* const gameScene = new GameScene();
|
||||||
* Core.setScene(gameScene);
|
* Core.setScene(gameScene);
|
||||||
* ```
|
* ```
|
||||||
@@ -333,10 +352,9 @@ export class Core {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 获取当前场景
|
* 获取当前场景
|
||||||
* @en Get current scene
|
|
||||||
*
|
*
|
||||||
* @returns @zh 当前场景,如果没有场景则返回null @en Current scene, or null if no scene
|
* @returns 当前场景,如果没有场景则返回null
|
||||||
*/
|
*/
|
||||||
public static get scene(): IScene | null {
|
public static get scene(): IScene | null {
|
||||||
if (!this._instance) {
|
if (!this._instance) {
|
||||||
@@ -346,22 +364,21 @@ export class Core {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 获取ECS流式API
|
* 获取ECS流式API
|
||||||
* @en Get ECS fluent API
|
|
||||||
*
|
*
|
||||||
* @returns @zh ECS API实例,如果当前没有场景则返回null @en ECS API instance, or null if no scene
|
* @returns ECS API实例,如果当前没有场景则返回null
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
* // @zh 使用流式API创建实体 | @en Create entity with fluent API
|
* // 使用流式API创建实体
|
||||||
* const player = Core.ecsAPI?.createEntity('Player')
|
* const player = Core.ecsAPI?.createEntity('Player')
|
||||||
* .addComponent(Position, 100, 100)
|
* .addComponent(Position, 100, 100)
|
||||||
* .addComponent(Velocity, 50, 0);
|
* .addComponent(Velocity, 50, 0);
|
||||||
*
|
*
|
||||||
* // @zh 查询实体 | @en Query entities
|
* // 查询实体
|
||||||
* const enemies = Core.ecsAPI?.query(Enemy, Transform);
|
* const enemies = Core.ecsAPI?.query(Enemy, Transform);
|
||||||
*
|
*
|
||||||
* // @zh 发射事件 | @en Emit event
|
* // 发射事件
|
||||||
* Core.ecsAPI?.emit('game:start', { level: 1 });
|
* Core.ecsAPI?.emit('game:start', { level: 1 });
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
@@ -373,14 +390,13 @@ export class Core {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 延迟加载场景(下一帧切换)
|
* 延迟加载场景(下一帧切换)
|
||||||
* @en Load scene with delay (switch on next frame)
|
|
||||||
*
|
*
|
||||||
* @param scene - @zh 要加载的场景 @en The scene to load
|
* @param scene - 要加载的场景
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
* // @zh 延迟切换场景(在下一帧生效)| @en Deferred scene switch (takes effect next frame)
|
* // 延迟切换场景(在下一帧生效)
|
||||||
* Core.loadScene(new MenuScene());
|
* Core.loadScene(new MenuScene());
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
@@ -394,29 +410,28 @@ export class Core {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 更新游戏逻辑
|
* 更新游戏逻辑
|
||||||
* @en Update game logic
|
|
||||||
*
|
*
|
||||||
* @zh 此方法应该在游戏引擎的更新循环中调用。会自动更新全局服务和当前场景。
|
* 此方法应该在游戏引擎的更新循环中调用。
|
||||||
* @en This method should be called in the game engine's update loop. Automatically updates global services and current scene.
|
* 会自动更新全局服务和当前场景。
|
||||||
*
|
*
|
||||||
* @param deltaTime - @zh 外部引擎提供的帧时间间隔(秒)@en Frame delta time in seconds from external engine
|
* @param deltaTime - 外部引擎提供的帧时间间隔(秒)
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
* // @zh 初始化 | @en Initialize
|
* // 初始化
|
||||||
* Core.create({ debug: true });
|
* Core.create({ debug: true });
|
||||||
* Core.setScene(new GameScene());
|
* Core.setScene(new GameScene());
|
||||||
*
|
*
|
||||||
* // @zh Laya引擎集成 | @en Laya engine integration
|
* // Laya引擎集成
|
||||||
* Laya.timer.frameLoop(1, this, () => {
|
* Laya.timer.frameLoop(1, this, () => {
|
||||||
* const deltaTime = Laya.timer.delta / 1000;
|
* const deltaTime = Laya.timer.delta / 1000;
|
||||||
* Core.update(deltaTime);
|
* Core.update(deltaTime); // 自动更新全局服务和场景
|
||||||
* });
|
* });
|
||||||
*
|
*
|
||||||
* // @zh Cocos Creator集成 | @en Cocos Creator integration
|
* // Cocos Creator集成
|
||||||
* update(deltaTime: number) {
|
* update(deltaTime: number) {
|
||||||
* Core.update(deltaTime);
|
* Core.update(deltaTime); // 自动更新全局服务和场景
|
||||||
* }
|
* }
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
@@ -431,29 +446,27 @@ export class Core {
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 调度定时器
|
* 调度定时器
|
||||||
* @en Schedule a timer
|
|
||||||
*
|
*
|
||||||
* @zh 创建一个定时器,在指定时间后执行回调函数。
|
* 创建一个定时器,在指定时间后执行回调函数。
|
||||||
* @en Create a timer that executes a callback after the specified time.
|
|
||||||
*
|
*
|
||||||
* @param timeInSeconds - @zh 延迟时间(秒)@en Delay time in seconds
|
* @param timeInSeconds - 延迟时间(秒)
|
||||||
* @param repeats - @zh 是否重复执行,默认为false @en Whether to repeat, defaults to false
|
* @param repeats - 是否重复执行,默认为false
|
||||||
* @param context - @zh 回调函数的上下文 @en Context for the callback
|
* @param context - 回调函数的上下文,默认为null
|
||||||
* @param onTime - @zh 定时器触发时的回调函数 @en Callback when timer fires
|
* @param onTime - 定时器触发时的回调函数
|
||||||
* @returns @zh 创建的定时器实例 @en The created timer instance
|
* @returns 创建的定时器实例
|
||||||
* @throws @zh 如果Core实例未创建或onTime回调未提供 @en If Core instance not created or onTime not provided
|
* @throws 如果Core实例未创建或onTime回调未提供
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
* // @zh 一次性定时器 | @en One-time timer
|
* // 一次性定时器
|
||||||
* Core.schedule(1.0, false, null, (timer) => {
|
* Core.schedule(1.0, false, null, (timer) => {
|
||||||
* console.log("Executed after 1 second");
|
* console.log("1秒后执行一次");
|
||||||
* });
|
* });
|
||||||
*
|
*
|
||||||
* // @zh 重复定时器 | @en Repeating timer
|
* // 重复定时器
|
||||||
* Core.schedule(0.5, true, null, (timer) => {
|
* Core.schedule(0.5, true, null, (timer) => {
|
||||||
* console.log("Executed every 0.5 seconds");
|
* console.log("每0.5秒执行一次");
|
||||||
* });
|
* });
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
@@ -468,10 +481,9 @@ export class Core {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 启用调试功能
|
* 启用调试功能
|
||||||
* @en Enable debug features
|
|
||||||
*
|
*
|
||||||
* @param config - @zh 调试配置 @en Debug configuration
|
* @param config 调试配置
|
||||||
*/
|
*/
|
||||||
public static enableDebug(config: IECSDebugConfig): void {
|
public static enableDebug(config: IECSDebugConfig): void {
|
||||||
if (!this._instance) {
|
if (!this._instance) {
|
||||||
@@ -499,8 +511,7 @@ export class Core {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 禁用调试功能
|
* 禁用调试功能
|
||||||
* @en Disable debug features
|
|
||||||
*/
|
*/
|
||||||
public static disableDebug(): void {
|
public static disableDebug(): void {
|
||||||
if (!this._instance) return;
|
if (!this._instance) return;
|
||||||
@@ -517,10 +528,9 @@ export class Core {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 获取调试数据
|
* 获取调试数据
|
||||||
* @en Get debug data
|
|
||||||
*
|
*
|
||||||
* @returns @zh 当前调试数据,如果调试未启用则返回null @en Current debug data, or null if debug is disabled
|
* @returns 当前调试数据,如果调试未启用则返回null
|
||||||
*/
|
*/
|
||||||
public static getDebugData(): unknown {
|
public static getDebugData(): unknown {
|
||||||
if (!this._instance?._debugManager) {
|
if (!this._instance?._debugManager) {
|
||||||
@@ -531,37 +541,34 @@ export class Core {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 检查调试是否启用
|
* 检查调试是否启用
|
||||||
* @en Check if debug is enabled
|
|
||||||
*
|
*
|
||||||
* @returns @zh 调试状态 @en Debug status
|
* @returns 调试状态
|
||||||
*/
|
*/
|
||||||
public static get isDebugEnabled(): boolean {
|
public static get isDebugEnabled(): boolean {
|
||||||
return this._instance?._config.debugConfig?.enabled || false;
|
return this._instance?._config.debugConfig?.enabled || false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 获取性能监视器实例
|
* 获取性能监视器实例
|
||||||
* @en Get performance monitor instance
|
|
||||||
*
|
*
|
||||||
* @returns @zh 性能监视器,如果Core未初始化则返回null @en Performance monitor, or null if Core not initialized
|
* @returns 性能监视器,如果Core未初始化则返回null
|
||||||
*/
|
*/
|
||||||
public static get performanceMonitor(): PerformanceMonitor | null {
|
public static get performanceMonitor(): PerformanceMonitor | null {
|
||||||
return this._instance?._performanceMonitor || null;
|
return this._instance?._performanceMonitor || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 安装插件
|
* 安装插件
|
||||||
* @en Install plugin
|
|
||||||
*
|
*
|
||||||
* @param plugin - @zh 插件实例 @en Plugin instance
|
* @param plugin - 插件实例
|
||||||
* @throws @zh 如果Core实例未创建或插件安装失败 @en If Core instance not created or plugin installation fails
|
* @throws 如果Core实例未创建或插件安装失败
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
* Core.create({ debug: true });
|
* Core.create({ debug: true });
|
||||||
*
|
*
|
||||||
* // @zh 安装插件 | @en Install plugin
|
* // 安装插件
|
||||||
* await Core.installPlugin(new MyPlugin());
|
* await Core.installPlugin(new MyPlugin());
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
@@ -574,11 +581,10 @@ export class Core {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 卸载插件
|
* 卸载插件
|
||||||
* @en Uninstall plugin
|
|
||||||
*
|
*
|
||||||
* @param name - @zh 插件名称 @en Plugin name
|
* @param name - 插件名称
|
||||||
* @throws @zh 如果Core实例未创建或插件卸载失败 @en If Core instance not created or plugin uninstallation fails
|
* @throws 如果Core实例未创建或插件卸载失败
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
@@ -594,11 +600,10 @@ export class Core {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 获取插件实例
|
* 获取插件实例
|
||||||
* @en Get plugin instance
|
|
||||||
*
|
*
|
||||||
* @param name - @zh 插件名称 @en Plugin name
|
* @param name - 插件名称
|
||||||
* @returns @zh 插件实例,如果未安装则返回undefined @en Plugin instance, or undefined if not installed
|
* @returns 插件实例,如果未安装则返回undefined
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
@@ -617,11 +622,10 @@ export class Core {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 检查插件是否已安装
|
* 检查插件是否已安装
|
||||||
* @en Check if plugin is installed
|
|
||||||
*
|
*
|
||||||
* @param name - @zh 插件名称 @en Plugin name
|
* @param name - 插件名称
|
||||||
* @returns @zh 是否已安装 @en Whether installed
|
* @returns 是否已安装
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
@@ -639,11 +643,9 @@ export class Core {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 初始化核心系统
|
* 初始化核心系统
|
||||||
* @en Initialize core system
|
|
||||||
*
|
*
|
||||||
* @zh 执行核心系统的初始化逻辑。
|
* 执行核心系统的初始化逻辑。
|
||||||
* @en Execute core system initialization logic.
|
|
||||||
*/
|
*/
|
||||||
protected initialize() {
|
protected initialize() {
|
||||||
// 核心系统初始化
|
// 核心系统初始化
|
||||||
@@ -654,43 +656,61 @@ export class Core {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 内部更新方法
|
* 内部更新方法
|
||||||
* @en Internal update method
|
|
||||||
*
|
*
|
||||||
* @param deltaTime - @zh 帧时间间隔(秒)@en Frame delta time in seconds
|
* @param deltaTime - 帧时间间隔(秒)
|
||||||
*/
|
*/
|
||||||
private updateInternal(deltaTime: number): void {
|
private updateInternal(deltaTime: number): void {
|
||||||
if (Core.paused) return;
|
if (Core.paused) return;
|
||||||
|
|
||||||
|
// 开始性能监控
|
||||||
const frameStartTime = this._performanceMonitor.startMonitoring('Core.update');
|
const frameStartTime = this._performanceMonitor.startMonitoring('Core.update');
|
||||||
|
|
||||||
|
// 更新时间系统
|
||||||
Time.update(deltaTime);
|
Time.update(deltaTime);
|
||||||
this._performanceMonitor.updateFPS?.(Time.deltaTime);
|
|
||||||
|
|
||||||
|
// 更新FPS监控(如果性能监控器支持)
|
||||||
|
if ('updateFPS' in this._performanceMonitor && typeof this._performanceMonitor.updateFPS === 'function') {
|
||||||
|
this._performanceMonitor.updateFPS(Time.deltaTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新所有可更新的服务
|
||||||
const servicesStartTime = this._performanceMonitor.startMonitoring('Services.update');
|
const servicesStartTime = this._performanceMonitor.startMonitoring('Services.update');
|
||||||
this._serviceContainer.updateAll(deltaTime);
|
this._serviceContainer.updateAll(deltaTime);
|
||||||
this._performanceMonitor.endMonitoring('Services.update', servicesStartTime, this._serviceContainer.getUpdatableCount());
|
this._performanceMonitor.endMonitoring('Services.update', servicesStartTime, this._serviceContainer.getUpdatableCount());
|
||||||
|
|
||||||
|
// 更新对象池管理器
|
||||||
this._poolManager.update();
|
this._poolManager.update();
|
||||||
|
|
||||||
|
// 更新默认场景(通过 SceneManager)
|
||||||
this._sceneManager.update();
|
this._sceneManager.update();
|
||||||
|
|
||||||
|
// 更新额外的 WorldManager
|
||||||
this._worldManager.updateAll();
|
this._worldManager.updateAll();
|
||||||
|
|
||||||
|
// 结束性能监控
|
||||||
this._performanceMonitor.endMonitoring('Core.update', frameStartTime);
|
this._performanceMonitor.endMonitoring('Core.update', frameStartTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 销毁Core实例
|
* 销毁Core实例
|
||||||
* @en Destroy Core instance
|
|
||||||
*
|
*
|
||||||
* @zh 清理所有资源,通常在应用程序关闭时调用。
|
* 清理所有资源,通常在应用程序关闭时调用。
|
||||||
* @en Clean up all resources, typically called when the application closes.
|
|
||||||
*/
|
*/
|
||||||
public static destroy(): void {
|
public static destroy(): void {
|
||||||
if (!this._instance) return;
|
if (!this._instance) return;
|
||||||
|
|
||||||
this._instance._debugManager?.stop();
|
// 停止调试管理器
|
||||||
|
if (this._instance._debugManager) {
|
||||||
|
this._instance._debugManager.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理所有服务
|
||||||
this._instance._serviceContainer.clear();
|
this._instance._serviceContainer.clear();
|
||||||
|
|
||||||
Core._logger.info('Core destroyed');
|
Core._logger.info('Core destroyed');
|
||||||
|
|
||||||
|
// 清空实例引用,允许重新创建Core实例
|
||||||
this._instance = null;
|
this._instance = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const updatableMetadata = new WeakMap<Constructor, UpdatableMetadata>();
|
|||||||
/**
|
/**
|
||||||
* 可注入元数据接口
|
* 可注入元数据接口
|
||||||
*/
|
*/
|
||||||
export type InjectableMetadata = {
|
export interface InjectableMetadata {
|
||||||
/**
|
/**
|
||||||
* 是否可注入
|
* 是否可注入
|
||||||
*/
|
*/
|
||||||
@@ -39,7 +39,7 @@ export type InjectableMetadata = {
|
|||||||
/**
|
/**
|
||||||
* 可更新元数据接口
|
* 可更新元数据接口
|
||||||
*/
|
*/
|
||||||
export type UpdatableMetadata = {
|
export interface UpdatableMetadata {
|
||||||
/**
|
/**
|
||||||
* 是否可更新
|
* 是否可更新
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export enum PluginState {
|
|||||||
* }
|
* }
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export type IPlugin = {
|
export interface IPlugin {
|
||||||
/**
|
/**
|
||||||
* 插件唯一名称
|
* 插件唯一名称
|
||||||
*
|
*
|
||||||
@@ -96,7 +96,7 @@ export type IPlugin = {
|
|||||||
/**
|
/**
|
||||||
* 插件元数据
|
* 插件元数据
|
||||||
*/
|
*/
|
||||||
export type IPluginMetadata = {
|
export interface IPluginMetadata {
|
||||||
/**
|
/**
|
||||||
* 插件名称
|
* 插件名称
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
* Note: __phantom is a required property to ensure TypeScript preserves generic
|
* Note: __phantom is a required property to ensure TypeScript preserves generic
|
||||||
* type information across packages.
|
* type information across packages.
|
||||||
*/
|
*/
|
||||||
export type ServiceToken<T> = {
|
export interface ServiceToken<T> {
|
||||||
readonly id: symbol;
|
readonly id: symbol;
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -30,7 +30,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { createServiceToken } from './PluginServiceRegistry';
|
import { createServiceToken } from './PluginServiceRegistry';
|
||||||
import { createLogger } from '../Utils/Logger';
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 接口定义 | Interface Definitions
|
// 接口定义 | Interface Definitions
|
||||||
@@ -40,7 +39,7 @@ import { createLogger } from '../Utils/Logger';
|
|||||||
* 运行时模式接口
|
* 运行时模式接口
|
||||||
* Runtime mode interface
|
* Runtime mode interface
|
||||||
*/
|
*/
|
||||||
export type IRuntimeMode = {
|
export interface IRuntimeMode {
|
||||||
/**
|
/**
|
||||||
* 是否为编辑器模式
|
* 是否为编辑器模式
|
||||||
* Whether in editor mode
|
* Whether in editor mode
|
||||||
@@ -111,7 +110,7 @@ type ModeChangeCallback = (mode: IRuntimeMode) => void;
|
|||||||
* 运行时模式服务配置
|
* 运行时模式服务配置
|
||||||
* Runtime mode service configuration
|
* Runtime mode service configuration
|
||||||
*/
|
*/
|
||||||
export type RuntimeModeConfig = {
|
export interface RuntimeModeConfig {
|
||||||
/** 是否为编辑器模式 | Whether in editor mode */
|
/** 是否为编辑器模式 | Whether in editor mode */
|
||||||
isEditor?: boolean;
|
isEditor?: boolean;
|
||||||
/** 是否正在播放 | Whether playing */
|
/** 是否正在播放 | Whether playing */
|
||||||
@@ -237,18 +236,16 @@ export class RuntimeModeService implements IRuntimeMode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly _logger = createLogger('RuntimeModeService');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 通知模式变化
|
* 通知模式变化
|
||||||
* @en Notify mode change
|
* Notify mode change
|
||||||
*/
|
*/
|
||||||
private _notifyChange(): void {
|
private _notifyChange(): void {
|
||||||
for (const callback of this._callbacks) {
|
for (const callback of this._callbacks) {
|
||||||
try {
|
try {
|
||||||
callback(this);
|
callback(this);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
RuntimeModeService._logger.error('Callback error:', error);
|
console.error('[RuntimeModeService] Callback error:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const logger = createLogger('ServiceContainer');
|
|||||||
* 服务基础接口
|
* 服务基础接口
|
||||||
* 所有通过 ServiceContainer 管理的服务都应该实现此接口
|
* 所有通过 ServiceContainer 管理的服务都应该实现此接口
|
||||||
*/
|
*/
|
||||||
export type IService = {
|
export interface IService {
|
||||||
/**
|
/**
|
||||||
* 释放服务占用的资源
|
* 释放服务占用的资源
|
||||||
* 当服务被注销或容器被清空时调用
|
* 当服务被注销或容器被清空时调用
|
||||||
|
|||||||
@@ -2,17 +2,13 @@ import type { IComponent } from '../Types';
|
|||||||
import { Int32 } from './Core/SoAStorage';
|
import { Int32 } from './Core/SoAStorage';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 游戏组件基类
|
* 游戏组件基类
|
||||||
* @en Base class for game components
|
|
||||||
*
|
*
|
||||||
* @zh ECS架构中的组件(Component)应该是纯数据容器。
|
* ECS架构中的组件(Component)应该是纯数据容器。
|
||||||
* 所有游戏逻辑应该在 EntitySystem 中实现,而不是在组件内部。
|
* 所有游戏逻辑应该在 EntitySystem 中实现,而不是在组件内部。
|
||||||
* @en Components in ECS architecture should be pure data containers.
|
|
||||||
* All game logic should be implemented in EntitySystem, not inside components.
|
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* @zh 推荐做法:纯数据组件
|
* 推荐做法:纯数据组件
|
||||||
* @en Recommended: Pure data component
|
|
||||||
* ```typescript
|
* ```typescript
|
||||||
* class HealthComponent extends Component {
|
* class HealthComponent extends Component {
|
||||||
* public health: number = 100;
|
* public health: number = 100;
|
||||||
@@ -21,8 +17,7 @@ import { Int32 } from './Core/SoAStorage';
|
|||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* @zh 推荐做法:在 System 中处理逻辑
|
* 推荐做法:在 System 中处理逻辑
|
||||||
* @en Recommended: Handle logic in System
|
|
||||||
* ```typescript
|
* ```typescript
|
||||||
* class HealthSystem extends EntitySystem {
|
* class HealthSystem extends EntitySystem {
|
||||||
* process(entities: Entity[]): void {
|
* process(entities: Entity[]): void {
|
||||||
@@ -38,66 +33,75 @@ import { Int32 } from './Core/SoAStorage';
|
|||||||
*/
|
*/
|
||||||
export abstract class Component implements IComponent {
|
export abstract class Component implements IComponent {
|
||||||
/**
|
/**
|
||||||
* @zh 组件ID生成器,用于为每个组件分配唯一的ID
|
* 组件ID生成器
|
||||||
* @en Component ID generator, used to assign unique IDs to each component
|
*
|
||||||
|
* 用于为每个组件分配唯一的ID。
|
||||||
|
*
|
||||||
|
* Component ID generator.
|
||||||
|
* Used to assign unique IDs to each component.
|
||||||
*/
|
*/
|
||||||
private static _idGenerator: number = 0;
|
private static idGenerator: number = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 组件唯一标识符,在整个游戏生命周期中唯一
|
* 组件唯一标识符
|
||||||
* @en Unique identifier for the component, unique throughout the game lifecycle
|
*
|
||||||
|
* 在整个游戏生命周期中唯一的数字ID。
|
||||||
*/
|
*/
|
||||||
public readonly id: number;
|
public readonly id: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 所属实体ID
|
* 所属实体ID
|
||||||
* @en Owner entity ID
|
|
||||||
*
|
*
|
||||||
* @zh 存储实体ID而非引用,避免循环引用,符合ECS数据导向设计
|
* 存储实体ID而非引用,避免循环引用,符合ECS数据导向设计。
|
||||||
* @en Stores entity ID instead of reference to avoid circular references, following ECS data-oriented design
|
|
||||||
*/
|
*/
|
||||||
@Int32
|
@Int32
|
||||||
public entityId: number | null = null;
|
public entityId: number | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 最后写入的 epoch,用于帧级变更检测
|
* 最后写入的 epoch
|
||||||
* @en Last write epoch, used for frame-level change detection
|
|
||||||
*
|
*
|
||||||
* @zh 记录组件最后一次被修改时的 epoch,0 表示从未被标记为已修改
|
* 用于帧级变更检测,记录组件最后一次被修改时的 epoch。
|
||||||
* @en Records the epoch when component was last modified, 0 means never marked as modified
|
* 0 表示从未被标记为已修改。
|
||||||
|
*
|
||||||
|
* Last write epoch.
|
||||||
|
* Used for frame-level change detection, records the epoch when component was last modified.
|
||||||
|
* 0 means never marked as modified.
|
||||||
*/
|
*/
|
||||||
private _lastWriteEpoch: number = 0;
|
private _lastWriteEpoch: number = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 获取最后写入的 epoch
|
* 获取最后写入的 epoch
|
||||||
* @en Get last write epoch
|
*
|
||||||
|
* Get last write epoch.
|
||||||
*/
|
*/
|
||||||
public get lastWriteEpoch(): number {
|
public get lastWriteEpoch(): number {
|
||||||
return this._lastWriteEpoch;
|
return this._lastWriteEpoch;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 创建组件实例,自动分配唯一ID
|
* 创建组件实例
|
||||||
* @en Create component instance, automatically assigns unique ID
|
*
|
||||||
|
* 自动分配唯一ID给组件。
|
||||||
*/
|
*/
|
||||||
constructor() {
|
constructor() {
|
||||||
this.id = Component._idGenerator++;
|
this.id = Component.idGenerator++;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 标记组件为已修改
|
* 标记组件为已修改
|
||||||
* @en Mark component as modified
|
|
||||||
*
|
*
|
||||||
* @zh 调用此方法会更新组件的 lastWriteEpoch 为当前帧的 epoch。
|
* 调用此方法会更新组件的 lastWriteEpoch 为当前帧的 epoch。
|
||||||
* 系统可以通过比较 lastWriteEpoch 和上次检查的 epoch 来判断组件是否发生变更。
|
* 系统可以通过比较 lastWriteEpoch 和上次检查的 epoch 来判断组件是否发生变更。
|
||||||
* @en Calling this method updates the component's lastWriteEpoch to the current frame's epoch.
|
*
|
||||||
|
* Mark component as modified.
|
||||||
|
* Calling this method updates the component's lastWriteEpoch to the current frame's epoch.
|
||||||
* Systems can compare lastWriteEpoch with their last checked epoch to detect changes.
|
* Systems can compare lastWriteEpoch with their last checked epoch to detect changes.
|
||||||
*
|
*
|
||||||
* @param epoch - @zh 当前帧的 epoch @en Current frame's epoch
|
* @param epoch 当前帧的 epoch | Current frame's epoch
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
* // @zh 在修改组件数据后调用 | @en Call after modifying component data
|
* // 在修改组件数据后调用
|
||||||
* velocity.x = 10;
|
* velocity.x = 10;
|
||||||
* velocity.markDirty(scene.epochManager.current);
|
* velocity.markDirty(scene.epochManager.current);
|
||||||
* ```
|
* ```
|
||||||
@@ -107,41 +111,35 @@ export abstract class Component implements IComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 组件添加到实体时的回调
|
* 组件添加到实体时的回调
|
||||||
* @en Callback when component is added to an entity
|
|
||||||
*
|
*
|
||||||
* @zh 当组件被添加到实体时调用,可以在此方法中进行初始化操作。
|
* 当组件被添加到实体时调用,可以在此方法中进行初始化操作。
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
* 这是一个生命周期钩子,用于组件的初始化逻辑。
|
* 这是一个生命周期钩子,用于组件的初始化逻辑。
|
||||||
* 虽然保留此方法,但建议将复杂的初始化逻辑放在 System 中处理。
|
* 虽然保留此方法,但建议将复杂的初始化逻辑放在 System 中处理。
|
||||||
* @en Called when component is added to an entity, can perform initialization here.
|
|
||||||
* This is a lifecycle hook for component initialization logic.
|
|
||||||
* While this method is available, complex initialization logic should be handled in System.
|
|
||||||
*/
|
*/
|
||||||
public onAddedToEntity(): void {}
|
public onAddedToEntity(): void {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 组件从实体移除时的回调
|
* 组件从实体移除时的回调
|
||||||
* @en Callback when component is removed from an entity
|
|
||||||
*
|
*
|
||||||
* @zh 当组件从实体中移除时调用,可以在此方法中进行清理操作。
|
* 当组件从实体中移除时调用,可以在此方法中进行清理操作。
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
* 这是一个生命周期钩子,用于组件的清理逻辑。
|
* 这是一个生命周期钩子,用于组件的清理逻辑。
|
||||||
* 虽然保留此方法,但建议将复杂的清理逻辑放在 System 中处理。
|
* 虽然保留此方法,但建议将复杂的清理逻辑放在 System 中处理。
|
||||||
* @en Called when component is removed from an entity, can perform cleanup here.
|
|
||||||
* This is a lifecycle hook for component cleanup logic.
|
|
||||||
* While this method is available, complex cleanup logic should be handled in System.
|
|
||||||
*/
|
*/
|
||||||
public onRemovedFromEntity(): void {}
|
public onRemovedFromEntity(): void {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 组件反序列化后的回调
|
* 组件反序列化后的回调
|
||||||
* @en Callback after component deserialization
|
|
||||||
*
|
*
|
||||||
* @zh 当组件从场景文件加载或快照恢复后调用,可以在此方法中恢复运行时数据。
|
* 当组件从场景文件加载或快照恢复后调用,可以在此方法中恢复运行时数据。
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
* 这是一个生命周期钩子,用于恢复无法序列化的运行时数据。
|
* 这是一个生命周期钩子,用于恢复无法序列化的运行时数据。
|
||||||
* 例如:从图片路径重新加载图片尺寸信息,重建缓存等。
|
* 例如:从图片路径重新加载图片尺寸信息,重建缓存等。
|
||||||
* @en Called after component is loaded from scene file or restored from snapshot.
|
|
||||||
* This is a lifecycle hook for restoring runtime data that cannot be serialized.
|
|
||||||
* For example: reloading image dimensions from image path, rebuilding caches, etc.
|
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
@@ -151,8 +149,7 @@ export abstract class Component implements IComponent {
|
|||||||
*
|
*
|
||||||
* public async onDeserialized(): Promise<void> {
|
* public async onDeserialized(): Promise<void> {
|
||||||
* if (this.tilesetImage) {
|
* if (this.tilesetImage) {
|
||||||
* // @zh 重新加载 tileset 图片并恢复运行时数据
|
* // 重新加载 tileset 图片并恢复运行时数据
|
||||||
* // @en Reload tileset image and restore runtime data
|
|
||||||
* const img = await loadImage(this.tilesetImage);
|
* const img = await loadImage(this.tilesetImage);
|
||||||
* this.setTilesetInfo(img.width, img.height, ...);
|
* this.setTilesetInfo(img.width, img.height, ...);
|
||||||
* }
|
* }
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export type ArchetypeId = BitMask64Data;
|
|||||||
/**
|
/**
|
||||||
* 原型数据结构
|
* 原型数据结构
|
||||||
*/
|
*/
|
||||||
export type Archetype = {
|
export interface Archetype {
|
||||||
/** 原型唯一标识符 */
|
/** 原型唯一标识符 */
|
||||||
id: ArchetypeId;
|
id: ArchetypeId;
|
||||||
/** 包含的组件类型 */
|
/** 包含的组件类型 */
|
||||||
@@ -23,7 +23,7 @@ export type Archetype = {
|
|||||||
/**
|
/**
|
||||||
* 原型查询结果
|
* 原型查询结果
|
||||||
*/
|
*/
|
||||||
export type ArchetypeQueryResult = {
|
export interface ArchetypeQueryResult {
|
||||||
/** 匹配的原型列表 */
|
/** 匹配的原型列表 */
|
||||||
archetypes: Archetype[];
|
archetypes: Archetype[];
|
||||||
/** 所有匹配实体的总数 */
|
/** 所有匹配实体的总数 */
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Entity } from '../Entity';
|
import { Entity } from '../Entity';
|
||||||
import { Component } from '../Component';
|
import { Component } from '../Component';
|
||||||
import { ComponentType, GlobalComponentRegistry } from './ComponentStorage';
|
import { ComponentType, GlobalComponentRegistry } from './ComponentStorage';
|
||||||
import { getComponentTypeName } from '../Decorators';
|
|
||||||
import { IScene } from '../IScene';
|
import { IScene } from '../IScene';
|
||||||
import { createLogger } from '../../Utils/Logger';
|
import { createLogger } from '../../Utils/Logger';
|
||||||
|
|
||||||
@@ -26,7 +25,7 @@ export enum CommandType {
|
|||||||
* 延迟命令接口
|
* 延迟命令接口
|
||||||
* Deferred command interface
|
* Deferred command interface
|
||||||
*/
|
*/
|
||||||
export type DeferredCommand = {
|
export interface DeferredCommand {
|
||||||
/** 命令类型 | Command type */
|
/** 命令类型 | Command type */
|
||||||
type: CommandType;
|
type: CommandType;
|
||||||
/** 目标实体 | Target entity */
|
/** 目标实体 | Target entity */
|
||||||
@@ -240,8 +239,7 @@ export class CommandBuffer {
|
|||||||
pending.adds.set(typeId, component);
|
pending.adds.set(typeId, component);
|
||||||
|
|
||||||
if (this._debug) {
|
if (this._debug) {
|
||||||
const typeName = getComponentTypeName(component.constructor as ComponentType);
|
logger.debug(`CommandBuffer: 延迟添加组件 ${component.constructor.name} 到实体 ${entity.name}`);
|
||||||
logger.debug(`CommandBuffer: 延迟添加组件 ${typeName} 到实体 ${entity.name}`);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 旧模式
|
// 旧模式
|
||||||
@@ -437,10 +435,9 @@ export class CommandBuffer {
|
|||||||
entity.addComponent(component);
|
entity.addComponent(component);
|
||||||
commandCount++;
|
commandCount++;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const typeName = getComponentTypeName(component.constructor as ComponentType);
|
|
||||||
logger.error(`CommandBuffer: 添加组件失败`, {
|
logger.error(`CommandBuffer: 添加组件失败`, {
|
||||||
entity: entity.name,
|
entity: entity.name,
|
||||||
component: typeName,
|
component: component.constructor.name,
|
||||||
error
|
error
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,25 +142,24 @@ interface ComponentUsageTracker {
|
|||||||
* 全局组件池管理器
|
* 全局组件池管理器
|
||||||
*/
|
*/
|
||||||
export class ComponentPoolManager {
|
export class ComponentPoolManager {
|
||||||
private static _instance: ComponentPoolManager;
|
private static instance: ComponentPoolManager;
|
||||||
private _pools = new Map<string, ComponentPool<Component>>();
|
private pools = new Map<string, ComponentPool<Component>>();
|
||||||
private _usageTracker = new Map<string, ComponentUsageTracker>();
|
private usageTracker = new Map<string, ComponentUsageTracker>();
|
||||||
|
|
||||||
private _autoCleanupInterval = 60000;
|
private autoCleanupInterval = 60000;
|
||||||
private _lastCleanupTime = 0;
|
private lastCleanupTime = 0;
|
||||||
|
|
||||||
private constructor() {}
|
private constructor() {}
|
||||||
|
|
||||||
static getInstance(): ComponentPoolManager {
|
static getInstance(): ComponentPoolManager {
|
||||||
if (!ComponentPoolManager._instance) {
|
if (!ComponentPoolManager.instance) {
|
||||||
ComponentPoolManager._instance = new ComponentPoolManager();
|
ComponentPoolManager.instance = new ComponentPoolManager();
|
||||||
}
|
}
|
||||||
return ComponentPoolManager._instance;
|
return ComponentPoolManager.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 注册组件池
|
* 注册组件池
|
||||||
* @en Register component pool
|
|
||||||
*/
|
*/
|
||||||
registerPool<T extends Component>(
|
registerPool<T extends Component>(
|
||||||
componentName: string,
|
componentName: string,
|
||||||
@@ -169,9 +168,9 @@ export class ComponentPoolManager {
|
|||||||
maxSize?: number,
|
maxSize?: number,
|
||||||
minSize?: number
|
minSize?: number
|
||||||
): void {
|
): void {
|
||||||
this._pools.set(componentName, new ComponentPool(createFn, resetFn, maxSize, minSize) as unknown as ComponentPool<Component>);
|
this.pools.set(componentName, new ComponentPool(createFn, resetFn, maxSize, minSize) as unknown as ComponentPool<Component>);
|
||||||
|
|
||||||
this._usageTracker.set(componentName, {
|
this.usageTracker.set(componentName, {
|
||||||
createCount: 0,
|
createCount: 0,
|
||||||
releaseCount: 0,
|
releaseCount: 0,
|
||||||
lastAccessTime: Date.now()
|
lastAccessTime: Date.now()
|
||||||
@@ -179,25 +178,23 @@ export class ComponentPoolManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 获取组件实例
|
* 获取组件实例
|
||||||
* @en Acquire component instance
|
|
||||||
*/
|
*/
|
||||||
acquireComponent<T extends Component>(componentName: string): T | null {
|
acquireComponent<T extends Component>(componentName: string): T | null {
|
||||||
const pool = this._pools.get(componentName);
|
const pool = this.pools.get(componentName);
|
||||||
|
|
||||||
this._trackUsage(componentName, 'create');
|
this.trackUsage(componentName, 'create');
|
||||||
|
|
||||||
return pool ? (pool.acquire() as T) : null;
|
return pool ? (pool.acquire() as T) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 释放组件实例
|
* 释放组件实例
|
||||||
* @en Release component instance
|
|
||||||
*/
|
*/
|
||||||
releaseComponent<T extends Component>(componentName: string, component: T): void {
|
releaseComponent<T extends Component>(componentName: string, component: T): void {
|
||||||
const pool = this._pools.get(componentName);
|
const pool = this.pools.get(componentName);
|
||||||
|
|
||||||
this._trackUsage(componentName, 'release');
|
this.trackUsage(componentName, 'release');
|
||||||
|
|
||||||
if (pool) {
|
if (pool) {
|
||||||
pool.release(component);
|
pool.release(component);
|
||||||
@@ -205,11 +202,10 @@ export class ComponentPoolManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 追踪使用情况
|
* 追踪使用情况
|
||||||
* @en Track usage
|
|
||||||
*/
|
*/
|
||||||
private _trackUsage(componentName: string, action: 'create' | 'release'): void {
|
private trackUsage(componentName: string, action: 'create' | 'release'): void {
|
||||||
let tracker = this._usageTracker.get(componentName);
|
let tracker = this.usageTracker.get(componentName);
|
||||||
|
|
||||||
if (!tracker) {
|
if (!tracker) {
|
||||||
tracker = {
|
tracker = {
|
||||||
@@ -217,7 +213,7 @@ export class ComponentPoolManager {
|
|||||||
releaseCount: 0,
|
releaseCount: 0,
|
||||||
lastAccessTime: Date.now()
|
lastAccessTime: Date.now()
|
||||||
};
|
};
|
||||||
this._usageTracker.set(componentName, tracker);
|
this.usageTracker.set(componentName, tracker);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action === 'create') {
|
if (action === 'create') {
|
||||||
@@ -230,72 +226,66 @@ export class ComponentPoolManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 自动清理(定期调用)
|
* 自动清理(定期调用)
|
||||||
* @en Auto cleanup (called periodically)
|
|
||||||
*/
|
*/
|
||||||
public update(): void {
|
public update(): void {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
if (now - this._lastCleanupTime < this._autoCleanupInterval) {
|
if (now - this.lastCleanupTime < this.autoCleanupInterval) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [name, tracker] of this._usageTracker.entries()) {
|
for (const [name, tracker] of this.usageTracker.entries()) {
|
||||||
const inactive = now - tracker.lastAccessTime > 120000;
|
const inactive = now - tracker.lastAccessTime > 120000;
|
||||||
|
|
||||||
if (inactive) {
|
if (inactive) {
|
||||||
const pool = this._pools.get(name);
|
const pool = this.pools.get(name);
|
||||||
if (pool) {
|
if (pool) {
|
||||||
pool.shrink();
|
pool.shrink();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this._lastCleanupTime = now;
|
this.lastCleanupTime = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 获取热点组件列表
|
* 获取热点组件列表
|
||||||
* @en Get hot components list
|
|
||||||
*/
|
*/
|
||||||
public getHotComponents(threshold: number = 100): string[] {
|
public getHotComponents(threshold: number = 100): string[] {
|
||||||
return Array.from(this._usageTracker.entries())
|
return Array.from(this.usageTracker.entries())
|
||||||
.filter(([_, tracker]) => tracker.createCount > threshold)
|
.filter(([_, tracker]) => tracker.createCount > threshold)
|
||||||
.map(([name]) => name);
|
.map(([name]) => name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 预热所有池
|
* 预热所有池
|
||||||
* @en Prewarm all pools
|
|
||||||
*/
|
*/
|
||||||
prewarmAll(count: number = 100): void {
|
prewarmAll(count: number = 100): void {
|
||||||
for (const pool of this._pools.values()) {
|
for (const pool of this.pools.values()) {
|
||||||
pool.prewarm(count);
|
pool.prewarm(count);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 清空所有池
|
* 清空所有池
|
||||||
* @en Clear all pools
|
|
||||||
*/
|
*/
|
||||||
clearAll(): void {
|
clearAll(): void {
|
||||||
for (const pool of this._pools.values()) {
|
for (const pool of this.pools.values()) {
|
||||||
pool.clear();
|
pool.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 重置管理器
|
* 重置管理器
|
||||||
* @en Reset manager
|
|
||||||
*/
|
*/
|
||||||
reset(): void {
|
reset(): void {
|
||||||
this._pools.clear();
|
this.pools.clear();
|
||||||
this._usageTracker.clear();
|
this.usageTracker.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 获取全局统计信息
|
* 获取全局统计信息
|
||||||
* @en Get global stats
|
|
||||||
*/
|
*/
|
||||||
getGlobalStats(): Array<{
|
getGlobalStats(): Array<{
|
||||||
componentName: string;
|
componentName: string;
|
||||||
@@ -308,11 +298,11 @@ export class ComponentPoolManager {
|
|||||||
usage: ComponentUsageTracker | undefined;
|
usage: ComponentUsageTracker | undefined;
|
||||||
}> = [];
|
}> = [];
|
||||||
|
|
||||||
for (const [name, pool] of this._pools.entries()) {
|
for (const [name, pool] of this.pools.entries()) {
|
||||||
stats.push({
|
stats.push({
|
||||||
componentName: name,
|
componentName: name,
|
||||||
poolStats: pool.getStats(),
|
poolStats: pool.getStats(),
|
||||||
usage: this._usageTracker.get(name)
|
usage: this.usageTracker.get(name)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,12 +310,11 @@ export class ComponentPoolManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 获取池统计信息
|
* 获取池统计信息
|
||||||
* @en Get pool stats
|
|
||||||
*/
|
*/
|
||||||
getPoolStats(): Map<string, { available: number; maxSize: number }> {
|
getPoolStats(): Map<string, { available: number; maxSize: number }> {
|
||||||
const stats = new Map();
|
const stats = new Map();
|
||||||
for (const [name, pool] of this._pools) {
|
for (const [name, pool] of this.pools) {
|
||||||
stats.set(name, {
|
stats.set(name, {
|
||||||
available: pool.getAvailableCount(),
|
available: pool.getAvailableCount(),
|
||||||
maxSize: pool.getMaxSize()
|
maxSize: pool.getMaxSize()
|
||||||
@@ -335,12 +324,11 @@ export class ComponentPoolManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 获取池利用率信息
|
* 获取池利用率信息
|
||||||
* @en Get pool utilization info
|
|
||||||
*/
|
*/
|
||||||
getPoolUtilization(): Map<string, { used: number; total: number; utilization: number }> {
|
getPoolUtilization(): Map<string, { used: number; total: number; utilization: number }> {
|
||||||
const utilization = new Map();
|
const utilization = new Map();
|
||||||
for (const [name, pool] of this._pools) {
|
for (const [name, pool] of this.pools) {
|
||||||
const available = pool.getAvailableCount();
|
const available = pool.getAvailableCount();
|
||||||
const maxSize = pool.getMaxSize();
|
const maxSize = pool.getMaxSize();
|
||||||
const used = maxSize - available;
|
const used = maxSize - available;
|
||||||
@@ -356,11 +344,10 @@ export class ComponentPoolManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 获取指定组件的池利用率
|
* 获取指定组件的池利用率
|
||||||
* @en Get component pool utilization
|
|
||||||
*/
|
*/
|
||||||
getComponentUtilization(componentName: string): number {
|
getComponentUtilization(componentName: string): number {
|
||||||
const pool = this._pools.get(componentName);
|
const pool = this.pools.get(componentName);
|
||||||
if (!pool) return 0;
|
if (!pool) return 0;
|
||||||
|
|
||||||
const available = pool.getAvailableCount();
|
const available = pool.getAvailableCount();
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Component } from '../Component';
|
import { Component } from '../Component';
|
||||||
import { BitMask64Utils, BitMask64Data } from '../Utils/BigIntCompatibility';
|
import { BitMask64Utils, BitMask64Data } from '../Utils/BigIntCompatibility';
|
||||||
import { SoAStorage } from './SoAStorage';
|
import { SoAStorage, SupportedTypedArray } from './SoAStorage';
|
||||||
import type { SupportedTypedArray } from './SoAStorage';
|
|
||||||
import { createLogger } from '../../Utils/Logger';
|
import { createLogger } from '../../Utils/Logger';
|
||||||
import { getComponentTypeName, ComponentType } from '../Decorators';
|
import { getComponentTypeName, ComponentType } from '../Decorators';
|
||||||
import { ComponentRegistry, GlobalComponentRegistry } from './ComponentStorage/ComponentRegistry';
|
import { ComponentRegistry, GlobalComponentRegistry } from './ComponentStorage/ComponentRegistry';
|
||||||
|
|||||||
@@ -53,11 +53,10 @@ export class ComponentRegistry implements IComponentRegistry {
|
|||||||
// 检查是否使用了 @ECSComponent 装饰器
|
// 检查是否使用了 @ECSComponent 装饰器
|
||||||
if (!hasECSComponentDecorator(componentType) && !this._warnedComponents.has(componentType)) {
|
if (!hasECSComponentDecorator(componentType) && !this._warnedComponents.has(componentType)) {
|
||||||
this._warnedComponents.add(componentType);
|
this._warnedComponents.add(componentType);
|
||||||
logger.warn(
|
console.warn(
|
||||||
`Component "${typeName}" is missing @ECSComponent decorator. ` +
|
`[ComponentRegistry] Component "${typeName}" is missing @ECSComponent decorator. ` +
|
||||||
`This may cause issues with serialization and code minification. ` +
|
`This may cause issues with serialization and code minification. ` +
|
||||||
`Please add: @ECSComponent('${typeName}') | ` +
|
`Please add: @ECSComponent('${typeName}')`
|
||||||
`组件 "${typeName}" 缺少 @ECSComponent 装饰器,可能导致序列化和代码压缩问题`
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,11 +14,7 @@ import type { Component } from '../../Component';
|
|||||||
/**
|
/**
|
||||||
* 组件类型定义
|
* 组件类型定义
|
||||||
* Component type definition
|
* Component type definition
|
||||||
*
|
|
||||||
* 注意:构造函数参数使用 any[] 是必要的,因为组件可以有各种不同签名的构造函数
|
|
||||||
* Note: Constructor args use any[] because components can have various constructor signatures
|
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
export type ComponentType<T extends Component = Component> = new (...args: any[]) => T;
|
export type ComponentType<T extends Component = Component> = new (...args: any[]) => T;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -43,7 +39,7 @@ export const COMPONENT_EDITOR_OPTIONS = Symbol('ComponentEditorOptions');
|
|||||||
* 组件编辑器选项
|
* 组件编辑器选项
|
||||||
* Component editor options
|
* Component editor options
|
||||||
*/
|
*/
|
||||||
export type ComponentEditorOptions = {
|
export interface ComponentEditorOptions {
|
||||||
/**
|
/**
|
||||||
* 是否在 Inspector 中隐藏此组件
|
* 是否在 Inspector 中隐藏此组件
|
||||||
* Whether to hide this component in Inspector
|
* Whether to hide this component in Inspector
|
||||||
@@ -65,51 +61,6 @@ export type ComponentEditorOptions = {
|
|||||||
icon?: string;
|
icon?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 组件构造函数上的元数据接口
|
|
||||||
* Metadata interface stored on component constructors
|
|
||||||
*
|
|
||||||
* 使用 Symbol 索引签名来类型安全地访问装饰器存储的元数据
|
|
||||||
* Uses Symbol index signature to safely access decorator-stored metadata
|
|
||||||
*/
|
|
||||||
export type ComponentTypeMetadata = {
|
|
||||||
readonly [COMPONENT_TYPE_NAME]?: string;
|
|
||||||
readonly [COMPONENT_DEPENDENCIES]?: string[];
|
|
||||||
readonly [COMPONENT_EDITOR_OPTIONS]?: ComponentEditorOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 可写的组件类型元数据(用于装饰器设置)
|
|
||||||
* Writable component type metadata (for decorator setting)
|
|
||||||
*/
|
|
||||||
export type WritableComponentTypeMetadata = {
|
|
||||||
[COMPONENT_TYPE_NAME]?: string;
|
|
||||||
[COMPONENT_DEPENDENCIES]?: string[];
|
|
||||||
[COMPONENT_EDITOR_OPTIONS]?: ComponentEditorOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取组件构造函数的元数据
|
|
||||||
* Get metadata from component constructor
|
|
||||||
*
|
|
||||||
* @param componentType 组件构造函数
|
|
||||||
* @returns 元数据对象
|
|
||||||
*/
|
|
||||||
export function getComponentTypeMetadata(componentType: ComponentType): ComponentTypeMetadata {
|
|
||||||
return componentType as unknown as ComponentTypeMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取可写的组件构造函数元数据(用于装饰器)
|
|
||||||
* Get writable metadata from component constructor (for decorators)
|
|
||||||
*
|
|
||||||
* @param componentType 组件构造函数
|
|
||||||
* @returns 可写的元数据对象
|
|
||||||
*/
|
|
||||||
export function getWritableComponentTypeMetadata(componentType: ComponentType): WritableComponentTypeMetadata {
|
|
||||||
return componentType as unknown as WritableComponentTypeMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查组件是否使用了 @ECSComponent 装饰器
|
* 检查组件是否使用了 @ECSComponent 装饰器
|
||||||
* Check if component has @ECSComponent decorator
|
* Check if component has @ECSComponent decorator
|
||||||
@@ -118,8 +69,7 @@ export function getWritableComponentTypeMetadata(componentType: ComponentType):
|
|||||||
* @returns 是否有装饰器
|
* @returns 是否有装饰器
|
||||||
*/
|
*/
|
||||||
export function hasECSComponentDecorator(componentType: ComponentType): boolean {
|
export function hasECSComponentDecorator(componentType: ComponentType): boolean {
|
||||||
const metadata = getComponentTypeMetadata(componentType);
|
return !!(componentType as any)[COMPONENT_TYPE_NAME];
|
||||||
return metadata[COMPONENT_TYPE_NAME] !== undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -132,8 +82,7 @@ export function hasECSComponentDecorator(componentType: ComponentType): boolean
|
|||||||
export function getComponentTypeName(componentType: ComponentType): string {
|
export function getComponentTypeName(componentType: ComponentType): string {
|
||||||
// 优先使用装饰器指定的名称
|
// 优先使用装饰器指定的名称
|
||||||
// Prefer decorator-specified name
|
// Prefer decorator-specified name
|
||||||
const metadata = getComponentTypeMetadata(componentType);
|
const decoratorName = (componentType as any)[COMPONENT_TYPE_NAME];
|
||||||
const decoratorName = metadata[COMPONENT_TYPE_NAME];
|
|
||||||
if (decoratorName) {
|
if (decoratorName) {
|
||||||
return decoratorName;
|
return decoratorName;
|
||||||
}
|
}
|
||||||
@@ -162,8 +111,7 @@ export function getComponentInstanceTypeName(component: Component): string {
|
|||||||
* @returns 依赖的组件名称列表
|
* @returns 依赖的组件名称列表
|
||||||
*/
|
*/
|
||||||
export function getComponentDependencies(componentType: ComponentType): string[] | undefined {
|
export function getComponentDependencies(componentType: ComponentType): string[] | undefined {
|
||||||
const metadata = getComponentTypeMetadata(componentType);
|
return (componentType as any)[COMPONENT_DEPENDENCIES];
|
||||||
return metadata[COMPONENT_DEPENDENCIES];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -174,8 +122,7 @@ export function getComponentDependencies(componentType: ComponentType): string[]
|
|||||||
* @returns 编辑器选项
|
* @returns 编辑器选项
|
||||||
*/
|
*/
|
||||||
export function getComponentEditorOptions(componentType: ComponentType): ComponentEditorOptions | undefined {
|
export function getComponentEditorOptions(componentType: ComponentType): ComponentEditorOptions | undefined {
|
||||||
const metadata = getComponentTypeMetadata(componentType);
|
return (componentType as any)[COMPONENT_EDITOR_OPTIONS];
|
||||||
return metadata[COMPONENT_EDITOR_OPTIONS];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import type { ComponentType } from './ComponentTypeUtils';
|
|||||||
* Component Registry Interface.
|
* Component Registry Interface.
|
||||||
* 组件注册表接口。
|
* 组件注册表接口。
|
||||||
*/
|
*/
|
||||||
export type IComponentRegistry = {
|
export interface IComponentRegistry {
|
||||||
/**
|
/**
|
||||||
* Register component type and allocate bitmask.
|
* Register component type and allocate bitmask.
|
||||||
* 注册组件类型并分配位掩码。
|
* 注册组件类型并分配位掩码。
|
||||||
|
|||||||
@@ -443,33 +443,29 @@ export class EventBus implements IEventBus {
|
|||||||
* 提供全局访问的事件总线
|
* 提供全局访问的事件总线
|
||||||
*/
|
*/
|
||||||
export class GlobalEventBus {
|
export class GlobalEventBus {
|
||||||
private static _instance: EventBus;
|
private static instance: EventBus;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 获取全局事件总线实例
|
* 获取全局事件总线实例
|
||||||
* @en Get global event bus instance
|
* @param debugMode 是否启用调试模式
|
||||||
*
|
|
||||||
* @param debugMode - @zh 是否启用调试模式 @en Whether to enable debug mode
|
|
||||||
*/
|
*/
|
||||||
public static getInstance(debugMode: boolean = false): EventBus {
|
public static getInstance(debugMode: boolean = false): EventBus {
|
||||||
if (!this._instance) {
|
if (!this.instance) {
|
||||||
this._instance = new EventBus(debugMode);
|
this.instance = new EventBus(debugMode);
|
||||||
}
|
}
|
||||||
return this._instance;
|
return this.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 重置全局事件总线实例
|
* 重置全局事件总线实例
|
||||||
* @en Reset global event bus instance
|
* @param debugMode 是否启用调试模式
|
||||||
*
|
|
||||||
* @param debugMode - @zh 是否启用调试模式 @en Whether to enable debug mode
|
|
||||||
*/
|
*/
|
||||||
public static reset(debugMode: boolean = false): EventBus {
|
public static reset(debugMode: boolean = false): EventBus {
|
||||||
if (this._instance) {
|
if (this.instance) {
|
||||||
this._instance.clear();
|
this.instance.clear();
|
||||||
}
|
}
|
||||||
this._instance = new EventBus(debugMode);
|
this.instance = new EventBus(debugMode);
|
||||||
return this._instance;
|
return this.instance;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export type AsyncEventHandler<T> = (event: T) => Promise<void>;
|
|||||||
/**
|
/**
|
||||||
* 事件监听器配置
|
* 事件监听器配置
|
||||||
*/
|
*/
|
||||||
export type EventListenerConfig = {
|
export interface EventListenerConfig {
|
||||||
/** 是否只执行一次 */
|
/** 是否只执行一次 */
|
||||||
once?: boolean;
|
once?: boolean;
|
||||||
/** 优先级(数字越大优先级越高) */
|
/** 优先级(数字越大优先级越高) */
|
||||||
@@ -41,7 +41,7 @@ interface InternalEventListener {
|
|||||||
/**
|
/**
|
||||||
* 事件统计信息
|
* 事件统计信息
|
||||||
*/
|
*/
|
||||||
export type EventStats = {
|
export interface EventStats {
|
||||||
/** 事件类型 */
|
/** 事件类型 */
|
||||||
eventType: string;
|
eventType: string;
|
||||||
/** 监听器数量 */
|
/** 监听器数量 */
|
||||||
@@ -59,7 +59,7 @@ export type EventStats = {
|
|||||||
/**
|
/**
|
||||||
* 事件批处理配置
|
* 事件批处理配置
|
||||||
*/
|
*/
|
||||||
export type EventBatchConfig = {
|
export interface EventBatchConfig {
|
||||||
/** 批处理大小 */
|
/** 批处理大小 */
|
||||||
batchSize: number;
|
batchSize: number;
|
||||||
/** 批处理延迟(毫秒) */
|
/** 批处理延迟(毫秒) */
|
||||||
|
|||||||
@@ -1,3 +1,2 @@
|
|||||||
export { EventBus, GlobalEventBus } from '../EventBus';
|
export { EventBus, GlobalEventBus } from '../EventBus';
|
||||||
export { TypeSafeEventSystem } from '../EventSystem';
|
export { TypeSafeEventSystem, EventListenerConfig, EventStats } from '../EventSystem';
|
||||||
export type { EventListenerConfig, EventStats } from '../EventSystem';
|
|
||||||
|
|||||||
@@ -6,12 +6,10 @@ import { createLogger } from '../../Utils/Logger';
|
|||||||
import { getComponentTypeName } from '../Decorators';
|
import { getComponentTypeName } from '../Decorators';
|
||||||
import { Archetype, ArchetypeSystem } from './ArchetypeSystem';
|
import { Archetype, ArchetypeSystem } from './ArchetypeSystem';
|
||||||
import { ReactiveQuery, ReactiveQueryConfig } from './ReactiveQuery';
|
import { ReactiveQuery, ReactiveQueryConfig } from './ReactiveQuery';
|
||||||
import type { QueryCondition, QueryResult } from './QueryTypes';
|
import { QueryCondition, QueryConditionType, QueryResult } from './QueryTypes';
|
||||||
import { QueryConditionType } from './QueryTypes';
|
|
||||||
import { CompiledQuery } from './Query/CompiledQuery';
|
import { CompiledQuery } from './Query/CompiledQuery';
|
||||||
|
|
||||||
export { QueryConditionType };
|
export { QueryCondition, QueryConditionType, QueryResult };
|
||||||
export type { QueryCondition, QueryResult };
|
|
||||||
export { CompiledQuery };
|
export { CompiledQuery };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export enum QueryConditionType {
|
|||||||
/**
|
/**
|
||||||
* 查询条件接口
|
* 查询条件接口
|
||||||
*/
|
*/
|
||||||
export type QueryCondition = {
|
export interface QueryCondition {
|
||||||
type: QueryConditionType;
|
type: QueryConditionType;
|
||||||
componentTypes: ComponentType[];
|
componentTypes: ComponentType[];
|
||||||
mask: BitMask64Data;
|
mask: BitMask64Data;
|
||||||
@@ -26,7 +26,7 @@ export type QueryCondition = {
|
|||||||
/**
|
/**
|
||||||
* 实体查询结果接口
|
* 实体查询结果接口
|
||||||
*/
|
*/
|
||||||
export type QueryResult = {
|
export interface QueryResult {
|
||||||
entities: readonly Entity[];
|
entities: readonly Entity[];
|
||||||
count: number;
|
count: number;
|
||||||
/** 查询执行时间(毫秒) */
|
/** 查询执行时间(毫秒) */
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Entity } from '../Entity';
|
import { Entity } from '../Entity';
|
||||||
import type { QueryCondition } from './QueryTypes';
|
import { QueryCondition, QueryConditionType } from './QueryTypes';
|
||||||
import { QueryConditionType } from './QueryTypes';
|
|
||||||
import { BitMask64Utils } from '../Utils/BigIntCompatibility';
|
import { BitMask64Utils } from '../Utils/BigIntCompatibility';
|
||||||
import { createLogger } from '../../Utils/Logger';
|
import { createLogger } from '../../Utils/Logger';
|
||||||
|
|
||||||
@@ -21,7 +20,7 @@ export enum ReactiveQueryChangeType {
|
|||||||
/**
|
/**
|
||||||
* 响应式查询变化事件
|
* 响应式查询变化事件
|
||||||
*/
|
*/
|
||||||
export type ReactiveQueryChange = {
|
export interface ReactiveQueryChange {
|
||||||
/** 变化类型 */
|
/** 变化类型 */
|
||||||
type: ReactiveQueryChangeType;
|
type: ReactiveQueryChangeType;
|
||||||
/** 变化的实体 */
|
/** 变化的实体 */
|
||||||
@@ -42,7 +41,7 @@ export type ReactiveQueryListener = (change: ReactiveQueryChange) => void;
|
|||||||
/**
|
/**
|
||||||
* 响应式查询配置
|
* 响应式查询配置
|
||||||
*/
|
*/
|
||||||
export type ReactiveQueryConfig = {
|
export interface ReactiveQueryConfig {
|
||||||
/** 是否启用批量模式(减少通知频率) */
|
/** 是否启用批量模式(减少通知频率) */
|
||||||
enableBatchMode?: boolean;
|
enableBatchMode?: boolean;
|
||||||
/** 批量模式的延迟时间(毫秒) */
|
/** 批量模式的延迟时间(毫秒) */
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ const WeakRefImpl: IWeakRefConstructor = (
|
|||||||
/**
|
/**
|
||||||
* Entity引用记录
|
* Entity引用记录
|
||||||
*/
|
*/
|
||||||
export type EntityRefRecord = {
|
export interface EntityRefRecord {
|
||||||
component: IWeakRef<Component>;
|
component: IWeakRef<Component>;
|
||||||
propertyKey: string;
|
propertyKey: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,182 +6,177 @@ import {
|
|||||||
TypedArrayTypeName
|
TypedArrayTypeName
|
||||||
} from './SoATypeRegistry';
|
} from './SoATypeRegistry';
|
||||||
import { SoASerializer } from './SoASerializer';
|
import { SoASerializer } from './SoASerializer';
|
||||||
import type { IComponentTypeMetadata, ComponentTypeWithMetadata } from '../../Types';
|
|
||||||
|
|
||||||
// 重新导出类型,保持向后兼容
|
// 重新导出类型,保持向后兼容
|
||||||
export type { SupportedTypedArray, TypedArrayTypeName } from './SoATypeRegistry';
|
export { SupportedTypedArray, TypedArrayTypeName } from './SoATypeRegistry';
|
||||||
export { SoATypeRegistry } from './SoATypeRegistry';
|
export { SoATypeRegistry } from './SoATypeRegistry';
|
||||||
export { SoASerializer } from './SoASerializer';
|
export { SoASerializer } from './SoASerializer';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh SoA 字段统计信息
|
* 启用SoA优化装饰器
|
||||||
* @en SoA field statistics
|
* 默认关闭SoA,只有在大规模批量操作场景下才建议开启
|
||||||
*/
|
|
||||||
export interface ISoAFieldStats {
|
|
||||||
size: number;
|
|
||||||
capacity: number;
|
|
||||||
type: string;
|
|
||||||
memory: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh SoA 存储统计信息
|
|
||||||
* @en SoA storage statistics
|
|
||||||
*/
|
|
||||||
export interface ISoAStorageStats {
|
|
||||||
size: number;
|
|
||||||
capacity: number;
|
|
||||||
totalSlots: number;
|
|
||||||
usedSlots: number;
|
|
||||||
freeSlots: number;
|
|
||||||
fragmentation: number;
|
|
||||||
memoryUsage: number;
|
|
||||||
fieldStats: Map<string, ISoAFieldStats>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 启用 SoA 优化装饰器 - 默认关闭,只在大规模批量操作场景下建议开启
|
|
||||||
* @en Enable SoA optimization decorator - disabled by default, recommended only for large-scale batch operations
|
|
||||||
*/
|
*/
|
||||||
export function EnableSoA<T extends ComponentType>(target: T): T {
|
export function EnableSoA<T extends ComponentType>(target: T): T {
|
||||||
(target as ComponentType & IComponentTypeMetadata).__enableSoA = true;
|
(target as any).__enableSoA = true;
|
||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 组件字段元数据键(仅 Set<string> 类型的字段)
|
* 64位浮点数装饰器
|
||||||
* @en Component field metadata keys (only Set<string> type fields)
|
* 标记字段使用Float64Array存储(更高精度但更多内存)
|
||||||
*/
|
*/
|
||||||
type ComponentFieldMetadataKey = Exclude<keyof IComponentTypeMetadata, '__enableSoA'>;
|
export function Float64(target: any, propertyKey: string | symbol): void {
|
||||||
|
const key = String(propertyKey);
|
||||||
/**
|
if (!target.constructor.__float64Fields) {
|
||||||
* @zh 装饰器目标原型接口
|
target.constructor.__float64Fields = new Set();
|
||||||
* @en Decorator target prototype interface
|
|
||||||
*/
|
|
||||||
interface IDecoratorTarget {
|
|
||||||
constructor: IComponentTypeMetadata & Function;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 辅助函数:获取或创建字段集合
|
|
||||||
* @en Helper function: get or create field set
|
|
||||||
*/
|
|
||||||
function getOrCreateFieldSet(
|
|
||||||
target: IDecoratorTarget,
|
|
||||||
fieldName: ComponentFieldMetadataKey
|
|
||||||
): Set<string> {
|
|
||||||
const ctor = target.constructor as IComponentTypeMetadata;
|
|
||||||
let fieldSet = ctor[fieldName];
|
|
||||||
if (!fieldSet) {
|
|
||||||
fieldSet = new Set<string>();
|
|
||||||
ctor[fieldName] = fieldSet;
|
|
||||||
}
|
}
|
||||||
return fieldSet;
|
target.constructor.__float64Fields.add(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 64位浮点数装饰器 - 标记字段使用 Float64Array 存储(更高精度但更多内存)
|
* 32位浮点数装饰器
|
||||||
* @en Float64 decorator - marks field to use Float64Array storage (higher precision but more memory)
|
* 标记字段使用Float32Array存储(默认类型,平衡性能和精度)
|
||||||
*/
|
*/
|
||||||
export function Float64(target: object, propertyKey: string | symbol): void {
|
export function Float32(target: any, propertyKey: string | symbol): void {
|
||||||
getOrCreateFieldSet(target as IDecoratorTarget, '__float64Fields').add(String(propertyKey));
|
const key = String(propertyKey);
|
||||||
|
if (!target.constructor.__float32Fields) {
|
||||||
|
target.constructor.__float32Fields = new Set();
|
||||||
|
}
|
||||||
|
target.constructor.__float32Fields.add(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 32位浮点数装饰器 - 标记字段使用 Float32Array 存储(默认类型,平衡性能和精度)
|
* 32位整数装饰器
|
||||||
* @en Float32 decorator - marks field to use Float32Array storage (default, balanced performance and precision)
|
* 标记字段使用Int32Array存储(适用于整数值)
|
||||||
*/
|
*/
|
||||||
export function Float32(target: object, propertyKey: string | symbol): void {
|
export function Int32(target: any, propertyKey: string | symbol): void {
|
||||||
getOrCreateFieldSet(target as IDecoratorTarget, '__float32Fields').add(String(propertyKey));
|
const key = String(propertyKey);
|
||||||
|
if (!target.constructor.__int32Fields) {
|
||||||
|
target.constructor.__int32Fields = new Set();
|
||||||
|
}
|
||||||
|
target.constructor.__int32Fields.add(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 32位整数装饰器 - 标记字段使用 Int32Array 存储(适用于整数值)
|
* 32位无符号整数装饰器
|
||||||
* @en Int32 decorator - marks field to use Int32Array storage (for integer values)
|
* 标记字段使用Uint32Array存储(适用于无符号整数,如ID、标志位等)
|
||||||
*/
|
*/
|
||||||
export function Int32(target: object, propertyKey: string | symbol): void {
|
export function Uint32(target: any, propertyKey: string | symbol): void {
|
||||||
getOrCreateFieldSet(target as IDecoratorTarget, '__int32Fields').add(String(propertyKey));
|
const key = String(propertyKey);
|
||||||
|
if (!target.constructor.__uint32Fields) {
|
||||||
|
target.constructor.__uint32Fields = new Set();
|
||||||
|
}
|
||||||
|
target.constructor.__uint32Fields.add(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 32位无符号整数装饰器 - 标记字段使用 Uint32Array 存储
|
* 16位整数装饰器
|
||||||
* @en Uint32 decorator - marks field to use Uint32Array storage
|
* 标记字段使用Int16Array存储(适用于小范围整数)
|
||||||
*/
|
*/
|
||||||
export function Uint32(target: object, propertyKey: string | symbol): void {
|
export function Int16(target: any, propertyKey: string | symbol): void {
|
||||||
getOrCreateFieldSet(target as IDecoratorTarget, '__uint32Fields').add(String(propertyKey));
|
const key = String(propertyKey);
|
||||||
|
if (!target.constructor.__int16Fields) {
|
||||||
|
target.constructor.__int16Fields = new Set();
|
||||||
|
}
|
||||||
|
target.constructor.__int16Fields.add(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 16位整数装饰器 - 标记字段使用 Int16Array 存储
|
* 16位无符号整数装饰器
|
||||||
* @en Int16 decorator - marks field to use Int16Array storage
|
* 标记字段使用Uint16Array存储(适用于小范围无符号整数)
|
||||||
*/
|
*/
|
||||||
export function Int16(target: object, propertyKey: string | symbol): void {
|
export function Uint16(target: any, propertyKey: string | symbol): void {
|
||||||
getOrCreateFieldSet(target as IDecoratorTarget, '__int16Fields').add(String(propertyKey));
|
const key = String(propertyKey);
|
||||||
|
if (!target.constructor.__uint16Fields) {
|
||||||
|
target.constructor.__uint16Fields = new Set();
|
||||||
|
}
|
||||||
|
target.constructor.__uint16Fields.add(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 16位无符号整数装饰器 - 标记字段使用 Uint16Array 存储
|
* 8位整数装饰器
|
||||||
* @en Uint16 decorator - marks field to use Uint16Array storage
|
* 标记字段使用Int8Array存储(适用于很小的整数值)
|
||||||
*/
|
*/
|
||||||
export function Uint16(target: object, propertyKey: string | symbol): void {
|
export function Int8(target: any, propertyKey: string | symbol): void {
|
||||||
getOrCreateFieldSet(target as IDecoratorTarget, '__uint16Fields').add(String(propertyKey));
|
const key = String(propertyKey);
|
||||||
|
if (!target.constructor.__int8Fields) {
|
||||||
|
target.constructor.__int8Fields = new Set();
|
||||||
|
}
|
||||||
|
target.constructor.__int8Fields.add(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 8位整数装饰器 - 标记字段使用 Int8Array 存储
|
* 8位无符号整数装饰器
|
||||||
* @en Int8 decorator - marks field to use Int8Array storage
|
* 标记字段使用Uint8Array存储(适用于字节值、布尔标志等)
|
||||||
*/
|
*/
|
||||||
export function Int8(target: object, propertyKey: string | symbol): void {
|
export function Uint8(target: any, propertyKey: string | symbol): void {
|
||||||
getOrCreateFieldSet(target as IDecoratorTarget, '__int8Fields').add(String(propertyKey));
|
const key = String(propertyKey);
|
||||||
|
if (!target.constructor.__uint8Fields) {
|
||||||
|
target.constructor.__uint8Fields = new Set();
|
||||||
|
}
|
||||||
|
target.constructor.__uint8Fields.add(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 8位无符号整数装饰器 - 标记字段使用 Uint8Array 存储
|
* 8位夹紧整数装饰器
|
||||||
* @en Uint8 decorator - marks field to use Uint8Array storage
|
* 标记字段使用Uint8ClampedArray存储(适用于颜色值等需要夹紧的数据)
|
||||||
*/
|
*/
|
||||||
export function Uint8(target: object, propertyKey: string | symbol): void {
|
export function Uint8Clamped(target: any, propertyKey: string | symbol): void {
|
||||||
getOrCreateFieldSet(target as IDecoratorTarget, '__uint8Fields').add(String(propertyKey));
|
const key = String(propertyKey);
|
||||||
|
if (!target.constructor.__uint8ClampedFields) {
|
||||||
|
target.constructor.__uint8ClampedFields = new Set();
|
||||||
|
}
|
||||||
|
target.constructor.__uint8ClampedFields.add(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 序列化Map装饰器
|
||||||
|
* 标记Map字段需要序列化/反序列化存储
|
||||||
|
*/
|
||||||
|
export function SerializeMap(target: any, propertyKey: string | symbol): void {
|
||||||
|
const key = String(propertyKey);
|
||||||
|
if (!target.constructor.__serializeMapFields) {
|
||||||
|
target.constructor.__serializeMapFields = new Set();
|
||||||
|
}
|
||||||
|
target.constructor.__serializeMapFields.add(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 8位夹紧整数装饰器 - 标记字段使用 Uint8ClampedArray 存储(适用于颜色值)
|
* 序列化Set装饰器
|
||||||
* @en Uint8Clamped decorator - marks field to use Uint8ClampedArray storage (for color values)
|
* 标记Set字段需要序列化/反序列化存储
|
||||||
*/
|
*/
|
||||||
export function Uint8Clamped(target: object, propertyKey: string | symbol): void {
|
export function SerializeSet(target: any, propertyKey: string | symbol): void {
|
||||||
getOrCreateFieldSet(target as IDecoratorTarget, '__uint8ClampedFields').add(String(propertyKey));
|
const key = String(propertyKey);
|
||||||
|
if (!target.constructor.__serializeSetFields) {
|
||||||
|
target.constructor.__serializeSetFields = new Set();
|
||||||
|
}
|
||||||
|
target.constructor.__serializeSetFields.add(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 序列化 Map 装饰器 - 标记 Map 字段需要序列化/反序列化存储
|
* 序列化Array装饰器
|
||||||
* @en SerializeMap decorator - marks Map field for serialization/deserialization
|
* 标记Array字段需要序列化/反序列化存储
|
||||||
*/
|
*/
|
||||||
export function SerializeMap(target: object, propertyKey: string | symbol): void {
|
export function SerializeArray(target: any, propertyKey: string | symbol): void {
|
||||||
getOrCreateFieldSet(target as IDecoratorTarget, '__serializeMapFields').add(String(propertyKey));
|
const key = String(propertyKey);
|
||||||
|
if (!target.constructor.__serializeArrayFields) {
|
||||||
|
target.constructor.__serializeArrayFields = new Set();
|
||||||
|
}
|
||||||
|
target.constructor.__serializeArrayFields.add(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 序列化 Set 装饰器 - 标记 Set 字段需要序列化/反序列化存储
|
* 深拷贝装饰器
|
||||||
* @en SerializeSet decorator - marks Set field for serialization/deserialization
|
* 标记字段需要深拷贝处理(适用于嵌套对象)
|
||||||
*/
|
*/
|
||||||
export function SerializeSet(target: object, propertyKey: string | symbol): void {
|
export function DeepCopy(target: any, propertyKey: string | symbol): void {
|
||||||
getOrCreateFieldSet(target as IDecoratorTarget, '__serializeSetFields').add(String(propertyKey));
|
const key = String(propertyKey);
|
||||||
}
|
if (!target.constructor.__deepCopyFields) {
|
||||||
|
target.constructor.__deepCopyFields = new Set();
|
||||||
/**
|
}
|
||||||
* @zh 序列化 Array 装饰器 - 标记 Array 字段需要序列化/反序列化存储
|
target.constructor.__deepCopyFields.add(key);
|
||||||
* @en SerializeArray decorator - marks Array field for serialization/deserialization
|
|
||||||
*/
|
|
||||||
export function SerializeArray(target: object, propertyKey: string | symbol): void {
|
|
||||||
getOrCreateFieldSet(target as IDecoratorTarget, '__serializeArrayFields').add(String(propertyKey));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 深拷贝装饰器 - 标记字段需要深拷贝处理(适用于嵌套对象)
|
|
||||||
* @en DeepCopy decorator - marks field for deep copy handling (for nested objects)
|
|
||||||
*/
|
|
||||||
export function DeepCopy(target: object, propertyKey: string | symbol): void {
|
|
||||||
getOrCreateFieldSet(target as IDecoratorTarget, '__deepCopyFields').add(String(propertyKey));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -191,8 +186,8 @@ export function DeepCopy(target: object, propertyKey: string | symbol): void {
|
|||||||
*/
|
*/
|
||||||
export class SoAStorage<T extends Component> {
|
export class SoAStorage<T extends Component> {
|
||||||
private fields = new Map<string, SupportedTypedArray>();
|
private fields = new Map<string, SupportedTypedArray>();
|
||||||
private stringFields = new Map<string, Array<string | undefined>>();
|
private stringFields = new Map<string, string[]>();
|
||||||
private serializedFields = new Map<string, Array<string | undefined>>();
|
private serializedFields = new Map<string, string[]>();
|
||||||
private complexFields = new Map<number, Map<string, unknown>>();
|
private complexFields = new Map<number, Map<string, unknown>>();
|
||||||
private entityToIndex = new Map<number, number>();
|
private entityToIndex = new Map<number, number>();
|
||||||
private indexToEntity: number[] = [];
|
private indexToEntity: number[] = [];
|
||||||
@@ -323,29 +318,30 @@ export class SoAStorage<T extends Component> {
|
|||||||
|
|
||||||
private updateComponentAtIndex(index: number, component: T): void {
|
private updateComponentAtIndex(index: number, component: T): void {
|
||||||
const entityId = this.indexToEntity[index]!;
|
const entityId = this.indexToEntity[index]!;
|
||||||
const complexFieldMap = new Map<string, unknown>();
|
const complexFieldMap = new Map<string, any>();
|
||||||
const typeWithMeta = this.type as ComponentTypeWithMetadata<T>;
|
const highPrecisionFields = (this.type as any).__highPrecisionFields || new Set();
|
||||||
const highPrecisionFields = typeWithMeta.__highPrecisionFields || new Set<string>();
|
const serializeMapFields = (this.type as any).__serializeMapFields || new Set();
|
||||||
const serializeMapFields = typeWithMeta.__serializeMapFields || new Set<string>();
|
const serializeSetFields = (this.type as any).__serializeSetFields || new Set();
|
||||||
const serializeSetFields = typeWithMeta.__serializeSetFields || new Set<string>();
|
const serializeArrayFields = (this.type as any).__serializeArrayFields || new Set();
|
||||||
const serializeArrayFields = typeWithMeta.__serializeArrayFields || new Set<string>();
|
const deepCopyFields = (this.type as any).__deepCopyFields || new Set();
|
||||||
const deepCopyFields = typeWithMeta.__deepCopyFields || new Set<string>();
|
|
||||||
|
|
||||||
const componentRecord = component as Record<string, unknown>;
|
// 处理所有字段
|
||||||
for (const key in component) {
|
for (const key in component) {
|
||||||
if (Object.prototype.hasOwnProperty.call(component, key) && key !== 'id') {
|
if (component.hasOwnProperty(key) && key !== 'id') {
|
||||||
const value = componentRecord[key];
|
const value = (component as any)[key];
|
||||||
const type = typeof value;
|
const type = typeof value;
|
||||||
|
|
||||||
if (type === 'number') {
|
if (type === 'number') {
|
||||||
const numValue = value as number;
|
|
||||||
if (highPrecisionFields.has(key) || !this.fields.has(key)) {
|
if (highPrecisionFields.has(key) || !this.fields.has(key)) {
|
||||||
complexFieldMap.set(key, numValue);
|
// 标记为高精度或未在TypedArray中的数值作为复杂对象存储
|
||||||
|
complexFieldMap.set(key, value);
|
||||||
} else {
|
} else {
|
||||||
|
// 存储到TypedArray
|
||||||
const array = this.fields.get(key)!;
|
const array = this.fields.get(key)!;
|
||||||
array[index] = numValue;
|
array[index] = value;
|
||||||
}
|
}
|
||||||
} else if (type === 'boolean' && this.fields.has(key)) {
|
} else if (type === 'boolean' && this.fields.has(key)) {
|
||||||
|
// 布尔值存储到TypedArray
|
||||||
const array = this.fields.get(key)!;
|
const array = this.fields.get(key)!;
|
||||||
array[index] = value ? 1 : 0;
|
array[index] = value ? 1 : 0;
|
||||||
} else if (this.stringFields.has(key)) {
|
} else if (this.stringFields.has(key)) {
|
||||||
@@ -530,8 +526,7 @@ export class SoAStorage<T extends Component> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 获取组件的快照副本(用于序列化等需要独立副本的场景)
|
* 获取组件的快照副本(用于序列化等需要独立副本的场景)
|
||||||
* @en Get a snapshot copy of the component (for serialization scenarios)
|
|
||||||
*/
|
*/
|
||||||
public getComponentSnapshot(entityId: number): T | null {
|
public getComponentSnapshot(entityId: number): T | null {
|
||||||
const index = this.entityToIndex.get(entityId);
|
const index = this.entityToIndex.get(entityId);
|
||||||
@@ -539,26 +534,32 @@ export class SoAStorage<T extends Component> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const component = new this.type();
|
// 需要 any 因为要动态写入泛型 T 的属性
|
||||||
const componentRecord = component as unknown as Record<string, unknown>;
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const component = new this.type() as any;
|
||||||
|
|
||||||
// 恢复数值字段
|
// 恢复数值字段
|
||||||
for (const [fieldName, array] of this.fields.entries()) {
|
for (const [fieldName, array] of this.fields.entries()) {
|
||||||
const value = array[index];
|
const value = array[index];
|
||||||
const fieldType = this.getFieldType(fieldName);
|
const fieldType = this.getFieldType(fieldName);
|
||||||
componentRecord[fieldName] = fieldType === 'boolean' ? value === 1 : value;
|
|
||||||
|
if (fieldType === 'boolean') {
|
||||||
|
component[fieldName] = value === 1;
|
||||||
|
} else {
|
||||||
|
component[fieldName] = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 恢复字符串字段
|
// 恢复字符串字段
|
||||||
for (const [fieldName, stringArray] of this.stringFields.entries()) {
|
for (const [fieldName, stringArray] of this.stringFields.entries()) {
|
||||||
componentRecord[fieldName] = stringArray[index];
|
component[fieldName] = stringArray[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 恢复序列化字段
|
// 恢复序列化字段
|
||||||
for (const [fieldName, serializedArray] of this.serializedFields.entries()) {
|
for (const [fieldName, serializedArray] of this.serializedFields.entries()) {
|
||||||
const serialized = serializedArray[index];
|
const serialized = serializedArray[index];
|
||||||
if (serialized) {
|
if (serialized) {
|
||||||
componentRecord[fieldName] = SoASerializer.deserialize(serialized, fieldName, {
|
component[fieldName] = SoASerializer.deserialize(serialized, fieldName, {
|
||||||
isMap: this.serializeMapFields.has(fieldName),
|
isMap: this.serializeMapFields.has(fieldName),
|
||||||
isSet: this.serializeSetFields.has(fieldName),
|
isSet: this.serializeSetFields.has(fieldName),
|
||||||
isArray: this.serializeArrayFields.has(fieldName)
|
isArray: this.serializeArrayFields.has(fieldName)
|
||||||
@@ -570,11 +571,11 @@ export class SoAStorage<T extends Component> {
|
|||||||
const complexFieldMap = this.complexFields.get(entityId);
|
const complexFieldMap = this.complexFields.get(entityId);
|
||||||
if (complexFieldMap) {
|
if (complexFieldMap) {
|
||||||
for (const [fieldName, value] of complexFieldMap.entries()) {
|
for (const [fieldName, value] of complexFieldMap.entries()) {
|
||||||
componentRecord[fieldName] = value;
|
component[fieldName] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return component;
|
return component as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getFieldType(fieldName: string): string {
|
private getFieldType(fieldName: string): string {
|
||||||
@@ -672,14 +673,14 @@ export class SoAStorage<T extends Component> {
|
|||||||
// 重置字符串字段数组
|
// 重置字符串字段数组
|
||||||
for (const stringArray of this.stringFields.values()) {
|
for (const stringArray of this.stringFields.values()) {
|
||||||
for (let i = 0; i < stringArray.length; i++) {
|
for (let i = 0; i < stringArray.length; i++) {
|
||||||
stringArray[i] = undefined;
|
stringArray[i] = undefined as any;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重置序列化字段数组
|
// 重置序列化字段数组
|
||||||
for (const serializedArray of this.serializedFields.values()) {
|
for (const serializedArray of this.serializedFields.values()) {
|
||||||
for (let i = 0; i < serializedArray.length; i++) {
|
for (let i = 0; i < serializedArray.length; i++) {
|
||||||
serializedArray[i] = undefined;
|
serializedArray[i] = undefined as any;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -739,13 +740,9 @@ export class SoAStorage<T extends Component> {
|
|||||||
this._size = activeEntries.length;
|
this._size = activeEntries.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public getStats(): any {
|
||||||
* @zh 获取 SoA 存储统计信息
|
|
||||||
* @en Get SoA storage statistics
|
|
||||||
*/
|
|
||||||
public getStats(): ISoAStorageStats {
|
|
||||||
let totalMemory = 0;
|
let totalMemory = 0;
|
||||||
const fieldStats = new Map<string, ISoAFieldStats>();
|
const fieldStats = new Map<string, any>();
|
||||||
|
|
||||||
for (const [fieldName, array] of this.fields.entries()) {
|
for (const [fieldName, array] of this.fields.entries()) {
|
||||||
const typeName = SoATypeRegistry.getTypeName(array);
|
const typeName = SoATypeRegistry.getTypeName(array);
|
||||||
@@ -764,9 +761,7 @@ export class SoAStorage<T extends Component> {
|
|||||||
return {
|
return {
|
||||||
size: this._size,
|
size: this._size,
|
||||||
capacity: this._capacity,
|
capacity: this._capacity,
|
||||||
totalSlots: this._capacity,
|
usedSlots: this._size, // 兼容原测试
|
||||||
usedSlots: this._size,
|
|
||||||
freeSlots: this._capacity - this._size,
|
|
||||||
fragmentation: this.freeIndices.length / this._capacity,
|
fragmentation: this.freeIndices.length / this._capacity,
|
||||||
memoryUsage: totalMemory,
|
memoryUsage: totalMemory,
|
||||||
fieldStats: fieldStats
|
fieldStats: fieldStats
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export type TypedArrayTypeName =
|
|||||||
/**
|
/**
|
||||||
* 字段元数据
|
* 字段元数据
|
||||||
*/
|
*/
|
||||||
export type FieldMetadata = {
|
export interface FieldMetadata {
|
||||||
name: string;
|
name: string;
|
||||||
type: 'number' | 'boolean' | 'string' | 'object';
|
type: 'number' | 'boolean' | 'string' | 'object';
|
||||||
arrayType?: TypedArrayTypeName;
|
arrayType?: TypedArrayTypeName;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
export { ComponentPoolManager } from '../ComponentPool';
|
export { ComponentPool, ComponentPoolManager } from '../ComponentPool';
|
||||||
export type { ComponentPool } from '../ComponentPool';
|
|
||||||
export { ComponentStorage, ComponentRegistry, GlobalComponentRegistry } from '../ComponentStorage';
|
export { ComponentStorage, ComponentRegistry, GlobalComponentRegistry } from '../ComponentStorage';
|
||||||
export type { IComponentRegistry } from '../ComponentStorage';
|
export type { IComponentRegistry } from '../ComponentStorage';
|
||||||
export { EnableSoA, Float64, Float32, Int32, SerializeMap, SoAStorage } from '../SoAStorage';
|
export { EnableSoA, Float64, Float32, Int32, SerializeMap, SoAStorage } from '../SoAStorage';
|
||||||
|
|||||||
@@ -17,8 +17,12 @@
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// 从SoAStorage导入所有装饰器和类型
|
||||||
export {
|
export {
|
||||||
|
// 启用装饰器
|
||||||
EnableSoA,
|
EnableSoA,
|
||||||
|
|
||||||
|
// 数值类型装饰器
|
||||||
Float64,
|
Float64,
|
||||||
Float32,
|
Float32,
|
||||||
Int32,
|
Int32,
|
||||||
@@ -28,10 +32,13 @@ export {
|
|||||||
Int8,
|
Int8,
|
||||||
Uint8,
|
Uint8,
|
||||||
Uint8Clamped,
|
Uint8Clamped,
|
||||||
|
|
||||||
|
// 序列化装饰器
|
||||||
SerializeMap,
|
SerializeMap,
|
||||||
SerializeSet,
|
SerializeSet,
|
||||||
SerializeArray,
|
SerializeArray,
|
||||||
DeepCopy
|
DeepCopy,
|
||||||
} from './SoAStorage';
|
|
||||||
|
|
||||||
export type { SupportedTypedArray } from './SoAStorage';
|
// 类型定义
|
||||||
|
SupportedTypedArray
|
||||||
|
} from './SoAStorage';
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ interface GraphNode {
|
|||||||
* 系统依赖信息
|
* 系统依赖信息
|
||||||
* System dependency info
|
* System dependency info
|
||||||
*/
|
*/
|
||||||
export type SystemDependencyInfo = {
|
export interface SystemDependencyInfo {
|
||||||
/** 系统名称 | System name */
|
/** 系统名称 | System name */
|
||||||
name: string;
|
name: string;
|
||||||
/** 在这些系统之前执行 | Execute before these systems */
|
/** 在这些系统之前执行 | Execute before these systems */
|
||||||
|
|||||||
@@ -27,11 +27,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { SystemDependencyGraph, CycleDependencyError, type SystemDependencyInfo } from './SystemDependencyGraph';
|
import { SystemDependencyGraph, CycleDependencyError, type SystemDependencyInfo } from './SystemDependencyGraph';
|
||||||
import { createLogger } from '../../Utils/Logger';
|
|
||||||
import type { EntitySystem } from '../Systems/EntitySystem';
|
import type { EntitySystem } from '../Systems/EntitySystem';
|
||||||
|
|
||||||
const logger = createLogger('SystemScheduler');
|
|
||||||
|
|
||||||
export { CycleDependencyError };
|
export { CycleDependencyError };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -56,7 +53,7 @@ export const DEFAULT_STAGE_ORDER: readonly SystemStage[] = [
|
|||||||
* 系统调度元数据
|
* 系统调度元数据
|
||||||
* System scheduling metadata
|
* System scheduling metadata
|
||||||
*/
|
*/
|
||||||
export type SystemSchedulingMetadata = {
|
export interface SystemSchedulingMetadata {
|
||||||
/** 执行阶段 | Execution stage */
|
/** 执行阶段 | Execution stage */
|
||||||
stage: SystemStage;
|
stage: SystemStage;
|
||||||
/** 在这些系统之前执行 | Execute before these systems */
|
/** 在这些系统之前执行 | Execute before these systems */
|
||||||
@@ -296,7 +293,7 @@ export class SystemScheduler {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
// 其他错误回退到 updateOrder 排序
|
// 其他错误回退到 updateOrder 排序
|
||||||
logger.warn('Topological sort failed, falling back to updateOrder | 拓扑排序失败,回退到 updateOrder 排序', error);
|
console.warn('[SystemScheduler] 拓扑排序失败,回退到 updateOrder 排序', error);
|
||||||
return this.fallbackSort(systems);
|
return this.fallbackSort(systems);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const ENTITY_REF_VALUES = Symbol('EntityRefValues');
|
|||||||
/**
|
/**
|
||||||
* EntityRef元数据
|
* EntityRef元数据
|
||||||
*/
|
*/
|
||||||
export type EntityRefMetadata = {
|
export interface EntityRefMetadata {
|
||||||
properties: Set<string>;
|
properties: Set<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
/**
|
import 'reflect-metadata';
|
||||||
* 属性元数据存储
|
|
||||||
* Property metadata storage
|
|
||||||
*/
|
|
||||||
const metadataStorage = new WeakMap<Function, Record<string, unknown>>();
|
|
||||||
|
|
||||||
export type PropertyType = 'number' | 'integer' | 'string' | 'boolean' | 'color' | 'vector2' | 'vector3' | 'vector4' | 'enum' | 'asset' | 'array' | 'animationClips' | 'collisionLayer' | 'collisionMask' | 'entityRef';
|
export type PropertyType = 'number' | 'integer' | 'string' | 'boolean' | 'color' | 'vector2' | 'vector3' | 'vector4' | 'enum' | 'asset' | 'array' | 'animationClips' | 'collisionLayer' | 'collisionMask';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 属性资源类型
|
* 属性资源类型
|
||||||
@@ -25,7 +21,7 @@ export type EnumOption = string | { label: string; value: any };
|
|||||||
* Action button configuration for property fields
|
* Action button configuration for property fields
|
||||||
* 属性字段的操作按钮配置
|
* 属性字段的操作按钮配置
|
||||||
*/
|
*/
|
||||||
export type PropertyAction = {
|
export interface PropertyAction {
|
||||||
/** Action identifier | 操作标识符 */
|
/** Action identifier | 操作标识符 */
|
||||||
id: string;
|
id: string;
|
||||||
/** Button label | 按钮标签 */
|
/** Button label | 按钮标签 */
|
||||||
@@ -40,7 +36,7 @@ export type PropertyAction = {
|
|||||||
* 控制关系声明
|
* 控制关系声明
|
||||||
* Control relationship declaration
|
* Control relationship declaration
|
||||||
*/
|
*/
|
||||||
export type PropertyControl = {
|
export interface PropertyControl {
|
||||||
/** 被控制的组件名称 | Target component name */
|
/** 被控制的组件名称 | Target component name */
|
||||||
component: string;
|
component: string;
|
||||||
/** 被控制的属性名称 | Target property name */
|
/** 被控制的属性名称 | Target property name */
|
||||||
@@ -56,16 +52,6 @@ interface PropertyOptionsBase {
|
|||||||
label?: string;
|
label?: string;
|
||||||
/** 是否只读 | Read-only flag */
|
/** 是否只读 | Read-only flag */
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
/**
|
|
||||||
* 是否在 Inspector 中隐藏
|
|
||||||
* Whether to hide this property in Inspector
|
|
||||||
*
|
|
||||||
* Hidden properties are still serialized but not shown in the default PropertyInspector.
|
|
||||||
* Useful when a custom Inspector handles the property.
|
|
||||||
* 隐藏的属性仍然会被序列化,但不会在默认的 PropertyInspector 中显示。
|
|
||||||
* 适用于自定义 Inspector 处理该属性的情况。
|
|
||||||
*/
|
|
||||||
hidden?: boolean;
|
|
||||||
/** Action buttons | 操作按钮 */
|
/** Action buttons | 操作按钮 */
|
||||||
actions?: PropertyAction[];
|
actions?: PropertyAction[];
|
||||||
/** 此属性控制的其他组件属性 | Properties this field controls */
|
/** 此属性控制的其他组件属性 | Properties this field controls */
|
||||||
@@ -207,17 +193,6 @@ interface CollisionMaskPropertyOptions extends PropertyOptionsBase {
|
|||||||
type: 'collisionMask';
|
type: 'collisionMask';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 实体引用属性选项
|
|
||||||
* Entity reference property options
|
|
||||||
*
|
|
||||||
* Used for properties that store entity IDs and support drag-and-drop from SceneHierarchy.
|
|
||||||
* 用于存储实体 ID 的属性,支持从场景层级面板拖放。
|
|
||||||
*/
|
|
||||||
interface EntityRefPropertyOptions extends PropertyOptionsBase {
|
|
||||||
type: 'entityRef';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 属性选项联合类型
|
* 属性选项联合类型
|
||||||
* Property options union type
|
* Property options union type
|
||||||
@@ -233,8 +208,7 @@ export type PropertyOptions =
|
|||||||
| ArrayPropertyOptions
|
| ArrayPropertyOptions
|
||||||
| AnimationClipsPropertyOptions
|
| AnimationClipsPropertyOptions
|
||||||
| CollisionLayerPropertyOptions
|
| CollisionLayerPropertyOptions
|
||||||
| CollisionMaskPropertyOptions
|
| CollisionMaskPropertyOptions;
|
||||||
| EntityRefPropertyOptions;
|
|
||||||
|
|
||||||
// 使用 Symbol.for 创建全局 Symbol,确保跨包共享元数据
|
// 使用 Symbol.for 创建全局 Symbol,确保跨包共享元数据
|
||||||
// Use Symbol.for to create a global Symbol to ensure metadata sharing across packages
|
// Use Symbol.for to create a global Symbol to ensure metadata sharing across packages
|
||||||
@@ -257,27 +231,25 @@ export const PROPERTY_METADATA = Symbol.for('@esengine/property:metadata');
|
|||||||
*/
|
*/
|
||||||
export function Property(options: PropertyOptions): PropertyDecorator {
|
export function Property(options: PropertyOptions): PropertyDecorator {
|
||||||
return (target: object, propertyKey: string | symbol) => {
|
return (target: object, propertyKey: string | symbol) => {
|
||||||
const constructor = target.constructor as Function;
|
const constructor = target.constructor;
|
||||||
const existingMetadata = metadataStorage.get(constructor) || {};
|
const existingMetadata = Reflect.getMetadata(PROPERTY_METADATA, constructor) || {};
|
||||||
|
|
||||||
existingMetadata[propertyKey as string] = options;
|
existingMetadata[propertyKey as string] = options;
|
||||||
|
|
||||||
metadataStorage.set(constructor, existingMetadata);
|
Reflect.defineMetadata(PROPERTY_METADATA, existingMetadata, constructor);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取组件类的所有属性元数据
|
* 获取组件类的所有属性元数据
|
||||||
* Get all property metadata for a component class
|
|
||||||
*/
|
*/
|
||||||
export function getPropertyMetadata(target: Function): Record<string, PropertyOptions> | undefined {
|
export function getPropertyMetadata(target: Function): Record<string, PropertyOptions> | undefined {
|
||||||
return metadataStorage.get(target) as Record<string, PropertyOptions> | undefined;
|
return Reflect.getMetadata(PROPERTY_METADATA, target);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查组件类是否有属性元数据
|
* 检查组件类是否有属性元数据
|
||||||
* Check if a component class has property metadata
|
|
||||||
*/
|
*/
|
||||||
export function hasPropertyMetadata(target: Function): boolean {
|
export function hasPropertyMetadata(target: Function): boolean {
|
||||||
return metadataStorage.has(target);
|
return Reflect.hasMetadata(PROPERTY_METADATA, target);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,7 @@ import {
|
|||||||
COMPONENT_TYPE_NAME,
|
COMPONENT_TYPE_NAME,
|
||||||
COMPONENT_DEPENDENCIES,
|
COMPONENT_DEPENDENCIES,
|
||||||
COMPONENT_EDITOR_OPTIONS,
|
COMPONENT_EDITOR_OPTIONS,
|
||||||
getWritableComponentTypeMetadata,
|
type ComponentEditorOptions
|
||||||
type ComponentEditorOptions,
|
|
||||||
type ComponentType
|
|
||||||
} from '../Core/ComponentStorage/ComponentTypeUtils';
|
} from '../Core/ComponentStorage/ComponentTypeUtils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -26,55 +24,11 @@ import {
|
|||||||
*/
|
*/
|
||||||
export const SYSTEM_TYPE_NAME = Symbol('SystemTypeName');
|
export const SYSTEM_TYPE_NAME = Symbol('SystemTypeName');
|
||||||
|
|
||||||
/**
|
|
||||||
* 系统类型元数据接口
|
|
||||||
* System type metadata interface
|
|
||||||
*/
|
|
||||||
export type SystemTypeMetadata = {
|
|
||||||
readonly [SYSTEM_TYPE_NAME]?: string;
|
|
||||||
readonly __systemMetadata__?: SystemMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 可写的系统类型元数据
|
|
||||||
* Writable system type metadata
|
|
||||||
*/
|
|
||||||
interface WritableSystemTypeMetadata {
|
|
||||||
[SYSTEM_TYPE_NAME]?: string;
|
|
||||||
__systemMetadata__?: SystemMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取系统类型元数据
|
|
||||||
* Get system type metadata
|
|
||||||
*/
|
|
||||||
function getSystemTypeMetadata(systemType: SystemConstructor): SystemTypeMetadata {
|
|
||||||
return systemType as unknown as SystemTypeMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取可写的系统类型元数据
|
|
||||||
* Get writable system type metadata
|
|
||||||
*/
|
|
||||||
function getWritableSystemTypeMetadata(systemType: SystemConstructor): WritableSystemTypeMetadata {
|
|
||||||
return systemType as unknown as WritableSystemTypeMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 系统构造函数类型
|
|
||||||
* System constructor type
|
|
||||||
*
|
|
||||||
* 注意:构造函数参数使用 any[] 是必要的,因为系统可以有各种不同签名的构造函数
|
|
||||||
* Note: Constructor args use any[] because systems can have various constructor signatures
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
type SystemConstructor<T extends EntitySystem = EntitySystem> = new (...args: any[]) => T;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 组件装饰器配置选项
|
* 组件装饰器配置选项
|
||||||
* Component decorator options
|
* Component decorator options
|
||||||
*/
|
*/
|
||||||
export type ComponentOptions = {
|
export interface ComponentOptions {
|
||||||
/** 依赖的其他组件名称列表 | List of required component names */
|
/** 依赖的其他组件名称列表 | List of required component names */
|
||||||
requires?: string[];
|
requires?: string[];
|
||||||
|
|
||||||
@@ -113,29 +67,25 @@ export type ComponentOptions = {
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export function ECSComponent(typeName: string, options?: ComponentOptions) {
|
export function ECSComponent(typeName: string, options?: ComponentOptions) {
|
||||||
return function <T extends ComponentType<Component>>(target: T): T {
|
return function <T extends new (...args: any[]) => Component>(target: T): T {
|
||||||
if (!typeName || typeof typeName !== 'string') {
|
if (!typeName || typeof typeName !== 'string') {
|
||||||
throw new Error('ECSComponent装饰器必须提供有效的类型名称');
|
throw new Error('ECSComponent装饰器必须提供有效的类型名称');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取可写的元数据对象
|
|
||||||
// Get writable metadata object
|
|
||||||
const metadata = getWritableComponentTypeMetadata(target);
|
|
||||||
|
|
||||||
// 在构造函数上存储类型名称
|
// 在构造函数上存储类型名称
|
||||||
// Store type name on constructor
|
// Store type name on constructor
|
||||||
metadata[COMPONENT_TYPE_NAME] = typeName;
|
(target as any)[COMPONENT_TYPE_NAME] = typeName;
|
||||||
|
|
||||||
// 存储依赖关系
|
// 存储依赖关系
|
||||||
// Store dependencies
|
// Store dependencies
|
||||||
if (options?.requires) {
|
if (options?.requires) {
|
||||||
metadata[COMPONENT_DEPENDENCIES] = options.requires;
|
(target as any)[COMPONENT_DEPENDENCIES] = options.requires;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 存储编辑器选项
|
// 存储编辑器选项
|
||||||
// Store editor options
|
// Store editor options
|
||||||
if (options?.editor) {
|
if (options?.editor) {
|
||||||
metadata[COMPONENT_EDITOR_OPTIONS] = options.editor;
|
(target as any)[COMPONENT_EDITOR_OPTIONS] = options.editor;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自动注册到全局 ComponentRegistry,使组件可以通过名称查找
|
// 自动注册到全局 ComponentRegistry,使组件可以通过名称查找
|
||||||
@@ -150,7 +100,7 @@ export function ECSComponent(typeName: string, options?: ComponentOptions) {
|
|||||||
* System 元数据配置
|
* System 元数据配置
|
||||||
* System metadata configuration
|
* System metadata configuration
|
||||||
*/
|
*/
|
||||||
export type SystemMetadata = {
|
export interface SystemMetadata {
|
||||||
/**
|
/**
|
||||||
* 更新顺序(数值越小越先执行,默认0)
|
* 更新顺序(数值越小越先执行,默认0)
|
||||||
* Update order (lower values execute first, default 0)
|
* Update order (lower values execute first, default 0)
|
||||||
@@ -162,21 +112,6 @@ export type SystemMetadata = {
|
|||||||
* Whether enabled by default (default true)
|
* Whether enabled by default (default true)
|
||||||
*/
|
*/
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否在编辑模式下运行(默认 true)
|
|
||||||
* Whether to run in edit mode (default true)
|
|
||||||
*
|
|
||||||
* 默认情况下,所有系统在编辑模式下都会运行。
|
|
||||||
* 当设置为 false 时,此系统在编辑模式(非 Play 状态)下不会执行。
|
|
||||||
* 适用于物理系统、AI 系统等只应在游戏运行时执行的系统。
|
|
||||||
*
|
|
||||||
* By default, all systems run in edit mode.
|
|
||||||
* When set to false, this system will NOT execute during edit mode
|
|
||||||
* (when not playing). Useful for physics, AI, and other systems
|
|
||||||
* that should only run during gameplay.
|
|
||||||
*/
|
|
||||||
runInEditMode?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -204,23 +139,19 @@ export type SystemMetadata = {
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export function ECSSystem(typeName: string, metadata?: SystemMetadata) {
|
export function ECSSystem(typeName: string, metadata?: SystemMetadata) {
|
||||||
return function <T extends SystemConstructor>(target: T): T {
|
return function <T extends new (...args: any[]) => EntitySystem>(target: T): T {
|
||||||
if (!typeName || typeof typeName !== 'string') {
|
if (!typeName || typeof typeName !== 'string') {
|
||||||
throw new Error('ECSSystem装饰器必须提供有效的类型名称');
|
throw new Error('ECSSystem装饰器必须提供有效的类型名称');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取可写的元数据对象
|
|
||||||
// Get writable metadata object
|
|
||||||
const meta = getWritableSystemTypeMetadata(target);
|
|
||||||
|
|
||||||
// 在构造函数上存储类型名称
|
// 在构造函数上存储类型名称
|
||||||
// Store type name on constructor
|
// Store type name on constructor
|
||||||
meta[SYSTEM_TYPE_NAME] = typeName;
|
(target as any)[SYSTEM_TYPE_NAME] = typeName;
|
||||||
|
|
||||||
// 存储元数据
|
// 存储元数据
|
||||||
// Store metadata
|
// Store metadata
|
||||||
if (metadata) {
|
if (metadata) {
|
||||||
meta.__systemMetadata__ = metadata;
|
(target as any).__systemMetadata__ = metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
return target;
|
return target;
|
||||||
@@ -231,20 +162,8 @@ export function ECSSystem(typeName: string, metadata?: SystemMetadata) {
|
|||||||
* 获取 System 的元数据
|
* 获取 System 的元数据
|
||||||
* Get System metadata
|
* Get System metadata
|
||||||
*/
|
*/
|
||||||
export function getSystemMetadata(systemType: SystemConstructor): SystemMetadata | undefined {
|
export function getSystemMetadata(systemType: new (...args: any[]) => EntitySystem): SystemMetadata | undefined {
|
||||||
const meta = getSystemTypeMetadata(systemType);
|
return (systemType as any).__systemMetadata__;
|
||||||
return meta.__systemMetadata__;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从系统实例获取元数据
|
|
||||||
* Get metadata from system instance
|
|
||||||
*
|
|
||||||
* @param system 系统实例 | System instance
|
|
||||||
* @returns 系统元数据 | System metadata
|
|
||||||
*/
|
|
||||||
export function getSystemInstanceMetadata(system: EntitySystem): SystemMetadata | undefined {
|
|
||||||
return getSystemMetadata(system.constructor as SystemConstructor);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -255,10 +174,9 @@ export function getSystemInstanceMetadata(system: EntitySystem): SystemMetadata
|
|||||||
* @returns 系统类型名称 | System type name
|
* @returns 系统类型名称 | System type name
|
||||||
*/
|
*/
|
||||||
export function getSystemTypeName<T extends EntitySystem>(
|
export function getSystemTypeName<T extends EntitySystem>(
|
||||||
systemType: SystemConstructor<T>
|
systemType: new (...args: any[]) => T
|
||||||
): string {
|
): string {
|
||||||
const meta = getSystemTypeMetadata(systemType);
|
const decoratorName = (systemType as any)[SYSTEM_TYPE_NAME];
|
||||||
const decoratorName = meta[SYSTEM_TYPE_NAME];
|
|
||||||
if (decoratorName) {
|
if (decoratorName) {
|
||||||
return decoratorName;
|
return decoratorName;
|
||||||
}
|
}
|
||||||
@@ -273,5 +191,5 @@ export function getSystemTypeName<T extends EntitySystem>(
|
|||||||
* @returns 系统类型名称 | System type name
|
* @returns 系统类型名称 | System type name
|
||||||
*/
|
*/
|
||||||
export function getSystemInstanceTypeName(system: EntitySystem): string {
|
export function getSystemInstanceTypeName(system: EntitySystem): string {
|
||||||
return getSystemTypeName(system.constructor as SystemConstructor);
|
return getSystemTypeName(system.constructor as new (...args: any[]) => EntitySystem);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ export {
|
|||||||
getSystemTypeName,
|
getSystemTypeName,
|
||||||
getSystemInstanceTypeName,
|
getSystemInstanceTypeName,
|
||||||
getSystemMetadata,
|
getSystemMetadata,
|
||||||
getSystemInstanceMetadata,
|
|
||||||
SYSTEM_TYPE_NAME
|
SYSTEM_TYPE_NAME
|
||||||
} from './TypeDecorators';
|
} from './TypeDecorators';
|
||||||
|
|
||||||
|
|||||||
+218
-180
@@ -9,156 +9,157 @@ import type { IScene } from './IScene';
|
|||||||
import { EntityHandle, NULL_HANDLE } from './Core/EntityHandle';
|
import { EntityHandle, NULL_HANDLE } from './Core/EntityHandle';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 组件活跃状态变化接口
|
* 组件活跃状态变化接口
|
||||||
* @en Interface for component active state change
|
|
||||||
*/
|
*/
|
||||||
interface IActiveChangeable {
|
interface IActiveChangeable {
|
||||||
onActiveChanged(): void;
|
onActiveChanged(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 比较两个实体的优先级
|
* 实体比较器
|
||||||
* @en Compare priority of two entities
|
|
||||||
*
|
*
|
||||||
* @param a - @zh 第一个实体 @en First entity
|
* 用于比较两个实体的优先级,首先按更新顺序比较,然后按ID比较。
|
||||||
* @param b - @zh 第二个实体 @en Second entity
|
|
||||||
* @returns @zh 比较结果:负数表示a优先级更高,正数表示b优先级更高,0表示相等
|
|
||||||
* @en Comparison result: negative means a has higher priority, positive means b has higher priority, 0 means equal
|
|
||||||
*/
|
*/
|
||||||
export function compareEntities(a: Entity, b: Entity): number {
|
export class EntityComparer {
|
||||||
return a.updateOrder - b.updateOrder || a.id - b.id;
|
/**
|
||||||
|
* 比较两个实体
|
||||||
|
*
|
||||||
|
* @param self - 第一个实体
|
||||||
|
* @param other - 第二个实体
|
||||||
|
* @returns 比较结果,负数表示self优先级更高,正数表示other优先级更高,0表示相等
|
||||||
|
*/
|
||||||
|
public compare(self: Entity, other: Entity): number {
|
||||||
|
let compare = self.updateOrder - other.updateOrder;
|
||||||
|
if (compare == 0) compare = self.id - other.id;
|
||||||
|
return compare;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 游戏实体类
|
* 游戏实体类
|
||||||
* @en Game entity class
|
|
||||||
*
|
*
|
||||||
* @zh ECS架构中的实体(Entity),作为组件的容器。
|
* ECS架构中的实体(Entity),作为组件的容器。
|
||||||
* 实体本身不包含游戏逻辑,所有功能都通过组件来实现。
|
* 实体本身不包含游戏逻辑,所有功能都通过组件来实现。
|
||||||
|
*
|
||||||
* 层级关系通过 HierarchyComponent 和 HierarchySystem 管理,
|
* 层级关系通过 HierarchyComponent 和 HierarchySystem 管理,
|
||||||
* 而非 Entity 内置属性,符合 ECS 组合原则。
|
* 而非 Entity 内置属性,符合 ECS 组合原则。
|
||||||
* @en Entity in ECS architecture, serves as a container for components.
|
|
||||||
* Entity itself contains no game logic, all functionality is implemented through components.
|
|
||||||
* Hierarchy relationships are managed by HierarchyComponent and HierarchySystem,
|
|
||||||
* not built-in Entity properties, following ECS composition principles.
|
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
* // @zh 创建实体 | @en Create entity
|
* // 创建实体
|
||||||
* const entity = scene.createEntity("Player");
|
* const entity = scene.createEntity("Player");
|
||||||
*
|
*
|
||||||
* // @zh 添加组件 | @en Add component
|
* // 添加组件
|
||||||
* const healthComponent = entity.addComponent(new HealthComponent(100));
|
* const healthComponent = entity.addComponent(new HealthComponent(100));
|
||||||
*
|
*
|
||||||
* // @zh 获取组件 | @en Get component
|
* // 获取组件
|
||||||
* const health = entity.getComponent(HealthComponent);
|
* const health = entity.getComponent(HealthComponent);
|
||||||
*
|
*
|
||||||
* // @zh 层级关系使用 HierarchySystem | @en Use HierarchySystem for hierarchy
|
* // 层级关系使用 HierarchySystem
|
||||||
* const hierarchySystem = scene.getSystem(HierarchySystem);
|
* const hierarchySystem = scene.getSystem(HierarchySystem);
|
||||||
* hierarchySystem.setParent(childEntity, parentEntity);
|
* hierarchySystem.setParent(childEntity, parentEntity);
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export class Entity {
|
export class Entity {
|
||||||
/**
|
/**
|
||||||
* @zh Entity专用日志器
|
* Entity专用日志器
|
||||||
* @en Entity logger
|
|
||||||
*/
|
*/
|
||||||
private static _logger = createLogger('Entity');
|
private static _logger = createLogger('Entity');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 实体名称
|
* 实体比较器实例
|
||||||
* @en Entity name
|
*/
|
||||||
|
public static entityComparer: EntityComparer = new EntityComparer();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 实体名称
|
||||||
*/
|
*/
|
||||||
public name: string;
|
public name: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 实体唯一标识符(运行时ID),用于快速查找
|
* 实体唯一标识符(运行时 ID)
|
||||||
* @en Unique entity identifier (runtime ID) for fast lookups
|
*
|
||||||
|
* Runtime identifier for fast lookups.
|
||||||
*/
|
*/
|
||||||
public readonly id: number;
|
public readonly id: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 持久化唯一标识符(GUID)
|
* 持久化唯一标识符(GUID)
|
||||||
* @en Persistent unique identifier (GUID)
|
|
||||||
*
|
*
|
||||||
* @zh 用于序列化/反序列化时保持实体引用一致性,在场景保存和加载时保持不变
|
* 用于序列化/反序列化时保持实体引用一致性。
|
||||||
* @en Used to maintain entity reference consistency during serialization/deserialization, remains stable across save/load cycles
|
* 在场景保存和加载时保持不变。
|
||||||
|
*
|
||||||
|
* Persistent identifier for serialization.
|
||||||
|
* Remains stable across save/load cycles.
|
||||||
*/
|
*/
|
||||||
public readonly persistentId: string;
|
public readonly persistentId: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 轻量级实体句柄
|
* 轻量级实体句柄
|
||||||
* @en Lightweight entity handle
|
|
||||||
*
|
*
|
||||||
* @zh 数值类型的实体标识符,包含索引和代数信息。
|
* 数值类型的实体标识符,包含索引和代数信息。
|
||||||
* 用于高性能场景下替代对象引用,支持 Archetype 存储等优化。
|
* 用于高性能场景下替代对象引用,支持 Archetype 存储等优化。
|
||||||
* @en Numeric identifier containing index and generation.
|
*
|
||||||
|
* Lightweight entity handle.
|
||||||
|
* Numeric identifier containing index and generation.
|
||||||
* Used for high-performance scenarios instead of object references,
|
* Used for high-performance scenarios instead of object references,
|
||||||
* supports Archetype storage optimizations.
|
* supports Archetype storage optimizations.
|
||||||
*/
|
*/
|
||||||
private _handle: EntityHandle = NULL_HANDLE;
|
private _handle: EntityHandle = NULL_HANDLE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 所属场景引用
|
* 所属场景引用
|
||||||
* @en Reference to the owning scene
|
|
||||||
*/
|
*/
|
||||||
public scene: IScene | null = null;
|
public scene: IScene | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 销毁状态标志
|
* 销毁状态标志
|
||||||
* @en Destroyed state flag
|
|
||||||
*/
|
*/
|
||||||
private _isDestroyed: boolean = false;
|
private _isDestroyed: boolean = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 激活状态
|
* 激活状态
|
||||||
* @en Active state
|
|
||||||
*/
|
*/
|
||||||
private _active: boolean = true;
|
private _active: boolean = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 实体标签,用于分类和查询
|
* 实体标签
|
||||||
* @en Entity tag for categorization and querying
|
|
||||||
*/
|
*/
|
||||||
private _tag: number = 0;
|
private _tag: number = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 启用状态
|
* 启用状态
|
||||||
* @en Enabled state
|
|
||||||
*/
|
*/
|
||||||
private _enabled: boolean = true;
|
private _enabled: boolean = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 更新顺序
|
* 更新顺序
|
||||||
* @en Update order
|
|
||||||
*/
|
*/
|
||||||
private _updateOrder: number = 0;
|
private _updateOrder: number = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 组件位掩码,用于快速 hasComponent 检查
|
* 组件位掩码(用于快速 hasComponent 检查)
|
||||||
* @en Component bitmask for fast hasComponent checks
|
|
||||||
*/
|
*/
|
||||||
private _componentMask: BitMask64Data = BitMask64Utils.clone(BitMask64Utils.ZERO);
|
private _componentMask: BitMask64Data = BitMask64Utils.clone(BitMask64Utils.ZERO);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 懒加载的组件数组缓存
|
* 懒加载的组件数组缓存
|
||||||
* @en Lazy-loaded component array cache
|
|
||||||
*/
|
*/
|
||||||
private _componentCache: Component[] | null = null;
|
private _componentCache: Component[] | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 生命周期策略,控制实体在场景切换时的行为
|
* 生命周期策略
|
||||||
* @en Lifecycle policy controlling entity behavior during scene transitions
|
*
|
||||||
|
* Lifecycle policy for scene transitions.
|
||||||
*/
|
*/
|
||||||
private _lifecyclePolicy: EEntityLifecyclePolicy = EEntityLifecyclePolicy.SceneLocal;
|
private _lifecyclePolicy: EEntityLifecyclePolicy = EEntityLifecyclePolicy.SceneLocal;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 构造函数
|
* 构造函数
|
||||||
* @en Constructor
|
|
||||||
*
|
*
|
||||||
* @param name - @zh 实体名称 @en Entity name
|
* @param name - 实体名称
|
||||||
* @param id - @zh 实体唯一标识符(运行时ID)@en Unique entity identifier (runtime ID)
|
* @param id - 实体唯一标识符(运行时 ID)
|
||||||
* @param persistentId - @zh 持久化标识符(可选,用于反序列化时恢复)@en Persistent identifier (optional, for deserialization)
|
* @param persistentId - 持久化标识符(可选,用于反序列化时恢复)
|
||||||
*/
|
*/
|
||||||
constructor(name: string, id: number, persistentId?: string) {
|
constructor(name: string, id: number, persistentId?: string) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
@@ -167,38 +168,44 @@ export class Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 获取生命周期策略
|
* 获取生命周期策略
|
||||||
* @en Get lifecycle policy
|
*
|
||||||
|
* Get lifecycle policy.
|
||||||
*/
|
*/
|
||||||
public get lifecyclePolicy(): EEntityLifecyclePolicy {
|
public get lifecyclePolicy(): EEntityLifecyclePolicy {
|
||||||
return this._lifecyclePolicy;
|
return this._lifecyclePolicy;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 检查实体是否为持久化实体(跨场景保留)
|
* 检查实体是否为持久化实体
|
||||||
* @en Check if entity is persistent (survives scene transitions)
|
*
|
||||||
|
* Check if entity is persistent (survives scene transitions).
|
||||||
*/
|
*/
|
||||||
public get isPersistent(): boolean {
|
public get isPersistent(): boolean {
|
||||||
return this._lifecyclePolicy === EEntityLifecyclePolicy.Persistent;
|
return this._lifecyclePolicy === EEntityLifecyclePolicy.Persistent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 获取实体句柄
|
* 获取实体句柄
|
||||||
* @en Get entity handle
|
|
||||||
*
|
*
|
||||||
* @zh 返回轻量级数值句柄,用于高性能场景。如果实体尚未分配句柄,返回 NULL_HANDLE。
|
* 返回轻量级数值句柄,用于高性能场景。
|
||||||
* @en Returns lightweight numeric handle for high-performance scenarios. Returns NULL_HANDLE if entity has no handle assigned.
|
* 如果实体尚未分配句柄,返回 NULL_HANDLE。
|
||||||
|
*
|
||||||
|
* Get entity handle.
|
||||||
|
* Returns lightweight numeric handle for high-performance scenarios.
|
||||||
|
* Returns NULL_HANDLE if entity has no handle assigned.
|
||||||
*/
|
*/
|
||||||
public get handle(): EntityHandle {
|
public get handle(): EntityHandle {
|
||||||
return this._handle;
|
return this._handle;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 设置实体句柄(内部使用)
|
* 设置实体句柄(内部使用)
|
||||||
* @en Set entity handle (internal use)
|
|
||||||
*
|
*
|
||||||
* @zh 此方法供 Scene 在创建实体时调用
|
* 此方法供 Scene 在创建实体时调用。
|
||||||
* @en Called by Scene when creating entities
|
*
|
||||||
|
* Set entity handle (internal use).
|
||||||
|
* Called by Scene when creating entities.
|
||||||
*
|
*
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
@@ -207,13 +214,14 @@ export class Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 设置实体为持久化(跨场景保留)
|
* 设置实体为持久化(跨场景保留)
|
||||||
* @en Mark entity as persistent (survives scene transitions)
|
|
||||||
*
|
*
|
||||||
* @zh 标记后的实体在场景切换时不会被销毁,会自动迁移到新场景
|
* 标记后的实体在场景切换时不会被销毁,会自动迁移到新场景。
|
||||||
* @en Persistent entities are automatically migrated to the new scene
|
|
||||||
*
|
*
|
||||||
* @returns @zh this,支持链式调用 @en Returns this for chaining
|
* Mark entity as persistent (survives scene transitions).
|
||||||
|
* Persistent entities are automatically migrated to the new scene.
|
||||||
|
*
|
||||||
|
* @returns this,支持链式调用 | Returns this for chaining
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
@@ -228,10 +236,14 @@ export class Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 设置实体为场景本地(随场景销毁),恢复默认行为
|
* 设置实体为场景本地(随场景销毁)
|
||||||
* @en Mark entity as scene-local (destroyed with scene), restores default behavior
|
|
||||||
*
|
*
|
||||||
* @returns @zh this,支持链式调用 @en Returns this for chaining
|
* 将实体恢复为默认行为。
|
||||||
|
*
|
||||||
|
* Mark entity as scene-local (destroyed with scene).
|
||||||
|
* Restores default behavior.
|
||||||
|
*
|
||||||
|
* @returns this,支持链式调用 | Returns this for chaining
|
||||||
*/
|
*/
|
||||||
public setSceneLocal(): this {
|
public setSceneLocal(): this {
|
||||||
this._lifecyclePolicy = EEntityLifecyclePolicy.SceneLocal;
|
this._lifecyclePolicy = EEntityLifecyclePolicy.SceneLocal;
|
||||||
@@ -239,21 +251,18 @@ export class Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 获取销毁状态
|
* 获取销毁状态
|
||||||
* @en Get destroyed state
|
* @returns 如果实体已被销毁则返回true
|
||||||
*
|
|
||||||
* @returns @zh 如果实体已被销毁则返回true @en Returns true if entity has been destroyed
|
|
||||||
*/
|
*/
|
||||||
public get isDestroyed(): boolean {
|
public get isDestroyed(): boolean {
|
||||||
return this._isDestroyed;
|
return this._isDestroyed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 设置销毁状态(内部使用)
|
* 设置销毁状态(内部使用)
|
||||||
* @en Set destroyed state (internal use)
|
|
||||||
*
|
*
|
||||||
* @zh 此方法供Scene和批量操作使用,以提高性能。不应在普通业务逻辑中调用,应使用destroy()方法
|
* 此方法供Scene和批量操作使用,以提高性能。
|
||||||
* @en Used by Scene and batch operations for performance. Should not be called in normal business logic, use destroy() instead
|
* 不应在普通业务逻辑中调用,应使用destroy()方法。
|
||||||
*
|
*
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
@@ -262,10 +271,8 @@ export class Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 获取组件数组(懒加载)
|
* 获取组件数组(懒加载)
|
||||||
* @en Get component array (lazy-loaded)
|
* @returns 只读的组件数组
|
||||||
*
|
|
||||||
* @returns @zh 只读的组件数组 @en Readonly component array
|
|
||||||
*/
|
*/
|
||||||
public get components(): readonly Component[] {
|
public get components(): readonly Component[] {
|
||||||
if (this._componentCache === null) {
|
if (this._componentCache === null) {
|
||||||
@@ -275,8 +282,7 @@ export class Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 从存储重建组件缓存
|
* 从存储重建组件缓存
|
||||||
* @en Rebuild component cache from storage
|
|
||||||
*/
|
*/
|
||||||
private _rebuildComponentCache(): void {
|
private _rebuildComponentCache(): void {
|
||||||
const components: Component[] = [];
|
const components: Component[] = [];
|
||||||
@@ -328,82 +334,74 @@ export class Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 获取实体标签
|
* 获取实体标签
|
||||||
* @en Get entity tag
|
|
||||||
*
|
*
|
||||||
* @returns @zh 实体的数字标签 @en Entity's numeric tag
|
* @returns 实体的数字标签
|
||||||
*/
|
*/
|
||||||
public get tag(): number {
|
public get tag(): number {
|
||||||
return this._tag;
|
return this._tag;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 设置实体标签
|
* 设置实体标签
|
||||||
* @en Set entity tag
|
|
||||||
*
|
*
|
||||||
* @param value - @zh 新的标签值 @en New tag value
|
* @param value - 新的标签值
|
||||||
*/
|
*/
|
||||||
public set tag(value: number) {
|
public set tag(value: number) {
|
||||||
this._tag = value;
|
this._tag = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 获取启用状态
|
* 获取启用状态
|
||||||
* @en Get enabled state
|
|
||||||
*
|
*
|
||||||
* @returns @zh 如果实体已启用则返回true @en Returns true if entity is enabled
|
* @returns 如果实体已启用则返回true
|
||||||
*/
|
*/
|
||||||
public get enabled(): boolean {
|
public get enabled(): boolean {
|
||||||
return this._enabled;
|
return this._enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 设置启用状态
|
* 设置启用状态
|
||||||
* @en Set enabled state
|
|
||||||
*
|
*
|
||||||
* @param value - @zh 新的启用状态 @en New enabled state
|
* @param value - 新的启用状态
|
||||||
*/
|
*/
|
||||||
public set enabled(value: boolean) {
|
public set enabled(value: boolean) {
|
||||||
this._enabled = value;
|
this._enabled = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 获取更新顺序
|
* 获取更新顺序
|
||||||
* @en Get update order
|
|
||||||
*
|
*
|
||||||
* @returns @zh 实体的更新顺序值 @en Entity's update order value
|
* @returns 实体的更新顺序值
|
||||||
*/
|
*/
|
||||||
public get updateOrder(): number {
|
public get updateOrder(): number {
|
||||||
return this._updateOrder;
|
return this._updateOrder;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 设置更新顺序
|
* 设置更新顺序
|
||||||
* @en Set update order
|
|
||||||
*
|
*
|
||||||
* @param value - @zh 新的更新顺序值 @en New update order value
|
* @param value - 新的更新顺序值
|
||||||
*/
|
*/
|
||||||
public set updateOrder(value: number) {
|
public set updateOrder(value: number) {
|
||||||
this._updateOrder = value;
|
this._updateOrder = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 获取组件位掩码
|
* 获取组件位掩码
|
||||||
* @en Get component bitmask
|
|
||||||
*
|
*
|
||||||
* @returns @zh 实体的组件位掩码 @en Entity's component bitmask
|
* @returns 实体的组件位掩码
|
||||||
*/
|
*/
|
||||||
public get componentMask(): BitMask64Data {
|
public get componentMask(): BitMask64Data {
|
||||||
return this._componentMask;
|
return this._componentMask;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 创建并添加组件
|
* 创建并添加组件
|
||||||
* @en Create and add component
|
|
||||||
*
|
*
|
||||||
* @param componentType - @zh 组件类型构造函数 @en Component type constructor
|
* @param componentType - 组件类型构造函数
|
||||||
* @param args - @zh 组件构造函数参数 @en Component constructor arguments
|
* @param args - 组件构造函数参数
|
||||||
* @returns @zh 创建的组件实例 @en Created component instance
|
* @returns 创建的组件实例
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
@@ -420,30 +418,51 @@ export class Entity {
|
|||||||
return this.addComponent(component);
|
return this.addComponent(component);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内部添加组件方法(不进行重复检查,用于初始化)
|
||||||
|
*
|
||||||
|
* @param component - 要添加的组件实例
|
||||||
|
* @returns 添加的组件实例
|
||||||
|
*/
|
||||||
private addComponentInternal<T extends Component>(component: T): T {
|
private addComponentInternal<T extends Component>(component: T): T {
|
||||||
const componentType = component.constructor as ComponentType<T>;
|
const componentType = component.constructor as ComponentType<T>;
|
||||||
|
|
||||||
|
// 更新位掩码(组件已通过 @ECSComponent 装饰器自动注册)
|
||||||
|
// Update bitmask (component already registered via @ECSComponent decorator)
|
||||||
const registry = this.scene?.componentRegistry ?? GlobalComponentRegistry;
|
const registry = this.scene?.componentRegistry ?? GlobalComponentRegistry;
|
||||||
const componentMask = registry.getBitMask(componentType);
|
const componentMask = registry.getBitMask(componentType);
|
||||||
BitMask64Utils.orInPlace(this._componentMask, componentMask);
|
BitMask64Utils.orInPlace(this._componentMask, componentMask);
|
||||||
|
|
||||||
|
// 使缓存失效
|
||||||
this._componentCache = null;
|
this._componentCache = null;
|
||||||
|
|
||||||
return component;
|
return component;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知Scene中的QuerySystem实体组件发生变动
|
||||||
|
*
|
||||||
|
* Notify the QuerySystem in Scene that entity components have changed
|
||||||
|
*
|
||||||
|
* @param changedComponentType 变化的组件类型(可选,用于优化通知) | Changed component type (optional, for optimized notification)
|
||||||
|
*/
|
||||||
private notifyQuerySystems(changedComponentType?: ComponentType): void {
|
private notifyQuerySystems(changedComponentType?: ComponentType): void {
|
||||||
if (!this.scene?.querySystem) return;
|
if (this.scene && this.scene.querySystem) {
|
||||||
|
this.scene.querySystem.updateEntity(this);
|
||||||
this.scene.querySystem.updateEntity(this);
|
this.scene.clearSystemEntityCaches();
|
||||||
this.scene.clearSystemEntityCaches();
|
// 事件驱动:立即通知关心该组件的系统 | Event-driven: notify systems that care about this component
|
||||||
this.scene.notifyEntityComponentChanged?.(this, changedComponentType);
|
if (this.scene.notifyEntityComponentChanged) {
|
||||||
|
this.scene.notifyEntityComponentChanged(this, changedComponentType);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 添加组件到实体
|
* 添加组件到实体
|
||||||
* @en Add component to entity
|
|
||||||
*
|
*
|
||||||
* @param component - @zh 要添加的组件实例 @en Component instance to add
|
* @param component - 要添加的组件实例
|
||||||
* @returns @zh 添加的组件实例 @en Added component instance
|
* @returns 添加的组件实例
|
||||||
* @throws @zh 如果实体已存在该类型的组件 @en If entity already has this component type
|
* @throws {Error} 如果实体已存在该类型的组件
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
@@ -473,15 +492,20 @@ export class Entity {
|
|||||||
this.scene.componentStorageManager.addComponent(this.id, component);
|
this.scene.componentStorageManager.addComponent(this.id, component);
|
||||||
|
|
||||||
component.entityId = this.id;
|
component.entityId = this.id;
|
||||||
this.scene.referenceTracker?.registerEntityScene(this.id, this.scene);
|
if (this.scene.referenceTracker) {
|
||||||
|
this.scene.referenceTracker.registerEntityScene(this.id, this.scene);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑器模式下延迟执行 onAddedToEntity | Defer onAddedToEntity in editor mode
|
||||||
if (this.scene.isEditorMode) {
|
if (this.scene.isEditorMode) {
|
||||||
this.scene.queueDeferredComponentCallback(() => component.onAddedToEntity());
|
this.scene.queueDeferredComponentCallback(() => {
|
||||||
|
component.onAddedToEntity();
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
component.onAddedToEntity();
|
component.onAddedToEntity();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.scene.eventSystem) {
|
if (this.scene && this.scene.eventSystem) {
|
||||||
this.scene.eventSystem.emitSync('component:added', {
|
this.scene.eventSystem.emitSync('component:added', {
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
source: 'Entity',
|
source: 'Entity',
|
||||||
@@ -499,11 +523,10 @@ export class Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 获取指定类型的组件
|
* 获取指定类型的组件
|
||||||
* @en Get component of specified type
|
|
||||||
*
|
*
|
||||||
* @param type - @zh 组件类型构造函数 @en Component type constructor
|
* @param type - 组件类型构造函数
|
||||||
* @returns @zh 组件实例,如果不存在则返回null @en Component instance, or null if not found
|
* @returns 组件实例,如果不存在则返回null
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
@@ -530,11 +553,10 @@ export class Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 检查实体是否拥有指定类型的组件
|
* 检查实体是否拥有指定类型的组件
|
||||||
* @en Check if entity has component of specified type
|
|
||||||
*
|
*
|
||||||
* @param type - @zh 组件类型构造函数 @en Component type constructor
|
* @param type - 组件类型构造函数
|
||||||
* @returns @zh 如果实体拥有该组件返回true @en Returns true if entity has the component
|
* @returns 如果实体拥有该组件返回true,否则返回false
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
@@ -555,19 +577,17 @@ export class Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 获取或创建指定类型的组件
|
* 获取或创建指定类型的组件
|
||||||
* @en Get or create component of specified type
|
|
||||||
*
|
*
|
||||||
* @zh 如果组件已存在则返回现有组件,否则创建新组件并添加到实体
|
* 如果组件已存在则返回现有组件,否则创建新组件并添加到实体
|
||||||
* @en Returns existing component if present, otherwise creates and adds new component
|
|
||||||
*
|
*
|
||||||
* @param type - @zh 组件类型构造函数 @en Component type constructor
|
* @param type - 组件类型构造函数
|
||||||
* @param args - @zh 组件构造函数参数(仅在创建新组件时使用)@en Constructor arguments (only used when creating new component)
|
* @param args - 组件构造函数参数(仅在创建新组件时使用)
|
||||||
* @returns @zh 组件实例 @en Component instance
|
* @returns 组件实例
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
* // @zh 确保实体拥有Position组件 | @en Ensure entity has Position component
|
* // 确保实体拥有Position组件
|
||||||
* const position = entity.getOrCreateComponent(Position, 0, 0);
|
* const position = entity.getOrCreateComponent(Position, 0, 0);
|
||||||
* position.x = 100;
|
* position.x = 100;
|
||||||
* ```
|
* ```
|
||||||
@@ -585,13 +605,16 @@ export class Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 标记组件为已修改
|
* 标记组件为已修改
|
||||||
* @en Mark component(s) as modified
|
|
||||||
*
|
*
|
||||||
* @zh 便捷方法,自动从场景获取当前 epoch 并标记组件。用于帧级变更检测系统。
|
* 便捷方法,自动从场景获取当前 epoch 并标记组件。
|
||||||
* @en Convenience method that auto-gets epoch from scene and marks components. Used for frame-level change detection system.
|
* 用于帧级变更检测系统。
|
||||||
*
|
*
|
||||||
* @param components - @zh 要标记的组件 @en Components to mark
|
* Mark component(s) as modified.
|
||||||
|
* Convenience method that auto-gets epoch from scene and marks components.
|
||||||
|
* Used for frame-level change detection system.
|
||||||
|
*
|
||||||
|
* @param components 要标记的组件 | Components to mark
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
@@ -599,7 +622,7 @@ export class Entity {
|
|||||||
* pos.x = 100;
|
* pos.x = 100;
|
||||||
* entity.markDirty(pos);
|
* entity.markDirty(pos);
|
||||||
*
|
*
|
||||||
* // @zh 或者标记多个组件 | @en Or mark multiple components
|
* // 或者标记多个组件
|
||||||
* entity.markDirty(pos, vel);
|
* entity.markDirty(pos, vel);
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
@@ -615,10 +638,9 @@ export class Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 移除指定的组件
|
* 移除指定的组件
|
||||||
* @en Remove specified component
|
|
||||||
*
|
*
|
||||||
* @param component - @zh 要移除的组件实例 @en Component instance to remove
|
* @param component - 要移除的组件实例
|
||||||
*/
|
*/
|
||||||
public removeComponent(component: Component): void {
|
public removeComponent(component: Component): void {
|
||||||
const componentType = component.constructor as ComponentType;
|
const componentType = component.constructor as ComponentType;
|
||||||
@@ -629,16 +651,29 @@ export class Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const bitIndex = registry.getBitIndex(componentType);
|
const bitIndex = registry.getBitIndex(componentType);
|
||||||
|
|
||||||
|
// 更新位掩码
|
||||||
BitMask64Utils.clearBit(this._componentMask, bitIndex);
|
BitMask64Utils.clearBit(this._componentMask, bitIndex);
|
||||||
|
|
||||||
|
// 使缓存失效
|
||||||
this._componentCache = null;
|
this._componentCache = null;
|
||||||
|
|
||||||
this.scene?.componentStorageManager?.removeComponent(this.id, componentType);
|
// 从Scene存储移除
|
||||||
this.scene?.referenceTracker?.clearComponentReferences(component);
|
if (this.scene?.componentStorageManager) {
|
||||||
|
this.scene.componentStorageManager.removeComponent(this.id, componentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.scene?.referenceTracker) {
|
||||||
|
this.scene.referenceTracker.clearComponentReferences(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (component.onRemovedFromEntity) {
|
||||||
|
component.onRemovedFromEntity();
|
||||||
|
}
|
||||||
|
|
||||||
component.onRemovedFromEntity?.();
|
|
||||||
component.entityId = null;
|
component.entityId = null;
|
||||||
|
|
||||||
if (this.scene?.eventSystem) {
|
if (this.scene && this.scene.eventSystem) {
|
||||||
this.scene.eventSystem.emitSync('component:removed', {
|
this.scene.eventSystem.emitSync('component:removed', {
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
source: 'Entity',
|
source: 'Entity',
|
||||||
@@ -654,11 +689,10 @@ export class Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 移除指定类型的组件
|
* 移除指定类型的组件
|
||||||
* @en Remove component by type
|
|
||||||
*
|
*
|
||||||
* @param type - @zh 组件类型 @en Component type
|
* @param type - 组件类型
|
||||||
* @returns @zh 被移除的组件实例或null @en Removed component instance or null
|
* @returns 被移除的组件实例或null
|
||||||
*/
|
*/
|
||||||
public removeComponentByType<T extends Component>(type: ComponentType<T>): T | null {
|
public removeComponentByType<T extends Component>(type: ComponentType<T>): T | null {
|
||||||
const component = this.getComponent(type);
|
const component = this.getComponent(type);
|
||||||
@@ -670,17 +704,24 @@ export class Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 移除所有组件
|
* 移除所有组件
|
||||||
* @en Remove all components
|
|
||||||
*/
|
*/
|
||||||
public removeAllComponents(): void {
|
public removeAllComponents(): void {
|
||||||
const componentsToRemove = [...this.components];
|
const componentsToRemove = [...this.components];
|
||||||
|
|
||||||
|
// 清除位掩码
|
||||||
BitMask64Utils.clear(this._componentMask);
|
BitMask64Utils.clear(this._componentMask);
|
||||||
|
|
||||||
|
// 使缓存失效
|
||||||
this._componentCache = null;
|
this._componentCache = null;
|
||||||
|
|
||||||
for (const component of componentsToRemove) {
|
for (const component of componentsToRemove) {
|
||||||
const componentType = component.constructor as ComponentType;
|
const componentType = component.constructor as ComponentType;
|
||||||
this.scene?.componentStorageManager?.removeComponent(this.id, componentType);
|
|
||||||
|
if (this.scene?.componentStorageManager) {
|
||||||
|
this.scene.componentStorageManager.removeComponent(this.id, componentType);
|
||||||
|
}
|
||||||
|
|
||||||
component.onRemovedFromEntity();
|
component.onRemovedFromEntity();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -688,11 +729,10 @@ export class Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 批量添加组件
|
* 批量添加组件
|
||||||
* @en Add multiple components
|
|
||||||
*
|
*
|
||||||
* @param components - @zh 要添加的组件数组 @en Array of components to add
|
* @param components - 要添加的组件数组
|
||||||
* @returns @zh 添加的组件数组 @en Array of added components
|
* @returns 添加的组件数组
|
||||||
*/
|
*/
|
||||||
public addComponents<T extends Component>(components: T[]): T[] {
|
public addComponents<T extends Component>(components: T[]): T[] {
|
||||||
const addedComponents: T[] = [];
|
const addedComponents: T[] = [];
|
||||||
@@ -788,11 +828,10 @@ export class Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 销毁实体
|
* 销毁实体
|
||||||
* @en Destroy entity
|
|
||||||
*
|
*
|
||||||
* @zh 移除所有组件并标记为已销毁。层级关系的清理由 HierarchySystem 处理。
|
* 移除所有组件并标记为已销毁。
|
||||||
* @en Removes all components and marks as destroyed. Hierarchy cleanup is handled by HierarchySystem.
|
* 层级关系的清理由 HierarchySystem 处理。
|
||||||
*/
|
*/
|
||||||
public destroy(): void {
|
public destroy(): void {
|
||||||
if (this._isDestroyed) {
|
if (this._isDestroyed) {
|
||||||
@@ -820,14 +859,13 @@ export class Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 比较实体优先级
|
* 比较实体
|
||||||
* @en Compare entity priority
|
|
||||||
*
|
*
|
||||||
* @param other - @zh 另一个实体 @en Another entity
|
* @param other - 另一个实体
|
||||||
* @returns @zh 比较结果 @en Comparison result
|
* @returns 比较结果
|
||||||
*/
|
*/
|
||||||
public compareTo(other: Entity): number {
|
public compareTo(other: Entity): number {
|
||||||
return compareEntities(this, other);
|
return EntityComparer.prototype.compare(this, other);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import type { IncrementalSnapshot, IncrementalSerializationOptions } from './Ser
|
|||||||
*
|
*
|
||||||
* 定义场景应该实现的核心功能和属性,使用接口而非继承提供更灵活的实现方式。
|
* 定义场景应该实现的核心功能和属性,使用接口而非继承提供更灵活的实现方式。
|
||||||
*/
|
*/
|
||||||
export type IScene = {
|
export interface IScene {
|
||||||
/**
|
/**
|
||||||
* 场景名称
|
* 场景名称
|
||||||
*/
|
*/
|
||||||
@@ -362,7 +362,7 @@ export type IScene = {
|
|||||||
/**
|
/**
|
||||||
* 场景工厂接口
|
* 场景工厂接口
|
||||||
*/
|
*/
|
||||||
export type ISceneFactory<T extends IScene> = {
|
export interface ISceneFactory<T extends IScene> {
|
||||||
/**
|
/**
|
||||||
* 创建场景实例
|
* 创建场景实例
|
||||||
*/
|
*/
|
||||||
@@ -370,29 +370,21 @@ export type ISceneFactory<T extends IScene> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 场景配置接口
|
* 场景配置接口
|
||||||
* @en Scene configuration interface
|
* Scene configuration interface
|
||||||
*/
|
*/
|
||||||
export type ISceneConfig = {
|
export interface ISceneConfig {
|
||||||
/**
|
/**
|
||||||
* @zh 场景名称
|
* 场景名称
|
||||||
* @en Scene name
|
* Scene name
|
||||||
*/
|
*/
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 是否从全局注册表继承组件类型
|
* 是否从全局注册表继承组件类型
|
||||||
* @en Whether to inherit component types from global registry
|
* Whether to inherit component types from global registry
|
||||||
*
|
*
|
||||||
* @default true
|
* @default true
|
||||||
*/
|
*/
|
||||||
inheritGlobalRegistry?: boolean;
|
inheritGlobalRegistry?: boolean;
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 系统最大错误次数,超过此阈值后系统将被自动禁用
|
|
||||||
* @en Maximum error count for systems, systems exceeding this threshold will be auto-disabled
|
|
||||||
*
|
|
||||||
* @default 10
|
|
||||||
*/
|
|
||||||
maxSystemErrorCount?: number;
|
|
||||||
}
|
}
|
||||||
|
|||||||
+155
-66
@@ -13,7 +13,7 @@ import { QuerySystem } from './Core/QuerySystem';
|
|||||||
import { TypeSafeEventSystem } from './Core/EventSystem';
|
import { TypeSafeEventSystem } from './Core/EventSystem';
|
||||||
import { ReferenceTracker } from './Core/ReferenceTracker';
|
import { ReferenceTracker } from './Core/ReferenceTracker';
|
||||||
import { IScene, ISceneConfig } from './IScene';
|
import { IScene, ISceneConfig } from './IScene';
|
||||||
import { getComponentInstanceTypeName, getSystemInstanceTypeName, getSystemMetadata, getSystemInstanceMetadata } from './Decorators';
|
import { getComponentInstanceTypeName, getSystemInstanceTypeName, getSystemMetadata } from './Decorators';
|
||||||
import { TypedQueryBuilder } from './Core/Query/TypedQuery';
|
import { TypedQueryBuilder } from './Core/Query/TypedQuery';
|
||||||
import {
|
import {
|
||||||
SceneSerializer,
|
SceneSerializer,
|
||||||
@@ -154,7 +154,7 @@ export class Scene implements IScene {
|
|||||||
/**
|
/**
|
||||||
* 日志记录器
|
* 日志记录器
|
||||||
*/
|
*/
|
||||||
private readonly _logger: ReturnType<typeof createLogger>;
|
private readonly logger: ReturnType<typeof createLogger>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 性能监控器缓存
|
* 性能监控器缓存
|
||||||
@@ -212,10 +212,11 @@ export class Scene implements IScene {
|
|||||||
private _systemErrorCount: Map<EntitySystem, number> = new Map();
|
private _systemErrorCount: Map<EntitySystem, number> = new Map();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 最大允许错误次数,系统错误次数超过此阈值后将被自动禁用
|
* 最大允许错误次数
|
||||||
* @en Maximum error count, systems exceeding this threshold will be auto-disabled
|
*
|
||||||
|
* 系统错误次数超过此阈值后将被自动禁用
|
||||||
*/
|
*/
|
||||||
private _maxErrorCount: number;
|
private _maxErrorCount: number = 10;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 系统添加计数器
|
* 系统添加计数器
|
||||||
@@ -294,17 +295,20 @@ export class Scene implements IScene {
|
|||||||
const systems = this._filterEntitySystems(allServices);
|
const systems = this._filterEntitySystems(allServices);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 使用 SystemScheduler 进行依赖排序
|
||||||
this._systemScheduler.markDirty();
|
this._systemScheduler.markDirty();
|
||||||
return this._systemScheduler.getAllSortedSystems(systems);
|
return this._systemScheduler.getAllSortedSystems(systems);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof CycleDependencyError) {
|
if (error instanceof CycleDependencyError) {
|
||||||
this._logger.error(
|
// 循环依赖错误,记录警告并回退到 updateOrder 排序
|
||||||
|
this.logger.error(
|
||||||
`[Scene] 系统存在循环依赖,回退到 updateOrder 排序 | Cycle dependency detected, falling back to updateOrder sort`,
|
`[Scene] 系统存在循环依赖,回退到 updateOrder 排序 | Cycle dependency detected, falling back to updateOrder sort`,
|
||||||
error.involvedNodes
|
error.involvedNodes
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this._logger.error(`[Scene] 系统排序失败 | System sorting failed`, error);
|
this.logger.error(`[Scene] 系统排序失败 | System sorting failed`, error);
|
||||||
}
|
}
|
||||||
|
// 回退到简单的 updateOrder 排序
|
||||||
return this._sortSystemsByUpdateOrder(systems);
|
return this._sortSystemsByUpdateOrder(systems);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -374,18 +378,20 @@ export class Scene implements IScene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 创建场景实例
|
* 创建场景实例
|
||||||
* @en Create scene instance
|
* Create scene instance
|
||||||
*
|
|
||||||
* @param config - @zh 场景配置 @en Scene configuration
|
|
||||||
*/
|
*/
|
||||||
constructor(config?: ISceneConfig) {
|
constructor(config?: ISceneConfig) {
|
||||||
this.entities = new EntityList(this);
|
this.entities = new EntityList(this);
|
||||||
this.identifierPool = new IdentifierPool();
|
this.identifierPool = new IdentifierPool();
|
||||||
this.componentStorageManager = new ComponentStorageManager();
|
this.componentStorageManager = new ComponentStorageManager();
|
||||||
|
|
||||||
|
// 创建场景级别的组件注册表
|
||||||
|
// Create scene-level component registry
|
||||||
this.componentRegistry = new ComponentRegistry();
|
this.componentRegistry = new ComponentRegistry();
|
||||||
|
|
||||||
|
// 从全局注册表继承框架组件(默认启用)
|
||||||
|
// Inherit framework components from global registry (enabled by default)
|
||||||
if (config?.inheritGlobalRegistry !== false) {
|
if (config?.inheritGlobalRegistry !== false) {
|
||||||
this.componentRegistry.cloneFrom(GlobalComponentRegistry);
|
this.componentRegistry.cloneFrom(GlobalComponentRegistry);
|
||||||
}
|
}
|
||||||
@@ -395,8 +401,7 @@ export class Scene implements IScene {
|
|||||||
this.referenceTracker = new ReferenceTracker();
|
this.referenceTracker = new ReferenceTracker();
|
||||||
this.handleManager = new EntityHandleManager();
|
this.handleManager = new EntityHandleManager();
|
||||||
this._services = new ServiceContainer();
|
this._services = new ServiceContainer();
|
||||||
this._logger = createLogger('Scene');
|
this.logger = createLogger('Scene');
|
||||||
this._maxErrorCount = config?.maxSystemErrorCount ?? 10;
|
|
||||||
|
|
||||||
if (config?.name) {
|
if (config?.name) {
|
||||||
this.name = config.name;
|
this.name = config.name;
|
||||||
@@ -460,19 +465,23 @@ export class Scene implements IScene {
|
|||||||
* In editor mode, this method also executes all deferred component lifecycle callbacks.
|
* In editor mode, this method also executes all deferred component lifecycle callbacks.
|
||||||
*/
|
*/
|
||||||
public begin() {
|
public begin() {
|
||||||
|
// 标记场景已开始运行
|
||||||
this._didSceneBegin = true;
|
this._didSceneBegin = true;
|
||||||
|
|
||||||
|
// 执行所有延迟的组件生命周期回调 | Execute all deferred component lifecycle callbacks
|
||||||
if (this._deferredComponentCallbacks.length > 0) {
|
if (this._deferredComponentCallbacks.length > 0) {
|
||||||
for (const callback of this._deferredComponentCallbacks) {
|
for (const callback of this._deferredComponentCallbacks) {
|
||||||
try {
|
try {
|
||||||
callback();
|
callback();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this._logger.error('Error executing deferred component callback:', error);
|
this.logger.error('Error executing deferred component callback:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 清空队列 | Clear the queue
|
||||||
this._deferredComponentCallbacks = [];
|
this._deferredComponentCallbacks = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 调用onStart方法
|
||||||
this.onStart();
|
this.onStart();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,18 +501,38 @@ export class Scene implements IScene {
|
|||||||
* - 系统清理:在 System.onDestroy() 中处理(实体已被清理)
|
* - 系统清理:在 System.onDestroy() 中处理(实体已被清理)
|
||||||
*/
|
*/
|
||||||
public end() {
|
public end() {
|
||||||
|
// 标记场景已结束运行
|
||||||
this._didSceneBegin = false;
|
this._didSceneBegin = false;
|
||||||
|
|
||||||
|
// 先调用用户的卸载方法,此时用户可以访问实体和系统进行清理
|
||||||
this.unload();
|
this.unload();
|
||||||
|
|
||||||
|
// 移除所有实体
|
||||||
this.entities.removeAllEntities();
|
this.entities.removeAllEntities();
|
||||||
|
|
||||||
|
// 清理查询系统中的实体引用和缓存
|
||||||
this.querySystem.setEntities([]);
|
this.querySystem.setEntities([]);
|
||||||
|
|
||||||
|
// 清空组件存储
|
||||||
this.componentStorageManager.clear();
|
this.componentStorageManager.clear();
|
||||||
|
|
||||||
|
// 清空服务容器(会调用所有服务的dispose方法,包括所有EntitySystem)
|
||||||
|
// 系统的 onDestroy 回调会在这里被触发
|
||||||
this._services.clear();
|
this._services.clear();
|
||||||
|
|
||||||
|
// 清空系统缓存
|
||||||
this._cachedSystems = null;
|
this._cachedSystems = null;
|
||||||
this._systemsOrderDirty = true;
|
this._systemsOrderDirty = true;
|
||||||
|
|
||||||
|
// 清空组件索引 | Clear component indices
|
||||||
this._componentIdToSystems.clear();
|
this._componentIdToSystems.clear();
|
||||||
this._globalNotifySystems.clear();
|
this._globalNotifySystems.clear();
|
||||||
|
|
||||||
|
// 清空句柄映射并重置句柄管理器 | Clear handle mapping and reset handle manager
|
||||||
this._handleToEntity.clear();
|
this._handleToEntity.clear();
|
||||||
this.handleManager.reset();
|
this.handleManager.reset();
|
||||||
|
|
||||||
|
// 重置 epoch 管理器 | Reset epoch manager
|
||||||
this.epochManager.reset();
|
this.epochManager.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -511,61 +540,68 @@ export class Scene implements IScene {
|
|||||||
* 更新场景
|
* 更新场景
|
||||||
*/
|
*/
|
||||||
public update() {
|
public update() {
|
||||||
|
// 递增帧计数(用于变更检测) | Increment epoch (for change detection)
|
||||||
this.epochManager.increment();
|
this.epochManager.increment();
|
||||||
|
|
||||||
|
// 开始性能采样帧
|
||||||
ProfilerSDK.beginFrame();
|
ProfilerSDK.beginFrame();
|
||||||
const frameHandle = ProfilerSDK.beginSample('Scene.update', ProfileCategory.ECS);
|
const frameHandle = ProfilerSDK.beginSample('Scene.update', ProfileCategory.ECS);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ComponentPoolManager.getInstance().update();
|
ComponentPoolManager.getInstance().update();
|
||||||
|
|
||||||
this.entities.updateLists();
|
this.entities.updateLists();
|
||||||
|
|
||||||
const systems = this.systems;
|
const systems = this.systems;
|
||||||
|
|
||||||
this._runSystemPhase(systems, 'update', 'Systems.update');
|
// Update 阶段
|
||||||
this._runSystemPhase(systems, 'lateUpdate', 'Systems.lateUpdate');
|
const updateHandle = ProfilerSDK.beginSample('Systems.update', ProfileCategory.ECS);
|
||||||
|
try {
|
||||||
|
for (const system of systems) {
|
||||||
|
if (system.enabled) {
|
||||||
|
const systemHandle = ProfilerSDK.beginSample(system.systemName, ProfileCategory.ECS);
|
||||||
|
try {
|
||||||
|
system.update();
|
||||||
|
} catch (error) {
|
||||||
|
this._handleSystemError(system, 'update', error);
|
||||||
|
} finally {
|
||||||
|
ProfilerSDK.endSample(systemHandle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
ProfilerSDK.endSample(updateHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
// LateUpdate 阶段
|
||||||
|
const lateUpdateHandle = ProfilerSDK.beginSample('Systems.lateUpdate', ProfileCategory.ECS);
|
||||||
|
try {
|
||||||
|
for (const system of systems) {
|
||||||
|
if (system.enabled) {
|
||||||
|
const systemHandle = ProfilerSDK.beginSample(`${system.systemName}.late`, ProfileCategory.ECS);
|
||||||
|
try {
|
||||||
|
system.lateUpdate();
|
||||||
|
} catch (error) {
|
||||||
|
this._handleSystemError(system, 'lateUpdate', error);
|
||||||
|
} finally {
|
||||||
|
ProfilerSDK.endSample(systemHandle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
ProfilerSDK.endSample(lateUpdateHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行所有系统的延迟命令
|
||||||
|
// Flush all systems' deferred commands
|
||||||
this.flushCommandBuffers(systems);
|
this.flushCommandBuffers(systems);
|
||||||
} finally {
|
} finally {
|
||||||
ProfilerSDK.endSample(frameHandle);
|
ProfilerSDK.endSample(frameHandle);
|
||||||
|
// 结束性能采样帧
|
||||||
ProfilerSDK.endFrame();
|
ProfilerSDK.endFrame();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 执行系统的指定阶段
|
|
||||||
* @en Run specified phase for all systems
|
|
||||||
*/
|
|
||||||
private _runSystemPhase(
|
|
||||||
systems: EntitySystem[],
|
|
||||||
phase: 'update' | 'lateUpdate',
|
|
||||||
profileName: string
|
|
||||||
): void {
|
|
||||||
const phaseHandle = ProfilerSDK.beginSample(profileName, ProfileCategory.ECS);
|
|
||||||
try {
|
|
||||||
for (const system of systems) {
|
|
||||||
if (!this._shouldSystemRun(system)) continue;
|
|
||||||
|
|
||||||
const suffix = phase === 'lateUpdate' ? '.late' : '';
|
|
||||||
const systemHandle = ProfilerSDK.beginSample(`${system.systemName}${suffix}`, ProfileCategory.ECS);
|
|
||||||
try {
|
|
||||||
system[phase]();
|
|
||||||
} catch (error) {
|
|
||||||
this._handleSystemError(system, phase, error);
|
|
||||||
} finally {
|
|
||||||
ProfilerSDK.endSample(systemHandle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
ProfilerSDK.endSample(phaseHandle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _shouldSystemRun(system: EntitySystem): boolean {
|
|
||||||
if (!system.enabled) return false;
|
|
||||||
if (!this.isEditorMode) return true;
|
|
||||||
|
|
||||||
const metadata = getSystemInstanceMetadata(system);
|
|
||||||
return metadata?.runInEditMode !== false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行所有系统的延迟命令
|
* 执行所有系统的延迟命令
|
||||||
* Flush all systems' deferred commands
|
* Flush all systems' deferred commands
|
||||||
@@ -580,7 +616,7 @@ export class Scene implements IScene {
|
|||||||
try {
|
try {
|
||||||
system.flushCommands();
|
system.flushCommands();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this._logger.error(`Error flushing commands for system ${system.systemName}:`, error);
|
this.logger.error(`Error flushing commands for system ${system.systemName}:`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -601,16 +637,15 @@ export class Scene implements IScene {
|
|||||||
const errorCount = (this._systemErrorCount.get(system) || 0) + 1;
|
const errorCount = (this._systemErrorCount.get(system) || 0) + 1;
|
||||||
this._systemErrorCount.set(system, errorCount);
|
this._systemErrorCount.set(system, errorCount);
|
||||||
|
|
||||||
const name = system.systemName;
|
this.logger.error(
|
||||||
this._logger.error(
|
`Error in system ${system.constructor.name}.${phase}() [${errorCount}/${this._maxErrorCount}]:`,
|
||||||
`Error in system ${name}.${phase}() [${errorCount}/${this._maxErrorCount}]:`,
|
|
||||||
error
|
error
|
||||||
);
|
);
|
||||||
|
|
||||||
if (errorCount >= this._maxErrorCount) {
|
if (errorCount >= this._maxErrorCount) {
|
||||||
system.enabled = false;
|
system.enabled = false;
|
||||||
this._logger.error(
|
this.logger.error(
|
||||||
`System ${name} has been disabled due to excessive errors (${errorCount} errors)`
|
`System ${system.constructor.name} has been disabled due to excessive errors (${errorCount} errors)`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -622,8 +657,11 @@ export class Scene implements IScene {
|
|||||||
public createEntity(name: string) {
|
public createEntity(name: string) {
|
||||||
const entity = new Entity(name, this.identifierPool.checkOut());
|
const entity = new Entity(name, this.identifierPool.checkOut());
|
||||||
|
|
||||||
|
// 分配轻量级句柄 | Assign lightweight handle
|
||||||
const handle = this.handleManager.create();
|
const handle = this.handleManager.create();
|
||||||
entity.setHandle(handle);
|
entity.setHandle(handle);
|
||||||
|
|
||||||
|
// 添加到句柄映射 | Add to handle mapping
|
||||||
this._handleToEntity.set(handle, entity);
|
this._handleToEntity.set(handle, entity);
|
||||||
|
|
||||||
this.eventSystem.emitSync('entity:created', { entityName: name, entity, scene: this });
|
this.eventSystem.emitSync('entity:created', { entityName: name, entity, scene: this });
|
||||||
@@ -656,8 +694,10 @@ export class Scene implements IScene {
|
|||||||
* @param changedComponentType 变化的组件类型(可选) | The changed component type (optional)
|
* @param changedComponentType 变化的组件类型(可选) | The changed component type (optional)
|
||||||
*/
|
*/
|
||||||
public notifyEntityComponentChanged(entity: Entity, changedComponentType?: ComponentType): void {
|
public notifyEntityComponentChanged(entity: Entity, changedComponentType?: ComponentType): void {
|
||||||
|
// 已通知的系统集合,避免重复通知 | Set of notified systems to avoid duplicates
|
||||||
const notifiedSystems = new Set<EntitySystem>();
|
const notifiedSystems = new Set<EntitySystem>();
|
||||||
|
|
||||||
|
// 如果提供了组件类型,使用索引优化 | If component type provided, use index optimization
|
||||||
if (changedComponentType && this.componentRegistry.isRegistered(changedComponentType)) {
|
if (changedComponentType && this.componentRegistry.isRegistered(changedComponentType)) {
|
||||||
const componentId = this.componentRegistry.getBitIndex(changedComponentType);
|
const componentId = this.componentRegistry.getBitIndex(changedComponentType);
|
||||||
const interestedSystems = this._componentIdToSystems.get(componentId);
|
const interestedSystems = this._componentIdToSystems.get(componentId);
|
||||||
@@ -670,6 +710,7 @@ export class Scene implements IScene {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 通知全局监听系统(none条件、tag/name查询等) | Notify global listener systems
|
||||||
for (const system of this._globalNotifySystems) {
|
for (const system of this._globalNotifySystems) {
|
||||||
if (!notifiedSystems.has(system)) {
|
if (!notifiedSystems.has(system)) {
|
||||||
system.handleEntityComponentChanged(entity);
|
system.handleEntityComponentChanged(entity);
|
||||||
@@ -677,6 +718,7 @@ export class Scene implements IScene {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果没有提供组件类型,回退到遍历所有系统 | Fallback to all systems if no component type
|
||||||
if (!changedComponentType) {
|
if (!changedComponentType) {
|
||||||
for (const system of this.systems) {
|
for (const system of this.systems) {
|
||||||
if (!notifiedSystems.has(system)) {
|
if (!notifiedSystems.has(system)) {
|
||||||
@@ -702,29 +744,35 @@ export class Scene implements IScene {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// nothing 匹配器不需要索引 | Nothing matcher doesn't need indexing
|
||||||
if (matcher.isNothing()) {
|
if (matcher.isNothing()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const condition = matcher.getCondition();
|
const condition = matcher.getCondition();
|
||||||
|
|
||||||
|
// 有 none/tag/name 条件的系统加入全局通知 | Systems with none/tag/name go to global
|
||||||
if (condition.none.length > 0 || condition.tag !== undefined || condition.name !== undefined) {
|
if (condition.none.length > 0 || condition.tag !== undefined || condition.name !== undefined) {
|
||||||
this._globalNotifySystems.add(system);
|
this._globalNotifySystems.add(system);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 空匹配器(匹配所有实体)加入全局通知 | Empty matcher (matches all) goes to global
|
||||||
if (matcher.isEmpty()) {
|
if (matcher.isEmpty()) {
|
||||||
this._globalNotifySystems.add(system);
|
this._globalNotifySystems.add(system);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 索引 all 条件中的组件 | Index components in all condition
|
||||||
for (const componentType of condition.all) {
|
for (const componentType of condition.all) {
|
||||||
this.addSystemToComponentIndex(componentType, system);
|
this.addSystemToComponentIndex(componentType, system);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 索引 any 条件中的组件 | Index components in any condition
|
||||||
for (const componentType of condition.any) {
|
for (const componentType of condition.any) {
|
||||||
this.addSystemToComponentIndex(componentType, system);
|
this.addSystemToComponentIndex(componentType, system);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 索引单组件查询 | Index single component query
|
||||||
if (condition.component) {
|
if (condition.component) {
|
||||||
this.addSystemToComponentIndex(condition.component, system);
|
this.addSystemToComponentIndex(condition.component, system);
|
||||||
}
|
}
|
||||||
@@ -758,8 +806,10 @@ export class Scene implements IScene {
|
|||||||
* @param system 要移除的系统 | The system to remove
|
* @param system 要移除的系统 | The system to remove
|
||||||
*/
|
*/
|
||||||
private removeSystemFromIndex(system: EntitySystem): void {
|
private removeSystemFromIndex(system: EntitySystem): void {
|
||||||
|
// 从全局通知列表移除 | Remove from global notify list
|
||||||
this._globalNotifySystems.delete(system);
|
this._globalNotifySystems.delete(system);
|
||||||
|
|
||||||
|
// 从所有组件索引中移除 | Remove from all component indices
|
||||||
for (const systems of this._componentIdToSystems.values()) {
|
for (const systems of this._componentIdToSystems.values()) {
|
||||||
systems.delete(system);
|
systems.delete(system);
|
||||||
}
|
}
|
||||||
@@ -774,12 +824,15 @@ export class Scene implements IScene {
|
|||||||
this.entities.add(entity);
|
this.entities.add(entity);
|
||||||
entity.scene = this;
|
entity.scene = this;
|
||||||
|
|
||||||
|
// 将实体添加到查询系统(可延迟缓存清理)
|
||||||
this.querySystem.addEntity(entity, deferCacheClear);
|
this.querySystem.addEntity(entity, deferCacheClear);
|
||||||
|
|
||||||
|
// 清除系统缓存以确保系统能及时发现新实体
|
||||||
if (!deferCacheClear) {
|
if (!deferCacheClear) {
|
||||||
this.clearSystemEntityCaches();
|
this.clearSystemEntityCaches();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 触发实体添加事件
|
||||||
this.eventSystem.emitSync('entity:added', { entity, scene: this });
|
this.eventSystem.emitSync('entity:added', { entity, scene: this });
|
||||||
|
|
||||||
return entity;
|
return entity;
|
||||||
@@ -794,22 +847,30 @@ export class Scene implements IScene {
|
|||||||
public createEntities(count: number, namePrefix: string = 'Entity'): Entity[] {
|
public createEntities(count: number, namePrefix: string = 'Entity'): Entity[] {
|
||||||
const entities: Entity[] = [];
|
const entities: Entity[] = [];
|
||||||
|
|
||||||
|
// 批量创建实体对象,不立即添加到系统
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
const entity = new Entity(`${namePrefix}_${i}`, this.identifierPool.checkOut());
|
const entity = new Entity(`${namePrefix}_${i}`, this.identifierPool.checkOut());
|
||||||
entity.scene = this;
|
entity.scene = this;
|
||||||
|
|
||||||
|
// 分配轻量级句柄 | Assign lightweight handle
|
||||||
const handle = this.handleManager.create();
|
const handle = this.handleManager.create();
|
||||||
entity.setHandle(handle);
|
entity.setHandle(handle);
|
||||||
|
|
||||||
|
// 添加到句柄映射 | Add to handle mapping
|
||||||
this._handleToEntity.set(handle, entity);
|
this._handleToEntity.set(handle, entity);
|
||||||
|
|
||||||
entities.push(entity);
|
entities.push(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 批量添加到实体列表
|
||||||
for (const entity of entities) {
|
for (const entity of entities) {
|
||||||
this.entities.add(entity);
|
this.entities.add(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 批量添加到查询系统(无重复检查,性能最优)
|
||||||
this.querySystem.addEntitiesUnchecked(entities);
|
this.querySystem.addEntitiesUnchecked(entities);
|
||||||
|
|
||||||
|
// 批量触发事件(可选,减少事件开销)
|
||||||
this.eventSystem.emitSync('entities:batch_added', { entities, scene: this, count });
|
this.eventSystem.emitSync('entities:batch_added', { entities, scene: this, count });
|
||||||
|
|
||||||
return entities;
|
return entities;
|
||||||
@@ -833,6 +894,7 @@ export class Scene implements IScene {
|
|||||||
this.entities.remove(entity);
|
this.entities.remove(entity);
|
||||||
this.querySystem.removeEntity(entity);
|
this.querySystem.removeEntity(entity);
|
||||||
|
|
||||||
|
// 销毁句柄并从映射中移除 | Destroy handle and remove from mapping
|
||||||
if (isValidHandle(entity.handle)) {
|
if (isValidHandle(entity.handle)) {
|
||||||
this._handleToEntity.delete(entity.handle);
|
this._handleToEntity.delete(entity.handle);
|
||||||
this.handleManager.destroy(entity.handle);
|
this.handleManager.destroy(entity.handle);
|
||||||
@@ -929,9 +991,14 @@ export class Scene implements IScene {
|
|||||||
const persistentEntities = this.findPersistentEntities();
|
const persistentEntities = this.findPersistentEntities();
|
||||||
|
|
||||||
for (const entity of persistentEntities) {
|
for (const entity of persistentEntities) {
|
||||||
|
// 从实体列表移除
|
||||||
this.entities.remove(entity);
|
this.entities.remove(entity);
|
||||||
|
|
||||||
|
// 从查询系统移除
|
||||||
this.querySystem.removeEntity(entity);
|
this.querySystem.removeEntity(entity);
|
||||||
entity.scene = null; // detach but preserve component data
|
|
||||||
|
// 清除场景引用(但保留组件数据)
|
||||||
|
entity.scene = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return persistentEntities;
|
return persistentEntities;
|
||||||
@@ -950,16 +1017,23 @@ export class Scene implements IScene {
|
|||||||
*/
|
*/
|
||||||
public receiveMigratedEntities(entities: Entity[]): void {
|
public receiveMigratedEntities(entities: Entity[]): void {
|
||||||
for (const entity of entities) {
|
for (const entity of entities) {
|
||||||
|
// 设置新场景引用
|
||||||
entity.scene = this;
|
entity.scene = this;
|
||||||
|
|
||||||
|
// 添加到实体列表
|
||||||
this.entities.add(entity);
|
this.entities.add(entity);
|
||||||
|
|
||||||
|
// 添加到查询系统
|
||||||
this.querySystem.addEntity(entity);
|
this.querySystem.addEntity(entity);
|
||||||
|
|
||||||
|
// 重新注册组件到新场景的存储
|
||||||
for (const component of entity.components) {
|
for (const component of entity.components) {
|
||||||
this.componentStorageManager.addComponent(entity.id, component);
|
this.componentStorageManager.addComponent(entity.id, component);
|
||||||
this.referenceTracker?.registerEntityScene(entity.id, this);
|
this.referenceTracker?.registerEntityScene(entity.id, this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清除系统缓存
|
||||||
if (entities.length > 0) {
|
if (entities.length > 0) {
|
||||||
this.clearSystemEntityCaches();
|
this.clearSystemEntityCaches();
|
||||||
}
|
}
|
||||||
@@ -1087,7 +1161,7 @@ export class Scene implements IScene {
|
|||||||
|
|
||||||
if (this._services.isRegistered(constructor)) {
|
if (this._services.isRegistered(constructor)) {
|
||||||
const existingSystem = this._services.resolve(constructor) as T;
|
const existingSystem = this._services.resolve(constructor) as T;
|
||||||
this._logger.debug(`System ${constructor.name} already registered, returning existing instance`);
|
this.logger.debug(`System ${constructor.name} already registered, returning existing instance`);
|
||||||
return existingSystem;
|
return existingSystem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1103,10 +1177,10 @@ export class Scene implements IScene {
|
|||||||
if (this._services.isRegistered(constructor)) {
|
if (this._services.isRegistered(constructor)) {
|
||||||
const existingSystem = this._services.resolve(constructor);
|
const existingSystem = this._services.resolve(constructor);
|
||||||
if (existingSystem === system) {
|
if (existingSystem === system) {
|
||||||
this._logger.debug(`System ${constructor.name} instance already registered, returning it`);
|
this.logger.debug(`System ${constructor.name} instance already registered, returning it`);
|
||||||
return system;
|
return system;
|
||||||
} else {
|
} else {
|
||||||
this._logger.warn(
|
this.logger.warn(
|
||||||
`Attempting to register a different instance of ${constructor.name}, ` +
|
`Attempting to register a different instance of ${constructor.name}, ` +
|
||||||
'but type is already registered. Returning existing instance.'
|
'but type is already registered. Returning existing instance.'
|
||||||
);
|
);
|
||||||
@@ -1116,7 +1190,10 @@ export class Scene implements IScene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
system.scene = this;
|
system.scene = this;
|
||||||
system.addOrder = this._systemAddCounter++; // for stable sorting
|
|
||||||
|
// 分配添加顺序,用于稳定排序 | Assign add order for stable sorting
|
||||||
|
system.addOrder = this._systemAddCounter++;
|
||||||
|
|
||||||
system.setPerformanceMonitor(this.performanceMonitor);
|
system.setPerformanceMonitor(this.performanceMonitor);
|
||||||
|
|
||||||
const metadata = getSystemMetadata(constructor);
|
const metadata = getSystemMetadata(constructor);
|
||||||
@@ -1128,18 +1205,23 @@ export class Scene implements IScene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this._services.registerInstance(constructor, system);
|
this._services.registerInstance(constructor, system);
|
||||||
|
|
||||||
|
// 标记系统列表已变化
|
||||||
this.markSystemsOrderDirty();
|
this.markSystemsOrderDirty();
|
||||||
|
|
||||||
|
// 建立组件类型到系统的索引 | Build component type to system index
|
||||||
this.indexSystemByComponents(system);
|
this.indexSystemByComponents(system);
|
||||||
|
|
||||||
injectProperties(system, this._services);
|
injectProperties(system, this._services);
|
||||||
|
|
||||||
// Auto-wrap system methods for profiling in debug mode
|
// 调试模式下自动包装系统方法以收集性能数据(ProfilerSDK 启用时表示调试模式)
|
||||||
if (ProfilerSDK.isEnabled()) {
|
if (ProfilerSDK.isEnabled()) {
|
||||||
AutoProfiler.wrapInstance(system, system.systemName, ProfileCategory.ECS);
|
AutoProfiler.wrapInstance(system, system.systemName, ProfileCategory.ECS);
|
||||||
}
|
}
|
||||||
|
|
||||||
system.initialize();
|
system.initialize();
|
||||||
|
|
||||||
this._logger.debug(`System ${constructor.name} registered and initialized`);
|
this.logger.debug(`System ${constructor.name} registered and initialized`);
|
||||||
|
|
||||||
return system;
|
return system;
|
||||||
}
|
}
|
||||||
@@ -1208,9 +1290,16 @@ export class Scene implements IScene {
|
|||||||
public removeEntityProcessor(processor: EntitySystem): void {
|
public removeEntityProcessor(processor: EntitySystem): void {
|
||||||
const constructor = processor.constructor as ServiceType<EntitySystem>;
|
const constructor = processor.constructor as ServiceType<EntitySystem>;
|
||||||
|
|
||||||
|
// 从ServiceContainer移除
|
||||||
this._services.unregister(constructor);
|
this._services.unregister(constructor);
|
||||||
|
|
||||||
|
// 标记系统列表已变化
|
||||||
this.markSystemsOrderDirty();
|
this.markSystemsOrderDirty();
|
||||||
|
|
||||||
|
// 从组件类型索引中移除 | Remove from component type index
|
||||||
this.removeSystemFromIndex(processor);
|
this.removeSystemFromIndex(processor);
|
||||||
|
|
||||||
|
// 重置System状态
|
||||||
processor.reset();
|
processor.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,133 +1,363 @@
|
|||||||
/**
|
/**
|
||||||
* 组件序列化器
|
* 组件序列化器
|
||||||
*
|
*
|
||||||
* Component serializer for ECS components.
|
* 负责组件的序列化和反序列化操作
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component } from '../Component';
|
import { Component } from '../Component';
|
||||||
import { ComponentType } from '../Core/ComponentStorage';
|
import { ComponentType } from '../Core/ComponentStorage';
|
||||||
import { getComponentTypeName, isEntityRefProperty } from '../Decorators';
|
import { getComponentTypeName, isEntityRefProperty } from '../Decorators';
|
||||||
import { getSerializationMetadata } from './SerializationDecorators';
|
import {
|
||||||
import { ValueSerializer, SerializableValue } from './ValueSerializer';
|
getSerializationMetadata
|
||||||
import { createLogger } from '../../Utils/Logger';
|
} from './SerializationDecorators';
|
||||||
import type { Entity } from '../Entity';
|
import type { Entity } from '../Entity';
|
||||||
import type { SerializationContext, SerializedEntityRef } from './SerializationContext';
|
import type { SerializationContext, SerializedEntityRef } from './SerializationContext';
|
||||||
|
|
||||||
const logger = createLogger('ComponentSerializer');
|
/**
|
||||||
|
* 可序列化的值类型
|
||||||
|
*/
|
||||||
|
export type SerializableValue =
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| null
|
||||||
|
| undefined
|
||||||
|
| SerializableValue[]
|
||||||
|
| { [key: string]: SerializableValue }
|
||||||
|
| { __type: 'Date'; value: string }
|
||||||
|
| { __type: 'Map'; value: Array<[SerializableValue, SerializableValue]> }
|
||||||
|
| { __type: 'Set'; value: SerializableValue[] }
|
||||||
|
| { __entityRef: SerializedEntityRef };
|
||||||
|
|
||||||
export type { SerializableValue } from './ValueSerializer';
|
/**
|
||||||
|
* 序列化后的组件数据
|
||||||
export type SerializedComponent = {
|
*/
|
||||||
|
export interface SerializedComponent {
|
||||||
|
/**
|
||||||
|
* 组件类型名称
|
||||||
|
*/
|
||||||
type: string;
|
type: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 序列化版本
|
||||||
|
*/
|
||||||
version: number;
|
version: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组件数据
|
||||||
|
*/
|
||||||
data: Record<string, SerializableValue>;
|
data: Record<string, SerializableValue>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组件序列化器类
|
||||||
|
*/
|
||||||
export class ComponentSerializer {
|
export class ComponentSerializer {
|
||||||
static serialize(component: Component): SerializedComponent | null {
|
/**
|
||||||
|
* 序列化单个组件
|
||||||
|
*
|
||||||
|
* @param component 要序列化的组件实例
|
||||||
|
* @returns 序列化后的组件数据,如果组件不可序列化则返回null
|
||||||
|
*/
|
||||||
|
public static serialize(component: Component): SerializedComponent | null {
|
||||||
const metadata = getSerializationMetadata(component);
|
const metadata = getSerializationMetadata(component);
|
||||||
if (!metadata) return null;
|
|
||||||
|
if (!metadata) {
|
||||||
|
// 组件没有使用@Serializable装饰器,不可序列化
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const componentType = component.constructor as ComponentType;
|
const componentType = component.constructor as ComponentType;
|
||||||
const typeName = metadata.options.typeId || getComponentTypeName(componentType);
|
const typeName = metadata.options.typeId || getComponentTypeName(componentType);
|
||||||
const data: Record<string, SerializableValue> = {};
|
const data: Record<string, SerializableValue> = {};
|
||||||
|
|
||||||
|
// 序列化标记的字段
|
||||||
for (const [fieldName, options] of metadata.fields) {
|
for (const [fieldName, options] of metadata.fields) {
|
||||||
if (metadata.ignoredFields.has(fieldName)) continue;
|
|
||||||
|
|
||||||
const fieldKey = typeof fieldName === 'symbol' ? fieldName.toString() : fieldName;
|
const fieldKey = typeof fieldName === 'symbol' ? fieldName.toString() : fieldName;
|
||||||
const value = (component as unknown as Record<string | symbol, unknown>)[fieldName];
|
const value = (component as unknown as Record<string | symbol, unknown>)[fieldName];
|
||||||
|
|
||||||
|
// 跳过忽略的字段
|
||||||
|
if (metadata.ignoredFields.has(fieldName)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let serializedValue: SerializableValue;
|
let serializedValue: SerializableValue;
|
||||||
|
|
||||||
|
// 检查是否为 EntityRef 属性
|
||||||
if (isEntityRefProperty(component, fieldKey)) {
|
if (isEntityRefProperty(component, fieldKey)) {
|
||||||
serializedValue = this.serializeEntityRef(value as Entity | null);
|
serializedValue = this.serializeEntityRef(value as Entity | null);
|
||||||
} else if (options.serializer) {
|
} else if (options.serializer) {
|
||||||
|
// 使用自定义序列化器
|
||||||
serializedValue = options.serializer(value);
|
serializedValue = options.serializer(value);
|
||||||
} else {
|
} else {
|
||||||
serializedValue = ValueSerializer.serialize(value);
|
// 使用默认序列化
|
||||||
|
serializedValue = this.serializeValue(value as SerializableValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
data[options.alias || fieldKey] = serializedValue;
|
// 使用别名或原始字段名
|
||||||
|
const key = options.alias || fieldKey;
|
||||||
|
data[key] = serializedValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { type: typeName, version: metadata.options.version, data };
|
return {
|
||||||
|
type: typeName,
|
||||||
|
version: metadata.options.version,
|
||||||
|
data
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static deserialize(
|
/**
|
||||||
|
* 反序列化组件
|
||||||
|
*
|
||||||
|
* @param serializedData 序列化的组件数据
|
||||||
|
* @param componentRegistry 组件类型注册表 (类型名 -> 构造函数)
|
||||||
|
* @param context 序列化上下文(可选,用于解析 EntityRef)
|
||||||
|
* @returns 反序列化后的组件实例,如果失败则返回null
|
||||||
|
*/
|
||||||
|
public static deserialize(
|
||||||
serializedData: SerializedComponent,
|
serializedData: SerializedComponent,
|
||||||
componentRegistry: Map<string, ComponentType>,
|
componentRegistry: Map<string, ComponentType>,
|
||||||
context?: SerializationContext
|
context?: SerializationContext
|
||||||
): Component | null {
|
): Component | null {
|
||||||
const componentClass = componentRegistry.get(serializedData.type);
|
const componentClass = componentRegistry.get(serializedData.type);
|
||||||
|
|
||||||
if (!componentClass) {
|
if (!componentClass) {
|
||||||
logger.warn(`Component type not found: ${serializedData.type} | 未找到组件类型: ${serializedData.type}`);
|
console.warn(`未找到组件类型: ${serializedData.type}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const metadata = getSerializationMetadata(componentClass);
|
const metadata = getSerializationMetadata(componentClass);
|
||||||
|
|
||||||
if (!metadata) {
|
if (!metadata) {
|
||||||
logger.warn(`Component ${serializedData.type} is not serializable | 组件 ${serializedData.type} 不可序列化`);
|
console.warn(`组件 ${serializedData.type} 不可序列化`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 创建组件实例
|
||||||
const component = new componentClass();
|
const component = new componentClass();
|
||||||
|
|
||||||
|
// 反序列化字段
|
||||||
for (const [fieldName, options] of metadata.fields) {
|
for (const [fieldName, options] of metadata.fields) {
|
||||||
const fieldKey = typeof fieldName === 'symbol' ? fieldName.toString() : fieldName;
|
const fieldKey = typeof fieldName === 'symbol' ? fieldName.toString() : fieldName;
|
||||||
const key = options.alias || fieldKey;
|
const key = options.alias || fieldKey;
|
||||||
const serializedValue = serializedData.data[key];
|
const serializedValue = serializedData.data[key];
|
||||||
|
|
||||||
if (serializedValue === undefined) continue;
|
if (serializedValue === undefined) {
|
||||||
|
continue; // 字段不存在于序列化数据中
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否为序列化的 EntityRef
|
||||||
if (this.isSerializedEntityRef(serializedValue)) {
|
if (this.isSerializedEntityRef(serializedValue)) {
|
||||||
|
// EntityRef 需要延迟解析
|
||||||
if (context) {
|
if (context) {
|
||||||
const ref = serializedValue.__entityRef;
|
const ref = serializedValue.__entityRef;
|
||||||
context.registerPendingRef(component, fieldKey, ref.id, ref.guid);
|
context.registerPendingRef(component, fieldKey, ref.id, ref.guid);
|
||||||
}
|
}
|
||||||
|
// 暂时设为 null,后续由 context.resolveAllReferences() 填充
|
||||||
(component as unknown as Record<string | symbol, unknown>)[fieldName] = null;
|
(component as unknown as Record<string | symbol, unknown>)[fieldName] = null;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 使用自定义反序列化器或默认反序列化
|
||||||
const value = options.deserializer
|
const value = options.deserializer
|
||||||
? options.deserializer(serializedValue)
|
? options.deserializer(serializedValue)
|
||||||
: ValueSerializer.deserialize(serializedValue);
|
: this.deserializeValue(serializedValue);
|
||||||
|
|
||||||
(component as unknown as Record<string | symbol, unknown>)[fieldName] = value;
|
(component as unknown as Record<string | symbol, SerializableValue>)[fieldName] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
return component;
|
return component;
|
||||||
}
|
}
|
||||||
|
|
||||||
static serializeComponents(components: Component[]): SerializedComponent[] {
|
/**
|
||||||
return components
|
* 批量序列化组件
|
||||||
.map(c => this.serialize(c))
|
*
|
||||||
.filter((s): s is SerializedComponent => s !== null);
|
* @param components 组件数组
|
||||||
|
* @returns 序列化后的组件数据数组
|
||||||
|
*/
|
||||||
|
public static serializeComponents(components: Component[]): SerializedComponent[] {
|
||||||
|
const result: SerializedComponent[] = [];
|
||||||
|
|
||||||
|
for (const component of components) {
|
||||||
|
const serialized = this.serialize(component);
|
||||||
|
if (serialized) {
|
||||||
|
result.push(serialized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
static deserializeComponents(
|
/**
|
||||||
|
* 批量反序列化组件
|
||||||
|
*
|
||||||
|
* @param serializedComponents 序列化的组件数据数组
|
||||||
|
* @param componentRegistry 组件类型注册表
|
||||||
|
* @param context 序列化上下文(可选,用于解析 EntityRef)
|
||||||
|
* @returns 反序列化后的组件数组
|
||||||
|
*/
|
||||||
|
public static deserializeComponents(
|
||||||
serializedComponents: SerializedComponent[],
|
serializedComponents: SerializedComponent[],
|
||||||
componentRegistry: Map<string, ComponentType>,
|
componentRegistry: Map<string, ComponentType>,
|
||||||
context?: SerializationContext
|
context?: SerializationContext
|
||||||
): Component[] {
|
): Component[] {
|
||||||
return serializedComponents
|
const result: Component[] = [];
|
||||||
.map(s => this.deserialize(s, componentRegistry, context))
|
|
||||||
.filter((c): c is Component => c !== null);
|
for (const serialized of serializedComponents) {
|
||||||
|
const component = this.deserialize(serialized, componentRegistry, context);
|
||||||
|
if (component) {
|
||||||
|
result.push(component);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
static validateVersion(serializedData: SerializedComponent, expectedVersion: number): boolean {
|
/**
|
||||||
|
* 默认值序列化
|
||||||
|
*
|
||||||
|
* 处理基本类型、数组、对象等的序列化
|
||||||
|
*/
|
||||||
|
private static serializeValue(value: SerializableValue): SerializableValue {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基本类型
|
||||||
|
const type = typeof value;
|
||||||
|
if (type === 'string' || type === 'number' || type === 'boolean') {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 日期
|
||||||
|
if (value instanceof Date) {
|
||||||
|
return {
|
||||||
|
__type: 'Date',
|
||||||
|
value: value.toISOString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数组
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map((item) => this.serializeValue(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map (如果没有使用@SerializeMap装饰器)
|
||||||
|
if (value instanceof Map) {
|
||||||
|
return {
|
||||||
|
__type: 'Map',
|
||||||
|
value: Array.from(value.entries())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set
|
||||||
|
if (value instanceof Set) {
|
||||||
|
return {
|
||||||
|
__type: 'Set',
|
||||||
|
value: Array.from(value)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 普通对象
|
||||||
|
if (type === 'object' && typeof value === 'object' && !Array.isArray(value)) {
|
||||||
|
const result: Record<string, SerializableValue> = {};
|
||||||
|
const obj = value as Record<string, SerializableValue>;
|
||||||
|
for (const key in obj) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||||
|
result[key] = this.serializeValue(obj[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他类型(函数等)不序列化
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认值反序列化
|
||||||
|
*/
|
||||||
|
private static deserializeValue(value: SerializableValue): SerializableValue {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基本类型直接返回
|
||||||
|
const type = typeof value;
|
||||||
|
if (type === 'string' || type === 'number' || type === 'boolean') {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理特殊类型标记
|
||||||
|
if (type === 'object' && typeof value === 'object' && '__type' in value) {
|
||||||
|
const typedValue = value as { __type: string; value: SerializableValue };
|
||||||
|
switch (typedValue.__type) {
|
||||||
|
case 'Date':
|
||||||
|
return { __type: 'Date', value: typeof typedValue.value === 'string' ? typedValue.value : String(typedValue.value) };
|
||||||
|
case 'Map':
|
||||||
|
return { __type: 'Map', value: typedValue.value as Array<[SerializableValue, SerializableValue]> };
|
||||||
|
case 'Set':
|
||||||
|
return { __type: 'Set', value: typedValue.value as SerializableValue[] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数组
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map((item) => this.deserializeValue(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 普通对象
|
||||||
|
if (type === 'object' && typeof value === 'object' && !Array.isArray(value)) {
|
||||||
|
const result: Record<string, SerializableValue> = {};
|
||||||
|
const obj = value as Record<string, SerializableValue>;
|
||||||
|
for (const key in obj) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||||
|
result[key] = this.deserializeValue(obj[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证序列化数据的版本
|
||||||
|
*
|
||||||
|
* @param serializedData 序列化数据
|
||||||
|
* @param expectedVersion 期望的版本号
|
||||||
|
* @returns 版本是否匹配
|
||||||
|
*/
|
||||||
|
public static validateVersion(
|
||||||
|
serializedData: SerializedComponent,
|
||||||
|
expectedVersion: number
|
||||||
|
): boolean {
|
||||||
return serializedData.version === expectedVersion;
|
return serializedData.version === expectedVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
static getSerializationInfo(component: Component | ComponentType): {
|
/**
|
||||||
|
* 获取组件的序列化信息
|
||||||
|
*
|
||||||
|
* @param component 组件实例或组件类
|
||||||
|
* @returns 序列化信息对象,包含类型名、版本、可序列化字段列表
|
||||||
|
*/
|
||||||
|
public static getSerializationInfo(component: Component | ComponentType): {
|
||||||
type: string;
|
type: string;
|
||||||
version: number;
|
version: number;
|
||||||
fields: string[];
|
fields: string[];
|
||||||
ignoredFields: string[];
|
ignoredFields: string[];
|
||||||
isSerializable: boolean;
|
isSerializable: boolean;
|
||||||
} {
|
} | null {
|
||||||
const metadata = getSerializationMetadata(component);
|
const metadata = getSerializationMetadata(component);
|
||||||
|
|
||||||
if (!metadata) {
|
if (!metadata) {
|
||||||
return { type: 'unknown', version: 0, fields: [], ignoredFields: [], isSerializable: false };
|
return {
|
||||||
|
type: 'unknown',
|
||||||
|
version: 0,
|
||||||
|
fields: [],
|
||||||
|
ignoredFields: [],
|
||||||
|
isSerializable: false
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const componentType = typeof component === 'function'
|
const componentType = typeof component === 'function'
|
||||||
@@ -137,18 +367,50 @@ export class ComponentSerializer {
|
|||||||
return {
|
return {
|
||||||
type: metadata.options.typeId || getComponentTypeName(componentType),
|
type: metadata.options.typeId || getComponentTypeName(componentType),
|
||||||
version: metadata.options.version,
|
version: metadata.options.version,
|
||||||
fields: Array.from(metadata.fields.keys()).map(k => typeof k === 'symbol' ? k.toString() : k),
|
fields: Array.from(metadata.fields.keys()).map((k) =>
|
||||||
ignoredFields: Array.from(metadata.ignoredFields).map(k => typeof k === 'symbol' ? k.toString() : k),
|
typeof k === 'symbol' ? k.toString() : k
|
||||||
|
),
|
||||||
|
ignoredFields: Array.from(metadata.ignoredFields).map((k) =>
|
||||||
|
typeof k === 'symbol' ? k.toString() : k
|
||||||
|
),
|
||||||
isSerializable: true
|
isSerializable: true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static serializeEntityRef(entity: Entity | null): SerializableValue {
|
/**
|
||||||
if (!entity) return null;
|
* 序列化 Entity 引用
|
||||||
return { __entityRef: { id: entity.id, guid: entity.persistentId } };
|
*
|
||||||
|
* Serialize an Entity reference to a portable format.
|
||||||
|
*
|
||||||
|
* @param entity Entity 实例或 null
|
||||||
|
* @returns 序列化的引用格式
|
||||||
|
*/
|
||||||
|
public static serializeEntityRef(entity: Entity | null): SerializableValue {
|
||||||
|
if (!entity) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
__entityRef: {
|
||||||
|
id: entity.id,
|
||||||
|
guid: entity.persistentId
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static isSerializedEntityRef(value: unknown): value is { __entityRef: SerializedEntityRef } {
|
/**
|
||||||
return typeof value === 'object' && value !== null && '__entityRef' in value;
|
* 检查值是否为序列化的 EntityRef
|
||||||
|
*
|
||||||
|
* Check if a value is a serialized EntityRef.
|
||||||
|
*
|
||||||
|
* @param value 要检查的值
|
||||||
|
* @returns 如果是 EntityRef 返回 true
|
||||||
|
*/
|
||||||
|
public static isSerializedEntityRef(value: unknown): value is { __entityRef: SerializedEntityRef } {
|
||||||
|
return (
|
||||||
|
typeof value === 'object' &&
|
||||||
|
value !== null &&
|
||||||
|
'__entityRef' in value
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user