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:
|
||||
ci:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -70,7 +67,17 @@ jobs:
|
||||
- name: Install dependencies
|
||||
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
|
||||
run: pnpm run build
|
||||
|
||||
|
||||
@@ -156,25 +156,18 @@ jobs:
|
||||
if: steps.check-signpath.outputs.enabled == 'true'
|
||||
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'
|
||||
id: get-artifact
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
# 获取 windows-unsigned artifact 的 ID
|
||||
ARTIFACT_ID=$(gh api \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"/repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts" \
|
||||
--jq '.artifacts[] | select(.name == "windows-unsigned") | .id')
|
||||
|
||||
if [ -z "$ARTIFACT_ID" ]; then
|
||||
echo "Error: Could not find artifact 'windows-unsigned'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "artifact-id=$ARTIFACT_ID" >> $GITHUB_OUTPUT
|
||||
echo "Found artifact ID: $ARTIFACT_ID"
|
||||
echo "Files to be signed:"
|
||||
find ./artifacts -type f \( -name "*.exe" -o -name "*.msi" \) | head -20
|
||||
|
||||
- name: Submit to SignPath for code signing
|
||||
if: steps.check-signpath.outputs.enabled == 'true'
|
||||
@@ -185,8 +178,8 @@ jobs:
|
||||
organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }}
|
||||
project-slug: 'ecs-framework'
|
||||
signing-policy-slug: 'test-signing'
|
||||
artifact-configuration-slug: 'initial'
|
||||
github-artifact-id: ${{ steps.get-artifact.outputs.artifact-id }}
|
||||
artifact-configuration-slug: 'default'
|
||||
github-artifact-name: 'windows-unsigned'
|
||||
wait-for-completion: true
|
||||
wait-for-completion-timeout-in-seconds: 600
|
||||
output-artifact-directory: './signed'
|
||||
@@ -197,8 +190,7 @@ jobs:
|
||||
with:
|
||||
files: ./signed/*
|
||||
tag_name: ${{ github.event_name == 'workflow_dispatch' && format('editor-v{0}', github.event.inputs.version) || github.ref_name }}
|
||||
# 保持 Draft 状态,需要手动发布 | Keep as draft, require manual publish
|
||||
draft: true
|
||||
draft: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
@@ -90,7 +90,3 @@ docs/.vitepress/dist/
|
||||
# Tauri 捆绑输出
|
||||
**/src-tauri/target/release/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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
npm install @esengine/ecs-framework # Core ECS (can be used standalone)
|
||||
npm install @esengine/engine-core # Full engine with module system
|
||||
```
|
||||
| Package | Description |
|
||||
|---------|-------------|
|
||||
| `@esengine/ecs-framework` | Core ECS framework with entity management, component system, and queries |
|
||||
| `@esengine/math` | Vector, matrix, and mathematical utilities |
|
||||
| `@esengine/engine` | Rust/WASM 2D renderer |
|
||||
| `@esengine/engine-core` | Engine module system and lifecycle management |
|
||||
|
||||
### Popular Modules
|
||||
### Runtime
|
||||
|
||||
| Category | Packages |
|
||||
|----------|----------|
|
||||
| **Rendering** | `sprite`, `tilemap`, `particle`, `mesh-3d`, `fairygui` |
|
||||
| **Physics** | `physics-rapier2d` |
|
||||
| **AI & Logic** | `behavior-tree`, `blueprint` |
|
||||
| **Network** | `network`, `network-server` |
|
||||
| **Platform** | `platform-web`, `platform-wechat` |
|
||||
| Package | Description |
|
||||
|---------|-------------|
|
||||
| `@esengine/sprite` | 2D sprite rendering and animation |
|
||||
| `@esengine/tilemap` | Tile-based map rendering |
|
||||
| `@esengine/physics-rapier2d` | 2D physics simulation (Rapier) |
|
||||
| `@esengine/behavior-tree` | Behavior tree AI system |
|
||||
| `@esengine/blueprint` | Visual scripting runtime |
|
||||
| `@esengine/camera` | Camera control and management |
|
||||
| `@esengine/audio` | Audio playback |
|
||||
| `@esengine/ui` | UI components |
|
||||
| `@esengine/material-system` | Material and shader system |
|
||||
| `@esengine/asset-system` | Asset loading and management |
|
||||
|
||||
<details>
|
||||
<summary><b>View all 50+ packages</b></summary>
|
||||
### Editor Extensions
|
||||
|
||||
#### Core
|
||||
- `@esengine/ecs-framework` - ECS framework core
|
||||
- `@esengine/math` - Vector, matrix utilities
|
||||
- `@esengine/engine` - Rust/WASM renderer
|
||||
- `@esengine/engine-core` - Module lifecycle
|
||||
| Package | Description |
|
||||
|---------|-------------|
|
||||
| `@esengine/sprite-editor` | Sprite inspector and tools |
|
||||
| `@esengine/tilemap-editor` | Visual tilemap editor |
|
||||
| `@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
|
||||
- `@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
|
||||
### Platform
|
||||
|
||||
#### Network
|
||||
- `@esengine/network` - Client (TSRPC)
|
||||
- `@esengine/network-server` - Server runtime
|
||||
- `@esengine/network-protocols` - Shared protocols
|
||||
|
||||
#### 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>
|
||||
| Package | Description |
|
||||
|---------|-------------|
|
||||
| `@esengine/platform-common` | Platform abstraction interfaces |
|
||||
| `@esengine/platform-web` | Web browser runtime |
|
||||
| `@esengine/platform-wechat` | WeChat Mini Game runtime |
|
||||
|
||||
## Editor
|
||||
|
||||
@@ -253,24 +238,13 @@ pnpm tauri:dev
|
||||
|
||||
```
|
||||
esengine/
|
||||
├── packages/
|
||||
│ ├── core/ # ECS Framework (@esengine/ecs-framework)
|
||||
│ ├── math/ # Math library (@esengine/math)
|
||||
│ ├── engine-core/ # Engine lifecycle management
|
||||
│ ├── sprite/ # 2D sprite rendering
|
||||
│ ├── 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
|
||||
├── packages/ # Engine packages (runtime, editor, platform)
|
||||
├── 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
|
||||
|
||||
- [Getting Started](https://esengine.cn/guide/getting-started.html)
|
||||
@@ -279,7 +253,6 @@ esengine/
|
||||
|
||||
## Community
|
||||
|
||||
- [Discord](https://discord.gg/gCAgzXFW) - Chat with the community
|
||||
- [GitHub Issues](https://github.com/esengine/esengine/issues) - Bug reports and feature requests
|
||||
- [GitHub Discussions](https://github.com/esengine/esengine/discussions) - Questions and ideas
|
||||
|
||||
|
||||
+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 游戏引擎。它提供完整的工具集,让开发者专注于游戏创作而非基础设施搭建。
|
||||
@@ -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` |
|
||||
| **物理** | `physics-rapier2d` |
|
||||
| **AI 逻辑** | `behavior-tree`, `blueprint` |
|
||||
| **网络** | `network`, `network-server` |
|
||||
| **平台** | `platform-web`, `platform-wechat` |
|
||||
| `@esengine/ecs-framework` | ECS 框架核心,包含实体管理、组件系统和查询 |
|
||||
| `@esengine/math` | 向量、矩阵和数学工具 |
|
||||
| `@esengine/engine` | Rust/WASM 2D 渲染器 |
|
||||
| `@esengine/engine-core` | 引擎模块系统和生命周期管理 |
|
||||
|
||||
<details>
|
||||
<summary><b>查看全部 50+ 个包</b></summary>
|
||||
### 运行时
|
||||
|
||||
#### 核心
|
||||
- `@esengine/ecs-framework` - ECS 框架核心
|
||||
- `@esengine/math` - 向量、矩阵工具
|
||||
- `@esengine/engine` - Rust/WASM 渲染器
|
||||
- `@esengine/engine-core` - 模块生命周期
|
||||
| 包名 | 描述 |
|
||||
|------|------|
|
||||
| `@esengine/sprite` | 2D 精灵渲染和动画 |
|
||||
| `@esengine/tilemap` | Tilemap 渲染 |
|
||||
| `@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/network-protocols` - 共享协议
|
||||
| 包名 | 描述 |
|
||||
|------|------|
|
||||
| `@esengine/sprite-editor` | 精灵检视器和工具 |
|
||||
| `@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-wechat` - 微信小游戏
|
||||
|
||||
</details>
|
||||
| 包名 | 描述 |
|
||||
|------|------|
|
||||
| `@esengine/platform-common` | 平台抽象接口 |
|
||||
| `@esengine/platform-web` | Web 浏览器运行时 |
|
||||
| `@esengine/platform-wechat` | 微信小游戏运行时 |
|
||||
|
||||
## 编辑器
|
||||
|
||||
@@ -253,24 +238,13 @@ pnpm tauri:dev
|
||||
|
||||
```
|
||||
esengine/
|
||||
├── packages/
|
||||
│ ├── core/ # ECS 框架 (@esengine/ecs-framework)
|
||||
│ ├── math/ # 数学库 (@esengine/math)
|
||||
│ ├── engine-core/ # 引擎生命周期管理
|
||||
│ ├── sprite/ # 2D 精灵渲染
|
||||
│ ├── tilemap/ # Tilemap 系统
|
||||
│ ├── physics-rapier2d/ # 物理引擎
|
||||
│ ├── behavior-tree/ # AI 行为树
|
||||
│ ├── editor-app/ # 桌面编辑器 (Tauri)
|
||||
│ └── ... # 其他模块
|
||||
├── docs/ # 文档源码
|
||||
├── examples/ # 示例项目
|
||||
├── scripts/ # 构建工具
|
||||
└── thirdparty/ # 第三方依赖
|
||||
├── packages/ # 引擎包(运行时、编辑器、平台)
|
||||
├── docs/ # 文档源码
|
||||
├── examples/ # 示例项目
|
||||
├── scripts/ # 构建工具
|
||||
└── thirdparty/ # 第三方依赖
|
||||
```
|
||||
|
||||
> **寻找 ECS 源码?** ECS 框架位于 `packages/core/`
|
||||
|
||||
## 文档
|
||||
|
||||
- [快速入门](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 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)
|
||||
|
||||
### 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)
|
||||
|
||||
### 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 类支持时间缩放功能,可以实现慢动作、快进等效果:
|
||||
@@ -106,10 +48,10 @@ class TimeControlSystem extends EntitySystem {
|
||||
console.log('快进模式启用');
|
||||
}
|
||||
|
||||
public enableBulletTime(): void {
|
||||
// 子弹时间效果(10%速度)
|
||||
Time.timeScale = 0.1;
|
||||
console.log('子弹时间启用');
|
||||
public pauseGame(): void {
|
||||
// 暂停游戏(时间静止)
|
||||
Time.timeScale = 0;
|
||||
console.log('游戏暂停');
|
||||
}
|
||||
|
||||
public resumeNormalSpeed(): void {
|
||||
|
||||
@@ -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',
|
||||
bp: 'blueprint',
|
||||
mat: 'material',
|
||||
particle: 'particle',
|
||||
|
||||
// FairyGUI
|
||||
fui: 'fui'
|
||||
particle: 'particle'
|
||||
};
|
||||
|
||||
return typeMap[ext] || 'binary';
|
||||
|
||||
@@ -30,9 +30,9 @@
|
||||
"author": "yhh",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@esengine/build-config": "workspace:*",
|
||||
"@esengine/ecs-framework": "workspace:*",
|
||||
"@esengine/engine-core": "workspace:*",
|
||||
"@esengine/build-config": "workspace:*",
|
||||
"rimraf": "^5.0.0",
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.8.3"
|
||||
@@ -44,9 +44,5 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/esengine/esengine.git",
|
||||
"directory": "packages/asset-system"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/pako": "^2.0.4",
|
||||
"pako": "^2.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,47 +10,6 @@ import {
|
||||
IAssetCatalogEntry
|
||||
} from '../types/AssetTypes';
|
||||
|
||||
/**
|
||||
* 纹理 Sprite 信息(从 meta 文件的 importSettings 读取)
|
||||
* Texture sprite info (read from meta file's importSettings)
|
||||
*/
|
||||
export interface ITextureSpriteInfo {
|
||||
/**
|
||||
* 九宫格切片边距 [top, right, bottom, left]
|
||||
* Nine-patch slice border
|
||||
*/
|
||||
sliceBorder?: [number, number, number, number];
|
||||
/**
|
||||
* Sprite 锚点 [x, y](0-1 归一化)
|
||||
* Sprite pivot point (0-1 normalized)
|
||||
*/
|
||||
pivot?: [number, number];
|
||||
/**
|
||||
* 纹理宽度(可选,需要纹理已加载)
|
||||
* Texture width (optional, requires texture to be loaded)
|
||||
*/
|
||||
width?: number;
|
||||
/**
|
||||
* 纹理高度(可选,需要纹理已加载)
|
||||
* Texture height (optional, requires texture to be loaded)
|
||||
*/
|
||||
height?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sprite settings in import settings
|
||||
* 导入设置中的 Sprite 设置
|
||||
*/
|
||||
interface ISpriteSettings {
|
||||
sliceBorder?: [number, number, number, number];
|
||||
pivot?: [number, number];
|
||||
pixelsPerUnit?: number;
|
||||
/** Texture width (from import settings) | 纹理宽度(来自导入设置) */
|
||||
width?: number;
|
||||
/** Texture height (from import settings) | 纹理高度(来自导入设置) */
|
||||
height?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset database implementation
|
||||
* 资产数据库实现
|
||||
@@ -253,41 +212,6 @@ export class AssetDatabase {
|
||||
return guid ? this._metadata.get(guid) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get texture sprite info from metadata
|
||||
* 从元数据获取纹理 Sprite 信息
|
||||
*
|
||||
* Extracts spriteSettings from importSettings if available.
|
||||
* 如果可用,从 importSettings 提取 spriteSettings。
|
||||
*
|
||||
* @param guid - Texture asset GUID | 纹理资产 GUID
|
||||
* @returns Sprite info or undefined if not found/not a texture | Sprite 信息或未找到/非纹理则为 undefined
|
||||
*/
|
||||
getTextureSpriteInfo(guid: AssetGUID): ITextureSpriteInfo | undefined {
|
||||
const metadata = this._metadata.get(guid);
|
||||
if (!metadata) return undefined;
|
||||
|
||||
// Check if it's a texture asset
|
||||
// 检查是否是纹理资产
|
||||
if (metadata.type !== AssetType.Texture) return undefined;
|
||||
|
||||
// Extract spriteSettings from importSettings
|
||||
// 从 importSettings 提取 spriteSettings
|
||||
const importSettings = metadata.importSettings as Record<string, unknown> | undefined;
|
||||
const spriteSettings = importSettings?.spriteSettings as ISpriteSettings | undefined;
|
||||
|
||||
if (!spriteSettings) return undefined;
|
||||
|
||||
return {
|
||||
sliceBorder: spriteSettings.sliceBorder,
|
||||
pivot: spriteSettings.pivot,
|
||||
// Include dimensions from import settings if available
|
||||
// 如果可用,包含来自导入设置的尺寸
|
||||
width: spriteSettings.width,
|
||||
height: spriteSettings.height
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Find assets by type
|
||||
* 按类型查找资产
|
||||
|
||||
@@ -132,10 +132,7 @@ export class AssetManager implements IAssetManager {
|
||||
labels: [],
|
||||
tags: new Map(),
|
||||
lastModified: Date.now(),
|
||||
version: 1,
|
||||
// Include importSettings for sprite slicing (nine-patch), etc.
|
||||
// 包含 importSettings 以支持精灵切片(九宫格)等功能
|
||||
importSettings: entry.importSettings
|
||||
version: 1
|
||||
};
|
||||
|
||||
this._database.addAsset(metadata);
|
||||
@@ -176,11 +173,7 @@ export class AssetManager implements IAssetManager {
|
||||
}
|
||||
|
||||
// 创建加载器 / Create loader
|
||||
// 优先使用基于路径的加载器选择,支持多个加载器对应同一资产类型
|
||||
// 例如 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);
|
||||
let loader = this._loaderFactory.createLoader(metadata.type);
|
||||
|
||||
// 如果没有找到 loader 且类型是 Custom,尝试重新解析类型
|
||||
// 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/IAssetManager';
|
||||
export * from './interfaces/IAssetReader';
|
||||
export * from './interfaces/IAssetFileLoader';
|
||||
export * from './interfaces/IResourceComponent';
|
||||
|
||||
// Core
|
||||
@@ -57,31 +56,15 @@ export { BinaryLoader } from './loaders/BinaryLoader';
|
||||
export { AudioLoader } from './loaders/AudioLoader';
|
||||
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
|
||||
export { EngineIntegration } from './integration/EngineIntegration';
|
||||
export type { ITextureEngineBridge, TextureLoadCallback } from './integration/EngineIntegration';
|
||||
export type { ITextureEngineBridge } from './integration/EngineIntegration';
|
||||
|
||||
// Services
|
||||
export { SceneResourceManager } from './services/SceneResourceManager';
|
||||
export type { IResourceLoader } from './services/SceneResourceManager';
|
||||
export { PathResolutionService } from './services/PathResolutionService';
|
||||
|
||||
// Asset Metadata Service (primary API for sprite info)
|
||||
// 资产元数据服务(sprite 信息的主要 API)
|
||||
export {
|
||||
setGlobalAssetDatabase,
|
||||
getGlobalAssetDatabase,
|
||||
setGlobalEngineBridge,
|
||||
getGlobalEngineBridge,
|
||||
getTextureSpriteInfo
|
||||
} from './services/AssetMetadataService';
|
||||
export type { ITextureSpriteInfo } from './core/AssetDatabase';
|
||||
|
||||
// Utils
|
||||
export { UVHelper } from './utils/UVHelper';
|
||||
export {
|
||||
|
||||
@@ -31,6 +31,12 @@ export interface ITextureEngineBridge {
|
||||
*/
|
||||
unloadTexture(id: number): void;
|
||||
|
||||
/**
|
||||
* Get texture info
|
||||
* 获取纹理信息
|
||||
*/
|
||||
getTextureInfo(id: number): { width: number; height: number } | null;
|
||||
|
||||
/**
|
||||
* Get or load texture by path.
|
||||
* 按路径获取或加载纹理。
|
||||
@@ -103,20 +109,6 @@ export interface ITextureEngineBridge {
|
||||
* @returns Promise that resolves when texture is ready | 纹理就绪时解析的 Promise
|
||||
*/
|
||||
loadTextureAsync?(id: number, url: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Get texture info by path.
|
||||
* 通过路径获取纹理信息。
|
||||
*
|
||||
* This is the primary API for getting texture dimensions.
|
||||
* The Rust engine is the single source of truth for texture dimensions.
|
||||
* 这是获取纹理尺寸的主要 API。
|
||||
* Rust 引擎是纹理尺寸的唯一事实来源。
|
||||
*
|
||||
* @param path Image path/URL | 图片路径/URL
|
||||
* @returns Texture info or null if not loaded | 纹理信息或未加载则为 null
|
||||
*/
|
||||
getTextureInfoByPath?(path: string): { width: number; height: number } | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -139,43 +131,10 @@ interface DataAssetEntry {
|
||||
path: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Texture load callback type
|
||||
* 纹理加载回调类型
|
||||
*/
|
||||
export type TextureLoadCallback = (guid: string, path: string, textureId: number) => void;
|
||||
|
||||
/**
|
||||
* Asset system engine integration
|
||||
* 资产系统引擎集成
|
||||
*/
|
||||
/**
|
||||
* Texture sprite info (nine-patch border, pivot, etc.)
|
||||
* 纹理 Sprite 信息(九宫格边距、锚点等)
|
||||
*/
|
||||
export interface ITextureSpriteInfo {
|
||||
/**
|
||||
* 九宫格切片边距 [top, right, bottom, left]
|
||||
* Nine-patch slice border
|
||||
*/
|
||||
sliceBorder?: [number, number, number, number];
|
||||
/**
|
||||
* Sprite 锚点 [x, y](0-1 归一化)
|
||||
* Sprite pivot point (0-1 normalized)
|
||||
*/
|
||||
pivot?: [number, number];
|
||||
/**
|
||||
* 纹理宽度
|
||||
* Texture width
|
||||
*/
|
||||
width: number;
|
||||
/**
|
||||
* 纹理高度
|
||||
* Texture height
|
||||
*/
|
||||
height: number;
|
||||
}
|
||||
|
||||
export class EngineIntegration {
|
||||
private _assetManager: AssetManager;
|
||||
private _engineBridge?: ITextureEngineBridge;
|
||||
@@ -187,54 +146,6 @@ export class EngineIntegration {
|
||||
// Path-stable ID cache (persists across Play/Stop cycles)
|
||||
private static _pathIdCache = new Map<string, number>();
|
||||
|
||||
// 纹理 Sprite 信息缓存(全局静态,可供渲染系统访问)
|
||||
// Texture sprite info cache (global static, accessible by render systems)
|
||||
private static _textureSpriteInfoCache = new Map<AssetGUID, ITextureSpriteInfo>();
|
||||
|
||||
// 纹理加载回调(用于动态图集集成等)
|
||||
// Texture load callback (for dynamic atlas integration, etc.)
|
||||
private static _textureLoadCallbacks: TextureLoadCallback[] = [];
|
||||
|
||||
/**
|
||||
* Register a callback to be called when textures are loaded
|
||||
* 注册纹理加载时调用的回调
|
||||
*
|
||||
* This can be used for dynamic atlas integration.
|
||||
* 可用于动态图集集成。
|
||||
*
|
||||
* @param callback - Callback function | 回调函数
|
||||
*/
|
||||
static onTextureLoad(callback: TextureLoadCallback): void {
|
||||
if (!EngineIntegration._textureLoadCallbacks.includes(callback)) {
|
||||
EngineIntegration._textureLoadCallbacks.push(callback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a texture load callback
|
||||
* 移除纹理加载回调
|
||||
*/
|
||||
static removeTextureLoadCallback(callback: TextureLoadCallback): void {
|
||||
const index = EngineIntegration._textureLoadCallbacks.indexOf(callback);
|
||||
if (index >= 0) {
|
||||
EngineIntegration._textureLoadCallbacks.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify all callbacks of a texture load
|
||||
* 通知所有回调纹理已加载
|
||||
*/
|
||||
private static notifyTextureLoad(guid: string, path: string, textureId: number): void {
|
||||
for (const callback of EngineIntegration._textureLoadCallbacks) {
|
||||
try {
|
||||
callback(guid, path, textureId);
|
||||
} catch (e) {
|
||||
console.error('[EngineIntegration] Error in texture load callback:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Audio resource mappings | 音频资源映射
|
||||
private _audioIdMap = new Map<AssetGUID, number>();
|
||||
private _pathToAudioId = new Map<string, number>();
|
||||
@@ -368,16 +279,6 @@ export class EngineIntegration {
|
||||
const result = await this._assetManager.loadAsset<ITextureAsset>(guid);
|
||||
const metadata = result.metadata;
|
||||
const assetPath = metadata.path;
|
||||
const textureAsset = result.asset;
|
||||
|
||||
// 缓存 sprite 信息(九宫格边距等)到静态缓存
|
||||
// Cache sprite info (slice border, etc.) to static cache
|
||||
EngineIntegration._textureSpriteInfoCache.set(guid, {
|
||||
sliceBorder: textureAsset.sliceBorder,
|
||||
pivot: textureAsset.pivot,
|
||||
width: textureAsset.width,
|
||||
height: textureAsset.height
|
||||
});
|
||||
|
||||
// 生成路径稳定 ID
|
||||
// Generate path-stable ID
|
||||
@@ -408,37 +309,9 @@ export class EngineIntegration {
|
||||
this._textureIdMap.set(guid, stableId);
|
||||
this._pathToTextureId.set(assetPath, stableId);
|
||||
|
||||
// 通知回调(用于动态图集等)
|
||||
// Notify callbacks (for dynamic atlas, etc.)
|
||||
EngineIntegration.notifyTextureLoad(guid, engineUrl, stableId);
|
||||
|
||||
return stableId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get texture sprite info by GUID (static method for render system access)
|
||||
* 通过 GUID 获取纹理 Sprite 信息(静态方法,供渲染系统访问)
|
||||
*
|
||||
* Returns cached sprite info including nine-patch slice border.
|
||||
* Must call loadTextureByGuid first to populate the cache.
|
||||
* 返回缓存的 sprite 信息,包括九宫格边距。
|
||||
* 必须先调用 loadTextureByGuid 来填充缓存。
|
||||
*
|
||||
* @param guid - Texture asset GUID | 纹理资产 GUID
|
||||
* @returns Sprite info or undefined if not loaded | Sprite 信息或未加载则为 undefined
|
||||
*/
|
||||
static getTextureSpriteInfo(guid: AssetGUID): ITextureSpriteInfo | undefined {
|
||||
return EngineIntegration._textureSpriteInfoCache.get(guid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear texture sprite info cache
|
||||
* 清除纹理 Sprite 信息缓存
|
||||
*/
|
||||
static clearTextureSpriteInfoCache(): void {
|
||||
EngineIntegration._textureSpriteInfoCache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 注册自定义加载器
|
||||
*/
|
||||
registerLoader(type: AssetType, loader: IAssetLoader): void;
|
||||
|
||||
/**
|
||||
* Register a loader for a specific file extension
|
||||
* 为特定文件扩展名注册加载器
|
||||
*/
|
||||
registerExtensionLoader(extension: string, loader: IAssetLoader): void;
|
||||
|
||||
/**
|
||||
* Unregister loader
|
||||
* 注销加载器
|
||||
@@ -161,24 +144,6 @@ export interface ITextureAsset {
|
||||
hasMipmaps: boolean;
|
||||
/** 原始数据(如果可用) / Raw image data if available */
|
||||
data?: ImageData | HTMLImageElement;
|
||||
|
||||
// ===== Sprite Settings =====
|
||||
// ===== Sprite 设置 =====
|
||||
|
||||
/**
|
||||
* 九宫格切片边距 [top, right, bottom, left]
|
||||
* Nine-patch slice border
|
||||
*
|
||||
* Defines the non-stretchable borders for nine-patch rendering.
|
||||
* 定义九宫格渲染时不可拉伸的边框区域。
|
||||
*/
|
||||
sliceBorder?: [number, number, number, number];
|
||||
|
||||
/**
|
||||
* Sprite 锚点 [x, y](0-1 归一化)
|
||||
* Sprite pivot point (0-1 normalized)
|
||||
*/
|
||||
pivot?: [number, number];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -218,109 +183,24 @@ export interface IAudioAsset {
|
||||
channels: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shader property type
|
||||
* 着色器属性类型
|
||||
*/
|
||||
export type ShaderPropertyType = 'float' | 'vec2' | 'vec3' | 'vec4' | 'int' | 'sampler2D' | 'mat3' | 'mat4';
|
||||
|
||||
/**
|
||||
* Shader property definition
|
||||
* 着色器属性定义
|
||||
*/
|
||||
export interface IShaderProperty {
|
||||
/** 属性名称(uniform 名) / Property name (uniform name) */
|
||||
name: string;
|
||||
/** 属性类型 / Property type */
|
||||
type: ShaderPropertyType;
|
||||
/** 默认值 / Default value */
|
||||
default: number | number[];
|
||||
/** 显示名称(编辑器用) / Display name for editor */
|
||||
displayName?: string;
|
||||
/** 值范围(用于 float/int) / Value range for float/int */
|
||||
range?: [number, number];
|
||||
/** 是否隐藏(内部使用) / Hidden from inspector */
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shader asset interface
|
||||
* 着色器资产接口
|
||||
*
|
||||
* Shader assets contain GLSL source code and property definitions.
|
||||
* 着色器资产包含 GLSL 源代码和属性定义。
|
||||
*/
|
||||
export interface IShaderAsset {
|
||||
/** 着色器名称 / Shader name (e.g., "UI/Shiny") */
|
||||
name: string;
|
||||
/** 顶点着色器源代码 / Vertex shader GLSL source */
|
||||
vertex: string;
|
||||
/** 片段着色器源代码 / Fragment shader GLSL source */
|
||||
fragment: string;
|
||||
/** 属性定义列表 / Property definitions */
|
||||
properties: IShaderProperty[];
|
||||
/** 编译后的着色器 ID(运行时填充) / Compiled shader ID (runtime) */
|
||||
shaderId?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Material property value
|
||||
* 材质属性值
|
||||
*/
|
||||
export type MaterialPropertyValue = number | number[] | string;
|
||||
|
||||
/**
|
||||
* Material animator configuration
|
||||
* 材质动画器配置
|
||||
*/
|
||||
export interface IMaterialAnimator {
|
||||
/** 要动画的属性名 / Property to animate */
|
||||
property: string;
|
||||
/** 起始值 / Start value */
|
||||
from: number;
|
||||
/** 结束值 / End value */
|
||||
to: number;
|
||||
/** 持续时间(秒) / Duration in seconds */
|
||||
duration: number;
|
||||
/** 是否循环 / Loop animation */
|
||||
loop?: boolean;
|
||||
/** 循环间隔(秒) / Delay between loops */
|
||||
loopDelay?: number;
|
||||
/** 缓动函数 / Easing function */
|
||||
easing?: 'linear' | 'easeIn' | 'easeOut' | 'easeInOut';
|
||||
/** 是否自动播放 / Auto play on start */
|
||||
autoPlay?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Material asset interface
|
||||
* 材质资产接口
|
||||
*
|
||||
* Material assets reference a shader and define property values.
|
||||
* 材质资产引用着色器并定义属性值。
|
||||
*/
|
||||
export interface IMaterialAsset {
|
||||
/** 材质名称 / Material name */
|
||||
name: string;
|
||||
/** 着色器 GUID 或内置路径 / Shader GUID or built-in path (e.g., "builtin://shaders/Shiny") */
|
||||
/** 着色器名称 / Shader name */
|
||||
shader: string;
|
||||
/** 材质属性值 / Material property values */
|
||||
properties: Record<string, MaterialPropertyValue>;
|
||||
/** 纹理映射 / Texture slot mappings (property name -> texture GUID) */
|
||||
textures?: Record<string, AssetGUID>;
|
||||
/** 材质属性 / Material properties */
|
||||
properties: Map<string, unknown>;
|
||||
/** 纹理映射 / Texture slot mappings */
|
||||
textures: Map<string, AssetGUID>;
|
||||
/** 渲染状态 / Render states */
|
||||
renderStates?: {
|
||||
renderStates: {
|
||||
cullMode?: 'none' | 'front' | 'back';
|
||||
blendMode?: 'none' | 'alpha' | 'additive' | 'multiply' | 'screen';
|
||||
blendMode?: 'none' | 'alpha' | 'additive' | 'multiply';
|
||||
depthTest?: boolean;
|
||||
depthWrite?: boolean;
|
||||
};
|
||||
/** 动画器配置(可选) / Animator configuration (optional) */
|
||||
animator?: IMaterialAnimator;
|
||||
/** 运行时:编译后的着色器 ID / Runtime: compiled shader ID */
|
||||
_shaderId?: number;
|
||||
/** 运行时:引擎材质 ID / Runtime: engine material ID */
|
||||
_materialId?: number;
|
||||
}
|
||||
|
||||
// 预制体资产接口从专用文件导出 | Prefab asset interface exported from dedicated file
|
||||
@@ -382,235 +262,3 @@ export interface IBinaryAsset {
|
||||
/** MIME类型 / MIME type */
|
||||
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 { AudioLoader } from './AudioLoader';
|
||||
import { PrefabLoader } from './PrefabLoader';
|
||||
import { GLTFLoader } from './GLTFLoader';
|
||||
import { OBJLoader } from './OBJLoader';
|
||||
import { FBXLoader } from './FBXLoader';
|
||||
|
||||
/**
|
||||
* Asset loader factory
|
||||
* 资产加载器工厂
|
||||
*
|
||||
* Supports multiple loaders per asset type (selected by file extension).
|
||||
* 支持每种资产类型的多个加载器(按文件扩展名选择)。
|
||||
*/
|
||||
export class AssetLoaderFactory implements IAssetLoaderFactory {
|
||||
private readonly _loaders = new Map<AssetType, IAssetLoader>();
|
||||
|
||||
/** Extension -> Loader map for precise loader selection */
|
||||
/** 扩展名 -> 加载器映射,用于精确选择加载器 */
|
||||
private readonly _extensionLoaders = new Map<string, IAssetLoader>();
|
||||
|
||||
constructor() {
|
||||
// 注册默认加载器 / Register default loaders
|
||||
this.registerDefaultLoaders();
|
||||
@@ -56,34 +46,6 @@ export class AssetLoaderFactory implements IAssetLoaderFactory {
|
||||
|
||||
// 预制体加载器 / Prefab loader
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 注册自定义加载器
|
||||
@@ -159,16 +89,6 @@ export class AssetLoaderFactory implements IAssetLoaderFactory {
|
||||
*/
|
||||
getAssetTypeByExtension(extension: string): AssetType | null {
|
||||
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) {
|
||||
if (loader.supportedExtensions.some(e => e.toLowerCase() === ext)) {
|
||||
return type;
|
||||
@@ -236,22 +156,14 @@ export class AssetLoaderFactory implements IAssetLoaderFactory {
|
||||
getAllSupportedExtensions(): string[] {
|
||||
const extensions = new Set<string>();
|
||||
|
||||
// From type-based loaders
|
||||
// 从基于类型的加载器
|
||||
for (const loader of this._loaders.values()) {
|
||||
for (const ext of loader.supportedExtensions) {
|
||||
// 转换为 glob 模式 | Convert to glob pattern
|
||||
const cleanExt = ext.startsWith('.') ? ext.substring(1) : ext;
|
||||
extensions.add(`*.${cleanExt}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -264,8 +176,6 @@ export class AssetLoaderFactory implements IAssetLoaderFactory {
|
||||
getExtensionTypeMap(): Record<string, string> {
|
||||
const map: Record<string, string> = {};
|
||||
|
||||
// From type-based loaders
|
||||
// 从基于类型的加载器
|
||||
for (const [type, loader] of this._loaders) {
|
||||
for (const ext of loader.supportedExtensions) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sprite settings from texture meta
|
||||
* 纹理 meta 中的 Sprite 设置
|
||||
*/
|
||||
interface ISpriteSettings {
|
||||
sliceBorder?: [number, number, number, number];
|
||||
pivot?: [number, number];
|
||||
pixelsPerUnit?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取全局引擎桥接
|
||||
* Get global engine bridge
|
||||
@@ -71,22 +61,13 @@ export class TextureLoader implements IAssetLoader<ITextureAsset> {
|
||||
|
||||
const image = content.image;
|
||||
|
||||
// Read sprite settings from import settings
|
||||
// 从导入设置读取 sprite 设置
|
||||
const importSettings = context.metadata.importSettings as Record<string, unknown> | undefined;
|
||||
const spriteSettings = importSettings?.spriteSettings as ISpriteSettings | undefined;
|
||||
|
||||
const textureAsset: ITextureAsset = {
|
||||
textureId: TextureLoader._nextTextureId++,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
format: 'rgba',
|
||||
hasMipmaps: false,
|
||||
data: image,
|
||||
// Include sprite settings if available
|
||||
// 如果有则包含 sprite 设置
|
||||
sliceBorder: spriteSettings?.sliceBorder,
|
||||
pivot: spriteSettings?.pivot
|
||||
data: image
|
||||
};
|
||||
|
||||
// Upload to GPU if bridge exists.
|
||||
|
||||
@@ -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',
|
||||
/** 网格 */
|
||||
Mesh: 'mesh',
|
||||
/** 3D模型 (GLTF/GLB) | 3D Model */
|
||||
Model3D: 'model3d',
|
||||
/** 材质 */
|
||||
Material: 'material',
|
||||
/** 着色器 */
|
||||
@@ -408,12 +406,6 @@ export interface IAssetCatalogEntry {
|
||||
|
||||
/** 可用变体 / Available variants (platform/quality specific) */
|
||||
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": {
|
||||
"@esengine/behavior-tree": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@esengine/editor-core": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@esengine/ecs-framework": "workspace:*",
|
||||
"@esengine/engine-core": "workspace:*",
|
||||
|
||||
@@ -24,64 +24,64 @@ export interface GlobalBlackboardVariable {
|
||||
* 管理跨行为树共享的全局变量
|
||||
*/
|
||||
export class GlobalBlackboardService {
|
||||
private static _instance: GlobalBlackboardService;
|
||||
private _variables: Map<string, GlobalBlackboardVariable> = new Map();
|
||||
private _changeCallbacks: Array<() => void> = [];
|
||||
private _projectPath: string | null = null;
|
||||
private static instance: GlobalBlackboardService;
|
||||
private variables: Map<string, GlobalBlackboardVariable> = new Map();
|
||||
private changeCallbacks: Array<() => void> = [];
|
||||
private projectPath: string | null = null;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
static getInstance(): GlobalBlackboardService {
|
||||
if (!this._instance) {
|
||||
this._instance = new GlobalBlackboardService();
|
||||
if (!this.instance) {
|
||||
this.instance = new GlobalBlackboardService();
|
||||
}
|
||||
return this._instance;
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置项目路径
|
||||
*/
|
||||
setProjectPath(path: string | null): void {
|
||||
this._projectPath = path;
|
||||
this.projectPath = path;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目路径
|
||||
*/
|
||||
getProjectPath(): string | null {
|
||||
return this._projectPath;
|
||||
return this.projectPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加全局变量
|
||||
*/
|
||||
addVariable(variable: GlobalBlackboardVariable): void {
|
||||
if (this._variables.has(variable.key)) {
|
||||
if (this.variables.has(variable.key)) {
|
||||
throw new Error(`全局变量 "${variable.key}" 已存在`);
|
||||
}
|
||||
this._variables.set(variable.key, variable);
|
||||
this._notifyChange();
|
||||
this.variables.set(variable.key, variable);
|
||||
this.notifyChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新全局变量
|
||||
*/
|
||||
updateVariable(key: string, updates: Partial<Omit<GlobalBlackboardVariable, 'key'>>): void {
|
||||
const variable = this._variables.get(key);
|
||||
const variable = this.variables.get(key);
|
||||
if (!variable) {
|
||||
throw new Error(`全局变量 "${key}" 不存在`);
|
||||
}
|
||||
this._variables.set(key, { ...variable, ...updates });
|
||||
this._notifyChange();
|
||||
this.variables.set(key, { ...variable, ...updates });
|
||||
this.notifyChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除全局变量
|
||||
*/
|
||||
deleteVariable(key: string): boolean {
|
||||
const result = this._variables.delete(key);
|
||||
const result = this.variables.delete(key);
|
||||
if (result) {
|
||||
this._notifyChange();
|
||||
this.notifyChange();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -90,36 +90,36 @@ export class GlobalBlackboardService {
|
||||
* 重命名全局变量
|
||||
*/
|
||||
renameVariable(oldKey: string, newKey: string): void {
|
||||
if (!this._variables.has(oldKey)) {
|
||||
if (!this.variables.has(oldKey)) {
|
||||
throw new Error(`全局变量 "${oldKey}" 不存在`);
|
||||
}
|
||||
if (this._variables.has(newKey)) {
|
||||
if (this.variables.has(newKey)) {
|
||||
throw new Error(`全局变量 "${newKey}" 已存在`);
|
||||
}
|
||||
|
||||
const variable = this._variables.get(oldKey)!;
|
||||
this._variables.delete(oldKey);
|
||||
this._variables.set(newKey, { ...variable, key: newKey });
|
||||
this._notifyChange();
|
||||
const variable = this.variables.get(oldKey)!;
|
||||
this.variables.delete(oldKey);
|
||||
this.variables.set(newKey, { ...variable, key: newKey });
|
||||
this.notifyChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取全局变量
|
||||
*/
|
||||
getVariable(key: string): GlobalBlackboardVariable | undefined {
|
||||
return this._variables.get(key);
|
||||
return this.variables.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有全局变量
|
||||
*/
|
||||
getAllVariables(): GlobalBlackboardVariable[] {
|
||||
return Array.from(this._variables.values());
|
||||
return Array.from(this.variables.values());
|
||||
}
|
||||
|
||||
getVariablesMap(): 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;
|
||||
}
|
||||
return map;
|
||||
@@ -129,15 +129,15 @@ export class GlobalBlackboardService {
|
||||
* 检查变量是否存在
|
||||
*/
|
||||
hasVariable(key: string): boolean {
|
||||
return this._variables.has(key);
|
||||
return this.variables.has(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有变量
|
||||
*/
|
||||
clear(): void {
|
||||
this._variables.clear();
|
||||
this._notifyChange();
|
||||
this.variables.clear();
|
||||
this.notifyChange();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -146,7 +146,7 @@ export class GlobalBlackboardService {
|
||||
toConfig(): GlobalBlackboardConfig {
|
||||
const variables: BlackboardVariable[] = [];
|
||||
|
||||
for (const variable of this._variables.values()) {
|
||||
for (const variable of this.variables.values()) {
|
||||
variables.push({
|
||||
name: variable.key,
|
||||
type: variable.type,
|
||||
@@ -162,11 +162,11 @@ export class GlobalBlackboardService {
|
||||
* 从配置导入
|
||||
*/
|
||||
fromConfig(config: GlobalBlackboardConfig): void {
|
||||
this._variables.clear();
|
||||
this.variables.clear();
|
||||
|
||||
if (config.variables && Array.isArray(config.variables)) {
|
||||
for (const variable of config.variables) {
|
||||
this._variables.set(variable.name, {
|
||||
this.variables.set(variable.name, {
|
||||
key: variable.name,
|
||||
type: variable.type,
|
||||
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 {
|
||||
this._changeCallbacks.push(callback);
|
||||
this.changeCallbacks.push(callback);
|
||||
return () => {
|
||||
const index = this._changeCallbacks.indexOf(callback);
|
||||
const index = this.changeCallbacks.indexOf(callback);
|
||||
if (index > -1) {
|
||||
this._changeCallbacks.splice(index, 1);
|
||||
this.changeCallbacks.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private _notifyChange(): void {
|
||||
this._changeCallbacks.forEach((cb) => {
|
||||
private notifyChange(): void {
|
||||
this.changeCallbacks.forEach((cb) => {
|
||||
try {
|
||||
cb();
|
||||
} catch (error) {
|
||||
|
||||
@@ -536,15 +536,15 @@ export const useBehaviorTreeDataStore = create<BehaviorTreeDataState>((set, get)
|
||||
* 将 Zustand Store 适配为 ITreeState 接口
|
||||
*/
|
||||
export class TreeStateAdapter implements ITreeState {
|
||||
private static _instance: TreeStateAdapter | null = null;
|
||||
private static instance: TreeStateAdapter | null = null;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
static getInstance(): TreeStateAdapter {
|
||||
if (!TreeStateAdapter._instance) {
|
||||
TreeStateAdapter._instance = new TreeStateAdapter();
|
||||
if (!TreeStateAdapter.instance) {
|
||||
TreeStateAdapter.instance = new TreeStateAdapter();
|
||||
}
|
||||
return TreeStateAdapter._instance;
|
||||
return TreeStateAdapter.instance;
|
||||
}
|
||||
|
||||
getTree(): BehaviorTree {
|
||||
|
||||
@@ -36,63 +36,63 @@ export interface NodePropertyConfig {
|
||||
* 提供编辑器级别的节点注册和管理功能
|
||||
*/
|
||||
export class NodeRegistryService {
|
||||
private static _instance: NodeRegistryService;
|
||||
private _customTemplates: Map<string, NodeTemplate> = new Map();
|
||||
private _registrationCallbacks: Array<(template: NodeTemplate) => void> = [];
|
||||
private static instance: NodeRegistryService;
|
||||
private customTemplates: Map<string, NodeTemplate> = new Map();
|
||||
private registrationCallbacks: Array<(template: NodeTemplate) => void> = [];
|
||||
|
||||
private constructor() {}
|
||||
|
||||
static getInstance(): NodeRegistryService {
|
||||
if (!this._instance) {
|
||||
this._instance = new NodeRegistryService();
|
||||
if (!this.instance) {
|
||||
this.instance = new NodeRegistryService();
|
||||
}
|
||||
return this._instance;
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册自定义节点类型
|
||||
*/
|
||||
registerNode(config: NodeRegistrationConfig): void {
|
||||
const nodeType = this._mapStringToNodeType(config.type);
|
||||
const nodeType = this.mapStringToNodeType(config.type);
|
||||
|
||||
const metadata: NodeMetadata = {
|
||||
implementationType: config.implementationType,
|
||||
nodeType: nodeType,
|
||||
displayName: config.displayName,
|
||||
description: config.description || '',
|
||||
category: config.category || this._getDefaultCategory(config.type),
|
||||
configSchema: this._convertPropertiesToSchema(config.properties || []),
|
||||
childrenConstraints: this._getChildrenConstraints(config)
|
||||
category: config.category || this.getDefaultCategory(config.type),
|
||||
configSchema: this.convertPropertiesToSchema(config.properties || []),
|
||||
childrenConstraints: this.getChildrenConstraints(config)
|
||||
};
|
||||
|
||||
class DummyExecutor {}
|
||||
NodeMetadataRegistry.register(DummyExecutor, metadata);
|
||||
|
||||
const template = this._createTemplate(config, metadata);
|
||||
this._customTemplates.set(config.implementationType, template);
|
||||
const template = this.createTemplate(config, metadata);
|
||||
this.customTemplates.set(config.implementationType, template);
|
||||
|
||||
this._registrationCallbacks.forEach((cb) => cb(template));
|
||||
this.registrationCallbacks.forEach((cb) => cb(template));
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销节点类型
|
||||
*/
|
||||
unregisterNode(implementationType: string): boolean {
|
||||
return this._customTemplates.delete(implementationType);
|
||||
return this.customTemplates.delete(implementationType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有自定义模板
|
||||
*/
|
||||
getCustomTemplates(): NodeTemplate[] {
|
||||
return Array.from(this._customTemplates.values());
|
||||
return Array.from(this.customTemplates.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查节点类型是否已注册
|
||||
*/
|
||||
hasNode(implementationType: string): boolean {
|
||||
return this._customTemplates.has(implementationType) ||
|
||||
return this.customTemplates.has(implementationType) ||
|
||||
NodeMetadataRegistry.getMetadata(implementationType) !== undefined;
|
||||
}
|
||||
|
||||
@@ -100,16 +100,16 @@ export class NodeRegistryService {
|
||||
* 监听节点注册事件
|
||||
*/
|
||||
onNodeRegistered(callback: (template: NodeTemplate) => void): () => void {
|
||||
this._registrationCallbacks.push(callback);
|
||||
this.registrationCallbacks.push(callback);
|
||||
return () => {
|
||||
const index = this._registrationCallbacks.indexOf(callback);
|
||||
const index = this.registrationCallbacks.indexOf(callback);
|
||||
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) {
|
||||
case 'composite': return NodeType.Composite;
|
||||
case 'decorator': return NodeType.Decorator;
|
||||
@@ -119,7 +119,7 @@ export class NodeRegistryService {
|
||||
}
|
||||
}
|
||||
|
||||
private _getDefaultCategory(type: string): string {
|
||||
private getDefaultCategory(type: string): string {
|
||||
switch (type) {
|
||||
case 'composite': 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> = {};
|
||||
|
||||
for (const prop of properties) {
|
||||
schema[prop.name] = {
|
||||
type: this._mapPropertyType(prop.type),
|
||||
type: this.mapPropertyType(prop.type),
|
||||
default: prop.defaultValue,
|
||||
description: prop.description,
|
||||
min: prop.min,
|
||||
@@ -146,7 +146,7 @@ export class NodeRegistryService {
|
||||
return schema;
|
||||
}
|
||||
|
||||
private _mapPropertyType(type: string): string {
|
||||
private mapPropertyType(type: string): string {
|
||||
switch (type) {
|
||||
case 'string':
|
||||
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) {
|
||||
return {
|
||||
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 = {
|
||||
nodeType: config.type
|
||||
};
|
||||
@@ -212,10 +212,10 @@ export class NodeRegistryService {
|
||||
const template: NodeTemplate = {
|
||||
type: metadata.nodeType,
|
||||
displayName: config.displayName,
|
||||
category: config.category || this._getDefaultCategory(config.type),
|
||||
category: config.category || this.getDefaultCategory(config.type),
|
||||
description: config.description || '',
|
||||
icon: config.icon || this._getDefaultIcon(config.type),
|
||||
color: config.color || this._getDefaultColor(config.type),
|
||||
icon: config.icon || this.getDefaultIcon(config.type),
|
||||
color: config.color || this.getDefaultColor(config.type),
|
||||
className: config.implementationType,
|
||||
defaultConfig,
|
||||
properties: (config.properties || []).map((p) => ({
|
||||
@@ -242,7 +242,7 @@ export class NodeRegistryService {
|
||||
return template;
|
||||
}
|
||||
|
||||
private _getDefaultIcon(type: string): string {
|
||||
private getDefaultIcon(type: string): string {
|
||||
switch (type) {
|
||||
case 'composite': return 'GitBranch';
|
||||
case 'decorator': return 'Settings';
|
||||
@@ -252,7 +252,7 @@ export class NodeRegistryService {
|
||||
}
|
||||
}
|
||||
|
||||
private _getDefaultColor(type: string): string {
|
||||
private getDefaultColor(type: string): string {
|
||||
switch (type) {
|
||||
case 'composite': return '#1976d2';
|
||||
case 'decorator': return '#fb8c00';
|
||||
|
||||
@@ -4,33 +4,33 @@ import type { MessageHub } from '@esengine/editor-runtime';
|
||||
const logger = createLogger('NotificationService');
|
||||
|
||||
export class NotificationService {
|
||||
private static _instance: NotificationService;
|
||||
private _messageHub: MessageHub | null = null;
|
||||
private static instance: NotificationService;
|
||||
private messageHub: MessageHub | null = null;
|
||||
|
||||
private constructor() {
|
||||
// 延迟获取 MessageHub,因为初始化时可能还不可用
|
||||
}
|
||||
|
||||
private _getMessageHub(): MessageHub | null {
|
||||
if (!this._messageHub && PluginAPI.isAvailable) {
|
||||
private getMessageHub(): MessageHub | null {
|
||||
if (!this.messageHub && PluginAPI.isAvailable) {
|
||||
try {
|
||||
this._messageHub = PluginAPI.messageHub;
|
||||
this.messageHub = PluginAPI.messageHub;
|
||||
} catch (error) {
|
||||
logger.warn('MessageHub not available');
|
||||
}
|
||||
}
|
||||
return this._messageHub;
|
||||
return this.messageHub;
|
||||
}
|
||||
|
||||
public static getInstance(): NotificationService {
|
||||
if (!NotificationService._instance) {
|
||||
NotificationService._instance = new NotificationService();
|
||||
if (!NotificationService.instance) {
|
||||
NotificationService.instance = new NotificationService();
|
||||
}
|
||||
return NotificationService._instance;
|
||||
return NotificationService.instance;
|
||||
}
|
||||
|
||||
public showToast(message: string, type: 'success' | 'error' | 'warning' | 'info' = 'info'): void {
|
||||
const hub = this._getMessageHub();
|
||||
const hub = this.getMessageHub();
|
||||
if (!hub) {
|
||||
logger.info(`[Toast ${type}] ${message}`);
|
||||
return;
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
{ "path": "../core" },
|
||||
{ "path": "../engine-core" },
|
||||
{ "path": "../editor-core" },
|
||||
{ "path": "../ui" },
|
||||
{ "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": {
|
||||
"@esengine/blueprint": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@esengine/editor-core": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@esengine/ecs-framework": "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
|
||||
export * from './runtime';
|
||||
|
||||
// Triggers
|
||||
export * from './triggers';
|
||||
|
||||
// Composition
|
||||
export * from './composition';
|
||||
|
||||
// Nodes (import to register)
|
||||
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 事件节点 - 蓝图执行的入口点
|
||||
* @en Event Nodes - Entry points for blueprint execution
|
||||
* Event Nodes - Entry points for blueprint execution
|
||||
* 事件节点 - 蓝图执行的入口点
|
||||
*/
|
||||
|
||||
// 生命周期事件 | Lifecycle events
|
||||
export * from './EventBeginPlay';
|
||||
export * from './EventTick';
|
||||
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 { BlueprintVM } from './BlueprintVM';
|
||||
import { IEntity, IScene } from './ExecutionContext';
|
||||
|
||||
/**
|
||||
* Component interface for ECS integration
|
||||
@@ -56,7 +56,7 @@ export function createBlueprintComponentData(): IBlueprintComponent {
|
||||
*/
|
||||
export function initializeBlueprintVM(
|
||||
component: IBlueprintComponent,
|
||||
entity: Entity,
|
||||
entity: IEntity,
|
||||
scene: IScene
|
||||
): void {
|
||||
if (!component.blueprintAsset) {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* 蓝图执行系统 - 管理蓝图生命周期和执行
|
||||
*/
|
||||
|
||||
import type { Entity, IScene } from '@esengine/ecs-framework';
|
||||
import {
|
||||
IBlueprintComponent,
|
||||
initializeBlueprintVM,
|
||||
@@ -11,6 +10,7 @@ import {
|
||||
tickBlueprint,
|
||||
cleanupBlueprint
|
||||
} from './BlueprintComponent';
|
||||
import { IEntity, IScene } from './ExecutionContext';
|
||||
|
||||
/**
|
||||
* Blueprint system interface for engine integration
|
||||
@@ -31,7 +31,7 @@ export interface IBlueprintSystem {
|
||||
* Entity with blueprint component
|
||||
* 带有蓝图组件的实体
|
||||
*/
|
||||
export interface IBlueprintEntity extends Entity {
|
||||
export interface IBlueprintEntity extends IEntity {
|
||||
/** Blueprint component data (蓝图组件数据) */
|
||||
blueprintComponent: IBlueprintComponent;
|
||||
}
|
||||
|
||||
@@ -3,10 +3,9 @@
|
||||
* 蓝图虚拟机 - 执行蓝图图
|
||||
*/
|
||||
|
||||
import type { Entity, IScene } from '@esengine/ecs-framework';
|
||||
import { BlueprintNode } from '../types/nodes';
|
||||
import { BlueprintAsset } from '../types/blueprint';
|
||||
import { ExecutionContext, ExecutionResult } from './ExecutionContext';
|
||||
import { ExecutionContext, ExecutionResult, IEntity, IScene } from './ExecutionContext';
|
||||
import { NodeRegistry } from './NodeRegistry';
|
||||
|
||||
/**
|
||||
@@ -58,7 +57,7 @@ export class BlueprintVM {
|
||||
/** Debug mode (调试模式) */
|
||||
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._cacheEventNodes();
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* 执行上下文 - 蓝图执行的运行时上下文
|
||||
*/
|
||||
|
||||
import type { Entity, IScene } from '@esengine/ecs-framework';
|
||||
import { BlueprintNode, BlueprintConnection } from '../types/nodes';
|
||||
import { BlueprintAsset } from '../types/blueprint';
|
||||
|
||||
@@ -43,6 +42,31 @@ export interface ExecutionResult {
|
||||
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
|
||||
* 执行上下文提供对运行时服务的访问
|
||||
@@ -52,7 +76,7 @@ export class ExecutionContext {
|
||||
readonly blueprint: BlueprintAsset;
|
||||
|
||||
/** Owner entity (所有者实体) */
|
||||
readonly entity: Entity;
|
||||
readonly entity: IEntity;
|
||||
|
||||
/** Current scene (当前场景) */
|
||||
readonly scene: IScene;
|
||||
@@ -81,7 +105,7 @@ export class ExecutionContext {
|
||||
/** Connection lookup by source (按源的连接查找) */
|
||||
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.entity = entity;
|
||||
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',
|
||||
'immer',
|
||||
|
||||
// Tauri (由宿主应用提供) | Provided by host app
|
||||
/^@tauri-apps\//,
|
||||
|
||||
// 所有 @esengine 包
|
||||
/^@esengine\//,
|
||||
] 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",
|
||||
"clean": "rimraf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@esengine/camera": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@esengine/editor-core": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@esengine/ecs-framework": "workspace:*",
|
||||
"@esengine/engine-core": "workspace:*",
|
||||
"@esengine/camera": "workspace:*",
|
||||
"@esengine/editor-core": "workspace:*",
|
||||
"@esengine/build-config": "workspace:*",
|
||||
"react": "^18.3.1",
|
||||
|
||||
@@ -15,13 +15,13 @@ import type {
|
||||
import {
|
||||
EntityStoreService,
|
||||
MessageHub,
|
||||
EditorComponentRegistry
|
||||
ComponentRegistry
|
||||
} from '@esengine/editor-core';
|
||||
import { CameraComponent } from '@esengine/camera';
|
||||
|
||||
export class CameraEditorModule implements IEditorModuleLoader {
|
||||
async install(services: ServiceContainer): Promise<void> {
|
||||
const componentRegistry = services.resolve(EditorComponentRegistry);
|
||||
const componentRegistry = services.resolve(ComponentRegistry);
|
||||
if (componentRegistry) {
|
||||
componentRegistry.register({
|
||||
name: 'Camera',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { IComponentRegistry, IScene } from '@esengine/ecs-framework';
|
||||
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 { CameraSystem } from './CameraSystem';
|
||||
|
||||
@@ -10,15 +10,15 @@ class CameraRuntimeModule implements IRuntimeModule {
|
||||
}
|
||||
|
||||
createSystems(scene: IScene, context: SystemContext): void {
|
||||
// 从服务注册表获取渲染配置服务 | Get render config service from registry
|
||||
const renderConfig = context.services.get(RenderConfigServiceToken);
|
||||
if (!renderConfig) {
|
||||
console.warn('[CameraPlugin] RenderConfigService not found, CameraSystem will not be created');
|
||||
// 从服务注册表获取 EngineBridge | Get EngineBridge from service registry
|
||||
const bridge = context.services.get(EngineBridgeToken);
|
||||
if (!bridge) {
|
||||
console.warn('[CameraPlugin] EngineBridge not found, CameraSystem will not be created');
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建并添加 CameraSystem | Create and add CameraSystem
|
||||
const cameraSystem = new CameraSystem(renderConfig);
|
||||
const cameraSystem = new CameraSystem(bridge);
|
||||
scene.addSystem(cameraSystem);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,18 +4,18 @@
|
||||
*/
|
||||
|
||||
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';
|
||||
|
||||
@ECSSystem('Camera', { updateOrder: -100 })
|
||||
export class CameraSystem extends EntitySystem {
|
||||
private renderConfig: IRenderConfigService;
|
||||
private bridge: IEngineBridge;
|
||||
private lastAppliedCameraId: number | null = null;
|
||||
|
||||
constructor(renderConfig: IRenderConfigService) {
|
||||
constructor(bridge: IEngineBridge) {
|
||||
// Match entities with CameraComponent
|
||||
super(Matcher.empty().all(CameraComponent));
|
||||
this.renderConfig = renderConfig;
|
||||
this.bridge = bridge;
|
||||
}
|
||||
|
||||
protected override onBegin(): void {
|
||||
@@ -47,6 +47,6 @@ export class CameraSystem extends EntitySystem {
|
||||
const r = parseInt(bgColor.slice(1, 3), 16) / 255;
|
||||
const g = parseInt(bgColor.slice(3, 5), 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",
|
||||
"version": "2.4.2",
|
||||
"version": "2.4.0",
|
||||
"description": "用于Laya、Cocos Creator等JavaScript游戏引擎的高性能ECS框架",
|
||||
"main": "dist/index.cjs",
|
||||
"module": "dist/index.mjs",
|
||||
|
||||
+188
-168
@@ -18,72 +18,69 @@ import { DebugConfigService } from './Utils/Debug/DebugConfigService';
|
||||
import { createInstance } from './Core/DI/Decorators';
|
||||
|
||||
/**
|
||||
* @zh 游戏引擎核心类
|
||||
* @en Game engine core class
|
||||
* 游戏引擎核心类
|
||||
*
|
||||
* @zh 职责:
|
||||
* 职责:
|
||||
* - 提供全局服务(Timer、Performance、Pool等)
|
||||
* - 管理场景生命周期(内置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
|
||||
* ```typescript
|
||||
* // @zh 初始化并设置场景 | @en Initialize and set scene
|
||||
* // 初始化并设置场景
|
||||
* Core.create({ debug: true });
|
||||
* Core.setScene(new GameScene());
|
||||
*
|
||||
* // @zh 游戏循环(自动更新全局服务和场景)| @en Game loop (auto-updates global services and scene)
|
||||
* // 游戏循环(自动更新全局服务和场景)
|
||||
* function gameLoop(deltaTime: number) {
|
||||
* Core.update(deltaTime);
|
||||
* }
|
||||
*
|
||||
* // @zh 使用定时器 | @en Use 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.setScene(new GameScene()); // @zh 立即切换 | @en Immediate switch
|
||||
* // 切换场景
|
||||
* Core.loadScene(new MenuScene()); // 延迟切换
|
||||
* Core.setScene(new GameScene()); // 立即切换
|
||||
*
|
||||
* // @zh 获取当前场景 | @en Get current scene
|
||||
* // 获取当前场景
|
||||
* const currentScene = Core.scene;
|
||||
* ```
|
||||
*/
|
||||
export class Core {
|
||||
/**
|
||||
* @zh 游戏暂停状态,当设置为true时,游戏循环将暂停执行
|
||||
* @en Game paused state, when set to true, game loop will pause execution
|
||||
* 游戏暂停状态
|
||||
*
|
||||
* 当设置为true时,游戏循环将暂停执行。
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* @zh Core专用日志器
|
||||
* @en Core logger
|
||||
* Core专用日志器
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* @zh 服务容器,管理所有服务的注册、解析和生命周期
|
||||
* @en Service container for managing registration, resolution, and lifecycle of all services
|
||||
* 服务容器
|
||||
*
|
||||
* 管理所有服务的注册、解析和生命周期。
|
||||
*/
|
||||
private _serviceContainer: ServiceContainer;
|
||||
|
||||
@@ -93,79 +90,110 @@ export class Core {
|
||||
private _debugManager?: DebugManager;
|
||||
|
||||
/**
|
||||
* @zh 场景管理器,管理当前场景的生命周期
|
||||
* @en Scene manager for managing current scene lifecycle
|
||||
* 场景管理器
|
||||
*
|
||||
* 管理当前场景的生命周期。
|
||||
*/
|
||||
private _sceneManager: SceneManager;
|
||||
|
||||
/**
|
||||
* @zh World管理器,管理多个独立的World实例(可选)
|
||||
* @en World manager for managing multiple independent World instances (optional)
|
||||
* World管理器
|
||||
*
|
||||
* 管理多个独立的World实例(可选)。
|
||||
*/
|
||||
private _worldManager: WorldManager;
|
||||
|
||||
/**
|
||||
* @zh 插件管理器,管理所有插件的生命周期
|
||||
* @en Plugin manager for managing all plugin lifecycles
|
||||
* 插件管理器
|
||||
*
|
||||
* 管理所有插件的生命周期。
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* @zh Core配置
|
||||
* @en Core configuration
|
||||
* Core配置
|
||||
*/
|
||||
private _config: ICoreConfig;
|
||||
|
||||
/**
|
||||
* @zh 创建核心实例
|
||||
* @en Create core instance
|
||||
* 创建核心实例
|
||||
*
|
||||
* @param config - @zh Core配置对象 @en Core configuration object
|
||||
* @param config - Core配置对象
|
||||
*/
|
||||
private constructor(config: ICoreConfig = {}) {
|
||||
Core._instance = this;
|
||||
this._config = { debug: true, ...config };
|
||||
|
||||
// 保存配置
|
||||
this._config = {
|
||||
debug: true,
|
||||
...config
|
||||
};
|
||||
|
||||
// 初始化服务容器
|
||||
this._serviceContainer = new ServiceContainer();
|
||||
|
||||
// 初始化定时器管理器
|
||||
this._timerManager = new TimerManager();
|
||||
this._serviceContainer.registerInstance(TimerManager, this._timerManager);
|
||||
|
||||
// 初始化性能监控器
|
||||
this._performanceMonitor = new PerformanceMonitor();
|
||||
this._serviceContainer.registerInstance(PerformanceMonitor, this._performanceMonitor);
|
||||
|
||||
// 在调试模式下启用性能监控
|
||||
if (this._config.debug) {
|
||||
this._performanceMonitor.enable();
|
||||
}
|
||||
|
||||
// 初始化对象池管理器
|
||||
this._poolManager = new PoolManager();
|
||||
this._serviceContainer.registerInstance(PoolManager, this._poolManager);
|
||||
|
||||
// 初始化场景管理器
|
||||
this._sceneManager = new SceneManager(this._performanceMonitor);
|
||||
this._serviceContainer.registerInstance(SceneManager, this._sceneManager);
|
||||
this._sceneManager.setSceneChangedCallback(() => 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._serviceContainer.registerInstance(WorldManager, this._worldManager);
|
||||
|
||||
// 初始化插件管理器
|
||||
this._pluginManager = new PluginManager();
|
||||
this._pluginManager.initialize(this, this._serviceContainer);
|
||||
this._serviceContainer.registerInstance(PluginManager, this._pluginManager);
|
||||
|
||||
// 初始化插件服务注册表
|
||||
// Initialize plugin service registry
|
||||
this._pluginServiceRegistry = new PluginServiceRegistry();
|
||||
this._serviceContainer.registerInstance(PluginServiceRegistry, this._pluginServiceRegistry);
|
||||
|
||||
this.debug = this._config.debug ?? true;
|
||||
|
||||
// 初始化调试管理器
|
||||
if (this._config.debugConfig?.enabled) {
|
||||
const configService = new DebugConfigService();
|
||||
configService.setConfig(this._config.debugConfig);
|
||||
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.onInitialize();
|
||||
}
|
||||
@@ -174,88 +202,82 @@ export class Core {
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取核心实例
|
||||
* @en Get core instance
|
||||
* 获取核心实例
|
||||
*
|
||||
* @returns @zh 全局核心实例 @en Global core instance
|
||||
* @returns 全局核心实例
|
||||
*/
|
||||
public static get Instance() {
|
||||
return this._instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取服务容器
|
||||
* @en Get service container
|
||||
* 获取服务容器
|
||||
*
|
||||
* @zh 用于注册和解析自定义服务。
|
||||
* @en Used for registering and resolving custom services.
|
||||
* 用于注册和解析自定义服务。
|
||||
*
|
||||
* @returns @zh 服务容器实例 @en Service container instance
|
||||
* @throws @zh 如果Core实例未创建 @en If Core instance is not created
|
||||
* @returns 服务容器实例
|
||||
* @throws 如果Core实例未创建
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // @zh 注册自定义服务 | @en Register custom service
|
||||
* // 注册自定义服务
|
||||
* Core.services.registerSingleton(MyService);
|
||||
*
|
||||
* // @zh 解析服务 | @en Resolve service
|
||||
* // 解析服务
|
||||
* const myService = Core.services.resolve(MyService);
|
||||
* ```
|
||||
*/
|
||||
public static get services(): ServiceContainer {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取插件服务注册表
|
||||
* @en Get plugin service registry
|
||||
* 获取插件服务注册表
|
||||
*
|
||||
* @zh 用于基于 ServiceToken 的类型安全服务注册和获取。
|
||||
* @en For type-safe service registration and retrieval based on ServiceToken.
|
||||
* 用于基于 ServiceToken 的类型安全服务注册和获取。
|
||||
* For type-safe service registration and retrieval based on ServiceToken.
|
||||
*
|
||||
* @returns @zh PluginServiceRegistry 实例 @en PluginServiceRegistry instance
|
||||
* @throws @zh 如果 Core 实例未创建 @en If Core instance is not created
|
||||
* @returns PluginServiceRegistry 实例
|
||||
* @throws 如果 Core 实例未创建
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { createServiceToken } from '@esengine/ecs-framework';
|
||||
*
|
||||
* // @zh 定义服务令牌 | @en Define service token
|
||||
* // 定义服务令牌
|
||||
* const MyServiceToken = createServiceToken<IMyService>('myService');
|
||||
*
|
||||
* // @zh 注册服务 | @en Register service
|
||||
* // 注册服务
|
||||
* Core.pluginServices.register(MyServiceToken, myServiceInstance);
|
||||
*
|
||||
* // @zh 获取服务(可选)| @en Get service (optional)
|
||||
* // 获取服务(可选)
|
||||
* const service = Core.pluginServices.get(MyServiceToken);
|
||||
*
|
||||
* // @zh 获取服务(必需,不存在则抛异常)| @en Get service (required, throws if not found)
|
||||
* // 获取服务(必需,不存在则抛异常)
|
||||
* const service = Core.pluginServices.require(MyServiceToken);
|
||||
* ```
|
||||
*/
|
||||
public static get pluginServices(): PluginServiceRegistry {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取World管理器
|
||||
* @en Get World manager
|
||||
* 获取World管理器
|
||||
*
|
||||
* @zh 用于管理多个独立的World实例(高级用户)。
|
||||
* @en For managing multiple independent World instances (advanced users).
|
||||
* 用于管理多个独立的World实例(高级用户)。
|
||||
*
|
||||
* @returns @zh WorldManager实例 @en WorldManager instance
|
||||
* @throws @zh 如果Core实例未创建 @en If Core instance is not created
|
||||
* @returns WorldManager实例
|
||||
* @throws 如果Core实例未创建
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // @zh 创建多个游戏房间 | @en Create multiple game rooms
|
||||
* // 创建多个游戏房间
|
||||
* const wm = Core.worldManager;
|
||||
* const room1 = wm.createWorld('room_001');
|
||||
* room1.createScene('game', new GameScene());
|
||||
@@ -264,24 +286,22 @@ export class Core {
|
||||
*/
|
||||
public static get worldManager(): WorldManager {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 创建Core实例
|
||||
* @en Create Core instance
|
||||
* 创建Core实例
|
||||
*
|
||||
* @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)
|
||||
* @returns @zh Core实例 @en Core instance
|
||||
* @param config - Core配置,也可以直接传入boolean表示debug模式(向后兼容)
|
||||
* @returns Core实例
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // @zh 方式1:使用配置对象 | @en Method 1: Use config object
|
||||
* // 方式1:使用配置对象
|
||||
* Core.create({
|
||||
* debug: true,
|
||||
* debugConfig: {
|
||||
@@ -290,7 +310,7 @@ export class Core {
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* // @zh 方式2:简单模式(向后兼容)| @en Method 2: Simple mode (backward compatible)
|
||||
* // 方式2:简单模式(向后兼容)
|
||||
* 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
|
||||
* @returns @zh 设置的场景实例 @en The scene instance that was set
|
||||
* @param scene - 要设置的场景
|
||||
* @returns 设置的场景实例
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* Core.create({ debug: true });
|
||||
*
|
||||
* // @zh 创建并设置场景 | @en Create and set scene
|
||||
* // 创建并设置场景
|
||||
* const gameScene = new 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 {
|
||||
if (!this._instance) {
|
||||
@@ -346,22 +364,21 @@ export class Core {
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取ECS流式API
|
||||
* @en Get ECS fluent API
|
||||
* 获取ECS流式API
|
||||
*
|
||||
* @returns @zh ECS API实例,如果当前没有场景则返回null @en ECS API instance, or null if no scene
|
||||
* @returns ECS API实例,如果当前没有场景则返回null
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // @zh 使用流式API创建实体 | @en Create entity with fluent API
|
||||
* // 使用流式API创建实体
|
||||
* const player = Core.ecsAPI?.createEntity('Player')
|
||||
* .addComponent(Position, 100, 100)
|
||||
* .addComponent(Velocity, 50, 0);
|
||||
*
|
||||
* // @zh 查询实体 | @en Query entities
|
||||
* // 查询实体
|
||||
* const enemies = Core.ecsAPI?.query(Enemy, Transform);
|
||||
*
|
||||
* // @zh 发射事件 | @en Emit event
|
||||
* // 发射事件
|
||||
* 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
|
||||
* ```typescript
|
||||
* // @zh 延迟切换场景(在下一帧生效)| @en Deferred scene switch (takes effect next frame)
|
||||
* // 延迟切换场景(在下一帧生效)
|
||||
* 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
|
||||
* ```typescript
|
||||
* // @zh 初始化 | @en Initialize
|
||||
* // 初始化
|
||||
* Core.create({ debug: true });
|
||||
* Core.setScene(new GameScene());
|
||||
*
|
||||
* // @zh Laya引擎集成 | @en Laya engine integration
|
||||
* // Laya引擎集成
|
||||
* Laya.timer.frameLoop(1, this, () => {
|
||||
* const deltaTime = Laya.timer.delta / 1000;
|
||||
* Core.update(deltaTime);
|
||||
* Core.update(deltaTime); // 自动更新全局服务和场景
|
||||
* });
|
||||
*
|
||||
* // @zh Cocos Creator集成 | @en Cocos Creator integration
|
||||
* // Cocos Creator集成
|
||||
* 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 repeats - @zh 是否重复执行,默认为false @en Whether to repeat, defaults to false
|
||||
* @param context - @zh 回调函数的上下文 @en Context for the callback
|
||||
* @param onTime - @zh 定时器触发时的回调函数 @en Callback when timer fires
|
||||
* @returns @zh 创建的定时器实例 @en The created timer instance
|
||||
* @throws @zh 如果Core实例未创建或onTime回调未提供 @en If Core instance not created or onTime not provided
|
||||
* @param timeInSeconds - 延迟时间(秒)
|
||||
* @param repeats - 是否重复执行,默认为false
|
||||
* @param context - 回调函数的上下文,默认为null
|
||||
* @param onTime - 定时器触发时的回调函数
|
||||
* @returns 创建的定时器实例
|
||||
* @throws 如果Core实例未创建或onTime回调未提供
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // @zh 一次性定时器 | @en One-time 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) => {
|
||||
* 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 {
|
||||
if (!this._instance) {
|
||||
@@ -499,8 +511,7 @@ export class Core {
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 禁用调试功能
|
||||
* @en Disable debug features
|
||||
* 禁用调试功能
|
||||
*/
|
||||
public static disableDebug(): void {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
return this._instance?._performanceMonitor || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 安装插件
|
||||
* @en Install plugin
|
||||
* 安装插件
|
||||
*
|
||||
* @param plugin - @zh 插件实例 @en Plugin instance
|
||||
* @throws @zh 如果Core实例未创建或插件安装失败 @en If Core instance not created or plugin installation fails
|
||||
* @param plugin - 插件实例
|
||||
* @throws 如果Core实例未创建或插件安装失败
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* Core.create({ debug: true });
|
||||
*
|
||||
* // @zh 安装插件 | @en Install plugin
|
||||
* // 安装插件
|
||||
* await Core.installPlugin(new MyPlugin());
|
||||
* ```
|
||||
*/
|
||||
@@ -574,11 +581,10 @@ export class Core {
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 卸载插件
|
||||
* @en Uninstall plugin
|
||||
* 卸载插件
|
||||
*
|
||||
* @param name - @zh 插件名称 @en Plugin name
|
||||
* @throws @zh 如果Core实例未创建或插件卸载失败 @en If Core instance not created or plugin uninstallation fails
|
||||
* @param name - 插件名称
|
||||
* @throws 如果Core实例未创建或插件卸载失败
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
@@ -594,11 +600,10 @@ export class Core {
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取插件实例
|
||||
* @en Get plugin instance
|
||||
* 获取插件实例
|
||||
*
|
||||
* @param name - @zh 插件名称 @en Plugin name
|
||||
* @returns @zh 插件实例,如果未安装则返回undefined @en Plugin instance, or undefined if not installed
|
||||
* @param name - 插件名称
|
||||
* @returns 插件实例,如果未安装则返回undefined
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
@@ -617,11 +622,10 @@ export class Core {
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 检查插件是否已安装
|
||||
* @en Check if plugin is installed
|
||||
* 检查插件是否已安装
|
||||
*
|
||||
* @param name - @zh 插件名称 @en Plugin name
|
||||
* @returns @zh 是否已安装 @en Whether installed
|
||||
* @param name - 插件名称
|
||||
* @returns 是否已安装
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
@@ -639,11 +643,9 @@ export class Core {
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 初始化核心系统
|
||||
* @en Initialize core system
|
||||
* 初始化核心系统
|
||||
*
|
||||
* @zh 执行核心系统的初始化逻辑。
|
||||
* @en Execute core system initialization logic.
|
||||
* 执行核心系统的初始化逻辑。
|
||||
*/
|
||||
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 {
|
||||
if (Core.paused) return;
|
||||
|
||||
// 开始性能监控
|
||||
const frameStartTime = this._performanceMonitor.startMonitoring('Core.update');
|
||||
|
||||
// 更新时间系统
|
||||
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');
|
||||
this._serviceContainer.updateAll(deltaTime);
|
||||
this._performanceMonitor.endMonitoring('Services.update', servicesStartTime, this._serviceContainer.getUpdatableCount());
|
||||
|
||||
// 更新对象池管理器
|
||||
this._poolManager.update();
|
||||
|
||||
// 更新默认场景(通过 SceneManager)
|
||||
this._sceneManager.update();
|
||||
|
||||
// 更新额外的 WorldManager
|
||||
this._worldManager.updateAll();
|
||||
|
||||
// 结束性能监控
|
||||
this._performanceMonitor.endMonitoring('Core.update', frameStartTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 销毁Core实例
|
||||
* @en Destroy Core instance
|
||||
* 销毁Core实例
|
||||
*
|
||||
* @zh 清理所有资源,通常在应用程序关闭时调用。
|
||||
* @en Clean up all resources, typically called when the application closes.
|
||||
* 清理所有资源,通常在应用程序关闭时调用。
|
||||
*/
|
||||
public static destroy(): void {
|
||||
if (!this._instance) return;
|
||||
|
||||
this._instance._debugManager?.stop();
|
||||
// 停止调试管理器
|
||||
if (this._instance._debugManager) {
|
||||
this._instance._debugManager.stop();
|
||||
}
|
||||
|
||||
// 清理所有服务
|
||||
this._instance._serviceContainer.clear();
|
||||
|
||||
Core._logger.info('Core destroyed');
|
||||
|
||||
// 清空实例引用,允许重新创建Core实例
|
||||
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
|
||||
* type information across packages.
|
||||
*/
|
||||
export type ServiceToken<T> = {
|
||||
export interface ServiceToken<T> {
|
||||
readonly id: symbol;
|
||||
readonly name: string;
|
||||
/**
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
*/
|
||||
|
||||
import { createServiceToken } from './PluginServiceRegistry';
|
||||
import { createLogger } from '../Utils/Logger';
|
||||
|
||||
// ============================================================================
|
||||
// 接口定义 | Interface Definitions
|
||||
@@ -40,7 +39,7 @@ import { createLogger } from '../Utils/Logger';
|
||||
* 运行时模式接口
|
||||
* Runtime mode interface
|
||||
*/
|
||||
export type IRuntimeMode = {
|
||||
export interface IRuntimeMode {
|
||||
/**
|
||||
* 是否为编辑器模式
|
||||
* Whether in editor mode
|
||||
@@ -111,7 +110,7 @@ type ModeChangeCallback = (mode: IRuntimeMode) => void;
|
||||
* 运行时模式服务配置
|
||||
* Runtime mode service configuration
|
||||
*/
|
||||
export type RuntimeModeConfig = {
|
||||
export interface RuntimeModeConfig {
|
||||
/** 是否为编辑器模式 | Whether in editor mode */
|
||||
isEditor?: boolean;
|
||||
/** 是否正在播放 | 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 {
|
||||
for (const callback of this._callbacks) {
|
||||
try {
|
||||
callback(this);
|
||||
} catch (error) {
|
||||
RuntimeModeService._logger.error('Callback error:', error);
|
||||
console.error('[RuntimeModeService] Callback error:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ const logger = createLogger('ServiceContainer');
|
||||
* 服务基础接口
|
||||
* 所有通过 ServiceContainer 管理的服务都应该实现此接口
|
||||
*/
|
||||
export type IService = {
|
||||
export interface IService {
|
||||
/**
|
||||
* 释放服务占用的资源
|
||||
* 当服务被注销或容器被清空时调用
|
||||
|
||||
@@ -2,17 +2,13 @@ import type { IComponent } from '../Types';
|
||||
import { Int32 } from './Core/SoAStorage';
|
||||
|
||||
/**
|
||||
* @zh 游戏组件基类
|
||||
* @en Base class for game components
|
||||
* 游戏组件基类
|
||||
*
|
||||
* @zh ECS架构中的组件(Component)应该是纯数据容器。
|
||||
* ECS架构中的组件(Component)应该是纯数据容器。
|
||||
* 所有游戏逻辑应该在 EntitySystem 中实现,而不是在组件内部。
|
||||
* @en Components in ECS architecture should be pure data containers.
|
||||
* All game logic should be implemented in EntitySystem, not inside components.
|
||||
*
|
||||
* @example
|
||||
* @zh 推荐做法:纯数据组件
|
||||
* @en Recommended: Pure data component
|
||||
* 推荐做法:纯数据组件
|
||||
* ```typescript
|
||||
* class HealthComponent extends Component {
|
||||
* public health: number = 100;
|
||||
@@ -21,8 +17,7 @@ import { Int32 } from './Core/SoAStorage';
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* @zh 推荐做法:在 System 中处理逻辑
|
||||
* @en Recommended: Handle logic in System
|
||||
* 推荐做法:在 System 中处理逻辑
|
||||
* ```typescript
|
||||
* class HealthSystem extends EntitySystem {
|
||||
* process(entities: Entity[]): void {
|
||||
@@ -38,66 +33,75 @@ import { Int32 } from './Core/SoAStorage';
|
||||
*/
|
||||
export abstract class Component implements IComponent {
|
||||
/**
|
||||
* @zh 组件ID生成器,用于为每个组件分配唯一的ID
|
||||
* @en Component ID generator, used to assign unique IDs to each component
|
||||
* 组件ID生成器
|
||||
*
|
||||
* 用于为每个组件分配唯一的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;
|
||||
|
||||
/**
|
||||
* @zh 所属实体ID
|
||||
* @en Owner entity ID
|
||||
* 所属实体ID
|
||||
*
|
||||
* @zh 存储实体ID而非引用,避免循环引用,符合ECS数据导向设计
|
||||
* @en Stores entity ID instead of reference to avoid circular references, following ECS data-oriented design
|
||||
* 存储实体ID而非引用,避免循环引用,符合ECS数据导向设计。
|
||||
*/
|
||||
@Int32
|
||||
public entityId: number | null = null;
|
||||
|
||||
/**
|
||||
* @zh 最后写入的 epoch,用于帧级变更检测
|
||||
* @en Last write epoch, used for frame-level change detection
|
||||
* 最后写入的 epoch
|
||||
*
|
||||
* @zh 记录组件最后一次被修改时的 epoch,0 表示从未被标记为已修改
|
||||
* @en Records the epoch when component was last modified, 0 means never marked as modified
|
||||
* 用于帧级变更检测,记录组件最后一次被修改时的 epoch。
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* @zh 获取最后写入的 epoch
|
||||
* @en Get last write epoch
|
||||
* 获取最后写入的 epoch
|
||||
*
|
||||
* Get last write epoch.
|
||||
*/
|
||||
public get lastWriteEpoch(): number {
|
||||
return this._lastWriteEpoch;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 创建组件实例,自动分配唯一ID
|
||||
* @en Create component instance, automatically assigns unique ID
|
||||
* 创建组件实例
|
||||
*
|
||||
* 自动分配唯一ID给组件。
|
||||
*/
|
||||
constructor() {
|
||||
this.id = Component._idGenerator++;
|
||||
this.id = Component.idGenerator++;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 标记组件为已修改
|
||||
* @en Mark component as modified
|
||||
* 标记组件为已修改
|
||||
*
|
||||
* @zh 调用此方法会更新组件的 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.
|
||||
*
|
||||
* @param epoch - @zh 当前帧的 epoch @en Current frame's epoch
|
||||
* @param epoch 当前帧的 epoch | Current frame's epoch
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // @zh 在修改组件数据后调用 | @en Call after modifying component data
|
||||
* // 在修改组件数据后调用
|
||||
* velocity.x = 10;
|
||||
* 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 中处理。
|
||||
* @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 {}
|
||||
|
||||
/**
|
||||
* @zh 组件从实体移除时的回调
|
||||
* @en Callback when component is removed from an entity
|
||||
* 组件从实体移除时的回调
|
||||
*
|
||||
* @zh 当组件从实体中移除时调用,可以在此方法中进行清理操作。
|
||||
* 当组件从实体中移除时调用,可以在此方法中进行清理操作。
|
||||
*
|
||||
* @remarks
|
||||
* 这是一个生命周期钩子,用于组件的清理逻辑。
|
||||
* 虽然保留此方法,但建议将复杂的清理逻辑放在 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 {}
|
||||
|
||||
/**
|
||||
* @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
|
||||
* ```typescript
|
||||
@@ -151,8 +149,7 @@ export abstract class Component implements IComponent {
|
||||
*
|
||||
* public async onDeserialized(): Promise<void> {
|
||||
* if (this.tilesetImage) {
|
||||
* // @zh 重新加载 tileset 图片并恢复运行时数据
|
||||
* // @en Reload tileset image and restore runtime data
|
||||
* // 重新加载 tileset 图片并恢复运行时数据
|
||||
* const img = await loadImage(this.tilesetImage);
|
||||
* this.setTilesetInfo(img.width, img.height, ...);
|
||||
* }
|
||||
|
||||
@@ -11,7 +11,7 @@ export type ArchetypeId = BitMask64Data;
|
||||
/**
|
||||
* 原型数据结构
|
||||
*/
|
||||
export type Archetype = {
|
||||
export interface Archetype {
|
||||
/** 原型唯一标识符 */
|
||||
id: ArchetypeId;
|
||||
/** 包含的组件类型 */
|
||||
@@ -23,7 +23,7 @@ export type Archetype = {
|
||||
/**
|
||||
* 原型查询结果
|
||||
*/
|
||||
export type ArchetypeQueryResult = {
|
||||
export interface ArchetypeQueryResult {
|
||||
/** 匹配的原型列表 */
|
||||
archetypes: Archetype[];
|
||||
/** 所有匹配实体的总数 */
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Entity } from '../Entity';
|
||||
import { Component } from '../Component';
|
||||
import { ComponentType, GlobalComponentRegistry } from './ComponentStorage';
|
||||
import { getComponentTypeName } from '../Decorators';
|
||||
import { IScene } from '../IScene';
|
||||
import { createLogger } from '../../Utils/Logger';
|
||||
|
||||
@@ -26,7 +25,7 @@ export enum CommandType {
|
||||
* 延迟命令接口
|
||||
* Deferred command interface
|
||||
*/
|
||||
export type DeferredCommand = {
|
||||
export interface DeferredCommand {
|
||||
/** 命令类型 | Command type */
|
||||
type: CommandType;
|
||||
/** 目标实体 | Target entity */
|
||||
@@ -240,8 +239,7 @@ export class CommandBuffer {
|
||||
pending.adds.set(typeId, component);
|
||||
|
||||
if (this._debug) {
|
||||
const typeName = getComponentTypeName(component.constructor as ComponentType);
|
||||
logger.debug(`CommandBuffer: 延迟添加组件 ${typeName} 到实体 ${entity.name}`);
|
||||
logger.debug(`CommandBuffer: 延迟添加组件 ${component.constructor.name} 到实体 ${entity.name}`);
|
||||
}
|
||||
} else {
|
||||
// 旧模式
|
||||
@@ -437,10 +435,9 @@ export class CommandBuffer {
|
||||
entity.addComponent(component);
|
||||
commandCount++;
|
||||
} catch (error) {
|
||||
const typeName = getComponentTypeName(component.constructor as ComponentType);
|
||||
logger.error(`CommandBuffer: 添加组件失败`, {
|
||||
entity: entity.name,
|
||||
component: typeName,
|
||||
component: component.constructor.name,
|
||||
error
|
||||
});
|
||||
}
|
||||
|
||||
@@ -142,25 +142,24 @@ interface ComponentUsageTracker {
|
||||
* 全局组件池管理器
|
||||
*/
|
||||
export class ComponentPoolManager {
|
||||
private static _instance: ComponentPoolManager;
|
||||
private _pools = new Map<string, ComponentPool<Component>>();
|
||||
private _usageTracker = new Map<string, ComponentUsageTracker>();
|
||||
private static instance: ComponentPoolManager;
|
||||
private pools = new Map<string, ComponentPool<Component>>();
|
||||
private usageTracker = new Map<string, ComponentUsageTracker>();
|
||||
|
||||
private _autoCleanupInterval = 60000;
|
||||
private _lastCleanupTime = 0;
|
||||
private autoCleanupInterval = 60000;
|
||||
private lastCleanupTime = 0;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
static getInstance(): ComponentPoolManager {
|
||||
if (!ComponentPoolManager._instance) {
|
||||
ComponentPoolManager._instance = new ComponentPoolManager();
|
||||
if (!ComponentPoolManager.instance) {
|
||||
ComponentPoolManager.instance = new ComponentPoolManager();
|
||||
}
|
||||
return ComponentPoolManager._instance;
|
||||
return ComponentPoolManager.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 注册组件池
|
||||
* @en Register component pool
|
||||
* 注册组件池
|
||||
*/
|
||||
registerPool<T extends Component>(
|
||||
componentName: string,
|
||||
@@ -169,9 +168,9 @@ export class ComponentPoolManager {
|
||||
maxSize?: number,
|
||||
minSize?: number
|
||||
): 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,
|
||||
releaseCount: 0,
|
||||
lastAccessTime: Date.now()
|
||||
@@ -179,25 +178,23 @@ export class ComponentPoolManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取组件实例
|
||||
* @en Acquire component instance
|
||||
* 获取组件实例
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 释放组件实例
|
||||
* @en Release component instance
|
||||
* 释放组件实例
|
||||
*/
|
||||
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) {
|
||||
pool.release(component);
|
||||
@@ -205,11 +202,10 @@ export class ComponentPoolManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 追踪使用情况
|
||||
* @en Track usage
|
||||
* 追踪使用情况
|
||||
*/
|
||||
private _trackUsage(componentName: string, action: 'create' | 'release'): void {
|
||||
let tracker = this._usageTracker.get(componentName);
|
||||
private trackUsage(componentName: string, action: 'create' | 'release'): void {
|
||||
let tracker = this.usageTracker.get(componentName);
|
||||
|
||||
if (!tracker) {
|
||||
tracker = {
|
||||
@@ -217,7 +213,7 @@ export class ComponentPoolManager {
|
||||
releaseCount: 0,
|
||||
lastAccessTime: Date.now()
|
||||
};
|
||||
this._usageTracker.set(componentName, tracker);
|
||||
this.usageTracker.set(componentName, tracker);
|
||||
}
|
||||
|
||||
if (action === 'create') {
|
||||
@@ -230,72 +226,66 @@ export class ComponentPoolManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 自动清理(定期调用)
|
||||
* @en Auto cleanup (called periodically)
|
||||
* 自动清理(定期调用)
|
||||
*/
|
||||
public update(): void {
|
||||
const now = Date.now();
|
||||
|
||||
if (now - this._lastCleanupTime < this._autoCleanupInterval) {
|
||||
if (now - this.lastCleanupTime < this.autoCleanupInterval) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [name, tracker] of this._usageTracker.entries()) {
|
||||
for (const [name, tracker] of this.usageTracker.entries()) {
|
||||
const inactive = now - tracker.lastAccessTime > 120000;
|
||||
|
||||
if (inactive) {
|
||||
const pool = this._pools.get(name);
|
||||
const pool = this.pools.get(name);
|
||||
if (pool) {
|
||||
pool.shrink();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._lastCleanupTime = now;
|
||||
this.lastCleanupTime = now;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取热点组件列表
|
||||
* @en Get hot components list
|
||||
* 获取热点组件列表
|
||||
*/
|
||||
public getHotComponents(threshold: number = 100): string[] {
|
||||
return Array.from(this._usageTracker.entries())
|
||||
return Array.from(this.usageTracker.entries())
|
||||
.filter(([_, tracker]) => tracker.createCount > threshold)
|
||||
.map(([name]) => name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 预热所有池
|
||||
* @en Prewarm all pools
|
||||
* 预热所有池
|
||||
*/
|
||||
prewarmAll(count: number = 100): void {
|
||||
for (const pool of this._pools.values()) {
|
||||
for (const pool of this.pools.values()) {
|
||||
pool.prewarm(count);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 清空所有池
|
||||
* @en Clear all pools
|
||||
* 清空所有池
|
||||
*/
|
||||
clearAll(): void {
|
||||
for (const pool of this._pools.values()) {
|
||||
for (const pool of this.pools.values()) {
|
||||
pool.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 重置管理器
|
||||
* @en Reset manager
|
||||
* 重置管理器
|
||||
*/
|
||||
reset(): void {
|
||||
this._pools.clear();
|
||||
this._usageTracker.clear();
|
||||
this.pools.clear();
|
||||
this.usageTracker.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取全局统计信息
|
||||
* @en Get global stats
|
||||
* 获取全局统计信息
|
||||
*/
|
||||
getGlobalStats(): Array<{
|
||||
componentName: string;
|
||||
@@ -308,11 +298,11 @@ export class ComponentPoolManager {
|
||||
usage: ComponentUsageTracker | undefined;
|
||||
}> = [];
|
||||
|
||||
for (const [name, pool] of this._pools.entries()) {
|
||||
for (const [name, pool] of this.pools.entries()) {
|
||||
stats.push({
|
||||
componentName: name,
|
||||
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 }> {
|
||||
const stats = new Map();
|
||||
for (const [name, pool] of this._pools) {
|
||||
for (const [name, pool] of this.pools) {
|
||||
stats.set(name, {
|
||||
available: pool.getAvailableCount(),
|
||||
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 }> {
|
||||
const utilization = new Map();
|
||||
for (const [name, pool] of this._pools) {
|
||||
for (const [name, pool] of this.pools) {
|
||||
const available = pool.getAvailableCount();
|
||||
const maxSize = pool.getMaxSize();
|
||||
const used = maxSize - available;
|
||||
@@ -356,11 +344,10 @@ export class ComponentPoolManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取指定组件的池利用率
|
||||
* @en Get component pool utilization
|
||||
* 获取指定组件的池利用率
|
||||
*/
|
||||
getComponentUtilization(componentName: string): number {
|
||||
const pool = this._pools.get(componentName);
|
||||
const pool = this.pools.get(componentName);
|
||||
if (!pool) return 0;
|
||||
|
||||
const available = pool.getAvailableCount();
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Component } from '../Component';
|
||||
import { BitMask64Utils, BitMask64Data } from '../Utils/BigIntCompatibility';
|
||||
import { SoAStorage } from './SoAStorage';
|
||||
import type { SupportedTypedArray } from './SoAStorage';
|
||||
import { SoAStorage, SupportedTypedArray } from './SoAStorage';
|
||||
import { createLogger } from '../../Utils/Logger';
|
||||
import { getComponentTypeName, ComponentType } from '../Decorators';
|
||||
import { ComponentRegistry, GlobalComponentRegistry } from './ComponentStorage/ComponentRegistry';
|
||||
|
||||
@@ -53,11 +53,10 @@ export class ComponentRegistry implements IComponentRegistry {
|
||||
// 检查是否使用了 @ECSComponent 装饰器
|
||||
if (!hasECSComponentDecorator(componentType) && !this._warnedComponents.has(componentType)) {
|
||||
this._warnedComponents.add(componentType);
|
||||
logger.warn(
|
||||
`Component "${typeName}" is missing @ECSComponent decorator. ` +
|
||||
console.warn(
|
||||
`[ComponentRegistry] Component "${typeName}" is missing @ECSComponent decorator. ` +
|
||||
`This may cause issues with serialization and code minification. ` +
|
||||
`Please add: @ECSComponent('${typeName}') | ` +
|
||||
`组件 "${typeName}" 缺少 @ECSComponent 装饰器,可能导致序列化和代码压缩问题`
|
||||
`Please add: @ECSComponent('${typeName}')`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,11 +14,7 @@ import type { Component } from '../../Component';
|
||||
/**
|
||||
* 组件类型定义
|
||||
* 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;
|
||||
|
||||
/**
|
||||
@@ -43,7 +39,7 @@ export const COMPONENT_EDITOR_OPTIONS = Symbol('ComponentEditorOptions');
|
||||
* 组件编辑器选项
|
||||
* Component editor options
|
||||
*/
|
||||
export type ComponentEditorOptions = {
|
||||
export interface ComponentEditorOptions {
|
||||
/**
|
||||
* 是否在 Inspector 中隐藏此组件
|
||||
* Whether to hide this component in Inspector
|
||||
@@ -65,51 +61,6 @@ export type ComponentEditorOptions = {
|
||||
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 装饰器
|
||||
* Check if component has @ECSComponent decorator
|
||||
@@ -118,8 +69,7 @@ export function getWritableComponentTypeMetadata(componentType: ComponentType):
|
||||
* @returns 是否有装饰器
|
||||
*/
|
||||
export function hasECSComponentDecorator(componentType: ComponentType): boolean {
|
||||
const metadata = getComponentTypeMetadata(componentType);
|
||||
return metadata[COMPONENT_TYPE_NAME] !== undefined;
|
||||
return !!(componentType as any)[COMPONENT_TYPE_NAME];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,8 +82,7 @@ export function hasECSComponentDecorator(componentType: ComponentType): boolean
|
||||
export function getComponentTypeName(componentType: ComponentType): string {
|
||||
// 优先使用装饰器指定的名称
|
||||
// Prefer decorator-specified name
|
||||
const metadata = getComponentTypeMetadata(componentType);
|
||||
const decoratorName = metadata[COMPONENT_TYPE_NAME];
|
||||
const decoratorName = (componentType as any)[COMPONENT_TYPE_NAME];
|
||||
if (decoratorName) {
|
||||
return decoratorName;
|
||||
}
|
||||
@@ -162,8 +111,7 @@ export function getComponentInstanceTypeName(component: Component): string {
|
||||
* @returns 依赖的组件名称列表
|
||||
*/
|
||||
export function getComponentDependencies(componentType: ComponentType): string[] | undefined {
|
||||
const metadata = getComponentTypeMetadata(componentType);
|
||||
return metadata[COMPONENT_DEPENDENCIES];
|
||||
return (componentType as any)[COMPONENT_DEPENDENCIES];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -174,8 +122,7 @@ export function getComponentDependencies(componentType: ComponentType): string[]
|
||||
* @returns 编辑器选项
|
||||
*/
|
||||
export function getComponentEditorOptions(componentType: ComponentType): ComponentEditorOptions | undefined {
|
||||
const metadata = getComponentTypeMetadata(componentType);
|
||||
return metadata[COMPONENT_EDITOR_OPTIONS];
|
||||
return (componentType as any)[COMPONENT_EDITOR_OPTIONS];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,7 +16,7 @@ import type { ComponentType } from './ComponentTypeUtils';
|
||||
* Component Registry Interface.
|
||||
* 组件注册表接口。
|
||||
*/
|
||||
export type IComponentRegistry = {
|
||||
export interface IComponentRegistry {
|
||||
/**
|
||||
* Register component type and allocate bitmask.
|
||||
* 注册组件类型并分配位掩码。
|
||||
|
||||
@@ -443,33 +443,29 @@ export class EventBus implements IEventBus {
|
||||
* 提供全局访问的事件总线
|
||||
*/
|
||||
export class GlobalEventBus {
|
||||
private static _instance: EventBus;
|
||||
private static instance: EventBus;
|
||||
|
||||
/**
|
||||
* @zh 获取全局事件总线实例
|
||||
* @en Get global event bus instance
|
||||
*
|
||||
* @param debugMode - @zh 是否启用调试模式 @en Whether to enable debug mode
|
||||
* 获取全局事件总线实例
|
||||
* @param debugMode 是否启用调试模式
|
||||
*/
|
||||
public static getInstance(debugMode: boolean = false): EventBus {
|
||||
if (!this._instance) {
|
||||
this._instance = new EventBus(debugMode);
|
||||
if (!this.instance) {
|
||||
this.instance = new EventBus(debugMode);
|
||||
}
|
||||
return this._instance;
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 重置全局事件总线实例
|
||||
* @en Reset global event bus instance
|
||||
*
|
||||
* @param debugMode - @zh 是否启用调试模式 @en Whether to enable debug mode
|
||||
* 重置全局事件总线实例
|
||||
* @param debugMode 是否启用调试模式
|
||||
*/
|
||||
public static reset(debugMode: boolean = false): EventBus {
|
||||
if (this._instance) {
|
||||
this._instance.clear();
|
||||
if (this.instance) {
|
||||
this.instance.clear();
|
||||
}
|
||||
this._instance = new EventBus(debugMode);
|
||||
return this._instance;
|
||||
this.instance = new EventBus(debugMode);
|
||||
return this.instance;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ export type AsyncEventHandler<T> = (event: T) => Promise<void>;
|
||||
/**
|
||||
* 事件监听器配置
|
||||
*/
|
||||
export type EventListenerConfig = {
|
||||
export interface EventListenerConfig {
|
||||
/** 是否只执行一次 */
|
||||
once?: boolean;
|
||||
/** 优先级(数字越大优先级越高) */
|
||||
@@ -41,7 +41,7 @@ interface InternalEventListener {
|
||||
/**
|
||||
* 事件统计信息
|
||||
*/
|
||||
export type EventStats = {
|
||||
export interface EventStats {
|
||||
/** 事件类型 */
|
||||
eventType: string;
|
||||
/** 监听器数量 */
|
||||
@@ -59,7 +59,7 @@ export type EventStats = {
|
||||
/**
|
||||
* 事件批处理配置
|
||||
*/
|
||||
export type EventBatchConfig = {
|
||||
export interface EventBatchConfig {
|
||||
/** 批处理大小 */
|
||||
batchSize: number;
|
||||
/** 批处理延迟(毫秒) */
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
export { EventBus, GlobalEventBus } from '../EventBus';
|
||||
export { TypeSafeEventSystem } from '../EventSystem';
|
||||
export type { EventListenerConfig, EventStats } from '../EventSystem';
|
||||
export { TypeSafeEventSystem, EventListenerConfig, EventStats } from '../EventSystem';
|
||||
|
||||
@@ -6,12 +6,10 @@ import { createLogger } from '../../Utils/Logger';
|
||||
import { getComponentTypeName } from '../Decorators';
|
||||
import { Archetype, ArchetypeSystem } from './ArchetypeSystem';
|
||||
import { ReactiveQuery, ReactiveQueryConfig } from './ReactiveQuery';
|
||||
import type { QueryCondition, QueryResult } from './QueryTypes';
|
||||
import { QueryConditionType } from './QueryTypes';
|
||||
import { QueryCondition, QueryConditionType, QueryResult } from './QueryTypes';
|
||||
import { CompiledQuery } from './Query/CompiledQuery';
|
||||
|
||||
export { QueryConditionType };
|
||||
export type { QueryCondition, QueryResult };
|
||||
export { QueryCondition, QueryConditionType, QueryResult };
|
||||
export { CompiledQuery };
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,7 +17,7 @@ export enum QueryConditionType {
|
||||
/**
|
||||
* 查询条件接口
|
||||
*/
|
||||
export type QueryCondition = {
|
||||
export interface QueryCondition {
|
||||
type: QueryConditionType;
|
||||
componentTypes: ComponentType[];
|
||||
mask: BitMask64Data;
|
||||
@@ -26,7 +26,7 @@ export type QueryCondition = {
|
||||
/**
|
||||
* 实体查询结果接口
|
||||
*/
|
||||
export type QueryResult = {
|
||||
export interface QueryResult {
|
||||
entities: readonly Entity[];
|
||||
count: number;
|
||||
/** 查询执行时间(毫秒) */
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Entity } from '../Entity';
|
||||
import type { QueryCondition } from './QueryTypes';
|
||||
import { QueryConditionType } from './QueryTypes';
|
||||
import { QueryCondition, QueryConditionType } from './QueryTypes';
|
||||
import { BitMask64Utils } from '../Utils/BigIntCompatibility';
|
||||
import { createLogger } from '../../Utils/Logger';
|
||||
|
||||
@@ -21,7 +20,7 @@ export enum ReactiveQueryChangeType {
|
||||
/**
|
||||
* 响应式查询变化事件
|
||||
*/
|
||||
export type ReactiveQueryChange = {
|
||||
export interface ReactiveQueryChange {
|
||||
/** 变化类型 */
|
||||
type: ReactiveQueryChangeType;
|
||||
/** 变化的实体 */
|
||||
@@ -42,7 +41,7 @@ export type ReactiveQueryListener = (change: ReactiveQueryChange) => void;
|
||||
/**
|
||||
* 响应式查询配置
|
||||
*/
|
||||
export type ReactiveQueryConfig = {
|
||||
export interface ReactiveQueryConfig {
|
||||
/** 是否启用批量模式(减少通知频率) */
|
||||
enableBatchMode?: boolean;
|
||||
/** 批量模式的延迟时间(毫秒) */
|
||||
|
||||
@@ -54,7 +54,7 @@ const WeakRefImpl: IWeakRefConstructor = (
|
||||
/**
|
||||
* Entity引用记录
|
||||
*/
|
||||
export type EntityRefRecord = {
|
||||
export interface EntityRefRecord {
|
||||
component: IWeakRef<Component>;
|
||||
propertyKey: string;
|
||||
}
|
||||
|
||||
@@ -6,182 +6,177 @@ import {
|
||||
TypedArrayTypeName
|
||||
} from './SoATypeRegistry';
|
||||
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 { SoASerializer } from './SoASerializer';
|
||||
|
||||
/**
|
||||
* @zh SoA 字段统计信息
|
||||
* @en SoA field statistics
|
||||
*/
|
||||
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
|
||||
* 启用SoA优化装饰器
|
||||
* 默认关闭SoA,只有在大规模批量操作场景下才建议开启
|
||||
*/
|
||||
export function EnableSoA<T extends ComponentType>(target: T): T {
|
||||
(target as ComponentType & IComponentTypeMetadata).__enableSoA = true;
|
||||
(target as any).__enableSoA = true;
|
||||
return target;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @zh 组件字段元数据键(仅 Set<string> 类型的字段)
|
||||
* @en Component field metadata keys (only Set<string> type fields)
|
||||
* 64位浮点数装饰器
|
||||
* 标记字段使用Float64Array存储(更高精度但更多内存)
|
||||
*/
|
||||
type ComponentFieldMetadataKey = Exclude<keyof IComponentTypeMetadata, '__enableSoA'>;
|
||||
|
||||
/**
|
||||
* @zh 装饰器目标原型接口
|
||||
* @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;
|
||||
export function Float64(target: any, propertyKey: string | symbol): void {
|
||||
const key = String(propertyKey);
|
||||
if (!target.constructor.__float64Fields) {
|
||||
target.constructor.__float64Fields = new Set();
|
||||
}
|
||||
return fieldSet;
|
||||
target.constructor.__float64Fields.add(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 64位浮点数装饰器 - 标记字段使用 Float64Array 存储(更高精度但更多内存)
|
||||
* @en Float64 decorator - marks field to use Float64Array storage (higher precision but more memory)
|
||||
* 32位浮点数装饰器
|
||||
* 标记字段使用Float32Array存储(默认类型,平衡性能和精度)
|
||||
*/
|
||||
export function Float64(target: object, propertyKey: string | symbol): void {
|
||||
getOrCreateFieldSet(target as IDecoratorTarget, '__float64Fields').add(String(propertyKey));
|
||||
export function Float32(target: any, propertyKey: string | symbol): void {
|
||||
const key = String(propertyKey);
|
||||
if (!target.constructor.__float32Fields) {
|
||||
target.constructor.__float32Fields = new Set();
|
||||
}
|
||||
target.constructor.__float32Fields.add(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 32位浮点数装饰器 - 标记字段使用 Float32Array 存储(默认类型,平衡性能和精度)
|
||||
* @en Float32 decorator - marks field to use Float32Array storage (default, balanced performance and precision)
|
||||
* 32位整数装饰器
|
||||
* 标记字段使用Int32Array存储(适用于整数值)
|
||||
*/
|
||||
export function Float32(target: object, propertyKey: string | symbol): void {
|
||||
getOrCreateFieldSet(target as IDecoratorTarget, '__float32Fields').add(String(propertyKey));
|
||||
export function Int32(target: any, propertyKey: string | symbol): void {
|
||||
const key = String(propertyKey);
|
||||
if (!target.constructor.__int32Fields) {
|
||||
target.constructor.__int32Fields = new Set();
|
||||
}
|
||||
target.constructor.__int32Fields.add(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 32位整数装饰器 - 标记字段使用 Int32Array 存储(适用于整数值)
|
||||
* @en Int32 decorator - marks field to use Int32Array storage (for integer values)
|
||||
* 32位无符号整数装饰器
|
||||
* 标记字段使用Uint32Array存储(适用于无符号整数,如ID、标志位等)
|
||||
*/
|
||||
export function Int32(target: object, propertyKey: string | symbol): void {
|
||||
getOrCreateFieldSet(target as IDecoratorTarget, '__int32Fields').add(String(propertyKey));
|
||||
export function Uint32(target: any, propertyKey: string | symbol): void {
|
||||
const key = String(propertyKey);
|
||||
if (!target.constructor.__uint32Fields) {
|
||||
target.constructor.__uint32Fields = new Set();
|
||||
}
|
||||
target.constructor.__uint32Fields.add(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 32位无符号整数装饰器 - 标记字段使用 Uint32Array 存储
|
||||
* @en Uint32 decorator - marks field to use Uint32Array storage
|
||||
* 16位整数装饰器
|
||||
* 标记字段使用Int16Array存储(适用于小范围整数)
|
||||
*/
|
||||
export function Uint32(target: object, propertyKey: string | symbol): void {
|
||||
getOrCreateFieldSet(target as IDecoratorTarget, '__uint32Fields').add(String(propertyKey));
|
||||
export function Int16(target: any, propertyKey: string | symbol): void {
|
||||
const key = String(propertyKey);
|
||||
if (!target.constructor.__int16Fields) {
|
||||
target.constructor.__int16Fields = new Set();
|
||||
}
|
||||
target.constructor.__int16Fields.add(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 16位整数装饰器 - 标记字段使用 Int16Array 存储
|
||||
* @en Int16 decorator - marks field to use Int16Array storage
|
||||
* 16位无符号整数装饰器
|
||||
* 标记字段使用Uint16Array存储(适用于小范围无符号整数)
|
||||
*/
|
||||
export function Int16(target: object, propertyKey: string | symbol): void {
|
||||
getOrCreateFieldSet(target as IDecoratorTarget, '__int16Fields').add(String(propertyKey));
|
||||
export function Uint16(target: any, propertyKey: string | symbol): void {
|
||||
const key = String(propertyKey);
|
||||
if (!target.constructor.__uint16Fields) {
|
||||
target.constructor.__uint16Fields = new Set();
|
||||
}
|
||||
target.constructor.__uint16Fields.add(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 16位无符号整数装饰器 - 标记字段使用 Uint16Array 存储
|
||||
* @en Uint16 decorator - marks field to use Uint16Array storage
|
||||
* 8位整数装饰器
|
||||
* 标记字段使用Int8Array存储(适用于很小的整数值)
|
||||
*/
|
||||
export function Uint16(target: object, propertyKey: string | symbol): void {
|
||||
getOrCreateFieldSet(target as IDecoratorTarget, '__uint16Fields').add(String(propertyKey));
|
||||
export function Int8(target: any, propertyKey: string | symbol): void {
|
||||
const key = String(propertyKey);
|
||||
if (!target.constructor.__int8Fields) {
|
||||
target.constructor.__int8Fields = new Set();
|
||||
}
|
||||
target.constructor.__int8Fields.add(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 8位整数装饰器 - 标记字段使用 Int8Array 存储
|
||||
* @en Int8 decorator - marks field to use Int8Array storage
|
||||
* 8位无符号整数装饰器
|
||||
* 标记字段使用Uint8Array存储(适用于字节值、布尔标志等)
|
||||
*/
|
||||
export function Int8(target: object, propertyKey: string | symbol): void {
|
||||
getOrCreateFieldSet(target as IDecoratorTarget, '__int8Fields').add(String(propertyKey));
|
||||
export function Uint8(target: any, propertyKey: string | symbol): void {
|
||||
const key = String(propertyKey);
|
||||
if (!target.constructor.__uint8Fields) {
|
||||
target.constructor.__uint8Fields = new Set();
|
||||
}
|
||||
target.constructor.__uint8Fields.add(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 8位无符号整数装饰器 - 标记字段使用 Uint8Array 存储
|
||||
* @en Uint8 decorator - marks field to use Uint8Array storage
|
||||
* 8位夹紧整数装饰器
|
||||
* 标记字段使用Uint8ClampedArray存储(适用于颜色值等需要夹紧的数据)
|
||||
*/
|
||||
export function Uint8(target: object, propertyKey: string | symbol): void {
|
||||
getOrCreateFieldSet(target as IDecoratorTarget, '__uint8Fields').add(String(propertyKey));
|
||||
export function Uint8Clamped(target: any, propertyKey: string | symbol): void {
|
||||
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 存储(适用于颜色值)
|
||||
* @en Uint8Clamped decorator - marks field to use Uint8ClampedArray storage (for color values)
|
||||
* 序列化Set装饰器
|
||||
* 标记Set字段需要序列化/反序列化存储
|
||||
*/
|
||||
export function Uint8Clamped(target: object, propertyKey: string | symbol): void {
|
||||
getOrCreateFieldSet(target as IDecoratorTarget, '__uint8ClampedFields').add(String(propertyKey));
|
||||
export function SerializeSet(target: any, propertyKey: string | symbol): void {
|
||||
const key = String(propertyKey);
|
||||
if (!target.constructor.__serializeSetFields) {
|
||||
target.constructor.__serializeSetFields = new Set();
|
||||
}
|
||||
target.constructor.__serializeSetFields.add(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 序列化 Map 装饰器 - 标记 Map 字段需要序列化/反序列化存储
|
||||
* @en SerializeMap decorator - marks Map field for serialization/deserialization
|
||||
* 序列化Array装饰器
|
||||
* 标记Array字段需要序列化/反序列化存储
|
||||
*/
|
||||
export function SerializeMap(target: object, propertyKey: string | symbol): void {
|
||||
getOrCreateFieldSet(target as IDecoratorTarget, '__serializeMapFields').add(String(propertyKey));
|
||||
export function SerializeArray(target: any, propertyKey: string | symbol): void {
|
||||
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 {
|
||||
getOrCreateFieldSet(target as IDecoratorTarget, '__serializeSetFields').add(String(propertyKey));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 序列化 Array 装饰器 - 标记 Array 字段需要序列化/反序列化存储
|
||||
* @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));
|
||||
export function DeepCopy(target: any, propertyKey: string | symbol): void {
|
||||
const key = String(propertyKey);
|
||||
if (!target.constructor.__deepCopyFields) {
|
||||
target.constructor.__deepCopyFields = new Set();
|
||||
}
|
||||
target.constructor.__deepCopyFields.add(key);
|
||||
}
|
||||
|
||||
|
||||
@@ -191,8 +186,8 @@ export function DeepCopy(target: object, propertyKey: string | symbol): void {
|
||||
*/
|
||||
export class SoAStorage<T extends Component> {
|
||||
private fields = new Map<string, SupportedTypedArray>();
|
||||
private stringFields = new Map<string, Array<string | undefined>>();
|
||||
private serializedFields = new Map<string, Array<string | undefined>>();
|
||||
private stringFields = new Map<string, string[]>();
|
||||
private serializedFields = new Map<string, string[]>();
|
||||
private complexFields = new Map<number, Map<string, unknown>>();
|
||||
private entityToIndex = new Map<number, number>();
|
||||
private indexToEntity: number[] = [];
|
||||
@@ -323,29 +318,30 @@ export class SoAStorage<T extends Component> {
|
||||
|
||||
private updateComponentAtIndex(index: number, component: T): void {
|
||||
const entityId = this.indexToEntity[index]!;
|
||||
const complexFieldMap = new Map<string, unknown>();
|
||||
const typeWithMeta = this.type as ComponentTypeWithMetadata<T>;
|
||||
const highPrecisionFields = typeWithMeta.__highPrecisionFields || new Set<string>();
|
||||
const serializeMapFields = typeWithMeta.__serializeMapFields || new Set<string>();
|
||||
const serializeSetFields = typeWithMeta.__serializeSetFields || new Set<string>();
|
||||
const serializeArrayFields = typeWithMeta.__serializeArrayFields || new Set<string>();
|
||||
const deepCopyFields = typeWithMeta.__deepCopyFields || new Set<string>();
|
||||
const complexFieldMap = new Map<string, any>();
|
||||
const highPrecisionFields = (this.type as any).__highPrecisionFields || new Set();
|
||||
const serializeMapFields = (this.type as any).__serializeMapFields || new Set();
|
||||
const serializeSetFields = (this.type as any).__serializeSetFields || new Set();
|
||||
const serializeArrayFields = (this.type as any).__serializeArrayFields || new Set();
|
||||
const deepCopyFields = (this.type as any).__deepCopyFields || new Set();
|
||||
|
||||
const componentRecord = component as Record<string, unknown>;
|
||||
// 处理所有字段
|
||||
for (const key in component) {
|
||||
if (Object.prototype.hasOwnProperty.call(component, key) && key !== 'id') {
|
||||
const value = componentRecord[key];
|
||||
if (component.hasOwnProperty(key) && key !== 'id') {
|
||||
const value = (component as any)[key];
|
||||
const type = typeof value;
|
||||
|
||||
if (type === 'number') {
|
||||
const numValue = value as number;
|
||||
if (highPrecisionFields.has(key) || !this.fields.has(key)) {
|
||||
complexFieldMap.set(key, numValue);
|
||||
// 标记为高精度或未在TypedArray中的数值作为复杂对象存储
|
||||
complexFieldMap.set(key, value);
|
||||
} else {
|
||||
// 存储到TypedArray
|
||||
const array = this.fields.get(key)!;
|
||||
array[index] = numValue;
|
||||
array[index] = value;
|
||||
}
|
||||
} else if (type === 'boolean' && this.fields.has(key)) {
|
||||
// 布尔值存储到TypedArray
|
||||
const array = this.fields.get(key)!;
|
||||
array[index] = value ? 1 : 0;
|
||||
} 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 {
|
||||
const index = this.entityToIndex.get(entityId);
|
||||
@@ -539,26 +534,32 @@ export class SoAStorage<T extends Component> {
|
||||
return null;
|
||||
}
|
||||
|
||||
const component = new this.type();
|
||||
const componentRecord = component as unknown as Record<string, unknown>;
|
||||
// 需要 any 因为要动态写入泛型 T 的属性
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const component = new this.type() as any;
|
||||
|
||||
// 恢复数值字段
|
||||
for (const [fieldName, array] of this.fields.entries()) {
|
||||
const value = array[index];
|
||||
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()) {
|
||||
componentRecord[fieldName] = stringArray[index];
|
||||
component[fieldName] = stringArray[index];
|
||||
}
|
||||
|
||||
// 恢复序列化字段
|
||||
for (const [fieldName, serializedArray] of this.serializedFields.entries()) {
|
||||
const serialized = serializedArray[index];
|
||||
if (serialized) {
|
||||
componentRecord[fieldName] = SoASerializer.deserialize(serialized, fieldName, {
|
||||
component[fieldName] = SoASerializer.deserialize(serialized, fieldName, {
|
||||
isMap: this.serializeMapFields.has(fieldName),
|
||||
isSet: this.serializeSetFields.has(fieldName),
|
||||
isArray: this.serializeArrayFields.has(fieldName)
|
||||
@@ -570,11 +571,11 @@ export class SoAStorage<T extends Component> {
|
||||
const complexFieldMap = this.complexFields.get(entityId);
|
||||
if (complexFieldMap) {
|
||||
for (const [fieldName, value] of complexFieldMap.entries()) {
|
||||
componentRecord[fieldName] = value;
|
||||
component[fieldName] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return component;
|
||||
return component as T;
|
||||
}
|
||||
|
||||
private getFieldType(fieldName: string): string {
|
||||
@@ -672,14 +673,14 @@ export class SoAStorage<T extends Component> {
|
||||
// 重置字符串字段数组
|
||||
for (const stringArray of this.stringFields.values()) {
|
||||
for (let i = 0; i < stringArray.length; i++) {
|
||||
stringArray[i] = undefined;
|
||||
stringArray[i] = undefined as any;
|
||||
}
|
||||
}
|
||||
|
||||
// 重置序列化字段数组
|
||||
for (const serializedArray of this.serializedFields.values()) {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取 SoA 存储统计信息
|
||||
* @en Get SoA storage statistics
|
||||
*/
|
||||
public getStats(): ISoAStorageStats {
|
||||
public getStats(): any {
|
||||
let totalMemory = 0;
|
||||
const fieldStats = new Map<string, ISoAFieldStats>();
|
||||
const fieldStats = new Map<string, any>();
|
||||
|
||||
for (const [fieldName, array] of this.fields.entries()) {
|
||||
const typeName = SoATypeRegistry.getTypeName(array);
|
||||
@@ -764,9 +761,7 @@ export class SoAStorage<T extends Component> {
|
||||
return {
|
||||
size: this._size,
|
||||
capacity: this._capacity,
|
||||
totalSlots: this._capacity,
|
||||
usedSlots: this._size,
|
||||
freeSlots: this._capacity - this._size,
|
||||
usedSlots: this._size, // 兼容原测试
|
||||
fragmentation: this.freeIndices.length / this._capacity,
|
||||
memoryUsage: totalMemory,
|
||||
fieldStats: fieldStats
|
||||
|
||||
@@ -40,7 +40,7 @@ export type TypedArrayTypeName =
|
||||
/**
|
||||
* 字段元数据
|
||||
*/
|
||||
export type FieldMetadata = {
|
||||
export interface FieldMetadata {
|
||||
name: string;
|
||||
type: 'number' | 'boolean' | 'string' | 'object';
|
||||
arrayType?: TypedArrayTypeName;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export { ComponentPoolManager } from '../ComponentPool';
|
||||
export type { ComponentPool } from '../ComponentPool';
|
||||
export { ComponentPool, ComponentPoolManager } from '../ComponentPool';
|
||||
export { ComponentStorage, ComponentRegistry, GlobalComponentRegistry } from '../ComponentStorage';
|
||||
export type { IComponentRegistry } from '../ComponentStorage';
|
||||
export { EnableSoA, Float64, Float32, Int32, SerializeMap, SoAStorage } from '../SoAStorage';
|
||||
|
||||
@@ -17,8 +17,12 @@
|
||||
* ```
|
||||
*/
|
||||
|
||||
// 从SoAStorage导入所有装饰器和类型
|
||||
export {
|
||||
// 启用装饰器
|
||||
EnableSoA,
|
||||
|
||||
// 数值类型装饰器
|
||||
Float64,
|
||||
Float32,
|
||||
Int32,
|
||||
@@ -28,10 +32,13 @@ export {
|
||||
Int8,
|
||||
Uint8,
|
||||
Uint8Clamped,
|
||||
|
||||
// 序列化装饰器
|
||||
SerializeMap,
|
||||
SerializeSet,
|
||||
SerializeArray,
|
||||
DeepCopy
|
||||
} from './SoAStorage';
|
||||
DeepCopy,
|
||||
|
||||
export type { SupportedTypedArray } from './SoAStorage';
|
||||
// 类型定义
|
||||
SupportedTypedArray
|
||||
} from './SoAStorage';
|
||||
|
||||
@@ -48,7 +48,7 @@ interface GraphNode {
|
||||
* 系统依赖信息
|
||||
* System dependency info
|
||||
*/
|
||||
export type SystemDependencyInfo = {
|
||||
export interface SystemDependencyInfo {
|
||||
/** 系统名称 | System name */
|
||||
name: string;
|
||||
/** 在这些系统之前执行 | Execute before these systems */
|
||||
|
||||
@@ -27,11 +27,8 @@
|
||||
*/
|
||||
|
||||
import { SystemDependencyGraph, CycleDependencyError, type SystemDependencyInfo } from './SystemDependencyGraph';
|
||||
import { createLogger } from '../../Utils/Logger';
|
||||
import type { EntitySystem } from '../Systems/EntitySystem';
|
||||
|
||||
const logger = createLogger('SystemScheduler');
|
||||
|
||||
export { CycleDependencyError };
|
||||
|
||||
/**
|
||||
@@ -56,7 +53,7 @@ export const DEFAULT_STAGE_ORDER: readonly SystemStage[] = [
|
||||
* 系统调度元数据
|
||||
* System scheduling metadata
|
||||
*/
|
||||
export type SystemSchedulingMetadata = {
|
||||
export interface SystemSchedulingMetadata {
|
||||
/** 执行阶段 | Execution stage */
|
||||
stage: SystemStage;
|
||||
/** 在这些系统之前执行 | Execute before these systems */
|
||||
@@ -296,7 +293,7 @@ export class SystemScheduler {
|
||||
throw error;
|
||||
}
|
||||
// 其他错误回退到 updateOrder 排序
|
||||
logger.warn('Topological sort failed, falling back to updateOrder | 拓扑排序失败,回退到 updateOrder 排序', error);
|
||||
console.warn('[SystemScheduler] 拓扑排序失败,回退到 updateOrder 排序', error);
|
||||
return this.fallbackSort(systems);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ const ENTITY_REF_VALUES = Symbol('EntityRefValues');
|
||||
/**
|
||||
* EntityRef元数据
|
||||
*/
|
||||
export type EntityRefMetadata = {
|
||||
export interface EntityRefMetadata {
|
||||
properties: Set<string>;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
/**
|
||||
* 属性元数据存储
|
||||
* Property metadata storage
|
||||
*/
|
||||
const metadataStorage = new WeakMap<Function, Record<string, unknown>>();
|
||||
import 'reflect-metadata';
|
||||
|
||||
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
|
||||
* 属性字段的操作按钮配置
|
||||
*/
|
||||
export type PropertyAction = {
|
||||
export interface PropertyAction {
|
||||
/** Action identifier | 操作标识符 */
|
||||
id: string;
|
||||
/** Button label | 按钮标签 */
|
||||
@@ -40,7 +36,7 @@ export type PropertyAction = {
|
||||
* 控制关系声明
|
||||
* Control relationship declaration
|
||||
*/
|
||||
export type PropertyControl = {
|
||||
export interface PropertyControl {
|
||||
/** 被控制的组件名称 | Target component name */
|
||||
component: string;
|
||||
/** 被控制的属性名称 | Target property name */
|
||||
@@ -56,16 +52,6 @@ interface PropertyOptionsBase {
|
||||
label?: string;
|
||||
/** 是否只读 | Read-only flag */
|
||||
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 | 操作按钮 */
|
||||
actions?: PropertyAction[];
|
||||
/** 此属性控制的其他组件属性 | Properties this field controls */
|
||||
@@ -207,17 +193,6 @@ interface CollisionMaskPropertyOptions extends PropertyOptionsBase {
|
||||
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
|
||||
@@ -233,8 +208,7 @@ export type PropertyOptions =
|
||||
| ArrayPropertyOptions
|
||||
| AnimationClipsPropertyOptions
|
||||
| CollisionLayerPropertyOptions
|
||||
| CollisionMaskPropertyOptions
|
||||
| EntityRefPropertyOptions;
|
||||
| CollisionMaskPropertyOptions;
|
||||
|
||||
// 使用 Symbol.for 创建全局 Symbol,确保跨包共享元数据
|
||||
// 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 {
|
||||
return (target: object, propertyKey: string | symbol) => {
|
||||
const constructor = target.constructor as Function;
|
||||
const existingMetadata = metadataStorage.get(constructor) || {};
|
||||
const constructor = target.constructor;
|
||||
const existingMetadata = Reflect.getMetadata(PROPERTY_METADATA, constructor) || {};
|
||||
|
||||
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 {
|
||||
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 {
|
||||
return metadataStorage.has(target);
|
||||
return Reflect.hasMetadata(PROPERTY_METADATA, target);
|
||||
}
|
||||
|
||||
@@ -15,9 +15,7 @@ import {
|
||||
COMPONENT_TYPE_NAME,
|
||||
COMPONENT_DEPENDENCIES,
|
||||
COMPONENT_EDITOR_OPTIONS,
|
||||
getWritableComponentTypeMetadata,
|
||||
type ComponentEditorOptions,
|
||||
type ComponentType
|
||||
type ComponentEditorOptions
|
||||
} from '../Core/ComponentStorage/ComponentTypeUtils';
|
||||
|
||||
/**
|
||||
@@ -26,55 +24,11 @@ import {
|
||||
*/
|
||||
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
|
||||
*/
|
||||
export type ComponentOptions = {
|
||||
export interface ComponentOptions {
|
||||
/** 依赖的其他组件名称列表 | List of required component names */
|
||||
requires?: string[];
|
||||
|
||||
@@ -113,29 +67,25 @@ export type 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') {
|
||||
throw new Error('ECSComponent装饰器必须提供有效的类型名称');
|
||||
}
|
||||
|
||||
// 获取可写的元数据对象
|
||||
// Get writable metadata object
|
||||
const metadata = getWritableComponentTypeMetadata(target);
|
||||
|
||||
// 在构造函数上存储类型名称
|
||||
// Store type name on constructor
|
||||
metadata[COMPONENT_TYPE_NAME] = typeName;
|
||||
(target as any)[COMPONENT_TYPE_NAME] = typeName;
|
||||
|
||||
// 存储依赖关系
|
||||
// Store dependencies
|
||||
if (options?.requires) {
|
||||
metadata[COMPONENT_DEPENDENCIES] = options.requires;
|
||||
(target as any)[COMPONENT_DEPENDENCIES] = options.requires;
|
||||
}
|
||||
|
||||
// 存储编辑器选项
|
||||
// Store editor options
|
||||
if (options?.editor) {
|
||||
metadata[COMPONENT_EDITOR_OPTIONS] = options.editor;
|
||||
(target as any)[COMPONENT_EDITOR_OPTIONS] = options.editor;
|
||||
}
|
||||
|
||||
// 自动注册到全局 ComponentRegistry,使组件可以通过名称查找
|
||||
@@ -150,7 +100,7 @@ export function ECSComponent(typeName: string, options?: ComponentOptions) {
|
||||
* System 元数据配置
|
||||
* System metadata configuration
|
||||
*/
|
||||
export type SystemMetadata = {
|
||||
export interface SystemMetadata {
|
||||
/**
|
||||
* 更新顺序(数值越小越先执行,默认0)
|
||||
* Update order (lower values execute first, default 0)
|
||||
@@ -162,21 +112,6 @@ export type SystemMetadata = {
|
||||
* Whether enabled by default (default true)
|
||||
*/
|
||||
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) {
|
||||
return function <T extends SystemConstructor>(target: T): T {
|
||||
return function <T extends new (...args: any[]) => EntitySystem>(target: T): T {
|
||||
if (!typeName || typeof typeName !== 'string') {
|
||||
throw new Error('ECSSystem装饰器必须提供有效的类型名称');
|
||||
}
|
||||
|
||||
// 获取可写的元数据对象
|
||||
// Get writable metadata object
|
||||
const meta = getWritableSystemTypeMetadata(target);
|
||||
|
||||
// 在构造函数上存储类型名称
|
||||
// Store type name on constructor
|
||||
meta[SYSTEM_TYPE_NAME] = typeName;
|
||||
(target as any)[SYSTEM_TYPE_NAME] = typeName;
|
||||
|
||||
// 存储元数据
|
||||
// Store metadata
|
||||
if (metadata) {
|
||||
meta.__systemMetadata__ = metadata;
|
||||
(target as any).__systemMetadata__ = metadata;
|
||||
}
|
||||
|
||||
return target;
|
||||
@@ -231,20 +162,8 @@ export function ECSSystem(typeName: string, metadata?: SystemMetadata) {
|
||||
* 获取 System 的元数据
|
||||
* Get System metadata
|
||||
*/
|
||||
export function getSystemMetadata(systemType: SystemConstructor): SystemMetadata | undefined {
|
||||
const meta = getSystemTypeMetadata(systemType);
|
||||
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);
|
||||
export function getSystemMetadata(systemType: new (...args: any[]) => EntitySystem): SystemMetadata | undefined {
|
||||
return (systemType as any).__systemMetadata__;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -255,10 +174,9 @@ export function getSystemInstanceMetadata(system: EntitySystem): SystemMetadata
|
||||
* @returns 系统类型名称 | System type name
|
||||
*/
|
||||
export function getSystemTypeName<T extends EntitySystem>(
|
||||
systemType: SystemConstructor<T>
|
||||
systemType: new (...args: any[]) => T
|
||||
): string {
|
||||
const meta = getSystemTypeMetadata(systemType);
|
||||
const decoratorName = meta[SYSTEM_TYPE_NAME];
|
||||
const decoratorName = (systemType as any)[SYSTEM_TYPE_NAME];
|
||||
if (decoratorName) {
|
||||
return decoratorName;
|
||||
}
|
||||
@@ -273,5 +191,5 @@ export function getSystemTypeName<T extends EntitySystem>(
|
||||
* @returns 系统类型名称 | System type name
|
||||
*/
|
||||
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,
|
||||
getSystemInstanceTypeName,
|
||||
getSystemMetadata,
|
||||
getSystemInstanceMetadata,
|
||||
SYSTEM_TYPE_NAME
|
||||
} from './TypeDecorators';
|
||||
|
||||
|
||||
+218
-180
@@ -9,156 +9,157 @@ import type { IScene } from './IScene';
|
||||
import { EntityHandle, NULL_HANDLE } from './Core/EntityHandle';
|
||||
|
||||
/**
|
||||
* @zh 组件活跃状态变化接口
|
||||
* @en Interface for component active state change
|
||||
* 组件活跃状态变化接口
|
||||
*/
|
||||
interface IActiveChangeable {
|
||||
onActiveChanged(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 比较两个实体的优先级
|
||||
* @en Compare priority of two entities
|
||||
* 实体比较器
|
||||
*
|
||||
* @param a - @zh 第一个实体 @en First entity
|
||||
* @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
|
||||
* 用于比较两个实体的优先级,首先按更新顺序比较,然后按ID比较。
|
||||
*/
|
||||
export function compareEntities(a: Entity, b: Entity): number {
|
||||
return a.updateOrder - b.updateOrder || a.id - b.id;
|
||||
export class EntityComparer {
|
||||
/**
|
||||
* 比较两个实体
|
||||
*
|
||||
* @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 管理,
|
||||
* 而非 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
|
||||
* ```typescript
|
||||
* // @zh 创建实体 | @en Create entity
|
||||
* // 创建实体
|
||||
* const entity = scene.createEntity("Player");
|
||||
*
|
||||
* // @zh 添加组件 | @en Add component
|
||||
* // 添加组件
|
||||
* const healthComponent = entity.addComponent(new HealthComponent(100));
|
||||
*
|
||||
* // @zh 获取组件 | @en Get component
|
||||
* // 获取组件
|
||||
* const health = entity.getComponent(HealthComponent);
|
||||
*
|
||||
* // @zh 层级关系使用 HierarchySystem | @en Use HierarchySystem for hierarchy
|
||||
* // 层级关系使用 HierarchySystem
|
||||
* const hierarchySystem = scene.getSystem(HierarchySystem);
|
||||
* hierarchySystem.setParent(childEntity, parentEntity);
|
||||
* ```
|
||||
*/
|
||||
export class Entity {
|
||||
/**
|
||||
* @zh Entity专用日志器
|
||||
* @en Entity logger
|
||||
* Entity专用日志器
|
||||
*/
|
||||
private static _logger = createLogger('Entity');
|
||||
|
||||
/**
|
||||
* @zh 实体名称
|
||||
* @en Entity name
|
||||
* 实体比较器实例
|
||||
*/
|
||||
public static entityComparer: EntityComparer = new EntityComparer();
|
||||
|
||||
/**
|
||||
* 实体名称
|
||||
*/
|
||||
public name: string;
|
||||
|
||||
/**
|
||||
* @zh 实体唯一标识符(运行时ID),用于快速查找
|
||||
* @en Unique entity identifier (runtime ID) for fast lookups
|
||||
* 实体唯一标识符(运行时 ID)
|
||||
*
|
||||
* Runtime identifier for fast lookups.
|
||||
*/
|
||||
public readonly id: number;
|
||||
|
||||
/**
|
||||
* @zh 持久化唯一标识符(GUID)
|
||||
* @en Persistent unique identifier (GUID)
|
||||
* 持久化唯一标识符(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;
|
||||
|
||||
/**
|
||||
* @zh 轻量级实体句柄
|
||||
* @en Lightweight entity handle
|
||||
* 轻量级实体句柄
|
||||
*
|
||||
* @zh 数值类型的实体标识符,包含索引和代数信息。
|
||||
* 数值类型的实体标识符,包含索引和代数信息。
|
||||
* 用于高性能场景下替代对象引用,支持 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,
|
||||
* supports Archetype storage optimizations.
|
||||
*/
|
||||
private _handle: EntityHandle = NULL_HANDLE;
|
||||
|
||||
/**
|
||||
* @zh 所属场景引用
|
||||
* @en Reference to the owning scene
|
||||
* 所属场景引用
|
||||
*/
|
||||
public scene: IScene | null = null;
|
||||
|
||||
/**
|
||||
* @zh 销毁状态标志
|
||||
* @en Destroyed state flag
|
||||
* 销毁状态标志
|
||||
*/
|
||||
private _isDestroyed: boolean = false;
|
||||
|
||||
/**
|
||||
* @zh 激活状态
|
||||
* @en Active state
|
||||
* 激活状态
|
||||
*/
|
||||
private _active: boolean = true;
|
||||
|
||||
/**
|
||||
* @zh 实体标签,用于分类和查询
|
||||
* @en Entity tag for categorization and querying
|
||||
* 实体标签
|
||||
*/
|
||||
private _tag: number = 0;
|
||||
|
||||
/**
|
||||
* @zh 启用状态
|
||||
* @en Enabled state
|
||||
* 启用状态
|
||||
*/
|
||||
private _enabled: boolean = true;
|
||||
|
||||
/**
|
||||
* @zh 更新顺序
|
||||
* @en Update order
|
||||
* 更新顺序
|
||||
*/
|
||||
private _updateOrder: number = 0;
|
||||
|
||||
/**
|
||||
* @zh 组件位掩码,用于快速 hasComponent 检查
|
||||
* @en Component bitmask for fast hasComponent checks
|
||||
* 组件位掩码(用于快速 hasComponent 检查)
|
||||
*/
|
||||
private _componentMask: BitMask64Data = BitMask64Utils.clone(BitMask64Utils.ZERO);
|
||||
|
||||
/**
|
||||
* @zh 懒加载的组件数组缓存
|
||||
* @en Lazy-loaded component array cache
|
||||
* 懒加载的组件数组缓存
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* @zh 构造函数
|
||||
* @en Constructor
|
||||
* 构造函数
|
||||
*
|
||||
* @param name - @zh 实体名称 @en Entity name
|
||||
* @param id - @zh 实体唯一标识符(运行时ID)@en Unique entity identifier (runtime ID)
|
||||
* @param persistentId - @zh 持久化标识符(可选,用于反序列化时恢复)@en Persistent identifier (optional, for deserialization)
|
||||
* @param name - 实体名称
|
||||
* @param id - 实体唯一标识符(运行时 ID)
|
||||
* @param persistentId - 持久化标识符(可选,用于反序列化时恢复)
|
||||
*/
|
||||
constructor(name: string, id: number, persistentId?: string) {
|
||||
this.name = name;
|
||||
@@ -167,38 +168,44 @@ export class Entity {
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取生命周期策略
|
||||
* @en Get lifecycle policy
|
||||
* 获取生命周期策略
|
||||
*
|
||||
* Get lifecycle policy.
|
||||
*/
|
||||
public get lifecyclePolicy(): EEntityLifecyclePolicy {
|
||||
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 {
|
||||
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 {
|
||||
return this._handle;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 设置实体句柄(内部使用)
|
||||
* @en Set entity handle (internal use)
|
||||
* 设置实体句柄(内部使用)
|
||||
*
|
||||
* @zh 此方法供 Scene 在创建实体时调用
|
||||
* @en Called by Scene when creating entities
|
||||
* 此方法供 Scene 在创建实体时调用。
|
||||
*
|
||||
* Set entity handle (internal use).
|
||||
* Called by Scene when creating entities.
|
||||
*
|
||||
* @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
|
||||
* ```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 {
|
||||
this._lifecyclePolicy = EEntityLifecyclePolicy.SceneLocal;
|
||||
@@ -239,21 +251,18 @@ export class Entity {
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取销毁状态
|
||||
* @en Get destroyed state
|
||||
*
|
||||
* @returns @zh 如果实体已被销毁则返回true @en Returns true if entity has been destroyed
|
||||
* 获取销毁状态
|
||||
* @returns 如果实体已被销毁则返回true
|
||||
*/
|
||||
public get isDestroyed(): boolean {
|
||||
return this._isDestroyed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 设置销毁状态(内部使用)
|
||||
* @en Set destroyed state (internal use)
|
||||
* 设置销毁状态(内部使用)
|
||||
*
|
||||
* @zh 此方法供Scene和批量操作使用,以提高性能。不应在普通业务逻辑中调用,应使用destroy()方法
|
||||
* @en Used by Scene and batch operations for performance. Should not be called in normal business logic, use destroy() instead
|
||||
* 此方法供Scene和批量操作使用,以提高性能。
|
||||
* 不应在普通业务逻辑中调用,应使用destroy()方法。
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
@@ -262,10 +271,8 @@ export class Entity {
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取组件数组(懒加载)
|
||||
* @en Get component array (lazy-loaded)
|
||||
*
|
||||
* @returns @zh 只读的组件数组 @en Readonly component array
|
||||
* 获取组件数组(懒加载)
|
||||
* @returns 只读的组件数组
|
||||
*/
|
||||
public get components(): readonly Component[] {
|
||||
if (this._componentCache === null) {
|
||||
@@ -275,8 +282,7 @@ export class Entity {
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 从存储重建组件缓存
|
||||
* @en Rebuild component cache from storage
|
||||
* 从存储重建组件缓存
|
||||
*/
|
||||
private _rebuildComponentCache(): void {
|
||||
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 {
|
||||
return this._tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 设置实体标签
|
||||
* @en Set entity tag
|
||||
* 设置实体标签
|
||||
*
|
||||
* @param value - @zh 新的标签值 @en New tag value
|
||||
* @param value - 新的标签值
|
||||
*/
|
||||
public set tag(value: number) {
|
||||
this._tag = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取启用状态
|
||||
* @en Get enabled state
|
||||
* 获取启用状态
|
||||
*
|
||||
* @returns @zh 如果实体已启用则返回true @en Returns true if entity is enabled
|
||||
* @returns 如果实体已启用则返回true
|
||||
*/
|
||||
public get enabled(): boolean {
|
||||
return this._enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 设置启用状态
|
||||
* @en Set enabled state
|
||||
* 设置启用状态
|
||||
*
|
||||
* @param value - @zh 新的启用状态 @en New enabled state
|
||||
* @param value - 新的启用状态
|
||||
*/
|
||||
public set enabled(value: boolean) {
|
||||
this._enabled = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取更新顺序
|
||||
* @en Get update order
|
||||
* 获取更新顺序
|
||||
*
|
||||
* @returns @zh 实体的更新顺序值 @en Entity's update order value
|
||||
* @returns 实体的更新顺序值
|
||||
*/
|
||||
public get updateOrder(): number {
|
||||
return this._updateOrder;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 设置更新顺序
|
||||
* @en Set update order
|
||||
* 设置更新顺序
|
||||
*
|
||||
* @param value - @zh 新的更新顺序值 @en New update order value
|
||||
* @param value - 新的更新顺序值
|
||||
*/
|
||||
public set updateOrder(value: number) {
|
||||
this._updateOrder = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取组件位掩码
|
||||
* @en Get component bitmask
|
||||
* 获取组件位掩码
|
||||
*
|
||||
* @returns @zh 实体的组件位掩码 @en Entity's component bitmask
|
||||
* @returns 实体的组件位掩码
|
||||
*/
|
||||
public get componentMask(): BitMask64Data {
|
||||
return this._componentMask;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 创建并添加组件
|
||||
* @en Create and add component
|
||||
* 创建并添加组件
|
||||
*
|
||||
* @param componentType - @zh 组件类型构造函数 @en Component type constructor
|
||||
* @param args - @zh 组件构造函数参数 @en Component constructor arguments
|
||||
* @returns @zh 创建的组件实例 @en Created component instance
|
||||
* @param componentType - 组件类型构造函数
|
||||
* @param args - 组件构造函数参数
|
||||
* @returns 创建的组件实例
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
@@ -420,30 +418,51 @@ export class Entity {
|
||||
return this.addComponent(component);
|
||||
}
|
||||
|
||||
/**
|
||||
* 内部添加组件方法(不进行重复检查,用于初始化)
|
||||
*
|
||||
* @param component - 要添加的组件实例
|
||||
* @returns 添加的组件实例
|
||||
*/
|
||||
private addComponentInternal<T extends Component>(component: T): T {
|
||||
const componentType = component.constructor as ComponentType<T>;
|
||||
|
||||
// 更新位掩码(组件已通过 @ECSComponent 装饰器自动注册)
|
||||
// Update bitmask (component already registered via @ECSComponent decorator)
|
||||
const registry = this.scene?.componentRegistry ?? GlobalComponentRegistry;
|
||||
const componentMask = registry.getBitMask(componentType);
|
||||
BitMask64Utils.orInPlace(this._componentMask, componentMask);
|
||||
|
||||
// 使缓存失效
|
||||
this._componentCache = null;
|
||||
|
||||
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 {
|
||||
if (!this.scene?.querySystem) return;
|
||||
|
||||
this.scene.querySystem.updateEntity(this);
|
||||
this.scene.clearSystemEntityCaches();
|
||||
this.scene.notifyEntityComponentChanged?.(this, changedComponentType);
|
||||
if (this.scene && this.scene.querySystem) {
|
||||
this.scene.querySystem.updateEntity(this);
|
||||
this.scene.clearSystemEntityCaches();
|
||||
// 事件驱动:立即通知关心该组件的系统 | Event-driven: notify systems that care about this component
|
||||
if (this.scene.notifyEntityComponentChanged) {
|
||||
this.scene.notifyEntityComponentChanged(this, changedComponentType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 添加组件到实体
|
||||
* @en Add component to entity
|
||||
* 添加组件到实体
|
||||
*
|
||||
* @param component - @zh 要添加的组件实例 @en Component instance to add
|
||||
* @returns @zh 添加的组件实例 @en Added component instance
|
||||
* @throws @zh 如果实体已存在该类型的组件 @en If entity already has this component type
|
||||
* @param component - 要添加的组件实例
|
||||
* @returns 添加的组件实例
|
||||
* @throws {Error} 如果实体已存在该类型的组件
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
@@ -473,15 +492,20 @@ export class Entity {
|
||||
this.scene.componentStorageManager.addComponent(this.id, component);
|
||||
|
||||
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) {
|
||||
this.scene.queueDeferredComponentCallback(() => component.onAddedToEntity());
|
||||
this.scene.queueDeferredComponentCallback(() => {
|
||||
component.onAddedToEntity();
|
||||
});
|
||||
} else {
|
||||
component.onAddedToEntity();
|
||||
}
|
||||
|
||||
if (this.scene.eventSystem) {
|
||||
if (this.scene && this.scene.eventSystem) {
|
||||
this.scene.eventSystem.emitSync('component:added', {
|
||||
timestamp: Date.now(),
|
||||
source: 'Entity',
|
||||
@@ -499,11 +523,10 @@ export class Entity {
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取指定类型的组件
|
||||
* @en Get component of specified type
|
||||
* 获取指定类型的组件
|
||||
*
|
||||
* @param type - @zh 组件类型构造函数 @en Component type constructor
|
||||
* @returns @zh 组件实例,如果不存在则返回null @en Component instance, or null if not found
|
||||
* @param type - 组件类型构造函数
|
||||
* @returns 组件实例,如果不存在则返回null
|
||||
*
|
||||
* @example
|
||||
* ```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
|
||||
* @returns @zh 如果实体拥有该组件返回true @en Returns true if entity has the component
|
||||
* @param type - 组件类型构造函数
|
||||
* @returns 如果实体拥有该组件返回true,否则返回false
|
||||
*
|
||||
* @example
|
||||
* ```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 args - @zh 组件构造函数参数(仅在创建新组件时使用)@en Constructor arguments (only used when creating new component)
|
||||
* @returns @zh 组件实例 @en Component instance
|
||||
* @param type - 组件类型构造函数
|
||||
* @param args - 组件构造函数参数(仅在创建新组件时使用)
|
||||
* @returns 组件实例
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // @zh 确保实体拥有Position组件 | @en Ensure entity has Position component
|
||||
* // 确保实体拥有Position组件
|
||||
* const position = entity.getOrCreateComponent(Position, 0, 0);
|
||||
* position.x = 100;
|
||||
* ```
|
||||
@@ -585,13 +605,16 @@ export class Entity {
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 标记组件为已修改
|
||||
* @en Mark component(s) as modified
|
||||
* 标记组件为已修改
|
||||
*
|
||||
* @zh 便捷方法,自动从场景获取当前 epoch 并标记组件。用于帧级变更检测系统。
|
||||
* @en Convenience method that auto-gets epoch from scene and marks components. Used for frame-level change detection system.
|
||||
* 便捷方法,自动从场景获取当前 epoch 并标记组件。
|
||||
* 用于帧级变更检测系统。
|
||||
*
|
||||
* @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
|
||||
* ```typescript
|
||||
@@ -599,7 +622,7 @@ export class Entity {
|
||||
* pos.x = 100;
|
||||
* entity.markDirty(pos);
|
||||
*
|
||||
* // @zh 或者标记多个组件 | @en Or mark multiple components
|
||||
* // 或者标记多个组件
|
||||
* 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 {
|
||||
const componentType = component.constructor as ComponentType;
|
||||
@@ -629,16 +651,29 @@ export class Entity {
|
||||
}
|
||||
|
||||
const bitIndex = registry.getBitIndex(componentType);
|
||||
|
||||
// 更新位掩码
|
||||
BitMask64Utils.clearBit(this._componentMask, bitIndex);
|
||||
|
||||
// 使缓存失效
|
||||
this._componentCache = null;
|
||||
|
||||
this.scene?.componentStorageManager?.removeComponent(this.id, componentType);
|
||||
this.scene?.referenceTracker?.clearComponentReferences(component);
|
||||
// 从Scene存储移除
|
||||
if (this.scene?.componentStorageManager) {
|
||||
this.scene.componentStorageManager.removeComponent(this.id, componentType);
|
||||
}
|
||||
|
||||
if (this.scene?.referenceTracker) {
|
||||
this.scene.referenceTracker.clearComponentReferences(component);
|
||||
}
|
||||
|
||||
if (component.onRemovedFromEntity) {
|
||||
component.onRemovedFromEntity();
|
||||
}
|
||||
|
||||
component.onRemovedFromEntity?.();
|
||||
component.entityId = null;
|
||||
|
||||
if (this.scene?.eventSystem) {
|
||||
if (this.scene && this.scene.eventSystem) {
|
||||
this.scene.eventSystem.emitSync('component:removed', {
|
||||
timestamp: Date.now(),
|
||||
source: 'Entity',
|
||||
@@ -654,11 +689,10 @@ export class Entity {
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 移除指定类型的组件
|
||||
* @en Remove component by type
|
||||
* 移除指定类型的组件
|
||||
*
|
||||
* @param type - @zh 组件类型 @en Component type
|
||||
* @returns @zh 被移除的组件实例或null @en Removed component instance or null
|
||||
* @param type - 组件类型
|
||||
* @returns 被移除的组件实例或null
|
||||
*/
|
||||
public removeComponentByType<T extends Component>(type: ComponentType<T>): T | null {
|
||||
const component = this.getComponent(type);
|
||||
@@ -670,17 +704,24 @@ export class Entity {
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 移除所有组件
|
||||
* @en Remove all components
|
||||
* 移除所有组件
|
||||
*/
|
||||
public removeAllComponents(): void {
|
||||
const componentsToRemove = [...this.components];
|
||||
|
||||
// 清除位掩码
|
||||
BitMask64Utils.clear(this._componentMask);
|
||||
|
||||
// 使缓存失效
|
||||
this._componentCache = null;
|
||||
|
||||
for (const component of componentsToRemove) {
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -688,11 +729,10 @@ export class Entity {
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 批量添加组件
|
||||
* @en Add multiple components
|
||||
* 批量添加组件
|
||||
*
|
||||
* @param components - @zh 要添加的组件数组 @en Array of components to add
|
||||
* @returns @zh 添加的组件数组 @en Array of added components
|
||||
* @param components - 要添加的组件数组
|
||||
* @returns 添加的组件数组
|
||||
*/
|
||||
public addComponents<T extends Component>(components: T[]): 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 {
|
||||
if (this._isDestroyed) {
|
||||
@@ -820,14 +859,13 @@ export class Entity {
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 比较实体优先级
|
||||
* @en Compare entity priority
|
||||
* 比较实体
|
||||
*
|
||||
* @param other - @zh 另一个实体 @en Another entity
|
||||
* @returns @zh 比较结果 @en Comparison result
|
||||
* @param other - 另一个实体
|
||||
* @returns 比较结果
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* @zh 是否从全局注册表继承组件类型
|
||||
* @en Whether to inherit component types from global registry
|
||||
* 是否从全局注册表继承组件类型
|
||||
* Whether to inherit component types from global registry
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
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 { ReferenceTracker } from './Core/ReferenceTracker';
|
||||
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 {
|
||||
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();
|
||||
|
||||
/**
|
||||
* @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);
|
||||
|
||||
try {
|
||||
// 使用 SystemScheduler 进行依赖排序
|
||||
this._systemScheduler.markDirty();
|
||||
return this._systemScheduler.getAllSortedSystems(systems);
|
||||
} catch (error) {
|
||||
if (error instanceof CycleDependencyError) {
|
||||
this._logger.error(
|
||||
// 循环依赖错误,记录警告并回退到 updateOrder 排序
|
||||
this.logger.error(
|
||||
`[Scene] 系统存在循环依赖,回退到 updateOrder 排序 | Cycle dependency detected, falling back to updateOrder sort`,
|
||||
error.involvedNodes
|
||||
);
|
||||
} else {
|
||||
this._logger.error(`[Scene] 系统排序失败 | System sorting failed`, error);
|
||||
this.logger.error(`[Scene] 系统排序失败 | System sorting failed`, error);
|
||||
}
|
||||
// 回退到简单的 updateOrder 排序
|
||||
return this._sortSystemsByUpdateOrder(systems);
|
||||
}
|
||||
}
|
||||
@@ -374,18 +378,20 @@ export class Scene implements IScene {
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 创建场景实例
|
||||
* @en Create scene instance
|
||||
*
|
||||
* @param config - @zh 场景配置 @en Scene configuration
|
||||
* 创建场景实例
|
||||
* Create scene instance
|
||||
*/
|
||||
constructor(config?: ISceneConfig) {
|
||||
this.entities = new EntityList(this);
|
||||
this.identifierPool = new IdentifierPool();
|
||||
this.componentStorageManager = new ComponentStorageManager();
|
||||
|
||||
// 创建场景级别的组件注册表
|
||||
// Create scene-level component registry
|
||||
this.componentRegistry = new ComponentRegistry();
|
||||
|
||||
// 从全局注册表继承框架组件(默认启用)
|
||||
// Inherit framework components from global registry (enabled by default)
|
||||
if (config?.inheritGlobalRegistry !== false) {
|
||||
this.componentRegistry.cloneFrom(GlobalComponentRegistry);
|
||||
}
|
||||
@@ -395,8 +401,7 @@ export class Scene implements IScene {
|
||||
this.referenceTracker = new ReferenceTracker();
|
||||
this.handleManager = new EntityHandleManager();
|
||||
this._services = new ServiceContainer();
|
||||
this._logger = createLogger('Scene');
|
||||
this._maxErrorCount = config?.maxSystemErrorCount ?? 10;
|
||||
this.logger = createLogger('Scene');
|
||||
|
||||
if (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.
|
||||
*/
|
||||
public begin() {
|
||||
// 标记场景已开始运行
|
||||
this._didSceneBegin = true;
|
||||
|
||||
// 执行所有延迟的组件生命周期回调 | Execute all deferred component lifecycle callbacks
|
||||
if (this._deferredComponentCallbacks.length > 0) {
|
||||
for (const callback of this._deferredComponentCallbacks) {
|
||||
try {
|
||||
callback();
|
||||
} 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 = [];
|
||||
}
|
||||
|
||||
// 调用onStart方法
|
||||
this.onStart();
|
||||
}
|
||||
|
||||
@@ -492,18 +501,38 @@ export class Scene implements IScene {
|
||||
* - 系统清理:在 System.onDestroy() 中处理(实体已被清理)
|
||||
*/
|
||||
public end() {
|
||||
// 标记场景已结束运行
|
||||
this._didSceneBegin = false;
|
||||
|
||||
// 先调用用户的卸载方法,此时用户可以访问实体和系统进行清理
|
||||
this.unload();
|
||||
|
||||
// 移除所有实体
|
||||
this.entities.removeAllEntities();
|
||||
|
||||
// 清理查询系统中的实体引用和缓存
|
||||
this.querySystem.setEntities([]);
|
||||
|
||||
// 清空组件存储
|
||||
this.componentStorageManager.clear();
|
||||
|
||||
// 清空服务容器(会调用所有服务的dispose方法,包括所有EntitySystem)
|
||||
// 系统的 onDestroy 回调会在这里被触发
|
||||
this._services.clear();
|
||||
|
||||
// 清空系统缓存
|
||||
this._cachedSystems = null;
|
||||
this._systemsOrderDirty = true;
|
||||
|
||||
// 清空组件索引 | Clear component indices
|
||||
this._componentIdToSystems.clear();
|
||||
this._globalNotifySystems.clear();
|
||||
|
||||
// 清空句柄映射并重置句柄管理器 | Clear handle mapping and reset handle manager
|
||||
this._handleToEntity.clear();
|
||||
this.handleManager.reset();
|
||||
|
||||
// 重置 epoch 管理器 | Reset epoch manager
|
||||
this.epochManager.reset();
|
||||
}
|
||||
|
||||
@@ -511,61 +540,68 @@ export class Scene implements IScene {
|
||||
* 更新场景
|
||||
*/
|
||||
public update() {
|
||||
// 递增帧计数(用于变更检测) | Increment epoch (for change detection)
|
||||
this.epochManager.increment();
|
||||
|
||||
// 开始性能采样帧
|
||||
ProfilerSDK.beginFrame();
|
||||
const frameHandle = ProfilerSDK.beginSample('Scene.update', ProfileCategory.ECS);
|
||||
|
||||
try {
|
||||
ComponentPoolManager.getInstance().update();
|
||||
|
||||
this.entities.updateLists();
|
||||
|
||||
const systems = this.systems;
|
||||
|
||||
this._runSystemPhase(systems, 'update', 'Systems.update');
|
||||
this._runSystemPhase(systems, 'lateUpdate', 'Systems.lateUpdate');
|
||||
// Update 阶段
|
||||
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);
|
||||
} finally {
|
||||
ProfilerSDK.endSample(frameHandle);
|
||||
// 结束性能采样帧
|
||||
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
|
||||
@@ -580,7 +616,7 @@ export class Scene implements IScene {
|
||||
try {
|
||||
system.flushCommands();
|
||||
} 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 {
|
||||
@@ -601,16 +637,15 @@ export class Scene implements IScene {
|
||||
const errorCount = (this._systemErrorCount.get(system) || 0) + 1;
|
||||
this._systemErrorCount.set(system, errorCount);
|
||||
|
||||
const name = system.systemName;
|
||||
this._logger.error(
|
||||
`Error in system ${name}.${phase}() [${errorCount}/${this._maxErrorCount}]:`,
|
||||
this.logger.error(
|
||||
`Error in system ${system.constructor.name}.${phase}() [${errorCount}/${this._maxErrorCount}]:`,
|
||||
error
|
||||
);
|
||||
|
||||
if (errorCount >= this._maxErrorCount) {
|
||||
system.enabled = false;
|
||||
this._logger.error(
|
||||
`System ${name} has been disabled due to excessive errors (${errorCount} errors)`
|
||||
this.logger.error(
|
||||
`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) {
|
||||
const entity = new Entity(name, this.identifierPool.checkOut());
|
||||
|
||||
// 分配轻量级句柄 | Assign lightweight handle
|
||||
const handle = this.handleManager.create();
|
||||
entity.setHandle(handle);
|
||||
|
||||
// 添加到句柄映射 | Add to handle mapping
|
||||
this._handleToEntity.set(handle, entity);
|
||||
|
||||
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)
|
||||
*/
|
||||
public notifyEntityComponentChanged(entity: Entity, changedComponentType?: ComponentType): void {
|
||||
// 已通知的系统集合,避免重复通知 | Set of notified systems to avoid duplicates
|
||||
const notifiedSystems = new Set<EntitySystem>();
|
||||
|
||||
// 如果提供了组件类型,使用索引优化 | If component type provided, use index optimization
|
||||
if (changedComponentType && this.componentRegistry.isRegistered(changedComponentType)) {
|
||||
const componentId = this.componentRegistry.getBitIndex(changedComponentType);
|
||||
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) {
|
||||
if (!notifiedSystems.has(system)) {
|
||||
system.handleEntityComponentChanged(entity);
|
||||
@@ -677,6 +718,7 @@ export class Scene implements IScene {
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有提供组件类型,回退到遍历所有系统 | Fallback to all systems if no component type
|
||||
if (!changedComponentType) {
|
||||
for (const system of this.systems) {
|
||||
if (!notifiedSystems.has(system)) {
|
||||
@@ -702,29 +744,35 @@ export class Scene implements IScene {
|
||||
return;
|
||||
}
|
||||
|
||||
// nothing 匹配器不需要索引 | Nothing matcher doesn't need indexing
|
||||
if (matcher.isNothing()) {
|
||||
return;
|
||||
}
|
||||
|
||||
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) {
|
||||
this._globalNotifySystems.add(system);
|
||||
}
|
||||
|
||||
// 空匹配器(匹配所有实体)加入全局通知 | Empty matcher (matches all) goes to global
|
||||
if (matcher.isEmpty()) {
|
||||
this._globalNotifySystems.add(system);
|
||||
return;
|
||||
}
|
||||
|
||||
// 索引 all 条件中的组件 | Index components in all condition
|
||||
for (const componentType of condition.all) {
|
||||
this.addSystemToComponentIndex(componentType, system);
|
||||
}
|
||||
|
||||
// 索引 any 条件中的组件 | Index components in any condition
|
||||
for (const componentType of condition.any) {
|
||||
this.addSystemToComponentIndex(componentType, system);
|
||||
}
|
||||
|
||||
// 索引单组件查询 | Index single component query
|
||||
if (condition.component) {
|
||||
this.addSystemToComponentIndex(condition.component, system);
|
||||
}
|
||||
@@ -758,8 +806,10 @@ export class Scene implements IScene {
|
||||
* @param system 要移除的系统 | The system to remove
|
||||
*/
|
||||
private removeSystemFromIndex(system: EntitySystem): void {
|
||||
// 从全局通知列表移除 | Remove from global notify list
|
||||
this._globalNotifySystems.delete(system);
|
||||
|
||||
// 从所有组件索引中移除 | Remove from all component indices
|
||||
for (const systems of this._componentIdToSystems.values()) {
|
||||
systems.delete(system);
|
||||
}
|
||||
@@ -774,12 +824,15 @@ export class Scene implements IScene {
|
||||
this.entities.add(entity);
|
||||
entity.scene = this;
|
||||
|
||||
// 将实体添加到查询系统(可延迟缓存清理)
|
||||
this.querySystem.addEntity(entity, deferCacheClear);
|
||||
|
||||
// 清除系统缓存以确保系统能及时发现新实体
|
||||
if (!deferCacheClear) {
|
||||
this.clearSystemEntityCaches();
|
||||
}
|
||||
|
||||
// 触发实体添加事件
|
||||
this.eventSystem.emitSync('entity:added', { entity, scene: this });
|
||||
|
||||
return entity;
|
||||
@@ -794,22 +847,30 @@ export class Scene implements IScene {
|
||||
public createEntities(count: number, namePrefix: string = 'Entity'): Entity[] {
|
||||
const entities: Entity[] = [];
|
||||
|
||||
// 批量创建实体对象,不立即添加到系统
|
||||
for (let i = 0; i < count; i++) {
|
||||
const entity = new Entity(`${namePrefix}_${i}`, this.identifierPool.checkOut());
|
||||
entity.scene = this;
|
||||
|
||||
// 分配轻量级句柄 | Assign lightweight handle
|
||||
const handle = this.handleManager.create();
|
||||
entity.setHandle(handle);
|
||||
|
||||
// 添加到句柄映射 | Add to handle mapping
|
||||
this._handleToEntity.set(handle, entity);
|
||||
|
||||
entities.push(entity);
|
||||
}
|
||||
|
||||
// 批量添加到实体列表
|
||||
for (const entity of entities) {
|
||||
this.entities.add(entity);
|
||||
}
|
||||
|
||||
// 批量添加到查询系统(无重复检查,性能最优)
|
||||
this.querySystem.addEntitiesUnchecked(entities);
|
||||
|
||||
// 批量触发事件(可选,减少事件开销)
|
||||
this.eventSystem.emitSync('entities:batch_added', { entities, scene: this, count });
|
||||
|
||||
return entities;
|
||||
@@ -833,6 +894,7 @@ export class Scene implements IScene {
|
||||
this.entities.remove(entity);
|
||||
this.querySystem.removeEntity(entity);
|
||||
|
||||
// 销毁句柄并从映射中移除 | Destroy handle and remove from mapping
|
||||
if (isValidHandle(entity.handle)) {
|
||||
this._handleToEntity.delete(entity.handle);
|
||||
this.handleManager.destroy(entity.handle);
|
||||
@@ -929,9 +991,14 @@ export class Scene implements IScene {
|
||||
const persistentEntities = this.findPersistentEntities();
|
||||
|
||||
for (const entity of persistentEntities) {
|
||||
// 从实体列表移除
|
||||
this.entities.remove(entity);
|
||||
|
||||
// 从查询系统移除
|
||||
this.querySystem.removeEntity(entity);
|
||||
entity.scene = null; // detach but preserve component data
|
||||
|
||||
// 清除场景引用(但保留组件数据)
|
||||
entity.scene = null;
|
||||
}
|
||||
|
||||
return persistentEntities;
|
||||
@@ -950,16 +1017,23 @@ export class Scene implements IScene {
|
||||
*/
|
||||
public receiveMigratedEntities(entities: Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
// 设置新场景引用
|
||||
entity.scene = this;
|
||||
|
||||
// 添加到实体列表
|
||||
this.entities.add(entity);
|
||||
|
||||
// 添加到查询系统
|
||||
this.querySystem.addEntity(entity);
|
||||
|
||||
// 重新注册组件到新场景的存储
|
||||
for (const component of entity.components) {
|
||||
this.componentStorageManager.addComponent(entity.id, component);
|
||||
this.referenceTracker?.registerEntityScene(entity.id, this);
|
||||
}
|
||||
}
|
||||
|
||||
// 清除系统缓存
|
||||
if (entities.length > 0) {
|
||||
this.clearSystemEntityCaches();
|
||||
}
|
||||
@@ -1087,7 +1161,7 @@ export class Scene implements IScene {
|
||||
|
||||
if (this._services.isRegistered(constructor)) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1103,10 +1177,10 @@ export class Scene implements IScene {
|
||||
if (this._services.isRegistered(constructor)) {
|
||||
const existingSystem = this._services.resolve(constructor);
|
||||
if (existingSystem === system) {
|
||||
this._logger.debug(`System ${constructor.name} instance already registered, returning it`);
|
||||
this.logger.debug(`System ${constructor.name} instance already registered, returning it`);
|
||||
return system;
|
||||
} else {
|
||||
this._logger.warn(
|
||||
this.logger.warn(
|
||||
`Attempting to register a different instance of ${constructor.name}, ` +
|
||||
'but type is already registered. Returning existing instance.'
|
||||
);
|
||||
@@ -1116,7 +1190,10 @@ export class Scene implements IScene {
|
||||
}
|
||||
|
||||
system.scene = this;
|
||||
system.addOrder = this._systemAddCounter++; // for stable sorting
|
||||
|
||||
// 分配添加顺序,用于稳定排序 | Assign add order for stable sorting
|
||||
system.addOrder = this._systemAddCounter++;
|
||||
|
||||
system.setPerformanceMonitor(this.performanceMonitor);
|
||||
|
||||
const metadata = getSystemMetadata(constructor);
|
||||
@@ -1128,18 +1205,23 @@ export class Scene implements IScene {
|
||||
}
|
||||
|
||||
this._services.registerInstance(constructor, system);
|
||||
|
||||
// 标记系统列表已变化
|
||||
this.markSystemsOrderDirty();
|
||||
|
||||
// 建立组件类型到系统的索引 | Build component type to system index
|
||||
this.indexSystemByComponents(system);
|
||||
|
||||
injectProperties(system, this._services);
|
||||
|
||||
// Auto-wrap system methods for profiling in debug mode
|
||||
// 调试模式下自动包装系统方法以收集性能数据(ProfilerSDK 启用时表示调试模式)
|
||||
if (ProfilerSDK.isEnabled()) {
|
||||
AutoProfiler.wrapInstance(system, system.systemName, ProfileCategory.ECS);
|
||||
}
|
||||
|
||||
system.initialize();
|
||||
|
||||
this._logger.debug(`System ${constructor.name} registered and initialized`);
|
||||
this.logger.debug(`System ${constructor.name} registered and initialized`);
|
||||
|
||||
return system;
|
||||
}
|
||||
@@ -1208,9 +1290,16 @@ export class Scene implements IScene {
|
||||
public removeEntityProcessor(processor: EntitySystem): void {
|
||||
const constructor = processor.constructor as ServiceType<EntitySystem>;
|
||||
|
||||
// 从ServiceContainer移除
|
||||
this._services.unregister(constructor);
|
||||
|
||||
// 标记系统列表已变化
|
||||
this.markSystemsOrderDirty();
|
||||
|
||||
// 从组件类型索引中移除 | Remove from component type index
|
||||
this.removeSystemFromIndex(processor);
|
||||
|
||||
// 重置System状态
|
||||
processor.reset();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,133 +1,363 @@
|
||||
/**
|
||||
* 组件序列化器
|
||||
*
|
||||
* Component serializer for ECS components.
|
||||
* 负责组件的序列化和反序列化操作
|
||||
*/
|
||||
|
||||
import { Component } from '../Component';
|
||||
import { ComponentType } from '../Core/ComponentStorage';
|
||||
import { getComponentTypeName, isEntityRefProperty } from '../Decorators';
|
||||
import { getSerializationMetadata } from './SerializationDecorators';
|
||||
import { ValueSerializer, SerializableValue } from './ValueSerializer';
|
||||
import { createLogger } from '../../Utils/Logger';
|
||||
import {
|
||||
getSerializationMetadata
|
||||
} from './SerializationDecorators';
|
||||
import type { Entity } from '../Entity';
|
||||
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;
|
||||
|
||||
/**
|
||||
* 序列化版本
|
||||
*/
|
||||
version: number;
|
||||
|
||||
/**
|
||||
* 组件数据
|
||||
*/
|
||||
data: Record<string, SerializableValue>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件序列化器类
|
||||
*/
|
||||
export class ComponentSerializer {
|
||||
static serialize(component: Component): SerializedComponent | null {
|
||||
/**
|
||||
* 序列化单个组件
|
||||
*
|
||||
* @param component 要序列化的组件实例
|
||||
* @returns 序列化后的组件数据,如果组件不可序列化则返回null
|
||||
*/
|
||||
public static serialize(component: Component): SerializedComponent | null {
|
||||
const metadata = getSerializationMetadata(component);
|
||||
if (!metadata) return null;
|
||||
|
||||
if (!metadata) {
|
||||
// 组件没有使用@Serializable装饰器,不可序列化
|
||||
return null;
|
||||
}
|
||||
|
||||
const componentType = component.constructor as ComponentType;
|
||||
const typeName = metadata.options.typeId || getComponentTypeName(componentType);
|
||||
const data: Record<string, SerializableValue> = {};
|
||||
|
||||
// 序列化标记的字段
|
||||
for (const [fieldName, options] of metadata.fields) {
|
||||
if (metadata.ignoredFields.has(fieldName)) continue;
|
||||
|
||||
const fieldKey = typeof fieldName === 'symbol' ? fieldName.toString() : fieldName;
|
||||
const value = (component as unknown as Record<string | symbol, unknown>)[fieldName];
|
||||
|
||||
// 跳过忽略的字段
|
||||
if (metadata.ignoredFields.has(fieldName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let serializedValue: SerializableValue;
|
||||
|
||||
// 检查是否为 EntityRef 属性
|
||||
if (isEntityRefProperty(component, fieldKey)) {
|
||||
serializedValue = this.serializeEntityRef(value as Entity | null);
|
||||
} else if (options.serializer) {
|
||||
// 使用自定义序列化器
|
||||
serializedValue = options.serializer(value);
|
||||
} 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,
|
||||
componentRegistry: Map<string, ComponentType>,
|
||||
context?: SerializationContext
|
||||
): Component | null {
|
||||
const componentClass = componentRegistry.get(serializedData.type);
|
||||
|
||||
if (!componentClass) {
|
||||
logger.warn(`Component type not found: ${serializedData.type} | 未找到组件类型: ${serializedData.type}`);
|
||||
console.warn(`未找到组件类型: ${serializedData.type}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const metadata = getSerializationMetadata(componentClass);
|
||||
|
||||
if (!metadata) {
|
||||
logger.warn(`Component ${serializedData.type} is not serializable | 组件 ${serializedData.type} 不可序列化`);
|
||||
console.warn(`组件 ${serializedData.type} 不可序列化`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 创建组件实例
|
||||
const component = new componentClass();
|
||||
|
||||
// 反序列化字段
|
||||
for (const [fieldName, options] of metadata.fields) {
|
||||
const fieldKey = typeof fieldName === 'symbol' ? fieldName.toString() : fieldName;
|
||||
const key = options.alias || fieldKey;
|
||||
const serializedValue = serializedData.data[key];
|
||||
|
||||
if (serializedValue === undefined) continue;
|
||||
if (serializedValue === undefined) {
|
||||
continue; // 字段不存在于序列化数据中
|
||||
}
|
||||
|
||||
// 检查是否为序列化的 EntityRef
|
||||
if (this.isSerializedEntityRef(serializedValue)) {
|
||||
// EntityRef 需要延迟解析
|
||||
if (context) {
|
||||
const ref = serializedValue.__entityRef;
|
||||
context.registerPendingRef(component, fieldKey, ref.id, ref.guid);
|
||||
}
|
||||
// 暂时设为 null,后续由 context.resolveAllReferences() 填充
|
||||
(component as unknown as Record<string | symbol, unknown>)[fieldName] = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 使用自定义反序列化器或默认反序列化
|
||||
const value = options.deserializer
|
||||
? 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;
|
||||
}
|
||||
|
||||
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[],
|
||||
componentRegistry: Map<string, ComponentType>,
|
||||
context?: SerializationContext
|
||||
): Component[] {
|
||||
return serializedComponents
|
||||
.map(s => this.deserialize(s, componentRegistry, context))
|
||||
.filter((c): c is Component => c !== null);
|
||||
const result: Component[] = [];
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
static getSerializationInfo(component: Component | ComponentType): {
|
||||
/**
|
||||
* 获取组件的序列化信息
|
||||
*
|
||||
* @param component 组件实例或组件类
|
||||
* @returns 序列化信息对象,包含类型名、版本、可序列化字段列表
|
||||
*/
|
||||
public static getSerializationInfo(component: Component | ComponentType): {
|
||||
type: string;
|
||||
version: number;
|
||||
fields: string[];
|
||||
ignoredFields: string[];
|
||||
isSerializable: boolean;
|
||||
} {
|
||||
} | null {
|
||||
const metadata = getSerializationMetadata(component);
|
||||
|
||||
if (!metadata) {
|
||||
return { type: 'unknown', version: 0, fields: [], ignoredFields: [], isSerializable: false };
|
||||
return {
|
||||
type: 'unknown',
|
||||
version: 0,
|
||||
fields: [],
|
||||
ignoredFields: [],
|
||||
isSerializable: false
|
||||
};
|
||||
}
|
||||
|
||||
const componentType = typeof component === 'function'
|
||||
@@ -137,18 +367,50 @@ export class ComponentSerializer {
|
||||
return {
|
||||
type: metadata.options.typeId || getComponentTypeName(componentType),
|
||||
version: metadata.options.version,
|
||||
fields: Array.from(metadata.fields.keys()).map(k => typeof k === 'symbol' ? k.toString() : k),
|
||||
ignoredFields: Array.from(metadata.ignoredFields).map(k => typeof k === 'symbol' ? k.toString() : k),
|
||||
fields: Array.from(metadata.fields.keys()).map((k) =>
|
||||
typeof k === 'symbol' ? k.toString() : k
|
||||
),
|
||||
ignoredFields: Array.from(metadata.ignoredFields).map((k) =>
|
||||
typeof k === 'symbol' ? k.toString() : k
|
||||
),
|
||||
isSerializable: true
|
||||
};
|
||||
}
|
||||
|
||||
static serializeEntityRef(entity: Entity | null): SerializableValue {
|
||||
if (!entity) return null;
|
||||
return { __entityRef: { id: entity.id, guid: entity.persistentId } };
|
||||
/**
|
||||
* 序列化 Entity 引用
|
||||
*
|
||||
* 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