Compare commits

..

24 Commits

Author SHA1 Message Date
yhh 8662449dcf feat(ci): 改进 SignPath 代码签名集成
- 添加 SignPath 配置检查步骤
- 使用 test-signing 策略进行测试
- 即使签名跳过也能继续版本更新 PR
2025-12-16 13:09:39 +08:00
yhh 1834bc2068 fix(tests): 更新测试以使用 GlobalComponentRegistry 实例
修复多个测试文件以适配 ComponentRegistry 从静态类变为实例类的变更:
- ComponentStorage.test.ts: 使用 GlobalComponentRegistry.reset()
- EntitySerializer.test.ts: 使用 GlobalComponentRegistry 实例
- IncrementalSerialization.test.ts: 使用 GlobalComponentRegistry 实例
- SceneSerializer.test.ts: 使用 GlobalComponentRegistry 实例
- ComponentRegistry.extended.test.ts: 使用 GlobalComponentRegistry,同时注册到 scene.componentRegistry
- SystemTypes.test.ts: 在 Scene 创建前注册组件
- QuerySystem.test.ts: mockScene 添加 componentRegistry
2025-12-16 12:38:14 +08:00
yhh c23c6c21db fix(asset-system): 移除未使用的 TextureLoader 导入 2025-12-16 12:06:09 +08:00
yhh b494283e9c refactor(asset-system-editor): 资产元数据改进
- AssetMetaFile 优化
- 导出调整
2025-12-16 11:55:39 +08:00
yhh 9b334f36e1 refactor(platform): 平台适配层优化
- BrowserRuntime 改进
- 新增 RuntimeSceneManager 服务
- 导出优化
2025-12-16 11:55:06 +08:00
yhh 7f8d2eb142 refactor(particle): 粒子系统改进
- 适配新的组件注册接口
- ParticleSystem 优化
- 添加单元测试
2025-12-16 11:52:21 +08:00
yhh 9d3eeb1980 feat(tauri): 添加文件修改时间查询命令
- 新增 get_file_mtime 命令
- 支持检测文件外部修改
2025-12-16 11:51:58 +08:00
yhh 0bcb675c3b feat(i18n): 更新国际化翻译
- 添加新功能相关翻译
- 更新中文、英文、西班牙文
2025-12-16 11:29:14 +08:00
yhh 574b4d08a3 refactor(editor-app): 编辑器服务和组件优化
- EngineService 改进引擎集成
- EditorEngineSync 同步优化
- AssetFileInspector 改进
- VectorFieldEditors 优化
- InstantiatePrefabCommand 改进
2025-12-16 11:28:50 +08:00
yhh d64e463a71 feat(editor-app): 添加渲染调试面板
- 新增 RenderDebugService 和调试面板 UI
- App/ContentBrowser 添加调试日志
- TitleBar/Viewport 优化
- DialogManager 改进
2025-12-16 11:28:34 +08:00
yhh 792fd05c85 feat(editor-app): 添加外部文件修改检测
- 新增 ExternalModificationDialog 组件
- TauriFileAPI 支持 getFileMtime
- 场景文件被外部修改时提示用户
2025-12-16 11:28:08 +08:00
yhh 7814b97ace feat(ui): 添加场景切换和文本闪烁组件
新增组件:
- SceneLoadTriggerComponent: 场景切换触发器
- TextBlinkComponent: 文本闪烁效果

新增系统:
- SceneLoadTriggerSystem: 处理场景切换逻辑
- TextBlinkSystem: 处理文本闪烁动画

其他改进:
- UIRuntimeModule 适配新组件注册接口
- UI 渲染系统优化
2025-12-16 11:25:49 +08:00
yhh 75be905f14 feat(engine): 改进 Rust 纹理管理器
- 支持任意 ID 的纹理加载(非递增)
- 添加纹理状态追踪 API
- 优化纹理缓存清理机制
- 更新 TypeScript 绑定
2025-12-16 11:25:28 +08:00
yhh 01293590e8 feat(editor-core): 改进编辑器核心服务
- EntityStoreService 添加调试日志
- AssetRegistryService 优化资产注册
- PluginManager 改进插件管理
- IFileAPI 添加 getFileMtime 接口
2025-12-16 11:23:50 +08:00
yhh b236b729b4 fix(editor-app): 在编译完成后调用 signalReady()
确保用户脚本编译完成后发出就绪信号:
- 编译成功后调用 userCodeService.signalReady()
- 编译失败也要发出信号,避免阻塞场景加载
2025-12-16 11:21:57 +08:00
yhh 0170dc6e9c feat(editor-core): 添加 UserCodeService 就绪信号机制
- 新增 waitForReady()/signalReady() API
- 支持等待用户脚本编译完成
- 解决场景加载时组件未注册的时序问题
2025-12-16 11:17:19 +08:00
yhh 7834328ae0 fix(physics-rapier2d): 修复物理插件组件注册
- PhysicsEditorPlugin 添加 runtimeModule 引用
- 适配 IComponentRegistry 接口
- 修复物理组件在场景加载时未注册的问题
2025-12-16 11:12:50 +08:00
yhh 39fa797299 refactor(modules): 适配新的组件注册接口
更新各模块 RuntimeModule 使用 IComponentRegistry 接口:
- audio, behavior-tree, camera
- sprite, tilemap, world-streaming
2025-12-16 11:12:17 +08:00
yhh 03229ffb59 refactor(engine-core): 改进插件服务注册机制
- 更新 IComponentRegistry 类型引用
- 优化 PluginServiceRegistry 服务管理
2025-12-16 11:11:48 +08:00
yhh 844a770335 refactor(core): 提取 IComponentRegistry 接口
将组件注册表抽象为接口,支持场景级组件注册:
- 新增 IComponentRegistry 接口定义
- Scene 持有独立的 componentRegistry 实例
- 支持从 GlobalComponentRegistry 克隆
- 各系统支持传入自定义注册表
2025-12-16 11:11:29 +08:00
yhh c8dc9869a3 fix(runtime-core): 修复 PluginManager 组件注册类型错误
将 ComponentRegistry 类改为 GlobalComponentRegistry 实例:
- registerComponents() 期望 IComponentRegistry 接口实例
- GlobalComponentRegistry 是 ComponentRegistry 的全局实例
2025-12-16 11:08:09 +08:00
yhh 38755c9014 fix(editor-core): 修复场景切换时的资源泄漏
在 openScene() 加载新场景前先卸载旧场景资源:
- 调用 sceneResourceManager.unloadSceneResources() 释放旧资源
- 使用引用计数机制,仅卸载不再被引用的资源
- 路径稳定 ID 缓存不受影响,保持 ID 稳定性
2025-12-16 11:07:48 +08:00
yhh 5d5537e4c7 fix(runtime-core): 移除 Play/Stop 循环中的 clearTextureMappings 调用
使用路径稳定 ID 后,不再需要在快照保存/恢复时清除纹理缓存:
- saveSceneSnapshot() 移除 clearTextureMappings() 调用
- restoreSceneSnapshot() 移除 clearTextureMappings() 调用
- 组件保存的 textureId 在 Play/Stop 后仍然有效
2025-12-16 11:07:15 +08:00
yhh 9da9f5f068 feat(asset-system): 实现路径稳定 ID 生成器
使用 FNV-1a hash 算法为纹理生成稳定的运行时 ID:
- 新增 _pathIdCache 静态缓存,跨 Play/Stop 循环保持稳定
- 新增 getStableIdForPath() 方法,相同路径永远返回相同 ID
- 修改 loadTextureForComponent/loadTextureByGuid 使用稳定 ID
- clearTextureMappings() 不再清除 _pathIdCache

这解决了 Play/Stop 后纹理 ID 失效的根本问题。
2025-12-16 11:06:59 +08:00
764 changed files with 27119 additions and 104560 deletions
+11 -4
View File
@@ -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
+13 -21
View File
@@ -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 }}
-4
View File
@@ -90,7 +90,3 @@ docs/.vitepress/dist/
# Tauri 捆绑输出
**/src-tauri/target/release/bundle/
**/src-tauri/target/debug/bundle/
# Rust 构建产物
**/engine-shared/target/
external/
+41 -68
View File
@@ -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
View File
@@ -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. **渐进迁移**:可分阶段进行,不影响现有功能
-13
View File
@@ -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
-13
View File
@@ -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
-402
View File
@@ -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.
+4 -62
View File
@@ -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 {
-14
View File
@@ -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';
+1 -5
View File
@@ -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
+1 -18
View File
@@ -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>;
}
/**
-15
View File
@@ -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;
+1
View File
@@ -32,6 +32,7 @@
{ "path": "../core" },
{ "path": "../engine-core" },
{ "path": "../editor-core" },
{ "path": "../ui" },
{ "path": "../editor-runtime" }
]
}
-15
View File
@@ -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"]
}
}
-3
View File
@@ -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';
-6
View File
@@ -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
}
};
}
}
+2 -10
View File
@@ -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;
-22
View File
@@ -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
};
}
-105
View File
@@ -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';
-3
View File
@@ -91,9 +91,6 @@ export const STANDARD_EXTERNALS = [
'zustand',
'immer',
// Tauri (由宿主应用提供) | Provided by host app
/^@tauri-apps\//,
// 所有 @esengine 包
/^@esengine\//,
] as const;
-15
View File
@@ -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"]
}
}
+1 -6
View File
@@ -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",
+2 -2
View File
@@ -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',
+6 -6
View File
@@ -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);
}
}
+5 -5
View File
@@ -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);
}
}
-188
View File
@@ -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>
-188
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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;
}
}
+2 -2
View File
@@ -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 {
/**
* 是否可更新
*/
+2 -2
View File
@@ -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;
/**
+5 -8
View File
@@ -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);
}
}
}
+1 -1
View File
@@ -7,7 +7,7 @@ const logger = createLogger('ServiceContainer');
* 服务基础接口
* 所有通过 ServiceContainer 管理的服务都应该实现此接口
*/
export type IService = {
export interface IService {
/**
* 释放服务占用的资源
* 当服务被注销或容器被清空时调用
+50 -53
View File
@@ -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 记录组件最后一次被修改时的 epoch0 表示从未被标记为已修改
* @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[];
/** 所有匹配实体的总数 */
+3 -6
View File
@@ -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
});
}
+44 -57
View File
@@ -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.
*
+12 -16
View File
@@ -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;
}
}
+3 -3
View File
@@ -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 -2
View File
@@ -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';
+2 -4
View File
@@ -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 };
/**
+2 -2
View File
@@ -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;
/** 查询执行时间(毫秒) */
+3 -4
View File
@@ -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;
}
+147 -152
View File
@@ -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 -2
View File
@@ -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
View File
@@ -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 truefalse
*
* @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);
}
/**
+9 -17
View File
@@ -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
View File
@@ -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